Files
ww/test/wcc/717_struct_byval_param.c
Hojun-Cho ce3a25a0b4 test: contain sepwork scratch per-driver tmpdir, fix /tmp+in-repo leak (#8)
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.
2026-06-22 23:29:39 +09:00

284 lines
9.2 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}