Compare commits

...

9 Commits

6 changed files with 250 additions and 60 deletions

View File

@ -86,6 +86,7 @@ class BytecodeCompiler(
private val loopVarSlots = HashSet<Int>()
private val loopStack = ArrayDeque<LoopContext>()
private var currentPos: Pos? = null
private var cachedVoidSlot: Int? = null
private data class LoopContext(
val label: String?,
@ -1374,23 +1375,6 @@ class BytecodeCompiler(
val rightRef = binaryRight(ref)
var a = compileRefWithFallback(leftRef, null, refPos(ref)) ?: return null
var b = compileRefWithFallback(rightRef, null, refPos(ref)) ?: return null
if (op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT)) {
val leftNeedsObj = a.type == SlotType.INT && b.type == SlotType.REAL
val rightNeedsObj = b.type == SlotType.INT && a.type == SlotType.REAL
if (leftNeedsObj || rightNeedsObj) {
val leftObj = if (leftNeedsObj) {
compileScopeSlotObj(leftRef) ?: a
} else {
a
}
val rightObj = if (rightNeedsObj) {
compileScopeSlotObj(rightRef) ?: b
} else {
b
}
return compileObjBinaryOp(leftRef, leftObj, rightObj, op, refPos(ref))
}
}
val intOps = setOf(
BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT,
BinOp.BAND, BinOp.BOR, BinOp.BXOR, BinOp.SHL, BinOp.SHR
@ -1413,7 +1397,11 @@ class BytecodeCompiler(
}
val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN
val allowMixedNumeric = op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH)
if (typesMismatch && op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT)) {
val isMixedNumeric = (a.type == SlotType.INT && b.type == SlotType.REAL) ||
(a.type == SlotType.REAL && b.type == SlotType.INT)
if (typesMismatch && op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT) &&
!(allowMixedNumeric && isMixedNumeric)
) {
return compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
if ((a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) &&
@ -1683,12 +1671,12 @@ class BytecodeCompiler(
val left = ensureObjSlot(a)
val right = ensureObjSlot(b)
val opcode = when {
isExactNonNullSlotClass(left.slot, ObjString.type) &&
isExactNonNullSlotClass(right.slot, ObjString.type) -> stringOp
isExactNonNullSlotClass(left.slot, ObjInt.type) &&
isExactNonNullSlotClass(right.slot, ObjInt.type) -> intOp
isExactNonNullSlotClass(left.slot, ObjReal.type) &&
isExactNonNullSlotClass(right.slot, ObjReal.type) -> realOp
isExactNonNullSlotClassOrTemp(left.slot, ObjString.type) &&
isExactNonNullSlotClassOrTemp(right.slot, ObjString.type) -> stringOp
isExactNonNullSlotClassOrTemp(left.slot, ObjInt.type) &&
isExactNonNullSlotClassOrTemp(right.slot, ObjInt.type) -> intOp
isExactNonNullSlotClassOrTemp(left.slot, ObjReal.type) &&
isExactNonNullSlotClassOrTemp(right.slot, ObjReal.type) -> realOp
else -> objOp
}
builder.emit(opcode, left.slot, right.slot, out)
@ -2352,60 +2340,36 @@ class BytecodeCompiler(
private fun compileLogicalAnd(ref: LogicalAndRef): CompiledValue? {
val leftValue = compileRefWithFallback(ref.left(), SlotType.BOOL, Pos.builtIn) ?: return null
val leftBool = if (leftValue.type == SlotType.BOOL) {
leftValue
} else {
val slot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, leftValue.slot, slot)
CompiledValue(slot, SlotType.BOOL)
}
if (leftValue.type != SlotType.BOOL) return null
val resultSlot = allocSlot()
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, resultSlot)
val endLabel = builder.label()
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(leftBool.slot), CmdBuilder.Operand.LabelRef(endLabel))
listOf(CmdBuilder.Operand.IntVal(leftValue.slot), CmdBuilder.Operand.LabelRef(endLabel))
)
val rightValue = compileRefWithFallback(ref.right(), SlotType.BOOL, Pos.builtIn) ?: return null
val rightBool = if (rightValue.type == SlotType.BOOL) {
rightValue
} else {
val slot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, rightValue.slot, slot)
CompiledValue(slot, SlotType.BOOL)
}
builder.emit(Opcode.MOVE_BOOL, rightBool.slot, resultSlot)
if (rightValue.type != SlotType.BOOL) return null
builder.emit(Opcode.MOVE_BOOL, rightValue.slot, resultSlot)
builder.mark(endLabel)
return CompiledValue(resultSlot, SlotType.BOOL)
}
private fun compileLogicalOr(ref: LogicalOrRef): CompiledValue? {
val leftValue = compileRefWithFallback(ref.left(), SlotType.BOOL, Pos.builtIn) ?: return null
val leftBool = if (leftValue.type == SlotType.BOOL) {
leftValue
} else {
val slot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, leftValue.slot, slot)
CompiledValue(slot, SlotType.BOOL)
}
if (leftValue.type != SlotType.BOOL) return null
val resultSlot = allocSlot()
val trueId = builder.addConst(BytecodeConst.Bool(true))
builder.emit(Opcode.CONST_BOOL, trueId, resultSlot)
val endLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(leftBool.slot), CmdBuilder.Operand.LabelRef(endLabel))
listOf(CmdBuilder.Operand.IntVal(leftValue.slot), CmdBuilder.Operand.LabelRef(endLabel))
)
val rightValue = compileRefWithFallback(ref.right(), SlotType.BOOL, Pos.builtIn) ?: return null
val rightBool = if (rightValue.type == SlotType.BOOL) {
rightValue
} else {
val slot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, rightValue.slot, slot)
CompiledValue(slot, SlotType.BOOL)
}
builder.emit(Opcode.MOVE_BOOL, rightBool.slot, resultSlot)
if (rightValue.type != SlotType.BOOL) return null
builder.emit(Opcode.MOVE_BOOL, rightValue.slot, resultSlot)
builder.mark(endLabel)
return CompiledValue(resultSlot, SlotType.BOOL)
}
@ -3569,6 +3533,11 @@ class BytecodeCompiler(
val elementClass = listElementClassBySlot[receiver.slot] ?: listElementClassFromReceiverRef(ref.targetRef)
if (elementClass != null) {
slotObjClass[dst] = elementClass
if (elementClass == ObjString.type && elementClass.isClosed) {
stableObjSlots.add(dst)
} else {
stableObjSlots.remove(dst)
}
}
return CompiledValue(dst, SlotType.OBJ)
}
@ -3635,6 +3604,12 @@ class BytecodeCompiler(
}
SlotType.OBJ -> {
if (objOp == null) return null
if (isExactNonNullSlotClassOrTemp(rhs.slot, ObjInt.type)) {
val right = allocSlot()
builder.emit(Opcode.UNBOX_INT_OBJ, rhs.slot, right)
builder.emit(intOp, out, right, out)
return CompiledValue(out, SlotType.INT)
}
val leftObj = allocSlot()
builder.emit(Opcode.BOX_OBJ, out, leftObj)
updateSlotType(leftObj, SlotType.OBJ)
@ -3660,6 +3635,12 @@ class BytecodeCompiler(
}
SlotType.OBJ -> {
if (objOp == null) return null
if (isExactNonNullSlotClassOrTemp(rhs.slot, ObjReal.type)) {
val right = allocSlot()
builder.emit(Opcode.UNBOX_REAL_OBJ, rhs.slot, right)
builder.emit(realOp, out, right, out)
return CompiledValue(out, SlotType.REAL)
}
val leftObj = allocSlot()
builder.emit(Opcode.BOX_OBJ, out, leftObj)
updateSlotType(leftObj, SlotType.OBJ)
@ -5660,6 +5641,17 @@ class BytecodeCompiler(
private fun emitInlineBlock(stmt: BlockStatement, needResult: Boolean): CompiledValue? =
emitInlineStatements(stmt.statements(), needResult)
private fun ensureVoidSlot(): Int {
val existing = cachedVoidSlot
if (existing != null) return existing
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, slot)
updateSlotType(slot, SlotType.OBJ)
cachedVoidSlot = slot
return slot
}
private fun shouldInlineBlock(stmt: BlockStatement): Boolean {
return allowLocalSlots
}
@ -6002,6 +5994,11 @@ class BytecodeCompiler(
try {
val needsBreakFlag = stmt.canBreak || stmt.elseStatement != null
val realWidenSlots = collectLoopRealWidenSlots(stmt.body)
val hasRealWiden = realWidenSlots.isNotEmpty()
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.REAL)
}
val breakFlagSlot = allocSlot()
if (range == null && rangeRef == null && typedRangeLocal == null) {
val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null
@ -6075,12 +6072,18 @@ class BytecodeCompiler(
)
)
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)
@ -6188,6 +6191,9 @@ class BytecodeCompiler(
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
@ -6195,6 +6201,9 @@ class BytecodeCompiler(
}
builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot)
if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
@ -6265,6 +6274,9 @@ class BytecodeCompiler(
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
@ -6272,6 +6284,9 @@ class BytecodeCompiler(
}
builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot)
if (hasRealWiden) {
emitLoopRealCoercions(realWidenSlots)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
@ -6331,6 +6346,9 @@ class BytecodeCompiler(
)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
if (hasRealWiden) {
applySlotTypes(realWidenSlots, SlotType.UNKNOWN)
}
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
@ -6460,10 +6478,7 @@ class BytecodeCompiler(
restoreFlowTypeOverride(elseRestore)
}
builder.mark(endLabel)
val slot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, slot)
return CompiledValue(slot, SlotType.OBJ)
return CompiledValue(ensureVoidSlot(), SlotType.OBJ)
}
private fun updateSlotTypeByName(name: String, type: SlotType) {

View File

@ -0,0 +1,93 @@
/*
* 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.Script
import net.sergeych.lyng.obj.ObjInt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.TimeSource
class MixedCompareBenchmarkTest {
@Test
fun benchmarkMixedCompareOps() = runTest {
if (!Benchmarks.enabled) return@runTest
val iterations = 200000
val script = """
fun mixedCompareBench(n) {
var acc = 0
var r = 0.0
val strs = ["a","b","aa","bb","abc","abd","zzz",""]
var i = 0
while(i < n) {
val si = strs[i % 8]
if( si == "a" ) acc += 1 else acc -= 1
if( si != "zzz" ) acc += 2
if( si == "" ) acc += 3
if( i < (i % 5) ) acc += 1 else acc -= 1
if( (i % 3) == 0 ) acc += 2
val r1 = i + 0.5
if( r1 > i ) acc += 1
if( i < r1 ) acc += 1
r += r1 * 0.25
if( r > 1000.0 ) acc += 1
i++
}
acc
}
""".trimIndent()
val scope = Script.newScope()
scope.eval(script)
val expected = expectedValue(iterations)
val start = TimeSource.Monotonic.markNow()
val result = scope.eval("mixedCompareBench($iterations)") as ObjInt
val elapsedMs = start.elapsedNow().inWholeMilliseconds
println("[DEBUG_LOG] [BENCH] mixed-compare elapsed=${elapsedMs} ms")
assertEquals(expected, result.value)
}
private fun expectedValue(iterations: Int): Long {
val strs = arrayOf("a", "b", "aa", "bb", "abc", "abd", "zzz", "")
var acc = 0L
var r = 0.0
var i = 0
while (i < iterations) {
val si = strs[i % 8]
if (si == "a") acc += 1 else acc -= 1
if (si != "zzz") acc += 2
if (si == "") acc += 3
if (i < (i % 5)) acc += 1 else acc -= 1
if ((i % 3) == 0) acc += 2
val r1 = i + 0.5
if (r1 > i) acc += 1
if (i < r1) acc += 1
r += r1 * 0.25
if (r > 1000.0) acc += 1
i += 1
}
return acc
}
}

View File

@ -1258,6 +1258,28 @@ class ScriptTest {
)
}
@Test
fun testForLoopRealWidenDisasm() = runTest {
val scope = Script.newScope()
scope.eval(
"""
fun widenFor() {
var acc = 0
for(i in 0..3) {
if (i == 1) acc = 0.5
}
}
""".trimIndent()
)
val disasm = scope.disassembleSymbol("widenFor")
println("[DEBUG_LOG] widenFor disasm:\n$disasm")
val incIndex = disasm.indexOf("INC_INT")
assertTrue(incIndex >= 0, "expected INC_INT in for-loop disasm")
val convIndex = disasm.indexOf("INT_TO_REAL")
assertTrue(convIndex >= 0, "expected INT_TO_REAL in for-loop disasm")
assertTrue(convIndex > incIndex, "INT_TO_REAL should appear after INC_INT")
}
@Test
fun testIntClosedRangeInclusive() = runTest {
eval(

View File

@ -0,0 +1,39 @@
# Fast Ops Optimizations Plan (Draft)
Baseline
- See `notes/nested_range_baseline.md`
Candidates (not started)
1) Primitive comparisons (done)
- Emit fast CMP variants for known ObjString/ObjInt/ObjReal using temp/stable slots.
- MixedCompareBenchmarkTest: 374 ms -> 347 ms.
2) Mixed numeric ops (done)
- Allow INT+REAL arithmetic to use primitive REAL ops (no obj fallback).
- MixedCompareBenchmarkTest: 347 ms -> 275 ms.
3) Boolean conversion (done; do not revert without review)
- Skip redundant OBJ_TO_BOOL in logical AND/OR when compiler already emits BOOL.
- MixedCompareBenchmarkTest: 275 ms -> 249 ms.
4) Range/loop hot path (done)
- Reuse a cached ObjVoid slot for if-statements in statement context (avoids per-iteration CONST_OBJ).
- MixedCompareBenchmarkTest: 249 ms -> 247 ms.
5) String ops (done)
- Mark GET_INDEX results as stable only for closed ObjString elements to enable fast compares.
- MixedCompareBenchmarkTest: 247 ms -> 240 ms.
6) Box/unbox audit (done)
- Unbox ObjInt/ObjReal in assign-op when target is INT/REAL to avoid boxing + obj ops.
- MixedCompareBenchmarkTest: 240 ms -> 234 ms.
7) Mixed compare coverage
- Emit CMP_*_REAL when one operand is known ObjReal in more expression forms (not just assign-op).
- Verify with disassembly that fast cmp opcodes are emitted.
8) Range-loop invariant hoist
- Cache range end/step into temps once per loop; avoid repeated slot reads/boxing in body.
- Confirm no extra CONST_OBJ in hot path.
9) Boxing elision pass
- Remove redundant BOX_OBJ when value feeds only primitive ops afterward (local liveness).
- Ensure no impact on closures/escaping values.
10) Closed-type fast paths expansion
- Apply closed-type trust for ObjBool/ObjInt/ObjReal/ObjString in ternaries and conditional chains.
- Guard with exact non-null temp/slot checks only.
11) VM hot op micro-optimizations
- Reduce frame reads/writes in ADD_INT, MUL_REAL, CMP_*_INT/REAL when operands are temps.
- Compare against baseline; revert if regression after 10-run median.

View File

@ -0,0 +1,12 @@
# Mixed Compare Benchmark Baseline
Date: 2026-02-16
Benchmark:
- MixedCompareBenchmarkTest.benchmarkMixedCompareOps
Command:
`BENCHMARKS=true timeout 20s ./gradlew :lynglib:jvmTest --tests MixedCompareBenchmarkTest --rerun-tasks`
Result:
- mixed-compare elapsed=374 ms

View File

@ -0,0 +1,9 @@
# Nested Range Benchmark Baseline
Date: 2026-02-16
Command:
`BENCHMARKS=true timeout 20s ./gradlew :lynglib:jvmTest --tests NestedRangeBenchmarkTest --rerun-tasks`
Result:
- nested-happy elapsed=56 ms