diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 2a5bddf..e5ab69b 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -9,10 +9,10 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [x] Enable bytecode for `ImplicitThisMethodCallRef`, `QualifiedThisMethodSlotCallRef`, `QualifiedThisFieldSlotRef`. - [x] Keep unsupported cases blocked: `ClassScopeMemberRef`, dynamic receivers, delegated members. - [x] JVM tests must be green before commit. -- [ ] Step 3: Bytecode support for `try/catch/finally`. - - [ ] Implement bytecode emission for try/catch and finally blocks. - - [ ] Preserve existing error/stack semantics. - - [ ] JVM tests must be green before commit. +- [x] Step 3: Bytecode support for `try/catch/finally`. + - [x] Implement bytecode emission for try/catch and finally blocks. + - [x] Preserve existing error/stack semantics. + - [x] JVM tests must be green before commit. ## Notes diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt index 20274d0..5860cd2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt @@ -21,6 +21,7 @@ import net.sergeych.lyng.obj.Obj class BlockStatement( val block: Script, val slotPlan: Map, + val scopeId: Int, val captureSlots: List = emptyList(), private val startPos: Pos, ) : Statement() { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index b1588e9..63e9c9c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1897,7 +1897,11 @@ class Compiler( is ContinueStatement -> false is ReturnStatement -> target.resultExpr?.let { containsUnsupportedForBytecode(it) } ?: false is ThrowStatement -> containsUnsupportedForBytecode(target.throwExpr) - is TryStatement -> true + is TryStatement -> { + containsUnsupportedForBytecode(target.body) || + target.catches.any { containsUnsupportedForBytecode(it.block) } || + (target.finallyClause?.let { containsUnsupportedForBytecode(it) } ?: false) + } is WhenStatement -> { containsUnsupportedForBytecode(target.value) || target.cases.any { case -> @@ -2099,7 +2103,7 @@ class Compiler( is BlockStatement -> { val unwrapped = stmt.statements().map { unwrapBytecodeDeep(it) } val script = Script(stmt.block.pos, unwrapped) - BlockStatement(script, stmt.slotPlan, stmt.captureSlots, stmt.pos) + BlockStatement(script, stmt.slotPlan, stmt.scopeId, stmt.captureSlots, stmt.pos) } is InlineBlockStatement -> { val unwrapped = stmt.statements().map { unwrapBytecodeDeep(it) } @@ -5388,19 +5392,12 @@ class Compiler( for ((name, idx) in basePlan) { newPlan[name] = idx + 1 } - return BlockStatement(stmt.block, newPlan, stmt.captureSlots, stmt.pos) + return BlockStatement(stmt.block, newPlan, stmt.scopeId, stmt.captureSlots, stmt.pos) } fun stripCatchCaptures(block: Statement): Statement { val stmt = block as? BlockStatement ?: return block if (stmt.captureSlots.isEmpty()) return stmt - return BlockStatement(stmt.block, stmt.slotPlan, emptyList(), stmt.pos) - } - fun resolveExceptionClass(scope: Scope, name: String): ObjClass { - val rec = scope[name] - val cls = rec?.value as? ObjClass - if (cls != null) return cls - if (name == "Exception") return ObjException.Root - scope.raiseSymbolNotFound("error class does not exist or is not a class: $name") + return BlockStatement(stmt.block, stmt.slotPlan, stmt.scopeId, emptyList(), stmt.pos) } fun resolveCatchVarClass(names: List): ObjClass? { if (names.size == 1) { @@ -5507,55 +5504,15 @@ class Compiler( throw ScriptError(cc.currentPos(), "try block must have either catch or finally clause or both") val stmtPos = body.pos - val tryStatement = object : Statement() { - override val pos: Pos = stmtPos - override suspend fun execute(scope: Scope): Obj { - var result: Obj = ObjVoid - try { - // body is a parsed block, it already has separate context - result = body.execute(scope) - } catch (e: ReturnException) { - throw e - } catch (e: LoopBreakContinueException) { - throw e - } catch (e: Exception) { - // convert to appropriate exception - val caughtObj = when (e) { - is ExecutionError -> e.errorObject - else -> ObjUnknownException(scope, e.message ?: e.toString()) - } - // let's see if we should catch it: - var isCaught = false - for (cdata in catches) { - var match: Obj? = null - for (exceptionClassName in cdata.classNames) { - val exObj = resolveExceptionClass(scope, exceptionClassName) - if (caughtObj.isInstanceOf(exObj)) { - match = caughtObj - break - } - } - if (match != null) { - val catchContext = scope.createChildScope(pos = cdata.catchVar.pos).apply { - skipScopeCreation = true - } - catchContext.addItem(cdata.catchVar.value, false, caughtObj) - result = cdata.block.execute(catchContext) - isCaught = true - break - } - } - // rethrow if not caught this exception - if (!isCaught) - throw e - } finally { - // finally clause does not alter result! - finallyClause?.execute(scope) - } - return result - } + val tryCatches = catches.map { cdata -> + TryStatement.CatchBlock( + catchVarName = cdata.catchVar.value, + catchVarPos = cdata.catchVar.pos, + classNames = cdata.classNames, + block = cdata.block + ) } - return TryStatement(tryStatement, stmtPos) + return TryStatement(body, tryCatches, finallyClause, stmtPos) } private fun parseEnumDeclaration(isExtern: Boolean = false): Statement { @@ -7164,7 +7121,7 @@ class Compiler( } val planSnapshot = slotPlanIndices(blockSlotPlan) val block = Script(startPos, listOf(expr)) - val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos) + val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, capturePlan.captures.toList(), startPos) resolutionSink?.exitScope(cc.currentPos()) return stmt } @@ -7480,7 +7437,7 @@ class Compiler( slotPlanStack.removeLast() } val planSnapshot = slotPlanIndices(blockSlotPlan) - val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos) + val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, capturePlan.captures.toList(), startPos) val wrapped = wrapBytecode(stmt) return wrapped.also { val t1 = cc.next() @@ -7511,7 +7468,7 @@ class Compiler( slotPlanStack.removeLast() } val planSnapshot = slotPlanIndices(blockSlotPlan) - val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos) + val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, capturePlan.captures.toList(), startPos) val wrapped = wrapBytecode(stmt) return wrapped.also { val t1 = cc.next() @@ -7541,7 +7498,7 @@ class Compiler( slotPlanStack.removeLast() } val planSnapshot = slotPlanIndices(blockSlotPlan) - val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos) + val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, capturePlan.captures.toList(), startPos) val wrapped = wrapBytecode(stmt) val t1 = cc.next() if (t1.type != Token.Type.RBRACE) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TryStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TryStatement.kt index 5b52039..d9bacf9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TryStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TryStatement.kt @@ -17,14 +17,71 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjException +import net.sergeych.lyng.obj.ObjUnknownException +import net.sergeych.lyng.obj.ObjVoid class TryStatement( - private val delegate: Statement, + val body: Statement, + val catches: List, + val finallyClause: Statement?, private val startPos: Pos, ) : Statement() { override val pos: Pos = startPos + data class CatchBlock( + val catchVarName: String, + val catchVarPos: Pos, + val classNames: List, + val block: Statement + ) + override suspend fun execute(scope: Scope): Obj { - return delegate.execute(scope) + var result: Obj = ObjVoid + try { + result = body.execute(scope) + } catch (e: ReturnException) { + throw e + } catch (e: LoopBreakContinueException) { + throw e + } catch (e: Exception) { + val caughtObj = when (e) { + is ExecutionError -> e.errorObject + else -> ObjUnknownException(scope, e.message ?: e.toString()) + } + var isCaught = false + for (cdata in catches) { + var match: Obj? = null + for (exceptionClassName in cdata.classNames) { + val exObj = resolveExceptionClass(scope, exceptionClassName) + if (caughtObj.isInstanceOf(exObj)) { + match = caughtObj + break + } + } + if (match != null) { + val catchContext = scope.createChildScope(pos = cdata.catchVarPos).apply { + skipScopeCreation = true + } + catchContext.addItem(cdata.catchVarName, false, caughtObj) + result = cdata.block.execute(catchContext) + isCaught = true + break + } + } + if (!isCaught) throw e + } finally { + finallyClause?.execute(scope) + } + return result + } + + private fun resolveExceptionClass(scope: Scope, name: String): ObjClass { + val rec = scope[name] + val cls = rec?.value as? ObjClass + if (cls != null) return cls + if (name == "Exception") return ObjException.Root + scope.raiseSymbolNotFound("error class does not exist or is not a class: $name") } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 6d633e4..2833bc7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -133,7 +133,7 @@ class BytecodeCompiler( is net.sergeych.lyng.ThrowStatement -> compileThrowStatement(name, stmt) is net.sergeych.lyng.ExtensionPropertyDeclStatement -> compileExtensionPropertyDecl(name, stmt) is net.sergeych.lyng.TryStatement -> { - val value = emitStatementEval(stmt) + val value = emitTry(stmt, true) ?: return null builder.emit(Opcode.RET, value.slot) val localCount = maxOf(nextSlot, value.slot + 1) - scopeSlotCount builder.build( @@ -3314,12 +3314,13 @@ class BytecodeCompiler( is net.sergeych.lyng.ClassDeclStatement -> emitStatementEval(target) is net.sergeych.lyng.FunctionDeclStatement -> emitStatementEval(target) is net.sergeych.lyng.EnumDeclStatement -> emitStatementEval(target) - is net.sergeych.lyng.TryStatement -> emitStatementEval(target) + is net.sergeych.lyng.TryStatement -> emitTry(target, true) is net.sergeych.lyng.WhenStatement -> compileWhen(target, true) is net.sergeych.lyng.BreakStatement -> compileBreak(target) is net.sergeych.lyng.ContinueStatement -> compileContinue(target) is net.sergeych.lyng.ReturnStatement -> compileReturn(target) is net.sergeych.lyng.ThrowStatement -> compileThrow(target) + is net.sergeych.lyng.TryStatement -> emitTry(target, false) else -> { emitFallbackStatement(target) } @@ -3362,6 +3363,7 @@ class BytecodeCompiler( is net.sergeych.lyng.ContinueStatement -> compileContinue(target) is net.sergeych.lyng.ReturnStatement -> compileReturn(target) is net.sergeych.lyng.ThrowStatement -> compileThrow(target) + is net.sergeych.lyng.TryStatement -> emitTry(target, false) is net.sergeych.lyng.WhenStatement -> compileWhen(target, false) else -> { emitFallbackStatement(target) @@ -3423,6 +3425,167 @@ class BytecodeCompiler( return result } + private fun emitTry(stmt: net.sergeych.lyng.TryStatement, needResult: Boolean): CompiledValue? { + val resultSlot = allocSlot() + updateSlotType(resultSlot, SlotType.OBJ) + val exceptionSlot = allocSlot() + updateSlotType(exceptionSlot, SlotType.OBJ) + val catchLabel = if (stmt.catches.isNotEmpty()) builder.label() else null + val finallyLabel = if (stmt.finallyClause != null) builder.label() else null + val endLabel = builder.label() + val catchOperand = catchLabel?.let { CmdBuilder.Operand.LabelRef(it) } + ?: CmdBuilder.Operand.IntVal(-1) + val finallyOperand = finallyLabel?.let { CmdBuilder.Operand.LabelRef(it) } + ?: CmdBuilder.Operand.IntVal(-1) + builder.emit( + Opcode.PUSH_TRY, + listOf( + CmdBuilder.Operand.IntVal(exceptionSlot), + catchOperand, + finallyOperand + ) + ) + val bodyValue = compileStatementValueOrFallback(stmt.body, needResult) ?: return null + if (needResult) { + emitMove(bodyValue, resultSlot) + } + if (finallyLabel != null) { + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(finallyLabel))) + } else { + builder.emit(Opcode.POP_TRY) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + } + if (catchLabel != null) { + builder.mark(catchLabel) + val catchBlockLabels = stmt.catches.map { builder.label() } + val noMatchLabel = builder.label() + for ((index, cdata) in stmt.catches.withIndex()) { + val handlerLabel = catchBlockLabels[index] + for (className in cdata.classNames) { + val classValue = compileCatchClassSlot(className) ?: return null + val checkSlot = allocSlot() + builder.emit(Opcode.CHECK_IS, exceptionSlot, classValue.slot, checkSlot) + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(checkSlot), CmdBuilder.Operand.LabelRef(handlerLabel)) + ) + } + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(noMatchLabel))) + for ((index, cdata) in stmt.catches.withIndex()) { + val handlerLabel = catchBlockLabels[index] + builder.mark(handlerLabel) + builder.emit(Opcode.CLEAR_PENDING_THROWABLE) + val catchValue = emitCatchBlock(cdata.block, cdata.catchVarName, exceptionSlot, needResult) + ?: return null + if (needResult) { + emitMove(catchValue, resultSlot) + } + if (finallyLabel != null) { + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(finallyLabel))) + } else { + builder.emit(Opcode.POP_TRY) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) + } + } + builder.mark(noMatchLabel) + if (finallyLabel != null) { + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(finallyLabel))) + } else { + builder.emit(Opcode.POP_TRY) + builder.emit(Opcode.RETHROW_PENDING) + } + } + if (finallyLabel != null) { + builder.mark(finallyLabel) + builder.emit(Opcode.POP_TRY) + stmt.finallyClause?.let { finallyClause -> + compileStatementValueOrFallback(finallyClause, false) ?: return null + } + builder.emit(Opcode.RETHROW_PENDING) + } + builder.mark(endLabel) + if (needResult) return CompiledValue(resultSlot, SlotType.OBJ) + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + val voidSlot = allocSlot() + builder.emit(Opcode.CONST_OBJ, voidId, voidSlot) + return CompiledValue(voidSlot, SlotType.OBJ) + } + + private fun emitCatchBlock( + block: Statement, + catchVarName: String, + exceptionSlot: Int, + needResult: Boolean + ): CompiledValue? { + val stmt = block as? BlockStatement + if (stmt == null) { + val declId = builder.addConst(BytecodeConst.LocalDecl(catchVarName, false, Visibility.Public, false)) + builder.emit(Opcode.DECL_LOCAL, declId, exceptionSlot) + return compileStatementValueOrFallback(block, needResult) + } + val captureNames = if (stmt.captureSlots.isEmpty()) emptyList() else stmt.captureSlots.map { it.name } + val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.slotPlan, captureNames)) + builder.emit(Opcode.PUSH_SCOPE, planId) + resetAddrCache() + val declId = builder.addConst(BytecodeConst.LocalDecl(catchVarName, false, Visibility.Public, false)) + builder.emit(Opcode.DECL_LOCAL, declId, exceptionSlot) + val catchSlotIndex = stmt.slotPlan[catchVarName] + if (catchSlotIndex != null) { + val key = ScopeSlotKey(stmt.scopeId, catchSlotIndex) + val localIndex = localSlotIndexByKey[key] + if (localIndex != null) { + val localSlot = scopeSlotCount + localIndex + if (localSlot != exceptionSlot) { + emitMove(CompiledValue(exceptionSlot, SlotType.OBJ), localSlot) + } + updateSlotType(localSlot, SlotType.OBJ) + } + } + val statements = stmt.statements() + var lastValue: CompiledValue? = null + for ((index, statement) in statements.withIndex()) { + val isLast = index == statements.lastIndex + val wantResult = needResult && isLast + val value = compileStatementValueOrFallback(statement, wantResult) ?: return null + if (wantResult) { + lastValue = value + } + } + val result = if (needResult) { + lastValue ?: run { + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + CompiledValue(slot, SlotType.OBJ) + } + } else { + lastValue ?: run { + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + CompiledValue(slot, SlotType.OBJ) + } + } + builder.emit(Opcode.POP_SCOPE) + resetAddrCache() + return result + } + + private fun compileCatchClassSlot(name: String): CompiledValue? { + val ref = LocalVarRef(name, Pos.builtIn) + val compiled = compileRef(ref) + if (compiled != null) { + return ensureObjSlot(compiled) + } + val cls = nameObjClass[name] ?: resolveTypeNameClass(name) ?: return null + val id = builder.addConst(BytecodeConst.ObjRef(cls)) + val slot = allocSlot() + builder.emit(Opcode.CONST_OBJ, id, slot) + updateSlotType(slot, SlotType.OBJ) + return CompiledValue(slot, SlotType.OBJ) + } + private fun emitInlineStatements(statements: List, needResult: Boolean): CompiledValue? { var lastValue: CompiledValue? = null for ((index, statement) in statements.withIndex()) { @@ -5181,6 +5344,13 @@ class BytecodeCompiler( } stmt.elseCase?.let { collectScopeSlots(it) } } + is net.sergeych.lyng.TryStatement -> { + collectScopeSlots(stmt.body) + for (catchBlock in stmt.catches) { + collectScopeSlots(catchBlock.block) + } + stmt.finallyClause?.let { collectScopeSlots(it) } + } is net.sergeych.lyng.BreakStatement -> { stmt.resultExpr?.let { collectScopeSlots(it) } } @@ -5362,6 +5532,13 @@ class BytecodeCompiler( is net.sergeych.lyng.ThrowStatement -> { collectLoopVarNames(stmt.throwExpr) } + is net.sergeych.lyng.TryStatement -> { + collectLoopVarNames(stmt.body) + for (catchBlock in stmt.catches) { + collectLoopVarNames(catchBlock.block) + } + stmt.finallyClause?.let { collectLoopVarNames(it) } + } else -> {} } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index 3d137e5..d829a8e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -126,7 +126,11 @@ class BytecodeStatement private constructor( is net.sergeych.lyng.ClassDeclStatement -> false is net.sergeych.lyng.FunctionDeclStatement -> false is net.sergeych.lyng.EnumDeclStatement -> false - is net.sergeych.lyng.TryStatement -> true + is net.sergeych.lyng.TryStatement -> { + containsUnsupportedStatement(target.body) || + target.catches.any { containsUnsupportedStatement(it.block) } || + (target.finallyClause?.let { containsUnsupportedStatement(it) } ?: false) + } is net.sergeych.lyng.WhenStatement -> { containsUnsupportedStatement(target.value) || target.cases.any { case -> @@ -147,6 +151,7 @@ class BytecodeStatement private constructor( net.sergeych.lyng.BlockStatement( net.sergeych.lyng.Script(stmt.pos, unwrapped), stmt.slotPlan, + stmt.scopeId, stmt.captureSlots, stmt.pos ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index a8bc970..e0c29f5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -119,7 +119,8 @@ class CmdBuilder { private fun operandKinds(op: Opcode): List { return when (op) { - Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN -> emptyList() + Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN, Opcode.POP_TRY, + Opcode.CLEAR_PENDING_THROWABLE, Opcode.RETHROW_PENDING -> emptyList() Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.OBJ_TO_BOOL, @@ -144,6 +145,8 @@ class CmdBuilder { listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) + Opcode.PUSH_TRY -> + listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP) Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE -> listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, @@ -238,6 +241,7 @@ class CmdBuilder { Opcode.MAKE_QUALIFIED_VIEW -> CmdMakeQualifiedView(operands[0], operands[1], operands[2]) Opcode.RET_LABEL -> CmdRetLabel(operands[0], operands[1]) Opcode.THROW -> CmdThrow(operands[0], operands[1]) + Opcode.RETHROW_PENDING -> CmdRethrowPending() Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1]) Opcode.LOAD_OBJ_ADDR -> CmdLoadObjAddr(operands[0], operands[1]) Opcode.STORE_OBJ_ADDR -> CmdStoreObjAddr(operands[0], operands[1]) @@ -376,6 +380,9 @@ class CmdBuilder { Opcode.POP_SCOPE -> CmdPopScope() Opcode.PUSH_SLOT_PLAN -> CmdPushSlotPlan(operands[0]) Opcode.POP_SLOT_PLAN -> CmdPopSlotPlan() + Opcode.PUSH_TRY -> CmdPushTry(operands[0], operands[1], operands[2]) + Opcode.POP_TRY -> CmdPopTry() + Opcode.CLEAR_PENDING_THROWABLE -> CmdClearPendingThrowable() Opcode.DECL_LOCAL -> CmdDeclLocal(operands[0], operands[1]) Opcode.DECL_DELEGATED -> CmdDeclDelegated(operands[0], operands[1]) Opcode.DECL_DESTRUCTURE -> CmdDeclDestructure(operands[0], operands[1]) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt index 4d1a552..b5af516 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -180,10 +180,14 @@ object CmdDisassembler { is CmdRetLabel -> Opcode.RET_LABEL to intArrayOf(cmd.labelId, cmd.slot) is CmdRetVoid -> Opcode.RET_VOID to intArrayOf() is CmdThrow -> Opcode.THROW to intArrayOf(cmd.posId, cmd.slot) + is CmdRethrowPending -> Opcode.RETHROW_PENDING to intArrayOf() is CmdPushScope -> Opcode.PUSH_SCOPE to intArrayOf(cmd.planId) is CmdPopScope -> Opcode.POP_SCOPE to intArrayOf() is CmdPushSlotPlan -> Opcode.PUSH_SLOT_PLAN to intArrayOf(cmd.planId) is CmdPopSlotPlan -> Opcode.POP_SLOT_PLAN to intArrayOf() + is CmdPushTry -> Opcode.PUSH_TRY to intArrayOf(cmd.exceptionSlot, cmd.catchIp, cmd.finallyIp) + is CmdPopTry -> Opcode.POP_TRY to intArrayOf() + is CmdClearPendingThrowable -> Opcode.CLEAR_PENDING_THROWABLE to intArrayOf() is CmdDeclLocal -> Opcode.DECL_LOCAL to intArrayOf(cmd.constId, cmd.slot) is CmdDeclDelegated -> Opcode.DECL_DELEGATED to intArrayOf(cmd.constId, cmd.slot) is CmdDeclDestructure -> Opcode.DECL_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot) @@ -214,7 +218,8 @@ object CmdDisassembler { private fun operandKinds(op: Opcode): List { return when (op) { - Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN, + Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN, Opcode.POP_TRY, + Opcode.CLEAR_PENDING_THROWABLE, Opcode.RETHROW_PENDING, Opcode.ITER_POP, Opcode.ITER_CANCEL -> emptyList() Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, @@ -242,6 +247,8 @@ object CmdDisassembler { listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) + Opcode.PUSH_TRY -> + listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP) Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE -> listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 93aa028..c27bdae 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -34,7 +34,14 @@ class CmdVm { while (result == null) { val cmd = cmds[frame.ip] frame.ip += 1 - cmd.perform(frame) + try { + cmd.perform(frame) + } catch (e: Throwable) { + if (!frame.handleException(e)) { + frame.cancelIterators() + throw e + } + } } } catch (e: Throwable) { frame.cancelIterators() @@ -1138,6 +1145,13 @@ class CmdThrow(internal val posId: Int, internal val slot: Int) : Cmd() { } } +class CmdRethrowPending : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.rethrowPending() + return + } +} + class CmdPushScope(internal val planId: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val planConst = frame.fn.constants[planId] as? BytecodeConst.SlotPlan @@ -1170,6 +1184,27 @@ class CmdPopSlotPlan : Cmd() { } } +class CmdPushTry(internal val exceptionSlot: Int, internal val catchIp: Int, internal val finallyIp: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.pushTry(exceptionSlot, catchIp, finallyIp) + return + } +} + +class CmdPopTry : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.popTry() + return + } +} + +class CmdClearPendingThrowable : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.clearPendingThrowable() + return + } +} + class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val decl = frame.fn.constants[constId] as? BytecodeConst.LocalDecl @@ -1627,6 +1662,14 @@ class CmdFrame( private var scopeDepth = 0 private var virtualDepth = 0 private val iterStack = ArrayDeque() + internal data class TryHandler( + val exceptionSlot: Int, + val catchIp: Int, + val finallyIp: Int, + var inCatch: Boolean = false + ) + internal val tryStack = ArrayDeque() + private var pendingThrowable: Throwable? = null internal val frame = BytecodeFrame(fn.localCount, args.size) private val addrScopes: Array = arrayOfNulls(fn.addrCount) @@ -1709,6 +1752,63 @@ class CmdFrame( return scope } + fun handleException(t: Throwable): Boolean { + val handler = tryStack.lastOrNull() ?: return false + val finallyIp = handler.finallyIp + if (t is ReturnException || t is LoopBreakContinueException) { + if (finallyIp >= 0) { + pendingThrowable = t + ip = finallyIp + return true + } + return false + } + if (handler.inCatch) { + if (finallyIp >= 0) { + pendingThrowable = t + ip = finallyIp + return true + } + return false + } + handler.inCatch = true + pendingThrowable = t + if (handler.catchIp >= 0) { + val caughtObj = when (t) { + is ExecutionError -> t.errorObject + else -> ObjUnknownException(ensureScope(), t.message ?: t.toString()) + } + storeObjResult(handler.exceptionSlot, caughtObj) + ip = handler.catchIp + return true + } + if (finallyIp >= 0) { + ip = finallyIp + return true + } + return false + } + + fun pushTry(exceptionSlot: Int, catchIp: Int, finallyIp: Int) { + tryStack.addLast(TryHandler(exceptionSlot, catchIp, finallyIp)) + } + + fun popTry() { + if (tryStack.isNotEmpty()) { + tryStack.removeLast() + } + } + + fun clearPendingThrowable() { + pendingThrowable = null + } + + fun rethrowPending() { + val t = pendingThrowable ?: return + pendingThrowable = null + throw t + } + private fun posForIp(ip: Int): Pos? { if (ip < 0) return null return fn.posByIp.getOrNull(ip) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index 2db9731..9279dab 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -127,6 +127,9 @@ enum class Opcode(val code: Int) { DECL_EXT_PROPERTY(0x8A), DECL_DELEGATED(0x8B), DECL_DESTRUCTURE(0x8C), + PUSH_TRY(0x8D), + POP_TRY(0x8E), + CLEAR_PENDING_THROWABLE(0x8F), CALL_DIRECT(0x90), CALL_MEMBER_SLOT(0x92), @@ -148,6 +151,7 @@ enum class Opcode(val code: Int) { LOAD_BOOL_ADDR(0xB8), STORE_BOOL_ADDR(0xB9), THROW(0xBB), + RETHROW_PENDING(0xBC), ITER_PUSH(0xBF), ITER_POP(0xC0), ITER_CANCEL(0xC1),