prometeu-studio/docs/compiler/pbs/decisions/Name Resolution - Scope, Lookup, and Imports Decision.md
2026-03-24 13:42:37 +00:00

271 lines
11 KiB
Markdown

# Name Resolution - Scope, Lookup, and Imports Decision
Status: Accepted (Implemented)
Cycle: Initial name-resolution closure pass
## 1. Context
PBS v1 needs a deterministic frontend-visible rule for:
- how module scope is formed,
- how lookup works by namespace,
- what imports actually introduce into local scope,
- how collisions between local and imported names are handled,
- and which failures belong to syntax, manifest/import resolution, static semantics, or linking.
Existing specs already fix important inputs:
- `mod.barrel` is the single source of module visibility,
- imports target modules, not files,
- only `pub` names may be imported from another module,
- PBS has distinct type, value, callable, and host-owner namespaces,
- builtin simple types `int`, `float`, `bool`, and `str` are always available in type position,
- and reserved stdlib project spaces resolve only from the selected stdlib environment.
The remaining goal of this decision is to close the minimum name-resolution baseline needed for normative frontend work around ordinary scope construction, ordinary lookup, and import naming.
## 2. Decision
PBS v1 adopts the following baseline for scope construction, lookup, and imports:
1. Top-level declarations of a module are collected across all `.pbs` files in the module before visibility filtering is applied.
2. `mod.barrel` is a visibility filter over existing module declarations, not the source of declaration existence.
3. Module-internal top-level availability does not depend on source-file order.
4. Local block scopes nest normally over module scope.
5. In value position, lookup prefers the nearest lexical value binding before any module-level or imported value.
6. Parameters and local `let` bindings participate in the same nearest lexical value-scope layer for lookup purposes.
7. In type position, visible module-local type declarations are considered before imported type declarations, and builtin simple types remain always-available reserved type names outside ordinary import competition.
8. In callable position, visible module-local callable declarations are considered before imported callable declarations.
9. In host-owner position, visible module-local host owners are considered before imported host owners.
10. The local visible name introduced by an import is always the post-alias name when an alias is present.
11. `import { X } from @project:path;` introduces the imported exported name `X` into the matching namespace.
12. `import { X as Y } from @project:path;` introduces only the local visible name `Y` into the matching namespace.
13. Alias spelling changes only the local visible name, never canonical builtin identity or canonical host identity.
14. `import { * } from @project:path;` is the whole-module import form for bringing the target module's exported names into local visibility under their exported names.
15. A collision between a module-local declaration and an imported visible name in the same namespace is a deterministic error rather than silent shadowing.
16. A collision between two imported visible names in the same namespace is not an error only when both imports denote the same canonical underlying declaration after module resolution.
17. If two imported visible names in the same namespace come from different canonical underlying declarations, the program is rejected and one of the imports must be aliased or removed.
18. `import { * } from @project:path;` does not create a first-class module object, module namespace value, or other source-visible binding by itself; it only introduces the exported names of the target module.
19. A module-local function and an imported function with the same visible name produce a deterministic error in this closure pass.
20. Non-callable namespaces do not merge by name.
## 3. Scope Construction
### 3.1 Module collection
For one module:
- the compiler collects top-level declarations from all `.pbs` files in that module,
- forms one module-level declaration space,
- then applies `mod.barrel` to determine `mod` and `pub` visibility.
This means:
- declaration existence is not derived from barrel entries,
- and module-internal declaration availability is not ordered by file traversal.
### 3.2 Lexical scope
Inside executable bodies:
- lexical block scope is nested normally,
- nearest local bindings win within value lookup,
- and lexical nesting remains independent from cross-module visibility.
## 4. Lookup By Namespace
### 4.1 Value position
Value-position lookup follows this order:
1. nearest local lexical bindings,
2. parameters in the current lexical function scope,
3. visible module-local values,
4. visible imported values.
For lookup purposes, parameters and local `let` bindings are one nearest lexical value layer.
The distinction between them may still matter for diagnostics wording.
### 4.2 Type position
Type-position lookup follows this order:
1. visible module-local type declarations,
2. visible imported type declarations,
3. builtin simple types `int`, `float`, `bool`, and `str` as always-available reserved type names.
Builtin simple types are not treated as ordinary imported declarations and do not participate in ordinary import competition.
### 4.3 Callable position
Callable-position lookup follows this order:
1. visible module-local callables,
2. visible imported callables.
If a visible module-local function name and a visible imported function name are the same, the program is rejected in this closure pass rather than merged.
### 4.4 Host-owner position
Host-owner lookup follows this order:
1. visible module-local host owners,
2. visible imported host owners.
Host-owner lookup remains separate from type, value, and callable lookup.
## 5. Import Surface
### 5.1 Named import
`import { X } from @project:path;`:
- resolves the target module,
- checks that `X` is exported and importable from that module,
- and introduces `X` into the corresponding namespace locally.
### 5.2 Aliased import
`import { X as Y } from @project:path;`:
- resolves the same exported declaration as the non-aliased form,
- but introduces only `Y` as the local visible name.
Alias spelling does not change canonical identity governed elsewhere.
### 5.3 Whole-module import
`import { * } from @project:path;`:
- validates and resolves the target module,
- introduces the target module's exported visible names under their exported spellings,
- but does not create a module-valued binding,
- does not create a namespace object,
- and does not authorize qualified member access by itself.
If PBS later wants module-object or namespace-qualified source semantics, that must be added explicitly rather than inferred from this form.
## 6. Collision Policy
### 6.1 Local versus imported
If a module-local declaration and an imported declaration produce the same visible name in the same namespace:
- the program is rejected,
- and the implementation must not silently shadow the imported declaration or the local declaration.
This includes function names.
In this closure pass, a local function and an imported function with the same visible name are rejected rather than merged.
### 6.2 Imported versus imported
If two imports produce the same visible name in the same namespace:
- the program is not rejected if both imports resolve to the same canonical underlying declaration after module resolution,
- but the duplicate import is still redundant,
- and the program is rejected if the imports resolve to different canonical underlying declarations.
### 6.3 Namespace separation
Names in different namespaces do not collide merely by spelling.
For example:
- a host owner and a type declaration do not collide by spelling alone,
- because host-owner namespace remains distinct from type namespace.
## 7. Relationship To Later Name-Resolution Decisions
This decision is intentionally limited to:
- ordinary scope construction,
- ordinary namespace lookup,
- import naming,
- and ordinary collision policy.
Later name-resolution decisions close:
- reserved builtin shells and host owners,
- callable-set visibility across modules,
- and the final phase boundary between syntax, manifest/import resolution, linking, and static semantics.
## 8. Invariants
- Name lookup must be deterministic.
- Module-internal top-level declaration availability must not depend on file order.
- `mod.barrel` remains a visibility mechanism rather than a declaration source.
- Imports must not invent first-class module-object semantics accidentally.
- The effective visible name of an import is always the post-alias name when an alias is present.
- Builtin simple types remain a reserved always-available core type set, distinct from ordinary imported declarations.
- Implementations must not assume silent local-over-import shadowing.
- Implementations must not merge local and imported function names automatically.
## 9. Explicit Non-Decisions
This decision record does not yet close:
- reserved-shell-specific lookup and collision details,
- callable-set import behavior across module boundaries beyond the local-versus-import collision baseline,
- and backend-facing lowering consequences of the resolved lookup model.
## 10. Spec Impact
This decision should feed at least:
- `docs/general/specs/14. Name Resolution and Module Linking Specification.md`
- `docs/pbs/specs/12. Diagnostics Specification.md`
- `docs/general/specs/13. Conformance Test Specification.md`
It should also constrain future work in:
- `docs/pbs/specs/13. Lowering IRBackend Specification.md`
- `docs/pbs/decisions/Name Resolution - Builtin Shells and Host Owners Decision.md`
- `docs/pbs/decisions/Name Resolution - Callable Sets and Cross-Module Linking Decision.md`
- `docs/pbs/decisions/Name Resolution - Linking Phase Boundary Decision.md`
## 11. Validation Notes
The intended baseline is:
- all top-level declarations in a module exist before barrel filtering,
- lookup is namespace-specific and deterministic,
- the effective visible import name is the alias name when an alias is present,
- whole-module import through `import { * } from @project:path;` introduces exported names rather than a module object,
- local/import collisions are rejected rather than shadowed,
- and builtin simple types remain reserved always-available names outside ordinary import competition.
Illustrative examples:
```pbs
import { Foo } from @a:m;
declare const Foo: int = 1;
```
This is rejected as a local-versus-import collision in the value namespace.
```pbs
import { f } from @a:m;
import { f } from @b:n;
```
This is rejected because the visible imported names match but the canonical origins differ.
```pbs
fn f(a: int) -> int { ... }
import { f } from @a:m;
```
This is rejected as a local-versus-import collision in callable position.
```pbs
import { * } from @a:m;
```
This introduces the exported visible names of `@a:m`, but does not introduce a source-visible module object in v1.
```pbs
import { A as aaa } from @a:m;
```
The visible local declaration name is `aaa`, not `A`.