Improve error handling and scope resolution in dynamic callbacks and bytecode execution, add new tests for dynamic behavior.

This commit is contained in:
Sergey Chernov 2026-02-20 10:15:01 +03:00
parent 529f76489b
commit 87ef1c38b8
6 changed files with 158 additions and 12 deletions

View File

@ -890,7 +890,7 @@ class Compiler(
val methodId = classCtx.memberMethodIds[name] val methodId = classCtx.memberMethodIds[name]
if (fieldId != null || methodId != null) { if (fieldId != null || methodId != null) {
resolutionSink?.referenceMember(name, pos) resolutionSink?.referenceMember(name, pos)
return ImplicitThisMemberRef(name, pos, fieldId, methodId, currentImplicitThisTypeName()) return ImplicitThisMemberRef(name, pos, fieldId, methodId, classCtx.name)
} }
} }
captureLocalRef(name, slotLoc, pos)?.let { ref -> captureLocalRef(name, slotLoc, pos)?.let { ref ->

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +19,8 @@ package net.sergeych.lyng
data class Pos(val source: Source, val line: Int, val column: Int) { data class Pos(val source: Source, val line: Int, val column: Int) {
override fun toString(): String { override fun toString(): String {
return "${source.fileName}:${line+1}:${column}" val col = if (column >= 0) column + 1 else column
return "${source.fileName}:${line+1}:$col"
} }
@Suppress("unused") @Suppress("unused")

View File

@ -3268,9 +3268,10 @@ class BytecodeCompiler(
if (ref.name.isBlank()) { if (ref.name.isBlank()) {
return compileRefWithFallback(ref.target, null, Pos.builtIn) return compileRefWithFallback(ref.target, null, Pos.builtIn)
} }
val pos = callSitePos()
val receiverClass = resolveReceiverClass(ref.target) ?: ObjDynamic.type val receiverClass = resolveReceiverClass(ref.target) ?: ObjDynamic.type
if (receiverClass == ObjDynamic.type) { if (receiverClass == ObjDynamic.type) {
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
val dst = allocSlot() val dst = allocSlot()
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name)) val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
if (!ref.isOptional) { if (!ref.isOptional) {
@ -3296,7 +3297,7 @@ class BytecodeCompiler(
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
} }
if (receiverClass is ObjInstanceClass && !isThisReceiver(ref.target)) { if (receiverClass is ObjInstanceClass && !isThisReceiver(ref.target)) {
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
val dst = allocSlot() val dst = allocSlot()
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name)) val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
if (!ref.isOptional) { if (!ref.isOptional) {
@ -3323,7 +3324,7 @@ class BytecodeCompiler(
} }
val resolvedMember = receiverClass.resolveInstanceMember(ref.name) val resolvedMember = receiverClass.resolveInstanceMember(ref.name)
if (resolvedMember?.declaringClass?.className == "Obj") { if (resolvedMember?.declaringClass?.className == "Obj") {
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
val dst = allocSlot() val dst = allocSlot()
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name)) val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
if (!ref.isOptional) { if (!ref.isOptional) {
@ -3352,7 +3353,7 @@ class BytecodeCompiler(
val methodId = if (resolvedMember != null) receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] else null val methodId = if (resolvedMember != null) receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] else null
val encodedFieldId = encodeMemberId(receiverClass, fieldId) val encodedFieldId = encodeMemberId(receiverClass, fieldId)
val encodedMethodId = encodeMemberId(receiverClass, methodId) val encodedMethodId = encodeMemberId(receiverClass, methodId)
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
val dst = allocSlot() val dst = allocSlot()
if (fieldId == null && methodId == null && (isKnownClassReceiver(ref.target) || isClassNameRef(ref.target, receiverClass)) && if (fieldId == null && methodId == null && (isKnownClassReceiver(ref.target) || isClassNameRef(ref.target, receiverClass)) &&
(isClassSlot(receiver.slot) || receiverClass == ObjClassType) (isClassSlot(receiver.slot) || receiverClass == ObjClassType)

View File

@ -3229,6 +3229,7 @@ class CmdGetMemberSlot(
internal val dst: Int, internal val dst: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val scope = frame.ensureScope()
val receiver = frame.slotToObj(recvSlot) val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance val inst = receiver as? ObjInstance
val cls = receiver as? ObjClass val cls = receiver as? ObjClass
@ -3251,7 +3252,26 @@ class CmdGetMemberSlot(
else -> receiver.objClass.methodRecordForId(methodIdResolved) else -> receiver.objClass.methodRecordForId(methodIdResolved)
} }
} else null } else null
} ?: frame.ensureScope().raiseSymbolNotFound("member") } ?: run {
val receiverClass = when {
cls != null && fieldOnObjClass -> cls.objClass
cls != null -> cls
else -> receiver.objClass
}
val fieldName = if (fieldIdResolved >= 0) {
receiverClass.fieldSlotMap().entries.firstOrNull { it.value.slot == fieldIdResolved }?.key
} else null
val methodName = if (methodIdResolved >= 0) {
receiverClass.methodSlotMap().entries.firstOrNull { it.value.slot == methodIdResolved }?.key
} else null
val memberName = fieldName ?: methodName
val message = if (memberName != null) {
"no such member: $memberName on ${receiverClass.className}"
} else {
"no such member slot (fieldId=$fieldIdResolved, methodId=$methodIdResolved) on ${receiverClass.className}"
}
scope.raiseError(message)
}
val rawName = rec.memberName ?: "<member>" val rawName = rec.memberName ?: "<member>"
val name = if (receiver is ObjInstance && rawName.contains("::")) { val name = if (receiver is ObjInstance && rawName.contains("::")) {
rawName.substringAfterLast("::") rawName.substringAfterLast("::")
@ -3283,6 +3303,7 @@ class CmdSetMemberSlot(
internal val valueSlot: Int, internal val valueSlot: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val scope = frame.ensureScope()
val receiver = frame.slotToObj(recvSlot) val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance val inst = receiver as? ObjInstance
val cls = receiver as? ObjClass val cls = receiver as? ObjClass
@ -3305,7 +3326,26 @@ class CmdSetMemberSlot(
else -> receiver.objClass.methodRecordForId(methodIdResolved) else -> receiver.objClass.methodRecordForId(methodIdResolved)
} }
} else null } else null
} ?: frame.ensureScope().raiseSymbolNotFound("member") } ?: run {
val receiverClass = when {
cls != null && fieldOnObjClass -> cls.objClass
cls != null -> cls
else -> receiver.objClass
}
val fieldName = if (fieldIdResolved >= 0) {
receiverClass.fieldSlotMap().entries.firstOrNull { it.value.slot == fieldIdResolved }?.key
} else null
val methodName = if (methodIdResolved >= 0) {
receiverClass.methodSlotMap().entries.firstOrNull { it.value.slot == methodIdResolved }?.key
} else null
val memberName = fieldName ?: methodName
val message = if (memberName != null) {
"no such member: $memberName on ${receiverClass.className}"
} else {
"no such member slot (fieldId=$fieldIdResolved, methodId=$methodIdResolved) on ${receiverClass.className}"
}
scope.raiseError(message)
}
val rawName = rec.memberName ?: "<member>" val rawName = rec.memberName ?: "<member>"
val name = if (receiver is ObjInstance && rawName.contains("::")) { val name = if (receiver is ObjInstance && rawName.contains("::")) {
rawName.substringAfterLast("::") rawName.substringAfterLast("::")
@ -3568,6 +3608,20 @@ class BytecodeLambdaCallable(
private val returnLabels: Set<String>, private val returnLabels: Set<String>,
override val pos: Pos, override val pos: Pos,
) : Statement(), BytecodeCallable { ) : Statement(), BytecodeCallable {
fun rebindClosure(newClosureScope: Scope): BytecodeLambdaCallable {
return BytecodeLambdaCallable(
fn = fn,
closureScope = newClosureScope,
captureRecords = captureRecords,
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
preferredThisType = preferredThisType,
returnLabels = returnLabels,
pos = pos
)
}
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also { val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also {
it.args = scope.args it.args = scope.args

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
class ObjDynamicContext(val delegate: ObjDynamic) : Obj() { class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
override val objClass: ObjClass get() = type override val objClass: ObjClass get() = type
@ -29,7 +30,8 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
val d = thisAs<ObjDynamicContext>().delegate val d = thisAs<ObjDynamicContext>().delegate
if (d.readCallback != null) if (d.readCallback != null)
raiseIllegalState("get already defined") raiseIllegalState("get already defined")
d.readCallback = requireOnlyArg() val callback = requireOnlyArg<Obj>()
d.readCallback = d.rebindCallback(requireScope(), callback)
ObjVoid ObjVoid
} }
@ -37,7 +39,8 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
val d = thisAs<ObjDynamicContext>().delegate val d = thisAs<ObjDynamicContext>().delegate
if (d.writeCallback != null) if (d.writeCallback != null)
raiseIllegalState("set already defined") raiseIllegalState("set already defined")
d.writeCallback = requireOnlyArg() val callback = requireOnlyArg<Obj>()
d.writeCallback = d.rebindCallback(requireScope(), callback)
ObjVoid ObjVoid
} }
@ -55,6 +58,11 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
override val objClass: ObjClass get() = type override val objClass: ObjClass get() = type
// Capture the lexical scope used to build this dynamic so callbacks can see outer locals // Capture the lexical scope used to build this dynamic so callbacks can see outer locals
internal var builderScope: Scope? = null internal var builderScope: Scope? = null
internal fun rebindCallback(contextScope: Scope, callback: Obj): Obj {
val snapshot = builderScope ?: return callback
val context = Scope(snapshot, contextScope.args, contextScope.pos, contextScope.thisObj)
return (callback as? BytecodeLambdaCallable)?.rebindClosure(context) ?: callback
}
/** /**
* Use read callback to dynamically resolve the field name. Note that it does not work * Use read callback to dynamically resolve the field name. Note that it does not work

View File

@ -23,6 +23,9 @@ package net.sergeych.lyng
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.binding.Binder import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.miniast.MiniAstBuilder import net.sergeych.lyng.miniast.MiniAstBuilder
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjString
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@ -129,4 +132,83 @@ class BindingTest {
val refs = snap.references.count { it.symbolId == xField.id } val refs = snap.references.count { it.symbolId == xField.id }
assertEquals(1, refs) assertEquals(1, refs)
} }
class ObjA: Obj() {
override val objClass = type
companion object {
val type = ObjClass("ObjA").apply {
addFn("get1") {
ObjString("get1")
}
}
}
}
@Test
fun testShortFormMethod() = runTest {
eval("""
class A {
fun get1() = "1"
fun get2() = get1() + "-2"
fun get3(): String = get2() + "-3"
override fun toString() = "!"+get3()+"!"
}
assert(A().get3() == "1-2-3")
assert(A().toString() == "!1-2-3!")
""".trimIndent())
}
@Test
fun testLateGlobalBinding() = runTest {
val ms = Script.newScope()
ms.eval("""
extern class A {
fun get1(): String
}
extern fun getA(): A
fun getB(a: A) = a.get1() + "-2"
""".trimIndent())
ms.addFn("getA") {
ObjA()
}
ms.addConst("A", ObjA.type)
ms.eval("""
assert(A() is A)
assert(getA() is A)
assertEquals(getB(getA()), "get1-2")
""".trimIndent())
}
@Test
fun testDynamicToDynamic() = runTest {
val ms = Script.newScope()
ms.eval("""
class A(prefix) {
val da = dynamic {
get { name -> "a:"+prefix+":"+name }
}
}
val B: A = dynamic {
get { p -> A(p) }
}
assertEquals(A("bar").da.foo, "a:bar:foo")
assertEquals( B.buzz.da.foo, "a:buzz:foo" )
val C = dynamic {
get { p -> A(p).da }
}
assertEquals(C.buzz.foo, "a:buzz:foo")
""".trimIndent())
ms.eval("""
""")
}
} }