wwstage: align direct-ptr tagged-field READ word-order to cstage (#17)

A >32B tagged-union field (slice payload) read through a direct *struct
pointer byte-diverged: wwstage's cgloadtaggedfield always loaded R8@+24
before CX@+16, but cstage's direct-*struct-ptr arm (cgen.c ~11926) loads in
offset order CX@+16 then R8@+24. Both ran correct -- a pre-existing rule-10
asm divergence, for a local *struct ptr as well as a global one.

Thread a cxlast flag through cgloadtaggedfield: the direct-ptr site
(cgptrfieldload, the shared local+global chokepoint) passes cxlast=false to
match cstage's offset order; the other 5 callers keep cxlast=true (byte
unchanged). A global flip was rejected -- it would clobber the CX-base
callers (CX@+16 first destroys the base before the R8@+24 read), and the
chained-BX caller must stay R8-first to mirror cstage's chained twin
(cgen.c ~12021); the order is a genuine per-arm property of cstage, not
derivable from the base register.

Test: +2 rows (tagged_slice_field via global *struct ptr, _local via local
*struct ptr), runtime + byte-id; both proven to fail byte-id with only the
compiler files reverted.
This commit is contained in:
2026-06-24 00:07:24 +09:00
parent ab3ac67afd
commit d6ea497da1
3 changed files with 63 additions and 18 deletions

View File

@@ -3256,7 +3256,10 @@ fn cgptrfieldload(c: *cgen, fi: *fieldinfo) void = {
// field tail. BX is never a load target, so order is harmless.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
cgloadtaggedfield(c, "BX", fi.foff, tsz);
// cxlast=false: BX is not a cursor target here, so match
// cstage's direct *struct-ptr arm and load CX@+16 before
// R8@+24 in strict offset order (#17).
cgloadtaggedfield(c, "BX", fi.foff, tsz, false);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap} as a slice field: load
@@ -3479,7 +3482,7 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
cgloadtaggedfield(c, "BP",
lc.off + fi.foff, tsz);
lc.off + fi.foff, tsz, true);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap}
@@ -4037,7 +4040,7 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
// through to fieldloadop and dropped payload.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
cgloadtaggedfield(c, "CX", fi.foff, tsz);
cgloadtaggedfield(c, "CX", fi.foff, tsz, true);
return;
};
// str IS []u8 — 3-word {ptr,len,cap}, the local
@@ -4511,10 +4514,10 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
if (viacx) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
cgloadtaggedfield(c, "CX", totaloff, ttsz);
cgloadtaggedfield(c, "CX", totaloff, ttsz, true);
} else {
cgloadtaggedfield(c, "BP",
rootoff + totaloff, ttsz);
rootoff + totaloff, ttsz, true);
};
return;
};
@@ -4715,7 +4718,7 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
emitline("\tMOVQ\tAX, BX\n");
cgloadtaggedfield(c, "BX",
tf.offset: i32,
plu.size: i32);
plu.size: i32, true);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap}

View File

@@ -3828,7 +3828,8 @@ fn taggedidcastpeel(c: *cgen, e: *syntax.node) *syntax.node = {
// Callers must guarantee basereg is one of "BP", "BX", "CX"; the
// only register loaded into that is NOT a target is BX, so AX-
// or DX-rooted callers must spill first.
fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = {
fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32,
cxlast: bool) void = {
// #37: >32B box — leave its ADDRESS in AX (taggedmemread, the
// sret-receive convention); the 4-reg cursor walk below would
// truncate past payload word 2. Mirrors cstage's N_DOT
@@ -3847,17 +3848,34 @@ fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = {
emitline("\tMOVQ\t");
emitdispreg((foff + 8): i64, basereg);
emitline(", DX\n");
// word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32).
if (slot_sz > 24) {
emitline("\tMOVQ\t");
emitdispreg((foff + 24): i64, basereg);
emitline(", R8\n");
};
// word1 → CX (load LAST; conflicts with CX-base globals).
if (slot_sz > 16) {
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, basereg);
emitline(", CX\n");
// word1 → CX, word2 → R8. cstage's direct *struct-ptr arm (cgen.c
// N_DOT TY_PTR→TY_STRUCT, ~11926) loads them in strict offset order
// (CX@+16 then R8@+24, BX is not a cursor target); every other arm —
// local/global struct (CX may be the base addr) and the chained-
// *struct twin (~12021) — loads R8 BEFORE CX. cxlast selects: true =
// R8 then CX, false = offset order.
if (cxlast) {
if (slot_sz > 24) {
emitline("\tMOVQ\t");
emitdispreg((foff + 24): i64, basereg);
emitline(", R8\n");
};
if (slot_sz > 16) {
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, basereg);
emitline(", CX\n");
};
} else {
if (slot_sz > 16) {
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, basereg);
emitline(", CX\n");
};
if (slot_sz > 24) {
emitline("\tMOVQ\t");
emitdispreg((foff + 24): i64, basereg);
emitline(", R8\n");
};
};
};

View File

@@ -96,6 +96,30 @@ static const struct row ROWS[] = {
"export fn main() i32 = { let s: S = S{a=0,t=5i32}; gp = &s;"
" let v: i32 = 0; match (gp.t) { case let x: i32 => v = x;"
" case let y: i64 => v = y: i32; }; return v; };\n", 5 },
{ "tagged_slice_field",
/* #17: a 32B slice-payload tagged field (8B tag + 24B slice =
* full 4-reg cursor) read via global *struct ptr. cgloadtaggedfield
* fills AX/DX/CX/R8; wwstage loaded R8@+24 before CX@+16 while
* cstage's direct *struct-ptr arm loads them in offset order, so the
* .s byte-diverged (both ran correct). The 16B tagged_field row above
* is too small to fire the R8/CX split. */
"package main;\n"
"type S = struct { a: i64, t: (i64 | []u8) };\n"
"let gp: *S = nil;\n"
"export fn main() i32 = { let s: S = S{a=0,t=3i64}; gp = &s;"
" let v: i32 = 0; match (gp.t) { case let x: i64 => v = x: i32;"
" case let y: []u8 => v = len(y): i32; }; return v; };\n", 3 },
{ "tagged_slice_field_local",
/* #17 twin: same 32B slice-payload tagged read, but via a LOCAL
* *struct ptr. cgptrfieldload is shared by the local (MOVQ off(BP),BX)
* and global (MOVQ name(SB),BX) direct *struct-ptr arms, so the same
* cxlast=false fix closes both — this row locks the local path against
* a future regression of the R8/CX order. */
"package main;\n"
"type S = struct { a: i64, t: (i64 | []u8) };\n"
"export fn main() i32 = { let s: S = S{a=0,t=3i64}; let p: *S = &s;"
" let v: i32 = 0; match (p.t) { case let x: i64 => v = x: i32;"
" case let y: []u8 => v = len(y): i32; }; return v; };\n", 3 },
{ "pslice_len",
"package main;\n"
"let gp: *[]u8 = nil;\n"