selfhost+test: route N_DOT base through indexvaluetnode + scanlocals for N_INDEX-lhs cgassign chain (#28+#30)

Wwstage's N_INDEX-lhs cgassign dispatch chain had a triple-site
N_DOT base gap (sister latents filed during #24 / #27 review):

  Read (#28): `obj.mat[i][k]` over a struct field mat: **u8.
  cgindex routes the outer N_INDEX's N_INDEX base through
  indexvaluetnode; the recursion bottomed out at the inner
  N_INDEX's N_DOT base with bt=nil. esz fell through to 8 +
  signed_elem to false — wwstage emitted a stray outer
  `MOVQ $8, CX; IMULQ CX, AX` plus `MOVQ (AX), AX` (8-byte
  read over a 1-byte u8) instead of cstage's bare
  `MOVZBQ (AX), AX`.

  Write (#30): `obj.arr[i] = v` over a struct field arr:
  [N]Tagged (e.g. (i64|str)). cgassign's N_DOT-base arm
  computed esz via indexbaseesz but never set elemtn, so the
  tagged-element store gate missed and the 24-byte tagged slot
  was overwritten by a single scalar MOVQ — wrong-width store
  + tag/payload junk in the upper 16 bytes.

Cstage walks `n->lhs->type` directly via the typed AST
(cmd/w6c/cgen.c idx_eff + the N_INDEX-lhs N_ASSIGN branch).
Wwstage now mirrors via indexvaluetnode, which #24 (aa8ca47)
introduced for the N_INDEX-base case; #28/#30 graduate it for
N_DOT base via the existing dotfieldtnode helper.

Bundle graduates N_DOT base for the entire N_INDEX-lhs cgassign
chain: (a) indexvaluetnode in cgenutil.ww handles N_DOT base via
dotfieldtnode; (b) cgassign N_DOT-base arm in cgenexpr.ww calls
indexvaluetnode for elemtn; (c) scanlocals N_DOT-base arm in
cgendecl.ww parallels the existing N_IDENT arm for tagscr-bump.
Splits are bisect-incoherent: (b)-alone clobbers locals via
under-sized frame, (a)-alone leaves the write path with wrong
elemtn, (c)-alone has no consumer. Only the triple delivers a
complete N_DOT-base graduation matching #24's N_INDEX-base
pattern.

Cstage's first-use+fail-loud strategy for @tagscr (#26 commit
069548d) handles the N_DOT-base shape naturally; the scanlocals
N_DOT arm is wwstage-specific. Long-term rule-10 convergence
(wwstage DOWN from scanlocals to first-use+fail-loud on BOTH
stages) is filed as task #15.

Class A wwstage cgen UNDER. No in-tree consumer; sister latents
filed during #24 + #27 reviews. Test 741_dotbase_chained pins
the dispatch + cstage-byte-identical asm for both rows.

Sister latent (filed): indexbaseesz has no N_TARRAY arm for
scalar struct-field array writes — `s.arr: [N]i32` scalar write
falls through to esz=8 on wwstage. No in-tree exerciser; tight
scope kept here.

115/115 ok. ww2 == ww3 == ww4 byte-id.
This commit is contained in:
2026-05-18 20:25:59 +09:00
parent 3ba19227ba
commit 4f1d7a462d
7 changed files with 349 additions and 0 deletions

View File

@@ -8208,6 +8208,9 @@ fn elemsizeofc(c: *cgen, t: *node) i32 = {
// the outer base type to *u8 (the post-inner-index value type), so
// cgindex can compute the outer element size honestly. Mirrors
// cstage's `n->lhs->type` via typed-AST (cmd/w6c/cgen.c idx_eff).
// N_DOT base graduated (tasks #28/#30) so `obj.mat[i][k]` reads
// and `obj.arr[i] = v` tagged-element writes route through the
// same helper as the N_IDENT/N_INDEX bases #24/#27 graduated.
fn indexvaluetnode(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_INDEX) { return nil; };
@@ -8220,6 +8223,7 @@ fn indexvaluetnode(c: *cgen, n: *node) *node = {
else { bt = letvartnode(c, base.str); };
};
if (base.kind == nkind.N_INDEX) { bt = indexvaluetnode(c, base); };
if (base.kind == nkind.N_DOT) { bt = dotfieldtnode(c, base); };
if (bt == nil) { return nil; };
let k: nkind = bt.kind;
if (k == nkind.N_TPTR) { return bt.lhs; };
@@ -14242,6 +14246,16 @@ fn cgassign(c: *cgen, n: *node) void = {
};
} else { if (base.kind == nkind.N_DOT) {
esz = indexbaseesz(c, base);
// Without this, the tagged-element gate
// below (keyed on elemtn) misses for
// `obj.arr[i] = v` over an [N]Tagged field
// and the store falls through to scalar —
// task #30, sister of the cgindex N_INDEX-
// base fix #24. indexvaluetnode now handles
// N_DOT base, so the element type drops out
// of the same helper.
let bt: *node = indexvaluetnode(c, lhs);
if (bt != nil) { elemtn = bt; };
} else { if (base.kind == nkind.N_INDEX) {
// Chained-write write-side parallel of the
// cgindex N_INDEX-base arm graduated in #24:
@@ -18010,6 +18024,12 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// cgwidentaggedstore to materialise the source in before copying
// to the element address. Slot is shared per function via
// c.tagscrsz (raised to the largest element slot_sz seen).
// N_DOT-base graduated (task #30) so `obj.arr[i] = v` for an
// [N]Tagged struct field pre-reserves the slot — parallels the
// cgassign N_DOT-base elemtn fix in cgenexpr.ww. Without this
// arm the runtime localadd allocates past the frame boundary
// and clobbers live locals; cstage handles the shape naturally
// via first-use+fail-loud (rule-10 convergence filed as #15).
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
@@ -18034,6 +18054,21 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
};
};
};
if (abase.kind == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, abase);
if (ft != nil) {
let bk: nkind = ft.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = ft.lhs; };
if (bk == nkind.N_TSLICE) { etn = ft.lhs; };
if (bk == nkind.N_TPTR) { etn = ft.lhs; };
if (etn != nil) {
if (istaggedtype(c, etn)) {
total += tagscrbump(c, slotsize(c, etn));
};
};
};
};
};
};
};

