wcc,ww: @test under separate compilation (M4 E1, #79)

Make `ww test --sep` work the Hare +test way: the -T synth test-main emits a
qualified test.run, and the test package is injected as an ordinary
separately-compiled dependency instead of splicing lib/test source into a
flat unit. Additive — combined stays the default and 910/997 are untouched
(their migration is M4 E2).

- compiler synth (both stages): the -T main emits N_DOT test.run plus a
  synthetic N_USE "test"; cmd/wcc/check.c + selfhost/cmd/wcc/check.ww.
- driver (both stages): build_one_sep gains is_test, injects the test package
  as a root dep, and passes -T to the root; do_test --sep routes a single-file
  test through the sep producer; cmd/ww/main.c + selfhost/cmd/ww/main.ww.
- 989_septest_run gate: ww test --sep on both stages, run-exit + cs==ww
  byte-id of the sep .s, non-vacuous.

The synth's test.run is left ty_err by the checker in both regimes (lib/test's
run is scope-keyed under "" not "test"; cgen emits the correct CALL via run's
//ww:module test directive) — wwstage tolerates it like cstage (rule-10). The
genuine fix, module-keying run under sep so the call type-resolves, is #80.

w6c_ww/wwdump_ww/ww_ww move (their embedded source changed); w6a_ww/w6l_ww and
the combined codegen output are unchanged.
This commit is contained in:
2026-06-17 07:45:46 +09:00
parent fffb6aaf91
commit 37c253c367
9 changed files with 601 additions and 41 deletions

View File

@@ -14229,6 +14229,22 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
// (ref/harec/src/check.c:1566-1581).
if (ms != nil && (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32)) {
s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm);
// Module-qualified callee whose leaf isn't scope-keyed
// under its module: align to cstage, which stamps ty_err
// here and lets cgen emit the call (cmd/wcc/check.c:1834-
// 1843; rule-10). The -T synth's `test.run` hits this in
// BOTH combined and sep (Q2-confirmed): lib/test's `run` is
// scope-keyed under "" not "test" — the directive-for-cgen
// vs declmod-for-scope keying split, the module-qualified
// arm of #27. cgen still emits the correct CALL test.run
// from run's `//ww:module test` directive. Load-bearing
// until #80 module-keys run under sep so the synth call
// type-resolves; NOT an M4-transient.
if (s == nil) {
e.type_ = c.tc.tyerr: *void;
callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it)
return nil;
};
};
};
if (s != nil) { if (s.skind == syntax.skind.SK_FN) { if (s.decl != nil) {
@@ -17275,13 +17291,34 @@ 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);
let cid: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
cid.str = "run";
call.lhs = cid;
// 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.
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";
dot.lhs = did;
dot.str = "run";
call.lhs = dot;
call.list = arg;
let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc);
ret.lhs = call;
body.list = ret;
// synth `use test;` so the N_DOT base binds as a module
// qualifier (SK_USE) and usepathfor maps `test` to its path.
// Mirror installdecl's N_USE SK_USE install. Appended to
// file.list below; emits no asm.
let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, pf, pl, pc);
usenode.str = "test";
usenode.usepath = "test";
syntax.scopedefine(c.top, "test", syntax.skind.SK_USE, nil, usenode);
let ul: *syntax.node = file.list;
if (ul == nil) { file.list = usenode; }
else { for (ul.next != nil) { ul = ul.next; }; ul.next = usenode; };
};
let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc);
m.str = "main";

View File

