/* * 990_selfhost — phase-10 marker test. Three probes: * * 1. The selfhost stubs (mem.ww, err.ww) must parse, typecheck and * codegen via C-side `w6c`. * * 2. selfhost/test/smoke.ww — an actual ww program exercising the * patterns the real port will use (bump arena, error union, * struct-of-fn-pointer dispatch, byte scanner, strconv) — must * build through `ww build` and exit 42 when run. Any non-42 exit * names which probe broke (1..N). * * 3. `wwdump` is deterministic: running it twice on the same input * must produce byte-identical output. This is the diff anchor for * self-host. When the ww-side wwdump lands (task 3), the same * bytes will have to come out of the ww frontend. * * The ceiling — `cmp ww2 ww3` — is gated on the full frontend port. */ #include #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; } static const char * absbin(void) { const char *b = getenv("BIN"); if (!b) b = "out/bin"; if (b[0] == '/') return b; static char buf[2048]; char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return NULL; snprintf(buf, sizeof buf, "%s/%s", cwd, b); return buf; } static int slurp(const char *path, char **outbuf, size_t *outlen) { FILE *f = fopen(path, "rb"); if (!f) return -1; fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); if (n < 0) { fclose(f); return -1; } char *b = malloc((size_t)n + 1); if (!b) { fclose(f); return -1; } if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } b[n] = '\0'; fclose(f); *outbuf = b; *outlen = (size_t)n; return 0; } /* Probe 1: each file must compile through `w6c`. */ static int probe_codegen(const char *bin) { static const char *files[] = { "selfhost/cmd/wcc/err.ww", NULL, }; char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; int fail = 0; for (int i = 0; files[i]; i++) { char cmd[2048]; snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c %s/%s > /dev/null 2>&1", bin, cwd, files[i]); if (runwait(cmd) != 0) { fprintf(stderr, "codegen FAIL: %s\n", files[i]); fail++; } } return fail ? -1 : 0; } /* Probe 2: end-to-end build+run of selfhost/test/smoke.ww. Exit 42 = ok. */ static int probe_smoke(const char *bin) { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsh_%d", getpid()); mkdir(tmpdir, 0755); char rmcmd[128]; snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); /* #8: -o routes the binary + .sepwork scratch into tmpdir; without it * the scratch lands next to the in-repo fixture as * selfhost/test/smoke.sepwork and races concurrent gates. rm -rf * clears the now-non-empty tmpdir on every exit path. */ char cmd[2048]; snprintf(cmd, sizeof cmd, "timeout 180 %s/ww build -o %s/smoke %s/selfhost/test/smoke.ww " ">/dev/null 2>&1", bin, tmpdir, cwd); if (runwait(cmd) != 0) { fprintf(stderr, "smoke FAIL: ww build did not succeed\n"); runwait(rmcmd); return -1; } char outbin[256], runcmd[512]; snprintf(outbin, sizeof outbin, "%s/smoke", tmpdir); snprintf(runcmd, sizeof runcmd, "timeout 180 %s", outbin); int rc = runwait(runcmd); runwait(rmcmd); if (rc != 42) { fprintf(stderr, "smoke FAIL: exit=%d (want 42; the number " "names which probe in selfhost/test/smoke.ww broke)\n", rc); return -1; } return 0; } /* Probe 3a: ww-side wwdump_ww must produce byte-identical output to * the C-side wwdump on every fixture. The token-stream diff (-t) is * the strongest signal: the lex.ww port has converged when this is * empty across realistic source. The AST diff (-a) is a partial * signal — the ww-side parser is currently a stub that handles only * `use` clauses, so we exercise it on a stripped fixture. As parse.ww * grows, more files will be added to the -a list. */ static int probe_dump_diff_mode(const char *bin, const char *flag, const char **inputs) { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; int fail = 0; for (int i = 0; inputs[i]; i++) { char a[256], b[256], cmd[2048]; snprintf(a, sizeof a, "/tmp/wwd_diff_%d_c", getpid()); snprintf(b, sizeof b, "/tmp/wwd_diff_%d_w", getpid()); snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump %s %s/%s > %s 2>/dev/null", bin, flag, cwd, inputs[i], a); runwait(cmd); snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump_ww %s %s/%s > %s 2>/dev/null", bin, flag, cwd, inputs[i], b); runwait(cmd); char *ba = NULL, *bb = NULL; size_t na = 0, nb = 0; int rc1 = slurp(a, &ba, &na); int rc2 = slurp(b, &bb, &nb); if (rc1 < 0 || rc2 < 0) { fprintf(stderr, "diff FAIL (%s): cannot read dumps for %s\n", flag, inputs[i]); fail++; } else if (na != nb || memcmp(ba, bb, na) != 0) { fprintf(stderr, "diff FAIL (%s): %s — C %zu bytes vs ww %zu bytes\n", flag, inputs[i], na, nb); fail++; } free(ba); free(bb); unlink(a); unlink(b); } return fail ? -1 : 0; } static int probe_dump_diff(const char *bin) { const char *tok_inputs[] = { "selfhost/cmd/wcc/err.ww", "lib/ww/syntax/lex.ww", "lib/ww/syntax/tok.ww", "lib/ww/syntax/ast.ww", "lib/ww/syntax/parse.ww", "lib/ww/syntax/expr.ww", "lib/ww/syntax/stmt.ww", "lib/ww/syntax/decl.ww", "selfhost/cmd/wwdump/main.ww", "selfhost/test/smoke.ww", NULL, }; /* AST diff coverage. The ww-side parser handles use/def/type/let/fn * (with bodies), expressions with full Pratt precedence, statements * (block/let/return/if/for/defer/break/continue), match arms, * tagged unions, struct literals, attributes, and tuples. We diff * against the C parser on every selfhost file plus the stdlib. */ const char *ast_inputs[] = { "selfhost/test/uses.ww", "selfhost/cmd/wcc/err.ww", "lib/ww/syntax/lex.ww", "lib/ww/syntax/tok.ww", "lib/ww/syntax/ast.ww", "lib/ww/syntax/parse.ww", "lib/ww/syntax/expr.ww", "lib/ww/syntax/stmt.ww", "lib/ww/syntax/decl.ww", "lib/ww/syntax/typ.ww", "lib/ww/syntax/sym.ww", "selfhost/cmd/wcc/check.ww", "selfhost/cmd/wcc/cgen.ww", "selfhost/cmd/wwdump/main.ww", "selfhost/test/smoke.ww", "lib/strconv/strconv.ww", "lib/os/os.ww", "lib/ascii/ascii.ww", "lib/fmt/fmt.ww", "lib/strings/strings.ww", "lib/bytes/bytes.ww", "lib/io/io.ww", "lib/errors/errors.ww", "lib/bufio/bufio.ww", "lib/types/types.ww", "lib/sort/sort.ww", "lib/path/path.ww", "lib/net/net.ww", "lib/encoding/utf8/utf8.ww", "lib/encoding/hex/hex.ww", "lib/hash/fnv/fnv.ww", "lib/time/time.ww", "lib/c/libc/libc.ww", NULL, }; int fail = 0; if (probe_dump_diff_mode(bin, "-t", tok_inputs) != 0) fail++; if (probe_dump_diff_mode(bin, "-a", ast_inputs) != 0) fail++; return fail ? -1 : 0; } /* Probe 5: ww-side cgen (wwdump_ww -c) emits Plan 9 amd64 asm. For * each fixture we (a) diff the ww asm against C-side w6c and (b) * assemble + link + run the ww output and compare exit codes. Both * must succeed. The cgen port handles literals, +/-/star/slash, * compound assignment, idents, calls, if/else, for loops, and * comparisons; fixtures stay within that surface. */ struct cprog { const char *src; int want_exit; }; static int probe_ww_compile(const char *bin) { static const struct cprog progs[] = { { "fn main() i32 = { return 42; };", 42 }, { "fn add(a: i32, b: i32) i32 = { return a + b; };\n" "fn main() i32 = { return add(7, 35); };", 42 }, { "fn sum(n: i32) i32 = {\n" " let s: i32 = 0;\n" " let i: i32 = 0;\n" " for (i < n) { s += i; i += 1; };\n" " return s;\n" "};\n" "fn main() i32 = { return sum(10); };", 45 }, { "fn check(x: i32) i32 = {\n" " if (x > 0) { return 1; };\n" " if (x < 0) { return 100; };\n" " return 0;\n" "};\n" "fn main() i32 = { return check(7); };", 1 }, { "fn fact(n: i32) i32 = {\n" " if (n <= 1) { return 1; };\n" " return n * fact(n - 1);\n" "};\n" "fn main() i32 = { return fact(5); };", 120 }, /* hello-world via syscall: exercises @symbol FFI, string * literal interning, DATA emission, str struct slot (16B), * str pseudo-fields (.ptr/.len), N_CAST. write(1,"hi\n",3) * returns 3 — we cast that down to the exit code. */ { "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n" "fn main() i32 = {\n" " let s: str = \"hi\\n\";\n" " let r: i64 = rt_syscall(1, 1, s.ptr: i64, s.len: i64);\n" " return r: i32;\n" "};", 3 }, /* struct fields (local + via *struct), &local, struct-name * registry: distance squared returns 25 = 3*3 + 4*4. */ { "type point = struct { x: i64, y: i64 };\n" "fn distance_sq(p: *point) i64 = { return p.x * p.x + p.y * p.y; };\n" "fn main() i32 = {\n" " let p: point;\n" " p.x = 3i64;\n" " p.y = 4i64;\n" " return distance_sq(&p): i32;\n" "};", 25 }, /* array indexing — element-size scaling for u8 arrays. * Sets buf[0..3] to 7,8,9,10 then sums them: 34. */ { "fn main() i32 = {\n" " let buf: [4]u8;\n" " buf[0] = 7u8;\n" " buf[1] = 8u8;\n" " buf[2] = 9u8;\n" " buf[3] = 10u8;\n" " let s: i32 = 0;\n" " let i: i32 = 0;\n" " for (i < 4) { s += buf[i]: i32; i += 1; };\n" " return s;\n" "};", 34 }, /* struct literal init — `let p: point = point { x=..., y=...};`. * Each field stored at its struct offset in the slot. */ { "type point = struct { x: i64, y: i64 };\n" "fn main() i32 = {\n" " let p: point = point { x = 7i64, y = 35i64 };\n" " return (p.x + p.y): i32;\n" "};", 42 }, /* fn-returning-str — exercises the SysV (AX, DX) → (AX, BX) * shuffle so str values flow through cgen as the canonical * pair. write(\"hello world\\n\") → exit 12 (length). */ { "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n" "fn greeting() str = { return \"hello world\\n\"; };\n" "fn main() i32 = {\n" " let g: str = greeting();\n" " rt_syscall(1, 1, g.ptr: i64, g.len: i64);\n" " return g.len: i32;\n" "};", 12 }, /* fn-pointer field call — `w.emit(b)` where w is a struct * with an emit: fn(b: u8) i32 field. Loads the field value * into AX and CALLs through it. The polymorphism shape ww * uses instead of interfaces. */ { "type writer = struct { emit: fn(b: u8) i32 };\n" "fn count_emit(b: u8) i32 = { return b: i32 + 1; };\n" "fn main() i32 = {\n" " let w: writer = writer { emit = count_emit };\n" " return w.emit(41u8);\n" "};", 42 }, /* Match expression — tagged union dispatch. classify(ok)=42, * classify(err)=-1, sum 41. Exercises tagged init, tagged-arg * push (3 regs), match arm tag-compare + payload bind. */ { "fn classify(r: (i32 | str)) i32 = {\n" " match (r) {\n" " case let v: i32 => return v;\n" " case let e: str => return -1;\n" " };\n" " return 0;\n" "};\n" "fn main() i32 = {\n" " let ok: (i32 | str) = 42;\n" " let err: (i32 | str) = \"fail\";\n" " return classify(ok) + classify(err);\n" "};", 41 }, /* Compound subtract via *struct field — regression for * the bug that made wwdump_ww segfault on its own source. * c.x = 5; dec; dec; dec → expect 2. Pre-fix, both C and * ww emitted SUBQ in the wrong direction (computing rhs-old * instead of old-rhs) and this returned -4 (= 252 as u8). */ { "type ctx = struct { x: i32 };\n" "fn dec(c: *ctx) void = { c.x -= 1; };\n" "fn main() i32 = {\n" " let c: ctx;\n" " c.x = 5;\n" " dec(&c); dec(&c); dec(&c);\n" " return c.x;\n" "};", 2 }, /* Uninit `[8]u8` local — raw size 8B, C cgen zero-inits via * `MOVQ $0, off(BP)`. Pre-fix the wwstage left the slot * holding stack junk and `buf[0]` returned non-zero. */ { "fn main() i32 = {\n" " let buf: [8]u8;\n" " return buf[0]: i32;\n" "};", 0 }, /* [N]i32 indexed write + read: MOVL store, MOVSXD load. * Pre-fix the wwstage emitted MOVQ in both directions — * overwrites adjacent slots and loads the wrong width. * arr[0..3] = -7, 3, 11, sum = 7. */ { "fn main() i32 = {\n" " let arr: [3]i32;\n" " arr[0] = 0 - 7;\n" " arr[1] = 3;\n" " arr[2] = 11;\n" " return arr[0] + arr[1] + arr[2];\n" "};", 7 }, /* Hare-style for-range over a slice: `for (let b .. s)`. * Allocates `.rgi`/`.rgl` scratch slots, walks i=0..s.len * loading s.ptr[i] into the binding. esz=1 here so the * load is MOVZBQ. Sum 10+20+30+40 = 100. */ { "import os;\n" "fn main() i32 = {\n" " let s: []u8;\n" " s.ptr = nil; s.len = 0; s.cap = 0;\n" " append(s, 10u8, 20u8, 30u8, 40u8);\n" " let total: i32 = 0;\n" " for (let b .. s) { total += b: i32; };\n" " return total;\n" "};", 100 }, /* for-range with tuple destructure on `(i64, i64)`. Element * size is 16 (sum of raw param sizes); each binding loads * via BX+foff. Buf has (1,10) (2,20); sum = 33. */ { "fn main() i32 = {\n" " let buf: [4]i64;\n" " buf[0] = 1i64; buf[1] = 10i64; buf[2] = 2i64; buf[3] = 20i64;\n" " let s: [](i64, i64);\n" " s.ptr = buf.ptr: *(i64, i64);\n" " s.len = 2; s.cap = 2;\n" " let total: i64 = 0i64;\n" " for (let (k, v) .. s) { total += k + v; };\n" " return total: i32;\n" "};", 33 }, /* append(s, v, ...) builtin — Hare's rt::ensure model. * Each value: PUSHQ AX, ADDQ $1 to s.len, LEAQ s/MOVQ esz * args for rt_ensure, then write into the freshly-grown * slot. Returns s.len = 3 after appending three u8s. */ { "import os;\n" "fn main() i32 = {\n" " let s: []u8;\n" " s.ptr = nil; s.len = 0; s.cap = 0;\n" " append(s, 65u8, 66u8, 67u8);\n" " return s.len: i32;\n" "};", 3 }, /* append(s, items...) spread variant — wraps the per-value * body in a counted loop over items.len. Combined with * single-value appends in the same fn. dst ends up with * [1, 10, 20, 30] — sum = 61. */ { "import os;\n" "fn main() i32 = {\n" " let src: []i64;\n" " src.ptr = nil; src.len = 0; src.cap = 0;\n" " append(src, 10i64, 20i64, 30i64);\n" " let dst: []i64;\n" " dst.ptr = nil; dst.len = 0; dst.cap = 0;\n" " append(dst, 1i64);\n" " append(dst, src...);\n" " let total: i64 = 0i64;\n" " let i: i32 = 0;\n" " for (i < dst.len) { total += dst[i]; i += 1; };\n" " return total: i32;\n" "};", 61 }, /* switch with multi-expr cases + default. Lowers to a chain * of CMPQ + JE; the scrutinee lands in a fresh 8B local slot * (".sw_N") so case bodies can spill SP without losing it. * classify(2)=10, classify(7)=70, classify(99)=0, sum 80. */ { "fn classify(x: i32) i32 = {\n" " switch (x) {\n" " case 1, 2, 3: return 10;\n" " case 7: return 70;\n" " case: return 0;\n" " };\n" " return -1;\n" "};\n" "fn main() i32 = {\n" " return classify(2) + classify(7) + classify(99);\n" "};", 80 }, /* Chained N_DOT: o.ptr_to_inner.val (read field through a * pointer-to-struct field). Pre-fix the cgen fell through * silently and returned the inner pointer instead of the * dereferenced field. Surfaced porting w6l/pass.ww. */ { "@symbol(\"rt_malloc\") fn rt_malloc(n: u64) *void;\n" "type inner = struct { val: u64 };\n" "type outer = struct { p: *inner };\n" "fn main() i32 = {\n" " let in_: *inner = rt_malloc(8u64): *inner;\n" " in_.val = 42u64;\n" " let o: *outer = rt_malloc(8u64): *outer;\n" " o.p = in_;\n" " return o.p.val: i32;\n" "};", 42 }, /* #29: `nomem` predeclared in the universe scope as a TYPE * must not block a value of the same name. scope_lookup is * kind-aware, so `let nomem: i32 = 42;` shadows the type in * value position. Byte-identity vs cstage proves both stages * route the value-position lookup past the universe-scope * type seed. */ { "fn main() i32 = {\n" " let nomem: i32 = 42;\n" " return nomem;\n" "};", 42 }, { NULL, 0 }, }; char rt[1024]; snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); int fail = 0; for (int i = 0; progs[i].src; i++) { char src[64], wws[64], cs[64], obj[64], exe[64], cmd[2048]; snprintf(src, sizeof src, "/tmp/wwc_%d_%d.ww", getpid(), i); snprintf(wws, sizeof wws, "/tmp/wwc_%d_%d_w.s", getpid(), i); snprintf(cs, sizeof cs, "/tmp/wwc_%d_%d_c.s", getpid(), i); snprintf(obj, sizeof obj, "/tmp/wwc_%d_%d.o", getpid(), i); snprintf(exe, sizeof exe, "/tmp/wwc_%d_%d", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) { fail++; continue; } fputs(progs[i].src, f); fclose(f); snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump_ww -c %s > %s 2>/dev/null", bin, src, wws); if (runwait(cmd) != 0) { fprintf(stderr, "ww-compile prog %d: wwdump_ww -c errored\n", i); fail++; goto cleanup; } snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c %s > %s 2>/dev/null", bin, src, cs); runwait(cmd); char *bw = NULL, *bc = NULL; size_t nw = 0, nc = 0; if (slurp(wws, &bw, &nw) >= 0 && slurp(cs, &bc, &nc) >= 0) { if (nw != nc || memcmp(bw, bc, nw) != 0) { fprintf(stderr, "ww-compile prog %d: asm differs (C %zu, ww %zu)\n", i, nc, nw); fail++; } } free(bw); free(bc); snprintf(cmd, sizeof cmd, "timeout 180 %s/w6a -o %s %s 2>/dev/null", bin, obj, wws); if (runwait(cmd) != 0) { fprintf(stderr, "ww-compile prog %d: w6a failed\n", i); fail++; goto cleanup; } snprintf(cmd, sizeof cmd, "timeout 180 %s/w6l -o %s %s %s 2>/dev/null", bin, exe, obj, rt); if (runwait(cmd) != 0) { fprintf(stderr, "ww-compile prog %d: w6l failed\n", i); fail++; goto cleanup; } snprintf(cmd, sizeof cmd, "timeout 180 %s", exe); int rc = runwait(cmd); if (rc != progs[i].want_exit) { fprintf(stderr, "ww-compile prog %d: exit=%d, want %d\n", i, rc, progs[i].want_exit); fail++; } cleanup: unlink(src); unlink(wws); unlink(cs); unlink(obj); unlink(exe); } return fail ? -1 : 0; } /* resolveunit — produce 's RESOLVED translation unit the way * the sep driver does. `ww build --sep` composes the root package's * .sepwork/__root.unit.ww: the transitive deps' `.wwi` interface * stubs followed by the root package body — the single self-contained * unit w6c/wwdump consume for the root (the producer compiles it with no * -I; cmd/ww/main.c sep_compose_unit). Feeding a raw module file instead * would leave its import refs (os.write, fmt.errorln, strconv.i64tos …) * unresolved — a partial unit harec's module::resolve never hands the * checker, and which the ww-stage asserttyped invariant must not see. * * Built in a private /tmp dir (a copy of the fixture) so the sepwork * lands off the repo source tree. The link step fails for an import-only * module (no main), but the unit is written before the link, so we detect * that artifact rather than gating on the build exit. Writes the owning * tmpdir into `td` (caller rm -rf's it) and the unit path into `out`. * Returns 0 on success, -1 otherwise. */ static int resolveunit(const char *bin, const char *cwd, const char *fixture, int idx, char *td, size_t tdsz, char *out, size_t outsz) { char cmd[2048]; snprintf(td, tdsz, "/tmp/wwru_%d_%d", getpid(), idx); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); const char *base = strrchr(fixture, '/'); base = base ? base + 1 : fixture; char tmpsrc[768]; snprintf(tmpsrc, sizeof tmpsrc, "%s/%s", td, base); snprintf(cmd, sizeof cmd, "cp %s/%s %s", cwd, fixture, tmpsrc); runwait(cmd); snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s/ww build --sep %s >/dev/null 2>&1", td, bin, base); runwait(cmd); char stem[768]; snprintf(stem, sizeof stem, "%s", tmpsrc); char *dot = strrchr(stem, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; snprintf(out, outsz, "%s.sepwork/__root.unit.ww", stem); return access(out, 0) == 0 ? 0 : -1; } /* Probe 4: ww-side checker (wwdump_ww -r) runs name resolution on each * selfhost fixture's RESOLVED unit (sep root unit — see * resolveunit) and reports zero unresolved. The raw file would leave * its `import` refs (os/fmt/strconv/ascii members) unresolved, i.e. the * partial unit the asserttyped invariant must never see (harec resolves * the module graph before check). */ static int probe_resolve(const char *bin) { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; const char *fixtures[] = { "selfhost/cmd/wcc/err.ww", "lib/ww/syntax/tok.ww", "selfhost/test/smoke.ww", NULL, }; int fail = 0; for (int i = 0; fixtures[i]; i++) { char td[64], unit[1024], a[256], cmd[2048]; if (resolveunit(bin, cwd, fixtures[i], i, td, sizeof td, unit, sizeof unit) != 0) { fprintf(stderr, "resolve FAIL: %s — no resolved unit\n", fixtures[i]); fail++; continue; } snprintf(a, sizeof a, "/tmp/wwd_r_%d", getpid()); snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump_ww -r %s > %s 2>/dev/null", bin, unit, a); int rc = runwait(cmd); if (rc != 0) { fprintf(stderr, "resolve FAIL: %s exited %d (unresolved names)\n", fixtures[i], rc); fail++; } unlink(a); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); } return fail ? -1 : 0; } /* Probe 3: wwdump must be deterministic across two runs. */ static int probe_dump_stable(const char *bin) { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; const char *inputs[] = { "selfhost/cmd/wcc/err.ww", "selfhost/test/smoke.ww", NULL, }; const char *modes[] = { "-t", "-a", NULL }; int fail = 0; for (int i = 0; inputs[i]; i++) { for (int m = 0; modes[m]; m++) { char a[256], b[256]; snprintf(a, sizeof a, "/tmp/wwd_%d_a", getpid()); snprintf(b, sizeof b, "/tmp/wwd_%d_b", getpid()); char cmd[2048]; snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump %s %s/%s > %s 2>/dev/null", bin, modes[m], cwd, inputs[i], a); if (runwait(cmd) != 0) { fprintf(stderr, "wwdump FAIL: %s %s\n", modes[m], inputs[i]); unlink(a); fail++; continue; } snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump %s %s/%s > %s 2>/dev/null", bin, modes[m], cwd, inputs[i], b); runwait(cmd); char *ba = NULL, *bb = NULL; size_t na = 0, nb = 0; if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { fprintf(stderr, "wwdump FAIL: cannot read dumps\n"); free(ba); free(bb); unlink(a); unlink(b); fail++; continue; } if (na == 0) { fprintf(stderr, "wwdump FAIL: empty dump for %s %s\n", modes[m], inputs[i]); fail++; } else if (na != nb || memcmp(ba, bb, na) != 0) { fprintf(stderr, "wwdump FAIL: nondeterministic %s %s\n", modes[m], inputs[i]); fail++; } free(ba); free(bb); unlink(a); unlink(b); } } return fail ? -1 : 0; } /* Probe 7: end-to-end via the ww driver's sep path. `ww_ww build --sep` * drives the ww-side toolchain (w6c_ww → w6a_ww → w6l_ww) over the * fixture's package graph — per-package compile, archive, reverse-topo * link — and we run the result, comparing the exit code to the fixture's * declared expectation. This is the bootstrap-grade signal: it doesn't * require byte-identity, only that the ww toolchain links the fixture's * dep stack (sym/typ/ast via the syntax package, in a SEPARATE `.o`) into * a functionally correct binary. The single-`.s` link the combined path * used is gone with combined-mode (the syntax symbols live in their own * `.o` under --sep, not in the root unit). */ static int probe_ww_links(const char *bin) { const char *fixtures[][2] = { { "selfhost/test/sym_link.ww", "42" }, /* sym/typ/ast via syntax */ { NULL, NULL }, }; char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; int fail = 0; for (int i = 0; fixtures[i][0]; i++) { const char *fix = fixtures[i][0]; int want = atoi(fixtures[i][1]); char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwlnk_%d_%d", getpid(), i); mkdir(tmpdir, 0755); const char *base = strrchr(fix, '/'); base = base ? base + 1 : fix; char stem[256]; snprintf(stem, sizeof stem, "%s/%s", tmpdir, base); char *dot = strrchr(stem, '.'); if (dot) *dot = '\0'; char tmpsrc[512]; snprintf(tmpsrc, sizeof tmpsrc, "%s.ww", stem); char cmd[4096]; snprintf(cmd, sizeof cmd, "cp %s/%s %s", cwd, fix, tmpsrc); runwait(cmd); /* Isolated WW_PKGCACHE: the default out/.pkgcache is shared with * the other phase-2 gates' sep builds (perturb + race). -o + the * absolute src/-I paths route every side file into tmpdir, so no * cwd dependency. */ char wexep[512]; snprintf(wexep, sizeof wexep, "%s_w", stem); snprintf(cmd, sizeof cmd, "WW_PKGCACHE='%s.cache' timeout 180 %s/ww_ww build --sep " "-o %s -I %s/lib/ww -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1", stem, bin, wexep, cwd, cwd, tmpsrc); if (runwait(cmd) != 0) { fprintf(stderr, "ww-links FAIL: ww_ww build --sep %s\n", fix); fail++; goto cleanup; } snprintf(cmd, sizeof cmd, "timeout 180 %s", wexep); int rc = runwait(cmd); if (rc != want) { fprintf(stderr, "ww-links FAIL: %s exit=%d want=%d\n", fix, rc, want); fail++; } cleanup: snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); runwait(cmd); } return fail ? -1 : 0; } /* Probe 6: ww-cgen byte-matches C-cgen on each selfhost fixture's * RESOLVED unit (sep root unit — see resolveunit). The producer feeds * w6c/wwdump that self-contained unit, so the byte-identity gate (rule * 10) belongs on that shape rather than the raw file. * * err.ww is included now (it was excluded under combined-mode, where its * unit inlined lib/fmt's BODY and the ww checker rejected fmt's * `formattable` match-exhaustiveness): the sep unit inlines only fmt's * `.wwi` INTERFACE, so the fmt body never reaches the checker and err's * root cgen converges cs==ww. */ static int probe_cgen_match(const char *bin) { const char *files[] = { "selfhost/cmd/wcc/err.ww", "lib/ww/syntax/tok.ww", "selfhost/test/smoke.ww", NULL, }; char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return -1; int fail = 0; for (int i = 0; files[i]; i++) { char td[64], unit[1024], a[256], b[256], cmd[2048]; if (resolveunit(bin, cwd, files[i], i, td, sizeof td, unit, sizeof unit) != 0) { fprintf(stderr, "cgen-match FAIL: %s — no resolved unit\n", files[i]); fail++; continue; } snprintf(a, sizeof a, "/tmp/wwd_cm_%d_c", getpid()); snprintf(b, sizeof b, "/tmp/wwd_cm_%d_w", getpid()); snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c %s > %s 2>/dev/null", bin, unit, a); runwait(cmd); snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump_ww -c %s > %s 2>/dev/null", bin, unit, b); runwait(cmd); char *bc = NULL, *bw = NULL; size_t nc = 0, nw = 0; int rc1 = slurp(a, &bc, &nc); int rc2 = slurp(b, &bw, &nw); if (rc1 < 0 || rc2 < 0) { fprintf(stderr, "cgen-match FAIL: read %s\n", files[i]); fail++; } else if (nc != nw || memcmp(bc, bw, nc) != 0) { fprintf(stderr, "cgen-match FAIL: %s — C %zu vs ww %zu bytes\n", files[i], nc, nw); fail++; } free(bc); free(bw); unlink(a); unlink(b); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); } return fail ? -1 : 0; } int main(void) { const char *bin = absbin(); if (!bin) return 1; int fail = 0; if (probe_codegen(bin) != 0) fail++; if (probe_smoke(bin) != 0) fail++; if (probe_dump_stable(bin) != 0) fail++; if (probe_dump_diff(bin) != 0) fail++; if (probe_resolve(bin) != 0) fail++; if (probe_ww_compile(bin) != 0) fail++; if (probe_cgen_match(bin) != 0) fail++; if (probe_ww_links(bin) != 0) fail++; if (fail) { fprintf(stderr, "selfhost: %d probe(s) failed\n", fail); return 1; } /* The ww1->ww2->ww3 binary fixed point is no longer reproducible here: * it self-compiled wwdump as a monolith from main.combined.ww, a model * combined-mode provided and --sep retires (the sep root unit holds only * the root body + dep INTERFACE stubs, not a linkable whole program). The * two axes it covered now live elsewhere: the self-application fixed point * (iterated self-compile converges, ww2==ww3==ww4) is `make bootstrap`'s * sep-path gate (migrated E2-C1); the orthogonal wwstage==cstage byte * identity is 995_self_rebuild, which drives ww_ww --sep over all five * tools (wwdump included) and byte-compares against the cstage binaries. */ printf("selfhost: codegen ok; smoke=42; wwdump stable; " "ww lexer matches C on 8 fixtures (-t); " "ww parser matches C on 32 fixtures (-a); " "ww checker resolves 3 fixtures' sep root units (-r); " "ww cgen compiles + runs 13 ww programs (incl. compound -= regression); " "ww cgen byte-matches C on 3 sep root units (err/tok/smoke); " "ww toolchain sep-builds + runs the syntax dep stack (sym_link=42)\n"); return 0; }