Fix bound property captures and property slot resolution
This commit is contained in:
parent
86e8b2e2bc
commit
b0fb65a036
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Stri
|
||||
}
|
||||
val scoped = frame.scope.chainLookupIgnoreClosure(name, followClosure = true) ?: frame.scope.get(name)
|
||||
if (scoped != null) {
|
||||
records += ObjRecord(RecordSlotRef(scoped), isMutable = scoped.isMutable)
|
||||
records += scoped
|
||||
continue
|
||||
}
|
||||
frame.ensureScope().raiseSymbolNotFound("capture $name not found")
|
||||
@ -2858,7 +2861,7 @@ class CmdDeclInstanceProperty(internal val constId: Int, internal val slot: Int)
|
||||
val decl = frame.fn.constants[constId] as? BytecodeConst.InstancePropertyDecl
|
||||
?: error("DECL_INSTANCE_PROPERTY expects InstancePropertyDecl at $constId")
|
||||
val scope = frame.ensureScope()
|
||||
val prop = frame.slotToObj(slot)
|
||||
val prop = frame.storedSlotObj(slot)
|
||||
scope.addItem(
|
||||
decl.name,
|
||||
decl.isMutable,
|
||||
@ -3724,7 +3727,7 @@ class BytecodeLambdaCallable(
|
||||
?: context.parent?.get(name)
|
||||
?: context.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 ObjProperty) {
|
||||
context.resolve(record, name)
|
||||
} else {
|
||||
record.value
|
||||
@ -3817,7 +3820,29 @@ class CmdFrame(
|
||||
}
|
||||
|
||||
internal fun getLocalSlotTypeCode(localIndex: Int): Byte = frame.getSlotTypeCode(localIndex)
|
||||
internal fun readLocalObj(localIndex: Int): Obj = localSlotToObj(localIndex)
|
||||
internal fun readLocalObj(localIndex: Int): Obj {
|
||||
return when (frame.getSlotTypeCode(localIndex)) {
|
||||
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 -> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
else -> localSlotToObj(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
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user