wcc: #152 let-initializer scope — defer the binding's localfind link past its own init (both stages)
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).
This commit is contained in:
5
Makefile
5
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 $@ $<
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
|
||||
69
test/wcc/989_letshadow.ww
Normal file
69
test/wcc/989_letshadow.ww
Normal file
@@ -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;
|
||||
};
|
||||
50
test/wcc/989_letshadow_run.c
Normal file
50
test/wcc/989_letshadow_run.c
Normal file
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user