Step 19: union member access
This commit is contained in:
parent
5305ced89f
commit
565dbf98ed
@ -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
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user