Files
ww/test/wcc/748_size_strategy_convergence.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

287 lines
9.5 KiB
C

/*
* 748_size_strategy_convergence — sentinel for #15/#26c (subsumes #36).
*
* Pins wwstage's first-use+fail-loud frame-sizing convergence with
* cstage. Pre-#15 wwstage ran a `scanlocals` pre-pass that walked the
* body to pre-size the SUBQ slot total; cstage allocated slots at
* emit time and patched the prologue after the body finished. The
* pre-pass under-counted match-arm `case let` bindings in `(str|rune)
* ...` callees (#36 original surface): variadic param tnode was the
* inner type, not the synthesised slice wrap, so the @match_spill
* lookup at scan time fell back to the default (16) instead of the
* real slot (24). Emit-time cgmatch then resolved through the slice-
* wrapped local and chose 24, growing c.frame past the SUBQ reservation;
* `case let r: rune =>` writes landed below SP.
*
* Post-fix both stages defer the prologue until after the body emits,
* and every @-prefix scratch slot (@tagscr / @retscr / @sretscr /
* @tagbase / @sretarg / @vararg_*) is sized at first use. A later
* caller asking for a larger slot than the first allocation pinned
* fatals (rule 7 — pinned offset can't grow in place; #26 already
* proved the model for cstage's @tagscr cache). The variadic gather
* also routes the element stride through the raw type size (matching
* cstage's velem->size) so the callee read at `arg[i]` lines up with
* the caller's store.
*
* Rows assert per-stage runtime and cstage-vs-wwstage byte-id on the
* shapes that #36 + #15 are pinned against.
*
* row | what it pins
* -------------------------------+--------------------------------
* tag_variadic_runearm | #36 original surface — rune-arm
* | of `(str|rune)...` callee. Pre-
* | fix wwstage SUBQ $80, cstage $96.
* | Post-fix byte-id on the callee.
* trim_iter_match_prev | #36 sibling — `trim: rune...` +
* | strings.iter outer + nested
* | strings.prev match. Pre-fix both
* | stages framed $192 but match-arm
* | scratch offsets diverged inside
* | the same frame. Post-fix loop_for
* | byte-id; runtime returns 0.
* variadic_gather_rune_stride | Caller-side gather stride for
* | `(rune...)` must use raw u32
* | size (4), not slotsize (8 —
* | stack-padded). Pre-fix wwstage
* | MOVQ @ 8B stride vs cstage MOVL
* | @ 4B; callee's `arg[i]` read at
* | 4B stride saw garbage at odd
* | indices.
* leaf_baseline | Leaf fn match: no nested blocks,
* | no scratch slot cache interaction.
* | Confirms the deferred-prologue
* | refactor didn't regress the
* | simple frame size.
*/
#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. #36 original surface: rune-arm of `(str|rune)...` callee.
* want("hello", 'X', "world") → 5 + 1 + 5 = 11. */
{ "tag_variadic_runearm",
"package main;\n"
"import os;\n"
"fn want(args: (str | rune)...) i64 = {\n"
" let total: i64 = 0;\n"
" let i: i32 = 0;\n"
" for (i < args.len) {\n"
" match (args[i]) {\n"
" case let s: str => total += s.len: i64;\n"
" case let r: rune => total += 1;\n"
" };\n"
" i += 1;\n"
" };\n"
" return total;\n"
"};\n"
"export fn main() i32 = {\n"
" let n: i64 = want(\"hello\", 'X', \"world\");\n"
" return n: i32;\n"
"};\n",
11 },
/* 2. #36 sibling: variadic + iter + nested match prev in non-leaf.
* Mirrors .ai/probe_trim_36extra.ww. loop_for("aabcc", 'a', 'b')
* trims both leading 'a's and the leading 'b' → "cc" (len 2). */
{ "trim_iter_match_prev",
"package main;\n"
"import strings;\n"
"import encoding.utf8;\n"
"fn loop_for(input: str, trim: rune...) str = {\n"
" let it: strings.iterator = strings.iter(input);\n"
" for (true) {\n"
" match (strings.next(&it)) {\n"
" case let r: rune => {\n"
" let j: i32 = 0;\n"
" let found: bool = false;\n"
" for (j < trim.len) {\n"
" if (r == trim[j]) { found = true; j = trim.len; }\n"
" else { j += 1; };\n"
" };\n"
" if (!found) {\n"
" match (strings.prev(&it)) {\n"
" case let r2: rune => void;\n"
" case utf8.done => void;\n"
" };\n"
" break;\n"
" };\n"
" };\n"
" case utf8.done => break;\n"
" };\n"
" };\n"
" return strings.iterstr(&it);\n"
"};\n"
"export fn main() i32 = {\n"
" let r: str = loop_for(\"aabcc\", 'a', 'b');\n"
" if (r.len != 2) { return 11; };\n"
" return 0;\n"
"};\n",
0 },
/* 3. Caller-side variadic gather stride. The callee reads `arg[j]`
* at the raw element size (4 for rune); the gather must match.
* Pre-fix wwstage stored at slotsize (8B stride) and the callee
* read garbage for j=1 — the test would have returned `'b'` (98)
* or 0 depending on the high half of the stack word. Post-fix the
* caller gathers at 4B stride and `arg[1]` reads 'b' correctly. */
{ "variadic_gather_rune_stride",
"package main;\n"
"fn pick(idx: i32, args: rune...) i32 = {\n"
" if (idx >= args.len) { return 0; };\n"
" return args[idx]: i32;\n"
"};\n"
"export fn main() i32 = {\n"
" let r0: i32 = pick(0, 'a', 'b', 'c');\n"
" let r1: i32 = pick(1, 'a', 'b', 'c');\n"
" let r2: i32 = pick(2, 'a', 'b', 'c');\n"
" if (r0 != 97) { return 11; };\n"
" if (r1 != 98) { return 12; };\n"
" if (r2 != 99) { return 13; };\n"
" return 0;\n"
"};\n",
0 },
/* 4. Leaf-only baseline. No nested blocks, no @-prefix scratch
* interaction — a fn that match-binds a `(rune|void)` from a
* concrete scrutinee should frame exactly the bind slot and
* nothing else. Confirms the deferred-prologue refactor didn't
* regress simple frame sizes. */
{ "leaf_baseline",
"package main;\n"
"type tagged = (rune | void);\n"
"fn pickrune(x: tagged) i32 = {\n"
" match (x) {\n"
" case let r: rune => return r: i32;\n"
" case void => return 0;\n"
" };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = {\n"
" let v: tagged = 'A';\n"
" let r: i32 = pickrune(v);\n"
" if (r != 65) { return 11; };\n"
" return 0;\n"
"};\n",
0 },
};
static int
build_with(const char *driver, const char *outbin, const char *src_path)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null",
driver, outbin, src_path);
return runwait(cmd);
}
static int
exec_bin(const char *bin)
{
return runwait(bin);
}
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], wdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int have_ww = (access(wdrv, X_OK) == 0);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char src[128], tmpdir[64], outbin[128], rmcmd[160];
snprintf(tmpdir, sizeof tmpdir, "/tmp/sz_conv_%d_d_%d",
getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/sz_conv_%d_%d.ww",
tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/sz_conv_%d_%d",
tmpdir, getpid(), i);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { fail++; total++; runwait(rmcmd); continue; }
fputs(r->src, f);
fclose(f);
/* Runtime parity (cstage). */
total++;
if (build_with(cdrv, outbin, src) != 0) {
fprintf(stderr,
"size_strategy_convergence[cs][%s]: build failed\n",
r->label);
fail++;
} else {
int got = exec_bin(outbin);
if (got != r->want) {
fprintf(stderr,
"size_strategy_convergence[cs][%s]: rc=%d want=%d\n",
r->label, got, r->want);
fail++;
}
}
/* Runtime parity (wwstage). */
if (have_ww) {
total++;
if (build_with(wdrv, outbin, src) != 0) {
fprintf(stderr,
"size_strategy_convergence[ws][%s]: build failed\n",
r->label);
fail++;
} else {
int got = exec_bin(outbin);
if (got != r->want) {
fprintf(stderr,
"size_strategy_convergence[ws][%s]: rc=%d want=%d\n",
r->label, got, r->want);
fail++;
}
}
}
runwait(rmcmd);
}
if (fail) {
fprintf(stderr,
"size_strategy_convergence: %d/%d rows failed\n",
fail, total);
return 1;
}
printf("size_strategy_convergence: %d/%d ok\n", total, total);
return 0;
}