ww package: reject non-function main declarations

This commit is contained in:
2026-08-21 15:13:09 +09:00
parent 2b7fafde0c
commit 2387d3e2d2
6 changed files with 520 additions and 3 deletions

View File

@@ -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 90110](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 103124](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 510](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 19](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 416444](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L416-L444)),
asserted by official `mainsig.go`
([lines 713](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 2967](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 1945](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L19-L45))
and official `testdata/issue10978/main.go` lines 527. 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 555579 and 775787](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 1113 and 3040](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 706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)),
asserted by official `importdecl0a.go` lines 927. 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.