@@ -3486,6 +3486,22 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
// (ref/harec/src/check.c:1566-1581).
if (ms != nil && (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32)) {
s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm);
// Module-qualified callee whose leaf isn't scope-keyed
// under its module: align to cstage, which stamps ty_err
// here and lets cgen emit the call (cmd/wcc/check.c:1834-
// 1843; rule-10). The -T synth's `test.run` hits this in
// BOTH combined and sep (Q2-confirmed): lib/test's `run` is
// scope-keyed under "" not "test" — the directive-for-cgen
// vs declmod-for-scope keying split, the module-qualified
// arm of #27. cgen still emits the correct CALL test.run
// from run's `//ww:module test` directive. Load-bearing
// until #80 module-keys run under sep so the synth call
// type-resolves; NOT an M4-transient.
if (s == nil) {
e.type_ = c.tc.tyerr: *void;
callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it)
return nil;
};
};
};
if (s != nil) { if (s.skind == syntax.skind.SK_FN) { if (s.decl != nil) {
@@ -6532,13 +6548,34 @@ 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);
let cid: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
cid.str = "run";
call.lhs = cid;
// 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.
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";
dot.lhs = did;
dot.str = "run";
call.lhs = dot;
call.list = arg;
let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc);
ret.lhs = call;
body.list = ret;
// synth `use test;` so the N_DOT base binds as a module
// qualifier (SK_USE) and usepathfor maps `test` to its path.
// Mirror installdecl's N_USE SK_USE install. Appended to
// file.list below; emits no asm.
let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, pf, pl, pc);
usenode.str = "test";
usenode.usepath = "test";
syntax.scopedefine(c.top, "test", syntax.skind.SK_USE, nil, usenode);
let ul: *syntax.node = file.list;
if (ul == nil) { file.list = usenode; }
else { for (ul.next != nil) { ul = ul.next; }; ul.next = usenode; };
};
let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc);
m.str = "main";

View File

