test: prove case-fold collision preflight

This commit is contained in:
2026-08-15 16:11:01 +09:00
parent 265be3f796
commit 6974bec878
4 changed files with 1037 additions and 13 deletions

View File

@@ -311,11 +311,21 @@ and matches its final single `linux` token, exactly as Go does.
Selection is additive, not replacement-based: every matching file belongs to
the package. The production variant then excludes `*_test.ww`; internal and
external test classification uses only the already platform-selected test
files. An excluded file creates no source occurrence, import, dependency edge,
package/action/variant identity, compiler input, export, archive member, link
input, artifact, status, or persistence dependency. Adding or editing one is a
producer no-op. Adding, removing, or editing a selected file changes the owning
unit normally.
files. After those decisions, distinct selected basenames in one canonical
directory MUST NOT be equal under Go 1.26.5's Unicode simple-fold comparison.
The check spans the production, internal-test, and external-test selections of
one `ww test` product without merging those source units. An ordinary
`ww build` sees production names only. Exact basename reuse by another action
view of that directory is not a collision.
An excluded file creates no source occurrence, collision, import, dependency
edge, package/action/variant identity, compiler input, export, archive member,
link input, artifact, status, or persistence dependency. Adding or editing one
is a producer no-op. Adding, removing, or editing a selected noncolliding file
changes the owning unit normally. This applicability is deliberately narrower
than Go's `Package.AllFiles`: WW omits wrong-platform and `*_test.ww` names
from an ordinary build because those files are not loaded in WW's
fixed-target, manifest-free source model.
WW implements no source-level build expressions, user tags, target descriptor,
`UseAllFiles` escape, `+tag` replacement scheme, or manifest-defined selector.
@@ -5588,6 +5598,154 @@ source-level build expressions, quoted/grouped/dot imports, and targets other
than the separately specified fixed `linux/amd64` filename selection. None is
used to define canonical package or directory-product identity.
### 11.23 Implemented case-fold collision preflight
This slice pins the collision semantics to Go 1.26.5, tag commit
`c19862e5f8415b4f24b189d065ed739517c548ba`. The official command loader uses
an exact import cache at `src/cmd/go/internal/load/pkg.go:633-636,757-775`,
performs contextual and vendor resolution before cache lookup at
`:840-911,974-1005`, owns one command-global folded import table at `:1725`,
and rejects a second fold-equivalent import spelling at `:1950-1959`. Its
selected-name collision is over `Package.AllFiles` (`:149-194`) and is applied
at `:1991-2000`. `src/cmd/go/internal/str/str.go:32-89` defines the pinned
`ToFold` and `FoldDup` algorithms; the direct, transitive, and filename
expectations are in
`src/cmd/go/testdata/script/list_case_collision.txt:1-41`. Filename discovery
and package/test classification are ordered by
`src/go/build/build.go:859-914,1005-1036,1076-1082,1450-1469`.
Before this slice, a Linux case-sensitive filesystem let distinct directories
such as `domain.Foo` and `domain.foo`, expanded vendor identities, and selected
files such as `File.ww` and `file.ww` build as unrelated packages or sources.
Both Cstage and WWstage did so byte-identically. A symlink making the two import
spellings reach one directory happened to trip the older exact
directory/identity check, but that host-physical consequence was neither the Go
diagnostic nor the required package-graph rule.
#### Exact identity and request-only folded keys
Canonical package identity remains the exact, case-sensitive effective dotted
identity. It is still the key used by action interning, source import maps,
`.wwi` ownership, mangled symbols, storage digests, archives, diagnostics, and
link closure. Default bindings, explicit aliases, blank imports, declared
package names, path leaves, artifacts, and physical directories do not replace
it. Exact repeated occurrences therefore continue to form one canonical edge
and one package action.
The loader graph additionally owns a checked, dynamically grown, request-only
table from a simple-fold key to the first exact package representative. Every
ordinary effective identity registers after contextual local/vendor expansion
and before exact action reuse or physical-directory conflict checks. Exact
reuse succeeds. A distinct spelling with the same key rejects as
`ww: case-insensitive import collision: "A" and "a"`; WW byte-sorts the two
exact spellings so root, request, import, and discovery order cannot select a
different diagnostic. It never stores, interns, looks up, or publishes the
folded spelling as package identity.
WW dotted import components are ASCII by construction: the C lexer/parser
accept them through `cmd/wcc/lex.c:53-63` and
`cmd/wcc/parse.c:1318-1353`, and the self-hosted syntax path mirrors that in
`lib/ww/syntax/lex.ww` and `lib/ww/syntax/decl.ww`. Context-derived identities
are revalidated, and arbitrary local filesystem bytes are escaped into ASCII.
The general Unicode fold routine is nevertheless shared with filename
preflight; for package identities its observable domain reduces exactly to
ASCII case folding.
Vendor imports register the fully expanded canonical identity, not the short
source spelling. Thus `lib.Foo` and `lib.foo` reached below one vendor owner
collide as, for example, `domain.app.vendor.lib.Foo` and
`domain.app.vendor.lib.foo`. Reaching one physical directory through two such
identities changes neither ownership nor the diagnostic. The physical
directory remains a resolution fact, never a substitute identity.
Production, same-package test (`ptest`), external test (`pxtest`), and
product-scoped recompiled actions share their one exact ordinary package
representative and do not re-register as different packages. External
compiler identity may still carry `_test`; that action path is not the folded
package key. Copy-on-write clones copy exact identity and do not register.
Generated main has no ordinary import identity and does not register. Ordinary
toolchain package `test` registers like any real package; only the reserved
synthetic `__wwtest` role stays outside the ordinary representative table.
#### Selected source basenames
Directory enumeration first excludes leading-dot, leading-underscore,
wrong-platform, and variant-ineligible files, validates the selected regular
sources, and byte-sorts their names. Before delegation, the coordinator performs
its required package-clause classification and parses selected production files
to reject `@test` declarations outside `*_test.ww`. The delegated driver then
registers each selected basename in a second request-only table scoped by
canonical physical directory. Repeated views of the exact same selected
basename across `p`, `ptest`, or `pxtest` are accepted. Distinct fold-equivalent
basenames reject as
`ww: case-insensitive file name collision: "File.ww" and "file.ww"` before the
driver's graph-import scan or any producer. The preflight is not an earlier
replacement for the coordinator-owned source validation parse.
One `ww test` request shares that directory scope across production,
same-package test, external test, same-only, external-only, and mixed test-only
actions while preserving their separate compilation units. This catches a
collision crossing classifications, such as production `X_TEST.ww` and test
`x_test.ww`. An ordinary `ww build` excludes `*_test.ww` before registration.
Hidden, underscore-prefixed, and wrong-platform files never register and
therefore create no collision or invalidation.
This last ordering is WW's explicit applicability boundary rather than a claim
that every upstream `AllFiles` member is selected here. Go includes test files
and some ignored Go files in `AllFiles`, so its ordinary build can diagnose a
broader set. WW intentionally follows its existing fixed-target source
eligibility and build/test isolation: files it does not load have no graph or
persistence effect.
Filesystem basenames are arbitrary non-NUL bytes, so their fold keys reproduce
the pinned Unicode 15.0 `unicode.SimpleFold` minimum-cycle behavior without
locale or normalization. Each malformed UTF-8 byte contributes one U+FFFD to
the temporary key, as Go string ranging does; diagnostics preserve the exact
original byte and quote it as `\xNN`. Printable Unicode remains UTF-8, other
nonprinting runes use Go-style `\u` or `\U` escapes, and composed/decomposed
Unicode spellings are not normalized.
#### Tool, artifact, transaction, and persistence ownership
The coordinator owns initial eligibility and production/test classification;
the delegated driver loader owns both fold checks. The language parser still
owns the exact import occurrence and qualifier. The driver owns per-site self,
internal, vendor, and imported-main structural legality, while the compiler
checker owns file-local binding, use, and visibility. The export writer/reader
owns exact canonical `.wwi` data. Compiler, assembler, archiver, and linker
protocols did not change. Successful neighboring units, `.wwi`, assembly,
objects, archives, generated mains, binaries, and exact tool argv therefore
remain byte-identical in Cstage and WWstage.
All root and reachable dependency loading, fold registration, and final exact
identity binding finish before scratch acquisition, support or generated-main
producer work, compilation, assembly, archive construction, or linking. A
collision invokes none of those tools and creates no unit, `.wwi`, assembly,
object, archive, main, binary, result, status, voucher, stamp, or `.new` stage.
Request-wide publication remains transactional: committed sibling and
dependency bytes survive a newly introduced collision, and removing the
colliding source restores precise warm reuse.
The fold tables live only for one command and are freed at graph teardown.
Entries become live only after every owned string and vector allocation
succeeds, so allocation failure cannot publish a partial table or make cleanup
traverse uninitialized entries. Successful action/unit/storage content did not
change; build workdir format stays `18`, test workdir format stays `19`, and
semantic storage stays `3`. A format bump would only discard valid exact-key
artifacts and is therefore not used.
Native proof extends `package_graph_diagnostics_are_stable`,
`platform_filename_source_selection`, and
`vendor_directory_import_resolution`, with command-global allocation failure
retained in `allocation_failure_is_command_global`. The matrix covers direct,
transitive, reversed, recursive, same-directory, and vendor-expanded imports;
exact-repeat acceptance; reversed vendor import and product order;
production/internal/external/test-only filenames; reversed creation and
direct/recursive collision-diagnostic parity; cross-classification, Unicode,
invalid UTF-8, ignored files, and absence of normalization; zero-tool
rejection; multi-product publication isolation; cold/warm add-remove reuse;
exact artifact preservation; and Cstage/WWstage diagnostic and byte identity.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -269,8 +269,14 @@ ImportPath = ident { "." ident } .
are ignored. Eligible names are byte-sorted before source validation.
Production excludes selected `*_test.ww`; test variants classify only those
selected test files. An excluded file contributes no declarations, imports,
package edge, action, export, artifact, initialization, test, or persistent
invalidation.
filename collision, package edge, action, export, artifact, initialization,
test, or persistent invalidation. After eligibility, two distinct selected
basenames in one canonical directory that are equal under Go 1.26.5 Unicode
simple folding are rejected after the coordinator's required package-clause
classification and production `@test` validation parses, but before the
delegated graph-import scan or tools. An ordinary build compares production
names only; one test product compares its production, same-package test, and
external-test selections without merging their units.
- `import acme.codec;` loads the canonical package `acme.codec`. If that
package declares `package wire;`, the importing file sees its exported names
as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias
@@ -294,6 +300,12 @@ ImportPath = ident { "." ident } .
edge. Test-only occurrences never enter ordinary production. Self-import is
rejected, except that toolchain-owned external-test self wiring is rebound to
the effective augmented package action after ordinary per-site validation.
- Canonical import identity is exact and case-sensitive. After contextual local
or vendor expansion, two distinct effective identities that are equal under
Go 1.26.5 simple folding are a request-wide structural error. Folding is only
a temporary collision key: it never changes lookup, action identity, `.wwi`
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
@@ -618,6 +630,13 @@ 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.
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
package therefore do not collide with each other. Their selected source units
remain separate, while the directory-owned filename preflight spans the
production/internal/external selections applicable to that test product.
Before a test function runs, the one generated product initializes its exact
effective graph dependency-first and once per canonical action. Imports,
runtime lets, and init declarations found only in `*_test.ww` never enter an

