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

@@ -1,212 +0,0 @@
// selfhost/test/smoke.ww — end-to-end smoke for the selfhost path.
//
// Exercises the patterns the real ww-side compiler port will use:
// - bump arena allocator (mem.ww shape)
// - error idiom (T | str)
// - struct of fn pointers + ctx pointer (the io.stream-style
// polymorphism we use instead of interfaces)
// - byte-level scanning that mirrors the hot path inside lex.ww
// - 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.
//
// Note: only stack-local mutable state. Top-level `let` mutation
// requires a writable .data segment in w6l, which is a separate
// task; until then we exercise polymorphism via ctx pointers, which
// is what the real port wants anyway.
package test;
import os;
import strconv;
import ascii;
// --- bump arena ---------------------------------------------------------
type arena = struct {
buf: *u8,
off: u64,
cap: u64,
};
// In-place init. Returning a 24-byte struct by value isn't yet
// supported in w6c (SysV requires a hidden return-slot pointer for
// structs >16 bytes), so we initialize through a pointer like the
// real compiler does today.
fn arena_init(a: *arena, buf: *u8, cap: u64) void = {
a.buf = buf;
a.off = 0u64;
a.cap = cap;
};
fn arena_alloc(a: *arena, n: u64) *u8 = {
if (n > a.cap - a.off) { return nil; };
let p: *u8 = a.buf + a.off;
a.off += n;
return p;
};
// --- (i32 | str) error idiom -------------------------------------------
fn checked_div(num: i32, den: i32) (i32 | str) = {
if (den == 0) { return "div by zero"; };
return num / den;
};
// --- struct-of-fn-pointer polymorphism ---------------------------------
//
// A trivial "writer" abstraction: a function pointer plus a context.
// This mirrors how io.stream / Plan 9 Bio work. The ctx pointer lets
// the implementation own its own state without a global.
type counter = struct {
n: i32,
};
type writer = struct {
ctx: *void,
emit: fn(ctx: *void, b: u8) void,
};
fn count_emit(ctx: *void, b: u8) void = {
let c: *counter = ctx: *counter;
c.n += 1;
};
// --- size/align/offset typed-builtin fixtures (#42) --------------------
type point = struct {
x: i32,
y: i32,
};
// Mixed-alignment struct: i8 lays at 0, then i64 needs to skip to
// offset 8 (the i64's natural align). Probe asserts both ends.
type mixalign = struct {
tag: i8,
val: i64,
};
// --- byte scanner like lex.ww's hot path -------------------------------
fn count_digits(s: str) i32 = {
let i: i32 = 0;
let n: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c >= 48u8) {
if (c <= 57u8) { n += 1; };
};
i += 1;
};
return n;
};
// --- entry --------------------------------------------------------------
export fn main() i32 = {
// Probe 1 — arena hands out distinct pointers, refuses oversize.
let buf: [256]u8;
let a: arena;
arena_init(&a, buf.ptr, 256u64);
let p1: *u8 = arena_alloc(&a, 32u64);
let p2: *u8 = arena_alloc(&a, 32u64);
if (p1 == nil) { return 1; };
if (p2 == nil) { return 2; };
if (p1 == p2) { return 3; };
let p3: *u8 = arena_alloc(&a, 1024u64);
if (p3 != nil) { return 4; };
// Probe 2 — error union both ways.
let r_ok: (i32 | str) = checked_div(84, 2);
let r_bad: (i32 | str) = checked_div(1, 0);
let acc: i32 = 0;
match (r_ok) {
case let v: i32 => acc = v;
case let e: str => return 5;
};
if (acc != 42) { return 6; };
match (r_bad) {
case let v: i32 => return 7;
case let e: str => acc = e.len: i32;
};
if (acc != 11) { return 8; }; // len("div by zero") == 11
// Probe 3 — struct-of-fn-pointer dispatch via ctx pointer.
let c: counter = counter { n = 0 };
let w: writer = writer { ctx = (&c): *void, emit = count_emit };
w.emit(w.ctx, 65u8);
w.emit(w.ctx, 66u8);
w.emit(w.ctx, 67u8);
if (c.n != 3) { return 9; };
// Probe 4 — byte scan over a literal.
let dn: i32 = count_digits("ww123abc");
if (dn != 3) { return 10; };
// Probe 5 — strconv round-trip via the real stdlib.
let s: str = strconv.i64tos(4242i64, strconv.base.DEC);
if (s.len != 4) { return 11; };
if (s.ptr[0] != 52u8) { return 12; }; // '4'
if (s.ptr[3] != 50u8) { return 13; }; // '2'
// Probe 6 — ascii classifications (rune-taking, Hare-shaped).
if (!ascii.isdigit(53)) { return 14; }; // '5'
if (ascii.isdigit(65)) { return 15; }; // 'A' is not a digit
if (!ascii.isalpha(122)) { return 16; }; // 'z'
if (!ascii.isxdigit(70)) { return 17; }; // 'F'
if (ascii.isxdigit(71)) { return 18; }; // 'G' is not hex
if (ascii.tolower(65) != 97) { return 19; }; // 'A' -> 'a'
if (ascii.toupper(122) != 90) { return 20; }; // 'z' -> 'Z'
// Probe 7 — file open/read via the new os APIs. /proc/self/cmdline
// 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
// 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.
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.
let n: i64 = os.read(fd, rbuf.ptr, 128u64);
os.close(fd);
if (n <= 0i64) { return 22; };
// Probe 8 — size(T) / align(T) / offset(e.f) typed-builtin folds
// (#42). Each call folds to an N_INTLIT at check time; cgen
// materialises the literal as a plain `MOVQ $N, AX`. Mirrors
// cstage cmd/wcc/check.c:907-960 byte-for-byte on this corpus.
if (size(str) != 24) { return 23; }; // str IS []u8: {ptr,len,cap} 24B (#1/Phase 3)
if (size(i64) != 8) { return 24; };
if (size(i32) != 4) { return 25; };
if (align(i64) != 8) { return 26; };
if (align(i32) != 4) { return 27; };
// Initialize struct locals explicitly so the cgen path doesn't
// drift from cstage on bare `let X: T;` zero-init (pre-existing
// wwstage divergence outside #42).
let pt: point = point { x = 0, y = 0 };
if (offset(pt.x) != 0) { return 28; };
if (offset(pt.y) != 4) { return 29; };
let mx: mixalign = mixalign { tag = 0i8, val = 0i64 };
if (offset(mx.tag) != 0) { return 30; };
if (offset(mx.val) != 8) { return 31; }; // align-padded to 8
// Width breadth: smallest prim, ptr, slice, struct (8B + padded),
// covering astsize's TPTR/TSLICE/TNAME-resolve-to-struct arms.
if (size(i8) != 1) { return 32; };
if (align(i8) != 1) { return 33; };
if (size(*i32) != 8) { return 34; };
if (size([]i32) != 24) { return 35; };
if (size(point) != 8) { return 36; };
if (size(mixalign) != 16) { return 37; };
return 42;
};

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