@@ -4374,7 +4374,7 @@ fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
// reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a
// cold `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags) i32 = {
objstem: *u8, incs: *u8, lf: *lflags, istest: i32) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_ww");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
@@ -4470,6 +4470,31 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!;
let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir);
if (root < 0) { return 1; };
// #79 (-T): lib/test is the synth main's `test.run` callee but @test
// files never `import test;`. Inject it as a direct dep of the root so
// sepscanpkg pulls test + its transitive deps; the producer adds -T to
// the root and `test.run` links against test's `.a`. Mirrors the
// combined path's auto-bundle (buildone istest) and sepscanfile dedup.
if (istest != 0) {
let td: i32 = 0;
let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td);
if (tp != nil) {
let ti: i32 = sepfindoradd(g, "test\0".ptr, tp, td);
if (ti < 0) { return 1; };
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[root].ndeps) {
if (g.pkg[root].deps[m] == ti) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[root].ndeps < SEP_MAXPKG) {
g.pkg[root].deps[g.pkg[root].ndeps] = ti;
g.pkg[root].ndeps += 1;
};
};
};
};
if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; };
// Reset colors, reverse-topo.
@@ -4517,13 +4542,17 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// LOCAL type (the root is never imported), which the
// export-check rejects. Build a shorter root argv
// without the -I/wwi pair; root's `.wwi` is unconsumed.
// #79: the root carries -T under `ww test --sep` so w6c
// synthesizes the test main; deps never get -T.
let roott: bool = (pi == root) && (istest != 0);
let alen: u64 = 8u64;
if (pi == root) { alen = 6u64; };
if (pi == root) { alen = 6u64; if (roott) { alen = 7u64; }; };
let argv: []*u8 = alloc([], alen)!;
argv.len = (alen: i32);
argv[0] = "w6c\0".ptr;
argv[1] = "-c\0".ptr;
let k: u64 = 2u64;
let k: u64 = 1u64;
if (roott) { argv[k] = "-T\0".ptr; k += 1u64; };
argv[k] = "-c\0".ptr; k += 1u64;
if (pi != root) {
argv[k] = "-I\0".ptr; k += 1u64;
argv[k] = wwi; k += 1u64;
@@ -5128,7 +5157,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.libs = libs.ptr;
lf.nlibs = nlibs;
if (wantsep != 0) {
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf);
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
@@ -5319,7 +5348,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// directory: open the dir, getdents64, build+run each *_test.ww,
// report ok/FAIL per file, return 0 iff all pass.
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = {
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8, usesep: i32) i32 = {
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
tmp.len = os.PATH_MAX;
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
@@ -5333,7 +5362,20 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *
makeruntmp(tmp.ptr);
outp = tmp.ptr;
};
if (buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32) != 0) {
// #79 E1: --sep routes the test build through the separate-compilation
// producer (buildonesep, istest=1) instead of the amalgamator.
let bres: i32 = 0;
if (usesep != 0) {
let lf: lflags;
lf.libdirs = nil;
lf.nlibdirs = 0;
lf.libs = nil;
lf.nlibs = 0;
bres = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32);
} else {
bres = buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32);
};
if (bres != 0) {
if (outstem == nil) { os.remove(pathstr(outp)); };
return 1;
};
@@ -5463,11 +5505,17 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// twin: cmd/ww/main.c do_test (error wording identical).
let compileonly: i32 = 0;
let outstem: *u8 = nil;
// #79 E1: --sep routes a single-file test through the separate-
// compilation producer (buildonesep, istest=1). Dir-mode --sep is
// deferred to E2. cstage twin: do_test use_sep.
let usesep: i32 = 0;
let i: i32 = start;
for (i < argc) {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) { // '-'
if (p[1u64] == 73u8) { // '-I'
if (cstreqlit(p, "--sep")) { // #79: separate-compile test
usesep = 1;
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
@@ -5501,7 +5549,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww test: unknown flag\n");
return 2;
}; }; };
}; }; }; }; // #79: extra close for the --sep else
} else {
if (target == nil) { target = p; }
else { if (patarg == nil) { patarg = p; }; };
@@ -5516,7 +5564,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// single-file mode: literal *.ww that exists
if (cstrendswithlit(target, ".ww")) {
if (os.access(pathstr(target), 0i32) == 0) {
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg);
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg, usesep);
};
};
@@ -5524,6 +5572,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
cerr("ww test: -c/-o need a single test file\n");
return 2;
};
// #79 E1: dir-mode --sep deferred to E2; single-file proves the path.
if (usesep != 0) {
cerr("ww test: --sep needs a single test file\n");
return 2;
};
// #17: a name-filter pattern is per-binary; dir mode builds one binary
// per *_test.ww, so a single pattern can't route. cstage twin parity.
if (patarg != nil) {

View File

@@ -1500,7 +1500,7 @@ fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
// reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a
// cold `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags) i32 = {
objstem: *u8, incs: *u8, lf: *lflags, istest: i32) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_ww");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
@@ -1596,6 +1596,31 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!;
let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir);
if (root < 0) { return 1; };
// #79 (-T): lib/test is the synth main's `test.run` callee but @test
// files never `import test;`. Inject it as a direct dep of the root so
// sepscanpkg pulls test + its transitive deps; the producer adds -T to
// the root and `test.run` links against test's `.a`. Mirrors the
// combined path's auto-bundle (buildone istest) and sepscanfile dedup.
if (istest != 0) {
let td: i32 = 0;
let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td);
if (tp != nil) {
let ti: i32 = sepfindoradd(g, "test\0".ptr, tp, td);
if (ti < 0) { return 1; };
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[root].ndeps) {
if (g.pkg[root].deps[m] == ti) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[root].ndeps < SEP_MAXPKG) {
g.pkg[root].deps[g.pkg[root].ndeps] = ti;
g.pkg[root].ndeps += 1;
};
};
};
};
if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; };
// Reset colors, reverse-topo.
@@ -1643,13 +1668,17 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// LOCAL type (the root is never imported), which the
// export-check rejects. Build a shorter root argv
// without the -I/wwi pair; root's `.wwi` is unconsumed.
// #79: the root carries -T under `ww test --sep` so w6c
// synthesizes the test main; deps never get -T.
let roott: bool = (pi == root) && (istest != 0);
let alen: u64 = 8u64;
if (pi == root) { alen = 6u64; };
if (pi == root) { alen = 6u64; if (roott) { alen = 7u64; }; };
let argv: []*u8 = alloc([], alen)!;
argv.len = (alen: i32);
argv[0] = "w6c\0".ptr;
argv[1] = "-c\0".ptr;
let k: u64 = 2u64;
let k: u64 = 1u64;
if (roott) { argv[k] = "-T\0".ptr; k += 1u64; };
argv[k] = "-c\0".ptr; k += 1u64;
if (pi != root) {
argv[k] = "-I\0".ptr; k += 1u64;
argv[k] = wwi; k += 1u64;
@@ -2254,7 +2283,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.libs = libs.ptr;
lf.nlibs = nlibs;
if (wantsep != 0) {
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf);
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
@@ -2445,7 +2474,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// directory: open the dir, getdents64, build+run each *_test.ww,
// report ok/FAIL per file, return 0 iff all pass.
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = {
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8, usesep: i32) i32 = {
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
tmp.len = os.PATH_MAX;
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
@@ -2459,7 +2488,20 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *
makeruntmp(tmp.ptr);
outp = tmp.ptr;
};
if (buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32) != 0) {
// #79 E1: --sep routes the test build through the separate-compilation
// producer (buildonesep, istest=1) instead of the amalgamator.
let bres: i32 = 0;
if (usesep != 0) {
let lf: lflags;
lf.libdirs = nil;
lf.nlibdirs = 0;
lf.libs = nil;
lf.nlibs = 0;
bres = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32);
} else {
bres = buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32);
};
if (bres != 0) {
if (outstem == nil) { os.remove(pathstr(outp)); };
return 1;
};
@@ -2589,11 +2631,17 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// twin: cmd/ww/main.c do_test (error wording identical).
let compileonly: i32 = 0;
let outstem: *u8 = nil;
// #79 E1: --sep routes a single-file test through the separate-
// compilation producer (buildonesep, istest=1). Dir-mode --sep is
// deferred to E2. cstage twin: do_test use_sep.
let usesep: i32 = 0;
let i: i32 = start;
for (i < argc) {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) { // '-'
if (p[1u64] == 73u8) { // '-I'
if (cstreqlit(p, "--sep")) { // #79: separate-compile test
usesep = 1;
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
@@ -2627,7 +2675,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww test: unknown flag\n");
return 2;
}; }; };
}; }; }; }; // #79: extra close for the --sep else
} else {
if (target == nil) { target = p; }
else { if (patarg == nil) { patarg = p; }; };
@@ -2642,7 +2690,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// single-file mode: literal *.ww that exists
if (cstrendswithlit(target, ".ww")) {
if (os.access(pathstr(target), 0i32) == 0) {
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg);
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg, usesep);
};
};
@@ -2650,6 +2698,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
cerr("ww test: -c/-o need a single test file\n");
return 2;
};
// #79 E1: dir-mode --sep deferred to E2; single-file proves the path.
if (usesep != 0) {
cerr("ww test: --sep needs a single test file\n");
return 2;
};
// #17: a name-filter pattern is per-binary; dir mode builds one binary
// per *_test.ww, so a single pattern can't route. cstage twin parity.
if (patarg != nil) {

View File

@@ -14229,6 +14229,22 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
// (ref/harec/src/check.c:1566-1581).
if (ms != nil && (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32)) {
s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm);
// Module-qualified callee whose leaf isn't scope-keyed
// under its module: align to cstage, which stamps ty_err
// here and lets cgen emit the call (cmd/wcc/check.c:1834-
// 1843; rule-10). The -T synth's `test.run` hits this in
// BOTH combined and sep (Q2-confirmed): lib/test's `run` is
// scope-keyed under "" not "test" — the directive-for-cgen
// vs declmod-for-scope keying split, the module-qualified
// arm of #27. cgen still emits the correct CALL test.run
// from run's `//ww:module test` directive. Load-bearing
// until #80 module-keys run under sep so the synth call
// type-resolves; NOT an M4-transient.
if (s == nil) {
e.type_ = c.tc.tyerr: *void;
callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it)
return nil;
};
};
};
if (s != nil) { if (s.skind == syntax.skind.SK_FN) { if (s.decl != nil) {
@@ -17275,13 +17291,34 @@ 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);
let cid: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
cid.str = "run";
call.lhs = cid;
// 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.
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";
dot.lhs = did;
dot.str = "run";
call.lhs = dot;
call.list = arg;
let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc);
ret.lhs = call;
body.list = ret;
// synth `use test;` so the N_DOT base binds as a module
// qualifier (SK_USE) and usepathfor maps `test` to its path.
// Mirror installdecl's N_USE SK_USE install. Appended to
// file.list below; emits no asm.
let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, pf, pl, pc);
usenode.str = "test";
usenode.usepath = "test";
syntax.scopedefine(c.top, "test", syntax.skind.SK_USE, nil, usenode);
let ul: *syntax.node = file.list;
if (ul == nil) { file.list = usenode; }
else { for (ul.next != nil) { ul = ul.next; }; ul.next = usenode; };
};
let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc);
m.str = "main";