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:
- Declaration collection: gather all symbol definitions for each lexical scope.
- 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:
- Local variables in the current lexical scope (including shadowing).
- Function parameters in the current lexical scope.
- Local variables/parameters from outer lexical scopes (captures).
thismembers (fields/properties/functions), using MI linearization.- 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 scopeouter: enclosing lexical scopemodule: 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.
overrideis 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
Outercan 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
thismembers 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 = falseshadow_capture: allow, warn = falseshadow_global: allow, warn = falseshadow_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 namesscope.set(name: String, value: Obj)// only if mutable and visiblescope.locals(): List<String>// visible locals in current framescope.captures(): List<String>// visible captures for this framescope.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:
moduleNamemoduleSlotCountmoduleSlotNames[]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.