From 37a8831fd7e40e94734d3088b707800293d05feb Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 28 Jan 2026 07:20:58 +0300 Subject: [PATCH] Bytecode for loop over typed range params --- .../kotlin/net/sergeych/lyng/Compiler.kt | 27 +++- .../lyng/bytecode/BytecodeCompiler.kt | 126 ++++++++++++++++-- .../lyng/bytecode/BytecodeStatement.kt | 15 ++- .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 3 + .../sergeych/lyng/bytecode/CmdDisassembler.kt | 3 + .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 22 +++ .../net/sergeych/lyng/bytecode/Opcode.kt | 1 + 7 files changed, 181 insertions(+), 16 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 4408757..94aad22 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -126,6 +126,18 @@ class Compiler( return null } + private fun isRangeType(type: TypeDecl): Boolean { + val name = when (type) { + is TypeDecl.Simple -> type.name + is TypeDecl.Generic -> type.name + else -> return false + } + return name == "Range" || + name == "IntRange" || + name.endsWith(".Range") || + name.endsWith(".IntRange") + } + var packageName: String? = null class Settings( @@ -371,6 +383,9 @@ class Compiler( private var lastLabel: String? = null private val useBytecodeStatements: Boolean = true private val returnLabelStack = ArrayDeque>() + private val rangeParamNamesStack = mutableListOf>() + private val currentRangeParamNames: Set + get() = rangeParamNamesStack.lastOrNull() ?: emptySet() private fun wrapBytecode(stmt: Statement): Statement { if (!useBytecodeStatements) return stmt @@ -380,7 +395,8 @@ class Compiler( stmt, "stmt@${stmt.pos}", allowLocalSlots = allowLocals, - returnLabels = returnLabels + returnLabels = returnLabels, + rangeLocalNames = currentRangeParamNames ) } @@ -391,7 +407,8 @@ class Compiler( stmt, "fn@$name", allowLocalSlots = true, - returnLabels = returnLabels + returnLabels = returnLabels, + rangeLocalNames = currentRangeParamNames ) } @@ -3041,11 +3058,16 @@ class Compiler( val paramNamesList = argsDeclaration.params.map { it.name } val paramNames: Set = paramNamesList.toSet() val paramSlotPlan = buildParamSlotPlan(paramNamesList) + val rangeParamNames = argsDeclaration.params + .filter { isRangeType(it.type) } + .map { it.name } + .toSet() // Parse function body while tracking declared locals to compute precise capacity hints currentLocalDeclCount localDeclCountStack.add(0) slotPlanStack.add(paramSlotPlan) + rangeParamNamesStack.add(rangeParamNames) val parsedFnStatements = try { val returnLabels = buildSet { add(name) @@ -3083,6 +3105,7 @@ class Compiler( returnLabelStack.removeLast() } } finally { + rangeParamNamesStack.removeLast() slotPlanStack.removeLast() } val fnStatements = parsedFnStatements?.let { 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 05cbbe9..3ca097c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.* class BytecodeCompiler( private val allowLocalSlots: Boolean = true, private val returnLabels: Set = emptySet(), + private val rangeLocalNames: Set = emptySet(), ) { private var builder = CmdBuilder() private var nextSlot = 0 @@ -48,6 +49,7 @@ class BytecodeCompiler( private var localSlotMutables = BooleanArray(0) private var localSlotDepths = IntArray(0) private val declaredLocalKeys = LinkedHashSet() + private val localRangeRefs = LinkedHashMap() private val slotTypes = mutableMapOf() private val intLoopVarNames = LinkedHashSet() private val loopStack = ArrayDeque() @@ -1672,8 +1674,12 @@ class BytecodeCompiler( } private fun emitForIn(stmt: net.sergeych.lyng.ForInStatement, wantResult: Boolean): Int? { val range = stmt.constRange - val rangeRef = if (range == null) extractRangeRef(stmt.source) else null - if (range == null && rangeRef == null) return null + var rangeRef = if (range == null) extractRangeRef(stmt.source) else null + if (range == null && rangeRef == null) { + 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 @@ -1685,15 +1691,83 @@ class BytecodeCompiler( builder.emit(Opcode.CONST_INT, startId, iSlot) builder.emit(Opcode.CONST_INT, endId, endSlot) } else { - val left = rangeRef?.left ?: return null - val right = rangeRef.right ?: return null - val startValue = compileRef(left) ?: return null - val endValue = compileRef(right) ?: return null - if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null - emitMove(startValue, iSlot) - emitMove(endValue, endSlot) - if (rangeRef.isEndInclusive) { - builder.emit(Opcode.INC_INT, endSlot) + if (rangeRef != null) { + val left = rangeRef.left ?: return null + val right = rangeRef.right ?: return null + val startValue = compileRef(left) ?: return null + val endValue = compileRef(right) ?: return null + if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null + emitMove(startValue, iSlot) + emitMove(endValue, endSlot) + if (rangeRef.isEndInclusive) { + builder.emit(Opcode.INC_INT, endSlot) + } + } else { + val rangeLocal = typedRangeLocal ?: return null + val rangeValue = compileRef(rangeLocal) ?: return null + val rangeObj = ensureObjSlot(rangeValue) + val okSlot = allocSlot() + builder.emit(Opcode.RANGE_INT_BOUNDS, rangeObj.slot, iSlot, endSlot, okSlot) + val fallbackLabel = builder.label() + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(okSlot), CmdBuilder.Operand.LabelRef(fallbackLabel)) + ) + 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() + val doneLabel = builder.label() + builder.mark(loopLabel) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot) + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel)) + ) + builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId) + updateSlotType(loopSlotId, SlotType.INT) + updateSlotTypeByName(stmt.loopVarName, SlotType.INT) + 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.INC_INT, iSlot) + 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) + } + builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(doneLabel))) + builder.mark(fallbackLabel) + val fallbackId = builder.addFallback(stmt) + builder.emit(Opcode.EVAL_FALLBACK, fallbackId, resultSlot) + builder.mark(doneLabel) + return resultSlot } } @@ -2105,6 +2179,7 @@ class BytecodeCompiler( localSlotMutables = BooleanArray(0) localSlotDepths = IntArray(0) declaredLocalKeys.clear() + localRangeRefs.clear() intLoopVarNames.clear() addrSlotByScopeSlot.clear() loopStack.clear() @@ -2168,6 +2243,11 @@ class BytecodeCompiler( if (!localSlotInfoMap.containsKey(key)) { localSlotInfoMap[key] = LocalSlotInfo(stmt.name, stmt.isMutable, slotDepth) } + if (!stmt.isMutable) { + extractDeclaredRange(stmt.initializer)?.let { range -> + localRangeRefs[key] = range + } + } } stmt.initializer?.let { collectScopeSlots(it) } } @@ -2527,6 +2607,30 @@ class BytecodeCompiler( return expr.ref as? RangeRef } + private fun extractDeclaredRange(stmt: Statement?): RangeRef? { + if (stmt == null) return null + val target = if (stmt is BytecodeStatement) stmt.original else stmt + val expr = target as? ExpressionStatement ?: return null + return expr.ref as? RangeRef + } + + private fun extractRangeFromLocal(source: Statement): RangeRef? { + val target = if (source is BytecodeStatement) source.original else source + val expr = target as? ExpressionStatement ?: return null + val localRef = expr.ref as? LocalSlotRef ?: return null + val key = ScopeSlotKey(refScopeDepth(localRef), refSlot(localRef)) + return localRangeRefs[key] + } + + private fun extractTypedRangeLocal(source: Statement): LocalSlotRef? { + if (rangeLocalNames.isEmpty()) return null + val target = if (source is BytecodeStatement) source.original else source + val expr = target as? ExpressionStatement ?: return null + val localRef = expr.ref as? LocalSlotRef ?: return null + if (localRef.isDelegated) return null + return if (rangeLocalNames.contains(localRef.name)) localRef else null + } + private fun effectiveLocalDepth(depth: Int): Int { if (depth == 0 || virtualScopeDepths.isEmpty()) return depth var virtualCount = 0 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 5bdfcf5..be27d98 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -40,12 +40,17 @@ class BytecodeStatement private constructor( nameHint: String, allowLocalSlots: Boolean, returnLabels: Set = emptySet(), + rangeLocalNames: Set = emptySet(), ): Statement { if (statement is BytecodeStatement) return statement val hasUnsupported = containsUnsupportedStatement(statement) if (hasUnsupported) return unwrapDeep(statement) val safeLocals = allowLocalSlots - val compiler = BytecodeCompiler(allowLocalSlots = safeLocals, returnLabels = returnLabels) + val compiler = BytecodeCompiler( + allowLocalSlots = safeLocals, + returnLabels = returnLabels, + rangeLocalNames = rangeLocalNames + ) val compiled = compiler.compileStatement(nameHint, statement) val fn = compiled ?: run { val builder = CmdBuilder() @@ -78,11 +83,15 @@ class BytecodeStatement private constructor( is net.sergeych.lyng.ForInStatement -> { val rangeSource = target.source val rangeRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref as? RangeRef - val hasRange = target.constRange != null || rangeRef != null - !hasRange || + 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) || containsUnsupportedStatement(target.body) || (target.elseStatement?.let { containsUnsupportedStatement(it) } ?: false) + unsupported } is net.sergeych.lyng.WhileStatement -> { containsUnsupportedStatement(target.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 884a8f4..981a499 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -122,6 +122,8 @@ class CmdBuilder { 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.RANGE_INT_BOUNDS -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.RET_LABEL, Opcode.THROW -> listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.RESOLVE_SCOPE_SLOT -> @@ -209,6 +211,7 @@ 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.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3]) 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 83f7641..5457c42 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,7 @@ 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 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) is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot) @@ -196,6 +197,8 @@ 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.RANGE_INT_BOUNDS -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.RET_LABEL, Opcode.THROW -> listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.RESOLVE_SCOPE_SLOT -> 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 f1de05b..5877fec 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,28 @@ class CmdBoxObj(internal val src: Int, internal val dst: Int) : Cmd() { } } +class CmdRangeIntBounds( + internal val src: Int, + internal val startSlot: Int, + internal val endSlot: Int, + internal val okSlot: Int, +) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + val obj = frame.slotToObj(src) + val range = obj as? ObjRange + if (range == null || !range.isIntRange) { + frame.setBool(okSlot, false) + return + } + val start = (range.start as ObjInt).value + val end = (range.end as ObjInt).value + frame.setInt(startSlot, start) + frame.setInt(endSlot, if (range.isEndInclusive) end + 1 else end) + frame.setBool(okSlot, true) + return + } +} + class CmdResolveScopeSlot(internal val scopeSlot: Int, internal val addrSlot: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { frame.resolveScopeSlotAddr(scopeSlot, addrSlot) 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 17fa2d2..f98752e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -28,6 +28,7 @@ enum class Opcode(val code: Int) { CONST_BOOL(0x08), CONST_NULL(0x09), BOX_OBJ(0x0A), + RANGE_INT_BOUNDS(0x0B), INT_TO_REAL(0x10), REAL_TO_INT(0x11),