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] 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
|
||||||
|
|
||||||
|
|||||||
@ -2442,10 +2442,15 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||||
|
} else {
|
||||||
|
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
|
||||||
|
if (unionCall != null) {
|
||||||
|
unionCall
|
||||||
} else {
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||||
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is QualifiedThisRef ->
|
is QualifiedThisRef ->
|
||||||
QualifiedThisMethodSlotCallRef(
|
QualifiedThisMethodSlotCallRef(
|
||||||
left.typeName,
|
left.typeName,
|
||||||
@ -2458,12 +2463,17 @@ class Compiler(
|
|||||||
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
|
||||||
|
if (unionCall != null) {
|
||||||
|
unionCall
|
||||||
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||||
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
MethodCallRef(left, next.value, args, tailBlock, isOptional)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
||||||
@ -2495,10 +2505,15 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MethodCallRef(left, next.value, args, true, isOptional)
|
MethodCallRef(left, next.value, args, true, isOptional)
|
||||||
|
} else {
|
||||||
|
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
|
||||||
|
if (unionCall != null) {
|
||||||
|
unionCall
|
||||||
} else {
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||||
MethodCallRef(left, next.value, args, true, isOptional)
|
MethodCallRef(left, next.value, args, true, isOptional)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is QualifiedThisRef ->
|
is QualifiedThisRef ->
|
||||||
QualifiedThisMethodSlotCallRef(
|
QualifiedThisMethodSlotCallRef(
|
||||||
left.typeName,
|
left.typeName,
|
||||||
@ -2511,11 +2526,16 @@ class Compiler(
|
|||||||
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
|
||||||
|
if (unionCall != null) {
|
||||||
|
unionCall
|
||||||
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||||
MethodCallRef(left, next.value, args, true, isOptional)
|
MethodCallRef(left, next.value, args, true, isOptional)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
@ -2527,10 +2547,15 @@ class Compiler(
|
|||||||
val implicitType = currentImplicitThisTypeName()
|
val implicitType = currentImplicitThisTypeName()
|
||||||
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 {
|
||||||
|
val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
|
||||||
|
if (unionField != null) {
|
||||||
|
unionField
|
||||||
} else {
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||||
FieldRef(left, next.value, isOptional)
|
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)
|
||||||
QualifiedThisFieldSlotRef(
|
QualifiedThisFieldSlotRef(
|
||||||
@ -2544,12 +2569,17 @@ class Compiler(
|
|||||||
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
|
||||||
|
if (unionField != null) {
|
||||||
|
unionField
|
||||||
|
} else {
|
||||||
enforceReceiverTypeForMember(left, next.value, next.pos)
|
enforceReceiverTypeForMember(left, next.value, next.pos)
|
||||||
FieldRef(left, next.value, isOptional)
|
FieldRef(left, next.value, isOptional)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
?: throw ScriptError(t.pos, "Expecting expression before dot")
|
?: throw ScriptError(t.pos, "Expecting expression before dot")
|
||||||
}
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user