wcc: -T test-mode collects @test fns + synthesizes entry, both stages (#15)

@test was parsed then dropped (no consumer); `ww test` needed a hand-written
main listing each test by hand, so adding a @test and forgetting the call
silently skipped it. -T makes the checker collect @test N_FNDECLs in source
order, loud-reject a user main, and append a synthetic
`export fn main() i32 { t0(); ...; return 0; }` at the install->body-check seam;
the existing cgfn emits it (cgen untouched) -> byte-identical by construction.
Mirrors harec's checker-side is_test placement.

Plan-9-lean reduction (user-sanctioned, reinstatable post-CSP): sequential,
abort/nonzero=fail; no setjmp isolation, no fnmatch filter, no file:line.

910/997 rewired from a regex scanner to driving `w6c -T` directly (thin trusted
drivers; the @test content stays ww), with a cross-stage byte-id assert on the
-T output. attest_userman/attest_badsig pin the user-main and bad-signature
rejects.
This commit is contained in:
2026-06-10 02:01:20 +09:00
parent fddd167ce8
commit b795c320c9
12 changed files with 693 additions and 267 deletions

View File

@@ -10311,6 +10311,8 @@ type checker = struct {
nresolved: i32,
nunresolved: i32,
errs: i32,
istest: i32, // #15: `w6c_ww -T` — collect @test fns +
// synth the entry; loud-reject a user main.
verbose: i32, // when non-zero, log each unresolved name
fnret: *node, // enclosing fn's return type AST (for `?`)
curmod: str, // importing-module bareword for the decl
@@ -15705,6 +15707,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = {
c.nresolved = 0;
c.nunresolved = 0;
c.errs = 0;
c.istest = 0i32; // #15: caller (w6c main) sets it after init
c.verbose = 0;
c.fnret = nil;
let empty: str;
@@ -15726,6 +15729,110 @@ export fn checkfile(c: *checker, file: *node) void = {
d = d.next;
};
// #15 @test harness — under `w6c_ww -T`, synthesize the entry the
// driver would otherwise hand-wire. We sit at the seam between
// fn-install (Pass 1, all names now in scope so the synth callees
// resolve) and fn-body-resolve (Pass 2 below, which stamps the
// appended entry for free). Mirrors harec's checker-side is_test
// work — keep @test fns + suppress/own the hosted main
// (ref/harec/src/check.c:3941,4000) — NOT the build driver. cgen is
// 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.
if (c.istest != 0) {
let pf: str = file.file;
let pl: i32 = file.line;
let pc: i32 = file.col;
// (b) the synth entry OWNS `main` — loud-reject a user one.
let u: *node = file.list;
for (u != nil) {
if (u.kind == nkind.N_FNDECL && u.body != nil
&& streq(u.str, "main")) {
cerr(u.file);
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
c.errs += 1;
};
u = u.next;
};
// (c) collect @test fns in file.list order; build the body chain.
let bhead: *node = nil;
let btail: *node = nil;
let t: *node = file.list;
for (t != nil) {
if (t.kind == nkind.N_FNDECL) {
let istest: bool = false;
let at: *node = t.attr;
for (at != nil) {
if (at.kind == nkind.N_ATTR
&& streq(at.str, "test")) {
istest = true;
};
at = at.next;
};
if (istest) {
// `fn f() void` parses the explicit void
// into t.lhs, so void-returning is lhs==nil
// OR an N_TNAME "void".
let retvoid: bool = t.lhs == nil
|| (t.lhs.kind == nkind.N_TNAME
&& streq(t.lhs.str, "void"));
if (t.list != nil || !retvoid) {
cerr(t.file);
cerr(": error: @test fn '");
cerr(t.str);
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;
};
};
};
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 m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
m.str = "main";
m.exported = 1i32;
let rety: *node = newnode(nkind.N_TNAME, pf, pl, pc);
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.
let tl: *node = file.list;
if (tl == nil) {
file.list = m;
} else {
for (tl.next != nil) { tl = tl.next; };
tl.next = m;
};
};
// Pass 2: walk decl bodies/types and resolve identifiers.
// Track the per-decl module bareword so bare-leaf lookups inside
// the body prefer same-module entries over alphabetically-earlier
@@ -42878,6 +42985,7 @@ fn slurp(path: *u8) (*u8, u64) = {
export fn main(argc: i32, argv: **u8) i32 = {
let src: *u8 = nil;
let out: *u8 = nil;
let testmode: i32 = 0i32; // #15: `-T` test-mode
let i: i32 = 1;
for (i < argc) {
@@ -42890,6 +42998,8 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
out = argv[i];
} else { if (cstreq(a, "-T")) {
testmode = 1i32;
} else { if (a[0u64] == 45u8) {
let m: str = "w6c: unknown flag\n";
os.write(2, m.ptr, m.len: u64);
@@ -42901,12 +43011,12 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
src = a;
}; };
}; }; };
i += 1;
};
if (src == nil) {
let m: str = "usage: w6c_ww [-o out.s] file.ww\n";
let m: str = "usage: w6c_ww [-T] [-o out.s] file.ww\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
@@ -42967,6 +43077,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
typesinit(&tc);
let ck: checker;
checkinit(&ck, &tc);
ck.istest = testmode;
checkfile(&ck, f);
if (ck.errs > 0) { return 1; };