Skip to content

Commit d07d522

Browse files
morealclaudeyouknowone
authored
Optimize redundant bool check (#7176)
* Add compile_bool_op_inner and optimize nested opposite-operator BoolOps to avoid redundant __bool__ calls When a nested BoolOp has the opposite operator (e.g., `And` inside `Or`), the inner BoolOp's short-circuit exits are redirected to skip the outer BoolOp's redundant truth test. This avoids calling `__bool__()` twice on the same value (e.g., `Test() and False or False` previously called `Test().__bool__()` twice instead of once). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add snapshot test for nested BoolOp bytecode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add runtime test for redundant __bool__ check (issue #3567) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Apply clippy and rustfmt * Apply ruff format * Refactor compile_bool_op: extract emit_short_circuit_test and unify with compile_bool_op_inner Reduce code duplication by: - Extracting the repeated Copy + conditional jump pattern into emit_short_circuit_test - Merging compile_bool_op and compile_bool_op_inner into a single compile_bool_op_with_target with an optional short_circuit_target parameter - Keeping compile_bool_op as a thin wrapper for the public interface Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Relocate redundant __bool__ check test snippet * Update extra_tests/snippets/syntax_short_circuit_bool.py * Fix assertion in syntax_short_circuit_bool --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
1 parent f0f3c9c commit d07d522

File tree

3 files changed

+79
-21
lines changed

3 files changed

+79
-21
lines changed

crates/codegen/src/compile.rs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6611,33 +6611,45 @@ impl Compiler {
66116611
/// Compile a boolean operation as an expression.
66126612
/// This means, that the last value remains on the stack.
66136613
fn compile_bool_op(&mut self, op: &ast::BoolOp, values: &[ast::Expr]) -> CompileResult<()> {
6614-
let after_block = self.new_block();
6614+
self.compile_bool_op_with_target(op, values, None)
6615+
}
66156616

6617+
/// Compile a boolean operation as an expression, with an optional
6618+
/// short-circuit target override. When `short_circuit_target` is `Some`,
6619+
/// the short-circuit jumps go to that block instead of the default
6620+
/// `after_block`, enabling jump threading to avoid redundant `__bool__` calls.
6621+
fn compile_bool_op_with_target(
6622+
&mut self,
6623+
op: &ast::BoolOp,
6624+
values: &[ast::Expr],
6625+
short_circuit_target: Option<BlockIdx>,
6626+
) -> CompileResult<()> {
6627+
let after_block = self.new_block();
66166628
let (last_value, values) = values.split_last().unwrap();
6629+
let jump_target = short_circuit_target.unwrap_or(after_block);
66176630

66186631
for value in values {
6619-
self.compile_expression(value)?;
6620-
6621-
emit!(self, Instruction::Copy { index: 1_u32 });
6622-
match op {
6623-
ast::BoolOp::And => {
6624-
emit!(
6625-
self,
6626-
Instruction::PopJumpIfFalse {
6627-
target: after_block,
6628-
}
6629-
);
6630-
}
6631-
ast::BoolOp::Or => {
6632-
emit!(
6633-
self,
6634-
Instruction::PopJumpIfTrue {
6635-
target: after_block,
6636-
}
6637-
);
6638-
}
6632+
// Optimization: when a non-last value is a BoolOp with the opposite
6633+
// operator, redirect its short-circuit exits to skip the outer's
6634+
// redundant __bool__ test (jump threading).
6635+
if short_circuit_target.is_none()
6636+
&& let ast::Expr::BoolOp(ast::ExprBoolOp {
6637+
op: inner_op,
6638+
values: inner_values,
6639+
..
6640+
}) = value
6641+
&& inner_op != op
6642+
{
6643+
let pop_block = self.new_block();
6644+
self.compile_bool_op_with_target(inner_op, inner_values, Some(pop_block))?;
6645+
self.emit_short_circuit_test(op, after_block);
6646+
self.switch_to_block(pop_block);
6647+
emit!(self, Instruction::PopTop);
6648+
continue;
66396649
}
66406650

6651+
self.compile_expression(value)?;
6652+
self.emit_short_circuit_test(op, jump_target);
66416653
emit!(self, Instruction::PopTop);
66426654
}
66436655

@@ -6647,6 +6659,20 @@ impl Compiler {
66476659
Ok(())
66486660
}
66496661

6662+
/// Emit `Copy 1` + conditional jump for short-circuit evaluation.
6663+
/// For `And`, emits `PopJumpIfFalse`; for `Or`, emits `PopJumpIfTrue`.
6664+
fn emit_short_circuit_test(&mut self, op: &ast::BoolOp, target: BlockIdx) {
6665+
emit!(self, Instruction::Copy { index: 1_u32 });
6666+
match op {
6667+
ast::BoolOp::And => {
6668+
emit!(self, Instruction::PopJumpIfFalse { target });
6669+
}
6670+
ast::BoolOp::Or => {
6671+
emit!(self, Instruction::PopJumpIfTrue { target });
6672+
}
6673+
}
6674+
}
6675+
66506676
fn compile_dict(&mut self, items: &[ast::DictItem]) -> CompileResult<()> {
66516677
let has_unpacking = items.iter().any(|item| item.key.is_none());
66526678

@@ -9006,6 +9032,15 @@ if (True and False) or (False and True):
90069032
));
90079033
}
90089034

9035+
#[test]
9036+
fn test_nested_bool_op() {
9037+
assert_dis_snapshot!(compile_exec(
9038+
"\
9039+
x = Test() and False or False
9040+
"
9041+
));
9042+
}
9043+
90099044
#[test]
90109045
fn test_nested_double_async_with() {
90119046
assert_dis_snapshot!(compile_exec(

crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_bool_op.snap

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extra_tests/snippets/syntax_short_circuit_bool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ def __bool__(self):
3131

3232
# if ExplodingBool(False) and False and True and False:
3333
# pass
34+
35+
# Issue #3567: nested BoolOps should not call __bool__ redundantly
36+
assert (ExplodingBool(False) and False or False) == False

0 commit comments

Comments
 (0)