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

@@ -2974,14 +2974,22 @@ check_file(Checker *c, Node *file)
* untouched: the appended N_FNDECL rides the existing cgfn path, so
* byte-id holds at the gated choke point by construction.
*
* D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
* reinstatable post-CSP; this is a documented departure, not a hare
* cite): D1 no __test_array linker section — straight-line calls;
* D2 call @test fns by their real symbols (no testfunc.%d rename);
* 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 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 — the old D3), synthesize a value
* table `[](str, *fn() void) = {("foo", &foo), ...}` and 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). The table is the harec
* __test_array reduced to an in-source value table (D1: no linker
* section; D2: real symbols, no testfunc.%d rename). 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).
* The table rides cgen's #117 slice-of-tuple-global DATA path; the
* `run` callee resolves bare against the auto-bundled lib/test (the
* synth runs post-pass-1, so lib/test's `run` sits in the same flat ""
* bucket as the @test fns — bare, like the @test calls themselves).
*/
if (c->is_test) {
Pos fp = file->pos;
@@ -2991,8 +2999,10 @@ check_file(Checker *c, Node *file)
&& strcmp(d->str, "main") == 0 && d->body != NULL)
err(c, d->pos, "test mode: main is synthesized "
"by -T; remove the explicit main");
/* (c) collect @test fns in file->list order, build the body. */
Node *bhead = NULL, *btail = NULL;
/* (c) collect @test fns in file->list order; build one table row
* `("<name>", &<name>)` per validated @test fn. */
Node *rhead = NULL, *rtail = NULL;
int ntest = 0;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_FNDECL)
continue;
@@ -3015,22 +3025,72 @@ check_file(Checker *c, Node *file)
d->str);
continue;
}
Node *nm = newnode(c->a, N_STRLIT, fp);
nm->str = d->str;
nm->strlen = strlen(d->str);
Node *id = newnode(c->a, N_IDENT, fp);
id->str = d->str;
Node *amp = newnode(c->a, N_UN, fp);
amp->op = TK_AMP;
amp->lhs = id;
Node *row = newnode(c->a, N_TUPLE, fp);
row->list = nm;
nm->next = amp;
if (rhead == NULL) rhead = row;
else rtail->next = row;
rtail = row;
ntest++;
}
Node *body = newnode(c->a, N_BLOCK, fp);
Node *tab = NULL;
if (ntest == 0) {
/* no @test fns in this unit — exit 0, nothing to run. */
Node *ret = newnode(c->a, N_RETURN, fp);
ret->lhs = newnode(c->a, N_INTLIT, fp);
ret->lhs->uval = 0;
body->list = ret;
} else {
/* const __wwtests: [](str, *fn() void) = [rows...];
* cstage tuple-type elements chain raw via ->next (no
* N_TPARAM wrap; parse.c:341). */
Node *e0 = newnode(c->a, N_TNAME, fp);
e0->str = "str"; e0->strlen = 3;
Node *vfn = newnode(c->a, N_TFN, fp);
vfn->lhs = newnode(c->a, N_TNAME, fp);
vfn->lhs->str = "void"; vfn->lhs->strlen = 4;
Node *e1 = newnode(c->a, N_TPTR, fp);
e1->lhs = vfn;
Node *tup = newnode(c->a, N_TTUPLE, fp);
tup->list = e0; e0->next = e1;
Node *tsl = newnode(c->a, N_TSLICE, fp);
tsl->lhs = tup;
Node *arr = newnode(c->a, N_ARRLIT, fp);
arr->list = rhead;
tab = newnode(c->a, N_LET, fp);
tab->op = TK_CONST;
tab->str = "__wwtests";
tab->lhs = tsl;
tab->rhs = arr;
/* pass 1 already ran, so install the table's name now —
* pass 2 (below) cexprs its rhs and main references it. */
Type *tt = resolve_type(c, tsl);
tab->type = tt;
scope_define_in_module(c->cur, tab->str, NULL, SK_VAR,
tt, tab);
/* return run(__wwtests); */
Node *arg = newnode(c->a, N_IDENT, fp);
arg->str = "__wwtests";
Node *call = newnode(c->a, N_CALL, fp);
call->lhs = newnode(c->a, N_IDENT, fp);
call->lhs->str = d->str;
Node *es = newnode(c->a, N_EXPRSTMT, fp);
es->lhs = call;
if (bhead == NULL) bhead = es;
else btail->next = es;
btail = es;
call->lhs->str = "run";
call->list = arg;
Node *ret = newnode(c->a, N_RETURN, fp);
ret->lhs = call;
body->list = ret;
}
Node *ret = newnode(c->a, N_RETURN, fp);
ret->lhs = newnode(c->a, N_INTLIT, fp);
ret->lhs->uval = 0;
if (bhead == NULL) bhead = ret;
else btail->next = ret;
Node *body = newnode(c->a, N_BLOCK, fp);
body->list = bhead;
Node *m = newnode(c->a, N_FNDECL, fp);
m->str = "main";
m->export = 1;
@@ -3039,12 +3099,16 @@ check_file(Checker *c, Node *file)
m->body = body;
m->type = build_fn_type(c, m);
/* pass 1 already ran, so the install loop never stamped m's
* type; set it explicitly (pass 2 below reads d->type). */
* type; set it explicitly (pass 2 below reads d->type).
* Append the table const (if any) then main to file->list. */
Node *tl = file->list;
if (tl == NULL) file->list = m;
else {
if (tl == NULL) {
file->list = tab ? tab : m;
if (tab) tab->next = m;
} else {
while (tl->next) tl = tl->next;
tl->next = m;
if (tab) { tl->next = tab; tab->next = m; }
else tl->next = m;
}
}