Stabilize pi benchmark optimizations for release

This commit is contained in:
Sergey Chernov 2026-04-03 23:16:08 +03:00
parent a7ab0d3905
commit 064b927b1a
6 changed files with 912 additions and 178 deletions

72
examples/pi-bench.lyng Normal file
View File

@ -0,0 +1,72 @@
import lyng.time
val WORK_SIZE = 200
val TASK_COUNT = 10
fn piSpigot(iThread: Int, n: Int) {
var pi = []
val boxes = n * 10 / 3
var reminders = List.fill(boxes) { 2 }
var heldDigits = 0
for (i in 0..n) {
var carriedOver = 0
var sum = 0
for (k in 1..boxes) {
val j = boxes - k
val denom = j * 2 + 1
reminders[j] *= 10
sum = reminders[j] + carriedOver
val quotient = sum / denom
reminders[j] = sum % denom
carriedOver = quotient * j
}
reminders[0] = sum % 10
var q = sum / 10
if (q == 9) {
++heldDigits
} else if (q == 10) {
q = 0
for (k in 1..heldDigits) {
var replaced = pi[i - k]
if (replaced == 9) {
replaced = 0
} else {
++replaced
}
pi[i - k] = replaced
}
heldDigits = 1
} else {
heldDigits = 1
}
pi.add(q)
}
var s = ""
for (i in (n - 8)..<n) {
s += pi[i]
}
println(iThread, " - done: ", s)
}
var counter = 0
for( repeat in 1..30) {
val t0 = Instant()
(1..TASK_COUNT).map {
val counterState = counter
val t = launch {
piSpigot(counterState, WORK_SIZE)
}
++counter
t
}.forEach { (it as Deferred).await() }
val dt = Instant() - t0
println("$repeat: all done, dt = ", dt)
delay(1000)
}

49
examples/pi-test.lyng Normal file
View File

@ -0,0 +1,49 @@
fn piSpigot(n) {
var pi = []
val boxes = n * 10 / 3
var reminders = []
for (i in 0..<boxes) {
reminders.add(2)
}
var heldDigits = 0
for (i in 0..n) {
var carriedOver = 0
var sum = 0
for (k in 1..boxes) {
val j = boxes - k
val denom = j * 2 + 1
reminders[j] *= 10
sum = reminders[j] + carriedOver
// Keep this integer-only. Real coercion here is much slower in the hot loop.
val quotient = sum / denom
reminders[j] = sum % denom
carriedOver = quotient * j
}
reminders[0] = sum % 10
var q = sum / 10
if (q == 9) {
++heldDigits
} else if (q == 10) {
q = 0
for (k in 1..heldDigits) {
var replaced = pi[i - k]
if (replaced == 9) {
replaced = 0
} else {
++replaced
}
pi[i - k] = replaced
}
heldDigits = 1
} else {
heldDigits = 1
}
pi.add(q)
}
var suffix = ""
for (i in (n - 8)..<n) {
suffix += pi[i]
}
suffix
}

View File

