diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 264440a..45bfeaa 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt index 5d71998..0208260 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt @@ -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 diff --git a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt index dd822b5..83a70bf 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt @@ -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()) + } }