/* * 717_struct_byval_param — wwstage param-spill ABI for user-defined * by-value struct parameters of size ≤ 16B. Sister to #9 (which * fixed an OVER-allocation in the match-scrutinee spill path); * this is the UNDER-allocation in the cgfn param-spill path. * * Pre-fix (#11): wwstage cgfnparams dispatched on TK_ELLIPSIS, * float, tagged, slice, str — then a catch-all 8B scalar arm. * User-defined struct params (TY_STRUCT after collectstructs) * had no branch and fell through to the 8B arm. For * `fn cmp(a: inst, b: inst)` with `inst = struct { sec: i64, * nsec: i64 }` cstage allocated `TEXT cmp,$32` and spilled all * four argregs DI/SI/DX/CX; wwstage allocated `TEXT cmp,$16` * and spilled only DI/SI — the second-half value words of each * arg (DX/CX) were never stored, and `b.sec`/`b.nsec` reads * trailed into the saved-BP word. Built-in `str` (also 16B, * ptr+len) routed correctly through the isstrtype arm, so the * bug was a TY_STRUCT type-dispatch miss, not a missing path. * * Fix (#11, wwstage-only per rule 10): add structparamsize in * cgenutil.ww (mirror cstage cgen.c struct_arg_size: returns the * struct's totsize for sizes in (0, 16], else 0). Add a struct * arm in cgfnparams between the str arm and the 8B catch-all, * matching cstage's struct_eb = (size > 8) ? 2 : 1 — including * the partial-fit stitch arm (idx=5, nw=2: one reg, one stack * tail). Mirror the same isstr branch in cgfn's pre-scan so the * frame reservation and emit stay lockstep. * * What this test pins: * - Asm byte-identity between cstage and wwstage for 2×16B * struct params, 1×16B struct + 1×8B struct (mixed * eightbyte counts), 1×16B struct + 1×str (regression guard * for the already-working str path), and 6 i64 + 1 16B * struct (partial-fit stitch case). * - Runtime end-to-end: each row's main returns a deterministic * exit code derived from the received struct's fields. A * regression in the spill ABI (e.g. CX→slot+8 missing) shows * up as a `got != want` exit-code mismatch. * * NOT covered: * - Structs > 16B by-value param: cstage's struct_arg_size * itself gates on size <= 16, so a 24B-struct param also * under-allocates in cstage (falls through to the 8B arm). * Both stages agree on the wrong behaviour, so it's bug- * compatible byte-identical; that's a separate task. * - Single-field i32-or-smaller struct: still 8B totsize after * wwstage's registerstruct rounds up, equivalent to a scalar * i64 in the spill path. The 1-field i64 row exercises the * nw=1 branch. */ #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 want; }; static const struct row rows[] = { /* 1. Canonical 2×16B struct shape (the original probe). The * comparator reads b.sec — that's the second-arg slot+0, which * pre-fix sat unwritten and aliased the caller's saved BP. */ { "two_16B_struct_cmp", "type inst = struct { sec: i64, nsec: i64 };\n" "fn cmp(a: inst, b: inst) i32 = {\n" " if (a.sec < b.sec) { return -1; };\n" " if (a.sec > b.sec) { return 1; };\n" " if (a.nsec < b.nsec) { return -1; };\n" " if (a.nsec > b.nsec) { return 1; };\n" " return 0;\n" "};\n" "fn main() i32 = {\n" " let x: inst = inst { sec = 5i64, nsec = 200i64 };\n" " let y: inst = inst { sec = 5i64, nsec = 100i64 };\n" " let r: i32 = cmp(x, y);\n" " if (r > 0) { return 7; };\n" " return 0;\n" "};\n", 7 }, /* 2. 16B struct + 8B struct (mixed eightbyte counts). a uses * DI/SI (2 eb), b uses DX (1 eb). Returns a.sec + a.nsec + b.v * = 10 + 20 + 30 = 60. */ { "mixed_16B_8B_struct", "type two = struct { sec: i64, nsec: i64 };\n" "type one = struct { v: i64 };\n" "fn sum(a: two, b: one) i32 = {\n" " return (a.sec + a.nsec + b.v): i32;\n" "};\n" "fn main() i32 = {\n" " let x: two = two { sec = 10i64, nsec = 20i64 };\n" " let y: one = one { v = 30i64 };\n" " return sum(x, y);\n" "};\n", 60 }, /* 3. 16B struct + 16B str (regression guard). The pre-fix * wwstage already handled str via the isstrtype arm; this row * pins that the new struct arm doesn't shadow it. struct in * DI/SI, str in DX/CX. Returns a.sec + s.len = 42 + 3 = 45. */ { "struct_plus_str", "type two = struct { sec: i64, nsec: i64 };\n" "fn pick(a: two, s: str) i32 = {\n" " return (a.sec + (s.len: i64)): i32;\n" "};\n" "fn main() i32 = {\n" " let x: two = two { sec = 42i64, nsec = 0i64 };\n" " return pick(x, \"abc\");\n" "};\n", 45 }, /* 4. Partial-fit stitch: 5 i64 args consume DI/SI/DX/CX/R8; * the 16B struct hits idx=5 with regs_left=1, nw=2 — one * half (struct word 0) lands in R9, the second half spills * onto the caller's stack at +16(BP). Wwstage's struct arm * stitches both into a contiguous local slot. Returns * sum(s0..s4) + x.a + x.b = 1+2+3+4+5+11+22 = 48. */ { "partial_fit_5i64_plus_16B", "type pair = struct { a: i64, b: i64 };\n" "fn manyfn(s0: i64, s1: i64, s2: i64, s3: i64, s4: i64,\n" " x: pair) i32 = {\n" " return (s0 + s1 + s2 + s3 + s4 + x.a + x.b): i32;\n" "};\n" "fn main() i32 = {\n" " let p: pair = pair { a = 11i64, b = 22i64 };\n" " return manyfn(1i64, 2i64, 3i64, 4i64, 5i64, p);\n" "};\n", 48 }, }; static int run_driver(const char *driver, const struct row *r, int i) { char tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsbp_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(src, sizeof src, "%s/wcsbp_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/wcsbp_%d_%d", tmpdir, getpid(), i); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(src, "wb"); if (!f) { runwait(rmcmd); return -1; } wwtest_fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s build -o %s %s", driver, outbin, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); runwait(rmcmd); return -1; } int got = runwait(outbin); runwait(rmcmd); return got; } /* asm_byte_identical — pin the spill prologue + frame size by * diffing the w6c vs w6c_ww text output. The whole point of #11 * is that wwstage's frame stops under-bloating for user-defined * struct params, so the bytes must match. */ static int asm_byte_identical(const char *bin, const struct row *r, int i) { char src[64], cs[64], ws[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/wcsbp_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/wcsbp_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/wcsbp_asm_%d_%d_w.s", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; wwtest_fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c errored\n", r->label); unlink(src); return -1; } snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", bin, ws, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); unlink(src); unlink(cs); return -1; } FILE *fc = fopen(cs, "rb"); FILE *fw = fopen(ws, "rb"); int rc = 0; if (!fc || !fw) { rc = -1; } else { for (;;) { int a = fgetc(fc); int b = fgetc(fw); if (a != b) { rc = -1; break; } if (a == EOF) break; } } if (fc) fclose(fc); if (fw) fclose(fw); if (rc != 0) fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", r->label); unlink(src); unlink(cs); unlink(ws); 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 cdrv[1024]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); char wdrv[1024]; 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, "struct_byval_param: 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, "struct_byval_param[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } if (access(wdrv, X_OK) == 0) { for (int i = 0; i < n; i++) { total++; if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; } } if (fail) { fprintf(stderr, "struct_byval_param: %d/%d fixtures failed\n", fail, total); return 1; } printf("struct_byval_param: %d/%d ok\n", total, total); return 0; }