View File

@@ -241,6 +241,12 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// cgwidentaggedstore to materialise the source in before copying
// to the element address. Slot is shared per function via
// c.tagscrsz (raised to the largest element slot_sz seen).
// N_DOT-base graduated (task #30) so `obj.arr[i] = v` for an
// [N]Tagged struct field pre-reserves the slot — parallels the
// cgassign N_DOT-base elemtn fix in cgenexpr.ww. Without this
// arm the runtime localadd allocates past the frame boundary
// and clobbers live locals; cstage handles the shape naturally
// via first-use+fail-loud (rule-10 convergence filed as #15).
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
@@ -265,6 +271,21 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
};
};
};
if (abase.kind == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, abase);
if (ft != nil) {
let bk: nkind = ft.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = ft.lhs; };
if (bk == nkind.N_TSLICE) { etn = ft.lhs; };
if (bk == nkind.N_TPTR) { etn = ft.lhs; };
if (etn != nil) {
if (istaggedtype(c, etn)) {
total += tagscrbump(c, slotsize(c, etn));
};
};
};
};
};
};
};

View File

@@ -3518,6 +3518,16 @@ fn cgassign(c: *cgen, n: *node) void = {
};
} else { if (base.kind == nkind.N_DOT) {
esz = indexbaseesz(c, base);
// Without this, the tagged-element gate
// below (keyed on elemtn) misses for
// `obj.arr[i] = v` over an [N]Tagged field
// and the store falls through to scalar —
// task #30, sister of the cgindex N_INDEX-
// base fix #24. indexvaluetnode now handles
// N_DOT base, so the element type drops out
// of the same helper.
let bt: *node = indexvaluetnode(c, lhs);
if (bt != nil) { elemtn = bt; };
} else { if (base.kind == nkind.N_INDEX) {
// Chained-write write-side parallel of the
// cgindex N_INDEX-base arm graduated in #24:

View File

@@ -1247,6 +1247,9 @@ fn elemsizeofc(c: *cgen, t: *node) i32 = {
// the outer base type to *u8 (the post-inner-index value type), so
// cgindex can compute the outer element size honestly. Mirrors
// cstage's `n->lhs->type` via typed-AST (cmd/w6c/cgen.c idx_eff).
// N_DOT base graduated (tasks #28/#30) so `obj.mat[i][k]` reads
// and `obj.arr[i] = v` tagged-element writes route through the
// same helper as the N_IDENT/N_INDEX bases #24/#27 graduated.
fn indexvaluetnode(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_INDEX) { return nil; };
@@ -1259,6 +1262,7 @@ fn indexvaluetnode(c: *cgen, n: *node) *node = {
else { bt = letvartnode(c, base.str); };
};
if (base.kind == nkind.N_INDEX) { bt = indexvaluetnode(c, base); };
if (base.kind == nkind.N_DOT) { bt = dotfieldtnode(c, base); };
if (bt == nil) { return nil; };
let k: nkind = bt.kind;
if (k == nkind.N_TPTR) { return bt.lhs; };

View File

@@ -8208,6 +8208,9 @@ fn elemsizeofc(c: *cgen, t: *node) i32 = {
// the outer base type to *u8 (the post-inner-index value type), so
// cgindex can compute the outer element size honestly. Mirrors
// cstage's `n->lhs->type` via typed-AST (cmd/w6c/cgen.c idx_eff).
// N_DOT base graduated (tasks #28/#30) so `obj.mat[i][k]` reads
// and `obj.arr[i] = v` tagged-element writes route through the
// same helper as the N_IDENT/N_INDEX bases #24/#27 graduated.
fn indexvaluetnode(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_INDEX) { return nil; };
@@ -8220,6 +8223,7 @@ fn indexvaluetnode(c: *cgen, n: *node) *node = {
else { bt = letvartnode(c, base.str); };
};
if (base.kind == nkind.N_INDEX) { bt = indexvaluetnode(c, base); };
if (base.kind == nkind.N_DOT) { bt = dotfieldtnode(c, base); };
if (bt == nil) { return nil; };
let k: nkind = bt.kind;
if (k == nkind.N_TPTR) { return bt.lhs; };
@@ -14242,6 +14246,16 @@ fn cgassign(c: *cgen, n: *node) void = {
};
} else { if (base.kind == nkind.N_DOT) {
esz = indexbaseesz(c, base);
// Without this, the tagged-element gate
// below (keyed on elemtn) misses for
// `obj.arr[i] = v` over an [N]Tagged field
// and the store falls through to scalar —
// task #30, sister of the cgindex N_INDEX-
// base fix #24. indexvaluetnode now handles
// N_DOT base, so the element type drops out
// of the same helper.
let bt: *node = indexvaluetnode(c, lhs);
if (bt != nil) { elemtn = bt; };
} else { if (base.kind == nkind.N_INDEX) {
// Chained-write write-side parallel of the
// cgindex N_INDEX-base arm graduated in #24:
@@ -18010,6 +18024,12 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// cgwidentaggedstore to materialise the source in before copying
// to the element address. Slot is shared per function via
// c.tagscrsz (raised to the largest element slot_sz seen).
// N_DOT-base graduated (task #30) so `obj.arr[i] = v` for an
// [N]Tagged struct field pre-reserves the slot — parallels the
// cgassign N_DOT-base elemtn fix in cgenexpr.ww. Without this
// arm the runtime localadd allocates past the frame boundary
// and clobbers live locals; cstage handles the shape naturally
// via first-use+fail-loud (rule-10 convergence filed as #15).
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
@@ -18034,6 +18054,21 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
};
};
};
if (abase.kind == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, abase);
if (ft != nil) {
let bk: nkind = ft.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = ft.lhs; };
if (bk == nkind.N_TSLICE) { etn = ft.lhs; };
if (bk == nkind.N_TPTR) { etn = ft.lhs; };
if (etn != nil) {
if (istaggedtype(c, etn)) {
total += tagscrbump(c, slotsize(c, etn));
};
};
};
};
};
};
};