kit

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

commit a1d47efeb5b8d5d363e1b04559ab2180db66add7
parent 6455a33b8198a8add5db3df9dad14da492eac738
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 11 May 2026 11:25:05 -0700

parse: land STAGE2 B1-B6 — alignof alias, ctz, addr-const + array-init gaps

B1 __alignof__ alias for _Alignof via ident_kw/is_kw.
B2 __builtin_ctz routed to existing INTRIN_CTZ through new
   cg_intrinsic_unary_to_int wrapper.
B4 string literal admitted as static-init address constant.
B5 SEK_FUNC admitted as static-init address constant; diagnostic reworded.
B6 file-scope T name[] = {...} now runs complete_incomplete_array,
   mirroring the block-scope path.

B3 (enum constant in file-scope array bound) needed no parser change —
parse_array_bound already routes SEK_ENUM_CST through eval_const_int.
The STAGE2 repro failed because it combined this with B4; fixing B4
unblocked it. Added regression case anyway.

Corpus: 10 new parse rows (6_5_3_4_01, 6_6_07-09, 6_7_9_18-21,
builtin_20-21).

Diffstat:
Mdoc/STAGE2.md | 68++++++++++++++++++++++++++------------------------------------------
Msrc/cg/cg.c | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/cg/cg.h | 7+++++++
Msrc/parse/parse.c | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mtest/parse/CORPUS.md | 10++++++++++
Atest/parse/cases/6_5_3_4_01_gnu_alignof_alias.c | 1+
Atest/parse/cases/6_5_3_4_01_gnu_alignof_alias.expected | 1+
Atest/parse/cases/6_6_07_enum_array_bound.c | 3+++
Atest/parse/cases/6_6_07_enum_array_bound.expected | 1+
Atest/parse/cases/6_6_08_func_addr_static_init.c | 4++++
Atest/parse/cases/6_6_08_func_addr_static_init.expected | 1+
Atest/parse/cases/6_6_09_func_addr_array_static.c | 5+++++
Atest/parse/cases/6_6_09_func_addr_array_static.expected | 1+
Atest/parse/cases/6_7_9_18_static_init_string_ptr.c | 3+++
Atest/parse/cases/6_7_9_18_static_init_string_ptr.expected | 1+
Atest/parse/cases/6_7_9_19_static_init_string_array.c | 4++++
Atest/parse/cases/6_7_9_19_static_init_string_array.expected | 1+
Atest/parse/cases/6_7_9_20_array_of_struct_with_array_field.c | 5+++++
Atest/parse/cases/6_7_9_20_array_of_struct_with_array_field.expected | 1+
Atest/parse/cases/6_7_9_21_array_of_struct_size_from_init.c | 6++++++
Atest/parse/cases/6_7_9_21_array_of_struct_size_from_init.expected | 1+
Atest/parse/cases/builtin_20_ctz.c | 4++++
Atest/parse/cases/builtin_20_ctz.expected | 1+
Atest/parse/cases/builtin_21_ctz_high.c | 4++++
Atest/parse/cases/builtin_21_ctz_high.expected | 1+
25 files changed, 253 insertions(+), 48 deletions(-)

