wwstage: copy full tagged-element slot for N_DOT/N_INDEX-base index read (#261)

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.
This commit is contained in:
2026-06-02 06:02:56 +09:00
parent be23d7227a
commit 0afc272f47
6 changed files with 243 additions and 77 deletions

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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 }
};