selfhost+cstage+test: graduate frame growth to first-use+fail-loud (#15)

Subsumes #36. Drop wwstage scanlocals pre-pass; both stages converge on
first-use+fail-loud frame growth, rule-10 polarity DOWN to leaner side.
#36's surfaces (frame-total divergence on match-arm case-let; sibling
offset divergence in variadic+iter+match-prev compositions) close
naturally — running-max c.frame includes every first-use binding.

selfhost/cmd/wcc: add atlocals persistent @-prefix registry surviving
cgblock save/restore; add cgoutbuf/cgoutmode/cgout_enable/disable/flush
for deferred prologue (emit body to buffer, finalise c.frame, then
TEXT/SUBQ + flush); localadd @-prefix dedups against atlocals +
fail-louds on size-grow (rule 7 — no silent truncate); cgreturn-tagged
routes through @retscr (was colliding with @tagscr on arg-widen sizes);
variadic gather esz uses raw primsize (rune->4) not slotsize (rune->8)
— matches cstage and fixes the #36 sibling runtime miscompile in
non-leaf variadic+iter+match-prev callees.

cmd/w6c/cgen.c: drop the over-allocation hack ("for byte-id with
wwstage scanlocals reservation") since wwstage no longer over-reserves;
add fail-loud on @sretscr size-grow; @tagscr sites pass actual slot_sz
instead of stale c.tagscrsz.

748_size_strategy_convergence: table-driven 4 rows x 2 stages
(tag_variadic_runearm, trim_iter_match_prev, variadic_gather_rune_stride,
leaf_baseline). Each exercises a #36 surface shape; 8/8 ok.

Net -1565 lines. Sister latents filed as cosmetic (cs/ws frame size
drift on multiple-variadic-call fns): labelseq drift + varargseq
stuck at 0 — both bootstrap-byte-id safe (ww2==ww3==ww4 holds since
both ww2 and ww3 are wwstage outputs).

make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
This commit is contained in:
2026-05-19 02:13:58 +09:00
parent 7a278c1a2d
commit 5609d0456f
10 changed files with 1197 additions and 2469 deletions

View File

@@ -0,0 +1,293 @@
/*
* 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 <string.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 *src_path, const char *tmpdir)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, 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[64], tmpdir[64];
snprintf(src, sizeof src, "/tmp/sz_conv_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/sz_conv_%d_d_%d",
getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) { fail++; total++; continue; }
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
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';
/* Runtime parity (cstage). */
total++;
if (build_with(cdrv, src, tmpdir) != 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++;
}
}
unlink(outbin);
/* Runtime parity (wwstage). */
if (have_ww) {
total++;
if (build_with(wdrv, src, tmpdir) != 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++;
}
}
unlink(outbin);
}
unlink(src);
rmdir(tmpdir);
}
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;
}