From feae910a9bc5de53bf42ebf604ad906defec14d9 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 8 Jun 2026 12:17:18 +0900 Subject: [PATCH] =?UTF-8?q?wcc:=20#152=20let-initializer=20scope=20?= =?UTF-8?q?=E2=80=94=20defer=20the=20binding's=20localfind=20link=20past?= =?UTF-8?q?=20its=20own=20init=20(both=20stages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A let's own name was visible during its OWN initializer: cgen prepended the new local into the name-keyed localfind chain BEFORE emitting the init, so `let x = f(x)` read the fresh UNINIT slot, not the outer/param x. Both-wrong- identical silent miscompile (gate-blind byte-id). Surfaced by path dirname/basename (was the c3-posix path->p rename). Align to Hare (harec check.c:1439 evals the init, then scope_insert). Fix, both stages, IDENTICAL asm: reserve the frame slot BEFORE the init emits, link the binding's name into the localfind chain only AFTER. - cstage cgen.c: split localoff -> localslot(reserve)+link; N_LET's 12 case-level breaks -> goto letlink (tail links once); the inner-for break is preserved; the 4 fatal() arms untouched. - wwstage cgen.ww/cgenstmt.ww: new localreserve (= localalloc minus the chain-link); cglet -> cgletbody(c,n,off) + a cglet wrapper that reserves -> calls body -> links after. Byte-id-safe on existing code: localfind is by-name, so deferring the link is a no-op on every non-self-shadow let (grep = 0 self-shadow sites) — 990-997 stay green. Because both stages emit identical now-correct asm, byte-id CANNOT catch this; the pin is a RUNTIME test, teeth-proven (revert -> pin fails). test/wcc/989_letshadow{.ww,_run.c}: param-shadow, let-in-init shadow, rename control, arrlit self-ref. Embedded regen: selfhost/cmd/{w6c,wwdump}/main.combined.ww. Gate: all 325 passed, byte-id 990-997 green, w6c c587f4a1 / w6c_ww 7a69f898 (deterministic). --- Makefile | 5 ++ cmd/w6c/cgen.c | 61 ++++++++++++++++-------- selfhost/cmd/w6c/main.combined.ww | 38 ++++++++++++++- selfhost/cmd/wcc/cgen.ww | 15 ++++++ selfhost/cmd/wcc/cgenstmt.ww | 23 +++++++++- selfhost/cmd/wwdump/main.combined.ww | 38 ++++++++++++++- test/wcc/989_letshadow.ww | 69 ++++++++++++++++++++++++++++ test/wcc/989_letshadow_run.c | 50 ++++++++++++++++++++ 8 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 test/wcc/989_letshadow.ww create mode 100644 test/wcc/989_letshadow_run.c diff --git a/Makefile b/Makefile index add8e2d3..7862f865 100644 --- a/Makefile +++ b/Makefile @@ -448,6 +448,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_strings_run \ $(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \ $(BIN)/test_path_run \ + $(BIN)/test_letshadow_run \ $(BIN)/test_decimal_run $(BIN)/test_strconv_int_run \ $(BIN)/test_stof_run $(BIN)/test_ftos_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ @@ -1997,6 +1998,10 @@ $(BIN)/test_path_run: test/wcc/989_path_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_letshadow_run: test/wcc/989_letshadow_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_ascii_run: test/wcc/904_ascii_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 581158ca..db8cec2a 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1892,18 +1892,30 @@ struct Local { * `a[127]` would land at +119(BP), past the saved RIP, into the * caller's frame. localfind walks from the head, so the most recent * binding still wins lookups inside its scope. */ -static int -localoff(Cg *c, Local **head, const char *name, int size, int *frame) +/* localslot — reserve a fresh stack slot (bump *frame, build the Local) + * but DON'T link it into the lookup chain. #152: the N_LET case links the + * binding only AFTER its initializer emits, so a self-shadowing init + * (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in + * the outer scope: harec check.c clet runs cexpr before scope_define). */ +static Local * +localslot(Cg *c, const char *name, int size, int *frame) { int al = 8; *frame = (*frame + size + al - 1) & ~(al - 1); - int off = -*frame; Local *l = amalloc(c->a, sizeof *l); l->name = name; - l->off = off; + l->off = -*frame; + l->next = NULL; + return l; +} + +static int +localoff(Cg *c, Local **head, const char *name, int size, int *frame) +{ + Local *l = localslot(c, name, size, frame); l->next = *head; *head = l; - return off; + return l->off; } /* local_alloc — synonym for localoff. Pre-#27 localoff deduped by name @@ -12254,7 +12266,11 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) || lu->kind == TY_STR || lu->kind == TY_STRUCT || lu->kind == TY_TUPLE || lu->kind == TY_TAGGED)) sz = (int)lu->size; - int off = localoff(c, locals, n->str, sz, frame); + /* #152: reserve the slot now (frame bump + nested-let + * offsets stay stable) but defer linking n->str into the + * lookup chain until AFTER the init emits — see letlink. */ + Local *letloc = localslot(c, n->str, sz, frame); + int off = letloc->off; int isf = cg_isfloat(lt); int isf32 = type_isf32(lt); /* alloc([], n) initialiser for a slice local: allocate @@ -12327,7 +12343,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); - break; + goto letlink; } } /* str IS []u8: cgexpr produces (AX=ptr, BX=len, CX=cap); @@ -12339,7 +12355,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); - break; + goto letlink; } /* Tuple initialiser (#105 / #164/#107): every IN-CAP tuple * receive routes here. Each element rides its SysV class: a @@ -12385,7 +12401,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) gpcur += tuple_eslot(p->type) / 8; eoff += tuple_eslot(p->type); } - break; + goto letlink; } /* #22a (rule 7, ken R1): an OVER-CAP tuple init whose rhs is * not a CALL has no store path — only the CALL shape rides @@ -12433,7 +12449,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) if (!rhs_sret_call) { cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz); - break; + goto letlink; } Type *ru = type_chase_named(n->rhs->type); if (!(ru == lu || type_eq(n->rhs->type, lt))) @@ -12454,7 +12470,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); - break; + goto letlink; } /* struct literal initialiser: field-by-field store via the * shared cg_structlit_fill_bp helper. The literal carries @@ -12466,7 +12482,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu && lu->kind == TY_STRUCT) { cg_structlit_fill_bp(c, locals, lu, n->rhs, off); - break; + goto letlink; } /* sret receive (#23 / #10 Fold B): the let's own slot IS the * caller-prealloc dest; the call writes through hidden RDI @@ -12482,7 +12498,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) cg_sret_dest_off = off; cgexpr(c, n->rhs, *locals); cg_sret_dest_off = 0; - break; + goto letlink; } /* Whole-struct receive for sizes <=24B (call-result rhs). * Counterpart of #4's cgreturn ABI: cgexpr leaves @@ -12533,7 +12549,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) gpcur++; } } - break; + goto letlink; } } if (n->rhs && n->rhs->kind == N_CALL && lu @@ -12554,7 +12570,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, op, areg(regs[full]), amem(D_BP, off + full * 8)); } - break; + goto letlink; } /* array literal initialiser: `let xs: [N]T = [a, b, c];`. * Walk elements in declaration order, store each at off + i*esz @@ -12573,7 +12589,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) if (n->rhs && n->rhs->kind == N_ARRLIT && lu && lu->kind == TY_ARRAY) { cg_arrlit_fill_bp(c, locals, lu, n->rhs, off); - break; + goto letlink; } /* Struct ident copy: `let p2: T = p1;` where T is a struct * >8B and rhs is a local ident. Pre-fix the path fell @@ -12611,7 +12627,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, lop, areg(D_AX), amem(D_BP, off + k)); } - break; + goto letlink; } } /* #265 fold-1/1b (#268): aggregate let-init copy from an @@ -12805,7 +12821,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) amem(D_BP, off + k)); k += 1; } - break; + goto letlink; } /* C4: nothing below this arm can initialise a >8B * struct/array slot — every fall-through was a silent @@ -12856,6 +12872,15 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } } /* arrays left uninitialised — caller writes via index */ + letlink: + /* #152: link the binding into the lookup chain AFTER its + * initializer emits, so a self-shadowing init (`let x = + * f(x)`) resolves x in the OUTER scope. Hare evals the init + * in the outer scope (harec check.c clet: cexpr before + * scope_define); localslot reserved the frame slot above so + * `off` and nested-let offsets are already stable. */ + letloc->next = *locals; + *locals = letloc; break; } case N_RETURN: diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d099ef6a..d0f7e836 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -35509,7 +35509,29 @@ fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { }; }; +// #152: reserve the let's frame slot, emit its initializer against the +// PRE-binding locals chain, then link the binding. A self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in +// c.locals while cgletbody runs (Hare evals the init in the outer scope: +// harec check.c clet runs cexpr before scope_define). localreserve bumps +// the frame now so off + nested-let offsets stay stable. fn cglet(c: *cgen, n: *node) void = { + let nm: str = n.str; + let sz: i32 = letslotsize(c, n); + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + let letloc: *local = localreserve(c, nm, sz, tn); + cgletbody(c, n, letloc.off); + letloc.lnext = c.locals; + c.locals = letloc; +}; + +// #152: cgletbody emits the initializer into the reserved slot `off`. +// The wrapper cglet reserves the slot BEFORE this runs and links the +// binding into c.locals only AFTER, so a self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in +// the outer scope: harec check.c clet runs cexpr before scope_define). +fn cgletbody(c: *cgen, n: *node, off: i32) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); // `let x = f()?` has no annotation but the cgen's struct-field @@ -35517,7 +35539,6 @@ fn cglet(c: *cgen, n: *node) void = { // success variant — see inferletcalltype. let tn: *node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; - let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; // `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45). @@ -38766,6 +38787,21 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { return off; }; +// localreserve — localalloc minus the chain-link. #152: cglet reserves +// the slot (frame bump + offset) before its initializer emits, then links +// the binding into c.locals only AFTER, so a self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in +// the outer scope: harec check.c clet runs cexpr before scope_define). +fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { + let asz: i32 = sz; + if (asz < 8) { asz = 8; }; + if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; + c.frame += asz; + let off: i32 = 0 - c.frame; + let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!; + return l; +}; + // localaddstack — register a param at a positive BP offset. Used for // args that overflow the 6 SysV int / 8 float reg windows; the caller // pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 48c98201..423856e8 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -584,6 +584,21 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { return off; }; +// localreserve — localalloc minus the chain-link. #152: cglet reserves +// the slot (frame bump + offset) before its initializer emits, then links +// the binding into c.locals only AFTER, so a self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in +// the outer scope: harec check.c clet runs cexpr before scope_define). +fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { + let asz: i32 = sz; + if (asz < 8) { asz = 8; }; + if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; + c.frame += asz; + let off: i32 = 0 - c.frame; + let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!; + return l; +}; + // localaddstack — register a param at a positive BP offset. Used for // args that overflow the 6 SysV int / 8 float reg windows; the caller // pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index aeb7cd80..974f6dff 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2115,7 +2115,29 @@ fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { }; }; +// #152: reserve the let's frame slot, emit its initializer against the +// PRE-binding locals chain, then link the binding. A self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in +// c.locals while cgletbody runs (Hare evals the init in the outer scope: +// harec check.c clet runs cexpr before scope_define). localreserve bumps +// the frame now so off + nested-let offsets stay stable. fn cglet(c: *cgen, n: *node) void = { + let nm: str = n.str; + let sz: i32 = letslotsize(c, n); + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + let letloc: *local = localreserve(c, nm, sz, tn); + cgletbody(c, n, letloc.off); + letloc.lnext = c.locals; + c.locals = letloc; +}; + +// #152: cgletbody emits the initializer into the reserved slot `off`. +// The wrapper cglet reserves the slot BEFORE this runs and links the +// binding into c.locals only AFTER, so a self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in +// the outer scope: harec check.c clet runs cexpr before scope_define). +fn cgletbody(c: *cgen, n: *node, off: i32) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); // `let x = f()?` has no annotation but the cgen's struct-field @@ -2123,7 +2145,6 @@ fn cglet(c: *cgen, n: *node) void = { // success variant — see inferletcalltype. let tn: *node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; - let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; // `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45). diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 19d92a39..4d2223c2 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -35509,7 +35509,29 @@ fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { }; }; +// #152: reserve the let's frame slot, emit its initializer against the +// PRE-binding locals chain, then link the binding. A self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in +// c.locals while cgletbody runs (Hare evals the init in the outer scope: +// harec check.c clet runs cexpr before scope_define). localreserve bumps +// the frame now so off + nested-let offsets stay stable. fn cglet(c: *cgen, n: *node) void = { + let nm: str = n.str; + let sz: i32 = letslotsize(c, n); + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + let letloc: *local = localreserve(c, nm, sz, tn); + cgletbody(c, n, letloc.off); + letloc.lnext = c.locals; + c.locals = letloc; +}; + +// #152: cgletbody emits the initializer into the reserved slot `off`. +// The wrapper cglet reserves the slot BEFORE this runs and links the +// binding into c.locals only AFTER, so a self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in +// the outer scope: harec check.c clet runs cexpr before scope_define). +fn cgletbody(c: *cgen, n: *node, off: i32) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); // `let x = f()?` has no annotation but the cgen's struct-field @@ -35517,7 +35539,6 @@ fn cglet(c: *cgen, n: *node) void = { // success variant — see inferletcalltype. let tn: *node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; - let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; // `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45). @@ -38766,6 +38787,21 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { return off; }; +// localreserve — localalloc minus the chain-link. #152: cglet reserves +// the slot (frame bump + offset) before its initializer emits, then links +// the binding into c.locals only AFTER, so a self-shadowing init +// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in +// the outer scope: harec check.c clet runs cexpr before scope_define). +fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { + let asz: i32 = sz; + if (asz < 8) { asz = 8; }; + if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; + c.frame += asz; + let off: i32 = 0 - c.frame; + let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!; + return l; +}; + // localaddstack — register a param at a positive BP offset. Used for // args that overflow the 6 SysV int / 8 float reg windows; the caller // pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), diff --git a/test/wcc/989_letshadow.ww b/test/wcc/989_letshadow.ww new file mode 100644 index 00000000..83aa051b --- /dev/null +++ b/test/wcc/989_letshadow.ww @@ -0,0 +1,69 @@ +// letshadow — #152 regression pin. A `let` binding must NOT be visible +// during its OWN initializer: `let x = f(x)` evaluates f(x) in the OUTER +// scope (Hare: harec check.c clet runs cexpr before scope_define). Both +// stages once linked the binding into the cgen localfind chain BEFORE +// emitting the init, so the init read the fresh UNINIT shadow slot — a +// silent miscompile identical on both stages (byte-id GREEN over it), so +// this is a RUNTIME assertion. Signalled-then-exit(+10) pinpoints the row. +// +// Run with `out/bin/ww run test/wcc/989_letshadow.ww`; exit 0 = all pass. + +package main; + +import os; + +let signalled: i32 = 0; +fn fail() void = { os.exit(signalled + 10); }; + +fn id(s: str) str = { return s; }; + +// Row 1 — PARAM self-shadow (ken's headline, the c3-posix shape): +// `let p = id(p)` must read the PARAM p, not the uninit shadow. +// "hello" → len(5)*100 + 'h'(104) = 604 (pre-fix: 0, the zero slot). +fn paramshadow(p: str) i32 = { + let p = id(p); + return (p.len: i32) * 100 + (p[0]: i32); +}; + +// Row 2 — LET shadows an OUTER-scope LET in its OWN init. ww rejects +// same-block redeclaration, so the inner let lives in a nested block; +// its init must read the OUTER x (=5) since the inner x isn't linked +// yet → 6 (pre-fix: garbage from the fresh uninit shadow slot). +fn letinletinit() i32 = { + let x: i32 = 5; + let r: i32 = 0; + { + let x: i32 = x + 1; + r = x; + }; + return r; +}; + +// Row 3 — CONTROL (rename, no shadow). Already correct both pre/post; +// pins no-regression. "hi" → 2. +fn renamecontrol(p: str) i32 = { + let p2 = id(p); + return p2.len: i32; +}; + +// Row 4 — UNIFORM-arm proof: arrlit self-ref. The inner `let a = [a, a]` +// (nested block; ww rejects same-block redeclaration) shadows the outer +// a; both elements must read the OUTER a (=3) → 6. Proves the N_ARRLIT +// arm defers the link too, not just the value-init arm. +fn arrlitselfref() i32 = { + let a: i32 = 3; + let r: i32 = 0; + { + let a: [2]i32 = [a, a]; + r = a[0] + a[1]; + }; + return r; +}; + +export fn main() i32 = { + signalled = 1; if (paramshadow("hello") != 604) { fail(); }; + signalled = 2; if (letinletinit() != 6) { fail(); }; + signalled = 3; if (renamecontrol("hi") != 2) { fail(); }; + signalled = 4; if (arrlitselfref() != 6) { fail(); }; + return 0; +}; diff --git a/test/wcc/989_letshadow_run.c b/test/wcc/989_letshadow_run.c new file mode 100644 index 00000000..9236e994 --- /dev/null +++ b/test/wcc/989_letshadow_run.c @@ -0,0 +1,50 @@ +/* + * 989_letshadow_run — #152 regression pin. Compile + run the letshadow + * fixture under the C-side `ww run` driver (cstage w6c) and assert exit 0. + * + * The bug (link-the-let-before-its-init) miscompiled IDENTICALLY on both + * stages, so byte-id 990-997 is blind to it; only a runtime value check + * catches the regression. Same thin-wrapper shape as 989_path_run. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "test/wcc/989_letshadow.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "letshadow_run FAIL: %s exited %d " + "(row %d miscompiled — #152)\n", src, rc, rc - 10); + return 1; + } + printf("letshadow_run: %s ok\n", src); + return 0; +}