fix: allow lexical import shadowing

This commit is contained in:
2026-08-22 17:53:31 +09:00
parent 89f519d5cb
commit 6279d46652
19 changed files with 1420 additions and 270 deletions

View File

@@ -9760,6 +9760,201 @@ No format bump. Build workdir format remains `18`, test workdir format remains
`19`, semantic storage format remains `3`, and no test-result cache is
introduced.
### 11.49 Implemented lexical shadowing of import bindings
An effective nonblank import qualifier is a file-local package-name object, not
a reserved spelling. An ordinary closer lexical binding may shadow it. Each
occurrence resolves to the nearest visible object: a selector before the local
declaration denotes the import and satisfies that import's use accounting; the
same spelling after a parameter, local, tuple-local, loop/range binder, or
match-arm binder denotes that closer binding. Leaving the nested scope restores
the import binding. A selector consumes an import only when its receiver
actually resolves to that import's package-name object. A selector whose
receiver is a local value neither consumes nor resurrects the same-spelled
import. This includes explicit aliases and qualifiers spelled like builtins.
This is lexical binding recovery, not a new import form or an identity rule.
Blank imports and rejected effective-`init` imports still install no binding;
an unresolved target still fails during resolution before checker binding
semantics. WW's existing `for ... else` behavior has no Go counterpart and is
unchanged.
#### Pinned Go evidence and fact classification
The sole semantic authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- the language specification defines imports as package objects with file-block
scope, and defines parameter/body/local declaration points, nested scopes,
and inner-declaration shadowing ([`doc/go_spec.html`, lines
21602174 and 21902233](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/doc/go_spec.html#L2160-L2233));
- `types2` constructs a file scope and `PkgName`, then resolves objects from
the innermost scope outwards
([`cmd/compile/internal/types2/resolver.go`, lines
223335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L223-L335),
[`check.go`, lines 7394](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L73-L94));
- parameter and local declaration timing is implemented in
[`signature.go`, lines 143180](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/signature.go#L143-L180)
and [`assignments.go`, lines 525602](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/assignments.go#L525-L602);
- selector checking marks an import used only after its receiver resolves to a
`PkgName`; bodies are processed before unused imports are diagnosed
([`call.go`, lines 672692](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/call.go#L672-L692),
[`check.go`, lines 496523](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L496-L523),
[`resolver.go`, lines 706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)); and
- official fixtures compile a selector before a later local, retain that
declaration-point distinction, and show parameter shadowing
([`test/fixedbugs/bug129.go`, lines 813](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug129.go#L8-L13),
[`issues0.go`, lines 1623](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/issues0.go#L16-L23),
[`bug107.go`, lines 815](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug107.go#L8-L15)).
Those specification, resolver, selector-use, declaration-point, and fixture
facts are **behavior directly implemented or asserted by pinned Go**. That a
WW occurrence before a local consumes the import, an occurrence after it sees
the local, a nested scope restores the import on exit, aliases and
builtin-spelled qualifiers follow the same rule, and a local selector does not
consume the import is **behavior derived from the pinned implementation**.
The behavior honestly applies to WW's existing file-local dotted bindings and
representable lexical scopes without introducing Go's modules, manifests,
quoted/grouped/dot imports, registry, cache, network resolution, or source
build expressions.
#### Fresh four-axis audit and direct pre-fix measurements
The fresh simultaneous audit classified multiple named source operands as a
different, applicable build/package slice; explicit `*_test.ww` build operands
as a different, applicable build/source-selection slice; shared top-level test
process state and fatal-abort topology as different, applicable test slices; and
Go-compatible regular-expression `-run` matching as a different, applicable
test slice. They remain open and unselected: each needs a wider true-owner
change. Grouped, quoted, and dot imports are inapplicable to WW's deliberately
narrow import grammar. No fresh pinned evidence reopened a completed section
through §11.48.
Before this slice, all following results were **directly measured WW behavior**:
- a directory build containing a real package selector followed by a legal
`let shadowmod` failed in both stages before publication; Cstage emitted
positioned 155-byte stderr (SHA-256
`13cfac9494a1f6c0ee958d3b74ecfe8b0efe2a685f31a8a1771fda3fa0af1b02`),
while WWstage emitted unpositioned 76-byte stderr (SHA-256
`298e4991ca9900ef32499762f8ae9abb8ace07c3e672dc66db1e911a8de62b75`).
Both had empty stdout, no output, and empty precreated workdirs;
- same-package and external test shadows failed before descriptor execution;
both emitted exactly `FAIL\n`, published/retained nothing, and left no work
entry. Cstage stderr was 277 bytes (SHA-256
`ac850802e5911f53c3e64be2ab55e1f3442438eadab35a40f63ddac869337e4e`),
WWstage was 186 bytes (SHA-256
`a1229bd19628e5dc909a0bcb848c30cc1dbdf9d370f21ed51e79489c06abcad0`);
- after a genuine selector, `let shadowmod: i32 = 1; return shadowmod.say()`
produced only the shadow prohibition in Cstage, but WWstage additionally
cascaded through `calling non-function` and `asserttyped: dot`; and
- an otherwise-unused import with only local `shadowmod.n` produced the shadow
prohibition rather than unused-import in both stages, proving the former
syntax-only use pass falsely consumed the import. Legal no-shadow controls
built, published byte-identical executables (SHA-256
`7b710cf0973821cec430878d1f90de64485438510512561263c1a16b367c3c78`),
and exited 42.
The direct probes also covered parameter, local, nested, explicit-alias,
selector-before/after, imported-dependency, same-package, external-test, and
honest test-only paths; both stages rejected every legal shadow. Thus the
pre-fix difference was legal-source rejection, false unused-import accounting,
and stage-divergent downstream recovery.
#### Ownership and complete four-axis behavior
The true owners are the Cstage semantic checker in `cmd/wcc/check.c`, its
self-hosted twin in `selfhost/cmd/wcc/check.ww`, and the latter's
`selfhost/cmd/wcc/cgenexpr.ww` local-versus-imported-enum fast path. They
remove the import-shadow prohibition; resolve use accounting through isolated
temporary lexical scopes rather than merely selector spelling; preserve
declaration timing; and gate dotted package/enum shortcuts on the visible
binding. The Cstage code generator already follows checker/local stamps and is
proved rather than redefined. Parsers, loader/source selection, drivers,
coordinator, canonical resolution, graph/action construction, assembler,
archiver, linker, runtime, publisher, and persistence records are not owners.
- **Go-like build:** raw, directory, and imported programs with legal shadowing
now pass checking, build through unchanged actions, publish normally, and run
the local value/field/function-pointer behavior. A genuinely invalid local
selector fails during checking before code generation or downstream tools.
- **Go-like test:** production called by test, same-package, external-test,
honest test-only, filtered, retained, and directly retained products use the
same rule before execution. Discovery, filters, descriptors, process state,
fatal/skip behavior, timeout, retention, and cleanup are unchanged.
- **Go-like package:** import binding remains file scoped; ordinary local scopes
nest within it, and sibling source files remain independent. Declared names,
source roles, package/variant identities, exported declarations,
initialization, symbols, and selected membership do not change.
- **Go-like import:** the nearest visible object wins. Only a selector whose
receiver is the visible package-name object satisfies unused-import accounting;
local field selectors do not. Default/explicit aliases, blank imports,
effective-`init`, missing-target precedence, canonical dotted identity,
contextual local/vendor mapping, visibility, cycles, and direct graph edges
retain their existing semantics.
#### Lifecycle, parity, proof, and formats
Filename/platform/test-role eligibility, byte-sorted source selection, package
clauses, source IDs, loading, and resolution remain earlier owners. Shadowing
does not add/remove an already-resolved direct import edge, rekey actions, or
change action order, variants, initialization dispatch, linker symbols,
`.wwi` ownership, artifacts, publication, or persistence identity. Physical
directories remain loader/runtime/presentation metadata, never canonical
package, import, graph, action, artifact, symbol, publication, or storage
identity.
The lexical-aware prepass keeps source-position diagnostic ordering. It removes
every `shadows imported module` diagnostic while preserving missing target,
invalid effective-`init`, blank/no-binding, collision/redeclaration, and
selector-only package-name diagnostics. An otherwise-unused import is reported
before a later invalid local selector according to source position; local
invalid dots reject with the same positioned recovery stamp in both stages and
cannot cascade into C/WW code generation, assembler, or linker diagnostics.
Valid shadowing reaches ordinary compiler, assembler, archiver, linker, and
runtime paths. The representative package matrix proves byte-identical
Cstage/WWstage unit, interface, assembly, object, archive, initializer, and
published executable artifacts; every test source role proves public binary
parity, and the retained case proves retained-binary parity. A cold invalid
action creates no public/retained/interface/archive/object/executable artifact
and no `.new`, `.install`, `.wwtxn.*`, adjacent `.sepwork`, capture, result, or
request scratch. A warm edit that
makes an import unused while also introducing an invalid local selector
preserves the prior committed generation and public product byte-for-byte;
exact restoration uses ordinary invalidation/reuse and cannot leave poisoned
state. Existing producer/runtime failure, late publication failure, rollback,
concurrency, interruption, process-group ownership, and cleanup remain their
existing owners because this checker slice adds no process, lock, transaction,
or shared runtime state.
The WW-native `lexical_import_bindings_shadow_normally` package observer and
the tool-suite `paramshadow_lexical_bindings` fixture matrix jointly prove
selector before local, self-shadowing initializer, parameter/let/tuple-let/
ordinary-for/range/match-arm declaration timing, all annotated tuple types
before any tuple binder, nested restoration, aliases and builtin-spelled
qualifiers, local struct/pseudo/function-pointer fields, imported-enum name
collisions, local dotted type/value rejection, unused accounting, and
sibling-file isolation. The package observer also covers raw and directory
builds, imported dependencies, every applicable test role, filtered and
retained/direct-retained execution, normalized diagnostic parity, valid
artifact/runtime parity, cold cleanup, warm preservation/restoration, and
residue absence. Blank/effective-`init`/missing-target behavior is unchanged
and remains proved by the immediately preceding focused observers.
Go has no range-loop `else` clause. That WW-only extension is therefore
inapplicable to this pinned-Go slice and was not redefined: the pre-existing
Cstage behavior keeps a range binder visible in `else`, whereas WWstage restores
the outer scope before `else`. Each stage's import-use prepass deliberately
matches its own live checker there; the cross-stage extension difference remains
open and is not presented as lexical-shadow parity proved by this slice.
No serialized format changes. This alters lexical resolution of source bytes
already present in the existing action vouchers and adds no action-key, graph,
artifact-layout, harness-protocol, cache, database, or publication field.
Build workdir format remains `18`, test workdir format remains `19`, semantic
storage format remains `3`, and no test-result cache is introduced.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -336,6 +336,21 @@ ImportPath = ident { "." ident } .
lookup rather than package-name diagnostics. Builtin spelling does not alter
the object: an import bound as `len`, `size`, `align`, or another builtin name
remains a selector-only package-name object.
An ordinary lexical binding may shadow that file-local package-name object.
Lookup at each occurrence chooses the nearest enclosing binding: a selector
before a later local declaration can use and count the import, while the
same spelling after that declaration denotes the local. Parameters bind for
their whole function body; local `let` bindings begin after their declared
type and initializer have been checked; tuple bindings begin after their
right-hand side and declared types; and loop, range, and match-arm bindings
begin only after their respective initializer/iterable or pattern/type has
been checked. Nested blocks and loop scopes restore the imported package-name
object on exit. These declaration-point rules apply equally to default,
explicit, and builtin-spelled qualifiers. A selector counts as an import use
only when its receiver resolves to that package-name object; a selector on a
closer local, including a field or pseudo-field selector, does not.
Go has no range-loop `else` clause; that WW-only extension is not assigned a
Go-derived scope rule by this paragraph.
Neither form exposes an imported declaration as a bare `Name`; ordinary
unqualified lookup remains limited to lexical, builtin, and same-package
declarations. A blank import creates no package-name object, an effective

View File

@@ -598,7 +598,53 @@ precedence. Type-shaped arguments parsed for `size` and `align` must retain type
checking when either spelling is instead an import binding; neither stage may
fall into an internal expression-kind diagnostic.
The same observer must exercise ordinary root and imported builds plus
The adjacent focused dual-stage
`lexical_import_bindings_shadow_normally` package observer, together with the
tool-suite `paramshadow_lexical_bindings` fixture matrix, is the acceptance
owner for ordinary lexical shadowing of an effective file-local package-name
object. Together they require a selector before a later local to resolve to and
consume the import, while the later spelling resolves to the closer parameter,
`let`, tuple-let, ordinary-`for`/range, or match-arm binding. Nested blocks and
loop exit restore the import; a parameter wins throughout its body;
self-shadowing initializers retain the outer import until the local declaration
point; and every annotated tuple type is checked before any tuple binder enters
scope. Default, explicit, and builtin-spelled qualifiers follow the same rule.
A local value, struct or pseudo-field, function-pointer field, or local
enum/member collision must use the local receiver rather than recover the
same-spelled import. Conversely, a shadowed dotted type or invalid scalar
selector must reject as a local use without a checker/backend cascade. A
selector-shaped local-only occurrence does not consume the import, so its
unused diagnostic retains source-position precedence. Sibling source files
retain independent import bindings and a separately selected legal import use
remains effective. Blank, effective-`init`, and missing-target controls remain
owned by their immediately adjacent focused observers and are explicit
non-effects of this acceptance pair.
This observer exercises raw-source and directory builds, imported dependencies,
production reached by tests, same-package, external-test, and honest
test-only sources, filtered execution, retained test execution, and later
direct retained-binary execution. Cstage and WWstage must agree on status,
stdout, normalized full stderr (including positioned diagnostic order) for
build and test rejection, representative comparable semantic artifacts, every
test role's public binary bytes, retained binary bytes, and valid runtime
results. Its lifecycle matrix requires cold invalid requests to leave no public
or retained product and an empty workdir; a warm valid-to-invalid unused-import
transition whose source also contains an invalid local selector to preserve the
prior public product and complete committed generation byte-for-byte; and
restoration to recover ordinary exact-byte reuse with no `.new`, `.install`,
`.wwtxn.*`, adjacent `.sepwork`, capture/result, or request-scratch residue. The
rule changes neither source eligibility,
package/import identity, direct graph edges, action/variant identity, init
ordering, test process/state/fatal-abort topology, runtime/process ownership,
publication transaction shape, persistence format, concurrency isolation,
interruption handling, nor descendant cleanup; those boundaries retain their
existing owners. Go has no range-loop `else` clause; that WW-only extension's
pre-existing Cstage/WWstage scope difference is explicitly outside this
pinned-Go observer, and each import-use prepass continues to mirror its own
stage's live checker there.
The `bare_import_bindings_require_selectors` observer must exercise ordinary
root and imported builds plus
production, same-package test, external-test, and honest test-only source
roles. Cstage and WWstage must agree on status and normalized stdout/stderr,
including diagnostic order and source position, and valid controls must produce