lyng/notes/compile_time_name_resolution_spec.md

250 lines
7.8 KiB
Markdown

# 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)
- Dynamic by-name lookup as part of core execution path.
- 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:
```lyng
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):
```lyng
class A { fun foo() = 1 }
class B { fun foo() = 2 }
class C : A, B { } // error: requires override
```
## 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):
```lyng
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
```lyng
class C { val x = 1 }
fun f() {
val x = 2 // warning by default: shadows member
x
}
```
### Closure Capture Determinism
```lyng
var g = 1
fun f() {
var g = 2
return { g } // captures local g, not global
}
```
### Explicit Reflection
```lyng
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.
## 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.