diff --git a/.gitignore b/.gitignore index 7f6a84e..6685d49 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ debug.log /compile_jvm_output.txt /compile_metadata_output.txt test_output*.txt +/site/src/version-template/lyng-version.js diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index f724448..0d68792 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.5.2" +version = "1.5.3-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below 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 eecf13a..e82cf94 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -100,6 +100,9 @@ class CmdMoveObj(internal val src: Int, internal val dst: Int) : Cmd() { class CmdMoveInt(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val value = frame.getInt(src) + if (frame.writeThroughPropertyLikeSlot(dst, ObjInt.of(value))) { + return + } if (frame.shouldBypassImmutableWrite(dst)) { frame.setIntUnchecked(dst, value) } else { @@ -120,6 +123,9 @@ class CmdMoveIntLocal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdMoveReal(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val value = frame.getReal(src) + if (frame.writeThroughPropertyLikeSlot(dst, ObjReal.of(value))) { + return + } if (frame.shouldBypassImmutableWrite(dst)) { frame.setRealUnchecked(dst, value) } else { @@ -140,6 +146,9 @@ class CmdMoveRealLocal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdMoveBool(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val value = frame.getBool(src) + if (frame.writeThroughPropertyLikeSlot(dst, if (value) ObjTrue else ObjFalse)) { + return + } if (frame.shouldBypassImmutableWrite(dst)) { frame.setBoolUnchecked(dst, value) } else { @@ -4344,12 +4353,13 @@ class CmdFrame( } val localIndex = slot - fn.scopeSlotCount val name = fn.localSlotNames.getOrNull(localIndex) ?: return false + val isCapture = fn.localSlotCaptures.getOrNull(localIndex) == true 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 + if (!isCapture && 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 diff --git a/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt b/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt index 8f1c715..5a851b5 100644 --- a/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt +++ b/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt @@ -18,9 +18,14 @@ package net.sergeych.lyng import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.bridge.bind import net.sergeych.lyng.obj.ObjRecord +import net.sergeych.lyng.bridge.data import net.sergeych.lyng.bridge.bindGlobalVar import net.sergeych.lyng.bridge.globalBinder +import net.sergeych.lyng.obj.ObjFalse +import net.sergeych.lyng.obj.ObjInstance +import net.sergeych.lyng.obj.ObjTrue import kotlin.test.Test import kotlin.test.assertEquals @@ -81,4 +86,66 @@ class GlobalPropertyCaptureRegressionTest { assertEquals(2.0, x, "bound extern var should stay live in child-scope execution") } + + @Test + fun externGlobalVarShouldStayLiveAfterExternClassPropertyBranchInChildScope() = runTest { + val base = Script.newScope() as ModuleScope + var x = 3.0 + + base.eval( + """ + extern var X: Real + + class ChoiceInputResult { + extern val isSkip: Bool + } + + extern fun requestChoice(): ChoiceInputResult + """.trimIndent() + ) + + base.bind("ChoiceInputResult") { + addVal("isSkip") { + if (thisObjData().isSkip) ObjTrue else ObjFalse + } + } + + base.globalBinder().bindGlobalVar( + name = "X", + get = { x }, + set = { x = it } + ) + + base.globalBinder().bindGlobalFunRaw("requestChoice") { _, _ -> + val instance = base.requireClass("ChoiceInputResult").callOn(base.createChildScope()) as ObjInstance + instance.data = ChoicePayload(isSkip = true) + instance + } + + val child = base.createChildScope() + child.eval( + """ + fun main() { + val c: ChoiceInputResult = requestChoice() + if (c.isSkip) { + X = 77.0 + } + } + """.trimIndent() + ) + + child.eval("main()") + + assertEquals(77.0, x, "bound extern var should stay live after extern class property branch in child scope") + } +} + +private data class ChoicePayload( + val isSkip: Boolean, +) + +@Suppress("UNCHECKED_CAST") +private fun ScopeFacade.thisObjData(): T { + val instance = thisObj as? ObjInstance ?: raiseClassCastError("Expected result object instance") + return instance.data as? T ?: raiseIllegalState("Bridge payload is not initialized") }