Fix captured bound global writes in nested bytecode blocks

This commit is contained in:
Sergey Chernov 2026-03-28 00:46:33 +03:00
parent b0fb65a036
commit 418b1ae2b6
4 changed files with 80 additions and 2 deletions

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ debug.log
/compile_jvm_output.txt /compile_jvm_output.txt
/compile_metadata_output.txt /compile_metadata_output.txt
test_output*.txt test_output*.txt
/site/src/version-template/lyng-version.js

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" 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 // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below

View File

@ -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() { class CmdMoveInt(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val value = frame.getInt(src) val value = frame.getInt(src)
if (frame.writeThroughPropertyLikeSlot(dst, ObjInt.of(value))) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) { if (frame.shouldBypassImmutableWrite(dst)) {
frame.setIntUnchecked(dst, value) frame.setIntUnchecked(dst, value)
} else { } 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() { class CmdMoveReal(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val value = frame.getReal(src) val value = frame.getReal(src)
if (frame.writeThroughPropertyLikeSlot(dst, ObjReal.of(value))) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) { if (frame.shouldBypassImmutableWrite(dst)) {
frame.setRealUnchecked(dst, value) frame.setRealUnchecked(dst, value)
} else { } 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() { class CmdMoveBool(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val value = frame.getBool(src) val value = frame.getBool(src)
if (frame.writeThroughPropertyLikeSlot(dst, if (value) ObjTrue else ObjFalse)) {
return
}
if (frame.shouldBypassImmutableWrite(dst)) { if (frame.shouldBypassImmutableWrite(dst)) {
frame.setBoolUnchecked(dst, value) frame.setBoolUnchecked(dst, value)
} else { } else {
@ -4344,12 +4353,13 @@ class CmdFrame(
} }
val localIndex = slot - fn.scopeSlotCount val localIndex = slot - fn.scopeSlotCount
val name = fn.localSlotNames.getOrNull(localIndex) ?: return false val name = fn.localSlotNames.getOrNull(localIndex) ?: return false
val isCapture = fn.localSlotCaptures.getOrNull(localIndex) == true
val raw = frame.getRawObj(localIndex) val raw = frame.getRawObj(localIndex)
if (raw is RecordSlotRef) { if (raw is RecordSlotRef) {
if (raw.write(scope, name, value)) return true if (raw.write(scope, name, value)) return true
return false 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 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) { if (record.type != ObjRecord.Type.Delegated && record.type != ObjRecord.Type.Property && record.value !is ObjProperty) {
return false return false

View File

@ -18,9 +18,14 @@
package net.sergeych.lyng package net.sergeych.lyng
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.bridge.bind
import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.bridge.data
import net.sergeych.lyng.bridge.bindGlobalVar import net.sergeych.lyng.bridge.bindGlobalVar
import net.sergeych.lyng.bridge.globalBinder 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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -81,4 +86,66 @@ class GlobalPropertyCaptureRegressionTest {
assertEquals(2.0, x, "bound extern var should stay live in child-scope execution") 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<ChoicePayload>().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 <T> ScopeFacade.thisObjData(): T {
val instance = thisObj as? ObjInstance ?: raiseClassCastError("Expected result object instance")
return instance.data as? T ?: raiseIllegalState("Bridge payload is not initialized")
} }