From 5305ced89f929f0facca5fe8307b5330cef9acd0 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 9 Feb 2026 11:00:29 +0300 Subject: [PATCH] Step 18: delegated member access in bytecode --- bytecode_migration_plan.md | 6 ++-- .../kotlin/net/sergeych/lyng/Compiler.kt | 30 ++--------------- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 32 +++++++++++++++++-- .../kotlin/BytecodeRecentOpsTest.kt | 24 ++++++++++++++ 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 463e6ef..abf444c 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -59,9 +59,9 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Step 17: Callable property calls in bytecode. - [x] Support `CallRef` where the target is a `FieldRef` (e.g., `(obj.fn)()`), keeping compile-time resolution. - [x] Add a JVM test for a callable property call compiled to bytecode. -- [ ] Step 18: Delegated member access in bytecode. - - [ ] Remove `containsDelegatedRefs` guard once bytecode emits delegated get/set/call correctly. - - [ ] Add JVM coverage for delegated member get/set/call in bytecode. +- [x] Step 18: Delegated member access in bytecode. + - [x] Remove `containsDelegatedRefs` guard once bytecode emits delegated get/set/call correctly. + - [x] Add JVM coverage for delegated member get/set/call in bytecode. - [ ] Step 19: Unknown receiver member access in bytecode. - [ ] Decide on allowed fallback behavior for unknown receiver types without runtime name resolution. - [ ] Add JVM tests for member access on unresolved receiver types and keep compile-time-only resolution. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index bc995a4..84fa9be 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2077,18 +2077,6 @@ class Compiler( private fun containsDelegatedRefs(ref: ObjRef): Boolean { return when (ref) { is LocalSlotRef -> ref.isDelegated - is ImplicitThisMemberRef -> { - val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName() - val targetClass = typeName?.let { resolveClassByName(it) } - val member = targetClass?.findFirstConcreteMember(ref.name) - member?.type == ObjRecord.Type.Delegated - } - is ImplicitThisMethodCallRef -> { - val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName() - val targetClass = typeName?.let { resolveClassByName(it) } - val member = targetClass?.findFirstConcreteMember(ref.methodName()) - member?.type == ObjRecord.Type.Delegated - } is BinaryOpRef -> containsDelegatedRefs(ref.left) || containsDelegatedRefs(ref.right) is UnaryOpRef -> containsDelegatedRefs(ref.a) is CastRef -> containsDelegatedRefs(ref.castValueRef()) || containsDelegatedRefs(ref.castTypeRef()) @@ -2103,14 +2091,7 @@ class Compiler( is ConditionalRef -> containsDelegatedRefs(ref.condition) || containsDelegatedRefs(ref.ifTrue) || containsDelegatedRefs(ref.ifFalse) is ElvisRef -> containsDelegatedRefs(ref.left) || containsDelegatedRefs(ref.right) - is FieldRef -> { - val receiverClass = resolveReceiverClassForMember(ref.target) - if (receiverClass != null) { - val member = receiverClass.findFirstConcreteMember(ref.name) - if (member?.type == ObjRecord.Type.Delegated) return true - } - containsDelegatedRefs(ref.target) - } + is FieldRef -> containsDelegatedRefs(ref.target) is IndexRef -> containsDelegatedRefs(ref.targetRef) || containsDelegatedRefs(ref.indexRef) is ListLiteralRef -> ref.entries().any { when (it) { @@ -2125,14 +2106,7 @@ class Compiler( } } is CallRef -> containsDelegatedRefs(ref.target) || ref.args.any { containsDelegatedRefs(it.value) } - is MethodCallRef -> { - val receiverClass = resolveReceiverClassForMember(ref.receiver) - if (receiverClass != null) { - val member = receiverClass.findFirstConcreteMember(ref.name) - if (member?.type == ObjRecord.Type.Delegated) return true - } - containsDelegatedRefs(ref.receiver) || ref.args.any { containsDelegatedRefs(it.value) } - } + is MethodCallRef -> containsDelegatedRefs(ref.receiver) || ref.args.any { containsDelegatedRefs(it.value) } is StatementRef -> containsDelegatedRefs(ref.statement) else -> false } 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 ff6350f..51a1c89 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -1673,7 +1673,12 @@ class CmdCallMemberSlot( } ?: frame.ensureScope().raiseError("member id $methodId not found on ${receiver.objClass.className}") val callArgs = frame.buildArguments(argBase, argCount) - val name = rec.memberName ?: "" + val rawName = rec.memberName ?: "" + val name = if (receiver is ObjInstance && rawName.contains("::")) { + rawName.substringAfterLast("::") + } else { + rawName + } if (receiver is ObjQualifiedView) { val result = receiver.invokeInstanceMethod(frame.ensureScope(), name, callArgs) if (frame.fn.localSlotNames.isNotEmpty()) { @@ -1688,10 +1693,33 @@ class CmdCallMemberSlot( if (callArgs.isEmpty()) (rec.value as ObjProperty).callGetter(frame.ensureScope(), receiver, decl) else frame.ensureScope().raiseError("property $name cannot be called with arguments") } - ObjRecord.Type.Fun, ObjRecord.Type.Delegated -> { + ObjRecord.Type.Fun -> { val callScope = inst?.instanceScope ?: frame.ensureScope() rec.value.invoke(callScope, receiver, callArgs, decl) } + ObjRecord.Type.Delegated -> { + val scope = frame.ensureScope() + val delegate = when (receiver) { + is ObjInstance -> { + val storageName = decl.mangledName(name) + var del = receiver.instanceScope[storageName]?.delegate ?: rec.delegate + if (del == null) { + for (c in receiver.objClass.mro) { + del = receiver.instanceScope[c.mangledName(name)]?.delegate + if (del != null) break + } + } + del ?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)") + } + is ObjClass -> rec.delegate ?: scope.raiseError("Internal error: delegated member $name has no delegate") + else -> rec.delegate ?: scope.raiseError("Internal error: delegated member $name has no delegate") + } + val allArgs = (listOf(receiver, ObjString(name)) + callArgs.list).toTypedArray() + delegate.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = { + val propVal = delegate.invokeInstanceMethod(scope, "getValue", Arguments(receiver, ObjString(name))) + propVal.invoke(scope, receiver, callArgs, decl) + }) + } else -> frame.ensureScope().raiseError("member $name is not callable") } if (frame.fn.localSlotNames.isNotEmpty()) { diff --git a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt index b5cc1e5..9d982cc 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt @@ -171,6 +171,30 @@ class BytecodeRecentOpsTest { ) } + @Test + fun delegatedMemberAccessAndCall() = runTest { + eval( + """ + class ConstDelegate(val v) : Delegate { + override fun getValue(thisRef: Object, name: String): Object = v + } + class ActionDelegate : Delegate { + override fun invoke(thisRef: Object, name: String, args...) { + val list: List = args as List + "Called %s with %d args: %s"(name, list.size, list.toString()) + } + } + class C { + val a by ConstDelegate(7) + fun greet by ActionDelegate() + } + val c = C() + assertEquals(7, c.a) + assertEquals("Called greet with 2 args: [hi,world]", c.greet("hi", "world")) + """.trimIndent() + ) + } + @Test fun qualifiedThisValueRef() = runTest { eval(