From 78c9eee82d8a9f23201ebf5a6d9967303992c815 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 22 Aug 2026 04:27:23 +0900 Subject: [PATCH] ww imports: reject init bindings before recovery --- cmd/wcc/check.c | 47 ++++- cmd/wcc/parse.c | 6 + cmd/wcc/ww.h | 4 + docs/build-system.md | 287 ++++++++++++++++++++++++++++ docs/spec.md | 15 ++ docs/test-system-v2.md | 33 ++++ lib/ww/syntax/ast.ww | 5 +- lib/ww/syntax/decl.ww | 6 + selfhost/cmd/wcc/check.ww | 48 ++++- test/package/package_test.ww | 353 +++++++++++++++++++++++++++++++++++ 10 files changed, 789 insertions(+), 15 deletions(-) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index c469c2e2..408e79d5 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2907,6 +2907,25 @@ decl_mod(Node *file, Node *d) return NULL; } +static int +invalid_init_import(Node *u) +{ + return u != NULL && u->kind == N_USE && !u->useblank + && u->str != NULL && strcmp(u->str, "init") == 0; +} + +static Pos +import_binding_pos(Node *u) +{ + Pos p = u->pos; + if (u->usefile != NULL) { + p.file = u->usefile; + p.line = u->useline; + p.col = u->usecol; + } + return p; +} + /* * use_path — map a source-file default qualifier (the imported package's * declared name) to the full canonical import path it binds, for @@ -2932,7 +2951,7 @@ find_use_path(Node *file, const char *curmod, int source, const char *alias, if (strcmp(alias, leaf) == 0) return curmod; } for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->str == NULL + if (u->kind != N_USE || u->str == NULL || invalid_init_import(u) || u->sourceid != source || strcmp(u->str, alias) != 0) continue; const char *p = u->usepath ? u->usepath : u->str; @@ -3960,6 +3979,19 @@ check_import_alt(Node *d, const char *name) d->pos.file ? d->pos.file : "?", d->pos.line, d->pos.col, name); } +/* Pinned Go 1.26.5 types2 rejects an effective import binding named init and + * immediately continues before creating its PkgName. Diagnose every resolved + * occurrence at the first import-spec token, then keep it out of all binding + * recovery below while retaining the real loader-owned dependency edge. */ +static void +reject_init_imports(Checker *c, Node *file) +{ + for (Node *u = file->list; u; u = u->next) + if (invalid_init_import(u)) + err(c, import_binding_pos(u), + "cannot import package as init - init must be a func"); +} + /* Go's default import binding lives in the importing file's scope. Reject * only another binding in that same source section; equal names in sibling * files are independent even though their canonical edges are package-wide. */ @@ -3968,11 +4000,11 @@ check_import_redeclarations(Checker *c, Node *file) { if (!c->sep_mode) return; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->useblank + if (u->kind != N_USE || u->useblank || invalid_init_import(u) || u->imported || u->str == NULL) continue; for (Node *v = file->list; v != u; v = v->next) { - if (v->kind != N_USE || v->useblank + if (v->kind != N_USE || v->useblank || invalid_init_import(v) || v->imported || v->str == NULL || v->sourceid != u->sourceid) continue; @@ -3994,7 +4026,7 @@ check_import_usage_and_collisions(Checker *c, Node *file) { if (!c->sep_mode) return; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->useblank + if (u->kind != N_USE || u->useblank || invalid_init_import(u) || u->imported || u->used || u->str == NULL) continue; const char *path = u->usesource ? u->usesource @@ -4011,7 +4043,7 @@ check_import_usage_and_collisions(Checker *c, Node *file) if (d->imported || !top_decl_kind(d) || d->str == NULL) continue; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->useblank + if (u->kind != N_USE || u->useblank || invalid_init_import(u) || u->imported || u->str == NULL || strcmp(d->str, u->str) != 0) continue; @@ -4078,6 +4110,7 @@ check_file(Checker *c, Node *file) file->list = usenode; } } + reject_init_imports(c, file); reject_nonfunction_main_decls(c, file); mark_import_uses(c, file); check_import_redeclarations(c, file); @@ -4106,10 +4139,8 @@ check_file(Checker *c, Node *file) "'%s' cannot import itself", owner); if (d->useblank) continue; - if (d->str != NULL && strcmp(d->str, "init") == 0) { - err(c, d->pos, "cannot import package as init - init must be a func"); + if (invalid_init_import(d)) continue; - } Sym *prev = scope_lookup_local(c->cur, d->str); if (prev != NULL) { /* Self-import: the driver concatenates the diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 85439beb..f39a5336 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1323,6 +1323,12 @@ parseuse(Parser *p) Pos pp = p->cur.pos; expect(p, TK_USE); Node *n = newnode(p->a, N_USE, pp); + /* Keep n->pos at the import keyword for structural diagnostics. Go's + * import declaration position is the first spec token: the explicit alias + * when present, otherwise the path. */ + n->usefile = p->cur.pos.file; + n->useline = p->cur.pos.line; + n->usecol = p->cur.pos.col; const char *alias = NULL; const char *first; if (p->cur.kind == TK_UNDER) { diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 46d32029..14efbf4f 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -349,6 +349,10 @@ struct Node { const char *usepath; /* N_USE: canonical, vendor-expanded identity; * initially equal to `usesource`. */ const char *usealias; /* N_USE: explicit file-local alias, or NULL. */ + const char *usefile; /* N_USE: first import-spec token position; + * alias when explicit, path otherwise. */ + int useline; + int usecol; const char *usepkgname; /* N_USE: imported declared package name, * independent of the visible binding in `str`. */ int useblank; /* N_USE: `_` spelling; no source binding. */ diff --git a/docs/build-system.md b/docs/build-system.md index c80204de..ec0bc8b0 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -9033,6 +9033,293 @@ the valid persisted-byte contract is unchanged. Build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. No test-result cache is introduced. +### 11.46 Implemented effective-`init` import-binding recovery + +An import whose effective file-local qualifier is `init` is rejected as +`cannot import package as init - init must be a func`. The rule covers an +explicit `init` alias and an unaliased dependency declared `package init`. +Every resolved rejected occurrence reports the core error at the first +import-spec token: the alias token when explicit, otherwise the first path +token. The occurrence creates no qualifier and takes no part in unused, +duplicate-binding, or declaration/import-collision recovery. A later +`init.Name` therefore recovers independently as undefined. Resolution retains +precedence, so a missing target fails without an additional effective-`init` +diagnostic. + +Only the file-local binding is rejected. The resolved source occurrence and +its exact dotted dependency remain loader and graph provenance. Neither the +effective qualifier nor the dependency's declared package name becomes +canonical package, import, graph, action, symbol, `.wwi`, artifact, +publication, or persistence identity. + +The paragraphs above state the normative implementation contract and are +**behavior derived from the pinned implementation**. The post-change results +recorded in the proof subsection below are **directly measured WW behavior**. + +#### Pinned evidence and fact classification + +The sole authority is official Go 1.26.5 at +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `(*Checker).collectObjects` resolves an import at lines 248–263, selects the + explicit alias or imported declared name at lines 264–274, rejects effective + name `init` and immediately continues at lines 275–278, before explicit + import recording, `PkgName` construction, used-import tracking, or file-scope + installation at lines 280–335 in + [`cmd/compile/internal/types2/resolver.go`, lines 223–335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L223-L335); +- `(*Checker).unusedImports` examines only admitted imports at + [`cmd/compile/internal/types2/resolver.go`, lines 706–740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740), + while package/file collision reconciliation is at lines 472–489 of that + file; +- the independent public checker implements the same effective-name rejection + and immediate continuation in + [`go/types/resolver.go`, lines 261–315, especially 290–293](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/types/resolver.go#L261-L315); +- compiler-syntax `(*parser).importDecl` assigns the declaration position to + the first import-spec token in + [`cmd/compile/internal/syntax/parser.go`, lines 543–574](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/parser.go#L543-L574), + with node-position ownership in + [`cmd/compile/internal/syntax/nodes.go`, lines 10–31 and 56–64](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/nodes.go#L10-L64); +- public `ast.ImportSpec.Pos` independently chooses the alias position when + present and the path position otherwise in + [`go/ast/ast.go`, lines 908–915 and 939–946](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/ast/ast.go#L908-L946); +- compiler diagnostics are stably position-sorted in + [`cmd/compile/internal/base/print.go`, lines 70–92](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/base/print.go#L70-L92); and +- package loading and action construction remain separate from file binding in + [`cmd/go/internal/load/pkg.go`, lines 757–805](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L757-L805) + and + [`cmd/go/internal/load/pkg.go`, lines 2024–2047](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L2024-L2047), + [`cmd/go/internal/work/action.go`, lines 647–658](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L647-L658), + and test-variant construction in + [`cmd/go/internal/load/test.go`, lines 175–240](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/test.go#L175-L240). + +Official assertions are +[`internal/types/testdata/check/importdecl0/importdecl0a.go`, lines 9–17](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L9-L17), +which expects only the core error for an explicit `init` alias; +[`test/fixedbugs/issue4517d.go`, lines 7–9](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/issue4517d.go#L7-L9), +which covers the explicit spelling; and +[`test/fixedbugs/issue43962.dir/a.go`, lines 1–5](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/issue43962.dir/a.go#L1-L5) +with +[`b.go`, lines 1–7](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/issue43962.dir/b.go#L1-L7), +which covers an imported package declared `init`. + +Those resolver, syntax-position, loader/action branches and official +assertions are **behavior directly implemented or asserted by pinned Go**. +The immediate continuation proves that the rejected occurrence contributes no +unused, duplicate-binding, or declaration/import-collision recovery; every +rejected occurrence reports independently; a later selector sees no package +binding; and missing resolution wins before effective-name checking. The +separate loader/action path proves that rejecting the qualifier does not erase +the resolved dependency occurrence or change canonical identity. Those +conclusions are **behavior derived from the pinned implementation**. They +honestly apply to WW's local, unquoted dotted-import, manifest-free model +without adding modules, manifests, registries, lock files, caches, databases, +CAS, network resolution, quoted/grouped/dot/general imports, or a source-level +build language. + +#### Fresh four-axis audit and direct pre-fix measurements + +The fresh audit also reconfirmed two applicable but unselected differences: +multiple named source files remain different across build, test, package, and +import construction; shared test-process state and ordinary-abort behavior +remain different across test runtime and package/imported mutable state. They +remain genuine future candidates and were not reclassified as aligned or +inapplicable. Effective-`init` recovery was selected because the pinned +resolver supplies one exact, bounded semantic owner whose complete binding and +lifecycle effects can be closed without redefining either broader gap. + +Before this slice, the following observations were **directly measured WW +behavior**. Every Cstage/WWstage pair had identical status, stdout bytes, +stderr bytes, and artifact state: + +| Input | Status and stdout | Pre-fix stderr SHA-256 | Pre-fix recovery | +|---|---|---|---| +| explicit unused `init` alias | 1; empty | `5248382591e88562ba9c3c648af47523f617be0dcaf16586dc46a2517f76fe58` | bogus unused, then core | +| explicit alias used by `init.value()` | 1; empty | `a3189e9aeaf56d5cb8aa63aa1a4de07d5176af531555a5834315298e4e9105a8` | core, then undefined | +| implicit declared-name `init`, unused | 1; empty | `c77b3cf3b052727d90662c9b33ba85c10cba133d25b1d8b41dd53e32e1ab4978` | bogus unused, then core | +| implicit qualifier used by `init.value()` | 1; empty | `cf0cf83a83077736497cd54a801762912f97ced4684d6c878257edbc79958340` | core, then undefined | +| two rejected occurrences plus use | 1; empty | `9124e49408a710e1ed49a0b39b46b0967b9fe4c5f8bc67b21205d174bcfaebee` | bogus redeclaration/alternate and unused; two cores; undefined | +| rejected import plus top-level `let init` | 1; empty | `b5e48cfab10e95d581d64d59e28409c63c7f450fa09c8d2bd11d2bb1bf487c08` | bogus unused/collision/alternate plus correct declaration/core errors | +| missing target under alias `init` | 1; empty | `5c0fd415bb8bc8791bb87cf7211f69eea06b87ea63f264f5b8b526cbd8ac1fd9` | only missing-package error; aligned precedence | +| same-package test, unused alias | 1; exact `FAIL\n` | `9b40aac1b160743787b4782a967350b9c97a4bf414ea0c05ea9b926c85403f9e` | bogus unused, then core | +| external test, later use | 1; exact `FAIL\n` | `e3fcb66e8fe4eabaf2bbac0b510c370dd904fca90daab334ad4cd9b82ac914f3` | core, then undefined | + +Representative pre-fix output placed both the bogus unused diagnostic and the +core error at generated-unit column 1, the `import` keyword: + +```text +...unit.new:3:1: error: "pkg.normal" imported as init and not used +...unit.new:3:1: error: cannot import package as init - init must be a func +``` + +Repeated imports additionally reported `init redeclared in this block`; a +top-level declaration additionally reported +`init already declared through import of package init`. These were checker +recovery artifacts forbidden by the pinned immediate continuation. Pinned +position ownership maps WW's `import init ...` spelling to the alias at source +column 8. + +Aligned controls succeeded in both stages with empty streams: blank +`import _ pkg.normal;` emitted a stage-byte-identical artifact with SHA-256 +`20847ada6923ab0bfd1dff6a3c387e0c5adace74152b84e895bc420a2e4134e9`, +and used `import stable pkg.normal;` emitted a stage-byte-identical artifact +with SHA-256 +`1a8fefbc12d5ea4a2bca938db6fbcdb66f3a14bbc0f7371756e4b5862ed8935c`. +Each rejected cold case created no new public artifact. A compiler wrapper +recorded exactly two calls per stage—dependency compilation followed by root +compilation with its canonical `.wwi`—and failure left the traced workdirs +empty with no `.new`, `.install`, or `.wwtxn.*` residue. Changing the warm +valid qualifier to `init` preserved the complete committed work-file manifest +and public product byte for byte in both stages, with no active transaction +residue. + +The direct pre-fix four-axis result was: + +- **Go-like build:** different diagnostic recovery and position; loading, + graph, producer order, failure, cold no-publication, warm rollback, and + cleanup were aligned; +- **Go-like test:** different for same-package and external-test sources for + the same checker reason; variant selection and final `FAIL\n` accounting + were aligned; +- **Go-like package:** different because a rejected qualifier spuriously + participated in package-declaration collision recovery; canonical package + identity was aligned; and +- **Go-like import:** the core rejection and missing-import precedence were + aligned, while binding admission, recovery exclusions, and diagnostic + position were different. + +#### Ownership and complete four-axis contract + +The semantic owners are the transient import-spec position plus checker-local +file-binding recovery: + +- Cstage: `N_USE` in `cmd/wcc/ww.h:319-375`, import parsing in + `cmd/wcc/parse.c:1321-1359`, and binding/recovery in + `cmd/wcc/check.c:2911-2972,3987-4144`; +- WWstage: the `N_USE` twin in `lib/ww/syntax/ast.ww:108-158`, import parsing + in `lib/ww/syntax/decl.ww:9-50`, and binding/recovery in + `selfhost/cmd/wcc/check.ww:178-221,418-434,7531-7675,8601-8650`. + +Both parsers retain the existing import-keyword node position for unrelated +structural diagnostics and record a transient first-spec-token position. Both +checkers identify a nonblank resolved occurrence whose effective qualifier is +exactly `init`, emit the core diagnostic in source order before qualifier-use, +duplicate, unused, collision, or installation recovery, exclude it from every +such table, and silently omit the rejected qualifier from file scope. The AST +occurrence itself remains intact. + +Declared-name binding in `cmd/w6c/main.c:315-357` and +`selfhost/cmd/w6c/main.ww:410-458`, driver graph construction in +`cmd/ww/main.c:3396-3425,3630-3689` and its self-hosted twin, and all canonical +dotted identity rules remain unchanged. + +- **Go-like build:** a resolved invalid root or dependency reaches the normal + dependency action, then the owning compiler action rejects once per + occurrence at the spec token without qualifier-recovery noise. Producer + failure still prevents new publication and preserves a prior committed + generation. +- **Go-like test:** the same checker rule applies after existing filename and + role selection to production-test, same-package-test, external-test, and + test-only sources. Failed products keep command-owned `FAIL\n` accounting and + are not installed; test runtime and process topology do not change. +- **Go-like package:** rejected qualifiers do not collide with package-scope + declarations. An imported declared name `init` remains the effective name + tested for an implicit import, but declared name, source role, package + identity, and package declaration semantics do not change. +- **Go-like import:** explicit and implicit effective-`init` bindings reject at + the alias/path spec token; every occurrence rejects; later `init.Name` is + undefined; missing resolution keeps precedence; and rejected bindings + create no unused, duplicate, or declaration-collision recovery. + +#### Loading, lifecycle, parity, and formats + +Filename/platform/test-role eligibility and selected-source UTF-8, BOM, NUL, +package-clause, and import-order validation remain earlier and unchanged. +Excluded and wrong-target files remain non-inputs. Existing dependencies load +normally; a missing dependency fails before checker binding recovery. Exact +canonical paths continue to own package nodes, sorted/deduplicated graph edges, +action keys, symbols, `.wwi` files, artifacts, publication, and invalidation. +Dependency compilation still precedes the rejecting root action; no action +ordering, scheduling, transaction, concurrency, or package-product topology +changes. + +Only transient parser/checker state changes. No valid compiler output, +assembler, archiver, linker, initialization, runtime, test-runtime, or +publication path changes. Invalid root input cannot complete its assembly, +object, archive, link, retained-test, install, or runtime phases. Dependency or +root producer failures, runtime failures, publication-only failures, and +cleanup failures keep their existing owners and classifications. + +Cold rejection creates no new public or retained root product and commits no +partial unit, `.wwi`, object, archive, executable, tool record, stamp, or +result. Warm rejection preserves the previous complete committed generation +and public product. Dependency reuse and ordinary source invalidation remain +unchanged, and a rejected generation never replaces root state. The rule adds +no process, signal, wait, timeout, cancellation, shared state, or lock, so +established request isolation, interruption rollback, descendant cleanup, and +concurrent valid/invalid request behavior remain unaffected. Failure leaves no +active `.new`, `.install`, `.wwtxn.*`, adjacent `.sepwork`, capture, scratch, +or tool-stage transaction residue; the fixed recipe-owned `out/bootstrap` tree +is not transaction residue. + +The WW-native `effective_init_imports_never_enter_binding_recovery` observer +owns explicit and implicit bindings; unused, used, repeated, +declaration-collision, selector, and missing-target recovery; valid blank and +explicit-alias controls; ordinary and imported builds; applicable +production/same/external/test-only source roles; fixed-path stage +status/stdout/stderr parity; invalid binding rejection inside a dependency +reached through an imported root; cold empty-workdir rollback; warm +preservation; valid artifact-byte parity; and residue cleanup. Concrete +post-change statuses, diagnostic bytes and hashes, artifact hashes, and ordered +full-gate results are recorded only after focused and full validation; they are +not inferred from the implementation. + +No persisted-byte contract changes. Import-spec coordinates and recovery +tables are transient compiler state. Build workdir format remains `18`, test +workdir format remains `19`, semantic storage format remains `3`, and no test +result cache is introduced. + +#### Direct post-fix proof and ordered validation + +The following results are **directly measured WW behavior**. Both rebuilt +stages reject explicit and implicit effective-`init` bindings at spec-token +column 8. Every occurrence produces one core error; rejected bindings produce +no unused, duplicate-binding, declaration/import-collision, or alternate- +location recovery. Independent invalid declarations and later undefined +selectors remain diagnosed, while missing-package resolution retains +precedence. + +A fixed-path explicit-unused probe produced identical Cstage and WWstage +results: status 1, empty stdout, and 131-byte stderr with SHA-256 +`615a06b827a41ce214b9e76049b402be94680a14ba5e3416455d7a628185c7d5`. +The valid blank-plus-explicit-alias control ran with status 41 and had identical +unit, `.wwi`, assembly, object, archive, generated-init, and executable bytes +across stages. Its 4,317-byte executable had SHA-256 +`64a48e8b18f035b8598fbe39feedc5a0e6df45d256305cb0f32e8b27a0471b92`. + +The WW-native observer passed for ordinary roots, a rejecting imported +dependency, production-test, same-package-test, external-test, and test-only +sources. It directly proved normalized diagnostic parity, cold empty-workdir +rollback and no publication, warm preservation and exact reuse of every +committed semantic file and public product, valid artifact-byte parity, and no +active transaction residue. `out/bin/test_300_check` passed 73/73 checks. + +All ordered full gates then passed serially in the required order: + +1. `make -j4 JOBS=4 test` +2. `make -j4 JOBS=4 test-commit` +3. `make -j4 JOBS=4 test-byteid` +4. `make -j1 JOBS=1 test-bootstrap` +5. `make -j1 JOBS=1 test-platform` +6. `make -j1 JOBS=1 test-all` + +The first byte-identity invocation encountered one transient `roster` timeout +under four-way load. The exact isolated row passed, and the unchanged third +gate was rerun successfully: 161 language files and 1,421 data fixtures were +byte-identical with zero pinned divergences. Bootstrap reached the ww2/ww3/ww4 +fixed point and round-tripped all five WWstage tools byte-identically; the +platform gate produced a byte-identical dynamic-link result. No production +code changed after the successful ordered sequence began. + ## 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 715925a9..c4379f75 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -328,6 +328,21 @@ ImportPath = ident { "." ident } . Neither form exposes an imported declaration as a bare `Name`; ordinary unqualified lookup remains limited to lexical, builtin, and same-package declarations. +- An import whose effective file-local qualifier is `init` is invalid. This + includes both `import init acme.codec;` and an unaliased import whose target + declares `package init;`. Each resolved occurrence is rejected at its first + import-spec token (the explicit alias when present, otherwise the first path + token) as `cannot import package as init - init must be a func`. The rejected + qualifier is never installed and does not participate in unused-import, + duplicate-binding, or declaration/import-collision recovery; consequently a + later `init.Name` independently reports an undefined `init`. Repeated invalid + occurrences each report the core error. Import resolution retains + precedence, so a missing target fails as missing without an additional + effective-`init` error. A resolved rejected occurrence remains source and + graph provenance for its exact dotted target: only its file-local qualifier + binding is absent. Canonical package, import, graph, action, artifact, + symbol, `.wwi`, publication, and persistence identity never derives from the + rejected qualifier or from the target's declared name. - `import _ acme.codec;` is a blank side-effect import. The lone `_` creates no qualifier, exposes no bare declaration, and is never diagnosed as unused. It is nevertheless a real import occurrence: resolution and all missing, diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index b99ba990..9d28a39e 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -568,6 +568,39 @@ exact-argv, command, and persistent-workdir observers, the package suite proves archive-only link argv and exact warm/rejection-state behavior without duplicating those broader mechanisms in this observer. +The focused dual-stage `effective_init_imports_never_enter_binding_recovery` +observer is +the acceptance owner for imports whose effective file-local qualifier is +`init`. Its validation matrix is required to cover an explicit alias and an +implicit qualifier obtained from `package init`; unused and used occurrences; +repeated rejected occurrences; a package-scope declaration named `init`; +later `init.Name` recovery; missing-target precedence; and valid blank and +non-`init` explicit-alias controls. It must exercise ordinary root and imported +builds plus production-test, same-package-test, external-test, and test-only +source roles where applicable. Each resolved rejected occurrence must produce +one core diagnostic at the alias/path spec token and no unused, duplicate, or +declaration-collision noise from that binding; a later selector remains an +independent undefined-name error. The resolved dotted occurrence must remain +in dependency graph and action provenance while the rejected qualifier stays +absent from file scope. + +That observer's lifecycle proof requires fixed-path Cstage/WWstage status, +stdout, and stderr parity; cold absence of public and retained products; warm +preservation of the complete committed work-file manifest and public product; +imported-dependency rejection and cold empty-workdir rollback; valid control +artifact-byte parity; and absence of active `.new`, `.install`, `.wwtxn.*`, +adjacent `.sepwork`, capture, scratch, and tool-stage transaction residue. +Producer/runtime failure, request concurrency, interruption, descendant +cleanup, and test-process topology retain their established owners because +the selected checker rejection adds no producer, runtime, process, signal, +timeout, shared-state, or publication boundary. The observer passed in both +stages, including imported-dependency rejection, all four test-source roles, +cold and warm rollback, normalized diagnostic parity, valid artifact-byte +parity, and residue checks. The complete ordered `test`, `test-commit`, +`test-byteid`, `test-bootstrap`, `test-platform`, and `test-all` gates also +passed. Build workdir format remains 18, test workdir format remains 19, +semantic storage remains 3, and no test-result cache is introduced. + The same package owner contains the focused `directory_test_execution_working_directory` observer. It constructs independent temporary directories from an unrelated caller cwd and compares diff --git a/lib/ww/syntax/ast.ww b/lib/ww/syntax/ast.ww index 536c9932..afa00298 100644 --- a/lib/ww/syntax/ast.ww +++ b/lib/ww/syntax/ast.ww @@ -130,6 +130,9 @@ export type node = struct { usesource: str, // N_USE: immutable dotted source spelling usepath: str, // N_USE: canonical vendor-expanded identity usealias: str, // N_USE: explicit file-local alias, or empty + usefile: str, // N_USE: first spec token (alias, otherwise path) + useline: i32, + usecol: i32, usepkgname: str,// N_USE: imported declared package name useblank: i32, // N_USE: `_` spelling; no source binding pkgname: str, // declared package name; independent of canonical nmod @@ -150,7 +153,7 @@ export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, packed=0, type_=nil, tsuffix="", nmod="", usesource="", usepath="", usealias="", usepkgname="", useblank=0, pkgname="", sourceid=0, used=0, initfn=0, initsynthetic=0, runtimeinit=0, initorder=0u64, linksym="", refdecl=nil, initmark=0u64, imported=0})!; + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, packed=0, type_=nil, tsuffix="", nmod="", usesource="", usepath="", usealias="", usefile="", useline=0, usecol=0, usepkgname="", useblank=0, pkgname="", sourceid=0, used=0, initfn=0, initsynthetic=0, runtimeinit=0, initorder=0u64, linksym="", refdecl=nil, initmark=0u64, imported=0})!; return n; }; diff --git a/lib/ww/syntax/decl.ww b/lib/ww/syntax/decl.ww index a2437b62..3b74672f 100644 --- a/lib/ww/syntax/decl.ww +++ b/lib/ww/syntax/decl.ww @@ -13,6 +13,12 @@ fn parseuse(p: *parser) *node = { advance(p); let n: *node = newnode(nkind.N_USE, pf, pl, pc); n.nmod = p.curmod; + // Keep the node position at the import keyword for structural diagnostics. + // The binding error belongs to the first spec token, matching Go's alias-or- + // path ImportDecl position. + n.usefile = p.curfile; + n.useline = p.curline; + n.usecol = p.curcol; let alias: str; let first: str; if (p.curkind == tkind.TK_UNDER) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 769d446b..2abf725c 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -175,6 +175,11 @@ fn declmod(file: *syntax.node, d: *syntax.node) str = { return empty; }; +fn invalidinitimport(u: *syntax.node) bool = { + return u != nil && u.kind == syntax.nkind.N_USE && u.useblank == 0 + && syntax.streq(u.str, "init"); +}; + // Map a source-file qualifier to canonical identity. Looking up the marker for // a possible DOT must not itself count as usage; only qualified resolution // marks the owning occurrence. @@ -192,6 +197,7 @@ fn findusepath(file: *syntax.node, modtag: str, source: i32, alias: str, let u: *syntax.node = file.list; for (u != nil) { if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && !invalidinitimport(u) && u.sourceid == source) { if (syntax.streq(u.str, alias)) { let um: str = declmod(file, u); @@ -425,12 +431,7 @@ fn installdecl(c: *checker, file: *syntax.node, d: *syntax.node) void = { cerr("' cannot import itself\n"); c.errs += 1i32; }; if (d.useblank != 0) { return; }; - if (syntax.streq(nm, "init")) { - importdiagprefix(d); - cerr("cannot import package as init - init must be a func\n"); - c.errs += 1; - return; - }; + if (invalidinitimport(d)) { return; }; // #30 value-before-use: a same-leaf VALUE/type decl is already // installed (source order placed `fn aa` before `import aa`). // Promote it in place with use_alias instead of installing a @@ -7527,6 +7528,20 @@ fn importdiagprefix(n: *syntax.node) void = { cerr(strconv.i32tos(n.col, strconv.base.DEC)); cerr(": error: "); }; +fn importbindingdiagprefix(n: *syntax.node) void = { + let file: str = n.usefile; + let line: i32 = n.useline; + let col: i32 = n.usecol; + if (file.len == 0) { + file = n.file; + line = n.line; + col = n.col; + }; + cerr(file); cerr(":"); + cerr(strconv.i32tos(line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(col, strconv.base.DEC)); cerr(": error: "); +}; + fn importdiagalt(n: *syntax.node, name: str) void = { cerr("\t"); cerr(n.file); cerr(":"); cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":"); @@ -7534,6 +7549,22 @@ fn importdiagalt(n: *syntax.node, name: str) void = { cerr(": other declaration of "); cerr(name); cerr("\n"); }; +// Pinned Go 1.26.5 types2 rejects an effective import binding named init and +// immediately continues before creating its PkgName. Diagnose every resolved +// occurrence at the first import-spec token, while retaining its loader-owned +// dependency edge and excluding it from binding recovery below. +fn rejectinitimports(c: *checker, file: *syntax.node) void = { + let u: *syntax.node = file.list; + for (u != nil) { + if (invalidinitimport(u)) { + importbindingdiagprefix(u); + cerr("cannot import package as init - init must be a func\n"); + c.errs += 1; + }; + u = u.next; + }; +}; + fn topdeclkind(d: *syntax.node) bool = { return d != nil && (d.kind == syntax.nkind.N_TYPEDECL || d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_FNDECL @@ -7581,10 +7612,12 @@ fn checkimportredeclarations(c: *checker, file: *syntax.node) void = { let u: *syntax.node = file.list; for (u != nil) { if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && !invalidinitimport(u) && u.imported == 0) { let v: *syntax.node = file.list; for (v != u) { if (v.kind == syntax.nkind.N_USE && v.useblank == 0 + && !invalidinitimport(v) && v.imported == 0 && v.sourceid == u.sourceid && syntax.streq(v.str, u.str)) { importdiagprefix(u); cerr(u.str); @@ -7605,6 +7638,7 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = { let u: *syntax.node = file.list; for (u != nil) { if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && !invalidinitimport(u) && u.imported == 0 && u.used == 0) { let path: str = u.usesource; if (path.len == 0) { path = u.usepath; }; @@ -7629,6 +7663,7 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = { u = file.list; for (u != nil) { if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && !invalidinitimport(u) && u.imported == 0 && syntax.streq(d.str, u.str)) { let path: str = u.usesource; @@ -8608,6 +8643,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { file.list = usenode; }; }; + rejectinitimports(c, file); rejectnonfunctionmaindecls(c, file); markimportuses(c, file); checkimportredeclarations(c, file); diff --git a/test/package/package_test.ww b/test/package/package_test.ww index a2c62c5c..e1b02719 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -18336,3 +18336,356 @@ fn runtimepath(relative: str) str = { && !directoryhasnew(root)); clean(root); }; + +// Go 1.26.5 resolves an import target, computes its effective file-local name, +// and rejects `init` before installing any binding or unused/duplicate/collision +// recovery state. The error belongs to the explicit alias token, or to the path +// token when the dependency's declared name supplies the effective binding. +@test fn effective_init_imports_never_enter_binding_recovery() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let normal: str = strings.concat(source, "/dep/normal"); + let named: str = strings.concat(source, "/dep/named"); + let badbinding: str = strings.concat(source, "/dep/badbinding"); + let imported: str = strings.concat(source, "/imported"); + let explicit: str = strings.concat(source, "/explicit"); + let implicit: str = strings.concat(source, "/implicit"); + let collision: str = strings.concat(source, "/collision"); + let missing: str = strings.concat(source, "/missing"); + let control: str = strings.concat(source, "/control"); + let warm: str = strings.concat(source, "/warm"); + let prodcase: str = strings.concat(source, "/prodcase"); + let samecase: str = strings.concat(source, "/samecase"); + let externalcase: str = strings.concat(source, "/externalcase"); + let onlycase: str = strings.concat(source, "/onlycase"); + let dirs: []str = [normal, named, badbinding, imported, explicit, implicit, + collision, missing, control, warm, prodcase, samecase, externalcase, + onlycase]; + let di: i32 = 0; + for (di < dirs.len) { mkdirall(dirs[di]); di += 1; }; + + writefile(strings.concat(normal, "/normal.ww"), strings.concat( + "package normal;\n", + "export fn value() i32 = { return 37; };\n")); + writefile(strings.concat(named, "/named.ww"), strings.concat( + "package init;\n", + "export fn value() i32 = { return 41; };\n")); + writefile(strings.concat(badbinding, "/badbinding.ww"), strings.concat( + "package badbinding;\n", + "import init dep.normal;\n", + "export fn value() i32 = { return 43; };\n")); + writefile(strings.concat(imported, "/main.ww"), strings.concat( + "package main;\n", + "import dep.badbinding;\n", + "fn main() i32 = { return badbinding.value(); };\n")); + writefile(strings.concat(explicit, "/main.ww"), strings.concat( + "package main;\n", + "import init dep.normal;\n", + "import init dep.normal;\n", + "fn main() i32 = { return init.value(); };\n")); + writefile(strings.concat(implicit, "/main.ww"), strings.concat( + "package main;\n", + "import dep.named;\n", + "fn main() i32 = { return init.value(); };\n")); + writefile(strings.concat(collision, "/main.ww"), strings.concat( + "package main;\n", + "import init dep.normal;\n", + "let init: i32 = 1;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(missing, "/main.ww"), strings.concat( + "package main;\n", + "import init dep.absent;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(control, "/main.ww"), strings.concat( + "package main;\n", + "import _ dep.named;\n", + "import stable dep.named;\n", + "fn main() i32 = { return stable.value(); };\n")); + let warmfile: str = strings.concat(warm, "/main.ww"); + let warmvalid: str = strings.concat( + "package main;\n", + "import stable dep.normal;\n", + "fn main() i32 = { return stable.value(); };\n"); + writefile(warmfile, warmvalid); + + writefile(strings.concat(prodcase, "/prod.ww"), strings.concat( + "package prodcase;\n", + "import init dep.normal;\n", + "fn value() i32 = { return 1; };\n")); + writefile(strings.concat(prodcase, "/prod_test.ww"), strings.concat( + "package prodcase;\n", + "@test fn must_not_run() void = { abort(); };\n")); + writefile(strings.concat(samecase, "/prod.ww"), + "package samecase;\nfn value() i32 = { return 1; };\n"); + writefile(strings.concat(samecase, "/same_test.ww"), strings.concat( + "package samecase;\n", + "import init dep.normal;\n", + "@test fn must_not_run() void = { abort(); };\n")); + writefile(strings.concat(externalcase, "/prod.ww"), strings.concat( + "package externalcase;\n", + "export fn value() i32 = { return 1; };\n")); + writefile(strings.concat(externalcase, "/external_test.ww"), strings.concat( + "package externalcase_test;\n", + "import init dep.normal;\n", + "@test fn must_not_run() void = { abort(); };\n")); + writefile(strings.concat(onlycase, "/only_test.ww"), strings.concat( + "package onlycase;\n", + "import init dep.normal;\n", + "@test fn must_not_run() void = { abort(); };\n")); + + let core: str = "cannot import package as init - init must be a func"; + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; + let explicitdiag: str = ""; + let implicitdiag: str = ""; + let collisiondiag: str = ""; + let missingdiag: str = ""; + let importeddiag: str = ""; + let controldata: []str = alloc([], 8u64)!; + let controlbin: str = ""; + let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a", + ".init.unit.ww", ".init.s", ".init.o"]; + let si: i32 = 0; + for (si < stages.len) { + // The same rejection owns a dependency compiler action reached through + // an imported root. Request-wide rollback leaves no cold semantic state. + let importedwork: str = strings.concat(root, "/imported-work-", tags[si]); + let importedout: str = strings.concat(root, "/imported-output-", tags[si]); + mkdirall(importedwork); + let importedav: []str = [driver(stages[si]), "build", "-w", + importedwork, "-I", source, "-o", importedout, "imported"]; + let importedresult: commandout; + runcommand(root, strings.concat("init-imported-", tags[si]), importedav, + (120i64 * (time.second: i64)): time.duration, &importedresult); + expectexit(&importedresult, 1); + assert(importedresult.stdout.len == 0 + && occurrences(importedresult.stderr, core) == 1 + && has(importedresult.stderr, + "/dep.badbinding.unit.new:3:8: error: cannot import package as init - init must be a func\n") + && !has(importedresult.stderr, "imported as init and not used") + && !os.exists(importedout) + && !os.exists(strings.concat(importedout, ".new")) + && !os.exists(strings.concat(importedout, ".sepwork")) + && directoryisempty(importedwork)); + let importednormalized: str = normalizedtrace(importedresult.stderr, + strings.concat(importedwork, "/"), importedout); + if (si == 0) { importeddiag = strings.dup(importednormalized); } + else { assert(same(importeddiag, importednormalized)); }; + + let work: str = strings.concat(root, "/reject-work-", tags[si]); + mkdirall(work); + let targets: []str = ["explicit", "implicit", "collision", "missing"]; + let outputs: []str = [strings.concat(root, "/explicit-", tags[si]), + strings.concat(root, "/implicit-", tags[si]), + strings.concat(root, "/collision-", tags[si]), + strings.concat(root, "/missing-", tags[si])]; + let ti: i32 = 0; + for (ti < targets.len) { + let av: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", outputs[ti], targets[ti]]; + let out: commandout; + runcommand(root, strings.concat("init-build-", tags[si], "-", + targets[ti]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && !os.exists(outputs[ti]) + && !os.exists(strings.concat(outputs[ti], ".new")) + && !os.exists(strings.concat(outputs[ti], ".sepwork"))); + let normalized: str = normalizedtrace(out.stderr, + strings.concat(work, "/"), outputs[ti]); + if (ti == 0) { + assert(occurrences(out.stderr, core) == 2 + && has(out.stderr, + "/explicit.unit.new:3:8: error: cannot import package as init - init must be a func\n") + && has(out.stderr, + "/explicit.unit.new:4:8: error: cannot import package as init - init must be a func\n") + && has(out.stderr, "undefined: init") + && !has(out.stderr, "imported as init and not used") + && !has(out.stderr, "init redeclared in this block") + && !has(out.stderr, + "already declared through import of package init")); + if (si == 0) { explicitdiag = strings.dup(normalized); } + else { assert(same(explicitdiag, normalized)); }; + } else if (ti == 1) { + assert(occurrences(out.stderr, core) == 1 + && has(out.stderr, + "/implicit.unit.new:3:8: error: cannot import package as init - init must be a func\n") + && has(out.stderr, "undefined: init") + && !has(out.stderr, "imported as init and not used")); + if (si == 0) { implicitdiag = strings.dup(normalized); } + else { assert(same(implicitdiag, normalized)); }; + } else if (ti == 2) { + assert(occurrences(out.stderr, core) == 1 + && has(out.stderr, + "/collision.unit.new:3:8: error: cannot import package as init - init must be a func\n") + && has(out.stderr, + "cannot declare init - must be func") + && !has(out.stderr, "imported as init and not used") + && !has(out.stderr, + "already declared through import of package init")); + if (si == 0) { collisiondiag = strings.dup(normalized); } + else { assert(same(collisiondiag, normalized)); }; + } else { + assert(occurrences(out.stderr, core) == 0 + && has(out.stderr, + ":2:1: error: cannot find package dep.absent\n")); + if (si == 0) { missingdiag = strings.dup(normalized); } + else { assert(same(missingdiag, normalized)); }; + }; + assert(directoryisempty(work)); + ti += 1; + }; + + // `_` never creates a binding, and a non-init explicit alias remains + // valid even when the loaded package declares its name as init. + let controlwork: str = strings.concat(root, "/control-work-", tags[si]); + let controlout: str = strings.concat(root, "/control-output-", tags[si]); + mkdirall(controlwork); + let controlav: []str = [driver(stages[si]), "build", "-w", controlwork, + "-I", source, "-o", controlout, "control"]; + let out: commandout; + runcommand(root, strings.concat("init-control-", tags[si]), controlav, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let runav: []str = [controlout]; + runcommand(root, strings.concat("init-control-run-", tags[si]), runav, + time.second, &out); + expectexit(&out, 41); + let fi: i32 = 0; + for (fi < suffixes.len) { + let bytes: str = readfile(strings.concat(controlwork, "/control", + suffixes[fi])); + if (si == 0) { append(controldata, strings.dup(bytes)); } + else { assert(same(controldata[fi], bytes)); }; + fi += 1; + }; + if (si == 0) { controlbin = strings.dup(readfile(controlout)); } + else { assert(same(controlbin, readfile(controlout))); }; + assert(!directoryhasnew(controlwork) + && !directoryhasfragment(controlwork, ".wwtxn.")); + si += 1; + }; + + // Production, same-package test, external-test, and test-only sources all + // use the same recovery rule before test-main construction or execution. + let testtargets: []str = ["prodcase", "samecase", "externalcase", + "onlycase"]; + let testdiags: []str = ["", "", "", ""]; + si = 0; + for (si < stages.len) { + let ti: i32 = 0; + for (ti < testtargets.len) { + let testwork: str = strings.concat(root, "/test-work-", tags[si], "-", + boundarypkgname(ti)); + let testout: str = strings.concat(root, "/test-output-", tags[si], "-", + boundarypkgname(ti)); + mkdirall(testwork); + let av: []str = [driver(stages[si]), "test", "-w", testwork, + "-I", source, "-o", testout, testtargets[ti]]; + let out: commandout; + runcommand(root, strings.concat("init-test-", tags[si], "-", + boundarypkgname(ti)), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n") + && occurrences(out.stderr, core) == 1 + && has(out.stderr, strings.concat(":8: error: ", core, "\n")) + && !has(out.stderr, "imported as init and not used") + && !has(out.stderr, "init redeclared in this block") + && !has(out.stderr, + "already declared through import of package init") + && !has(out.stdout, "must_not_run") + && !has(out.stdout, "discovered") + && !has(out.stdout, "passed") && !has(out.stdout, "failed") + && !os.exists(testout) + && !os.exists(strings.concat(testout, ".new")) + && !os.exists(strings.concat(testout, ".sepwork")) + && directoryisempty(testwork)); + let normalized: str = normalizedtrace(out.stderr, + strings.concat(testwork, "/"), testout); + if (si == 0) { testdiags[ti] = strings.dup(normalized); } + else { assert(same(testdiags[ti], normalized)); }; + ti += 1; + }; + si += 1; + }; + + // A warm semantic rejection preserves every committed action and public + // byte; exact source restoration returns to the same persisted generation. + let warmpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c", "/.wwtool.w6a", + "/.wwtool.stamp", "/dep.normal.unit.ww", "/dep.normal.wwi", + "/dep.normal.s", "/dep.normal.o", "/dep.normal.a", "/warm.unit.ww", + "/warm.wwi", "/warm.s", "/warm.o", "/warm.a", + "/warm.init.unit.ww", "/warm.init.s", "/warm.init.o"]; + let warmcross: []str = alloc([], warmpaths.len: u64)!; + let warmbincross: str = ""; + si = 0; + for (si < stages.len) { + let work: str = strings.concat(root, "/warm-work-", tags[si]); + let output: str = strings.concat(root, "/warm-output-", tags[si]); + mkdirall(work); + let av: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", output, "warm"]; + let out: commandout; + runcommand(root, strings.concat("init-warm-cold-", tags[si]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let snapshots: []str = alloc([], warmpaths.len: u64)!; + let pi: i32 = 0; + for (pi < warmpaths.len) { + let bytes: str = strings.dup(readfile(strings.concat(work, + warmpaths[pi]))); + append(snapshots, bytes); + if (si == 0) { append(warmcross, strings.dup(bytes)); } + else if (pi >= 4) { assert(same(warmcross[pi], bytes)); }; + pi += 1; + }; + let binbytes: str = strings.dup(readfile(output)); + if (si == 0) { warmbincross = strings.dup(binbytes); } + else { assert(same(warmbincross, binbytes)); }; + + rewritefile(warmfile, strings.concat( + "package main;\n", + "import init dep.normal;\n", + "fn main() i32 = { return 0; };\n")); + runcommand(root, strings.concat("init-warm-reject-", tags[si]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && occurrences(out.stderr, core) == 1 + && has(out.stderr, + "/warm.unit.new:3:8: error: cannot import package as init - init must be a func\n") + && !has(out.stderr, "imported as init and not used") + && same(binbytes, readfile(output)) + && !os.exists(strings.concat(output, ".new")) + && !os.exists(strings.concat(output, ".sepwork"))); + pi = 0; + for (pi < warmpaths.len) { + assert(same(snapshots[pi], readfile(strings.concat(work, + warmpaths[pi])))); + pi += 1; + }; + assert(!directoryhasnew(work) + && !directoryhasfragment(work, ".wwtxn.") + && !directoryhasfragment(work, ".install")); + + rewritefile(warmfile, warmvalid); + runcommand(root, strings.concat("init-warm-restored-", tags[si]), av, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(binbytes, readfile(output))); + pi = 0; + for (pi < warmpaths.len) { + assert(same(snapshots[pi], readfile(strings.concat(work, + warmpaths[pi])))); + pi += 1; + }; + si += 1; + }; + + assert(!directoryhasnew(root) + && !directoryhasfragment(root, ".wwtxn.") + && !directoryhasfragment(root, ".install")); + clean(root); +};