kit

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

commit e55665b3c5ce178b17e7838c903429bf62eaf2cc
parent 09bf3f299ea580495f0b4ce4a88dcacfe7a10f31
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri,  5 Jun 2026 21:34:21 -0700

Fix O1 label-address block liveness

Diffstat:
Mdoc/plan/TODO.md | 38--------------------------------------
Msrc/opt/pass_analysis.c | 20++++++++++++++++++++
Msrc/opt/pass_cfg.c | 37+++++++++++++++++++++++++++++++------
Msrc/opt/pass_native_emit.c | 5++---
Mtest/opt/run.sh | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 104 insertions(+), 47 deletions(-)

diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -5,44 +5,6 @@ fixed, remove it instead of checking it off or keeping a closed entry. Add new deferred fixes below as they are discovered. -## O1: `&&label` whose address is taken but never `goto`'d → undefined `.Lcfblk.N` - -At `-O1`, taking a label's address with the GNU labels-as-values extension -(`&&label`) when that label is **not** also a computed-goto target produces a -dangling reference to an internal control-flow-block symbol: the optimizer -elides/merges the block but the address-of relocation survives, so the output -has a relocation against an undefined `.Lcfblk.N`. The JIT path reports -`fatal: link: undefined reference to '.Lcfblk.6'`; an emitted object carries an -undefined `.Lcfblk.N` (visible via `kit nm`). O0 is fine (the block is kept). -The plain `goto *p; done:` form is also fine because `done` is a real goto -target, so its block is retained. - -This is distinct from the fixed `.Lkit_jt.*` / `.Lkit_ro.*` DCE leak class: -those were unreferenced orphan data symbols minted before O1 replay. `.Lcfblk.N` -is a referenced MC/code-label symbol; the object has live text relocations to -it, so the linker is correctly rejecting the undefined symbol. - -Minimal repro: - -```c -int p(void) { L: return &&L != 0; } -``` - -This emits a live text relocation against an undefined `.Lcfblk.N` at O1 and -`kit run -O1` fails to link. The same construct inside a dead inline is clean: - -```c -static inline void *q(void) { L: return &&L; } -int p(void) { return 0; } -``` - -Likely in the O1 CFG/block-merge passes (`src/opt/pass_cfg.c` / block dedup): -the target block of `IR_LOAD_LABEL_ADDR` must be treated as address-taken/live -through CFG cleanup and native emission, or any CFG merge/elision must retarget -the label-address relocation to the surviving block label. Found while writing -the backtrace anchor test (doc/plan/BACKTRACE.md); worked around there by -anchoring on a function address instead of `&&label`. - ## `__kit_syscallN` (`rt/include/kit/syscall.h`) is declared/documented but unimplemented The header declares `__kit_syscall0..6` and documents them as lowering to the diff --git a/src/opt/pass_analysis.c b/src/opt/pass_analysis.c @@ -217,11 +217,31 @@ static void block_list_add_unique(Arena* arena, OptBlockList* list, u32 block) { block_list_add(arena, list, block); } +static void order_dfs(OptAnalysis* a, u32 block); + +static void order_label_addr_target(OptAnalysis* a, const Inst* in) { + switch ((IROp)in->op) { + case IR_LOAD_LABEL_ADDR: + order_dfs(a, (u32)in->extra.imm); + break; + case IR_LOCAL_STATIC_DATA_LABEL_ADDR: { + CgIrLocalStaticLabelAux* aux = + (CgIrLocalStaticLabelAux*)in->extra.aux; + if (aux) order_dfs(a, (u32)aux->target); + break; + } + default: + break; + } +} + static void order_dfs(OptAnalysis* a, u32 block) { if (block >= a->nblocks || a->reachable[block]) return; a->reachable[block] = 1; Block* bl = &a->f->blocks[block]; for (u32 i = 0; i < bl->nsucc; ++i) order_dfs(a, bl->succ[i]); + for (u32 i = 0; i < bl->ninsts; ++i) + order_label_addr_target(a, &bl->insts[i]); a->po[a->npo] = block; a->po_index[block] = a->npo; ++a->npo; diff --git a/src/opt/pass_cfg.c b/src/opt/pass_cfg.c @@ -62,25 +62,50 @@ static int scope_control_succ_count(const Block* bl, const Inst* in, } } +static void push_reachable(Func* f, u8* reachable, u32* stack, u32* sp, + u32 block) { + if (block < f->nblocks && !reachable[block]) { + reachable[block] = 1; + stack[(*sp)++] = block; + } +} + +static void mark_label_addr_targets_reachable(Func* f, const Inst* in, + u8* reachable, u32* stack, + u32* sp) { + switch ((IROp)in->op) { + case IR_LOAD_LABEL_ADDR: + push_reachable(f, reachable, stack, sp, (u32)in->extra.imm); + break; + case IR_LOCAL_STATIC_DATA_LABEL_ADDR: { + CgIrLocalStaticLabelAux* aux = + (CgIrLocalStaticLabelAux*)in->extra.aux; + if (aux) push_reachable(f, reachable, stack, sp, (u32)aux->target); + break; + } + default: + break; + } +} + static u8* mark_reachable(Func* f) { u8* reachable = arena_zarray(f->arena, u8, f->nblocks ? f->nblocks : 1u); if (f->entry >= f->nblocks) return reachable; u32* stack = arena_array(f->arena, u32, f->nblocks ? f->nblocks : 1u); u32 sp = 0; - reachable[f->entry] = 1; - stack[sp++] = f->entry; + push_reachable(f, reachable, stack, &sp, f->entry); while (sp) { u32 b = stack[--sp]; if (b >= f->nblocks) continue; Block* bl = &f->blocks[b]; for (u32 s = 0; s < bl->nsucc; ++s) { u32 t = bl->succ[s]; - if (t < f->nblocks && !reachable[t]) { - reachable[t] = 1; - stack[sp++] = t; - } + push_reachable(f, reachable, stack, &sp, t); } + for (u32 i = 0; i < bl->ninsts; ++i) + mark_label_addr_targets_reachable(f, &bl->insts[i], reachable, stack, + &sp); } return reachable; } diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c @@ -1305,9 +1305,8 @@ static void emit_block(NativeEmitCtx* e, u32 block, u32 order_index, if (block >= e->f->nblocks) return; if (!e->label_placed[block]) { e->label_placed[block] = 1u; - if (block != e->f->entry) - e->target->label_place(e->target, - ensure_label(e, block, (SrcLoc){0, 0, 0})); + e->target->label_place(e->target, + ensure_label(e, block, (SrcLoc){0, 0, 0})); } Block* bl = &e->f->blocks[block]; int is_last_block = order_index + 1u == e->f->emit_order_n; diff --git a/test/opt/run.sh b/test/opt/run.sh @@ -117,4 +117,55 @@ if grep -Eq '[[:space:]][uU][[:space:]]+\.Lkit_ro' "$WORK/live_ro.nm" || exit 1 fi +cat > "$WORK/live_label_addr.c" <<'EOF' +int p(void) { L: return &&L != 0 ? 42 : 7; } +int q(void) { void *x = &&L; return x != 0 ? 11 : 3; L: return 5; } +EOF +"$KIT" cc -O1 -c "$WORK/live_label_addr.c" \ + -o "$WORK/live_label_addr.o" > "$WORK/live_label_addr.cc.out" 2>&1 +"$KIT" nm "$WORK/live_label_addr.o" > "$WORK/live_label_addr.nm" 2>&1 +if grep -Eq '[[:space:]][uU][[:space:]]+\.Lcfblk' "$WORK/live_label_addr.nm"; then + printf 'O1 label-address check FAILED: live &&label target is undefined:\n' >&2 + sed 's/^/ | /' "$WORK/live_label_addr.nm" >&2 + exit 1 +fi +if "$KIT" run -O1 -e p "$WORK/live_label_addr.c" \ + > "$WORK/live_label_addr.run.out" 2>&1; then + rc=0 +else + rc=$? +fi +if [ "$rc" -ne 42 ]; then + printf 'O1 label-address runtime check FAILED: exit %d (want 42)\n' "$rc" >&2 + sed 's/^/ | /' "$WORK/live_label_addr.run.out" >&2 + exit 1 +fi +if "$KIT" run -O1 -e q "$WORK/live_label_addr.c" \ + > "$WORK/live_label_addr_q.run.out" 2>&1; then + rc=0 +else + rc=$? +fi +if [ "$rc" -ne 11 ]; then + printf 'O1 label-address runtime check FAILED: q exit %d (want 11)\n' \ + "$rc" >&2 + sed 's/^/ | /' "$WORK/live_label_addr_q.run.out" >&2 + exit 1 +fi + +cat > "$WORK/dead_inline_label_addr.c" <<'EOF' +static inline void *q(void) { L: return &&L; } +int p(void) { return 0; } +EOF +"$KIT" cc -O1 -c "$WORK/dead_inline_label_addr.c" \ + -o "$WORK/dead_inline_label_addr.o" \ + > "$WORK/dead_inline_label_addr.cc.out" 2>&1 +"$KIT" nm "$WORK/dead_inline_label_addr.o" \ + > "$WORK/dead_inline_label_addr.nm" 2>&1 +if grep -q '\.Lcfblk' "$WORK/dead_inline_label_addr.nm"; then + printf 'O1 label-address check FAILED: dead inline leaked .Lcfblk:\n' >&2 + sed 's/^/ | /' "$WORK/dead_inline_label_addr.nm" >&2 + exit 1 +fi + printf 'tiny-inline: ok\n'