From e59881cb300135cffbeb75d8aa1d6faabee6b800 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 03:46:28 +0900 Subject: [PATCH] compiler: support package test runtime aliases --- cmd/w6c/main.c | 15 ++++++++++++++ cmd/wcc/check.c | 43 ++++++++++++++++++--------------------- cmd/wcc/parse.c | 6 +++++- cmd/wcc/ww.h | 5 ++++- lib/ww/syntax/parse.ww | 10 ++++++++- selfhost/cmd/w6c/main.ww | 20 +++++++++++++++++- selfhost/cmd/wcc/check.ww | 43 ++++++++++++++++++--------------------- 7 files changed, 92 insertions(+), 50 deletions(-) diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index 5cb62645..be51e607 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -28,6 +28,7 @@ main(int argc, char **argv) const char *src = NULL; const char *out = NULL; const char *wwiout = NULL; /* -I : M2 export-data producer */ + const char *testsupport = NULL; int testmode = 0; int sepmode = 0; /* -c: #22 M3 separate-compile / primary- * only codegen (emit imported==0 decls @@ -40,6 +41,12 @@ main(int argc, char **argv) wwiout = argv[++i]; } else if (strcmp(a, "-T") == 0) { testmode = 1; + } else if (strcmp(a, "--test-support-module") == 0) { + if (i + 1 >= argc) { + fputs("w6c: --test-support-module requires arg\n", stderr); + return 2; + } + testsupport = argv[++i]; } else if (strcmp(a, "-c") == 0) { sepmode = 1; } else if (a[0] == '-') { @@ -56,6 +63,12 @@ main(int argc, char **argv) fputs("usage: w6c [-T] [-c] [-I out.wwi] [-o out.s] file.ww\n", stderr); return 2; } + if (testsupport != NULL && (!sepmode + || (strcmp(testsupport, "test") != 0 + && strcmp(testsupport, "__wwtest") != 0))) { + fputs("w6c: invalid --test-support-module\n", stderr); + return 2; + } char *buf; u64 len; @@ -72,11 +85,13 @@ main(int argc, char **argv) lexinit(&l, a, src, buf, len); parserinit(&p, a, &l); + p.testmodule = testsupport; Node *file = parsefile(&p); if (l.errs || p.errs) return 1; check_init(&c, a); c.is_test = testmode; + if (testsupport != NULL) c.test_module = testsupport; c.sep_mode = sepmode; check_file(&c, file); if (c.errs) return 1; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 31670fce..c5763923 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1483,7 +1483,7 @@ cexpr(Checker *c, Node *n) if (fs) return n->type = fs->type; /* A bare `w6c -T` intentionally leaves the - * compiler-generated test.run hook external; the + * compiler-generated support.run hook external; the * ordinary driver supplies lib/test. This is the only * missing direct member that is not a package export * error. */ @@ -2843,6 +2843,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 */ + c->test_module = "test"; typesinit(a); c->top = newscope(a, NULL); c->cur = c->top; @@ -3120,20 +3121,19 @@ check_file(Checker *c, Node *file) if (file == NULL || file->kind != N_FILE) return; c->file = file; - /* #80: under -T, PREPEND the synth `use test;` BEFORE pass 1 so decl_mod - * (called per-decl during install) sees a matching `use test` when it - * keys lib/test's `run` — keying it under module="test", not "". A pure + /* Under -T, prepend the dispatcher support import before pass 1 so + * decl_mod keys the runner under the selected support module. A pure * ORDER fix: the synth N_USE was appended AFTER install (below), too late - * for decl_mod, so `run` keyed under "" — leaving the synth `test.run` + * for decl_mod, so `run` keyed under "" — leaving the qualified call * ty_err (the E1 bridge) and colliding with a user root `fn run` (also * module=""). Decoupled from cgen: the symbol mangle keys off d->module * (the //ww:module directive, mod_collect), NOT this scope keying — cgen - * already emits the correct CALL test.run. wwstage twin in check.ww. */ + * already emits the qualified call. wwstage twin in check.ww. */ if (c->is_test) { Node *usenode = newnode(c->a, N_USE, file->pos); - usenode->str = "test"; - usenode->strlen = 4; - usenode->usepath = "test"; + usenode->str = c->test_module; + usenode->strlen = strlen(c->test_module); + usenode->usepath = c->test_module; usenode->next = file->list; file->list = usenode; } @@ -3506,21 +3506,18 @@ check_file(Checker *c, Node *file) scope_define_in_module(c->cur, tab->str, NULL, SK_VAR, tt, tab); - /* return test.run(__wwtests); - * QUALIFIED, not bare `run`: under the sep producer - * lib/test is a real imported package, so the call must - * carry the `test` module qualifier (N_DOT base ident - * `test`, member `run`). The combined path tags the - * auto-bundled lib/test `//ww:module test` too, so the - * qualified form resolves+mangles identically there - * (test.run either way) — byte-neutral. The base ident - * resolves through a synthetic N_USE injected below. */ + /* Return support.run(__wwtests). + * QUALIFIED, not bare `run`: under the sep producer the + * toolchain test runtime is a real imported package. The + * selected module is normally `test`, or the reserved alias + * chosen by the package driver when user source owns `test`. + * The base ident resolves through the synthetic N_USE. */ Node *arg = newnode(c->a, N_IDENT, fp); arg->str = "__wwtests"; Node *call = newnode(c->a, N_CALL, fp); Node *dot = newnode(c->a, N_DOT, fp); dot->lhs = newnode(c->a, N_IDENT, fp); - dot->lhs->str = "test"; + dot->lhs->str = c->test_module; dot->str = "run"; c->synth_test_run = dot; call->lhs = dot; @@ -3528,11 +3525,11 @@ check_file(Checker *c, Node *file) Node *ret = newnode(c->a, N_RETURN, fp); ret->lhs = call; body->list = ret; - /* #80: the synth `use test;` (the N_DOT base qualifier + - * the use_path source mapping `test`→its path) is now + /* The synthetic support use (the N_DOT base qualifier plus + * its use_path source mapping) is now * PREPENDED before pass 1 at the top of check_file, so - * decl_mod keys lib/test's `run` under "test" and the synth - * `test.run` type-resolves. Pass 1's N_USE arm installs it + * decl_mod keys the runtime's `run` under the selected module + * and the synthesized call type-resolves. Pass 1 installs it * (SK_USE). */ } diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index f289cf5b..3f556e67 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1571,7 +1571,11 @@ parsefile(Parser *p) p->pathmod ? p->pathmod : p->resetmod; const char *dot = strrchr(active, '.'); const char *last = dot ? dot + 1 : active; - if (strcmp(name, last) != 0) { + int testsupport = p->testmodule != NULL + && strcmp(p->testmodule, "__wwtest") == 0 + && strcmp(active, "__wwtest") == 0 + && strcmp(name, "test") == 0; + if (strcmp(name, last) != 0 && !testsupport) { errorf(p->cur.pos, "package %s does not match import path %s", name, active); diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 41d3c3f3..f92eeff2 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -379,6 +379,8 @@ struct Parser { * bare-main rule stay intact), and the in-file * `package` clause asserts (leaf == last * component) instead of overwriting curmod. */ + const char *testmodule; /* hidden package-driver alias for toolchain + * `package test`; NULL outside that compile */ }; void parserinit(Parser*, Arena*, Lex*); @@ -588,9 +590,10 @@ struct Checker { int errs; int is_test; /* #15: `w6c -T` — collect @test fns + synth * the entry; loud-reject a user main. */ + const char *test_module; /* generated dispatcher support qualifier */ int sep_mode; /* -c package compilation: imported interfaces are * present, so absent members are hard export errors. */ - Node *synth_test_run; /* exact compiler-generated test.run DOT; + Node *synth_test_run; /* exact compiler-generated support.run DOT; * its unresolved external hook is intentional */ Node *alloc_octx; /* #3/B': the one empty `alloc([], n)` call node * that has let-declared slice context this walk; diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index 643524fd..ba1b1d60 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -46,6 +46,9 @@ export type parser = struct { // rule stay intact), and the in-file `package` clause asserts (leaf == // last component) instead of overwriting curmod. "" means inactive. resetmod: str, + // Hidden package-driver alias for the toolchain `package test` source. + // Empty outside that one separate-compilation edge. + testmodule: str, }; fn refill(p: *parser) void = { @@ -67,6 +70,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.nocast = 0; p.pathmod = ""; p.resetmod = ""; + p.testmodule = ""; refill(p); }; @@ -611,7 +615,11 @@ export fn parsefile(p: *parser) *node = { let (pre, post) = strings.rcut(active, "."); let last: str = post; if (post.len == 0) { last = active; }; - if (strings.compare(name, last) != 0) { + let testsupport: bool = strings.compare( + p.testmodule, "__wwtest") == 0 + && strings.compare(active, "__wwtest") == 0 + && strings.compare(name, "test") == 0; + if (strings.compare(name, last) != 0 && !testsupport) { errmsg(p, "package does not match import path"); }; } else { diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index c403a93b..0194ef95 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -66,6 +66,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let src: *u8 = nil; let out: *u8 = nil; let wwiout: *u8 = nil; // -I : M2 export-data producer + let testsupport: *u8 = nil; let testmode: i32 = 0i32; // #15: `-T` test-mode let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- // only codegen (emit imported==0 decls @@ -92,6 +93,14 @@ export fn main(argc: i32, argv: **u8) i32 = { wwiout = argv[i]; } else { if (cstreq(a, "-T")) { testmode = 1i32; + } else { if (cstreq(a, "--test-support-module")) { + i += 1; + if (i >= argc) { + let m: str = "w6c: --test-support-module requires arg\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + testsupport = argv[i]; } else { if (cstreq(a, "-c")) { sepmode = 1i32; } else { if (a[0u64] == 45u8) { @@ -105,7 +114,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; }; }; + }; }; }; }; }; }; i += 1; }; @@ -114,6 +123,13 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; + if (testsupport != nil && (sepmode == 0 + || (!cstreq(testsupport, "test") + && !cstreq(testsupport, "__wwtest")))) { + let m: str = "w6c: invalid --test-support-module\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; let buf: *u8; let blen: u64; @@ -155,6 +171,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let ps: parser; parserinit(&ps, &l); + if (testsupport != nil) { ps.testmodule = pathstr(testsupport); }; let f: *node = parsefile(&ps); // Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's // `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches @@ -172,6 +189,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let ck: checker; checkinit(&ck, &tc); ck.istest = testmode; + if (testsupport != nil) { ck.testmodule = pathstr(testsupport); }; ck.sepmode = sepmode; checkfile(&ck, f); if (ck.errs > 0) { return 1; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index db610233..67869e5d 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -20,9 +20,10 @@ type checker = struct { // twin (c->matcharms) istest: i32, // #15: `w6c_ww -T` — collect @test fns + // synth the entry; loud-reject a user main. + testmodule: str, // generated dispatcher support qualifier sepmode: i32, // -c package compilation: imported interfaces are // present, so absent members are hard export errors. - synthtestrun: *syntax.node, // exact generated test.run DOT; its + synthtestrun: *syntax.node, // exact generated support.run DOT; its // unresolved external hook is intentional verbose: i32, // when non-zero, log each unresolved name fnret: *syntax.node, // enclosing fn's return type AST (for `?`) @@ -361,7 +362,7 @@ fn packageaccesserr(c: *checker, e: *syntax.node, pkg: str, member: str, c.errs += 1; }; -// A bare `w6c -T` leaves the compiler-generated test.run hook external; +// A bare `w6c -T` leaves the compiler-generated support.run hook external; // the ordinary driver supplies lib/test. No user-written missing member is // exempt from package export checking. fn synthesizedtestrun(c: *checker, e: *syntax.node) bool = { @@ -7480,6 +7481,7 @@ export fn checkinit(c: *checker, tc: *syntax.tctx) void = { c.nunresolved = 0; c.errs = 0; c.istest = 0i32; // #15: caller (w6c main) sets it after init + c.testmodule = "test"; c.sepmode = 0i32; // caller (w6c main) sets it from -c c.synthtestrun = nil; c.verbose = 0; @@ -7496,20 +7498,19 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { if (file.kind != syntax.nkind.N_FILE) { return; }; c.file = file; - // #80: under -T, PREPEND the synth `use test;` BEFORE Pass 1 so declmod - // (called per-decl during install) sees a matching `use test` when it - // keys lib/test's `run` — keying it under mod="test", not "". This is a + // Under -T, prepend the dispatcher support import before Pass 1 so + // declmod keys the runner under the selected support module. This is a // pure ORDER fix: the synth N_USE was appended AFTER install (below), - // too late for declmod, so `run` keyed under "" — leaving the synth - // `test.run` ty_err (the E1 bridge) and colliding with a user root + // too late for declmod, so `run` keyed under "" — leaving the qualified + // call ty_err (the E1 bridge) and colliding with a user root // `fn run` (also mod=""). Decoupled from cgen: the symbol mangle keys // off d.module (the //ww:module directive, cgen mod_collect), NOT this - // scope keying — cgen already emits the correct CALL test.run. Twin of + // scope keying — cgen already emits the qualified call. Twin of // cstage cmd/wcc/check.c. if (c.istest != 0) { let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col); - usenode.str = "test"; - usenode.usepath = "test"; + usenode.str = c.testmodule; + usenode.usepath = c.testmodule; usenode.next = file.list; file.list = usenode; }; @@ -7719,16 +7720,14 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { let arg: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); arg.str = "__wwtests"; let call: *syntax.node = syntax.newnode(syntax.nkind.N_CALL, pf, pl, pc); - // QUALIFIED test.run (N_DOT base ident `test`, member `run`): - // the sep producer sees lib/test as a real imported package, - // so the call must carry the module qualifier; the combined - // path tags auto-bundled lib/test `//ww:module test` too, so - // it resolves+mangles identically (test.run). Cstage twin: - // cmd/wcc/check.c synth. The base ident binds through the - // synth N_USE below. + // Qualified support.run (N_DOT base ident + member `run`): + // the sep producer sees the toolchain test runtime as a real + // imported package. Its selected module is normally `test`, or + // the reserved alias when user source owns that identity. The + // base ident binds through the synthetic N_USE. let dot: *syntax.node = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc); let did: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - did.str = "test"; + did.str = c.testmodule; dot.lhs = did; dot.str = "run"; c.synthtestrun = dot; @@ -7737,11 +7736,9 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc); ret.lhs = call; body.list = ret; - // #80: the synth `use test;` (the N_DOT base qualifier + the - // usepathfor source mapping `test`→its path) is now PREPENDED - // before Pass 1 at the top of checkfile, so declmod keys - // lib/test's `run` under "test" and the synth `test.run` - // type-resolves. It is installed (SK_USE) by Pass 1's N_USE arm. + // The synthetic support use is prepended before Pass 1, so + // declmod keys the runtime's `run` under the selected module and + // the synthesized call type-resolves. Pass 1 installs the use. }; let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc); m.str = "main";