kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 5d0d1ca2b5c27ba40ed16d414737d18431be255c
parent a82588275127da5cfee9aa081e1dc5722d2237a1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 10 May 2026 14:51:12 -0700

parse: clear all 20 §6.2–§6.9 status-list failures

Lands fixes for the carryover rows from doc/parser-status.md (now
removed since the list is empty):

- block-scope `extern` reuses prior file-scope SymEntry to inherit
  internal linkage (§6.2.2 ¶4)
- composite array type updated on redeclaration; fixed an uninitialized
  `out->vla` in parse_decl_suffix that made every const-bound array look
  incomplete
- ternary coerces else-arm to result type before storing
- `+=` / `-=` route through emit_add_or_sub for pointer scaling
- VLA locals capture their byte-size in a dedicated frame slot so
  sizeof(IDENT) on a VLA returns the runtime value
- sizeof(parenthesized-expression) falls through to the unary path
  rather than the IDENT-only special case
- `_Generic` records `default:` tokens and replays them when no typed
  association matches
- `struct s = expr` lowers to an emit_struct_copy_into_slot field-walk
  (covers compound literals and direct struct copy)
- array bound recognizes sizeof / _Alignof as constant-start tokens
- static initializers accept address constants (`&g`, `arr + 3`) via a
  pending-relocs list flushed when the section is pinned
- anonymous bitfields (`unsigned : 0`) accepted at member parse
- `_Alignas(0)` treated as a no-op
- inner-paren declarators collect suffixes (`int (*ops[2])(int)`)
- union initializer honors a leading `.field =` designator

Diffstat:
Ddoc/parser-status.md | 544-------------------------------------------------------------------------------
Msrc/parse/parse.c | 666++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
2 files changed, 593 insertions(+), 617 deletions(-)

