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]
|
||||
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 ->
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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("""
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user