From 8dfdbaa0a05c8da40bc8bd65dfa5f12d65c62f83 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 28 Jan 2026 08:23:04 +0300 Subject: [PATCH] Bytecode for iterable for-in loops --- .../kotlin/net/sergeych/lyng/Compiler.kt | 10 +-- .../lyng/bytecode/BytecodeCompiler.kt | 73 ++++++++++++++++++- .../lyng/bytecode/BytecodeStatement.kt | 9 +-- .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 9 ++- .../sergeych/lyng/bytecode/CmdDisassembler.kt | 7 ++ .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 31 ++++++++ .../net/sergeych/lyng/bytecode/Opcode.kt | 3 + lynglib/src/commonTest/kotlin/ScriptTest.kt | 58 +++++++++++++++ 8 files changed, 185 insertions(+), 15 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 94aad22..c9dcebb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -422,8 +422,7 @@ class Compiler( (target.elseBody?.let { containsUnsupportedForBytecode(it) } ?: false) } is ForInStatement -> { - target.constRange == null || - containsUnsupportedForBytecode(target.source) || + containsUnsupportedForBytecode(target.source) || containsUnsupportedForBytecode(target.body) || (target.elseStatement?.let { containsUnsupportedForBytecode(it) } ?: false) } @@ -2079,8 +2078,8 @@ class Compiler( cc.currentPos(), "when else block already defined" ) - elseCase = - parseStatement() ?: throw ScriptError( + elseCase = parseStatement()?.let { unwrapBytecodeDeep(it) } + ?: throw ScriptError( cc.currentPos(), "when else block expected" ) @@ -2102,7 +2101,8 @@ class Compiler( } // parsed conditions? if (!skipParseBody) { - val block = parseStatement() ?: throw ScriptError(cc.currentPos(), "when case block expected") + val block = parseStatement()?.let { unwrapBytecodeDeep(it) } + ?: throw ScriptError(cc.currentPos(), "when case block expected") for (c in currentCondition) cases += WhenCase(c, block) } } 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 3ca097c..a7ec2c3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -1679,10 +1679,81 @@ class BytecodeCompiler( rangeRef = extractRangeFromLocal(stmt.source) } val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null - if (range == null && rangeRef == null && typedRangeLocal == null) return null val loopLocalIndex = localSlotIndexByName[stmt.loopVarName] ?: return null val loopSlotId = scopeSlotCount + loopLocalIndex + if (range == null && rangeRef == null && typedRangeLocal == null) { + val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null + val sourceObj = ensureObjSlot(sourceValue) + val typeId = builder.addConst(BytecodeConst.ObjRef(ObjIterable)) + val typeSlot = allocSlot() + builder.emit(Opcode.CONST_OBJ, typeId, typeSlot) + builder.emit(Opcode.ASSERT_IS, sourceObj.slot, typeSlot) + + val iterSlot = allocSlot() + val iteratorId = builder.addConst(BytecodeConst.StringVal("iterator")) + builder.emit(Opcode.CALL_VIRTUAL, sourceObj.slot, iteratorId, 0, 0, iterSlot) + + val breakFlagSlot = allocSlot() + val falseId = builder.addConst(BytecodeConst.Bool(false)) + builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) + + val resultSlot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, resultSlot) + + val loopLabel = builder.label() + val continueLabel = builder.label() + val endLabel = builder.label() + builder.mark(loopLabel) + + val hasNextSlot = allocSlot() + val hasNextId = builder.addConst(BytecodeConst.StringVal("hasNext")) + builder.emit(Opcode.CALL_VIRTUAL, iterSlot, hasNextId, 0, 0, hasNextSlot) + val condSlot = allocSlot() + builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot) + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel)) + ) + + val nextSlot = allocSlot() + val nextId = builder.addConst(BytecodeConst.StringVal("next")) + builder.emit(Opcode.CALL_VIRTUAL, iterSlot, nextId, 0, 0, nextSlot) + val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN)) + builder.emit(Opcode.MOVE_OBJ, nextObj.slot, loopSlotId) + updateSlotType(loopSlotId, SlotType.OBJ) + updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ) + + loopStack.addLast( + LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null) + ) + val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null + loopStack.removeLast() + if (wantResult) { + val bodyObj = ensureObjSlot(bodyValue) + builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) + } + builder.mark(continueLabel) + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel))) + + builder.mark(endLabel) + if (stmt.elseStatement != null) { + val afterElse = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterElse)) + ) + val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null + if (wantResult) { + val elseObj = ensureObjSlot(elseValue) + builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) + } + builder.mark(afterElse) + } + return resultSlot + } + val iSlot = allocSlot() val endSlot = allocSlot() if (range != null) { 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 be27d98..abecba9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -81,14 +81,7 @@ class BytecodeStatement private constructor( (target.elseBody?.let { containsUnsupportedStatement(it) } ?: false) } is net.sergeych.lyng.ForInStatement -> { - val rangeSource = target.source - val rangeRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref as? RangeRef - val sourceRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref - val hasRange = target.constRange != null || - rangeRef != null || - (sourceRef is net.sergeych.lyng.obj.LocalSlotRef) - val unsupported = !hasRange || - containsUnsupportedStatement(target.source) || + val unsupported = containsUnsupportedStatement(target.source) || containsUnsupportedStatement(target.body) || (target.elseStatement?.let { containsUnsupportedStatement(it) } ?: false) unsupported 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 981a499..226d41c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -120,8 +120,12 @@ class CmdBuilder { Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN -> 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 -> + Opcode.OBJ_TO_BOOL, + Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT, + Opcode.ASSERT_IS -> listOf(OperandKind.SLOT, OperandKind.SLOT) + Opcode.CHECK_IS -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.RANGE_INT_BOUNDS -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.RET_LABEL, Opcode.THROW -> @@ -211,7 +215,10 @@ class CmdBuilder { Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1]) Opcode.CONST_NULL -> CmdConstNull(operands[0]) Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1]) + Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1]) Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3]) + Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2]) + Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1]) Opcode.RET_LABEL -> CmdRetLabel(operands[0], operands[1]) Opcode.THROW -> CmdThrow(operands[0], operands[1]) Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(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 5457c42..7860974 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -68,6 +68,9 @@ object CmdDisassembler { is CmdConstBool -> Opcode.CONST_BOOL to intArrayOf(cmd.constId, cmd.dst) is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst) is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst) + is CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst) + is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) + is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot) is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.okSlot) is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot) is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst) @@ -197,6 +200,10 @@ object CmdDisassembler { 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 -> listOf(OperandKind.SLOT, OperandKind.SLOT) + Opcode.OBJ_TO_BOOL, Opcode.ASSERT_IS -> + listOf(OperandKind.SLOT, OperandKind.SLOT) + Opcode.CHECK_IS -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.RANGE_INT_BOUNDS -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.RET_LABEL, Opcode.THROW -> 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 5877fec..4e9d008 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -154,6 +154,37 @@ class CmdBoxObj(internal val src: Int, internal val dst: Int) : Cmd() { } } +class CmdObjToBool(internal val src: Int, internal val dst: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + frame.setBool(dst, frame.slotToObj(src).toBool()) + return + } +} + +class CmdCheckIs(internal val objSlot: Int, internal val typeSlot: Int, internal val dst: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val obj = frame.slotToObj(objSlot) + val typeObj = frame.slotToObj(typeSlot) + val clazz = typeObj as? ObjClass + frame.setBool(dst, clazz != null && obj.isInstanceOf(clazz)) + return + } +} + +class CmdAssertIs(internal val objSlot: Int, internal val typeSlot: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val obj = frame.slotToObj(objSlot) + val typeObj = frame.slotToObj(typeSlot) + val clazz = typeObj as? ObjClass ?: frame.scope.raiseClassCastError( + "${typeObj.inspect(frame.scope)} is not the class instance" + ) + if (!obj.isInstanceOf(clazz)) { + frame.scope.raiseClassCastError("expected ${clazz.className}, got ${obj.objClass.className}") + } + return + } +} + class CmdRangeIntBounds( internal val src: Int, internal val startSlot: Int, 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 f98752e..8d8d290 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -34,6 +34,9 @@ enum class Opcode(val code: Int) { REAL_TO_INT(0x11), BOOL_TO_INT(0x12), INT_TO_BOOL(0x13), + OBJ_TO_BOOL(0x14), + CHECK_IS(0x15), + ASSERT_IS(0x16), ADD_INT(0x20), SUB_INT(0x21), diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 578c7f7..bbd7d28 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -5044,4 +5044,62 @@ class ScriptTest { assertEquals( [1], t(1) ) """.trimIndent()) } + + @Test + fun testForInIterableDisasm() = runTest { + val scope = Script.newScope() + scope.eval(""" + fun type(x) { + when(x) { + "42", 42 -> "answer to the great question" + is Real, is Int -> "number" + is String -> { + for( d in x ) { + if( d !in '0'..'9' ) + break "unknown" + } + else "number" + } + } + } + """.trimIndent()) + println("[DEBUG_LOG] type disasm:\n${scope.disassembleSymbol("type")}") + val r1 = scope.eval("""type("12%")""") + val r2 = scope.eval("""type("153")""") + println("[DEBUG_LOG] type(\"12%\")=${r1.inspect(scope)}") + println("[DEBUG_LOG] type(\"153\")=${r2.inspect(scope)}") + } + + @Test + fun testForInIterableBytecode() = runTest { + val result = eval(""" + fun sumAll(x) { + var s = 0 + for (i in x) s += i + s + } + sumAll([1,2,3]) + sumAll(0..3) + """.trimIndent()) + assertEquals(ObjInt(12), result) + } + + @Test + fun testForInIterableUnknownTypeDisasm() = runTest { + val scope = Script.newScope() + scope.eval(""" + fun countAll(x) { + var c = 0 + for (i in x) c++ + c + } + """.trimIndent()) + val disasm = scope.disassembleSymbol("countAll") + println("[DEBUG_LOG] countAll disasm:\n$disasm") + assertFalse(disasm.contains("not a compiled body")) + assertFalse(disasm.contains("EVAL_FALLBACK")) + val r1 = scope.eval("countAll([1,2,3])") + val r2 = scope.eval("countAll(0..3)") + assertEquals(ObjInt(3), r1) + assertEquals(ObjInt(4), r2) + } }