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:
2026-06-08 12:17:18 +09:00
parent 3c7f1aa027
commit feae910a9b
8 changed files with 278 additions and 21 deletions

View File

@@ -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: