ww test: fork-isolated record-and-continue harness (lib/test, both stages)

lib/test/run.ww: fork+wait4 runner; each @test runs in its own child,
abort/SEGV/FPE decoded from wait-status, failures recorded and the run
continues; exit = fail count. Tests are hermetic: module globals do not
persist test-to-test (fresh fork image; sanctioned divergence from
harec's shared-process __test_main, no setjmp/signal layer needed).
-T synth (both stages) emits a module-global (str,*fn() void) table +
return run(table) instead of straight-line calls. Driver twins bundle
lib/test under test mode and gain ww test -c/-o (go test -c) so the
byte-id gates diff the same artifact the real path builds. Gates
989/910/997 rewired onto it; new 911 pins record-and-continue across
all three fault classes; 949 +3 rows. (#17-team commit-2)
This commit is contained in:
2026-06-11 00:08:39 +09:00
parent 08a76cf4c8
commit 16c83e70d3
15 changed files with 961 additions and 194 deletions

View File

@@ -15801,14 +15801,20 @@ export fn checkfile(c: *checker, file: *node) void = {
// untouched: the appended N_FNDECL rides cgfn, byte-id by
// construction (rule 10; cstage twin at cmd/wcc/check.c).
//
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
// reinstatable post-CSP; documented departure, not a hare cite):
// D1 no __test_array section — straight-line calls; D2 call @test
// fns by their real symbols (no testfunc.%d); D3 no per-test
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
// filter — source/collection order; D5 no reflective file:line, no
// per-test ok/FAIL line — failure is the nonzero process exit.
// #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec
// §a): instead of straight-line `foo(); bar();` calls (which abort the
// whole run on the first failing @test — old D3), synthesize a value
// table `[](str, *fn() void) = {("foo", &foo), ...}` + a single call to
// the lib/test runner. The runner forks per test and reads the child's
// wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test and the
// run proceeds (lib/test/run.ww). Table = harec __test_array reduced to
// a value table (D1 no section; D2 real symbols). RETAINED reductions
// (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter
// (source/collection order; fnmatch is #17 commit-3); D5 no reflective
// file:line (the runner prints `name ... ok/FAIL` + a count summary).
// Table rides cgen's #117 slice-of-tuple-global path; `run` resolves
// bare against the auto-bundled lib/test (synth runs post-pass-1, so
// run sits in the same flat "" bucket as the @test fns).
if (c.istest != 0) {
let pf: str = file.file;
let pl: i32 = file.line;
@@ -15824,9 +15830,11 @@ export fn checkfile(c: *checker, file: *node) void = {
};
u = u.next;
};
// (c) collect @test fns in file.list order; build the body chain.
let bhead: *node = nil;
let btail: *node = nil;
// (c) collect @test fns in file.list order; build one table row
// `("<name>", &<name>)` per validated @test fn.
let rhead: *node = nil;
let rtail: *node = nil;
let ntest: i32 = 0;
let t: *node = file.list;
for (t != nil) {
if (t.kind == nkind.N_FNDECL) {
@@ -15853,28 +15861,77 @@ export fn checkfile(c: *checker, file: *node) void = {
cerr("' must be fn() void\n");
c.errs += 1;
} else {
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
cid.str = t.str;
call.lhs = cid;
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
es.lhs = call;
if (bhead == nil) { bhead = es; }
else { btail.next = es; };
btail = es;
let nm: *node = newnode(nkind.N_STRLIT, pf, pl, pc);
nm.str = t.str;
let id: *node = newnode(nkind.N_IDENT, pf, pl, pc);
id.str = t.str;
let amp: *node = newnode(nkind.N_UN, pf, pl, pc);
amp.op = tkind.TK_AMP;
amp.lhs = id;
let row: *node = newnode(nkind.N_TUPLE, pf, pl, pc);
row.list = nm;
nm.next = amp;
if (rhead == nil) { rhead = row; }
else { rtail.next = row; };
rtail = row;
ntest += 1i32;
};
};
};
t = t.next;
};
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
zero.uval = 0u64;
ret.lhs = zero;
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
btail = ret;
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
body.list = bhead;
let tab: *node = nil;
if (ntest == 0i32) {
// no @test fns in this unit — exit 0, nothing to run.
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
zero.uval = 0u64;
ret.lhs = zero;
body.list = ret;
} else {
// const __wwtests: [](str, *fn() void) = [rows...];
// wwstage tuple-type elements wrap in N_TPARAM (parse.ww:308).
let e0: *node = newnode(nkind.N_TNAME, pf, pl, pc);
e0.str = "str";
let p0: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
p0.lhs = e0;
let vret: *node = newnode(nkind.N_TNAME, pf, pl, pc);
vret.str = "void";
let vfn: *node = newnode(nkind.N_TFN, pf, pl, pc);
vfn.lhs = vret;
let e1: *node = newnode(nkind.N_TPTR, pf, pl, pc);
e1.lhs = vfn;
let p1: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
p1.lhs = e1;
let tup: *node = newnode(nkind.N_TTUPLE, pf, pl, pc);
tup.list = p0;
p0.next = p1;
let tsl: *node = newnode(nkind.N_TSLICE, pf, pl, pc);
tsl.lhs = tup;
let arr: *node = newnode(nkind.N_ARRLIT, pf, pl, pc);
arr.list = rhead;
tab = newnode(nkind.N_LET, pf, pl, pc);
tab.op = tkind.TK_CONST;
tab.str = "__wwtests";
tab.lhs = tsl;
tab.rhs = arr;
// pass 1 already ran; install the table name now so main
// resolves it (declmod returns "" for tab.nmod="").
installdecl(c, file, tab);
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
arg.str = "__wwtests";
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
cid.str = "run";
call.lhs = cid;
call.list = arg;
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
ret.lhs = call;
body.list = ret;
};
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
m.str = "main";
m.exported = 1i32;
@@ -15882,16 +15939,18 @@ export fn checkfile(c: *checker, file: *node) void = {
rety.str = "i32";
m.lhs = rety;
m.body = body;
// Append to file.list tail. No type_ pre-set: installdecl
// never stamps a fn's type_ on the ww side either — cgfn reads
// lhs/list on demand, like every parsed fn. Pass-2 below
// resolve-walks the appended body.
// Append the table const (if any) then main to file.list tail. No
// type_ pre-set on main: installdecl never stamps a fn's type_ on
// the ww side — cgfn reads lhs/list on demand. Pass-2 below
// resolve-walks the appended bodies.
let tl: *node = file.list;
if (tl == nil) {
file.list = m;
if (tab != nil) { file.list = tab; tab.next = m; }
else { file.list = m; };
} else {
for (tl.next != nil) { tl = tl.next; };
tl.next = m;
if (tab != nil) { tl.next = tab; tab.next = m; }
else { tl.next = m; };
};
};