prometeu-studio/docs/pbs/decisions/Name Resolution - Scope, Lookup, and Imports Decision.md

11 KiB

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:

import { Foo } from @a:m;
declare const Foo: int = 1;

This is rejected as a local-versus-import collision in the value namespace.

import { f } from @a:m;
import { f } from @b:n;

This is rejected because the visible imported names match but the canonical origins differ.

fn f(a: int) -> int { ... }
import { f } from @a:m;

This is rejected as a local-versus-import collision in callable position.

import { * } from @a:m;

This introduces the exported visible names of @a:m, but does not introduce a source-visible module object in v1.

import { A as aaa } from @a:m;

The visible local declaration name is aaa, not A.