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] 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.
- [x] Step 19: Unknown receiver member access in bytecode.
- [x] Reject Object/unknown receiver member calls without explicit cast or Dynamic.
- [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

View File

@ -2443,8 +2443,13 @@ class Compiler(
}
MethodCallRef(left, next.value, args, tailBlock, isOptional)
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, tailBlock, isOptional)
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, tailBlock, isOptional)
}
}
is QualifiedThisRef ->
QualifiedThisMethodSlotCallRef(
@ -2458,8 +2463,13 @@ class Compiler(
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
else -> {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, tailBlock, isOptional)
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
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)
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, true, isOptional)
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, true, isOptional)
}
}
is QualifiedThisRef ->
QualifiedThisMethodSlotCallRef(
@ -2511,8 +2526,13 @@ class Compiler(
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
else -> {
enforceReceiverTypeForMember(left, next.value, next.pos)
MethodCallRef(left, next.value, args, true, isOptional)
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
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)
ThisFieldSlotRef(next.value, ids.fieldId, ids.methodId, isOptional)
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
FieldRef(left, next.value, isOptional)
val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
if (unionField != null) {
unionField
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
FieldRef(left, next.value, isOptional)
}
}
is QualifiedThisRef -> run {
val ids = resolveMemberIds(next.value, next.pos, left.typeName)
@ -2544,8 +2569,13 @@ class Compiler(
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
else -> {
enforceReceiverTypeForMember(left, next.value, next.pos)
FieldRef(left, next.value, isOptional)
val unionField = buildUnionFieldAccess(left, next.value, next.pos, 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) {
for (cls in receiverClass.mro) {
val wrapperNames = listOf(

View File

@ -18,11 +18,13 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source
import net.sergeych.lyng.eval
import net.sergeych.lyng.obj.toInt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
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
fun qualifiedThisValueRef() = runTest {
eval(