Optimize int loop jumps and document loop var immutability

This commit is contained in:
Sergey Chernov 2026-02-15 15:12:52 +03:00
parent db9dc73da8
commit 637258581d
13 changed files with 569 additions and 110 deletions

View File

@ -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 )

View File

@ -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",

View File

@ -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

View File

@ -77,6 +77,8 @@ class BytecodeCompiler(
private val slotInitClassByKey = mutableMapOf<ScopeSlotKey, ObjClass>()
private val intLoopVarNames = LinkedHashSet<String>()
private val valueFnRefs = LinkedHashSet<ValueFnRef>()
private val loopVarKeys = LinkedHashSet<ScopeSlotKey>()
private val loopVarSlots = HashSet<Int>()
private val loopStack = ArrayDeque<LoopContext>()
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()
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()
if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
val resultSlot = allocSlot()
}
val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
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,12 +5518,13 @@ 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)
if (needsBreakFlag) {
val afterPop = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
@ -5486,29 +5532,38 @@ class BytecodeCompiler(
)
builder.emit(Opcode.ITER_POP)
builder.mark(afterPop)
} else {
builder.emit(Opcode.ITER_POP)
}
if (stmt.elseStatement != null) {
val afterElse = builder.label()
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))
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!!)
}
builder.mark(afterElse)
if (needsBreakFlag) {
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()
if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
val resultSlot = allocSlot()
}
val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
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()
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))
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()
if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
val resultSlot = allocSlot()
}
val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
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()
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))
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!!)
}
builder.mark(afterElse)
if (needsBreakFlag) {
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
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
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()
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()
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)

View File

@ -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])

View File

@ -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 ->

View File

@ -34,12 +34,14 @@ class CmdVm {
frame.applyCaptureRecords()
binder?.invoke(frame, args)
val cmds = fn.cmds
try {
while (result == null) {
try {
while (result == null) {
val cmd = cmds[frame.ip]
frame.ip += 1
try {
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)

View File

@ -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),

View File

@ -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()
}
}

View File

@ -1937,7 +1937,6 @@ class ScriptTest {
println("limit reached after "+n+" rounds")
break sum
}
n++
}
else {
println("limit not reached")

View File

@ -129,7 +129,6 @@ class TypesTest {
println("limit reached after "+n+" rounds")
break sum
}
n++
}
else {
println("limit not reached")

View File

@ -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.

View File

@ -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.