diff --git a/Makefile b/Makefile index 73a357eb..4ab41e6a 100644 --- a/Makefile +++ b/Makefile @@ -159,7 +159,7 @@ $(BIN)/w6l: $(W6L_OBJ) | $(BIN) $(OBJ)/w6l/%.o: cmd/w6l/%.c cmd/w6l/l.h | $(OBJ)/w6l $(CC) $(CFLAGS) -Icmd/w6l -c -o $@ $< -# ---- ww-side wwdump (the lexer port, exercised by 990_selfhost) -------- +# ---- ww-side wwdump (the frontend dump tool; oracle for 950/994) ------- # Built via the user-facing ww driver. The frontend (lex, tok, ast, # parse, typ, sym) is the `syntax` package in lib/ww/syntax/; the # compiler-only bits (check, cgen*) stay in selfhost/cmd/wcc/. Output named wwdump_ww @@ -403,7 +403,7 @@ BYTEID_WRAPPER_SOURCES = test/wcc/631_def_neg_global.c \ test/wcc/989_m2wwi_run.c test/wcc/989_structlocal_frame.c \ test/wcc/989_wwileaf_run.c test/wcc/989_lib_byteid.c BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ - test/wcc/990_selfhost.c test/wcc/991_w6a_ww.c \ + test/wcc/991_w6a_ww.c \ test/wcc/992_w6l_ww.c test/wcc/993_ww_ww.c \ test/wcc/994_w6c_ww.c test/wcc/995_self_rebuild.c PLATFORM_WRAPPER_SOURCES = test/wcc/996_dyn_ww.c @@ -438,6 +438,7 @@ $(BIN)/test_%: test/wcc/%.c test/wcc/wwtestpkg.h $(WRAPPER_TOOLS) | $(BIN) # launchers added no assertion beyond this exit status. LIBRARY_TESTS = lib/errors/errnotest.ww lib/ascii/asciitest.ww \ lib/ww/syntax/toktest.ww lib/ww/syntax/asttest.ww \ + lib/ww/syntax/symtest.ww \ lib/strconv/test/ftostest.ww lib/strconv/test/stoftest.ww \ lib/strconv/test/inttest.ww lib/strings/stringstest.ww \ lib/bytes/bytestest.ww lib/encoding/utf8/utf8test.ww \ @@ -576,7 +577,7 @@ test-lang-byteid: $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww # .s and their both-stage reject parity is owned by the fixture corpus. DATABYTEID_DIR = $(OUT)/databyteid DATABYTEID_FILES = $(wildcard test/wcc/data/*/case.ww) -DATABYTEID_EXPECTED_MIN = 910 +DATABYTEID_EXPECTED_MIN = 911 # Known cs!=ww divergences (loud over blind, the 989_lib_byteid DIVERGE # discipline): each entry must still build on both stages AND still differ. # When a compiler fix lands the entry fails demanding graduation out of diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 766aea4c..ee880221 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -32,12 +32,12 @@ categories out of the ordinary developer target. | Fixed point and self-host | `test-bootstrap` | | Host linker/platform behavior | `test-platform` | -The live declarative compiler corpus has 1,224 fixtures and 2,448 C/WW cells: +The live declarative compiler corpus has 1,225 fixtures and 2,450 C/WW cells: 314 expected rejections (290 shared and 24 stage-specific), 12 compile-only -successes, 136 exit-zero programs, and 762 explicit-exit programs. +successes, 136 exit-zero programs, and 763 explicit-exit programs. -175 native C carriers remain. They are partitioned exactly once as five -in-process units, 27 byte/artifact gates, seven bootstrap gates, one platform +174 native C carriers remain. They are partitioned exactly once as five +in-process units, 27 byte/artifact gates, six bootstrap gates, one platform gate, and 135 residual compiler, package-layout, ABI, diagnostic-observer, driver, linker, or FFI gates. Rows migrated to fixtures or native `@test` owners were removed from those carriers; there is no compatibility execution @@ -59,7 +59,7 @@ wwstage-driver leg. | `test-library` | Library-owned `@test` behavior plus four direct standalone-source C/WW compilation checks | | `test-commit` | Unit + compiler + package + language + library behavior | | `test-byteid` | Compiler-output identity gates | -| `test-bootstrap` | Fixed-point bootstrap plus the 950/990–995 native gates | +| `test-bootstrap` | Fixed-point bootstrap plus the 950/991–995 native gates | | `test-platform` | Host-dependent dynamic-link gate | | `test-wwfixture` | Fixture CLI/process/protocol integration boundary | | `test-all` | Commit + byte-ID + bootstrap + platform + test-infrastructure checks | @@ -196,7 +196,7 @@ parity is owned by the fixture corpus itself. Known cs/ww divergences are pinned in `DATABYTEID_DIVERGED` with the `989_lib_byteid` discipline: a pinned fixture must still build on both stages and still differ, so a compiler fix fails the gate demanding graduation rather than silently -widening coverage. The full sweep compares 910 fixtures in about a minute +widening coverage. The full sweep compares 911 fixtures in about a minute and is scratch-rooted under `out/`, not `/tmp`. Byte identity is an explicit proof gate. It is not a prerequisite of `test` or @@ -204,7 +204,7 @@ Byte identity is an explicit proof gate. It is not a prerequisite of `test` or ## Bootstrap, subprocesses, and CSP -Stage-2-through-stage-4 fixed-point proofs and the 950/990–995 self-host gates +Stage-2-through-stage-4 fixed-point proofs and the 950/991–995 self-host gates are reachable through `test-bootstrap` and `test-all`, never through `test` or `test-commit`. Cold ordinary targets may still build their C- and WW-stage tool prerequisites once; they do not iterate those tools to a fixed point. The C diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index 96c4e3c2..6ef3fbdd 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -1,13 +1,13 @@ package wwfixture; def protocolversion: i32 = 1; -def corpuscount: i32 = 1224; +def corpuscount: i32 = 1225; def errorcount: i32 = 314; def compilecount: i32 = 12; def runcount: i32 = 136; -def runexitcount: i32 = 762; -def nativecount: i32 = 2448; -def corpushash: str = "8b522e42cbce8b389e33e917229dede8045d85f1af0c54630cfef83c4ef242be"; +def runexitcount: i32 = 763; +def nativecount: i32 = 2450; +def corpushash: str = "a2e47b2967d51a517d39598caf69c2d23618ba488594a0b7e33fded27cc62f93"; type directive = enum i32 { ERROR = 0, diff --git a/lib/ww/syntax/ast.ww b/lib/ww/syntax/ast.ww index 84c1acec..d3c98a82 100644 --- a/lib/ww/syntax/ast.ww +++ b/lib/ww/syntax/ast.ww @@ -14,13 +14,12 @@ import strconv; // ---- Nkind ------------------------------------------------------------ // -// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal so -// the AST diff probe in 990_selfhost works. +// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal +// to the C side (rule-6 data-shape mirror). // Mirror of the C `Nkind` enum in cmd/wcc/ww.h. Numeric values are -// explicit and must stay in sync — the 990_selfhost test diffs -// astprint against the C side byte-for-byte. Tail-appended entries -// (TYPETEST onward) preserve every prior N_* value. +// explicit and must stay in sync with the C side. Tail-appended +// entries (TYPETEST onward) preserve every prior N_* value. export type nkind = enum i32 { N_NONE = 0, diff --git a/lib/ww/syntax/lex.ww b/lib/ww/syntax/lex.ww index 3f64fbd8..f3557cb8 100644 --- a/lib/ww/syntax/lex.ww +++ b/lib/ww/syntax/lex.ww @@ -1,9 +1,8 @@ // lib/ww/syntax/lex.ww — port of cmd/wcc/lex.c. // // The DFA, the helpers, and the order of decisions all mirror the C -// version exactly. The 990_selfhost test diffs the resulting token -// stream against the C-side wwdump byte-for-byte; any divergence is -// a port bug. +// version exactly; any divergence surfaces as a cs/ww byte split in +// the compiler-output identity gates and is a port bug. // // Calling-convention note: w6c can't yet pass or return structs >16 // bytes by value, so `tok` and `pos` are passed by pointer (out diff --git a/lib/ww/syntax/symtest.ww b/lib/ww/syntax/symtest.ww new file mode 100644 index 00000000..769c98d0 --- /dev/null +++ b/lib/ww/syntax/symtest.ww @@ -0,0 +1,47 @@ +// symtest — behavior pin for [[newscope]]/[[scopedefine]]/[[scopelookup]] +// (hashtable scope semantics: define, same-scope duplicate reject, +// kind-preserving lookup, not-found nil). Run with +// `ww test -I lib/ww lib/ww/syntax/symtest.ww`. +// +// Migrated from selfhost/test/sym_link.ww (the 990_selfhost link +// probe): the toolchain-link half of that probe is owned by the +// fixture corpus' ww-stage cells, so only the scope behavior rows +// survive, as in-language rows. `package main` + bare `import syntax` +// mirrors toktest/asttest. + +package main; + +import syntax; + +@test fn scope_define_lookup() void = { + let s: *scope = newscope(nil); + assert(!(s == nil)); + + let r1: *sym = scopedefine(s, "foo", skind.SK_VAR, nil, nil); + assert(!(r1 == nil)); + let r2: *sym = scopedefine(s, "bar", skind.SK_TYPE, nil, nil); + assert(!(r2 == nil)); + + let l1: *sym = scopelookup(s, "foo"); + assert(!(l1 == nil)); + assert(!(l1.skind != skind.SK_VAR)); + let l2: *sym = scopelookup(s, "bar"); + assert(!(l2 == nil)); + assert(!(l2.skind != skind.SK_TYPE)); +}; + +@test fn scope_duplicate_reject() void = { + let s: *scope = newscope(nil); + assert(!(s == nil)); + let r1: *sym = scopedefine(s, "foo", skind.SK_VAR, nil, nil); + assert(!(r1 == nil)); + let r3: *sym = scopedefine(s, "foo", skind.SK_VAR, nil, nil); + assert(!(r3 != nil)); +}; + +@test fn scope_notfound_nil() void = { + let s: *scope = newscope(nil); + assert(!(s == nil)); + let l3: *sym = scopelookup(s, "baz"); + assert(!(l3 != nil)); +}; diff --git a/lib/ww/syntax/tok.ww b/lib/ww/syntax/tok.ww index a0550511..da7fb62e 100644 --- a/lib/ww/syntax/tok.ww +++ b/lib/ww/syntax/tok.ww @@ -1,10 +1,9 @@ // lib/ww/syntax/tok.ww — port of cmd/wcc/tok.c plus the Tkind / // Tok / Pos shapes from cmd/wcc/ww.h. // -// Token kind values must stay numerically equal to the C side: the -// 990_selfhost test diffs ww-side wwdump output against C-side -// wwdump output, byte-for-byte. Reordering this list shifts the -// integers and breaks the diff. +// Token kind values must stay numerically equal to the C side +// (rule-6 data-shape mirror of cmd/wcc/ww.h). Reordering this list +// shifts the integers and splits the two frontends. // // Bottom of file: tokprint, which emits one token per line in a // format identical to cmd/wcc/tok.c:tokprint(). @@ -17,8 +16,7 @@ import strings; // ---- tkind ------------------------------------------------------------ // Mirror of the C `Tkind` enum in cmd/wcc/ww.h. Numeric values are -// explicit and must stay in sync — the 990_selfhost test diffs wwdump -// output against the C side, byte for byte. +// explicit and must stay in sync with the C side. export type tkind = enum i32 { TK_NONE = 0, @@ -112,7 +110,7 @@ export type tkind = enum i32 { TK_FATARROW = 81, // Tail-appended values — keeps every prior TK_* numeric value - // stable for the 990_selfhost byte-diff against the C side. + // stable against the C side. TK_IS = 82, TK_VOID = 83, TK_YIELD = 84, diff --git a/lib/ww/syntax/toktest.ww b/lib/ww/syntax/toktest.ww index 3b5dcc28..f0986d16 100644 --- a/lib/ww/syntax/toktest.ww +++ b/lib/ww/syntax/toktest.ww @@ -11,8 +11,8 @@ // switch (STR/IDENT/ERR vs INT/RUNE vs the value-less default) and, // through the STR text, fputq's full escape switch (\\, ", \n, \t, // \r, the c<0x20 and c==0x7f \xNN arms, and the printable tail) — -// branches the 990_selfhost corpus does not exercise (source tokens -// hold raw `\`+`n`, never a literal control byte). +// branches ordinary source tokens do not exercise (they hold raw +// `\`+`n`, never a literal control byte). // A failing row aborts via the assert/abort builtin (task #5 @test // conversion); per-row exit-code pinpoint is intentionally dropped (the // abort reports the file, not the row; drew-t2-conversion-spec sec.5). diff --git a/selfhost/test/sym_link.ww b/selfhost/test/sym_link.ww deleted file mode 100644 index d7509e59..00000000 --- a/selfhost/test/sym_link.ww +++ /dev/null @@ -1,40 +0,0 @@ -// selfhost/test/sym_link.ww — link-and-run probe for the ww-cgen -// against the sym/typ/ast dep stack. Exercises hashtable scope -// (sym), and pulls in typ/ast as type carriers. -// Returns 42 on success; smaller values name the probe that broke. - -package test; - -import syntax; - -export fn main() i32 = { - let s: *scope = newscope(nil); - if (s == nil) { return 2; }; - - let n1: str = "foo"; - let r1: *sym = scopedefine(s, n1, skind.SK_VAR, nil, nil); - if (r1 == nil) { return 3; }; - - let n2: str = "bar"; - let r2: *sym = scopedefine(s, n2, skind.SK_TYPE, nil, nil); - if (r2 == nil) { return 4; }; - - // Duplicate define in same scope must fail. - let r3: *sym = scopedefine(s, n1, skind.SK_VAR, nil, nil); - if (r3 != nil) { return 5; }; - - let l1: *sym = scopelookup(s, n1); - if (l1 == nil) { return 6; }; - if (l1.skind != skind.SK_VAR) { return 7; }; - - let l2: *sym = scopelookup(s, n2); - if (l2 == nil) { return 8; }; - if (l2.skind != skind.SK_TYPE) { return 9; }; - - // Not-found lookup returns nil. - let n3: str = "baz"; - let l3: *sym = scopelookup(s, n3); - if (l3 != nil) { return 10; }; - - return 42; -}; diff --git a/selfhost/test/tagged_ptr_ret.ww b/selfhost/test/tagged_ptr_ret.ww deleted file mode 100644 index c05d59a1..00000000 --- a/selfhost/test/tagged_ptr_ret.ww +++ /dev/null @@ -1,99 +0,0 @@ -// selfhost/test/tagged_ptr_ret.ww — smoke for the (*T | nomem) return ABI. -// -// Task #25 (ww-strings-redesign): cstage used to fold `(*T | !void)`-shaped -// returns into the nullable-pointer-in-AX encoding (richer optimization), -// while wwstage emitted the documented general tagged-return ABI -// (AX=tag, DX=word0). Per CLAUDE.md rule 10 the richer side aligns DOWN — -// cstage now restricts the nullable fold to literal `void` variants, so -// `(*T | nomem)` takes the general path on both stages and the 993/995 -// byte-identity tests stay green. -// -// Task #29: `nomem` is now predeclared in the compiler universe scope, so -// neither `import errors;` nor a local `type nomem = !void;` is needed. -// Two match arms cover both runtime outcomes — success unwrap (tag=0, -// ptr payload in DX) and error propagation (tag=1) — exercising the -// same AX/DX ABI both stages must agree on. -// -// Task #30: the `alloc` builtin itself now returns `(*T | nomem)`. The -// allocbox arm below propagates the builtin's tagged return through -// the enclosing fn via `?` against a same-shape `(*point | nomem)` -// — matching the team-lead spec's "exercise `alloc(T)?` against a -// real function returning `(T | nomem)`". - -package main; - -import fmt; -import os; - -type point = struct { x: i32, y: i32 }; - -fn alloc1(fail: i64) (*u8 | nomem) = { - if (fail != 0i64) { let e: nomem; return e; }; - let buf: [1]u8; - return buf.ptr; -}; - -fn caller(fail: i64) (*u8 | nomem) = { - let p: *u8 = alloc1(fail)?; - return p; -}; - -fn allocbox() (*point | nomem) = { - let p: *point = alloc(point { x = 3, y = 4 })?; - return p; -}; - -// Task #32: slice-form `let s: []T = alloc([], n)!;` shortcut. Both -// stages must lower to `n*esz` bytes via rt_malloc, abort on null, and -// build a {ptr, 0, n} header in the let slot. Pre-#32 wwstage fell -// through to cgalloc, allocating 8B and dropping the slice header -// entirely — silent miscompile. Cap-only would pass on a junk header -// pointing to dead memory; write-then-read on s[0]/s[cap-1] proves -// the ptr field is a real rt_malloc'd region (would SIGSEGV otherwise). -// IMULQ esz path is currently unreachable from user code — check.c -// pins the alloc shape to []u8 (cstage check.c:1052-1082) — so this -// row only exercises esz=1; the cgen elemsizeofc resolution stays -// defensive against a future check.c relaxation. -fn sliceshort() i32 = { - let s: []u8 = alloc([], 16)!; - if (s.cap != 16) { return -1i32; }; - s[0] = 42u8; - s[15] = 99u8; - return (s[0]: i32) + (s[15]: i32); -}; - -export fn main() i32 = { - let rc: i32 = 0; - match (caller(0i64)) { - case let p: *u8 => { - fmt.println("ok"); - if (p == nil) { rc = 1; }; - }; - case nomem => { - fmt.println("unexpected nomem on ok path"); - rc = 2; - }; - }; - match (caller(1i64)) { - case let p: *u8 => { - fmt.println("unexpected ptr on err path"); - rc = 3; - }; - case nomem => { - fmt.println("nomem as expected"); - }; - }; - match (allocbox()) { - case let p: *point => { - fmt.println("allocbox ok"); - if (p.x * p.x + p.y * p.y != 25) { rc = 4; }; - }; - case nomem => { - fmt.println("allocbox unexpected nomem"); - rc = 5; - }; - }; - if (sliceshort() != 141i32) { rc = 6; } - else { fmt.println("sliceshort ok"); }; - return rc; -}; diff --git a/selfhost/test/trypromote.ww b/selfhost/test/trypromote.ww deleted file mode 100644 index 06be42ad..00000000 --- a/selfhost/test/trypromote.ww +++ /dev/null @@ -1,55 +0,0 @@ -// selfhost/test/trypromote.ww — smoke for `?` propagation through a -// `!void`-shaped alias. -// -// Task #2 (ww-strings-redesign): proves cgen's TRYPROP tag-remap works -// for the exact pattern lib/errors + os.alloc are about to lean on. We -// cannot smoke this against lib/ today because there are zero lib-side -// `?` users on a `!void` alias yet. Mirrors the shlex.syntaxerr shape -// (lib/shlex/shlex.ww:112) for the "nomem" stub. -// -// Not table-driven on purpose: same-shape `?` is a single cgen emit -// pattern, so varying the operand exercises the same asm. The two -// match arms below cover both runtime outcomes (success unwrap, error -// propagation); asm-level regressions of task #18 are gated by the -// byte-identity tests (994_w6c_ww, 995_self_rebuild). - -package test; - -import fmt; - -// #29: `nomem` is predeclared in the universe scope — no local -// `type nomem = !void;` (or `import errors;`) needed. - -fn stub(fail: i64) (i64 | nomem) = { - if (fail != 0i64) { let e: nomem; return e; }; - return 42i64; -}; - -fn caller(fail: i64) (i64 | nomem) = { - let v = stub(fail)?; - return v + 1i64; -}; - -export fn main() i32 = { - let rc: i32 = 0; - match (caller(0i64)) { - case let n: i64 => { - fmt.println("ok ", n); - if (n != 43i64) { rc = 1; }; - }; - case nomem => { - fmt.println("unexpected nomem on ok path"); - rc = 2; - }; - }; - match (caller(1i64)) { - case let n: i64 => { - fmt.println("unexpected ", n, " on err path"); - rc = 3; - }; - case nomem => { - fmt.println("nomem as expected"); - }; - }; - return rc; -}; diff --git a/selfhost/test/uses.ww b/selfhost/test/uses.ww deleted file mode 100644 index ef479d85..00000000 --- a/selfhost/test/uses.ww +++ /dev/null @@ -1,30 +0,0 @@ -// selfhost/test/uses.ww — AST-diff fixture. Grows as parse.ww does. -// Currently exercises: `use IDENT;`, `def NAME: TYPE = LIT;`, -// `type NAME = TYPE;` (alias + struct), top-level `let NAME: TYPE = LIT;`. -// -// Function declarations are still recovered past — the body parser -// is the next major chunk. See lib/ww/parse.ww header. - -package test; - -import os; -import fmt; - -def MAX_LINE: i32 = 4096; -def NAME: str = "ww"; -def READY: bool = true; - -type byte = u8; -type rune = i32; -type pos = struct { - file: str, - line: i32, - col: i32, -}; -type buffer = [4096]u8; -type bytes = []u8; -type linkptr = *byte; - -let nerrors: i32 = 0; -let nwarnings: i32 = 0; -let prog_name: str = "ww"; diff --git a/test/wcc/901_asserttyped_gap.c b/test/wcc/901_asserttyped_gap.c index 84df0c2a..a3559b5b 100644 --- a/test/wcc/901_asserttyped_gap.c +++ b/test/wcc/901_asserttyped_gap.c @@ -147,7 +147,7 @@ main(void) struct { const char *rel; const char *cls; int want; int sep; } manifest[] = { { "lib/math/checked/checked_test.ww", "A module-qual N_DOT call result", 0, 1 }, - { "selfhost/test/smoke.ww", + { "test/wcc/data/selfhost_smoke/case.ww", "B fn-ptr struct-field call", 0, 1 }, { "lib/encoding/utf8/utf8.ww", "C abort intrinsic callee", 0, 1 }, diff --git a/test/wcc/990_selfhost.c b/test/wcc/990_selfhost.c deleted file mode 100644 index ba05d931..00000000 --- a/test/wcc/990_selfhost.c +++ /dev/null @@ -1,824 +0,0 @@ -/* - * 990_selfhost — explicit bootstrap/self-host gate. 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 -#include "wwtestpkg.h" - -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; } - wwtest_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` 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 %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` - * 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 , 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); - /* -o plus absolute source and include paths route every side file - * into tmpdir, so the probe has no cwd dependency. */ - char wexep[512]; - snprintf(wexep, sizeof wexep, "%s_w", stem); - snprintf(cmd, sizeof cmd, - "timeout 180 %s/ww_ww build " - "-o %s -I %s/lib/ww -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1", - bin, wexep, cwd, cwd, tmpsrc); - if (runwait(cmd) != 0) { - fprintf(stderr, "ww-links FAIL: ww_ww build %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 - * the retired combined mode provided (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 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; -} diff --git a/selfhost/test/smoke.ww b/test/wcc/data/selfhost_smoke/case.ww similarity index 91% rename from selfhost/test/smoke.ww rename to test/wcc/data/selfhost_smoke/case.ww index 6892f821..336c256a 100644 --- a/selfhost/test/smoke.ww +++ b/test/wcc/data/selfhost_smoke/case.ww @@ -1,4 +1,6 @@ -// selfhost/test/smoke.ww — end-to-end smoke for the selfhost path. +//ww:run-exit 42 +// selfhost_smoke — end-to-end smoke for the selfhost patterns (migrated +// from selfhost/test/smoke.ww, the 990_selfhost probe fixture). // // Exercises the patterns the real ww-side compiler port will use: // - bump arena allocator (mem.ww shape) @@ -9,7 +11,7 @@ // - strconv round-trip via the real stdlib // // `main` returns 42 when every check passes, 1..N on failure -// indicating which probe broke. The 990_selfhost test asserts 42. +// indicating which probe broke. The compiler-fixture corpus asserts 42. // // Note: only stack-local mutable state. Top-level `let` mutation // requires a writable .data segment in w6l, which is a separate @@ -164,19 +166,15 @@ export fn main() i32 = { // always exists on Linux, no write side, and is non-empty. let path: str = "/proc/self/cmdline"; // Use raw os.open here (returns i32 with -errno) for the same - // reason as os.read below: probe 6 in 990_selfhost compiles + // reason as os.read below: the retired 990 probes compiled // smoke.ww standalone (no `use` expansion), so cross-module type - // references like `os.oserror` and `os.flag` don't resolve at - // that step. RDONLY is 0; passing the literal keeps the call - // site standalone-compilable to byte-identical asm on both - // compilers. + // references like `os.oserror` and `os.flag` kept off the hot + // path. RDONLY is 0; the literal keeps the historical shape. let fd: i32 = os.open(path, 0, 0i32); if (fd < 0) { return 21; }; let rbuf: [128]u8; // Use raw os.read here (single syscall, plain i64) instead of - // os.readall: the 990 cgen-match probe compiles smoke.ww - // standalone without `use os;` expansion, so cross-module type - // references like `os.oserror` can't be resolved. + // os.readall, keeping the retired standalone-compile shape. let n: i64 = os.read(fd, rbuf.ptr, 128u64); os.close(fd); if (n <= 0i64) { return 22; };