From 9d508e219ff0c4e160c31fcfc0f4f39a9b2c96d2 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 9 Feb 2026 12:13:33 +0300 Subject: [PATCH] Step 21: union mismatch bytecode throw --- bytecode_migration_plan.md | 6 +++--- .../kotlin/net/sergeych/lyng/Compiler.kt | 15 ++++----------- .../commonTest/kotlin/BytecodeRecentOpsTest.kt | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index b6375fd..4f496f2 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -70,9 +70,9 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Allow `NopStatement` in `containsUnsupportedForBytecode`. - [x] Emit `ObjVoid` directly in bytecode for `NopStatement` in statement/value contexts. - [x] Add a JVM test that exercises a code path returning `NopStatement` in bytecode (e.g., static class member decl in class body). -- [ ] Step 21: Union mismatch path in bytecode. - - [ ] Replace `UnionTypeMismatchStatement` branch with a bytecode-compilable throw path (no custom `StatementRef` that blocks bytecode). - - [ ] Add a JVM test that forces the union mismatch at runtime and asserts the error message. +- [x] Step 21: Union mismatch path in bytecode. + - [x] Replace `UnionTypeMismatchStatement` branch with a bytecode-compilable throw path (no custom `StatementRef` that blocks bytecode). + - [x] Add a JVM test that forces the union mismatch at runtime and asserts the error message. - [ ] Step 22: Delegated local slots in bytecode. - [ ] Support reads/writes/assign-ops/inc/dec for delegated locals (`LocalSlotRef.isDelegated`) in `BytecodeCompiler`. - [ ] Remove `containsDelegatedRefs` guard once delegated locals are bytecode-safe. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 24b54f6..47f8b97 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -4341,15 +4341,6 @@ 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 @@ -4392,8 +4383,10 @@ class Compiler( 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) + val errorMessage = ObjString("value is not $unionName").asReadonly + val throwExpr = ExpressionStatement(ConstRef(errorMessage), pos) + val throwStmt = ThrowStatement(throwExpr, pos) + var current: ObjRef = net.sergeych.lyng.obj.StatementRef(throwStmt) for (option in options.asReversed()) { val typeRef = net.sergeych.lyng.obj.TypeDeclRef(option, pos) val cond = BinaryOpRef(BinOp.IS, left, typeRef) diff --git a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt index 89e9bf6..de960c3 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeRecentOpsTest.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.test.runTest import net.sergeych.lyng.Compiler +import net.sergeych.lyng.ExecutionError import net.sergeych.lyng.Script import net.sergeych.lyng.ScriptError import net.sergeych.lyng.Source @@ -24,6 +25,7 @@ import net.sergeych.lyng.eval import net.sergeych.lyng.obj.toInt import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertTrue class BytecodeRecentOpsTest { @@ -221,6 +223,21 @@ class BytecodeRecentOpsTest { ) } + @Test + fun unionMemberDispatchMismatch() = runTest { + val err = assertFailsWith { + eval( + """ + class A { fun who() = "A" } + class B { fun who() = "B" } + val x: A | B = 1 + x.who() + """.trimIndent() + ) + } + assertTrue(err.message?.contains("value is not A | B") == true) + } + @Test fun objectReceiverMemberError() = runTest { val failed = try {