diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index de46c68..5afaf5b 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -106,9 +106,9 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Keep interpreter path using `ClosureScope` until interpreter removal. - [x] JVM tests must be green before commit. - [x] Step 24E: Isolate interpreter-only capture logic. - - [ ] Mark `resolveCaptureRecord` paths as interpreter-only. - - [ ] Guard or delete any bytecode path that tries to sync captures into scopes. - - [ ] JVM tests must be green before commit. + - [x] Mark `resolveCaptureRecord` paths as interpreter-only. + - [x] Guard or delete any bytecode path that tries to sync captures into scopes. + - [x] JVM tests must be green before commit. ## Interpreter Removal (next) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt index 43194f7..deb3c83 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt @@ -31,23 +31,33 @@ class BlockStatement( val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos) if (slotPlan.isNotEmpty()) target.applySlotPlan(slotPlan) if (captureSlots.isNotEmpty()) { - val applyScope = scope as? ApplyScope - for (capture in captureSlots) { - // Interpreter-only capture resolution; bytecode paths must use captureRecords instead. - val rec = if (applyScope != null) { - applyScope.resolveCaptureRecord(capture.name) - ?: applyScope.callScope.resolveCaptureRecord(capture.name) - } else { - scope.resolveCaptureRecord(capture.name) + val captureRecords = scope.captureRecords + if (captureRecords != null) { + for (i in captureSlots.indices) { + val capture = captureSlots[i] + val rec = captureRecords.getOrNull(i) + ?: scope.raiseSymbolNotFound("capture ${capture.name} not found") + target.updateSlotFor(capture.name, rec) } - if (rec == null) { - if (scope.getSlotIndexOf(capture.name) == null && scope.getLocalRecordDirect(capture.name) == null) { - continue + } else { + val applyScope = scope as? ApplyScope + for (capture in captureSlots) { + // Interpreter-only capture resolution; bytecode paths must use captureRecords instead. + val rec = if (applyScope != null) { + applyScope.resolveCaptureRecord(capture.name) + ?: applyScope.callScope.resolveCaptureRecord(capture.name) + } else { + scope.resolveCaptureRecord(capture.name) } - (applyScope?.callScope ?: scope) - .raiseSymbolNotFound("symbol ${capture.name} not found") + if (rec == null) { + if (scope.getSlotIndexOf(capture.name) == null && scope.getLocalRecordDirect(capture.name) == null) { + continue + } + (applyScope?.callScope ?: scope) + .raiseSymbolNotFound("symbol ${capture.name} not found") + } + target.updateSlotFor(capture.name, rec) } - target.updateSlotFor(capture.name, rec) } } return block.execute(target) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index b116c31..48c4340 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1705,9 +1705,12 @@ class Compiler( private val currentRangeParamNames: Set get() = rangeParamNamesStack.lastOrNull() ?: emptySet() private val capturePlanStack = mutableListOf() + private var lambdaDepth = 0 private data class CapturePlan( val slotPlan: SlotPlan, + val isFunction: Boolean, + val propagateToParentFunction: Boolean, val captures: MutableList = mutableListOf(), val captureMap: MutableMap = mutableMapOf(), val captureOwners: MutableMap = mutableMapOf() @@ -1715,6 +1718,19 @@ class Compiler( private fun recordCaptureSlot(name: String, slotLoc: SlotLocation) { val plan = capturePlanStack.lastOrNull() ?: return + recordCaptureSlotInto(plan, name, slotLoc) + if (plan.propagateToParentFunction) { + for (i in capturePlanStack.size - 2 downTo 0) { + val parent = capturePlanStack[i] + if (parent.isFunction) { + recordCaptureSlotInto(parent, name, slotLoc) + break + } + } + } + } + + private fun recordCaptureSlotInto(plan: CapturePlan, name: String, slotLoc: SlotLocation) { if (plan.captureMap.containsKey(name)) return val capture = CaptureSlot( name = name, @@ -1734,6 +1750,12 @@ class Compiler( private fun captureLocalRef(name: String, slotLoc: SlotLocation, pos: Pos): LocalSlotRef? { if (capturePlanStack.isEmpty() || slotLoc.depth == 0) return null + val functionPlan = capturePlanStack.asReversed().firstOrNull { it.isFunction } ?: return null + val functionIndex = functionPlan?.let { plan -> + slotPlanStack.indexOfLast { it.id == plan.slotPlan.id } + } ?: -1 + val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId } + if (functionIndex >= 0 && scopeIndex >= functionIndex) return null val moduleId = moduleSlotPlan()?.id if (moduleId != null && slotLoc.scopeId == moduleId) return null recordCaptureSlot(name, slotLoc) @@ -2158,6 +2180,7 @@ class Compiler( stmt.label, stmt.canBreak, stmt.loopSlotPlan, + stmt.loopScopeId, stmt.pos ) } @@ -2843,7 +2866,7 @@ class Compiler( label?.let { cc.labels.add(it) } slotPlanStack.add(paramSlotPlan) - val capturePlan = CapturePlan(paramSlotPlan) + val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = false) capturePlanStack.add(capturePlan) val parsedBody = try { inCodeContext(CodeContext.Function("", implicitThisMembers = true, implicitThisTypeName = expectedReceiverType)) { @@ -2854,8 +2877,13 @@ class Compiler( for (param in slotParamNames) { resolutionSink?.declareSymbol(param, SymbolKind.PARAM, isMutable = false, pos = startPos) } - withLocalNames(slotParamNames.toSet()) { - parseBlock(skipLeadingBrace = true) + lambdaDepth += 1 + try { + withLocalNames(slotParamNames.toSet()) { + parseBlock(skipLeadingBrace = true) + } + } finally { + lambdaDepth -= 1 } } finally { resolutionSink?.exitScope(cc.currentPos()) @@ -2872,25 +2900,56 @@ class Compiler( val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan) val captureSlots = capturePlan.captures.toList() val returnClass = inferReturnClassFromStatement(body) + val paramKnownClasses = mutableMapOf() + argsDeclaration?.params?.forEach { param -> + val cls = resolveTypeDeclObjClass(param.type) ?: return@forEach + paramKnownClasses[param.name] = cls + } + val returnLabels = label?.let { setOf(it) } ?: emptySet() + val fnStatements = if (useBytecodeStatements && !containsUnsupportedForBytecode(body)) { + returnLabelStack.addLast(returnLabels) + try { + wrapFunctionBytecode(body, "", paramKnownClasses) + } catch (e: net.sergeych.lyng.bytecode.BytecodeCompileException) { + body + } finally { + returnLabelStack.removeLast() + } + } else { + body + } val ref = ValueFnRef { closureScope -> - val stmt = object : Statement() { - override val pos: Pos = body.pos + val captureRecords = closureScope.captureRecords + val stmt = object : Statement(), BytecodeBodyProvider { + override val pos: Pos = fnStatements.pos + + override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement + override suspend fun execute(scope: Scope): Obj { - // and the source closure of the lambda which might have other thisObj. - val useBytecodeClosure = closureScope.captureRecords != null - val context = if (useBytecodeClosure) { - scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType) + // TODO(bytecode): remove this fallback once lambdas are fully bytecode-backed (Step 26). + val usesBytecodeBody = fnStatements is BytecodeStatement && + (captureSlots.isEmpty() || captureRecords != null) + val useBytecodeClosure = captureRecords != null && usesBytecodeBody + val context = if (usesBytecodeBody) { + scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also { + it.args = scope.args + } } else { scope.applyClosure(closureScope, preferredThisType = expectedReceiverType) } if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot) if (captureSlots.isNotEmpty()) { - 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) + val records = captureRecords as List + if (useBytecodeClosure) { + context.captureRecords = records + context.captureNames = captureSlots.map { it.name } + } else { + for (i in captureSlots.indices) { + val rec = records.getOrNull(i) + ?: closureScope.raiseSymbolNotFound("capture ${captureSlots[i].name} not found") + context.updateSlotFor(captureSlots[i].name, rec) + } } } else { val moduleScope = if (context is ApplyScope) { @@ -2902,38 +2961,44 @@ class Compiler( } else { null } + val usesBytecodeBody = fnStatements is BytecodeStatement + val resolvedRecords = if (usesBytecodeBody) ArrayList() else null + val resolvedNames = if (usesBytecodeBody) ArrayList() 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) + if (usesBytecodeBody) { + resolvedRecords?.add(rec) + resolvedNames?.add(capture.name) + } else { + context.updateSlotFor(capture.name, rec) + } + } + if (usesBytecodeBody) { + context.captureRecords = resolvedRecords + context.captureNames = resolvedNames } } } - // Execute lambda body in a closure-aware context. Blocks inside the lambda - // will create child scopes as usual, so re-declarations inside loops work. if (argsDeclaration == null) { - // no args: automatic var 'it' val l = scope.args.list val itValue: Obj = when (l.size) { - // no args: it == void 0 -> ObjVoid - // one args: it is this arg 1 -> l[0] - // more args: it is a list of args else -> ObjList(l.toMutableList()) } context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument) } else { - // assign vars as declared the standard way - argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val) + argsDeclaration.assignToContext(context, scope.args, defaultAccessType = AccessType.Val) } + val effectiveStatements = if (usesBytecodeBody) fnStatements else body return try { - body.execute(context) + effectiveStatements.execute(context) } catch (e: ReturnException) { - if (e.label == null || e.label == label) e.result + if (e.label == null || returnLabels.contains(e.label)) e.result else throw e } } @@ -2948,22 +3013,27 @@ class Compiler( if (returnClass != null) { lambdaReturnTypeByRef[ref] = returnClass } - val moduleScopeId = moduleSlotPlan()?.id - val captureEntries = captureSlots.map { capture -> - val owner = capturePlan.captureOwners[capture.name] - ?: error("Missing capture owner for ${capture.name}") - val kind = if (moduleScopeId != null && owner.scopeId == moduleScopeId) { - net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.MODULE - } else { - net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.LOCAL + if (captureSlots.isNotEmpty()) { + val moduleScopeId = moduleSlotPlan()?.id + val captureEntries = captureSlots.map { capture -> + val owner = capturePlan.captureOwners[capture.name] + ?: error("Missing capture owner for ${capture.name}") + val kind = if (moduleScopeId != null && owner.scopeId == moduleScopeId) { + net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.MODULE + } else { + net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.LOCAL + } + net.sergeych.lyng.bytecode.LambdaCaptureEntry( + ownerKind = kind, + ownerScopeId = owner.scopeId, + ownerSlotId = owner.slot, + ownerName = capture.name, + ownerIsMutable = owner.isMutable, + ownerIsDelegated = owner.isDelegated + ) } - net.sergeych.lyng.bytecode.LambdaCaptureEntry( - ownerKind = kind, - ownerScopeId = owner.scopeId, - ownerSlotId = owner.slot - ) + lambdaCaptureEntriesByRef[ref] = captureEntries } - lambdaCaptureEntriesByRef[ref] = captureEntries return ref } @@ -3039,7 +3109,7 @@ class Compiler( return when (t.value) { "class" -> { val ref = ValueFnRef { scope -> - operand.get(scope).value.objClass.asReadonly + operand.evalValue(scope).objClass.asReadonly } lambdaReturnTypeByRef[ref] = ObjClassType ref @@ -4162,6 +4232,7 @@ class Compiler( val payload = inferEncodedPayloadClass(ref.args) if (payload != null) return payload } + val receiverClass = resolveReceiverClassForMember(ref.receiver) return inferMethodCallReturnClass(ref.name) } @@ -6393,6 +6464,7 @@ class Compiler( label = label, canBreak = canBreak, loopSlotPlan = loopSlotPlanSnapshot, + loopScopeId = loopSlotPlan.id, pos = body.pos ) } else { @@ -6818,7 +6890,7 @@ class Compiler( val typeParamNames = mergedTypeParamDecls.map { it.name } val paramNames: Set = paramNamesList.toSet() val paramSlotPlan = buildParamSlotPlan(paramNamesList + typeParamNames) - val capturePlan = CapturePlan(paramSlotPlan) + val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = false) val rangeParamNames = argsDeclaration.params .filter { isRangeType(it.type) } .map { it.name } @@ -6945,11 +7017,17 @@ class Compiler( if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot) val captureBase = closureBox.captureContext ?: closureBox.closure if (captureBase != null && captureSlots.isNotEmpty()) { - for (capture in captureSlots) { - // Interpreter-only capture resolution; bytecode functions do not use resolveCaptureRecord. - val rec = captureBase.resolveCaptureRecord(capture.name) - ?: captureBase.raiseSymbolNotFound("symbol ${capture.name} not found") - context.updateSlotFor(capture.name, rec) + if (fnStatements is BytecodeStatement) { + captureBase.raiseIllegalState( + "bytecode function captures require frame slots; scope capture resolution disabled" + ) + } else { + for (capture in captureSlots) { + // Interpreter-only capture resolution; bytecode functions do not use resolveCaptureRecord. + val rec = captureBase.resolveCaptureRecord(capture.name) + ?: captureBase.raiseSymbolNotFound("symbol ${capture.name} not found") + context.updateSlotFor(capture.name, rec) + } } } @@ -7040,7 +7118,7 @@ class Compiler( resolutionSink?.declareSymbol(name, SymbolKind.LOCAL, isMutable, startPos, isOverride = false) } slotPlanStack.add(exprSlotPlan) - val capturePlan = CapturePlan(exprSlotPlan) + val capturePlan = CapturePlan(exprSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0) capturePlanStack.add(capturePlan) val expr = try { parseExpression() ?: throw ScriptError(cc.current().pos, "Expected expression") @@ -7062,7 +7140,7 @@ class Compiler( declareSlotNameIn(blockSlotPlan, name, isMutable, isDelegated = false) } slotPlanStack.add(blockSlotPlan) - val capturePlan = CapturePlan(blockSlotPlan) + val capturePlan = CapturePlan(blockSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0) capturePlanStack.add(capturePlan) val expr = try { parseExpression() ?: throw ScriptError(cc.current().pos, "Expected expression") @@ -7379,7 +7457,7 @@ class Compiler( } } slotPlanStack.add(blockSlotPlan) - val capturePlan = CapturePlan(blockSlotPlan) + val capturePlan = CapturePlan(blockSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0) capturePlanStack.add(capturePlan) val block = try { parseScript() @@ -7410,7 +7488,7 @@ class Compiler( resolutionSink?.enterScope(ScopeKind.BLOCK, startPos, null) val blockSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++) slotPlanStack.add(blockSlotPlan) - val capturePlan = CapturePlan(blockSlotPlan) + val capturePlan = CapturePlan(blockSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0) capturePlanStack.add(capturePlan) val block = try { parseScript() @@ -7440,7 +7518,7 @@ class Compiler( resolutionSink?.enterScope(ScopeKind.BLOCK, startPos, null) val blockSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++) slotPlanStack.add(blockSlotPlan) - val capturePlan = CapturePlan(blockSlotPlan) + val capturePlan = CapturePlan(blockSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0) capturePlanStack.add(capturePlan) val block = try { parseScript() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt index 17384e7..2ff3cd3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt @@ -55,3 +55,16 @@ class FrameSlotRef( } } } + +class RecordSlotRef( + private val record: ObjRecord, +) : net.sergeych.lyng.obj.Obj() { + fun read(): Obj { + val direct = record.value + return if (direct is FrameSlotRef) direct.read() else direct + } + + fun write(value: Obj) { + record.value = value + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index ea3cdc0..1d7b233 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -60,6 +60,7 @@ open class Scope( private val slots: MutableList = mutableListOf() private val nameToSlot: MutableMap = mutableMapOf() internal var captureRecords: List? = null + internal var captureNames: 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 76d2085..f045065 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -53,10 +53,13 @@ class BytecodeCompiler( private val localSlotInfoMap = LinkedHashMap() private val localSlotIndexByKey = LinkedHashMap() private val localSlotIndexByName = LinkedHashMap() + private val captureSlotKeys = LinkedHashSet() + private val forcedObjSlots = LinkedHashSet() private val loopSlotOverrides = LinkedHashMap() private var localSlotNames = emptyArray() private var localSlotMutables = BooleanArray(0) private var localSlotDelegated = BooleanArray(0) + private var localSlotCaptures = BooleanArray(0) private val declaredLocalKeys = LinkedHashSet() private val localRangeRefs = LinkedHashMap() private val slotTypes = mutableMapOf() @@ -65,6 +68,7 @@ class BytecodeCompiler( private val knownClassNames = knownNameObjClass.keys.toSet() private val slotInitClassByKey = mutableMapOf() private val intLoopVarNames = LinkedHashSet() + private val valueFnRefs = LinkedHashSet() private val loopStack = ArrayDeque() private var forceScopeSlots = false private var currentPos: Pos? = null @@ -100,8 +104,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is BlockStatement -> compileBlock(name, stmt) @@ -120,8 +125,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is DestructuringVarDeclStatement -> { @@ -137,8 +143,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is net.sergeych.lyng.ThrowStatement -> compileThrowStatement(name, stmt) @@ -156,8 +163,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is net.sergeych.lyng.ClassDeclStatement -> { @@ -173,8 +181,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is net.sergeych.lyng.FunctionDeclStatement -> { @@ -190,8 +199,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is net.sergeych.lyng.EnumDeclStatement -> { @@ -207,8 +217,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } is net.sergeych.lyng.NopStatement -> { @@ -225,8 +236,9 @@ class BytecodeCompiler( scopeSlotNames, scopeSlotIsModule, localSlotNames, - localSlotMutables, - localSlotDelegated + localSlotMutables, + localSlotDelegated, + localSlotCaptures ) } else -> null @@ -246,8 +258,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileExtensionPropertyDecl( @@ -268,8 +281,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } fun compileExpression(name: String, stmt: ExpressionStatement): CmdFunction? { @@ -287,8 +301,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private data class CompiledValue(val slot: Int, val type: SlotType) @@ -358,16 +373,16 @@ class BytecodeCompiler( } if (allowLocalSlots) { if (!forceScopeSlots) { - scopeSlotIndexByName[ref.name]?.let { slot -> - val resolved = slotTypes[slot] ?: SlotType.UNKNOWN - return CompiledValue(slot, resolved) - } val localIndex = localSlotIndexByName[ref.name] if (localIndex != null) { val slot = scopeSlotCount + localIndex val resolved = slotTypes[slot] ?: SlotType.UNKNOWN return CompiledValue(slot, resolved) } + scopeSlotIndexByName[ref.name]?.let { slot -> + val resolved = slotTypes[slot] ?: SlotType.UNKNOWN + return CompiledValue(slot, resolved) + } } if (forceScopeSlots) { scopeSlotIndexByName[ref.name]?.let { slot -> @@ -388,16 +403,16 @@ class BytecodeCompiler( } if (allowLocalSlots) { if (!forceScopeSlots) { - scopeSlotIndexByName[ref.name]?.let { slot -> - val resolved = slotTypes[slot] ?: SlotType.UNKNOWN - return CompiledValue(slot, resolved) - } val localIndex = localSlotIndexByName[ref.name] if (localIndex != null) { val slot = scopeSlotCount + localIndex val resolved = slotTypes[slot] ?: SlotType.UNKNOWN return CompiledValue(slot, resolved) } + scopeSlotIndexByName[ref.name]?.let { slot -> + val resolved = slotTypes[slot] ?: SlotType.UNKNOWN + return CompiledValue(slot, resolved) + } } if (forceScopeSlots) { scopeSlotIndexByName[ref.name]?.let { slot -> @@ -3970,8 +3985,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileForIn(name: String, stmt: net.sergeych.lyng.ForInStatement): CmdFunction? { @@ -3988,8 +4004,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileWhile(name: String, stmt: net.sergeych.lyng.WhileStatement): CmdFunction? { @@ -4007,8 +4024,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileDoWhile(name: String, stmt: net.sergeych.lyng.DoWhileStatement): CmdFunction? { @@ -4026,8 +4044,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileBlock(name: String, stmt: BlockStatement): CmdFunction? { @@ -4048,8 +4067,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileVarDecl(name: String, stmt: VarDeclStatement): CmdFunction? { @@ -4066,8 +4086,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileStatementValue(stmt: Statement): CompiledValue? { @@ -4520,8 +4541,9 @@ class BytecodeCompiler( scopeSlotIsModule, localSlotNames, localSlotMutables, - localSlotDelegated - ) + localSlotDelegated, + localSlotCaptures + ) } private fun compileLoopBody(stmt: Statement, needResult: Boolean): CompiledValue? { @@ -4788,7 +4810,9 @@ class BytecodeCompiler( val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null val loopSlotPlan = stmt.loopSlotPlan var useLoopScope = loopSlotPlan.isNotEmpty() - val loopLocalIndex = localSlotIndexByName[stmt.loopVarName] + val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] + val loopKey = loopSlotIndex?.let { ScopeSlotKey(stmt.loopScopeId, it) } + val loopLocalIndex = loopKey?.let { localSlotIndexByKey[it] } ?: localSlotIndexByName[stmt.loopVarName] var usedOverride = false var loopSlotId = when { loopLocalIndex != null -> scopeSlotCount + loopLocalIndex @@ -4905,7 +4929,7 @@ class BytecodeCompiler( val nextSlot = allocSlot() builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, nextMethodId, 0, 0, nextSlot) val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN)) - builder.emit(Opcode.MOVE_OBJ, nextObj.slot, loopSlotId) + emitMove(CompiledValue(nextObj.slot, SlotType.OBJ), loopSlotId) updateSlotType(loopSlotId, SlotType.OBJ) updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ) if (emitDeclLocal) { @@ -5012,7 +5036,7 @@ class BytecodeCompiler( Opcode.JMP_IF_TRUE, listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel)) ) - builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId) + emitMove(CompiledValue(iSlot, SlotType.INT), loopSlotId) updateSlotType(loopSlotId, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT) if (emitDeclLocal) { @@ -6084,16 +6108,22 @@ class BytecodeCompiler( scopeSlotIndexByName[ref.name]?.let { return it } } if (ref.captureOwnerScopeId != null) { - val ownerKey = ScopeSlotKey(ref.captureOwnerScopeId, ref.captureOwnerSlot ?: refSlot(ref)) - val ownerLocal = localSlotIndexByKey[ownerKey] - if (ownerLocal != null) { - return scopeSlotCount + ownerLocal + val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref)) + val localIndex = localSlotIndexByKey[scopeKey] + if (localIndex != null) { + if (localSlotCaptures.getOrNull(localIndex) == true) { + return scopeSlotCount + localIndex + } } val nameLocal = localSlotIndexByName[ref.name] - if (nameLocal != null) { + if (nameLocal != null && localSlotCaptures.getOrNull(nameLocal) == true) { return scopeSlotCount + nameLocal } - val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref)) + for (idx in localSlotNames.indices) { + if (localSlotNames[idx] != ref.name) continue + if (localSlotCaptures.getOrNull(idx) != true) continue + return scopeSlotCount + idx + } return scopeSlotMap[scopeKey] } if (ref.isDelegated) { @@ -6117,6 +6147,7 @@ class BytecodeCompiler( } private fun updateSlotType(slot: Int, type: SlotType) { + if (forcedObjSlots.contains(slot) && type != SlotType.OBJ) return if (type == SlotType.UNKNOWN) { slotTypes.remove(slot) } else { @@ -6141,18 +6172,22 @@ class BytecodeCompiler( localSlotInfoMap.clear() localSlotIndexByKey.clear() localSlotIndexByName.clear() + captureSlotKeys.clear() + forcedObjSlots.clear() loopSlotOverrides.clear() scopeSlotIndexByName.clear() pendingScopeNameRefs.clear() localSlotNames = emptyArray() localSlotMutables = BooleanArray(0) localSlotDelegated = BooleanArray(0) + localSlotCaptures = BooleanArray(0) declaredLocalKeys.clear() localRangeRefs.clear() intLoopVarNames.clear() + valueFnRefs.clear() addrSlotByScopeSlot.clear() loopStack.clear() - forceScopeSlots = allowLocalSlots && containsValueFnRef(stmt) + forceScopeSlots = false if (slotTypeByScopeId.isNotEmpty()) { for ((scopeId, slots) in slotTypeByScopeId) { for ((slotIndex, cls) in slots) { @@ -6167,6 +6202,22 @@ class BytecodeCompiler( if (allowLocalSlots) { collectLoopSlotPlans(stmt, 0) } + if (allowLocalSlots && valueFnRefs.isNotEmpty() && lambdaCaptureEntriesByRef.isNotEmpty()) { + for (ref in valueFnRefs) { + val entries = lambdaCaptureEntriesByRef[ref] ?: continue + for (entry in entries) { + if (entry.ownerKind != CaptureOwnerFrameKind.LOCAL) continue + val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) + if (!localSlotInfoMap.containsKey(key)) { + localSlotInfoMap[key] = LocalSlotInfo( + entry.ownerName, + entry.ownerIsMutable, + entry.ownerIsDelegated + ) + } + } + } + } if (pendingScopeNameRefs.isNotEmpty()) { val existingNames = HashSet(scopeSlotNameMap.values) var maxSlotIndex = scopeSlotMap.keys.maxOfOrNull { it.slot } ?: -1 @@ -6209,6 +6260,21 @@ class BytecodeCompiler( localSlotMutables = mutables localSlotDelegated = delegated } + localSlotCaptures = BooleanArray(localSlotNames.size) + if (captureSlotKeys.isNotEmpty()) { + for (key in captureSlotKeys) { + val localIndex = localSlotIndexByKey[key] ?: continue + val slot = scopeSlotCount + localIndex + localSlotCaptures[localIndex] = true + forcedObjSlots.add(slot) + slotTypes[slot] = SlotType.OBJ + } + } + for (i in localSlotNames.indices) { + if (localSlotCaptures.getOrNull(i) != true) continue + val name = localSlotNames[i] ?: continue + localSlotIndexByName[name] = i + } if (scopeSlotCount > 0) { for ((key, index) in scopeSlotMap) { val name = scopeSlotNameMap[key] ?: continue @@ -6572,12 +6638,12 @@ class BytecodeCompiler( val scopeId = refScopeId(ref) val key = ScopeSlotKey(scopeId, refSlot(ref)) if (ref.captureOwnerScopeId != null) { - if (!scopeSlotMap.containsKey(key)) { - scopeSlotMap[key] = scopeSlotMap.size - } - if (!scopeSlotNameMap.containsKey(key)) { - scopeSlotNameMap[key] = ref.name + if (allowLocalSlots) { + if (!localSlotInfoMap.containsKey(key)) { + localSlotInfoMap[key] = LocalSlotInfo(ref.name, ref.isMutable, ref.isDelegated) + } } + captureSlotKeys.add(key) return } val shouldLocalize = ref.isDelegated || !forceScopeSlots || intLoopVarNames.contains(ref.name) @@ -6622,12 +6688,12 @@ class BytecodeCompiler( val scopeId = refScopeId(target) val key = ScopeSlotKey(scopeId, refSlot(target)) if (target.captureOwnerScopeId != null) { - if (!scopeSlotMap.containsKey(key)) { - scopeSlotMap[key] = scopeSlotMap.size - } - if (!scopeSlotNameMap.containsKey(key)) { - scopeSlotNameMap[key] = target.name + if (allowLocalSlots) { + if (!localSlotInfoMap.containsKey(key)) { + localSlotInfoMap[key] = LocalSlotInfo(target.name, target.isMutable, target.isDelegated) + } } + captureSlotKeys.add(key) } else { val shouldLocalize = target.isDelegated || !forceScopeSlots || intLoopVarNames.contains(target.name) val isModuleSlot = if (target.isDelegated) false else isModuleSlot(scopeId, target.name) @@ -6656,6 +6722,9 @@ class BytecodeCompiler( collectScopeSlotsRef(ref.target) collectScopeSlotsRef(ref.value) } + is ValueFnRef -> { + valueFnRefs.add(ref) + } is AssignIfNullRef -> { collectScopeSlotsRef(ref.target) collectScopeSlotsRef(ref.value) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index b816f61..08d075f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -212,6 +212,7 @@ class BytecodeStatement private constructor( stmt.label, stmt.canBreak, stmt.loopSlotPlan, + stmt.loopScopeId, stmt.pos ) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index b39c466..95dff74 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -69,7 +69,8 @@ class CmdBuilder { scopeSlotIsModule: BooleanArray = BooleanArray(0), localSlotNames: Array = emptyArray(), localSlotMutables: BooleanArray = BooleanArray(0), - localSlotDelegated: BooleanArray = BooleanArray(0) + localSlotDelegated: BooleanArray = BooleanArray(0), + localSlotCaptures: BooleanArray = BooleanArray(0) ): CmdFunction { val scopeSlotCount = scopeSlotIndices.size require(scopeSlotNames.isEmpty() || scopeSlotNames.size == scopeSlotCount) { @@ -80,6 +81,7 @@ class CmdBuilder { } require(localSlotNames.size == localSlotMutables.size) { "local slot metadata size mismatch" } require(localSlotNames.size == localSlotDelegated.size) { "local slot delegation size mismatch" } + require(localSlotNames.size == localSlotCaptures.size) { "local slot capture size mismatch" } val labelIps = mutableMapOf() for ((label, idx) in labelPositions) { labelIps[label] = idx @@ -114,6 +116,7 @@ class CmdBuilder { localSlotNames = localSlotNames, localSlotMutables = localSlotMutables, localSlotDelegated = localSlotDelegated, + localSlotCaptures = localSlotCaptures, constants = constPool.toList(), cmds = cmds.toTypedArray(), posByIp = posByInstr.toTypedArray() @@ -150,7 +153,8 @@ class CmdBuilder { listOf(OperandKind.SLOT, OperandKind.ADDR) Opcode.CONST_NULL -> listOf(OperandKind.SLOT) - Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL, Opcode.MAKE_VALUE_FN -> + Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL, + Opcode.MAKE_VALUE_FN -> listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) 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 b35328a..b453a46 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -279,7 +279,8 @@ object CmdDisassembler { listOf(OperandKind.SLOT, OperandKind.ADDR) Opcode.CONST_NULL -> listOf(OperandKind.SLOT) - Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL, Opcode.MAKE_VALUE_FN -> + Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL, + Opcode.MAKE_VALUE_FN -> listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt index 829a915..2590756 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt @@ -29,6 +29,7 @@ data class CmdFunction( val localSlotNames: Array, val localSlotMutables: BooleanArray, val localSlotDelegated: BooleanArray, + val localSlotCaptures: BooleanArray, val constants: List, val cmds: Array, val posByIp: Array, @@ -39,6 +40,7 @@ data class CmdFunction( require(scopeSlotIsModule.size == scopeSlotCount) { "scopeSlotIsModule size mismatch" } require(localSlotNames.size == localSlotMutables.size) { "localSlot metadata size mismatch" } require(localSlotNames.size == localSlotDelegated.size) { "localSlot delegation size mismatch" } + require(localSlotNames.size == localSlotCaptures.size) { "localSlot capture size mismatch" } require(localSlotNames.size <= localCount) { "localSlotNames exceed localCount" } require(addrCount >= 0) { "addrCount must be non-negative" } if (posByIp.isNotEmpty()) { 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 0923a05..ee03378 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -26,6 +26,7 @@ class CmdVm { suspend fun execute(fn: CmdFunction, scope0: Scope, args: List): Obj { result = null val frame = CmdFrame(this, fn, scope0, args) + frame.applyCaptureRecords() val cmds = fn.cmds if (fn.localSlotNames.isNotEmpty()) { frame.syncScopeToFrame() @@ -236,12 +237,23 @@ class CmdAssertIs(internal val objSlot: Int, internal val typeSlot: Int) : Cmd() override suspend fun perform(frame: CmdFrame) { val obj = frame.slotToObj(objSlot) val typeObj = frame.slotToObj(typeSlot) - val clazz = typeObj as? ObjClass ?: frame.ensureScope().raiseClassCastError( - "${typeObj.inspect(frame.ensureScope())} is not the class instance" - ) - if (!obj.isInstanceOf(clazz)) { - frame.ensureScope().raiseClassCastError( - "Cannot cast ${obj.objClass.className} to ${clazz.className}" + when (typeObj) { + is ObjClass -> { + if (!obj.isInstanceOf(typeObj)) { + frame.ensureScope().raiseClassCastError( + "Cannot cast ${obj.objClass.className} to ${typeObj.className}" + ) + } + } + is ObjTypeExpr -> { + if (!matchesTypeDecl(frame.ensureScope(), obj, typeObj.typeDecl)) { + frame.ensureScope().raiseClassCastError( + "Cannot cast ${obj.objClass.className} to ${typeObj.typeDecl}" + ) + } + } + else -> frame.ensureScope().raiseClassCastError( + "${typeObj.inspect(frame.ensureScope())} is not the class instance" ) } return @@ -256,17 +268,22 @@ class CmdMakeQualifiedView( override suspend fun perform(frame: CmdFrame) { val obj0 = frame.slotToObj(objSlot) val typeObj = frame.slotToObj(typeSlot) - val clazz = typeObj as? ObjClass ?: frame.ensureScope().raiseClassCastError( - "${typeObj.inspect(frame.ensureScope())} is not the class instance" - ) val base = when (obj0) { is ObjQualifiedView -> obj0.instance else -> obj0 } - val result = if (base is ObjInstance && base.isInstanceOf(clazz)) { - ObjQualifiedView(base, clazz) - } else { - base + val result = when (typeObj) { + is ObjClass -> { + if (base is ObjInstance && base.isInstanceOf(typeObj)) { + ObjQualifiedView(base, typeObj) + } else { + base + } + } + is ObjTypeExpr -> base + else -> frame.ensureScope().raiseClassCastError( + "${typeObj.inspect(frame.ensureScope())} is not the class instance" + ) } frame.storeObjResult(dst, result) return @@ -1488,6 +1505,9 @@ class CmdCallSlot( } else { // Pooling for Statement-based callables (lambdas) can still alter closure semantics; keep safe path for now. val scope = frame.ensureScope() + if (callee is Statement && callee !is BytecodeStatement) { + frame.syncFrameToScope(useRefs = true) + } callee.callOn(scope.createChildScope(scope.pos, args = args)) } frame.storeObjResult(dst, result) @@ -1938,6 +1958,57 @@ class CmdFrame( } } + internal fun applyCaptureRecords() { + val captureRecords = scope.captureRecords ?: return + val captureNames = scope.captureNames ?: return + val localNames = fn.localSlotNames + if (localNames.isEmpty()) return + for (i in captureNames.indices) { + val name = captureNames[i] + val record = captureRecords.getOrNull(i) ?: continue + var localIndex = -1 + for (idx in localNames.indices) { + if (localNames[idx] != name) continue + if (fn.localSlotCaptures.getOrNull(idx) != true) continue + localIndex = idx + break + } + if (localIndex < 0) { + for (idx in localNames.indices) { + if (localNames[idx] != name) continue + localIndex = idx + break + } + } + if (localIndex < 0) continue + if (record.type == ObjRecord.Type.Delegated) { + frame.setObj(localIndex, record.delegate ?: ObjNull) + } else { + val value = record.value + if (value is FrameSlotRef) { + frame.setObj(localIndex, value) + } else { + frame.setObj(localIndex, RecordSlotRef(record)) + } + } + for (idx in localNames.indices) { + if (idx == localIndex) continue + if (localNames[idx] != name) continue + if (fn.localSlotCaptures.getOrNull(idx) == true) continue + if (record.type == ObjRecord.Type.Delegated) { + frame.setObj(idx, record.delegate ?: ObjNull) + } else { + val value = record.value + if (value is FrameSlotRef) { + frame.setObj(idx, value) + } else { + frame.setObj(idx, RecordSlotRef(record)) + } + } + } + } + } + internal fun buildCaptureRecords(captureTableId: Int): List { val table = fn.constants.getOrNull(captureTableId) as? BytecodeConst.CaptureTable ?: error("Capture table $captureTableId missing") @@ -1987,7 +2058,11 @@ class CmdFrame( .firstOrNull { fn.scopeSlotIsModule.getOrNull(it) == true } ?.let { fn.scopeSlotNames[it] } if (moduleSlotName != null) { - findScopeWithSlot(scope, moduleSlotName)?.let { return it } + val bySlot = findScopeWithSlot(scope, moduleSlotName) + bySlot?.let { return it } + val byRecord = findScopeWithRecord(scope, moduleSlotName) + byRecord?.let { return it } + return scope } findModuleScope(scope)?.let { return it } return scope @@ -2013,15 +2088,14 @@ class CmdFrame( return null } - private fun findModuleScope(scope: Scope): Scope? { + private fun findScopeWithRecord(scope: Scope, name: String): Scope? { val visited = HashSet(16) val queue = ArrayDeque() queue.add(scope) while (queue.isNotEmpty()) { val current = queue.removeFirst() if (!visited.add(current)) continue - if (current is ModuleScope) return current - if (current.parent is ModuleScope) return current + if (current.getLocalRecordDirect(name) != null) return current current.parent?.let { queue.add(it) } if (current is ClosureScope) { queue.add(current.closureScope) @@ -2034,14 +2108,15 @@ class CmdFrame( return null } - private fun findScopeWithRecord(scope: Scope, name: String): Scope? { + private fun findModuleScope(scope: Scope): Scope? { val visited = HashSet(16) val queue = ArrayDeque() queue.add(scope) while (queue.isNotEmpty()) { val current = queue.removeFirst() if (!visited.add(current)) continue - if (current.getLocalRecordDirect(name) != null) return current + if (current is ModuleScope) return current + if (current.parent is ModuleScope) return current current.parent?.let { queue.add(it) } if (current is ClosureScope) { queue.add(current.closureScope) @@ -2215,7 +2290,7 @@ class CmdFrame( return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot) } else { - frame.getObj(slot - fn.scopeSlotCount) + localSlotToObj(slot - fn.scopeSlotCount) } } @@ -2225,7 +2300,17 @@ class CmdFrame( val index = ensureScopeSlot(target, slot) target.setSlotValue(index, value) } else { - frame.setObj(slot - fn.scopeSlotCount, value) + val localIndex = slot - fn.scopeSlotCount + val existing = frame.getObj(localIndex) + if (existing is FrameSlotRef) { + existing.write(value) + return + } + if (existing is RecordSlotRef) { + existing.write(value) + return + } + frame.setObj(localIndex, value) } } @@ -2238,7 +2323,14 @@ class CmdFrame( SlotType.INT.code -> frame.getInt(local) SlotType.REAL.code -> frame.getReal(local).toLong() SlotType.BOOL.code -> if (frame.getBool(local)) 1L else 0L - SlotType.OBJ.code -> frame.getObj(local).toLong() + SlotType.OBJ.code -> { + val obj = frame.getObj(local) + when (obj) { + is FrameSlotRef -> obj.read().toLong() + is RecordSlotRef -> obj.read().toLong() + else -> obj.toLong() + } + } else -> 0L } } @@ -2252,7 +2344,17 @@ class CmdFrame( val index = ensureScopeSlot(target, slot) target.setSlotValue(index, ObjInt.of(value)) } else { - frame.setInt(slot - fn.scopeSlotCount, value) + val localIndex = slot - fn.scopeSlotCount + val existing = frame.getObj(localIndex) + if (existing is FrameSlotRef) { + existing.write(ObjInt.of(value)) + return + } + if (existing is RecordSlotRef) { + existing.write(ObjInt.of(value)) + return + } + frame.setInt(localIndex, value) } } @@ -2269,7 +2371,14 @@ class CmdFrame( SlotType.REAL.code -> frame.getReal(local) SlotType.INT.code -> frame.getInt(local).toDouble() SlotType.BOOL.code -> if (frame.getBool(local)) 1.0 else 0.0 - SlotType.OBJ.code -> frame.getObj(local).toDouble() + SlotType.OBJ.code -> { + val obj = frame.getObj(local) + when (obj) { + is FrameSlotRef -> obj.read().toDouble() + is RecordSlotRef -> obj.read().toDouble() + else -> obj.toDouble() + } + } else -> 0.0 } } @@ -2281,7 +2390,17 @@ class CmdFrame( val index = ensureScopeSlot(target, slot) target.setSlotValue(index, ObjReal.of(value)) } else { - frame.setReal(slot - fn.scopeSlotCount, value) + val localIndex = slot - fn.scopeSlotCount + val existing = frame.getObj(localIndex) + if (existing is FrameSlotRef) { + existing.write(ObjReal.of(value)) + return + } + if (existing is RecordSlotRef) { + existing.write(ObjReal.of(value)) + return + } + frame.setReal(localIndex, value) } } @@ -2294,7 +2413,14 @@ class CmdFrame( SlotType.BOOL.code -> frame.getBool(local) SlotType.INT.code -> frame.getInt(local) != 0L SlotType.REAL.code -> frame.getReal(local) != 0.0 - SlotType.OBJ.code -> frame.getObj(local).toBool() + SlotType.OBJ.code -> { + val obj = frame.getObj(local) + when (obj) { + is FrameSlotRef -> obj.read().toBool() + is RecordSlotRef -> obj.read().toBool() + else -> obj.toBool() + } + } else -> false } } @@ -2308,7 +2434,17 @@ class CmdFrame( val index = ensureScopeSlot(target, slot) target.setSlotValue(index, if (value) ObjTrue else ObjFalse) } else { - frame.setBool(slot - fn.scopeSlotCount, value) + val localIndex = slot - fn.scopeSlotCount + val existing = frame.getObj(localIndex) + if (existing is FrameSlotRef) { + existing.write(if (value) ObjTrue else ObjFalse) + return + } + if (existing is RecordSlotRef) { + existing.write(if (value) ObjTrue else ObjFalse) + return + } + frame.setBool(localIndex, value) } } @@ -2361,6 +2497,9 @@ class CmdFrame( return getScopeSlotValue(slot) } val local = slot - fn.scopeSlotCount + if (fn.localSlotCaptures.getOrNull(local) == true) { + return localSlotToObj(local) + } val localName = fn.localSlotNames.getOrNull(local) if (localName != null && fn.localSlotDelegated.getOrNull(local) != true) { val rec = scope.getLocalRecordDirect(localName) ?: scope.localBindings[localName] @@ -2372,7 +2511,10 @@ class CmdFrame( SlotType.INT.code -> ObjInt.of(frame.getInt(local)) SlotType.REAL.code -> ObjReal.of(frame.getReal(local)) SlotType.BOOL.code -> if (frame.getBool(local)) ObjTrue else ObjFalse - SlotType.OBJ.code -> frame.getObj(local) + SlotType.OBJ.code -> { + val obj = frame.getObj(local) + if (obj is FrameSlotRef) obj.read() else obj + } else -> ObjVoid } } @@ -2415,6 +2557,7 @@ class CmdFrame( if (names.isEmpty()) return for (i in names.indices) { val name = names[i] ?: continue + if (fn.localSlotCaptures.getOrNull(i) == true) continue if (scopeSlotNames.contains(name)) continue val target = resolveLocalScope(i) ?: continue val isDelegated = fn.localSlotDelegated.getOrNull(i) == true @@ -2450,6 +2593,7 @@ class CmdFrame( if (names.isEmpty()) return for (i in names.indices) { val name = names[i] ?: continue + if (fn.localSlotCaptures.getOrNull(i) == true) continue val target = resolveLocalScope(i) ?: continue val rec = target.getLocalRecordDirect(name) ?: continue if (fn.localSlotDelegated.getOrNull(i) == true && rec.type == ObjRecord.Type.Delegated) { @@ -2459,13 +2603,6 @@ class CmdFrame( } val value = rec.value if (value is FrameSlotRef) { - val resolved = value.read() - when (resolved) { - is ObjInt -> frame.setInt(i, resolved.value) - is ObjReal -> frame.setReal(i, resolved.value) - is ObjBool -> frame.setBool(i, resolved.value) - else -> frame.setObj(i, resolved) - } continue } when (value) { @@ -2563,8 +2700,22 @@ class CmdFrame( SlotType.INT.code -> ObjInt.of(frame.getInt(localIndex)) SlotType.REAL.code -> ObjReal.of(frame.getReal(localIndex)) SlotType.BOOL.code -> if (frame.getBool(localIndex)) ObjTrue else ObjFalse - SlotType.OBJ.code -> frame.getObj(localIndex) - else -> ObjNull + SlotType.OBJ.code -> { + val obj = frame.getObj(localIndex) + when (obj) { + is FrameSlotRef -> obj.read() + is RecordSlotRef -> obj.read() + else -> obj + } + } + else -> { + val obj = frame.getObj(localIndex) + when (obj) { + is FrameSlotRef -> obj.read() + is RecordSlotRef -> obj.read() + else -> obj + } + } } } 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 1f2a1ac..328fe24 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt @@ -22,6 +22,9 @@ data class LambdaCaptureEntry( val ownerKind: CaptureOwnerFrameKind, val ownerScopeId: Int, val ownerSlotId: Int, + val ownerName: String, + val ownerIsMutable: Boolean, + val ownerIsDelegated: Boolean, ) data class BytecodeCaptureEntry( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index a0fc342..bd123c0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -20,6 +20,8 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.* +import net.sergeych.lyng.FrameSlotRef +import net.sergeych.lyng.RecordSlotRef /** * A reference to a value with optional write-back path. @@ -43,7 +45,12 @@ sealed interface ObjRef { if (rec.receiver != null && rec.declaringClass != null) { return rec.receiver!!.resolveRecord(scope, rec, "unknown", rec.declaringClass).value } - return rec.value + val value = rec.value + return when (value) { + is FrameSlotRef -> value.read() + is RecordSlotRef -> value.read() + else -> value + } } suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { throw ScriptError(pos, "can't assign value") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index aea333f..5f0458b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -99,6 +99,7 @@ class ForInStatement( val label: String?, val canBreak: Boolean, val loopSlotPlan: Map, + val loopScopeId: Int, override val pos: Pos, ) : Statement() { override suspend fun execute(scope: Scope): Obj { diff --git a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt index e302f5a..c97cea1 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt @@ -180,6 +180,39 @@ class BytecodeRecentOpsTest { ) } + @Test + fun lambdaCapturesLocalByReference() = runTest { + eval( + """ + fun make() { + var base = 3 + val f = { x -> x + base } + base = 7 + return f(1) + } + assertEquals(8, make()) + """.trimIndent() + ) + } + + @Test + fun lambdaCapturesDelegatedLocal() = runTest { + eval( + """ + class BoxDelegate(var v) : Delegate { + override fun getValue(thisRef: Object, name: String): Object = v + override fun setValue(thisRef: Object, name: String, value: Object) { v = value } + } + fun make() { + var x by BoxDelegate(1) + val f = { y -> x += y; return x } + return f(2) + } + assertEquals(3, make()) + """.trimIndent() + ) + } + @Test fun delegatedMemberAccessAndCall() = runTest { eval(