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:
293
test/wcc/748_size_strategy_convergence.c
Normal file
293
test/wcc/748_size_strategy_convergence.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user