fix: allow lexical import shadowing
This commit is contained in:
@@ -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
|
||||
2160–2174 and 2190–2233](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
|
||||
223–335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L223-L335),
|
||||
[`check.go`, lines 73–94](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 143–180](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/signature.go#L143-L180)
|
||||
and [`assignments.go`, lines 525–602](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 672–692](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/call.go#L672-L692),
|
||||
[`check.go`, lines 496–523](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L496-L523),
|
||||
[`resolver.go`, lines 706–740](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 8–13](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/fixedbugs/bug129.go#L8-L13),
|
||||
[`issues0.go`, lines 16–23](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/issues0.go#L16-L23),
|
||||
[`bug107.go`, lines 8–15](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.
|
||||
|
||||
15
docs/spec.md
15
docs/spec.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user