Step 6 — Short-Circuit Logic
&& and || are not arithmetic operators — they have to avoid evaluating the right-hand side when the left settles the answer. That's "short-circuiting", and it's expressed entirely with jumps.
a && b
<a>
JumpIfFalse L_END
Pop ; drop the (truthy) <a>
<b>
L_END: ; either <a> (if it was falsy) or <b> sits on the stack
If a is falsy, we skip the Pop <b> block and the falsy value of a remains as the result — exactly what a && b should return when a is falsy. If a is truthy, we pop it and evaluate b, so b is the result.
void Compiler::visit(LogicalExpr& e) {
e.lhs->accept(*this);
if (e.op == TokenKind::AmpAmp) {
size_t endJump = emitJump(Op::JumpIfFalse, e.line);
emit(Op::Pop, e.line);
e.rhs->accept(*this);
patchJump(endJump, e.line);
} else if (e.op == TokenKind::PipePipe) {
// see below
}
}
a || b
<a>
JumpIfFalse L_RHS
Jump L_END ; <a> was truthy; keep it as result
L_RHS:
Pop ; drop the (falsy) <a>
<b>
L_END:
If a is truthy, we jump straight to L_END leaving a on the stack. If a is falsy, we pop it and evaluate b so b becomes the result.
size_t elseJump = emitJump(Op::JumpIfFalse, e.line);
size_t endJump = emitJump(Op::Jump, e.line);
patchJump(elseJump, e.line);
emit(Op::Pop, e.line);
e.rhs->accept(*this);
patchJump(endJump, e.line);
Why JumpIfFalse Doesn't Pop
If JumpIfFalse popped its condition, encoding &&/|| would need a Dup opcode (push another copy first) to preserve the value as the result. By keeping JumpIfFalse non-popping and emitting an explicit Pop on the "consume-the-condition" branch only, we save the Dup and one byte per logical operator.
Lox's compiler in Crafting Interpreters makes the same call.
Truthiness Reminder
Because JumpIfFalse uses Value::isTruthy, you get JavaScript-like coalescing semantics:
print 0 || 5; // 0 (since 0 is truthy in MiniLang — Lua semantics)
print nil || "hi"; // "hi"
print false || 42; // 42
print 3 && 4; // 4
To get C-style &&/|| that return 0 or 1 you'd add a final Op::Not Op::Not to coerce — the test of "is this booleanable" runs twice.
Verifying the Compilation
The unit tests assert the exact opcode sequence:
void test_short_circuit_and() {
auto out = compileSource("print false && true;");
CHECK(opsMatch(out.chunk, {
Op::False, Op::JumpIfFalse, Op::Pop, Op::True,
Op::Print, Op::Return
}));
}
This is exactly the lowering we sketched.
Self-Check
- Why don't we need a
Dupopcode given howJumpIfFalseis defined? - Walk through the compiled bytecode for
a && b || c. (Hint:||has the lowest precedence;&&binds tighter.) - How would you implement
??(null-coalescing, "use rhs if lhs isnilelse lhs")?