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
32 changes: 25 additions & 7 deletions src/decompiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions tests/behavior/cases/b_chained_comprehension_filters.py
Original file line number Diff line number Diff line change
@@ -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))
61 changes: 61 additions & 0 deletions tests/fixtures/chained_comprehension_filters.py
Original file line number Diff line number Diff line change
@@ -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]
37 changes: 37 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>().join(" ");
assert_contains(
&normalized,
&["def broken_lines(values): return [value for value in values if isinstance(value, str) if value]"],
&fixture,
);
}
}
Binary file added tests/pyc/chained_comprehension_filters.3.11.pyc
Binary file not shown.
Binary file added tests/pyc/chained_comprehension_filters.3.12.pyc
Binary file not shown.
Binary file added tests/pyc/chained_comprehension_filters.3.13.pyc
Binary file not shown.
Binary file added tests/pyc/chained_comprehension_filters.3.14.pyc
Binary file not shown.