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:
| D | doc/parser-status.md | | | 544 | ------------------------------------------------------------------------------- |
| M | src/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;