Step 18: delegated member access in bytecode

This commit is contained in:
Sergey Chernov 2026-02-09 11:00:29 +03:00
parent b2d5897aa8
commit 5305ced89f
4 changed files with 59 additions and 33 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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 ?: "<member>"
val rawName = rec.memberName ?: "<member>"
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()) {

View File

@ -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(