Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 57 additions & 16 deletions src/decompiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26399,18 +26399,18 @@ impl<'a> Ctx<'a> {
))
}

/// 3.14 value-position `A1 and ... An or C`: every operand carries its
/// 3.13+ value-position `A1 and ... An or C1 or ... Cm`: each link carries its
/// own COPY/TO_BOOL value-preserving idiom; the and-links PJIF to the
/// or-continuation (this jump's target) and the LAST and-operand PJITs
/// to the merge M; [target, M) evaluates C (t_pb f1/g_val 3.14: the
/// per-jump value_merge blocks mis-nest the chain into
/// `a and (b or c)`). Read-only; returns (merged value, merge offset).
fn try_value_and_or_314(
fn try_value_and_or(
&self,
cond: &ExprRef,
target: usize,
) -> Option<(ExprRef, usize)> {
if !self.version.at_least(3, 14) {
if !self.version.at_least(3, 13) {
return None;
}
let is_pjif = |o: Op| {
Expand Down Expand Up @@ -26491,8 +26491,9 @@ impl<'a> Ctx<'a> {
if ands.len() < 2 {
return None;
}
// [target, m) is the or's second operand: a pure value region
// (its leading POP_TOP discards the arriving operand copy)
// [target, m) contains the remaining OR operands. Each non-final
// operand must use the same COPY/TO_BOOL/PJIT-to-M value protocol;
// arbitrary statement branches or other merge targets do not qualify.
let mi = *self.idx_of.get(&m)?;
if mi <= ti {
return None;
Expand All @@ -26507,14 +26508,42 @@ impl<'a> Ctx<'a> {
if cstart >= mi {
return None;
}
if !self.instrs[cstart..mi].iter().all(|x| {
x.target.is_none()
&& (is_pure_value_op(x.op)
|| matches!(x.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::POP_TOP))
}) {
let mut or_tail = Vec::new();
let mut operand_start = cstart;
let mut k = cstart;
while k < mi {
let ins = self.instrs[k];
if is_pjit(ins.op) && idiom_link(k) {
if ins.target != Some(m)
|| self.instrs[k - 2].arg != 1
|| k - 2 <= operand_start
{
return None;
}
or_tail.push(self.sim_value_region(operand_start, k - 2)?);
k += 1;
while k < mi && pad(self.instrs[k].op) {
k += 1;
}
if k >= mi || self.instrs[k].op != Op::POP_TOP {
return None;
}
k += 1;
operand_start = k;
continue;
}
if ins.target.is_some()
|| !(is_pure_value_op(ins.op)
|| matches!(ins.op, Op::COPY | Op::TO_BOOL | Op::NOP | Op::NOT_TAKEN | Op::CACHE))
{
return None;
}
k += 1;
}
if operand_start >= mi {
return None;
}
let c = self.sim_value_region(cstart, mi)?;
or_tail.push(self.sim_value_region(operand_start, mi)?);
let mut and_flat = Vec::new();
for v in ands {
flatten_boolop(v, BoolOpKind::And, &mut and_flat);
Expand All @@ -26525,7 +26554,9 @@ impl<'a> Ctx<'a> {
}) as ExprRef;
let mut or_flat = Vec::new();
flatten_boolop(and, BoolOpKind::Or, &mut or_flat);
flatten_boolop(c, BoolOpKind::Or, &mut or_flat);
for value in or_tail {
flatten_boolop(value, BoolOpKind::Or, &mut or_flat);
}
Some((
Rc::new(Expr::BoolOp {
op: BoolOpKind::Or,
Expand Down Expand Up @@ -34775,10 +34806,20 @@ if split_cond {
// (the per-jump value_merge blocks below would mis-nest it).
// !jump_if_true path only, where `c` is `cond` un-negated.
if !jump_if_true {
if let Some((merged, m)) = self.try_value_and_or_314(&c, target) {
self.push(merged);
self.skip_until = Some(m);
return;
if let Some((merged, m)) = self.try_value_and_or(&c, target) {
// COPY kept this exact operand below its consumed truth
// test. The folded expression already owns that value;
// leaving it live creates an extra call argument (or steals
// the caller's callee/NULL prefix). Consume only this saved
// operand, preserving every unrelated stack entry.
if self.stack.last().map_or(false, |value| {
matches!(value, Sv::E(saved) if expr_eq(saved, &c))
}) {
self.stack.pop();
self.push(merged);
self.skip_until = Some(m);
return;
}
}
}
// 3.12+ chained comparison: the then arm recomputes the next link
Expand Down
31 changes: 31 additions & 0 deletions tests/behavior/cases/b_boolean_call_arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# MIN_VERSION: 3.13


def select(call, before, first, second, last, after):
return call(before(), (first() and second()) or last(), after())


def alternatives(a, b, c, d, e):
return bool((a and b) or c or d or e)


def before():
print("before")
return 1
def first():
print("first")
return 0
def second():
print("second")
return 2
def last():
print("last")
return 3
def after():
print("after")
return 4
def call(*args):
return args
print(select(call, before, first, second, last, after))
print(alternatives(1, 0, 0, 0, 5))
print(alternatives(1, 2, 0, 0, 0))
35 changes: 35 additions & 0 deletions tests/fixtures/boolean_call_arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
def select(call, before, first, second, last, after):
return call(before(), (first() and second()) or last(), after())


def repeated_prefix(call, value, second, last, after):
return call(value, (value and second()) or last(), after())


def alternatives(a, b, c, d, e):
return bool((a and b) or c or d or e)


def or_prefix(a, b, c, d, e):
return bool(c or d or (a and b) or e)


def method(a, b, c, d, e):
return bool((a and a.get("available")) or b or c or d or e)


def assignment(a, b, c, d, e):
value = (a and a.get("available")) or b or c or d or e
return bool(value)


def pure_or(a, b, c, d, e):
return bool(a or b or c or d or e)


def pure_and(a, b, c, d, e):
return bool(a and b and c and d and e)


def following_branch(call, a, b, c, d, e):
return call((a and b) or c, d if e else 0)
24 changes: 24 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,27 @@ fn py314_hello() {
"py3.14 hello",
);
}

#[test]
fn boolean_value_chains_preserve_call_arguments_and_prefixes() {
for version in ["3.11", "3.12", "3.13", "3.14"] {
let fixture = format!("boolean_call_arguments.{version}.pyc");
let (src, clean) = decompile_fixture(&fixture);
assert!(clean, "{fixture}: output marked incomplete:\n{src}");
assert_contains(
&src,
&[
"return call(before(), first() and second() or last(), after())",
"return call(value, value and second() or last(), after())",
"return bool(a and b or c or d or e)",
"return bool(c or d or a and b or e)",
"return bool(a and a.get('available') or b or c or d or e)",
"value = a and a.get('available') or b or c or d or e\n return bool(value)",
"return bool(a or b or c or d or e)",
"return bool(a and b and c and d and e)",
"return call(a and b or c, d if e else 0)",
],
&fixture,
);
}
}
Binary file added tests/pyc/boolean_call_arguments.3.11.pyc
Binary file not shown.
Binary file added tests/pyc/boolean_call_arguments.3.12.pyc
Binary file not shown.
Binary file added tests/pyc/boolean_call_arguments.3.13.pyc
Binary file not shown.
Binary file added tests/pyc/boolean_call_arguments.3.14.pyc
Binary file not shown.