@ -1783,7 +1783,11 @@ class BytecodeCompiler(
return when (slotTypes[slot]) { return when (slotTypes[slot]) {
SlotType.INT -> NumericKind.INT SlotType.INT -> NumericKind.INT
SlotType.REAL -> NumericKind.REAL SlotType.REAL -> NumericKind.REAL
else -> NumericKind.UNKNOWN else -> when (slotObjClass[slot]) {
ObjInt.type -> NumericKind.INT
ObjReal.type -> NumericKind.REAL
else -> NumericKind.UNKNOWN
}
} }
} }
@ -1800,7 +1804,21 @@ class BytecodeCompiler(
is ConstRef -> numericKindFromConst(ref.constValue) is ConstRef -> numericKindFromConst(ref.constValue)
is LocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN is LocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN
is FastLocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN is FastLocalVarRef -> resolveDirectNameSlot(ref.name)?.let { numericKindFromSlot(it.slot) } ?: NumericKind.UNKNOWN
is LocalSlotRef -> resolveSlot(ref)?.let { numericKindFromSlot(it) } ?: NumericKind.UNKNOWN is LocalSlotRef -> resolveLocalSlotByRefOrName(ref)?.let { numericKindFromSlot(it) } ?: NumericKind.UNKNOWN
is IndexRef -> {
val receiver = when (val target = ref.targetRef) {
is LocalSlotRef -> resolveLocalSlotByRefOrName(target)
is LocalVarRef -> resolveDirectNameSlot(target.name)?.slot
is FastLocalVarRef -> resolveDirectNameSlot(target.name)?.slot
else -> null
}
val elementClass = receiver?.let { listElementClassBySlot[it] } ?: listElementClassFromReceiverRef(ref.targetRef)
when (elementClass) {
ObjInt.type -> NumericKind.INT
ObjReal.type -> NumericKind.REAL
else -> NumericKind.UNKNOWN
}
}
is UnaryOpRef -> inferNumericKind(ref.a) is UnaryOpRef -> inferNumericKind(ref.a)
is BinaryOpRef -> { is BinaryOpRef -> {
val op = ref.op val op = ref.op
@ -2431,7 +2449,7 @@ class BytecodeCompiler(
updateSlotType(slot, SlotType.OBJ) updateSlotType(slot, SlotType.OBJ)
return value return value
} }
val value = compileRef(assignValue(ref)) ?: return null var value = compileRef(assignValue(ref)) ?: return null
if (isLoopVarRef(localTarget)) { if (isLoopVarRef(localTarget)) {
emitLoopVarReassignError(localTarget.name, localTarget.pos()) emitLoopVarReassignError(localTarget.name, localTarget.pos())
return value return value
@ -2473,7 +2491,7 @@ class BytecodeCompiler(
else -> null else -> null
} }
if (nameTarget != null) { if (nameTarget != null) {
val value = compileRef(assignValue(ref)) ?: return null var value = compileRef(assignValue(ref)) ?: return null
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
@ -2698,6 +2716,7 @@ class BytecodeCompiler(
if (!target.optionalRef) { if (!target.optionalRef) {
val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot)
noteListElementClassMutation(receiver.slot, value)
} else { } else {
val nullSlot = allocSlot() val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot) builder.emit(Opcode.CONST_NULL, nullSlot)
@ -2710,6 +2729,7 @@ class BytecodeCompiler(
) )
val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null val index = compileRefWithFallback(target.indexRef, null, Pos.builtIn) ?: return null
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot) builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, value.slot)
noteListElementClassMutation(receiver.slot, value)
builder.mark(endLabel) builder.mark(endLabel)
} }
return value return value
@ -3026,9 +3046,32 @@ class BytecodeCompiler(
val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null val receiver = compileRefWithFallback(indexTarget.targetRef, null, Pos.builtIn) ?: return null
val current = allocSlot() val current = allocSlot()
val result = allocSlot() val result = allocSlot()
val rhs = compileRef(ref.value) ?: return compileEvalRef(ref) var rhs = compileRef(ref.value) ?: return compileEvalRef(ref)
val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(indexTarget.targetRef)
if (!indexTarget.optionalRef) { if (!indexTarget.optionalRef) {
val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null val index = compileRefWithFallback(indexTarget.indexRef, null, Pos.builtIn) ?: return null
if (elementClass == ObjInt.type) {
builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current)
val currentInt = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, current, currentInt)
updateSlotType(currentInt, SlotType.INT)
if (rhs.type != SlotType.INT) {
coerceToArithmeticInt(ref.value, rhs)?.let { rhs = it }
}
val typed = when (ref.op) {
BinOp.PLUS -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.ADD_INT, Opcode.ADD_REAL, Opcode.ADD_OBJ)
BinOp.MINUS -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.SUB_INT, Opcode.SUB_REAL, Opcode.SUB_OBJ)
BinOp.STAR -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.MUL_INT, Opcode.MUL_REAL, Opcode.MUL_OBJ)
BinOp.SLASH -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.DIV_INT, Opcode.DIV_REAL, Opcode.DIV_OBJ)
BinOp.PERCENT -> compileAssignOpBinary(SlotType.INT, rhs, currentInt, Opcode.MOD_INT, null, Opcode.MOD_OBJ)
else -> null
}
if (typed != null && typed.type == SlotType.INT) {
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, currentInt)
noteListElementClassMutation(receiver.slot, typed)
return CompiledValue(currentInt, SlotType.INT)
}
}
builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current) builder.emit(Opcode.GET_INDEX, receiver.slot, index.slot, current)
builder.emit(objOp, current, rhs.slot, result) builder.emit(objOp, current, rhs.slot, result)
builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result) builder.emit(Opcode.SET_INDEX, receiver.slot, index.slot, result)
@ -3586,7 +3629,7 @@ class BytecodeCompiler(
val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(ref.targetRef) val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(ref.targetRef)
if (elementClass != null) { if (elementClass != null) {
slotObjClass[dst] = elementClass slotObjClass[dst] = elementClass
if (elementClass == ObjString.type && elementClass.isClosed) { if (elementClass.isClosed) {
stableObjSlots.add(dst) stableObjSlots.add(dst)
} else { } else {
stableObjSlots.remove(dst) stableObjSlots.remove(dst)
@ -4649,6 +4692,9 @@ class BytecodeCompiler(
val encodedCount = encodeCallArgCount(args) ?: return null val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos) setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst) builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst)
if (receiverClass == ObjList.type && ref.name == "add" && ref.args.size == 1 && !ref.args.first().isSplat) {
noteListElementClassMutation(receiver.slot, CompiledValue(args.base, SlotType.OBJ))
}
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
} }
val nullSlot = allocSlot() val nullSlot = allocSlot()
@ -4815,7 +4861,7 @@ class BytecodeCompiler(
" receiver=$kind(${ref.name}) slot=$slot slotClass=$slotCls nameClass=$nameCls" " receiver=$kind(${ref.name}) slot=$slot slotClass=$slotCls nameClass=$nameCls"
} }
is LocalSlotRef -> { is LocalSlotRef -> {
val slot = resolveSlot(ref) val slot = resolveLocalSlotByRefOrName(ref)
val slotCls = slot?.let { slotObjClass[it]?.className } val slotCls = slot?.let { slotObjClass[it]?.className }
val nameCls = nameObjClass[ref.name]?.className val nameCls = nameObjClass[ref.name]?.className
val scopeId = refScopeId(ref) val scopeId = refScopeId(ref)
@ -4971,9 +5017,10 @@ class BytecodeCompiler(
val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size) else null val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size) else null
for ((index, arg) in args.withIndex()) { for ((index, arg) in args.withIndex()) {
val compiled = compileArgValue(arg.value) ?: return null val compiled = compileArgValue(arg.value) ?: return null
val objValue = ensureObjSlot(compiled)
val dst = argSlots[index] val dst = argSlots[index]
if (compiled.slot != dst || compiled.type != SlotType.OBJ) { if (objValue.slot != dst) {
builder.emit(Opcode.BOX_OBJ, compiled.slot, dst) emitMove(objValue, dst)
} }
updateSlotType(dst, SlotType.OBJ) updateSlotType(dst, SlotType.OBJ)
specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat)) specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat))
@ -5833,7 +5880,8 @@ class BytecodeCompiler(
emitMove(value, localSlot) emitMove(value, localSlot)
} }
updateSlotType(localSlot, value.type) updateSlotType(localSlot, value.type)
updateSlotObjClass(localSlot, stmt.initializer, stmt.initializerObjClass) slotObjClass[value.slot]?.let { slotObjClass[localSlot] = it }
?: updateSlotObjClass(localSlot, stmt.initializer, stmt.initializerObjClass)
updateListElementClassFromDecl(localSlot, scopeId, stmt.slotIndex) updateListElementClassFromDecl(localSlot, scopeId, stmt.slotIndex)
updateListElementClassFromInitializer(localSlot, stmt.initializer) updateListElementClassFromInitializer(localSlot, stmt.initializer)
updateNameObjClassFromSlot(stmt.name, localSlot) updateNameObjClassFromSlot(stmt.name, localSlot)
@ -5865,7 +5913,8 @@ class BytecodeCompiler(
} }
updateSlotType(scopeSlot, value.type) updateSlotType(scopeSlot, value.type)
updateNameObjClassFromSlot(stmt.name, scopeSlot) updateNameObjClassFromSlot(stmt.name, scopeSlot)
updateSlotObjClass(scopeSlot, stmt.initializer, stmt.initializerObjClass) slotObjClass[value.slot]?.let { slotObjClass[scopeSlot] = it }
?: updateSlotObjClass(scopeSlot, stmt.initializer, stmt.initializerObjClass)
updateListElementClassFromDecl(scopeSlot, scopeId, stmt.slotIndex) updateListElementClassFromDecl(scopeSlot, scopeId, stmt.slotIndex)
updateListElementClassFromInitializer(scopeSlot, stmt.initializer) updateListElementClassFromInitializer(scopeSlot, stmt.initializer)
val declId = builder.addConst( val declId = builder.addConst(
@ -5901,7 +5950,9 @@ class BytecodeCompiler(
updateSlotTypeByName(stmt.name, value.type) updateSlotTypeByName(stmt.name, value.type)
} }
updateNameObjClassFromSlot(stmt.name, value.slot) updateNameObjClassFromSlot(stmt.name, value.slot)
updateSlotObjClass(value.slot, stmt.initializer, stmt.initializerObjClass) if (slotObjClass[value.slot] == null) {
updateSlotObjClass(value.slot, stmt.initializer, stmt.initializerObjClass)
}
updateListElementClassFromDecl(value.slot, scopeId, stmt.slotIndex) updateListElementClassFromDecl(value.slot, scopeId, stmt.slotIndex)
updateListElementClassFromInitializer(value.slot, stmt.initializer) updateListElementClassFromInitializer(value.slot, stmt.initializer)
return value return value
@ -5991,6 +6042,16 @@ class BytecodeCompiler(
listElementClassBySlot[slot] = elementClass listElementClassBySlot[slot] = elementClass
} }
private fun noteListElementClassMutation(receiverSlot: Int, value: CompiledValue) {
val newClass = elementClassFromValue(value) ?: return
val current = listElementClassBySlot[receiverSlot]
if (current == null || current == newClass) {
listElementClassBySlot[receiverSlot] = newClass
} else {
listElementClassBySlot.remove(receiverSlot)
}
}
private fun updateNameObjClassFromSlot(name: String, slot: Int) { private fun updateNameObjClassFromSlot(name: String, slot: Int) {
val cls = slotObjClass[slot] ?: return val cls = slotObjClass[slot] ?: return
nameObjClass[name] = cls nameObjClass[name] = cls
@ -6086,9 +6147,6 @@ class BytecodeCompiler(
if (range == null && rangeRef == null) { if (range == null && rangeRef == null) {
rangeRef = extractRangeFromLocal(stmt.source) rangeRef = extractRangeFromLocal(stmt.source)
} }
if (rangeRef != null && !isConstIntRange(rangeRef)) {
rangeRef = null
}
val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null
val loopSlotPlan = stmt.loopSlotPlan val loopSlotPlan = stmt.loopSlotPlan
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName]
@ -6129,120 +6187,16 @@ class BytecodeCompiler(
val breakFlagSlot = allocSlot() 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) return emitIterableForIn(
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjIterable)) stmt = stmt,
val typeSlot = allocSlot() sourceValue = sourceValue,
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot) wantResult = wantResult,
builder.emit(Opcode.ASSERT_IS, sourceObj.slot, typeSlot) loopSlotId = loopSlotId,
breakFlagSlot = breakFlagSlot,
val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true) needsBreakFlag = needsBreakFlag,
val iteratorMethodId = iterableMethods["iterator"] hasRealWiden = hasRealWiden,
if (iteratorMethodId == null) { realWidenSlots = realWidenSlots,
throw BytecodeCompileException("Missing member id for Iterable.iterator", stmt.pos)
}
val iteratorMethods = ObjIterator.instanceMethodIdMap(includeAbstract = true)
val hasNextMethodId = iteratorMethods["hasNext"]
if (hasNextMethodId == null) {
throw BytecodeCompileException("Missing member id for Iterator.hasNext", stmt.pos)
}
val nextMethodId = iteratorMethods["next"]
if (nextMethodId == null) {
throw BytecodeCompileException("Missing member id for Iterator.next", stmt.pos)
}
val iterSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, sourceObj.slot, iteratorMethodId, 0, 0, iterSlot)
builder.emit(Opcode.ITER_PUSH, iterSlot)
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 hasNextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, hasNextMethodId, 0, 0, hasNextSlot)
val condSlot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot)
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel))
) )
val nextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, nextMethodId, 0, 0, nextSlot)
val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN))
emitMove(CompiledValue(nextObj.slot, SlotType.OBJ), loopSlotId)
updateSlotType(loopSlotId, SlotType.OBJ)
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
loopStack.addLast(
LoopContext(
stmt.label,
endLabel,
continueLabel,
breakFlagSlot,
resultSlot,
hasIterator = true
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
}
builder.mark(continueLabel)
if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (needsBreakFlag) {
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)
} 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!!)
}
if (needsBreakFlag) {
builder.mark(afterElse!!)
}
}
return resultSlot ?: breakFlagSlot
} }
val iSlot = loopSlotId val iSlot = loopSlotId
@ -6261,9 +6215,23 @@ class BytecodeCompiler(
if (rangeRef != null) { if (rangeRef != null) {
val left = rangeRef.left ?: return null val left = rangeRef.left ?: return null
val right = rangeRef.right ?: return null val right = rangeRef.right ?: return null
val startValue = compileRef(left) ?: return null val startCompiled = compileRef(left) ?: return null
val endValue = compileRef(right) ?: return null val endCompiled = compileRef(right) ?: return null
if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null val startValue = coerceToLoopInt(startCompiled)
val endValue = coerceToLoopInt(endCompiled)
if (startValue == null || endValue == null) {
val rangeValue = emitRangeObject(startCompiled, endCompiled, rangeRef)
return emitIterableForIn(
stmt = stmt,
sourceValue = rangeValue,
wantResult = wantResult,
loopSlotId = loopSlotId,
breakFlagSlot = breakFlagSlot,
needsBreakFlag = needsBreakFlag,
hasRealWiden = hasRealWiden,
realWidenSlots = realWidenSlots,
)
}
val descendingId = builder.addConst(BytecodeConst.Bool(rangeRef.isDescending)) val descendingId = builder.addConst(BytecodeConst.Bool(rangeRef.isDescending))
emitMove(startValue, iSlot) emitMove(startValue, iSlot)
emitMove(endValue, endSlot) emitMove(endValue, endSlot)
@ -7308,7 +7276,8 @@ class BytecodeCompiler(
is LocalSlotRef -> { is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot) resolveLocalSlotByRefOrName(ref)?.let { slotObjClass[it] }
?: slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
?: slotInitClassByKey[ScopeSlotKey(ownerScopeId, ownerSlot)] ?: slotInitClassByKey[ScopeSlotKey(ownerScopeId, ownerSlot)]
?: nameObjClass[ref.name] ?: nameObjClass[ref.name]
?: resolveTypeNameClass(ref.name) ?: resolveTypeNameClass(ref.name)
@ -7721,6 +7690,11 @@ class BytecodeCompiler(
return resolved return resolved
} }
private fun resolveLocalSlotByRefOrName(ref: LocalSlotRef): Int? {
return resolveSlot(ref)
?: ref.name.takeIf { it.isNotEmpty() }?.let { name -> resolveDirectNameSlot(name)?.slot }
}
private fun resolveCapturedOwnerScopeSlot(ref: LocalSlotRef): Int? { private fun resolveCapturedOwnerScopeSlot(ref: LocalSlotRef): Int? {
val ownerScopeId = ref.captureOwnerScopeId ?: return null val ownerScopeId = ref.captureOwnerScopeId ?: return null
val ownerSlot = ref.captureOwnerSlot ?: return null val ownerSlot = ref.captureOwnerSlot ?: return null
@ -8698,10 +8672,237 @@ class BytecodeCompiler(
return if (ref.step != null) null else ref return if (ref.step != null) null else ref
} }
private fun isConstIntRange(ref: RangeRef): Boolean { private fun emitIterableForIn(
val left = ref.left as? ConstRef ?: return false stmt: net.sergeych.lyng.ForInStatement,
val right = ref.right as? ConstRef ?: return false sourceValue: CompiledValue,
return left.constValue is ObjInt && right.constValue is ObjInt wantResult: Boolean,
loopSlotId: Int,
breakFlagSlot: Int,
needsBreakFlag: Boolean,
hasRealWiden: Boolean,
realWidenSlots: Set<Int>,
): Int? {
val sourceObj = ensureObjSlot(sourceValue)
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjIterable))
val typeSlot = allocSlot()
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot)
builder.emit(Opcode.ASSERT_IS, sourceObj.slot, typeSlot)
val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true)
val iteratorMethodId = iterableMethods["iterator"]
?: throw BytecodeCompileException("Missing member id for Iterable.iterator", stmt.pos)
val iteratorMethods = ObjIterator.instanceMethodIdMap(includeAbstract = true)
val hasNextMethodId = iteratorMethods["hasNext"]
?: throw BytecodeCompileException("Missing member id for Iterator.hasNext", stmt.pos)
val nextMethodId = iteratorMethods["next"]
?: throw BytecodeCompileException("Missing member id for Iterator.next", stmt.pos)
val iterSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, sourceObj.slot, iteratorMethodId, 0, 0, iterSlot)
builder.emit(Opcode.ITER_PUSH, iterSlot)
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 hasNextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, hasNextMethodId, 0, 0, hasNextSlot)
val condSlot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot)
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel))
)
val nextSlot = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, iterSlot, nextMethodId, 0, 0, nextSlot)
val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN))
emitMove(CompiledValue(nextObj.slot, SlotType.OBJ), loopSlotId)
updateSlotType(loopSlotId, SlotType.OBJ)
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
loopStack.addLast(
LoopContext(
stmt.label,
endLabel,
continueLabel,
breakFlagSlot,
resultSlot,
hasIterator = true
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot!!)
}
builder.mark(continueLabel)
if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (needsBreakFlag) {
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)
} 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!!)
}
if (needsBreakFlag) {
builder.mark(afterElse!!)
}
}
return resultSlot ?: breakFlagSlot
}
private fun emitRangeObject(startValue: CompiledValue, endValue: CompiledValue, ref: RangeRef): CompiledValue {
val startObj = ensureObjSlot(startValue)
val endObj = ensureObjSlot(endValue)
val inclusiveSlot = allocSlot()
val inclusiveId = builder.addConst(BytecodeConst.Bool(ref.isEndInclusive))
builder.emit(Opcode.CONST_BOOL, inclusiveId, inclusiveSlot)
val descendingSlot = allocSlot()
val descendingId = builder.addConst(BytecodeConst.Bool(ref.isDescending))
builder.emit(Opcode.CONST_BOOL, descendingId, descendingSlot)
val stepSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, stepSlot)
updateSlotType(stepSlot, SlotType.OBJ)
val dst = allocSlot()
builder.emit(Opcode.MAKE_RANGE, startObj.slot, endObj.slot, inclusiveSlot, descendingSlot, stepSlot, dst)
updateSlotType(dst, SlotType.OBJ)
slotObjClass[dst] = ObjRange.type
return CompiledValue(dst, SlotType.OBJ)
}
private fun isDynamicIntRangeCandidate(ref: RangeRef): Boolean {
val left = ref.left ?: return false
val right = ref.right ?: return false
return isIntLikeRef(left) && isIntLikeRef(right)
}
private fun isIntLikeRef(ref: ObjRef): Boolean {
if (inferNumericKind(ref) == NumericKind.INT) {
return true
}
return when (ref) {
is ConstRef -> ref.constValue is ObjInt
is LocalSlotRef,
is LocalVarRef,
is FastLocalVarRef,
is BoundLocalVarRef,
is CallRef,
is MethodCallRef,
is FieldRef,
is CastRef,
is StatementRef -> resolveReceiverClass(ref) == ObjInt.type
is ThisMethodSlotCallRef,
is ImplicitThisMethodCallRef,
is ThisFieldSlotRef,
is ImplicitThisMemberRef -> resolveReceiverClassForScopeCollection(ref) == ObjInt.type
is UnaryOpRef -> ref.op == UnaryOp.NEGATE && isIntLikeRef(unaryOperand(ref))
is BinaryOpRef -> when (binaryOp(ref)) {
BinOp.PLUS,
BinOp.MINUS,
BinOp.STAR,
BinOp.SLASH,
BinOp.PERCENT,
BinOp.BAND,
BinOp.BXOR,
BinOp.BOR,
BinOp.SHL,
BinOp.SHR -> isIntLikeRef(binaryLeft(ref)) && isIntLikeRef(binaryRight(ref))
else -> false
}
else -> false
}
}
private fun coerceToLoopInt(value: CompiledValue): CompiledValue? {
return when (value.type) {
SlotType.INT -> value
SlotType.OBJ -> {
val isExactInt = isExactNonNullSlotClassOrTemp(value.slot, ObjInt.type)
val isStableIntObj = slotObjClass[value.slot] == ObjInt.type && isStablePrimitiveSourceSlot(value.slot)
if (!isExactInt && !isStableIntObj && !isStablePrimitiveSourceSlot(value.slot)) return null
val objSlot = if (isExactInt || isStableIntObj) {
value.slot
} else {
val boxed = allocSlot()
builder.emit(Opcode.BOX_OBJ, value.slot, boxed)
updateSlotType(boxed, SlotType.OBJ)
emitAssertObjSlotIsInt(boxed)
}
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, objSlot, intSlot)
updateSlotType(intSlot, SlotType.INT)
CompiledValue(intSlot, SlotType.INT)
}
SlotType.UNKNOWN -> {
if (!isStablePrimitiveSourceSlot(value.slot)) return null
val boxed = allocSlot()
builder.emit(Opcode.BOX_OBJ, value.slot, boxed)
updateSlotType(boxed, SlotType.OBJ)
val checked = emitAssertObjSlotIsInt(boxed)
val intSlot = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, checked, intSlot)
updateSlotType(intSlot, SlotType.INT)
CompiledValue(intSlot, SlotType.INT)
}
else -> null
}
}
private fun coerceToArithmeticInt(ref: ObjRef, value: CompiledValue): CompiledValue? {
if (value.type == SlotType.INT) return value
val refSuggestsInt = inferNumericKind(ref) == NumericKind.INT
val stableNonTemp = !isTempSlot(value.slot) && isStablePrimitiveSourceSlot(value.slot)
if (!refSuggestsInt && !stableNonTemp) return null
return coerceToLoopInt(value)
}
private fun emitAssertObjSlotIsInt(slot: Int): Int {
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjInt.type))
val typeSlot = allocSlot()
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot)
builder.emit(Opcode.ASSERT_IS, slot, typeSlot)
return slot
} }
private fun extractDeclaredRange(stmt: Statement?): RangeRef? { private fun extractDeclaredRange(stmt: Statement?): RangeRef? {

View File

@ -314,8 +314,13 @@ class CmdUnboxIntObj(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdUnboxIntObjLocal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdUnboxIntObjLocal(internal val src: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val value = frame.frame.getRawObj(src) as ObjInt when (frame.frame.getSlotTypeCode(src)) {
frame.setLocalInt(dst, value.value) SlotType.INT.code -> frame.setLocalInt(dst, frame.frame.getInt(src))
else -> {
val value = frame.frame.getRawObj(src) as ObjInt
frame.setLocalInt(dst, value.value)
}
}
return return
} }
} }
@ -331,8 +336,13 @@ class CmdUnboxRealObj(internal val src: Int, internal val dst: Int) : Cmd() {
class CmdUnboxRealObjLocal(internal val src: Int, internal val dst: Int) : Cmd() { class CmdUnboxRealObjLocal(internal val src: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val value = frame.frame.getRawObj(src) as ObjReal when (frame.frame.getSlotTypeCode(src)) {
frame.setLocalReal(dst, value.value) SlotType.REAL.code -> frame.setLocalReal(dst, frame.frame.getReal(src))
else -> {
val value = frame.frame.getRawObj(src) as ObjReal
frame.setLocalReal(dst, value.value)
}
}
return return
} }
} }
@ -1540,9 +1550,15 @@ class CmdCmpEqIntObj(internal val a: Int, internal val b: Int, internal val dst:
class CmdCmpEqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpEqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value == right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left == right)
return return
} }
} }
@ -1563,9 +1579,15 @@ class CmdCmpNeqIntObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpNeqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpNeqIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value != right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left != right)
return return
} }
} }
@ -1586,9 +1608,15 @@ class CmdCmpLtIntObj(internal val a: Int, internal val b: Int, internal val dst:
class CmdCmpLtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value < right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left < right)
return return
} }
} }
@ -1609,9 +1637,15 @@ class CmdCmpLteIntObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpLteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value <= right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left <= right)
return return
} }
} }
@ -1632,9 +1666,15 @@ class CmdCmpGtIntObj(internal val a: Int, internal val b: Int, internal val dst:
class CmdCmpGtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGtIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value > right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left > right)
return return
} }
} }
@ -1655,9 +1695,15 @@ class CmdCmpGteIntObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpGteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGteIntObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjInt val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjInt SlotType.INT.code -> frame.frame.getInt(a)
frame.setLocalBool(dst, left.value >= right.value) else -> (frame.frame.getRawObj(a) as ObjInt).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.INT.code -> frame.frame.getInt(b)
else -> (frame.frame.getRawObj(b) as ObjInt).value
}
frame.setLocalBool(dst, left >= right)
return return
} }
} }
@ -1678,9 +1724,15 @@ class CmdCmpEqRealObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpEqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpEqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value == right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left == right)
return return
} }
} }
@ -1701,9 +1753,15 @@ class CmdCmpNeqRealObj(internal val a: Int, internal val b: Int, internal val ds
class CmdCmpNeqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpNeqRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value != right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left != right)
return return
} }
} }
@ -1724,9 +1782,15 @@ class CmdCmpLtRealObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpLtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value < right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left < right)
return return
} }
} }
@ -1747,9 +1811,15 @@ class CmdCmpLteRealObj(internal val a: Int, internal val b: Int, internal val ds
class CmdCmpLteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpLteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value <= right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left <= right)
return return
} }
} }
@ -1770,9 +1840,15 @@ class CmdCmpGtRealObj(internal val a: Int, internal val b: Int, internal val dst
class CmdCmpGtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGtRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value > right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left > right)
return return
} }
} }
@ -1793,9 +1869,15 @@ class CmdCmpGteRealObj(internal val a: Int, internal val b: Int, internal val ds
class CmdCmpGteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() { class CmdCmpGteRealObjLocal(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override val isFast: Boolean = true override val isFast: Boolean = true
override fun performFast(frame: CmdFrame) { override fun performFast(frame: CmdFrame) {
val left = frame.frame.getRawObj(a) as ObjReal val left = when (frame.frame.getSlotTypeCode(a)) {
val right = frame.frame.getRawObj(b) as ObjReal SlotType.REAL.code -> frame.frame.getReal(a)
frame.setLocalBool(dst, left.value >= right.value) else -> (frame.frame.getRawObj(a) as ObjReal).value
}
val right = when (frame.frame.getSlotTypeCode(b)) {
SlotType.REAL.code -> frame.frame.getReal(b)
else -> (frame.frame.getRawObj(b) as ObjReal).value
}
frame.setLocalBool(dst, left >= right)
return return
} }
} }
@ -3624,7 +3706,13 @@ class CmdGetIndex(
internal val dst: Int, internal val dst: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
val result = frame.slotToObj(targetSlot).getAt(frame.ensureScope(), frame.slotToObj(indexSlot)) val target = frame.storedSlotObj(targetSlot)
val index = frame.storedSlotObj(indexSlot)
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
frame.storeObjResult(dst, target.list[index.toInt()])
return
}
val result = target.getAt(frame.ensureScope(), index)
frame.storeObjResult(dst, result) frame.storeObjResult(dst, result)
return return
} }
@ -3636,7 +3724,14 @@ class CmdSetIndex(
internal val valueSlot: Int, internal val valueSlot: Int,
) : Cmd() { ) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
frame.slotToObj(targetSlot).putAt(frame.ensureScope(), frame.slotToObj(indexSlot), frame.slotToObj(valueSlot)) val target = frame.storedSlotObj(targetSlot)
val index = frame.storedSlotObj(indexSlot)
val value = frame.slotToObj(valueSlot)
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
target.list[index.toInt()] = value
return
}
target.putAt(frame.ensureScope(), index, value)
return return
} }
} }

