lyng/notes/compile_time_name_resolution_spec.md

8.7 KiB

Compile-Time Name Resolution Spec (Draft)

Goals

  • Resolve all identifiers at compile time; unresolved names are errors.
  • Generate direct slot/method accesses with no runtime scope traversal.
  • Make closure capture deterministic and safe (no accidental shadowing).
  • Keep metaprogramming via explicit reflective APIs only.

Non-Goals (initial phase)

  • firbidden: Dynamic by-name lookup as part of core execution path.

  • forbidden: Runtime scope walking to discover names.

Overview

Compilation is split into two passes:

  1. Declaration collection: gather all symbol definitions for each lexical scope.
  2. Resolution/codegen: resolve every identifier to a concrete reference:
    • local/arg slot
    • captured slot (outer scope or module)
    • this-member slot or method slot
    • explicit reflection (opt-in)

If resolution fails, compilation errors immediately.

Resolution Priority

When resolving a name x in a given scope:

  1. Local variables in the current lexical scope (including shadowing).
  2. Function parameters in the current lexical scope.
  3. Local variables/parameters from outer lexical scopes (captures).
  4. this members (fields/properties/functions), using MI linearization.
  5. Module/global symbols (treated as deep captures).

Notes:

  • Steps 3-5 require explicit capture or member-slot resolution.
  • This order is deterministic and does not change at runtime.

Closures and Captures

Closures capture a fixed list of referenced symbols from outer scopes.

  • Captures are immutable references to outer slots unless the original is mutable.
  • Captures are stored in the frame metadata, not looked up by name.
  • Globals are just captures from the module frame.

Example:

var g = 1
fun f(a) {
    var b = a + g
    return { b + g }
}

Compiled captures: b and g, both resolved at compile time.

Capture Sources (Metadata)

Captures are a single mechanism. The module scope is simply the outermost scope and is captured the same way as any other scope.

For debugging/tooling, captures are tagged with their origin:

  • local: current lexical scope
  • outer: enclosing lexical scope
  • module: module/root scope

This tagging is metadata only and does not change runtime behavior.

this Member Resolution (MI)

  • Resolve members via MI linearization at compile time.
  • Ambiguous or inaccessible members are compile-time errors.
  • override is required when MI introduces conflicts.
  • Qualified access is allowed when unambiguous: this@BaseA.method()

Example (conflict):

class A { fun foo() = 1 }
class B { fun foo() = 2 }
class C : A, B { }  // error: requires override

Class Namespace (Nested Declarations)

Nested classes, objects, enums, and type aliases belong to the class namespace of their enclosing class. They are not instance members and do not capture an outer instance.

Resolution rules:

  • Qualified access (Outer.Inner) resolves to a class-namespace member at compile time.
  • Unqualified access inside Outer can resolve to nested declarations if not shadowed by locals/params.
  • Class-namespace members are never resolved via runtime name lookup; failures are compile-time errors.

Enum lifting:

  • enum E* { ... } lifts entries into the enclosing class namespace (e.g., Outer.Entry).
  • Any ambiguity with existing class members is a compile-time error.

Shadowing Rules

Shadowing policy is configurable:

  • Locals may shadow parameters (allowed by default).
  • Locals may shadow captures/globals (allowed by default).
  • Locals shadowing this members should emit warnings by default.
  • Shadowing can be escalated to errors by policy.

Example (allowed by default):

fun test(a) {
    var a = a * 10
    a
}

Suggested configuration (default):

  • shadow_param: allow, warn = false
  • shadow_capture: allow, warn = false
  • shadow_global: allow, warn = false
  • shadow_member: allow, warn = true

Reflection and Metaprogramming

Reflection must be explicit:

  • scope.get("x")/scope.set("x", v) are allowed but limited to the compile-time-visible set.
  • No implicit name lookup falls back to reflection.
  • Reflection uses frame metadata, not dynamic scope traversal.

Implication:

  • Metaprogramming can still inspect locals/captures/members that were visible at compile time.
  • Unknown names remain errors unless accessed explicitly via reflection.

Reflection API (Lyng)

