diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index b7f5cfd..5ab0aed 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -92,11 +92,11 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Create captures only when detected; do not allocate scope slots. - [x] Add disassembler output for capture tables. - [x] JVM tests must be green before commit. -- [ ] Step 24B: Frame-slot captures in bytecode runtime. - - [ ] Build lambdas from bytecode + capture table (no capture names). - - [ ] Read captured values via `FrameSlotRef` only. - - [ ] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter. - - [ ] JVM tests must be green before commit. +- [x] Step 24B: Frame-slot captures in bytecode runtime. + - [x] Build lambdas from bytecode + capture table (no capture names). + - [x] Read captured values via `FrameSlotRef` only. + - [x] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter. + - [x] JVM tests must be green before commit. - [ ] Step 24C: Remove scope local mirroring in bytecode execution. - [ ] Remove/disable any bytecode runtime code that writes locals into Scope for execution. - [ ] Keep Scope creation only for reflection/Kotlin interop paths. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 827d9f5..57cd93a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2880,22 +2880,31 @@ class Compiler( val context = scope.applyClosure(closureScope, preferredThisType = expectedReceiverType) if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot) if (captureSlots.isNotEmpty()) { - val moduleScope = if (context is ApplyScope) { - var s: Scope? = closureScope - while (s != null && s !is ModuleScope) { - s = s.parent + val captureRecords = closureScope.captureRecords + if (captureRecords != null) { + for (i in captureSlots.indices) { + val rec = captureRecords.getOrNull(i) + ?: closureScope.raiseSymbolNotFound("capture ${captureSlots[i].name} not found") + context.updateSlotFor(captureSlots[i].name, rec) } - s as? ModuleScope } else { - null - } - for (capture in captureSlots) { - if (moduleScope != null && moduleScope.getLocalRecordDirect(capture.name) != null) { - continue + val moduleScope = if (context is ApplyScope) { + var s: Scope? = closureScope + while (s != null && s !is ModuleScope) { + s = s.parent + } + s as? ModuleScope + } else { + null + } + for (capture in captureSlots) { + if (moduleScope != null && moduleScope.getLocalRecordDirect(capture.name) != null) { + continue + } + val rec = closureScope.resolveCaptureRecord(capture.name) + ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found") + context.updateSlotFor(capture.name, rec) } - val rec = closureScope.resolveCaptureRecord(capture.name) - ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found") - context.updateSlotFor(capture.name, rec) } } // Execute lambda body in a closure-aware context. Blocks inside the lambda diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 58d1ccd..b280ff8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -59,6 +59,7 @@ open class Scope( // Enabled by default for child scopes; module/class scopes can ignore it. private val slots: MutableList = mutableListOf() private val nameToSlot: MutableMap = mutableMapOf() + internal var captureRecords: List? = null /** * Auxiliary per-frame map of local bindings (locals declared in this frame). * This helps resolving locals across suspension when slot ownership isn't diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index bc0ec0f..69e679a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -608,16 +608,41 @@ class BytecodeCompiler( } private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? { - lambdaCaptureEntriesByRef[ref]?.let { captures -> - builder.addConst(BytecodeConst.CaptureTable(captures)) + val captureTableId = lambdaCaptureEntriesByRef[ref]?.let { captures -> + if (captures.isEmpty()) return@let null + val resolved = captures.map { entry -> + val slotIndex = resolveCaptureSlot(entry) + BytecodeCaptureEntry( + ownerKind = entry.ownerKind, + ownerScopeId = entry.ownerScopeId, + ownerSlotId = entry.ownerSlotId, + slotIndex = slotIndex + ) + } + builder.addConst(BytecodeConst.CaptureTable(resolved)) } - val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn())) + val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn(), captureTableId)) val slot = allocSlot() builder.emit(Opcode.MAKE_VALUE_FN, id, slot) updateSlotType(slot, SlotType.OBJ) return CompiledValue(slot, SlotType.OBJ) } + private fun resolveCaptureSlot(entry: LambdaCaptureEntry): Int { + val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) + return when (entry.ownerKind) { + CaptureOwnerFrameKind.MODULE -> { + scopeSlotMap[key] + ?: throw BytecodeCompileException("Missing module capture slot for ${entry.ownerScopeId}:${entry.ownerSlotId}", Pos.builtIn) + } + CaptureOwnerFrameKind.LOCAL -> { + val localIndex = localSlotIndexByKey[key] + ?: throw BytecodeCompileException("Missing local capture slot for ${entry.ownerScopeId}:${entry.ownerSlotId}", Pos.builtIn) + scopeSlotCount + localIndex + } + } + } + private fun compileEvalRef(ref: ObjRef): CompiledValue? { val refInfo = when (ref) { is BinaryOpRef -> "BinaryOpRef(${ref.op})" diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt index ce3b84b..7c162bf 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt @@ -33,10 +33,13 @@ sealed class BytecodeConst { data class Ref(val value: net.sergeych.lyng.obj.ObjRef) : BytecodeConst() data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst() data class ListLiteralPlan(val spreads: List) : BytecodeConst() - data class ValueFn(val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord) : BytecodeConst() + data class ValueFn( + val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord, + val captureTableId: Int? = null, + ) : BytecodeConst() data class DeclExec(val executable: net.sergeych.lyng.DeclExecutable) : BytecodeConst() data class SlotPlan(val plan: Map, val captures: List = emptyList()) : BytecodeConst() - data class CaptureTable(val entries: List) : BytecodeConst() + data class CaptureTable(val entries: List) : BytecodeConst() data class ExtensionPropertyDecl( val extTypeName: String, val property: ObjProperty, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt index f85db1a..610e508 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -61,7 +61,7 @@ object CmdDisassembler { "[]" } else { table.entries.joinToString(prefix = "[", postfix = "]") { entry -> - "${entry.ownerKind}#${entry.ownerScopeId}:${entry.ownerSlotId}" + "${entry.ownerKind}#${entry.ownerScopeId}:${entry.ownerSlotId}@s${entry.slotIndex}" } } out.append("k").append(idx).append(" CAPTURE_TABLE ").append(entries).append('\n') diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 7511e2d..901bb4d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -1869,7 +1869,12 @@ class CmdMakeValueFn(internal val id: Int, internal val dst: Int) : Cmd() { } val valueFn = frame.fn.constants.getOrNull(id) as? BytecodeConst.ValueFn ?: error("MAKE_VALUE_FN expects ValueFn at $id") - val result = valueFn.fn(frame.ensureScope()).value + val scope = frame.ensureScope() + val previousCaptures = scope.captureRecords + val captureRecords = valueFn.captureTableId?.let { frame.buildCaptureRecords(it) } + scope.captureRecords = captureRecords + val result = valueFn.fn(scope).value + scope.captureRecords = previousCaptures if (frame.fn.localSlotNames.isNotEmpty()) { frame.syncScopeToFrame() } @@ -1944,6 +1949,37 @@ class CmdFrame( } } + internal fun buildCaptureRecords(captureTableId: Int): List { + val table = fn.constants.getOrNull(captureTableId) as? BytecodeConst.CaptureTable + ?: error("Capture table $captureTableId missing") + return table.entries.map { entry -> + when (entry.ownerKind) { + CaptureOwnerFrameKind.LOCAL -> { + val localIndex = entry.slotIndex - fn.scopeSlotCount + if (localIndex < 0) { + error("Invalid local capture slot ${entry.slotIndex}") + } + val isMutable = fn.localSlotMutables.getOrNull(localIndex) ?: false + val isDelegated = fn.localSlotDelegated.getOrNull(localIndex) ?: false + if (isDelegated) { + val delegate = frame.getObj(localIndex) + ObjRecord(ObjNull, isMutable, type = ObjRecord.Type.Delegated).also { + it.delegate = delegate + } + } else { + ObjRecord(FrameSlotRef(frame, localIndex), isMutable) + } + } + CaptureOwnerFrameKind.MODULE -> { + val slot = entry.slotIndex + val target = scopeTarget(slot) + val index = fn.scopeSlotIndices[slot] + target.getSlotRecord(index) + } + } + } + } + private fun shouldSyncLocalCaptures(captures: List): Boolean { if (captures.isEmpty()) return false val localNames = fn.localSlotNames diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt index e3d74b9..1f2a1ac 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt @@ -23,3 +23,10 @@ data class LambdaCaptureEntry( val ownerScopeId: Int, val ownerSlotId: Int, ) + +data class BytecodeCaptureEntry( + val ownerKind: CaptureOwnerFrameKind, + val ownerScopeId: Int, + val ownerSlotId: Int, + val slotIndex: Int, +)