diff --git a/docs/build-system.md b/docs/build-system.md index 6b2ab0f6..a5efa78f 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -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. diff --git a/docs/spec.md b/docs/spec.md index 2e7b4a66..122e2ebc 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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 diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 4fa3ff09..0a39f2c8 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -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 diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 836f6620..102f79ea 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -481,6 +481,36 @@ fn directoryhasnew(path: str) bool = { return false; }; +fn directoryisempty(path: str) bool = { + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + assert(fd >= 0); + let buf: []u8 = alloc([], 16384u64)!; + buf.len = 16384; + let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64); + for (n > 0i64) { + let off: i32 = 0; + for (off < n: i32) { + let reclen: i32 = (buf[off + 16]: i32) + + ((buf[off + 17]: i32) * 256); + assert(reclen >= 20 && off + reclen <= n: i32); + let len: i32 = 0; + for (buf[off + 19 + len] != 0u8) { len += 1; }; + let dot: bool = len == 1 && buf[off + 19] == '.'; + let dotdot: bool = len == 2 && buf[off + 19] == '.' + && buf[off + 20] == '.'; + if (!dot && !dotdot) { + assert(os.close(fd) == 0); + return false; + }; + off += reclen; + }; + n = os.getdents64(fd, buf.ptr, buf.len: u64); + }; + assert(n == 0i64); + assert(os.close(fd) == 0); + return true; +}; + fn byteshasat(s: str, off: i32, want: str) bool = { if (off < 0 || off + want.len > s.len) { return false; }; let i: i32 = 0; @@ -5749,6 +5779,23 @@ fn hexbytes(value: str) str = { let identityactual: str = strings.concat(root, "/identityactual"); let identityalias: str = strings.concat(root, "/identityalias"); let reserved: str = strings.concat(root, "/reservedprobe"); + let foldsource: str = strings.concat(root, "/foldsource"); + let foldupper: str = strings.concat(foldsource, "/domain/Foo"); + let foldlower: str = strings.concat(foldsource, "/domain/foo"); + let folddirect: str = strings.concat(foldsource, "/cmd/direct"); + let foldbackward: str = strings.concat(foldsource, "/cmd/backward"); + let foldindirect: str = strings.concat(foldsource, "/cmd/indirect"); + let foldleft: str = strings.concat(foldsource, "/parent/left"); + let foldright: str = strings.concat(foldsource, "/parent/right"); + let foldrepeat: str = strings.concat(foldsource, "/cmd/repeat"); + let foldaliasroot: str = strings.concat(foldsource, "/aliasroot"); + let foldaliasactual: str = strings.concat(foldsource, "/alias/Foo"); + let foldalias: str = strings.concat(foldsource, "/alias/foo"); + let foldrootleft: str = strings.concat(foldsource, "/roots/left"); + let foldrootright: str = strings.concat(foldsource, "/roots/right"); + let foldrecursive: str = strings.concat(root, "/foldrecursive"); + let foldrecursiveupper: str = strings.concat(foldrecursive, "/domain/Foo"); + let foldrecursivelower: str = strings.concat(foldrecursive, "/domain/foo"); assert(os.mkdir(missing, 448i32) == 0); assert(os.mkdir(cycle, 448i32) == 0); assert(os.mkdir(left, 448i32) == 0); @@ -5758,6 +5805,11 @@ fn hexbytes(value: str) str = { assert(os.mkdir(identityroot, 448i32) == 0); assert(os.mkdir(identityactual, 448i32) == 0); assert(os.mkdir(reserved, 448i32) == 0); + mkdirall(foldupper); mkdirall(foldlower); mkdirall(folddirect); + mkdirall(foldbackward); mkdirall(foldindirect); mkdirall(foldleft); + mkdirall(foldright); mkdirall(foldrepeat); mkdirall(foldaliasroot); + mkdirall(foldaliasactual); mkdirall(foldrootleft); mkdirall(foldrootright); + mkdirall(foldrecursiveupper); mkdirall(foldrecursivelower); writefile(strings.concat(missing, "/missing.ww"), "package missing;\nfn local() void = { };\n"); writefile(strings.concat(missing, "/missing_test.ww"), strings.concat( @@ -5798,6 +5850,49 @@ fn hexbytes(value: str) str = { writefile(strings.concat(reserved, "/reservedprobe.ww"), strings.concat( "package reservedprobe;\nimport __wwlocal.fake;\n", "export fn value() i32 = { return 1; };\n")); + writefile(strings.concat(foldupper, "/pkg.ww"), + "package upperdecl;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(foldlower, "/pkg.ww"), + "package lowerdecl;\nexport fn value() i32 = { return 2; };\n"); + // File creation and import order deliberately disagree with byte order. + writefile(strings.concat(folddirect, "/z.ww"), strings.concat( + "package main;\nimport Lower domain.foo;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(folddirect, "/a.ww"), + "package main;\nimport Upper domain.Foo;\n"); + writefile(strings.concat(foldbackward, "/a.ww"), + "package main;\nimport Lower domain.foo;\n"); + writefile(strings.concat(foldbackward, "/z.ww"), strings.concat( + "package main;\nimport Upper domain.Foo;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(foldleft, "/pkg.ww"), strings.concat( + "package left;\nimport _ domain.Foo;\n", + "export fn value() i32 = { return 1; };\n")); + writefile(strings.concat(foldright, "/pkg.ww"), strings.concat( + "package right;\nimport _ domain.foo;\n", + "export fn value() i32 = { return 2; };\n")); + writefile(strings.concat(foldindirect, "/main.ww"), strings.concat( + "package main;\nimport _ parent.left;\nimport _ parent.right;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(foldrepeat, "/a.ww"), + "package main;\nimport _ domain.Foo;\n"); + writefile(strings.concat(foldrepeat, "/z.ww"), strings.concat( + "package main;\nimport _ domain.Foo;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(foldaliasactual, "/pkg.ww"), + "package aliasdecl;\nexport fn value() i32 = { return 1; };\n"); + assert(os.symlink(foldaliasactual, foldalias) == 0); + writefile(strings.concat(foldaliasroot, "/main.ww"), strings.concat( + "package main;\nimport _ alias.Foo;\nimport _ alias.foo;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(foldrootleft, "/pkg.ww"), + "package leftroot;\nimport _ domain.Foo;\n"); + writefile(strings.concat(foldrootright, "/pkg.ww"), + "package rightroot;\nimport _ domain.foo;\n"); + writefile(strings.concat(foldrecursiveupper, "/pkg.ww"), + "package upper;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(foldrecursivelower, "/pkg.ww"), + "package lower;\nexport fn value() i32 = { return 2; };\n"); rejectpackagestable(root, "missing", missing, "cannot find package nowhere"); rejectpackagestable(root, "cycle", cycle, "ww: dependency cycle:"); @@ -5808,32 +5903,46 @@ fn hexbytes(value: str) str = { let stages: []str = ["ww", "ww_ww"]; let tooltrace: str = strings.concat(root, "/identity-tool.trace"); let compilerwrapper: str = strings.concat(root, "/identity-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/identity-w6a.sh"); let linkerwrapper: str = strings.concat(root, "/identity-w6l.sh"); writefile(tooltrace, ""); writeexecutable(compilerwrapper, strings.concat( - "#!/bin/sh\nprintf 'compile\\n' >> \"$WW_IDENTITY_TRACE\"\n", + "#!/bin/sh\nprintf 'compile' >> \"$WW_IDENTITY_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_IDENTITY_TRACE\"; done\nprintf '\\n' >> ", + "\"$WW_IDENTITY_TRACE\"\n", "exec \"$WW_IDENTITY_W6C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble' >> \"$WW_IDENTITY_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_IDENTITY_TRACE\"; done\nprintf '\\n' >> ", + "\"$WW_IDENTITY_TRACE\"\n", + "exec \"$WW_IDENTITY_W6A\" \"$@\"\n")); writeexecutable(linkerwrapper, strings.concat( - "#!/bin/sh\nprintf 'link\\n' >> \"$WW_IDENTITY_TRACE\"\n", + "#!/bin/sh\nprintf 'link' >> \"$WW_IDENTITY_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_IDENTITY_TRACE\"; done\nprintf '\\n' >> ", + "\"$WW_IDENTITY_TRACE\"\n", "exec \"$WW_IDENTITY_W6L\" \"$@\"\n")); let baseenv: []str = os.getenvs(); - let traceenv: []str = alloc([], (baseenv.len + 5): u64)!; + let traceenv: []str = alloc([], (baseenv.len + 4): u64)!; let ei: i32 = 0; for (ei < baseenv.len) { if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_W6A=") && !strings.hasprefix(baseenv[ei], "WW_W6L=") && !strings.hasprefix(baseenv[ei], "WW_IDENTITY_TRACE=") && !strings.hasprefix(baseenv[ei], "WW_IDENTITY_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_IDENTITY_W6A=") && !strings.hasprefix(baseenv[ei], "WW_IDENTITY_W6L=")) { append(traceenv, baseenv[ei]); }; ei += 1; }; append(traceenv, strings.concat("WW_W6C=", compilerwrapper)); + append(traceenv, strings.concat("WW_W6A=", assemblerwrapper)); append(traceenv, strings.concat("WW_W6L=", linkerwrapper)); append(traceenv, strings.concat("WW_IDENTITY_TRACE=", tooltrace)); - append(traceenv, strings.concat("WW_IDENTITY_W6C=", driver("w6c"))); - append(traceenv, strings.concat("WW_IDENTITY_W6L=", driver("w6l"))); let out: commandout; let i: i32 = 0; let identitydiagnostics: []str = ["", ""]; @@ -5857,6 +5966,187 @@ fn hexbytes(value: str) str = { // compiler or linker invocation. The converse is covered by the shared // multi-context resolution test above. assert(readfile(tooltrace).len == 0); + + let foldexpected: str = + "ww: case-insensitive import collision: \"domain.Foo\" and \"domain.foo\"\n"; + let aliasexpected: str = + "ww: case-insensitive import collision: \"alias.Foo\" and \"alias.foo\"\n"; + let foldtargets: []str = [folddirect, foldbackward, foldindirect, + foldaliasroot]; + let foldlabels: []str = ["direct", "backward", "indirect", "one-dir"]; + let foldwant: []str = [foldexpected, foldexpected, foldexpected, + aliasexpected]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let foldrefs: []str = ["", "", "", "", "", "", ""]; + let repeatref: str = ""; + i = 0; + for (i < stages.len) { + let foldenv: []str = alloc([], (traceenv.len + 3): u64)!; + let tei: i32 = 0; + for (tei < traceenv.len) { + append(foldenv, traceenv[tei]); tei += 1; + }; + append(foldenv, strings.concat("WW_IDENTITY_W6C=", + driver(compilers[i]))); + append(foldenv, strings.concat("WW_IDENTITY_W6A=", + driver(assemblers[i]))); + append(foldenv, strings.concat("WW_IDENTITY_W6L=", + driver(linkers[i]))); + let fi: i32 = 0; + for (fi < foldtargets.len) { + rewritefile(tooltrace, ""); + let work: str = strings.concat(root, "/fold-", foldlabels[fi], + "-", stages[i], "-work"); + assert(os.mkdir(work, 448i32) == 0); + let output: str = strings.concat(root, "/fold-", foldlabels[fi], + "-", stages[i]); + let av: []str = [driver(stages[i]), "build", "-w", work, + "-I", foldsource, "-o", output, foldtargets[fi]]; + runcommandenv(root, strings.concat("case-fold-", foldlabels[fi], + "-", stages[i]), av, foldenv, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, foldwant[fi])); + assert(readfile(tooltrace).len == 0); + assert(!os.exists(output)); + assert(!os.exists(strings.concat(work, "/.wwtool.stamp"))); + assert(directoryisempty(work)); + if (i == 0) { foldrefs[fi] = strings.dup(out.stderr); } + else { assert(same(foldrefs[fi], out.stderr)); }; + fi += 1; + }; + + // The coordinator canonicalizes product order, so reversing roots cannot + // choose a different representative or diagnostic. + let rootfirst: []str = [foldrootleft, foldrootright]; + let rootsecond: []str = [foldrootright, foldrootleft]; + let oi: i32 = 0; + for (oi < rootfirst.len) { + rewritefile(tooltrace, ""); + let work: str = strings.concat(root, "/fold-roots-", stages[i], + "-", foldlabels[oi], "-work"); + assert(os.mkdir(work, 448i32) == 0); + let av: []str = [driver(stages[i]), "build", "-w", work, + "-I", foldsource, rootfirst[oi], rootsecond[oi]]; + runcommandenv(root, strings.concat("case-fold-roots-", stages[i], + "-", foldlabels[oi]), av, foldenv, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && has(out.stderr, foldexpected)); + assert(readfile(tooltrace).len == 0); + assert(!os.exists(strings.concat(work, "/.wwtool.stamp"))); + assert(directoryisempty(work)); + let refindex: i32 = foldtargets.len + oi; + if (i == 0) { foldrefs[refindex] = strings.dup(out.stderr); } + else { assert(same(foldrefs[refindex], out.stderr)); }; + oi += 1; + }; + assert(same(foldrefs[4], foldrefs[5])); + + rewritefile(tooltrace, ""); + let recursivework: str = strings.concat(root, "/fold-recursive-", + stages[i], "-work"); + assert(os.mkdir(recursivework, 448i32) == 0); + let recursiveav: []str = [driver(stages[i]), "build", "-w", + recursivework, "-I", foldrecursive, + strings.concat(foldrecursive, "/...")]; + runcommandenv(root, strings.concat("case-fold-recursive-", stages[i]), + recursiveav, foldenv, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && has(out.stderr, foldexpected)); + assert(readfile(tooltrace).len == 0); + assert(!os.exists(strings.concat(recursivework, "/.wwtool.stamp"))); + assert(directoryisempty(recursivework)); + if (i == 0) { foldrefs[6] = strings.dup(out.stderr); } + else { assert(same(foldrefs[6], out.stderr)); }; + + // Exact repeated occurrences retain one canonical action and accepted + // compiler input, rather than being merged by the folded key. + rewritefile(tooltrace, ""); + let repeatwork: str = strings.concat(root, "/fold-repeat-", stages[i], + "-work"); + let repeatout: str = strings.concat(root, "/fold-repeat-", stages[i]); + assert(os.mkdir(repeatwork, 448i32) == 0); + let repeatav: []str = [driver(stages[i]), "build", "-w", repeatwork, + "-I", foldsource, "-o", repeatout, foldrepeat]; + runcommandenv(root, strings.concat("case-fold-repeat-", stages[i]), + repeatav, foldenv, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let repeattrace: str = readfile(tooltrace); + assert(occurrences(repeattrace, + "/domain.Foo.unit.new>") == 1); + let repeatline: str = linecontaining(repeattrace, + "/cmd.repeat.unit.new>"); + assert(occurrences(repeatline, "<--import>") == 1); + assert(occurrences(repeatline, "/domain.Foo.wwi.new>") == 1); + assert(os.exists(strings.concat(repeatwork, "/.wwtool.stamp"))); + let repeatpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c", + "/.wwtool.w6a", "/.wwtool.stamp", "/cmd.repeat.unit.ww", + "/cmd.repeat.wwi", "/cmd.repeat.s", "/cmd.repeat.o", + "/cmd.repeat.a", "/cmd.repeat.init.unit.ww", + "/cmd.repeat.init.s", "/cmd.repeat.init.o", + "/domain.Foo.unit.ww", "/domain.Foo.wwi", "/domain.Foo.s", + "/domain.Foo.o", "/domain.Foo.a"]; + let repeatbytes: []str = alloc([], repeatpaths.len: u64)!; + let rsi: i32 = 0; + for (rsi < repeatpaths.len) { + append(repeatbytes, strings.dup(readfile(strings.concat(repeatwork, + repeatpaths[rsi])))); + rsi += 1; + }; + let repeatoutput: str = strings.dup(readfile(repeatout)); + if (i == 0) { repeatref = strings.dup(repeatoutput); } + else { assert(same(repeatref, repeatoutput)); }; + let repeatcollision: str = strings.concat(foldrepeat, "/m.ww"); + writefile(repeatcollision, + "package main;\nimport _ domain.foo;\n"); + rewritefile(tooltrace, ""); + runcommandenv(root, strings.concat("case-fold-repeat-added-", stages[i]), + repeatav, foldenv, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stderr, foldexpected)); + assert(readfile(tooltrace).len == 0); + rsi = 0; + for (rsi < repeatpaths.len) { + assert(same(repeatbytes[rsi], readfile(strings.concat(repeatwork, + repeatpaths[rsi])))); + rsi += 1; + }; + assert(!directoryhasnew(repeatwork)); + assert(same(repeatoutput, readfile(repeatout))); + assert(!os.exists(strings.concat(repeatout, ".new"))); + assert(!os.exists(strings.concat(repeatout, ".sepwork"))); + assert(!os.exists(strings.concat(repeatwork, "/domain.foo.unit.ww"))); + assert(!os.exists(strings.concat(repeatwork, "/domain.foo.wwi"))); + assert(!os.exists(strings.concat(repeatwork, "/domain.foo.a"))); + assert(os.remove(repeatcollision) == 0); + rewritefile(tooltrace, ""); + runcommandenv(root, strings.concat("case-fold-repeat-removed-", stages[i]), + repeatav, foldenv, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(tooltrace), "compile") == 0); + assert(occurrences(readfile(tooltrace), "assemble") == 0); + assert(occurrences(readfile(tooltrace), "link") == 1); + rsi = 0; + for (rsi < repeatpaths.len) { + assert(same(repeatbytes[rsi], readfile(strings.concat(repeatwork, + repeatpaths[rsi])))); + rsi += 1; + }; + assert(!directoryhasnew(repeatwork)); + assert(same(repeatoutput, readfile(repeatout))); + assert(!os.exists(strings.concat(repeatout, ".new"))); + assert(!os.exists(strings.concat(repeatout, ".sepwork"))); + i += 1; + }; + rewritefile(tooltrace, ""); let reserveddiagnostics: []str = ["", ""]; i = 0; for (i < stages.len) { @@ -5889,9 +6179,25 @@ fn hexbytes(value: str) str = { let vendored: str = strings.concat(app, "/vendor/vendored"); let order: str = strings.concat(root, "/order"); let tools: str = strings.concat(root, "/tools"); + let foldfiles: str = strings.concat(root, "/foldfiles"); + let foldunicode: str = strings.concat(foldfiles, "/unicode"); + let foldinvalid: str = strings.concat(foldfiles, "/invalid"); + let foldnonormal: str = strings.concat(foldfiles, "/nonormal"); + let foldinternal: str = strings.concat(foldfiles, "/internal"); + let foldexternal: str = strings.concat(foldfiles, "/external"); + let foldsameonly: str = strings.concat(foldfiles, "/sameonly"); + let foldexternalonly: str = strings.concat(foldfiles, "/externalonly"); + let foldmixed: str = strings.concat(foldfiles, "/mixed"); + let foldcross: str = strings.concat(foldfiles, "/cross"); + let foldcreationroot: str = strings.concat(root, "/fold-creation-order"); + let foldcreation: str = strings.concat(foldcreationroot, "/prod"); mkdirall(app); mkdirall(good); mkdirall(pkg); mkdirall(testgood); mkdirall(wrongonly); mkdirall(notests); mkdirall(internal); mkdirall(command); mkdirall(vendored); mkdirall(order); mkdirall(tools); + mkdirall(foldunicode); mkdirall(foldinvalid); mkdirall(foldnonormal); + mkdirall(foldinternal); mkdirall(foldexternal); mkdirall(foldsameonly); + mkdirall(foldexternalonly); mkdirall(foldmixed); mkdirall(foldcross); + mkdirall(foldcreation); let good5: str = strings.concat("package good;\n", "export fn value() i32 = { return 5; };\n"); @@ -5949,6 +6255,12 @@ fn hexbytes(value: str) str = { ignoredinternal); writefile(strings.concat(app, "/badmain_linux_arm64.ww"), ignoredmain); writefile(strings.concat(app, "/badvendor_windows.ww"), ignoredvendor); + writefile(strings.concat(app, "/.Fold.ww"), ignoredparse); + writefile(strings.concat(app, "/.fold.ww"), ignoredparseedit); + writefile(strings.concat(app, "/_Fold.ww"), ignoredparse); + writefile(strings.concat(app, "/_fold.ww"), ignoredparseedit); + writefile(strings.concat(app, "/Fold_windows.ww"), ignoredparse); + writefile(strings.concat(app, "/fold_windows.ww"), ignoredparseedit); writefile(strings.concat(internal, "/secret.ww"), strings.concat( "package secret;\n", "export fn value() i32 = { return 19; };\n")); @@ -6004,6 +6316,76 @@ fn hexbytes(value: str) str = { writefile(strings.concat(notests, "/base.ww"), "package notests;\nexport fn value() i32 = { return 1; };\n"); writefile(strings.concat(notests, "/only_windows_test.ww"), ignoredparse); + writefile(strings.concat(pkg, "/.Fold_test.ww"), ignoredparse); + writefile(strings.concat(pkg, "/.fold_test.ww"), ignoredparseedit); + writefile(strings.concat(pkg, "/_Fold_test.ww"), ignoredparse); + writefile(strings.concat(pkg, "/_fold_test.ww"), ignoredparseedit); + writefile(strings.concat(pkg, "/Fold_windows_test.ww"), ignoredparse); + writefile(strings.concat(pkg, "/fold_windows_test.ww"), ignoredparseedit); + + writefile(strings.concat(foldunicode, "/K.ww"), + "package main;\nfn latin() i32 = { return 1; };\n"); + writefile(strings.concat(foldunicode, "/K.ww"), + "package main;\nfn kelvin() i32 = { return 2; };\n"); + writefile(strings.concat(foldunicode, "/main.ww"), + "package main;\nfn main() i32 = { return 0; };\n"); + let invalidupperbytes: []u8 = alloc([], 6u64)!; + append(invalidupperbytes, 'A'); append(invalidupperbytes, '-'); + append(invalidupperbytes, 255u8); append(invalidupperbytes, '.'); + append(invalidupperbytes, 'w'); append(invalidupperbytes, 'w'); + let invalidlowerbytes: []u8 = alloc([], 6u64)!; + append(invalidlowerbytes, 'a'); append(invalidlowerbytes, '-'); + append(invalidlowerbytes, 254u8); append(invalidlowerbytes, '.'); + append(invalidlowerbytes, 'w'); append(invalidlowerbytes, 'w'); + writefile(strings.concat(foldinvalid, "/", + strings.frombytes(invalidupperbytes)), + "package main;\nfn invalidupper() i32 = { return 1; };\n"); + writefile(strings.concat(foldinvalid, "/", + strings.frombytes(invalidlowerbytes)), + "package main;\nfn invalidlower() i32 = { return 2; };\n"); + writefile(strings.concat(foldinvalid, "/main.ww"), + "package main;\nfn main() i32 = { return 0; };\n"); + writefile(strings.concat(foldnonormal, "/É.ww"), + "package main;\nfn composed() i32 = { return 1; };\n"); + writefile(strings.concat(foldnonormal, "/É.ww"), + "package main;\nfn decomposed() i32 = { return 2; };\n"); + writefile(strings.concat(foldnonormal, "/main.ww"), + "package main;\nfn main() i32 = { return 0; };\n"); + writefile(strings.concat(foldinternal, "/base.ww"), + "package sample;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(foldinternal, "/File_test.ww"), + "package sample;\n@test fn upper() void = { assert(true); };\n"); + writefile(strings.concat(foldinternal, "/file_test.ww"), + "package sample;\n@test fn lower() void = { assert(true); };\n"); + writefile(strings.concat(foldexternal, "/base.ww"), + "package sample;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(foldexternal, "/File_test.ww"), + "package sample_test;\n@test fn upper() void = { assert(true); };\n"); + writefile(strings.concat(foldexternal, "/file_test.ww"), + "package sample_test;\n@test fn lower() void = { assert(true); };\n"); + writefile(strings.concat(foldsameonly, "/File_test.ww"), + "package sample;\n@test fn upper() void = { assert(true); };\n"); + writefile(strings.concat(foldsameonly, "/file_test.ww"), + "package sample;\n@test fn lower() void = { assert(true); };\n"); + writefile(strings.concat(foldexternalonly, "/File_test.ww"), + "package sample_test;\n@test fn upper() void = { assert(true); };\n"); + writefile(strings.concat(foldexternalonly, "/file_test.ww"), + "package sample_test;\n@test fn lower() void = { assert(true); };\n"); + writefile(strings.concat(foldmixed, "/File_test.ww"), + "package sample;\n@test fn upper() void = { assert(true); };\n"); + writefile(strings.concat(foldmixed, "/file_test.ww"), + "package sample_test;\n@test fn lower() void = { assert(true); };\n"); + writefile(strings.concat(foldcross, "/X_TEST.ww"), + "package cross;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(foldcross, "/x_test.ww"), + "package cross;\n@test fn lower() void = { assert(value() == 1); };\n"); + // The lower spelling is created first to prove getdents creation order is + // irrelevant to both direct and recursive collision diagnostics. + writefile(strings.concat(foldcreation, "/file.ww"), + "package main;\nfn lower() i32 = { return 1; };\n"); + writefile(strings.concat(foldcreation, "/File.ww"), strings.concat( + "package main;\nfn upper() i32 = { return 2; };\n", + "fn main() i32 = { return 0; };\n")); // Creation order is deliberately the inverse of byte order. writefile(strings.concat(order, "/z_linux.ww"), "package order;\n@test fn z() void = { };\n"); @@ -6061,6 +6443,12 @@ fn hexbytes(value: str) str = { let faildiagref: str = ""; let recursiveoutref: str = ""; let recursiveerrref: str = ""; + let foldfilerefs: []str = ["", "", "", "", "", "", "", ""]; + let crossref: str = ""; + let nonormalref: str = ""; + let nonormalunitref: str = ""; + let foldcreationref: str = ""; + let foldcreationrecursiveref: str = ""; let baseenv: []str = os.getenvs(); let si: i32 = 0; for (si < stages.len) { @@ -6513,6 +6901,267 @@ fn hexbytes(value: str) str = { assert(same(recursiveoutref, out.stdout)); assert(same(recursiveerrref, out.stderr)); }; + + // An upper-case TEST suffix is production source, while its lower-case + // twin is test source. Ordinary build stays isolated; test selects both + // categories into one directory-owned filename-collision scope. + let crosswork: str = strings.concat(root, "/", labels[si], + "-fold-cross-build-work"); + let crossout: str = strings.concat(root, "/", labels[si], + "-fold-cross.a"); + assert(os.mkdir(crosswork, 448i32) == 0); + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + let crossbuildav: []str = [driver(stages[si]), "build", "-w", + crosswork, "-I", foldfiles, "-o", crossout, foldcross]; + runcommandenv(root, strings.concat("fold-cross-build-", labels[si]), + crossbuildav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(!has(readfile(strings.concat(crosswork, "/cross.unit.ww")), + "@test fn lower")); + if (si == 0) { crossref = strings.dup(readfile(crossout)); } + else { assert(same(crossref, readfile(crossout))); }; + + let foldtargets: []str = [foldunicode, foldinvalid, foldinternal, + foldexternal, foldsameonly, foldexternalonly, foldmixed, foldcross]; + let foldmodes: []str = ["build", "build", "test", "test", "test", + "test", "test", "test"]; + let foldpackages: []str = ["", "", "sample", "sample", "sample", + "sample", "sample", "cross"]; + let foldlabels: []str = ["unicode", "invalid", "internal", "external", + "same-only", "external-only", "mixed", "cross"]; + let foldwants: []str = [ + "ww: case-insensitive file name collision: \"K.ww\" and \"K.ww\"\n", + "ww: case-insensitive file name collision: \"A-\\xff.ww\" and \"a-\\xfe.ww\"\n", + "ww: case-insensitive file name collision: \"File_test.ww\" and \"file_test.ww\"\n", + "ww: case-insensitive file name collision: \"File_test.ww\" and \"file_test.ww\"\n", + "ww: case-insensitive file name collision: \"File_test.ww\" and \"file_test.ww\"\n", + "ww: case-insensitive file name collision: \"File_test.ww\" and \"file_test.ww\"\n", + "ww: case-insensitive file name collision: \"File_test.ww\" and \"file_test.ww\"\n", + "ww: case-insensitive file name collision: \"X_TEST.ww\" and \"x_test.ww\"\n", + ]; + let fi: i32 = 0; + for (fi < foldtargets.len) { + let foldwork: str = strings.concat(root, "/", labels[si], + "-fold-", foldlabels[fi], "-work"); + let foldoutput: str = strings.concat(root, "/", labels[si], + "-fold-", foldlabels[fi]); + assert(os.mkdir(foldwork, 448i32) == 0); + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + let foldbuildav: []str = [driver(stages[si]), "build", "-w", + foldwork, "-I", foldfiles, "-o", foldoutput, foldtargets[fi]]; + let foldtestav: []str = [driver(stages[si]), "test", "-w", + foldwork, "-I", foldfiles, foldtargets[fi]]; + let foldav: []str = foldtestav; + if (same(foldmodes[fi], "build")) { foldav = foldbuildav; }; + runcommandenv(root, strings.concat("filename-fold-", labels[si], + "-", foldlabels[fi]), foldav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let foldfullwant: str = foldwants[fi]; + if (same(foldmodes[fi], "test")) { + foldfullwant = strings.concat(foldwants[fi], "FAIL ", + foldtargets[fi], " [", foldpackages[fi], + "] (build exit 1)\n"); + }; + assert(out.stdout.len == 0 && same(out.stderr, foldfullwant)); + assert(readfile(compilertrace).len == 0); + assert(readfile(assemblertrace).len == 0); + assert(readfile(linkertrace).len == 0); + assert(!os.exists(foldoutput)); + assert(!os.exists(strings.concat(foldoutput, ".new"))); + assert(!os.exists(strings.concat(foldoutput, ".sepwork"))); + assert(!os.exists(strings.concat(foldwork, "/.wwtool.stamp"))); + assert(directoryisempty(foldwork)); + if (si == 0) { foldfilerefs[fi] = strings.dup(out.stderr); } + else { assert(same(foldfilerefs[fi], out.stderr)); }; + fi += 1; + }; + + let creationmodes: []str = ["direct", "recursive"]; + let creationfull: []str = ["", ""]; + let creationwant: str = + "ww: case-insensitive file name collision: \"File.ww\" and \"file.ww\"\n"; + let ci: i32 = 0; + for (ci < creationmodes.len) { + let creationwork: str = strings.concat(root, "/", labels[si], + "-fold-creation-", creationmodes[ci], "-work"); + let creationout: str = strings.concat(root, "/", labels[si], + "-fold-creation-", creationmodes[ci]); + assert(os.mkdir(creationwork, 448i32) == 0); + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + let directav: []str = [driver(stages[si]), "build", "-w", + creationwork, "-I", foldcreationroot, "-o", creationout, + foldcreation]; + if (ci != 0) { assert(os.mkdir(creationout, 448i32) == 0); }; + let recursiveav: []str = [driver(stages[si]), "build", "-w", + creationwork, "-I", foldcreationroot, "-o", + strings.concat(creationout, "/"), + strings.concat(foldcreationroot, "/...")]; + let creationav: []str = directav; + if (ci != 0) { creationav = recursiveav; }; + runcommandenv(root, strings.concat("filename-fold-creation-", + labels[si], "-", creationmodes[ci]), creationav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0); + let creationfullwant: str = creationwant; + if (ci != 0) { + creationfullwant = strings.concat(creationwant, "FAIL ", + foldcreation, " [main] (build exit 1)\n"); + }; + assert(same(out.stderr, creationfullwant)); + assert(readfile(compilertrace).len == 0); + assert(readfile(assemblertrace).len == 0); + assert(readfile(linkertrace).len == 0); + if (ci == 0) { assert(!os.exists(creationout)); } + else { assert(directoryisempty(creationout)); }; + assert(!os.exists(strings.concat(creationout, ".new"))); + assert(!os.exists(strings.concat(creationout, ".sepwork"))); + assert(directoryisempty(creationwork)); + creationfull[ci] = strings.dup(out.stderr); + ci += 1; + }; + if (si == 0) { + foldcreationref = strings.dup(creationfull[0]); + foldcreationrecursiveref = strings.dup(creationfull[1]); + } else { + assert(same(foldcreationref, creationfull[0])); + assert(same(foldcreationrecursiveref, creationfull[1])); + }; + + // Canonically equivalent Unicode normalization forms remain distinct. + let nonormalwork: str = strings.concat(root, "/", labels[si], + "-fold-nonormal-work"); + let nonormalout: str = strings.concat(root, "/", labels[si], + "-fold-nonormal"); + assert(os.mkdir(nonormalwork, 448i32) == 0); + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + let nonormalav: []str = [driver(stages[si]), "build", "-w", + nonormalwork, "-I", foldfiles, "-o", nonormalout, foldnonormal]; + runcommandenv(root, strings.concat("filename-no-normalization-", + labels[si]), nonormalav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let nonormalunit: str = readfile(strings.concat(nonormalwork, + "/nonormal.unit.ww")); + assert(has(nonormalunit, "fn composed() i32")); + assert(has(nonormalunit, "fn decomposed() i32")); + if (si == 0) { nonormalref = strings.dup(readfile(nonormalout)); } + else { assert(same(nonormalref, readfile(nonormalout))); }; + if (si == 0) { nonormalunitref = strings.dup(nonormalunit); } + else { assert(same(nonormalunitref, nonormalunit)); }; + + // A collision introduced after a committed generation is pre-tool and + // cannot mutate any successful action, stamp, or published output. Its + // removal restores exact warm reuse (only the normal relink remains). + let warmname: str = strings.concat("warm", labels[si]); + let warmdir: str = strings.concat(foldfiles, "/", warmname); + mkdirall(warmdir); + writefile(strings.concat(warmdir, "/File.ww"), strings.concat( + "package main;\nfn upper() i32 = { return 1; };\n")); + writefile(strings.concat(warmdir, "/main.ww"), strings.concat( + "package main;\nfn main() i32 = { return upper() - 1; };\n")); + let warmwork: str = strings.concat(root, "/", labels[si], + "-fold-warm-work"); + let warmout: str = strings.concat(root, "/", labels[si], + "-fold-warm"); + assert(os.mkdir(warmwork, 448i32) == 0); + let warmav: []str = [driver(stages[si]), "build", "-w", warmwork, + "-I", foldfiles, "-o", warmout, warmdir]; + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + runcommandenv(root, strings.concat("filename-fold-warm-cold-", + labels[si]), warmav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let warmsuffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a", + ".init.unit.ww", ".init.s", ".init.o"]; + let warmrefs: []str = ["", "", "", "", "", "", "", ""]; + let wi: i32 = 0; + for (wi < warmsuffixes.len) { + warmrefs[wi] = strings.dup(readfile(strings.concat(warmwork, "/", + warmname, warmsuffixes[wi]))); + wi += 1; + }; + let warmbinref: str = strings.dup(readfile(warmout)); + let warmstampref: str = strings.dup(readfile(strings.concat(warmwork, + "/.wwtool.stamp"))); + let warmtoolpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c", + "/.wwtool.w6a"]; + let warmtoolrefs: []str = alloc([], warmtoolpaths.len: u64)!; + wi = 0; + for (wi < warmtoolpaths.len) { + append(warmtoolrefs, strings.dup(readfile(strings.concat(warmwork, + warmtoolpaths[wi])))); + wi += 1; + }; + let collidingfile: str = strings.concat(warmdir, "/file.ww"); + writefile(collidingfile, + "package main;\nfn lower() i32 = { return 2; };\n"); + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + runcommandenv(root, strings.concat("filename-fold-warm-added-", + labels[si]), warmav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stderr, + "ww: case-insensitive file name collision: \"File.ww\" and \"file.ww\"\n")); + assert(readfile(compilertrace).len == 0); + assert(readfile(assemblertrace).len == 0); + assert(readfile(linkertrace).len == 0); + wi = 0; + for (wi < warmsuffixes.len) { + assert(same(warmrefs[wi], readfile(strings.concat(warmwork, "/", + warmname, warmsuffixes[wi])))); + wi += 1; + }; + wi = 0; + for (wi < warmtoolpaths.len) { + assert(same(warmtoolrefs[wi], readfile(strings.concat(warmwork, + warmtoolpaths[wi])))); + wi += 1; + }; + assert(!directoryhasnew(warmwork)); + assert(same(warmbinref, readfile(warmout))); + assert(!os.exists(strings.concat(warmout, ".new"))); + assert(!os.exists(strings.concat(warmout, ".sepwork"))); + assert(same(warmstampref, readfile(strings.concat(warmwork, + "/.wwtool.stamp")))); + assert(os.remove(collidingfile) == 0); + rewritefile(compilertrace, ""); rewritefile(assemblertrace, ""); + rewritefile(linkertrace, ""); + runcommandenv(root, strings.concat("filename-fold-warm-removed-", + labels[si]), warmav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(readfile(compilertrace).len == 0); + assert(readfile(assemblertrace).len == 0); + assert(occurrences(readfile(linkertrace), "\n") == 1); + wi = 0; + for (wi < warmsuffixes.len) { + assert(same(warmrefs[wi], readfile(strings.concat(warmwork, "/", + warmname, warmsuffixes[wi])))); + wi += 1; + }; + wi = 0; + for (wi < warmtoolpaths.len) { + assert(same(warmtoolrefs[wi], readfile(strings.concat(warmwork, + warmtoolpaths[wi])))); + wi += 1; + }; + assert(!directoryhasnew(warmwork)); + assert(same(warmbinref, readfile(warmout))); + assert(!os.exists(strings.concat(warmout, ".new"))); + assert(!os.exists(strings.concat(warmout, ".sepwork"))); + assert(same(warmstampref, readfile(strings.concat(warmwork, + "/.wwtool.stamp")))); si += 1; }; clean(root); @@ -8508,6 +9157,26 @@ fn runtimepath(relative: str) str = { let cleanroot: str = strings.concat(root, "/clean"); let cleanclient: str = strings.concat(cleanroot, "/domain/app"); let cleanordinary: str = strings.concat(cleanroot, "/lib/math"); + let foldvendorupper: str = strings.concat(source, + "/domain/fold/vendor/lib/Foo"); + let foldvendorlower: str = strings.concat(source, + "/domain/fold/vendor/lib/foo"); + let foldvendorleft: str = strings.concat(source, "/domain/fold/left"); + let foldvendorright: str = strings.concat(source, "/domain/fold/right"); + let foldvendorclient: str = strings.concat(source, "/domain/fold/client"); + let foldvendorreverseclient: str = strings.concat(source, + "/domain/fold/reverseclient"); + let foldaliasvendorlib: str = strings.concat(source, + "/domain/foldalias/vendor/lib"); + let foldaliasvendorupper: str = strings.concat(foldaliasvendorlib, "/Foo"); + let foldaliasvendorlower: str = strings.concat(foldaliasvendorlib, "/foo"); + let foldaliasvendorleft: str = strings.concat(source, + "/domain/foldalias/left"); + let foldaliasvendorright: str = strings.concat(source, + "/domain/foldalias/right"); + let foldaliasvendorclient: str = strings.concat(source, + "/domain/foldalias/client"); + let foldaliasvendorvault: str = strings.concat(root, "/fold-vendor-vault"); let dirs: []str = [leaf, nearest, outer, rootvendor, ordinary, client, one, two, outerclient, rootclient, emptyclient, @@ -8523,6 +9192,9 @@ fn runtimepath(relative: str) str = { alpha, beta, strings.concat(alpha, "/vendor/lib"), strings.concat(beta, "/vendor/lib"), pairvault, boundaryclient, boundaryordinary, boundaryabove, cleanclient, cleanordinary, + foldvendorupper, foldvendorlower, foldvendorleft, foldvendorright, + foldvendorclient, foldvendorreverseclient, foldaliasvendorlib, foldaliasvendorleft, + foldaliasvendorright, foldaliasvendorclient, foldaliasvendorvault, runtime, tools]; let di: i32 = 0; for (di < dirs.len) { mkdirall(dirs[di]); di += 1; }; @@ -8649,6 +9321,35 @@ fn runtimepath(relative: str) str = { writefile(strings.concat(cleanclient, "/main.ww"), mainbody); writefile(strings.concat(cleanordinary, "/math.ww"), "package math;\nexport fn value() i32 = { return 44; };\n"); + writefile(strings.concat(foldvendorupper, "/pkg.ww"), + "package upper;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(foldvendorlower, "/pkg.ww"), + "package lower;\nexport fn value() i32 = { return 2; };\n"); + writefile(strings.concat(foldvendorleft, "/pkg.ww"), strings.concat( + "package left;\nimport _ lib.Foo;\n", + "export fn value() i32 = { return 1; };\n")); + writefile(strings.concat(foldvendorright, "/pkg.ww"), strings.concat( + "package right;\nimport _ lib.foo;\n", + "export fn value() i32 = { return 2; };\n")); + writefile(strings.concat(foldvendorclient, "/main.ww"), strings.concat( + "package main;\nimport _ domain.fold.left;\n", + "import _ domain.fold.right;\nfn main() i32 = { return 0; };\n")); + writefile(strings.concat(foldvendorreverseclient, "/main.ww"), strings.concat( + "package main;\nimport _ domain.fold.right;\n", + "import _ domain.fold.left;\nfn main() i32 = { return 0; };\n")); + writefile(strings.concat(foldaliasvendorvault, "/pkg.ww"), + "package shared;\nexport fn value() i32 = { return 1; };\n"); + assert(os.symlink(foldaliasvendorvault, foldaliasvendorupper) == 0); + assert(os.symlink(foldaliasvendorvault, foldaliasvendorlower) == 0); + writefile(strings.concat(foldaliasvendorleft, "/pkg.ww"), strings.concat( + "package left;\nimport _ lib.Foo;\n", + "export fn value() i32 = { return 1; };\n")); + writefile(strings.concat(foldaliasvendorright, "/pkg.ww"), strings.concat( + "package right;\nimport _ lib.foo;\n", + "export fn value() i32 = { return 2; };\n")); + writefile(strings.concat(foldaliasvendorclient, "/main.ww"), strings.concat( + "package main;\nimport _ domain.foldalias.left;\n", + "import _ domain.foldalias.right;\nfn main() i32 = { return 0; };\n")); let compilerwrapper: str = strings.concat(tools, "/w6c.sh"); let assemblerwrapper: str = strings.concat(tools, "/w6a.sh"); @@ -8711,6 +9412,8 @@ fn runtimepath(relative: str) str = { let referencealiasdirectdiag: str = ""; let referenceprefixdiag: str = ""; let candidate_diag_refs: []str = ["", ""]; + let foldvendorrefs: []str = ["", "", ""]; + let foldvendormultiref: str = ""; let supplementalrefs: []str = ["", "", "", "", "", "", "", "", ""]; let referencevendortestout: str = ""; @@ -8764,9 +9467,112 @@ fn runtimepath(relative: str) str = { append(env, strings.concat("WW_VENDOR_REAL_LINKER=", driver(linkers[si]))); + let out: commandout; + let foldtargets: []str = [foldvendorclient, foldvendorreverseclient, + foldaliasvendorclient]; + let foldlabels: []str = ["distinct", "reverse-imports", "one-dir"]; + let foldwants: []str = [ + "ww: case-insensitive import collision: \"domain.fold.vendor.lib.Foo\" and \"domain.fold.vendor.lib.foo\"\n", + "ww: case-insensitive import collision: \"domain.fold.vendor.lib.Foo\" and \"domain.fold.vendor.lib.foo\"\n", + "ww: case-insensitive import collision: \"domain.foldalias.vendor.lib.Foo\" and \"domain.foldalias.vendor.lib.foo\"\n", + ]; + let fi: i32 = 0; + for (fi < foldtargets.len) { + rewritefile(compilertraces[si], ""); + rewritefile(assemblertraces[si], ""); + rewritefile(linkertraces[si], ""); + let foldwork: str = strings.concat(root, "/vendor-fold-", + foldlabels[fi], "-", stages[si], "-work"); + let foldout: str = strings.concat(root, "/vendor-fold-", + foldlabels[fi], "-", stages[si]); + assert(os.mkdir(foldwork, 448i32) == 0); + let foldav: []str = [driver(stages[si]), "build", "-w", + foldwork, "-I", source, "-o", foldout, foldtargets[fi]]; + runcommandenv(root, strings.concat("vendor-fold-", foldlabels[fi], + "-", stages[si]), foldav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, foldwants[fi])); + assert(readfile(compilertraces[si]).len == 0); + assert(readfile(assemblertraces[si]).len == 0); + assert(readfile(linkertraces[si]).len == 0); + assert(!os.exists(foldout)); + assert(!os.exists(strings.concat(foldout, ".new"))); + assert(!os.exists(strings.concat(foldout, ".sepwork"))); + assert(!os.exists(strings.concat(foldwork, "/.wwtool.stamp"))); + assert(directoryisempty(foldwork)); + if (si == 0) { foldvendorrefs[fi] = strings.dup(out.stderr); } + else { assert(same(foldvendorrefs[fi], out.stderr)); }; + fi += 1; + }; + assert(same(foldvendorrefs[0], foldvendorrefs[1])); + + // A command-global multi-product request is one rejection transaction. + // Reversing its descriptors cannot publish the unrelated product or + // mutate either pre-existing status. + let foldmultiwork: str = strings.concat(root, "/vendor-fold-multi-", + stages[si], "-work"); + assert(os.mkdir(foldmultiwork, 448i32) == 0); + let foldallowedout: str = strings.concat(root, "/vendor-fold-allowed-", + stages[si]); + let foldcollisionout: str = strings.concat(root, + "/vendor-fold-collision-", stages[si]); + let foldallowedstatus: str = strings.concat(foldallowedout, ".status"); + let foldcollisionstatus: str = strings.concat(foldcollisionout, ".status"); + writefile(foldallowedstatus, "sentinel-allowed\n"); + writefile(foldcollisionstatus, "sentinel-collision\n"); + let foldforward: []str = [driver(stages[si]), "test", "-c", "-w", + foldmultiwork, "-I", source, "--ww-package-build", + "--ww-package-test", "build", "main", "main", "-", "-", client, + foldallowedout, foldallowedstatus, + "--ww-package-test", "build", "main", "main", "-", "-", + foldvendorclient, foldcollisionout, foldcollisionstatus, client]; + let foldreverse: []str = [driver(stages[si]), "test", "-c", "-w", + foldmultiwork, "-I", source, "--ww-package-build", + "--ww-package-test", "build", "main", "main", "-", "-", + foldvendorclient, foldcollisionout, foldcollisionstatus, + "--ww-package-test", "build", "main", "main", "-", "-", client, + foldallowedout, foldallowedstatus, client]; + let foldrequests: [][]str = [foldforward, foldreverse]; + let foldmultidiags: []str = ["", ""]; + let foldorderlabels: []str = ["forward", "reverse"]; + let foi: i32 = 0; + for (foi < foldrequests.len) { + rewritefile(compilertraces[si], ""); + rewritefile(assemblertraces[si], ""); + rewritefile(linkertraces[si], ""); + runcommandenv(root, strings.concat("vendor-fold-multi-", stages[si], + "-", foldorderlabels[foi]), foldrequests[foi], env, + (180i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, foldwants[0])); + foldmultidiags[foi] = strings.dup(out.stderr); + assert(readfile(compilertraces[si]).len == 0); + assert(readfile(assemblertraces[si]).len == 0); + assert(readfile(linkertraces[si]).len == 0); + assert(same(readfile(foldallowedstatus), "sentinel-allowed\n")); + assert(same(readfile(foldcollisionstatus), "sentinel-collision\n")); + assert(!os.exists(strings.concat(foldallowedstatus, ".new"))); + assert(!os.exists(strings.concat(foldcollisionstatus, ".new"))); + assert(!os.exists(foldallowedout)); + assert(!os.exists(strings.concat(foldallowedout, ".new"))); + assert(!os.exists(strings.concat(foldallowedout, ".sepwork"))); + assert(!os.exists(strings.concat(foldallowedout, ".result"))); + assert(!os.exists(strings.concat(foldallowedout, ".result.new"))); + assert(!os.exists(foldcollisionout)); + assert(!os.exists(strings.concat(foldcollisionout, ".new"))); + assert(!os.exists(strings.concat(foldcollisionout, ".sepwork"))); + assert(!os.exists(strings.concat(foldcollisionout, ".result"))); + assert(!os.exists(strings.concat(foldcollisionout, ".result.new"))); + assert(directoryisempty(foldmultiwork)); + foi += 1; + }; + assert(same(foldmultidiags[0], foldmultidiags[1])); + if (si == 0) { foldvendormultiref = strings.dup(foldmultidiags[0]); } + else { assert(same(foldvendormultiref, foldmultidiags[0])); }; + let av: []str = [driver(stages[si]), "build", "-w", works[si], "-I", source, "-o", bins[si], client]; - let out: commandout; runcommandenv(root, strings.concat("vendor-cold-", stages[si]), av, env, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0);