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).
70 lines
2.2 KiB
Plaintext
70 lines
2.2 KiB
Plaintext
// 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;
|
|
};
|