diff --git a/Makefile b/Makefile index c87ac6c3..97781323 100644 --- a/Makefile +++ b/Makefile @@ -569,6 +569,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_separchive_run \ $(BIN)/test_pkgcache_run \ $(BIN)/test_c6soak_run \ + $(BIN)/test_septest_run \ $(BIN)/test_floatlit_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ @@ -3219,6 +3220,17 @@ $(BIN)/test_c6soak_run: test/wcc/989_c6soak_run.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_septest_run — M4 E1 (#79-B) gate: `ww test --sep`. Drives BOTH driver +# stages on a passing + a failing inline @test fixture (COLD per-(case,stage) +# WW_PKGCACHE), asserting run-exit (pass=0, fail=1 = the non-vacuity teeth), +# cs==ww per-pkg .s/.wwi (rule 10), and the synth `CALL test.run` in __root.s +# (the -T synth fired under sep). Needs both driver + both compiler + both +# linker stages + libwwrt.a for the test-binary link. +$(BIN)/test_septest_run: test/wcc/989_septest_run.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index a40347e7..8e7e3812 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -3175,16 +3175,40 @@ check_file(Checker *c, Node *file) scope_define_in_module(c->cur, tab->str, NULL, SK_VAR, tt, tab); - /* return run(__wwtests); */ + /* 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. */ 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 = "run"; + Node *dot = newnode(c->a, N_DOT, fp); + dot->lhs = newnode(c->a, N_IDENT, fp); + dot->lhs->str = "test"; + dot->str = "run"; + call->lhs = dot; call->list = arg; Node *ret = newnode(c->a, N_RETURN, fp); ret->lhs = call; body->list = ret; + /* synth `use test;` so the N_DOT base ident binds as a + * module qualifier (SK_USE) and use_path maps `test` to + * its import path. Mirror the typedecl-pass N_USE install + * (check.c:2892). Appended to file->list below; emits no + * asm (cgen keys off module-tagged decls). */ + Node *usenode = newnode(c->a, N_USE, fp); + usenode->str = "test"; + usenode->strlen = 4; + usenode->usepath = "test"; + scope_define(c->cur, "test", SK_USE, NULL, usenode); + Node *ul = file->list; + if (ul == NULL) file->list = usenode; + else { while (ul->next) ul = ul->next; ul->next = usenode; } } Node *m = newnode(c->a, N_FNDECL, fp); diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 70a07c96..a61c225b 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -1132,7 +1132,7 @@ cache_store(struct sepgraph *g, int pi, const char *manifest, static int build_one_sep(const char *src, int entry_is_dir, const char *out, const char *objstem, const char *extra_includes, const char *extra_libs, - const char *extra_libdirs) + const char *extra_libdirs, int is_test) { const char *c6 = toolpath("WW_W6C", "w6c"); const char *a6 = toolpath("WW_W6A", "w6a"); @@ -1197,7 +1197,27 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; int root = sep_find_or_add(g, "", src, entry_is_dir); - if (root < 0 || sep_scan_pkg(g, root, srcdir) < 0) { free(g); return 1; } + if (root < 0) { free(g); 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 + * sep_scan_pkg 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 (build_one is_test) and the sep_scan_file + * dedup-guarded dep append. */ + if (is_test) { + char tpath[1024]; + int tdir = 0; + if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { + int ti = sep_find_or_add(g, "test", tpath, tdir); + if (ti < 0) { free(g); return 1; } + int seen = 0; + for (int k = 0; k < g->pkg[root].ndeps; k++) + if (g->pkg[root].deps[k] == ti) { seen = 1; break; } + if (!seen && g->pkg[root].ndeps < SEP_MAXPKG) + g->pkg[root].deps[g->pkg[root].ndeps++] = ti; + } + } + if (sep_scan_pkg(g, root, srcdir) < 0) { free(g); return 1; } for (int i = 0; i < g->n; i++) g->pkg[i].color = 0; int *order = calloc((size_t)g->n, sizeof *order); int *stack = calloc((size_t)g->n, sizeof *stack); @@ -1233,8 +1253,11 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, * export-check rejects. Skip -I for the root; its `.wwi` * is never consumed. */ if (pi == root) - snprintf(cmd, sizeof cmd, "%s -c -o %s %s", - c6, asmf, unitf); + /* #79: the root carries -T under `ww test --sep` + * so w6c synthesizes the test main. Deps never + * get -T. */ + snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", + c6, is_test ? "-T " : "", asmf, unitf); else snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", c6, wwi, asmf, unitf); @@ -1542,7 +1565,7 @@ do_build(int argc, char **argv) } if (want_sep) return build_one_sep(resolved, is_dir, out, objstem, incs, - libs, libdirs); + libs, libdirs, 0); return build_one(resolved, is_dir, out, objstem, incs, libs, libdirs, 0); } @@ -1606,12 +1629,21 @@ do_test(int argc, char **argv) * wwstage twin (selfhost/cmd/ww/main.ww dotest). */ int compileonly = 0; char outstem[1024] = {0}; + /* #79 E1: --sep routes a single-file/module test through the + * separate-compilation producer (build_one_sep, is_test=1) instead of + * the amalgamator. Additive (combined stays default); dir-mode --sep is + * deferred to E2. */ + int use_sep = 0; /* #17: an optional second positional after the target is a fnmatch * name-filter pattern, forwarded to the test binary as argv[1]. Only * meaningful for a single test file/module — rejected in dir mode. */ const char *pattern = NULL; for (int i = 0; i < argc; i++) { if (argv[i][0] == '-') { + if (strcmp(argv[i], "--sep") == 0) { + use_sep = 1; + continue; + } if (argv[i][1] == 'I') { const char *dir; if (argv[i][2]) { @@ -1664,8 +1696,12 @@ do_test(int argc, char **argv) const char *outp; if (outstem[0]) outp = outstem; else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - if (build_one(resolved, is_dir, outp, outstem[0] ? outstem : NULL, - incs, "", "", 1) != 0) return 1; + int br = use_sep + ? build_one_sep(resolved, is_dir, outp, + outstem[0] ? outstem : NULL, incs, "", "", 1) + : build_one(resolved, is_dir, outp, + outstem[0] ? outstem : NULL, incs, "", "", 1); + if (br != 0) return 1; if (compileonly) return 0; int rc = run_test_bin(outp, pattern); if (!outstem[0]) unlink(outp); @@ -1677,8 +1713,12 @@ do_test(int argc, char **argv) const char *outp; if (outstem[0]) outp = outstem; else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - if (build_one(target, 0, outp, outstem[0] ? outstem : NULL, - incs, "", "", 1) != 0) return 1; + int br = use_sep + ? build_one_sep(target, 0, outp, outstem[0] ? outstem : NULL, + incs, "", "", 1) + : build_one(target, 0, outp, outstem[0] ? outstem : NULL, + incs, "", "", 1); + if (br != 0) return 1; if (compileonly) return 0; int rc = run_test_bin(outp, pattern); if (!outstem[0]) unlink(outp); @@ -1692,6 +1732,13 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: -c/-o need a single test file\n"); return 2; } + /* #79 E1: dir-mode --sep is deferred to E2 — single-file/module is + * enough to prove the sep test path. Loud-reject rather than silently + * fall back to the combined per-file build. */ + if (use_sep) { + fprintf(stderr, "ww test: --sep needs a single test file\n"); + return 2; + } /* #17: a name-filter pattern is per-binary; directory mode builds one * binary per *_test.ww, so a single pattern can't sensibly route. */ if (pattern) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1750efef..9b2497db 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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"; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 00ffcfc9..c63100f1 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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"; diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index db830671..0973c617 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -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 `.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 ; 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) { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 0934a3e5..4554dd10 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -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 `.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 ; 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) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1724ea40..6e539e8e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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"; diff --git a/test/wcc/989_septest_run.c b/test/wcc/989_septest_run.c new file mode 100644 index 00000000..508963fa --- /dev/null +++ b/test/wcc/989_septest_run.c @@ -0,0 +1,260 @@ +/* + * 989_septest_run — M4 E1 (#79-B) regression gate: `ww test --sep`. + * + * E1 makes `ww test` work under the separate-compilation producer as an + * ADDITIVE opt-in (combined stays default; 910/997 untouched). The synth + * test-main emits `test.run(__wwtests)` (N_DOT, qualified) + injects a + * synthetic `use test;`; the driver injects `test` as a graph dep of the + * root, passes `-T` to the root producer only, and routes a single-file + * `ww test --sep` through build_one_sep(is_test=1). This gate is the + * STANDING regression for that capability until E2 migrates 997. + * + * Asserts (all COLD — fresh per-(case,stage) WW_PKGCACHE, scratch wiped): + * 1. RUN-EXIT: `ww test --sep ` exits with the expected code for + * BOTH driver stages — 0 when every @test passes, non-zero when one + * fails. The failing row is the non-vacuity teeth: it proves the @tests + * genuinely RUN under sep (a no-op --sep that linked an empty main would + * exit 0 on the failing fixture and the gate would catch it). + * 2. cs==ww (rule 10): the cstage `ww` and wwstage `ww_ww` sep-drivers emit + * byte-identical per-package .s/.wwi for the passing fixture — including + * __root.s, which carries the synth `CALL test.run`. + * 3. SYNTH PRESENCE (non-vacuity on the -T path): the passing fixture's + * __root.s defines `TEXT main` and emits `CALL test.run(SB)` — proving + * the qualified-synth -T path fired under sep, not a stray combined fall- + * back. + * + * Light wwstage-driver test (CLAUDE.md rule 14): every intermediate is + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models + * 989_c6soak_run.c conventions; 989 prefix per the sep-gate precedent. + */ +#include +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + 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; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + 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 -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +files_eq(const char *a, const char *b) +{ + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + free(ba); free(bb); + return -1; + } + int eq = (na == nb && memcmp(ba, bb, na) == 0); + free(ba); free(bb); + return eq ? 0 : 1; +} + +/* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */ +static int +file_contains(const char *path, const char *needle) +{ + char *b = NULL; + size_t n = 0; + if (slurp(path, &b, &n) < 0) return -1; + int found = (strstr(b, needle) != NULL); + free(b); + return found ? 0 : 1; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +/* cs==ww over a sep test build's per-package output: every .s/.wwi the + * cstage driver produced in `csdir` must be byte-identical to the wwstage + * driver's same-named file in `wwdir`. Returns the count of mismatches. */ +static int +cmp_sepwork(const char *csdir, const char *wwdir, const char *label) +{ + DIR *d = opendir(csdir); + if (!d) { + fprintf(stderr, "septest FAIL: %s — no cs sepwork %s\n", label, csdir); + return 1; + } + int bad = 0, seen = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + const char *nm = ent->d_name; + size_t nl = strlen(nm); + int is_s = (nl > 2 && strcmp(nm + nl - 2, ".s") == 0); + int is_wwi = (nl > 4 && strcmp(nm + nl - 4, ".wwi") == 0); + if (!is_s && !is_wwi) continue; + seen++; + char a[2048], b[2048]; + snprintf(a, sizeof a, "%s/%s", csdir, nm); + snprintf(b, sizeof b, "%s/%s", wwdir, nm); + if (files_eq(a, b) != 0) { + fprintf(stderr, "septest FAIL: %s — cs!=ww for %s (rule 10)\n", + label, nm); + bad++; + } + } + closedir(d); + /* An existing-but-empty sepwork would pass the cs==ww loop vacuously. */ + if (seen == 0) { + fprintf(stderr, "septest FAIL: %s — no .s/.wwi in %s\n", label, csdir); + bad++; + } + return bad; +} + +struct tcase { + const char *label; + const char *src; /* inline fixture written to /