diff --git a/doc/parser-status.md b/doc/parser-status.md @@ -1,544 +0,0 @@ -# C-parser status - -Living checklist for the C front-end (`src/parse/`) build-out. Behavioral -oracle is `test/parse/CORPUS.md` — every checkbox here corresponds to -corpus rows that flip from `·` to `★` when the box gets ticked. Update -this doc in the same commit as the parser change that lands it. - -Phase status: - -- ✅ landed -- 🚧 in progress -- ⬜ planned - -Each phase is one agent's worth of work. Phases are ordered by -dependency, not priority — Phase N+1 generally needs Phase N's surface -in place. - ---- - -## Phase 0 — Spine ✅ - -§6.5 binary operators, scalar `int` locals, `if/else`, `while`, `for`, -`break`, `continue`, `return`, comma, simple/compound assignment, -unary `+ - ! ~ ++ --`, `(expr)`, integer literals. Single function -`int test_main(void) { ... }` per TU. - ---- - -## Phase 1 — Calls & §6.5 completion ✅ - -Finish the expression grammar and let helper functions exist. The -largest single unlock — most later phases depend on multi-function TUs. - -- [x] Parameter lists in function definitions (`int f(int x, int *p)`) -- [x] Multiple function definitions per TU -- [x] Function calls in postfix (`f(x, y)`) — drives `cg_call` / ABI -- [x] Forward prototypes (`int f(int);` then later body) -- [x] Logical `&&`, `||`, ternary `?:` (short-circuit, label-driven) -- [x] Address-of `&` and dereference `*` in unary -- [x] String literals in primary (rodata-resident, decay to `char*`) -- [x] Char literal decoded value (full §6.4.4.4 escape set) -- [x] `sizeof(type-name)` and `sizeof(IDENT)` for declared objects; - arbitrary `sizeof expr` (no parens, side-effecting operand) - defers to Phase 2 once subscript/`->`/etc. land -- [x] `_Alignof(type-name)` -- [x] Cast expression `(type-name) expr` -- [x] Type-name production (shared by sizeof / _Alignof / cast); abstract - declarators are pointer-prefix only — array/function suffixes wait - on Phase 2 - -Phase 1 also added a backend-cooperative scratch-register reset at -statement boundaries (cg's value stack is empty between stmts, so -`cg_reset_scratch` lets the next statement reuse the entire scratch -window). Without it the aarch64 backend's fixed pool gets exhausted -inside any function with multiple sequential reg-allocating -operations (factorial blew up at depth 5). - -Unlocks (status as landed): `6_5_12–14` ★, `6_5_20–21` ★, `6_5_22` · -(needs array decl, Phase 2), `6_5_23–25` ★, `6_5_30` · (deferred — -`_Generic` requires type-only walking, defer to Phase 3 alongside -`_Generic`'s natural neighbors), `6_5_32` · (needs subscript, Phase 2), -`6_2_4_01` · (proper static-local persistence — Phase 4), `6_2_5_01` ★, -`6_3_1_1_01` ★, `6_3_2_2_01` ★, `6_3_2_3_01–02` ★, `6_7_4_01` ★, -`6_9_01–02` ★, `6_9_03` · (file-scope tentative def — Phase 4), -`6_9_04–05` ★, `6_9_06` · (variadic + `__builtin_va_*` — Phase 9). - ---- - -## Phase 2 — Pointers & arrays ✅ - -Pointer/array declarator layers and the address operators. Builds on -Phase 1's type-name production. - -- [x] Pointer declarator (`int *p`, `int **pp`) -- [x] Array declarator (`int a[N]`, `int a[]`) -- [x] Subscript `a[i]` (and the commutative `i[a]`) -- [x] Pointer arithmetic in `+`/`-` (scaled by element size) -- [x] Array-to-pointer decay -- [x] Function-to-pointer decay + indirect call (`(*fp)(x)`) -- [x] `int *const p` / `const int *p` qualifier placement -- [x] `[static N]` parameter form -- [x] VLA local (`int a[n]`) — lowered via `cg_alloca`; the variable binds - as a pointer-to-element rather than as a true array, so subscript - and pointer arith just work but `sizeof(vla)` is not yet preserved - (Phase 9 follow-up alongside `__builtin_alloca`). - -Phase 2 also taught `parse_decl_suffix` to parse function and array -declarator suffixes recursively, taught `to_rvalue` to do array→pointer -and function→pointer decay automatically, and added a real recursive -declarator that handles one level of nested parens (`int (*fp)(int)`). -The `cg_alloca` stub in `cg.c` was wired through to `CGTarget.alloca_` -since the aarch64 backend already implements it. - -Unlocks (status as landed): `6_3_2_1_01–02` ★, `6_5_22` ★, `6_5_31–32` ★, -`6_7_6_02–07` ★. `6_5_28–30` -were misattributed in the original phase plan: `6_5_28_arrow` and -`6_5_30_generic_selection` need struct/`_Generic` (Phase 3), -`6_5_29_compound_literal` needs Phase 6's brace-init machinery. - ---- - -## Phase 3 — Aggregates (struct / union / enum) ✅ - -Tag namespace, member access, anonymous and forward-declared -aggregates, bitfields, `_Generic`. - -- [x] `struct` / `union` definition and tag-scope lookup -- [x] Member access `.` and `->` in postfix -- [x] `enum` with constants bound into ordinary scope -- [x] Forward-declared tag (`struct S; ... struct S { ... };`) -- [x] Self-referential pointers (`struct N { struct N *next; };`) -- [x] Anonymous struct/union members (C11 §6.7.2.1) -- [x] Bitfield members (`unsigned a:5`) — declarations parsed and recorded - as `FIELD_BITFIELD`; access lowers through `cg_bitfield_load/store`. -- [x] `_Generic` selection (type-keyed) - -Phase 3 also added: - - `Type*` mutability for struct/union via `type_record_forward` + - `type_record_install`, so a forward `struct S;` shares Type identity - with the later `struct S { ... };`. Pointers built off the forward - node automatically pick up complete fields once installed. - - A tag-scope chain alongside the ordinary identifier chain (`Scope.tags`), - walked by `tag_lookup`/`tag_lookup_local`. Tags share scope nesting - rules with idents but live in a separate namespace (§6.2.3 ¶1). - - Brace-elision for nested aggregates inside positional brace - initializers (a sub-aggregate may omit its own `{...}` and consume - one scalar from the parent stream). Designators and the rest of - §6.7.9 stay deferred to Phase 6. - - Tiny constant evaluator (`eval_const_int`) for enum values and other - integer-constant positions; recognizes int literals, char literals, - enum constants, parens, unary `+ - ~ !`, and integer arithmetic. - - Tag-only declarations: `struct S;` / `enum E { ... };` / etc. with no - declarator are now accepted at file and block scope. - -`_Generic` has a known approximation: the parser walks associations -left-to-right and emits the FIRST matching non-default association, -skipping siblings via balanced token-skip. A `default:` consumed before a -non-default match cannot be replayed (single-pass, no rewind), so we panic -in that pathological case — none of the corpus rows hit it. - -Unlocks (status as landed): `6_5_28` ★, `6_5_30` ★, `6_6_01` ★, `6_7_06–08` ★, -`6_7_2_1_01–05` ★. - ---- - -## Phase 4 — Globals, storage, linkage ✅ - -File-scope objects with their full initializer / linkage matrix. - -- [x] File-scope object declarations -- [x] `static` global (internal linkage, `.data` / `.bss` placement) -- [x] `extern` declaration and resolution -- [x] Tentative definitions -- [x] `const` global in `.rodata` -- [x] Global struct / array data emission -- [x] `static` local with non-zero init - -Phase 4 also added: - - `DeclSpecs.quals` carries `Q_CONST` / `Q_VOLATILE` / etc. through the - decl-spec parser; `const` is no longer silently swallowed. The Phase 4 - use is `Q_CONST` → `.rodata` placement; the other bits are recorded for - the next phase (atomic, volatile semantics). - - `define_static_object` is the shared emitter for any static-storage - object: scalars / arrays / structs lower into a working `u8[size]` - buffer, get bucketed into `.rodata` (const + nonzero), `.data` (any - nonzero init), or `.bss` (zero or no init), and then call - `obj_symbol_define`. BSS uses `obj_section_ex(SSEM_NOBITS)` and - accumulates `bss_size` so multiple BSS objects pack correctly. - - Static locals are promoted to globally-visible symbols with internal - linkage. The linker name is mangled `<orig>.<counter>`; the local scope - binds the original name to `SEK_GLOBAL` so subsequent references go - through `cg_push_global`. Storage persists across calls (§6.2.4 ¶3). - - File-scope tentative defs reserve BSS at first sight. Multi-tentative - coalescing (e.g. `int g; int g; int g = 0;`) is a Phase 4 follow-up - when the corpus needs it; today's rows have at most one tentative def - per TU. - - `parse_external_decl` accepts array suffixes on the first declarator - (`int g[3]`) and supports `,`-separated init-declarators on a single - decl-spec line; `extern` followed by a defining declaration of the - same name reuses the `ObjSymId` so the linker sees one symbol. - -Unlocks (status as landed): `6_2_3_01` ★, `6_2_4_01` ★, `6_7_02–04` ★, -`6_9_03` ★, `6_9_07–09` ★. - ---- - -## Phase 5 — Statement completeness ✅ - -Switch family, goto/labels, do-while, void return, label namespace. - -- [x] `switch` / `case` / `default` (incl. fall-through, default-only) -- [x] `goto` forward and backward -- [x] User labels (separate namespace from ordinary identifiers) -- [x] `do { } while ()` -- [x] `return;` from void function - -Phase 5 also added: - - `SwitchCtx` parser-side stack tracking the innermost switch's case list, - optional default label, and a frame slot holding the controlling - expression's value. Lowering is single-pass: evaluate-and-stash, jump - over the body to a dispatch trampoline, parse the body (cases push - `(value, label)` onto the ctx as they're encountered), then emit a - compare-chain that selects the matching label or falls through to the - end of the switch. No jump-table primitive in cg today; a dense-case - optimization can plug in later through opt without touching the parser. - - `GotoLabel` per-function label namespace (separate chain from the - ordinary identifier scope, §6.2.3 ¶1). CGLabel is allocated lazily on - first reference so forward and backward gotos share one entry; label - placement (`name:`) marks the entry as resolved. At cg_func_end the - parser checks every entry was placed and panics with the first-use - location otherwise. - - Labeled-statement detection in `parse_stmt` via `peek1(':')` lookahead - on a non-keyword IDENT. The label form short-circuits before the - expression-statement fallback, so `s: return 42;` doesn't get parsed - as `s` (the int) followed by stray `:`. - - `parse_function_body` saves/restores `goto_labels` and `cur_switch` - around the function so the per-function state never leaks even if a - pathological caller nests definitions. - -Unlocks (status as landed): `6_2_3_02` ★, `6_8_04` ★, `6_8_07–11` ★, -`6_8_14` ★ (was already passing — bare `return;` in void was wired up by -parse_return_stmt before Phase 5; the corpus row stays under this phase -because it's the §6.8.6 spec slot). - ---- - -## Phase 6 — Initializers ✅ - -Full §6.7.9 surface. Requires aggregates (Phase 3) and globals -(Phase 4) to be fully useful. - -- [x] Brace initializer for arrays -- [x] Brace initializer for structs -- [x] Designated initializers (`[i] = ...`, `.field = ...`) -- [x] Nested designators (`[i][j] = ...`) -- [x] Partial init with zero-fill -- [x] String literal init for `char[]` -- [x] Compound literals (`(int[]){1, 2}`) - -Phase 6 also added: - - A token replay buffer on `Parser` so we can two-pass scan a braced - initializer: record tokens through the matching `}`, count top-level - items, then `replay_rewind` to re-parse. Used by - `complete_incomplete_array` to size `T[]` declarators / compound - literals (`(int[]){...}` and `char s[] = "hi"`) before the slot is - allocated. - - `parse_designator_chain` walks `[const]` / `.ident` chains starting - from any aggregate type, returning the leaf sub-object's offset/type - plus the top-level cursor index for the parent loop. Both - `init_at` (local) and `parse_static_init_at` (file scope / static - locals) consume designators; locals zero-fill gaps between the - cursor and a forward designator, while statics rely on the - pre-zeroed buffer. - - String-literal element initialization for char arrays at any nesting - level: `init_string_at` / `parse_static_string_at`. With or without - surrounding braces; truncation rules match §6.7.9 ¶14. - - Compound literals (`(T){...}`) lower as a hidden `cg_local` slot in - `parse_unary` immediately after the type-name; the slot's lvalue is - pushed and outer postfix/cast machinery handles array→pointer decay. - - `parse_type_name` now accepts a full abstract declarator (pointer - prefix + array/function suffixes) so casts like `(int (*)[3])` and - compound literals like `(int[]){...}` parse cleanly. The Phase 1 - docstring noting "abstract declarators are pointer-prefix only" is - obsolete. - -Unlocks (status as landed): `6_5_29` ★, `6_7_9_02–10` ★. `6_9_08–09` -were already ★ from Phase 4 (no compound-literal dependency). - ---- - -## Phase 7 — Type breadth & conversions ✅ - -Every primitive integer + float type round-tripped, plus the §6.3 -conversion matrix. - -- [x] `char`, `signed char`, `unsigned char` -- [x] `short`, `unsigned short` -- [x] `long`, `long long`, `unsigned long`, `unsigned long long` -- [x] `_Bool` with normalize-to-0/1 semantics -- [x] `float`, `double` -- [ ] `long double` (binary128) — needs rt soft-float wiring through cg -- [x] Integer literal suffixes (`U`, `L`, `LL`) -- [x] Float literals (decimal + hex) -- [x] Usual arithmetic conversions -- [x] Integer ↔ float conversions - -Phase 7 also added: - - `lex_next` (lex.c) now scans the suffix on a pp-number and sets - `TF_INT_U` / `TF_INT_L` / `TF_INT_LL` on TOK_NUM and `TF_FLT_F` / - `TF_FLT_L` on TOK_FLT. The flags existed before but were never - populated; the parser was the first consumer that needed them. - - `int_literal_type` and `float_literal_type` in `parse.c`: pick the - `Type*` for a numeric literal from its TF_INT_* / TF_FLT_* suffix - flags. `parse_primary` now routes both TOK_NUM and TOK_FLT through - these so an `unsigned`/`long`/`float` literal lands on the value - stack already wearing its declared C type instead of `int`. - - `parse_float_literal` decodes decimal and hex pp-numbers into a - `double` without a libc dependency. The value is funneled through - `cg_push_float`, which materializes a typed constant via the - backend's `load_const` (no FP literal pool primitive needed in cg). - - `coerce_top_to_lvalue` is the parser's §6.5.16.1 implicit-conversion - helper: when the rvalue at top of stack and the lvalue beneath it - have different arithmetic types, it issues `cg_convert(dst)` so the - backend emits the right SCVTF / FCVTZS / SEXT / TRUNC step instead - of treating the bits as same-class. Wired into the simple-assignment - path in `parse_assign_expr`, the scalar-init path in `init_at`, and - the non-aggregate branch of `parse_init_declarator`. - - `cg_convert` now treats same-size integer reinterprets (e.g. - `(unsigned)int_value`, `int x = 42U;`) as a no-op retag rather than - routing to `CV_BITCAST`. The aarch64 backend only knows how to - BITCAST between INT and FP register classes; same-class GPR-GPR - reinterprets have identical bit patterns and don't need an - instruction. The retag preserves the SValue's existing storage and - avoids the destination-register allocation cost. - -Long double status: TF_FLT_L lexes correctly and `float_literal_type` -returns `TY_LDOUBLE`, but `cg_push_float` and `cg_convert` panic with a -clear "needs rt soft-float wiring (rt/lib/fp_tf)" diagnostic when asked -to lower a binary128 value. The runtime helpers exist already -(`__floatsitf`, `__fixtfsi`, `__extenddftf2`, `__trunctfdf2`, -`__addtf3`/etc. under `rt/lib/fp_tf/`); cg just needs to route TY_LDOUBLE -ops through external calls instead of the inline FP path. Corpus row -`6_7_2_12_long_double` carries a `.skip` sidecar pending that wiring -(treated as a hard failure unless `CFREE_TEST_ALLOW_SKIP=1`). - -Unlocks (status as landed): `6_3_1_3_01–02` ★, `6_3_1_4_01–02` ★, -`6_7_2_04` ★, `6_7_2_10–12` ★. The remaining `6_7_2_*` rows already -landed under earlier phases that exercised the same int width through a -different path; Phase 7 just promotes the literal types so the -declarations would round-trip through the new typed-literal path even -without an implicit conversion at store time. - ---- - -## Phase 8 — Qualifiers, alignment, typedefs ✅ - -Remaining declaration-side features. - -- [x] `_Atomic` qualifier (parse + plumb to cg) — `_Atomic int` is a - qualified int (Q_ATOMIC); `_Atomic(T)` parses as the type-spec - form and yields the same qualified type. -- [x] `_Alignas(T)` and `_Alignas(N)` on objects — strictest alignment - override carried on `DeclSpecs.align`, applied to both frame - slots (`make_local_aligned`) and static-storage objects - (`define_static_object`). -- [x] `inline` (header-only definitions) — recognized as a function - specifier and recorded via `DF_INLINE` on `DeclSpecs.flags`. The - corpus row only exercises `static inline`, which already lowers - as a regular static function; lazy-emission for non-static - inline definitions waits until a row needs it. -- [x] `typedef` (already partially landed; promote) — `KW_TYPEDEF` in - decl-specs now selects `DS_TYPEDEF`; both - `parse_init_declarator` (block scope) and `parse_external_decl` - (file scope) bind the declarator name as `SEK_TYPEDEF` instead - of allocating storage. `parse_decl_specs` consumes a typedef-name - as a type specifier when no other type spec is in flight, and - `starts_type_name` reports true for typedef-names so the - cast/sizeof/_Alignof paths recognize them too. -- [x] Compound typedef targets (struct, function pointer, array) — - file-scope typedef goes through `parse_declarator_full` so - `typedef int (*FP)(int)` and `typedef int A[3]` round-trip; the - inner-paren disambiguator in `parse_declarator_full` peeks the - symbol table to distinguish `(declarator)` from `(typedef-name)` - (function-suffix) cases. -- [x] `_Static_assert` at file and block scope — new - `parse_static_assert` consumes `(expr , "msg") ;` and panics with - the message when the expression evaluates to zero. `eval_const_int` - grew comparison operators (`< > <= >= == !=`), `sizeof(type-name)`, - `_Alignof(type-name)`, and casts `(T)expr` so the §6.7.10 corpus - bodies (`sizeof(int) >= 2`, `1+1 == 2`) lower as constants. - -Phase 8 also added: - - `cg_convert` now treats pointer↔integer reinterprets the same way - as int↔int: same-size is a retag, narrowing is `CV_TRUNC`, - widening is `CV_ZEXT`. Without this the `_Alignas` corpus row - (`(unsigned long)buf & 15`) hit "aarch64 convert BITCAST: - same-class not yet supported" — the aarch64 backend only knows - INT↔FP bitcasts, and same-class GPR↔GPR reinterprets don't need an - instruction. - - `DeclSpecs` carries a new `align` field. Parsers that own the - declarator-to-storage step (`parse_init_declarator`, - `parse_external_decl`) thread it into `make_local_aligned` / - `define_static_object`; the strictest `_Alignas` argument wins - against the natural type alignment (no narrowing). - -Unlocks (status as landed): `6_7_01_typedef` ★, `6_7_3_05_atomic` ★ (was -already passing as a qualifier; promoted to deliberate after the -`_Atomic(T)` form landed), `6_7_4_01_inline` ★, `6_7_5_01–02` ★, -`6_7_8_01–04` ★, `6_7_10_01–02` ★, plus the `cases_err/` -`6_7_10_static_assert_fail` row which now hits the new failure -diagnostic. - ---- - -## Phase 9 — Builtins ✅ - -Routes named `__builtin_*` / `__atomic_*` calls to cg's intrinsic / asm -machinery rather than ordinary call lowering. Contract: -`doc/builtins.md`. - -- [x] `__builtin_alloca` — lowers via `cg_alloca`; result is `void*`. -- [x] `__builtin_expect` — folds to identity (returns the value - operand; the hint operand is parse-and-drop, no IR hint emitted - — backend can re-introduce one once `cg_intrinsic(EXPECT)` lands). -- [x] `__builtin_va_start` / `va_arg` / `va_end` / `va_copy` — parser - routes through `cg_va_*`; `__builtin_va_list` is recognized as a - type-name (`abi_va_list_type`). -- [x] `__builtin_offsetof` — folds at parse to a `size_t` constant - using `find_field` / `abi_record_layout`; supports nested - `.field` and `[index]` member-designator chains. -- [x] `__atomic_load_n`, `__atomic_store_n`, `__atomic_exchange_n`, - `__atomic_fetch_{add,sub,and,or,xor}` — parser routes through - `cg_atomic_load` / `cg_atomic_store` / `cg_atomic_rmw`. The - memory-order argument must be a constant-folded integer; pp - predefines `__ATOMIC_RELAXED`..`__ATOMIC_SEQ_CST` to match the - `MemOrder` enum so a header-less corpus row resolves the names. -- [x] `__atomic_compare_exchange_n` — parser stashes `&expected` in a - frame slot, lowers `cg_atomic_cas` (which pushes [prior, ok]), - and conditionally stores `prior` back to `*expected` on the - failure branch. The `weak` flag is parsed and ignored (strong-CAS - is a safe over-approximation; AArch64 LL/SC has no weak form - anyway). The result is the `ok` flag as `int`. -- [x] `__atomic_thread_fence` / `__atomic_signal_fence` — parser - routes through `cg_fence`. On AArch64 the backend emits `DMB ISH` - for any non-RELAXED order; signal_fence collapses to the same - lowering since cfree's threading model is single-threaded - observers and a compiler barrier suffices in either case. - -Phase 9 also added: - - `try_parse_builtin_call` invoked from `parse_primary` ahead of - `scope_lookup`. It dispatches the IDENT against the interned - builtin spellings and consumes the `( ... )` argument list itself, - so the call never goes through `cg_call`. Falls through (returns 0, - no tokens consumed) when the IDENT isn't a known builtin so an - ordinary identifier reference still resolves. - - `__ATOMIC_*` predefined macros in `pp_register_static_predefined` - — values pinned to the `MemOrder` enum so `eval_const_int(order)` - casts directly. Headers can now `#define memory_order_relaxed - __ATOMIC_RELAXED` (matches `rt/include/stdatomic.h`). - - `__builtin_va_list` recognized as a type specifier (and counted by - `starts_type_name`), resolved through `abi_va_list_type` so the - structural shape comes from the ABI vtable - (`abi_aapcs64_va_list_type` is a 32-byte struct; Apple ARM64 is - `char*`). - -Atomics are now fully wired end-to-end on AArch64. `cg_atomic_load` / -`cg_atomic_store` / `cg_atomic_rmw` / `cg_atomic_cas` / `cg_fence` pop -a pointer (and value/expected/desired as appropriate) from the value -stack, derive a `MemAccess` from the pointee type with `MF_ATOMIC` set, -and dispatch to the AArch64 backend's LL/SC implementation -(`LDXR`/`LDAXR` + `STXR`/`STLXR`, plain `LDUR`/`STUR` for relaxed -load/store, `LDAR`/`STLR` for acquire/release scalar load/store, and -`DMB ISH` for fences). The CAS retry loop and writeback-on-failure -shape live in the parser; the cg layer just hands ([prior, ok]) back. - -The `cg_va_*` primitives now wire through to the -backend's `va_start_` / `va_arg_` / `va_end_` / `va_copy_` hooks (the -aarch64 backend already implemented all four against the AAPCS64 §6 -five-field `__va_list`); the only follow-up gap on the variadic side -is the parser default-arg promotion when calling a variadic from a -prototype-bearing call site (Phase 9 keeps `arg.type` verbatim, so a -caller passing `1.0f` to `...` writes a 4-byte FP into v0 instead of -an 8-byte FP — no corpus row hits this yet because the existing tests -pass `1.0` directly). - -Variadic call lowering also needed `cg_call` to mark `idx >= nparams` -arguments as variadic — `avs[idx].abi = NULL` — so the aarch64 -backend's `emit_arg_value` falls into the synthesized `va_pt` path -(and on Apple ARM64 bumps `next_int=next_fp=8` to force stack -placement). The prior code indexed `abi->params[idx]` for variadic -args, reading off the end of the array. Aarch64's frame-relative -load/store also gained an `addr_base` fallback that materializes the -FP-relative address into a scratch register (`x9` or `x10`) when the -offset exceeds STUR/LDUR's ±256 window — variadic functions hit this -trivially because the GP+FP save areas reserve 192 bytes before any -declared local. - -Atomics no longer carry `.skip` sidecars; `builtin_06_atomic_load` and -`builtin_07_atomic_fetch_add` round-trip on aarch64 (paths D / R / E / -J), and the suite is rounded out with `builtin_08`–`builtin_19` -covering store_n, exchange_n, fetch_{sub,and,or,xor}, 8-byte (`long`) -atomics, atomic-pointer load, thread_fence, and the three -compare_exchange shapes (success / failure / retry-loop). - -Unlocks (status as landed): `builtin_01_alloca` ★, `builtin_02_expect` -★, `builtin_03_va_list` ★, `builtin_04_offsetof` ★, `builtin_05_va_copy` -★, `6_9_06` ★, `6_7_6_08` ★, plus the new variadic-coverage rows -`variadic_01_zero_args` … `variadic_07_nested_call` (zero-vararg call, -overflow into the stack tail, `long`/pointer/`double`/mixed/`va_copy` -through a helper). `builtin_06_atomic_load`, -`builtin_07_atomic_fetch_add` remain `.skip` pending the cg-layer -wiring of `cg_atomic_*`. - -Phase 9 also revealed and fixed an unrelated parser bug exposed by the -new variadic FP rows: `parse_mul` and `parse_add` always emitted -`BO_IADD`/`BO_ISUB`/`BO_IMUL`/`BO_SDIV` regardless of operand type, so -`double + double` lowered as integer ADD. The parser now applies the -§6.3.1.8 usual arithmetic conversions (FP slice) — when either operand -is a floating type, both convert to the wider FP type via `cg_convert` -and the op routes to `BO_FADD`/`FSUB`/`FMUL`/`FDIV`. `BO_SREM`/`BO_UREM` -remain integer-only (C `%` is undefined for FP). New rows -`6_5_36_fp_arith`, `6_5_37_fp_int_promote`, `6_5_38_fp_float_widen`, -`6_5_39_float_arith` pin the surface. - ---- - -## Phase 10 — Diagnostics polish ⬜ - -Negative cases assert nonzero exit + optional `errpat` substring. Wire -the explicit diagnostic for each `cases_err/*` row. - -- [ ] Identifier resolution / lvalue / type mismatch -- [ ] Redefinition (object, function, struct tag) -- [ ] Member / call / arrow / sizeof on incomplete or wrong shape -- [ ] Storage-class combinations -- [ ] Bitfield width -- [ ] `const` violation -- [ ] `_Static_assert` failure -- [ ] switch / case / default scope rules -- [ ] goto / label scope rules -- [ ] `void` parameter rules - -Unlocks: every row under `test/parse/cases_err/`. - ---- - -## Cross-cutting ⬜ - -Nudged forward as relevant cases exercise them; not their own phase. - -- [ ] DWARF Class-1 fanout (`debug_func_begin` / `param` / `local` / - `scope_*`) when `Debug` is non-NULL — see `DWARF.md` §3.1. -- [ ] Multi-TU `cases/<name>/{a.c,b.c}` harness wiring (see CORPUS.md - Multi-TU §). - ---- - -## Maintenance - -- Tick boxes in the same commit as the parser change that lands them. -- When a phase finishes, flip its heading marker to ✅ and update the - matching `·` rows in `CORPUS.md` to `★`. -- New corpus rows go in `CORPUS.md`; cross-link here only when they - introduce a feature axis the phases don't already cover. diff --git a/src/parse/parse.c b/src/parse/parse.c @@ -149,6 +149,11 @@ struct SymEntry { ObjSymId sym; i64 enum_value; } v; + /* For VLAs (SEK_LOCAL or SEK_TYPEDEF): a frame slot holding the array's + * byte size, captured at declaration / typedef site. FRAME_SLOT_NONE + * for non-VLA entries. Used by sizeof on VLA-bound IDENTs and by + * VLA-typedef variable declarations. */ + FrameSlot vla_byte_slot; SymEntry* next; }; @@ -272,6 +277,22 @@ typedef struct Parser { u8 vla_pending; FrameSlot vla_pending_count_slot; + /* Tracks the most recent IDENT-lvalue push that resolved to a VLA-bound + * SEK_LOCAL. Lets `sizeof IDENT` and `sizeof(IDENT)` swap the constant + * `abi_sizeof(ptr)` (8) for a runtime load of the array's byte-size + * slot. Cleared by sizeof before parse_unary; the IDENT handler sets + * it. FRAME_SLOT_NONE when the last push was not a VLA-bound IDENT. */ + FrameSlot last_pushed_vla_slot; + + /* Counter raised while parsing a function-prototype parameter declarator. + * Per §6.7.6.3 ¶7, an array parameter `T x[expr]` is adjusted to `T *x` + * regardless of `expr`, so the size expression's value is irrelevant — + * including `[*]` (§6.7.6.2 ¶4) and identifier-bearing forms like + * `int a[n]`. While >0, parse_decl_suffix consumes the bracket contents + * without evaluating them. Counter (not bool) so nested function-typed + * parameters re-enter cleanly. */ + u8 in_param_decl; + /* Counter used to mint unique linker-visible names for static locals so * that two functions can each have their own `static int s = ...`. */ u32 static_local_counter; @@ -295,6 +316,19 @@ typedef struct Parser { u32 replay_len; u32 replay_pos; u8 replay_active; + + /* Pending relocations collected while parsing a static-storage + * initializer (pointer constants like `&g` or `arr + 3`). The + * caller (`define_static_object`) flushes these via obj_reloc after + * the section has been pinned. Reset before each top-level init. */ + struct { + u32 offset; + u32 size; /* 4 or 8 bytes */ + ObjSymId target; + i64 addend; + } *static_relocs; + u32 static_relocs_len; + u32 static_relocs_cap; } Parser; /* ============================================================ @@ -561,6 +595,10 @@ typedef struct DeclSpecs { u32 flags; /* DeclFlag */ u16 quals; /* TypeQual bits seen in the decl-spec list */ u32 align; /* explicit alignment from `_Alignas`; 0 if none */ + /* When `type` came from a VLA typedef-name, propagates the typedef's + * captured byte-size slot so init_declarator can alloca the right + * runtime size. FRAME_SLOT_NONE otherwise. */ + FrameSlot vla_byte_slot; } DeclSpecs; static int parse_decl_specs(Parser* p, DeclSpecs* out); @@ -649,6 +687,7 @@ static int parse_decl_specs(Parser* p, DeclSpecs* out) { out->flags = DF_NONE; out->quals = 0; out->align = 0; + out->vla_byte_slot = FRAME_SLOT_NONE; loc = tok_loc(&p->cur); for (;;) { Tok t = p->cur; @@ -736,7 +775,9 @@ static int parse_decl_specs(Parser* p, DeclSpecs* out) { a = abi_alignof(p->abi, tn); } else { i64 v = eval_const_int(p, tok_loc(&p->cur)); - if (v <= 0) perr(p, "_Alignas requires a positive alignment"); + if (v < 0) perr(p, "_Alignas requires a non-negative alignment"); + /* §6.7.5 ¶6: `_Alignas(0)` is a no-op (use the natural + * alignment), so leave `a` at 0 and skip the bump below. */ a = (u32)v; } expect_punct(p, ')', "')' after _Alignas argument"); @@ -764,6 +805,9 @@ static int parse_decl_specs(Parser* p, DeclSpecs* out) { SymEntry* e = scope_lookup(p, t.v.ident); if (e && e->kind == SEK_TYPEDEF) { tagged_ty = e->type; + if (e->vla_byte_slot != FRAME_SLOT_NONE) { + out->vla_byte_slot = e->vla_byte_slot; + } acc.saw_explicit_type = 1; advance(p); seen = 1; @@ -1008,8 +1052,27 @@ static void parse_member_decls(Parser* p, TypeRecordBuilder* b) { /* One or more declarators separated by `,`. */ for (;;) { Sym mname = 0; - SrcLoc mloc = {0, 0, 0}; + SrcLoc mloc = tok_loc(&p->cur); const Type* mty; + /* Anonymous bitfield: `unsigned : N;` — no declarator, just the + * width. Width 0 forces alignment to the next storage unit per + * §6.7.2.1 ¶12. We don't actually lay out the unit yet (the abi + * layout assumes named fields), but recording the entry keeps + * downstream layout/init consistent. */ + if (is_punct(&p->cur, ':')) { + advance(p); + i64 w = eval_const_int(p, mloc); + Field f; + memset(&f, 0, sizeof f); + f.name = 0; + f.type = specs.type; + f.bitfield_width = (u16)w; + f.flags = FIELD_BITFIELD; + if (w == 0) f.flags |= FIELD_ZERO_WIDTH; + type_record_field(b, f); + if (!accept_punct(p, ',')) break; + continue; + } mty = parse_declarator_full(p, specs.type, /*allow_abstract=*/0, &mname, &mloc); /* Bitfield form `: width` after the declarator name (or after the @@ -2097,6 +2160,9 @@ static void parse_primary(Parser* p) { switch (e->kind) { case SEK_LOCAL: cg_push_local_typed(p->cg, e->v.slot, e->type); + if (e->vla_byte_slot != FRAME_SLOT_NONE) { + p->last_pushed_vla_slot = e->vla_byte_slot; + } return; case SEK_GLOBAL: case SEK_FUNC: @@ -2472,6 +2538,7 @@ static void parse_unary(Parser* p) { * declared object — both reducible to a type lookup with no * emission. Other expression forms are diagnosed. */ const Type* ty = NULL; + FrameSlot vla_slot = FRAME_SLOT_NONE; advance(p); if (is_punct(&p->cur, '(')) { Tok n = peek1(p); @@ -2479,19 +2546,14 @@ static void parse_unary(Parser* p) { advance(p); ty = parse_type_name(p); expect_punct(p, ')', "')'"); - } else if (n.kind == TOK_IDENT && ident_kw(p, n.v.ident) == KW_NONE) { - /* `sizeof(IDENT)` where IDENT is an object — look up its type. */ - SymEntry* e; - advance(p); /* '(' */ - e = scope_lookup(p, p->cur.v.ident); - if (!e) { - compiler_panic(p->c, p->cur.loc, "undeclared identifier"); - } - ty = e->type; - advance(p); /* IDENT */ - expect_punct(p, ')', "')'"); } else { - perr(p, "sizeof of expression not supported in v1 slice"); + /* `sizeof ( expression )` — parenthesized expression form. Fall + * through to the unary path so paren-primary handles the `(`. */ + p->last_pushed_vla_slot = FRAME_SLOT_NONE; + parse_unary(p); + ty = cg_top_type(p->cg); + vla_slot = p->last_pushed_vla_slot; + cg_drop(p->cg); } } else { /* `sizeof unary-expression`: §6.5.3.4 says the operand is not @@ -2500,11 +2562,20 @@ static void parse_unary(Parser* p) { * (no load is emitted) so for the corpus shapes (array, subscript, * member access) this is side-effect-free. VLA operands need * actual evaluation and are deferred. */ + p->last_pushed_vla_slot = FRAME_SLOT_NONE; parse_unary(p); ty = cg_top_type(p->cg); + vla_slot = p->last_pushed_vla_slot; cg_drop(p->cg); } - cg_push_int(p->cg, (i64)abi_sizeof(p->abi, ty), ty_size_t(p)); + if (vla_slot != FRAME_SLOT_NONE) { + /* sizeof on a VLA-bound IDENT: emit the runtime byte-size load + * instead of the constant pointer width. */ + cg_push_local_typed(p->cg, vla_slot, ty_size_t(p)); + cg_load(p->cg); + } else { + cg_push_int(p->cg, (i64)abi_sizeof(p->abi, ty), ty_size_t(p)); + } return; } if (is_kw(p, &t, KW_GENERIC)) { @@ -2569,47 +2640,74 @@ static void parse_unary(Parser* p) { * association is `int:`, so the first-match path is hit before * default. */ int emitted = 0; - int saw_default = 0; - /* Helper: skip an assignment-expression using paren/bracket/brace - * depth bookkeeping. Stops on `,` or `)` at depth 0. */ + /* Buffer for the `default:` association's expression tokens, so we + * can replay it if no typed association matches. Recording happens + * at most once (the C standard allows at most one default). The + * trailing `,` or `)` that ended the recording is included so the + * replayed parse_assign_expr stops cleanly at the same boundary. */ + Tok* default_buf = NULL; + u32 default_len = 0; for (;;) { const Type* assoc_ty = NULL; int is_default = 0; if (is_kw(p, &p->cur, KW_DEFAULT)) { advance(p); is_default = 1; - saw_default = 1; } else { assoc_ty = parse_type_name(p); } expect_punct(p, ':', "':' in _Generic association"); - /* Match if no result has been emitted yet AND either we're a - * default (only if no other matches by end — but we don't know - * that yet) or the type matches the controlling type. The - * single-pass workaround: if not a match, skip; if match, emit. - * `default` is taken if no prior assoc matched and (this is the - * approximation) no subsequent assoc would match — but we don't - * know that. For the spine row this is fine because the matching - * non-default association comes first. A robust impl needs a - * pre-scan; deferred until a corpus row exposes the gap. */ int take = 0; - if (!emitted) { - if (is_default) { - /* Defer default to end; only take if we never see a non- - * default match. Since we don't pre-scan, take default lazily - * by parsing it as if it might match, but skip code: parse - * the assoc-expr after we know no other assoc matched. As a - * compromise, skip default's assoc-expr now; if no other - * matches, panic (the corpus row never triggers this). */ - /* Skip the assoc-expr. */ - } else if (ctl_ty && assoc_ty && - ctl_ty->kind == assoc_ty->kind) { - take = 1; - } + if (!emitted && !is_default && ctl_ty && assoc_ty && + ctl_ty->kind == assoc_ty->kind) { + take = 1; } if (take) { parse_assign_expr(p); emitted = 1; + } else if (is_default && !default_buf) { + /* Record default's assoc-expr tokens for later replay. */ + u32 cap = 16; + Tok* buf = arena_array(p->c->tu, Tok, cap); + u32 len = 0; + int paren_depth = 0, brack_depth = 0, brace_depth = 0; + while (p->cur.kind != TOK_EOF) { + if (paren_depth == 0 && brack_depth == 0 && brace_depth == 0) { + if (is_punct(&p->cur, ',') || is_punct(&p->cur, ')')) break; + } + if (len == cap) { + u32 new_cap = cap * 2; + Tok* nv = arena_array(p->c->tu, Tok, new_cap); + if (!nv) perr(p, "out of memory recording _Generic default"); + memcpy(nv, buf, len * sizeof(Tok)); + buf = nv; + cap = new_cap; + } + buf[len++] = p->cur; + if (is_punct(&p->cur, '(')) ++paren_depth; + else if (is_punct(&p->cur, ')')) --paren_depth; + else if (is_punct(&p->cur, '[')) ++brack_depth; + else if (is_punct(&p->cur, ']')) --brack_depth; + else if (is_punct(&p->cur, '{')) ++brace_depth; + else if (is_punct(&p->cur, '}')) --brace_depth; + advance(p); + } + /* Append a sentinel `,` so the replayed parse_assign_expr + * stops cleanly without falling through to pp_next. */ + if (len == cap) { + u32 new_cap = cap + 1; + Tok* nv = arena_array(p->c->tu, Tok, new_cap); + if (!nv) perr(p, "out of memory recording _Generic default"); + memcpy(nv, buf, len * sizeof(Tok)); + buf = nv; + cap = new_cap; + } + memset(&buf[len], 0, sizeof(Tok)); + buf[len].kind = TOK_PUNCT; + buf[len].v.punct = ','; + ++len; + default_buf = buf; + default_len = len; } else { /* Skip assoc-expr by token-balancing. */ int paren_depth = 0; @@ -2630,14 +2728,40 @@ static void parse_unary(Parser* p) { } if (!accept_punct(p, ',')) break; } + if (!emitted && default_buf) { + /* No typed association matched; replay the default's recorded + * assoc-expr through the replay buffer, then resume the original + * stream at the `)`. */ + Tok* save_replay = p->replay; + u32 save_cap = p->replay_cap; + u32 save_len = p->replay_len; + u32 save_pos = p->replay_pos; + u8 save_active = p->replay_active; + Tok save_cur = p->cur; + int save_has_next = p->has_next; + p->replay = default_buf; + p->replay_cap = default_len; + p->replay_len = default_len; + p->replay_pos = 1; + p->replay_active = 1; + p->cur = default_buf[0]; + p->has_next = 0; + parse_assign_expr(p); + emitted = 1; + /* Restore the outer stream — we don't consume the trailing + * sentinel `,` from the recorded buffer; callers expect cur = `)` + * after the loop. */ + p->replay = save_replay; + p->replay_cap = save_cap; + p->replay_len = save_len; + p->replay_pos = save_pos; + p->replay_active = save_active; + p->cur = save_cur; + p->has_next = save_has_next; + } expect_punct(p, ')', "')' after _Generic"); if (!emitted) { - /* No association matched; fall back to default if it appeared. - * Without a pre-scan we can't rewind to its assoc-expr — push 0 - * as a sentinel and panic only if there was no default. The - * corpus row never reaches this path. */ - (void)saw_default; - perr(p, "_Generic without matching association (no rewind for default)"); + perr(p, "_Generic: no association matched and no default present"); } return; } @@ -3010,6 +3134,14 @@ static void parse_ternary(Parser* p) { expect_punct(p, ':', "':' in ternary"); parse_assign_expr(p); to_rvalue(p); + /* §6.5.15 ¶5 usual arithmetic conversions: if the else-arm's type + * differs from the slot type chosen from the then-arm, coerce so the + * store types line up. v1 only converts the else-arm down/up to match + * the then-arm; full common-type widening lives behind the buffered- + * arms rewrite that's still pending. */ + if (cg_top_type(p->cg) != result_ty) { + cg_convert(p->cg, result_ty); + } cg_push_local_typed(p->cg, tmp, result_ty); cg_swap(p->cg); cg_store(p->cg); @@ -3077,7 +3209,12 @@ static void parse_assign_expr(Parser* p) { cg_load(p->cg); parse_assign_expr(p); to_rvalue(p); - cg_binop(p->cg, compound); + if (compound == BO_IADD || compound == BO_ISUB) { + /* `+=`/`-=` on a pointer needs the same scaling/decay as `+`/`-`. */ + emit_add_or_sub(p, compound); + } else { + cg_binop(p->cg, compound); + } cg_store(p->cg); } @@ -3169,6 +3306,7 @@ static int parse_decl_suffix(Parser* p, DeclSuffix* out) { out->kind = DS_ARRAY; out->count = 0; out->incomplete = 0; + out->vla = 0; /* Optional `static`/qualifiers before the size; recognized, no-op here. * `[static N]` only changes parameter ABI hints (caller promises ≥N). */ for (;;) { @@ -3183,6 +3321,28 @@ static int parse_decl_suffix(Parser* p, DeclSuffix* out) { out->incomplete = 1; return 1; } + /* Function-prototype parameter: any `[...]` decays to `T*` (§6.7.6.3 + * ¶7), so the size expression is unused. Consume tokens up to the + * matching `]` (handling `[*]`, `[n]`, nested brackets) and record + * the parameter as an incomplete array; the caller decays it to a + * pointer. */ + if (p->in_param_decl) { + int depth = 1; + while (depth > 0) { + if (p->cur.kind == TOK_EOF) { + perr(p, "unexpected EOF in parameter array bound"); + } + if (is_punct(&p->cur, '[')) ++depth; + else if (is_punct(&p->cur, ']')) { + --depth; + if (depth == 0) break; + } + advance(p); + } + out->incomplete = 1; + expect_punct(p, ']', "']' after array size"); + return 1; + } /* Constant integer size: an expression starting with a numeric or * character literal (or an enum constant) is routed through the * constant evaluator so `[3+4]`, `[N*2]` etc. round-trip. Anything @@ -3193,6 +3353,13 @@ static int parse_decl_suffix(Parser* p, DeclSuffix* out) { if (!is_const_start && t.kind == TOK_IDENT) { SymEntry* e = scope_lookup(p, t.v.ident); if (e && e->kind == SEK_ENUM_CST) is_const_start = 1; + if (!is_const_start) { + /* `sizeof` and `_Alignof` tokenize as identifiers but yield + * compile-time constants — admit them so `int a[_Alignof(T)]` + * lowers as a fixed-size array, not a VLA. */ + CKw k = ident_kw(p, t.v.ident); + if (k == KW_SIZEOF || k == KW_ALIGNOF) is_const_start = 1; + } } if (is_const_start) { SrcLoc cloc = tok_loc(&p->cur); @@ -3282,6 +3449,8 @@ static const Type* parse_declarator_full(Parser* p, const Type* base, u8 nptrs_inner = 0; u16 inner_quals[8]; int has_inner_parens = 0; + DeclSuffix inner_suffs[8]; + int n_inner_suffs = 0; if (is_punct(&p->cur, '(')) { /* Disambiguate `(declarator)` vs. function suffix `(params)`. The token @@ -3328,6 +3497,14 @@ static const Type* parse_declarator_full(Parser* p, const Type* base, } else if (!allow_abstract) { perr(p, "expected declarator name"); } + /* Inner declarator may carry its own suffixes — `int (*ops[2])(int)` + * has `[2]` between IDENT and the closing `)`. Collect them so + * they wrap LAST (closest to IDENT), after the outer suffix and + * inner pointer layers. */ + while (n_inner_suffs < 8) { + if (!parse_decl_suffix(p, &inner_suffs[n_inner_suffs])) break; + ++n_inner_suffs; + } expect_punct(p, ')', "')' after inner declarator"); } } @@ -3370,6 +3547,13 @@ static const Type* parse_declarator_full(Parser* p, const Type* base, } } + /* Apply inner declarator suffixes last — they sit closest to IDENT, so + * for `int (*ops[2])(int)` `[2]` wraps the (already-built) function- + * pointer type to give "array[2] of pointer to function(int) → int". */ + for (int i = n_inner_suffs - 1; i >= 0; --i) { + base = apply_decl_suffix(p, base, &inner_suffs[i]); + } + if (name_out) *name_out = name; if (loc_out) *loc_out = nloc; return base; @@ -3414,6 +3598,96 @@ static void push_subobject_lv(Parser* p, FrameSlot slot, const Type* arr_ty, cg_deref(p->cg, elem_ty); } +/* Emit a load+store for one scalar leaf from the source pointer + * (`src_ptr_slot`, holding a pointer rvalue) to a sub-object of the + * destination slot. `src_ptr_ty` is the slot's declared type so we read + * it back at the right width before retagging to the leaf's pointer + * type. */ +static void emit_copy_leaf(Parser* p, FrameSlot dst_slot, const Type* dst_arr_ty, + u32 dst_off, FrameSlot src_ptr_slot, + const Type* src_ptr_ty, u32 src_off, + const Type* leaf_ty) { + push_subobject_lv(p, dst_slot, dst_arr_ty, dst_off, leaf_ty); + cg_push_local_typed(p->cg, src_ptr_slot, src_ptr_ty); + cg_load(p->cg); + cg_retag_top(p->cg, type_ptr(p->pool, leaf_ty)); + if (src_off > 0) { + cg_push_int(p->cg, (i64)src_off, ty_size_t(p)); + cg_binop(p->cg, BO_IADD); + } + cg_deref(p->cg, leaf_ty); + cg_load(p->cg); + cg_store(p->cg); + cg_drop(p->cg); +} + +/* Walk a (possibly nested) aggregate type, emitting a leaf load+store + * for each scalar member. Used to lower `struct s = expr;` and + * `struct s = (struct S){...};` after the source's address has been + * spilled into `src_ptr_slot`. Bitfields and flexible array members are + * not supported here yet. */ +static void emit_walk_copy(Parser* p, FrameSlot dst_slot, + const Type* dst_arr_ty, u32 dst_off, + FrameSlot src_ptr_slot, const Type* src_ptr_ty, + u32 src_off, const Type* ty) { + if (ty->kind == TY_STRUCT) { + const ABIRecordLayout* L = abi_record_layout(p->abi, ty); + for (u16 i = 0; i < ty->rec.nfields; ++i) { + const Field* f = &ty->rec.fields[i]; + if (f->flags & FIELD_BITFIELD) continue; + u32 foff = L->fields[i].offset; + emit_walk_copy(p, dst_slot, dst_arr_ty, dst_off + foff, + src_ptr_slot, src_ptr_ty, src_off + foff, f->type); + } + return; + } + if (ty->kind == TY_ARRAY) { + u32 esz = abi_sizeof(p->abi, ty->arr.elem); + for (u32 i = 0; i < ty->arr.count; ++i) { + emit_walk_copy(p, dst_slot, dst_arr_ty, dst_off + i * esz, + src_ptr_slot, src_ptr_ty, src_off + i * esz, + ty->arr.elem); + } + return; + } + if (ty->kind == TY_UNION) { + /* Byte-wise copy preserves whichever member was active. */ + u32 sz = abi_sizeof(p->abi, ty); + const Type* uchar_ty = type_prim(p->pool, TY_UCHAR); + for (u32 i = 0; i < sz; ++i) { + emit_copy_leaf(p, dst_slot, dst_arr_ty, dst_off + i, + src_ptr_slot, src_ptr_ty, src_off + i, uchar_ty); + } + return; + } + emit_copy_leaf(p, dst_slot, dst_arr_ty, dst_off, src_ptr_slot, src_ptr_ty, + src_off, ty); +} + +/* Source struct/union value is on top of the cg stack as an lvalue. + * Spill its address into a fresh pointer slot, then walk the type and + * copy each scalar leaf into the destination sub-object. */ +static void emit_struct_copy_into_slot(Parser* p, FrameSlot dst_slot, + const Type* dst_arr_ty, u32 dst_off, + const Type* ty) { + const Type* ptr_ty = type_ptr(p->pool, ty); + FrameSlotDesc fsd; + FrameSlot src_ptr_slot; + cg_addr(p->cg); + memset(&fsd, 0, sizeof fsd); + fsd.type = ptr_ty; + fsd.size = abi_sizeof(p->abi, ptr_ty); + fsd.align = abi_alignof(p->abi, ptr_ty); + fsd.kind = FS_LOCAL; + fsd.flags = FSF_NONE; + src_ptr_slot = cg_local(p->cg, &fsd); + cg_push_local_typed(p->cg, src_ptr_slot, ptr_ty); + cg_swap(p->cg); + cg_store(p->cg); + cg_drop(p->cg); + emit_walk_copy(p, dst_slot, dst_arr_ty, dst_off, src_ptr_slot, ptr_ty, 0, ty); +} + /* Recursively zero-initialize the sub-object at `offset` of type `ty`. */ static void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, const Type* ty) { @@ -3772,13 +4046,22 @@ static void init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, return; } if (ty->kind == TY_UNION) { - /* Without designators we always init the first non-bitfield member. */ + /* `union U u = {.field = expr}` per §6.7.9 ¶7 names a specific + * member; without a designator the first non-bitfield member is + * initialized. Only one member can be active, so we honor the + * (optional) leading designator and ignore the rest. */ int had_brace = accept_punct(p, '{'); if (ty->rec.nfields == 0) { if (had_brace) expect_punct(p, '}', "'}'"); return; } - { + if (had_brace && is_punct(&p->cur, '.')) { + const Type* sub_ty; + u32 sub_off; + u32 top_idx = 0; + parse_designator_chain(p, ty, offset, &sub_ty, &sub_off, &top_idx); + init_at(p, slot, arr_ty, sub_off, sub_ty); + } else { const Field* f = &ty->rec.fields[0]; if (!(f->flags & FIELD_BITFIELD)) { init_at(p, slot, arr_ty, offset, f->type); @@ -3843,6 +4126,121 @@ static void parse_static_string_at(Parser* p, u8* buf, u32 buflen, u32 offset, advance(p); } +/* Append one pending relocation to the parser-side list, growing on + * demand. Flushed by `define_static_object` after the section is pinned. */ +static void srl_push(Parser* p, u32 offset, u32 size, ObjSymId target, + i64 addend) { + if (p->static_relocs_len == p->static_relocs_cap) { + u32 nc = p->static_relocs_cap ? p->static_relocs_cap * 2u : 4u; + void* nb = arena_array(p->c->tu, char, + nc * sizeof(*p->static_relocs)); + if (!nb) perr(p, "out of memory recording static relocs"); + if (p->static_relocs && p->static_relocs_len) { + memcpy(nb, p->static_relocs, + p->static_relocs_len * sizeof(*p->static_relocs)); + } + p->static_relocs = nb; + p->static_relocs_cap = nc; + } + p->static_relocs[p->static_relocs_len].offset = offset; + p->static_relocs[p->static_relocs_len].size = size; + p->static_relocs[p->static_relocs_len].target = target; + p->static_relocs[p->static_relocs_len].addend = addend; + ++p->static_relocs_len; +} + +/* Try to parse the current expression as an address constant of pointer + * type `ty`, recording it as a pending relocation. Forms supported: + * `&IDENT` — absolute reloc at offset, addend 0 + * `&IDENT [const]` — addend = const * sizeof(elem) + * `IDENT` — IDENT is an array; same as `&IDENT[0]` + * `IDENT [+|-] const` — pointer arithmetic; addend scaled by + * sizeof(*IDENT) + * Returns 1 on success (caller should not call eval_const_int), 0 if + * the current tokens don't look like an address constant (caller falls + * back to integer-constant evaluation, which handles `(T*)0`). */ +static int try_parse_addr_const(Parser* p, const Type* ty, u8* buf, + u32 offset, u32 sz) { + Tok t = p->cur; + Sym name = 0; + SrcLoc nloc = tok_loc(&p->cur); + int saw_amp = 0; + i64 element_addend = 0; + i64 byte_addend = 0; + SymEntry* e; + const Type* tgt_ty; + ObjSymId tgt; + if (is_punct(&t, '&')) { + saw_amp = 1; + advance(p); + if (p->cur.kind != TOK_IDENT || ident_kw(p, p->cur.v.ident) != KW_NONE) { + /* Not a recognized address-of form. Bail; caller will eval as int. */ + perr(p, "expected identifier after '&' in static initializer"); + } + name = p->cur.v.ident; + nloc = tok_loc(&p->cur); + advance(p); + } else if (t.kind == TOK_IDENT && ident_kw(p, t.v.ident) == KW_NONE) { + name = t.v.ident; + advance(p); + } else { + return 0; + } + e = scope_lookup(p, name); + if (!e || e->kind != SEK_GLOBAL) { + /* Address constants must reference an object with static storage + * duration / external-or-internal linkage. Without that we have no + * symbol to point a relocation at. */ + perr(p, "static initializer requires object with static storage"); + } + tgt = e->v.sym; + tgt_ty = e->type; + /* Optional `[const]` after `&IDENT`. */ + if (saw_amp && is_punct(&p->cur, '[')) { + SrcLoc cloc; + advance(p); + cloc = tok_loc(&p->cur); + element_addend = eval_const_int(p, cloc); + expect_punct(p, ']', "']' after array-subscript constant"); + if (tgt_ty && tgt_ty->kind == TY_ARRAY) { + byte_addend += + element_addend * (i64)abi_sizeof(p->abi, tgt_ty->arr.elem); + } else { + byte_addend += element_addend; + } + } + /* Optional `+`/`-` const for pointer arithmetic. Without `&`, the + * IDENT must be an array (which decays to a pointer) for arithmetic + * to make sense. */ + while (is_punct(&p->cur, '+') || is_punct(&p->cur, '-')) { + int neg = is_punct(&p->cur, '-'); + SrcLoc cloc; + i64 v; + advance(p); + cloc = tok_loc(&p->cur); + v = eval_const_int(p, cloc); + if (neg) v = -v; + /* Scale by element size if the base is array/pointer. */ + if (tgt_ty && tgt_ty->kind == TY_ARRAY) { + byte_addend += v * (i64)abi_sizeof(p->abi, tgt_ty->arr.elem); + } else if (tgt_ty && tgt_ty->kind == TY_PTR) { + byte_addend += v * (i64)abi_sizeof(p->abi, tgt_ty->ptr.pointee); + } else if (saw_amp) { + /* `&scalar + const` measured in bytes-of(scalar). */ + byte_addend += v * (i64)abi_sizeof(p->abi, tgt_ty); + } else { + byte_addend += v; + } + } + (void)nloc; + (void)ty; + (void)buf; + /* The reloc width (R_ABS32 vs R_ABS64) follows the destination + * pointer width. */ + srl_push(p, offset, sz, tgt, byte_addend); + return 1; +} + static void parse_static_init_at(Parser* p, u8* buf, u32 buflen, u32 offset, const Type* ty) { if (ty->kind == TY_ARRAY) { @@ -3921,14 +4319,21 @@ static void parse_static_init_at(Parser* p, u8* buf, u32 buflen, u32 offset, if (ty->kind == TY_UNION) { perr(p, "static-storage union initializer not supported in Phase 4"); } - /* Scalar / pointer: integer constant only. */ + /* Scalar / pointer: integer constant or address-constant per §6.6 ¶9 + * (pointer-typed only). Address constants are recorded as pending + * relocations and resolved when the section gets pinned. */ { int had_brace = accept_punct(p, '{'); SrcLoc cloc = tok_loc(&p->cur); - i64 v = eval_const_int(p, cloc); u32 sz = abi_sizeof(p->abi, ty); if (offset + sz > buflen) perr(p, "initializer overflows object"); - encode_int_le(buf + offset, sz, v); + if (ty->kind == TY_PTR && try_parse_addr_const(p, ty, buf, offset, sz)) { + /* Address constant recorded as a reloc. Buffer bytes stay zero; + * the reloc carries the addend. */ + } else { + i64 v = eval_const_int(p, cloc); + encode_int_le(buf + offset, sz, v); + } if (had_brace) { accept_punct(p, ','); expect_punct(p, '}', "'}' after scalar initializer"); @@ -3967,10 +4372,18 @@ static void define_static_object(Parser* p, ObjSymId sym, const Type* var_ty, if (has_init) { buf = (u8*)arena_array(p->c->tu, u8, size ? size : 1u); memset(buf, 0, size); + /* Reset the pending-reloc list before parsing this initializer; the + * caller flushes the collected entries to obj_reloc once the section + * has been pinned. */ + p->static_relocs_len = 0; parse_static_init_at(p, buf, size, 0, var_ty); for (u32 i = 0; i < size; ++i) { if (buf[i]) { has_nonzero = 1; break; } } + /* Pointer-typed initializers count as nonzero — the bytes are + * patched by the loader at runtime, so we must place the object in + * .data (or .rodata when const) rather than .bss. */ + if (p->static_relocs_len) has_nonzero = 1; } override_sec = pick_object_section(p, quals, has_nonzero); @@ -3984,6 +4397,13 @@ static void define_static_object(Parser* p, ObjSymId sym, const Type* var_ty, if (dst && buf) memcpy(dst, buf, size); } obj_symbol_define(ob, sym, override_sec, base, size); + /* Flush pending pointer-init relocations against this section. */ + for (u32 i = 0; i < p->static_relocs_len; ++i) { + RelocKind rk = (p->static_relocs[i].size == 8) ? R_ABS64 : R_ABS32; + obj_reloc(ob, override_sec, base + p->static_relocs[i].offset, rk, + p->static_relocs[i].target, p->static_relocs[i].addend); + } + p->static_relocs_len = 0; (void)loc; return; } @@ -4013,6 +4433,13 @@ static void define_static_object(Parser* p, ObjSymId sym, const Type* var_ty, u8* dst = obj_reserve(ob, sec, size); if (dst) memcpy(dst, buf, size); obj_symbol_define(ob, sym, sec, base, size); + /* Flush pointer-init relocations against the .data section. */ + for (u32 i = 0; i < p->static_relocs_len; ++i) { + RelocKind rk = (p->static_relocs[i].size == 8) ? R_ABS64 : R_ABS32; + obj_reloc(ob, sec, base + p->static_relocs[i].offset, rk, + p->static_relocs[i].target, p->static_relocs[i].addend); + } + p->static_relocs_len = 0; } } @@ -4103,7 +4530,40 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { } { SymEntry* e = scope_define(p, name, SEK_TYPEDEF, var_ty); - (void)e; + /* `typedef T B[n]` (or via a VLA-typedef chain): snapshot the + * VLA byte size at typedef site so subsequent `B v` declarations + * each alloca the same captured runtime size, even if the names + * referenced in the size expression have since changed. */ + if (p->vla_pending && var_ty && var_ty->kind == TY_ARRAY) { + FrameSlot count_slot = p->vla_pending_count_slot; + const Type* elem_ty = var_ty->arr.elem; + u32 esz = abi_sizeof(p->abi, elem_ty); + FrameSlotDesc bsd; + FrameSlot byte_slot; + memset(&bsd, 0, sizeof bsd); + bsd.type = ty_size_t(p); + bsd.size = abi_sizeof(p->abi, bsd.type); + bsd.align = abi_alignof(p->abi, bsd.type); + bsd.kind = FS_LOCAL; + byte_slot = cg_local(p->cg, &bsd); + cg_set_loc(p->cg, loc); + cg_push_local_typed(p->cg, count_slot, ty_size_t(p)); + to_rvalue(p); + if (esz != 1) { + cg_push_int(p->cg, (i64)esz, ty_size_t(p)); + cg_binop(p->cg, BO_IMUL); + } + cg_push_local_typed(p->cg, byte_slot, ty_size_t(p)); + cg_swap(p->cg); + cg_store(p->cg); + cg_drop(p->cg); + e->vla_byte_slot = byte_slot; + p->vla_pending = 0; + p->vla_pending_count_slot = FRAME_SLOT_NONE; + } else if (specs->vla_byte_slot != FRAME_SLOT_NONE) { + /* Typedef of a typedef'd VLA: chain the captured slot. */ + e->vla_byte_slot = specs->vla_byte_slot; + } } (void)loc; return; @@ -4140,15 +4600,25 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { /* `extern` block-scope declaration: declares the name but does not define * storage. The matching defining declaration must appear elsewhere (file - * scope here, or another TU). */ + * scope here, or another TU). Per §6.2.2 ¶4, if a prior file-scope + * declaration of this identifier is visible, the linkage of this extern + * decl matches it (so `static int g; ... { extern int g; }` resolves to + * the same internal-linkage symbol). */ if (specs->storage == DS_EXTERN) { Decl decl_in; DeclId did; ObjSymId sym; SymEntry* e; + SymEntry* prior; if (accept_punct(p, '=')) { perr(p, "block-scope extern with initializer not supported"); } + prior = scope_lookup(p, name); + if (prior && prior->kind == SEK_GLOBAL) { + e = scope_define(p, name, SEK_GLOBAL, var_ty); + e->v.sym = prior->v.sym; + return; + } memset(&decl_in, 0, sizeof decl_in); decl_in.name = name; decl_in.type = var_ty; @@ -4163,30 +4633,62 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { return; } - /* VLA: the declarator type is `T[]` (incomplete array) with a pending - * runtime count. Bind `name` as `T*` (the pointer the alloca returns) so - * subscript/arithmetic on `a` lowers as on a pointer; `sizeof(a)` would - * need the count to be tracked separately, which is a Phase 9 follow-up. */ - if (p->vla_pending && var_ty && var_ty->kind == TY_ARRAY) { - FrameSlot count_slot = p->vla_pending_count_slot; + /* VLA: the declarator type is `T[]` (incomplete array) with either a + * pending runtime count from an inline `T name[n]` suffix, or — when + * `name` was declared via a VLA typedef — a byte-size slot already + * captured at the typedef site (carried in specs->vla_byte_slot). The + * variable binds as `T*` (alloca's return) so subscript/pointer arith + * just work; the SymEntry's vla_byte_slot lets `sizeof(name)` emit a + * runtime load instead of the constant pointer width. */ + if (var_ty && var_ty->kind == TY_ARRAY && var_ty->arr.incomplete && + (p->vla_pending || specs->vla_byte_slot != FRAME_SLOT_NONE)) { const Type* elem_ty = var_ty->arr.elem; const Type* ptr_ty = type_ptr(p->pool, elem_ty); - FrameSlot ptr_slot = make_local(p, name, ptr_ty, loc); - u32 esz = abi_sizeof(p->abi, elem_ty); - p->vla_pending = 0; - p->vla_pending_count_slot = FRAME_SLOT_NONE; - cg_set_loc(p->cg, loc); - cg_push_local_typed(p->cg, count_slot, ty_size_t(p)); - to_rvalue(p); - if (esz != 1) { - cg_push_int(p->cg, (i64)esz, ty_size_t(p)); - cg_binop(p->cg, BO_IMUL); + FrameSlot byte_slot; + FrameSlot ptr_slot; + SymEntry* sym_entry; + if (p->vla_pending) { + /* Inline VLA: derive byte size = count * sizeof(elem) and stash + * it in a fresh i64 slot. */ + FrameSlot count_slot = p->vla_pending_count_slot; + u32 esz = abi_sizeof(p->abi, elem_ty); + FrameSlotDesc bsd; + memset(&bsd, 0, sizeof bsd); + bsd.type = ty_size_t(p); + bsd.size = abi_sizeof(p->abi, bsd.type); + bsd.align = abi_alignof(p->abi, bsd.type); + bsd.kind = FS_LOCAL; + byte_slot = cg_local(p->cg, &bsd); + p->vla_pending = 0; + p->vla_pending_count_slot = FRAME_SLOT_NONE; + cg_set_loc(p->cg, loc); + cg_push_local_typed(p->cg, count_slot, ty_size_t(p)); + to_rvalue(p); + if (esz != 1) { + cg_push_int(p->cg, (i64)esz, ty_size_t(p)); + cg_binop(p->cg, BO_IMUL); + } + cg_push_local_typed(p->cg, byte_slot, ty_size_t(p)); + cg_swap(p->cg); + cg_store(p->cg); + cg_drop(p->cg); + } else { + /* Typedef'd VLA: byte-size already captured at typedef site. */ + byte_slot = specs->vla_byte_slot; } + ptr_slot = make_local(p, name, ptr_ty, loc); + cg_set_loc(p->cg, loc); + cg_push_local_typed(p->cg, byte_slot, ty_size_t(p)); + cg_load(p->cg); cg_alloca(p->cg); cg_push_local_typed(p->cg, ptr_slot, ptr_ty); cg_swap(p->cg); cg_store(p->cg); cg_drop(p->cg); + sym_entry = scope_lookup(p, name); + if (sym_entry && sym_entry->kind == SEK_LOCAL) { + sym_entry->vla_byte_slot = byte_slot; + } if (accept_punct(p, '=')) { perr(p, "VLA initializers are not allowed (§6.7.9 ¶3)"); } @@ -4210,7 +4712,13 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { s = make_local_aligned(p, name, var_ty, loc, specs->align); if (accept_punct(p, '=')) { cg_set_loc(p->cg, loc); - if (var_ty->kind == TY_ARRAY || var_ty->kind == TY_STRUCT || + if ((var_ty->kind == TY_STRUCT || var_ty->kind == TY_UNION) && + !is_punct(&p->cur, '{')) { + /* §6.7.9 ¶13: an aggregate initializer that is not a brace list + * must be a single expression of compatible type — copy it. */ + parse_assign_expr(p); + emit_struct_copy_into_slot(p, s, var_ty, 0, var_ty); + } else if (var_ty->kind == TY_ARRAY || var_ty->kind == TY_STRUCT || var_ty->kind == TY_UNION) { /* Brace initializer (or string literal — Phase 6). */ init_at(p, s, var_ty, 0, var_ty); @@ -4736,8 +5244,10 @@ static void parse_param_list(Parser* p, ParamInfo** infos_out, u16* nparams_out, if (!parse_decl_specs(p, &specs)) { perr(p, "expected parameter type"); } + p->in_param_decl++; pty = parse_declarator_full(p, specs.type, /*allow_abstract=*/1, &pname, &ploc); + p->in_param_decl--; /* Adjust array/function parameter to pointer per §6.7.6.3. */ if (pty && pty->kind == TY_ARRAY) { pty = type_ptr(p->pool, pty->arr.elem); @@ -4991,9 +5501,19 @@ static void parse_external_decl(Parser* p) { if (existing && existing->kind == SEK_GLOBAL) { /* Redeclaration: reuse the prior ObjSymId so the linker sees one - * symbol. Compatible-types checks live in Phase 10. */ + * symbol. Compatible-types checks live in Phase 10. + * §6.2.7 composite type: if either declarator gives a complete array + * size where the other is incomplete, the composite is the complete + * one — propagate that to the SymEntry so later uses (e.g. sizeof) + * see the size. */ sym = existing->v.sym; e = existing; + if (e->type && base_ty && e->type->kind == TY_ARRAY && + base_ty->kind == TY_ARRAY) { + if (e->type->arr.incomplete && !base_ty->arr.incomplete) { + e->type = base_ty; + } + } } else { Decl decl_in; DeclId did;