View File

@@ -242,6 +242,22 @@ directory entry order. A recursive pattern omits a directory with no eligible
source, while an explicit wrong-target-only build rejects it as having no WW
package source. There is no source-level build-expression or user-tag mode.
After that eligibility boundary and the coordinator's required package-clause
classification and production `@test` validation parses, the delegated loader
performs selected-basename Go 1.26.5 simple-fold preflight before its graph
import scan. During graph loading, effective canonical imports register only
after contextual local/vendor expansion; exact identity reuse is accepted, but
two distinct fold-equivalent identities reject the entire shared plan.
Selected basenames register in one canonical-directory scope. A production
build registers production names only. A test plan shares the filename scope
across production, internal-test, and external-test actions, including
same-only, external-only, and mixed directories, without combining their source
units. Hidden, underscore-prefixed, wrong-platform, and otherwise ineligible
files never register. Package variants, product-scoped recompiled copies,
support, and generated main remain action wiring over exact identities and do
not manufacture package collisions. The fold preflight does not replace or
precede those coordinator-owned source-validation parses.
Separate compilation is the only driver build path; no compatibility mode
switch remains.
@@ -294,6 +310,24 @@ have the same `ww: out of memory` diagnostic, invoke no compiler, assembler, or
linker, and leave an empty caller work directory with no output, status, or
staging path.
The same observer now owns selected-basename folding: ASCII production,
internal, external, same-only, external-only, mixed, and cross-classification
collisions; printable Unicode Kelvin folding; malformed UTF-8 replacement-rune
keys with exact `\xNN` diagnostics; accepted composed/decomposed neighbors;
ordinary-build isolation from test collisions; ignored folded pairs; reversed
creation order; and direct/recursive collision-diagnostic parity. It proves zero
compiler/assembler/linker calls and an empty workdir, including no
test-support/main artifact, on cold rejection. A committed warm package then
gains and loses a colliding file; the failed middle request preserves every
unit, `.wwi`, assembly, object, archive, binary, tool record, and stamp, and
removal restores exact reuse.
`package_graph_diagnostics_are_stable` provides the corresponding direct,
transitive, reversed-root, recursive, same-directory, exact-repeat, and warm
import proof. `vendor_directory_import_resolution` proves folding uses the
expanded vendor identity under reversed imports and product descriptors, even
when both spellings reach one physical directory, and that multi-product
rejection publishes no output, status, tool state, or staging residue.
`test/sep/sepinit_test.ww` is the single focused package-initialization owner.
It generates all source trees temporarily and runs independent cold/persistent
Cstage and WWstage legs. Its matrix covers blank-only reachability; dependency,
@@ -353,6 +387,13 @@ real directory test products always run, while no-selected-test directories do
not create a process. The byte-identity and bootstrap gates keep building on
fresh scratch. `make clean` reclaims every workdir under `out/`.
Fold keys and first-spelling tables are request-only loader state and are never
stored in a workdir. They do not alter successful unit bytes, action/storage
keys, or tool records, so build format 18, test format 19, and semantic storage
format 3 remain unchanged. Structural fold rejection occurs before staging and
cannot mutate a committed generation; a later request with the collision
removed evaluates the original exact-key reuse contract.
On a package or tree target, `ww build/test -w DIR` forwards that exact caller
path as the one command-global semantic-action store; neither pattern spelling
nor package-group name derives a persistent subdirectory. The delegated driver