The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the compiler's <stem>.sepwork scratch landed beside the source and was never cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs and fabricates phantom test failures + silent harness aborts, and for in-repo fixture builds leaked .sepwork into the tracked tree. Each leaking build now writes its source + output inside a per-invocation tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and rm -rf's the tmpdir on every exit path -- including fopen-fail and the expected-fail reject builds (scratch is mkdir'd before the build can fail). `ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993 byte-id comparison logic is byte-for-byte unchanged. Two items filed separately (this commit holds the no-Makefile / no-main.c rail): - #13: a stale <src>.s byte-id readback (749) silently no-ops since separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline. - #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no -o and leak main.sepwork in-tree (bounded, gitignored; own commit). One concern -- sepwork leak hygiene -- across 228 drivers; uniform transform applied per-file and two-round reviewed. make test: all 402 passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
284 lines
9.2 KiB
C
284 lines
9.2 KiB
C
/*
|
||
* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/wait.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; }
|
||
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;
|
||
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;
|
||
}
|