Improve error handling and scope resolution in dynamic callbacks and bytecode execution, add new tests for dynamic behavior.
This commit is contained in:
parent
529f76489b
commit
87ef1c38b8
@ -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 ->
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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("""
|
||||||
|
""")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user