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? { 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 a = compileRef(binaryLeft(ref)) ?: return null
val b = compileRef(binaryRight(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 if (a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN) return null
val out = allocSlot() val out = allocSlot()
val op = binaryOp(ref)
return when (op) { return when (op) {
BinOp.PLUS -> when (a.type) { BinOp.PLUS -> when (a.type) {
SlotType.INT -> { 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? { private fun compileAssign(ref: AssignRef): CompiledValue? {
val target = assignTarget(ref) ?: return null val target = assignTarget(ref) ?: return null
if (refDepth(target) != 0) return null if (refDepth(target) != 0) return null
@ -394,4 +424,5 @@ class BytecodeCompiler {
private fun unaryOp(ref: UnaryOpRef): UnaryOp = ref.op private fun unaryOp(ref: UnaryOpRef): UnaryOp = ref.op
private fun assignTarget(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef private fun assignTarget(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef
private fun assignValue(ref: AssignRef): ObjRef = ref.value 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 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 -> { Opcode.EVAL_FALLBACK -> {
val id = decoder.readConstId(code, ip, 2) val id = decoder.readConstId(code, ip, 2)
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.BinaryOpRef
import net.sergeych.lyng.obj.BinOp import net.sergeych.lyng.obj.BinOp
import net.sergeych.lyng.obj.ConstRef import net.sergeych.lyng.obj.ConstRef
import net.sergeych.lyng.obj.ObjFalse
import net.sergeych.lyng.obj.ObjInt 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.ObjVoid
import net.sergeych.lyng.obj.toBool
import net.sergeych.lyng.obj.toInt import net.sergeych.lyng.obj.toInt
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -93,4 +97,36 @@ class BytecodeVmTest {
val result = BytecodeVm().execute(fn, Scope(), emptyList()) val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(ObjVoid, result) 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())
}
} }