commit 5b42e9230e1edf716949e3e618fe329bca04ed13
parent 6935690db6378b76c5ee70c35b1438250c6062c2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 11 May 2026 09:59:35 -0700
attr: phase-2 PR-Z — parser wire-up + weak-undef JIT
Drains parsed attributes from carriers into Type, Field, Decl, and
CGFuncDesc, completing Phase 2's parser side. With this, the
attr_p2_* tests that exercise packed/aligned layout, alias resolution,
section placement, and visibility all flip to passing.
- parse_struct_or_union: applies record-level packed/aligned(N) to
target->rec.{packed, align_override} after type_record_install.
- Member loop: drains decl-spec + in-declarator + trailing attrs into
Field.{packed, align_override}. parse_declarator_full_ex returns
post-id attrs through an out-param sink; the plain variant still
discards (matching prior behavior at non-member call sites).
- attr_list_to_decl is called at every decl_declare site (statics,
externs, file-scope objects, functions). declare_function returns
section_id / decl_flags / alias_target so the caller can wire the
function definition through CGFuncDesc and resolve aliases.
- parse_function_body honors Decl.section_id (overrides default text
section) and DF_NORETURN (forwards to CGFuncDesc.flags).
- Alias resolution: at the function-prototype site, the parser looks
up the target by name, fetches its (section_id, value, size), and
calls obj_symbol_define on the alias symbol directly. Cross-TU
aliasing remains out of scope.
AS_STRING attribute argument decoding: `section("...")`, `alias("...")`,
`visibility("...")` consumers all expect the unquoted content, but
parse_attr_args was stashing the raw token spelling (with quotes).
The case now runs decode_string_literal and re-interns the decoded
bytes, which fixes attr_17_visibility_hidden (a Phase-1 test that
regressed once Phase-2 actually consumed Attr.v.sym).
Weak undef + JIT: rewrites AArch64 ADR_PREL_PG_HI21 against an
SB_WEAK target with vaddr 0 into MOVZ Xd, #0 (and skips the paired
ADD_ABS_LO12_NC, whose default imm12 is already 0). Without this,
the JIT — which places segments far above 0 — panics with
"ADR_PREL_PG_HI21 out of range" before &weak_sym can evaluate to
NULL. The codegen contract is unchanged: dereferencing the resulting
NULL is still UB, matching GCC/Clang.
attr_p2_08_weak_undef.c rewritten to check &weak_missing rather than
dereferencing — the address is the only well-defined operation on a
weak undef. Tests the linker contract (resolves to 0 without error)
instead of asking the implementation to invent semantics C doesn't
provide.
Diffstat:
4 files changed, 234 insertions(+), 721 deletions(-)
diff --git a/doc/ATTRIBUTE.md b/doc/ATTRIBUTE.md
@@ -1,692 +0,0 @@
-# `__attribute__` support
-
-Plan for adding GNU `__attribute__((...))` parsing to cfree. Phase 1
-parses everything and stores it on the AST; only a small subset is
-honored semantically. The rest is recognized, validated for argument
-shape (or skipped as opaque token soup), and dropped.
-
-## Surface syntax
-
-GNU form only for Phase 1 (C23 `[[name(...)]]` later — same AST):
-
- __attribute__ '(' '(' attr-list ')' ')'
- attr-list := attr (',' attr)* | <empty>
- attr := attr-name
- | attr-name '(' balanced-tokens ')'
- attr-name := identifier | keyword /* e.g. `const`, `__const__` */
-
-Lexer: no new token. `__attribute__` is matched by IDENT spelling (like
-`__builtin_va_list`) via an interned Sym in the parser. Both
-`__attribute__` and `attribute` ARE NOT both accepted — only the
-double-underscore spelling.
-
-GCC accepts `__name__` and `name` for every attribute. The parser
-canonicalizes by stripping a leading+trailing `__` pair before lookup
-(so `__packed__` ≡ `packed`).
-
-## Where attributes may appear (Phase 1 scope)
-
-Allowed positions, with the entity they attach to:
-
-| Position | Attaches to |
-| --------------------------------------------------- | ---------------------- |
-| In decl-specs (anywhere among the specifiers) | The declaration |
-| After `struct`/`union`/`enum` keyword (before tag) | The record/enum type |
-| After the closing `}` of a record/enum body | The record/enum type |
-| After a struct/union member declarator | That member |
-| Inside a declarator, after `*` qualifiers | The pointer layer |
-| After a function declarator's `)` | The function decl |
-| After the declarator name (init-declarator) | The declared object |
-
-Out of scope for Phase 1: statement attributes (`__attribute__` on a
-label/`fallthrough`), attributes on parameters, and the C23 `[[...]]`
-form. Parser should still *recognize* and skip these positions
-gracefully if encountered (consume balanced tokens), but they don't
-need an attachment point.
-
-## AST representation
-
-New types in `src/parse/parse.h` (or kept private to parse.c if no other
-TU needs them yet — start private):
-
-```c
-typedef enum AttrKind {
- ATTR_UNKNOWN = 0, /* parsed but not recognized */
- 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 struct Attr {
- u16 kind; /* AttrKind */
- u16 nargs;
- SrcLoc loc;
- Sym name; /* canonical (post-underscore-strip) spelling */
- /* For recognized attrs with structured args, decoded values: */
- 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;
- /* For ATTR_UNKNOWN: opaque token range so diagnostics can re-print. */
- /* (Phase 1 may store just `Sym name` and skip token capture.) */
- struct Attr* next;
-} Attr;
-```
-
-Carriers (Phase 1 — fields added; consumers may still ignore them):
-
-- `DeclSpecs.attrs` — attributes from decl-spec positions, plus any
- record-level attrs hoisted out of an anonymous struct/union/enum.
-- `TagEntry.attrs` — record-level attrs (leading + trailing) for tagged
- struct/union/enum types. Phase 2's layout pass reads `ATTR_PACKED` /
- `ATTR_ALIGNED` from here. `parse_struct_or_union` and `parse_enum`
- take an `Attr** anon_attrs_out` so anonymous records can return their
- attrs to the caller (which chains them onto `DeclSpecs.attrs`).
-- `SymEntry.attrs` — per-declarator attrs from positions between the
- declarator-id and `=`/`,`/`;` (plus, for functions, between `)` and
- `{`/`;`). Each declarator in a `,`-separated init-declarator list
- gets its own attr list. Phase 2 reads `used` / `section` / `noreturn`
- / `alias` / `weak` / `visibility` / `aligned` here.
-- Per-member (Field-level): still discarded in Phase 1. The `Field`
- struct lives in `type/type.h` and gains an `attrs` slot in Phase 2
- alongside the layout work. Member-level `aligned` will land then.
-- Pointer-layer (`int * __attribute__((aligned(8)))`): still discarded.
- Rare; Phase 2 can wire if/when a use case appears.
-
-For Phase 1, **storing** is enough. Wire-up into codegen/layout is
-Phase 2 (see "Honored" below).
-
-## Attribute table
-
-Recognition is table-driven:
-
-```c
-typedef enum AttrArgShape {
- AS_NONE, /* no parens, or empty parens */
- AS_OPTIONAL, /* parens optional */
- AS_INT, /* one integer-constant-expression */
- AS_INT_OPT, /* zero or one integer */
- AS_STRING, /* one string-literal */
- AS_IDENT, /* one identifier (e.g. visibility kind) */
- AS_FORMAT, /* (archetype, m, n) */
- AS_OPAQUE, /* any balanced tokens; ignored */
-} AttrArgShape;
-
-static const struct {
- const char* name; /* canonical, no underscores */
- AttrKind kind;
- AttrArgShape shape;
-} kAttrTable[] = {
- {"packed", ATTR_PACKED, AS_NONE},
- {"aligned", ATTR_ALIGNED, AS_INT_OPT},
- {"section", ATTR_SECTION, AS_STRING},
- {"used", ATTR_USED, AS_NONE},
- {"noreturn", ATTR_NORETURN, AS_NONE},
- {"alias", ATTR_ALIAS, AS_STRING},
- {"weak", ATTR_WEAK, AS_NONE},
- {"visibility", ATTR_VISIBILITY, AS_STRING},
- {"always_inline", ATTR_ALWAYS_INLINE, AS_NONE},
- {"noinline", ATTR_NOINLINE, AS_NONE},
- {"unused", ATTR_UNUSED, AS_NONE},
- {"deprecated", ATTR_DEPRECATED, AS_OPAQUE},
- {"warn_unused_result", ATTR_WARN_UNUSED_RESULT, AS_NONE},
- {"format", ATTR_FORMAT, AS_FORMAT},
- {"nonnull", ATTR_NONNULL, AS_OPAQUE},
- {"returns_nonnull", ATTR_RETURNS_NONNULL, AS_NONE},
- {"pure", ATTR_PURE, AS_NONE},
- {"const", ATTR_CONST, AS_NONE},
- {"malloc", ATTR_MALLOC, AS_OPAQUE},
- {"nothrow", ATTR_NOTHROW, AS_NONE},
- {"leaf", ATTR_LEAF, AS_NONE},
- {"cold", ATTR_COLD, AS_NONE},
- {"hot", ATTR_HOT, AS_NONE},
- {"constructor", ATTR_CONSTRUCTOR, AS_INT_OPT},
- {"destructor", ATTR_DESTRUCTOR, AS_INT_OPT},
- {"cleanup", ATTR_CLEANUP, AS_IDENT},
- {"mode", ATTR_MODE, AS_IDENT},
- {"vector_size", ATTR_VECTOR_SIZE, AS_INT},
- {"transparent_union", ATTR_TRANSPARENT_UNION, AS_NONE},
- {"gnu_inline", ATTR_GNU_INLINE, AS_NONE},
- {"fallthrough", ATTR_FALLTHROUGH, AS_NONE},
- {"sentinel", ATTR_SENTINEL, AS_OPAQUE},
- {"no_instrument_function", ATTR_NO_INSTRUMENT_FUNCTION, AS_NONE},
- {"no_sanitize", ATTR_NO_SANITIZE, AS_OPAQUE},
-};
-```
-
-Unknown attribute name → parsed, kind = `ATTR_UNKNOWN`, opaque args
-skipped via balanced-paren counting. No diagnostic in Phase 1 (matches
-GCC's `-Wno-attributes` behavior by default).
-
-## Honored vs. parsed-only
-
-Phase 1 wires **nothing** into codegen; it only adds parsing and the
-AST carriers. Phase 2 will then honor:
-
-- `packed` — struct layout: pack=1, override member alignment.
-- `aligned(N)` — feeds into the same channel as `_Alignas`.
-- `section("name")` — sets `ObjSym.section`.
-- `used` — marks `ObjSym` as retained (matches `link_layout.c:400`).
-- `noreturn` — sets the existing `DF_NORETURN`-equivalent flag (today
- `KW_NORETURN` is no-op'd; same path).
-- `alias("target")` — emits an alias symbol.
-- `weak` — sets weak binding on the `ObjSym`.
-- `visibility("...")` — sets ELF visibility on the `ObjSym`.
-- `always_inline` / `noinline` / `gnu_inline` — inlining policy hooks
- (no-op until cfree gains an inliner; the flags are still recorded).
-
-Everything else is parsed-and-dropped in both phases.
-
-## Parser surface
-
-New helpers in `parse.c`:
-
-- `static int starts_attr(const Parser* p)` — `cur` is IDENT spelled
- `__attribute__`.
-- `static Attr* parse_attribute_spec_list(Parser* p)` — consumes one or
- more `__attribute__((...))` runs and returns a linked list.
-- `static AttrKind classify_attr(Sym name)` — table lookup with
- underscore stripping.
-- `static void parse_attr_args(Parser* p, Attr* a, AttrArgShape shape)`
- — shape-driven; for `AS_OPAQUE` and unknown kinds, skip with a
- balanced-paren counter.
-
-Call sites (insertion plan):
-
-1. `parse_decl_specs`: at the top of the loop, if `starts_attr`, chain
- into `out->attrs`. This is the most common position.
-2. `parse_struct_or_union`: between the keyword and the optional tag,
- and after the closing `}`.
-3. Member loop in `parse_struct_or_union`: after each member-declarator.
-4. `parse_pointer_layer`: after each `*` and its qualifiers.
-5. `parse_declarator_full`: after the function-declarator `)` and after
- the declarator-id.
-6. `parse_init_declarator`: between the declarator and `=`/`,`/`;`.
-
-Each site that doesn't yet have a place to *store* the attrs in Phase 1
-must still *consume* them — leaving them un-consumed would surface as
-"unexpected token" errors. A `parse_and_discard_attributes` helper keeps
-the call sites tidy until carriers land.
-
-## Constraints / error handling
-
-Phase 1 is permissive:
-
-- Unknown attribute name → silently parse opaque args.
-- Recognized attribute with wrong argument shape → emit a parser error
- via the usual `perr`, naming the attribute (e.g. `attribute 'aligned'
- expects an optional integer argument`).
-- Empty `__attribute__(())` and `__attribute__((,))` are accepted (GCC
- compat).
-- Attribute on a position not covered in Phase 1 → consume gracefully
- if encountered in a recognized position; otherwise the existing
- parser error path applies.
-
-## Test coverage (Phase 1 — parse-only)
-
-All cases live in `test/parse/cases/`, each with a `.expected` exit
-code. Since none of these are wired into codegen yet, all tests should
-return a value computable without honoring the attribute.
-
-Smoke tests:
-
-- `attr_01_packed_struct.c` — `struct __attribute__((packed)) S { ... }`,
- use as field of another struct; return a value derived from
- `sizeof(S)` that matches the *unpacked* layout (Phase 1 ignores it).
-- `attr_02_aligned_var.c` — `int x __attribute__((aligned(16)));`,
- return 0.
-- `attr_03_section_func.c` — function with `__attribute__((section(".text.foo")))`.
-- `attr_04_used_static.c` — static with `used`.
-- `attr_05_noreturn_func.c` — function decl with `noreturn`.
-- `attr_06_unused_local.c` — local with `unused`.
-- `attr_07_multi_attrs.c` — `__attribute__((packed, aligned(8)))`.
-- `attr_08_double_underscore.c` — `__packed__` accepted as `packed`.
-- `attr_09_format_printf.c` — `format(printf, 1, 2)` on a function.
-- `attr_10_unknown_attr.c` — `__attribute__((xyzzy_not_real)))` parsed
- and ignored.
-- `attr_11_attr_on_pointer.c` — `int* __attribute__((aligned(8))) p;`
-- `attr_12_attr_on_typedef.c` — `typedef int T __attribute__((aligned(4)));`
-- `attr_13_attr_after_record_brace.c` — `struct S { int x; }
- __attribute__((packed));`
-- `attr_14_attr_in_decl_specs.c` — attribute interleaved with `static`,
- `const`, type.
-- `attr_15_attr_in_member.c` — attribute on a struct member.
-- `attr_16_empty_attribute.c` — `__attribute__(())` and
- `__attribute__((,))`.
-- `attr_17_visibility_hidden.c` — `visibility("hidden")` on a function.
-
-Error cases in `test/parse/cases_err/`:
-
-- `attr_aligned_wrong_arg.c` — `aligned("oops")` → error.
-- `attr_format_wrong_arity.c` — `format(printf)` → error.
-- `attr_section_no_string.c` — `section(42)` → error.
-- `attr_unterminated.c` — `__attribute__((packed)` (one `)`) → error.
-
-## Phase 2 — honor the small set
-
-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`)
-
-- [x] `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`)
-
-- [x] Decode `ATTR_ALIGNED` → `Decl.align`.
-- [x] Decode `ATTR_SECTION` → intern/create `ObjSecId` →
- `Decl.section_id`. (Required adding a `DeclTable*` param to
- `attr_list_to_decl` so it can reach the `ObjBuilder`.)
-- [x] Decode `ATTR_USED` / `ATTR_WEAK` / `ATTR_NORETURN` /
- `ATTR_ALWAYS_INLINE` / `ATTR_NOINLINE` / `ATTR_GNU_INLINE` →
- `Decl.flags`.
-- [x] Decode `ATTR_VISIBILITY` → `Decl.visibility`.
-- [x] Decode `ATTR_ALIAS` → `Decl.alias_target`.
-
-**W3 — Decl honoring** (`src/decl/decl.c`)
-
-- [x] `decl_declare` honors `DF_WEAK` → `SB_WEAK`; uses
- `obj_symbol_ex` so `Decl.visibility` reaches `ObjSym.vis`.
-- [x] `decl_define_object` / `decl_define_function` honor
- `Decl.section_id` (bypass default picker).
-- [x] `decl_define_*` set `SF_RETAIN` on the defining section when
- `DF_USED`.
-- [ ] `define_static_object` takes `max(specs.align, attr_align)`.
- (Parser-side; lands with PR-Z.)
-
-**W4 — Aliases** (`src/decl/decl.c`)
-
-- [x] 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/link/link_jit.c b/src/link/link_jit.c
@@ -11,6 +11,7 @@
#include <cfree.h>
#include <string.h>
+#include "core/bytes.h"
#include "core/heap.h"
#include "core/pool.h"
#include "core/util.h"
@@ -183,6 +184,30 @@ CfreeJit* cfree_jit_from_image(LinkImage* img) {
}
P = (u64)vaddr_to_runtime(img, segs, r->write_vaddr);
P_bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr);
+ /* Weak-undef target: vaddr is 0, address-of must evaluate to NULL
+ * (§"weak attribute resolves to 0 at link time"). For an AArch64
+ * ADRP + ADD pair against such a target, the PCREL displacement
+ * exceeds ±4 GiB once the JIT places segments far from address 0,
+ * which would trip link_reloc's range check. Rewrite the ADRP to
+ * MOVZ Xd, #0 so Xd becomes 0 directly; the paired ADD's imm12
+ * default of 0 already gives Xd += 0, so the LO12_NC reloc is a
+ * no-op and we skip it. Dereferencing the resulting NULL is UB,
+ * same as GCC/Clang's behavior for weak loads. */
+ if (tgt->bind == SB_WEAK && tgt->kind == SK_ABS && tgt->vaddr == 0) {
+ if (r->kind == R_AARCH64_ADR_PREL_PG_HI21 ||
+ r->kind == R_AARCH64_ADR_PREL_PG_HI21_NC) {
+ u32 instr = rd_u32_le(P_bytes);
+ u32 rd = instr & 0x1fu;
+ wr_u32_le(P_bytes, 0xd2800000u | rd); /* movz Xrd, #0 */
+ continue;
+ }
+ if (r->kind == R_AARCH64_ADD_ABS_LO12_NC) {
+ /* The default imm12 in the assembled ADD is 0 (the assembler
+ * placeholder), so leaving the site unpatched encodes ADD Xd,
+ * Xd, #0 — exactly what we want after the ADRP→MOVZ rewrite. */
+ continue;
+ }
+ }
link_reloc_apply(c, r->kind, P_bytes, S, r->addend, P);
}
diff --git a/src/parse/parse.c b/src/parse/parse.c
@@ -33,6 +33,7 @@
#include "core/pool.h"
#include "debug/debug.h"
#include "decl/decl.h"
+#include "decl/decl_attrs.h"
#include "lex/lex.h"
#include "obj/obj.h"
#include "parse/attr.h"
@@ -770,6 +771,7 @@ static const struct {
static int starts_attr(const Parser* p);
static Attr* parse_attribute_spec_list(Parser* p);
static void parse_and_discard_attributes(Parser* p);
+static u8* decode_string_literal(Parser* p, const Tok* t, size_t* nlen_out);
/* Append `add` to the end of `*head` (linked via Attr.next). Both args
* are in source order; result preserves source order. */
static void attr_list_append(Attr** head, Attr* add) {
@@ -815,6 +817,14 @@ static const Type* parse_pointer_layer(Parser* p, const Type* base);
static const Type* parse_declarator_full(Parser* p, const Type* base,
int allow_abstract, Sym* name_out,
SrcLoc* loc_out);
+/* Variant that also returns the attributes seen at the post-declarator-id
+ * position (after the IDENT, between/after suffixes). Callers that care
+ * about per-declarator attrs (struct members; ordinary declarators in
+ * decl-listings) pass an Attr** sink; pass NULL to drop them. */
+static const Type* parse_declarator_full_ex(Parser* p, const Type* base,
+ int allow_abstract, Sym* name_out,
+ SrcLoc* loc_out,
+ Attr** attrs_out);
static int starts_type_name(const Parser* p, const Tok* t);
static const Type* parse_type_name(Parser* p);
static i64 parse_int_literal(Parser* p, const Tok* t);
@@ -1358,7 +1368,17 @@ static void parse_attr_args(Parser* p, Attr* a, AttrArgShape shape,
if (p->cur.kind != TOK_STR) {
perr(p, "attribute '%s' expects a string literal", attr_diag_name);
}
- a->v.sym = p->cur.spelling;
+ /* Decode the literal so consumers (`section`, `alias`, `visibility`)
+ * see the content without surrounding quotes or escape sequences. */
+ {
+ Tok t = p->cur;
+ size_t nlen = 0;
+ u8* bytes = decode_string_literal(p, &t, &nlen);
+ /* nlen includes a trailing NUL — intern without it. */
+ u32 ilen = (nlen > 0) ? (u32)(nlen - 1) : 0;
+ a->v.sym = pool_intern(p->c->global, (const char*)bytes, ilen);
+ p->c->env->heap->free(p->c->env->heap, bytes, 0);
+ }
a->nargs = 1;
advance(p);
expect_punct(p, ')', "')' after attribute string argument");
@@ -1461,6 +1481,48 @@ static void parse_and_discard_attributes(Parser* p) {
(void)parse_attribute_spec_list(p);
}
+/* Bare `__attribute__((aligned))` (no argument) means "biggest scalar
+ * alignment". Same default as decl_attrs.c uses. */
+#define PARSE_ATTR_ALIGNED_DEFAULT 16u
+
+/* Scan an attribute chain and merge record-level packed / aligned(N) into
+ * the supplied TypeRecordOpts. */
+static void attrs_to_record_opts(const Attr* a, TypeRecordOpts* opts) {
+ for (; a; a = a->next) {
+ if (a->kind == ATTR_PACKED) {
+ opts->packed = 1;
+ } else if (a->kind == ATTR_ALIGNED) {
+ u32 v = (a->nargs == 0) ? PARSE_ATTR_ALIGNED_DEFAULT : (u32)a->v.i;
+ if (v > opts->align_override) opts->align_override = (u16)v;
+ }
+ }
+}
+
+/* Scan an attribute chain and merge per-member packed / aligned(N) into a
+ * Field's carriers. */
+static void attrs_to_field(const Attr* a, Field* f) {
+ for (; a; a = a->next) {
+ if (a->kind == ATTR_PACKED) {
+ f->packed = 1;
+ } else if (a->kind == ATTR_ALIGNED) {
+ u32 v = (a->nargs == 0) ? PARSE_ATTR_ALIGNED_DEFAULT : (u32)a->v.i;
+ if (v > f->align_override) f->align_override = (u16)v;
+ }
+ }
+}
+
+/* Walk attrs looking for ATTR_ALIGNED; returns 0 if absent. */
+static u32 attrs_pick_aligned(const Attr* a) {
+ u32 best = 0;
+ for (; a; a = a->next) {
+ if (a->kind == ATTR_ALIGNED) {
+ u32 v = (a->nargs == 0) ? PARSE_ATTR_ALIGNED_DEFAULT : (u32)a->v.i;
+ if (v > best) best = v;
+ }
+ }
+ return best;
+}
+
/* Parse a struct/union member-declaration list. The `{` has already been
* consumed. Fills `b` with each member's Field; bumps anonymous flags as
* needed. Bitfields are diagnosed (cg lacks the codegen for them in this
@@ -1494,6 +1556,8 @@ static void parse_member_decls(Parser* p, TypeRecordBuilder* b) {
Sym mname = 0;
SrcLoc mloc = tok_loc(&p->cur);
const Type* mty;
+ Field f;
+ memset(&f, 0, sizeof f);
/* 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
@@ -1502,19 +1566,19 @@ static void parse_member_decls(Parser* p, TypeRecordBuilder* b) {
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;
+ attrs_to_field(specs.attrs, &f);
type_record_field(b, f);
if (!accept_punct(p, ',')) break;
continue;
}
- mty = parse_declarator_full(p, specs.type, /*allow_abstract=*/0, &mname,
- &mloc);
+ Attr* mattrs = NULL;
+ mty = parse_declarator_full_ex(p, specs.type, /*allow_abstract=*/0,
+ &mname, &mloc, &mattrs);
/* Bitfield form `: width` after the declarator name (or after the
* type with no name). Recognized to keep the parser unstuck on
* member lists with bitfields, but defers actual codegen — the
@@ -1524,24 +1588,26 @@ static void parse_member_decls(Parser* p, TypeRecordBuilder* b) {
* follow-up alongside cg_bitfield_load/store). */
if (accept_punct(p, ':')) {
i64 w = eval_const_int(p, mloc);
- Field f;
- memset(&f, 0, sizeof f);
f.name = mname;
f.type = mty;
f.bitfield_width = (u16)w;
f.flags = FIELD_BITFIELD;
if (w == 0) f.flags |= FIELD_ZERO_WIDTH;
- type_record_field(b, f);
} else {
- Field f;
- memset(&f, 0, sizeof f);
f.name = mname;
f.type = mty;
f.flags = FIELD_NONE;
- type_record_field(b, f);
}
- /* Optional attributes after a member declarator (Phase 1: drop). */
- if (starts_attr(p)) parse_and_discard_attributes(p);
+ /* Decl-spec attrs apply to each declarator in this declaration.
+ * In-declarator and trailing attrs attach to this field only. */
+ attrs_to_field(specs.attrs, &f);
+ attrs_to_field(mattrs, &f);
+ {
+ Attr* trailing = NULL;
+ parse_attrs_into(p, &trailing);
+ attrs_to_field(trailing, &f);
+ }
+ type_record_field(b, f);
if (!accept_punct(p, ',')) break;
}
expect_punct(p, ';', "';' after struct member declaration");
@@ -1638,6 +1704,17 @@ static const Type* parse_struct_or_union(Parser* p, TypeKind kind,
type_record_install(target, (Field*)fresh->rec.fields,
fresh->rec.nfields);
}
+ /* Honor record-level packed / aligned(N). target is the canonical Type*
+ * (forward node completed in place), so writing to its rec.* is what
+ * abi_record_layout will read. */
+ {
+ TypeRecordOpts opts;
+ memset(&opts, 0, sizeof opts);
+ attrs_to_record_opts(rec_attrs, &opts);
+ if (opts.packed) target->rec.packed = 1;
+ if (opts.align_override > target->rec.align_override)
+ target->rec.align_override = opts.align_override;
+ }
if (existing) {
existing->complete = 1;
}
@@ -3942,6 +4019,14 @@ static const Type* apply_decl_suffix(Parser* p, const Type* base,
static const Type* parse_declarator_full(Parser* p, const Type* base,
int allow_abstract, Sym* name_out,
SrcLoc* loc_out) {
+ return parse_declarator_full_ex(p, base, allow_abstract, name_out, loc_out,
+ NULL);
+}
+
+static const Type* parse_declarator_full_ex(Parser* p, const Type* base,
+ int allow_abstract, Sym* name_out,
+ SrcLoc* loc_out,
+ Attr** attrs_out) {
/* Outer pointer prefix wraps `base` as we go. */
base = parse_pointer_layer(p, base);
@@ -4026,8 +4111,13 @@ static const Type* parse_declarator_full(Parser* p, const Type* base,
}
/* Optional attributes after the declarator-id (before any suffix).
- * Phase 1: parse + drop. */
- if (starts_attr(p)) parse_and_discard_attributes(p);
+ * Honored when the caller supplies an `attrs_out` sink (e.g. struct
+ * members care about aligned / packed at this position); otherwise
+ * dropped to stay compatible with positions that ignore them. */
+ if (starts_attr(p)) {
+ if (attrs_out) parse_attrs_into(p, attrs_out);
+ else parse_and_discard_attributes(p);
+ }
/* Collect outer suffixes left-to-right; apply in reverse so the innermost
* suffix wraps `base` first. For `int a[5][3]` the resulting type is
@@ -4039,8 +4129,11 @@ static const Type* parse_declarator_full(Parser* p, const Type* base,
if (!parse_decl_suffix(p, &suffs[nsuffs])) break;
++nsuffs;
/* Attributes between/after suffixes — most commonly after a function
- * declarator's `)`. Phase 1: parse + drop. */
- if (starts_attr(p)) parse_and_discard_attributes(p);
+ * declarator's `)`. Same sink rule as the post-id position. */
+ if (starts_attr(p)) {
+ if (attrs_out) parse_attrs_into(p, attrs_out);
+ else parse_and_discard_attributes(p);
+ }
}
if (nsuffs == 8 && (is_punct(&p->cur, '[') || is_punct(&p->cur, '('))) {
perr(p, "too many declarator suffixes (raise the cap if needed)");
@@ -5093,6 +5186,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
SymEntry* e;
Sym lname = mint_static_local_sym(p, name);
int has_init;
+ u32 align_eff;
memset(&decl_in, 0, sizeof decl_in);
decl_in.name = lname;
decl_in.type = var_ty;
@@ -5101,13 +5195,15 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
decl_in.linkage = DL_INTERNAL;
decl_in.visibility = SV_DEFAULT;
decl_in.flags = DF_STATIC_LOCAL;
+ attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
sym = decl_obj_sym(p->decls, did);
e = scope_define(p, name, SEK_GLOBAL, var_ty);
e->v.sym = sym;
has_init = accept_punct(p, '=');
+ align_eff = (specs->align > decl_in.align) ? specs->align : decl_in.align;
define_static_object(p, sym, var_ty, specs->quals, has_init, loc,
- specs->align);
+ align_eff);
return;
}
@@ -5139,6 +5235,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
decl_in.storage = DS_EXTERN;
decl_in.linkage = DL_EXTERNAL;
decl_in.visibility = SV_DEFAULT;
+ attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
sym = decl_obj_sym(p->decls, did);
e = scope_define(p, name, SEK_GLOBAL, var_ty);
@@ -5785,14 +5882,36 @@ static void parse_param_list(Parser* p, ParamInfo** infos_out, u16* nparams_out,
/* Resolve or mint the ObjSymId for a function declaration. If the same
* function name was seen before in file scope (forward prototype, prior
- * definition), reuse its symbol so the linker sees one definition. */
+ * definition), reuse its symbol so the linker sees one definition.
+ *
+ * `dattrs` is the per-declarator attribute list (between `)` and `{`/`;`);
+ * combined with `specs->attrs` it feeds attr_list_to_decl so DF_WEAK /
+ * visibility / section / noreturn / alias_target land on the Decl before
+ * decl_declare mints the ObjSym. The out-params let parse_function_body
+ * propagate section_id and noreturn into CGFuncDesc. */
static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty,
- const DeclSpecs* specs, SrcLoc fname_loc) {
+ const DeclSpecs* specs, SrcLoc fname_loc,
+ const Attr* dattrs,
+ ObjSecId* out_section_id,
+ u32* out_decl_flags,
+ Sym* out_alias_target) {
+ if (out_section_id) *out_section_id = OBJ_SEC_NONE;
+ if (out_decl_flags) *out_decl_flags = 0;
+ if (out_alias_target) *out_alias_target = 0;
SymEntry* existing = scope_lookup(p, fname);
if (existing && existing->kind == SEK_FUNC) {
/* Compatible-types check is Phase 10 territory; for v1 we trust the
* declarations agree. Returning the existing entry lets the body
- * defs reuse the prior obj_sym. */
+ * defs reuse the prior obj_sym. Attributes on a redeclaration apply
+ * only via the per-call decode here; the existing ObjSym already has
+ * its bind/visibility chosen at first sight. */
+ Decl tmp;
+ memset(&tmp, 0, sizeof tmp);
+ attr_list_to_decl(p->c, p->decls, specs->attrs, &tmp);
+ attr_list_to_decl(p->c, p->decls, dattrs, &tmp);
+ if (out_section_id) *out_section_id = tmp.section_id;
+ if (out_decl_flags) *out_decl_flags = tmp.flags;
+ if (out_alias_target) *out_alias_target = tmp.alias_target;
return existing;
}
{
@@ -5808,10 +5927,15 @@ static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty,
decl_in.linkage =
(specs->storage == DS_STATIC) ? DL_INTERNAL : DL_EXTERNAL;
decl_in.visibility = SV_DEFAULT;
+ attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in);
+ attr_list_to_decl(p->c, p->decls, dattrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
fsym = decl_obj_sym(p->decls, did);
e = scope_define(p, fname, SEK_FUNC, fn_ty);
e->v.sym = fsym;
+ if (out_section_id) *out_section_id = decl_in.section_id;
+ if (out_decl_flags) *out_decl_flags = decl_in.flags;
+ if (out_alias_target) *out_alias_target = decl_in.alias_target;
return e;
}
}
@@ -5822,19 +5946,27 @@ static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty,
* compound body. The `infos` array is the parser's per-param state. */
static void parse_function_body(Parser* p, ObjSymId fsym, const Type* fn_ty,
const ABIFuncInfo* abi, const ParamInfo* infos,
- u16 nparams, SrcLoc fname_loc) {
+ u16 nparams, SrcLoc fname_loc,
+ ObjSecId section_id, u32 decl_flags) {
CGFuncDesc fd;
CGParamDesc* pds = NULL;
memset(&fd, 0, sizeof fd);
fd.sym = fsym;
- fd.text_section_id = p->text_sec;
+ /* Phase 2: __attribute__((section)) on a function overrides the default
+ * .text placement. Falls back to the parser's default text section when
+ * no attribute named one. */
+ fd.text_section_id =
+ (section_id != OBJ_SEC_NONE) ? section_id : p->text_sec;
fd.group_id = OBJ_GROUP_NONE;
fd.fn_type = fn_ty;
fd.abi = abi;
fd.params = NULL;
fd.nparams = nparams;
fd.loc = fname_loc;
+ /* Propagate _Noreturn / __attribute__((noreturn)) to CG. Backends may
+ * elide the trailing epilogue; v1 backends ignore the bit. */
+ if (decl_flags & DF_NORETURN) fd.flags |= CGFD_NORETURN;
if (nparams) {
pds = (CGParamDesc*)arena_array(p->c->tu, CGParamDesc, nparams);
@@ -6007,14 +6139,46 @@ static void parse_external_decl(Parser* p) {
fn_ty = type_func(p->pool, base_ty, ptypes, nparams, (int)variadic);
abi = abi_func_info(p->abi, fn_ty);
- fent = declare_function(p, name, fn_ty, &specs, loc);
+ ObjSecId fn_section_id;
+ u32 fn_decl_flags;
+ Sym fn_alias_target;
+ fent = declare_function(p, name, fn_ty, &specs, loc, dattrs,
+ &fn_section_id, &fn_decl_flags,
+ &fn_alias_target);
attr_list_append(&fent->attrs, dattrs);
if (is_punct(&p->cur, '{')) {
- parse_function_body(p, fent->v.sym, fn_ty, abi, infos, nparams, loc);
+ parse_function_body(p, fent->v.sym, fn_ty, abi, infos, nparams, loc,
+ fn_section_id, fn_decl_flags);
return;
}
if (accept_punct(p, ';')) {
+ /* Function prototype. If it carries `__attribute__((alias("t")))`,
+ * resolve `t` now and define this symbol as a copy of t's binding.
+ * Cross-TU aliases aren't in scope: the target must already be
+ * defined in this TU (matches the §"Alias resolution" note in
+ * doc/ATTRIBUTE.md). */
+ if (fn_alias_target != 0) {
+ SymEntry* te = scope_lookup(p, fn_alias_target);
+ if (!te) {
+ size_t nl = 0;
+ const char* nm = pool_str(p->pool, fn_alias_target, &nl);
+ compiler_panic(p->c, loc,
+ "alias target '%s' is undefined",
+ nm ? nm : "?");
+ }
+ ObjBuilder* ob = decl_obj(p->decls);
+ const ObjSym* ts = obj_symbol_get(ob, te->v.sym);
+ if (!ts || ts->kind == SK_UNDEF) {
+ size_t nl = 0;
+ const char* nm = pool_str(p->pool, fn_alias_target, &nl);
+ compiler_panic(p->c, loc,
+ "alias target '%s' is undefined",
+ nm ? nm : "?");
+ }
+ obj_symbol_define(ob, fent->v.sym, ts->section_id, ts->value,
+ ts->size);
+ }
return; /* prototype only */
}
perr(p, "expected '{' or ';' after function declarator");
@@ -6063,6 +6227,8 @@ static void parse_external_decl(Parser* p) {
decl_in.linkage = DL_EXTERNAL;
}
decl_in.visibility = SV_DEFAULT;
+ attr_list_to_decl(p->c, p->decls, specs.attrs, &decl_in);
+ attr_list_to_decl(p->c, p->decls, dattrs, &decl_in);
did = decl_declare(p->decls, &decl_in);
sym = decl_obj_sym(p->decls, did);
e = scope_define(p, name, SEK_GLOBAL, base_ty);
@@ -6070,16 +6236,25 @@ static void parse_external_decl(Parser* p) {
}
attr_list_append(&e->attrs, dattrs);
+ /* The effective alignment is the max of _Alignas and any
+ * __attribute__((aligned(N))) seen in decl-specs or per-declarator. */
+ u32 attr_align = attrs_pick_aligned(specs.attrs);
+ {
+ u32 a2 = attrs_pick_aligned(dattrs);
+ if (a2 > attr_align) attr_align = a2;
+ }
+ u32 align_eff = (specs.align > attr_align) ? specs.align : attr_align;
+
if (has_init) {
advance(p); /* '=' */
define_static_object(p, sym, base_ty, specs.quals, /*has_init=*/1,
- loc, specs.align);
+ loc, align_eff);
} else if (!is_pure_extern) {
/* Tentative def: emit a BSS reservation now. End-of-TU coalescing of
* multiple tentative defs into one is a Phase 4 follow-up; the
* Phase 4 corpus only has a single tentative def per TU. */
define_static_object(p, sym, base_ty, specs.quals, /*has_init=*/0,
- loc, specs.align);
+ loc, align_eff);
}
if (!accept_punct(p, ',')) break;
diff --git a/test/parse/cases/attr_p2_08_weak_undef.c b/test/parse/cases/attr_p2_08_weak_undef.c
@@ -1,8 +1,13 @@
/* 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. */
+ * doesn't honor it, so the link step fails with an unresolved symbol.
+ *
+ * The well-defined operation on a weak undef is taking its address —
+ * &sym is 0 when the linker can't resolve it. Dereferencing the
+ * resulting NULL is UB (matches GCC/Clang: they emit a plain load and
+ * trust the programmer to guard), so this test checks the address. */
extern int weak_missing __attribute__((weak));
int test_main(void) {
- return weak_missing;
+ return (&weak_missing != 0) ? 1 : 0;
}