diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b5d00b28..41511ce4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -23170,17 +23170,26 @@ fn cgun(c: *cgen, n: *node) void = { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; } else { + // #11: addr-of twin of the #10 cgindex read + // fix. Dispatch esz + base load off the + // global's RESOLVED type, NOT an N_TARRAY/ + // N_TPTR kind whitelist — a global str (tnode + // N_TNAME) / slice (N_TSLICE) matched NEITHER + // old arm, so esz stayed at the default 8 and + // the base fell to the complex-base fallback, + // yielding a wide-stride &s[i]. cstage's + // TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform: + // esz=bu->sub->size, base is_arr?LEAQ:MOVQ + // name(SB) (a str/slice's .ptr IS the symbol's + // first word). Align UP, mirroring cgindex. let tn: *node = letvartnode(c, base.str); if (tn != nil) { + globalname = base.str; + esz = elemsizeofc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; - globalname = base.str; - esz = elemsizeofc(c, tn); - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = base.str; - esz = elemsizeofc(c, tn); }; }; }; @@ -24797,19 +24806,34 @@ fn cgassign(c: *cgen, n: *node) void = { if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { + // #11: store/compound twin of the #10 cgindex + // read fix. A global str/slice element store hit + // the same kind whitelist — N_TNAME (str) / + // N_TSLICE matched NEITHER arm, so esz stayed 8 + // and the store emitted a full-word MOVQ — an + // 8-byte OUT-OF-BOUNDS write past a 1-byte + // element — instead of MOVB. cstage + // (cmd/w6c/cgen.c N_INDEX store) dispatches esz + // off idx_eff->sub->size + the elem-kind flags + // off eff->sub uniformly, base is_arr?LEAQ:MOVQ + // name(SB). Align UP and resolve elemtn exactly + // like the local branch above (element node for + // ARRAY/SLICE/PTR; nil for str so tnodestoreop + // picks MOVB on the store arm, and the compound + // arm's str/slice hard-error still fires on a + // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { - if (tn.kind == nkind.N_TARRAY) { + globalname = bn; + esz = elemsizeofc(c, tn); + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); elemtn = tn.lhs; - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - elemtn = tn.lhs; + if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = tn.lhs; }; }; }; }; @@ -25229,19 +25253,34 @@ fn cgassign(c: *cgen, n: *node) void = { if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { + // #11: store/compound twin of the #10 cgindex + // read fix. A global str/slice element store hit + // the same kind whitelist — N_TNAME (str) / + // N_TSLICE matched NEITHER arm, so esz stayed 8 + // and the store emitted a full-word MOVQ — an + // 8-byte OUT-OF-BOUNDS write past a 1-byte + // element — instead of MOVB. cstage + // (cmd/w6c/cgen.c N_INDEX store) dispatches esz + // off idx_eff->sub->size + the elem-kind flags + // off eff->sub uniformly, base is_arr?LEAQ:MOVQ + // name(SB). Align UP and resolve elemtn exactly + // like the local branch above (element node for + // ARRAY/SLICE/PTR; nil for str so tnodestoreop + // picks MOVB on the store arm, and the compound + // arm's str/slice hard-error still fires on a + // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { - if (tn.kind == nkind.N_TARRAY) { + globalname = bn; + esz = elemsizeofc(c, tn); + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); elemtn = tn.lhs; - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - elemtn = tn.lhs; + if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = tn.lhs; }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 56a21e32..feb4dac9 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3446,17 +3446,26 @@ fn cgun(c: *cgen, n: *node) void = { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; } else { + // #11: addr-of twin of the #10 cgindex read + // fix. Dispatch esz + base load off the + // global's RESOLVED type, NOT an N_TARRAY/ + // N_TPTR kind whitelist — a global str (tnode + // N_TNAME) / slice (N_TSLICE) matched NEITHER + // old arm, so esz stayed at the default 8 and + // the base fell to the complex-base fallback, + // yielding a wide-stride &s[i]. cstage's + // TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform: + // esz=bu->sub->size, base is_arr?LEAQ:MOVQ + // name(SB) (a str/slice's .ptr IS the symbol's + // first word). Align UP, mirroring cgindex. let tn: *node = letvartnode(c, base.str); if (tn != nil) { + globalname = base.str; + esz = elemsizeofc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; - globalname = base.str; - esz = elemsizeofc(c, tn); - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = base.str; - esz = elemsizeofc(c, tn); }; }; }; @@ -5073,19 +5082,34 @@ fn cgassign(c: *cgen, n: *node) void = { if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { + // #11: store/compound twin of the #10 cgindex + // read fix. A global str/slice element store hit + // the same kind whitelist — N_TNAME (str) / + // N_TSLICE matched NEITHER arm, so esz stayed 8 + // and the store emitted a full-word MOVQ — an + // 8-byte OUT-OF-BOUNDS write past a 1-byte + // element — instead of MOVB. cstage + // (cmd/w6c/cgen.c N_INDEX store) dispatches esz + // off idx_eff->sub->size + the elem-kind flags + // off eff->sub uniformly, base is_arr?LEAQ:MOVQ + // name(SB). Align UP and resolve elemtn exactly + // like the local branch above (element node for + // ARRAY/SLICE/PTR; nil for str so tnodestoreop + // picks MOVB on the store arm, and the compound + // arm's str/slice hard-error still fires on a + // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { - if (tn.kind == nkind.N_TARRAY) { + globalname = bn; + esz = elemsizeofc(c, tn); + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); elemtn = tn.lhs; - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - elemtn = tn.lhs; + if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = tn.lhs; }; }; }; }; @@ -5505,19 +5529,34 @@ fn cgassign(c: *cgen, n: *node) void = { if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { + // #11: store/compound twin of the #10 cgindex + // read fix. A global str/slice element store hit + // the same kind whitelist — N_TNAME (str) / + // N_TSLICE matched NEITHER arm, so esz stayed 8 + // and the store emitted a full-word MOVQ — an + // 8-byte OUT-OF-BOUNDS write past a 1-byte + // element — instead of MOVB. cstage + // (cmd/w6c/cgen.c N_INDEX store) dispatches esz + // off idx_eff->sub->size + the elem-kind flags + // off eff->sub uniformly, base is_arr?LEAQ:MOVQ + // name(SB). Align UP and resolve elemtn exactly + // like the local branch above (element node for + // ARRAY/SLICE/PTR; nil for str so tnodestoreop + // picks MOVB on the store arm, and the compound + // arm's str/slice hard-error still fires on a + // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { - if (tn.kind == nkind.N_TARRAY) { + globalname = bn; + esz = elemsizeofc(c, tn); + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); elemtn = tn.lhs; - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - elemtn = tn.lhs; + if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = tn.lhs; }; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 5a872731..675a6596 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -23170,17 +23170,26 @@ fn cgun(c: *cgen, n: *node) void = { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; } else { + // #11: addr-of twin of the #10 cgindex read + // fix. Dispatch esz + base load off the + // global's RESOLVED type, NOT an N_TARRAY/ + // N_TPTR kind whitelist — a global str (tnode + // N_TNAME) / slice (N_TSLICE) matched NEITHER + // old arm, so esz stayed at the default 8 and + // the base fell to the complex-base fallback, + // yielding a wide-stride &s[i]. cstage's + // TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform: + // esz=bu->sub->size, base is_arr?LEAQ:MOVQ + // name(SB) (a str/slice's .ptr IS the symbol's + // first word). Align UP, mirroring cgindex. let tn: *node = letvartnode(c, base.str); if (tn != nil) { + globalname = base.str; + esz = elemsizeofc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; - globalname = base.str; - esz = elemsizeofc(c, tn); - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = base.str; - esz = elemsizeofc(c, tn); }; }; }; @@ -24797,19 +24806,34 @@ fn cgassign(c: *cgen, n: *node) void = { if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { + // #11: store/compound twin of the #10 cgindex + // read fix. A global str/slice element store hit + // the same kind whitelist — N_TNAME (str) / + // N_TSLICE matched NEITHER arm, so esz stayed 8 + // and the store emitted a full-word MOVQ — an + // 8-byte OUT-OF-BOUNDS write past a 1-byte + // element — instead of MOVB. cstage + // (cmd/w6c/cgen.c N_INDEX store) dispatches esz + // off idx_eff->sub->size + the elem-kind flags + // off eff->sub uniformly, base is_arr?LEAQ:MOVQ + // name(SB). Align UP and resolve elemtn exactly + // like the local branch above (element node for + // ARRAY/SLICE/PTR; nil for str so tnodestoreop + // picks MOVB on the store arm, and the compound + // arm's str/slice hard-error still fires on a + // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { - if (tn.kind == nkind.N_TARRAY) { + globalname = bn; + esz = elemsizeofc(c, tn); + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); elemtn = tn.lhs; - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - elemtn = tn.lhs; + if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = tn.lhs; }; }; }; }; @@ -25229,19 +25253,34 @@ fn cgassign(c: *cgen, n: *node) void = { if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { + // #11: store/compound twin of the #10 cgindex + // read fix. A global str/slice element store hit + // the same kind whitelist — N_TNAME (str) / + // N_TSLICE matched NEITHER arm, so esz stayed 8 + // and the store emitted a full-word MOVQ — an + // 8-byte OUT-OF-BOUNDS write past a 1-byte + // element — instead of MOVB. cstage + // (cmd/w6c/cgen.c N_INDEX store) dispatches esz + // off idx_eff->sub->size + the elem-kind flags + // off eff->sub uniformly, base is_arr?LEAQ:MOVQ + // name(SB). Align UP and resolve elemtn exactly + // like the local branch above (element node for + // ARRAY/SLICE/PTR; nil for str so tnodestoreop + // picks MOVB on the store arm, and the compound + // arm's str/slice hard-error still fires on a + // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { - if (tn.kind == nkind.N_TARRAY) { + globalname = bn; + esz = elemsizeofc(c, tn); + let bk: nkind = tn.kind; + if (bk == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); elemtn = tn.lhs; - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - elemtn = tn.lhs; + if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = tn.lhs; }; }; }; }; diff --git a/test/wcc/803_globalidx_run.c b/test/wcc/803_globalidx_run.c index a2b984ed..7061a101 100644 --- a/test/wcc/803_globalidx_run.c +++ b/test/wcc/803_globalidx_run.c @@ -1,7 +1,13 @@ /* - * 803_globalidx_run — BUG #10. Runtime + cs==ww byte-id net for INDEXING - * a GLOBAL `str` / GLOBAL slice (`s[i]` / `g[i]` where s/g are module-level - * lets). + * 803_globalidx_run — BUG #10 + #11. Runtime + cs==ww byte-id net for + * the GLOBAL `str` / GLOBAL slice index family (`s/g` are module-level lets): + * #10 — the READ `s[i]` / `g[i]` (cgindex). + * #11 — the ADDR-OF `&s[i]` / `&g[i]` (cgun) and the STORE `g[i] = v` + * (cgassign). Same root as #10 (the N_TARRAY/N_TPTR kind whitelist + * in the global-resolution arm), three more sibling arms; the store + * miscompile is an 8-byte OUT-OF-BOUNDS write (full-word MOVQ instead + * of MOVB), so the store rows below also assert ADJACENT elements stay + * uncorrupted. * * THE BUG (wwstage WRONG, cstage correct — a rule-10 divergence): * wwstage `cgindex` (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element @@ -113,6 +119,119 @@ static const struct row rows[] = { " g = a;\n" " return g[1] - g[0];\n" "};\n", 25, 1 }, + /* #11 ADDR-OF, global str: &s[1] then read the byte through the + * pointer == 'B' == 66. Pre-#11 cgun materialised the wide {ptr,len, + * cap} header + an 8-byte stride (IMULQ $8 + LEAQ s(SB)) instead of + * MOVQ s(SB) (.ptr) + ADDQ; FULL byte-id (string-literal DATA is + * stage-identical, same as the gstr READ rows). */ + { "gstr_addr", + "package main;\n" + "let s: str = \"ABC\";\n" + "export fn main() i32 = { let p: *u8 = &s[1]; return (*p): i32; };\n", + 66, 0 }, + /* #11 ADDR-OF, global slice []u8: &g[1] read back == 20. Bare + * `let g: []u8;` decl → orthogonal zero-header DATAW, so TEXT-only + * (same data gap as gslice_mid; the addr codegen is all TEXT). */ + { "gslice_addr", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " g = a;\n" + " let p: *u8 = &g[1];\n" + " return (*p): i32;\n" + "};\n", 20, 1 }, + /* #11 ADDR-OF, global slice WIDTH>1: &g[1] over []i32, *p == 20. + * Pins esz=4 stride on the addr path — a stride-8 regression lands + * &g[2] (*p == 30) and re-diverges the TEXT (IMULQ $4 vs IMULQ $8). + * Same bare-decl DATAW gap → TEXT-only. */ + { "gislice_addr", + "package main;\n" + "let g: []i32;\n" + "export fn main() i32 = {\n" + " let a: [3]i32 = [-5, 20, 30];\n" + " g = a;\n" + " let p: *i32 = &g[1];\n" + " return *p;\n" + "};\n", 20, 1 }, + /* #11 STORE, global slice []u8: g[1] = 99, then g[1]+g[0]+g[2] == + * 99 + 10 + 30 == 139. The g[0]/g[2] addends are the OOB-WRITE GUARD: + * pre-#11 the store was a full-word MOVQ at an 8-byte stride, writing + * 99 at ptr+8 (8-byte OOB) and leaving g[1] == 20 (sum 60, AND clobbered + * memory). The fix stores MOVB at ptr+1; adjacent bytes intact. TEXT- + * only (bare-decl DATAW gap). */ + { "gslice_store", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " g = a;\n" + " g[1] = 99u8;\n" + " return g[1]: i32 + g[0]: i32 + g[2]: i32;\n" + "};\n", 139, 1 }, + /* #11 STORE, global slice WIDTH>1 SIGNED: g[1] = 42, then + * g[1]+g[0]+g[2] == 42 + (-5) + 30 == 67. Pins esz=4 store WIDTH + + * stride: a full-word/stride-8 regression writes 8 bytes at ptr+8 + * (OOB) and re-diverges the TEXT (IMULQ $4 + MOVL vs IMULQ $8 + MOVQ). + * g[0]/g[2] are the adjacency guard. TEXT-only. */ + { "gislice_store", + "package main;\n" + "let g: []i32;\n" + "export fn main() i32 = {\n" + " let a: [3]i32 = [-5, 20, 30];\n" + " g = a;\n" + " g[1] = 42;\n" + " return g[1] + g[0] + g[2];\n" + "};\n", 67, 1 }, + /* #11 COMPOUND STORE, global slice []u8: g[1] += 79 → 20+79 == 99, + * then g[1]+g[0]+g[2] == 99+10+30 == 139. Exercises the THIRD fixed + * arm (cgenexpr.ww cgassign n.op != TK_ASSIGN), a distinct code path + * from the plain-store rows above: it load-combines-stores in place. + * Pre-#11 it hit the same kind whitelist → esz=8 + a full-word RMW: an + * 8-byte OOB load AND store at ptr+8, leaving g[1] == 20 (sum 60) and + * clobbering adjacent memory. The g[0]/g[2] addends are the OOB guard. + * TEXT-only (bare-decl DATAW gap). */ + { "gslice_compound", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " g = a;\n" + " g[1] += 79u8;\n" + " return g[1]: i32 + g[0]: i32 + g[2]: i32;\n" + "};\n", 139, 1 }, + /* #11 COMPOUND STORE, global slice WIDTH>1 SIGNED: g[1] += 47 → + * (-5)+47 == 42, then g[1]+g[0]+g[2] == 42+(-25)+30 == 47. Pins the + * compound arm's esz=4 load/store WIDTH + stride on the global path — + * a stride-8/full-word RMW regression reads+writes 8 bytes at ptr+8 + * (OOB) and re-diverges the TEXT (IMULQ $4 + MOVL/MOVSXD vs IMULQ $8 + + * MOVQ). g[0]/g[2] are the adjacency guard. TEXT-only. */ + { "gislice_compound", + "package main;\n" + "let g: []i32;\n" + "export fn main() i32 = {\n" + " let a: [3]i32 = [-25, -5, 30];\n" + " g = a;\n" + " g[1] += 47;\n" + " return g[1] + g[0] + g[2];\n" + "};\n", 47, 1 }, + /* REGRESSION PIN: local str ADDR-OF (already clean pre-#11) — the + * fix must not perturb the local path. &s[1] read back == 66. FULL. */ + { "lstr_addr", + "package main;\n" + "export fn main() i32 = { let s: str = \"ABC\"; let p: *u8 = &s[1]; return (*p): i32; };\n", + 66, 0 }, + /* REGRESSION PIN: local slice STORE (already clean) — g[1] = 99 then + * g[1]+g[0]+g[2] == 139, adjacency intact. FULL byte-id (a local slice + * carries no bare-decl data gap). */ + { "lslice_store", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " let g: []u8 = a;\n" + " g[1] = 99u8;\n" + " return g[1]: i32 + g[0]: i32 + g[2]: i32;\n" + "};\n", 139, 0 }, /* REGRESSION PIN: local str index (already byte-id-clean pre-fix) — * the fix must not perturb the local path. s[1] == 'B' == 66. */ { "lstr_mid",