/* * 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 * size + base-materialisation off the base's tnode KIND, enumerating only * N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode * N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz * stayed at the default 8 and the base fell to the wide-header fallback — * it materialised the full {ptr,len,cap} header and indexed with an 8-byte * stride + a full-word MOVQ load. So `s[1]` over a global str read 8 bytes * at ptr+8 instead of the single byte at ptr+1 (cstage emits MOVZBQ). * cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the RESOLVED * base TYPE (`idx_eff(lhs->type)->sub->size`), uniform across local/global/ * str/slice/ptr — so it was already correct. LOCAL str/slice index was also * already byte-id-clean (esz resolves off the local's tnode). * * THE FIX (#10): align cgindex's global-resolution arm UP to cstage's uniform * type-driven dispatch — the same template the sister fn `cgslice` already * uses (generic globaltn, esz = elemsizeofc(c, tn), base = N_TARRAY ? LEAQ : * MOVQ name(SB)). A global str/slice now resolves esz=1 off the type table * and routes through the existing isglobalptr emission (MOVQ name(SB),BX; * ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage. * * EACH ROW CARRIES BOTH DIMENSIONS (802 model): * (a) cstage `ww build` + run, asserting the exit — pins that the converged * asm reads the correct element value, not a ptr word. * (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (rule-10). A * stride-8 regression of the fix re-diverges wwstage from cstage here. * * BYTE-ID SCOPE PER ROW: * Most rows compare the FULL `.s`. The global-slice row compares the TEXT * section only (lines before the first DATA/GLOBL directive): a bare * module-level `let g: []u8;` decl emits a divergent zero-header DATAW in * wwstage that cstage omits — a SEPARATE, pre-existing data-emission gap * (module-level slice static-init, task #7/#18 family) orthogonal to the * index read. The TEXT comparison still pins the index codegen exactly * (where a stride-8 regression manifests: MOVZBQ vs IMULQ $8 + MOVQ), so * the #10 fix is fully gated; only the unrelated DATA noise is excluded. * (Same kind of orthogonal block 802 documents for its slice rows.) * * GATE POLARITY: must stay GREEN. A wrong exit means a global str/slice index * regressed back to a ptr-word read; a byte-id FAIL means the stages diverged. */ #include #include #include #include #include #include static int runwait(const char *cmd) { int rc = system(cmd); if (rc == -1) return -1; if (WIFEXITED(rc)) return WEXITSTATUS(rc); return -1; } struct row { const char *label; const char *src; int want_exit; int text_only; }; static const struct row rows[] = { /* the exact #10 repro: a global str "ABC", s[1] == 'B' == 66. * Pre-fix this read an 8-byte word at ptr+8 (wide-header stride). */ { "gstr_mid", "package main;\n" "let s: str = \"ABC\";\n" "export fn main() i32 = { return s[1]: i32; };\n", 66, 0 }, /* first element s[0] == 'A' == 65 — guards an off-by-one in the * index scale (a wide stride would land elsewhere). */ { "gstr_first", "package main;\n" "let s: str = \"ABC\";\n" "export fn main() i32 = { return s[0]: i32; };\n", 65, 0 }, /* last element s[2] == 'C' == 67. */ { "gstr_last", "package main;\n" "let s: str = \"ABC\";\n" "export fn main() i32 = { return s[2]: i32; };\n", 67, 0 }, /* two global-str indices summed (65 + 66 == 131) — pins the byte * load width: a full-word load would carry the high bytes. */ { "gstr_sum", "package main;\n" "let s: str = \"ABC\";\n" "export fn main() i32 = { return s[0]: i32 + s[1]: i32; };\n", 131, 0 }, /* global slice index: g[1] == 20. The bare `let g: []u8;` decl emits * a divergent zero-header DATAW (orthogonal #7/#18 data gap), so this * row is TEXT-only byte-id; cstage runtime pins the value, TEXT-id * pins the index read. g is assigned at runtime to dodge the SEPARATE * module-level slice-literal static-init gap (which fails to link in * BOTH stages identically). */ { "gslice_mid", "package main;\n" "let g: []u8;\n" "export fn main() i32 = {\n" " let a: [3]u8 = [10u8, 20u8, 30u8];\n" " g = a;\n" " return g[1]: i32;\n" "};\n", 20, 1 }, /* global slice with a WIDTH>1, SIGNED element: g[1] - g[0] == 20 - (-5) * == 25. Pins that the global path resolves esz=4 off the type table * (not the default 8) AND sign-extends (MOVSXD) — a stride-8 regression * re-diverges the TEXT (IMULQ $8 + MOVQ word vs IMULQ $4 + MOVSXD). Same * bare-decl DATAW data-gap as gslice_mid, so TEXT-only. */ { "gislice_mid", "package main;\n" "let g: []i32;\n" "export fn main() i32 = {\n" " let a: [3]i32 = [-5, 20, 30];\n" " 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", "package main;\n" "export fn main() i32 = { let s: str = \"ABC\"; return s[1]: i32; };\n", 66, 0 }, /* REGRESSION PIN: local slice index (already clean). g[2] == 30. */ { "lslice_last", "package main;\n" "export fn main() i32 = {\n" " let a: [3]u8 = [10u8, 20u8, 30u8];\n" " let g: []u8 = a;\n" " return g[2]: i32;\n" "};\n", 30, 0 }, /* REGRESSION PIN: local array index (already clean). a[1] == 20. */ { "larr_mid", "package main;\n" "export fn main() i32 = {\n" " let a: [3]u8 = [10u8, 20u8, 30u8];\n" " return a[1]: i32;\n" "};\n", 20, 0 }, { NULL, NULL, 0, 0 } }; /* A directive line that begins the DATA/GLOBL section. The TEXT-only * compare stops at the first such line (the index codegen lives entirely * in the TEXT segment above it). */ static int isdataline(const char *ln) { return strncmp(ln, "DATA", 4) == 0 || strncmp(ln, "GLOBL", 5) == 0; } /* Byte-compare two .s files. With text_only, both files are truncated at * the first DATA/GLOBL line before comparison (orthogonal data-section * divergence excluded; see the BYTE-ID SCOPE note above). */ static int asm_eq(const char *a, const char *b, int text_only) { FILE *fa = fopen(a, "rb"); FILE *fb = fopen(b, "rb"); if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } int rc = 0; char la[4096], lb[4096]; for (;;) { char *ra = fgets(la, sizeof la, fa); char *rb = fgets(lb, sizeof lb, fb); if (text_only && ra && isdataline(la)) ra = NULL; if (text_only && rb && isdataline(lb)) rb = NULL; if (ra == NULL && rb == NULL) break; if (ra == NULL || rb == NULL) { rc = -1; break; } if (strcmp(la, lb) != 0) { rc = -1; break; } } fclose(fa); fclose(fb); return rc; } int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char absbin[1024]; if (bin[0] != '/') { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); bin = absbin; } char w6c[1100], w6c_ww[1100]; snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); if (access(w6c_ww, X_OK) != 0) { fprintf(stderr, "globalidx: w6c_ww missing — cannot run the " "cs==ww byte-id gate\n"); return 1; } int n = 0, fail = 0; for (int i = 0; rows[i].src; i++, n++) { char src[64]; snprintf(src, sizeof src, "/tmp/wwgi_%d_%d.ww", getpid(), i); FILE *f = fopen(src, "wb"); if (f == NULL) { fail++; continue; } fputs(rows[i].src, f); fclose(f); /* (a) cstage build + run in a scratch dir. */ char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwgi_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); char cmd[2048]; snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", tmpdir, bin, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: cstage build failed\n", rows[i].label); fail++; unlink(src); rmdir(tmpdir); continue; } char outbin[128]; const char *base = strrchr(src, '/'); base = base ? base + 1 : src; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; int got = runwait(outbin); if (got != rows[i].want_exit) { fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", rows[i].label, got, rows[i].want_exit); fail++; } unlink(outbin); rmdir(tmpdir); /* (b) cs==ww byte-id gate. */ char cs_s[64], ws_s[64]; snprintf(cs_s, sizeof cs_s, "/tmp/wwgi_%d_%d_cs.s", getpid(), i); snprintf(ws_s, sizeof ws_s, "/tmp/wwgi_%d_%d_ww.s", getpid(), i); snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); fail++; unlink(src); continue; } snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c_ww failed\n", rows[i].label); fail++; unlink(src); unlink(cs_s); continue; } if (asm_eq(cs_s, ws_s, rows[i].text_only) != 0) { fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER%s " "(rule-10 byte-id violation)\n", rows[i].label, rows[i].text_only ? " (TEXT section)" : ""); fail++; } unlink(src); unlink(cs_s); unlink(ws_s); } if (fail) { fprintf(stderr, "%d/%d globalidx tests failed\n", fail, n); return 1; } printf("globalidx: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n); return 0; }