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]
if (fieldId != null || methodId != null) {
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 ->

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");
* 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) {
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")

View File

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

View File

@ -3229,6 +3229,7 @@ class CmdGetMemberSlot(
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val scope = frame.ensureScope()
val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance
val cls = receiver as? ObjClass
@ -3251,7 +3252,26 @@ class CmdGetMemberSlot(
else -> receiver.objClass.methodRecordForId(methodIdResolved)
}
} 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 name = if (receiver is ObjInstance && rawName.contains("::")) {
rawName.substringAfterLast("::")
@ -3283,6 +3303,7 @@ class CmdSetMemberSlot(
internal val valueSlot: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val scope = frame.ensureScope()
val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance
val cls = receiver as? ObjClass
@ -3305,7 +3326,26 @@ class CmdSetMemberSlot(
else -> receiver.objClass.methodRecordForId(methodIdResolved)
}
} 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 name = if (receiver is ObjInstance && rawName.contains("::")) {
rawName.substringAfterLast("::")
@ -3568,6 +3608,20 @@ class BytecodeLambdaCallable(
private val returnLabels: Set<String>,
override val pos: Pos,
) : 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 {
val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also {
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");
* 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.Scope
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
override val objClass: ObjClass get() = type
@ -29,7 +30,8 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
val d = thisAs<ObjDynamicContext>().delegate
if (d.readCallback != null)
raiseIllegalState("get already defined")
d.readCallback = requireOnlyArg()
val callback = requireOnlyArg<Obj>()
d.readCallback = d.rebindCallback(requireScope(), callback)
ObjVoid
}
@ -37,7 +39,8 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
val d = thisAs<ObjDynamicContext>().delegate
if (d.writeCallback != null)
raiseIllegalState("set already defined")
d.writeCallback = requireOnlyArg()
val callback = requireOnlyArg<Obj>()
d.writeCallback = d.rebindCallback(requireScope(), callback)
ObjVoid
}
@ -55,6 +58,11 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
override val objClass: ObjClass get() = type
// Capture the lexical scope used to build this dynamic so callbacks can see outer locals
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

View File

@ -23,6 +23,9 @@ package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.binding.Binder
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.assertEquals
import kotlin.test.assertNotNull
@ -129,4 +132,83 @@ class BindingTest {
val refs = snap.references.count { it.symbolId == xField.id }
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("""
""")
}
}