From ad51971f6e894bc43fca16833a576ba12c916de0 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:58:56 -0400 Subject: [PATCH 1/2] Preserve each chained comprehension filter's polarity --- src/decompiler.rs | 32 +++++++-- .../fixtures/chained_comprehension_filters.py | 61 ++++++++++++++++++ tests/integration.rs | 37 +++++++++++ .../chained_comprehension_filters.3.11.pyc | Bin 0 -> 6062 bytes .../chained_comprehension_filters.3.12.pyc | Bin 0 -> 3765 bytes .../chained_comprehension_filters.3.13.pyc | Bin 0 -> 4276 bytes .../chained_comprehension_filters.3.14.pyc | Bin 0 -> 4322 bytes 7 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/chained_comprehension_filters.py create mode 100644 tests/pyc/chained_comprehension_filters.3.11.pyc create mode 100644 tests/pyc/chained_comprehension_filters.3.12.pyc create mode 100644 tests/pyc/chained_comprehension_filters.3.13.pyc create mode 100644 tests/pyc/chained_comprehension_filters.3.14.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..d38543e9 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -31680,9 +31680,12 @@ return None; comp.tern_chain_t = None; // 3.12+ inline filters come in two layouts: // - INVERTED (same-line 3.12; ALL of 3.13/3.14): the - // jump lands ON the append (target is - // LIST_APPEND/SET_ADD/MAP_ADD/YIELD after pads) and - // the fall-through is the skip back edge. The jump + // jump accepts this filter, either at the next filter + // or the append, while fall-through skips the item. + // Recognize the exact pad/back-edge span first: scanning + // from the target can encounter a later filter's skip + // before reaching the append and invert this filter. + // The jump // polarity IS the filter polarity: PJT->`if C`, // PJF->`if not C`. // - CLASSIC (3.12 line-broken filters): PJF->shared @@ -31699,7 +31702,22 @@ return None; // loop TOP, and scanning from there would wrap around // and find the append after the filter again // (_py_abc 3.12 __new__ line-broken setcomp) - let target_is_append = target > self.cur_offset + let fallthrough_skips_item = self.version.at_least(3, 12) + && target > self.cur_offset + && target < comp.end + && self.idx_of.get(&self.cur_offset).zip(self.idx_of.get(&target)) + .map_or(false, |(&ci, &ti)| { + if ti <= ci { return false; } + let mut significant = self.instrs[ci + 1..ti].iter() + .filter(|x| !matches!(x.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)); + significant.next().map_or(false, |x| { + x.is_backward + && matches!(x.op, Op::JUMP_BACKWARD | Op::JUMP_BACKWARD_NO_INTERRUPT + | Op::JUMP_ABSOLUTE | Op::JUMP) + && x.target == comp.for_iter_offsets.last().copied() + }) && significant.next().is_none() + }); + let target_accepts_item = fallthrough_skips_item || (target > self.cur_offset && self .idx_of .get(&target) @@ -31734,8 +31752,8 @@ return None; | Op::YIELD_VALUE ) }) - }); - let c = if target_is_append { + })); + let c = if target_accepts_item { if jump_if_true { cond } else { @@ -31755,7 +31773,7 @@ return None; cond }; if let Some(cur) = &mut comp.cur { - if comp_if_break && !target_is_append { + if comp_if_break && !target_accepts_item { cur.if_break = true; } cur.ifs.push(c); diff --git a/tests/fixtures/chained_comprehension_filters.py b/tests/fixtures/chained_comprehension_filters.py new file mode 100644 index 00000000..e3d73b5e --- /dev/null +++ b/tests/fixtures/chained_comprehension_filters.py @@ -0,0 +1,61 @@ +def list_positive_separate(values): + return [value for value in values if isinstance(value, str) if value] + + +def list_positive_joined(values): + return [value for value in values if isinstance(value, str) and value] + + +def list_negative_separate(values): + return [value for value in values if not isinstance(value, str) if value] + + +def list_negative_joined(values): + return [value for value in values if not isinstance(value, str) and value] + + +def set_positive_separate(values): + return {value for value in values if isinstance(value, str) if value} + + +def set_positive_joined(values): + return {value for value in values if isinstance(value, str) and value} + + +def set_negative_separate(values): + return {value for value in values if not isinstance(value, str) if value} + + +def set_negative_joined(values): + return {value for value in values if not isinstance(value, str) and value} + + +def dict_positive_separate(values): + return {value: value for value in values if isinstance(value, str) if value} + + +def dict_positive_joined(values): + return {value: value for value in values if isinstance(value, str) and value} + + +def dict_negative_separate(values): + return {value: value for value in values if not isinstance(value, str) if value} + + +def dict_negative_joined(values): + return {value: value for value in values if not isinstance(value, str) and value} + + +def methods_equal(rows, wanted): + return [row.tag for row in rows if row.value == wanted if row.ready() if row.final()] + + +def methods_unequal(rows, wanted): + return [row.tag for row in rows if row.value != wanted if not row.ready() if row.final()] + + +def broken_lines(values): + return [value + for value in values + if isinstance(value, str) + if value] diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..c4b834d3 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,40 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn chained_comprehension_filters_keep_each_polarity() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("chained_comprehension_filters.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + for (kind, open, close, element) in [ + ("list", "[", "]", "value"), + ("set", "{", "}", "value"), + ("dict", "{", "}", "value: value"), + ] { + for (polarity, negation) in [("positive", ""), ("negative", "not ")] { + for join in ["separate", "joined"] { + let expected = format!( + "def {kind}_{polarity}_{join}(values):\n return {open}{element} for value in values if {negation}isinstance(value, str) if value{close}" + ); + assert_contains(&src, &[&expected], &fixture); + } + } + } + assert_contains( + &src, + &[ + "return [row.tag for row in rows if row.value == wanted if row.ready() if row.final()]", + "return [row.tag for row in rows if row.value != wanted if not row.ready() if row.final()]", + ], + &fixture, + ); + let normalized = src.split_whitespace().collect::>().join(" "); + assert_contains( + &normalized, + &["def broken_lines(values): return [value for value in values if isinstance(value, str) if value]"], + &fixture, + ); + } +} diff --git a/tests/pyc/chained_comprehension_filters.3.11.pyc b/tests/pyc/chained_comprehension_filters.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1ed116217c932601eea7e767c47363b0542f549 GIT binary patch literal 6062 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 literal 0 HcmV?d00001 diff --git a/tests/pyc/chained_comprehension_filters.3.13.pyc b/tests/pyc/chained_comprehension_filters.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..278ea6af7915d4fcd34ea7df9d0967e6e63aa122 GIT binary patch literal 4276 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 literal 0 HcmV?d00001 From 1c897187513b6e16eaf8f18e9fceedcdd62ef8c4 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:34:35 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../cases/b_chained_comprehension_filters.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/behavior/cases/b_chained_comprehension_filters.py diff --git a/tests/behavior/cases/b_chained_comprehension_filters.py b/tests/behavior/cases/b_chained_comprehension_filters.py new file mode 100644 index 00000000..0320c450 --- /dev/null +++ b/tests/behavior/cases/b_chained_comprehension_filters.py @@ -0,0 +1,25 @@ +# MIN_VERSION: 3.12 + + +def methods_equal(rows, wanted): + return [row.tag for row in rows if row.value == wanted if row.ready() if row.final()] + + +def methods_unequal(rows, wanted): + return [row.tag for row in rows if row.value != wanted if not row.ready() if row.final()] + + +class Row: + def __init__(self, tag, value, ready): + self.tag = tag + self.value = value + self.available = ready + def ready(self): + print("ready", self.tag) + return self.available + def final(self): + print("final", self.tag) + return True +rows = [Row("a", 1, True), Row("b", 2, True), Row("c", 1, False)] +print(methods_equal(rows, 1)) +print(methods_unequal(rows, 2))