diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..9e0bcc7f 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -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!( diff --git a/tests/behavior/cases/b_comprehension_default_arguments.py b/tests/behavior/cases/b_comprehension_default_arguments.py new file mode 100644 index 00000000..2ff920a8 --- /dev/null +++ b/tests/behavior/cases/b_comprehension_default_arguments.py @@ -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"]}, {}])) diff --git a/tests/fixtures/comprehension_default_arguments.py b/tests/fixtures/comprehension_default_arguments.py new file mode 100644 index 00000000..254bc415 --- /dev/null +++ b/tests/fixtures/comprehension_default_arguments.py @@ -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 diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..24cb7561 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -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); + } + } +} diff --git a/tests/pyc/comprehension_default_arguments.3.11.pyc b/tests/pyc/comprehension_default_arguments.3.11.pyc new file mode 100644 index 00000000..59434d06 Binary files /dev/null and b/tests/pyc/comprehension_default_arguments.3.11.pyc differ diff --git a/tests/pyc/comprehension_default_arguments.3.12.pyc b/tests/pyc/comprehension_default_arguments.3.12.pyc new file mode 100644 index 00000000..6c90b1c9 Binary files /dev/null and b/tests/pyc/comprehension_default_arguments.3.12.pyc differ diff --git a/tests/pyc/comprehension_default_arguments.3.13.pyc b/tests/pyc/comprehension_default_arguments.3.13.pyc new file mode 100644 index 00000000..efab9741 Binary files /dev/null and b/tests/pyc/comprehension_default_arguments.3.13.pyc differ diff --git a/tests/pyc/comprehension_default_arguments.3.14.pyc b/tests/pyc/comprehension_default_arguments.3.14.pyc new file mode 100644 index 00000000..869ec159 Binary files /dev/null and b/tests/pyc/comprehension_default_arguments.3.14.pyc differ