diff --git a/Makefile b/Makefile index b8f2de64..06cb89da 100644 --- a/Makefile +++ b/Makefile @@ -385,7 +385,7 @@ XMOD_WW_TARGETS = $(XMOD_WW_TESTS:%=wwtest/%) # w6c_ww per row — not identity gates — so they run under # test-compiler beside the surviving residual carriers. ASM_WW_TESTS = test/asm/modshadow_test.ww test/asm/sret_test.ww \ - test/asm/callarg_test.ww + test/asm/callarg_test.ww test/asm/chain_test.ww ASM_WW_TARGETS = $(ASM_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ diff --git a/test/asm/chain_test.ww b/test/asm/chain_test.ww new file mode 100644 index 00000000..c99c47d2 --- /dev/null +++ b/test/asm/chain_test.ww @@ -0,0 +1,273 @@ +package chain_test; + +// Direct-w6c asm-window gate over chained-index element dispatch. +// Port of the retired native carriers test/wcc/739_chained_index.c, +// 740_chained_write.c and 741_dotbase_chained.c; every assertion +// preserved. test/lang/chainidx_test.ww owns ARRAY chains only — the +// **T pointer-chain narrow width, the exact stride-scale pair counts +// and the N_DOT-base recursion have no other owner; 740/741 are the +// sole exercisers of their shapes. +// +// 739 (#24, read) / 740 (#27, write): inside [TEXT main.probe .. +// first RET line) the element access must use the narrow MOV +// mnemonic (MOVZBQ/MOVSXD load, MOVB/MOVL store), the u8 write row +// anti-checks the pre-fix 8-byte `MOVQ AX, (BX)`, and the +// `MOVQ $N, CX` .. `IMULQ CX, AX` scale pairs must count EXACTLY +// inner=1 for **u8 (a stray outer scale was the pre-fix wwstage +// emit) and inner=1 outer=1 for **i32. The pair scan is the +// carriers' greedy two-strstr walk, ported positionally. +// +// 741 (#28 + #30): the N_DOT-base chained read must land MOVZBQ +// (esz recursion through dotfieldtnode) and the tagged-array write +// must scale by the 32B element stride; the write row's +// scalar-overwrite divergence net is the byte-id leg, exactly the +// carrier's design (its anti-needle machinery was never armed). +// +// Byte-id legs ride every row as in the C. Dropped C machinery, not +// assertions: w6c_ww-absent skip gates, getpid()-keyed /tmp names, +// slurp caps. The `package main;` prefix reproduces wwtest_fputs. + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("chain FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +fn emitstage(td: str, label: str, stage: str, drv: str, + outname: str) void = { + let av: []str = []; + append(av, drv); + append(av, "-o"); + append(av, outname); + append(av, "src.ww"); + let co: testenv.commandout; + testenv.runcommand(td, td, stage, av, tmo(), &co); + let ok: bool = co.termination == exec.termination.EXIT && co.code == 0; + if (!ok) { fail(label, strings.concat(stage, " compile failed")); }; +}; + +fn posafter(s: str, start: i32, needle: str) i32 = { + let p: i32 = testenv.pos(strings.sub(s, start, s.len), needle); + if (p < 0) { return -1; }; + return start + p; +}; + +// [TEXT main.probe .. first `\tRET\n`), widened to EOF when the RET +// needle is missing — the carriers' probe window. +fn probewindow(label: str, stage: str, s: str) str = { + let fp: i32 = testenv.pos(s, "TEXT main.probe"); + if (fp < 0) { + fail(label, strings.concat(stage, ": no TEXT probe in .s")); + }; + let tail: str = strings.sub(s, fp, s.len); + let rp: i32 = testenv.pos(tail, "\tRET\n"); + if (rp < 0) { return tail; }; + return strings.sub(tail, 0, rp); +}; + +// Greedy `movq` .. `IMULQ CX, AX` pair count (the carriers' +// two-strstr walk: an unpaired movq re-scans from movq+1, a paired +// one advances past its IMULQ). +fn paircount(w: str, movq: str) i32 = { + let n: i32 = 0; + let p: i32 = 0; + for (p < w.len) { + let ip: i32 = posafter(w, p, movq); + if (ip < 0) { break; }; + let nx: i32 = posafter(w, ip, "\tIMULQ\tCX, AX\n"); + if (nx < 0) { p = ip + 1; continue; }; + n += 1; + p = nx + 1; + }; + return n; +}; + +// outerscale 1 -> exactly 1 inner $8 pair and NO outer scale; +// outerscale 4 -> 1 inner $8 pair AND 1 outer $4 pair. +fn scalecheck(label: str, stage: str, w: str, outerscale: i32) void = { + let inner: i32 = paircount(w, "\tMOVQ\t$8, CX\n"); + if (outerscale == 1) { + if (inner != 1) { + fail(label, strings.concat(stage, + ": expected exactly 1 inner `MOVQ $8, CX; IMULQ` ", + "pair (no outer scaling for u8)")); + }; + } else { + let outer: i32 = paircount(w, "\tMOVQ\t$4, CX\n"); + if (inner != 1 || outer != 1) { + fail(label, strings.concat(stage, + ": expected 1 inner `MOVQ $8` + 1 outer `MOVQ $4` ", + "IMULQ pair")); + }; + }; +}; + +// One 739 row: narrow load mnemonic + scale-pair counts + byte-id. +fn chainreadrow(label: str, src: str, loadmov: str, + outerscale: i32) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + let cs: str = testenv.readfile(strings.concat(td, "/cs.s")); + let ws: str = testenv.readfile(strings.concat(td, "/ws.s")); + let needle: str = strings.concat("\t", loadmov, "\t(AX), AX\n"); + let cw: str = probewindow(label, "cstage", cs); + let ww: str = probewindow(label, "wwstage", ws); + if (!testenv.has(cw, needle)) { + fail(label, strings.concat("cstage: expected `", loadmov, + " (AX), AX` in probe body")); + }; + if (!testenv.has(ww, needle)) { + fail(label, strings.concat("wwstage: expected `", loadmov, + " (AX), AX` in probe body")); + }; + scalecheck(label, "cstage", cw, outerscale); + scalecheck(label, "wwstage", ww, outerscale); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +// 739: **T chained read. esz of the outer index must come from the +// inner index's value type, not the default 8. +@test fn chainedindex() void = { + chainreadrow("chained_u8", strings.concat( + "package main;\n", + "fn probe(names: **u8) u8 = {\n", + " let i: i32 = 0;\n", + " let k: u64 = 0u64;\n", + " return names[i][k];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVZBQ", 1); + chainreadrow("chained_i32", strings.concat( + "package main;\n", + "fn probe(mat: **i32) i32 = {\n", + " let i: i32 = 0;\n", + " let k: u64 = 0u64;\n", + " return mat[i][k];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVSXD", 4); +}; + +// One 740 row: narrow store mnemonic, the u8 row's wrong-width-store +// anti-needle, scale-pair counts, byte-id. +fn chainwriterow(label: str, src: str, storemov: str, + outerscale: i32) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + let cs: str = testenv.readfile(strings.concat(td, "/cs.s")); + let ws: str = testenv.readfile(strings.concat(td, "/ws.s")); + let needle: str = strings.concat("\t", storemov, "\tAX, (BX)\n"); + let cw: str = probewindow(label, "cstage", cs); + let ww: str = probewindow(label, "wwstage", ws); + if (!testenv.has(cw, needle)) { + fail(label, strings.concat("cstage: expected `", storemov, + " AX, (BX)` in probe body")); + }; + if (!testenv.has(ww, needle)) { + fail(label, strings.concat("wwstage: expected `", storemov, + " AX, (BX)` in probe body")); + }; + if (outerscale == 1) { + if (testenv.has(cw, "\tMOVQ\tAX, (BX)\n") + || testenv.has(ww, "\tMOVQ\tAX, (BX)\n")) { + fail(label, strings.concat( + "stray 8-byte `MOVQ AX, (BX)` in probe body -- ", + "pre-#27 wrong-width-store regression")); + }; + }; + scalecheck(label, "cstage", cw, outerscale); + scalecheck(label, "wwstage", ww, outerscale); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +// 740: **T chained write, sister of 739 on the cgassign side — the +// wrong-width store corrupts memory adjacent to the u8 slot. +@test fn chainedwrite() void = { + chainwriterow("chained_u8", strings.concat( + "package main;\n", + "fn probe(names: **u8) void = {\n", + " let i: i32 = 0;\n", + " let k: u64 = 0u64;\n", + " names[i][k] = 65u8;\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVB", 1); + chainwriterow("chained_i32", strings.concat( + "package main;\n", + "fn probe(mat: **i32) void = {\n", + " let i: i32 = 0;\n", + " let k: u64 = 0u64;\n", + " mat[i][k] = 42i32;\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVL", 4); +}; + +// One 741 row: dispatch-defining needle in the probe window plus +// byte-id. +fn dotbaserow(label: str, src: str, needle: str) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + let cs: str = testenv.readfile(strings.concat(td, "/cs.s")); + let ws: str = testenv.readfile(strings.concat(td, "/ws.s")); + if (!testenv.has(probewindow(label, "cstage", cs), needle)) { + fail(label, strings.concat("cstage: expected `", needle, + "` in probe body")); + }; + if (!testenv.has(probewindow(label, "wwstage", ws), needle)) { + fail(label, strings.concat("wwstage: expected `", needle, + "` in probe body")); + }; + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +// 741: N_DOT-base chains. The (i64|str) element is 8(tag)+24(str +// payload)=32B, so the write row's stride needle is $32. +@test fn dotbasechained() void = { + dotbaserow("dotbase_chained_read", strings.concat( + "package main;\n", + "type S = struct{ pad: i64, mat: **u8 };\n", + "fn probe(s: *S) u8 = {\n", + " let i: i32 = 0;\n", + " let k: u64 = 0u64;\n", + " return s.mat[i][k];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "\tMOVZBQ\t(AX), AX\n"); + dotbaserow("dotbase_array_tagged_write", strings.concat( + "package main;\n", + "type T = (i64 | str);\n", + "type S = struct{ pad: i64, arr: [4]T };\n", + "fn probe(s: *S) void = {\n", + " let i: i32 = 0;\n", + " s.arr[i] = 42i64;\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "\tMOVQ\t$32, CX\n"); +}; diff --git a/test/wcc/739_chained_index.c b/test/wcc/739_chained_index.c deleted file mode 100644 index c21bd26b..00000000 --- a/test/wcc/739_chained_index.c +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 739_chained_index — sentinel for #24. Pins wwstage's cgindex to - * compute the element size of the outer N_INDEX in `arr[i][k]` from - * the value-type of the inner N_INDEX, instead of defaulting to 8. - * - * Pre-fix wwstage cgindex (selfhost/cmd/wcc/cgenexpr.ww) only - * walked `base.kind == N_IDENT` and `base.kind == N_DOT` for the - * esz/signed_elem dispatch. When base was the inner N_INDEX of a - * chained `names[i][k]` shape (names: **u8), esz stayed at the - * default 8 and the load fell through to MOVQ — an 8-byte load over - * a 1-byte u8 element, plus a stray `MOVQ $8, CX; IMULQ CX, AX` on - * the outer index that cstage doesn't emit. - * - * Cstage walks `n->lhs->type` directly (cmd/w6c/cgen.c idx_eff + - * N_INDEX, ~line 6011) — the typed AST already says the post-inner- - * index value is *u8, so eff->sub->size = 1 lands naturally. Wwstage - * needed the mirror via indexvaluetnode. - * - * Class A wwstage cgen UNDER. Surfaced first time the codebase - * exercised the **T[i][k] shape — through the dir-enum work in - * selfhost/cmd/ww/main.ww `expanddir` (task #22, predecessor commit - * 9e0816e). Workaround there split `names[i][k]` into `let nm: *u8 = - * names[i]; nm[k]` to route through the bare-pointer index path. - * - * Sentinel per row: in the `probe` body, assert the final element - * load uses the expected narrow MOV mnemonic, the outer-index scale - * is absent (esz=1) or matches `MOVQ $, CX`, and the cstage - * vs wwstage asm is byte-identical. - */ -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -/* `loadmov` = expected mnemonic for the trailing ` (AX), AX` - * (or similar) load at the outer index. `outerscale` = expected esz - * for the outer index (1 → no IMULQ; 4 → `MOVQ $4, CX; IMULQ`). */ -struct row { - const char *label; - const char *src; - const char *loadmov; - int outerscale; -}; - -static const struct row rows[] = { - /* The load-bearing case: **u8 chained-index that the dir-enum - * workaround in selfhost/cmd/ww/main.ww `expanddir` had to dodge. */ - { "chained_u8", - "fn probe(names: **u8) u8 = {\n" - " let i: i32 = 0;\n" - " let k: u64 = 0u64;\n" - " return names[i][k];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVZBQ", 1 }, - /* **i32 — 4-byte signed-narrow load on the outer index, IMULQ $4 - * for the outer scaling, IMULQ $8 for the inner *i32 stride. */ - { "chained_i32", - "fn probe(mat: **i32) i32 = {\n" - " let i: i32 = 0;\n" - " let k: u64 = 0u64;\n" - " return mat[i][k];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVSXD", 4 }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/chidx_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/chidx_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - unlink(src); - return rc; -} - -/* Inside `TEXT probe`, the final element load must use the - * expected narrow MOV mnemonic on `(AX), AX`. Pre-fix wwstage - * emitted plain `MOVQ (AX), AX`. */ -static int -check_loadmov(const char *spath, const struct row *r, const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) { - fprintf(stderr, "row[%s][%s]: cannot read %s\n", - r->label, stage, spath); - return -1; - } - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) { - fprintf(stderr, - "row[%s][%s]: no TEXT probe in %s\n", - r->label, stage, spath); - return -1; - } - const char *ret = strstr(fn, "\tRET\n"); - char needle[64]; - snprintf(needle, sizeof needle, "\t%s\t(AX), AX\n", r->loadmov); - const char *m = strstr(fn, needle); - if (!m || (ret && m > ret)) { - fprintf(stderr, - "row[%s][%s]: expected `%s (AX), AX` in probe body\n", - r->label, stage, r->loadmov); - return -1; - } - return 0; -} - -/* Negative: pre-fix wwstage emitted a stray `MOVQ $8, CX; IMULQ CX, - * AX` for the OUTER index of a **u8 chain (because esz defaulted to - * 8). For a u8 outer the correct emit is no scaling at all. Assert - * the count of `MOVQ $8, CX` followed by `IMULQ CX, AX` pairs in the - * probe body matches the expected inner-only count (= 1 for **T; - * the inner index of *T elements always scales by 8). */ -static int -check_inner_scale_only(const char *spath, const struct row *r, - const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) return -1; - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) return -1; - const char *ret = strstr(fn, "\tRET\n"); - if (!ret) ret = fn + strlen(fn); - - int n_inner = 0, n_outer = 0; - const char *p = fn; - while (p < ret) { - const char *inner = strstr(p, "\tMOVQ\t$8, CX\n"); - if (!inner || inner >= ret) break; - const char *next = strstr(inner, "\tIMULQ\tCX, AX\n"); - if (!next || next >= ret) { p = inner + 1; continue; } - n_inner++; - p = next + 1; - } - if (r->outerscale == 1) { - /* Pre-fix wwstage had two `MOVQ $8, CX; IMULQ` pairs (one - * for the outer index that shouldn't scale at all). Cstage - * has one — for the inner *u8 stride only. */ - if (n_inner != 1) { - fprintf(stderr, - "row[%s][%s]: expected exactly 1 inner `MOVQ $8, " - "CX; IMULQ CX, AX` pair (no outer scaling for u8), " - "got %d\n", r->label, stage, n_inner); - return -1; - } - } else { - /* For **i32: inner stride is 8 (sizeof *i32), outer scale is - * 4 (sizeof i32). Assert one `MOVQ $8, CX` for inner and one - * `MOVQ $4, CX` for outer. */ - p = fn; - while (p < ret) { - const char *outer = strstr(p, "\tMOVQ\t$4, CX\n"); - if (!outer || outer >= ret) break; - const char *next = strstr(outer, "\tIMULQ\tCX, AX\n"); - if (!next || next >= ret) { p = outer + 1; continue; } - n_outer++; - p = next + 1; - } - if (n_inner != 1 || n_outer != 1) { - fprintf(stderr, - "row[%s][%s]: expected 1 inner `MOVQ $8` + 1 outer " - "`MOVQ $4` IMULQ pair, got inner=%d outer=%d\n", - r->label, stage, n_inner, n_outer); - return -1; - } - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "chained_index[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total += 2; - if (check_loadmov(cs_path, &rows[i], "cstage") != 0) fail++; - if (check_inner_scale_only(cs_path, &rows[i], "cstage") != 0) - fail++; - - if (!have_ww) { unlink(cs_path); continue; } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "chained_index[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - unlink(cs_path); continue; - } - total += 2; - if (check_loadmov(ws_path, &rows[i], "wwstage") != 0) fail++; - if (check_inner_scale_only(ws_path, &rows[i], "wwstage") != 0) - fail++; - - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "chained_index[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "chained_index: %d/%d fixtures failed\n", fail, total); - return 1; - } - printf("chained_index: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/740_chained_write.c b/test/wcc/740_chained_write.c deleted file mode 100644 index 20cd3ba5..00000000 --- a/test/wcc/740_chained_write.c +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 740_chained_write — sentinel for #27. Pins wwstage's cgassign to - * compute the element size of the outer N_INDEX in `arr[i][k] = v` - * from the value-type of the inner N_INDEX, instead of defaulting - * to 8. Sister of 739_chained_index (#24, read path); same dispatch - * gap on the write side. - * - * Pre-fix wwstage cgassign (selfhost/cmd/wcc/cgenexpr.ww) only - * walked `base.kind == N_IDENT` and `base.kind == N_DOT` for the - * esz/elemtn dispatch on the N_INDEX-lhs branch. When base was the - * inner N_INDEX of a chained `names[i][k]` shape (names: **u8), esz - * stayed at the default 8 and the store fell through to MOVQ — an - * 8-byte write over a 1-byte u8 slot (corrupting adjacent memory), - * plus a stray `MOVQ $8, CX; IMULQ CX, AX` on the outer index that - * cstage doesn't emit. - * - * Cstage walks `n->lhs->type` directly (cmd/w6c/cgen.c N_ASSIGN + - * N_INDEX lhs) — the typed AST already says the post-inner-index - * value is *u8, so eff->sub->size = 1 lands naturally. Wwstage now - * mirrors via indexvaluetnode (already graduated for cgindex in #24). - * - * Class A wwstage cgen UNDER. No in-tree consumer surfaced before - * the fix (selfhost + lib grep is empty for chained-write); the - * filed-latent sister of #24's surfaced bug. This sentinel is the - * sole exerciser of the shape. - * - * Sentinel per row: in the `probe` body, assert the final element - * store uses the expected narrow MOV mnemonic, the outer-index - * scale is absent (esz=1) or matches `MOVQ $, CX`, and the - * cstage vs wwstage asm is byte-identical. - */ -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -/* `storemov` = expected mnemonic for the trailing ` AX, (BX)` - * store at the outer index. `outerscale` = expected esz for the - * outer index (1 → no IMULQ; 4 → `MOVQ $4, CX; IMULQ`). */ -struct row { - const char *label; - const char *src; - const char *storemov; - int outerscale; -}; - -static const struct row rows[] = { - /* The load-bearing case: **u8 chained-write. Sister of 739's - * **u8 read. esz must drop to 1; MOVB store; no outer scale. */ - { "chained_u8", - "fn probe(names: **u8) void = {\n" - " let i: i32 = 0;\n" - " let k: u64 = 0u64;\n" - " names[i][k] = 65u8;\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVB", 1 }, - /* **i32 — 4-byte store, IMULQ $4 outer scaling, IMULQ $8 inner - * *i32 stride. */ - { "chained_i32", - "fn probe(mat: **i32) void = {\n" - " let i: i32 = 0;\n" - " let k: u64 = 0u64;\n" - " mat[i][k] = 42i32;\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVL", 4 }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/chwr_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/chwr_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - unlink(src); - return rc; -} - -/* Inside `TEXT probe`, the final element store must use the - * expected narrow MOV mnemonic on `AX, (BX)`. Pre-fix wwstage - * emitted plain `MOVQ AX, (BX)`. */ -static int -check_storemov(const char *spath, const struct row *r, const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) { - fprintf(stderr, "row[%s][%s]: cannot read %s\n", - r->label, stage, spath); - return -1; - } - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) { - fprintf(stderr, - "row[%s][%s]: no TEXT probe in %s\n", - r->label, stage, spath); - return -1; - } - const char *ret = strstr(fn, "\tRET\n"); - char needle[64]; - snprintf(needle, sizeof needle, "\t%s\tAX, (BX)\n", r->storemov); - const char *m = strstr(fn, needle); - if (!m || (ret && m > ret)) { - fprintf(stderr, - "row[%s][%s]: expected `%s AX, (BX)` in probe body\n", - r->label, stage, r->storemov); - return -1; - } - /* Pre-fix wwstage **u8 write emitted plain `MOVQ AX, (BX)`. For - * the u8 row that's a wrong-width store — anti-check it is - * absent. */ - if (r->outerscale == 1) { - const char *bad = strstr(fn, "\tMOVQ\tAX, (BX)\n"); - if (bad && (!ret || bad < ret)) { - fprintf(stderr, - "row[%s][%s]: stray 8-byte `MOVQ AX, (BX)` in " - "probe body — pre-#27 wrong-width-store regression\n", - r->label, stage); - return -1; - } - } - return 0; -} - -/* Negative: pre-fix wwstage emitted a stray `MOVQ $8, CX; IMULQ CX, - * AX` for the OUTER index of a **u8 chain (because esz defaulted to - * 8). For a u8 outer the correct emit is no scaling at all. Assert - * the count of `MOVQ $8, CX` followed by `IMULQ CX, AX` pairs in the - * probe body matches the expected inner-only count (= 1 for **T; - * the inner index of *T elements always scales by 8). */ -static int -check_inner_scale_only(const char *spath, const struct row *r, - const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) return -1; - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) return -1; - const char *ret = strstr(fn, "\tRET\n"); - if (!ret) ret = fn + strlen(fn); - - int n_inner = 0, n_outer = 0; - const char *p = fn; - while (p < ret) { - const char *inner = strstr(p, "\tMOVQ\t$8, CX\n"); - if (!inner || inner >= ret) break; - const char *next = strstr(inner, "\tIMULQ\tCX, AX\n"); - if (!next || next >= ret) { p = inner + 1; continue; } - n_inner++; - p = next + 1; - } - if (r->outerscale == 1) { - /* Pre-fix wwstage had two `MOVQ $8, CX; IMULQ` pairs (one - * for the outer index that shouldn't scale at all). Cstage - * has one — for the inner *u8 stride only. */ - if (n_inner != 1) { - fprintf(stderr, - "row[%s][%s]: expected exactly 1 inner `MOVQ $8, " - "CX; IMULQ CX, AX` pair (no outer scaling for u8), " - "got %d\n", r->label, stage, n_inner); - return -1; - } - } else { - /* For **i32: inner stride is 8 (sizeof *i32), outer scale is - * 4 (sizeof i32). Assert one `MOVQ $8, CX` for inner and one - * `MOVQ $4, CX` for outer. */ - p = fn; - while (p < ret) { - const char *outer = strstr(p, "\tMOVQ\t$4, CX\n"); - if (!outer || outer >= ret) break; - const char *next = strstr(outer, "\tIMULQ\tCX, AX\n"); - if (!next || next >= ret) { p = outer + 1; continue; } - n_outer++; - p = next + 1; - } - if (n_inner != 1 || n_outer != 1) { - fprintf(stderr, - "row[%s][%s]: expected 1 inner `MOVQ $8` + 1 outer " - "`MOVQ $4` IMULQ pair, got inner=%d outer=%d\n", - r->label, stage, n_inner, n_outer); - return -1; - } - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "chained_write[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total += 2; - if (check_storemov(cs_path, &rows[i], "cstage") != 0) fail++; - if (check_inner_scale_only(cs_path, &rows[i], "cstage") != 0) - fail++; - - if (!have_ww) { unlink(cs_path); continue; } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "chained_write[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - unlink(cs_path); continue; - } - total += 2; - if (check_storemov(ws_path, &rows[i], "wwstage") != 0) fail++; - if (check_inner_scale_only(ws_path, &rows[i], "wwstage") != 0) - fail++; - - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "chained_write[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "chained_write: %d/%d fixtures failed\n", fail, total); - return 1; - } - printf("chained_write: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/741_dotbase_chained.c b/test/wcc/741_dotbase_chained.c deleted file mode 100644 index b002d2b5..00000000 --- a/test/wcc/741_dotbase_chained.c +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 741_dotbase_chained — sentinel for #28 + #30. Pins wwstage's - * `indexvaluetnode` to handle N_DOT base (graduates the helper #24 - * introduced for chained N_INDEX so the N_DOT base shape rides the - * same path). - * - * Two sister latents, one helper graduation: - * - * Read (#28): `obj.mat[i][k]` where obj is a struct with field - * `mat: **u8`. The outer N_INDEX's base is the inner N_INDEX, - * whose base is N_DOT. cgindex routes through `indexvaluetnode` - * for the inner base type; pre-fix `indexvaluetnode` only handled - * N_IDENT + N_INDEX bases, so the recursion bottomed out at the - * N_DOT base with bt=nil. esz fell through to 8 and signed_elem - * to false — wwstage emitted a stray outer `MOVQ $8, CX; IMULQ - * CX, AX` plus `MOVQ (AX), AX` (8-byte read over a 1-byte u8) - * instead of cstage's bare `MOVZBQ (AX), AX`. - * - * Write (#30): `obj.arr[i] = v` where obj is a struct with field - * `arr: [N]Tagged` (e.g. (i64|str)). cgassign's N_DOT-base arm - * computed esz via `indexbaseesz` but never set `elemtn`, so the - * tagged-element store gate (keyed on elemtn) missed and the - * 24-byte tagged slot was overwritten by a single MOVQ scalar - * store — wrong-width store + tag/payload junk in the upper - * 16 bytes. The matching scanlocals pre-pass arm (cgendecl.ww) - * was also missing N_DOT base, so post-elemtn-fix the @tagscr - * slot allocated past the frame boundary and clobbered live - * locals (s, i). - * - * Cstage walks `n->lhs->type` directly via the typed AST (cmd/w6c/ - * cgen.c idx_eff + the N_INDEX-lhs N_ASSIGN branch). Wwstage - * mirrors via `indexvaluetnode`, which #24 introduced for the - * N_INDEX-base case and #28/#30 now graduate for N_DOT base via - * `dotfieldtnode` lookup. - * - * Class A wwstage cgen UNDER. No in-tree consumer; sister latents - * filed during #24 + #27 reviews (commits aa8ca47, 3ba1922). This - * sentinel is the sole exerciser of the shape. - * - * Per row: in the `probe` body, assert the dispatch-defining MOV - * mnemonic + scaling is present, plus cstage vs wwstage asm is - * byte-identical. - */ -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -/* `needle` = a substring that must appear in the probe body and - * uniquely identifies the post-fix dispatch (narrow MOV mnemonic - * for read, 24B element scale for tagged write). - * `antineedle` = a substring that MUST NOT appear — pre-fix wwstage - * regression marker. Empty string skips the anti-check (write row - * relies on cmp -s byte-id to catch divergence, since the tagged- - * store byte-copy legitimately contains MOVQ-store fragments). */ -struct row { - const char *label; - const char *src; - const char *needle; - const char *antineedle; -}; - -static const struct row rows[] = { - /* Read: chained `obj.mat[i][k]` where mat: **u8. Post-fix wwstage - * routes the inner N_INDEX's N_DOT base through indexvaluetnode → - * dotfieldtnode, esz collapses to 1, outer load becomes MOVZBQ. - * Pre-fix had stray `MOVQ (AX), AX` (8-byte read over u8). */ - { "dotbase_chained_read", - "type S = struct{ pad: i64, mat: **u8 };\n" - "fn probe(s: *S) u8 = {\n" - " let i: i32 = 0;\n" - " let k: u64 = 0u64;\n" - " return s.mat[i][k];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "\tMOVZBQ\t(AX), AX\n", - "" }, - /* Write: `obj.arr[i] = v` where arr: [N](i64|str) — tagged element. - * Post-fix wwstage: cgassign N_DOT arm sets elemtn → tagged-store - * path → IMULQ (stride) + byte-copy from scratch. Scanlocals N_DOT - * arm pre-reserves @tagscr in the frame. Pre-fix: scalar `MOVQ AX, - * (BX)` over the slot. - * #1/Phase 3: str IS []u8 (24B), so the (i64|str) slot is - * 8(tag)+24(str payload)=32B — stride is $32, not the 16B-world - * $24. Byte-identical across stages. */ - { "dotbase_array_tagged_write", - "type T = (i64 | str);\n" - "type S = struct{ pad: i64, arr: [4]T };\n" - "fn probe(s: *S) void = {\n" - " let i: i32 = 0;\n" - " s.arr[i] = 42i64;\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "\tMOVQ\t$32, CX\n", - "" }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/dotbase_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/dotbase_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - unlink(src); - return rc; -} - -/* Inside `TEXT probe`, the needle must appear and the antineedle - * must NOT appear (anti-regression on pre-fix wwstage emit). */ -static int -check_probe(const char *spath, const struct row *r, const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) { - fprintf(stderr, "row[%s][%s]: cannot read %s\n", - r->label, stage, spath); - return -1; - } - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) { - fprintf(stderr, - "row[%s][%s]: no TEXT probe in %s\n", - r->label, stage, spath); - return -1; - } - const char *ret = strstr(fn, "\tRET\n"); - const char *m = strstr(fn, r->needle); - if (!m || (ret && m > ret)) { - fprintf(stderr, - "row[%s][%s]: expected `%s` in probe body\n", - r->label, stage, r->needle); - return -1; - } - if (r->antineedle[0] != '\0') { - const char *bad = strstr(fn, r->antineedle); - if (bad && (!ret || bad < ret)) { - fprintf(stderr, - "row[%s][%s]: stray `%s` in probe body — " - "pre-fix wwstage regression\n", - r->label, stage, r->antineedle); - return -1; - }; - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "dotbase_chained[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_probe(cs_path, &rows[i], "cstage") != 0) fail++; - - if (!have_ww) { unlink(cs_path); continue; } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "dotbase_chained[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - unlink(cs_path); continue; - } - total++; - if (check_probe(ws_path, &rows[i], "wwstage") != 0) fail++; - - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "dotbase_chained[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "dotbase_chained: %d/%d fixtures failed\n", fail, total); - return 1; - } - printf("dotbase_chained: %d/%d ok\n", total, total); - return 0; -}