diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..1096b813 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -259,14 +259,16 @@ struct InlineComp { key: Option, cleared_vars: Vec, target_seen: bool, - /// pending element-ternary folds: (merge offset, condition). The + /// Pending value folds: (merge offset, left/condition, optional bool op). + /// An and/or fold combines its saved left with the right value; a ternary + /// combines its condition with both arm values. The /// linear comp walk evaluates BOTH arms of an element ternary onto /// the value stack (then under else); when the walk reaches the /// ternary's merge offset the pair is folded into a Ternary. This /// works for nested ternaries (inner merge folds first) and for /// dict-comp key AND value positions alike (folding at the append /// op cannot tell which slot a pending cond belongs to). - pending_merges: Vec<(usize, ExprRef)>, + pending_merges: Vec<(usize, ExprRef, Option)>, /// else-arm target shared by the current and-chain's ternary links tern_chain_t: Option, } @@ -31631,6 +31633,26 @@ return None; .unwrap_or(false); if let Some(comp) = &mut self.inline_comp { if self.cur_offset < comp.end { + // A 3.12+ value-preserving COPY guard inside an element + // is an and/or expression, rather than a comprehension + // filter. Its fall-through discards the copied left value. + if let Some(&index) = self.idx_of.get(&self.cur_offset) { + let mut previous = index; + while previous > 0 && matches!(self.instrs[previous - 1].op, + Op::TO_BOOL | Op::NOP | Op::NOT_TAKEN | Op::CACHE) + { previous -= 1; } + let copied = previous > 0 + && self.instrs[previous - 1].op == Op::COPY + && self.instrs[previous - 1].arg == 1; + let discard = self.instrs[index + 1..].iter() + .find(|instruction| !matches!(instruction.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .map_or(false, |instruction| instruction.op == Op::POP_TOP); + if copied && discard && target > self.cur_offset && target < comp.end { + comp.pending_merges.push((target, cond, + Some(if jump_if_true { BoolOpKind::Or } else { BoolOpKind::And }))); + return; + } + } // element ternary guard: same recognition as the // code-object builder (a forward JF between the jump // and its target skips the else arm to a merge, both @@ -31670,7 +31692,7 @@ return None; }); } } else { - comp.pending_merges.push((m_off, cj)); + comp.pending_merges.push((m_off, cj, None)); comp.tern_chain_t = Some(t_off); } return; @@ -52863,20 +52885,22 @@ impl<'a> Ctx<'a> { // arm-skip JFs all land on the append): fold them all, // innermost (LAST recorded) first so the stack pairs line up while let Some(idx) = self.inline_comp.as_ref().and_then(|c| { - c.pending_merges.iter().rposition(|(m, _)| *m == offset) + c.pending_merges.iter().rposition(|(m, _, _)| *m == offset) }) { - let (_, cond) = self + let (_, cond, bool_op) = self .inline_comp .as_mut() .map(|c| c.pending_merges.remove(idx)) .unwrap(); let else_v = self.pop_expr(); - let then_v = self.pop_expr(); - self.push(Rc::new(Expr::Ternary { - cond, - then_expr: then_v, - else_expr: else_v, - })); + if let Some(op) = bool_op { + self.push(Rc::new(Expr::BoolOp { op, values: vec![cond, else_v] })); + } else { + let then_v = self.pop_expr(); + self.push(Rc::new(Expr::Ternary { + cond, then_expr: then_v, else_expr: else_v, + })); + } } } diff --git a/tests/behavior/cases/b_comprehension_boolean_values.py b/tests/behavior/cases/b_comprehension_boolean_values.py new file mode 100644 index 00000000..bdfc5ead --- /dev/null +++ b/tests/behavior/cases/b_comprehension_boolean_values.py @@ -0,0 +1,27 @@ +# MIN_VERSION: 3.12 + + +def select(rows): + return [max(0.0, float(row.get("x") or 0.0)) for row in rows] + + +def calls(rows): + return [row.first() or row.second() for row in rows] + + +def and_calls(rows): + return [row.first() and row.second() for row in rows] + + +print(select([{}, {"x": None}, {"x": 0}, {"x": -2}, {"x": 7}])) +class Row: + def __init__(self, value): + self.value = value + def first(self): + print("first", self.value) + return self.value + def second(self): + print("second", self.value) + return 9 +print(calls([Row(0), Row(3)])) +print(and_calls([Row(0), Row(3)])) diff --git a/tests/fixtures/comprehension_boolean_values.py b/tests/fixtures/comprehension_boolean_values.py new file mode 100644 index 00000000..5ed4cdf3 --- /dev/null +++ b/tests/fixtures/comprehension_boolean_values.py @@ -0,0 +1,38 @@ +def select(rows): + return [max(0.0, float(row.get("x") or 0.0)) for row in rows] + + +def element_or(rows): + return [row[1] or 0 for row in rows] + + +def element_and(rows): + return [row[1] and 7 for row in rows] + + +def set_values(rows): + return {row[1] or 0 for row in rows} + + +def dict_values(rows): + return {row[0]: row[1] and 7 for row in rows} + + +def calls(rows): + return [row.first() or row.second() for row in rows] + + +def and_calls(rows): + return [row.first() and row.second() for row in rows] + + +def single_filter(rows): + return [row[1] for row in rows if row[0]] + + +def filtered_value(rows): + return [row[1] or 0 for row in rows if row[0]] + + +def ternary_control(rows): + return [row[1] if row[0] else 0 for row in rows] diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..b725319d 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,28 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn comprehension_boolean_values_remain_elements_not_filters() { + for version in ["3.12", "3.13", "3.14"] { + let fixture = format!("comprehension_boolean_values.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "return [max(0.0, float(row.get('x') or 0.0)) for row in rows]", + "return [row[1] or 0 for row in rows]", + "return [row[1] and 7 for row in rows]", + "return {row[1] or 0 for row in rows}", + "return {row[0]: row[1] and 7 for row in rows}", + "return [row.first() or row.second() for row in rows]", + "return [row.first() and row.second() for row in rows]", + "return [row[1] for row in rows if row[0]]", + "return [row[1] or 0 for row in rows if row[0]]", + "return [row[1] if row[0] else 0 for row in rows]", + ], + &fixture, + ); + } +} diff --git a/tests/pyc/comprehension_boolean_values.3.12.pyc b/tests/pyc/comprehension_boolean_values.3.12.pyc new file mode 100644 index 00000000..44b3d7f9 Binary files /dev/null and b/tests/pyc/comprehension_boolean_values.3.12.pyc differ diff --git a/tests/pyc/comprehension_boolean_values.3.13.pyc b/tests/pyc/comprehension_boolean_values.3.13.pyc new file mode 100644 index 00000000..b59a5800 Binary files /dev/null and b/tests/pyc/comprehension_boolean_values.3.13.pyc differ diff --git a/tests/pyc/comprehension_boolean_values.3.14.pyc b/tests/pyc/comprehension_boolean_values.3.14.pyc new file mode 100644 index 00000000..bf57c917 Binary files /dev/null and b/tests/pyc/comprehension_boolean_values.3.14.pyc differ