diff --git a/Makefile b/Makefile index 9b790a70..8a59cd3f 100644 --- a/Makefile +++ b/Makefile @@ -2128,7 +2128,7 @@ $(BIN)/test_dyn_ww: test/wcc/996_dyn_ww.c $(BIN)/ww $(BIN)/w6l $(BIN)/w6l_ww \ $(CC) $(CFLAGS) -o $@ $< $(BIN)/test_at_test_ww: test/wcc/997_at_test_ww.c $(BIN)/ww_ww $(BIN)/w6c_ww \ - $(BIN)/w6a_ww $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(BIN)/w6a_ww $(BIN)/w6l_ww $(BIN)/w6c $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< $(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \ diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index bc227806..89529a31 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -33,10 +33,13 @@ main(int argc, char **argv) { const char *src = NULL; const char *out = NULL; + int testmode = 0; for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (strcmp(a, "-o") == 0 && i + 1 < argc) { out = argv[++i]; + } else if (strcmp(a, "-T") == 0) { + testmode = 1; } else if (a[0] == '-') { fprintf(stderr, "w6c: unknown flag %s\n", a); return 2; @@ -48,7 +51,7 @@ main(int argc, char **argv) } } if (src == NULL) { - fputs("usage: w6c [-o out.s] file.ww\n", stderr); + fputs("usage: w6c [-T] [-o out.s] file.ww\n", stderr); return 2; } @@ -71,6 +74,7 @@ main(int argc, char **argv) if (l.errs || p.errs) return 1; check_init(&c, a); + c.is_test = testmode; check_file(&c, file); if (c.errs) return 1; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 2402812a..1fc9681c 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2650,6 +2650,7 @@ check_init(Checker *c, Arena *a) { memset(c, 0, sizeof *c); c->a = a; + c->is_test = 0; /* #15: caller (w6c main) sets it after init */ typesinit(a); c->top = newscope(a, NULL); c->cur = c->top; @@ -2951,6 +2952,92 @@ check_file(Checker *c, Node *file) } c->cur_mod = NULL; + /* + * #15 @test harness — under `w6c -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-check (pass 2 below, which stamps the + * appended entry for free). This 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, which + * only flips a mode bit (ref/hare/cmd/hare/build.ha:46-49). cgen is + * 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. + */ + if (c->is_test) { + Pos fp = file->pos; + /* (b) the synth entry OWNS `main` — loud-reject a user one. */ + for (Node *d = file->list; d; d = d->next) + if (d->kind == N_FNDECL && d->str + && 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; + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_FNDECL) + continue; + int istest = 0; + for (Node *at = d->attr; at; at = at->next) + if (at->str && strcmp(at->str, "test") == 0) { + istest = 1; + break; + } + if (!istest) + continue; + /* `fn f() void` parses the explicit `void` into d->lhs + * (parse.c:1329), so void-returning is lhs==NULL OR an + * N_TNAME "void" — not lhs==NULL alone. */ + int retvoid = d->lhs == NULL + || (d->lhs->kind == N_TNAME && d->lhs->str + && strcmp(d->lhs->str, "void") == 0); + if (d->list != NULL || !retvoid) { + err(c, d->pos, "@test fn '%s' must be fn() void", + d->str); + continue; + } + 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; + } + 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; + m->lhs = newnode(c->a, N_TNAME, fp); + m->lhs->str = "i32"; + 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). */ + Node *tl = file->list; + if (tl == NULL) file->list = m; + else { + while (tl->next) tl = tl->next; + tl->next = m; + } + } + /* pass 2: check def initialisers and fn bodies */ for (Node *d = file->list; d; d = d->next) { c->cur_mod = decl_mod(file, d); diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 3928885e..fef05179 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -555,6 +555,8 @@ struct Checker { * would shadow an imported module bareword. */ int loops; /* nesting count for break/continue */ int errs; + int is_test; /* #15: `w6c -T` — collect @test fns + synth + * the entry; loud-reject a user main. */ Node *alloc_octx; /* #3/B': the one empty `alloc([], n)` call node * that has let-declared slice context this walk; * any OTHER empty alloc has no element-type hint diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9f8cd675..5b3acc0e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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; }; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 9cecefef..60f0ca90 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -79,6 +79,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) { @@ -91,6 +92,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); @@ -102,12 +105,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; }; @@ -168,6 +171,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; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index dd5481b8..4ce4c3d7 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -30,6 +30,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 @@ -5424,6 +5426,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; @@ -5445,6 +5448,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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 639a2868..579965d3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 diff --git a/test/wcc/910_at_test.c b/test/wcc/910_at_test.c index 912ef3f1..0f2578e9 100644 --- a/test/wcc/910_at_test.c +++ b/test/wcc/910_at_test.c @@ -1,16 +1,27 @@ /* - * 910_at_test — runs @test fn blocks in a source file by - * generating a synthetic main() that calls each one. A test - * passes iff the generated binary exits 0; if any @test calls - * abort/exit(1), the run fails. + * 910_at_test — drives the compiler's `-T` @test mode (task #15). * - * Scanning is regex-free, matching the literal sequence - * `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts. + * Under `w6c -T`, the checker collects every @test fn and synthesizes + * `export fn main() i32 { t0(); t1(); ...; return 0; }`, calling each in + * source order; a test that runs to completion passes, an abort/div0/ + * nonzero exit FAILS the run. This replaces the old driver-side regex + * scan (find_attest) — the @test set now comes from the AST, the single + * source of truth (rob #15 ruling). cgen is untouched: the synthesized + * entry rides the existing cgfn path. + * + * Probes: + * 1. RUN — `w6c -T` the @test-only fixture, assemble, link, run; the + * synthesized entry must exit 0 (every @test passed). Imports are + * resolved by `ww build` into a *.combined.ww (the fixture uses + * `alloc`, whose rt_malloc symbol the driver concatenates); we then + * compile that combined unit with `-T`. + * 2. REJECT — `-T` of a fixture that also defines a user `main` must + * exit nonzero (the synth entry owns main). + * 3. GUARD — `-T` of a fixture whose @test fn is not `fn() void` must + * exit nonzero (rule-7 loud, no silent skip/coerce). */ #include #include -#include -#include #include #include @@ -23,149 +34,104 @@ runwait(const char *cmd) 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; +} + +/* run_fixture — ` -T` the @test fixture (import-resolved into a + * combined unit by build), assemble, link, run; the synthesized + * entry must exit 0. Returns 0 on success. */ static int -isws(int c) +run_fixture(const char *bin, const char *comp, const char *drv) { - return c == ' ' || c == '\t' || c == '\n' || c == '\r'; -} + int pid = getpid(); + char src[256], comb[256], asmf[256], obj[256], exe[256]; + char rt[1024], cmd[4096]; + snprintf(src, sizeof src, "/tmp/at910_%s_%d.ww", comp, pid); + snprintf(comb, sizeof comb, "/tmp/at910_%s_%d.combined.ww", comp, pid); + snprintf(asmf, sizeof asmf, "/tmp/at910_%s_%d.s", comp, pid); + snprintf(obj, sizeof obj, "/tmp/at910_%s_%d.o", comp, pid); + snprintf(exe, sizeof exe, "/tmp/at910_%s_%d.exe", comp, pid); + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); -static int -isident(int c) -{ - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || c == '_'; -} + snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src); + if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: cp\n"); return 1; } -/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match, - * write the identifier into `out` (NUL-terminated) and return the - * scan position past the matched ident. Returns -1 when no more. */ -static long -find_attest(const char *src, long pos, char *out, size_t outsz) -{ - long n = (long)strlen(src); - while (pos < n) { - const char *q = strstr(src + pos, "@test"); - if (q == NULL) return -1; - long off = q - src; - /* must be a top-level marker: previous non-space char is - * '\n' or we're at the file start. */ - long pre = off - 1; - while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--; - if (pre >= 0 && src[pre] != '\n') { - pos = off + 1; - continue; - } - long c = off + 5; /* past "@test" */ - while (c < n && isws(src[c])) c++; - if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) { - pos = off + 1; - continue; - } - c += 2; - if (c < n && isident(src[c])) { - pos = off + 1; - continue; - } - while (c < n && isws(src[c])) c++; - long ids = c; - while (c < n && isident(src[c])) c++; - if (c == ids) { - pos = off + 1; - continue; - } - size_t len = (size_t)(c - ids); - if (len + 1 > outsz) return -1; - memcpy(out, src + ids, len); - out[len] = '\0'; - return c; - } - return -1; -} - -static char * -slurp(const char *path) -{ - FILE *f = fopen(path, "rb"); - if (!f) return NULL; - fseek(f, 0, SEEK_END); - long n = ftell(f); - fseek(f, 0, SEEK_SET); - char *b = malloc((size_t)n + 1); - if (!b) { fclose(f); return NULL; } - if (fread(b, 1, (size_t)n, f) != (size_t)n) { - free(b); fclose(f); return NULL; - } - b[n] = '\0'; - fclose(f); - return b; -} - -static int -runtests(const char *path, const char *bin) -{ - char *src = slurp(path); - if (!src) { - fprintf(stderr, "910 FAIL: cannot read %s\n", path); - return -1; + /* Driver resolves `import rt` into .combined.ww. The build + * itself exits nonzero (the @test-only fixture has no main — that + * is exactly what -T synthesizes), but the combined unit is written + * before any compile step, so we depend on the ARTIFACT, not the + * exit code. */ + snprintf(cmd, sizeof cmd, "%s/%s build %s > /dev/null 2>&1", bin, drv, src); + runwait(cmd); + if (access(comb, 0) != 0) { + fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb); + return 1; } - /* Build a synthetic test driver: original source + a main() - * that calls each @test fn. Each test that runs to completion - * (no abort/exit) counts as a pass. */ - char tmpsrc[256]; - snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_%d.ww", getpid()); - FILE *out = fopen(tmpsrc, "wb"); - if (!out) { free(src); return -1; } - fputs(src, out); - fputs("\nexport fn main() i32 = {\n", out); - - long pos = 0; - int count = 0; - for (;;) { - char name[128]; - long next = find_attest(src, pos, name, sizeof name); - if (next < 0) break; - fprintf(out, "\t%s();\n", name); - pos = next; - count++; + snprintf(cmd, sizeof cmd, "%s/%s -T %s -o %s 2>/dev/null", bin, comp, comb, asmf); + if (runwait(cmd) != 0) { + fprintf(stderr, "910 FAIL: %s -T errored\n", comp); + return 1; } - fputs("\treturn 0;\n};\n", out); - fclose(out); - free(src); + snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, asmf); + if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: w6a\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null", bin, exe, obj, rt); + if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: w6l\n"); return 1; } - if (count == 0) { - fprintf(stderr, "910 FAIL: no @test fns found in %s\n", path); - unlink(tmpsrc); - return -1; - } - - char cmd[2048]; - snprintf(cmd, sizeof cmd, "%s/ww run %s 2>/dev/null", bin, tmpsrc); - int rc = runwait(cmd); - unlink(tmpsrc); + int rc = runwait(exe); if (rc != 0) { - fprintf(stderr, "910 FAIL: %s: %d @test fn(s), driver exited %d\n", - path, count, rc); - return -1; + fprintf(stderr, "910 FAIL: synth entry exited %d (a @test failed)\n", rc); + return 1; } - return count; + unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe); + return 0; } +/* reject — ` -T ` must exit nonzero. */ +static int +reject(const char *bin, const char *comp, const char *fixture, const char *what) +{ + char cmd[4096]; + snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null", + bin, comp, fixture); + if (runwait(cmd) == 0) { + fprintf(stderr, "910 FAIL: %s -T accepted %s (expected reject)\n", + comp, what); + return 1; + } + return 0; +} + +/* Each row: a fixture `-T` must loud-reject, and why. */ +static const struct { + const char *fixture; + const char *what; +} rejects[] = { + { "test/wcc/data/attest_userman.ww", "user main" }, + { "test/wcc/data/attest_badsig.ww", "non-void @test" }, +}; + int main(void) { - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - const char *files[] = { - "test/wcc/data/attest_pass.ww", - NULL, - }; - int total = 0; - for (int i = 0; files[i]; i++) { - int n = runtests(files[i], bin); - if (n < 0) return 1; - total += n; - } - printf("@test: %d test(s) ran ok\n", total); + const char *bin = absbin(); + if (!bin) { fprintf(stderr, "910 FAIL: getcwd\n"); return 1; } + + if (run_fixture(bin, "w6c", "ww") != 0) return 1; + for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++) + if (reject(bin, "w6c", rejects[i].fixture, rejects[i].what) != 0) + return 1; + + printf("@test -T: run ok + user-main and bad-signature rejected\n"); return 0; } diff --git a/test/wcc/997_at_test_ww.c b/test/wcc/997_at_test_ww.c index b0ab6c6b..fd8214c8 100644 --- a/test/wcc/997_at_test_ww.c +++ b/test/wcc/997_at_test_ww.c @@ -1,11 +1,19 @@ /* - * 997_at_test_ww — same as 910_at_test, driven through `ww_ww` - * (the selfhost driver) instead of `ww` (the C driver). Confirms - * the ww-built toolchain parses, compiles, and runs an @test - * fixture end-to-end. + * 997_at_test_ww — the wwstage twin of 910_at_test, driving the `-T` + * @test mode through `w6c_ww` (the ww-built compiler) instead of `w6c` + * (the C bootstrap). Task #15. * - * Scanning is regex-free, matching the literal sequence - * `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts. + * Probes: + * 1. RUN — `w6c_ww -T` the @test-only fixture (import-resolved by + * `ww_ww build` into a combined unit), assemble, link, run; the + * synthesized entry must exit 0. + * 2. BYTE-ID (rule 10) — `w6c -T` and `w6c_ww -T` must emit + * byte-identical asm for the same source. The synthesized entry + * rides cgfn at the gated choke point, so the @test set + the + * collection-order calls are identical by construction. This is + * the new entry-synth byte-id gate. + * 3. REJECT — `-T` of a user-main fixture exits nonzero. + * 4. GUARD — `-T` of a non-`fn() void` @test exits nonzero. */ #include #include @@ -22,149 +30,144 @@ runwait(const char *cmd) return 1; } -static int -isws(int c) +static const char * +absbin(void) { - return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + 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 -isident(int c) -{ - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || c == '_'; -} - -/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match, - * write the identifier into `out` (NUL-terminated) and return the - * scan position past the matched ident. Returns -1 when no more. */ -static long -find_attest(const char *src, long pos, char *out, size_t outsz) -{ - long n = (long)strlen(src); - while (pos < n) { - const char *q = strstr(src + pos, "@test"); - if (q == NULL) return -1; - long off = q - src; - /* must be a top-level marker: previous non-space char is - * '\n' or we're at the file start. */ - long pre = off - 1; - while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--; - if (pre >= 0 && src[pre] != '\n') { - pos = off + 1; - continue; - } - long c = off + 5; /* past "@test" */ - while (c < n && isws(src[c])) c++; - if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) { - pos = off + 1; - continue; - } - c += 2; - if (c < n && isident(src[c])) { - pos = off + 1; - continue; - } - while (c < n && isws(src[c])) c++; - long ids = c; - while (c < n && isident(src[c])) c++; - if (c == ids) { - pos = off + 1; - continue; - } - size_t len = (size_t)(c - ids); - if (len + 1 > outsz) return -1; - memcpy(out, src + ids, len); - out[len] = '\0'; - return c; - } - return -1; -} - -static char * -slurp(const char *path) +slurp(const char *path, char **outbuf, size_t *outlen) { FILE *f = fopen(path, "rb"); - if (!f) return NULL; + 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 NULL; } - if (fread(b, 1, (size_t)n, f) != (size_t)n) { - free(b); fclose(f); return NULL; - } + 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); - return b; + *outbuf = b; + *outlen = (size_t)n; + return 0; } static int -runtests(const char *path, const char *bin) +run_fixture(const char *bin) { - char *src = slurp(path); - if (!src) { - fprintf(stderr, "997 FAIL: cannot read %s\n", path); - return -1; + int pid = getpid(); + char src[256], comb[256], asmf[256], obj[256], exe[256]; + char rt[1024], cmd[4096]; + snprintf(src, sizeof src, "/tmp/at997_%d.ww", pid); + snprintf(comb, sizeof comb, "/tmp/at997_%d.combined.ww", pid); + snprintf(asmf, sizeof asmf, "/tmp/at997_%d.s", pid); + snprintf(obj, sizeof obj, "/tmp/at997_%d.o", pid); + snprintf(exe, sizeof exe, "/tmp/at997_%d.exe", pid); + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); + + snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: cp\n"); return 1; } + + /* ww_ww build resolves `import rt` into the combined unit; it exits + * nonzero (no main — that is what -T synthesizes), so we gate on the + * combined ARTIFACT existing, not the exit code. */ + snprintf(cmd, sizeof cmd, "%s/ww_ww build %s > /dev/null 2>&1", bin, src); + runwait(cmd); + if (access(comb, 0) != 0) { + fprintf(stderr, "997 FAIL: ww_ww build produced no %s\n", comb); + return 1; } - /* Build a synthetic test driver: original source + a main() - * that calls each @test fn. Each test that runs to completion - * (no abort/exit) counts as a pass. */ - char tmpsrc[256]; - snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_ww_%d.ww", getpid()); - FILE *out = fopen(tmpsrc, "wb"); - if (!out) { free(src); return -1; } - fputs(src, out); - fputs("\nexport fn main() i32 = {\n", out); + snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o %s 2>/dev/null", bin, comb, asmf); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T errored\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6a_ww -o %s %s 2>/dev/null", bin, obj, asmf); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6a_ww\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6l_ww -o %s %s %s 2>/dev/null", bin, exe, obj, rt); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6l_ww\n"); return 1; } - long pos = 0; - int count = 0; - for (;;) { - char name[128]; - long next = find_attest(src, pos, name, sizeof name); - if (next < 0) break; - fprintf(out, "\t%s();\n", name); - pos = next; - count++; - } - fputs("\treturn 0;\n};\n", out); - fclose(out); - free(src); - - if (count == 0) { - fprintf(stderr, "997 FAIL: no @test fns found in %s\n", path); - unlink(tmpsrc); - return -1; - } - - char cmd[2048]; - snprintf(cmd, sizeof cmd, "%s/ww_ww run %s 2>/dev/null", bin, tmpsrc); - int rc = runwait(cmd); - unlink(tmpsrc); + int rc = runwait(exe); if (rc != 0) { - fprintf(stderr, "997 FAIL: %s: %d @test fn(s), driver exited %d\n", - path, count, rc); - return -1; + fprintf(stderr, "997 FAIL: synth entry exited %d (a @test failed)\n", rc); + return 1; } - return count; + unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe); + return 0; } +/* byteid — `w6c -T` and `w6c_ww -T` on the same source must agree. */ +static int +byteid(const char *bin) +{ + int pid = getpid(); + char cs[256], ws[256], cmd[4096]; + snprintf(cs, sizeof cs, "/tmp/at997_bid_c_%d.s", pid); + snprintf(ws, sizeof ws, "/tmp/at997_bid_w_%d.s", pid); + + snprintf(cmd, sizeof cmd, "%s/w6c -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null", + bin, cs); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c -T (byteid)\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null", + bin, ws); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T (byteid)\n"); return 1; } + + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + int rc = 0; + if (slurp(cs, &bc, &nc) < 0 || slurp(ws, &bw, &nw) < 0) { + fprintf(stderr, "997 FAIL: slurp byteid asm\n"); + rc = 1; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "997 FAIL: -T asm differs (cs %zu, ww %zu)\n", nc, nw); + rc = 1; + } + free(bc); free(bw); + unlink(cs); unlink(ws); + return rc; +} + +static int +reject(const char *bin, const char *fixture, const char *what) +{ + char cmd[4096]; + snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o /dev/null 2>/dev/null", bin, fixture); + if (runwait(cmd) == 0) { + fprintf(stderr, "997 FAIL: w6c_ww -T accepted %s (expected reject)\n", what); + return 1; + } + return 0; +} + +/* Each row: a fixture `w6c_ww -T` must loud-reject, and why. */ +static const struct { + const char *fixture; + const char *what; +} rejects[] = { + { "test/wcc/data/attest_userman.ww", "user main" }, + { "test/wcc/data/attest_badsig.ww", "non-void @test" }, +}; + int main(void) { - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - const char *files[] = { - "test/wcc/data/attest_pass.ww", - NULL, - }; - int total = 0; - for (int i = 0; files[i]; i++) { - int n = runtests(files[i], bin); - if (n < 0) return 1; - total += n; - } - printf("@test (ww_ww): %d test(s) ran ok\n", total); + const char *bin = absbin(); + if (!bin) { fprintf(stderr, "997 FAIL: getcwd\n"); return 1; } + + if (run_fixture(bin) != 0) return 1; + if (byteid(bin) != 0) return 1; + for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++) + if (reject(bin, rejects[i].fixture, rejects[i].what) != 0) + return 1; + + printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects ok\n"); return 0; } diff --git a/test/wcc/data/attest_badsig.ww b/test/wcc/data/attest_badsig.ww new file mode 100644 index 00000000..5f2be8f0 --- /dev/null +++ b/test/wcc/data/attest_badsig.ww @@ -0,0 +1,18 @@ +// #15 negative fixture: an @test fn with a non-`fn() void` signature. +// The collector must loud-reject (nonzero exit) rather than silently +// skip or coerce — rule-7. Pins the signature guard on both stages. +// +// TEETH: a non-void RETURN with NO params is the case ONLY the explicit +// guard catches. The synth entry emits `bad();` (arg count matches 0 +// params) and a discarded non-void return is silently allowed, so +// without the guard this compiles clean (exit 0). A param-bearing @test +// would instead trip the checker's independent "not enough arguments" +// (check.c:1879) on the synth `bad()` call — so a param fixture would +// pass the reject probe whether or not the guard exists, isolating +// nothing. Hence the no-param / non-void-return shape. + +package data; + +@test fn bad() i32 = { + return 0; +}; diff --git a/test/wcc/data/attest_userman.ww b/test/wcc/data/attest_userman.ww new file mode 100644 index 00000000..df335365 --- /dev/null +++ b/test/wcc/data/attest_userman.ww @@ -0,0 +1,17 @@ +// #15 negative fixture: @test fns PLUS an explicit user `main`. Under +// `-T` the entry is synthesized, so a hand-written main collides — both +// stages MUST loud-reject (nonzero exit), mirroring harec's hosted-main +// suppression under is_test (ref/harec/src/check.c:4000). + +package data; + +@test fn check_ok() void = { + let a: i32 = 1; + if (a != 1) { + let _: i32 = 1 / 0; + }; +}; + +export fn main() i32 = { + return 0; +};