Skip to content
Merged
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
12 changes: 11 additions & 1 deletion src/decompiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52942,7 +52942,17 @@ impl<'a> Ctx<'a> {
continue
}
Op::BUILD_LIST | Op::BUILD_SET | Op::BUILD_MAP => {
return ins.arg == 0;
if ins.arg != 0 {
return false;
}
// Modern accumulators are lifted under the iterator
// by SWAP 2. An empty default argument consumed by a
// preceding CALL is not a new comprehension level.
// Preserve the distinct py2 prologue handled above.
return !self.version.at_least(3, 12)
|| self.instrs.get(back + 1).map_or(false, |next| {
next.op == Op::SWAP && next.arg == 2
});
}
o if is_pure_value_op(o)
|| matches!(
Expand Down
27 changes: 27 additions & 0 deletions tests/behavior/cases/b_comprehension_default_arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# MIN_VERSION: 3.12


def list_default(rows):
row = "saved-row"
field = "saved-field"
result = [field for row in rows for field in row.get("values", [])]
return result, row, field


def dict_default(rows):
row = "saved-row"
field = "saved-field"
result = {field: len(field) for row in rows for field in row.get("values", {})}
return result, row, field


def nested_list(rows):
row = "saved-row"
field = "saved-field"
result = [[field for field in row.get("values", [])] for row in rows]
return result, row, field


print(list_default([{}, {"values": ["a", "bb"]}, {}]))
print(dict_default([{}, {"values": {"a": 1, "bb": 2}}, {}]))
print(nested_list([{}, {"values": ["a"]}, {}]))
66 changes: 66 additions & 0 deletions tests/fixtures/comprehension_default_arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Compile with each named CPython version; the committed .pyc files are
# exercised by the existing Rust integration suite.

# Python 3.12+ inlines these comprehensions; 3.11 is the legacy control.

def list_default(rows):
row = "saved-row"
field = "saved-field"
result = [field for row in rows for field in row.get("values", [])]
return result, row, field


def set_default(rows):
row = "saved-row"
field = "saved-field"
result = {field for row in rows for field in row.get("values", {})}
return result, row, field


def dict_default(rows):
row = "saved-row"
field = "saved-field"
result = {field: len(field) for row in rows for field in row.get("values", {})}
return result, row, field


def nested_list(rows):
row = "saved-row"
field = "saved-field"
result = [[field for field in row.get("values", [])] for row in rows]
return result, row, field


def nested_set(rows):
row = "saved-row"
field = "saved-field"
result = [{field for field in row.get("values", {})} for row in rows]
return result, row, field


def nested_dict(rows):
row = "saved-row"
field = "saved-field"
result = [{field: len(field) for field in row.get("values", {})} for row in rows]
return result, row, field


def subscript(rows):
row = "saved-row"
field = "saved-field"
result = [field for row in rows for field in row["values"]]
return result, row, field


def no_default(rows):
row = "saved-row"
field = "saved-field"
result = [field for row in rows for field in row.get("values")]
return result, row, field


def tuple_default(rows):
row = "saved-row"
field = "saved-field"
result = [field for row in rows for field in row.get("values", ())]
return result, row, field
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 empty_defaults_keep_the_comprehension_level() {
// PEP 709 inlining starts in 3.12; retain the 3.11 compatibility case.
for version in ["3.11", "3.12", "3.13", "3.14"] {
let (src, clean) = decompile_fixture(&format!("comprehension_default_arguments.{version}.pyc"));
assert!(clean, "Python {version}: output marked incomplete:\n{src}");
for (name, expression) in [
("list_default", "[field for row in rows for field in row.get('values', [])]"),
("set_default", "{field for row in rows for field in row.get('values', {})}"),
("dict_default", "{field: len(field) for row in rows for field in row.get('values', {})}"),
("nested_list", "[[field for field in row.get('values', [])] for row in rows]"),
("nested_set", "[{field for field in row.get('values', {})} for row in rows]"),
("nested_dict", "[{field: len(field) for field in row.get('values', {})} for row in rows]"),
("subscript", "[field for row in rows for field in row['values']]"),
("no_default", "[field for row in rows for field in row.get('values')]"),
("tuple_default", "[field for row in rows for field in row.get('values', ())]"),
] {
let expected = format!(
"def {name}(rows):\n row = 'saved-row'\n field = 'saved-field'\n result = {expression}\n return result, row, field");
assert_contains(&src, &[&expected], version);
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.