diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..53ebf8ee 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -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| { @@ -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; @@ -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); @@ -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, @@ -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 diff --git a/tests/behavior/cases/b_boolean_call_arguments.py b/tests/behavior/cases/b_boolean_call_arguments.py new file mode 100644 index 00000000..173d1eed --- /dev/null +++ b/tests/behavior/cases/b_boolean_call_arguments.py @@ -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)) diff --git a/tests/fixtures/boolean_call_arguments.py b/tests/fixtures/boolean_call_arguments.py new file mode 100644 index 00000000..de66ce33 --- /dev/null +++ b/tests/fixtures/boolean_call_arguments.py @@ -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) diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..e3c53d12 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -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, + ); + } +} diff --git a/tests/pyc/boolean_call_arguments.3.11.pyc b/tests/pyc/boolean_call_arguments.3.11.pyc new file mode 100644 index 00000000..82e30327 Binary files /dev/null and b/tests/pyc/boolean_call_arguments.3.11.pyc differ diff --git a/tests/pyc/boolean_call_arguments.3.12.pyc b/tests/pyc/boolean_call_arguments.3.12.pyc new file mode 100644 index 00000000..4e97c86e Binary files /dev/null and b/tests/pyc/boolean_call_arguments.3.12.pyc differ diff --git a/tests/pyc/boolean_call_arguments.3.13.pyc b/tests/pyc/boolean_call_arguments.3.13.pyc new file mode 100644 index 00000000..1f0cf92c Binary files /dev/null and b/tests/pyc/boolean_call_arguments.3.13.pyc differ diff --git a/tests/pyc/boolean_call_arguments.3.14.pyc b/tests/pyc/boolean_call_arguments.3.14.pyc new file mode 100644 index 00000000..98fbb201 Binary files /dev/null and b/tests/pyc/boolean_call_arguments.3.14.pyc differ