diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 04de9dd..904b34a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt index 6593d71..8fc1065 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt @@ -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") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 7b5505d..9ed60f0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index c668c17..bb83ce0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -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 ?: "" 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 ?: "" val name = if (receiver is ObjInstance && rawName.contains("::")) { rawName.substringAfterLast("::") @@ -3568,6 +3608,20 @@ class BytecodeLambdaCallable( private val returnLabels: Set, 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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt index 94f526e..6b9194a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt @@ -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().delegate if (d.readCallback != null) raiseIllegalState("get already defined") - d.readCallback = requireOnlyArg() + val callback = requireOnlyArg() + d.readCallback = d.rebindCallback(requireScope(), callback) ObjVoid } @@ -37,7 +39,8 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() { val d = thisAs().delegate if (d.writeCallback != null) raiseIllegalState("set already defined") - d.writeCallback = requireOnlyArg() + val callback = requireOnlyArg() + 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 diff --git a/lynglib/src/commonTest/kotlin/BindingTest.kt b/lynglib/src/commonTest/kotlin/BindingTest.kt index c1b4a25..303fa14 100644 --- a/lynglib/src/commonTest/kotlin/BindingTest.kt +++ b/lynglib/src/commonTest/kotlin/BindingTest.kt @@ -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(""" + """) + } } + +