wwstage: fold chained global-ptr field READ to cstage offset-fold (#16)

wwstage's chained-N_DOT resolver (dotchainresolve) didn't resolve a global
*struct root (only local *T and global value-struct), so gp.sf.len / gp.x.y
bailed to an inner-dot load + shuffle, byte-diverging from cstage's offset-fold.
Both stages already ran correct after #15 (475c003) -- a pure rule-10 asm
divergence. cstage is untouched (the oracle); wwstage aligns up.

Resolve a global N_TPTR root, and extract emitchainbase for the viacx base-load
(byte-identical across the 5 read + 2 store sites it replaces). The chained
STORE caller declines the global-ptr root (yok=false) so it falls to cstage's
address-spine mirror -- matching the #6/#15 decline-to-resolver discipline;
local *T chained stores still fold.

Test: +2 chained rows (gp.sf.len, gp.x.q), runtime + byte-id; proven to fail
byte-id with only the compiler files reverted, pass with the fix.

Sibling follow-ups filed: #17 (>32B tagged word-order), #18 (chained read into
an i64 sink MOVSXD check).
This commit is contained in:
2026-06-23 07:25:33 +09:00
parent 475c003b0d
commit ab3ac67afd
3 changed files with 95 additions and 67 deletions

View File

@@ -3293,6 +3293,29 @@ fn cgptrfieldload(c: *cgen, fi: *fieldinfo) void = {
emitline(", AX\n");
};
// emitchainbase — load the chained-DOT root BASE into CX for the viacx
// (global or `*T`-root) spine. Mirrors cstage cgen.c's uniform base
// resolution: a global is reached via LEAQ name(SB),CX; a `*struct`
// ROOT then derefs that address once (MOVQ (CX),CX) — covering the
// global `*struct` root, where BOTH flags hold (#16) — while a LOCAL
// `*T` root loads its frame slot (MOVQ off(BP),CX). The leaf load then
// indexes at totaloff off CX.
fn emitchainbase(c: *cgen, ptrroot: bool, isglobal: bool,
rootoff: i32, rootname: str) void = {
if (isglobal) {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
if (ptrroot) {
emitline("\tMOVQ\t(CX), CX\n");
};
} else {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
};
};
fn cgdot(c: *cgen, n: *syntax.node) void = {
let lhs: *syntax.node = n.lhs;
let fld: str = n.str;
@@ -4460,15 +4483,8 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
let viacx: bool = isglobal || ptrroot;
if (slicedelta >= 0) {
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
emitline("\tMOVQ\t");
emitdispreg((totaloff + slicedelta): i64, "CX");
emitline(", AX\n");
@@ -4493,15 +4509,8 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
tlu = tichase(tlu);
let ttsz: i32 = tlu.size: i32;
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
cgloadtaggedfield(c, "CX", totaloff, ttsz);
} else {
cgloadtaggedfield(c, "BP",
@@ -4521,15 +4530,8 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
// BP-rooted locals the registers do not alias so order is
// free.
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
emitline("\tMOVQ\t");
emitdispreg(totaloff: i64, "CX");
emitline(", AX\n");
@@ -4556,15 +4558,8 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
let mov: str = "MOVSD";
if (syntax.typeisf32(leaftype)) { mov = "MOVSS"; };
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
emitline("\t");
emitline(mov);
emitline("\t");
@@ -4582,15 +4577,8 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
let lop: str = loadopsz(syntax.typeissigned(leaftype),
leaftype.slotsize: i32);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
emitline("\t");
emitline(lop);
emitline("\t");
@@ -11216,6 +11204,15 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
let yok: bool = dotchainresolve(c, lhs,
&rootname, &rootoff, &totaloff,
&leaftype, &slicedelta, &isglobal, &ptrroot);
// A global `*struct` root chained STORE: cstage stores
// via the address-compute spine (LEAQ name(SB),BX; MOVQ
// (BX),BX; ADDQ; MOVQ AX,(BX)), NOT the READ path's
// offset-fold — so the fold spine below would diverge.
// Decline and fall through to the address-spine mirror,
// exactly as the addr-of path declines a ptr-rooted chain
// (cgenexpr.ww:5114). A LOCAL `*T` root (ptrroot &&
// !isglobal) DOES fold in both stages and stays. (#16)
if (isglobal && ptrroot) { yok = false; };
if (yok) {
// `*T` root and global share the CX-based emit:
// loader runs AFTER cgexpr(rhs) so AX/BX/X0 stay
@@ -11224,15 +11221,8 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
if (slicedelta >= 0) {
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + slicedelta): i64, "CX");
emitline("\n");
@@ -11539,15 +11529,8 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
if (syntax.typeisf32(leaftype)) { mov = "MOVSS"; };
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
emitline("\t");
emitline(mov);
emitline("\tX0, ");

View File

@@ -4942,6 +4942,28 @@ export fn dotchainresolve(c: *cgen, n: *syntax.node,
};
};
};
// Global `*struct` root (`let gp: *S`): localfindnode is nil and
// letvarstructinfo below only matches a VALUE-struct global, so a
// `*struct` global root needs its own arm. Gate on N_TPTR→N_TNAME
// exactly as the local `*T` arm above; the post-resolution loop peels
// the TY_PTR off cur.type_ and validates the pointee is a struct.
// Mirrors cstage cgen.c's ptr_root + let_islet base resolution
// (LEAQ name(SB),CX; MOVQ (CX),CX). (#16, chained twin of #15.)
if (!resolved && lc == nil) {
let gtn: *syntax.node = letvartnode(c, cur.str);
if (gtn != nil) {
if (gtn.kind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = gtn.lhs;
if (pe != nil) {
if (pe.kind == syntax.nkind.N_TNAME) {
*outisglobal = true;
*outptrroot = true;
resolved = true;
};
};
};
};
};
if (!resolved) {
let gsi: *structinfo = letvarstructinfo(c, cur.str);
if (gsi != nil) {

View File

@@ -29,14 +29,19 @@
* gp.len (*[]u8 pseudo-field) | 3
* lp.f (LOCAL ptr control) | 42
* bump param p.f (param control) | 10
* gp.sf.len (CHAINED str leaf) | 3 (#16)
* gp.x.q (CHAINED struct leaf)| 7 (#16)
* Each row also asserts w6c .s == w6c_ww .s byte-identical.
* Pre-fix: cstage SEGV(139) on every global-ptr row; wwstage ran wrong;
* cs vs ww .s differed.
*
* SCOPE: single-dot field reads only. Chained-through-global-ptr-root
* (`gp.x.y`, `gp.sf.len` inline) is a sibling — both stages run correct
* after this fix, but the chained-N_DOT spines still diverge in idiom
* (cstage offset-folds; wwstage inner-loads + shuffles), filed separately.
* #16 (chained-through-global-ptr-root, the last two rows): after #15
* both stages RAN correct but byte-DIVERGED on the chained-N_DOT spine —
* cstage offset-folds (LEAQ name(SB),CX; MOVQ (CX),CX; MOVQ <folded>(CX))
* while wwstage's chained spine bailed (dotchainresolve missed a global
* `*struct` root) onto the naive inner-load-and-shuffle catch-all. Fix
* aligns wwstage UP: dotchainresolve resolves a global `*struct` root as
* isglobal && ptrroot and the spine emits the same offset-fold (rule 10).
*/
#include <stdio.h>
#include <stdlib.h>
@@ -106,6 +111,24 @@ static const struct row ROWS[] = {
"type S = struct { f: i64, g: i64 };\n"
"fn rd(p: *S) i32 = { return (p.f + p.g): i32; };\n"
"export fn main() i32 = { let s: S = S{f=4,g=6}; return rd(&s); };\n", 10 },
/* #16: CHAINED read through a global-`*struct` root. Pre-fix BOTH stages
* ran correct after #15 but byte-DIVERGED — cstage offset-folds (LEAQ
* name(SB),CX; MOVQ (CX),CX; MOVQ <folded>(CX)) while wwstage's chained
* spine bailed (dotchainresolve missed a global `*struct` root) onto the
* naive inner-load-and-shuffle catch-all. */
{ "chain_str_len",
"package main;\n"
"type S = struct { a: i64, sf: str };\n"
"let gp: *S = nil;\n"
"export fn main() i32 = { let s: S = S{a=0,sf=\"abc\"}; gp = &s;"
" return gp.sf.len: i32; };\n", 3 },
{ "chain_struct_field",
"package main;\n"
"type Inner = struct { p: i64, q: i64 };\n"
"type S = struct { a: i64, x: Inner };\n"
"let gp: *S = nil;\n"
"export fn main() i32 = { let s: S = S{a=0,x=Inner{p=0,q=0}}; gp = &s;"
" s.x.q = 7; return gp.x.q: i32; };\n", 7 },
};
#define NROWS ((int)(sizeof ROWS / sizeof ROWS[0]))