From 0afc272f473166eecfbd6ab5c3ad7c65149b3438 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 06:02:56 +0900 Subject: [PATCH] wwstage: copy full tagged-element slot for N_DOT/N_INDEX-base index read (#261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #259 store fix unmasked a pre-existing latent cs!=ww in the tagged- element READ via an N_DOT base (`x.o[i]`) / chained N_INDEX base (`m[i][j]`): wwstage materialized the element as a SCALAR one-word load + zeroed tag where cstage copies the full tagged slot — silently dropping the tag/payload-high word (wrong variant). Three sites all keyed off the same N_IDENT-only gate; cstage classifies TY_TAGGED for ANY base off the checker-stamped element type. Align wwstage UP: - cgindex (cgenexpr.ww): the N_DOT/N_INDEX-base arm now sets elem_tagged/elem_slot_sz from n.type_ (the stamped element tinfo), mirroring cstage cgen.c:8101 — the full-slot copy arms then fire. - rhstaggedabicall (cgenutil.ww): the N_INDEX branch reads typeistagged(src.type_) for any base instead of an N_IDENT-only structural lookup, mirroring cstage's src->type keying — fixes the let-init / call-arg widen-source spill. - forwardtagged (cgenstmt.ww): the return-path passthrough gate now accepts N_INDEX/N_DOT tagged rhs (which cgexpr materializes into the tagged ABI), not just N_CALL — fixes `return x.o[i]`. read + call-arg + return + chained 2D all close by construction (one materialization path). cstage unchanged (pure wwstage-align-up). 949 gains 9 #261 rows (i32 + explicit-void variant per shape proves the tag survives) and flips the two #259 read-back rows to byteid=1. --- selfhost/cmd/w6c/main.combined.ww | 62 +++++++----- selfhost/cmd/wcc/cgenexpr.ww | 19 ++++ selfhost/cmd/wcc/cgenstmt.ww | 11 ++- selfhost/cmd/wcc/cgenutil.ww | 32 +++--- selfhost/cmd/wwdump/main.combined.ww | 62 +++++++----- test/wcc/949_dotbase_addr_slice_run.c | 134 +++++++++++++++++++++++--- 6 files changed, 243 insertions(+), 77 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 209f7530..65304fb6 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18058,27 +18058,17 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; if (src.kind == nkind.N_INDEX) { - let base: *node = src.lhs; - if (base != nil) { - if (base.kind == nkind.N_IDENT) { - let bl: *local = localfindnode(c, base.str); - if (bl != nil) { - let btn: *node = bl.tnode; - if (btn != nil) { - let bk: nkind = btn.kind; - let elemt: *node = nil; - if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; - if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; - if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; - if (elemt != nil) { - if (istaggedtype(c, elemt)) { - return true; - }; - }; - }; - }; - }; - }; + // The checker stamps every N_INDEX node's type_ to the element + // tinfo (check.ww:2334-2337 indexresult) for ANY base shape — + // N_IDENT, N_DOT (`x.o[i]`), or chained N_INDEX (`m[i][j]`). Read + // it directly, mirroring cstage cg_widen_tagged_store keying on + // src->type (cmd/w6c/cgen.c:2020-2023). #261: the prior + // N_IDENT-base-only structural lookup missed N_DOT/N_INDEX bases, + // so a tagged element materialized via `x.o[i]` (correct AX/DX + // slot from cgindex) was then spilled by the scalar-widen arm, + // dropping the tag/payload-high word — silent cs≠ww. + if (typeistagged(src.type_: *tinfo)) { return true; }; + return false; }; // N_DOT of a tagged-typed struct field — cgdot loads // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream @@ -20750,6 +20740,25 @@ fn cgindex(c: *cgen, n: *node) void = { elemisstr = isstrtype(c, etn); elemisslice = isslicetype(c, etn); }; + // N_DOT / N_INDEX base (`x.o[i]`, chained `m[i][j]`): the + // element tinfo is n.type_ (the checker-stamped indexresult), + // same source the esz/str/slice/float flags read above. Mirror + // cstage cgen.c:8101 `esubu->kind == TY_TAGGED` — classified for + // ANY base, NOT gated on N_IDENT, and NOT excluding the nullable + // fold (slot_sz=8 degrades the copy arm to one MOVQ, matching + // cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without + // this an N_DOT-base tagged element fell to the scalar + // loadopsz path and silently dropped the tag/payload-high word. + if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) { + let dt: *tinfo = n.type_: *tinfo; + if (dt != nil) { + if (typeistagged(dt)) { + elem_tagged = true; + elem_slot_sz = dt.size: i32; + esz = elem_slot_sz; + }; + }; + }; }; cgexpr(c, idx); if (esz > 1) { @@ -27761,8 +27770,17 @@ fn cgreturn(c: *cgen, n: *node) void = { // chain suffices because variants are primitives (single // tctx tinfo) or NAMED (per-decl identity); a full recursive // tinfo structural-eq helper is gated by #178. + // #261: N_INDEX of a tagged element (`return x.o[i]`) and + // N_DOT of a tagged field both materialize the full tagged + // ABI shape via cgexpr (cgindex slot-copy / cgdot field-load, + // AX=tag/DX=v0/...), exactly like an N_CALL of a tagged- + // returning fn — so a same-type return forwards them + // unchanged. cstage gates passthrough purely on the rhs type + // (no kind filter, cgen.c:8845); without these kinds an + // N_INDEX tagged-element return fell to the scalar-variant + // shuffle (MOVQ AX,DX; MOVQ $0,AX), dropping the payload. let forwardtagged: bool = false; - if (rhs.kind == nkind.N_CALL && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { + if ((rhs.kind == nkind.N_CALL || rhs.kind == nkind.N_INDEX || rhs.kind == nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { let ru: *tinfo = rhs.type_: *tinfo; for (ru != nil && ru.kind == tykind.TY_NAMED) { ru = ru.under; }; let fu: *tinfo = c.fnret.type_: *tinfo; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 6ac2e03d..f4fe7979 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1272,6 +1272,25 @@ fn cgindex(c: *cgen, n: *node) void = { elemisstr = isstrtype(c, etn); elemisslice = isslicetype(c, etn); }; + // N_DOT / N_INDEX base (`x.o[i]`, chained `m[i][j]`): the + // element tinfo is n.type_ (the checker-stamped indexresult), + // same source the esz/str/slice/float flags read above. Mirror + // cstage cgen.c:8101 `esubu->kind == TY_TAGGED` — classified for + // ANY base, NOT gated on N_IDENT, and NOT excluding the nullable + // fold (slot_sz=8 degrades the copy arm to one MOVQ, matching + // cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without + // this an N_DOT-base tagged element fell to the scalar + // loadopsz path and silently dropped the tag/payload-high word. + if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) { + let dt: *tinfo = n.type_: *tinfo; + if (dt != nil) { + if (typeistagged(dt)) { + elem_tagged = true; + elem_slot_sz = dt.size: i32; + esz = elem_slot_sz; + }; + }; + }; }; cgexpr(c, idx); if (esz > 1) { diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index e66bb6a7..f5489239 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -630,8 +630,17 @@ fn cgreturn(c: *cgen, n: *node) void = { // chain suffices because variants are primitives (single // tctx tinfo) or NAMED (per-decl identity); a full recursive // tinfo structural-eq helper is gated by #178. + // #261: N_INDEX of a tagged element (`return x.o[i]`) and + // N_DOT of a tagged field both materialize the full tagged + // ABI shape via cgexpr (cgindex slot-copy / cgdot field-load, + // AX=tag/DX=v0/...), exactly like an N_CALL of a tagged- + // returning fn — so a same-type return forwards them + // unchanged. cstage gates passthrough purely on the rhs type + // (no kind filter, cgen.c:8845); without these kinds an + // N_INDEX tagged-element return fell to the scalar-variant + // shuffle (MOVQ AX,DX; MOVQ $0,AX), dropping the payload. let forwardtagged: bool = false; - if (rhs.kind == nkind.N_CALL && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { + if ((rhs.kind == nkind.N_CALL || rhs.kind == nkind.N_INDEX || rhs.kind == nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { let ru: *tinfo = rhs.type_: *tinfo; for (ru != nil && ru.kind == tykind.TY_NAMED) { ru = ru.under; }; let fu: *tinfo = c.fnret.type_: *tinfo; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 552248de..9646eb14 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2548,27 +2548,17 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; if (src.kind == nkind.N_INDEX) { - let base: *node = src.lhs; - if (base != nil) { - if (base.kind == nkind.N_IDENT) { - let bl: *local = localfindnode(c, base.str); - if (bl != nil) { - let btn: *node = bl.tnode; - if (btn != nil) { - let bk: nkind = btn.kind; - let elemt: *node = nil; - if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; - if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; - if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; - if (elemt != nil) { - if (istaggedtype(c, elemt)) { - return true; - }; - }; - }; - }; - }; - }; + // The checker stamps every N_INDEX node's type_ to the element + // tinfo (check.ww:2334-2337 indexresult) for ANY base shape — + // N_IDENT, N_DOT (`x.o[i]`), or chained N_INDEX (`m[i][j]`). Read + // it directly, mirroring cstage cg_widen_tagged_store keying on + // src->type (cmd/w6c/cgen.c:2020-2023). #261: the prior + // N_IDENT-base-only structural lookup missed N_DOT/N_INDEX bases, + // so a tagged element materialized via `x.o[i]` (correct AX/DX + // slot from cgindex) was then spilled by the scalar-widen arm, + // dropping the tag/payload-high word — silent cs≠ww. + if (typeistagged(src.type_: *tinfo)) { return true; }; + return false; }; // N_DOT of a tagged-typed struct field — cgdot loads // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 721f5b74..fd344835 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18058,27 +18058,17 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; if (src.kind == nkind.N_INDEX) { - let base: *node = src.lhs; - if (base != nil) { - if (base.kind == nkind.N_IDENT) { - let bl: *local = localfindnode(c, base.str); - if (bl != nil) { - let btn: *node = bl.tnode; - if (btn != nil) { - let bk: nkind = btn.kind; - let elemt: *node = nil; - if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; - if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; - if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; - if (elemt != nil) { - if (istaggedtype(c, elemt)) { - return true; - }; - }; - }; - }; - }; - }; + // The checker stamps every N_INDEX node's type_ to the element + // tinfo (check.ww:2334-2337 indexresult) for ANY base shape — + // N_IDENT, N_DOT (`x.o[i]`), or chained N_INDEX (`m[i][j]`). Read + // it directly, mirroring cstage cg_widen_tagged_store keying on + // src->type (cmd/w6c/cgen.c:2020-2023). #261: the prior + // N_IDENT-base-only structural lookup missed N_DOT/N_INDEX bases, + // so a tagged element materialized via `x.o[i]` (correct AX/DX + // slot from cgindex) was then spilled by the scalar-widen arm, + // dropping the tag/payload-high word — silent cs≠ww. + if (typeistagged(src.type_: *tinfo)) { return true; }; + return false; }; // N_DOT of a tagged-typed struct field — cgdot loads // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream @@ -20750,6 +20740,25 @@ fn cgindex(c: *cgen, n: *node) void = { elemisstr = isstrtype(c, etn); elemisslice = isslicetype(c, etn); }; + // N_DOT / N_INDEX base (`x.o[i]`, chained `m[i][j]`): the + // element tinfo is n.type_ (the checker-stamped indexresult), + // same source the esz/str/slice/float flags read above. Mirror + // cstage cgen.c:8101 `esubu->kind == TY_TAGGED` — classified for + // ANY base, NOT gated on N_IDENT, and NOT excluding the nullable + // fold (slot_sz=8 degrades the copy arm to one MOVQ, matching + // cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without + // this an N_DOT-base tagged element fell to the scalar + // loadopsz path and silently dropped the tag/payload-high word. + if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) { + let dt: *tinfo = n.type_: *tinfo; + if (dt != nil) { + if (typeistagged(dt)) { + elem_tagged = true; + elem_slot_sz = dt.size: i32; + esz = elem_slot_sz; + }; + }; + }; }; cgexpr(c, idx); if (esz > 1) { @@ -27761,8 +27770,17 @@ fn cgreturn(c: *cgen, n: *node) void = { // chain suffices because variants are primitives (single // tctx tinfo) or NAMED (per-decl identity); a full recursive // tinfo structural-eq helper is gated by #178. + // #261: N_INDEX of a tagged element (`return x.o[i]`) and + // N_DOT of a tagged field both materialize the full tagged + // ABI shape via cgexpr (cgindex slot-copy / cgdot field-load, + // AX=tag/DX=v0/...), exactly like an N_CALL of a tagged- + // returning fn — so a same-type return forwards them + // unchanged. cstage gates passthrough purely on the rhs type + // (no kind filter, cgen.c:8845); without these kinds an + // N_INDEX tagged-element return fell to the scalar-variant + // shuffle (MOVQ AX,DX; MOVQ $0,AX), dropping the payload. let forwardtagged: bool = false; - if (rhs.kind == nkind.N_CALL && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { + if ((rhs.kind == nkind.N_CALL || rhs.kind == nkind.N_INDEX || rhs.kind == nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { let ru: *tinfo = rhs.type_: *tinfo; for (ru != nil && ru.kind == tykind.TY_NAMED) { ru = ru.under; }; let fu: *tinfo = c.fnret.type_: *tinfo; diff --git a/test/wcc/949_dotbase_addr_slice_run.c b/test/wcc/949_dotbase_addr_slice_run.c index 0d79e114..14d73eba 100644 --- a/test/wcc/949_dotbase_addr_slice_run.c +++ b/test/wcc/949_dotbase_addr_slice_run.c @@ -511,15 +511,26 @@ static const struct row rows[] = { " 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. */ + /* #261 tagged-element READ materialization via an N_DOT / chained + * N_INDEX base. The #259 store fix UNMASKED a pre-existing latent + * cs!=ww in the read-back: wwstage's cgindex N_DOT/N_INDEX-base arm + * never set elem_tagged (the detection block was gated on an N_IDENT + * base), so a tagged element fell to the SCALAR loadopsz path — one + * word into AX + a zeroed tag — where cstage copies the full 16B slot + * (AX=tag, DX=payload). Two more sites keyed off the same N_IDENT-only + * gate: rhstaggedabicall (let/call-arg widen source) and the return- + * path forwardtagged. All three drop the tag/payload-high word -> the + * wrong variant. Fix mirrors cstage (classify TY_TAGGED for ANY base, + * read the checker-stamped element type_): cgindex slot-copy + + * rhstaggedabicall typeistagged(src.type_) + forwardtagged broadened + * to N_INDEX/N_DOT. read + call-arg + return + chained 2D all close by + * construction. byteid=1 throughout: post-fix the materialization is + * byte-identical. + * + * Tag-survival proof: each shape is tested with BOTH an i32 variant + * AND an explicit `= void` variant. The old one-word load that zeroed + * the tag would misread the void slot as the i32 variant (tag 0); the + * void rows return 1 only if the tag survived. */ { "tagged_store_own_rd", "package main;\n" "type e = struct { o: [4](i32 | void) };\n" @@ -531,7 +542,7 @@ static const struct row rows[] = { " case let n: i32 => yield n;\n" " case void => yield 0: i32;\n" " };\n" - "};\n", 66, 0 }, + "};\n", 66, 1 }, { "tagged_store_ptr_rd", "package main;\n" "type e = struct { o: [4](i32 | void) };\n" @@ -544,7 +555,108 @@ static const struct row rows[] = { " case let n: i32 => yield n;\n" " case void => yield 0: i32;\n" " };\n" - "};\n", 77, 0 }, + "};\n", 77, 1 }, + { "tagged_rd_void", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = void;\n" + " let v: (i32 | void) = x.o[1];\n" + " return match (v) {\n" + " case let n: i32 => yield 99: i32;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n", 1, 1 }, + { "tagged_callarg_i32", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "fn take(v: (i32 | void)) i32 = {\n" + " return match (v) {\n" + " case let n: i32 => yield n;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[2] = 55;\n" + " return take(x.o[2]);\n" + "};\n", 55, 1 }, + { "tagged_callarg_void", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "fn take(v: (i32 | void)) i32 = {\n" + " return match (v) {\n" + " case let n: i32 => yield 99: i32;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[2] = void;\n" + " return take(x.o[2]);\n" + "};\n", 1, 1 }, + { "tagged_return_i32", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "fn ret(x: *e) (i32 | void) = { return x.o[1]; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 88;\n" + " let v: (i32 | void) = ret(&x);\n" + " return match (v) {\n" + " case let n: i32 => yield n;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n", 88, 1 }, + { "tagged_return_void", + "package main;\n" + "type e = struct { o: [4](i32 | void) };\n" + "fn ret(x: *e) (i32 | void) = { return x.o[1]; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = void;\n" + " let v: (i32 | void) = ret(&x);\n" + " return match (v) {\n" + " case let n: i32 => yield 99: i32;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n", 1, 1 }, + { "tagged_chained_i32", + "package main;\n" + "type m = struct { g: [2][2](i32 | void) };\n" + "export fn main() i32 = {\n" + " let y: m;\n" + " y.g[1][1] = 44;\n" + " let v: (i32 | void) = y.g[1][1];\n" + " return match (v) {\n" + " case let n: i32 => yield n;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n", 44, 1 }, + { "tagged_chained_void", + "package main;\n" + "type m = struct { g: [2][2](i32 | void) };\n" + "export fn main() i32 = {\n" + " let y: m;\n" + " y.g[1][1] = void;\n" + " let v: (i32 | void) = y.g[1][1];\n" + " return match (v) {\n" + " case let n: i32 => yield 99: i32;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n", 1, 1 }, + { "tagged_ctrl_bare_rd", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4](i32 | void);\n" + " a[1] = 33;\n" + " let v: (i32 | void) = a[1];\n" + " return match (v) {\n" + " case let n: i32 => yield n;\n" + " case void => yield 1: i32;\n" + " };\n" + "};\n", 33, 1 }, { NULL, NULL, 0, 0 } };