Step 19: union member access

This commit is contained in:
Sergey Chernov 2026-02-09 11:38:04 +03:00
parent 5305ced89f
commit 565dbf98ed
3 changed files with 182 additions and 15 deletions

View File

@ -62,9 +62,10 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te
- [x] Step 18: Delegated member access in bytecode. - [x] Step 18: Delegated member access in bytecode.
- [x] Remove `containsDelegatedRefs` guard once bytecode emits delegated get/set/call correctly. - [x] Remove `containsDelegatedRefs` guard once bytecode emits delegated get/set/call correctly.
- [x] Add JVM coverage for delegated member get/set/call in bytecode. - [x] Add JVM coverage for delegated member get/set/call in bytecode.
- [ ] Step 19: Unknown receiver member access in bytecode. - [x] Step 19: Unknown receiver member access in bytecode.
- [ ] Decide on allowed fallback behavior for unknown receiver types without runtime name resolution. - [x] Reject Object/unknown receiver member calls without explicit cast or Dynamic.
- [ ] Add JVM tests for member access on unresolved receiver types and keep compile-time-only resolution. - [x] Add union-member dispatch with ordered type checks and runtime mismatch error.
- [x] Add JVM tests for unknown receiver and union member access.
## Notes ## Notes

View File

@ -2443,8 +2443,13 @@ class Compiler(
} }
MethodCallRef(left, next.value, args, tailBlock, isOptional) MethodCallRef(left, next.value, args, tailBlock, isOptional)
} else { } else {
enforceReceiverTypeForMember(left, next.value, next.pos) val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
MethodCallRef(left, next.value, args, tailBlock, isOptional) if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, tailBlock, isOptional)
}
} }
is QualifiedThisRef -> is QualifiedThisRef ->
QualifiedThisMethodSlotCallRef( QualifiedThisMethodSlotCallRef(
@ -2458,8 +2463,13 @@ class Compiler(
resolutionSink?.referenceMember(next.value, next.pos, left.typeName) resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
} }
else -> { else -> {
enforceReceiverTypeForMember(left, next.value, next.pos) val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
MethodCallRef(left, next.value, args, tailBlock, isOptional) if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, tailBlock, isOptional)
}
} }
} }
} }
@ -2496,8 +2506,13 @@ class Compiler(
} }
MethodCallRef(left, next.value, args, true, isOptional) MethodCallRef(left, next.value, args, true, isOptional)
} else { } else {
enforceReceiverTypeForMember(left, next.value, next.pos) val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
MethodCallRef(left, next.value, args, true, isOptional) if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, true, isOptional)
}
} }
is QualifiedThisRef -> is QualifiedThisRef ->
QualifiedThisMethodSlotCallRef( QualifiedThisMethodSlotCallRef(
@ -2511,8 +2526,13 @@ class Compiler(
resolutionSink?.referenceMember(next.value, next.pos, left.typeName) resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
} }
else -> { else -> {
enforceReceiverTypeForMember(left, next.value, next.pos) val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
MethodCallRef(left, next.value, args, true, isOptional) if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, true, isOptional)
}
} }
} }
} }
@ -2528,8 +2548,13 @@ class Compiler(
val ids = resolveMemberIds(next.value, next.pos, implicitType) val ids = resolveMemberIds(next.value, next.pos, implicitType)
ThisFieldSlotRef(next.value, ids.fieldId, ids.methodId, isOptional) ThisFieldSlotRef(next.value, ids.fieldId, ids.methodId, isOptional)
} else { } else {
enforceReceiverTypeForMember(left, next.value, next.pos) val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
FieldRef(left, next.value, isOptional) if (unionField != null) {
unionField
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
FieldRef(left, next.value, isOptional)
}
} }
is QualifiedThisRef -> run { is QualifiedThisRef -> run {
val ids = resolveMemberIds(next.value, next.pos, left.typeName) val ids = resolveMemberIds(next.value, next.pos, left.typeName)
@ -2544,8 +2569,13 @@ class Compiler(
resolutionSink?.referenceMember(next.value, next.pos, left.typeName) resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
} }
else -> { else -> {
enforceReceiverTypeForMember(left, next.value, next.pos) val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
FieldRef(left, next.value, isOptional) if (unionField != null) {
unionField
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
FieldRef(left, next.value, isOptional)
}
} }
} }
} }
@ -4310,6 +4340,100 @@ class Compiler(
} }
} }
private class UnionTypeMismatchStatement(
private val message: String,
override val pos: Pos
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
throw ScriptError(pos, message)
}
}
private fun resolveMemberHostForUnion(typeDecl: TypeDecl, memberName: String, pos: Pos): ObjClass {
val receiverClass = when (typeDecl) {
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> Obj.rootObjectType
else -> resolveTypeDeclObjClass(typeDecl)
} ?: throw ScriptError(pos, "member access requires compile-time receiver type: $memberName")
if (receiverClass == ObjDynamic.type) {
return receiverClass
}
registerExtensionWrapperBindings(receiverClass, memberName, pos)
val hasMember = receiverClass.instanceFieldIdMap()[memberName] != null ||
receiverClass.instanceMethodIdMap(includeAbstract = true)[memberName] != null
if (!hasMember && !hasExtensionFor(receiverClass.className, memberName)) {
if (receiverClass == Obj.rootObjectType && isAllowedObjectMember(memberName)) {
return receiverClass
}
val ownerName = receiverClass.className
val message = if (receiverClass == Obj.rootObjectType) {
"member $memberName is not available on Object without explicit cast"
} else {
"unknown member $memberName on $ownerName"
}
throw ScriptError(pos, message)
}
return receiverClass
}
private fun buildUnionMemberAccess(
left: ObjRef,
union: TypeDecl.Union,
memberName: String,
pos: Pos,
isOptional: Boolean,
makeRef: (ObjRef) -> ObjRef
): ObjRef {
val options = union.options
if (options.isEmpty()) {
throw ScriptError(pos, "member access requires compile-time receiver type: $memberName")
}
for (option in options) {
resolveMemberHostForUnion(option, memberName, pos)
}
val unionName = typeDeclName(union)
val failStmt = UnionTypeMismatchStatement("value is not $unionName", pos)
var current: ObjRef = net.sergeych.lyng.obj.StatementRef(failStmt)
for (option in options.asReversed()) {
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(option, pos)
val cond = BinaryOpRef(BinOp.IS, left, typeRef)
val casted = CastRef(left, typeRef, false, pos)
val branch = makeRef(casted)
current = ConditionalRef(cond, branch, current)
}
if (isOptional) {
val nullRef = ConstRef(ObjNull.asReadonly)
val nullCond = BinaryOpRef(BinOp.REF_EQ, left, nullRef)
current = ConditionalRef(nullCond, nullRef, current)
}
return current
}
private fun buildUnionFieldAccess(
left: ObjRef,
memberName: String,
pos: Pos,
isOptional: Boolean
): ObjRef? {
val receiverDecl = resolveReceiverTypeDecl(left) as? TypeDecl.Union ?: return null
return buildUnionMemberAccess(left, receiverDecl, memberName, pos, isOptional) { receiver ->
FieldRef(receiver, memberName, false)
}
}
private fun buildUnionMethodCall(
left: ObjRef,
memberName: String,
pos: Pos,
isOptional: Boolean,
args: List<ParsedArgument>,
tailBlock: Boolean
): ObjRef? {
val receiverDecl = resolveReceiverTypeDecl(left) as? TypeDecl.Union ?: return null
return buildUnionMemberAccess(left, receiverDecl, memberName, pos, isOptional) { receiver ->
MethodCallRef(receiver, memberName, args, tailBlock, false)
}
}
private fun registerExtensionWrapperBindings(receiverClass: ObjClass, memberName: String, pos: Pos) { private fun registerExtensionWrapperBindings(receiverClass: ObjClass, memberName: String, pos: Pos) {
for (cls in receiverClass.mro) { for (cls in receiverClass.mro) {
val wrapperNames = listOf( val wrapperNames = listOf(

View File

@ -18,11 +18,13 @@
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
import net.sergeych.lyng.eval import net.sergeych.lyng.eval
import net.sergeych.lyng.obj.toInt import net.sergeych.lyng.obj.toInt
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BytecodeRecentOpsTest { class BytecodeRecentOpsTest {
@ -195,6 +197,46 @@ class BytecodeRecentOpsTest {
) )
} }
@Test
fun unionMemberDispatchSubtype() = runTest {
eval(
"""
class A { fun who() = "A" }
class B : A { override fun who() = "B" }
fun pick(x: A | B) { x.who() }
assertEquals("B", pick(B()))
""".trimIndent()
)
}
@Test
fun objectReceiverMemberError() = runTest {
val failed = try {
eval("fun bad(x) { x.missing() }")
false
} catch (_: ScriptError) {
true
}
assertTrue(failed)
}
@Test
fun unionMissingMemberError() = runTest {
val failed = try {
eval(
"""
class A { fun who() = "A" }
class B { fun other() = "B" }
fun pick(x: A | B) { x.who() }
""".trimIndent()
)
false
} catch (_: ScriptError) {
true
}
assertTrue(failed)
}
@Test @Test
fun qualifiedThisValueRef() = runTest { fun qualifiedThisValueRef() = runTest {
eval( eval(