View File

@ -0,0 +1,145 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Benchmarks
import net.sergeych.lyng.BytecodeBodyProvider
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.PerfProfiles
import net.sergeych.lyng.Script
import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.bytecode.CmdCallMemberSlot
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdGetIndex
import net.sergeych.lyng.bytecode.CmdIterPush
import net.sergeych.lyng.bytecode.CmdMakeRange
import net.sergeych.lyng.bytecode.CmdSetIndex
import net.sergeych.lyng.obj.ObjString
import java.nio.file.Files
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.TimeSource
class PiSpigotBenchmarkTest {
@Test
fun benchmarkPiSpigot() = runTest {
if (!Benchmarks.enabled) return@runTest
val source = Files.readString(resolveExample("pi-test.lyng"))
val legacySource = source.replace(
"val quotient = sum / denom",
"var quotient = floor((sum / (denom * 1.0))).toInt()"
)
assertTrue(legacySource != source, "failed to build legacy piSpigot benchmark case")
val digits = 200
val expectedSuffix = "49303819"
val legacyElapsed = runCase("legacy-real-division", legacySource, digits, expectedSuffix, dumpBytecode = true)
val saved = PerfProfiles.snapshot()
PerfFlags.RVAL_FASTPATH = false
val optimizedRvalOffElapsed = runCase(
"optimized-int-division-rval-off",
source,
digits,
expectedSuffix,
dumpBytecode = false
)
PerfProfiles.restore(saved)
val optimizedElapsed = runCase("optimized-int-division-rval-on", source, digits, expectedSuffix, dumpBytecode = true)
val sourceSpeedup = legacyElapsed.toDouble() / optimizedRvalOffElapsed.toDouble()
val runtimeSpeedup = optimizedRvalOffElapsed.toDouble() / optimizedElapsed.toDouble()
val totalSpeedup = legacyElapsed.toDouble() / optimizedElapsed.toDouble()
println(
"[DEBUG_LOG] [BENCH] pi-spigot compare n=$digits legacy=${legacyElapsed} ms " +
"intDiv=${optimizedRvalOffElapsed} ms rvalOn=${optimizedElapsed} ms " +
"intDivSpeedup=${"%.2f".format(sourceSpeedup)}x " +
"rvalSpeedup=${"%.2f".format(runtimeSpeedup)}x " +
"total=${"%.2f".format(totalSpeedup)}x"
)
}
private suspend fun runCase(
name: String,
source: String,
digits: Int,
expectedSuffix: String,
dumpBytecode: Boolean,
): Long {
val scope = Script.newScope()
scope.eval(source)
if (dumpBytecode) {
println("[DEBUG_LOG] [BENCH] pi-spigot cmd:\n${scope.disassembleSymbol("piSpigot")}")
dumpHotOps(scope, "piSpigot")
}
val first = scope.eval("piSpigot($digits)") as ObjString
assertEquals(expectedSuffix, first.value)
repeat(2) {
val warm = scope.eval("piSpigot($digits)") as ObjString
assertEquals(expectedSuffix, warm.value)
}
val iterations = 3
val start = TimeSource.Monotonic.markNow()
repeat(iterations) {
val result = scope.eval("piSpigot($digits)") as ObjString
assertEquals(expectedSuffix, result.value)
}
val elapsedMs = start.elapsedNow().inWholeMilliseconds
val avgMs = elapsedMs.toDouble() / iterations.toDouble()
println(
"[DEBUG_LOG] [BENCH] pi-spigot $name n=$digits iterations=$iterations " +
"elapsed=${elapsedMs} ms avg=${"%.2f".format(avgMs)} ms"
)
return elapsedMs
}
private fun dumpHotOps(scope: net.sergeych.lyng.Scope, name: String) {
val fn = resolveBytecodeFunction(scope, name) ?: return
val makeRange = fn.cmds.count { it is CmdMakeRange }
val callMemberSlot = fn.cmds.count { it is CmdCallMemberSlot }
val iterPush = fn.cmds.count { it is CmdIterPush }
val getIndex = fn.cmds.count { it is CmdGetIndex }
val setIndex = fn.cmds.count { it is CmdSetIndex }
println(
"[DEBUG_LOG] [BENCH] pi-spigot hot-ops " +
"makeRange=$makeRange callMemberSlot=$callMemberSlot iterPush=$iterPush " +
"getIndex=$getIndex setIndex=$setIndex total=${fn.cmds.size}"
)
}
private fun resolveBytecodeFunction(scope: net.sergeych.lyng.Scope, name: String): CmdFunction? {
val record = scope.get(name) ?: return null
val stmt = record.value as? Statement ?: return null
return (stmt as? BytecodeStatement)?.bytecodeFunction()
?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction()
}
private fun resolveExample(name: String): Path {
val direct = Path.of("examples", name)
if (Files.exists(direct)) return direct
val parent = Path.of("..", "examples", name)
if (Files.exists(parent)) return parent
error("example not found: $name")
}
}

