/* * 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). * * 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 }, /* 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; }