Proposed minimal surface:

  • scope.get(name: String): Obj? // only compile-time-visible names
  • scope.set(name: String, value: Obj) // only if mutable and visible
  • scope.locals(): List<String> // visible locals in current frame
  • scope.captures(): List<String> // visible captures for this frame
  • scope.members(): List<String> // visible this-members for this frame

Reflection API (Kotlin)

Expose a restricted view aligned with compile-time metadata:

  • Scope.getVisible(name: String): ObjRecord?
  • Scope.setVisible(name: String, value: Obj)
  • Scope.visibleLocals(): List<String>
  • Scope.visibleCaptures(): List<String>
  • Scope.visibleMembers(): List<String>

Notes:

  • These APIs never traverse parent scopes.
  • Errors are thrown if name is not visible or not mutable.

Frame Model

Each compiled unit includes:

  • localSlots: fixed indexes for locals/args.
  • captureSlots: fixed indexes for captured outer values.
  • thisSlots: fixed member/method slots resolved at compile time.
  • debugNames: optional for disassembly/debugger.

Slot resolution is constant-time with no name lookup in hot paths.

Module Slot Allocation

Module slots are assigned deterministically per module:

  • Stable order: declaration order in source (after preprocessing/import resolution).
  • No reordering across builds unless source changes.
  • Slots are fixed at compile time and embedded in compiled units.

Recommended metadata:

  • moduleName
  • moduleSlotCount
  • moduleSlotNames[]
  • moduleSlotMutables[]

Capture Slot Allocation

Capture slots are assigned per compiled unit:

  • Stable order: first occurrence in lexical traversal.
  • Captures include locals, outer locals, and module symbols.
  • Captures include mutability metadata and origin (local/outer/module).

Example capture table:

idx  name   origin     mutable
0    b      outer      true
1    G      module     false

Error Cases (compile time)

  • Unresolved identifier.
  • Ambiguous MI member.
  • Inaccessible member (visibility).
  • Illegal write to immutable slot.

Resolution Algorithm (pseudocode)

pass1_collect_decls(module):
  for each scope in module:
    record locals/args declared in that scope
  record module-level decls

pass2_resolve(module):
  for each compiled unit (function/block):
    for each identifier reference:
      if name in current_scope.locals:
        bind LocalSlot(current_scope, slot)
      else if name in current_scope.args:
        bind LocalSlot(current_scope, slot)
      else if name in any outer_scope.locals_or_args:
        bind CaptureSlot(outer_scope, slot)
      else if name in this_members:
        resolve via MI linearization
        bind ThisSlot(member_slot)
      else if name in module_symbols:
        bind CaptureSlot(module_scope, slot)
      else:
        error "unresolved name"

    for each assignment:
      verify target is mutable
      error if immutable

Examples

Local vs Member Shadowing

class C { val x = 1 }
fun f() {
    val x = 2      // warning by default: shadows member
    x
}

Closure Capture Determinism

var g = 1
fun f() {
    var g = 2
    return { g }   // captures local g, not global
}

Explicit Reflection

fun f() {
    val x = 1
    scope.get("x") // ok (compile-time-visible set)
    scope.get("y") // error at compile time unless via explicit dynamic API
}

Dry Run / Metadata Mode

The compiler supports a "dry run" that performs full declaration and resolution without generating executable code. It returns:

  • Symbol tables (locals, captures, members) with slots and origins
  • Documentation strings and source positions
  • MI linearization and member resolution results
  • Shadowing diagnostics (warnings/errors)

This metadata drives:

  • IDE autocompletion and navigation
  • Mini-doc tooltips and documentation generators
  • Static analysis (visibility and override checks)

Migration Notes

  • Keep reflection APIs separate to audit usage.
  • Add warnings for member shadowing to surface risky code.
  • Runtime fallback opcodes are removed (CALL_VIRTUAL/GET_FIELD/SET_FIELD); unresolved names or members are compile-time errors.

Compatibility Notes (Kotlin interop)

  • Provide minimal Kotlin-facing APIs that mirror compile-time-visible names.
  • Do not preserve legacy runtime scope traversal.
  • Any existing Kotlin code relying on dynamic lookup must migrate to explicit reflection calls or pre-resolved handles.