diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 70de3e32..85439beb 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1361,6 +1361,10 @@ parseimports(Parser *p) Node *head = NULL, *tail = NULL; Node *packages = NULL, *packagetail = NULL; int sawpackage = 0; + /* Go source files admit one import section before ordinary declarations. + * Keep recovery permissive, but diagnose the first import in each later + * section. Compiler-owned bundle markers begin a fresh source section. */ + int previmport = 1; while (p->cur.kind != TK_EOF) { /* Compiler/driver bundle markers carry package identity out of @@ -1368,6 +1372,7 @@ parseimports(Parser *p) * the following package clause and imports. */ if (p->cur.kind == TK_MODPATH) { sawpackage = 0; + previmport = 1; p->sourceid++; p->pathmod = p->cur.text; p->curmod = p->cur.text; @@ -1379,6 +1384,7 @@ parseimports(Parser *p) if (p->cur.kind == TK_MODRESET) { const char *rp = p->cur.text; sawpackage = 0; + previmport = 1; p->sourceid++; advance(p); p->pathmod = NULL; @@ -1389,6 +1395,7 @@ parseimports(Parser *p) } if (p->cur.kind == TK_MODULE) { Pos pp = p->cur.pos; + previmport = 1; advance(p); if (p->cur.kind != TK_IDENT) { errorf(p->cur.pos, "invalid or missing package clause"); @@ -1426,6 +1433,12 @@ parseimports(Parser *p) sawpackage = 1; } if (p->cur.kind == TK_USE) { + if (!previmport) { + errorf(p->cur.pos, + "imports must appear before other declarations"); + p->errs++; + } + previmport = 1; Node *d = parseuse(p); d->module = p->curmod; d->pkgname = p->curpkg; @@ -1437,6 +1450,7 @@ parseimports(Parser *p) tail = d; continue; } + previmport = 0; if (p->cur.kind == TK_AT) { skipimportattrs(p); if (p->cur.kind == TK_EXPORT) advance(p); @@ -1574,6 +1588,9 @@ parsefile(Parser *p) Node *head = NULL, *tail = NULL; Node *packages = NULL, *packagetail = NULL; int sawpackage = 0; + /* Semantic twin of parseimports: full/direct parsing reports the same + * per-source import-section ordering error while retaining the AST. */ + int previmport = 1; while (p->cur.kind != TK_EOF) { /* `package foo;` — directory-as-module declaration. Every * primary section opens with one (`package main;` for an @@ -1584,6 +1601,7 @@ parsefile(Parser *p) if (p->cur.kind == TK_MODULE) { Pos packagepos = p->cur.pos; sawpackage = 1; + previmport = 1; advance(p); const char *name = expectident(p); expect(p, TK_SEMI); @@ -1611,6 +1629,7 @@ parsefile(Parser *p) * root-only bare-`main` rule, #32). */ if (p->cur.kind == TK_MODPATH) { sawpackage = 0; + previmport = 1; p->sourceid++; p->pathmod = p->cur.text; p->curmod = p->cur.text; @@ -1630,6 +1649,7 @@ parsefile(Parser *p) * decls to bare — that usage is deliberate-only. */ if (p->cur.kind == TK_MODRESET) { sawpackage = 0; + previmport = 1; p->sourceid++; /* #57: a path-carrying reset (sep primary body) mangles * decls on the dotted path so definer == importer, but @@ -1668,6 +1688,13 @@ parsefile(Parser *p) p->errs++; sawpackage = 1; } + int thisimport = p->cur.kind == TK_USE; + if (thisimport && !previmport) { + errorf(p->cur.pos, + "imports must appear before other declarations"); + p->errs++; + } + previmport = thisimport; Node *attrs = parseattrs(p); int exp = accept(p, TK_EXPORT); Node *d = NULL; diff --git a/docs/build-system.md b/docs/build-system.md index df0c1fe9..53336b38 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -8246,6 +8246,187 @@ this presentation rule does not change. No persisted-byte contract changed. Build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. +### 11.41 Implemented source-file import-section ordering + +Every eligible WW source now has one contiguous import section immediately +after its package clause. Once an ordinary top-level declaration begins, the +first `import` in a later section is rejected as +`imports must appear before other declarations`. The parser continues for +recovery: consecutive imports in that late section do not repeat the ordering +diagnostic, while another ordinary declaration followed by another import +starts a separately diagnosed late section. + +This is a source-file syntax rule, not a new import form. Existing unquoted +dotted default, explicit-alias, and blank imports are unchanged. Existing +aggregate module/reset boundaries and each constituent package clause begin a +new source section. The boundary bookkeeping remains parser metadata rather +than package, import, graph, action, artifact, symbol, `.wwi`, publication, or +persistence identity. + +#### Pinned Go evidence and fact classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `syntax.(*parser).fileOrNil` states the source-file grammar as a package + clause, zero or more imports, and then zero or more top-level declarations. + Its recovery loop accepts a later import only to continue parsing and emits + exactly `imports must appear before other declarations` when the preceding + declaration was not an import + ([`cmd/compile/internal/syntax/parser.go`, lines 397–428](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/parser.go#L397-L428)). +- The public parser first consumes the initial import section, then applies the + same predecessor check while parsing the rest of the file + ([`go/parser/parser.go`, method `(*parser).parseFile`, lines 2887–2923](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/parser/parser.go#L2887-L2923)). +- Official types testdata requires one diagnostic for a late import followed by + contiguous imports, then another diagnostic when an ordinary declaration + separates a second late section + ([`internal/types/testdata/fixedbugs/issue43190.go`, lines 5–30](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/fixedbugs/issue43190.go#L5-L30)). + +Those grammar branches, diagnostic text, error-recovery behavior, and testdata +assertions are **behavior directly implemented or asserted by pinned Go**. +That the state belongs to one source parser, resets at WW's existing aggregate +source boundaries, and must reject before import-graph construction is +**behavior derived from the pinned implementation**. + +The rule honestly applies to WW's model because it orders declaration classes +WW already implements. It requires no quoted, grouped, dot, or generalized +import syntax; module or manifest identity; registry, lock, cache, database, +CAS, or network resolution; or source-level build expression. + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit examined all four permanent axes against the pinned checkout +before selecting this import difference: + +- **Go-like build:** pinned linker method + `(*ErrorReporter).errorUnresolved` gives unresolved `main.main` a dedicated + error + ([`cmd/link/internal/ld/errors.go`, lines 29–67](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L29-L67)), + asserted by `TestUndefinedRelocErrors` and its source fixture + ([`cmd/link/internal/ld/ld_test.go`, lines 19–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L19-L45), + [`testdata/issue10978/main.go`, lines 5–27](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/testdata/issue10978/main.go#L5-L27)). + Both WW stages rejected a selected declared-`main` package without an entry, + using empty stdout and the same linker/driver diagnostics. This applicable + control was aligned. +- **Go-like test:** pinned `testFlags` explicitly permits known test flags + before and after the package list and implements the transition between + package operands and flags + ([`cmd/go/internal/test/testflag.go`, lines 219–345](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/testflag.go#L219-L345)); + official `test_flag.txt` asserts both placements + ([lines 1–4](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_flag.txt#L1-L4)). + In both WW stages, `-run selected` before or after a directory operand ran + exactly the same one of two registered tests and produced identical output. + This applicable control was aligned for WW's supported option set. +- **Go-like package:** pinned `MultiplePackageError` and directory scanning + reject two eligible declarations with different package names + ([`go/build/build.go`, lines 538–549](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L549) + and [lines 939–967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L939-L967)), + asserted by `TestMultiplePackageImport` + ([`go/build/build_test.go`, lines 105–133](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L133)). + Both WW stages rejected a `first`/`second` directory before tools with the + identical positioned diagnostic. This applicable control was aligned. +- **Go-like import:** a command source declared a helper, then imported + `audit.dep`, then used that package from `main`. Both WW stages exited 0, + emitted empty build output, produced byte-identical executables (SHA-256 + `f28892147ab0ae283dff5ceea7114dbc81142ed48cb9088ee7fb8294a5ce44cd`), + and those executables exited 42. Corresponding owner unit, interface, + assembly, object, archive, and initializer bytes were stage-identical. A + same-package test source with the same late-import shape ran successfully in + both stages with identical 251-byte stdout (SHA-256 + `3c42fa9840f485fb21b5b5318a13b89abfe29e94b530db00779262368f4fbeba`) + and empty stderr. This acceptance was the selected difference. + +The WW statuses, streams, runtime exits, and artifact hashes are **directly +measured WW behavior**. The cited implementation and testdata facts are +**behavior directly implemented or asserted by pinned Go**. Applying their +per-file ordering state to WW's existing dotted declarations is **behavior +derived from the pinned implementation**. + +#### Ownership and final four-axis behavior + +`parseimports` and `parsefile` in `cmd/wcc/parse.c`, with their semantic twins +in `lib/ww/syntax/parse.ww`, are the only production owners. Each keeps one +parser-local `previmport` bit. A normal import following a non-import reports +the pinned diagnostic, then sets the bit so adjacent imports remain one +recovery section. Any ordinary declaration clears it. The existing +module-path, module-reset, and package-clause boundaries set it for a new +source section. + +The imports-only pass is used by public driver loading and therefore rejects a +selected or imported late source before graph and producer construction. The +full parser independently gives direct `w6c`/`w6c_ww` input and aggregate units +the same rule. The only tracked compatibility fixture that deliberately put a +declaration before its import was reordered; it still proves file-scoped import +binding and declaration installation order with byte-identical Cstage/WWstage +artifacts, without asserting the rejected syntax. + +- **Go-like build:** selected and imported late sources now fail during parser + loading, before compiler, assembler, archiver, linker, output planning side + effects, or runtime. Missing-target resolution does not replace the earlier + syntax error. Valid import-first commands still build, publish, and run. +- **Go-like test:** late imports in production, same-package test, + external-test, and test-only sources fail before variant actions, generated + main, test binary, runtime, accounting, or retained publication. The + directory command emits its existing attributable final `FAIL\n`. A valid + import-first test retains and runs normally. +- **Go-like package:** source eligibility and package-clause classification + remain earlier owners. Wrong-platform sources produce no ordering error; + selected package-name conflicts retain their coordinator diagnostic. + Declared names and command/test family classification are unchanged. +- **Go-like import:** a file can no longer introduce a qualifier or side-effect + edge after ordinary declarations. Valid imports retain their exact source + spelling, declared-name qualifier, file scope, contextual/vendor resolution, + canonical identity, visibility checks, cycle checks, and initialization + edges. + +Direct post-fix `w6c` and `w6c_ww`, and public `ww build`/`ww_ww build`, reject +the measured source with empty stdout and byte-identical 149-byte stderr +(SHA-256 +`791ac87ab0aa2c91228f863ae80a8815aa83edf78c4996c5f11191193e3e4240`). +The diagnostic points to the late import at line 7, column 1. Directory tests +emit byte-identical `FAIL\n` stdout (SHA-256 +`4f8e9e45f8a9e1843b81eaf3bdf52a6b778d415d23bf985774a9d34a43f69bd5`) +and byte-identical 314-byte stderr (SHA-256 +`82ec52b26eaff053f475ce0773b7aee902e734cd87dc100848aee3772063f5b1`), +with no test body or accounting. A direct three-import recovery probe emits +exactly two stage-identical ordering diagnostics: one for the first of two +contiguous late imports and one after the intervening declaration. + +Loading and fixed-target filename selection otherwise do not change. An +excluded `_windows.ww` or `_windows_test.ww` file contributes no parse, +package, import, graph, action, artifact, diagnostic, or invalidation state. +For valid files, graph nodes, action dependencies and scheduling, compiler and +linker arguments, initialization, runtime behavior, result ordering, and +publication remain unchanged. A package canonically named `domain.dep` may +still declare `renamed`; its importer uses `renamed.Name`, and its unit/export +remain owned by `domain.dep`. + +Cold rejection creates no work artifact, output, capture, or adjacent scratch. +A warm source reordered into the invalid form preserves the complete committed +unit/interface/assembly/object/archive/initializer generation, tool vouchers, +stamp, and public executable byte for byte. Exact restoration reuses the +committed producers and republishes the same executable. Because rejection +occurs before a producer or test child, producer failure, runtime failure, +signals, timeout, interruption, and process-group cleanup acquire no new path; +their existing owners remain authoritative. Concurrent valid and invalid +requests use independent parser state, workdirs, captures, and outputs. No +`.new`, `.install`, `.wwtxn.*`, cold scratch, test process, or capture residue +survives the observed failure boundaries. + +The WW-native `imports_precede_other_top_level_declarations` observer proves +direct compiler parity, exact recovery-section counts, selected and imported +build rejection, syntax-before-resolution precedence, all directory test +source variants, wrong-platform exclusion, valid runtime behavior, declared +name versus canonical identity, cold/warm persistence and rollback, restored +reuse, concurrent isolation, diagnostic equality, retained executable equality, +and intermediate artifact-byte equality. The C parser unit and the existing +`sepimport` observer separately pin the imports-only AST recovery and valid +file-scoped binding regression. + +Rejected source creates no persisted byte contract, while valid source bytes +are unchanged. Build workdir format remains `18`, test workdir format remains +`19`, and semantic storage format remains `3`. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 85953152..91289bac 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -257,6 +257,17 @@ ImportPath = ident { "." ident } . use the related `p_test`, and the actions remain separate even though one canonical directory owns their test product. The declared name need not equal the directory name or the final component of its canonical import identity. +- Each source file has one contiguous import section immediately after its + package clause. Once a non-import top-level declaration begins, a later + `import` is rejected as `imports must appear before other declarations`. + Parsing continues for recovery: consecutive imports in that late section + produce one ordering diagnostic, while another ordinary declaration followed + by another import starts a separately diagnosed late section. Existing + aggregate module/reset boundaries and constituent package clauses reset this + parser state per source; they do not relax the rule within a source or become + package/import identity. + A filename excluded by the target-selection rule below reaches no parser and + therefore cannot contribute an ordering diagnostic or import edge. - Directory source eligibility uses Go 1.26.5 filename suffix semantics for WW's fixed `linux/amd64` target. In the basename stem before the first dot, a final `_test` token is ignored for platform matching. A final known OS or diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 62bb4054..319176ea 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -402,6 +402,19 @@ 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. +Within every eligible production, same-package test, external-test, or +test-only source, the package clause is followed by one contiguous import +section and then ordinary top-level declarations. A later import is a parser +error, `imports must appear before other declarations`. The imports-only load +pass diagnoses the first import in each separated late section before graph or +producer construction; the full parser gives direct compiler input the same +result. Consequently an invalid test source creates no variant, generated main, +test binary, runtime process, accounting, retained output, or new persistent +generation. The directory command retains its existing attributable final +`FAIL\n`. Wrong-target files remain excluded before this rule and contribute no +diagnostic. This ordering state is per source and never package, import, +variant, action, artifact, publication, or persistence identity. + 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 diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index f57a8a88..cde23a2a 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -497,12 +497,16 @@ export fn parseimports(p: *parser) *node = { let packages: *node = nil; let packagetail: *node = nil; let sawpackage: bool = false; + // One import section precedes ordinary declarations. Keep parsing for + // recovery, diagnosing the first import in each later source section. + let previmport: bool = true; for (p.curkind != tkind.TK_EOF) { // Compiler/driver bundle markers carry package identity out of // band. They are not source declarations, so keep scanning for // the following package clause and imports. if (p.curkind == tkind.TK_MODPATH) { sawpackage = false; + previmport = true; p.sourceid += 1; p.pathmod = p.curtext; p.curmod = p.curtext; @@ -514,6 +518,7 @@ export fn parseimports(p: *parser) *node = { if (p.curkind == tkind.TK_MODRESET) { let rp: str = p.curtext; sawpackage = false; + previmport = true; p.sourceid += 1; advance(p); p.pathmod = ""; @@ -526,6 +531,7 @@ export fn parseimports(p: *parser) *node = { let pf: str = p.curfile; let pl: i32 = p.curline; let pc: i32 = p.curcol; + previmport = true; advance(p); if (p.curkind != tkind.TK_IDENT) { errmsg(p, "invalid or missing package clause"); @@ -563,6 +569,10 @@ export fn parseimports(p: *parser) *node = { sawpackage = true; }; if (p.curkind == tkind.TK_USE) { + if (!previmport) { + errmsg(p, "imports must appear before other declarations"); + }; + previmport = true; let d: *node = parseuse(p); d.nmod = p.curmod; d.pkgname = p.curpkg; @@ -571,6 +581,7 @@ export fn parseimports(p: *parser) *node = { tail = d; continue; }; + previmport = false; if (p.curkind == tkind.TK_AT) { skipimportattrs(p); if (p.curkind == tkind.TK_EXPORT) { advance(p); }; @@ -618,6 +629,8 @@ export fn parsefile(p: *parser) *node = { let packages: *node = nil; let packagetail: *node = nil; let sawpackage: i32 = 0; + // Full/direct parsing is the semantic twin of the imports-only pass. + let previmport: bool = true; for (p.curkind != tkind.TK_EOF) { // `package foo;` — directory-as-module declaration. Every // primary section opens with one (`package main;` for an @@ -630,6 +643,7 @@ export fn parsefile(p: *parser) *node = { let pl: i32 = p.curline; let pc: i32 = p.curcol; sawpackage = 1; + previmport = true; advance(p); let name: str; expectident(p, &name); @@ -658,6 +672,7 @@ export fn parsefile(p: *parser) *node = { // root-only bare-`main` rule, #32). if (p.curkind == tkind.TK_MODPATH) { sawpackage = 0; + previmport = true; p.sourceid += 1; p.pathmod = p.curtext; p.curmod = p.curtext; @@ -677,6 +692,7 @@ export fn parsefile(p: *parser) *node = { // decls to bare — that usage is deliberate-only. if (p.curkind == tkind.TK_MODRESET) { sawpackage = 0; + previmport = true; p.sourceid += 1; // #57: a path-carrying reset (sep primary body) mangles decls // on the dotted path so definer == importer, but leaves @@ -714,6 +730,11 @@ export fn parsefile(p: *parser) *node = { errmsg(p, "missing package clause"); sawpackage = 1; }; + let thisimport: bool = p.curkind == tkind.TK_USE; + if (thisimport && !previmport) { + errmsg(p, "imports must appear before other declarations"); + }; + previmport = thisimport; let attrs = parseattrs(p); let exported: i32 = 0; if (p.curkind == tkind.TK_EXPORT) { exported = 1; advance(p); }; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 66b1ef5f..20319e5e 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -854,6 +854,452 @@ fn cwdwritedata(dir: str, label: str) void = { clean(root); }; +// A source file has one contiguous import section immediately after its +// package clause. The parser may continue after a late import for recovery, +// but the imports-only loader and the full compiler parser both reject it. +// This observer keeps that source rule separate from dotted import identity. +@test fn imports_precede_other_top_level_declarations() void = { + let root: str = fresh(); + let tree: str = strings.concat(root, "/tree"); + let base: str = strings.concat(tree, "/domain/base"); + let dep: str = strings.concat(tree, "/domain/dep"); + let app: str = strings.concat(tree, "/domain/app"); + let missinglate: str = strings.concat(tree, "/domain/missinglate"); + let prodtest: str = strings.concat(tree, "/domain/prodtest"); + let internaltest: str = strings.concat(tree, "/domain/internaltest"); + let externaltest: str = strings.concat(tree, "/domain/externaltest"); + let testonly: str = strings.concat(tree, "/domain/testonly"); + let validtest: str = strings.concat(tree, "/domain/validtest"); + mkdirall(base); mkdirall(dep); mkdirall(app); mkdirall(missinglate); + mkdirall(prodtest); mkdirall(internaltest); mkdirall(externaltest); + mkdirall(testonly); mkdirall(validtest); + + writefile(strings.concat(base, "/base.ww"), + "package base;\nexport fn value() i32 = { return 40; };\n"); + let depfile: str = strings.concat(dep, "/dep.ww"); + let depvalid: str = strings.concat( + "package renamed;\nimport domain.base;\n", + "fn delta() i32 = { return 1; };\n", + "export fn value() i32 = { return base.value() + delta(); };\n"); + let deplate: str = strings.concat( + "package renamed;\nfn delta() i32 = { return 1; };\n", + "import domain.base;\n", + "export fn value() i32 = { return base.value() + delta(); };\n"); + writefile(depfile, depvalid); + let appfile: str = strings.concat(app, "/main.ww"); + let appvalid: str = strings.concat( + "package main;\nimport domain.dep;\n", + "fn local() i32 = { return 1; };\n", + "fn main() i32 = { return renamed.value() + local(); };\n"); + let applate: str = strings.concat( + "package main;\nfn local() i32 = { return 1; };\n", + "import domain.dep;\n", + "fn main() i32 = { return renamed.value() + local(); };\n"); + writefile(appfile, appvalid); + // Platform exclusion precedes package/import parsing. Neither the wrong + // declared name nor this late missing import may affect linux/amd64. + writefile(strings.concat(app, "/ignored_windows.ww"), strings.concat( + "package wrong;\nfn ignored() void = {};\n", + "import missing.platform;\n")); + writefile(strings.concat(missinglate, "/main.ww"), strings.concat( + "package main;\nfn before() i32 = { return 0; };\n", + "import missing.pkg;\nfn main() i32 = { return before(); };\n")); + + // Production, same-package test, external-test, and test-only sources all + // pass through the same per-source ordering rule before product actions. + writefile(strings.concat(prodtest, "/pkg.ww"), strings.concat( + "package prodtest;\nfn local() i32 = { return 1; };\n", + "import domain.dep;\n", + "export fn value() i32 = { return local() + renamed.value(); };\n")); + writefile(strings.concat(prodtest, "/pkg_test.ww"), + "package prodtest;\n@test fn must_not_run() void = { abort(\"ran\"); };\n"); + writefile(strings.concat(internaltest, "/pkg.ww"), + "package internaltest;\nfn local() i32 = { return 1; };\n"); + writefile(strings.concat(internaltest, "/pkg_test.ww"), strings.concat( + "package internaltest;\nfn testlocal() i32 = { return 1; };\n", + "import domain.dep;\n", + "@test fn must_not_run() void = { abort(\"ran\"); };\n")); + writefile(strings.concat(externaltest, "/pkg.ww"), + "package externaltest;\nexport fn local() i32 = { return 1; };\n"); + writefile(strings.concat(externaltest, "/pkg_test.ww"), strings.concat( + "package externaltest_test;\nfn testlocal() i32 = { return 1; };\n", + "import domain.dep;\n", + "@test fn must_not_run() void = { abort(\"ran\"); };\n")); + writefile(strings.concat(testonly, "/only_test.ww"), strings.concat( + "package testonly;\nfn testlocal() i32 = { return 1; };\n", + "import domain.dep;\n", + "@test fn must_not_run() void = { abort(\"ran\"); };\n")); + + writefile(strings.concat(validtest, "/pkg.ww"), strings.concat( + "package validtest;\nimport domain.dep;\n", + "fn local() i32 = { return renamed.value(); };\n")); + writefile(strings.concat(validtest, "/pkg_test.ww"), strings.concat( + "package validtest;\nimport domain.base;\n", + "@test fn import_first_runs() void = {", + " assert(local() + base.value() == 81); };\n")); + writefile(strings.concat(validtest, "/ignored_windows_test.ww"), + strings.concat("package wrong;\nfn ignored() void = {};\n", + "import missing.platform;\n")); + + let expected: str = "imports must appear before other declarations"; + let sections: str = strings.concat(root, "/sections.ww"); + writefile(sections, strings.concat( + "package sections;\nfn first() void = {};\n", + "import one;\nimport two;\n", + "fn second() void = {};\nimport three;\n")); + let compilers: []str = ["w6c", "w6c_ww"]; + let directdiag: str = ""; + let out: commandout; + let i: i32 = 0; + for (i < compilers.len) { + let asmout: str = strings.concat(root, "/direct-", compilers[i], ".s"); + let av: []str = [driver(compilers[i]), "-o", asmout, sections]; + runcommand(root, strings.concat("import-order-direct-", compilers[i]), + av, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && !os.exists(asmout)); + assert(occurrences(out.stderr, expected) == 2); + assert(same(primarydiagnostic(out.stderr), expected)); + if (i == 0) { directdiag = strings.dup(out.stderr); } + else { assert(same(directdiag, out.stderr)); }; + i += 1; + }; + + let compilerwrapper: str = strings.concat(root, "/import-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/import-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/import-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'compile\\n' >> \"$WW_IMPORT_CTRACE\"\n", + "exec \"$WW_IMPORT_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_IMPORT_ATRACE\"\n", + "exec \"$WW_IMPORT_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link\\n' >> \"$WW_IMPORT_LTRACE\"\n", + "exec \"$WW_IMPORT_REAL_L\" \"$@\"\n")); + + let stages: []str = ["ww", "ww_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let works: []str = [strings.concat(root, "/work-c"), + strings.concat(root, "/work-ww")]; + let outputs: []str = [strings.concat(root, "/app-c"), + strings.concat(root, "/app-ww")]; + let testworks: []str = [strings.concat(root, "/test-work-c"), + strings.concat(root, "/test-work-ww")]; + let testbins: []str = [strings.concat(root, "/valid-c.test"), + strings.concat(root, "/valid-ww.test")]; + let baseenv: []str = os.getenvs(); + let appdiag: str = ""; + let depdiag: str = ""; + let missingdiag: str = ""; + let validtestout: str = ""; + let testdiags: []str = ["", "", "", ""]; + let artifactrefs: []str = alloc([], 64u64)!; + let testartifactrefs: []str = alloc([], 64u64)!; + let appbinref: str = ""; + let testbinref: str = ""; + let actions: []str = ["domain.base", "domain.dep", "domain.app"]; + let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a"]; + let initsuffixes: []str = [".init.unit.ww", ".init.s", ".init.o"]; + let testactions: []str = ["domain.base", "domain.dep", + "domain.validtest-internal-test", "domain.validtest-test-main"]; + let testtargets: []str = ["domain.prodtest", "domain.internaltest", + "domain.externaltest", "domain.testonly"]; + i = 0; + for (i < stages.len) { + mkdirall(works[i]); mkdirall(testworks[i]); + let ctrace: str = strings.concat(root, "/compile-", tags[i]); + let atrace: str = strings.concat(root, "/assemble-", tags[i]); + let ltrace: str = strings.concat(root, "/link-", tags[i]); + writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); + let env: []str = alloc([], (baseenv.len + 10): 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_IMPORT_")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_W6A=", assemblerwrapper)); + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_IMPORT_CTRACE=", ctrace)); + append(env, strings.concat("WW_IMPORT_ATRACE=", atrace)); + append(env, strings.concat("WW_IMPORT_LTRACE=", ltrace)); + append(env, strings.concat("WW_IMPORT_REAL_C=", driver(compilers[i]))); + append(env, strings.concat("WW_IMPORT_REAL_A=", driver(assemblers[i]))); + append(env, strings.concat("WW_IMPORT_REAL_L=", driver(linkers[i]))); + + let buildav: []str = [driver(stages[i]), "build", "-I", tree, + "-w", works[i], "-o", outputs[i], "domain.app"]; + runcommandenv(root, strings.concat("import-order-cold-", tags[i]), + buildav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0); + let built: str = strings.dup(readfile(outputs[i])); + if (i == 0) { appbinref = strings.dup(built); } + else { assert(same(appbinref, built)); }; + let runav: []str = [outputs[i]]; + runcommand(root, strings.concat("import-order-run-", tags[i]), runav, + time.second, &out); + expectexit(&out, 42); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(has(readfile(strings.concat(works[i], + "/domain.dep.unit.ww")), + "//ww:module-reset domain.dep\npackage renamed;")); + + // Save and compare every valid semantic output affected by this graph. + let before: []str = alloc([], 32u64)!; + let refi: i32 = 0; + let ai: i32 = 0; + for (ai < actions.len) { + let si: i32 = 0; + for (si < suffixes.len) { + let bytes: str = strings.dup(readfile(strings.concat(works[i], + "/", actions[ai], suffixes[si]))); + append(before, bytes); + if (i == 0) { append(artifactrefs, strings.dup(bytes)); } + else { assert(same(artifactrefs[refi], bytes)); }; + refi += 1; si += 1; + }; + ai += 1; + }; + let ii: i32 = 0; + for (ii < initsuffixes.len) { + let bytes: str = strings.dup(readfile(strings.concat(works[i], + "/domain.app", initsuffixes[ii]))); + append(before, bytes); + if (i == 0) { append(artifactrefs, strings.dup(bytes)); } + else { assert(same(artifactrefs[refi], bytes)); }; + refi += 1; ii += 1; + }; + + // A selected source becoming late rejects before every producer and + // preserves the complete committed generation and public executable. + rewritefile(appfile, applate); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("import-order-app-late-", tags[i]), + buildav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(primarydiagnostic(out.stderr), expected)); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && same(built, readfile(outputs[i]))); + if (i == 0) { appdiag = strings.dup(out.stderr); } + else { assert(same(appdiag, out.stderr)); }; + refi = 0; ai = 0; + for (ai < actions.len) { + let si: i32 = 0; + for (si < suffixes.len) { + assert(same(before[refi], readfile(strings.concat(works[i], + "/", actions[ai], suffixes[si])))); + refi += 1; si += 1; + }; + ai += 1; + }; + ii = 0; + for (ii < initsuffixes.len) { + assert(same(before[refi], readfile(strings.concat(works[i], + "/domain.app", initsuffixes[ii])))); + refi += 1; ii += 1; + }; + assert(!directoryhasnew(works[i]) + && !os.exists(strings.concat(outputs[i], ".new")) + && !os.exists(strings.concat(outputs[i], ".install")) + && !directoryhasfragment(works[i], ".wwtxn.")); + + // Exact restoration reuses the committed producer bytes. + rewritefile(appfile, appvalid); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("import-order-app-restored-", tags[i]), + buildav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && same(built, readfile(outputs[i]))); + + // The same rule applies while loading an imported package; the root's + // prior bytes and all graph identities remain unchanged. + rewritefile(depfile, deplate); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("import-order-dep-late-", tags[i]), + buildav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(primarydiagnostic(out.stderr), expected) + && has(out.stderr, "/domain/dep/dep.ww:")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && same(built, readfile(outputs[i]))); + if (i == 0) { depdiag = strings.dup(out.stderr); } + else { assert(same(depdiag, out.stderr)); }; + rewritefile(depfile, depvalid); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("import-order-dep-restored-", tags[i]), + buildav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && same(built, readfile(outputs[i]))); + + // Syntax ordering precedes import resolution, output creation, and cold + // scratch even when the late import target does not exist. + let misswork: str = strings.concat(root, "/missing-work-", tags[i]); + let missout: str = strings.concat(root, "/missing-output-", tags[i]); + mkdirall(misswork); + let missav: []str = [driver(stages[i]), "build", "-I", tree, + "-w", misswork, "-o", missout, "domain.missinglate"]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("import-order-missing-", tags[i]), + missav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(primarydiagnostic(out.stderr), expected) + && !has(out.stderr, "cannot find package")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && directoryisempty(misswork) + && !os.exists(missout) && !os.exists(strings.concat(missout, ".new")) + && !os.exists(strings.concat(missout, ".sepwork"))); + if (i == 0) { missingdiag = strings.dup(out.stderr); } + else { assert(same(missingdiag, out.stderr)); }; + + let ti: i32 = 0; + for (ti < testtargets.len) { + let twork: str = strings.concat(root, "/invalid-test-work-", + tags[i], "-", boundarypkgname(ti)); + let tbin: str = strings.concat(root, "/invalid-test-bin-", + tags[i], "-", boundarypkgname(ti)); + mkdirall(twork); + let tav: []str = [driver(stages[i]), "test", "-I", tree, + "-w", twork, "-o", tbin, testtargets[ti]]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("import-order-test-", tags[i], + "-", boundarypkgname(ti)), tav, env, + (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n") + && same(primarydiagnostic(out.stderr), expected)); + assert(!has(out.stdout, "must_not_run") + && !has(out.stdout, " discovered,") + && !has(out.stderr, "ok ")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && directoryisempty(twork)); + assert(!os.exists(tbin) && !os.exists(strings.concat(tbin, ".new")) + && !os.exists(strings.concat(tbin, ".install")) + && !os.exists(strings.concat(tbin, ".sepwork"))); + if (i == 0) { testdiags[ti] = strings.dup(out.stderr); } + else { assert(same(testdiags[ti], out.stderr)); }; + ti += 1; + }; + + // A valid import-first test still compiles to the same retained binary + // and runs with the same bytes on both stages. Its excluded wrong-target + // late import remains completely absent from loading. + let validav: []str = [driver(stages[i]), "test", "-c", "-I", tree, + "-w", testworks[i], "-o", testbins[i], "domain.validtest"]; + runcommandenv(root, strings.concat("import-order-valid-test-", tags[i]), + validav, env, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + let testbytes: str = strings.dup(readfile(testbins[i])); + if (i == 0) { testbinref = strings.dup(testbytes); } + else { assert(same(testbinref, testbytes)); }; + let testrunav: []str = [testbins[i]]; + runcommand(root, strings.concat("import-order-valid-test-run-", tags[i]), + testrunav, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 + && has(out.stdout, "import_first_runs ... ok\n")); + if (i == 0) { validtestout = strings.dup(out.stdout); } + else { assert(same(validtestout, out.stdout)); }; + refi = 0; ai = 0; + for (ai < testactions.len) { + let si: i32 = 0; + for (si < suffixes.len) { + let bytes: str = readfile(strings.concat(testworks[i], "/", + testactions[ai], suffixes[si])); + if (i == 0) { append(testartifactrefs, strings.dup(bytes)); } + else { assert(same(testartifactrefs[refi], bytes)); }; + refi += 1; si += 1; + }; + ai += 1; + }; + ii = 0; + for (ii < initsuffixes.len) { + let bytes: str = readfile(strings.concat(testworks[i], + "/domain.validtest-test-main", initsuffixes[ii])); + if (i == 0) { append(testartifactrefs, strings.dup(bytes)); } + else { assert(same(testartifactrefs[refi], bytes)); }; + refi += 1; ii += 1; + }; + assert(!directoryhasnew(works[i]) && !directoryhasnew(testworks[i]) + && !directoryhasfragment(works[i], ".wwtxn.") + && !directoryhasfragment(testworks[i], ".wwtxn.")); + i += 1; + }; + + // Parser state is request-local. A valid Cstage build and an invalid + // WWstage build can overlap without changing either result or residue. + let pcwork: str = strings.concat(root, "/parallel-c-work"); + let pwwork: str = strings.concat(root, "/parallel-ww-work"); + let pcout: str = strings.concat(root, "/parallel-c-output"); + let pwout: str = strings.concat(root, "/parallel-ww-output"); + mkdirall(pcwork); mkdirall(pwwork); + let pcav: []str = [driver("ww"), "build", "-I", tree, "-w", pcwork, + "-o", pcout, "domain.app"]; + let pwav: []str = [driver("ww_ww"), "build", "-I", tree, "-w", pwwork, + "-o", pwout, "domain.missinglate"]; + let pc: exec.command; + pc.path = pcav[0]; pc.argv = pcav; pc.env = os.getenvs(); pc.dir = repo(); + pc.stdoutpath = strings.concat(root, "/parallel-c.stdout"); + pc.stderrpath = strings.concat(root, "/parallel-c.stderr"); + pc.deadline = time.add(time.now(time.clock.monotonic), + (120i64 * (time.second: i64)): time.duration); + pc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pw: exec.command; + pw.path = pwav[0]; pw.argv = pwav; pw.env = os.getenvs(); pw.dir = repo(); + pw.stdoutpath = strings.concat(root, "/parallel-ww.stdout"); + pw.stderrpath = strings.concat(root, "/parallel-ww.stderr"); + pw.deadline = time.add(time.now(time.clock.monotonic), + (120i64 * (time.second: i64)): time.duration); + pw.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pcp: exec.process; + let pwp: exec.process; + exec.start(&pcp, &pc); exec.start(&pwp, &pw); + let pcdone: bool = false; + let pwdone: bool = false; + for (!pcdone || !pwdone) { + if (!pcdone) { pcdone = exec.poll(&pcp); }; + if (!pwdone) { pwdone = exec.poll(&pwp); }; + if (!pcdone || !pwdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + assert(pcp.result.errno == 0 && pcp.result.cleanuperrno == 0 + && pcp.result.termination == exec.termination.EXIT + && pcp.result.code == 0); + assert(pwp.result.errno == 0 && pwp.result.cleanuperrno == 0 + && pwp.result.termination == exec.termination.EXIT + && pwp.result.code == 1); + assert(readfile(pc.stdoutpath).len == 0 && readfile(pc.stderrpath).len == 0); + assert(readfile(pw.stdoutpath).len == 0 + && same(primarydiagnostic(readfile(pw.stderrpath)), expected)); + assert(same(appbinref, readfile(pcout)) && !os.exists(pwout) + && directoryisempty(pwwork) && !directoryhasnew(pcwork) + && !directoryhasfragment(pcwork, ".wwtxn.")); + let pcrun: []str = [pcout]; + runcommand(root, "import-order-parallel-run", pcrun, time.second, &out); + expectexit(&out, 42); + assert(!directoryhasfragment(root, ".wwtxn.") + && !directoryhasfragment(root, ".install") + && !directoryhasnew(root)); + clean(root); +}; + @test fn list_mode_with_no_matches_emits_no_sentinel() void = { let root: str = fresh(); let alpha: str = strings.concat(root, "/alpha"); diff --git a/test/sep/sepimport_test.ww b/test/sep/sepimport_test.ww index 6f95b601..bc4dd052 100644 --- a/test/sep/sepimport_test.ww +++ b/test/sep/sepimport_test.ww @@ -20,8 +20,9 @@ package sepimport_test; // // declns (#23/#30 modfn leg) — a distinct local `fn localaa` and the // file-scoped imported qualifier aa both resolve (exit 6 = localaa() + -// aa.helper()); the _vbu layout flips declaration/import order and must stay -// byte-identical on {aa.s,aa.wwi,__root.s}; __root.s carries both calls. +// aa.helper()); the _vbu layout reorders the valid declarations after its +// import and must stay byte-identical on {aa.s,aa.wwi,__root.s}; __root.s +// carries both calls. // // slttypepref (#58/#50 c1) — scopelookuptype must prefer the current // module's SK_TYPE when a param shadows a type leaf two modules both diff --git a/test/wcc/200_parse.c b/test/wcc/200_parse.c index 12e6f464..bfdb0dfd 100644 --- a/test/wcc/200_parse.c +++ b/test/wcc/200_parse.c @@ -249,9 +249,9 @@ main(void) const char *src = "package main;\n" "import zed;\n" + "import alpha;\n" "fn f() void = { let s: str = \"import fake;\"; };\n" - "/* import hidden; */\n" - "import alpha;\n"; + "/* import hidden; */\n"; int errs; const char *pkg; char *got = imports_to_str(src, &errs, &pkg); @@ -266,6 +266,28 @@ main(void) free((void *)pkg); free(got); } + { + const char *src = + "package main;\n" + "fn first() void = {};\n" + "import alpha;\n" + "import beta;\n" + "fn second() void = {};\n" + "import gamma;\n"; + int errs; + const char *pkg; + char *got = imports_to_str(src, &errs, &pkg); + if (errs != 2 || got == NULL + || strstr(got, "(use \"alpha\"") == NULL + || strstr(got, "(use \"beta\"") == NULL + || strstr(got, "(use \"gamma\"") == NULL) { + fputs("late import sections were not diagnosed and retained\n", + stderr); + fail++; + } + free((void *)pkg); + free(got); + } { int errs; const char *pkg; diff --git a/test/wcc/data/modfn_coexist_vbu/main.ww b/test/wcc/data/modfn_coexist_vbu/main.ww index 97b092aa..32744178 100644 --- a/test/wcc/data/modfn_coexist_vbu/main.ww +++ b/test/wcc/data/modfn_coexist_vbu/main.ww @@ -1,7 +1,7 @@ -// Declaration-before-import twin of ../modfn_coexist/main.ww. The distinct -// local name follows Go's import/package-scope collision rule while retaining -// the order-independence check for installing a real import binding. +// Reordered-source twin of ../modfn_coexist/main.ww. The distinct local name +// follows Go's import/package-scope collision rule while retaining the +// declaration/import installation-order check for a real file-scoped binding. package main; -fn localaa() i32 = { return 1; }; import aa; +fn localaa() i32 = { return 1; }; export fn main() i32 = { return localaa() + aa.helper(); };