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 >>> 3
>>> void >>> void
The loop variable is read-only inside the loop body (behaves like a `val`).
but but
for( i in 1..<3 ) for( i in 1..<3 )

View File

@ -105,6 +105,7 @@ arguments list in almost arbitrary ways. For example:
var result = "" var result = ""
for( a in args ) result += a for( a in args ) result += a
} }
// loop variables are read-only inside the loop body
assertEquals( assertEquals(
"4231", "4231",

View File

@ -739,6 +739,7 @@ See also: [Testing and Assertions](Testing.md)
var result = [] var result = []
for( x in iterable ) result += transform(x) 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 })) assert( [11, 21, 31] == mapValues( [1,2,3], { it*10+1 }))
>>> void >>> void

View File

@ -77,6 +77,8 @@ class BytecodeCompiler(
private val slotInitClassByKey = mutableMapOf<ScopeSlotKey, ObjClass>() private val slotInitClassByKey = mutableMapOf<ScopeSlotKey, ObjClass>()
private val intLoopVarNames = LinkedHashSet<String>() private val intLoopVarNames = LinkedHashSet<String>()
private val valueFnRefs = LinkedHashSet<ValueFnRef>() private val valueFnRefs = LinkedHashSet<ValueFnRef>()
private val loopVarKeys = LinkedHashSet<ScopeSlotKey>()
private val loopVarSlots = HashSet<Int>()
private val loopStack = ArrayDeque<LoopContext>() private val loopStack = ArrayDeque<LoopContext>()
private var currentPos: Pos? = null private var currentPos: Pos? = null
@ -1950,6 +1952,10 @@ class BytecodeCompiler(
return value return value
} }
val value = compileRef(assignValue(ref)) ?: return null val value = compileRef(assignValue(ref)) ?: return null
if (isLoopVarRef(localTarget)) {
emitLoopVarReassignError(localTarget.name, localTarget.pos())
return value
}
if (!localTarget.isMutable || localTarget.isDelegated) { if (!localTarget.isMutable || localTarget.isDelegated) {
val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val ${localTarget.name}")) val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val ${localTarget.name}"))
val msgSlot = allocSlot() val msgSlot = allocSlot()
@ -1990,6 +1996,11 @@ class BytecodeCompiler(
val resolved = resolveAssignableSlotByName(nameTarget) ?: return null val resolved = resolveAssignableSlotByName(nameTarget) ?: return null
val slot = resolved.first val slot = resolved.first
val isMutable = resolved.second val isMutable = resolved.second
if (isLoopVarSlot(slot)) {
val pos = (ref.target as? LocalVarRef)?.pos() ?: Pos.builtIn
emitLoopVarReassignError(nameTarget, pos)
return value
}
if (!isMutable) { if (!isMutable) {
val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val $nameTarget")) val msgId = builder.addConst(BytecodeConst.StringVal("can't reassign val $nameTarget"))
val msgSlot = allocSlot() val msgSlot = allocSlot()
@ -2227,6 +2238,11 @@ class BytecodeCompiler(
val localTarget = ref.target as? LocalSlotRef val localTarget = ref.target as? LocalSlotRef
if (localTarget != null) { if (localTarget != null) {
if (!allowLocalSlots) return compileEvalRef(ref) 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) { if (localTarget.isDelegated) {
val slot = resolveSlot(localTarget) ?: return null val slot = resolveSlot(localTarget) ?: return null
if (slot < scopeSlotCount) return null if (slot < scopeSlotCount) return null
@ -2299,6 +2315,12 @@ class BytecodeCompiler(
} }
val varTarget = ref.target as? LocalVarRef val varTarget = ref.target as? LocalVarRef
if (varTarget != null) { 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) return compileEvalRef(ref)
} }
val objOp = when (ref.op) { val objOp = when (ref.op) {
@ -2591,6 +2613,10 @@ class BytecodeCompiler(
} }
is LocalSlotRef -> { is LocalSlotRef -> {
if (!allowLocalSlots || !target.isMutable) return null if (!allowLocalSlots || !target.isMutable) return null
if (isLoopVarRef(target)) {
emitLoopVarReassignError(target.name, target.pos())
return CompiledValue(currentObj.slot, SlotType.OBJ)
}
if (target.isDelegated) { if (target.isDelegated) {
val slot = resolveSlot(target) ?: return null val slot = resolveSlot(target) ?: return null
if (slot < scopeSlotCount) return null if (slot < scopeSlotCount) return null
@ -3178,6 +3204,10 @@ class BytecodeCompiler(
val target = ref.target as? LocalSlotRef val target = ref.target as? LocalSlotRef
if (target != null) { if (target != null) {
if (!allowLocalSlots) return 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.isMutable) return null
if (target.isDelegated) { if (target.isDelegated) {
val slot = resolveSlot(target) ?: return null val slot = resolveSlot(target) ?: return null
@ -3345,6 +3375,14 @@ class BytecodeCompiler(
else -> null 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 val thisFieldTarget = ref.target as? ThisFieldSlotRef
if (thisFieldTarget != null) { if (thisFieldTarget != null) {
@ -4380,18 +4418,18 @@ class BytecodeCompiler(
} else { } else {
stmt.condition 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 resultSlot = allocSlot()
val elseLabel = builder.label() val elseLabel = builder.label()
val endLabel = 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( builder.emit(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condValue.slot), CmdBuilder.Operand.LabelRef(elseLabel)) listOf(CmdBuilder.Operand.IntVal(condValue.slot), CmdBuilder.Operand.LabelRef(elseLabel))
) )
}
val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null
emitMove(thenValue, resultSlot) emitMove(thenValue, resultSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
@ -5403,6 +5441,8 @@ class BytecodeCompiler(
} }
try { try {
val needsBreakFlag = stmt.canBreak || stmt.elseStatement != null
val breakFlagSlot = allocSlot()
if (range == null && rangeRef == null && typedRangeLocal == null) { if (range == null && rangeRef == null && typedRangeLocal == null) {
val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null
val sourceObj = ensureObjSlot(sourceValue) 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.CALL_MEMBER_SLOT, sourceObj.slot, iteratorMethodId, 0, 0, iterSlot)
builder.emit(Opcode.ITER_PUSH, iterSlot) builder.emit(Opcode.ITER_PUSH, iterSlot)
val breakFlagSlot = allocSlot() if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false)) val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
}
val resultSlot = allocSlot() val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) 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 loopLabel = builder.label()
val continueLabel = builder.label() val continueLabel = builder.label()
@ -5465,7 +5510,7 @@ class BytecodeCompiler(
endLabel, endLabel,
continueLabel, continueLabel,
breakFlagSlot, breakFlagSlot,
if (wantResult) resultSlot else null, resultSlot,
hasIterator = true hasIterator = true
) )
) )
@ -5473,12 +5518,13 @@ class BytecodeCompiler(
loopStack.removeLast() loopStack.removeLast()
if (wantResult) { if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue) val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel))) builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel) builder.mark(endLabel)
if (needsBreakFlag) {
val afterPop = builder.label() val afterPop = builder.label()
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_TRUE,
@ -5486,29 +5532,38 @@ class BytecodeCompiler(
) )
builder.emit(Opcode.ITER_POP) builder.emit(Opcode.ITER_POP)
builder.mark(afterPop) builder.mark(afterPop)
} else {
builder.emit(Opcode.ITER_POP)
}
if (stmt.elseStatement != null) { if (stmt.elseStatement != null) {
val afterElse = builder.label() val afterElse = if (needsBreakFlag) builder.label() else null
if (needsBreakFlag) {
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, 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 val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) { if (wantResult) {
val elseObj = ensureObjSlot(elseValue) 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() val endSlot = allocSlot()
if (range != null) { if (range != null) {
val startId = builder.addConst(BytecodeConst.IntVal(range.start)) val startId = builder.addConst(BytecodeConst.IntVal(range.start))
val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive)) val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive))
builder.emit(Opcode.CONST_INT, startId, iSlot) builder.emit(Opcode.CONST_INT, startId, iSlot)
builder.emit(Opcode.CONST_INT, endId, endSlot) builder.emit(Opcode.CONST_INT, endId, endSlot)
updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
} else { } else {
if (rangeRef != null) { if (rangeRef != null) {
val left = rangeRef.left ?: return null val left = rangeRef.left ?: return null
@ -5521,6 +5576,8 @@ class BytecodeCompiler(
if (rangeRef.isEndInclusive) { if (rangeRef.isEndInclusive) {
builder.emit(Opcode.INC_INT, endSlot) builder.emit(Opcode.INC_INT, endSlot)
} }
updateSlotType(iSlot, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
} else { } else {
val rangeLocal = typedRangeLocal ?: return null val rangeLocal = typedRangeLocal ?: return null
val rangeValue = compileRef(rangeLocal) ?: return null val rangeValue = compileRef(rangeLocal) ?: return null
@ -5532,27 +5589,33 @@ class BytecodeCompiler(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(okSlot), CmdBuilder.Operand.LabelRef(badRangeLabel)) listOf(CmdBuilder.Operand.IntVal(okSlot), CmdBuilder.Operand.LabelRef(badRangeLabel))
) )
val breakFlagSlot = allocSlot() if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false)) val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
}
val resultSlot = allocSlot() val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) 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 loopLabel = builder.label()
val continueLabel = builder.label() val continueLabel = builder.label()
val endLabel = builder.label() val endLabel = builder.label()
val doneLabel = builder.label() val doneLabel = builder.label()
builder.mark(loopLabel) builder.mark(loopLabel)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot)
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_GTE_INT,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel)) 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) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast( loopStack.addLast(
LoopContext( LoopContext(
@ -5560,7 +5623,7 @@ class BytecodeCompiler(
endLabel, endLabel,
continueLabel, continueLabel,
breakFlagSlot, breakFlagSlot,
if (wantResult) resultSlot else null, resultSlot,
hasIterator = false hasIterator = false
) )
) )
@ -5568,7 +5631,7 @@ class BytecodeCompiler(
loopStack.removeLast() loopStack.removeLast()
if (wantResult) { if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue) val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot) builder.emit(Opcode.INC_INT, iSlot)
@ -5576,49 +5639,60 @@ class BytecodeCompiler(
builder.mark(endLabel) builder.mark(endLabel)
if (stmt.elseStatement != null) { if (stmt.elseStatement != null) {
val afterElse = builder.label() val afterElse = if (needsBreakFlag) builder.label() else null
if (needsBreakFlag) {
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, 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 val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) { if (wantResult) {
val elseObj = ensureObjSlot(elseValue) 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.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(doneLabel)))
builder.mark(badRangeLabel) builder.mark(badRangeLabel)
val msgId = builder.addConst(BytecodeConst.StringVal("expected Int range")) 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)) val posId = builder.addConst(BytecodeConst.PosVal(stmt.pos))
builder.emit(Opcode.THROW, posId, resultSlot) builder.emit(Opcode.THROW, posId, errorSlot)
builder.mark(doneLabel) builder.mark(doneLabel)
return resultSlot return resultSlot ?: breakFlagSlot
} }
} }
val breakFlagSlot = allocSlot() if (needsBreakFlag) {
val falseId = builder.addConst(BytecodeConst.Bool(false)) val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot) builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
}
val resultSlot = allocSlot() val resultSlot = if (wantResult) {
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) 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 loopLabel = builder.label()
val continueLabel = builder.label() val continueLabel = builder.label()
val endLabel = builder.label() val endLabel = builder.label()
builder.mark(loopLabel) builder.mark(loopLabel)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot)
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_GTE_INT,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel)) 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) updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast( loopStack.addLast(
LoopContext( LoopContext(
@ -5626,7 +5700,7 @@ class BytecodeCompiler(
endLabel, endLabel,
continueLabel, continueLabel,
breakFlagSlot, breakFlagSlot,
if (wantResult) resultSlot else null, resultSlot,
hasIterator = false hasIterator = false
) )
) )
@ -5634,7 +5708,7 @@ class BytecodeCompiler(
loopStack.removeLast() loopStack.removeLast()
if (wantResult) { if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue) val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
} }
builder.mark(continueLabel) builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot) builder.emit(Opcode.INC_INT, iSlot)
@ -5642,19 +5716,23 @@ class BytecodeCompiler(
builder.mark(endLabel) builder.mark(endLabel)
if (stmt.elseStatement != null) { if (stmt.elseStatement != null) {
val afterElse = builder.label() val afterElse = if (needsBreakFlag) builder.label() else null
if (needsBreakFlag) {
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, 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 val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) { if (wantResult) {
val elseObj = ensureObjSlot(elseValue) 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 { } finally {
if (usedOverride) { if (usedOverride) {
loopSlotOverrides.remove(stmt.loopVarName) loopSlotOverrides.remove(stmt.loopVarName)
@ -5693,12 +5771,16 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
} }
builder.mark(continueLabel) 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 if (condition.type != SlotType.BOOL) return null
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel)) listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel))
) )
}
builder.mark(endLabel) builder.mark(endLabel)
if (stmt.elseStatement != null) { if (stmt.elseStatement != null) {
@ -5748,12 +5830,16 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
} }
builder.mark(continueLabel) 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 if (condition.type != SlotType.BOOL) return null
builder.emit( builder.emit(
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel)) listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(loopLabel))
) )
}
builder.mark(endLabel) builder.mark(endLabel)
if (stmt.elseStatement != null) { if (stmt.elseStatement != null) {
@ -5773,14 +5859,18 @@ class BytecodeCompiler(
} }
private fun compileIfStatement(stmt: IfStatement): CompiledValue? { 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 elseLabel = builder.label()
val endLabel = 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( builder.emit(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel))
) )
}
val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true)) val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true))
compileStatementValueOrFallback(stmt.ifBody, false) ?: return null compileStatementValueOrFallback(stmt.ifBody, false) ?: return null
restoreFlowTypeOverride(thenRestore) restoreFlowTypeOverride(thenRestore)
@ -5792,7 +5882,10 @@ class BytecodeCompiler(
restoreFlowTypeOverride(elseRestore) restoreFlowTypeOverride(elseRestore)
} }
builder.mark(endLabel) 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) { private fun updateSlotTypeByName(name: String, type: SlotType) {
@ -5809,15 +5902,19 @@ class BytecodeCompiler(
} }
private fun compileIfExpression(stmt: IfStatement): CompiledValue? { 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 resultSlot = allocSlot()
val elseLabel = builder.label() val elseLabel = builder.label()
val endLabel = 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( builder.emit(
Opcode.JMP_IF_FALSE, Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel)) listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(elseLabel))
) )
}
val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true)) val thenRestore = applyFlowTypeOverride(flowTypeOverrideForIf(stmt.condition, applyForThen = true))
val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null
restoreFlowTypeOverride(thenRestore) restoreFlowTypeOverride(thenRestore)
@ -5840,6 +5937,70 @@ class BytecodeCompiler(
return CompiledValue(resultSlot, SlotType.OBJ) 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? { private fun compileCondition(stmt: Statement, pos: Pos): CompiledValue? {
val target = if (stmt is BytecodeStatement) stmt.original else stmt val target = if (stmt is BytecodeStatement) stmt.original else stmt
return when (target) { return when (target) {
@ -6325,6 +6486,21 @@ class BytecodeCompiler(
private fun refSlot(ref: LocalSlotRef): Int = ref.slot private fun refSlot(ref: LocalSlotRef): Int = ref.slot
private fun refScopeId(ref: LocalSlotRef): Int = ref.scopeId 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 binaryLeft(ref: BinaryOpRef): ObjRef = ref.left
private fun binaryRight(ref: BinaryOpRef): ObjRef = ref.right private fun binaryRight(ref: BinaryOpRef): ObjRef = ref.right
private fun binaryOp(ref: BinaryOpRef): BinOp = ref.op private fun binaryOp(ref: BinaryOpRef): BinOp = ref.op
@ -6682,6 +6858,8 @@ class BytecodeCompiler(
declaredLocalKeys.clear() declaredLocalKeys.clear()
localRangeRefs.clear() localRangeRefs.clear()
intLoopVarNames.clear() intLoopVarNames.clear()
loopVarKeys.clear()
loopVarSlots.clear()
valueFnRefs.clear() valueFnRefs.clear()
addrSlotByScopeSlot.clear() addrSlotByScopeSlot.clear()
loopStack.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 nextSlot = scopeSlotCount + localSlotNames.size
} }
@ -7118,6 +7306,10 @@ class BytecodeCompiler(
} }
when (stmt) { when (stmt) {
is net.sergeych.lyng.ForInStatement -> { is net.sergeych.lyng.ForInStatement -> {
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName]
if (loopSlotIndex != null) {
loopVarKeys.add(ScopeSlotKey(stmt.loopScopeId, loopSlotIndex))
}
collectLoopSlotPlans(stmt.source, scopeDepth) collectLoopSlotPlans(stmt.source, scopeDepth)
val loopDepth = scopeDepth + 1 val loopDepth = scopeDepth + 1
collectLoopSlotPlans(stmt.body, loopDepth) collectLoopSlotPlans(stmt.body, loopDepth)

View File

@ -193,6 +193,10 @@ class CmdBuilder {
listOf(OperandKind.IP) listOf(OperandKind.IP)
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE -> Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE ->
listOf(OperandKind.SLOT, OperandKind.IP) 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 -> Opcode.CALL_DIRECT ->
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_MEMBER_SLOT -> Opcode.CALL_MEMBER_SLOT ->
@ -406,6 +410,36 @@ class CmdBuilder {
Opcode.JMP -> CmdJmp(operands[0]) Opcode.JMP -> CmdJmp(operands[0])
Opcode.JMP_IF_TRUE -> CmdJmpIfTrue(operands[0], operands[1]) Opcode.JMP_IF_TRUE -> CmdJmpIfTrue(operands[0], operands[1])
Opcode.JMP_IF_FALSE -> CmdJmpIfFalse(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 -> CmdRet(operands[0])
Opcode.RET_VOID -> CmdRetVoid() Opcode.RET_VOID -> CmdRetVoid()
Opcode.PUSH_SCOPE -> CmdPushScope(operands[0]) 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 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 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 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 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 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) is CmdCmpRefEqObj -> Opcode.CMP_REF_EQ_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
@ -329,6 +365,10 @@ object CmdDisassembler {
listOf(OperandKind.IP) listOf(OperandKind.IP)
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE -> Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE ->
listOf(OperandKind.SLOT, OperandKind.IP) 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 -> Opcode.CALL_DIRECT ->
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT, Opcode.CALL_BRIDGE_SLOT -> Opcode.CALL_SLOT, Opcode.CALL_BRIDGE_SLOT ->

View File

@ -34,12 +34,14 @@ class CmdVm {
frame.applyCaptureRecords() frame.applyCaptureRecords()
binder?.invoke(frame, args) binder?.invoke(frame, args)
val cmds = fn.cmds val cmds = fn.cmds
try {
while (result == null) {
try { try {
while (result == null) { while (result == null) {
val cmd = cmds[frame.ip] val cmd = cmds[frame.ip]
frame.ip += 1 frame.ip += 1
try {
cmd.perform(frame) cmd.perform(frame)
}
} catch (e: Throwable) { } catch (e: Throwable) {
if (!frame.handleException(e)) { if (!frame.handleException(e)) {
frame.cancelIterators() 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() { class CmdRet(internal val slot: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
frame.vm.result = frame.slotToObj(slot) frame.vm.result = frame.slotToObj(slot)

View File

@ -117,6 +117,12 @@ enum class Opcode(val code: Int) {
JMP(0x80), JMP(0x80),
JMP_IF_TRUE(0x81), JMP_IF_TRUE(0x81),
JMP_IF_FALSE(0x82), 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(0x83),
RET_VOID(0x84), RET_VOID(0x84),
RET_LABEL(0xBA), RET_LABEL(0xBA),

View File

@ -17,11 +17,13 @@
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Benchmarks import net.sergeych.lyng.Benchmarks
import net.sergeych.lyng.BytecodeBodyProvider
import net.sergeych.lyng.Compiler import net.sergeych.lyng.Compiler
import net.sergeych.lyng.ForInStatement import net.sergeych.lyng.ForInStatement
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.CmdDisassembler import net.sergeych.lyng.bytecode.CmdDisassembler
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.BytecodeStatement import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.obj.ObjInt import net.sergeych.lyng.obj.ObjInt
import kotlin.time.TimeSource import kotlin.time.TimeSource
@ -57,6 +59,7 @@ class NestedRangeBenchmarkTest {
scope.eval(script) scope.eval(script)
val fnDisasm = scope.disassembleSymbol("naiveCountHappyNumbers") val fnDisasm = scope.disassembleSymbol("naiveCountHappyNumbers")
println("[DEBUG_LOG] [BENCH] nested-happy function naiveCountHappyNumbers cmd:\n$fnDisasm") println("[DEBUG_LOG] [BENCH] nested-happy function naiveCountHappyNumbers cmd:\n$fnDisasm")
dumpFunctionSlots(scope, "naiveCountHappyNumbers")
runMode(scope) 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") println("limit reached after "+n+" rounds")
break sum break sum
} }
n++
} }
else { else {
println("limit not reached") println("limit not reached")

View File

@ -129,7 +129,6 @@ class TypesTest {
println("limit reached after "+n+" rounds") println("limit reached after "+n+" rounds")
break sum break sum
} }
n++
} }
else { else {
println("limit not reached") 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.