lyng/notes/plan_scope_slots_elimination.md

5.1 KiB

Plan: Eliminate Scope Slots for Module + Extensions (Frame-Backed Only)

Goal (1 + 2)

  • Module-level vars must be frame-backed just like locals; no special scope storage.
  • Extension wrappers must use frame slots as well; no scope-slot storage.
  • Scope remains only as a facade for Kotlin extern symbols (reflection/interop), not as primary storage.

Non-Goals (for now)

  • Full reflection redesign (set aside).
  • Removing scope slots entirely (interop/reflection may still need a facade).

Current Findings (high level)

  • BytecodeCompiler still builds scope slots for module symbols and for extension wrappers via pendingScopeNameRefs.
  • CmdRuntime reads/writes scope slots whenever slot < scopeSlotCount.
  • RESOLVE_SCOPE_SLOT + addr slots are emitted for moving between scope and frame slots.

Design Direction

  • Frame slots are the sole storage for module vars and extension wrappers.
  • Scope slots are reserved for explicit extern/interop names only (not part of this change).
  • Module symbols are captured like any other (closure capture via FrameSlotRef), no module slot plan or scope slot mapping.
  • Persist the module frame inside ModuleScope so its lifetime matches the module.

Key Decisions

  • Module scope is just another frame in the parent chain; do not seed or persist module vars through scope slots.
  • Extension wrappers are treated like ordinary locals/globals in frame slots; no pending scope name refs.
  • Compatibility: when seedScope is not a ModuleScope, module-level declarations still map to scope slots so Scope.eval(...) retains cross-call variables.

Work Plan

Phase A: Compiler changes (scope slot creation)

  1. Stop adding module declarations to scope slots

    • In BytecodeCompiler.collectScopeSlots, when moduleScopeId matches a declaration, treat it as a local slot, not a scope slot.
    • Remove isModuleDecl special-casing that pushes into scopeSlotMap.
  2. Remove synthetic scope slots for extension wrappers

    • Gate pendingScopeNameRefs (extension callable/getter/setter queueing) behind explicit scope-slot usage.
    • Ensure extension wrappers are resolved via local slot indices only (frame-backed).
  3. Tighten scope-slot eligibility

    • isModuleSlot(...) should no longer route module names to scope slots.
    • Only names in allowedScopeNames / scopeSlotNameSet (extern/interop) should become scope slots.
  4. ResolveSlot prefers frame slots for everything

    • Update resolveSlot to pick local slots for module/extension names.
    • Only return scopeSlotMap entries for explicit extern/interop names.

Phase B: Runtime changes (module frame)

  1. Persist the module frame inside ModuleScope

    • Add ModuleScope.ensureModuleFrame(fn) that creates/reuses a BytecodeFrame with fn.localCount.
    • Store local slot metadata on ModuleScope for future interop (names/mutables/delegated).
  2. Use the module frame when executing the module bytecode

    • CmdFrame should reuse the module frame when scope0 is ModuleScope and args are empty.
    • Avoid clobbering module locals with args.
  3. Remove module seed path that depends on scope slots

    • Remove moduleSlotPlan application in Script.execute.
    • Keep seedImportBindings and seedModuleLocals, but write into the module frame (scopeSlotCount should be 0).

Phase C: Bytecode + ABI considerations

  1. No new opcodes required for (1) + (2)

    • If we later need persistence across module reloads, introduce explicit sync opcodes (requires approval).
  2. Migration safety

    • Keep existing scope-slot ops for interop only; do not delete opcodes yet.

Detailed Change List (by file)

  • lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt

    • collectScopeSlots: treat module declarations as locals; do not map into scopeSlotMap.
    • Remove/guard pendingScopeNameRefs queuing for extension wrappers.
    • isModuleSlot: stop routing by moduleScopeId; only allow for explicit interop names.
    • resolveSlot: prefer local slots; avoid scope slot fallback for module/extension names.
  • lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt

    • Remove module slot plan application that assumes scope slots.
    • Ensure module vars survive via frame capture rather than scope seeding.
  • lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt

    • Add module frame storage + metadata and ensureModuleFrame(fn).
  • lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt

    • Use module frame when scope0 is ModuleScope in CmdFrame.

Testing / Validation

  • Disassemble compiled modules: verify no RESOLVE_SCOPE_SLOT for module vars or extension wrappers.
  • Run :lynglib:jvmTest and at least one module script execution that references module vars across calls.
  • Verify extension call wrappers still resolve and invoke correctly without scope-slot backing.

Risks / Open Questions

  • Module persistence: if module vars were previously “persisted” via scope slots, confirm frame-backed storage matches desired lifetime.
  • Extension wrapper lookup: ensure name resolution does not rely on scope slot indexing anywhere else.

Approval Gates

  • Any new opcode or runtime sync step must be proposed separately and approved before implementation.