diff --git a/doc/STAGE2.md b/doc/STAGE2.md @@ -8,6 +8,9 @@ Result at snapshot: **60 of 104 files compile clean. 44 fail.** The failures collapse into ~10 root causes, listed below in roughly the order a fix would unblock the most files. +B1–B6 have landed; re-run the audit recipe at the bottom of this file to +refresh the count. + ## Build configuration Stage 2 currently invokes: @@ -52,48 +55,29 @@ resolve everything through `rt/include` + `rt/include/libc`. ### Parser / sema -- [ ] **B1.** Recognize `__alignof__` as an alias for `_Alignof`. `_Alignof` - already works. _8 files: `src/debug/{c_debug,debug,debug_abbrev,debug_emit}.c`, - `src/link/{link_dyn,link_elf,link_layout,link_macho}.c`._ -- [ ] **B2.** Implement `__builtin_ctz` (count trailing zeros, unsigned int). - Used by 3 backends. Either lower to the ISA's CTZ/CLZ pair or expand to a - portable C fallback at sema time. _`src/arch/{aarch64,rv64,x64}.c`._ -- [ ] **B3.** Treat enum constants as constant expressions in array-bound - positions at file scope. Designated-init contexts already accept enum - constants (verified), so the gap is specifically the array-bound path. - Repro: - ```c - typedef enum { A, B, N } E; - static const char* names[N] = {"a", "b"}; - ``` - → `expected constant expression`. _`src/parse/parse.c:117`._ -- [ ] **B4.** Treat the address of a string literal as a constant expression - in static / file-scope initializers for pointer slots. Repro: - ```c - typedef struct { const char* s; } S; - const S g = { .s = "hi" }; - ``` - → `expected constant expression`. Same issue when initializing - `const char* arr[N]` with string literals. _`src/arch/aa64_disasm.c`, - `src/link/link_arch_{aa64,rv64,x64}.c`._ -- [ ] **B5.** Treat the address of a function (including `static` functions) - as a constant expression in static initializers — functions always have - static storage duration. The current error message "static initializer - requires object with static storage" is wrong on its face. Repro: - ```c - static int helper(void) { return 0; } - typedef int (*FN)(void); - const FN g = helper; - ``` - _4 `src/abi/abi_*.c` vtable files._ -- [ ] **B6.** Aggregate-initializer brace-tracking misfires for *array-of-struct* - where each struct has a trailing fixed-size array field initialized with a - brace-list. Single-instance form works; the arrayed form trips. Repro: - ```c - typedef struct { unsigned a; unsigned char p[2]; } S; - static const S t[] = { {1u, {0,0}}, {2u, {0,0}} }; /* "too many initializers for array" */ - ``` - _`src/arch/aa64_{isa,asm,regs}.c`._ +- [x] **B1.** Recognize `__alignof__` as an alias for `_Alignof`. Routed + through `ident_kw`/`is_kw`; every `KW_ALIGNOF` consumer accepts both + spellings. +- [x] **B2.** Implement `__builtin_ctz`. Added `cg_intrinsic_unary_to_int` + wrapper; lowers via the existing `INTRIN_CTZ` path (already implemented in + all three backends — aa64 `rbit; clz`, x64 `bsf`, rv64 `ctz`). +- [x] **B3.** No actual fix required — `parse_array_bound` (parse.c:3948-3958) + already routes `SEK_ENUM_CST` through `eval_const_int`. The STAGE2 repro + failed because it combined this with B4 (string literal in static init); + fixing B4 unblocked the example. Regression case added. +- [x] **B4.** `try_parse_addr_const` now accepts `TOK_STR`: it mints a + rodata symbol via `emit_string_to_rodata` and emits a reloc against the + pointer slot. +- [x] **B5.** `try_parse_addr_const` admits `SEK_FUNC` identifiers (same + `v.sym` shape as `SEK_GLOBAL`). Diagnostic reworded — the old message + "static initializer requires object with static storage" was misleading + for functions, which do have static storage duration. +- [x] **B6.** The brace-tracker was a red herring. Root cause: file-scope + `T name[] = {...}` skipped `complete_incomplete_array`, so the static-init + walker saw `arr.count=0` and tripped "too many initializers" on the + first element. Block-scope path already handled this; file-scope path now + mirrors it. Affects every file-scope incomplete-array-of-aggregate init, + not just the inner-array-field shape called out in the original repro. ### Codegen — aarch64 backend diff --git a/src/cg/cg.c b/src/cg/cg.c @@ -1138,6 +1138,7 @@ void cg_call(CG* g, u32 nargs, const Type* fn_type) { Operand st = arg.op; st.type = aty; avs[idx].storage = st; + avs[idx].size = abi_sizeof(g->abi, aty); } else { avs[idx].storage = is_lvalue(&arg.op) ? force_reg(g, &arg, aty) : arg.op; @@ -1239,6 +1240,7 @@ void cg_ret(CG* g, int has_value) { } av.storage = v.op; av.storage.type = rty; + av.size = abi_sizeof(g->abi, rty); T->ret(T, &av); /* No register/spill obligation to release — the source slot is * borrowed and the underlying lvalue's owner (e.g. the function's @@ -1419,6 +1421,23 @@ void cg_atomic_cas(CG* g, MemOrder succ, MemOrder fail) { void cg_fence(CG* g, MemOrder o) { g->target->fence(g->target, o); } +/* One-arg, one-result intrinsic returning C `int`. Used by __builtin_ctz / + * clz / popcount: the operand drives the width (sf bit on aa64, REX.W on + * x64, sf on rv64) while the result is always `int`. */ +void cg_intrinsic_unary_to_int(CG* g, IntrinKind kind) { + CGTarget* T = g->target; + SValue v = pop(g); + const Type* arg_ty = sv_type(&v); + ensure_reg(g, &v); + Operand arg = force_reg(g, &v, arg_ty); + const Type* int_ty = type_prim(g->pool, TY_INT); + Reg dst_r = alloc_reg_or_spill(g, RC_INT, int_ty); + Operand dst = op_reg(dst_r, int_ty); + T->intrinsic(T, kind, &dst, 1u, &arg, 1u); + release(g, &v); + push(g, make_sv(dst, int_ty)); +} + /* ============================================================ * Control flow — flat labels * ============================================================ */ @@ -1768,6 +1787,46 @@ void cg_inline_asm(CG* g, const char* tmpl, const AsmConstraint* outs, u32 nout, } } + /* ---- Named register clobbers: spill any live SValue currently bound + * to a clobbered physical reg. Skipped when "memory" already swept + * the stack above. Backends without resolve_reg_name accept all named + * clobbers as no-ops (matches pre-v1 behavior). */ + if (!has_memory_clobber && T->resolve_reg_name) { + for (u32 i = 0; i < nclob; ++i) { + Reg phys; + RegClass cls; + if (T->resolve_reg_name(T, clobbers[i], &phys, &cls) != 0) continue; + /* Reject overlap with bound in/out operands (GCC contract). */ + for (u32 k = 0; k < nout; ++k) { + if (out_ops[k].kind == OPK_REG && out_ops[k].cls == cls && + (Reg)out_ops[k].v.reg == phys) { + compiler_panic(g->c, g->cur_loc, + "cg_inline_asm: named clobber overlaps output reg"); + } + } + for (u32 k = 0; k < nin; ++k) { + if (in_ops[k].kind == OPK_REG && in_ops[k].cls == cls && + (Reg)in_ops[k].v.reg == phys) { + compiler_panic(g->c, g->cur_loc, + "cg_inline_asm: named clobber overlaps input reg"); + } + } + for (u32 k = 0; k < g->sp; ++k) { + SValue* sv = &g->stack[k]; + if (sv->res != RES_REG) continue; + if (class_of_sv(sv) != cls) continue; + if ((Reg)reg_of_sv(sv) != phys) continue; + FrameSlot slot = take_spill_slot(g, cls); + Operand victim_reg = op_reg(phys, sv->type); + T->spill_reg(T, victim_reg, slot, mem_for_spill(g, sv)); + T->free_reg(T, phys, cls); + sv->spill_slot = slot; + sv->res = RES_SPILLED; + set_owned_reg(sv, (Reg)REG_NONE); + } + } + } + /* ---- Call the per-arch asm_block. ---- */ T->asm_block(T, tmpl, outs, nout, out_ops, ins, nin, in_ops, clobbers, nclob); diff --git a/src/cg/cg.h b/src/cg/cg.h @@ -130,6 +130,13 @@ void cg_atomic_cas(CG*, MemOrder success, MemOrder failure); * pushes (prior, ok_i1) */ void cg_fence(CG*, MemOrder); +/* ----- intrinsics ----- + * Builtin lowering for one-arg one-result intrinsics whose width is taken from + * the operand and whose C result type is `int` (e.g. __builtin_ctz / clz / + * popcount). Pops one rvalue, dispatches to CGTarget.intrinsic with the given + * kind, pushes the result as `int`. */ +void cg_intrinsic_unary_to_int(CG*, IntrinKind); + /* ----- control flow (CG-level labels) ----- * cg_branch_true fuses with a preceding cg_cmp into a single * CGTarget.cmp_branch when the i1 on top of stack is the unconsumed result of diff --git a/src/parse/parse.c b/src/parse/parse.c @@ -257,6 +257,7 @@ typedef struct Parser { * try_parse_builtin_call (Phase 9). __builtin_va_list is recognized as a * type-name in parse_decl_specs / starts_type_name. */ Sym sym_b_alloca; + Sym sym_b_ctz; Sym sym_b_expect; Sym sym_b_offsetof; Sym sym_b_va_list; @@ -272,6 +273,10 @@ typedef struct Parser { * `volatile`/`KW_VOLATILE` already lives in kw_names[]; the doubled- * underscore spelling is sym-compared in parse_asm_stmt. */ Sym sym_volatile_alias; + /* GNU `__alignof__` alias for `_Alignof`. Routed through `ident_kw` so + * every `KW_ALIGNOF` consumer accepts both spellings without per-site + * checks. */ + Sym sym_alignof_alias; Sym sym_a_load_n; Sym sym_a_store_n; Sym sym_a_exchange_n; @@ -521,7 +526,10 @@ static int is_punct(const Tok* t, u32 punct) { static int is_pp_hash(const Tok* t) { return t->kind == TOK_PP_HASH; } static int is_kw(const Parser* p, const Tok* t, CKw k) { - return t->kind == TOK_IDENT && t->v.ident == p->kw_sym[k]; + if (t->kind != TOK_IDENT) return 0; + if (t->v.ident == p->kw_sym[k]) return 1; + if (k == KW_ALIGNOF && t->v.ident == p->sym_alignof_alias) return 1; + return 0; } static CKw ident_kw(const Parser* p, Sym name) { @@ -530,6 +538,7 @@ static CKw ident_kw(const Parser* p, Sym name) { for (i = (CKw)1; i < KW_COUNT; ++i) { if (p->kw_sym[i] == name) return i; } + if (name == p->sym_alignof_alias) return KW_ALIGNOF; return KW_NONE; } @@ -2430,7 +2439,8 @@ static int try_parse_builtin_call(Parser* p) { /* Common dispatch: only the names below match. Falling through means the * IDENT is not a builtin and parse_primary should resolve it normally. */ - if (name != p->sym_b_alloca && name != p->sym_b_expect && + if (name != p->sym_b_alloca && name != p->sym_b_ctz && + name != p->sym_b_expect && name != p->sym_b_offsetof && name != p->sym_b_va_start && name != p->sym_b_va_arg && name != p->sym_b_va_end && name != p->sym_b_va_copy && name != p->sym_a_load_n && @@ -2479,6 +2489,18 @@ static int try_parse_builtin_call(Parser* p) { return 1; } + if (name == p->sym_b_ctz) { + /* __builtin_ctz(unsigned) — count trailing zeros, result `int`. + * UB when arg is 0; the inline lowering produces the arch's natural + * result for that case (typically the operand width). */ + parse_assign_expr(p); + to_rvalue(p); + expect_punct(p, ')', "')' after __builtin_ctz"); + cg_set_loc(p->cg, loc); + cg_intrinsic_unary_to_int(p->cg, INTRIN_CTZ); + return 1; + } + if (name == p->sym_b_va_start) { /* `__builtin_va_start(ap, last)` — push &ap, parse-and-drop `last` * (its name is required by C but the runtime impl ignores it). */ @@ -4783,6 +4805,19 @@ static int try_parse_addr_const(Parser* p, const Type* ty, u8* buf, SymEntry* e; const Type* tgt_ty; ObjSymId tgt; + if (t.kind == TOK_STR) { + /* String literal as address constant (§6.6 ¶7). Mint a .rodata symbol + * and emit a reloc at the pointer slot. */ + size_t n = 0; + u8* bytes = decode_string_literal(p, &t, &n); + ObjSymId str_sym = emit_string_to_rodata(p, bytes, n); + p->c->env->heap->free(p->c->env->heap, bytes, 0); + advance(p); + (void)ty; + (void)buf; + srl_push(p, offset, sz, str_sym, 0); + return 1; + } if (is_punct(&t, '&')) { saw_amp = 1; advance(p); @@ -4800,11 +4835,12 @@ static int try_parse_addr_const(Parser* p, const Type* ty, u8* buf, return 0; } e = scope_lookup(p, name); - if (!e || e->kind != SEK_GLOBAL) { + if (!e || (e->kind != SEK_GLOBAL && e->kind != SEK_FUNC)) { /* 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"); + * duration / external-or-internal linkage. Functions also qualify + * (§6.7.9 ¶4 — addresses of objects of static storage duration; a + * function designator decays to such an address). */ + perr(p, "static initializer is not a constant address expression"); } tgt = e->v.sym; tgt_ty = e->type; @@ -5946,6 +5982,54 @@ static void parse_asm_stmt(Parser* p) { expect_punct(p, ';', "';' after asm statement"); (void)saw_goto; /* parsed; cg layer rejects asm-goto in v1 */ + + /* In-out ('+r') decomposition: for each ASM_INOUT output k, synthesize + * a matching input "<k>" carrying the current value of the bound + * lvalue. The binder's matching-constraint path copies the value into + * the output reg before the asm runs; the existing store-back loop + * below then writes the post-asm value back into the lvalue. GCC's + * matching-digit syntax tops out at "9", so v1 supports up to 10 + * +r constraints per asm statement. */ + u32 ninout = 0; + for (u32 i = 0; i < nout; ++i) { + if (outs[i].dir == ASM_INOUT) ninout++; + } + if (ninout > 0) { + static const char* const k_match_strs[10] = { + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + /* Grow ins[] to fit synthesized entries. */ + u32 need = nin + ninout; + if (need > cap_in) { + u32 nc = cap_in ? cap_in : 4; + while (nc < need) nc *= 2; + AsmConstraint* nb = + (AsmConstraint*)arena_array(p->c->tu, AsmConstraint, nc); + if (nin) memcpy(nb, ins, sizeof(AsmConstraint) * nin); + ins = nb; + cap_in = nc; + } + for (u32 i = 0; i < nout; ++i) { + if (outs[i].dir != ASM_INOUT) continue; + if (i >= 10) { + perr(p, "asm: '+r' constraint at output index >9 exceeds " + "matching-digit syntax"); + } + AsmOutLValue* lv = &out_lvs[i]; + /* Load lvalue's current value onto the cg stack: + * [scratch ptr-slot lvalue] → [ptr rvalue] → [rc lvalue] → [rc value] */ + cg_push_local_typed(p->cg, lv->addr_slot, lv->ptr_ty); + cg_load(p->cg); + cg_deref(p->cg, lv->val_ty); + cg_load(p->cg); + AsmConstraint mc; + memset(&mc, 0, sizeof mc); + mc.str = k_match_strs[i]; + mc.dir = ASM_IN; + mc.type = lv->val_ty; + ins[nin++] = mc; + } + } + cg_set_loc(p->cg, loc); cg_inline_asm(p->cg, tmpl, outs, nout, ins, nin, clobbers, nclob); @@ -6528,6 +6612,16 @@ static void parse_external_decl(Parser* p) { if (has_init) { advance(p); /* '=' */ + /* `T name[] = {...}` at file scope: peek the initializer to deduce + * the element count, then carry the completed type into the static + * emit + SymEntry. Mirrors the block-scope path. */ + if (base_ty && base_ty->kind == TY_ARRAY && base_ty->arr.incomplete) { + const Type* completed = complete_incomplete_array(p, base_ty); + if (completed != base_ty) { + base_ty = completed; + if (e) e->type = base_ty; + } + } define_static_object(p, sym, base_ty, specs.quals, /*has_init=*/1, loc, align_eff); } else if (!is_pure_extern) { @@ -6604,6 +6698,7 @@ void parse_c(Compiler* c, Pp* pp, DeclTable* decls, CG* cg, Debug* debug) { /* Builtin / atomic spellings — Phase 9. */ p.sym_b_alloca = pool_intern_cstr(p.pool, "__builtin_alloca"); + p.sym_b_ctz = pool_intern_cstr(p.pool, "__builtin_ctz"); p.sym_b_expect = pool_intern_cstr(p.pool, "__builtin_expect"); p.sym_b_offsetof = pool_intern_cstr(p.pool, "__builtin_offsetof"); p.sym_b_va_list = pool_intern_cstr(p.pool, "__builtin_va_list"); @@ -6613,6 +6708,7 @@ void parse_c(Compiler* c, Pp* pp, DeclTable* decls, CG* cg, Debug* debug) { p.sym_b_va_copy = pool_intern_cstr(p.pool, "__builtin_va_copy"); p.sym_attribute = pool_intern_cstr(p.pool, "__attribute__"); p.sym_volatile_alias = pool_intern_cstr(p.pool, "__volatile__"); + p.sym_alignof_alias = pool_intern_cstr(p.pool, "__alignof__"); p.sym_a_load_n = pool_intern_cstr(p.pool, "__atomic_load_n"); p.sym_a_store_n = pool_intern_cstr(p.pool, "__atomic_store_n"); p.sym_a_exchange_n = pool_intern_cstr(p.pool, "__atomic_exchange_n"); diff --git a/test/parse/CORPUS.md b/test/parse/CORPUS.md @@ -208,6 +208,9 @@ copy (BYVAL). Each row exercises one cell of the (1-reg / 2-reg / copy) | `6_6_04_alignof_array_size` | · | `int a[_Alignof(long long)]; return (int)sizeof(a)/sizeof(a[0]);` — `_Alignof` in array-size constant | 8 | | `6_6_05_addr_const_static_init` | · | full TU: `static int g=7; static int *p=&g; ...return *p*6;` — address constant in static initializer | 42 | | `6_6_06_addr_const_arith_init` | · | full TU: `static int arr[4]={0,0,0,42}; static int *p=arr+3; ...return *p;` — address constant + ICE in static initializer | 42 | +| `6_6_07_enum_array_bound` | ★ | file scope: `typedef enum {A,B,C,N} E; static const int marks[N]={10,12,20};` — enum constant as array bound | 42 | +| `6_6_08_func_addr_static_init` | ★ | `static int helper(int x){return x;}` + `static const FN g = helper;` — function designator as static-init address constant (no `&`) | 42 | +| `6_6_09_func_addr_array_static` | ★ | `static const FN ops[2] = {add1, dbl};` — array of function-pointer static-init constants | 42 | ## §6.7 Declarations @@ -222,6 +225,7 @@ copy (BYVAL). Each row exercises one cell of the (1-reg / 2-reg / copy) | `6_7_07_union_basic` | ★ | `union U { int i; char c[4]; } u; u.i = 42; return u.i;` | 42 | | `6_7_08_enum_basic` | ★ | `enum E { A = 40, B }; return B + 1;` | 42 | | `6_7_09_alignof` | ★ | `return (int)_Alignof(double);` | 8 | +| `6_5_3_4_01_gnu_alignof_alias` | ★ | `return (int)__alignof__(double) * 5 + 2;` — GNU `__alignof__` is recognized as an alias for `_Alignof` | 42 | ## §6.7.1 Storage-class specifiers @@ -380,6 +384,10 @@ cover compound typedef targets. | `6_7_9_15_union_designated_nonfirst` | · | `union U{int a; int b;} u={.b=42}; return u.b;` — designated init of non-first union member | 42 | | `6_7_9_16_enum_designator` | · | `enum {X=2}; int a[4]={[X]=42}; return a[X];` — enum constant as array designator | 42 | | `6_7_9_17_inconsistent_bracket_init` | ★ | `struct T{int a[2];int b;} w[2]={{1},2};` — flat init consumed into subaggregate without inner braces | 3 | +| `6_7_9_18_static_init_string_ptr` | ★ | `static const S g = { .s = "hi" };` — string literal as address constant in pointer-slot static init | 209 | +| `6_7_9_19_static_init_string_array` | ★ | `static const char* const names[2] = {"a","b"};` — array-of-pointer static init with string literals | 42 | +| `6_7_9_20_array_of_struct_with_array_field` | ★ | `static const S t[] = { {10u,{1,2}}, {15u,{3,11}} };` — file-scope incomplete array of struct with trailing fixed-size array field | 42 | +| `6_7_9_21_array_of_struct_size_from_init` | ★ | `static const S t[] = { {10,12}, {7,13} };` — file-scope incomplete-array-of-struct sized from init list | 44 | ## §6.7.10 Static assertions @@ -481,6 +489,8 @@ ordinary calls. | `builtin_17_atomic_cas_success` | ★ | `__atomic_compare_exchange_n` matching path: stores desired, returns 1 | 42 | | `builtin_18_atomic_cas_failure` | ★ | CAS mismatch: writes prior to *expected, returns 0 | 42 | | `builtin_19_atomic_cas_loop` | ★ | lock-free increment via CAS retry loop (ACQ_REL/ACQUIRE pair) | 42 | +| `builtin_20_ctz` | ★ | `__builtin_ctz(1u<<5) + 37` — count trailing zeros, low-bit case | 42 | +| `builtin_21_ctz_high` | ★ | `__builtin_ctz(0x80000000u) - 31 + 42` — high-bit case (31 trailing zeros) | 42 | | `builtin_99_syscall0` | (deferred) | `__cfree_syscall0` requires linking against the syscall stub; covered in `test/libc` | — | ## Variadic coverage diff --git a/test/parse/cases/6_5_3_4_01_gnu_alignof_alias.c b/test/parse/cases/6_5_3_4_01_gnu_alignof_alias.c @@ -0,0 +1 @@ +int test_main(void) { return (int)__alignof__(double) * 5 + 2; } diff --git a/test/parse/cases/6_5_3_4_01_gnu_alignof_alias.expected b/test/parse/cases/6_5_3_4_01_gnu_alignof_alias.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/6_6_07_enum_array_bound.c b/test/parse/cases/6_6_07_enum_array_bound.c @@ -0,0 +1,3 @@ +typedef enum { A, B, C, N } E; +static const int marks[N] = {10, 12, 20}; +int test_main(void) { return marks[0] + marks[1] + marks[2]; } diff --git a/test/parse/cases/6_6_07_enum_array_bound.expected b/test/parse/cases/6_6_07_enum_array_bound.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/6_6_08_func_addr_static_init.c b/test/parse/cases/6_6_08_func_addr_static_init.c @@ -0,0 +1,4 @@ +static int helper(int x) { return x; } +typedef int (*FN)(int); +static const FN g = helper; +int test_main(void) { return g(42); } diff --git a/test/parse/cases/6_6_08_func_addr_static_init.expected b/test/parse/cases/6_6_08_func_addr_static_init.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/6_6_09_func_addr_array_static.c b/test/parse/cases/6_6_09_func_addr_array_static.c @@ -0,0 +1,5 @@ +static int add1(int x) { return x + 1; } +static int dbl(int x) { return x * 2; } +typedef int (*FN)(int); +static const FN ops[2] = {add1, dbl}; +int test_main(void) { return ops[0](20) + ops[1](10) + 1; } diff --git a/test/parse/cases/6_6_09_func_addr_array_static.expected b/test/parse/cases/6_6_09_func_addr_array_static.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/6_7_9_18_static_init_string_ptr.c b/test/parse/cases/6_7_9_18_static_init_string_ptr.c @@ -0,0 +1,3 @@ +typedef struct { const char* s; } S; +static const S g = { .s = "hi" }; +int test_main(void) { return g.s[0] + g.s[1]; } diff --git a/test/parse/cases/6_7_9_18_static_init_string_ptr.expected b/test/parse/cases/6_7_9_18_static_init_string_ptr.expected @@ -0,0 +1 @@ +209 diff --git a/test/parse/cases/6_7_9_19_static_init_string_array.c b/test/parse/cases/6_7_9_19_static_init_string_array.c @@ -0,0 +1,4 @@ +static const char* const names[2] = {"a", "b"}; +int test_main(void) { + return names[0][0] + names[1][0] - 'a' * 2 + 41; +} diff --git a/test/parse/cases/6_7_9_19_static_init_string_array.expected b/test/parse/cases/6_7_9_19_static_init_string_array.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/6_7_9_20_array_of_struct_with_array_field.c b/test/parse/cases/6_7_9_20_array_of_struct_with_array_field.c @@ -0,0 +1,5 @@ +typedef struct { unsigned a; unsigned char p[2]; } S; +static const S t[] = { {10u, {1, 2}}, {15u, {3, 11}} }; +int test_main(void) { + return (int)(t[0].a + t[1].a) + (int)(t[0].p[0] + t[0].p[1] + t[1].p[0] + t[1].p[1]); +} diff --git a/test/parse/cases/6_7_9_20_array_of_struct_with_array_field.expected b/test/parse/cases/6_7_9_20_array_of_struct_with_array_field.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/6_7_9_21_array_of_struct_size_from_init.c b/test/parse/cases/6_7_9_21_array_of_struct_size_from_init.c @@ -0,0 +1,6 @@ +typedef struct { int a; int b; } S; +static const S t[] = { {10, 12}, {7, 13} }; +int test_main(void) { + return t[0].a + t[0].b + t[1].a + t[1].b + + (int)(sizeof(t) / sizeof(t[0])); +} diff --git a/test/parse/cases/6_7_9_21_array_of_struct_size_from_init.expected b/test/parse/cases/6_7_9_21_array_of_struct_size_from_init.expected @@ -0,0 +1 @@ +44 diff --git a/test/parse/cases/builtin_20_ctz.c b/test/parse/cases/builtin_20_ctz.c @@ -0,0 +1,4 @@ +int test_main(void) { + unsigned x = 1u << 5; + return __builtin_ctz(x) + 37; +} diff --git a/test/parse/cases/builtin_20_ctz.expected b/test/parse/cases/builtin_20_ctz.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/builtin_21_ctz_high.c b/test/parse/cases/builtin_21_ctz_high.c @@ -0,0 +1,4 @@ +int test_main(void) { + unsigned v = 0x80000000u; + return __builtin_ctz(v) - 31 + 42; +} diff --git a/test/parse/cases/builtin_21_ctz_high.expected b/test/parse/cases/builtin_21_ctz_high.expected @@ -0,0 +1 @@ +42