/* * 927_composite_call_arg_run — runtime sentinel for #24 (Class A * wwstage cgen miscompile). When a 3-reg composite (`[]u8` slice) * CALL result is passed inline as a composite arg to another call, * pre-fix wwstage emitted a single `PUSHQ AX` (loses .len/.cap) and * under-popped the receiver's arg-regs by two words. Net: arg-shift * collision corrupts every subsequent arg; the receiver reads the * caller's spilled `p.ptr` as its own `s.len`, etc. * * Fix-site: `nodeisslice` in selfhost/cmd/wcc/cgenutil.ww gained an * N_CALL arm mirroring `nodeisstr`'s existing one. Both the push side * (pushargsrev's 3-PUSH `CX, BX, AX` arm) and the pop side (cgcall's * `extra = 2`) then fire for slice-returning CALLs as args. * * Rows pin the runtime semantics across the matrix rob-pike outlined: * (a) canonical `f(g())` with g returning `[]u8`. * (b) slice-CALL then let-slice `f(g(), p)` (strings.hasprefix shape). * (c) two composite CALLs `f(g(in1), g(in2))` (arg-shift collision). * (d) slice-CALL in middle arg position `f(p, g(), q)`. * (e) slice-CALL as last arg `f(p, q, g())` (stack-spill region). * (f) nested composite `f(g(h(s)))` (composite-in-composite). * (g) tagged-CALL regression alongside slice-CALL — confirm #21 * still holds and the two natural-push arms compose. * (h) slice-CALL followed by a scalar `f(g(), 42)` (pop-count mix). */ #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; }; static const struct row rows[] = { /* (a) canonical: f(g()) with g returning `[]u8`. Pre-fix wwstage * dispatches via fewer PUSHes; check.len reads .ptr-low (a tiny * non-13 number) → return 11. */ { "canonical_slice_call", "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) i32 = {\n" " if (a.len == 13) { return 0; };\n" " return 11;\n" "};\n" "export fn main() i32 = {\n" " let s: str = \"hello, world!\";\n" " return check(view(s));\n" "};\n", 0 }, /* (b) f(g(), p) — strings.hasprefix shape: slice-CALL into 6-reg * argpack, let-slice as second. Pre-fix p.ptr lands in DI/SI * instead of R8/R9; check2 sees garbage for `b.len`. */ { "slice_call_then_letslice", "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 check2(a: []u8, b: []u8) i32 = {\n" " if (a.len != 13) { return 11; };\n" " if (b.len != 5) { return 12; };\n" " return 0;\n" "};\n" "export fn main() i32 = {\n" " let s: str = \"hello, world!\";\n" " let p: []u8;\n" " p.ptr = s.ptr;\n" " p.len = 5;\n" " p.cap = 5;\n" " return check2(view(s), p);\n" "};\n", 0 }, /* (c) f(g(in1), g(in2)) — two composite CALLs back-to-back. The * second call's pushargsrev runs first (right-to-left), so the * stack must hold (a.cap,a.len,a.ptr,b.cap,b.len,b.ptr) before * the 6-POP drain. Pre-fix only `a.ptr` and `b.ptr` made it. */ { "two_composite_calls_args", "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 check2(a: []u8, b: []u8) i32 = {\n" " if (a.len != 3) { return 11; };\n" " if (b.len != 5) { return 12; };\n" " return 0;\n" "};\n" "export fn main() i32 = {\n" " let s1: str = \"foo\";\n" " let s2: str = \"hello\";\n" " return check2(view(s1), view(s2));\n" "};\n", 0 }, /* (d) slice-CALL in middle of three args. Composite arg-shift * collision pulls scalar arg2 into the wrong reg-class window. */ { "slice_call_middle_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 check3(k0: i32, a: []u8, k1: i32) i32 = {\n" " if (k0 != 7) { return 11; };\n" " if (a.len != 4) { return 12; };\n" " if (k1 != 9) { return 13; };\n" " return 0;\n" "};\n" "export fn main() i32 = {\n" " let s: str = \"abcd\";\n" " return check3(7, view(s), 9);\n" "};\n", 0 }, /* (e) f(g(h(s))) — composite-in-composite nesting. Both inner and * outer calls must route slice CALL → slice arg through the same * pushargsrev path. */ { "nested_composite_calls", "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 passthrough(a: []u8) []u8 = {\n" " return a;\n" "};\n" "fn check(a: []u8) i32 = {\n" " if (a.len == 6) { return 0; };\n" " return 11;\n" "};\n" "export fn main() i32 = {\n" " let s: str = \"abcdef\";\n" " return check(passthrough(view(s)));\n" "};\n", 0 }, /* (f) slice-CALL followed by scalar — pop-count mix; pre-fix the * scalar would land off-by-2 in the int-stream. */ { "slice_call_then_scalar", "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 use2(a: []u8, k: i32) i32 = {\n" " if (a.len != 13) { return 11; };\n" " if (k != 99) { return 12; };\n" " return 0;\n" "};\n" "export fn main() i32 = {\n" " let s: str = \"hello, world!\";\n" " return use2(view(s), 99);\n" "};\n", 0 }, /* (g) tagged-CALL regression alongside the slice path. Confirms * #21's natural-push arm composes with #24's. The dispatch shape * mirrors lib/encoding/utf8.next's call-chain in selfhost. */ { "tagged_call_regression", "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", 0 }, }; static int run_driver(const char *driver, const struct row *r, int i) { char src[64], tmpdir[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/cca_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/cca_%d_d_%d", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); mkdir(tmpdir, 0755); snprintf(cmd, sizeof cmd, "cd %s && %s build %s", tmpdir, driver, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); unlink(src); rmdir(tmpdir); return -1; } const char *base = strrchr(src, '/'); base = base ? base + 1 : src; char outbin[128]; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; int got = runwait(outbin); unlink(src); unlink(outbin); rmdir(tmpdir); return got; } 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 cdrv[640]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); char wdrv[640]; snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); struct { const char *name; const char *path; int gated_on_existence; } drivers[] = { { "cstage", cdrv, 0 }, { "wwstage", wdrv, 1 }, { NULL, NULL, 0 }, }; int n = (int)(sizeof rows / sizeof rows[0]); int total = 0, fail = 0; for (int d = 0; drivers[d].name; d++) { if (drivers[d].gated_on_existence && access(drivers[d].path, X_OK) != 0) { fprintf(stderr, "composite_call_arg_run: skip %s (no %s)\n", drivers[d].name, drivers[d].path); continue; } for (int i = 0; i < n; i++) { int got = run_driver(drivers[d].path, &rows[i], i); total++; if (got != rows[i].want) { fprintf(stderr, "composite_call_arg_run[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } if (fail) { fprintf(stderr, "composite_call_arg_run: %d/%d fixtures failed\n", fail, total); return 1; } printf("composite_call_arg_run: %d/%d ok\n", total, total); return 0; }