View File

@ -0,0 +1,172 @@
# Pi Spigot Benchmark Baseline
Date: 2026-04-03
Command:
`./gradlew :lynglib:jvmTest -Pbenchmarks=true --tests 'PiSpigotBenchmarkTest' --rerun-tasks`
Results for `n=200`:
- legacy-real-division: 1108 ms (3 iters, avg 369.33 ms)
- optimized-int-division-rval-off: 756 ms (3 iters, avg 252.00 ms)
- optimized-int-division-rval-on: 674 ms (3 iters, avg 224.67 ms)
Derived speedups:
- intDivSpeedup: 1.47x
- rvalSpeedup: 1.12x
- total: 1.64x
Notes:
- Bytecode still shows generic range iteration (`MAKE_RANGE`, `CALL_MEMBER_SLOT`, `ITER_PUSH`) for loop constructs in the legacy benchmark case.
- This baseline is captured before enabling counted-loop lowering for dynamic inline int ranges.
Optimization #1 follow-up:
- Attempt: broaden compiler loop lowering for dynamic int ranges and validate with `PiSpigotBenchmarkTest` bytecode dumps.
- Final result: success after switching loop-bound coercion to a runtime-checked int path for stable slots with missing metadata.
- Latest measured run after the working compiler change:
- legacy-real-division: 783 ms (3 iters, avg 261.00 ms)
- optimized-int-division-rval-off: 729 ms (3 iters, avg 243.00 ms)
- optimized-int-division-rval-on: 593 ms (3 iters, avg 197.67 ms)
- Hot-op counts for optimized bytecode now show the generic range iterator path is gone from the main loops:
- `makeRange=0`
- `callMemberSlot=2`
- `iterPush=0`
- `getIndex=4`
- `setIndex=4`
- The remaining member calls are non-loop overhead; the main improvement came from lowering `for` ranges to counted int loops.
Optimization #2 follow-up:
- Attempt: coerce stable integer operands into `INT` arithmetic during binary-op lowering so hot expressions stop falling back to `OBJ` math.
- Latest measured run after the arithmetic change:
- legacy-real-division: 593 ms (3 iters, avg 197.67 ms)
- optimized-int-division-rval-off: 542 ms (3 iters, avg 180.67 ms)
- optimized-int-division-rval-on: 516 ms (3 iters, avg 172.00 ms)
- Compiled-code impact in the optimized case:
- `boxes = n * 10 / 3` is now `UNBOX_INT_OBJ` + `MUL_INT` + `DIV_INT`
- `j = boxes - k` is now `SUB_INT`
- `denom = j * 2 + 1` is now `MUL_INT` + `ADD_INT`
- `carriedOver = quotient * j` is now `MUL_INT`
- Remaining hot object arithmetic is centered on list-backed reminder values and derived sums:
- `reminders[j] * 10`
- `reminders[j] + carriedOver`
- `sum / denom`, `sum % denom`, `sum / 10`
- Conclusion: loop lowering is fixed; the next likely win is preserving `List<Int>` element typing for `reminders` so indexed loads stay in int space.
Optimization #3 follow-up:
- Attempt: teach numeric-kind inference that `IndexRef` can be `INT`/`REAL` when the receiver list has a known element class.
- Compiler change:
- `inferNumericKind()` now handles `IndexRef` and resolves the receiver slot or receiver-declared list element class before choosing `INT`/`REAL`.
- Latest measured run after the indexed-load inference change:
- legacy-real-division: 656 ms (3 iters, avg 218.67 ms)
- optimized-int-division-rval-off: 509 ms (3 iters, avg 169.67 ms)
- optimized-int-division-rval-on: 403 ms (3 iters, avg 134.33 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 1.29x
- rvalSpeedup: 1.26x
- total: 1.63x
- Compiled-code impact in the optimized case:
- `carriedOver = quotient * j` stays in `INT` space (`ASSERT_IS` + `UNBOX_INT_OBJ` + `MUL_INT`) instead of plain object multiply.
- Counted int loops remain intact (`MAKE_RANGE=0`, `ITER_PUSH=0`).
- Remaining bottlenecks in the optimized bytecode:
- `GET_INDEX reminders[j]` still feeds `MUL_OBJ` / `ADD_OBJ`
- `sum / denom`, `sum % denom`, and `sum / 10` still compile to object arithmetic
- `suffix += pi[i]` remains `ADD_OBJ`, which is expected because it is string/object concatenation
- Conclusion:
- The new inference produced a real VM-speed gain, especially with `RVAL_FASTPATH` enabled.
- The next compiler win is stronger propagation from `List<Int>` indexed loads into the produced temporary slot so `sum` can stay typed as `INT` across the inner loop.
Optimization #4 follow-up:
- Attempt: preserve boxed-argument metadata through `compileCallArgs()` so `list.add(x)` retains `ObjInt` / `ObjReal` element typing.
- Compiler/runtime fixes:
- `compileCallArgs()` now routes arguments through `ensureObjSlot()` + `emitMove()` instead of raw `BOX_OBJ`, preserving `slotObjClass` and `stableObjSlots`.
- `CmdSetIndex` now reads `valueSlot` via `slotToObj()` so `SET_INDEX` can safely accept primitive slots.
- Fast local unbox ops (`CmdUnboxIntObjLocal`, `CmdUnboxRealObjLocal`) now handle already-primitive source slots directly instead of assuming a raw object payload.
- Plain assignment now coerces object-int RHS back into `INT` when the destination slot is currently compiled as `INT`, keeping loop-carried locals type-consistent.
- Latest measured run after the propagation + VM fixes:
- legacy-real-division: 438 ms (3 iters, avg 146.00 ms)
- optimized-int-division-rval-off: 238 ms (3 iters, avg 79.33 ms)
- optimized-int-division-rval-on: 201 ms (3 iters, avg 67.00 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 1.84x
- rvalSpeedup: 1.18x
- total: 2.18x
- Compiled-code impact in the optimized case:
- `sum = reminders[j] + carriedOver` is now `GET_INDEX` + `UNBOX_INT_OBJ` + `ADD_INT`
- `reminders[j] = sum % denom` is now `MOD_INT` + `SET_INDEX`
- `q = sum / 10` is now `DIV_INT`
- `carriedOver = quotient * j` is now `MUL_INT`
- Remaining hot object arithmetic in the optimized case:
- `reminders[j] *= 10` still compiles as `GET_INDEX` + `MUL_OBJ` + `SET_INDEX`
- `suffix += pi[i]` remains `ADD_OBJ`, which is expected string/object concatenation
- Conclusion:
- The main remaining arithmetic bottleneck is the compound index assignment path for `reminders[j] *= 10`.
- The next direct win is to specialize `AssignOpRef` on typed list elements so indexed compound assignment can lower to `UNBOX_INT_OBJ` + `MUL_INT` + boxed `SET_INDEX`.
Optimization #5 follow-up:
- Attempt: specialize typed `IndexRef` compound assignment so `List<Int>` element updates avoid object arithmetic.
- Compiler change:
- `compileAssignOp()` now detects non-optional typed `List<Int>` index targets and lowers arithmetic assign-ops through `UNBOX_INT_OBJ` + `*_INT` + `SET_INDEX`.
- Latest measured run after the indexed compound-assignment change:
- legacy-real-division: 394 ms (3 iters, avg 131.33 ms)
- optimized-int-division-rval-off: 216 ms (3 iters, avg 72.00 ms)
- optimized-int-division-rval-on: 184 ms (3 iters, avg 61.33 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 1.82x
- rvalSpeedup: 1.17x
- total: 2.14x
- Compiled-code impact in the optimized case:
- `reminders[j] *= 10` is now:
- `GET_INDEX`
- `UNBOX_INT_OBJ`
- `MUL_INT`
- `SET_INDEX`
- The optimized inner loop no longer contains object arithmetic for the `reminders` state update path.
- Remaining hot object work in the optimized case:
- `suffix += pi[i]` remains `ADD_OBJ` and is expected string/object concatenation
- The legacy benchmark case still carries real/object work because it intentionally keeps the original `floor(sum / (denom * 1.0))` path
- Conclusion:
- The inner arithmetic hot loop is now effectively int-lowered end-to-end in the optimized benchmark path.
- Further wins will likely require reducing list access overhead itself (`GET_INDEX` / `SET_INDEX`) or changing the source algorithm/data layout, not more basic arithmetic lowering.
Optimization #6 follow-up:
- Attempt: move the direct `ObjList` index fast path out from behind `RVAL_FASTPATH` so the common plain-list case is fast by default.
- Runtime change:
- `CmdGetIndex` and `CmdSetIndex` now always use direct `target.list[index]` / `target.list[index] = value` for exact `ObjList` receivers with `ObjInt` indices.
- Subclasses such as `ObjObservableList` still use their overridden `getAt` / `putAt` logic, so semantics stay intact.
- Latest measured run after the default plain-list path:
- legacy-real-division: 397 ms (3 iters, avg 132.33 ms)
- optimized-int-division-rval-off: 138 ms (3 iters, avg 46.00 ms)
- optimized-int-division-rval-on: 164 ms (3 iters, avg 54.67 ms)
- Derived speedups vs legacy in this run:
- intDivSpeedup: 2.88x
- rvalSpeedup: 0.84x
- total: 2.42x
- Interpretation:
- The stable fast baseline is now the `rval-off` case, because the direct plain-`ObjList` path no longer depends on `RVAL_FASTPATH`.
- `RVAL_FASTPATH` no longer improves this benchmark and only reflects remaining unrelated runtime variance.
- Conclusion:
- For `piSpigot`, the main VM list-access bottleneck is addressed in the default runtime path.
- Further work on this benchmark should target algorithm/data-layout changes or string-result construction, not the old `RVAL_FASTPATH` gate.
Remaining optimization candidates:
- `suffix += pi[i]` still compiles as repeated `ADD_OBJ` string/object concatenation.
- Best next option: build the suffix through a dedicated buffer/list-join path instead of per-iteration concatenation.
- The benchmark still performs many `GET_INDEX` / `SET_INDEX` operations even after the direct plain-`ObjList` fast path.
- Best next option: reduce indexed access count at the source level or introduce a more specialized typed-list storage layout if this benchmark matters enough.
- The legacy benchmark variant intentionally keeps the real-number `floor(sum / (denom * 1.0))` path.
- No release optimization needed there; it remains only as a regression/control case.
- `RVAL_FASTPATH` is no longer a useful tuning knob for this workload after the plain-list VM fast path.
- Best next option: profile other workloads before changing or removing it globally.
Release stabilization note:
- The broad assignment-side `INT` coercion and subclass-bypassing list fast path were rolled back/narrowed to restore correctness across numeric-mix, decimal, list, observable-list, and wasm tests.
- Full release gates now pass:
- `./gradlew test`
- `./gradlew :lynglib:wasmJsNodeTest`
- Current release-safe benchmark on the stabilized tree:
- legacy-real-division: 732 ms (3 iters, avg 244.00 ms)
- optimized-int-division-rval-off: 545 ms (3 iters, avg 181.67 ms)
- optimized-int-division-rval-on: 697 ms (3 iters, avg 232.33 ms)
- Interpretation:
- The release baseline is now `optimized-int-division-rval-off` at 545 ms for the current correct/stable tree.
- The removed coercion had been masking a real compiler typing gap; reintroducing it broadly is not release-safe.
- Highest-value remaining compiler optimization after release:
- Recover typed int lowering for `j = boxes - k`, `denom = j * 2 + 1`, `sum = reminders[j] + carriedOver`, and `carriedOver = quotient * j` using a narrower proof than the removed generic arithmetic coercion.