diff --git a/Makefile b/Makefile index da030aa1..b8f2de64 100644 --- a/Makefile +++ b/Makefile @@ -384,7 +384,8 @@ XMOD_WW_TARGETS = $(XMOD_WW_TESTS:%=wwtest/%) # byte-id legs). They observe compiler BEHAVIOR by driving w6c and # 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 +ASM_WW_TESTS = test/asm/modshadow_test.ww test/asm/sret_test.ww \ + test/asm/callarg_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/callarg_test.ww b/test/asm/callarg_test.ww new file mode 100644 index 00000000..900cbd35 --- /dev/null +++ b/test/asm/callarg_test.ww @@ -0,0 +1,331 @@ +package callarg_test; + +// Direct-w6c asm-window gate over caller-side argument lowering. +// Port of the retired native carriers test/wcc/720_tagged_call_arg.c, +// 723_composite_call_arg.c, 727_modcall_widen_slice.c and +// 743_variadic_pack.c; every assertion preserved. Runtime rows are +// owned elsewhere (test/lang/{tagged_call_arg,composite_call_arg} +// _test.ww, lib/strings concat vectors) — the ordered-push windows, +// tag-synth/cleanup needles and store-count floors here are the +// unowned remainder. +// +// 720 (#21): after EVERY CALL main.yield_* site a PUSHQ DX must land +// before the next CALL and precede the following PUSHQ AX (payload +// before tag, natural-push order). The C skipped the 4-variant row's +// byte-id leg behind the then-open #22 !void zero-init gate; #22 +// landed (724's cmp is its fix pin), so that leg is ENABLED here — a +// strengthening over the retired carrier. +// +// 723 (#24): between CALL main.view and the next CALL all three +// slice-composite pushes must appear in strict CX < BX < AX order. +// +// 727 (#28): before CALL needle.want the widened slice arg's +// tag-synth `MOVQ $1, AX` + `PUSHQ AX` pair must appear inside TEXT +// main, and the 7-arg-word stack-overflow cleanup `ADDQ $8, SP` must +// follow the CALL before the next TEXT. +// +// 743 (STATUS-3 #16): the variadic gather in [TEXT main, .. next +// TEXT) must store >= 3 ptr eightbytes (MOVQ AX, -) AND >= 3 len +// eightbytes (MOVQ BX, - — the bug pin; pre-fix cstage stored none). +// +// Dropped C machinery, not assertions: w6c_ww-absent skip gates, +// getpid()-keyed /tmp names, slurp caps. The `package main;` source +// prefix reproduces wwtest_fputs where the carrier injected it. + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("callarg 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")); }; +}; + +// First occurrence of `needle` at or after `start`, absolute; -1 when +// missing (the carriers' find_after). +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; +}; + +// 720: ordered-offset window per yield_ CALL site. +fn pushdxcheck(label: str, stage: str, s: str) void = { + let off: i32 = 0; + let saw: bool = false; + for (off < s.len) { + let call: i32 = posafter(s, off, "CALL\tmain.yield_"); + if (call < 0) { break; }; + saw = true; + let pdx: i32 = posafter(s, call, "PUSHQ\tDX"); + let pax: i32 = posafter(s, call, "PUSHQ\tAX"); + let nextcall: i32 = posafter(s, call + 1, "CALL\t"); + if (pdx < 0 || (nextcall >= 0 && pdx > nextcall)) { + fail(label, strings.concat(stage, + ": no PUSHQ DX between CALL yield_ and next CALL")); + }; + if (pax < 0 || pdx > pax) { + fail(label, strings.concat(stage, + ": PUSHQ DX does not precede PUSHQ AX after CALL ", + "yield_")); + }; + off = call + 1; + }; + if (!saw) { + fail(label, strings.concat(stage, + ": no CALL yield_ site found")); + }; +}; + +fn taggedrow(label: str, src: 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")); + pushdxcheck(label, "cstage", cs); + pushdxcheck(label, "wwstage", ws); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +@test fn taggedcallarg() void = { + taggedrow("4variant_call_source", strings.concat( + "package main;\n", + "type done = !void; type more = !void; type invalid = !void;\n", + "fn yield_rune() (rune | done | more | invalid) = {\n", + " return 65u32: rune;\n", + "};\n", + "fn dispatch(v: (rune | done | more | invalid)) i32 = {\n", + " match (v) {\n", + " case let r: rune => return r: i32;\n", + " case let d: done => return -1;\n", + " case let m: more => return -2;\n", + " case let e: invalid => return -3;\n", + " };\n", + " return -99;\n", + "};\n", + "export fn main() i32 = {\n", + " if (dispatch(yield_rune()) != 65) { return 11; };\n", + " return 0;\n", + "};\n")); + taggedrow("2variant_call_source_ptr", strings.concat( + "package main;\n", + "type oserror = !i32;\n", + "fn yield_ptr() (*u8 | oserror) = {\n", + " let p: *u8 = nil;\n", + " return p;\n", + "};\n", + "fn dispatch(v: (*u8 | oserror)) i32 = {\n", + " match (v) {\n", + " case let p: *u8 => return 7;\n", + " case let e: oserror => return e: i32;\n", + " };\n", + " return -99;\n", + "};\n", + "export fn main() i32 = {\n", + " if (dispatch(yield_ptr()) != 7) { return 11; };\n", + " return 0;\n", + "};\n")); +}; + +// 723: the three slice-composite pushes between CALL view and the +// next CALL, high -> low. +fn threepushcheck(label: str, stage: str, s: str) void = { + let call: i32 = posafter(s, 0, "CALL\tmain.view"); + if (call < 0) { + fail(label, strings.concat(stage, ": no CALL view site")); + }; + let nextcall: i32 = posafter(s, call + 1, "CALL\t"); + if (nextcall < 0) { + fail(label, strings.concat(stage, ": no follow-up CALL")); + }; + let pcx: i32 = posafter(s, call, "PUSHQ\tCX"); + let pbx: i32 = posafter(s, call, "PUSHQ\tBX"); + let pax: i32 = posafter(s, call, "PUSHQ\tAX"); + if (pcx < 0 || pcx > nextcall) { + fail(label, strings.concat(stage, + ": no PUSHQ CX between CALL view and next CALL")); + }; + if (pbx < 0 || pbx > nextcall) { + fail(label, strings.concat(stage, + ": no PUSHQ BX between CALL view and next CALL")); + }; + if (pax < 0 || pax > nextcall) { + fail(label, strings.concat(stage, + ": no PUSHQ AX between CALL view and next CALL")); + }; + if (!(pcx < pbx && pbx < pax)) { + fail(label, strings.concat(stage, + ": PUSHQ order != CX,BX,AX")); + }; +}; + +@test fn compositecallarg() void = { + let label: str = "slice_call_into_2slice_arg"; + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), strings.concat( + "package main;\n", + "fn view(s: str) []u8 = {\n", + " let r: []u8;\n", + " r.ptr = s.ptr;\n", + " r.len = s.len;\n", + " r.cap = s.len;\n", + " return r;\n", + "};\n", + "fn check(a: []u8, b: []u8) bool = {\n", + " if (a.len != b.len) { return false; };\n", + " return true;\n", + "};\n", + "export fn caller(in: str, p: []u8) bool = {\n", + " return check(view(in), p);\n", + "};\n")); + 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")); + threepushcheck(label, "cstage", cs); + threepushcheck(label, "wwstage", ws); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +// 727: tag-synth pair before the CALL ($1 = the slice-variant tag in +// (u8 | []u8)) and the stack-overflow cleanup after it. +fn tagpushcheck(label: str, stage: str, s: str) void = { + let fnp: i32 = testenv.pos(s, "TEXT main"); + if (fnp < 0) { + fail(label, strings.concat(stage, ": no TEXT main in .s")); + }; + let call: i32 = posafter(s, fnp, "CALL\tneedle.want"); + if (call < 0) { + fail(label, strings.concat(stage, ": no CALL needle.want")); + }; + let found: bool = false; + let p: i32 = fnp; + for (p < call) { + let m: i32 = posafter(s, p, "MOVQ\t$1, AX"); + if (m < 0 || m >= call) { break; }; + let nx: i32 = posafter(s, m, "PUSHQ\tAX"); + if (nx >= 0 && nx < call) { found = true; break; }; + p = m + 1; + }; + if (!found) { + fail(label, strings.concat(stage, + ": no tag-synth `MOVQ $1, AX; PUSHQ AX` before CALL ", + "needle.want")); + }; + let cleanup: i32 = posafter(s, call, "ADDQ\t$8, SP"); + let nexttext: i32 = posafter(s, call, "TEXT "); + if (cleanup < 0 || (nexttext >= 0 && cleanup > nexttext)) { + fail(label, strings.concat(stage, + ": no `ADDQ $8, SP` overflow cleanup after CALL ", + "needle.want")); + }; +}; + +@test fn modcallwidenslice() void = { + let label: str = "dot_callee_slice_widen"; + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), strings.concat( + "package needle;\n", + "export fn want(haystack: []u8, needle: (u8 | []u8)) i32 = {\n", + " let r: i32 = haystack.len;\n", + " match (needle) {\n", + " case let b: u8 => r = r + (b: i32);\n", + " case let s: []u8 => r = r + s.len;\n", + " };\n", + " return r;\n", + "};\n", + "package caller;\n", + "import needle;\n", + "export fn main() i32 = {\n", + " let h: []u8;\n", + " let n: []u8 = h;\n", + " return needle.want(h, n);\n", + "};\n")); + 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")); + tagpushcheck(label, "cstage", cs); + tagpushcheck(label, "wwstage", ws); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +// 743: eightbyte store-count floors over the caller gather window. +fn storescheck(label: str, stage: str, s: str) void = { + let body: i32 = testenv.pos(s, "TEXT main,"); + if (body < 0) { + fail(label, strings.concat(stage, ": no TEXT main label")); + }; + let end: i32 = posafter(s, body, "\nTEXT "); + if (end < 0) { end = s.len; }; + let w: str = strings.sub(s, body, end); + if (testenv.occurrences(w, "MOVQ\tBX, -") < 3) { + fail(label, strings.concat(stage, + ": fewer than 3 len-stores (MOVQ BX, -K(BP)) in main")); + }; + if (testenv.occurrences(w, "MOVQ\tAX, -") < 3) { + fail(label, strings.concat(stage, + ": fewer than 3 ptr-stores in main")); + }; +}; + +@test fn variadicpack() void = { + let label: str = "variadic_pack"; + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), strings.concat( + "package main;\n", + "fn sumlen(parts: str...) i32 = {\n", + " let z: i32 = 0;\n", + " let i: i32 = 0;\n", + " for (i < parts.len) {\n", + " z += parts[i].len;\n", + " i += 1;\n", + " };\n", + " return z;\n", + "};\n", + "fn main() i32 = {\n", + " return sumlen(\"a\", \"bb\", \"ccc\");\n", + "};\n")); + 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")); + storescheck(label, "cstage", cs); + storescheck(label, "wwstage", ws); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; diff --git a/test/wcc/720_tagged_call_arg.c b/test/wcc/720_tagged_call_arg.c deleted file mode 100644 index d483e696..00000000 --- a/test/wcc/720_tagged_call_arg.c +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 720_tagged_call_arg — sentinel for #21. Asserts wwstage's caller - * emit sequences `PUSHQ DX` *before* `PUSHQ AX` for the (post-CALL) - * tagged-return ABI when the arg is a fn call returning a 2-word - * tagged union. Pre-fix wwstage emitted - * PUSHQ AX - * MOVQ $0, AX - * PUSHQ AX - * dropping the DX (payload) word and substituting widentag (the - * concrete-variant index) in the second push slot. Post-fix the - * natural-push arm pushes (R8 if >24B), (CX if >16B), DX (if >8B), - * AX — matching cstage at cmd/w6c/cgen.c:4373-4387. - * - * Plus a cstage-vs-wwstage byte-id diff for the !void-free shapes - * (rune-only payload variants) — diffs on shapes that include !void - * variants are gated by task #22 (wwstage skips the prologue - * zero-init of the !void let-decl slot), an orthogonal divergence. - * 924_tagged_call_arg_run pins the runtime behaviour; 995_self_rebuild - * pins the global bootstrap byte-id; this row pins the asm shape so a - * future cgen refactor that re-routes the dispatch can't silently - * regress back to the dropped-DX sequence. - */ -#include -#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; -} - -struct row { - const char *label; - const char *src; - int check_byteid; /* 1 iff the row contains no !void variants */ -}; - -static const struct row rows[] = { - /* 4-variant CALL-source. The bug's original repro shape. - * Pre-fix wwstage main emits `PUSHQ AX; MOVQ $0, AX; PUSHQ AX` - * after the CALL — no `PUSHQ DX` anywhere in main. The needle - * search below pins that a PUSHQ DX appears within a window - * after the CALL to yield_rune. byte-id is skipped because the - * !void variants trigger #22. */ - { "4variant_call_source", - "type done = !void; type more = !void; type invalid = !void;\n" - "fn yield_rune() (rune | done | more | invalid) = {\n" - " return 65u32: rune;\n" - "};\n" - "fn dispatch(v: (rune | done | more | invalid)) i32 = {\n" - " match (v) {\n" - " case let r: rune => return r: i32;\n" - " case let d: done => return -1;\n" - " case let m: more => return -2;\n" - " case let e: invalid => return -3;\n" - " };\n" - " return -99;\n" - "};\n" - "export fn main() i32 = {\n" - " if (dispatch(yield_rune()) != 65) { return 11; };\n" - " return 0;\n" - "};\n", - 0 }, - /* 2-variant (*u8 | oserror): pure-scalar variants, no !void — - * #22 doesn't trigger so byte-id between stages is asserted. */ - { "2variant_call_source_ptr", - "type oserror = !i32;\n" - "fn yield_ptr() (*u8 | oserror) = {\n" - " let p: *u8 = nil;\n" - " return p;\n" - "};\n" - "fn dispatch(v: (*u8 | oserror)) i32 = {\n" - " match (v) {\n" - " case let p: *u8 => return 7;\n" - " case let e: oserror => return e: i32;\n" - " };\n" - " return -99;\n" - "};\n" - "export fn main() i32 = {\n" - " if (dispatch(yield_ptr()) != 7) { return 11; };\n" - " return 0;\n" - "};\n", - 1 }, -}; - -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; -} - -/* Find the first occurrence of `needle` in `buf` after `start`. - * Returns the offset relative to buf, or -1 if missing. */ -static long -find_after(const char *buf, long start, const char *needle) -{ - const char *p = strstr(buf + start, needle); - if (!p) return -1; - return (long)(p - buf); -} - -/* Assert: after every `CALL\tyield_` site, the *next* PUSHQ - * instruction's operand is DX, not AX, and a subsequent PUSHQ AX - * lands the tag (per the high-→-low convention). The pre-fix - * sequence had PUSHQ AX immediately after the CALL with no - * PUSHQ DX in between. */ -static int -check_pushdx_before_pushax(const char *spath, const struct row *r) -{ - char buf[1 << 16]; - if (slurp(spath, buf, sizeof buf) < 0) return -1; - - long off = 0; - int saw_site = 0; - for (;;) { - long call = find_after(buf, off, "CALL\tmain.yield_"); - if (call < 0) break; - saw_site = 1; - long pdx = find_after(buf, call, "PUSHQ\tDX"); - long pax = find_after(buf, call, "PUSHQ\tAX"); - long nextcall = find_after(buf, call + 1, "CALL\t"); - if (pdx < 0 || (nextcall >= 0 && pdx > nextcall)) { - fprintf(stderr, - "row[%s]: no PUSHQ DX between CALL yield_ and next CALL\n", - r->label); - return -1; - } - if (pax < 0 || pdx > pax) { - fprintf(stderr, - "row[%s]: PUSHQ DX does not precede PUSHQ AX after CALL yield_\n", - r->label); - return -1; - } - off = call + 1; - } - if (!saw_site) { - fprintf(stderr, "row[%s]: no CALL yield_ site found\n", r->label); - return -1; - } - return 0; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/tca_asm_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/tca_asm_%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; -} - -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]; - - /* cstage row: presence of PUSHQ DX before PUSHQ AX. */ - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "tagged_call_arg[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_pushdx_before_pushax(cs_path, &rows[i]) != 0) { - fail++; - } - - if (!have_ww) { unlink(cs_path); continue; } - - /* wwstage row: same shape assertion. Pre-fix this is the - * one that fires (cstage was always correct). */ - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "tagged_call_arg[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - unlink(cs_path); - continue; - } - total++; - if (check_pushdx_before_pushax(ws_path, &rows[i]) != 0) { - fail++; - } - - /* Byte-id diff between stages, only for !void-free rows. - * Rows that contain `!void` variants are gated by task #22 - * (wwstage skips the prologue zero-init of the !void slot) - * — that divergence is orthogonal to #21. */ - if (rows[i].check_byteid) { - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", - cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "tagged_call_arg[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "tagged_call_arg: %d/%d fixtures failed\n", fail, total); - return 1; - } - printf("tagged_call_arg: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/723_composite_call_arg.c b/test/wcc/723_composite_call_arg.c deleted file mode 100644 index 680f85f7..00000000 --- a/test/wcc/723_composite_call_arg.c +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 723_composite_call_arg — sentinel for #24. Pins that wwstage emits - * three PUSHQs (CX, BX, AX) after a CALL whose return type is a 3-reg - * composite (`[]u8` slice, ptr/len/cap = AX/BX/CX) when that call's - * result is fed directly as a composite arg to another call. Pre-fix - * `nodeisslice` in cgenutil.ww had no N_CALL arm, so the natural-push - * branch in pushargsrev fell through to a single `PUSHQ AX` and the - * receiver's R8/R9 stayed unset (and arg2's pop drained off residual - * stack words, shifting all subsequent args). - * - * Cstage already had the right shape via typed-AST `node_isslice` - * (cmd/w6c/cgen.c:node_isslice). Fix aligns wwstage DOWN to cstage - * (rule 10): add an N_CALL arm to `nodeisslice` that mirrors the - * existing N_CALL arm in `nodeisstr` (cgenutil.ww:574+). - * - * test/lang/composite_call_arg_test.ww pins the runtime behaviour; this row - * pins the asm shape so a future cgen refactor that re-routes - * pushargsrev can't silently regress back to the dropped-len/cap - * sequence. - */ -#include -#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; -} - -struct row { const char *label; const char *src; }; - -/* Canonical row: f(g()) with g returning `[]u8`, f taking 2x `[]u8`. - * Mirrors strings.hasprefix(bytes.X(toutf8(in), p)) shape. */ -static const struct row rows[] = { - { "slice_call_into_2slice_arg", - "fn view(s: str) []u8 = {\n" - " let r: []u8;\n" - " r.ptr = s.ptr;\n" - " r.len = s.len;\n" - " r.cap = s.len;\n" - " return r;\n" - "};\n" - "fn check(a: []u8, b: []u8) bool = {\n" - " if (a.len != b.len) { return false; };\n" - " return true;\n" - "};\n" - "export fn caller(in: str, p: []u8) bool = {\n" - " return check(view(in), p);\n" - "};\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 long -find_after(const char *buf, long start, const char *needle) -{ - const char *p = strstr(buf + start, needle); - if (!p) return -1; - return (long)(p - buf); -} - -/* After every `CALL\tview` site within caller, three PUSHQs (CX, BX, - * AX in that order) must appear before the next CALL site. Pre-fix - * wwstage emitted only one PUSHQ AX. */ -static int -check_three_push_after_call(const char *spath, const struct row *r) -{ - char buf[1 << 16]; - if (slurp(spath, buf, sizeof buf) < 0) return -1; - - long call = find_after(buf, 0, "CALL\tmain.view"); - if (call < 0) { - fprintf(stderr, "row[%s]: no CALL view site\n", r->label); - return -1; - } - long nextcall = find_after(buf, call + 1, "CALL\t"); - if (nextcall < 0) { - fprintf(stderr, "row[%s]: no follow-up CALL\n", r->label); - return -1; - } - long pcx = find_after(buf, call, "PUSHQ\tCX"); - long pbx = find_after(buf, call, "PUSHQ\tBX"); - long pax = find_after(buf, call, "PUSHQ\tAX"); - if (pcx < 0 || pcx > nextcall) { - fprintf(stderr, - "row[%s]: no PUSHQ CX between CALL view and next CALL\n", - r->label); - return -1; - } - if (pbx < 0 || pbx > nextcall) { - fprintf(stderr, - "row[%s]: no PUSHQ BX between CALL view and next CALL\n", - r->label); - return -1; - } - if (pax < 0 || pax > nextcall) { - fprintf(stderr, - "row[%s]: no PUSHQ AX between CALL view and next CALL\n", - r->label); - return -1; - } - /* High → low order: CX first, then BX, then AX. */ - if (!(pcx < pbx && pbx < pax)) { - fprintf(stderr, - "row[%s]: PUSHQ order != CX,BX,AX (%ld,%ld,%ld)\n", - r->label, pcx, pbx, pax); - return -1; - } - return 0; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/cca_asm_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/cca_asm_%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; -} - -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, - "composite_call_arg[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_three_push_after_call(cs_path, &rows[i]) != 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, - "composite_call_arg[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - unlink(cs_path); - continue; - } - total++; - if (check_three_push_after_call(ws_path, &rows[i]) != 0) { - fail++; - } - - /* Byte-id diff: this canonical row has no !void / no tagged - * variants, so it is not gated by #22 or #21 and must - * cmp -s clean post-#24. */ - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "composite_call_arg[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "composite_call_arg: %d/%d fixtures failed\n", fail, total); - return 1; - } - printf("composite_call_arg: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/727_modcall_widen_slice.c b/test/wcc/727_modcall_widen_slice.c deleted file mode 100644 index bfc752e1..00000000 --- a/test/wcc/727_modcall_widen_slice.c +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 727_modcall_widen_slice — sentinel for #28. Pins wwstage's cgcall to - * look up calleeparams for module-qualified `mod.fn(...)` callees, so - * pushargsrev's widening detection fires for an N_IDENT slice arg - * passed to a tagged-union parameter slot. - * - * Pre-fix wwstage `cgcall` (selfhost/cmd/wcc/cgenexpr.ww:2861) only - * called fnparamslookup when `callee.kind == nkind.N_IDENT`. For - * `mod.fn(...)` (N_DOT callee), `calleeparams` stayed nil; pushargsrev's - * widening detection is gated on `param != nil` so it never fired; - * the N_IDENT-slice fast path (cgenutil.ww:368-383) then pushed only - * 3 slot words (ptr/len/cap) and DROPPED the variant tag word. The - * callee subsequently dispatched on (callee-arg-reg holds ptr instead - * of tag) — a runtime miscompile, not a pure byte-id divergence. - * - * Cstage finds params via `n->lhs->type` (the checker-set type on the - * N_DOT callee node, cmd/w6c/cgen.c:4161-4165), bypassing the name- - * driven registry. Wwstage needed the mirror via N_DOT.lhs.str. - * - * Same-family sister of #21 (N_CALL dispatch in pushargsrev), #19 - * (N_TSLICE match arm), #24 (N_CALL slice arm) — the pattern is - * wwstage dispatchers periodically missing arms cstage gets natively - * via typed AST. - * - * Bootstrap-blocking when lib/strings v3 (drags utf8 + bytes into the - * build) lands. - */ -#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; -}; - -/* Two-module probe: `caller` calls `needle.want(h, n)` where `n: []u8` - * widens into `(u8 | []u8)`. The // MODULE: directives + `use needle;` - * shape parallels the cmd/ww driver's resolved .unit.ww form. */ -static const struct row rows[] = { - { "dot_callee_slice_widen", - "package needle;\n" - "export fn want(haystack: []u8, needle: (u8 | []u8)) i32 = {\n" - " let r: i32 = haystack.len;\n" - " match (needle) {\n" - " case let b: u8 => r = r + (b: i32);\n" - " case let s: []u8 => r = r + s.len;\n" - " };\n" - " return r;\n" - "};\n" - "package caller;\n" - "import needle;\n" - "export fn main() i32 = {\n" - " let h: []u8;\n" - " let n: []u8 = h;\n" - " return needle.want(h, n);\n" - "};\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[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/mws_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/mws_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - 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 main, the call to needle.want must push 4 needle words - * (cap, len, ptr, tag) before the haystack pushes. Asserts the tag- - * synth `MOVQ $1, AX; PUSHQ AX` lands between the slice-triple pushes - * and the next-arg load. Pre-fix the tag synth is absent — the - * needle slot ends at the third PUSHQ AX. */ -static int -check_tag_push_present(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"); - if (!fn) { - fprintf(stderr, "row[%s][%s]: no TEXT main in %s\n", - r->label, stage, spath); - return -1; - } - const char *call = strstr(fn, "CALL\tneedle.want"); - if (!call) { - fprintf(stderr, "row[%s][%s]: no CALL needle.want\n", - r->label, stage); - return -1; - } - /* Tag synth: a `MOVQ\t$1, AX` followed by a `PUSHQ\tAX` between - * fn-start and the call. The literal `$1` is the slice-variant - * tag index in (u8 | []u8). */ - int found = 0; - const char *p = fn; - while (p < call) { - const char *m = strstr(p, "MOVQ\t$1, AX"); - if (!m || m >= call) break; - const char *nx = strstr(m, "PUSHQ\tAX"); - if (nx && nx < call) { found = 1; break; } - p = m + 1; - } - if (!found) { - fprintf(stderr, - "row[%s][%s]: no tag-synth `MOVQ $1, AX; PUSHQ AX` before CALL needle.want\n", - r->label, stage); - return -1; - } - /* Also assert the post-call `ADDQ $8, SP` (overflow cleanup) is - * emitted — 7 SysV reg slots for {haystack 3 + needle 4} leaves - * 1 word stack-overflowed. Pre-fix wwstage only pushed 6, so no - * cleanup either. */ - const char *cleanup = strstr(call, "ADDQ\t$8, SP"); - const char *nexttext = strstr(call, "TEXT "); - if (!cleanup || (nexttext && cleanup > nexttext)) { - fprintf(stderr, - "row[%s][%s]: no `ADDQ $8, SP` overflow cleanup after CALL needle.want\n", - r->label, stage); - 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, - "modcall_widen_slice[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_tag_push_present(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, - "modcall_widen_slice[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - unlink(cs_path); continue; - } - total++; - if (check_tag_push_present(ws_path, &rows[i], "wwstage") != 0) - fail++; - - /* byte-id between stages — the principled sentinel for the - * polarity catalog. */ - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "modcall_widen_slice[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - unlink(cs_path); unlink(ws_path); - } - - if (fail) { - fprintf(stderr, - "modcall_widen_slice: %d/%d fixtures failed\n", fail, total); - return 1; - } - printf("modcall_widen_slice: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/743_variadic_pack.c b/test/wcc/743_variadic_pack.c deleted file mode 100644 index d02f1f35..00000000 --- a/test/wcc/743_variadic_pack.c +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 743_variadic_pack — sentinel for STATUS-3 #16. cstage cgen's - * variadic gather (`f(args: str...)` called as `f("a", "b", "c")`) - * emitted only the ptr eightbyte of each str element; the .len - * eightbyte was never stored, so the callee's `args[i].len` read - * stack residue. Tagged-union variadics escaped because they took - * the cg_widen_tagged_store branch; primitive-type variadics - * (str..., slice..., rune... bigger than 8B) did not. - * - * Surfaced by worker-strings pre-flight on the Hare-faithful - * `concat(strs: str...)` shape; no in-tree caller exercised it - * because the c1 strings subset shipped non-variadic. Bootstrap - * byte-id masked it (selfhost only calls fmt.println, which uses - * tagged-union variadics, hitting the widen-store branch). - * - * Rule 10: both stages must emit the same {ptr-store, len-store} - * pair sequence. Pre-fix cstage emitted 3 ptr-stores only; wwstage - * already emitted both halves via cgenexpr.ww's velemstr branch. - * - * Rows pin asm-presence in both stages and cmp -s byte-id. - */ -#include -#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; -} - -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; -} - -/* Count occurrences of `needle` in `hay` between [start, end). */ -static int -count_substr(const char *hay, const char *start, const char *end, - const char *needle) -{ - int n = 0; - size_t nlen = strlen(needle); - const char *p = start; - while (p + nlen <= end) { - if (memcmp(p, needle, nlen) == 0) { n++; p += nlen; } - else { p++; } - } - return n; -} - -static int -check_caller_stores(const char *spath) -{ - char buf[1 << 16]; - if (slurp(spath, buf, sizeof buf) < 0) return -1; - - const char *body = strstr(buf, "TEXT main,"); - if (!body) { - fprintf(stderr, "variadic_pack: no TEXT main label\n"); - return -1; - } - const char *end = strstr(body, "\nTEXT "); - if (!end) end = buf + strlen(buf); - - /* Three str elements => 3 ptr-stores (MOVQ AX, off(BP)) AND - * 3 len-stores (MOVQ BX, off(BP)). The len-store is the bug - * pin: pre-fix it was missing entirely. */ - int ptr_stores = count_substr(buf, body, end, "MOVQ\tAX, -"); - int len_stores = count_substr(buf, body, end, "MOVQ\tBX, -"); - if (len_stores < 3) { - fprintf(stderr, - "variadic_pack: only %d len-stores (MOVQ BX, -K(BP)) " - "in main; expected >= 3\n", len_stores); - return -1; - } - if (ptr_stores < 3) { - fprintf(stderr, - "variadic_pack: only %d ptr-stores in main; " - "expected >= 3\n", ptr_stores); - return -1; - } - return 0; -} - -static const char *src_text = - "fn sumlen(parts: str...) i32 = {\n" - " let z: i32 = 0;\n" - " let i: i32 = 0;\n" - " for (i < parts.len) {\n" - " z += parts[i].len;\n" - " i += 1;\n" - " };\n" - " return z;\n" - "};\n" - "fn main() i32 = {\n" - " return sumlen(\"a\", \"bb\", \"ccc\");\n" - "};\n"; - -static int -emit_s(const char *w6c, char *out_s, size_t cap, int tag) -{ - char src[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/vp_asm_%d_%d.ww", getpid(), tag); - snprintf(out_s, cap, "/tmp/vp_asm_%d_%d.s", getpid(), tag); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(src_text, 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; -} - -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 total = 0, fail = 0; - - char cs_path[128], ws_path[128]; - if (emit_s(w6c, cs_path, sizeof cs_path, 0) != 0) { - fprintf(stderr, "variadic_pack[cstage]: w6c failed\n"); - return 1; - } - total++; - if (check_caller_stores(cs_path) != 0) fail++; - - if (have_ww) { - if (emit_s(w6c_ww, ws_path, sizeof ws_path, 1) != 0) { - fprintf(stderr, - "variadic_pack[wwstage]: w6c_ww failed\n"); - unlink(cs_path); - return 1; - } - total++; - if (check_caller_stores(ws_path) != 0) fail++; - - /* Byte-id between stages on the variadic-pack shape. */ - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "variadic_pack: cstage vs wwstage asm differs\n"); - fail++; - } - unlink(ws_path); - } - unlink(cs_path); - - if (fail) { - fprintf(stderr, - "variadic_pack: %d/%d checks failed\n", fail, total); - return 1; - } - printf("variadic_pack: %d/%d ok\n", total, total); - return 0; -}