diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 7bf03a4f..c469c2e2 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -3885,6 +3885,35 @@ classify_init_decls(Checker *c, Node *file) } } +/* Pinned Go 1.26.5 types2 rejects a non-function package-scope `main` + * before declaring it, but only when the declared package name is `main` + * (resolver.go:90-110 declarePkgObj). WW's function entry ABI deliberately + * permits argument/result-bearing functions, so this is the independent + * declaration-kind rule: canonical identity, physical directory, path leaf, + * and root/action status are not inputs. Remove rejected declarations from + * later name installation just as the pinned resolver returns without + * declaring its object. */ +static void +reject_nonfunction_main_decls(Checker *c, Node *file) +{ + Node *prev = NULL; + for (Node *d = file->list; d; ) { + Node *next = d->next; + int invalid = top_decl_kind(d) && d->kind != N_FNDECL + && d->str != NULL && strcmp(d->str, "main") == 0 + && d->pkgname != NULL && strcmp(d->pkgname, "main") == 0; + if (invalid) { + err(c, d->pos, "cannot declare main - must be func"); + if (prev == NULL) + file->list = next; + else + prev->next = next; + } else + prev = d; + d = next; + } +} + /* Import usage is a property of the file-local qualifier occurrence. Record * qualified syntax before resolving declaration bodies so import diagnostics * retain production Go's source order without making a failed bare lookup a @@ -4049,6 +4078,7 @@ check_file(Checker *c, Node *file) file->list = usenode; } } + reject_nonfunction_main_decls(c, file); mark_import_uses(c, file); check_import_redeclarations(c, file); check_import_usage_and_collisions(c, file); diff --git a/docs/build-system.md b/docs/build-system.md index 16114f10..3b4911d1 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -7771,6 +7771,181 @@ ordinary non-list `[no matches]` plus accounting pinned. No persisted-byte contract changed: build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. +### 11.38 Implemented declared-`main` function-kind semantics + +A package whose **declared package name** is `main` now rejects every +package-scope non-function declaration named `main`. `let`, `const`, `def`, and +`type` forms receive `cannot declare main - must be func` from either compiler +checker and are not installed in package scope. The rule is deliberately +narrower than Go's complete source signature rule: WW retains its established +C/Hare-style program-entry ABI, including supported argument- and +result-bearing function declarations. + +#### Pinned Go evidence and fact classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `types2.(*Checker).declarePkgObj` tests both identifier spelling `main` and + `check.pkg.name == "main"`, emits `cannot declare main - must be func`, and + returns without declaring the object + ([`cmd/compile/internal/types2/resolver.go`, lines 90–110](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L90-L110)). + The public checker implements the same condition and return + ([`go/types/resolver.go`, method `(*Checker).declarePkgObj`, lines 103–124](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/types/resolver.go#L103-L124)). +- Official type-checker testdata rejects constant, type, and variable + declarations named `main` in package `main` + ([`internal/types/testdata/check/decls5.go`, lines 5–10](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/decls5.go#L5-L10)). + The fixed-bug test also rejects `var main = func() {}`: a variable containing + a function is still not a function declaration + ([`test/fixedbugs/issue21256.go`, lines 1–9](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/issue21256.go#L1-L9)). + +Those conditions, diagnostics, early returns, and testdata assertions are +**behavior directly implemented or asserted by pinned Go**. That the semantic +owner is package declaration checking; that a rejected object does not become +the entry binding; and that declared package name rather than canonical path, +path leaf, physical directory, or command selection owns the rule are +**behavior derived from the pinned implementation**. + +Pinned Go separately requires a function `main` in package `main` to have no +arguments or results +([`cmd/compile/internal/types2/resolver.go`, method +`(*Checker).collectObjects`, lines 416–444](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L416-L444)), +asserted by official `mainsig.go` +([lines 7–13](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/mainsig.go#L7-L13)). +That is also **behavior directly implemented or asserted by pinned Go**, but it +does not honestly apply to WW's source ABI. Both baseline WW stages accepted +`fn main(x: i32) void` and `fn main() i32`, produced byte-identical +executables, and ran them successfully; WW's own self-hosted command tools use +`main(argc: i32, argv: **u8) i32`. Those observations are **directly measured +WW behavior**. Preserving those function forms while applying the independent +declaration-kind requirement is **behavior derived from the pinned +implementation within WW's applicable model boundary**. + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit examined all four permanent axes before this package slice +was selected: + +- **Go-like build:** pinned `(*ErrorReporter).errorUnresolved` gives missing + `main.main` a dedicated error + ([`cmd/link/internal/ld/errors.go`, lines 29–67](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L29-L67)), + asserted by `TestUndefinedRelocErrors` + ([`cmd/link/internal/ld/ld_test.go`, lines 19–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L19-L45)) + and official `testdata/issue10978/main.go` lines 5–27. Both WW stages + rejected a selected `main` package with no entry, emitted empty stdout and + the same 50 stderr bytes (SHA-256 + `9ed4d7684412c6d2e615041902072c81e9e09acb3970246d89a2c8bdddd2fcfa`), + and published nothing. This applicable control was aligned. +- **Go-like test:** pinned `isTestFunc` and `checkTestFunc` define and reject a + wrong test function shape + ([`cmd/go/internal/load/test.go`, lines 555–579 and 775–787](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/test.go#L555-L579)), + with the official wrong-signature script anchor at + [`cmd/go/testdata/script/test_main.txt`, lines 11–13 and 30–40](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_main.txt#L11-L13). + Both WW stages rejected `@test fn bad(x: i32) void` before execution, emitted + the same semantic diagnostic, no accounting, and final `FAIL\n` stdout + (SHA-256 + `4f8e9e45f8a9e1843b81eaf3bdf52a6b778d415d23bf985774a9d34a43f69bd5`). + This applicable control was aligned. +- **Go-like package:** baseline Cstage accepted `let main`, `const main`, and + `def main`, published mode-0755 executables, and those executables exited 139 + with empty output. The `let`/`const` executable SHA-256 was + `edd3bad62be69701a373aa0567972bfb976690b0332b8c117891125254fc85b5`; + the `def` executable SHA-256 was + `9e56d6710395b287e18e85187eee86d846927c6f3ea91216858c6026f38ebdff`. + Cstage `type main` and every WWstage non-function form instead reached the + linker's missing-`main` failure. Neither stage emitted the pinned package + diagnostic. Both stages accepted a directory command package containing + `let main` plus a valid internal test, ran it, and reported package `ok` with + byte-identical 178-byte stdout (SHA-256 + `a6e165fb558be62932e217ccd1d3175348f7490489bca8e9828872e28c01236f`). + This was the selected difference. +- **Go-like import:** pinned `unusedImports` and `errorUnusedPkg` reject a + nonblank unused alias + ([`cmd/compile/internal/types2/resolver.go`, lines 706–740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)), + asserted by official `importdecl0a.go` lines 9–27. Both WW stages rejected + unused `import spare audit.dep;`, emitted empty stdout, and reported + `"audit.dep" imported as spare and not used`. This applicable control was + aligned. + +The WW command results, output lengths, hashes, exit statuses, and runtime +signals are **directly measured WW behavior**. The linked source and testdata +facts are **behavior directly implemented or asserted by pinned Go**. Selecting +the declaration-kind rule while excluding the incompatible function-signature +rule is **behavior derived from the pinned implementation**. + +As an identity control, both stages built `package utility; export let main: +i32 = 7` as byte-identical 924-byte archives (SHA-256 +`10382e7812229d73c4acefdf8988a13372b6eb7a2981559b3adade524ed5a929`). +That is **directly measured WW behavior** and pins the required non-effect for +non-`main` declared packages. + +#### Ownership, final behavior, and preserved boundaries + +`reject_nonfunction_main_decls` in `cmd/wcc/check.c` and its self-hosted twin +`rejectnonfunctionmaindecls` in `selfhost/cmd/wcc/check.ww` are the semantic +owners. They run after parsing but before qualified-use discovery and package +name installation. Each walks selected top-level declarations, tests the +declaration-carried `pkgname`, reports the pinned diagnostic, and removes only +the rejected node from subsequent package-scope checking. This mirrors the +pinned resolver's return-before-declare behavior. No driver mode, entry flag, +canonical action key, directory classification, or linker-symbol heuristic is +consulted. + +Direct post-fix calls to `w6c` and `w6c_ww` on the same invalid source now exit +1 with empty stdout, no assembly output, and byte-identical 102-byte stderr +(SHA-256 +`39001ed88e2ab8b7675fcc51b4b794cf8ebc2a803e1f05de45d7d0ba1cd98a38`) +ending in `cannot declare main - must be func`. Directory builds of all four +forms fail through `ww: w6c failed for ...`, never reach `w6a` or `w6l`, publish +no output, and give byte-identical Cstage/WWstage diagnostics when the owned +output path is the same. Directory tests emit only the command-owned final +`FAIL\n` on stdout, report build failure on stderr, and emit no test body, +accounting, or package `ok` result. + +Loading and Go-platform source eligibility are unchanged. Production and test +source selection still determines which declarations reach the checker; an +excluded source has no effect. Declared package name remains independent from +canonical dotted identity, aliases, path leaf, filename, physical directory, +requested root, output name, linker order, and artifact/storage locator. A +dependency physically and canonically ending in `main` but declared `utility` +continues to export `main`, bind through its declared qualifier, and produce +stage-byte-identical `.unit.ww`, `.wwi`, assembly, object, archive, and command +executable bytes. Valid `main(argc, argv) i32` and `main() i32` commands remain +accepted and byte-identical between stages. + +Graph construction and action identities are unchanged for valid programs. An +invalid selected command or command-test variant reaches its normal compiler +action and fails there; assembler, archiver, linker, runtime, generated test +execution, and publication do not become alternative semantic owners. An +ordinary import of a declared-`main` package is still rejected earlier by the +loader as `ww: package PATH is a program, not an importable package`, even when +that command also contains the malformed declaration. This preserves import +diagnostic precedence and the toolchain-owned external-test exception. + +Cold rejection creates no output or retained scratch. Warm rejection after a +successful command preserves the complete committed owner unit, interface, +assembly, object, archive, init unit/assembly/object, tool vouchers, workdir +stamp, and public executable byte for byte. It installs no staged generation; +exact source restoration reuses the committed action and reproduces the prior +binary. There is no test-result cache and no new reuse key. Producer failure, +rollback, existing-output preservation, concurrent action isolation, +interruption, process cleanup, and transaction cleanup continue through their +existing owners; the checker adds no process, descriptor, mutable global state, +or cleanup path. No active `.new`, `.install`, `.wwtxn.*`, adjacent rejection +scratch, test child, or capture survives the tested failure boundaries. + +The WW-native `nonfunction_main_declarations_reject` observer proves all four +non-function kinds, exact direct-compiler stage parity, cold build rejection, +directory-test nonexecution, declared-name/dotted-import/physical-leaf +separation, supported entry ABI preservation, valid artifact-byte parity, +command-import precedence, complete warm work/publication rollback, restored +reuse, and residue absence. Existing interruption and concurrent-transaction +observers remain the owners of those unchanged mechanisms; this declaration +check introduces no independently interruptible or shared state. + +No valid persisted-byte contract changed. Build workdir format remains `18`, +test workdir format remains `19`, and semantic storage format remains `3`. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 3613711a..36cbdb8c 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -307,9 +307,17 @@ ImportPath = ident { "." ident } . ownership, symbols, artifacts, storage, or diagnostics. Repeated occurrences of the same exact identity remain valid and deduplicate normally. - An executable package is one declared `package main` and containing a - `fn main`; path and directory spelling do not classify commands. An ordinary - import of a package declared `main` is rejected, except for the toolchain's - colocated external-test wiring. + `fn main`; path and directory spelling do not classify commands. Within a + package declared `main`, a package-scope declaration named `main` must be a + function: `let`, `const`, `def`, and `type` forms reject as + `cannot declare main - must be func` and do not enter package scope. The + restriction depends only on the declared package name. A package with any + other declared name may use or export `main` regardless of its dotted import + path, path leaf, physical directory, or selection role. WW retains its + established program-entry ABI, so a function `main` may carry WW's supported + arguments and result; this rule does not adopt Go's source signature. An + ordinary import of a package declared `main` is rejected, except for the + toolchain's colocated external-test wiring. - For `ww build`, an explicit `-o` names an output directory when ordinary `stat` reports an existing directory (following symlinks) or its spelling ends in `/`. This classification is independent of whether one or many @@ -414,6 +422,13 @@ called as `init()`, selected as `pkg.init`, exported, or used by another kind of declaration. Package-variable initialization completes before these functions run. +In a package whose declared name is `main`, only a function declaration may +claim the package-scope name `main`. A rejected non-function declaration is not +installed and cannot satisfy the executable entry. This is independent of +canonical import identity and physical location. The accepted function shape +continues to use WW's entry ABI, including its supported argument and result +forms. + ### 5.2 `let` ``` @@ -697,6 +712,12 @@ under test binds to the augmented white-box action when it exists; affected transitive importers are copied and rewired so ordinary and augmented package state do not coexist in the linked closure. +A non-function package-scope `main` in production or same-package test source +of a package declared `main` is a package-checker failure. Generated test-main +ownership does not hide or replace it: the product is not linked or executed, +no test accounting or package `ok` result is emitted, and ordinary build/test +failure presentation and rollback apply. + Those variants are action distinctions over exact package representatives, not new ordinary package identities for case-fold comparison. Production, same-package test, external test, and recompiled copies of one exact canonical diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 7c3ec1d7..4a0b8abf 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -268,6 +268,21 @@ check rather than part of `ww build`. Assembly-only `-S` still selects command actions and rejects a no-command directory request, but it performs no install-only destination validation or output-directory creation. +Package declaration checking also precedes test execution. In a package whose +declared name is `main`, `let main`, `const main`, `def main`, and `type main` +are rejected by both compiler stages as `cannot declare main - must be func` +and are not installed into package scope. The generated directory-test main is +an independent tool-owned action and cannot mask the malformed production or +same-package declaration: the product emits no test body output, accounting, +or `ok` result. This condition keys only on the source-declared package name; +a package with another declared name may export `main` even when its dotted +path or physical leaf is `main`, and WW's supported argument/result-bearing +function entries remain valid. The native +`nonfunction_main_declarations_reject` observer covers all four declaration +kinds, direct compiler ownership, build/test diagnostics, command-import +precedence, declared-name/import identity, valid artifact bytes, cold cleanup, +and warm work/output rollback and reuse across both stages. + The retained executable is byte-identical to the temporary runnable and has executable mode `0777` filtered by the caller's umask, but it is never the path executed by the coordinator. Compile-only publication participates in the diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index dfe0196a..769d446b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -7696,6 +7696,36 @@ fn classifyinitdecls(c: *checker, file: *syntax.node) void = { }; }; +// Pinned Go 1.26.5 types2 rejects a non-function package-scope `main` +// before declaring it, but only when the declared package name is `main` +// (resolver.go:90-110 declarePkgObj). WW's function entry ABI deliberately +// permits argument/result-bearing functions, so this is the independent +// declaration-kind rule: canonical identity, physical directory, path leaf, +// and root/action status are not inputs. Remove rejected declarations from +// later name installation just as the pinned resolver returns without +// declaring its object. Twin of cmd/wcc/check.c. +fn rejectnonfunctionmaindecls(c: *checker, file: *syntax.node) void = { + let prev: *syntax.node = nil; + let d: *syntax.node = file.list; + for (d != nil) { + let next: *syntax.node = d.next; + let invalid: bool = topdeclkind(d) + && d.kind != syntax.nkind.N_FNDECL + && syntax.streq(d.str, "main") + && syntax.streq(d.pkgname, "main"); + if (invalid) { + importdiagprefix(d); + cerr("cannot declare main - must be func\n"); + c.errs += 1; + if (prev == nil) { file.list = next; } + else { prev.next = next; }; + } else { + prev = d; + }; + d = next; + }; +}; + fn initstripcast(n: *syntax.node) *syntax.node = { for (n != nil && n.kind == syntax.nkind.N_CAST) { n = n.lhs; }; return n; @@ -8578,6 +8608,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { file.list = usenode; }; }; + rejectnonfunctionmaindecls(c, file); markimportuses(c, file); checkimportredeclarations(c, file); checkimportusageandcollisions(c, file); diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 01c9aab9..55506083 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -8716,6 +8716,251 @@ fn cwdwritedata(dir: str, label: str) void = { clean(root); }; +// A package declared `main` may reserve the entry spelling only for a function. +// This is the declaration-kind half of pinned Go's main rule; WW deliberately +// retains its C/Hare-style argument/result entry ABI. The checker owns the +// reject before assembly/linking, directory tests reject before execution, and +// declared package name remains independent from dotted/physical identity. +@test fn nonfunction_main_declarations_reject() void = { + let root: str = fresh(); + let shapes: []str = [ + "let main: i32 = 0;\n", + "const main: i32 = 0;\n", + "def main: i32 = 0;\n", + "type main = i32;\n", + ]; + let labels: []str = ["let", "const", "def", "type"]; + let expected: str = "cannot declare main - must be func"; + let i: i32 = 0; + for (i < shapes.len) { + let dir: str = strings.concat(root, "/invalid-", labels[i]); + assert(os.mkdir(dir, 448i32) == 0); + let src: str = strings.concat(dir, "/main.ww"); + writefile(src, strings.concat("package main;\n", shapes[i])); + let output: str = strings.concat(root, "/invalid-output-", labels[i]); + let avc: []str = [driver("ww"), "build", "-o", output, dir]; + let avw: []str = [driver("ww_ww"), "build", "-o", output, dir]; + let outc: commandout; + let outw: commandout; + runcommand(root, strings.concat("invalid-main-c-", labels[i]), avc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, strings.concat("invalid-main-ww-", labels[i]), avw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(outc.stdout.len == 0 && outw.stdout.len == 0); + assert(same(primarydiagnostic(outc.stderr), expected)); + assert(same(primarydiagnostic(outw.stderr), expected)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "ww: w6c failed for ")); + assert(!has(outc.stderr, "w6a") && !has(outc.stderr, "w6l")); + assert(!os.exists(output)); + assert(!os.exists(strings.concat(output, ".new"))); + assert(!os.exists(strings.concat(output, ".install"))); + assert(!os.exists(strings.concat(output, ".sepwork"))); + assert(!directoryhasfragment(root, ".wwtxn.")); + i += 1; + }; + + // Direct compiler calls pin the true owner and byte-identical frontend + // diagnostic independently from driver-generated unit paths. + let directsrc: str = strings.concat(root, "/invalid-let/main.ww"); + let directc: str = strings.concat(root, "/direct-c.s"); + let directw: str = strings.concat(root, "/direct-ww.s"); + let dcav: []str = [driver("w6c"), "-c", "--entry", "-o", directc, + directsrc]; + let dwav: []str = [driver("w6c_ww"), "-c", "--entry", "-o", directw, + directsrc]; + let directco: commandout; + let directwo: commandout; + runcommand(root, "invalid-main-direct-c", dcav, + (30i64 * (time.second: i64)): time.duration, &directco); + runcommand(root, "invalid-main-direct-ww", dwav, + (30i64 * (time.second: i64)): time.duration, &directwo); + expectexit(&directco, 1); + expectexit(&directwo, 1); + assert(directco.stdout.len == 0 && directwo.stdout.len == 0); + assert(same(directco.stderr, directwo.stderr)); + assert(same(primarydiagnostic(directco.stderr), expected)); + assert(!os.exists(directc) && !os.exists(directw)); + + // The same malformed production package must not become a passing test + // package merely because the dispatcher owns a separate synthetic entry. + let testdir: str = strings.concat(root, "/invalid-test"); + assert(os.mkdir(testdir, 448i32) == 0); + writefile(strings.concat(testdir, "/main.ww"), + "package main;\nlet main: i32 = 0;\n"); + writefile(strings.concat(testdir, "/main_test.ww"), + "package main;\n@test fn must_not_run() void = { abort(\"ran\"); };\n"); + let testdiags: []str = ["", ""]; + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; + i = 0; + for (i < stages.len) { + let av: []str = [driver(stages[i]), "test", testdir]; + let out: commandout; + runcommand(root, strings.concat("invalid-main-test-", tags[i]), av, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n")); + assert(!has(out.stdout, "must_not_run") && !has(out.stdout, " passed,")); + assert(!has(out.stderr, "must_not_run") && !has(out.stderr, "ok ")); + assert(has(out.stderr, expected)); + assert(has(out.stderr, strings.concat("FAIL ", testdir))); + testdiags[i] = strings.dup(primarydiagnostic(out.stderr)); + i += 1; + }; + assert(same(testdiags[0], expected) && same(testdiags[0], testdiags[1])); + + // A warm semantic failure preserves every committed action byte, tool + // voucher, and public binary, then exact source restoration reuses them. + let warmdiags: []str = ["", ""]; + i = 0; + for (i < stages.len) { + let warmdir: str = strings.concat(root, "/warm-", tags[i]); + let work: str = strings.concat(root, "/warm-work-", tags[i]); + let output: str = strings.concat(root, "/warm-output-", tags[i]); + assert(os.mkdir(warmdir, 448i32) == 0); + assert(os.mkdir(work, 448i32) == 0); + let source: str = strings.concat(warmdir, "/main.ww"); + let valid: str = "package main;\nfn main() i32 = { return 0; };\n"; + writefile(source, valid); + let av: []str = [driver(stages[i]), "build", "-w", work, + "-o", output, warmdir]; + let out: commandout; + runcommand(root, strings.concat("invalid-main-warm-cold-", tags[i]), av, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let action: str = localidentity(warmdir, "main"); + let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a", + ".init.unit.ww", ".init.s", ".init.o"]; + let refs: []str = alloc([], suffixes.len: u64)!; + let j: i32 = 0; + for (j < suffixes.len) { + append(refs, strings.dup(readfile(strings.concat(strings.concat(work, + "/"), strings.concat(action, suffixes[j]))))); + j += 1; + }; + let toolpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c", + "/.wwtool.w6a", "/.wwtool.stamp"]; + let toolrefs: []str = alloc([], toolpaths.len: u64)!; + j = 0; + for (j < toolpaths.len) { + append(toolrefs, strings.dup(readfile(strings.concat(work, + toolpaths[j])))); + j += 1; + }; + let binref: str = strings.dup(readfile(output)); + rewritefile(source, "package main;\nlet main: i32 = 0;\n"); + runcommand(root, strings.concat("invalid-main-warm-reject-", tags[i]), + av, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(primarydiagnostic(out.stderr), expected)); + warmdiags[i] = strings.dup(primarydiagnostic(out.stderr)); + j = 0; + for (j < suffixes.len) { + assert(same(refs[j], readfile(strings.concat(strings.concat(work, + "/"), strings.concat(action, suffixes[j]))))); + j += 1; + }; + j = 0; + for (j < toolpaths.len) { + assert(same(toolrefs[j], readfile(strings.concat(work, + toolpaths[j])))); + j += 1; + }; + assert(same(binref, readfile(output))); + assert(!directoryhasnew(work)); + assert(!os.exists(strings.concat(output, ".new"))); + assert(!os.exists(strings.concat(output, ".install"))); + assert(!os.exists(strings.concat(output, ".sepwork"))); + assert(!directoryhasfragment(root, ".wwtxn.")); + rewritefile(source, valid); + runcommand(root, strings.concat("invalid-main-warm-restored-", tags[i]), + av, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(same(binref, readfile(output)) && !directoryhasnew(work)); + i += 1; + }; + assert(same(warmdiags[0], expected) && same(warmdiags[0], warmdiags[1])); + + // The declared package name, never the dotted path leaf or physical + // directory, owns the rule. A non-main package may export `main`, and a + // command may import it while retaining WW's argument/result entry ABI. + let tree: str = strings.concat(root, "/tree"); + let dep: str = strings.concat(tree, "/domain/main"); + let app: str = strings.concat(tree, "/app"); + mkdirall(dep); + assert(os.mkdir(app, 448i32) == 0); + writefile(strings.concat(dep, "/dep.ww"), + "package utility;\nexport let main: i32 = 7;\n"); + writefile(strings.concat(app, "/main.ww"), strings.concat( + "package main;\nimport domain.main;\n", + "fn main(argc: i32, argv: **u8) i32 = { return utility.main - 7; };\n")); + let binaries: []str = [strings.concat(root, "/identity-c"), + strings.concat(root, "/identity-ww")]; + i = 0; + for (i < stages.len) { + let av: []str = [driver(stages[i]), "build", "-I", tree, + "-o", binaries[i], app]; + let out: commandout; + runcommand(root, strings.concat("main-name-boundary-", tags[i]), av, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let runav: []str = [binaries[i], "one", "two"]; + runcommand(root, strings.concat("main-name-boundary-run-", tags[i]), + runav, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + i += 1; + }; + assert(same(readfile(binaries[0]), readfile(binaries[1]))); + let artifacts: []str = ["domain.main.unit.ww", "domain.main.wwi", + "domain.main.s", "domain.main.o", "domain.main.a"]; + i = 0; + for (i < artifacts.len) { + assert(same(readfile(strings.concat(strings.concat(binaries[0], + ".sepwork/"), artifacts[i])), + readfile(strings.concat(strings.concat(binaries[1], ".sepwork/"), + artifacts[i])))); + i += 1; + }; + + // Importing any declared-main package is still the loader-owned error and + // retains precedence over the declaration checker, even when that command + // also contains the malformed declaration. + let badcmd: str = strings.concat(tree, "/badcmd"); + let client: str = strings.concat(tree, "/client"); + assert(os.mkdir(badcmd, 448i32) == 0); + assert(os.mkdir(client, 448i32) == 0); + writefile(strings.concat(badcmd, "/main.ww"), + "package main;\nlet main: i32 = 0;\n"); + writefile(strings.concat(client, "/client.ww"), strings.concat( + "package client;\nimport _ badcmd;\n", + "export fn value() i32 = { return 7; };\n")); + let importout: str = strings.concat(root, "/bad-import-output"); + let importdiag: str = + "ww: package badcmd is a program, not an importable package\n"; + i = 0; + for (i < stages.len) { + let av: []str = [driver(stages[i]), "build", "-I", tree, + "-o", importout, client]; + let out: commandout; + runcommand(root, strings.concat("bad-main-import-", tags[i]), av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, importdiag)); + assert(!has(out.stderr, expected)); + assert(!os.exists(importout) && !os.exists(strings.concat(importout, + ".sepwork"))); + i += 1; + }; + clean(root); +}; + // Invalid @test attribute shapes reject at build with stable text on // BOTH frontends (the -T synth checker owns them; the fixture corpus // cannot reach -T, so these rows live here). Fragments only — the