/* * 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 #include #include #include #include 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; }