From 939c984f51a7991dc0ed6567eca7a3e5d5b86b9f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 17 Jun 2026 23:54:21 +0900 Subject: [PATCH] wcc,ww: prepend synth `use test`; user fn run coexists with runner (M4 E2, #80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The -T harness synthesized `use test;` after name-binding, so the lib/test runner run keyed the bare scope and collided with a user-defined bare fn run — a spurious "duplicate fn run" reject (the E1 tolerance seam). Prepending the synth use before binding keys the runner as test.run in the test module namespace, distinct from the user bare run; the two coexist. Hare-faithful: the runner is its own test module (ref/hare/test/+test.ha:97). Inverts attest_userrun.ww from the #23-mandated reject to a coexist fixture; gate asserts exactly 1 TEXT run + 1 TEXT test.run on the -T asm (distinct symbols, not a dead-dup). Closes #80. --- cmd/wcc/check.c | 37 +++++---- lib/test/run.ww | 15 ++-- selfhost/cmd/w6c/main.combined.ww | 51 +++++++----- selfhost/cmd/wcc/check.ww | 51 +++++++----- selfhost/cmd/wwdump/main.combined.ww | 51 +++++++----- test/wcc/910_at_test.c | 86 +++++++++++++++++---- test/wcc/997_at_test_ww.c | 111 +++++++++++++++++++++++---- test/wcc/data/attest_userrun.ww | 18 +++-- 8 files changed, 308 insertions(+), 112 deletions(-) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 8e7e3812..ad6a47ad 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2856,6 +2856,24 @@ 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 + * 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` + * 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. */ + if (c->is_test) { + Node *usenode = newnode(c->a, N_USE, file->pos); + usenode->str = "test"; + usenode->strlen = 4; + usenode->usepath = "test"; + usenode->next = file->list; + file->list = usenode; + } + /* pass 1: install names (types first, then defs/fns). * For self-referential types we install the named-type placeholder * BEFORE resolving its body; the body may legitimately mention @@ -3196,19 +3214,12 @@ check_file(Checker *c, Node *file) 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; } + /* #80: the synth `use test;` (the N_DOT base qualifier + + * the use_path source mapping `test`→its path) 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 + * (SK_USE). */ } Node *m = newnode(c->a, N_FNDECL, fp); diff --git a/lib/test/run.ww b/lib/test/run.ww index ac7fee6a..3bb5bcca 100644 --- a/lib/test/run.ww +++ b/lib/test/run.ww @@ -53,13 +53,14 @@ import os; // lib/test symbol isolation, every -T build flat-bundles this module into // the SAME bare-leaf scope as the test's own modules, so an un-prefixed // `puts`/`runone` collides with a same-named module-private fn (lib/dirs -// and lib/temp both ship a private `puts(off, s)`). `run` stays the bound -// runner name (the synth's callee). A user `fn run` (or `const __wwtests`) -// in a @test unit collides with the synth: cstage — the shipping `ww test` -// path — rejects it loudly ("duplicate fn run" / type mismatch, rc!=0). -// wwstage TOLERATES the duplicate (#23, pre-existing rule-10 gap: cstage -// rejects a duplicate fn, wwstage does not), so the collision is loud on -// the reference stage but silent on wwstage until #23 lands. +// and lib/temp both ship a private `puts(off, s)`). `run` is the bound +// runner name (the synth's callee), reached module-qualified as `test.run`: +// the -T synth prepends `use test;` before name-binding (#80), so this +// runner keys into the `test` module namespace — a symbol distinct from any +// bare user `fn run` in the @test unit. The two COEXIST: `run` and +// `test.run` are separate symbols (ref/hare/test/+test.ha:97 — the runner +// is its own `test` module), so a user `fn run` is no longer a duplicate and +// both stages accept it under @test/-T. export fn run(tests: [](str, *fn() void)) i32 = { let av: []str = os.args(); let nfail: i32 = 0; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7414c4ac..a5a629b2 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14232,14 +14232,15 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // 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. + // 1843; rule-10). This now tolerates only a genuinely- + // extern leaf (raw w6c on a single module-qualified file + // with no driver concat), the module-qualified arm of #27. + // The -T synth's `test.run` NO LONGER lands here: #80 + // PREPENDS the synth `use test;` before Pass 1, so declmod + // keys lib/test's `run` under "test" (not "") and the synth + // `test.run` type-resolves — the E1 bridge is closed. cgen + // keys the CALL off run's `//ww:module test` directive + // either way, so the resolution change is asm-neutral. if (s == nil) { e.type_ = c.tc.tyerr: *void; callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it) @@ -17108,6 +17109,24 @@ 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 + // 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 + // `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 + // 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.next = file.list; + file.list = usenode; + }; + // Pass 1: install all top-level names. let d: *syntax.node = file.list; for (d != nil) { @@ -17308,17 +17327,11 @@ 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; - // 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; }; + // #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. }; 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 c63100f1..800f5104 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3489,14 +3489,15 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // 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. + // 1843; rule-10). This now tolerates only a genuinely- + // extern leaf (raw w6c on a single module-qualified file + // with no driver concat), the module-qualified arm of #27. + // The -T synth's `test.run` NO LONGER lands here: #80 + // PREPENDS the synth `use test;` before Pass 1, so declmod + // keys lib/test's `run` under "test" (not "") and the synth + // `test.run` type-resolves — the E1 bridge is closed. cgen + // keys the CALL off run's `//ww:module test` directive + // either way, so the resolution change is asm-neutral. if (s == nil) { e.type_ = c.tc.tyerr: *void; callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it) @@ -6365,6 +6366,24 @@ 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 + // 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 + // `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 + // 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.next = file.list; + file.list = usenode; + }; + // Pass 1: install all top-level names. let d: *syntax.node = file.list; for (d != nil) { @@ -6565,17 +6584,11 @@ 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; - // 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; }; + // #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. }; let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc); m.str = "main"; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 44ef631d..75ed2839 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14232,14 +14232,15 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // 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. + // 1843; rule-10). This now tolerates only a genuinely- + // extern leaf (raw w6c on a single module-qualified file + // with no driver concat), the module-qualified arm of #27. + // The -T synth's `test.run` NO LONGER lands here: #80 + // PREPENDS the synth `use test;` before Pass 1, so declmod + // keys lib/test's `run` under "test" (not "") and the synth + // `test.run` type-resolves — the E1 bridge is closed. cgen + // keys the CALL off run's `//ww:module test` directive + // either way, so the resolution change is asm-neutral. if (s == nil) { e.type_ = c.tc.tyerr: *void; callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it) @@ -17108,6 +17109,24 @@ 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 + // 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 + // `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 + // 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.next = file.list; + file.list = usenode; + }; + // Pass 1: install all top-level names. let d: *syntax.node = file.list; for (d != nil) { @@ -17308,17 +17327,11 @@ 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; - // 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; }; + // #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. }; let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc); m.str = "main"; diff --git a/test/wcc/910_at_test.c b/test/wcc/910_at_test.c index 01fbdb04..5d3a567e 100644 --- a/test/wcc/910_at_test.c +++ b/test/wcc/910_at_test.c @@ -22,6 +22,7 @@ */ #include #include +#include #include #include @@ -282,17 +283,49 @@ accept(const char *bin, const char *comp, const char *fixture, const char *what) return 0; } -/* collide_run — a @test unit defining a user `fn run` collides with - * lib/test's bound runner once ` test -c` bundles it; ` -T` of - * the combined must loud-reject the duplicate (#23). Pre-fix wwstage built - * a binary that called the user run and silently skipped every @test. */ +/* count_text — number of `TEXT ,` directive lines in an asm file + * (Plan-9 `TEXT name,$frame`), -1 on open failure. A COUNT (not presence) + * is what distinguishes the #84 dead-dup: the bug yields 0x `TEXT run` + + * 2x `TEXT test.run`, which a mere presence check would miss. */ static int -collide_run(const char *bin, const char *comp, const char *drv) +count_text(const char *path, const char *sym) +{ + char want[128]; + snprintf(want, sizeof want, "TEXT %s,", sym); + size_t wlen = strlen(want); + FILE *f = fopen(path, "r"); + if (f == NULL) return -1; + char line[8192]; + int n = 0; + while (fgets(line, sizeof line, f) != NULL) + if (strncmp(line, want, wlen) == 0) n++; + fclose(f); + return n; +} + +/* coexist_run — a @test unit that ALSO defines a user `fn run` must + * COEXIST with lib/test's bound runner, not collide. Post-#80 the synth + * `use test;` is prepended before binding, so lib/test's `run` keys under + * "test" (not "") and no longer duplicate-collides with the user's bare + * `run` (which mangles bare via #84). ` -T` of the ` test -c` + * combined now ACCEPTS; the -T asm carries EXACTLY 1 `TEXT run` (user, + * bare) + 1 `TEXT test.run` (lib runner) — the static-label proof that the + * user run is distinct in the real @test/-T path where #84 lives; then, + * assembled, linked and run, the synth entry exits 0 (the user `run` did + * not hijack the runner and the @test passes). Pre-#80 this loud-rejected + * "duplicate fn run". */ +static int +coexist_run(const char *bin, const char *comp, const char *drv) { int pid = getpid(); - char stem[256], comb[300], cmd[4096]; + char stem[256], comb[300], asmf[320], obj[320], exe[320]; + char rt[1024], cmd[4096]; snprintf(stem, sizeof stem, "/tmp/at910cr_%s_%d", comp, pid); snprintf(comb, sizeof comb, "%s.combined.ww", stem); + snprintf(asmf, sizeof asmf, "%s.run.s", stem); + snprintf(obj, sizeof obj, "%s.run.o", stem); + snprintf(exe, sizeof exe, "%s.exe", stem); + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); snprintf(cmd, sizeof cmd, "%s/%s test -c -o %s test/wcc/data/attest_userrun.ww > /dev/null 2>&1", bin, drv, stem); @@ -301,15 +334,40 @@ collide_run(const char *bin, const char *comp, const char *drv) fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb); return 1; } - snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null", - bin, comp, comb); int rc = 0; - if (runwait(cmd) == 0) { - fprintf(stderr, "910 FAIL: %s -T accepted user `fn run` collision " - "(expected duplicate-fn reject)\n", comp); + 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 rejected user `fn run` coexist " + "(expected accept post-#80)\n", comp); rc = 1; } - unlink(comb); + if (rc == 0) { + int nr = count_text(asmf, "run"); + int nt = count_text(asmf, "test.run"); + if (nr != 1 || nt != 1) { + fprintf(stderr, "910 FAIL: %s -T asm label count run=%d " + "test.run=%d (want 1/1) — user `fn run` not distinct from " + "lib runner (a #84 dead-dup gives run=0/test.run=2)\n", + comp, nr, nt); + rc = 1; + } + } + if (rc == 0) { + snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, asmf); + if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: coexist w6a\n"); rc = 1; } + } + if (rc == 0) { + 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: coexist w6l\n"); rc = 1; } + } + if (rc == 0 && runwait(exe) != 0) { + fprintf(stderr, "910 FAIL: coexist synth entry nonzero — user " + "`fn run` hijacked the runner or the @test failed\n"); + rc = 1; + } + unlink(comb); unlink(asmf); unlink(obj); unlink(exe); char tmp[320]; snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); @@ -384,11 +442,11 @@ main(void) "use-before-value") != 0) return 1; if (modfn_run(bin, "ww", "test/wcc/data/modfn_coexist_vbu_ok.ww", "value-before-use") != 0) return 1; - if (collide_run(bin, "w6c", "ww") != 0) return 1; + if (coexist_run(bin, "w6c", "ww") != 0) return 1; printf("@test -T: run ok + user-main and bad-signature rejected + " "non-T @test drop + checked-body + dangling-call link-fail (#6) + " "dup fn/type/def/let reject + xpkg + builtin-redecl accept + " - "modfn coexist (#30) + fn-run collision (#23)\n"); + "modfn coexist (#30) + fn-run coexist (#80)\n"); return 0; } diff --git a/test/wcc/997_at_test_ww.c b/test/wcc/997_at_test_ww.c index e0b910fc..bf896065 100644 --- a/test/wcc/997_at_test_ww.c +++ b/test/wcc/997_at_test_ww.c @@ -330,34 +330,113 @@ accept_byteid(const char *bin, const char *fixture, const char *what) return rc; } -/* collide_run — a @test unit defining a user `fn run` collides with - * lib/test's bound runner once `ww test -c` bundles it; `w6c_ww -T` of the - * combined must loud-reject the duplicate (#23). Pre-fix wwstage built a - * binary that called the user run and silently skipped every @test. */ +/* count_text — number of `TEXT ,` directive lines in an asm file + * (Plan-9 `TEXT name,$frame`), -1 on open failure. A COUNT (not presence) + * is what distinguishes the #84 dead-dup: the bug yields 0x `TEXT run` + + * 2x `TEXT test.run`, which a mere presence check would miss. */ static int -collide_run(const char *bin) +count_text(const char *path, const char *sym) +{ + char want[128]; + snprintf(want, sizeof want, "TEXT %s,", sym); + size_t wlen = strlen(want); + FILE *f = fopen(path, "r"); + if (f == NULL) return -1; + char line[8192]; + int n = 0; + while (fgets(line, sizeof line, f) != NULL) + if (strncmp(line, want, wlen) == 0) n++; + fclose(f); + return n; +} + +/* coexist_run — a @test unit that ALSO defines a user `fn run` must + * COEXIST with lib/test's bound runner, not collide. Post-#80 the synth + * `use test;` is prepended before binding, so lib/test's `run` keys under + * "test" (not "") and no longer duplicate-collides with the user's bare + * `run` (which mangles bare via #84). `w6c_ww -T` of the `ww_ww test -c` + * combined now ACCEPTS; the -T asm carries EXACTLY 1 `TEXT run` (user, + * bare) + 1 `TEXT test.run` (lib runner) and is byte-identical to w6c's + * (cs==ww) — the static-label proof that the user run is distinct in the + * real @test/-T path where #84 lives; then, assembled, linked and run, the + * synth entry exits 0 (the user `run` did not hijack the runner and the + * @test passes). Pre-#80 this loud-rejected "duplicate fn run". */ +static int +coexist_run(const char *bin) { int pid = getpid(); - char stem[256], comb[300], cmd[4096]; + char stem[256], comb[300], asmf[320], csasm[320], obj[320], exe[320]; + char rt[1024], cmd[4096]; snprintf(stem, sizeof stem, "/tmp/at997cr_%d", pid); snprintf(comb, sizeof comb, "%s.combined.ww", stem); + snprintf(asmf, sizeof asmf, "%s.run.s", stem); + snprintf(csasm, sizeof csasm, "%s.cs.s", stem); + snprintf(obj, sizeof obj, "%s.run.o", stem); + snprintf(exe, sizeof exe, "%s.exe", stem); + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); snprintf(cmd, sizeof cmd, - "%s/ww test -c -o %s test/wcc/data/attest_userrun.ww > /dev/null 2>&1", + "%s/ww_ww test -c -o %s test/wcc/data/attest_userrun.ww > /dev/null 2>&1", bin, stem); runwait(cmd); if (access(comb, 0) != 0) { - fprintf(stderr, "997 FAIL: ww build produced no %s\n", comb); + fprintf(stderr, "997 FAIL: ww_ww build produced no %s\n", comb); return 1; } - snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o /dev/null 2>/dev/null", - bin, comb); int rc = 0; - if (runwait(cmd) == 0) { - fprintf(stderr, "997 FAIL: w6c_ww -T accepted user `fn run` collision " - "(expected duplicate-fn reject)\n"); + 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 rejected user `fn run` coexist " + "(expected accept post-#80)\n"); rc = 1; } - unlink(comb); + if (rc == 0) { + int nr = count_text(asmf, "run"); + int nt = count_text(asmf, "test.run"); + if (nr != 1 || nt != 1) { + fprintf(stderr, "997 FAIL: w6c_ww -T asm label count run=%d " + "test.run=%d (want 1/1) — user `fn run` not distinct from " + "lib runner (a #84 dead-dup gives run=0/test.run=2)\n", + nr, nt); + rc = 1; + } + } + if (rc == 0) { + snprintf(cmd, sizeof cmd, "%s/w6c -T %s -o %s 2>/dev/null", + bin, comb, csasm); + if (runwait(cmd) != 0) { + fprintf(stderr, "997 FAIL: w6c -T (coexist cs==ww)\n"); + rc = 1; + } + } + if (rc == 0) { + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + if (slurp(csasm, &bc, &nc) < 0 || slurp(asmf, &bw, &nw) < 0) { + fprintf(stderr, "997 FAIL: slurp coexist asm\n"); + rc = 1; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "997 FAIL: coexist -T asm cs!=ww " + "(cs %zu, ww %zu)\n", nc, nw); + rc = 1; + } + free(bc); free(bw); + } + if (rc == 0) { + snprintf(cmd, sizeof cmd, "%s/w6a_ww -o %s %s 2>/dev/null", bin, obj, asmf); + if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: coexist w6a_ww\n"); rc = 1; } + } + if (rc == 0) { + 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: coexist w6l_ww\n"); rc = 1; } + } + if (rc == 0 && runwait(exe) != 0) { + fprintf(stderr, "997 FAIL: coexist synth entry nonzero — user " + "`fn run` hijacked the runner or the @test failed\n"); + rc = 1; + } + unlink(comb); unlink(asmf); unlink(csasm); unlink(obj); unlink(exe); char tmp[320]; snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); @@ -434,11 +513,11 @@ main(void) "use-before-value") != 0) return 1; if (modfn_run(bin, "test/wcc/data/modfn_coexist_vbu_ok.ww", "value-before-use") != 0) return 1; - if (collide_run(bin) != 0) return 1; + if (coexist_run(bin) != 0) return 1; printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects + " "non-T @test drop cs/ww byte-id + checked-body (#6) + " "dup fn/type/def/let reject + xpkg/builtin-redecl byte-id + " - "modfn coexist byte-id (#30) + fn-run collision (#23)\n"); + "modfn coexist byte-id (#30) + fn-run coexist (#80)\n"); return 0; } diff --git a/test/wcc/data/attest_userrun.ww b/test/wcc/data/attest_userrun.ww index 813a99a7..16f96157 100644 --- a/test/wcc/data/attest_userrun.ww +++ b/test/wcc/data/attest_userrun.ww @@ -1,8 +1,16 @@ -// #23 -T collision — a user `fn run` collides with lib/test's bound -// runner `run` (the synth's callee) once `ww test -c` bundles lib/test. -// Both stages must loud-reject "duplicate fn run" (rc!=0), never silently -// build a binary that calls the wrong run and skips every @test. -fn run() void = { return; }; +// #80/#84 -T coexist — a user `fn run` COEXISTS with lib/test's bound +// runner `run` (the synth main's callee). Once `ww test -c` bundles +// lib/test, the synth `use test;` keys lib/test's `run` under module +// "test" (#80: prepended before binding) and the user's package-less +// `run` mangles to a DISTINCT bare `run` (#84) — no duplicate. Both +// stages accept under -T and the synth runner runs the @test to exit 0; +// the user `run` must NOT hijack the entry. Pre-#80 this loud-rejected +// "duplicate fn run". The gate (910/997 coexist_run) static-label-counts +// the -T asm — exactly 1 `TEXT run` (user, bare) + 1 `TEXT test.run` (lib +// runner) — pinning #84 distinctness here in the REAL @test/-T path (a +// dead-dup regression yields 0x run / 2x test.run). General callability of +// the bare run is gated separately by 989_barefn_collide. +fn run() i32 = { return 9; }; @test fn t_one() void = { assert(1 == 1);