diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index df072aa..404b262 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -8253,7 +8253,7 @@ class Compiler( ?: context.parent?.get(localName) ?: context.get(localName) ?: continue - val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { + val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) { context.resolve(record, localName) } else { record.value diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt index a014155..d1c2d0c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt @@ -167,6 +167,19 @@ class RecordSlotRef( } } + suspend fun read(scope: Scope, name: String?): Obj { + val direct = record.value + if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) { + return scope.resolve(record, name) + } + return when (direct) { + is FrameSlotRef -> direct.read() + is RecordSlotRef -> direct.read(scope, name) + is ScopeSlotRef -> direct.read() + else -> direct + } + } + override suspend fun callOn(scope: Scope): Obj { val resolved = read() if (resolved === this) { @@ -193,4 +206,18 @@ class RecordSlotRef( record.value = value } } + + suspend fun write(scope: Scope, name: String?, value: Obj): Boolean { + val direct = record.value + if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) { + scope.assign(record, name, value) + return true + } + when (direct) { + is ScopeSlotRef -> direct.write(value) + is RecordSlotRef -> if (direct.write(scope, name, value)) return true else direct.write(value) + else -> record.value = value + } + return false + } } 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 2d302da..44e6cf8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -2396,7 +2396,8 @@ class BytecodeCompiler( builder.emit(Opcode.THROW, posId, msgSlot) return value } - val slot = resolveSlot(localTarget) + val slot = resolveCapturedOwnerScopeSlot(localTarget) + ?: resolveSlot(localTarget) ?: resolveAssignableSlotByName(localTarget.name)?.first ?: return null if (slot < scopeSlotCount && value.type != SlotType.UNKNOWN) { @@ -2702,7 +2703,7 @@ class BytecodeCompiler( return CompiledValue(result, SlotType.OBJ) } if (localTarget.isDelegated) return compileEvalRef(ref) - val slot = resolveSlot(localTarget) ?: return null + val slot = resolveCapturedOwnerScopeSlot(localTarget) ?: resolveSlot(localTarget) ?: return null val targetType = slotTypes[slot] ?: SlotType.OBJ if (!localTarget.isMutable) { if (targetType != SlotType.OBJ && targetType != SlotType.UNKNOWN) return compileEvalRef(ref) @@ -7630,6 +7631,13 @@ class BytecodeCompiler( return scopeSlotMap[scopeKey] } + private fun resolveCapturedOwnerScopeSlot(ref: LocalSlotRef): Int? { + val ownerScopeId = ref.captureOwnerScopeId ?: return null + val ownerSlot = ref.captureOwnerSlot ?: return null + val key = ScopeSlotKey(ownerScopeId, ownerSlot) + return scopeSlotMap[key] + } + private fun updateSlotType(slot: Int, type: SlotType) { if (forcedObjSlots.contains(slot) && type != SlotType.OBJ) return if (type == SlotType.UNKNOWN) { 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 ee71e68..ee3d852 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -50,7 +50,7 @@ class BytecodeStatement private constructor( ?: scope.parent?.get(name) ?: scope.get(name) ?: continue - val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { + val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is net.sergeych.lyng.obj.ObjProperty) { scope.resolve(record, name) } else { record.value 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 e2916e0..eecf13a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -85,6 +85,9 @@ class CmdNop : Cmd() { class CmdMoveObj(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val value = frame.slotToObj(src) + if (frame.writeThroughPropertyLikeSlot(dst, value)) { + return + } if (frame.shouldBypassImmutableWrite(dst)) { frame.setObjUnchecked(dst, value) } else { @@ -2595,7 +2598,7 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List 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 -> { + 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 + } + } + } + } internal fun isFastLocalSlot(slot: Int): Boolean { if (slot < fn.scopeSlotCount) return false val localIndex = slot - fn.scopeSlotCount @@ -4262,7 +4287,7 @@ class CmdFrame( return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot) } else { - localSlotToObj(slot - fn.scopeSlotCount) + readLocalSlotValue(slot - fn.scopeSlotCount) } } @@ -4305,6 +4330,34 @@ class CmdFrame( } } + suspend fun writeThroughPropertyLikeSlot(slot: Int, value: Obj): Boolean { + if (slot < fn.scopeSlotCount) { + val target = scopeTarget(slot) + val index = ensureScopeSlot(target, slot) + val name = fn.scopeSlotNames.getOrNull(slot) + val record = resolveScopeSlotRecordForWrite(target, index, name) + if (name != null && record != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) { + target.assign(record, name, value) + return true + } + return false + } + val localIndex = slot - fn.scopeSlotCount + val name = fn.localSlotNames.getOrNull(localIndex) ?: return false + val raw = frame.getRawObj(localIndex) + if (raw is RecordSlotRef) { + if (raw.write(scope, name, value)) return true + return false + } + if (raw !== ObjUnset && raw !is ObjProperty) return false + val record = scope.parent?.get(name) ?: scope.get(name) ?: return false + if (record.type != ObjRecord.Type.Delegated && record.type != ObjRecord.Type.Property && record.value !is ObjProperty) { + return false + } + scope.assign(record, name, value) + return true + } + suspend fun getInt(slot: Int): Long { return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot).toLong() @@ -4314,14 +4367,7 @@ 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 -> { - val obj = frame.getObj(local) - when (obj) { - is FrameSlotRef -> obj.read().toLong() - is RecordSlotRef -> obj.read().toLong() - else -> obj.toLong() - } - } + SlotType.OBJ.code -> readLocalSlotValue(local).toLong() else -> 0L } } @@ -4401,14 +4447,7 @@ 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 -> { - val obj = frame.getObj(local) - when (obj) { - is FrameSlotRef -> obj.read().toDouble() - is RecordSlotRef -> obj.read().toDouble() - else -> obj.toDouble() - } - } + SlotType.OBJ.code -> readLocalSlotValue(local).toDouble() else -> 0.0 } } @@ -4477,14 +4516,7 @@ 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 -> { - val obj = frame.getObj(local) - when (obj) { - is FrameSlotRef -> obj.read().toBool() - is RecordSlotRef -> obj.read().toBool() - else -> obj.toBool() - } - } + SlotType.OBJ.code -> readLocalSlotValue(local).toBool() else -> false } } @@ -4596,21 +4628,42 @@ class CmdFrame( } val local = slot - fn.scopeSlotCount if (fn.localSlotCaptures.getOrNull(local) == true) { - return localSlotToObj(local) + return readLocalSlotValue(local) } return when (frame.getSlotTypeCode(local)) { 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 -> { - val obj = frame.getObj(local) - when (obj) { - is FrameSlotRef -> obj.read() - is RecordSlotRef -> obj.read() - else -> obj - } + SlotType.OBJ.code -> readLocalSlotValue(local) + else -> readLocalSlotValue(local) + } + } + + fun storedSlotObj(slot: Int): Obj { + if (slot < fn.scopeSlotCount) { + val target = scopeTarget(slot) + val index = ensureScopeSlot(target, slot) + val record = target.getSlotRecord(index) + return when (val direct = record.value) { + is FrameSlotRef -> direct.read() + is RecordSlotRef -> direct.read() + is ScopeSlotRef -> direct.read() + else -> direct } - else -> localSlotToObj(local) + } + val local = slot - fn.scopeSlotCount + return when (frame.getSlotTypeCode(local)) { + 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, SlotType.UNKNOWN.code -> when (val raw = frame.getRawObj(local)) { + is FrameSlotRef -> raw.read() + is RecordSlotRef -> raw.read() + is ScopeSlotRef -> raw.read() + null -> ObjNull + else -> raw + } + else -> frame.getRawObj(local) ?: ObjNull } } @@ -4766,7 +4819,8 @@ class CmdFrame( } } - private fun localSlotToObj(localIndex: Int): Obj { + private suspend fun readLocalSlotValue(localIndex: Int): Obj { + val localName = fn.localSlotNames.getOrNull(localIndex) return when (frame.getSlotTypeCode(localIndex)) { SlotType.INT.code -> ObjInt.of(frame.getInt(localIndex)) SlotType.REAL.code -> ObjReal.of(frame.getReal(localIndex)) @@ -4775,7 +4829,9 @@ class CmdFrame( val obj = frame.getObj(localIndex) when (obj) { is FrameSlotRef -> obj.read() - is RecordSlotRef -> obj.read() + is RecordSlotRef -> obj.read(scope, localName) + is ObjProperty -> resolvePropertyLikeLocal(localName, obj) + ObjUnset -> resolveUnsetLocal(localName) else -> obj } } @@ -4783,13 +4839,34 @@ class CmdFrame( val obj = frame.getObj(localIndex) when (obj) { is FrameSlotRef -> obj.read() - is RecordSlotRef -> obj.read() + is RecordSlotRef -> obj.read(scope, localName) + is ObjProperty -> resolvePropertyLikeLocal(localName, obj) + ObjUnset -> resolveUnsetLocal(localName) else -> obj } } } } + private suspend fun resolvePropertyLikeLocal(localName: String?, property: ObjProperty): Obj { + if (localName != null) { + val record = scope.parent?.get(localName) ?: scope.get(localName) + if (record != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) { + return scope.resolve(record, localName) + } + } + return property.callGetter(scope, scope.thisObj) + } + + private suspend fun resolveUnsetLocal(localName: String?): Obj { + if (localName == null) return ObjUnset + val record = scope.parent?.get(localName) ?: scope.get(localName) ?: return ObjUnset + if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) { + return scope.resolve(record, localName) + } + return record.value + } + private suspend fun getScopeSlotValue(slot: Int): Obj { val target = scopeTarget(slot) val name = fn.scopeSlotNames[slot] @@ -4878,16 +4955,33 @@ class CmdFrame( private suspend fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) { val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved") val index = addrIndices[addrSlot] - val record = target.getSlotRecord(index) val slotId = addrScopeSlots[addrSlot] val name = fn.scopeSlotNames.getOrNull(slotId) - if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) { + val record = resolveScopeSlotRecordForWrite(target, index, name) + if (name != null && record != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) { target.assign(record, name, value) return } target.setSlotValue(index, value) } + private fun resolveScopeSlotRecordForWrite(target: Scope, index: Int, name: String?): ObjRecord? { + val record = target.getSlotRecord(index) + if (name == null) return record + if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) { + return record + } + if (record.value !== ObjUnset && record.memberName == null) { + return record + } + val resolved = target.get(name) ?: return record + if (resolved.value !== ObjUnset || resolved.type == ObjRecord.Type.Delegated || resolved.type == ObjRecord.Type.Property || resolved.value is ObjProperty) { + target.updateSlotFor(name, resolved) + return resolved + } + return record + } + internal fun ensureScopeSlot(target: Scope, slot: Int): Int { val name = fn.scopeSlotNames[slot] if (name != null) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt index eaca35c..5af9b41 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt @@ -31,7 +31,7 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) { val record = scope.getLocalRecordDirect(name) ?: scope.chainLookupIgnoreClosure(name, followClosure = true) ?: continue - val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { + val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is net.sergeych.lyng.obj.ObjProperty) { scope.resolve(record, name) } else { record.value diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index c0f9b50..001fdad 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -591,11 +591,14 @@ open class Obj { return obj.copy(value = res, type = ObjRecord.Type.Other) } val value = obj.value - if (value is ObjProperty || obj.type == ObjRecord.Type.Property) { - val prop = (value as? ObjProperty) - ?: scope.raiseError("Expected ObjProperty for property member $name, got ${value::class}") - val res = prop.callGetter(scope, this, decl) - return ObjRecord(res, obj.isMutable) + if (value is ObjProperty) { + val res = value.callGetter(scope, this, decl) + return obj.copy(value = res, type = ObjRecord.Type.Other) + } + if (obj.type == ObjRecord.Type.Property) { + // Some runtime paths cache the resolved property value back into the record. + // Treat that as an already-resolved read result instead of trying to call a getter again. + return obj.copy(type = ObjRecord.Type.Other) } val caller = scope.currentClassCtx // Check visibility for non-property members here if they weren't checked before diff --git a/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt b/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt index 6d0f911..8f1c715 100644 --- a/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt +++ b/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt @@ -18,6 +18,7 @@ package net.sergeych.lyng import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.bridge.bindGlobalVar import net.sergeych.lyng.bridge.globalBinder import kotlin.test.Test @@ -49,4 +50,35 @@ class GlobalPropertyCaptureRegressionTest { assertEquals(2.0, x, "bound extern var should stay live inside function bodies") } + + @Test + fun externGlobalVarShouldStayLiveWhenScriptRunsInChildScope() = runTest { + val base = Script.newScope() as ModuleScope + var x = 1.0 + + base.eval("extern var X: Real") + base.globalBinder().bindGlobalVar( + name = "X", + get = { x }, + set = { x = it } + ) + + val child = base.createChildScope() + child.eval( + Source( + "child-scope-probe", + """ + fun main() { + X = X + 1.0 + } + """.trimIndent() + ) + ) + + val mainRecord = child["main"] + check(mainRecord?.type == ObjRecord.Type.Fun) + child.eval("main()") + + assertEquals(2.0, x, "bound extern var should stay live in child-scope execution") + } }