Add short-circuit ops in bytecode compiler and VM

This commit is contained in:
Sergey Chernov 2026-01-25 19:59:22 +03:00
parent 3d9170d677
commit 8ae6eb8d69
3 changed files with 77 additions and 1 deletions

View File

@ -125,11 +125,14 @@ class BytecodeCompiler {
}
private fun compileBinary(ref: BinaryOpRef): CompiledValue? {
val op = binaryOp(ref)
if (op == BinOp.AND || op == BinOp.OR) {
return compileLogical(op, binaryLeft(ref), binaryRight(ref), refPos(ref))
}
val a = compileRef(binaryLeft(ref)) ?: return null
val b = compileRef(binaryRight(ref)) ?: return null
if (a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN) return null
val out = allocSlot()
val op = binaryOp(ref)
return when (op) {
BinOp.PLUS -> when (a.type) {
SlotType.INT -> {
@ -305,6 +308,33 @@ class BytecodeCompiler {
}
}
private fun compileLogical(op: BinOp, left: ObjRef, right: ObjRef, pos: Pos): CompiledValue? {
val leftValue = compileRefWithFallback(left, SlotType.BOOL, pos) ?: return null
if (leftValue.type != SlotType.BOOL) return null
val resultSlot = allocSlot()
val shortLabel = builder.label()
val endLabel = builder.label()
if (op == BinOp.AND) {
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(BytecodeBuilder.Operand.IntVal(leftValue.slot), BytecodeBuilder.Operand.LabelRef(shortLabel))
)
} else {
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(BytecodeBuilder.Operand.IntVal(leftValue.slot), BytecodeBuilder.Operand.LabelRef(shortLabel))
)
}
val rightValue = compileRefWithFallback(right, SlotType.BOOL, pos) ?: return null
emitMove(rightValue, resultSlot)
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
builder.mark(shortLabel)
val constId = builder.addConst(BytecodeConst.Bool(op == BinOp.OR))
builder.emit(Opcode.CONST_BOOL, constId, resultSlot)
builder.mark(endLabel)
return CompiledValue(resultSlot, SlotType.BOOL)
}
private fun compileAssign(ref: AssignRef): CompiledValue? {
val target = assignTarget(ref) ?: return null
if (refDepth(target) != 0) return null
@ -394,4 +424,5 @@ class BytecodeCompiler {
private fun unaryOp(ref: UnaryOpRef): UnaryOp = ref.op
private fun assignTarget(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef
private fun assignValue(ref: AssignRef): ObjRef = ref.value
private fun refPos(ref: BinaryOpRef): Pos = Pos.builtIn
}

View File

@ -200,6 +200,15 @@ class BytecodeVm {
ip = target
}
}
Opcode.JMP_IF_TRUE -> {
val cond = decoder.readSlot(code, ip)
ip += fn.slotWidth
val target = decoder.readIp(code, ip, fn.ipWidth)
ip += fn.ipWidth
if (frame.getBool(cond)) {
ip = target
}
}
Opcode.EVAL_FALLBACK -> {
val id = decoder.readConstId(code, ip, 2)
ip += 2

View File

@ -25,8 +25,12 @@ import net.sergeych.lyng.bytecode.Opcode
import net.sergeych.lyng.obj.BinaryOpRef
import net.sergeych.lyng.obj.BinOp
import net.sergeych.lyng.obj.ConstRef
import net.sergeych.lyng.obj.ObjFalse
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjTrue
import net.sergeych.lyng.obj.ValueFnRef
import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.obj.toBool
import net.sergeych.lyng.obj.toInt
import kotlin.test.Test
import kotlin.test.assertEquals
@ -93,4 +97,36 @@ class BytecodeVmTest {
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(ObjVoid, result)
}
@Test
fun andIsShortCircuit() = kotlinx.coroutines.test.runTest {
val throwingRef = ValueFnRef { error("should not execute") }
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.AND,
ConstRef(ObjFalse.asReadonly),
throwingRef
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("andShort", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(false, result.toBool())
}
@Test
fun orIsShortCircuit() = kotlinx.coroutines.test.runTest {
val throwingRef = ValueFnRef { error("should not execute") }
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.OR,
ConstRef(ObjTrue.asReadonly),
throwingRef
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("orShort", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(true, result.toBool())
}
}