diff --git a/docs/Range.md b/docs/Range.md index 644711e..4ad61bc 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -63,6 +63,8 @@ In spite of this you can use ranges in for loops: >>> 3 >>> void +The loop variable is read-only inside the loop body (behaves like a `val`). + but for( i in 1..<3 ) diff --git a/docs/advanced_topics.md b/docs/advanced_topics.md index 90c8fd3..1bdfe9d 100644 --- a/docs/advanced_topics.md +++ b/docs/advanced_topics.md @@ -105,6 +105,7 @@ arguments list in almost arbitrary ways. For example: var result = "" for( a in args ) result += a } + // loop variables are read-only inside the loop body assertEquals( "4231", diff --git a/docs/tutorial.md b/docs/tutorial.md index aa6f787..5162813 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -739,6 +739,7 @@ See also: [Testing and Assertions](Testing.md) var result = [] for( x in iterable ) result += transform(x) } + // loop variables are read-only inside the loop body assert( [11, 21, 31] == mapValues( [1,2,3], { it*10+1 })) >>> void 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 03032f9..6a6526a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -77,6 +77,8 @@ class BytecodeCompiler( private val slotInitClassByKey = mutableMapOf() private val intLoopVarNames = LinkedHashSet() private val valueFnRefs = LinkedHashSet() + private val loopVarKeys = LinkedHashSet() + private val loopVarSlots = HashSet() private val loopStack = ArrayDeque() private var currentPos: Pos? = null @@ -1950,6 +1952,10 @@ class BytecodeCompiler( return value } val value = compileRef(assignValue(ref)) ?: return null + if (isLoopVarRef(localTarget)) { + emitLoopVarReassignError(localTarget.name, localTarget.pos()) + return value + } if (!localTarget.isMutable || localTarget.isDelegated) { val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val ${localTarget.name}")) val msgSlot = allocSlot() @@ -1990,6 +1996,11 @@ class BytecodeCompiler( val resolved = resolveAssignableSlotByName(nameTarget) ?: return null val slot = resolved.first val isMutable = resolved.second + if (isLoopVarSlot(slot)) { + val pos = (ref.target as? LocalVarRef)?.pos() ?: Pos.builtIn + emitLoopVarReassignError(nameTarget, pos) + return value + } if (!isMutable) { val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val $nameTarget")) val msgSlot = allocSlot() @@ -2227,6 +2238,11 @@ class BytecodeCompiler( val localTarget = ref.target as? LocalSlotRef if (localTarget != null) { if (!allowLocalSlots) return compileEvalRef(ref) + if (isLoopVarRef(localTarget)) { + val rhs = compileRef(ref.value) ?: return compileEvalRef(ref) + emitLoopVarReassignError(localTarget.name, localTarget.pos()) + return rhs + } if (localTarget.isDelegated) { val slot = resolveSlot(localTarget) ?: return null if (slot < scopeSlotCount) return null @@ -2299,6 +2315,12 @@ class BytecodeCompiler( } val varTarget = ref.target as? LocalVarRef if (varTarget != null) { + val resolved = resolveAssignableSlotByName(varTarget.name) + if (resolved != null && isLoopVarSlot(resolved.first)) { + val rhs = compileRef(ref.value) ?: return compileEvalRef(ref) + emitLoopVarReassignError(varTarget.name, varTarget.pos()) + return rhs + } return compileEvalRef(ref) } val objOp = when (ref.op) { @@ -2591,6 +2613,10 @@ class BytecodeCompiler( } is LocalSlotRef -> { if (!allowLocalSlots || !target.isMutable) return null + if (isLoopVarRef(target)) { + emitLoopVarReassignError(target.name, target.pos()) + return CompiledValue(currentObj.slot, SlotType.OBJ) + } if (target.isDelegated) { val slot = resolveSlot(target) ?: return null if (slot < scopeSlotCount) return null @@ -3178,6 +3204,10 @@ class BytecodeCompiler( val target = ref.target as? LocalSlotRef if (target != null) { if (!allowLocalSlots) return null + if (isLoopVarRef(target)) { + val errorSlot = emitLoopVarReassignError(target.name, target.pos()) + return CompiledValue(errorSlot, SlotType.OBJ) + } if (!target.isMutable) return null if (target.isDelegated) { val slot = resolveSlot(target) ?: return null @@ -3345,6 +3375,14 @@ class BytecodeCompiler( else -> null } } + val varTarget = ref.target as? LocalVarRef + if (varTarget != null) { + val resolved = resolveAssignableSlotByName(varTarget.name) + if (resolved != null && isLoopVarSlot(resolved.first)) { + val errorSlot = emitLoopVarReassignError(varTarget.name, varTarget.pos()) + return CompiledValue(errorSlot, SlotType.OBJ) + } + } val thisFieldTarget = ref.target as? ThisFieldSlotRef if (thisFieldTarget != null) { @@ -4380,18 +4418,18 @@ class BytecodeCompiler( } else { stmt.condition } - val conditionStmt = conditionTarget as? ExpressionStatement ?: return null - val condValue = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null - if (condValue.type != SlotType.BOOL) return null - val resultSlot = allocSlot() val elseLabel = builder.label() val endLabel = builder.label() - - builder.emit( - Opcode.JMP_IF_FALSE, - listOf(CmdBuilder.Operand.IntVal(condValue.slot), CmdBuilder.Operand.LabelRef(elseLabel)) - ) + val conditionStmt = conditionTarget as? ExpressionStatement ?: return null + if (!emitIntCompareJump(conditionStmt.ref, jumpOnTrue = false, target = elseLabel)) { + val condValue = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null + if (condValue.type != SlotType.BOOL) return null + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(condValue.slot), CmdBuilder.Operand.LabelRef(elseLabel)) + ) + } val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null emitMove(thenValue, resultSlot) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) @@ -5403,6 +5441,8 @@ class BytecodeCompiler( } try { + val needsBreakFlag = stmt.canBreak || stmt.elseStatement != null + val breakFlagSlot = allocSlot() if (range == null && rangeRef == null && typedRangeLocal == null) { val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null val sourceObj = ensureObjSlot(sourceValue) @@ -5430,13 +5470,18 @@ class BytecodeCompiler( builder.emit(Opcode.CALL_MEMBER_SLOT, sourceObj.slot, iteratorMethodId, 0, 0, iterSlot) builder.emit(Opcode.ITER_PUSH, 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) + if (needsBreakFlag) { + val falseId = builder.addConst(BytecodeConst.Bool(false)) + builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) + } + val resultSlot = if (wantResult) { + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + slot + } else { + null + } val loopLabel = builder.label() val continueLabel = builder.label() @@ -5465,7 +5510,7 @@ class BytecodeCompiler( endLabel, continueLabel, breakFlagSlot, - if (wantResult) resultSlot else null, + resultSlot, hasIterator = true ) ) @@ -5473,42 +5518,52 @@ class BytecodeCompiler( loopStack.removeLast() if (wantResult) { val bodyObj = ensureObjSlot(bodyValue) - builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) + builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) } builder.mark(continueLabel) 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() + if (needsBreakFlag) { + val afterPop = builder.label() builder.emit( Opcode.JMP_IF_TRUE, - listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterElse)) + listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterPop)) ) + builder.emit(Opcode.ITER_POP) + builder.mark(afterPop) + } else { + builder.emit(Opcode.ITER_POP) + } + if (stmt.elseStatement != null) { + val afterElse = if (needsBreakFlag) builder.label() else null + if (needsBreakFlag) { + 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.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot!!) + } + if (needsBreakFlag) { + builder.mark(afterElse!!) } - builder.mark(afterElse) } - return resultSlot + return resultSlot ?: breakFlagSlot } - val iSlot = allocSlot() + val iSlot = loopSlotId val endSlot = allocSlot() if (range != null) { val startId = builder.addConst(BytecodeConst.IntVal(range.start)) val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive)) builder.emit(Opcode.CONST_INT, startId, iSlot) builder.emit(Opcode.CONST_INT, endId, endSlot) + updateSlotType(iSlot, SlotType.INT) + updateSlotTypeByName(stmt.loopVarName, SlotType.INT) } else { if (rangeRef != null) { val left = rangeRef.left ?: return null @@ -5521,6 +5576,8 @@ class BytecodeCompiler( if (rangeRef.isEndInclusive) { builder.emit(Opcode.INC_INT, endSlot) } + updateSlotType(iSlot, SlotType.INT) + updateSlotTypeByName(stmt.loopVarName, SlotType.INT) } else { val rangeLocal = typedRangeLocal ?: return null val rangeValue = compileRef(rangeLocal) ?: return null @@ -5532,27 +5589,33 @@ class BytecodeCompiler( Opcode.JMP_IF_FALSE, listOf(CmdBuilder.Operand.IntVal(okSlot), CmdBuilder.Operand.LabelRef(badRangeLabel)) ) - 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) + if (needsBreakFlag) { + val falseId = builder.addConst(BytecodeConst.Bool(false)) + builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) + } + val resultSlot = if (wantResult) { + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + slot + } else { + null + } 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)) + Opcode.JMP_IF_GTE_INT, + listOf( + CmdBuilder.Operand.IntVal(iSlot), + CmdBuilder.Operand.IntVal(endSlot), + CmdBuilder.Operand.LabelRef(endLabel) + ) ) - emitMove(CompiledValue(iSlot, SlotType.INT), loopSlotId) - updateSlotType(loopSlotId, SlotType.INT) + updateSlotType(iSlot, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT) loopStack.addLast( LoopContext( @@ -5560,7 +5623,7 @@ class BytecodeCompiler( endLabel, continueLabel, breakFlagSlot, - if (wantResult) resultSlot else null, + resultSlot, hasIterator = false ) ) @@ -5568,7 +5631,7 @@ class BytecodeCompiler( loopStack.removeLast() if (wantResult) { val bodyObj = ensureObjSlot(bodyValue) - builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) + builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) } builder.mark(continueLabel) builder.emit(Opcode.INC_INT, iSlot) @@ -5576,49 +5639,60 @@ class BytecodeCompiler( 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 afterElse = if (needsBreakFlag) builder.label() else null + if (needsBreakFlag) { + 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.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot!!) + } + if (needsBreakFlag) { + builder.mark(afterElse!!) } - builder.mark(afterElse) } builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(doneLabel))) builder.mark(badRangeLabel) val msgId = builder.addConst(BytecodeConst.StringVal("expected Int range")) - builder.emit(Opcode.CONST_OBJ, msgId, resultSlot) + val errorSlot = resultSlot ?: allocSlot() + builder.emit(Opcode.CONST_OBJ, msgId, errorSlot) val posId = builder.addConst(BytecodeConst.PosVal(stmt.pos)) - builder.emit(Opcode.THROW, posId, resultSlot) + builder.emit(Opcode.THROW, posId, errorSlot) builder.mark(doneLabel) - return resultSlot + return resultSlot ?: breakFlagSlot } } - 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) + if (needsBreakFlag) { + val falseId = builder.addConst(BytecodeConst.Bool(false)) + builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) + } + val resultSlot = if (wantResult) { + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + slot + } else { + null + } val loopLabel = builder.label() val continueLabel = builder.label() val endLabel = 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)) + Opcode.JMP_IF_GTE_INT, + listOf( + CmdBuilder.Operand.IntVal(iSlot), + CmdBuilder.Operand.IntVal(endSlot), + CmdBuilder.Operand.LabelRef(endLabel) + ) ) - builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId) - updateSlotType(loopSlotId, SlotType.INT) + updateSlotType(iSlot, SlotType.INT) updateSlotTypeByName(stmt.loopVarName, SlotType.INT) loopStack.addLast( LoopContext( @@ -5626,7 +5700,7 @@ class BytecodeCompiler( endLabel, continueLabel, breakFlagSlot, - if (wantResult) resultSlot else null, + resultSlot, hasIterator = false ) ) @@ -5634,7 +5708,7 @@ class BytecodeCompiler( loopStack.removeLast() if (wantResult) { val bodyObj = ensureObjSlot(bodyValue) - builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) + builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!) } builder.mark(continueLabel) builder.emit(Opcode.INC_INT, iSlot) @@ -5642,19 +5716,23 @@ class BytecodeCompiler( 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 afterElse = if (needsBreakFlag) builder.label() else null + if (needsBreakFlag) { + 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.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot!!) + } + if (needsBreakFlag) { + builder.mark(afterElse!!) } - builder.mark(afterElse) } - return resultSlot + return resultSlot ?: breakFlagSlot } finally { if (usedOverride) { loopSlotOverrides.remove(stmt.loopVarName) @@ -5693,12 +5771,16 @@ class BytecodeCompiler( builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) } builder.mark(continueLabel) - val condition = compileCondition(stmt.condition, stmt.pos) ?: return null - if (condition.type != SlotType.BOOL) return null - builder.emit( - Opcode.JMP_IF_TRUE, - listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel)) - ) + val conditionTarget = if (stmt.condition is BytecodeStatement) stmt.condition.original else stmt.condition + val conditionStmt = conditionTarget as? ExpressionStatement ?: return null + if (!emitIntCompareJump(conditionStmt.ref, jumpOnTrue = true, target = loopLabel)) { + val condition = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null + if (condition.type != SlotType.BOOL) return null + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel)) + ) + } builder.mark(endLabel) if (stmt.elseStatement != null) { @@ -5748,12 +5830,16 @@ class BytecodeCompiler( builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) } builder.mark(continueLabel) - val condition = compileCondition(stmt.condition, stmt.pos) ?: return null - if (condition.type != SlotType.BOOL) return null - builder.emit( - Opcode.JMP_IF_TRUE, - listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel)) - ) + val conditionTarget = if (stmt.condition is BytecodeStatement) stmt.condition.original else stmt.condition + val conditionStmt = conditionTarget as? ExpressionStatement ?: return null + if (!emitIntCompareJump(conditionStmt.ref, jumpOnTrue = true, target = loopLabel)) { + val condition = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null + if (condition.type != SlotType.BOOL) return null + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel)) + ) + } builder.mark(endLabel) if (stmt.elseStatement != null) { @@ -5773,14 +5859,18 @@ class BytecodeCompiler( } private fun compileIfStatement(stmt: IfStatement): CompiledValue? { - val condition = compileCondition(stmt.condition, stmt.pos) ?: return null - if (condition.type != SlotType.BOOL) return null val elseLabel = builder.label() val endLabel = builder.label() - builder.emit( - Opcode.JMP_IF_FALSE, - listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) - ) + val conditionTarget = if (stmt.condition is BytecodeStatement) stmt.condition.original else stmt.condition + val conditionStmt = conditionTarget as? ExpressionStatement ?: return null + if (!emitIntCompareJump(conditionStmt.ref, jumpOnTrue = false, target = elseLabel)) { + val condition = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null + if (condition.type != SlotType.BOOL) return null + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) + ) + } val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true)) compileStatementValueOrFallback(stmt.ifBody, false) ?: return null restoreFlowTypeOverride(thenRestore) @@ -5792,7 +5882,10 @@ class BytecodeCompiler( restoreFlowTypeOverride(elseRestore) } builder.mark(endLabel) - return condition + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + return CompiledValue(slot, SlotType.OBJ) } private fun updateSlotTypeByName(name: String, type: SlotType) { @@ -5809,15 +5902,19 @@ class BytecodeCompiler( } private fun compileIfExpression(stmt: IfStatement): CompiledValue? { - val condition = compileCondition(stmt.condition, stmt.pos) ?: return null - if (condition.type != SlotType.BOOL) return null val resultSlot = allocSlot() val elseLabel = builder.label() val endLabel = builder.label() - builder.emit( - Opcode.JMP_IF_FALSE, - listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) - ) + val conditionTarget = if (stmt.condition is BytecodeStatement) stmt.condition.original else stmt.condition + val conditionStmt = conditionTarget as? ExpressionStatement ?: return null + if (!emitIntCompareJump(conditionStmt.ref, jumpOnTrue = false, target = elseLabel)) { + val condition = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null + if (condition.type != SlotType.BOOL) return null + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) + ) + } val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true)) val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null restoreFlowTypeOverride(thenRestore) @@ -5840,6 +5937,70 @@ class BytecodeCompiler( return CompiledValue(resultSlot, SlotType.OBJ) } + private fun emitIntCompareJump(ref: ObjRef, jumpOnTrue: Boolean, target: CmdBuilder.Label): Boolean { + setPos(refPosOrCurrent(ref)) + val binary = ref as? BinaryOpRef ?: return false + val op = binaryOp(binary) + if (op != BinOp.EQ && op != BinOp.NEQ && op != BinOp.LT && op != BinOp.LTE && op != BinOp.GT && op != BinOp.GTE) { + return false + } + val leftRef = binaryLeft(binary) + val rightRef = binaryRight(binary) + if (!isSimpleIntCompareRef(leftRef) || !isSimpleIntCompareRef(rightRef)) return false + val left = compileRef(leftRef) ?: return false + val right = compileRef(rightRef) ?: return false + if (left.type != SlotType.INT || right.type != SlotType.INT) return false + val opcode = if (jumpOnTrue) { + intCompareJumpOpcode(op) + } else { + intCompareJumpOpcode(invertIntCompareOp(op)) + } + builder.emit( + opcode, + listOf( + CmdBuilder.Operand.IntVal(left.slot), + CmdBuilder.Operand.IntVal(right.slot), + CmdBuilder.Operand.LabelRef(target) + ) + ) + return true + } + + private fun isSimpleIntCompareRef(ref: ObjRef): Boolean { + return when (ref) { + is ConstRef -> ref.constValue is ObjInt + is LocalVarRef -> ref.name != "this" + is FastLocalVarRef -> ref.name != "this" + is BoundLocalVarRef -> true + is LocalSlotRef -> !ref.isDelegated && ref.name != "this" + else -> false + } + } + + private fun invertIntCompareOp(op: BinOp): BinOp { + return when (op) { + BinOp.EQ -> BinOp.NEQ + BinOp.NEQ -> BinOp.EQ + BinOp.LT -> BinOp.GTE + BinOp.LTE -> BinOp.GT + BinOp.GT -> BinOp.LTE + BinOp.GTE -> BinOp.LT + else -> op + } + } + + private fun intCompareJumpOpcode(op: BinOp): Opcode { + return when (op) { + BinOp.EQ -> Opcode.JMP_IF_EQ_INT + BinOp.NEQ -> Opcode.JMP_IF_NEQ_INT + BinOp.LT -> Opcode.JMP_IF_LT_INT + BinOp.LTE -> Opcode.JMP_IF_LTE_INT + BinOp.GT -> Opcode.JMP_IF_GT_INT + BinOp.GTE -> Opcode.JMP_IF_GTE_INT + else -> Opcode.JMP_IF_NEQ_INT + } + } + private fun compileCondition(stmt: Statement, pos: Pos): CompiledValue? { val target = if (stmt is BytecodeStatement) stmt.original else stmt return when (target) { @@ -6325,6 +6486,21 @@ class BytecodeCompiler( private fun refSlot(ref: LocalSlotRef): Int = ref.slot private fun refScopeId(ref: LocalSlotRef): Int = ref.scopeId + + private fun isLoopVarRef(ref: LocalSlotRef): Boolean { + return loopVarKeys.contains(ScopeSlotKey(refScopeId(ref), refSlot(ref))) + } + + private fun isLoopVarSlot(slot: Int): Boolean = loopVarSlots.contains(slot) + + private fun emitLoopVarReassignError(name: String, pos: Pos): Int { + val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign loop variable $name")) + val msgSlot = allocSlot() + builder.emit(Opcode.CONST_OBJ, msgId, msgSlot) + val posId = builder.addConst(BytecodeConst.PosVal(pos)) + builder.emit(Opcode.THROW, posId, msgSlot) + return msgSlot + } private fun binaryLeft(ref: BinaryOpRef): ObjRef = ref.left private fun binaryRight(ref: BinaryOpRef): ObjRef = ref.right private fun binaryOp(ref: BinaryOpRef): BinOp = ref.op @@ -6682,6 +6858,8 @@ class BytecodeCompiler( declaredLocalKeys.clear() localRangeRefs.clear() intLoopVarNames.clear() + loopVarKeys.clear() + loopVarSlots.clear() valueFnRefs.clear() addrSlotByScopeSlot.clear() loopStack.clear() @@ -6936,6 +7114,16 @@ class BytecodeCompiler( } } } + if (loopVarKeys.isNotEmpty()) { + for (key in loopVarKeys) { + val localIndex = localSlotIndexByKey[key] + if (localIndex != null) { + loopVarSlots.add(scopeSlotCount + localIndex) + continue + } + scopeSlotMap[key]?.let { loopVarSlots.add(it) } + } + } nextSlot = scopeSlotCount + localSlotNames.size } @@ -7118,6 +7306,10 @@ class BytecodeCompiler( } when (stmt) { is net.sergeych.lyng.ForInStatement -> { + val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] + if (loopSlotIndex != null) { + loopVarKeys.add(ScopeSlotKey(stmt.loopScopeId, loopSlotIndex)) + } collectLoopSlotPlans(stmt.source, scopeDepth) val loopDepth = scopeDepth + 1 collectLoopSlotPlans(stmt.body, loopDepth) 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 b1e73ac..6bb2b5d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -193,6 +193,10 @@ class CmdBuilder { listOf(OperandKind.IP) Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE -> listOf(OperandKind.SLOT, OperandKind.IP) + Opcode.JMP_IF_EQ_INT, Opcode.JMP_IF_NEQ_INT, + Opcode.JMP_IF_LT_INT, Opcode.JMP_IF_LTE_INT, + Opcode.JMP_IF_GT_INT, Opcode.JMP_IF_GTE_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.IP) Opcode.CALL_DIRECT -> listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.CALL_MEMBER_SLOT -> @@ -406,6 +410,36 @@ class CmdBuilder { Opcode.JMP -> CmdJmp(operands[0]) Opcode.JMP_IF_TRUE -> CmdJmpIfTrue(operands[0], operands[1]) Opcode.JMP_IF_FALSE -> CmdJmpIfFalse(operands[0], operands[1]) + Opcode.JMP_IF_EQ_INT -> if (operands[0] >= scopeSlotCount && operands[1] >= scopeSlotCount) { + CmdJmpIfEqIntLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2]) + } else { + CmdJmpIfEqInt(operands[0], operands[1], operands[2]) + } + Opcode.JMP_IF_NEQ_INT -> if (operands[0] >= scopeSlotCount && operands[1] >= scopeSlotCount) { + CmdJmpIfNeqIntLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2]) + } else { + CmdJmpIfNeqInt(operands[0], operands[1], operands[2]) + } + Opcode.JMP_IF_LT_INT -> if (operands[0] >= scopeSlotCount && operands[1] >= scopeSlotCount) { + CmdJmpIfLtIntLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2]) + } else { + CmdJmpIfLtInt(operands[0], operands[1], operands[2]) + } + Opcode.JMP_IF_LTE_INT -> if (operands[0] >= scopeSlotCount && operands[1] >= scopeSlotCount) { + CmdJmpIfLteIntLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2]) + } else { + CmdJmpIfLteInt(operands[0], operands[1], operands[2]) + } + Opcode.JMP_IF_GT_INT -> if (operands[0] >= scopeSlotCount && operands[1] >= scopeSlotCount) { + CmdJmpIfGtIntLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2]) + } else { + CmdJmpIfGtInt(operands[0], operands[1], operands[2]) + } + Opcode.JMP_IF_GTE_INT -> if (operands[0] >= scopeSlotCount && operands[1] >= scopeSlotCount) { + CmdJmpIfGteIntLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2]) + } else { + CmdJmpIfGteInt(operands[0], operands[1], operands[2]) + } Opcode.RET -> CmdRet(operands[0]) Opcode.RET_VOID -> CmdRetVoid() Opcode.PUSH_SCOPE -> CmdPushScope(operands[0]) 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 072b532..ae36d01 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -176,6 +176,42 @@ object CmdDisassembler { is CmdCmpGteRealInt -> Opcode.CMP_GTE_REAL_INT to intArrayOf(cmd.a, cmd.b, cmd.dst) is CmdCmpNeqIntReal -> Opcode.CMP_NEQ_INT_REAL to intArrayOf(cmd.a, cmd.b, cmd.dst) is CmdCmpNeqRealInt -> Opcode.CMP_NEQ_REAL_INT to intArrayOf(cmd.a, cmd.b, cmd.dst) + is CmdJmpIfEqInt -> Opcode.JMP_IF_EQ_INT to intArrayOf(cmd.a, cmd.b, cmd.target) + is CmdJmpIfEqIntLocal -> Opcode.JMP_IF_EQ_INT to intArrayOf( + cmd.a + fn.scopeSlotCount, + cmd.b + fn.scopeSlotCount, + cmd.target + ) + is CmdJmpIfNeqInt -> Opcode.JMP_IF_NEQ_INT to intArrayOf(cmd.a, cmd.b, cmd.target) + is CmdJmpIfNeqIntLocal -> Opcode.JMP_IF_NEQ_INT to intArrayOf( + cmd.a + fn.scopeSlotCount, + cmd.b + fn.scopeSlotCount, + cmd.target + ) + is CmdJmpIfLtInt -> Opcode.JMP_IF_LT_INT to intArrayOf(cmd.a, cmd.b, cmd.target) + is CmdJmpIfLtIntLocal -> Opcode.JMP_IF_LT_INT to intArrayOf( + cmd.a + fn.scopeSlotCount, + cmd.b + fn.scopeSlotCount, + cmd.target + ) + is CmdJmpIfLteInt -> Opcode.JMP_IF_LTE_INT to intArrayOf(cmd.a, cmd.b, cmd.target) + is CmdJmpIfLteIntLocal -> Opcode.JMP_IF_LTE_INT to intArrayOf( + cmd.a + fn.scopeSlotCount, + cmd.b + fn.scopeSlotCount, + cmd.target + ) + is CmdJmpIfGtInt -> Opcode.JMP_IF_GT_INT to intArrayOf(cmd.a, cmd.b, cmd.target) + is CmdJmpIfGtIntLocal -> Opcode.JMP_IF_GT_INT to intArrayOf( + cmd.a + fn.scopeSlotCount, + cmd.b + fn.scopeSlotCount, + cmd.target + ) + is CmdJmpIfGteInt -> Opcode.JMP_IF_GTE_INT to intArrayOf(cmd.a, cmd.b, cmd.target) + is CmdJmpIfGteIntLocal -> Opcode.JMP_IF_GTE_INT to intArrayOf( + cmd.a + fn.scopeSlotCount, + cmd.b + fn.scopeSlotCount, + cmd.target + ) is CmdCmpEqObj -> Opcode.CMP_EQ_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst) is CmdCmpNeqObj -> Opcode.CMP_NEQ_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst) is CmdCmpRefEqObj -> Opcode.CMP_REF_EQ_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst) @@ -329,6 +365,10 @@ object CmdDisassembler { listOf(OperandKind.IP) Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE -> listOf(OperandKind.SLOT, OperandKind.IP) + Opcode.JMP_IF_EQ_INT, Opcode.JMP_IF_NEQ_INT, + Opcode.JMP_IF_LT_INT, Opcode.JMP_IF_LTE_INT, + Opcode.JMP_IF_GT_INT, Opcode.JMP_IF_GTE_INT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.IP) Opcode.CALL_DIRECT -> listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.CALL_SLOT, Opcode.CALL_BRIDGE_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 28ae783..fb8fc90 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -36,10 +36,12 @@ class CmdVm { val cmds = fn.cmds try { while (result == null) { - val cmd = cmds[frame.ip] - frame.ip += 1 try { - cmd.perform(frame) + while (result == null) { + val cmd = cmds[frame.ip] + frame.ip += 1 + cmd.perform(frame) + } } catch (e: Throwable) { if (!frame.handleException(e)) { frame.cancelIterators() @@ -1303,6 +1305,114 @@ class CmdJmpIfFalse(internal val cond: Int, internal val target: Int) : Cmd() { } } +class CmdJmpIfEqInt(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getInt(a) == frame.getInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfEqIntLocal(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getLocalInt(a) == frame.getLocalInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfNeqInt(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getInt(a) != frame.getInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfNeqIntLocal(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getLocalInt(a) != frame.getLocalInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfLtInt(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getInt(a) < frame.getInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfLtIntLocal(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getLocalInt(a) < frame.getLocalInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfLteInt(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getInt(a) <= frame.getInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfLteIntLocal(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getLocalInt(a) <= frame.getLocalInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfGtInt(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getInt(a) > frame.getInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfGtIntLocal(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getLocalInt(a) > frame.getLocalInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfGteInt(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getInt(a) >= frame.getInt(b)) { + frame.ip = target + } + return + } +} + +class CmdJmpIfGteIntLocal(internal val a: Int, internal val b: Int, internal val target: Int) : Cmd() { + override suspend fun perform(frame: CmdFrame) { + if (frame.getLocalInt(a) >= frame.getLocalInt(b)) { + frame.ip = target + } + return + } +} + class CmdRet(internal val slot: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { frame.vm.result = frame.slotToObj(slot) 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 3e76136..2445abd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -117,6 +117,12 @@ enum class Opcode(val code: Int) { JMP(0x80), JMP_IF_TRUE(0x81), JMP_IF_FALSE(0x82), + JMP_IF_EQ_INT(0xD0), + JMP_IF_NEQ_INT(0xD1), + JMP_IF_LT_INT(0xD2), + JMP_IF_LTE_INT(0xD3), + JMP_IF_GT_INT(0xD4), + JMP_IF_GTE_INT(0xD5), RET(0x83), RET_VOID(0x84), RET_LABEL(0xBA), diff --git a/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt b/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt index a3b6332..bafc8b5 100644 --- a/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt +++ b/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt @@ -17,11 +17,13 @@ import kotlinx.coroutines.test.runTest import net.sergeych.lyng.Benchmarks +import net.sergeych.lyng.BytecodeBodyProvider import net.sergeych.lyng.Compiler import net.sergeych.lyng.ForInStatement import net.sergeych.lyng.Script import net.sergeych.lyng.Statement import net.sergeych.lyng.bytecode.CmdDisassembler +import net.sergeych.lyng.bytecode.CmdFunction import net.sergeych.lyng.bytecode.BytecodeStatement import net.sergeych.lyng.obj.ObjInt import kotlin.time.TimeSource @@ -57,6 +59,7 @@ class NestedRangeBenchmarkTest { scope.eval(script) val fnDisasm = scope.disassembleSymbol("naiveCountHappyNumbers") println("[DEBUG_LOG] [BENCH] nested-happy function naiveCountHappyNumbers cmd:\n$fnDisasm") + dumpFunctionSlots(scope, "naiveCountHappyNumbers") runMode(scope) } @@ -96,4 +99,30 @@ class NestedRangeBenchmarkTest { } } + private fun dumpFunctionSlots(scope: net.sergeych.lyng.Scope, name: String) { + val record = scope[name]?.value as? Statement ?: return + val fn = bytecodeFromStatement(record) ?: return + val scopeNames = fn.scopeSlotNames.mapIndexedNotNull { idx, slotName -> + slotName?.let { "$it@${fn.scopeSlotIndices[idx]}" } + } + val localNames = fn.localSlotNames.mapIndexedNotNull { idx, slotName -> + slotName?.let { "$it@$idx" } + } + val captures = fn.localSlotNames.mapIndexedNotNull { idx, slotName -> + if (slotName != null && fn.localSlotCaptures.getOrNull(idx) == true) "$slotName@$idx" else null + } + println( + "[DEBUG_LOG] [BENCH] nested-happy function $name slots: " + + "scopeCount=${fn.scopeSlotCount} " + + "scope=[${scopeNames.joinToString(", ")}] " + + "locals=[${localNames.joinToString(", ")}] " + + "captures=[${captures.joinToString(", ")}]" + ) + } + + private fun bytecodeFromStatement(stmt: Statement): CmdFunction? { + return (stmt as? BytecodeStatement)?.bytecodeFunction() + ?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction() + } + } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 464e7e2..187c223 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1937,7 +1937,6 @@ class ScriptTest { println("limit reached after "+n+" rounds") break sum } - n++ } else { println("limit not reached") diff --git a/lynglib/src/commonTest/kotlin/TypesTest.kt b/lynglib/src/commonTest/kotlin/TypesTest.kt index a7b3adb..c591896 100644 --- a/lynglib/src/commonTest/kotlin/TypesTest.kt +++ b/lynglib/src/commonTest/kotlin/TypesTest.kt @@ -129,7 +129,6 @@ class TypesTest { println("limit reached after "+n+" rounds") break sum } - n++ } else { println("limit not reached") diff --git a/notes/bytecode_vm_notes.md b/notes/bytecode_vm_notes.md new file mode 100644 index 0000000..fb28c61 --- /dev/null +++ b/notes/bytecode_vm_notes.md @@ -0,0 +1,3 @@ +# Bytecode VM notes + +- Opcode switch dispatch was measured slower than virtual dispatch; keep the per-op Cmd class path for now. diff --git a/notes/nested_loop_vm_state.md b/notes/nested_loop_vm_state.md new file mode 100644 index 0000000..13e9d4e --- /dev/null +++ b/notes/nested_loop_vm_state.md @@ -0,0 +1,43 @@ +## Nested loop performance investigation state (2026-02-15) + +### Key findings +- Bytecode for `NestedRangeBenchmarkTest` is fully int-local ops; no dynamic lookups or scopes in hot path. +- Loop vars now live directly in local int slots (`n1..n6`), removing per-iteration `MOVE_INT`. +- Per-instruction try/catch in VM was replaced with an outer try/catch loop; on JVM this improved the benchmark. +- Native slowdown is likely dominated by suspend/virtual dispatch overhead in VM, not allocations in int ops. + +### Current bytecode shape (naiveCountHappyNumbers) +- Ops: `CONST_INT`, `CMP_GTE_INT`, `INC_INT`, `ADD_INT`, `CMP_EQ_INT`, `JMP*`, `RET`. +- All are `*Local` variants hitting `BytecodeFrame` primitive arrays. + +### Changes made +- Loop vars are enforced read-only inside loop bodies at bytecode compile time (reassign, op=, ?=, ++/-- throw). +- Range loops reuse the loop var slot as the counter; no per-iteration move. +- VM loop now uses outer try/catch (no per-op try/catch). +- VM stats instrumentation was added temporarily, then removed for MP safety. + +### Files changed +- `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt` + - loop var immutability checks + - loop var slot reuse for range loops + - skip break/result init when not needed +- `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt` + - outer try/catch VM loop (removed per-op try/catch) + - stats instrumentation removed +- `lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt` + - temporary VM stats debug removed + - slot dump remains for visibility +- `notes/bytecode_vm_notes.md` + - note: opcode switch dispatch tested slower than virtual dispatch + +### Benchmark snapshots (JVM) +- Before VM loop change: ~96–110 ms. +- With VM stats enabled: ~234–240 ms (stats overhead). +- After VM loop change, stats disabled: ~85 ms. +- 2026-02-15 baseline (fused int-compare jumps): 74 ms. + - Command: `./gradlew :lynglib:jvmTest -Pbenchmarks=true --tests '*NestedRangeBenchmarkTest*'` + - Notes: loop range checks use `JMP_IF_GTE_INT` (no CMP+bool temp). + +### Hypothesis for Native slowdown +- Suspend/virtual dispatch per opcode dominates on K/N, even with no allocations in int ops. +- Next idea: a non-suspend fast path for hot opcodes, or a dual-path VM loop.