prometeu-studio/docs/pbs/decisions/Name Resolution - Callable Sets and Cross-Module Linking Decision.md

6.4 KiB

Name Resolution - Callable Sets and Cross-Module Linking Decision

Status: Accepted (Implemented) Cycle: Initial name-resolution closure pass

1. Context

PBS v1 needs a deterministic rule for how callable names cross module boundaries.

The open questions were:

  • what exactly import { f } from @project:path; imports when f has multiple exported signatures,
  • whether callable sets from different origins may merge,
  • whether same-name imported callables from different origins are tolerated until callsite analysis,
  • whether local and imported callable names may coexist under one visible function name,
  • and how mod.barrel bounds the callable set that becomes importable.

Earlier closure already fixed important inputs:

  • mod.barrel is the single source of module visibility,
  • only pub names may be imported from another module,
  • local function names and imported function names with the same visible name are already being treated as collisions in this closure pass,
  • and callable identity is determined by callable shape rather than parameter-label spelling alone.

2. Decision

PBS v1 adopts the following baseline for callable sets and cross-module linking:

  1. import { f } from @project:path; imports the exported callable set named f from the target module.
  2. The imported callable set for f consists only of those overload signatures of f that are exported through that module's mod.barrel.
  3. Non-exported overloads in the target module do not participate in the imported callable set.
  4. Two imports of the same visible callable name are tolerated when they resolve to the same canonical underlying callable-set origin after module resolution.
  5. Such same-origin duplicate imports are redundant but not errors.
  6. Two imports of the same visible callable name from different canonical origins are rejected immediately.
  7. The program must not wait until a callsite to reject different-origin callable-name collisions.
  8. A module-local function name and an imported function name with the same visible name are rejected immediately.
  9. Automatic merging of callable sets across different origins is not permitted in this closure pass.
  10. Ordinary overload resolution occurs only after one unambiguous visible callable set has already been formed.

3. Imported Callable Set

The meaning of:

import { f } from @project:path;

is:

  • resolve the target module,
  • collect the exported callable entries named f from that module,
  • form the callable set consisting only of those exported overloads,
  • and make that callable set visible locally under the imported visible name.

This import does not:

  • reach non-exported overloads,
  • merge with hidden module-internal overloads,
  • or import one overload at a time independently from the exported callable name.

4. Same-Origin Versus Different-Origin Imports

4.1 Same origin

If the same visible callable name is imported more than once and both imports resolve to the same canonical underlying callable-set origin:

  • the duplicate import is tolerated,
  • but it remains redundant.

4.2 Different origin

If the same visible callable name is imported from different canonical origins:

  • the program is rejected immediately,
  • and one of the imports must be removed or aliased.

The implementation must not:

  • merge the callable sets,
  • defer the conflict to overload resolution,
  • or allow callsite context to decide which imported origin wins.

5. Local Versus Imported Callable Names

If a module-local function name and an imported function name are the same visible callable name:

  • the program is rejected immediately,
  • and the implementation must not merge the local and imported callable sets.

This preserves deterministic visibility and keeps callable origin traceable.

6. Barrel Boundary

mod.barrel defines the exact boundary of the callable set that may cross module boundaries.

Rules:

  • only overloads exported through mod.barrel are part of the importable callable set,
  • overloads with the same source-level function name but not exported through mod.barrel are invisible to importing modules,
  • and imported callable-set visibility is therefore a direct consequence of the source module's barrel contract.

7. Overload Resolution Boundary

This decision deliberately separates:

  • callable-set formation,
  • and overload resolution at a callsite.

The implementation must first form one unambiguous visible callable set. Only then may ordinary overload resolution operate on the candidate signatures of that set.

Callsite context must not be used to resolve ambiguity between different imported callable origins.

8. Invariants

  • Imported callable visibility is bounded by barrel export.
  • Different callable origins must not merge automatically.
  • Same-origin duplicate imports may be tolerated as redundancy, not as new callable contributions.
  • Local and imported function names do not coexist under one merged visible callable name in this closure pass.
  • Overload resolution happens after visible callable-set formation, not instead of it.

9. Explicit Non-Decisions

This decision record does not yet close:

  • per-overload import syntax,
  • aliasing policy for callable imports beyond the already-closed generic alias rules,
  • and any lowering consequences in 13. Lowering IRBackend Specification.md.

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

11. Validation Notes

The intended baseline is:

  • callable imports operate on exported callable names,
  • all exported overloads of that name come together from one module,
  • same-origin duplicate imports are tolerated,
  • different-origin same-name imports are rejected immediately,
  • and local/import callable collisions are also rejected immediately.

Illustrative examples:

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

This is tolerated as redundant same-origin duplication.

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

This is rejected immediately because the visible callable 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 callable collision.