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_metadata_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
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

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() {
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

View File

@ -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<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")
}