From 6bcb0929f8dad80db1d0af8f0916c35da13cae47 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 05:17:45 +0900 Subject: [PATCH] w6c+wwstage: tagged-element indexed store via dotbaseaddr + align dotchainaddr guard (#259,#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #259: the tagged-union array-field indexed STORE arm computed &arr[i] from a non-ident base (`x.o[1]=v` where o:[N](T|void)) with a plain cgexpr(base) — the N_DOT array field auto-derefs (loads the field's first 8 bytes AS a pointer) -> garbage dest -> SEGFAULT. Route the base through the array-gated helper cg_dotbase_addr/dotbaseaddr (dst BX keeps the scaled index live in AX; viaptr + chained handled by the shared helper), mirroring #257. Symmetric both stages. This was the last unrouted cgexpr(base) cell in the array-field-base-address family (#135/#252/#253/#255/#257) — proof-grep of both stages now shows ZERO unrouted base cells in the slice/decay/addr/index/store builders, so the family is closed by construction. (The chained-ptr-field scalar/str/ float store sites at cgenexpr.ww:6489+ / cgen.c:4379+ correctly cgexpr the pointer spine and are the #133 family, not array-field-address.) #256: align wwstage dotchainaddr's N_IDENT non-local arm to carry cstage cg_dotchain_addr's `let_islet || def_isstructdef` guard (here isletvar || deflookup) instead of emitting LEAQ name(SB) unconditionally. Unreachable on valid input (a struct-typed chain root is always local / let-global / struct def) so zero divergent asm — never-silent ethos only. Tests (949): store-only byte-id rows (tagged_store_own/_ptr) gate the #259 store base-address emission cs==ww; store+readback rows (tagged_store_*_rd) are run-only (cstage) proving the store wrote the right slot (66/77) and no longer segfaults. byte-id on the readback rows is blocked by an ORTHOGONAL newly-surfaced divergence in the N_DOT-base tagged-element READ materialization (sibling of #255: wwstage loads one word + zeroes the tag where cstage copies the full 16-byte slot) — the store base is already byte-id; only the read-back diverges. Reported separately for triage. combined.ww regen'd (w6c + wwdump embed cgen). --- cmd/w6c/cgen.c | 6 +++ selfhost/cmd/w6c/main.combined.ww | 30 ++++++++++--- selfhost/cmd/wcc/cgenexpr.ww | 30 ++++++++++--- selfhost/cmd/wwdump/main.combined.ww | 30 ++++++++++--- test/wcc/949_dotbase_addr_slice_run.c | 65 +++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 21 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 93f317b1..d227dcc7 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4967,6 +4967,12 @@ cgexpr(Cg *c, Node *n, Local *locals) int boff = localfind(locals, base->str); ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); + } else if (cg_dotbase_addr(c, base, D_BX, locals)) { + /* #259: N_DOT base resolved inline to the + * field address; cgexpr fallback would + * auto-deref + load the array field as a + * VALUE (the broken shape). dst BX keeps the + * scaled index live in AX (spill contract). */ } else { ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, base, locals); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d1df0bb7..5c18434a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20231,12 +20231,22 @@ fn dotchainaddr(c: *cgen, n: *node, dstreg: str) bool = { emitline("\n"); return true; }; - emitline("\tLEAQ\t"); - emitsymname(c, n.str); - emitline("(SB), "); - emitline(dstreg); - emitline("\n"); - return true; + // #256: carry cstage cg_dotchain_addr's `let_islet || + // def_isstructdef` guard (never-silent ethos). Unreachable on + // valid input — a struct-typed chain root is always local, a + // let-global, or a struct def — so this adds zero divergent + // asm; it just refuses to LEAQ name(SB) for a name that names + // neither. deflookup is the broader def-registry twin (ww has no + // struct-specific def predicate; harmless given unreachability). + if (isletvar(c, n.str) || deflookup(c, n.str)) { + emitline("\tLEAQ\t"); + emitsymname(c, n.str); + emitline("(SB), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + return false; }; if (n.kind != nkind.N_DOT) { return false; }; let x: *node = n.lhs; @@ -24337,12 +24347,18 @@ fn cgassign(c: *cgen, n: *node) void = { emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; + } else { if (dotbaseaddr(c, base, "BX")) { + // #259: N_DOT base resolved inline to + // the field address; cgexpr fallback + // would auto-deref + load the array + // field as a VALUE (broken shape). dst + // BX keeps the scaled index live in AX. } else { emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); - };};}; + };};};}; emitline("\tADDQ\tAX, BX\n"); let cc: i32 = 0; for (cc < slot_sz) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index ee06fcf1..6ac2e03d 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -913,12 +913,22 @@ fn dotchainaddr(c: *cgen, n: *node, dstreg: str) bool = { emitline("\n"); return true; }; - emitline("\tLEAQ\t"); - emitsymname(c, n.str); - emitline("(SB), "); - emitline(dstreg); - emitline("\n"); - return true; + // #256: carry cstage cg_dotchain_addr's `let_islet || + // def_isstructdef` guard (never-silent ethos). Unreachable on + // valid input — a struct-typed chain root is always local, a + // let-global, or a struct def — so this adds zero divergent + // asm; it just refuses to LEAQ name(SB) for a name that names + // neither. deflookup is the broader def-registry twin (ww has no + // struct-specific def predicate; harmless given unreachability). + if (isletvar(c, n.str) || deflookup(c, n.str)) { + emitline("\tLEAQ\t"); + emitsymname(c, n.str); + emitline("(SB), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + return false; }; if (n.kind != nkind.N_DOT) { return false; }; let x: *node = n.lhs; @@ -5019,12 +5029,18 @@ fn cgassign(c: *cgen, n: *node) void = { emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; + } else { if (dotbaseaddr(c, base, "BX")) { + // #259: N_DOT base resolved inline to + // the field address; cgexpr fallback + // would auto-deref + load the array + // field as a VALUE (broken shape). dst + // BX keeps the scaled index live in AX. } else { emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); - };};}; + };};};}; emitline("\tADDQ\tAX, BX\n"); let cc: i32 = 0; for (cc < slot_sz) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b90faf74..b3491b1b 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20231,12 +20231,22 @@ fn dotchainaddr(c: *cgen, n: *node, dstreg: str) bool = { emitline("\n"); return true; }; - emitline("\tLEAQ\t"); - emitsymname(c, n.str); - emitline("(SB), "); - emitline(dstreg); - emitline("\n"); - return true; + // #256: carry cstage cg_dotchain_addr's `let_islet || + // def_isstructdef` guard (never-silent ethos). Unreachable on + // valid input — a struct-typed chain root is always local, a + // let-global, or a struct def — so this adds zero divergent + // asm; it just refuses to LEAQ name(SB) for a name that names + // neither. deflookup is the broader def-registry twin (ww has no + // struct-specific def predicate; harmless given unreachability). + if (isletvar(c, n.str) || deflookup(c, n.str)) { + emitline("\tLEAQ\t"); + emitsymname(c, n.str); + emitline("(SB), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + return false; }; if (n.kind != nkind.N_DOT) { return false; }; let x: *node = n.lhs; @@ -24337,12 +24347,18 @@ fn cgassign(c: *cgen, n: *node) void = { emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; + } else { if (dotbaseaddr(c, base, "BX")) { + // #259: N_DOT base resolved inline to + // the field address; cgexpr fallback + // would auto-deref + load the array + // field as a VALUE (broken shape). dst + // BX keeps the scaled index live in AX. } else { emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); - };};}; + };};};}; emitline("\tADDQ\tAX, BX\n"); let cc: i32 = 0; for (cc < slot_sz) { diff --git a/test/wcc/949_dotbase_addr_slice_run.c b/test/wcc/949_dotbase_addr_slice_run.c index c3f02f9f..0d79e114 100644 --- a/test/wcc/949_dotbase_addr_slice_run.c +++ b/test/wcc/949_dotbase_addr_slice_run.c @@ -480,6 +480,71 @@ static const struct row rows[] = { " q.v = \"hello\";\n" " return rd(q.v[1:4]);\n" "};\n", 3, 1 }, + /* #259 tagged-union array-field indexed STORE via N_DOT base. The + * tagged-element store arm computed &arr[i] from a non-ident base + * (`x.o`) with a plain cgexpr(base) — the N_DOT array field auto- + * derefs (loads the field's first 8 bytes AS a pointer) -> garbage + * dest -> SEGFAULT. Last unrouted cell of the array-field-base- + * address family (#135/#252/#253/#255/#257 proof-grep residual). Fix + * routes the store base through cg_dotbase_addr/dotbaseaddr (array- + * gated; viaptr + chained handled by the shared helper). cs==ww + * broken identically pre-fix (gate-blind correctness). + * + * STORE byte-id rows (byteid=1): store-only into a tagged array field + * (own value-struct + via *struct param), return a constant. These + * gate the #259 store base-address emission cs==ww — the store + * portion is byte-identical post-fix (LEAQ base, not auto-deref). */ + { "tagged_store_own", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 66;\n" + " return 0;\n" + "};\n", 0, 1 }, + { "tagged_store_ptr", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "fn wr(x: *e) void = { x.o[2] = 77; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " wr(&x);\n" + " return 0;\n" + "};\n", 0, 1 }, + /* #259 STORE-correctness rows (byteid=0, run-only on cstage): store + * then read the element back via match to confirm the store wrote the + * right slot (66/77, not garbage) and no longer segfaults. byte-id is + * BLOCKED here by an ORTHOGONAL newly-surfaced divergence in the + * N_DOT-base tagged-element READ materialization (sibling of #255 in + * the same N_DOT-base index fallback: wwstage loads ONE word + zeroes + * the tag where cstage copies the full 16-byte slot) — the store base + * is already byte-id (see the store-only rows above); only the + * read-back diverges. Filed separately; do not gate byte-id here. */ + { "tagged_store_own_rd", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 66;\n" + " let v: (i32 | void) = x.o[1];\n" + " return match (v) {\n" + " case let n: i32 => yield n;\n" + " case void => yield 0: i32;\n" + " };\n" + "};\n", 66, 0 }, + { "tagged_store_ptr_rd", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "fn wr(x: *e) void = { x.o[2] = 77; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " wr(&x);\n" + " let v: (i32 | void) = x.o[2];\n" + " return match (v) {\n" + " case let n: i32 => yield n;\n" + " case void => yield 0: i32;\n" + " };\n" + "};\n", 77, 0 }, { NULL, NULL, 0, 0 } };