From d6ea497da1baffb0c4a82b701f45be1924030d1f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 24 Jun 2026 00:07:24 +0900 Subject: [PATCH] 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. --- selfhost/cmd/wcc/cgenexpr.ww | 15 ++++++---- selfhost/cmd/wcc/cgenutil.ww | 42 +++++++++++++++++++-------- test/wcc/689_globptr_field_read_run.c | 24 +++++++++++++++ 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1fd175e9..60b27f42 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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} diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index c667aab0..5450fea2 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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"); + }; }; }; diff --git a/test/wcc/689_globptr_field_read_run.c b/test/wcc/689_globptr_field_read_run.c index eaac96be..252e82c9 100644 --- a/test/wcc/689_globptr_field_read_run.c +++ b/test/wcc/689_globptr_field_read_run.c @@ -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"