test/wcc: retire 990_selfhost; its live assertions move to their owners

Every probe's assertion is owned by a current gate: the compile and
link probes by make all and the bootstrap fixed point; build/run and
cs/ww byte identity by the fixture corpus, test-data-byteid, and
989_lib_byteid; wwstage driver and toolchain parity by 993/995;
checker-diagnostic parity by the corpus' both-stage //ww:error rows.
The wwdump -t/-a dump-parity probes gated the frontend port's
convergence, which the compiler-output identity gates now own end to
end; carrier ran green at retirement.

What was still uniquely alive migrates: smoke.ww becomes corpus
fixture selfhost_smoke (upgraded from a cstage-only build to both
frontends, byte-identical, exit 42 on both toolchains; corpus pins
move to 1,225/763/2,450 with the new identity hash in the same
commit), and sym_link.ww's scope/sym behavior rows become in-language
lib/ww/syntax/symtest.ww under LIBRARY_TESTS. uses.ww (parser-stub-era
-a fixture) and the already-orphaned tagged_ptr_ret.ww/trypromote.ww
retire with the probe corpus. Bootstrap native gates drop to six;
frontend numeric-sync comments now cite the rule-6 mirror instead of
the retired diff probe.
This commit is contained in:
2026-08-07 23:31:59 +09:00
parent cdc8bda721
commit 83c8a4f34f
15 changed files with 84 additions and 1090 deletions

View File

@@ -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

View File

@@ -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/990995 native gates |
| `test-bootstrap` | Fixed-point bootstrap plus the 950/991995 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/990995 self-host gates
Stage-2-through-stage-4 fixed-point proofs and the 950/991995 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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

47
lib/ww/syntax/symtest.ww Normal file
View File

@@ -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));
};

View File

@@ -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,

View File

@@ -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).

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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";

View File

@@ -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 },

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#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 <fixture>'s RESOLVED translation unit the way
* the sep driver does. `ww build` composes the root package's
* <stem>.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;
}

View File

@@ -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; };