diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 1ac366c..ad74b01 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -3401,26 +3401,15 @@ class Compiler( val names = mutableListOf() pattern.forEachVariable { names.add(it) } - return object : Statement() { - override val pos: Pos = start - override suspend fun execute(context: Scope): Obj { - val value = initialExpression.execute(context) - for (name in names) { - context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient) - } - pattern.setAt(start, context, value) - if (!isMutable) { - for (name in names) { - val rec = context.objects[name]!! - val immutableRec = rec.copy(isMutable = false) - context.objects[name] = immutableRec - context.localBindings[name] = immutableRec - context.updateSlotFor(name, immutableRec) - } - } - return ObjVoid - } - } + return DestructuringVarDeclStatement( + pattern, + names, + initialExpression, + isMutable, + visibility, + isTransient, + start + ) } if (nextToken.type != Token.Type.ID) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/DestructuringVarDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/DestructuringVarDeclStatement.kt new file mode 100644 index 0000000..c0f9533 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/DestructuringVarDeclStatement.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sergeych.lyng + +import net.sergeych.lyng.obj.ListLiteralRef +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjVoid + +class DestructuringVarDeclStatement( + val pattern: ListLiteralRef, + val names: List, + val initializer: Statement, + val isMutable: Boolean, + val visibility: Visibility, + val isTransient: Boolean, + override val pos: Pos, +) : Statement() { + override suspend fun execute(context: Scope): Obj { + val value = initializer.execute(context) + for (name in names) { + context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient) + } + pattern.setAt(pos, context, value) + if (!isMutable) { + for (name in names) { + val rec = context.objects[name]!! + val immutableRec = rec.copy(isMutable = false) + context.objects[name] = immutableRec + context.localBindings[name] = immutableRec + context.updateSlotFor(name, immutableRec) + } + } + return ObjVoid + } +} 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 9db5e9d..a7c5d3e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -17,6 +17,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.BlockStatement +import net.sergeych.lyng.DestructuringVarDeclStatement import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.IfStatement import net.sergeych.lyng.ParsedArgument @@ -69,6 +70,7 @@ class BytecodeCompiler( val continueLabel: CmdBuilder.Label, val breakFlagSlot: Int, val resultSlot: Int?, + val hasIterator: Boolean, ) fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): CmdFunction? { @@ -1973,6 +1975,7 @@ class BytecodeCompiler( } is BlockStatement -> emitBlock(target, true) is VarDeclStatement -> emitVarDecl(target) + is DestructuringVarDeclStatement -> emitStatementEval(target) is net.sergeych.lyng.ExtensionPropertyDeclStatement -> emitExtensionPropertyDecl(target) is net.sergeych.lyng.ClassDeclStatement -> emitStatementEval(target) is net.sergeych.lyng.FunctionDeclStatement -> emitStatementEval(target) @@ -2018,6 +2021,7 @@ class BytecodeCompiler( } is BlockStatement -> emitBlock(target, false) is VarDeclStatement -> emitVarDecl(target) + is DestructuringVarDeclStatement -> emitStatementEval(target) is net.sergeych.lyng.ExtensionPropertyDeclStatement -> emitExtensionPropertyDecl(target) is net.sergeych.lyng.BreakStatement -> compileBreak(target) is net.sergeych.lyng.ContinueStatement -> compileContinue(target) @@ -2202,6 +2206,7 @@ class BytecodeCompiler( val iterSlot = allocSlot() val iteratorId = builder.addConst(BytecodeConst.StringVal("iterator")) builder.emit(Opcode.CALL_VIRTUAL, sourceObj.slot, iteratorId, 0, 0, iterSlot) + builder.emit(Opcode.ITER_PUSH, iterSlot) val breakFlagSlot = allocSlot() val falseId = builder.addConst(BytecodeConst.Bool(false)) @@ -2235,7 +2240,14 @@ class BytecodeCompiler( updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ) loopStack.addLast( - LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null) + LoopContext( + stmt.label, + endLabel, + continueLabel, + breakFlagSlot, + if (wantResult) resultSlot else null, + hasIterator = true + ) ) val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null loopStack.removeLast() @@ -2247,6 +2259,13 @@ class BytecodeCompiler( builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel))) builder.mark(endLabel) + val afterPop = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterPop)) + ) + builder.emit(Opcode.ITER_POP) + builder.mark(afterPop) if (stmt.elseStatement != null) { val afterElse = builder.label() builder.emit( @@ -2316,7 +2335,14 @@ class BytecodeCompiler( updateSlotType(loopSlotId, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT) loopStack.addLast( - LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null) + LoopContext( + stmt.label, + endLabel, + continueLabel, + breakFlagSlot, + if (wantResult) resultSlot else null, + hasIterator = false + ) ) val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null loopStack.removeLast() @@ -2375,7 +2401,14 @@ class BytecodeCompiler( updateSlotType(loopSlotId, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT) loopStack.addLast( - LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null) + LoopContext( + stmt.label, + endLabel, + continueLabel, + breakFlagSlot, + if (wantResult) resultSlot else null, + hasIterator = false + ) ) val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null loopStack.removeLast() @@ -2429,7 +2462,14 @@ class BytecodeCompiler( listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(endLabel)) ) loopStack.addLast( - LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null) + LoopContext( + stmt.label, + endLabel, + continueLabel, + breakFlagSlot, + if (wantResult) resultSlot else null, + hasIterator = false + ) ) val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null loopStack.removeLast() @@ -2471,7 +2511,14 @@ class BytecodeCompiler( val endLabel = builder.label() builder.mark(loopLabel) loopStack.addLast( - LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null) + LoopContext( + stmt.label, + endLabel, + continueLabel, + breakFlagSlot, + if (wantResult) resultSlot else null, + hasIterator = false + ) ) val bodyValue = compileStatementValueOrFallback(stmt.body, wantResult) ?: return null loopStack.removeLast() @@ -2577,17 +2624,28 @@ class BytecodeCompiler( } } - private fun findLoopContext(label: String?): LoopContext? { + private fun findLoopContextIndex(label: String?): Int? { if (loopStack.isEmpty()) return null - if (label == null) return loopStack.last() - for (ctx in loopStack.reversed()) { - if (ctx.label == label) return ctx + val stack = loopStack.toList() + if (label == null) return stack.lastIndex + for (i in stack.indices.reversed()) { + if (stack[i].label == label) return i } return null } + private fun emitIteratorCancel(stack: List, startIndex: Int) { + for (i in stack.lastIndex downTo startIndex) { + if (stack[i].hasIterator) { + builder.emit(Opcode.ITER_CANCEL) + } + } + } + private fun compileBreak(stmt: net.sergeych.lyng.BreakStatement): CompiledValue? { - val ctx = findLoopContext(stmt.label) ?: return null + val stack = loopStack.toList() + val targetIndex = findLoopContextIndex(stmt.label) ?: return null + val ctx = stack[targetIndex] val value = stmt.resultExpr?.let { compileStatementValueOrFallback(it) } if (ctx.resultSlot != null) { val objValue = value?.let { ensureObjSlot(it) } ?: run { @@ -2601,6 +2659,7 @@ class BytecodeCompiler( } else if (value != null) { ensureObjSlot(value) } + emitIteratorCancel(stack, targetIndex) val trueId = builder.addConst(BytecodeConst.Bool(true)) builder.emit(Opcode.CONST_BOOL, trueId, ctx.breakFlagSlot) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(ctx.breakLabel))) @@ -2608,7 +2667,12 @@ class BytecodeCompiler( } private fun compileContinue(stmt: net.sergeych.lyng.ContinueStatement): CompiledValue? { - val ctx = findLoopContext(stmt.label) ?: return null + val stack = loopStack.toList() + val targetIndex = findLoopContextIndex(stmt.label) ?: return null + val ctx = stack[targetIndex] + if (targetIndex < stack.lastIndex) { + emitIteratorCancel(stack, targetIndex + 1) + } builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(ctx.continueLabel))) return CompiledValue(ctx.breakFlagSlot, SlotType.BOOL) } 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 eb76e79..3c2dbc5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -19,6 +19,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.Pos import net.sergeych.lyng.Scope import net.sergeych.lyng.Statement +import net.sergeych.lyng.DestructuringVarDeclStatement import net.sergeych.lyng.WhenCase import net.sergeych.lyng.WhenCondition import net.sergeych.lyng.WhenEqualsCondition @@ -100,6 +101,8 @@ class BytecodeStatement private constructor( target.statements().any { containsUnsupportedStatement(it) } is net.sergeych.lyng.VarDeclStatement -> target.initializer?.let { containsUnsupportedStatement(it) } ?: false + is net.sergeych.lyng.DestructuringVarDeclStatement -> + containsUnsupportedStatement(target.initializer) is net.sergeych.lyng.BreakStatement -> target.resultExpr?.let { containsUnsupportedStatement(it) } ?: false is net.sergeych.lyng.ContinueStatement -> false @@ -147,6 +150,17 @@ class BytecodeStatement private constructor( stmt.pos ) } + is net.sergeych.lyng.DestructuringVarDeclStatement -> { + net.sergeych.lyng.DestructuringVarDeclStatement( + stmt.pattern, + stmt.names, + unwrapDeep(stmt.initializer), + stmt.isMutable, + stmt.visibility, + stmt.isTransient, + stmt.pos + ) + } is net.sergeych.lyng.IfStatement -> { net.sergeych.lyng.IfStatement( unwrapDeep(stmt.condition), 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 e3b30da..74591c8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -192,6 +192,10 @@ class CmdBuilder { listOf(OperandKind.ID, OperandKind.SLOT) Opcode.EVAL_FALLBACK, Opcode.EVAL_REF, Opcode.EVAL_STMT, Opcode.EVAL_VALUE_FN -> listOf(OperandKind.ID, OperandKind.SLOT) + Opcode.ITER_PUSH -> + listOf(OperandKind.SLOT) + Opcode.ITER_POP, Opcode.ITER_CANCEL -> + emptyList() } } @@ -387,6 +391,9 @@ class CmdBuilder { Opcode.EVAL_REF -> CmdEvalRef(operands[0], operands[1]) Opcode.EVAL_STMT -> CmdEvalStmt(operands[0], operands[1]) Opcode.EVAL_VALUE_FN -> CmdEvalValueFn(operands[0], operands[1]) + Opcode.ITER_PUSH -> CmdIterPush(operands[0]) + Opcode.ITER_POP -> CmdIterPop() + Opcode.ITER_CANCEL -> CmdIterCancel() } } } 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 2692207..63cffaa 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -191,6 +191,9 @@ object CmdDisassembler { is CmdEvalRef -> Opcode.EVAL_REF to intArrayOf(cmd.id, cmd.dst) is CmdEvalStmt -> Opcode.EVAL_STMT to intArrayOf(cmd.id, cmd.dst) is CmdEvalValueFn -> Opcode.EVAL_VALUE_FN to intArrayOf(cmd.id, cmd.dst) + is CmdIterPush -> Opcode.ITER_PUSH to intArrayOf(cmd.iterSlot) + is CmdIterPop -> Opcode.ITER_POP to intArrayOf() + is CmdIterCancel -> Opcode.ITER_CANCEL to intArrayOf() } } @@ -205,7 +208,8 @@ object CmdDisassembler { 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.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, Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> @@ -248,7 +252,7 @@ object CmdDisassembler { Opcode.ADD_OBJ, Opcode.SUB_OBJ, Opcode.MUL_OBJ, Opcode.DIV_OBJ, Opcode.MOD_OBJ, Opcode.CONTAINS_OBJ, Opcode.AND_BOOL, Opcode.OR_BOOL -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) - Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET -> + Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET, Opcode.ITER_PUSH -> listOf(OperandKind.SLOT) Opcode.JMP -> listOf(OperandKind.IP) 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 e7b44d9..d4755fa 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -32,11 +32,17 @@ class CmdVm { result = null val frame = CmdFrame(this, fn, scope0, args) val cmds = fn.cmds - while (result == null) { - val cmd = cmds[frame.ip] - frame.ip += 1 - cmd.perform(frame) + try { + while (result == null) { + val cmd = cmds[frame.ip] + frame.ip += 1 + cmd.perform(frame) + } + } catch (e: Throwable) { + frame.cancelIterators() + throw e } + frame.cancelIterators() return result ?: ObjVoid } } @@ -1419,6 +1425,27 @@ class CmdEvalValueFn(internal val id: Int, internal val dst: Int) : Cmd() { } } +class CmdIterPush(internal val iterSlot: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.pushIterator(frame.slotToObj(iterSlot)) + return + } +} + +class CmdIterPop : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.popIterator() + return + } +} + +class CmdIterCancel : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.cancelTopIterator() + return + } +} + class CmdFrame( val vm: CmdVm, val fn: CmdFunction, @@ -1440,6 +1467,7 @@ class CmdFrame( internal val slotPlanScopeStack = ArrayDeque() private var scopeDepth = 0 private var virtualDepth = 0 + private val iterStack = ArrayDeque() internal val frame = BytecodeFrame(fn.localCount, args.size) private val addrScopes: Array = arrayOfNulls(fn.addrCount) @@ -1484,6 +1512,26 @@ class CmdFrame( scopeDepth -= 1 } + fun pushIterator(iter: Obj) { + iterStack.addLast(iter) + } + + fun popIterator() { + iterStack.removeLastOrNull() + } + + suspend fun cancelTopIterator() { + val iter = iterStack.removeLastOrNull() ?: return + iter.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid } + } + + suspend fun cancelIterators() { + while (iterStack.isNotEmpty()) { + val iter = iterStack.removeLast() + iter.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid } + } + } + fun pushSlotPlan(plan: Map) { if (scope.hasSlotPlanConflict(plan)) { scopeStack.addLast(scope) 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 790c56d..4857808 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -149,6 +149,9 @@ enum class Opcode(val code: Int) { EVAL_REF(0xBC), EVAL_STMT(0xBD), EVAL_VALUE_FN(0xBE), + ITER_PUSH(0xBF), + ITER_POP(0xC0), + ITER_CANCEL(0xC1), ; companion object { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index dfa0aa2..5792252 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -186,7 +186,6 @@ class ScriptTest { assertEquals(0, ti.cancelCount) } - @Ignore("Bytecode: unsupported or incorrect behavior") @Test fun testForLoopCancelsOnBreak() = runTest { val scope = Script.newScope() @@ -202,7 +201,6 @@ class ScriptTest { assertEquals(1, ti.cancelCount) } - @Ignore("Bytecode: unsupported or incorrect behavior") @Test fun testForLoopCancelsOnException() = runTest { val scope = Script.newScope() @@ -4567,7 +4565,6 @@ class ScriptTest { } - @Ignore("Bytecode: unsupported or incorrect behavior") @Test fun testDestructuringAssignment() = runTest { eval(