commit 7229b6f010e4525747e734627c71671bdef4275b parent 1434889223137803ad7d7f6b4263ab08d9682dfa Author: Ryan Sepassi <rsepassi@gmail.com> Date: Mon, 11 May 2026 09:05:55 -0700 attr: phase-2 PR-0 — interface seed, plan doc, drafted tests PR-0 is the inert structural change that unblocks the Phase 2 parallel workers (W1–W5 in doc/ATTRIBUTE.md). No behavior change: new fields default to zero, new function is a no-op, existing callers untouched. - src/parse/attr.h: extract Attr/AttrKind/AttrArgShape so consumers in src/decl can decode the parser's attribute lists. - src/type: Field.{packed,align_override}; Type.rec.{packed, align_override}; TypeRecordOpts + type_record_begin_ex. - src/decl: DF_NORETURN/DF_ALWAYS_INLINE/DF_NOINLINE/DF_GNU_INLINE; Decl.{align,alias_target}; decl_attrs.{h,c} with a no-op attr_list_to_decl stub. - src/arch/arch.h: CGFuncDescFlag with CGFD_NORETURN; CGFuncDesc.flags. - doc/ATTRIBUTE.md: full Phase 2 plan, parallelization writeup (PR-0 → W1–W5 → PR-Z), checklist by worker. - test/parse/cases{,_err}/attr_p2_*: 14 Phase 2 tests drafted up-front; flip to passing as each worker's feature lands. Diffstat:
29 files changed, 718 insertions(+), 65 deletions(-)
diff --git a/doc/ATTRIBUTE.md b/doc/ATTRIBUTE.md @@ -296,6 +296,395 @@ Error cases in `test/parse/cases_err/`: ## Phase 2 — honor the small set -Out of scope for this PR. Tracked separately: wire `packed`, `aligned`, -`section`, `used`, `noreturn`, `alias`, `weak`, `visibility` through -the existing `ObjSym` / record-layout / link paths. +Phase 1 lands the parser, the AST carriers (`DeclSpecs.attrs`, +`TagEntry.attrs`, `SymEntry.attrs`), and the attribute table. Phase 2 +is the consumer side: drain those lists at well-defined points and +fold their effects into `Type`, `ABIRecordLayout`, `Decl`, `ObjSym`, +and `CGFuncDesc`. + +### Scope (the honored set) + +| Attribute | Carrier read | Effect | +| ---------------- | ------------------ | ---------------------------------------------------------- | +| `packed` | `TagEntry.attrs` | record layout: per-member align clamped to 1 | +| `aligned(N)` | `TagEntry.attrs` | record `align` raised to `max(natural, N)` | +| `aligned(N)` | `Field.attrs` | per-member alignment raised (interacts with `packed`) | +| `aligned(N)` | `SymEntry.attrs` | object/function alignment — same channel as `_Alignas` | +| `section("nm")` | `SymEntry.attrs` | `Decl.section_id` → object placement / `CGFuncDesc` | +| `used` | `SymEntry.attrs` | `SF_RETAIN` on the defining section (clang/GCC parity) | +| `noreturn` | `SymEntry.attrs` | `DF_NORETURN` on the `Decl`; CG may drop epilogue | +| `alias("tgt")` | `SymEntry.attrs` | implement `decl_define_alias`; no body | +| `weak` | `SymEntry.attrs` | `DF_WEAK` (already exists) → `SB_WEAK` in `decl_declare` | +| `visibility(s)` | `SymEntry.attrs` | `Decl.visibility = SV_HIDDEN/PROTECTED/INTERNAL/DEFAULT` | +| `always_inline` | `SymEntry.attrs` | new `DF_ALWAYS_INLINE` — recorded, no-op until inliner | +| `noinline` | `SymEntry.attrs` | new `DF_NOINLINE` — recorded, no-op until inliner | +| `gnu_inline` | `SymEntry.attrs` | new `DF_GNU_INLINE` — recorded, no-op until inliner | + +Field-level (`Field.attrs`) is new in Phase 2: extend `Field` with one +`u16 align_override` and one `u8 packed` (cheaper than threading the +whole `Attr*` into the immutable `Type`). The parser populates these +before calling `type_record_field`. + +### Type-layer changes + +1. `src/type/type.h` — `Field`: add `u16 align_override;` and + `u8 packed;`. Extend `Type.rec` with `u8 packed;` and + `u16 align_override;` (both zero = "no override"). +2. `src/type/type.c`: + - `type_record_begin` gains `int packed, u16 align_override` (or a + small `TypeRecordOpts` struct so future flags don't churn the + signature). Stash on the builder, copy into `Type.rec` in + `type_record_end`. + - `type_record_field` accepts `Field` with the new fields already + filled — no other change. +3. `src/abi/abi.c::compute_record_layout`: + - For each field, if `t->rec.packed` is set, clamp `fi.align` to 1 + before the offset bump; bitfields still honor their storage-unit + size but with `align = 1`. + - If a field has `align_override > fi.align`, raise its alignment + (subject to packed clamp; GCC: packed wins over field-level + aligned for *reducing* alignment, but field-level `aligned(N)` on + a packed record still increases that field's alignment). + - `max_align` calculation uses the post-clamp value, so packed + + no record-level aligned ⇒ record align 1. + - After natural-align rounding, if `t->rec.align_override` is set, + raise `L->align` (and the size-rounding mask) to it. +4. Layout cache key remains `Type*` identity — no change. Anonymous + inline records with attributes still get a unique `Type` (already + true since `type_record_end` allocates fresh). + +### Parser plumbing (carrier → effect) + +1. **Record attrs → Type construction.** `parse_struct_or_union` + already chains attrs onto `TagEntry.attrs`. Before + `type_record_install` / `type_record_end`, scan that list once: + - `ATTR_PACKED` → `packed=1` + - `ATTR_ALIGNED` → `align_override = max(curr, v.i)`; bare + `aligned` with no arg uses the ABI's default + "biggest scalar align" (`abi_alignof(ptrdiff_t)` is a sufficient + stand-in for v1). + Unknown / non-honored attrs in the list are ignored. +2. **Member attrs.** New helper `attr_list_to_field(Attr*, Field*)` + sets `Field.packed` and `Field.align_override`. Called inside the + member loop in `parse_struct_or_union` after the member declarator + parses; the per-member `Attr*` is currently discarded — wire it in + here. +3. **Symbol attrs → Decl.** New helper + `attr_list_to_decl(const Attr*, Decl*)` in a new file + `src/decl/decl_attrs.c` (kept out of `parse.c` since several call + sites need it): + + ```c + void attr_list_to_decl(Compiler*, const Attr*, Decl* out); + ``` + + For each attribute it sets the corresponding `Decl` field + (`section_id`, `visibility`, `DF_WEAK`, `DF_USED`, `DF_NORETURN`, + `DF_ALWAYS_INLINE`, etc.). For `section`, it interns the section + name into the right `SecKind` (heuristic: contains `.text` → + `SEC_TEXT`; `.rodata` → `SEC_RODATA`; `.bss` if zero-init, + otherwise `SEC_DATA`) and calls `obj_section` to get the + `ObjSecId`. For `alias`, it records the target name on the `Decl` + (new field `Sym alias_target;`) — resolution happens in + `decl_finalize` (see §"Alias resolution"). +4. Call sites: every place in `parse.c` that builds a `Decl` and then + calls `decl_declare` (lines ~5149, ~5180, ~5858, ~6107) calls + `attr_list_to_decl(...)` on the matching `SymEntry.attrs` *before* + `decl_declare`. The function-definition path also propagates + `Decl.section_id` into `CGFuncDesc.text_section_id` (today set by + the caller of `cg_func_begin`). +5. `_Alignas` interaction: `DeclSpecs.align` and an + `aligned(N)`-attached attr feed the same channel. Reuse the + existing `align_override` parameter passed into + `define_static_object` — take `max(specs.align, attr_align)` at + the call site. + +### Decl / ObjSym wiring + +1. `src/decl/decl.h`: + - Extend `DeclFlag` with `DF_NORETURN`, `DF_ALWAYS_INLINE`, + `DF_NOINLINE`, `DF_GNU_INLINE`. + - Add `Sym alias_target;` and `u32 align;` to `Decl` (align lifts + out of the per-call-site `align_override` so it's a single + `Decl` truth). +2. `src/decl/decl.c::decl_declare`: + - If `DF_WEAK` is set, bind is `SB_WEAK` (override the + `DL_EXTERNAL → SB_GLOBAL` default). + - Use `obj_symbol_ex` (not `obj_symbol`) so `Decl.visibility` + reaches `ObjSym.vis`. + - When `Decl.section_id != OBJ_SEC_NONE`, pass it through. +3. `decl_define_function` / `decl_define_object`: + - Honor `Decl.section_id` if set — bypass the default + `.text`/`.data`/`.bss`/`.rodata` picker. + - If `DF_USED`, call `obj_section_set_flags` to OR in `SF_RETAIN` + on the defining section (matches clang's + `__attribute__((retain))` and `__attribute__((used))` on syms + in `--gc-sections` builds; matches the GC root rule at + `link_layout.c:399`). +4. `decl_define_alias` — currently a stub. Implementation: + - Look up `target`'s `ObjSymId` (must be a prior `decl_declare` in + the same TU; cross-TU aliasing isn't in scope). + - If `target` is already defined: `obj_symbol_define(self, + target.section_id, target.value, target.size)`. + - Otherwise queue a fixup on `decl_finalize` (or fail loudly — + v1 can require the target to precede the alias, matching cfree's + single-pass parse). + - `self.bind` follows `DF_WEAK` (weakref-like) and `Decl.visibility`. + +### Codegen / parser-side function attrs + +- `parse_function_body` reads `SymEntry.attrs` to populate + `CGFuncDesc`: + - `section(".text.foo")` → `text_section_id` from + `Decl.section_id` (set by `attr_list_to_decl`). + - `DF_NORETURN` → propagate to a new `CGFuncDesc.flags & + CGFD_NORETURN`. CG may use it to omit the trailing epilogue; v1 + can still emit the epilogue (matches Phase 1 of `_Noreturn`). + - `always_inline` / `noinline` / `gnu_inline` — store on + `Decl.flags` only. cfree has no inliner, so no codegen change. + +### Diagnostics + +Phase 1 is permissive on unknowns and validates argument shape. Phase +2 adds three semantic checks at attribute-consumption time: + +- `aligned(N)` where N is not a power of two ≤ some cap (256 is GCC's + default, but cfree's ABI never asks for more than 16; cap at 4096 + with a soft warning above 16). +- `alias("target")` with unresolved target at finalize. +- `visibility("...")` with an unknown string (only `default`, + `hidden`, `protected`, `internal`). + +`section("name")` strings are not validated against the obj format +beyond minimum length > 0 — GCC also accepts arbitrary names. + +### Test coverage (Phase 2) + +Phase 2 tests live alongside the Phase 1 ones in `test/parse/cases/` +but their `.expected` reflects honored semantics. New cases: + +- `attr_p2_01_packed_sizeof.c` — `sizeof(packed struct)` returns the + *packed* layout (Phase 1's case returns unpacked). +- `attr_p2_02_packed_member_offset.c` — `offsetof(S, b) == 1` for + `struct __attribute__((packed)) { char a; int b; }`. +- `attr_p2_03_aligned_record.c` — `_Alignof(S) == 16` for record with + `aligned(16)`. +- `attr_p2_04_aligned_field.c` — per-member `aligned(8)` raises field + offset. +- `attr_p2_05_packed_with_field_aligned.c` — packed record with + field-level aligned: field aligned, record packed. +- `attr_p2_06_section_var.c` — runs an integration check (read the + emitted `.o`, assert the symbol's section name). +- `attr_p2_07_used_static.c` — `--gc-sections` link drops nothing + (parallel to existing `link_layout` retain tests). +- `attr_p2_08_weak_undef.c` — undefined weak resolves to 0 at link + time without error. +- `attr_p2_09_visibility_hidden.c` — `.o` symbol table shows + `STV_HIDDEN` (ELF) / `N_PEXT` (Mach-O). +- `attr_p2_10_alias.c` — `int foo() __attribute__((alias("bar")));` + — calls through `foo` execute `bar`'s body. +- `attr_p2_11_noreturn.c` — function marked `noreturn`; behavior is a + no-op today, test just confirms it compiles & runs the same as the + `_Noreturn` keyword path. + +Error cases: + +- `attr_p2_aligned_not_pow2.c` — `aligned(3)` → error. +- `attr_p2_alias_unresolved.c` — alias to a name with no + prior declaration → error. +- `attr_p2_visibility_bad.c` — `visibility("totallyfake")` → error. + +### Parallelization + +Phase 2's conflict surface is small — three multi-touch files +(`parse.c`, `decl.c`, `abi.c`) and a long tail of single-owner files. +Landing the headers first as inert no-ops lets four workers fan out +in parallel with no merge friction. + +**PR-0 — interface seed (done).** Single small PR, zero behavior +change. Adds the *shape* every other worker needs: + +- `src/parse/attr.h` (new) — extracts `AttrKind` / `AttrArgShape` / + `Attr` from `parse.c` so consumers in `src/decl` can decode them. +- `src/type/type.h,c` — `Field.{packed, align_override}`; + `Type.rec.{packed, align_override}`; `TypeRecordOpts` + + `type_record_begin_ex` (plain `type_record_begin` delegates with + zeros — existing callers unchanged). +- `src/decl/decl.h` — `DF_NORETURN`, `DF_ALWAYS_INLINE`, + `DF_NOINLINE`, `DF_GNU_INLINE`; `Decl.{align, alias_target}`. +- `src/decl/decl_attrs.{h,c}` (new) — declares and stubs + `attr_list_to_decl(Compiler*, const Attr*, Decl*)` as a no-op. +- `src/arch/arch.h` — `CGFuncDescFlag` enum with `CGFD_NORETURN`; + `CGFuncDesc.flags`. Unread by backends. + +After PR-0 lands every header is final and the workers below branch +without touching each other's interfaces. + +**Parallel workers** (all independent after PR-0): + +| Worker | Files (sole owner) | Output | +| ------ | --------------------------------------------------- | ------------------------------------------------- | +| **W1** | `src/abi/abi.c::compute_record_layout` | Layout honors `Type.rec` + `Field` packed/aligned | +| **W2** | `src/decl/decl_attrs.c` (fill body) | `attr_list_to_decl` decodes `Attr*` → `Decl` | +| **W3** | `src/decl/decl.c` (`decl_declare`, `decl_define_*`) | `DF_WEAK`/visibility/section/`used` honoring | +| **W4** | `src/decl/decl.c` (`decl_define_alias`) | Alias resolution | +| **W5** | `test/parse/cases{,_err}/attr_p2_*.c` (14 files) | Tests drafted up-front; flipped on as features land | + +Notes: + +- W3 + W4 both edit `decl.c` but disjoint functions — sequence as + two commits on a shared branch. +- W1 stands alone: `compute_record_layout` is ~40 LOC; the new + `Type` / `Field` bits arrive zeroed today, so the patch is + immediately exercisable by hand-building a `Type` in a fixture. +- W5 is pure throughput — every test is its own `.c` file with no + interdependencies. Drafted up-front against the spec; each test + flips from "skip/fail" to "pass" as its underlying feature lands. + +**Sequential tail — PR-Z.** Parser wire-up (~50 LOC in `parse.c`): + +1. `parse_struct_or_union` drains `TagEntry.attrs` into + `TypeRecordOpts`; drains per-member `Attr*` into `Field` before + `type_record_field`. +2. Each `decl_declare` site (parse.c lines ~5149, ~5180, ~5858, + ~6107) calls `attr_list_to_decl(p->c, ent->attrs, &decl_in)`. +3. `parse_function_body` copies `Decl.section_id` → + `CGFuncDesc.text_section_id` and `DF_NORETURN` → + `CGFuncDesc.flags`. + +Critical path: **PR-0 → max(W1, W2, W3+W4) → PR-Z**. With W5 fully +parallel and the longest non-test worker measured in single-day +chunks, Phase 2 is plausibly two to three working days of +wall-clock instead of two weeks serial. + +## Checklist + +### Phase 1 — parse-only (done) + +- [x] Lexer: `__attribute__` matched via IDENT (interned `Sym`). +- [x] `AttrKind` enum. +- [x] `Attr` struct with `kind`, `nargs`, `loc`, canonical `name`, + decoded `v` union, `next`. +- [x] `kAttrTable` recognition table. +- [x] `parse_attribute_spec_list` consumes one or more + `__attribute__((...))` runs. +- [x] `classify_attr` with `__name__` ↔ `name` canonicalization. +- [x] `parse_attr_args` shape-driven dispatch, `AS_OPAQUE` skip via + balanced-paren counter. +- [x] `DeclSpecs.attrs` carrier; populated in `parse_decl_specs`. +- [x] `TagEntry.attrs` carrier; populated for leading + trailing + record attrs; `Attr** anon_attrs_out` parameter on + `parse_struct_or_union` and `parse_enum`. +- [x] `SymEntry.attrs` carrier; populated in init-declarator and + function-declarator paths. +- [x] Pointer-layer attribute consumption (discarded). +- [x] Member-position attribute consumption (discarded — wires to + `Field` in Phase 2). +- [x] Empty `__attribute__(())` / `__attribute__((,))` accepted. +- [x] Unknown attributes silently parsed. +- [x] Argument-shape errors via `perr`. +- [x] Phase 1 smoke tests (`attr_01_…attr_17_…`). +- [x] Phase 1 error tests (`attr_aligned_wrong_arg.c`, + `attr_format_wrong_arity.c`, `attr_section_no_string.c`, + `attr_unterminated.c`). + +### Phase 2 — PR-0 interface seed (done) + +Inert structural changes that unblock W1–W5. + +- [x] Extract `AttrKind` / `AttrArgShape` / `Attr` into + `src/parse/attr.h`; `parse.c` re-includes. +- [x] Add `u8 packed; u16 align_override;` to `Type.rec`. +- [x] Add `u8 packed; u16 align_override;` to `Field`. +- [x] `TypeRecordOpts` + `type_record_begin_ex`; plain + `type_record_begin` delegates with zeros. +- [x] `type_record_end` and `type_record_forward` initialize the + new `Type.rec` fields. +- [x] `DeclFlag`: add `DF_NORETURN`, `DF_ALWAYS_INLINE`, + `DF_NOINLINE`, `DF_GNU_INLINE`. +- [x] `Decl`: add `Sym alias_target;` and `u32 align;`. +- [x] New `src/decl/decl_attrs.{h,c}` declaring + `attr_list_to_decl` as a no-op stub. +- [x] `CGFuncDescFlag` enum with `CGFD_NORETURN`; `CGFuncDesc.flags`. + +### Phase 2 — honor the small set + +**W1 — Type / ABI layout** (`src/abi/abi.c`) + +- [ ] `compute_record_layout`: per-field align clamp under + `rec.packed`; per-field `align_override` raise; + record-level `align_override` raise. + +**W2 — `attr_list_to_decl` body** (`src/decl/decl_attrs.c`) + +- [ ] Decode `ATTR_ALIGNED` → `Decl.align`. +- [ ] Decode `ATTR_SECTION` → intern/create `ObjSecId` → + `Decl.section_id`. +- [ ] Decode `ATTR_USED` / `ATTR_WEAK` / `ATTR_NORETURN` / + `ATTR_ALWAYS_INLINE` / `ATTR_NOINLINE` / `ATTR_GNU_INLINE` → + `Decl.flags`. +- [ ] Decode `ATTR_VISIBILITY` → `Decl.visibility`. +- [ ] Decode `ATTR_ALIAS` → `Decl.alias_target`. + +**W3 — Decl honoring** (`src/decl/decl.c`) + +- [ ] `decl_declare` honors `DF_WEAK` → `SB_WEAK`; uses + `obj_symbol_ex` so `Decl.visibility` reaches `ObjSym.vis`. +- [ ] `decl_define_object` / `decl_define_function` honor + `Decl.section_id` (bypass default picker). +- [ ] `decl_define_*` set `SF_RETAIN` on the defining section when + `DF_USED`. +- [ ] `define_static_object` takes `max(specs.align, attr_align)`. + +**W4 — Aliases** (`src/decl/decl.c`) + +- [ ] Implement `decl_define_alias` (presently a stub). + +**PR-Z — Parser wire-up** (`src/parse/parse.c`) + +- [ ] `parse_struct_or_union`: drain `TagEntry.attrs` into + `TypeRecordOpts` before `type_record_end`. +- [ ] Member loop: drain per-member `Attr*` into `Field` before + `type_record_field`. +- [ ] Call `attr_list_to_decl` at each `decl_declare` site + (file-scope objects, statics, externs, functions). +- [ ] `parse_function_body`: copy `Decl.section_id` → + `CGFuncDesc.text_section_id`; copy `DF_NORETURN` → + `CGFuncDesc.flags`. + +**Diagnostics** + +- [ ] `aligned(N)` power-of-two check + soft cap. +- [ ] `visibility(s)` value validation. +- [ ] `alias("target")` unresolved-target check at finalize. + +**W5 — Tests** (all drafted; will flip to passing as features land) + +- [x] `attr_p2_01_packed_sizeof.c` +- [x] `attr_p2_02_packed_member_offset.c` +- [x] `attr_p2_03_aligned_record.c` +- [x] `attr_p2_04_aligned_field.c` +- [x] `attr_p2_05_packed_with_field_aligned.c` +- [x] `attr_p2_06_section_var.c` +- [x] `attr_p2_07_used_static.c` (`--gc-sections` retention) +- [x] `attr_p2_08_weak_undef.c` +- [x] `attr_p2_09_visibility_hidden.c` +- [x] `attr_p2_10_alias.c` +- [x] `attr_p2_11_noreturn.c` +- [x] `attr_p2_aligned_not_pow2.c` (error) +- [x] `attr_p2_alias_unresolved.c` (error) +- [x] `attr_p2_visibility_bad.c` (error) + +### Out of scope (deferred past Phase 2) + +- [ ] C23 `[[...]]` attribute form on the same AST. +- [ ] Statement attributes (`__attribute__` on labels, `fallthrough`). +- [ ] Attributes on parameters. +- [ ] Pointer-layer `aligned` (currently discarded). +- [ ] `format(printf, m, n)` checking — recorded but never + diagnosed. +- [ ] `constructor` / `destructor` (need `.init_array`/`.fini_array` + emission + priority sort). +- [ ] `cleanup(fn)` (block-scope lifetime hook; needs scope-exit + runtime). +- [ ] `mode(...)`, `vector_size(...)`, `transparent_union`. diff --git a/src/arch/arch.h b/src/arch/arch.h @@ -310,6 +310,14 @@ typedef struct CGParamDesc { * carries the user's request; CG/decl decides the section name policy * (default .text, vs .text.<sym> under -ffunction-sections, vs explicit * attribute). The backend just writes to the named section. */ +/* Phase 2 attribute-derived hints. The backends are free to ignore these; + * they exist so the parser can communicate _Noreturn / __attribute__ + * info down to CG without forcing every backend to consult the Decl. */ +typedef enum CGFuncDescFlag { + CGFD_NONE = 0, + CGFD_NORETURN = 1u << 0, +} CGFuncDescFlag; + typedef struct CGFuncDesc { ObjSymId sym; ObjSecId text_section_id; @@ -319,6 +327,7 @@ typedef struct CGFuncDesc { const CGParamDesc* params; u32 nparams; SrcLoc loc; + u32 flags; /* CGFuncDescFlag */ } CGFuncDesc; typedef enum CGCallFlag { diff --git a/src/decl/decl.h b/src/decl/decl.h @@ -34,6 +34,13 @@ typedef enum DeclFlag { DF_USED = 1u << 3, DF_WEAK = 1u << 4, DF_STATIC_LOCAL = 1u << 5, + /* Phase 2 attribute-honoring flags. DF_NORETURN is the unified bit for + * _Noreturn and __attribute__((noreturn)); the inline-policy flags are + * recorded but not yet consulted (cfree has no inliner). */ + DF_NORETURN = 1u << 6, + DF_ALWAYS_INLINE = 1u << 7, + DF_NOINLINE = 1u << 8, + DF_GNU_INLINE = 1u << 9, } DeclFlag; typedef struct Decl { @@ -48,6 +55,9 @@ typedef struct Decl { u8 visibility; /* SymVis */ u8 pad; u32 flags; /* DeclFlag */ + /* Phase 2 attribute carriers — populated by attr_list_to_decl. */ + u32 align; /* explicit alignment from _Alignas or aligned(N); 0=natural */ + Sym alias_target; /* target name for __attribute__((alias("..."))); 0=none */ } Decl; typedef enum InitKind { diff --git a/src/decl/decl_attrs.c b/src/decl/decl_attrs.c @@ -0,0 +1,12 @@ +#include "decl/decl_attrs.h" + +/* PR-0 interface seed: no-op body. The Phase 2 worker (W2 in + * doc/ATTRIBUTE.md's parallelization plan) fills this in. Once + * implemented, parse.c gains a single call per decl-declaring site + * and every honored attribute lights up without parser changes. */ + +void attr_list_to_decl(Compiler* c, const Attr* attrs, Decl* out) { + (void)c; + (void)attrs; + (void)out; +} diff --git a/src/decl/decl_attrs.h b/src/decl/decl_attrs.h @@ -0,0 +1,30 @@ +#ifndef CFREE_DECL_ATTRS_H +#define CFREE_DECL_ATTRS_H + +#include "core/core.h" +#include "decl/decl.h" +#include "parse/attr.h" + +/* Decodes a parser-produced Attr* list onto a Decl. Walks the chain and + * applies every honored attribute (see doc/ATTRIBUTE.md "Phase 2"): + * + * packed — N/A here (record-level; see Type.rec.packed) + * aligned(N) — Decl.align = max(Decl.align, N) + * section("s") — interns/creates ObjSecId, stores Decl.section_id + * used — Decl.flags |= DF_USED + * noreturn — Decl.flags |= DF_NORETURN + * alias("t") — Decl.alias_target = intern("t") + * weak — Decl.flags |= DF_WEAK + * visibility(s)— Decl.visibility = SV_* + * always_inline / noinline / gnu_inline — Decl.flags |= DF_* + * + * Unknown / non-honored attributes (deprecated, format, nonnull, ...) + * are silently skipped — they were validated for argument shape during + * parsing and have no Decl-side effect in Phase 2. + * + * `attrs` may be NULL; `out` must be non-NULL. Idempotent: applying a + * list twice produces the same Decl state. Phase 2 callers invoke this + * once, between filling out the bulk Decl fields and decl_declare(). */ +void attr_list_to_decl(Compiler*, const Attr* attrs, Decl* out); + +#endif diff --git a/src/parse/attr.h b/src/parse/attr.h @@ -0,0 +1,78 @@ +#ifndef CFREE_PARSE_ATTR_H +#define CFREE_PARSE_ATTR_H + +#include "core/core.h" + +/* GNU __attribute__((...)) AST node, shared between the parser (producer) + * and the Phase 2 consumers in src/decl (decl_attrs.c). The parser builds + * the list and chains it onto its carriers (DeclSpecs.attrs, + * TagEntry.attrs, SymEntry.attrs); consumers walk a `const Attr*` head + * and decode according to `kind`. See doc/ATTRIBUTE.md. */ + +typedef enum AttrKind { + ATTR_UNKNOWN = 0, + ATTR_PACKED, + ATTR_ALIGNED, + ATTR_SECTION, + ATTR_USED, + ATTR_NORETURN, + ATTR_ALIAS, + ATTR_WEAK, + ATTR_VISIBILITY, + ATTR_ALWAYS_INLINE, + ATTR_NOINLINE, + ATTR_UNUSED, + ATTR_DEPRECATED, + ATTR_WARN_UNUSED_RESULT, + ATTR_FORMAT, + ATTR_NONNULL, + ATTR_RETURNS_NONNULL, + ATTR_PURE, + ATTR_CONST, + ATTR_MALLOC, + ATTR_NOTHROW, + ATTR_LEAF, + ATTR_COLD, + ATTR_HOT, + ATTR_CONSTRUCTOR, + ATTR_DESTRUCTOR, + ATTR_CLEANUP, + ATTR_MODE, + ATTR_VECTOR_SIZE, + ATTR_TRANSPARENT_UNION, + ATTR_GNU_INLINE, + ATTR_FALLTHROUGH, + ATTR_SENTINEL, + ATTR_NO_INSTRUMENT_FUNCTION, + ATTR_NO_SANITIZE, +} AttrKind; + +typedef enum AttrArgShape { + AS_NONE, + AS_OPTIONAL, + AS_INT, + AS_INT_OPT, + AS_STRING, + AS_IDENT, + AS_FORMAT, + AS_OPAQUE, +} AttrArgShape; + +typedef struct Attr Attr; +struct Attr { + u16 kind; /* AttrKind */ + u16 nargs; + SrcLoc loc; + Sym name; /* canonical (post-underscore-strip) spelling */ + union { + i64 i; /* aligned(N), vector_size(N), constructor(prio) */ + Sym sym; /* section("..."), alias("..."), visibility("...") */ + struct { + u16 fmt_idx; + u16 first; + } format; /* format(printf, m, n) */ + } v; + Attr* next; +}; + +#endif diff --git a/src/parse/parse.c b/src/parse/parse.c @@ -35,6 +35,7 @@ #include "decl/decl.h" #include "lex/lex.h" #include "obj/obj.h" +#include "parse/attr.h" #include "pp/pp.h" #include "type/type.h" @@ -722,68 +723,8 @@ static const Type* ty_size_t(Parser* p) { * Both `name` and `__name__` map to the same attribute. Phase 1 stores * the parsed list on DeclSpecs.attrs; other carrier sites consume tokens * cleanly via parse_and_discard_attributes until Phase 2 wires them up. */ -typedef enum AttrKind { - ATTR_UNKNOWN = 0, - ATTR_PACKED, - ATTR_ALIGNED, - ATTR_SECTION, - ATTR_USED, - ATTR_NORETURN, - ATTR_ALIAS, - ATTR_WEAK, - ATTR_VISIBILITY, - ATTR_ALWAYS_INLINE, - ATTR_NOINLINE, - ATTR_UNUSED, - ATTR_DEPRECATED, - ATTR_WARN_UNUSED_RESULT, - ATTR_FORMAT, - ATTR_NONNULL, - ATTR_RETURNS_NONNULL, - ATTR_PURE, - ATTR_CONST, - ATTR_MALLOC, - ATTR_NOTHROW, - ATTR_LEAF, - ATTR_COLD, - ATTR_HOT, - ATTR_CONSTRUCTOR, - ATTR_DESTRUCTOR, - ATTR_CLEANUP, - ATTR_MODE, - ATTR_VECTOR_SIZE, - ATTR_TRANSPARENT_UNION, - ATTR_GNU_INLINE, - ATTR_FALLTHROUGH, - ATTR_SENTINEL, - ATTR_NO_INSTRUMENT_FUNCTION, - ATTR_NO_SANITIZE, -} AttrKind; - -typedef enum AttrArgShape { - AS_NONE, - AS_OPTIONAL, - AS_INT, - AS_INT_OPT, - AS_STRING, - AS_IDENT, - AS_FORMAT, - AS_OPAQUE, -} AttrArgShape; - -typedef struct Attr Attr; -struct Attr { - u16 kind; - u16 nargs; - SrcLoc loc; - Sym name; - union { - i64 i; - Sym sym; - struct { u16 fmt_idx; u16 first; } format; - } v; - Attr* next; -}; +/* AttrKind / AttrArgShape / Attr live in src/parse/attr.h so the Phase 2 + * decl consumers (src/decl/decl_attrs.c) can decode the same nodes. */ static const struct { const char* name; diff --git a/src/type/type.c b/src/type/type.c @@ -179,6 +179,7 @@ struct TypeRecordBuilder { Field* fields; u32 nfields; u32 cap; + TypeRecordOpts opts; }; TagId type_tag_new(Pool* p, TagDeclKind kind, Sym spelling, SrcLoc loc) { @@ -199,6 +200,13 @@ const TagDecl* type_tag_get(Pool* p, TagId id) { TypeRecordBuilder* type_record_begin(Pool* p, TypeKind kind, TagId tag_id, Sym tag) { + TypeRecordOpts opts; + memset(&opts, 0, sizeof opts); + return type_record_begin_ex(p, kind, tag_id, tag, opts); +} + +TypeRecordBuilder* type_record_begin_ex(Pool* p, TypeKind kind, TagId tag_id, + Sym tag, TypeRecordOpts opts) { TypeRecordBuilder* b = arena_new(&p->arena, TypeRecordBuilder); if (!b) return NULL; memset(b, 0, sizeof *b); @@ -206,6 +214,7 @@ TypeRecordBuilder* type_record_begin(Pool* p, TypeKind kind, TagId tag_id, b->kind = kind; b->tag_id = tag_id; b->tag = tag; + b->opts = opts; return b; } @@ -233,6 +242,8 @@ const Type* type_record_end(Pool* p, TypeRecordBuilder* b) { t->rec.fields = b->fields; t->rec.nfields = (u16)b->nfields; t->rec.incomplete = 0; + t->rec.packed = b->opts.packed; + t->rec.align_override = b->opts.align_override; return t; } @@ -248,6 +259,8 @@ Type* type_record_forward(Pool* p, TypeKind kind, TagId tag_id, Sym tag) { t->rec.fields = NULL; t->rec.nfields = 0; t->rec.incomplete = 1; + t->rec.packed = 0; + t->rec.align_override = 0; return t; } diff --git a/src/type/type.h b/src/type/type.h @@ -70,6 +70,12 @@ typedef struct Field { const Type* type; u16 bitfield_width; /* valid when FIELD_BITFIELD is set; may be 0 */ u16 flags; /* FieldFlag */ + /* Phase 2 attribute carriers — populated by the parser when the member + * carries __attribute__((aligned(N))) / ((packed)). Zero means "no + * override"; abi_record_layout interprets them. */ + u16 align_override; + u8 packed; + u8 pad; } Field; struct Type { @@ -96,6 +102,11 @@ struct Type { const Field* fields; u16 nfields; u8 incomplete; + /* Phase 2 attribute carriers — record-level + * __attribute__((packed)) / ((aligned(N))). Both zero means + * "natural layout". abi_record_layout honors them. */ + u8 packed; + u16 align_override; } rec; /* struct / union */ struct { TagId tag_id; @@ -121,6 +132,21 @@ TagId type_tag_new(Pool*, TagDeclKind, Sym spelling, SrcLoc); const TagDecl* type_tag_get(Pool*, TagId); TypeRecordBuilder* type_record_begin(Pool*, TypeKind kind, TagId, Sym tag); /* TY_STRUCT or TY_UNION */ + +/* Phase 2 record options carried from __attribute__((packed))/aligned(N)). + * Zero-initialized = natural layout. Fields kept as a struct so future + * options (e.g. transparent_union) don't churn the call sites. */ +typedef struct TypeRecordOpts { + u8 packed; + u16 align_override; +} TypeRecordOpts; + +/* Variant of type_record_begin that records record-level attribute + * options on the builder; type_record_end copies them to Type.rec. The + * plain type_record_begin is equivalent to passing a zeroed + * TypeRecordOpts. */ +TypeRecordBuilder* type_record_begin_ex(Pool*, TypeKind kind, TagId, + Sym tag, TypeRecordOpts); void type_record_field(TypeRecordBuilder*, Field); const Type* type_record_end(Pool*, TypeRecordBuilder*); /* Forward-declared struct/union: returns a mutable, incomplete Type with the diff --git a/test/parse/cases/attr_p2_01_packed_sizeof.c b/test/parse/cases/attr_p2_01_packed_sizeof.c @@ -0,0 +1,12 @@ +/* Phase 2: __attribute__((packed)) on a record honors the packed layout. + * struct { char a; int b; }: unpacked is 8 (char + 3 pad + int); + * packed is 5 (char + int with no padding). + * Phase 1 returns 8; Phase 2 returns 5. */ +struct __attribute__((packed)) S { + char a; + int b; +}; + +int test_main(void) { + return (int)sizeof(struct S); +} diff --git a/test/parse/cases/attr_p2_01_packed_sizeof.expected b/test/parse/cases/attr_p2_01_packed_sizeof.expected @@ -0,0 +1 @@ +5 diff --git a/test/parse/cases/attr_p2_02_packed_member_offset.c b/test/parse/cases/attr_p2_02_packed_member_offset.c @@ -0,0 +1,10 @@ +/* Phase 2: packed struct → offsetof(S, b) == 1. + * Unpacked offset would be 4. */ +struct __attribute__((packed)) S { + char a; + int b; +}; + +int test_main(void) { + return (int)__builtin_offsetof(struct S, b); +} diff --git a/test/parse/cases/attr_p2_02_packed_member_offset.expected b/test/parse/cases/attr_p2_02_packed_member_offset.expected @@ -0,0 +1 @@ +1 diff --git a/test/parse/cases/attr_p2_03_aligned_record.c b/test/parse/cases/attr_p2_03_aligned_record.c @@ -0,0 +1,8 @@ +/* Phase 2: __attribute__((aligned(16))) on a record raises _Alignof to 16. */ +struct __attribute__((aligned(16))) S { + int x; +}; + +int test_main(void) { + return (int)_Alignof(struct S); +} diff --git a/test/parse/cases/attr_p2_03_aligned_record.expected b/test/parse/cases/attr_p2_03_aligned_record.expected @@ -0,0 +1 @@ +16 diff --git a/test/parse/cases/attr_p2_04_aligned_field.c b/test/parse/cases/attr_p2_04_aligned_field.c @@ -0,0 +1,10 @@ +/* Phase 2: member-level __attribute__((aligned(8))) raises field offset. + * Natural layout: a at 0, b at 4 (sizeof int = 4, but aligned(8) forces 8). */ +struct S { + int a; + int b __attribute__((aligned(8))); +}; + +int test_main(void) { + return (int)__builtin_offsetof(struct S, b); +} diff --git a/test/parse/cases/attr_p2_04_aligned_field.expected b/test/parse/cases/attr_p2_04_aligned_field.expected @@ -0,0 +1 @@ +8 diff --git a/test/parse/cases/attr_p2_05_packed_with_field_aligned.c b/test/parse/cases/attr_p2_05_packed_with_field_aligned.c @@ -0,0 +1,13 @@ +/* Phase 2: packed record + field-level aligned. The packed attribute on + * the record reduces alignment to 1 for fields without their own align; + * a field-level aligned(N) can still raise its own alignment. + * Layout: a @ 0 (1 byte), b @ 4 (aligned(4) on a packed record). + * Return offsetof(S, b) * 10 + sizeof(S). Phase 2: 4*10 + 8 = 48. */ +struct __attribute__((packed)) S { + char a; + int b __attribute__((aligned(4))); +}; + +int test_main(void) { + return (int)__builtin_offsetof(struct S, b) * 10 + (int)sizeof(struct S); +} diff --git a/test/parse/cases/attr_p2_05_packed_with_field_aligned.expected b/test/parse/cases/attr_p2_05_packed_with_field_aligned.expected @@ -0,0 +1 @@ +48 diff --git a/test/parse/cases/attr_p2_06_section_var.c b/test/parse/cases/attr_p2_06_section_var.c @@ -0,0 +1,10 @@ +/* Phase 2: section("name") on a file-scope variable places it in the + * named section. Verifying section placement requires reading the + * emitted object; here we only confirm the program compiles and the + * variable is reachable at runtime. The .o-inspecting integration check + * lives in a separate harness extension (TODO Phase 2). */ +int v __attribute__((section(".data.cfree_attr_test"))) = 7; + +int test_main(void) { + return v - 7; +} diff --git a/test/parse/cases/attr_p2_07_used_static.c b/test/parse/cases/attr_p2_07_used_static.c @@ -0,0 +1,9 @@ +/* Phase 2: __attribute__((used)) on a static prevents --gc-sections from + * removing it. The retention check requires a link-time test (parallel + * to test/link/link_layout retain tests). Here we only confirm parse + + * codegen succeed and the value is reachable. */ +static int kept __attribute__((used)) = 11; + +int test_main(void) { + return kept - 11; +} diff --git a/test/parse/cases/attr_p2_08_weak_undef.c b/test/parse/cases/attr_p2_08_weak_undef.c @@ -0,0 +1,8 @@ +/* Phase 2: an undefined weak symbol resolves to 0 at link time without + * an "undefined reference" error. Phase 1 records the attribute but + * doesn't honor it, so the link step fails with an unresolved symbol. */ +extern int weak_missing __attribute__((weak)); + +int test_main(void) { + return weak_missing; +} diff --git a/test/parse/cases/attr_p2_09_visibility_hidden.c b/test/parse/cases/attr_p2_09_visibility_hidden.c @@ -0,0 +1,12 @@ +/* Phase 2: __attribute__((visibility("hidden"))) on a function sets the + * ELF symbol visibility to STV_HIDDEN (Mach-O: N_PEXT). Visibility is + * not observable at runtime within a single TU; the symbol-table check + * lives in a separate ELF-inspecting integration harness. Here we only + * confirm the program compiles and the function is callable. */ +int hidden_fn(void) __attribute__((visibility("hidden"))); + +int hidden_fn(void) { return 13; } + +int test_main(void) { + return hidden_fn() - 13; +} diff --git a/test/parse/cases/attr_p2_10_alias.c b/test/parse/cases/attr_p2_10_alias.c @@ -0,0 +1,8 @@ +/* Phase 2: alias("target") emits a symbol that points at target's body. + * Calling through the alias must execute target's code. */ +int bar(void) { return 42; } +int foo(void) __attribute__((alias("bar"))); + +int test_main(void) { + return foo(); +} diff --git a/test/parse/cases/attr_p2_10_alias.expected b/test/parse/cases/attr_p2_10_alias.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/attr_p2_11_noreturn.c b/test/parse/cases/attr_p2_11_noreturn.c @@ -0,0 +1,17 @@ +/* Phase 2: __attribute__((noreturn)) on a function declaration sets + * DF_NORETURN. Behavior at runtime is unchanged from the _Noreturn + * keyword path (no codegen change yet). The test exercises the + * compile-and-run path. */ +static int side; + +__attribute__((noreturn)) static void bail(void); + +static void bail(void) { + side = 7; + while (1) {} +} + +int test_main(void) { + if (0) bail(); + return side; +} diff --git a/test/parse/cases_err/attr_p2_alias_unresolved.c b/test/parse/cases_err/attr_p2_alias_unresolved.c @@ -0,0 +1,7 @@ +/* Phase 2: alias("target") with no prior declaration of `target` in the + * same TU must be diagnosed at finalize. */ +int foo(void) __attribute__((alias("nonexistent_target"))); + +int test_main(void) { + return foo(); +} diff --git a/test/parse/cases_err/attr_p2_aligned_not_pow2.c b/test/parse/cases_err/attr_p2_aligned_not_pow2.c @@ -0,0 +1,6 @@ +/* Phase 2: aligned(N) requires N to be a power of two. 3 is invalid. */ +int x __attribute__((aligned(3))); + +int test_main(void) { + return x; +} diff --git a/test/parse/cases_err/attr_p2_visibility_bad.c b/test/parse/cases_err/attr_p2_visibility_bad.c @@ -0,0 +1,9 @@ +/* Phase 2: visibility() accepts only "default", "hidden", "protected", + * or "internal". Any other string must be rejected. */ +int hidden_fn(void) __attribute__((visibility("totallyfake"))); + +int hidden_fn(void) { return 0; } + +int test_main(void) { + return hidden_fn(); +}