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);