From e5116e126ac314a9846d569d5910164f6bd63d6f Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:03:14 -0400 Subject: [PATCH 01/25] Recognize fused local stores in chained assignments --- .github/workflows/assignment_chains.yml | 24 +++++++ src/decompiler.rs | 1 + tests/test_assignment_chains.py | 89 +++++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 .github/workflows/assignment_chains.yml create mode 100644 tests/test_assignment_chains.py diff --git a/.github/workflows/assignment_chains.yml b/.github/workflows/assignment_chains.yml new file mode 100644 index 00000000..374db366 --- /dev/null +++ b/.github/workflows/assignment_chains.yml @@ -0,0 +1,24 @@ +name: Assignment Chains + +on: + pull_request: + push: + branches: [fix/fused-store-assignment-chains] + +permissions: + contents: read + +jobs: + roundtrip: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - run: cargo build --locked + - run: python tests/test_assignment_chains.py diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..a413e0ee 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -42238,6 +42238,7 @@ impl<'a> Ctx<'a> { | Some(Op::STORE_DEREF) | Some(Op::STORE_GLOBAL) | Some(Op::STORE_FAST_MAYBE_NULL) + | Some(Op::STORE_FAST_STORE_FAST) ) }; if is_store(self.instrs.get(ci + 1).map(|x| x.op)) { diff --git a/tests/test_assignment_chains.py b/tests/test_assignment_chains.py new file mode 100644 index 00000000..f37cd249 --- /dev/null +++ b/tests/test_assignment_chains.py @@ -0,0 +1,89 @@ +"""Compile, decompile and execute original fixtures with CPython 3.11–3.14.""" + +import importlib.util +import marshal +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) + + +class RoundTrip(unittest.TestCase): + def recover(self, source): + code = compile(source, "fixture.py", "exec", dont_inherit=True) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "fixture.pyc" + path.write_bytes( + importlib.util.MAGIC_NUMBER + bytes(12) + marshal.dumps(code) + ) + result = subprocess.run( + [str(BINARY), str(path)], capture_output=True, text=True, timeout=10 + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("incomplete", result.stderr.lower()) + self.assertNotIn("INCOMPLETE", result.stdout) + original, recovered = {}, {} + exec(code, original) + exec( + compile(result.stdout, "recovered.py", "exec", dont_inherit=True), recovered + ) + return original["select"], recovered["select"] + + def test_long_copy_chains_keep_one_evaluation_and_fused_stores(self): + for size in (3, 4, 5, 6): + names = [f"value{index}" for index in range(size)] + for expression in ("read()", "None"): + source = ( + "def select(read):\n " + + " = ".join(names) + + " = " + + expression + + "\n return " + + ", ".join(names) + + "\n" + ) + original, recovered = self.recover(source) + self.assertEqual(original.__code__.co_code, recovered.__code__.co_code) + self.assertEqual( + original.__code__.co_exceptiontable, + recovered.__code__.co_exceptiontable, + ) + for failure in (False, True): + outcomes = [] + for function in (original, recovered): + events = [] + value = object() + + def read(): + events.append("read") + if failure: + raise ValueError("owned chain") + return value + + try: + result = function(read) + outcome = ( + "value", + all(item is result[0] for item in result), + result[0] is value + if expression == "read()" + else result[0] is None, + ) + except ValueError as error: + outcome = (type(error).__name__, str(error)) + outcomes.append((outcome, events)) + with self.subTest( + size=size, expression=expression, failure=failure + ): + self.assertEqual(outcomes[1], outcomes[0]) + self.assertEqual( + len(outcomes[1][1]), int(expression == "read()") + ) + + +if __name__ == "__main__": + unittest.main() From cd4357e3a7556a11fae24d3d392f25a2f0b6132b Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:03:14 -0400 Subject: [PATCH 02/25] Handle expanded generator call slots on Python 3.13 and 3.14 --- .../workflows/generator_expanded_calls.yml | 24 +++++++ docs/LIMITATIONS.md | 7 ++ src/decompiler.rs | 10 +-- tests/test_generator_expanded_calls.py | 71 +++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/generator_expanded_calls.yml create mode 100644 tests/test_generator_expanded_calls.py diff --git a/.github/workflows/generator_expanded_calls.yml b/.github/workflows/generator_expanded_calls.yml new file mode 100644 index 00000000..184adc71 --- /dev/null +++ b/.github/workflows/generator_expanded_calls.yml @@ -0,0 +1,24 @@ +name: Generator Expanded Calls + +on: + pull_request: + push: + branches: [fix/generator-expanded-call-slot] + +permissions: + contents: read + +jobs: + roundtrip: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - run: cargo build --locked + - run: python tests/test_generator_expanded_calls.py diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 6c90274a..26b59d71 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -153,6 +153,13 @@ inner `BUILD_MAP 0` accumulator lands on the walker's stack, `SWAP` is not modeled, and the following `FOR_ITER` treats it as a second `for` clause (`... for c, v in {}`), so the output does not parse. +Simple expanded calls in a Python 3.13 or 3.14 generator, such as +`(factory(*row) for row in rows)` and `(factory(**dict(row)) for row in rows)`, +now respect `CALL_FUNCTION_EX`'s callable/NULL order introduced in 3.13. In +3.14 they also consume its unconditional keyword slot and treat a NULL marker +as absent keyword arguments. The nested-comprehension stack issues below +remain. + A frame-stack prototype (one frame per comprehension level, `SWAP` modeled, the accumulator placeholder replaced by Rc identity when a level ends, `pending_or_filter` flushed per level) was implemented **twice** and diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..1bbdd3e8 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -52150,15 +52150,17 @@ fn genexpr_ternary_merge( } Op::CALL_FUNCTION_EX => { // `f(*t)` / `f(*a, **k)` element: [callable(+marker), - // args-iterable, (kwargs dict when arg&1)] + // args-iterable, kwargs dict (or NULL in 3.14 when absent)]. + // Before 3.14 the kwargs slot exists only when arg&1 is set. let underflow = || Rc::new(Expr::Name("?".to_string())); - let star_kw = if inst.arg & 1 != 0 { - stack.pop() + let star_kw = if self.version.at_least(3, 14) || inst.arg & 1 != 0 { + stack.pop().filter(|value| !is_null_marker(value)) } else { None }; let star = stack.pop().unwrap_or_else(underflow); - let func = if self.version.at_least(3, 14) { + // Since 3.13 the NULL marker follows the callable. + let func = if self.version.at_least(3, 13) { if matches!(stack.last(), Some(m) if is_null_marker(m)) { stack.pop(); } diff --git a/tests/test_generator_expanded_calls.py b/tests/test_generator_expanded_calls.py new file mode 100644 index 00000000..4b357e22 --- /dev/null +++ b/tests/test_generator_expanded_calls.py @@ -0,0 +1,71 @@ +"""Round-trip expanded generator calls across Python 3.11–3.14 stack layouts.""" + +import importlib.util +import marshal +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) + + +class RoundTrip(unittest.TestCase): + def recover(self, source): + code = compile(source, "fixture.py", "exec", dont_inherit=True) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "fixture.pyc" + path.write_bytes( + importlib.util.MAGIC_NUMBER + bytes(12) + marshal.dumps(code) + ) + result = subprocess.run( + [str(BINARY), str(path)], capture_output=True, text=True, timeout=10 + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("incomplete", result.stderr.lower()) + self.assertNotIn("INCOMPLETE", result.stdout) + original, recovered = {}, {} + exec(code, original) + exec( + compile(result.stdout, "recovered.py", "exec", dont_inherit=True), recovered + ) + return original["select"], recovered["select"] + + def test_expanded_calls_keep_arguments_and_error_order(self): + cases = ( + ("factory(*row)", [(1, 2), (3,)]), + ("factory(**dict(row))", [{"a": 1}, {"b": 2}]), + ("factory(*row[0], **row[1])", [((1, 2), {"flag": 7}), ((3,), {})]), + ) + for expression, rows in cases: + source = ( + "def select(rows, factory):\n" + f" return tuple({expression} for row in rows)\n" + ) + for failure_at in (None, 1, 2): + with self.subTest(expression=expression, failure_at=failure_at): + original, recovered = self.recover(source) + outcomes = [] + for function in (original, recovered): + events = [] + + def factory(*args, **kwargs): + events.append((args, tuple(kwargs.items()))) + if len(events) == failure_at: + raise ValueError("factory failed") + return args, kwargs + + try: + value = ("value", function(rows, factory)) + except ValueError as error: + value = (type(error).__name__, str(error)) + outcomes.append((value, events)) + self.assertEqual(outcomes[1], outcomes[0]) + self.assertEqual(len(outcomes[1][1]), failure_at or 2) + self.assertEqual(recovered([], factory), ()) + + +if __name__ == "__main__": + unittest.main() From 139a8259402571f2e9ff0891663cf5635b4455cf Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:03:14 -0400 Subject: [PATCH 03/25] Preserve separate evaluations when grouping assignment stores --- .github/workflows/assignment_identity.yml | 24 ++++++++ src/decompiler.rs | 5 +- tests/test_assignment_identity.py | 74 +++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/assignment_identity.yml create mode 100644 tests/test_assignment_identity.py diff --git a/.github/workflows/assignment_identity.yml b/.github/workflows/assignment_identity.yml new file mode 100644 index 00000000..3c5d0f13 --- /dev/null +++ b/.github/workflows/assignment_identity.yml @@ -0,0 +1,24 @@ +name: Assignment Identity + +on: + pull_request: + push: + branches: [fix/assignment-evaluation-identity] + +permissions: + contents: read + +jobs: + roundtrip: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - run: cargo build --locked + - run: python tests/test_assignment_identity.py diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..4164c23d 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -21174,7 +21174,10 @@ let reopen = self let group = std::mem::take(&mut self.pending_stores); self.last_store_line = None; self.pending_unpack_wrap.clear(); - let all_same = group.windows(2).all(|w| expr_eq(&w[0].1, &w[1].1)); + // COPY/DUP preserves the identity of one evaluated stack value. + // Structurally equal expressions can be independent evaluations: + // `left, right = read(), read()` must still call read twice. + let all_same = group.windows(2).all(|w| Rc::ptr_eq(&w[0].1, &w[1].1)); if all_same { let value = group[0].1.clone(); let targets: Vec = group.iter().map(|(t, _)| t.clone()).collect(); diff --git a/tests/test_assignment_identity.py b/tests/test_assignment_identity.py new file mode 100644 index 00000000..b6d29878 --- /dev/null +++ b/tests/test_assignment_identity.py @@ -0,0 +1,74 @@ +"""Compile, decompile and execute original fixtures with CPython 3.11–3.14.""" + +import importlib.util +import marshal +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) + + +class RoundTrip(unittest.TestCase): + def recover(self, source): + code = compile(source, "fixture.py", "exec", dont_inherit=True) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "fixture.pyc" + path.write_bytes( + importlib.util.MAGIC_NUMBER + bytes(12) + marshal.dumps(code) + ) + result = subprocess.run( + [str(BINARY), str(path)], capture_output=True, text=True, timeout=10 + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("incomplete", result.stderr.lower()) + self.assertNotIn("INCOMPLETE", result.stdout) + original, recovered = {}, {} + exec(code, original) + exec( + compile(result.stdout, "recovered.py", "exec", dont_inherit=True), recovered + ) + return original["select"], recovered["select"] + + def test_independent_equal_assignments_keep_both_evaluations_and_true_chains(self): + sources = ( + "def select(read):\n left, right = read(), read()\n return left, right\n", + "def select(read):\n left = right = read()\n return left, right\n", + "def select(read):\n left, right = None, None\n return left, right\n", + "def select(read):\n right = left = None\n return left, right\n", + ) + for index, source in enumerate(sources): + original, recovered = self.recover(source) + self.assertEqual(original.__code__.co_code, recovered.__code__.co_code) + self.assertEqual( + original.__code__.co_exceptiontable, + recovered.__code__.co_exceptiontable, + ) + for failure_at in (None, 1, 2): + outcomes = [] + for function in (original, recovered): + events = [] + + def read(): + events.append(len(events) + 1) + if events[-1] == failure_at: + raise ValueError("owned evaluation") + return events[-1] + + try: + result = ("value", function(read)) + except ValueError as error: + result = (type(error).__name__, str(error)) + outcomes.append((result, events)) + with self.subTest(source=index, failure_at=failure_at): + self.assertEqual(outcomes[1], outcomes[0]) + if index == 0: + counter = iter((7, 8)) + self.assertEqual(recovered(lambda: next(counter)), (7, 8)) + + +if __name__ == "__main__": + unittest.main() From 075050a5aa1320310b01e4aa2c7d187a0dca2e29 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:03:14 -0400 Subject: [PATCH 04/25] Retain all operands when extending comparison chains --- src/decompiler.rs | 14 ++++-- src/decompiler_comparison_tests.rs | 68 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 src/decompiler_comparison_tests.rs diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..fcd01de7 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -41885,7 +41885,8 @@ fn merge_chain_compare(cond: &ExprRef, v: &ExprRef) -> Option { if o2.len() < 2 || ops2.len() + 1 != o2.len() { return None; } - let shared = match (&*operands[1], &*o2[0]) { + let last = operands.last()?; + let shared = match (&**last, &*o2[0]) { (Expr::Name(a), Expr::Name(b)) => a == b, (Expr::Const(c1), Expr::Const(c2)) => Rc::ptr_eq(c1, c2) || const_value_eq(c1, c2), (Expr::Attribute { value: v1, attr: a1 }, Expr::Attribute { value: v2, attr: a2 }) => { @@ -41893,14 +41894,17 @@ fn merge_chain_compare(cond: &ExprRef, v: &ExprRef) -> Option { } // calls/subscripts/etc: deep structural equality (side-effecting // operands must not be duplicated across the merged chain) - _ => expr_eq(&operands[1], &o2[0]), + _ => expr_eq(last, &o2[0]), }; if !shared { return None; } let mut merged_ops = ops.clone(); merged_ops.extend(ops2.iter().copied()); - let mut merged_operands = vec![operands[0].clone()]; + // Extend the complete accumulated chain, sharing only its last operand. + // Using operands[1] or retaining just operands[0] loses earlier links + // once the left side already contains more than two operands. + let mut merged_operands = operands[..operands.len() - 1].to_vec(); merged_operands.extend(o2.iter().cloned()); Some(Rc::new(Expr::Compare { operands: merged_operands, @@ -53096,3 +53100,7 @@ impl<'a> Ctx<'a> { } } } + +#[cfg(test)] +#[path = "decompiler_comparison_tests.rs"] +mod comparison_tests; diff --git a/src/decompiler_comparison_tests.rs b/src/decompiler_comparison_tests.rs new file mode 100644 index 00000000..88a72f68 --- /dev/null +++ b/src/decompiler_comparison_tests.rs @@ -0,0 +1,68 @@ +use super::*; + +fn name(value: &str) -> ExprRef { + Rc::new(Expr::Name(value.into())) +} + +fn call(value: &str) -> ExprRef { + Rc::new(Expr::Call { + func: name(value), args: Vec::new(), keywords: Vec::new(), + star_args: None, star_kwargs: None, + }) +} + +fn compare(operands: Vec, ops: Vec) -> ExprRef { + Rc::new(Expr::Compare { operands, ops }) +} + +#[test] +fn extend_accumulated_chain_keeps_all_operands_and_operators() { + // 0 <= first() <= second(), extended by second() < third() <= 15. + let zero = Rc::new(Expr::Const(Rc::new(PyObject::Int(0)))); + let limit = Rc::new(Expr::Const(Rc::new(PyObject::Int(15)))); + let first = call("first"); + let second = call("second"); + let third = call("third"); + let left = compare(vec![zero.clone(), first.clone(), second.clone()], + vec![CmpOp::LtE, CmpOp::LtE]); + let right = compare(vec![call("second"), third.clone(), limit.clone()], + vec![CmpOp::Lt, CmpOp::LtE]); + let merged = merge_chain_compare(&left, &right).expect("shared last operand"); + let expected = compare(vec![zero, first, second, third, limit], + vec![CmpOp::LtE, CmpOp::LtE, CmpOp::Lt, CmpOp::LtE]); + assert!(expr_eq(&merged, &expected), "{merged:?}"); +} + +#[test] +fn matching_an_earlier_operand_does_not_join_unrelated_chains() { + let left = compare(vec![name("a"), name("b"), name("c")], + vec![CmpOp::Lt, CmpOp::LtE]); + let right = compare(vec![name("b"), name("d")], vec![CmpOp::Lt]); + assert!(merge_chain_compare(&left, &right).is_none()); +} + +#[test] +fn retain_every_link_when_repeatedly_extending_a_chain() { + let mut result = compare(vec![name("a"), name("b")], vec![CmpOp::Lt]); + let names = ["a", "b", "c", "d", "e", "f"]; + for end in 2..names.len() { + let right = compare(vec![name(names[end - 1]), name(names[end])], + vec![CmpOp::Lt]); + result = merge_chain_compare(&result, &right).unwrap(); + let expected = compare(names[..=end].iter().map(|n| name(n)).collect(), + vec![CmpOp::Lt; end]); + assert!(expr_eq(&result, &expected)); + } +} + +#[test] +fn unchanged_two_operand_chain_and_its_terminal_arm_are_preserved() { + let left = compare(vec![name("a"), name("b")], vec![CmpOp::Lt]); + let terminal = merge_chain_compare(&left, &left).unwrap(); + assert!(Rc::ptr_eq(&terminal, &left)); + let right = compare(vec![name("b"), name("c")], vec![CmpOp::LtE]); + let merged = merge_chain_compare(&left, &right).unwrap(); + let expected = compare(vec![name("a"), name("b"), name("c")], + vec![CmpOp::Lt, CmpOp::LtE]); + assert!(expr_eq(&merged, &expected)); +} From cdcd61fb4ab40738718f1b67a71a9e534e27788a Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:03:14 -0400 Subject: [PATCH 05/25] Support COPY and SWAP in bounded expression simulation --- src/decompiler.rs | 18 +++ src/decompiler_stack_tests.rs | 207 ++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/decompiler_stack_tests.rs diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..a4be131f 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -29054,6 +29054,20 @@ return None; Op::PUSH_NULL => { st.push(self.name_expr("\u{0}null")); } + Op::COPY => { + if arg == 0 || arg > st.len() { + return None; + } + st.push(st[st.len() - arg].clone()); + } + Op::SWAP => { + if arg == 0 || arg > st.len() { + return None; + } + let top = st.len() - 1; + let other = st.len() - arg; + st.swap(top, other); + } Op::LOAD_ATTR | Op::LOAD_METHOD => { let v = pop1(&mut st); let idx = if ins.op == Op::LOAD_ATTR && self.version.at_least(3, 12) { @@ -53096,3 +53110,7 @@ impl<'a> Ctx<'a> { } } } + +#[cfg(test)] +#[path = "decompiler_stack_tests.rs"] +mod stack_tests; diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs new file mode 100644 index 00000000..b7483fb3 --- /dev/null +++ b/src/decompiler_stack_tests.rs @@ -0,0 +1,207 @@ +use super::*; +use crate::pyc::PycModule; + +fn context(code: &CodeObject, version: PythonVersion, instrs: Vec) -> Ctx<'_> { + Ctx { + code, + table: table_for(version).unwrap(), + version, + instrs, + idx_of: Default::default(), + targets: Default::default(), + stack: Vec::new(), + blocks: vec![Block::new(BlockType::Main, 0, usize::MAX)], + clean: true, + unpack_frames: Vec::new(), + comp_target_store: false, + await_mode: false, + pending_async_with_ctx: None, + skip_end_send: false, + with_exit_await_drop: false, + with_handler_starts: std::collections::HashSet::new(), + async_for_guard: None, + legacy_nest: Vec::new(), + legacy_nest_depth: 0, + py26_temps: HashMap::new(), + completed_comp_regions: Vec::new(), + last_py26_temp: None, + as_cleanup_wrappers: Vec::new(), + used_exc_info: false, + region_collect_only: false, + skipped_chain_landing: None, + pending_gen_code: None, + recent_code_const: None, + pending_match_class: None, + last_kw_names: Vec::new(), + globals: Vec::new(), + nonlocals: Vec::new(), + exc_entries: Vec::new(), + raw_exc_entries: Vec::new(), + try_ctxs: HashMap::new(), + handler_zone: None, + chain_heads: Default::default(), + with_regions: Default::default(), + with_region_handlers: Default::default(), + active_try: None, + active_try_stack: Vec::new(), + last_try_body_end: None, + pending_try_tail: None, + pending_post_chain_stmt: None, + tail_pair_at: None, + pending_try_ctx: None, + emitting_try: std::collections::HashSet::new(), + fin_copy_guard: None, + pending_nested_finally: None, + nested_inner_handlers: Vec::new(), + legacy_try: None, + legacy_handler: None, + legacy_handler_end: None, + legacy_handler_name_store: false, + legacy_handler_cleanup: false, + fused_guard_pad_at: std::collections::HashSet::new(), + swallowed_cleanup_del: None, + in_handler_prelude: false, + pending_then: Vec::new(), + pending_handlers: Vec::new(), + pending_try_body: Vec::new(), + pending_orelse_mark: None, + return_offsets: std::collections::HashSet::new(), + legacy_body_redirect: None, + pending_loop_close_at_chain: None, + chain_absorb_body: None, + sunk_return_fold_at: None, + sunk_loop_return_lift: false, + sunk_pair_drop: None, + sunk_while_lift: None, + nested_tail_defer: false, + pending_try_handlers: Vec::new(), + pending_loop: Vec::new(), + pending_with: Vec::new(), + pending_try_orelse: Vec::new(), + with_exits: 0, + pending_print: Vec::new(), + pending_print_dest: None, + unpack_targets: FrameTargets(Vec::new()), + unpack_target_lines: Vec::new(), + pending_unpack_wrap: Vec::new(), + awaiting_for_target: false, + pending_if_stmts: Vec::new(), + cur_offset: 0, + cur_next: 0, + prev_op: None, + prev_op_at_exec: None, + skip_until: None, + import_star_pop_pending: false, + cond_jump_redirect: std::collections::HashMap::new(), + dup_copy_redirect: HashMap::new(), + dup_copy_skip: Vec::new(), + broken_loop_top: None, + py2_else_pop_at: None, + pending_star_kw_call: false, + threaded_break_at: HashSet::new(), + closed_loop_tops: Vec::new(), + while_true_loops: Vec::new(), + or_rot_revals: Vec::new(), + bare_try_parse: false, + handler_sunk_tail: Vec::new(), + dup_tail_ternary_skip: None, + pending_as_cleanup: None, + star_tail: Vec::new(), + star_tail_end: None, + region_result_expr: None, + held_cleanup_store: None, + class_scope: Vec::new(), + inline_comp: None, + inline_comp_stack: Vec::new(), + pending_restore_vars: Vec::new(), + comp_restore_vars: Vec::new(), + cur_line: None, + pending_stores: Vec::new(), + last_store_line: None, + group_start: 0, + last_flush_offset: 0, + flushing: false, + pending_aug: None, + pending_decorators: Vec::new(), + pending_class_decorators: Vec::new(), + pending_py2_class: None, + import_names: Vec::new(), + dotted_as_just_emitted: false, + import_module: None, + } +} + +fn instruction(op: Op, arg: u32) -> Instruction { + Instruction { + offset: 0, start: 0, size: 2, opcode: 0, op, arg, + has_arg: true, target: None, is_backward: false, line: None, + } +} + +fn fixture() -> PycModule { + PycModule::from_bytes(include_bytes!("../tests/pyc/comprehensions.3.11.pyc")).unwrap() +} + +#[test] +fn copy_keeps_one_evaluation_and_swap_reorders_without_losing_operands() { + let module = fixture(); + // The intermediate stack in `lower <= read() <= upper` is reordered + // and copied so the middle expression is evaluated exactly once. + let lower = Rc::new(Expr::Name("lower".into())); + let middle = Rc::new(Expr::Call { + func: Rc::new(Expr::Name("read".into())), args: Vec::new(), + keywords: Vec::new(), star_args: None, star_kwargs: None, + }); + for minor in 11..=14 { + let version = PythonVersion { minor, ..module.version() }; + let ctx = context(&module.code, version, + vec![instruction(Op::SWAP, 2), instruction(Op::COPY, 2)]); + let result = ctx.sim_stack_region(0, 2, vec![lower.clone(), middle.clone()]) + .expect("COPY and SWAP are available from Python 3.11"); + assert_eq!(result.len(), 3); + assert!(Rc::ptr_eq(&result[0], &middle)); + assert!(Rc::ptr_eq(&result[1], &lower)); + assert!(Rc::ptr_eq(&result[2], &middle)); + } +} + +#[test] +fn copy_and_swap_respect_deeper_stack_entries_and_region_boundaries() { + let module = fixture(); + let values: Vec<_> = ["callee", "argument", "value"].iter() + .map(|name| Rc::new(Expr::Name((*name).into()))).collect(); + let ctx = context(&module.code, module.version(), vec![ + instruction(Op::SWAP, 3), instruction(Op::COPY, 3), + instruction(Op::STORE_FAST, 0), + ]); + let result = ctx.sim_stack_region(0, 2, values.clone()).unwrap(); + assert_eq!(result.len(), 4); + for (index, source) in [2, 1, 0, 2].iter().enumerate() { + assert!(Rc::ptr_eq(&result[index], &values[*source])); + } + // Simulation must still reject a region that includes a statement. + assert!(ctx.sim_stack_region(0, 3, values).is_none()); +} + +#[test] +fn copy_and_swap_reject_zero_depth_and_stack_underflow() { + let module = fixture(); + for op in [Op::COPY, Op::SWAP] { + for (depth, length) in [(0, 0), (0, 1), (1, 0), (2, 1), (u32::MAX, 1)] { + let ctx = context(&module.code, module.version(), vec![instruction(op, depth)]); + let values = vec![Rc::new(Expr::Name("value".into())); length]; + assert!(ctx.sim_stack_region(0, 1, values).is_none(), "{op:?} {depth}"); + } + } +} + +#[test] +fn top_copy_and_identity_swap_are_valid() { + let module = fixture(); + let value = Rc::new(Expr::Name("value".into())); + let ctx = context(&module.code, module.version(), + vec![instruction(Op::COPY, 1), instruction(Op::SWAP, 1)]); + let result = ctx.sim_stack_region(0, 2, vec![value.clone()]).unwrap(); + assert_eq!(result.len(), 2); + assert!(result.iter().all(|entry| Rc::ptr_eq(entry, &value))); +} From a109342d87a8d70d7456a2d866bf3b180667aec9 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:19:59 -0400 Subject: [PATCH 06/25] Run version-gated Python regressions in the shared CI matrix --- .github/workflows/assignment_chains.yml | 24 ------------------------ .github/workflows/ci.yml | 17 +++++++++++++++++ tests/test_assignment_chains.py | 5 +++++ 3 files changed, 22 insertions(+), 24 deletions(-) delete mode 100644 .github/workflows/assignment_chains.yml diff --git a/.github/workflows/assignment_chains.yml b/.github/workflows/assignment_chains.yml deleted file mode 100644 index 374db366..00000000 --- a/.github/workflows/assignment_chains.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Assignment Chains - -on: - pull_request: - push: - branches: [fix/fused-store-assignment-chains] - -permissions: - contents: read - -jobs: - roundtrip: - runs-on: ubuntu-latest - strategy: - matrix: - python: ["3.11", "3.12", "3.13", "3.14"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - - run: cargo build --locked - - run: python tests/test_assignment_chains.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96ff9e11..240ed91a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,23 @@ jobs: "$das" tests/corpus/3.12.14/base64.pyc | head -3 cat tests/corpus/3.12.14/base64.pyc | "$bin" - | head -3 + python-regressions: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14", "3.15"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: cargo build --locked + - run: python -m unittest discover -s tests -p 'test_*.py' -v + clippy: # informational: the codebase carries a style-warning long tail # (see docs/LIMITATIONS.md); this job never blocks diff --git a/tests/test_assignment_chains.py b/tests/test_assignment_chains.py index f37cd249..c20585c3 100644 --- a/tests/test_assignment_chains.py +++ b/tests/test_assignment_chains.py @@ -5,6 +5,7 @@ import os from pathlib import Path import subprocess +import sys import tempfile import unittest @@ -12,6 +13,10 @@ BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) +@unittest.skipUnless( + sys.implementation.name == "cpython" and (3, 11) <= sys.version_info[:2] < (3, 15), + "requires CPython 3.11–3.14; 3.15 LOAD_COMMON_CONSTANT 7 (None) is not handled", +) class RoundTrip(unittest.TestCase): def recover(self, source): code = compile(source, "fixture.py", "exec", dont_inherit=True) From c0d890d4af2f0d4e9f0fbcae7b97b6c2da27e7ab Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:19:59 -0400 Subject: [PATCH 07/25] Run version-gated Python regressions in the shared CI matrix --- .github/workflows/ci.yml | 17 +++++++++++++ .../workflows/generator_expanded_calls.yml | 24 ------------------- tests/test_generator_expanded_calls.py | 5 ++++ 3 files changed, 22 insertions(+), 24 deletions(-) delete mode 100644 .github/workflows/generator_expanded_calls.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96ff9e11..240ed91a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,23 @@ jobs: "$das" tests/corpus/3.12.14/base64.pyc | head -3 cat tests/corpus/3.12.14/base64.pyc | "$bin" - | head -3 + python-regressions: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14", "3.15"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: cargo build --locked + - run: python -m unittest discover -s tests -p 'test_*.py' -v + clippy: # informational: the codebase carries a style-warning long tail # (see docs/LIMITATIONS.md); this job never blocks diff --git a/.github/workflows/generator_expanded_calls.yml b/.github/workflows/generator_expanded_calls.yml deleted file mode 100644 index 184adc71..00000000 --- a/.github/workflows/generator_expanded_calls.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Generator Expanded Calls - -on: - pull_request: - push: - branches: [fix/generator-expanded-call-slot] - -permissions: - contents: read - -jobs: - roundtrip: - runs-on: ubuntu-latest - strategy: - matrix: - python: ["3.11", "3.12", "3.13", "3.14"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - - run: cargo build --locked - - run: python tests/test_generator_expanded_calls.py diff --git a/tests/test_generator_expanded_calls.py b/tests/test_generator_expanded_calls.py index 4b357e22..1adcf2d8 100644 --- a/tests/test_generator_expanded_calls.py +++ b/tests/test_generator_expanded_calls.py @@ -5,6 +5,7 @@ import os from pathlib import Path import subprocess +import sys import tempfile import unittest @@ -12,6 +13,10 @@ BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) +@unittest.skipUnless( + sys.implementation.name == "cpython" and (3, 11) <= sys.version_info[:2] < (3, 15), + "requires CPython 3.11–3.14; 3.15 LOAD_COMMON_CONSTANT 7 (None) is not handled", +) class RoundTrip(unittest.TestCase): def recover(self, source): code = compile(source, "fixture.py", "exec", dont_inherit=True) From 0d295953fc4b20dba443d0924d9a253b51f0b261 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:19:59 -0400 Subject: [PATCH 08/25] Run version-gated Python regressions in the shared CI matrix --- .github/workflows/assignment_identity.yml | 24 ----------------------- .github/workflows/ci.yml | 17 ++++++++++++++++ tests/test_assignment_identity.py | 5 +++++ 3 files changed, 22 insertions(+), 24 deletions(-) delete mode 100644 .github/workflows/assignment_identity.yml diff --git a/.github/workflows/assignment_identity.yml b/.github/workflows/assignment_identity.yml deleted file mode 100644 index 3c5d0f13..00000000 --- a/.github/workflows/assignment_identity.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Assignment Identity - -on: - pull_request: - push: - branches: [fix/assignment-evaluation-identity] - -permissions: - contents: read - -jobs: - roundtrip: - runs-on: ubuntu-latest - strategy: - matrix: - python: ["3.11", "3.12", "3.13", "3.14"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - - run: cargo build --locked - - run: python tests/test_assignment_identity.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96ff9e11..240ed91a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,23 @@ jobs: "$das" tests/corpus/3.12.14/base64.pyc | head -3 cat tests/corpus/3.12.14/base64.pyc | "$bin" - | head -3 + python-regressions: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14", "3.15"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: cargo build --locked + - run: python -m unittest discover -s tests -p 'test_*.py' -v + clippy: # informational: the codebase carries a style-warning long tail # (see docs/LIMITATIONS.md); this job never blocks diff --git a/tests/test_assignment_identity.py b/tests/test_assignment_identity.py index b6d29878..b345425e 100644 --- a/tests/test_assignment_identity.py +++ b/tests/test_assignment_identity.py @@ -5,6 +5,7 @@ import os from pathlib import Path import subprocess +import sys import tempfile import unittest @@ -12,6 +13,10 @@ BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) +@unittest.skipUnless( + sys.implementation.name == "cpython" and (3, 11) <= sys.version_info[:2] < (3, 15), + "requires CPython 3.11–3.14; 3.15 LOAD_COMMON_CONSTANT 7 (None) is not handled", +) class RoundTrip(unittest.TestCase): def recover(self, source): code = compile(source, "fixture.py", "exec", dont_inherit=True) From f3324397df44c9c74b9c2ecada4f3031ecdcd222 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:37:19 -0400 Subject: [PATCH 09/25] Use committed Python fixtures in the existing integration suite --- .github/workflows/ci.yml | 17 ----- tests/fixtures/assignment_chains.py | 40 ++++++++++++ tests/integration.rs | 29 +++++++++ tests/pyc/assignment_chains.3.11.pyc | Bin 0 -> 1848 bytes tests/pyc/assignment_chains.3.12.pyc | Bin 0 -> 1716 bytes tests/pyc/assignment_chains.3.13.pyc | Bin 0 -> 1620 bytes tests/pyc/assignment_chains.3.14.pyc | Bin 0 -> 1622 bytes tests/test_assignment_chains.py | 94 --------------------------- 8 files changed, 69 insertions(+), 111 deletions(-) create mode 100644 tests/fixtures/assignment_chains.py create mode 100644 tests/pyc/assignment_chains.3.11.pyc create mode 100644 tests/pyc/assignment_chains.3.12.pyc create mode 100644 tests/pyc/assignment_chains.3.13.pyc create mode 100644 tests/pyc/assignment_chains.3.14.pyc delete mode 100644 tests/test_assignment_chains.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 240ed91a..96ff9e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,23 +38,6 @@ jobs: "$das" tests/corpus/3.12.14/base64.pyc | head -3 cat tests/corpus/3.12.14/base64.pyc | "$bin" - | head -3 - python-regressions: - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.12", "3.13", "3.14", "3.15"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - run: cargo build --locked - - run: python -m unittest discover -s tests -p 'test_*.py' -v - clippy: # informational: the codebase carries a style-warning long tail # (see docs/LIMITATIONS.md); this job never blocks diff --git a/tests/fixtures/assignment_chains.py b/tests/fixtures/assignment_chains.py new file mode 100644 index 00000000..c2a067e0 --- /dev/null +++ b/tests/fixtures/assignment_chains.py @@ -0,0 +1,40 @@ +# COPY-backed chains; CPython 3.13+ can fuse the final two local stores. + +def three(read): + a = b = c = read() + return a, b, c + + +def four(read): + a = b = c = d = read() + return a, b, c, d + + +def five(read): + a = b = c = d = e = read() + return a, b, c, d, e + + +def six(read): + a = b = c = d = e = f = read() + return a, b, c, d, e, f + + +def three_none(): + a = b = c = None + return a, b, c + + +def four_none(): + a = b = c = d = None + return a, b, c, d + + +def five_none(): + a = b = c = d = e = None + return a, b, c, d, e + + +def six_none(): + a = b = c = d = e = f = None + return a, b, c, d, e, f diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..13ba7a43 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -151,6 +151,35 @@ fn py312_hello() { ); } +#[test] +fn assignment_chains() { + // 3.13 introduces fused stores; older fixtures check COPY-only chains. + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("assignment_chains.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + for (name, names) in [ + ("three", &['a', 'b', 'c'][..]), + ("four", &['a', 'b', 'c', 'd'][..]), + ("five", &['a', 'b', 'c', 'd', 'e'][..]), + ("six", &['a', 'b', 'c', 'd', 'e', 'f'][..]), + ] { + let names: Vec = names.iter().map(char::to_string).collect(); + let targets = names.join(" = "); + let values = names.join(", "); + assert_contains( + &src, + &[ + &format!("def {name}(read):\n {targets} = read()\n return {values}"), + &format!("def {name}_none():\n {targets} = None\n return {values}"), + ], + &fixture, + ); + } + assert_eq!(src.matches("read()").count(), 4, "{fixture}:\n{src}"); + } +} + #[test] fn py313_hello() { let (src, clean) = decompile_fixture("hello.3.13.pyc"); diff --git a/tests/pyc/assignment_chains.3.11.pyc b/tests/pyc/assignment_chains.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65e1370941dfacd93a0f789730fa5cca31483c54 GIT binary patch literal 1848 zcmbVN%}*0S9G&UTZa)wMQn5m9mTHXE1Y$HAi5hDNH@R^0LMpqJCQ@Xk7)-Va2NUb%R&o9<0SL)o9%qOxAWfi>)>F(n-y;QhM=t7ut-DqJDS> zu+0+8V_#5xM~~V%Z*ufR)9|>L@QkW4&-VD-9xI8qkZ#3k^Gch)Y4b%Tu);<=y4i45 zLrqm{>y0((s;P=nwYT+Ux87J-T&mRSYV!TZw%J&ct}8QGL_6U^JH#y3rGQS=3MNrpqyQ?6rifH{qAKQ@q;--uD4Eiv+9F62 zqyTO1D0)nyY?-DfqPnss0z9*7c1f_#lOPANahtn}lxZEYklF`V4&y1!Q1<~4lFPTm zjoJ~3V3jQ4?#It^`C_zcUBLbqV-Ip_!m9UWHzl6O`cV?x>P0ms**!E zqs^;|?kkek(PpUHy3FF~6?F9k0BL!lC)w#RZwo7+*YvKng|ER zLopsn#13&Id0Ju)1!WKts3RjDBO#QKQ_eDaOr>Zs=th3Z=oBo}SL&`D#ShoeRUV+D z5Q;2u&Gv-gEGdB%np6SBX;L6Y(bdEd-fq*29%|Ywaf~B^X)=7oj|3Anbqk*w~iimdm+dPl0HbHMl(^Yso;yi|PCeYPAfHrt2^29k$ zobz=5`To|%nz+T*rp%v59rP)lF?|HI+sam70dfR;o{4A*HjY%#Tfa?H9ssnLLt&E& zn^f4O!X_8n7rA(@l+s^Y{q@qx*BQ~alXSO|UY4Y@^fytvOq<{+^>lf~S$pR`mpAY$ s{eP&>fCGbbeqbf|$N`HXli;H?>;Q(>Y1jb_^EB)LhX1`b-bEAr1N9wgmjD0& literal 0 HcmV?d00001 diff --git a/tests/pyc/assignment_chains.3.12.pyc b/tests/pyc/assignment_chains.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6d6bf6876c0ee5fd17b65aa365b0ce880e2c6c7 GIT binary patch literal 1716 zcmaKty>HV%7{>47v-43}^&^zfn#c_Z5fJSF2!sGB5@PJqjim~;O(W5i-leFO94P~X zM@t=AsS{(xpTgp?WQ36gHCrd1_nf*-nsCxRI$eBszvukC&X38-g25R3^KJRX)UVmM zzcEf5?~i&KT%nYIz|AQKqAvBpmL>Itigu8>~c?ecn;f9UcJCGfa|c7C_zsg_#t z8+)x?>8X{vQjL$zEw9;H-`K1-nriv;e%EYmO3#x8ETW@T8PZ`tr;7(xEap2kNXWz5 z>BNn&9?gW$JF7iB!3MbLM9uvGgVEezH`*d$4}>q)n4}ewmR3z^(%2%&lH?#=?kbul zD~`<5jPSR2rJTZ$9#HooKrNOI-o;{pP!~c@Qj2(>H>!m%V{ubX;L!nO+F2tnV=x{W z*rIJ-U~c?$A5EX)H@`|>;Zd|0s7Y=LI^*lj?WQN^@xv{2WkWjbbh;ReBC!^SVtyS;V1JbFYB|8>2G26s!)BCr_2O4YAmInGE zrbeLyp9(U?M@bIG*cxj<&SB4MMVUi{6Bl&r^hr@qAaG$jv2e(RLoOV0;ZTa5|D|}f zn$zD>{gu+SuIod`PSU%TbXk(l(%(LvG97~R+^el^clVR`M&7}%^uM4#5q}NN`I(jB Vb7zGNpC`FZVv}5%>(QLv?jMFpOp5>j literal 0 HcmV?d00001 diff --git a/tests/pyc/assignment_chains.3.13.pyc b/tests/pyc/assignment_chains.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e90e0a0b9b7f30fe845125e4c4d1c58f253f1d4d GIT binary patch literal 1620 zcmaKs%TC)s6ozNwv2&qR5K`&}H*y2LM7^*{RjDeKSkNV1SV7sL01iYd24+GB_?#F`E@j&8GmQyKQo-3E*Ok4yZ!z1>JQ`3FT_dX zqi9%1aK=1VW|Dav%DoIU;~7xH6HwuqP}8%ZmY0QQcXPG;_VpgL75#|%umU+_4OV4u z5m8&=0bk*Vi_K!gs2fY{g0Ek&nuvt-TRRbNMSLsb+e%=nixw?*ebrUV?aoQ}So&(Y zrBr7(*!P3(=JsBz6R0KdUo%(^)SYfXZc z)7UKHzM>p7af00W>dRS-(hT(+0%Y4c$C*|LxkAX5K1GB0bO%TMl~CBX$!Tv*{tChW z$O8fT4We&u)U7f4?49&-2D?VQt>^~g6h=81s&gVs7%if!*AS%OxaTxLx9QiQ6Stx|3YFUd!qCPQNL-O100ml_cGqq+^lv5B*YU_rzD6`>=no obNt2sC?Df1z1zAnejA+gD=Wk2t_m4GPx6SwA-OR&q%pnSUr+HJ6951J literal 0 HcmV?d00001 diff --git a/tests/pyc/assignment_chains.3.14.pyc b/tests/pyc/assignment_chains.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76a7ae4b9ff521a6d8934330558e9c4c705706e3 GIT binary patch literal 1622 zcmZ{kPfrs;7{+J1v)exifl^_GwOQJTHmDp*G!Zr7fJY8{Xpf}A7Me&YGez;%V~oeuNZGHWxBlX5RXn(z!#!xN;!Gf7R)BDK6EY0^s-St)&R9|gg%71~{%q{Av~ zjcrj6-!^aYZ7z$|Y{e)WMRv{iu31ThLi)ACkk>-~IphaQ(A0I;b;DxYS8cUfZ(Ov` zrLQ(?N;Qs}Cw{ZNe{fuDG}Y_Y_t0z~OW&6nT7+%Y3Q33kn$KNYfmrJ7kaT%}=235< z`?j-pM}OG}FLu+s1-)>Rx0N8~t!lQqRTf1iF;`+y$&@Dg79Y zKRuUnj=GqjToMAz*_WRJu>!!AE{E(yl6k5pPvdiCpKvG0*vDhii(h{yJNPr9z>RlfI?(o--D;f zk`Xt~b{$hTNQm2U1L1<(1-A=s7p`=txpJ?R((j#qQ*@PTpKB{g+?>R*NbI3sD(#-= uElRyVIXyi8;(wG+=`Y@GT^WB2&iSpC;ET7J1Yd$YfjE$%vA-D8;r;l{J= literal 0 HcmV?d00001 diff --git a/tests/test_assignment_chains.py b/tests/test_assignment_chains.py deleted file mode 100644 index c20585c3..00000000 --- a/tests/test_assignment_chains.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Compile, decompile and execute original fixtures with CPython 3.11–3.14.""" - -import importlib.util -import marshal -import os -from pathlib import Path -import subprocess -import sys -import tempfile -import unittest - -ROOT = Path(__file__).resolve().parents[1] -BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) - - -@unittest.skipUnless( - sys.implementation.name == "cpython" and (3, 11) <= sys.version_info[:2] < (3, 15), - "requires CPython 3.11–3.14; 3.15 LOAD_COMMON_CONSTANT 7 (None) is not handled", -) -class RoundTrip(unittest.TestCase): - def recover(self, source): - code = compile(source, "fixture.py", "exec", dont_inherit=True) - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "fixture.pyc" - path.write_bytes( - importlib.util.MAGIC_NUMBER + bytes(12) + marshal.dumps(code) - ) - result = subprocess.run( - [str(BINARY), str(path)], capture_output=True, text=True, timeout=10 - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertNotIn("incomplete", result.stderr.lower()) - self.assertNotIn("INCOMPLETE", result.stdout) - original, recovered = {}, {} - exec(code, original) - exec( - compile(result.stdout, "recovered.py", "exec", dont_inherit=True), recovered - ) - return original["select"], recovered["select"] - - def test_long_copy_chains_keep_one_evaluation_and_fused_stores(self): - for size in (3, 4, 5, 6): - names = [f"value{index}" for index in range(size)] - for expression in ("read()", "None"): - source = ( - "def select(read):\n " - + " = ".join(names) - + " = " - + expression - + "\n return " - + ", ".join(names) - + "\n" - ) - original, recovered = self.recover(source) - self.assertEqual(original.__code__.co_code, recovered.__code__.co_code) - self.assertEqual( - original.__code__.co_exceptiontable, - recovered.__code__.co_exceptiontable, - ) - for failure in (False, True): - outcomes = [] - for function in (original, recovered): - events = [] - value = object() - - def read(): - events.append("read") - if failure: - raise ValueError("owned chain") - return value - - try: - result = function(read) - outcome = ( - "value", - all(item is result[0] for item in result), - result[0] is value - if expression == "read()" - else result[0] is None, - ) - except ValueError as error: - outcome = (type(error).__name__, str(error)) - outcomes.append((outcome, events)) - with self.subTest( - size=size, expression=expression, failure=failure - ): - self.assertEqual(outcomes[1], outcomes[0]) - self.assertEqual( - len(outcomes[1][1]), int(expression == "read()") - ) - - -if __name__ == "__main__": - unittest.main() From 7d80cf5081044077478aab024876b74ea4bf05c7 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:37:19 -0400 Subject: [PATCH 10/25] Use committed Python fixtures in the existing integration suite --- .github/workflows/ci.yml | 17 ----- tests/fixtures/generator_expanded_calls.py | 12 ++++ tests/integration.rs | 19 +++++ tests/pyc/generator_expanded_calls.3.11.pyc | Bin 0 -> 1421 bytes tests/pyc/generator_expanded_calls.3.12.pyc | Bin 0 -> 1222 bytes tests/pyc/generator_expanded_calls.3.13.pyc | Bin 0 -> 1246 bytes tests/pyc/generator_expanded_calls.3.14.pyc | Bin 0 -> 1483 bytes tests/test_generator_expanded_calls.py | 76 -------------------- 8 files changed, 31 insertions(+), 93 deletions(-) create mode 100644 tests/fixtures/generator_expanded_calls.py create mode 100644 tests/pyc/generator_expanded_calls.3.11.pyc create mode 100644 tests/pyc/generator_expanded_calls.3.12.pyc create mode 100644 tests/pyc/generator_expanded_calls.3.13.pyc create mode 100644 tests/pyc/generator_expanded_calls.3.14.pyc delete mode 100644 tests/test_generator_expanded_calls.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 240ed91a..96ff9e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,23 +38,6 @@ jobs: "$das" tests/corpus/3.12.14/base64.pyc | head -3 cat tests/corpus/3.12.14/base64.pyc | "$bin" - | head -3 - python-regressions: - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.12", "3.13", "3.14", "3.15"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - run: cargo build --locked - - run: python -m unittest discover -s tests -p 'test_*.py' -v - clippy: # informational: the codebase carries a style-warning long tail # (see docs/LIMITATIONS.md); this job never blocks diff --git a/tests/fixtures/generator_expanded_calls.py b/tests/fixtures/generator_expanded_calls.py new file mode 100644 index 00000000..7f20ebc1 --- /dev/null +++ b/tests/fixtures/generator_expanded_calls.py @@ -0,0 +1,12 @@ +# CALL_FUNCTION_EX changes stack layout in CPython 3.13 and 3.14. + +def positional(rows, factory): + return tuple(factory(*row) for row in rows) + + +def keywords(rows, factory): + return tuple(factory(**dict(row)) for row in rows) + + +def both(rows, factory): + return tuple(factory(*row[0], **row[1]) for row in rows) diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..49816199 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -158,6 +158,25 @@ fn py313_hello() { assert_contains(&src, &["class Greeter:", "while a < n:", "a, b = b, a + b"], "py3.13 hello"); } +#[test] +fn generator_expanded_calls() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("generator_expanded_calls.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "def positional(rows, factory):\n return tuple((factory(*row) for row in rows))", + "def keywords(rows, factory):\n return tuple((factory(*(), **{**dict(row)}) for row in rows))", + "def both(rows, factory):\n return tuple((factory(*row[0], **{**row[1]}) for row in rows))", + ], + &fixture, + ); + assert_eq!(src.matches("factory(").count(), 3, "{fixture}:\n{src}"); + } +} + #[test] fn py314_hello() { // 3.14 is brand new; accept incomplete-marker output as long as the diff --git a/tests/pyc/generator_expanded_calls.3.11.pyc b/tests/pyc/generator_expanded_calls.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb86771dd7cfd8e5fc05e41c452d4f1ada06f452 GIT binary patch literal 1421 zcmcgs-%Aux6h3EW%ylPY1i>F6NS3e#JA$!cOcE8n1WOPE6=B=nv6ePxxijJqHt->+ zAVh-Dz`zGF7hn7z`UghXr-7b&(Kl&d#HY@gac4)d9=+W8_MAEAe)rD#?%5xmooN*S zo_+cGXYt9yq2FZ31COjmFB!`q;5|GAfrKa&LPe;LAfpwPcm;q)HWM~(5`4gA@R}^- zf5b_o^{Fb{gAMYx=!wP#Wo^Zo#H!?ru}djTJ4#RE62Vt|07wU_!t;)IsK6(sii_|V zmJ#7|S^zD_d<|-9pkzk^RXQ_4`(&Z$IdabBvkeX|9A5Qo*K^Iu(yTWvZP$EdPua3S zDHHZ=r7$IIF;Og(%Wk$Zci3TZbhaYL=Laf|Tk=ZIRH2-;$_}Nt)H6_sKw607s5tnuFh3uf8Al=$m8uWrc z!yP6#Sb$%;k)+(bOx})R)(Lg9uA9DQN}W>ALMJ|WsM8%X5<2xoIteTtyAV}8F8n$! z;*pO1p^i#ka#1rFXrff~WE(k64a3UFG`*R>_{N@_ainlh5hbkU7-hwxL%h0hhra+{2VEWOS45zt4GXq2EzUE1;!qVx%cnoHf|#69lMNS=Sm((}^sUY{Zf zm(t}wh*%QKH>_XVhi>agE+ZSC$RuHPqvr@;xy`9DoG-d6Gl@U=gtM~h?IBf1^K zsuSu~UAKJA3Q6-BHEgj^(2ym(g3*TDjE5=fts|z(_PFdPm&@Q<1p6u??w>;(*@qZX Qh&R|h0&;J$dlEwSzeZ{@DF6Tf literal 0 HcmV?d00001 diff --git a/tests/pyc/generator_expanded_calls.3.12.pyc b/tests/pyc/generator_expanded_calls.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52f5631926ba98d0525d008a4964618667e8c92f GIT binary patch literal 1222 zcmcJOL2DCH5XWbBw>E7o-IQw7&{C)sV?^ zDoUZCrlN-k9ukU>@8Kme?Pc-i#onZ$Px78uB5Y_P*O#qY5KO#lX-`?0jYr^-0K5)iVD0^ins#rv4jY3!WtCd ztGa-3Ojn_*`buKTSDCxu_s(Xrp34ec^fWfMym!H~xyOy!+@d$nY;HWX=WLdtkQsY1 zpP934YbKj<9G=J*_WDerF6LQkX(aFRoR@RwGETyDTngc-L`N3A@)Klsf5GlDeAkWg z^lEf%{Ow3XH~w%TKN}pvD*8R%eBQCEimy?3oP|W9z&CcE(Zj&+|91}?qyy15A0|U( ze}wg>ZdA^0=teVa)HS0;=WsOiMPriwb#l7s(>M+WNmmuPd3BJx|U=*sYe4>+$-T;C8rDSPyn}x z7Mvz62jrpfy08utRz)tmxH~fNIuMvA1iP59jKU z$?}Z~e!jICnQZ808||ssVQ4jR2NS&t^#5TsYu<)#Hp6CJGbPq^T$S4`ryz6Wen?I3 l!(PaI;#%{LonmLGns}kC_*+H9?PG{j+abiuEi%_+)?Wm1{~-VX literal 0 HcmV?d00001 diff --git a/tests/pyc/generator_expanded_calls.3.13.pyc b/tests/pyc/generator_expanded_calls.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a96386734b92865db1902b084f7d29ca66da482 GIT binary patch literal 1246 zcmcJO&rcIk5XWbB7b#TCR*D$0q}pOqNxDsdB_?ZQq9I=FrS@bZ)^@>a?KbZ%0ym6* z0f#0SJ>ZRUlKvZBYHBdqcrl*18G6Bsao+2;wh0{Bq|@1*A8+>aoo$bejj90f_UD&K zZu|S+U*z{L^u6{Ka%zx;Yrr6jWHGCdRaVu6R#1BR02=wCT{}tUDc0agw+G~K9!2fT zKn})X!7p0y<8eqRnL;m_qN3Ft{bZ{&#vn=~h|-6n02O?!+@zK!yn)(K6dqj!2vWu> zEUGJ*z)T(Ls;4AVp30ncZ+InNa2>YE#ca{&mUr8=xyy}}(uTXnY;LUD6`SQLW!c`S z<|{?JxLnAW%RE`#^hPWZ-Kes(7pXeD46xVhsm65 z!H=N+O*iWCmTt6yMpHA|^c>Dh1u>dbVTqgp6=)|qDr7M=AahsJ2WX~G6$WK8niA5;OM{PF3d$H}wuc4Wb^SkPYJBOi} zM%YI_eFGV-2e3=xoPNxK?g>mR_^=4~LpBe>E!}Jd&8B9u5Jk@mvN%~nzDLdgB@g;*13u9xIU^_Ts@)Z_@NoZ@$@V)+o)r3we37?{nt&W~T@G`(yy9 zel0G1IeGozC+Yr!p4PZbN(HhI1sk$R60;IXDJv@w59B330w6%TNZ42-p<@M>eB*6= zAT2;43NQlWkboS{qR43ma}b6(&o52U8ELT#pclp<*{#4t2hTaLKk5PVtzJbIiiKJF zHD*X?c#W!6;1Q%l0401ViF^~(Xc;P9l5!E_n5jWccBI6NBQtB$=~>nD zw#Bx%*jDUn#kp=9+~(?Pv21TJgR4)Bb%W{TvSO4=`g*}AtmJjm~?fVc{BQH@;4Xe=NE^thEA8g zQ8JC1X05Rao+8Acw8lxQjk#8e%a~K4&Sz#Hq&72^5NXn~AAu zF?LUq_0bz=_^GkAX|V!71DbpGqvYj5OYIHrD%;U-k*RlcRr&3$!^qVBpa*Yk0@(#} zWdov4JFsA@0;BCfk1+p>i_+G^DS;~`a5b0pP>R+pLhk1dm)L`g>pm%MxQ1J}a<~jP zu8`4tRJxMO*#4#s938L=4!sL19f_zWy-w7kk1c!c3}S)+S4eoov_1Q?EUwv({yugP znXca3#gFO3$n?JLaZ_TQxCKNFMtcJKu>8-A64%3;V5SLX&1F5L(b{E}iOcTobeX$J r+-Y=e&3hcwo>_$r)0k%?lwQm>A0+WhM#ST8#F^tD;>{BhcigBy_OlWS literal 0 HcmV?d00001 diff --git a/tests/test_generator_expanded_calls.py b/tests/test_generator_expanded_calls.py deleted file mode 100644 index 1adcf2d8..00000000 --- a/tests/test_generator_expanded_calls.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Round-trip expanded generator calls across Python 3.11–3.14 stack layouts.""" - -import importlib.util -import marshal -import os -from pathlib import Path -import subprocess -import sys -import tempfile -import unittest - -ROOT = Path(__file__).resolve().parents[1] -BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) - - -@unittest.skipUnless( - sys.implementation.name == "cpython" and (3, 11) <= sys.version_info[:2] < (3, 15), - "requires CPython 3.11–3.14; 3.15 LOAD_COMMON_CONSTANT 7 (None) is not handled", -) -class RoundTrip(unittest.TestCase): - def recover(self, source): - code = compile(source, "fixture.py", "exec", dont_inherit=True) - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "fixture.pyc" - path.write_bytes( - importlib.util.MAGIC_NUMBER + bytes(12) + marshal.dumps(code) - ) - result = subprocess.run( - [str(BINARY), str(path)], capture_output=True, text=True, timeout=10 - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertNotIn("incomplete", result.stderr.lower()) - self.assertNotIn("INCOMPLETE", result.stdout) - original, recovered = {}, {} - exec(code, original) - exec( - compile(result.stdout, "recovered.py", "exec", dont_inherit=True), recovered - ) - return original["select"], recovered["select"] - - def test_expanded_calls_keep_arguments_and_error_order(self): - cases = ( - ("factory(*row)", [(1, 2), (3,)]), - ("factory(**dict(row))", [{"a": 1}, {"b": 2}]), - ("factory(*row[0], **row[1])", [((1, 2), {"flag": 7}), ((3,), {})]), - ) - for expression, rows in cases: - source = ( - "def select(rows, factory):\n" - f" return tuple({expression} for row in rows)\n" - ) - for failure_at in (None, 1, 2): - with self.subTest(expression=expression, failure_at=failure_at): - original, recovered = self.recover(source) - outcomes = [] - for function in (original, recovered): - events = [] - - def factory(*args, **kwargs): - events.append((args, tuple(kwargs.items()))) - if len(events) == failure_at: - raise ValueError("factory failed") - return args, kwargs - - try: - value = ("value", function(rows, factory)) - except ValueError as error: - value = (type(error).__name__, str(error)) - outcomes.append((value, events)) - self.assertEqual(outcomes[1], outcomes[0]) - self.assertEqual(len(outcomes[1][1]), failure_at or 2) - self.assertEqual(recovered([], factory), ()) - - -if __name__ == "__main__": - unittest.main() From f2d2dc44a9b89ba59b1311715930d8f12340b796 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 14:37:19 -0400 Subject: [PATCH 11/25] Use committed Python fixtures in the existing integration suite --- .github/workflows/ci.yml | 17 ------ tests/fixtures/assignment_identity.py | 20 +++++++ tests/integration.rs | 20 +++++++ tests/pyc/assignment_identity.3.11.pyc | Bin 0 -> 839 bytes tests/pyc/assignment_identity.3.12.pyc | Bin 0 -> 757 bytes tests/pyc/assignment_identity.3.13.pyc | Bin 0 -> 725 bytes tests/pyc/assignment_identity.3.14.pyc | Bin 0 -> 723 bytes tests/test_assignment_identity.py | 79 ------------------------- 8 files changed, 40 insertions(+), 96 deletions(-) create mode 100644 tests/fixtures/assignment_identity.py create mode 100644 tests/pyc/assignment_identity.3.11.pyc create mode 100644 tests/pyc/assignment_identity.3.12.pyc create mode 100644 tests/pyc/assignment_identity.3.13.pyc create mode 100644 tests/pyc/assignment_identity.3.14.pyc delete mode 100644 tests/test_assignment_identity.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 240ed91a..96ff9e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,23 +38,6 @@ jobs: "$das" tests/corpus/3.12.14/base64.pyc | head -3 cat tests/corpus/3.12.14/base64.pyc | "$bin" - | head -3 - python-regressions: - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.12", "3.13", "3.14", "3.15"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - run: cargo build --locked - - run: python -m unittest discover -s tests -p 'test_*.py' -v - clippy: # informational: the codebase carries a style-warning long tail # (see docs/LIMITATIONS.md); this job never blocks diff --git a/tests/fixtures/assignment_identity.py b/tests/fixtures/assignment_identity.py new file mode 100644 index 00000000..d121ab02 --- /dev/null +++ b/tests/fixtures/assignment_identity.py @@ -0,0 +1,20 @@ +# Separate evaluations must not become one COPY-backed chained assignment. + +def separate(read): + left, right = read(), read() + return left, right + + +def shared(read): + left = right = read() + return left, right + + +def separate_none(): + left, right = None, None + return left, right + + +def shared_none(): + right = left = None + return left, right diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..ece00650 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -106,6 +106,26 @@ fn py310_basics() { assert_contains(&src, &["while i < 10:", "v = 1 < x < 100"], "py3.10 basics"); } +#[test] +fn assignment_identity() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("assignment_identity.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "def separate(read):\n left, right = read(), read()\n return left, right", + "def shared(read):\n left = right = read()\n return left, right", + "def separate_none():\n left, right = None, None\n return left, right", + "def shared_none():\n right = left = None\n return left, right", + ], + &fixture, + ); + assert_eq!(src.matches("read()").count(), 3, "{fixture}:\n{src}"); + } +} + #[test] fn py311_comprehensions() { let (src, clean) = decompile_fixture("comprehensions.3.11.pyc"); diff --git a/tests/pyc/assignment_identity.3.11.pyc b/tests/pyc/assignment_identity.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3f73a5a9594a53bef37070321dbf34ea6a2d154 GIT binary patch literal 839 zcma))!D|yi6vp4oZfZ;j#cC4@MOH}yxwIg)7sXS0v5-q|B9x^&Nf+9*%xs~BEJW~- zV+w^-@FrNI=l&l#mc9B91aj-i@6DQRMG=Rck2kaL&HmmudsQkG93r~@)@g3M9ew$M zI2+-Mj+u@>cVZcf zp6JQ8+@Y~(jcM#8u2z1KxCg2oB{>~-_9MfmdY*S%i}x+#x`0A1HqRh^x>+g@tLOEL`epU9`lVOx5R;BPIRide*Q-h$>_?Xu(`b8h_*i)6v<`&tFMghig;u0p`{xH8;0 z-@MqIxGVkU;N=~TE+SzMGaDJs6Kvsb#Cz<-1P`ev*u#cw;b~cfhSU#`rtP=5((_V~ z(%hci?on6iMZ97rvkYO6RzGY5r1oDROQ_+W1U@hZKp5lB2SD)mEs41gQB2)P5gs9h zPcmhQwPfN}`dfp0cVo?HHC%h8)nqSZr|#q&cg*#^WljgfwC!2eP;mU5HU;Rxpr-6)aK; z7IIu|5iI>PY*OmB{sUs*3M;>F_BQ6a8+O0E`SIR-<~ws=sgxWd^6&n1`!_pJPl%^Y z{Ea572(Brh3)-bXKt&*-GH{?y-)$B4^R=A91^J&eA=e~nK(~mv&Bn2e#Xt;XM{d(d zv_~{@QdcWKNZqdLL}@{X2Zxd2N}lJvHX>yrv(^dEqEoHRnr}>a&^uDSXfF()VbuR} z(oc&jUgF<|ktW6vU= ziCDxkht|AGyDSArPO9h8N$1R=uT`Kc=oEQ&83KyM`qj6wyOL}SzrI80GA8%&PumpY z6Nccu_=%C2;1~@AJJynId~LgZOX?3;rtLnQ>3JDMB{yTQcidBY0aMIlmLLf|R9DZp z0Mhs`kScQc5x@h+00?8edjJUD-;$VFh;rschVT(fxRR+sfUsB}yY*yqICCS`jCRYl w7ij&n;F?o2x7jUwz}MpUqvPPTtA1!M%8$_A_Ol~|c$t%;{!+?i!jY~02GV1bUjP6A literal 0 HcmV?d00001 diff --git a/tests/pyc/assignment_identity.3.13.pyc b/tests/pyc/assignment_identity.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d19476a3ad706ce200bbac06a897cbf1043322f GIT binary patch literal 725 zcma)4yH3ME5WKS;2#G?#JVFqKK?ErcG(f|nf(j{3gp>|hj5G*@vPC#(cY2YT(qy9_#{^qQ-+*J_`uc5vi1fnIR6>R*Lft$df47pTHGnEjzgJWkd3Wszwc8EvBs+ zG>Q<4>vNXlB;UGZu!Tp#>Pz&PUhvC*?^Ofda#&md>sSb_da zo=-YYsF~5m)(oJYD)p^WcYmn)TGfglDV|jY5S_i-XiSJ7r{=Vg_!S6(3W|k_m9`dFm{Jb2>VYOQyMpPiOmCwI_W2Js z{)3cy*!c(4_ye4|^J2utg?-HK?96*_?v{oNIU4|0A7Abt-ZwL^6!(_+j=D1xgy4Y- z20TG1Jd4osY(m?U^H9kgrLVb&Tk`JC5rl96v#?GPul^WktInD=4{fpEhPE9^RDLaz zXX@BQ84ONNjpi=pQYl(AN*ld$99)@8RC>kNIykAHtGYQ1YJ`DlE?+dGtX3C3`bJ@n z3b|NM5`^&V6tBy-3wPz#;!}R4wbhZuE+ycXY>O^p*;8>%ztk;(P9hfh1xkhtL8=gk z=nxI)Qy*0g3v|eGc7lM|$KC6Vj+_p6Q*z8x;)r%{^~i_mil^-Xj#dTF=ZCUl;UJ|k z?#&gYQbJrvjUUz-n&8bALVOHZ!u`ml J@X1l^>kIF$fm;9o literal 0 HcmV?d00001 diff --git a/tests/test_assignment_identity.py b/tests/test_assignment_identity.py deleted file mode 100644 index b345425e..00000000 --- a/tests/test_assignment_identity.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Compile, decompile and execute original fixtures with CPython 3.11–3.14.""" - -import importlib.util -import marshal -import os -from pathlib import Path -import subprocess -import sys -import tempfile -import unittest - -ROOT = Path(__file__).resolve().parents[1] -BINARY = Path(os.environ.get("PYCDC_BINARY", ROOT / "target/debug/pycdc")) - - -@unittest.skipUnless( - sys.implementation.name == "cpython" and (3, 11) <= sys.version_info[:2] < (3, 15), - "requires CPython 3.11–3.14; 3.15 LOAD_COMMON_CONSTANT 7 (None) is not handled", -) -class RoundTrip(unittest.TestCase): - def recover(self, source): - code = compile(source, "fixture.py", "exec", dont_inherit=True) - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "fixture.pyc" - path.write_bytes( - importlib.util.MAGIC_NUMBER + bytes(12) + marshal.dumps(code) - ) - result = subprocess.run( - [str(BINARY), str(path)], capture_output=True, text=True, timeout=10 - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertNotIn("incomplete", result.stderr.lower()) - self.assertNotIn("INCOMPLETE", result.stdout) - original, recovered = {}, {} - exec(code, original) - exec( - compile(result.stdout, "recovered.py", "exec", dont_inherit=True), recovered - ) - return original["select"], recovered["select"] - - def test_independent_equal_assignments_keep_both_evaluations_and_true_chains(self): - sources = ( - "def select(read):\n left, right = read(), read()\n return left, right\n", - "def select(read):\n left = right = read()\n return left, right\n", - "def select(read):\n left, right = None, None\n return left, right\n", - "def select(read):\n right = left = None\n return left, right\n", - ) - for index, source in enumerate(sources): - original, recovered = self.recover(source) - self.assertEqual(original.__code__.co_code, recovered.__code__.co_code) - self.assertEqual( - original.__code__.co_exceptiontable, - recovered.__code__.co_exceptiontable, - ) - for failure_at in (None, 1, 2): - outcomes = [] - for function in (original, recovered): - events = [] - - def read(): - events.append(len(events) + 1) - if events[-1] == failure_at: - raise ValueError("owned evaluation") - return events[-1] - - try: - result = ("value", function(read)) - except ValueError as error: - result = (type(error).__name__, str(error)) - outcomes.append((result, events)) - with self.subTest(source=index, failure_at=failure_at): - self.assertEqual(outcomes[1], outcomes[0]) - if index == 0: - counter = iter((7, 8)) - self.assertEqual(recovered(lambda: next(counter)), (7, 8)) - - -if __name__ == "__main__": - unittest.main() From fc5d7404b7505e5807731353c2ff49b667d47ba4 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:05:42 -0400 Subject: [PATCH 12/25] Exclude frame StopIteration cleanup from exception chains --- src/decompiler.rs | 11 +++++++ .../fixtures/sequential_generator_handlers.py | 29 ++++++++++++++++++ tests/integration.rs | 16 ++++++++++ .../sequential_generator_handlers.3.11.pyc | Bin 0 -> 1167 bytes .../sequential_generator_handlers.3.12.pyc | Bin 0 -> 1015 bytes .../sequential_generator_handlers.3.13.pyc | Bin 0 -> 1021 bytes .../sequential_generator_handlers.3.14.pyc | Bin 0 -> 1015 bytes 7 files changed, 56 insertions(+) create mode 100644 tests/fixtures/sequential_generator_handlers.py create mode 100644 tests/pyc/sequential_generator_handlers.3.11.pyc create mode 100644 tests/pyc/sequential_generator_handlers.3.12.pyc create mode 100644 tests/pyc/sequential_generator_handlers.3.13.pyc create mode 100644 tests/pyc/sequential_generator_handlers.3.14.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..7e1768f5 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -8108,6 +8108,17 @@ impl<'a> Ctx<'a> { loop { let mut grew = false; for e in &self.raw_exc_entries { + // The generator/coroutine StopIteration handler spans the + // whole frame, not this source-level except/finally chain. + // Following its redirects would swallow later main-flow + // statements into the current handler region. + let frame_handler = self.idx_of.get(&e.target) + .and_then(|&index| self.instrs.get(index)) + .map_or(false, |instruction| instruction.op == Op::CALL_INTRINSIC_1 + && instruction.arg == 3); + if frame_handler { + continue; + } let seed = e.start == from || (e.target == from && e.start < from); let inside = e.start >= from diff --git a/tests/fixtures/sequential_generator_handlers.py b/tests/fixtures/sequential_generator_handlers.py new file mode 100644 index 00000000..ffc228ba --- /dev/null +++ b/tests/fixtures/sequential_generator_handlers.py @@ -0,0 +1,29 @@ +# Compile with each named CPython version; the committed .pyc files are +# exercised by the existing Rust integration suite. + +# The frame-wide StopIteration intrinsic is emitted starting in Python 3.12. + +def synchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 + + +async def asynchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..7e54b7fc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,19 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn sequential_generator_handlers_keep_the_shared_mainline() { + // CALL_INTRINSIC_1 3 starts in 3.12; 3.11 has no frame-wide handler. + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture( + &format!("sequential_generator_handlers.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + let body = " try:\n yield work('first')\n except ValueError:\n yield 2\n yield 3\n try:\n yield work('last')\n except KeyError:\n yield 5\n yield 6"; + for definition in ["def synchronous(work):", "async def asynchronous(work):"] { + let expected = format!("{definition}\n{body}"); + assert_contains(&src, &[&expected], version); + } + assert!(!src.contains("else:"), "Python {version}: spurious handler else:\n{src}"); + } +} diff --git a/tests/pyc/sequential_generator_handlers.3.11.pyc b/tests/pyc/sequential_generator_handlers.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..008ddc55f84d67317e746ae8e2c877d886280670 GIT binary patch literal 1167 zcmah|F=*RB6#b`@Y)7_aW5-Lf6x<9MT5m0dwveH82xJLONtYlZr7@MI=1#^jbjXmw zfefV`vX_uz0;P28P-w;uh0rMo3WYAsrsBn9>OaYnW9-^G>GAdN|0mtOckeY#r2z2p zyY}ttk0*bBW1L*1A9afu9Dxb@@C;0XNK9!J7U^bOAqr)DD)@9y5SD>p#|%jD3Oi1) z?lh@ggCjz~nXQ0_mGN~GSek$-MYHSR$W7R#>(C@7jdFc9`M+#Z%mvVAQ^v5RqMQk< z&`GcO%2thW?^5($ik@!WDOW_T{!*ft!~wiv(tvgK!a%h}v!uH!a1DJxd^;VFfZ|-Q*ZZYr_a2LJ zjqJ{WeQ2L&=8_x9N&}uSnoK4L`8@vUzE*@cdWF6t6?&@OgU|YsO3!8p;)pXSZgBS# zHcfKIQ;4*2$|K&$2yevm!|sT)NGvaat<`rOYk?Z5(W)o`lP%mOdsRsiEjffO;kQ@p<3KgL&&9)70vpTaO33!k; zx7I(PwkM%@5P}p@`~%){_Lf|9uU_oQ(DYEM2j3){C8k-Jx8HvA&D)**&9_8CX84b8|Jp? zHX)E_TSVT&S!HN8)|;(jGaozkuJgd-xRUld%){nn87gcZ$^=t%D5XZJi~ZCtE0cPB zE>M>WT=;u(#h%=Cptv?ZtMWO9rA`C=w(XXjTbz45&}N-#SJg?Nls$e=$eo-$O8buR zMS7`F@k`thY2iF5IYqx)s(c!l5@r4z;n0Fm6F5j;PhNu=7SGZU7N`lLuDPJb; zV)+G!kDyJNn)+dlJ#pYx50YS>-Up(lzFW!7c$=a6 zbD)DcK4ktCH}M5)B&(5{eh!`}D4y$A;FXHvbz+*mqbS~!Y4(Ak_>{=1tU;yMP>9s3 zw2wh%`1d+H*m*SMU8Rp1)kx4$BGykFgRM!}{T$+sKn~%SIdvd+X%IM4JWc@!Ntq@3U2*~46KteMDVv(A00iLQ^i@Y?h!%hsv8!N*; zQWT#_hJ9fuzFJv>H8klpbRxAO?YSx1JwjZ5b{#rcp}VUi?Z&E?FS*VPKZTw=szUA- RPY5CVrcTVx1QtGy-ao0S+jal| literal 0 HcmV?d00001 diff --git a/tests/pyc/sequential_generator_handlers.3.13.pyc b/tests/pyc/sequential_generator_handlers.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c083b3a4ece3b16cb8a473828e6973ded609066 GIT binary patch literal 1021 zcmZva%WD%s9LImNvyZUZJg8M6J{nsQqhb!lrr-l}kRF6wf?hn7rQJkhY&O&+3-jCWe&@Th`kp53>jchCRj;D%F&f*Uc_V^b2x$*eUZidakv4qn7=T{-Gvs( zLYd-S$wssC$gCR9R^SBO!j-~H15*RQccpNR87Q5!;WC{8n_x~F%PGTLF3J_CP1;(? zSL`a|URS)+6`lLCW8$+So>eSu$=7e1PTji3n9Kawb*s@=ZQ{%KT(-_dH(5XLS={4! zyL#WNGmGcBb*FCCysGIeSFD=FOwVP@Yi4cLu^2ypyWz)qqqeff+?reGOl+w57#BTu z;Q{m{s-%A;r=AtIH+Eu2dMdGxg=j6(qsCH*~_dZhIzsy}-= z7}F!>Uw#$W-y}tyH1%0{BBOY&FThI$#p~oeeJ`Q-K<4R3isEy!sL_rrypBqQ)`UIA znd0UJZ*b)~WHQdVAcwnOrhPSm^)R7Ei80=>kav_s;-@2$BgDg`vM+fqN($yoKxVoq z2`nK#mO|+p=6)tqXdMuVNlbPJOt$`GvO8eX9AWZ!5|e>BWRfP$_$hd*6w{;?zX-2X z6mL_D^pk|*3t6OJDT;5YVuE&b;dKlmbVAsroYX%=O#F2l0;~`=6}z3|j7rsF4$_0tLxf(Gl%?6)#cft+HrU)s zji>qr;!zPiNIZ(-2YAT8(0b6l_T(*?90Wc1PBt5Ax(oB}!!yskJ3GHQkxr=;0A{sA z-*!%0*LP449@1BxQ55S?f`i~eiC`rqDF@SZDNKl93`Y(}=Ovb7ki`|_umDGK-I;!F z4c18($`s#nHk>6xvq~^4zHkI?Vz1y)pppPMjD5>e&}Ra!P1NU)yk1>eVQ$r}ai(Dp1qZ)@YyFy0+EgHY5ifmO;P>ns-W#3 zGXL_+*nfi*Q>3Bh;BgGaGxa>YP*A+q3-q0Y;yo$Q4-~~Gy{OVwTzIV!BD5;(J!B~! zZ|5y`jwV}(1t(pmJrzNBkWj)z1mTg8aF|BM$3t>MjKi$FE1M6qf-(~qn~5W5n1ui$ zOQQ5Ob2phZS{aduBD38mbN@dw+kG+*hR8gMBGWeqWHO|YoPsBEF+-jwFTtxgiZ{j# z{V1XMOlIg8isGwLG-xX&yp~RcHiSJtE%mMv6T5Cg2P*_WmAJU-R%(tl%l4rst}NHF S;8h{ym!=Y}JBEcB==}p1pU1-h literal 0 HcmV?d00001 From f317eef3a213025660f9b5f39699cc769451517b Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:05:42 -0400 Subject: [PATCH 13/25] Distinguish empty call defaults from comprehension accumulators --- src/decompiler.rs | 12 +++- .../comprehension_default_arguments.py | 66 ++++++++++++++++++ tests/integration.rs | 24 +++++++ .../comprehension_default_arguments.3.11.pyc | Bin 0 -> 4396 bytes .../comprehension_default_arguments.3.12.pyc | Bin 0 -> 2745 bytes .../comprehension_default_arguments.3.13.pyc | Bin 0 -> 2812 bytes .../comprehension_default_arguments.3.14.pyc | Bin 0 -> 2785 bytes 7 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/comprehension_default_arguments.py create mode 100644 tests/pyc/comprehension_default_arguments.3.11.pyc create mode 100644 tests/pyc/comprehension_default_arguments.3.12.pyc create mode 100644 tests/pyc/comprehension_default_arguments.3.13.pyc create mode 100644 tests/pyc/comprehension_default_arguments.3.14.pyc 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/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 0000000000000000000000000000000000000000..59434d0668a1bcf5ea24f9ff390129b7c4234814 GIT binary patch literal 4396 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 D9tJw? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..efab974191f5670f1e9520e3aac23e7985b4d00f GIT binary patch literal 2812 zcmcIm&u<$=6rSDnkNC$r1lI~FxCw3`wGd8R5>gOpN$jMFL_(B}n*tJ1u-C4UvyNug zq1%E6wM(yKiRa zeg58iQxQeGKnTg6+W30?_uGH}33mPBbgS|<7`KT;P7_Kb4v>?0Kwc661<3?xlFWc+ z$pUDRtbkUj4X`a?^V_4fm2kl}82HJ`bpTkEQ(!fPj}SMx!d>B+&`%ieCX-%5(j9Y$ zXM#Uo5_k;pRoe}-CQab!n-!)lgg8hKneMLsDuTkny21jtzz+(&uxbxsg2F3Yoa9Y2 zPi$;yRgS$*)1N(YRLyu&PQ}WW)umKMW_hkGBxE&jDx1KCl?A}ES$Cl_%cN8WUnV_p zm4m9xRMsC)URN{lu0NVyUZL_eSz*bvvKW)&OW?k^L=%~1Sy5SF<@2&Lm1Jt|6>W!= zVE?2t4zNb{?4GsIp0jIh{EzOlCGqlCYEJm6^M2>n3)|;+gA*I(tXv3A+>pOi*Cq<$ z|pW%FK&1uqT_Q29RP-KaJnX@FTiLdrh#fWea*%6R5-AOwC^KaQ@bMkkS@VRVH)X$yl%vuo`}88xjUSQkh;7+ z1C$2|$=4E+L3+d8t1VaqkGLU0kCBK1=_bb(F@q+KTxQjS8wB|aGRGy*beh|&QMv{d^Rj23bU zG?&ZMQR>OrJmvUW%IOjsIJty87)+xoqeqS@NTFj8NNU1905C{scD^J=inH?%-9IGq ziOp-MQL(Hr6>wu zp0E)B13TE8E@5xly!xXTo6>!6ttVYFvVcA5T~4d?*-pPPU~B|hz^h=i2>7Mu%BmIV zhD4nCmqaw}NOmP%Z`8f8$6**O1aO3m&_Sc#4USdY^jM9O&{0alIwfa^jFi+%%CjGm zB-4LAB>BxV)C5sdlo&->(w7mMHEvPOtfb^RQ&8{8Mf3-uR$KRs-=-aOC{|U9{Z1?1 zTIJDlP?H)(iYd<4K34&QR8IzN?<}WdnUs8yo`YxDm)UuMzXXos_MIl~%)aYbsGtjl H^8on=COZ!c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..869ec15964899bfd358faa955b586b86eec4358f GIT binary patch literal 2785 zcmcIm%}*Ow5P!St5Bv!zL4>vjVww;{VuxUfR8644#tEWI(Sj3NRYG7d#Er3+x4UV3 zvwCW*9;$|ebECO5mLk=;G^u(BD3=`LU?J*j|A1Dwm*mu$=MTKW8%nCK^nN?LJNw?7 z-~MK12i-2aKnVG>-*0=m_RSA}fL%Yhs#U%KMw*DElc+@G06CEdCn%ACl_{58@>B3y*m1Rv4(@Oy-;wof;{ z2i|yyypIIaLt+LB=_SIc|<07W!bu9-;{{FLQB{6JFVr zglfAvY zr6E;PRK>p(|4P;1nLnCZk*Rb;N-FVGay}+4Er9#{0$tWtq@=3&2Sey zdB@TI{czU&+~Ij{^1Lhy#C?S;;}qQFZ+LP|D^(iw|K_rmN1*P}3$j{$U_e$ZcZ&Ex{>!6(;$y}mK`jBZ~P z^Al?`*^WD(y>RywE{eCMTmTg?vtI$T9OE`~delGx-?8NkQm@aLw#L+hRE1Oonn7%k zf_yCnHD2c7>ckre1CO{aB@a+W5;9DR&12TkF4+3JFv_32PFd(m(KWdldOY(}|BwCm zPvysp-jOZ$`GR*O+mX@WRCJ%;3?XHcddhYM()z*6E!SijWtTa2iCFI0KXg8@g7KRf zK&QQW{d!vx8?}cdvb19)-uxd)v{y-lETbL>(+h*XgMe)6B6PqFmR;*$G|Mjb+0Piu ziJWWR8KZzxBVFsh5z<5G+?4odI&WmpeE^Y1fwSHQZ+dt)k%U5OGeSZ-lne(*w_yZ#ZpSK zzp;wVN)AgvrZI}_Qhb{&R|bPr{sLMqtfXRELK>yeaS8Te^Q?P7~L?>pl?v KMi<%-ul)lLSqnq} literal 0 HcmV?d00001 From 6121e3c7ae30a9a7942610968d82b108d232d8eb Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:05:42 -0400 Subject: [PATCH 14/25] Preserve walrus bindings in bounded expression simulation --- src/decompiler.rs | 13 +++++++++++ src/decompiler_stack_tests.rs | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/decompiler.rs b/src/decompiler.rs index a4be131f..f4d28af0 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -29060,6 +29060,19 @@ return None; } st.push(st[st.len() - arg].clone()); } + Op::STORE_FAST => { + // A walrus stores a COPY of the value still on top of the + // expression stack. Standalone assignment is a statement. + let value = st.pop()?; + let surviving = st.last_mut()?; + if !Rc::ptr_eq(surviving, &value) { + return None; + } + *surviving = Rc::new(Expr::Named { + target: self.name_expr(self.local_name(arg)), + value, + }); + } Op::SWAP => { if arg == 0 || arg > st.len() { return None; diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index b7483fb3..236a612e 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -205,3 +205,47 @@ fn top_copy_and_identity_swap_are_valid() { assert_eq!(result.len(), 2); assert!(result.iter().all(|entry| Rc::ptr_eq(entry, &value))); } + +#[test] +fn walrus_keeps_the_local_binding_and_one_evaluation() { + let mut module = fixture(); + Rc::get_mut(&mut module.code).unwrap().varnames = vec!["state".into()]; + let value = Rc::new(Expr::Call { + func: Rc::new(Expr::Name("get".into())), + args: vec![Rc::new(Expr::Name("entity_id".into()))], + keywords: Vec::new(), star_args: None, star_kwargs: None, + }); + let outer = Rc::new(Expr::Name("outer".into())); + // Assignment expressions exist since 3.8; this COPY-backed encoding + // starts in 3.11. The bounded walker must preserve the binding in + // `(state := get(entity_id))` while retaining unrelated stack entries. + for minor in 11..=14 { + let version = PythonVersion { minor, ..module.version() }; + let ctx = context(&module.code, version, vec![ + instruction(Op::COPY, 1), instruction(Op::STORE_FAST, 0), + ]); + let result = ctx.sim_stack_region(0, 2, vec![outer.clone(), value.clone()]) + .expect("COPY followed by STORE_FAST is an assignment expression"); + assert_eq!(result.len(), 2); + assert!(Rc::ptr_eq(&result[0], &outer)); + match &*result[1] { + Expr::Named { target, value: assigned } => { + assert!(matches!(&**target, Expr::Name(name) if name == "state")); + assert!(Rc::ptr_eq(assigned, &value)); + } + other => panic!("expected a named expression, got {other:?}"), + } + } +} + +#[test] +fn store_fast_rejects_separate_evaluations_and_standalone_assignments() { + let module = fixture(); + let ctx = context(&module.code, module.version(), + vec![instruction(Op::STORE_FAST, 0)]); + let name = || Rc::new(Expr::Name("value".into())); + // Equal syntax does not prove that the VM copied a single evaluation. + for stack in [vec![], vec![name()], vec![name(), name()]] { + assert!(ctx.sim_stack_region(0, 1, stack).is_none()); + } +} From d1b139d28882fbfdf3c2a7fc1b94bbc7c74541f6 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:31:17 -0400 Subject: [PATCH 15/25] Add harness coverage for the reconstruction fix --- tests/behavior/cases/b_assignment_chains.py | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/behavior/cases/b_assignment_chains.py diff --git a/tests/behavior/cases/b_assignment_chains.py b/tests/behavior/cases/b_assignment_chains.py new file mode 100644 index 00000000..f43b9930 --- /dev/null +++ b/tests/behavior/cases/b_assignment_chains.py @@ -0,0 +1,26 @@ +# MIN_VERSION: 3.13 + + +def three(read): + a = b = c = read() + return a, b, c + + +def six(read): + a = b = c = d = e = f = read() + return a, b, c, d, e, f + + +def three_none(): + a = b = c = None + return a, b, c + + +events = [] +def read(): + events.append("read") + return len(events) + +print(three(read), events) +print(six(read), events) +print(three_none()) From 831540978d84633750d47a8e26b7812884ee0de0 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:31:57 -0400 Subject: [PATCH 16/25] Add harness coverage for the reconstruction fix --- .../cases/b_generator_expanded_calls.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/behavior/cases/b_generator_expanded_calls.py diff --git a/tests/behavior/cases/b_generator_expanded_calls.py b/tests/behavior/cases/b_generator_expanded_calls.py new file mode 100644 index 00000000..f2164184 --- /dev/null +++ b/tests/behavior/cases/b_generator_expanded_calls.py @@ -0,0 +1,20 @@ +# MIN_VERSION: 3.13 + + +def positional(rows, factory): + return tuple(factory(*row) for row in rows) + + +def keywords(rows, factory): + return tuple(factory(**dict(row)) for row in rows) + + +def both(rows, factory): + return tuple(factory(*row[0], **row[1]) for row in rows) + + +def factory(*args, **kwargs): + return args, sorted(kwargs.items()) +print(positional([[1, 2], [3]], factory)) +print(keywords([{"a": 1}, {"b": 2}], factory)) +print(both([([1], {"a": 2})], factory)) From f47057c30f6b5686b9724b4554854d99ab6487f4 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:32:08 -0400 Subject: [PATCH 17/25] Add harness coverage for the reconstruction fix --- tests/behavior/cases/b_assignment_identity.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/behavior/cases/b_assignment_identity.py diff --git a/tests/behavior/cases/b_assignment_identity.py b/tests/behavior/cases/b_assignment_identity.py new file mode 100644 index 00000000..471da697 --- /dev/null +++ b/tests/behavior/cases/b_assignment_identity.py @@ -0,0 +1,20 @@ +# MIN_VERSION: 3.9 + + +def separate(read): + left, right = read(), read() + return left, right + + +def shared(read): + left = right = read() + return left, right + + +events = [] +def read(): + events.append("read") + return len(events) + +print(separate(read), events) +print(shared(read), events) From 583d2cdbbb08ef46367586d6e5da8d0d3f9dff0f Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:32:17 -0400 Subject: [PATCH 18/25] Add harness coverage for the reconstruction fix --- .../cases/b_sequential_generator_handlers.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/behavior/cases/b_sequential_generator_handlers.py diff --git a/tests/behavior/cases/b_sequential_generator_handlers.py b/tests/behavior/cases/b_sequential_generator_handlers.py new file mode 100644 index 00000000..54ae3d4c --- /dev/null +++ b/tests/behavior/cases/b_sequential_generator_handlers.py @@ -0,0 +1,41 @@ +# MIN_VERSION: 3.12 +import asyncio + +def synchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 + + +async def asynchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 + + +def work(stage): + print(stage) + if stage == "first": + raise ValueError(stage) + return 9 +print(tuple(synchronous(work))) +async def consume(): + result = [] + async for value in asynchronous(work): + result.append(value) + return result +print(asyncio.run(consume())) From 2c39f524bd87ce711f2d83daacab24101c37bd1c Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:32:26 -0400 Subject: [PATCH 19/25] Add harness coverage for the reconstruction fix --- .../b_comprehension_default_arguments.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/behavior/cases/b_comprehension_default_arguments.py 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"]}, {}])) From 34df3c9d8e0593735c85766c3564ce3362cf9c73 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:33:08 -0400 Subject: [PATCH 20/25] Isolate expanded generator calls for strict round-trip verification --- tests/fixtures/generator_positional_call.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/fixtures/generator_positional_call.py diff --git a/tests/fixtures/generator_positional_call.py b/tests/fixtures/generator_positional_call.py new file mode 100644 index 00000000..e8f727a6 --- /dev/null +++ b/tests/fixtures/generator_positional_call.py @@ -0,0 +1,2 @@ +def select(rows, factory): + return tuple(factory(*row) for row in rows) From 60148fb59657d27f34c69fcb13c21b209ea75ebb Mon Sep 17 00:00:00 2001 From: ejfkdev <152914458+ejfkdev@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:10:33 +0800 Subject: [PATCH 21/25] PR24 src (resolve mod conflict) --- src/codegen.rs | 24 ++ src/decompiler.rs | 584 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 491 insertions(+), 117 deletions(-) diff --git a/src/codegen.rs b/src/codegen.rs index 89be6894..eef087ce 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -535,6 +535,28 @@ impl Printer { orelse, finalbody, } => { + // Exception-region reconstruction can split try/except from + // its success and finally suites. An else without handlers + // cannot belong to the outer try. Rejoin the single inner + // handler-bearing try, retaining every recovered statement. + if self.version.at_least(2, 5) + && handlers.is_empty() && !orelse.is_empty() && !finalbody.is_empty() + && body.len() == 1 + { + if let Stmt::Try { + body: inner_body, handlers: inner_handlers, + orelse: inner_else, finalbody: inner_finally, + } = &body[0] + { + if !inner_handlers.is_empty() && inner_else.is_empty() && inner_finally.is_empty() { + self.stmt(&Stmt::Try { + body: inner_body.clone(), handlers: inner_handlers.clone(), + orelse: orelse.clone(), finalbody: finalbody.clone(), + }); + return; + } + } + } // a bare `except:` must be last (and unique); drop extras let mut handlers = handlers.clone(); handlers.sort_by_key(|h| if h.type_.is_none() { 1 } else { 0 }); @@ -1134,6 +1156,8 @@ impl Printer { first = false; if let Some(kn) = k { self.write(&format!("{kn}=")); + } else { + self.write("**"); } self.expr(v, prec::TERNARY); } diff --git a/src/decompiler.rs b/src/decompiler.rs index 9102f139..d923f258 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, } @@ -7540,6 +7542,12 @@ impl<'a> Ctx<'a> { while k2 < self.instrs.len() { let c = &self.instrs[k2]; match c.op { + Op::NOP | Op::NOT_TAKEN | Op::CACHE => { + // Padding does not end the post-POP_EXCEPT return + // trailer. Keep scanning through it so an unnamed + // handler retains its already loaded return value. + k2 += 1; + } Op::LOAD_CONST | Op::STORE_FAST | Op::STORE_NAME | Op::DELETE_FAST | Op::DELETE_NAME | Op::STORE_DEREF | Op::DELETE_DEREF => { @@ -14808,6 +14816,7 @@ impl<'a> Ctx<'a> { | Op::COPY_DICT_WITHOUT_KEYS => { if self.inline_comp.is_some() && matches!(inst.op, Op::LIST_APPEND | Op::SET_ADD | Op::MAP_ADD) + && (!self.version.at_least(3, 10) || arg >= 2) { self.comp_add_element(inst); } else { @@ -15291,8 +15300,8 @@ impl<'a> Ctx<'a> { let (ex_kws, star_kw) = if let Some(kw) = kwargs_opt { let (k, s) = flatten_ex_kwargs(kw.clone()); // a DICT_MERGE-built kwargs dict with const keys - // plus ONE trailing star is the compiler's own - // layout for `f(kw=..., **x)` — decompose it back + // plus expansion entries is the compiler's own + // layout for `f(kw=..., **x, **y)` — decompose it back // (the whole-dict rendering recompiles to // BUILD_MAP + DICT_UPDATE and breaks sig-exactness; // configparser 3.12 _get_conv). A DICT_UPDATE-built @@ -15306,34 +15315,24 @@ impl<'a> Ctx<'a> { break 'dc None; } if let Expr::Dict(entries) = &*kw { - if entries.len() >= 2 - && matches!( - &*entries[entries.len() - 1].0, - Expr::Starred(_) - ) - && entries[..entries.len() - 1].iter().all( - |(kk, _)| { - matches!(&**kk, Expr::Const(o) - if matches!(&**o, PyObject::Str(_))) - }, - ) - { + if entries.iter().any(|(key, _)| matches!(&**key, Expr::Starred(_))) { let mut kws = Vec::new(); - for (kk, v) in &entries[..entries.len() - 1] { - if let Expr::Const(o) = &**kk { - if let PyObject::Str(name) = &**o { - kws.push(( - Some(name.clone()), - v.clone(), - )); + let mut named = Vec::new(); + for (key, value) in entries { + match &**key { + Expr::Starred(inner) => kws.push((None, inner.clone())), + Expr::Const(object) => { + let PyObject::Str(name) = &**object else { break 'dc None; }; + if !is_kw_name_ok(name) || named.contains(name) { + break 'dc None; + } + named.push(name.clone()); + kws.push((Some(name.clone()), value.clone())); } + _ => break 'dc None, } } - if let Expr::Starred(inner) = - &*entries[entries.len() - 1].0 - { - break 'dc Some((kws, Some(inner.clone()))); - } + break 'dc Some((kws, None)); } } None @@ -19325,20 +19324,10 @@ let reopen = self return true; } let target = inst.target.unwrap_or(inst.end()); - // async for: the iterable expression is an Await, or we are - // inside an async def using GET_AITER - let is_async = matches!( - self.stack.last(), - Some(Sv::E(e)) if matches!(&**e, Expr::Await(_)) - ); - if is_async { - if let Some(Sv::E(e)) = self.stack.last_mut() { - if let Expr::Await(inner) = &**e { - *e = inner.clone(); - } - } - } - self.handle_for_iter(target, is_async); + // FOR_ITER iterates synchronously, even when the collection + // was produced by `await fetch()`. Genuine async iteration + // uses GET_AITER/GET_ANEXT and its separate decoder path. + self.handle_for_iter(target, false); true } Op::FOR_LOOP => { @@ -23206,16 +23195,21 @@ impl<'a> Ctx<'a> { fn is_loop_top_prefix(&self, t: usize, start: usize) -> bool { // 3.11+ folds inline CACHE entries into each instruction's size, // so offsets inside a cached instruction have no idx_of entry - // either - a back edge landing <=8 bytes before the loop top - // would falsely match a CACHE tail (compileall 3.13 main tore - // into an INCOMPLETE stub). The EXTENDED_ARG-prefix shape this - // recognizes is pre-3.11 wordcode only (no CACHE gaps). - if self.version.at_least(3, 11) { - return false; - } + // either. Absence from the instruction map cannot distinguish + // a CACHE tail from an EXTENDED_ARG prefix: modern offsets need + // proof from the original bytes. if t >= start || start - t > 8 { return false; } + if self.version.at_least(3, 11) { + if t % 2 != 0 || start % 2 != 0 { + return false; + } + if !(t..start).step_by(2).all(|offset| self.code.code.get(offset) + .map_or(false, |byte| self.table.op(*byte) == Op::EXTENDED_ARG)) { + return false; + } + } // no instruction STARTS at t (idx_of also maps END offsets: the // preceding instruction's end lands on t and must not count as // an instruction start) @@ -28805,6 +28799,15 @@ return None; if st_t.len() != st_e.len() || st_t.is_empty() { return None; } + // CALL_KW support must not make an arm-local callable/argument + // prefix look like a preload from before the condition. Merging + // only its final slot would hoist that prefix across the truth + // test. Decline this shape until the whole arm can be preserved. + if st_t.len() != base.len() + 1 + && self.instrs[tstart..=re].iter().any(|ins| ins.op == Op::CALL_KW) + { + return None; + } let slot_t = st_t.last()?.clone(); let slot_e = st_e.last()?.clone(); // compare the UNDER-stack only — the tops are the slot values @@ -28931,11 +28934,19 @@ return None; ( Expr::Call { func: fa, args: aa, keywords: ka, star_args: sa, star_kwargs: ska }, Expr::Call { func: fb, args: ab, keywords: kb, star_args: sb, star_kwargs: skb }, - ) if ka.is_empty() && kb.is_empty() && sa.is_none() && sb.is_none() && ska.is_none() && skb.is_none() && aa.len() == ab.len() => { + ) if sa.is_none() && sb.is_none() && ska.is_none() && skb.is_none() + && aa.len() == ab.len() && ka.len() == kb.len() + && ka.iter().zip(kb).all(|((a, _), (b, _))| a == b) => { let mut slots: Vec<(&ExprRef, &ExprRef)> = vec![(fa, fb)]; for (x, y) in aa.iter().zip(ab.iter()) { slots.push((x, y)); } + // CALL_KW can now reach this merge through the bounded + // walker. Keep keyword values in the same ordered slot + // model as positional values, preserving the call itself. + for ((_, x), (_, y)) in ka.iter().zip(kb) { + slots.push((x, y)); + } let mut merged_idx: Option = None; let mut merged_val: Option = None; for (i, (x, y)) in slots.iter().enumerate() { @@ -28954,8 +28965,9 @@ return None; reps[idx] = val; Some(Rc::new(Expr::Call { func: reps[0].clone(), - args: reps[1..].to_vec(), - keywords: Vec::new(), + args: reps[1..1 + aa.len()].to_vec(), + keywords: ka.iter().zip(&reps[1 + aa.len()..]) + .map(|((name, _), value)| (name.clone(), value.clone())).collect(), star_args: None, star_kwargs: None, })) @@ -29152,6 +29164,28 @@ return None; star_kwargs: None, })); } + Op::CALL_KW => { + let names = st.pop()?; + let Expr::Const(names) = &*names else { return None; }; + let PyObject::Tuple(names) = &**names else { return None; }; + let names: Option> = names.iter().map(|value| match &**value { + PyObject::Str(name) => Some(name.clone()), _ => None, + }).collect(); + let names = names?; + if names.len() > arg || st.len() < arg + 1 { return None; } + let all = st.split_off(st.len() - arg); + let positional = arg - names.len(); + let args = all[..positional].to_vec(); + let keywords = names.into_iter().zip(all[positional..].iter().cloned()) + .map(|(name, value)| (Some(name), value)).collect(); + let is_null = |value: &ExprRef| matches!(&**value, Expr::Name(name) if name == "\u{0}null"); + let mut callable = st.pop()?; + if is_null(&callable) { callable = st.pop()?; } + if st.last().map_or(false, is_null) { st.pop(); } + st.push(Rc::new(Expr::Call { + func: callable, args, keywords, star_args: None, star_kwargs: None, + })); + } Op::BINARY_OP => { let r = pop1(&mut st); let l = pop1(&mut st); @@ -31672,6 +31706,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 @@ -31711,7 +31765,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; @@ -31721,9 +31775,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 @@ -31740,7 +31797,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) @@ -31775,8 +31847,8 @@ return None; | Op::YIELD_VALUE ) }) - }); - let c = if target_is_append { + })); + let c = if target_accepts_item { if jump_if_true { cond } else { @@ -31796,7 +31868,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); @@ -31809,10 +31881,30 @@ return None; // elif test would create a nested If at the same target, so keep // the Else open in that case. if let Some(top) = self.blocks.last() { + // A modern Else opened immediately after the then arm's + // unconditional skip has an authoritative finite span. A guard + // inside that span can share its merge without ending the Else: + // closing it here ejects the guarded query result and may read + // its local on the other arm before assignment. Keep older and + // inferred/try-else boundaries on their existing path. + let guarded_else_merge = self.version.at_least(3, 11) + && top.kind == BlockType::Else + && self.cur_offset > top.start + && self.cur_offset < top.end + && self.idx_of.get(&top.start).map_or(false, |&si| { + si > 0 && { + let skip = &self.instrs[si - 1]; + !skip.is_backward + && skip.end() == top.start + && skip.target == Some(top.end) + && matches!(skip.op, Op::JUMP_FORWARD | Op::JUMP | Op::JUMP_ABSOLUTE) + } + }); if matches!(top.kind, BlockType::Else | BlockType::TryElse) && top.end == target && !top.is_elif && self.cur_offset != top.start + && !guarded_else_merge { self.close_blocks_at(target); } @@ -33269,6 +33361,10 @@ if split_cond { // `if c: continue`-style: fall through to the // arm below that opens an If ending at that back // edge instead of closing the loop here. + // A prefixed loop top has the same identity here + // as when matching the conditional above. Otherwise + // a later edge to EXTENDED_ARG is missed and a + // conditional continue prematurely closes the loop. let loop_start0 = self.blocks[i].start; let has_later_back_edge = self .idx_of @@ -33276,7 +33372,10 @@ if split_cond { .map_or(false, |&ci0| { self.instrs.iter().skip(ci0 + 1).any(|ins| { ins.is_backward - && ins.target == Some(loop_start0) + && ins.target.map_or(false, |t| { + t == loop_start0 + || self.is_loop_top_prefix(t, loop_start0) + }) && matches!( ins.op, Op::JUMP_ABSOLUTE @@ -33456,8 +33555,21 @@ if split_cond { } return; } + // A forward conditional can skip this back + // edge and land on the next one. Keep the + // skipped edge inside its fall-through arm; + // ending before it moves `continue` outside + // the guard and changes the recompiled jumps. + let body_end = if self.version.at_least(3, 11) + && raw_target == inst.end() + && self.is_loop_top_prefix(target, loop_start) + { + inst.end() + } else { + inst.start + }; let mut blk = - Block::new(BlockType::If, self.cur_next, inst.start); + Block::new(BlockType::If, self.cur_next, body_end); let c = if jump_if_true { negate_cond(cond.clone()) } else { @@ -42488,33 +42600,30 @@ fn simplify_not(e: ExprRef) -> ExprRef { } } -/// 3.14: extended slices arrive as `slice(a, b, c)` calls or marshalled -/// slice constants; normalize both to Expr::Slice. +/// Extended indexes can contain actual marshalled slice constants. Preserve +/// runtime slice calls: their global lookup may be shadowed or have effects. fn normalize_slice_call(idx: ExprRef) -> ExprRef { + normalize_slice_constant(idx) +} + +/// Normalize actual marshalled slice values inside multi-axis indexes. +/// A runtime `slice(...)` call must retain its global lookup and evaluation. +fn normalize_slice_constant(idx: ExprRef) -> ExprRef { match &*idx { - Expr::Call { func, args, keywords, star_args: None, star_kwargs: None } - if keywords.is_empty() - && matches!(&**func, Expr::Name(n) if n == "slice") - && (2..=3).contains(&args.len()) - // a starred operand (`slice(*argvals, None)`) cannot be a - // slice display — keep the call (yt-dlp jsinterp - // `obj[*argvals:]` was not valid source) - && args.iter().all(|a| !matches!(&**a, Expr::Starred(_))) => - { - Rc::new(Expr::Slice(Box::new(SliceExpr { - start: none_if_const_none(args[0].clone()), - stop: none_if_const_none(args[1].clone()), - step: args.get(2).cloned().and_then(none_if_const_none), - }))) - } - Expr::Const(o) => match &**o { - PyObject::Slice(a, b, c) => Rc::new(Expr::Slice(Box::new(SliceExpr { - start: none_if_const_const(a.clone()), - stop: none_if_const_const(b.clone()), - step: none_if_const_const(c.clone()), + Expr::Const(object) => match &**object { + PyObject::Slice(start, stop, step) => Rc::new(Expr::Slice(Box::new(SliceExpr { + start: none_if_const_const(start.clone()), + stop: none_if_const_const(stop.clone()), + step: none_if_const_const(step.clone()), }))), + PyObject::Tuple(values) if values.iter().any(|value| matches!(&**value, PyObject::Slice(_ , _, _))) => + Rc::new(Expr::Tuple(values.iter().cloned() + .map(|value| normalize_slice_constant(Rc::new(Expr::Const(value)))).collect())), _ => idx, }, + Expr::Tuple(values) => Rc::new(Expr::Tuple( + values.iter().cloned().map(normalize_slice_constant).collect(), + )), _ => idx, } } @@ -42585,9 +42694,39 @@ fn cmp_from_index(idx: usize) -> CmpOp { // ===================== stack events, calls, functions, classes ===================== impl<'a> Ctx<'a> { + fn discarded_unpack_target(&self, remaining: usize) -> ExprRef { + // Repeated local targets can have their earlier stores replaced by + // POP_TOP. Their names may then be obfuscated while the surviving + // store keeps its slot. Reuse the next direct local store's spelling: + // the compiler elides the duplicate instead of creating an extra `_` + // local. Do not cross nested unpacking, side effects or control flow. + let name = if self.version.at_least(3, 13) { + self.idx_of.get(&self.cur_offset).and_then(|&index| { + let mut discarded = 1usize; + for next in &self.instrs[index + 1..] { + match next.op { + Op::NOP | Op::CACHE | Op::EXTENDED_ARG | Op::NOT_TAKEN => {}, + Op::POP_TOP => { + discarded += 1; + if discarded >= remaining { return None; } + } + Op::STORE_FAST => return self.code.varnames.get(next.arg as usize).cloned(), + Op::STORE_FAST_STORE_FAST | Op::STORE_FAST_LOAD_FAST => { + return self.code.varnames.get(((next.arg >> 4) & 0xF) as usize).cloned(); + } + Op::STORE_DEREF => return self.code.deref_name(next.arg as usize).map(str::to_string), + _ => return None, + } + } + None + }) + } else { None }; + self.name_expr(name.as_deref().unwrap_or("_")) + } + fn handle_pop_top(&mut self) { - // 3.14 `_` elision: the compiler replaces the store of a - // throwaway `_` name inside a tuple unpack with a POP_TOP + // 3.13+ repeated-target elision: the compiler replaces an + // overwritten local store inside a tuple unpack with a POP_TOP // discarding that slot (UNPACK_SEQUENCE; POP_TOP; // STORE_FAST_STORE_FAST). The open unpack frame must consume // the slot as a placeholder target or the tuple never @@ -42598,8 +42737,9 @@ impl<'a> Ctx<'a> { if *remaining > 0 { let is_slot = matches!(self.stack.last(), Some(Sv::E(e)) if Rc::ptr_eq(e, fv)); if is_slot { + let target = self.discarded_unpack_target(*remaining); if let Some(Sv::E(_)) = self.stack.pop() { - self.collect_unpack_target(self.name_expr("_")); + self.collect_unpack_target(target); } return; } @@ -43282,6 +43422,12 @@ impl<'a> Ctx<'a> { cur.target = Some(target); } comp.target_seen = true; + // UNPACK_SEQUENCE consumed the modeled iterator while + // collecting its tuple target. The VM keeps a distinct + // iterator slot until END_FOR/POP_ITER. Restore that slot, + // as the simple-target path already does, instead of + // suppressing cleanup for generators that still own one. + self.push(value); return; } } @@ -43965,6 +44111,24 @@ impl<'a> Ctx<'a> { .iter() .map(|x| (canon(x.op), x.arg)) .collect(); + // Masked runtime exits can leave padding between POP_EXCEPT and + // a return whose value was computed earlier. Padding is not a + // value-building tail: matching it against the mainline would + // swallow the actual response/call and resume with an empty stack. + // Look through padding, then require a real independent value head. + let head = tail.iter().find(|(op, _)| !matches!( + op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::EXTENDED_ARG | Op::RESUME + )); + let value_head = head.map_or(false, |(op, arg)| { + matches!(op, Op::LOAD_FAST | Op::LOAD_FAST_LOAD_FAST | Op::LOAD_NAME + | Op::LOAD_DEREF | Op::LOAD_CLASSDEREF | Op::LOAD_GLOBAL + | Op::LOAD_CONST | Op::LOAD_SMALL_INT | Op::LOAD_COMMON_CONSTANT) + || (*arg == 0 && matches!(op, + Op::BUILD_TUPLE | Op::BUILD_LIST | Op::BUILD_SET | Op::BUILD_MAP)) + }); + if !value_head { + return None; + } // require a NON-DEGENERATE tail: at least one value op before // the RETURN. A lone RETURN_VALUE (the clause's value was // loaded+SWAPed UNDER the POP_EXCEPT, _collections_abc 3.12 @@ -47856,7 +48020,11 @@ impl<'a> Ctx<'a> { _ => Vec::new(), }; match &*other { - Expr::Dict(d) => entries.extend(d.iter().cloned()), + // A call expansion of a constructed dictionary keeps + // that boundary. Flattening it into outer DICT_MERGE + // entries would change its internal DICT_UPDATE's + // overwrite semantics into duplicate-key errors. + Expr::Dict(d) if inst.op == Op::DICT_UPDATE => entries.extend(d.iter().cloned()), o => entries.push(( Rc::new(Expr::Starred(Rc::new(o.clone()))), self.name_expr(""), @@ -48674,7 +48842,7 @@ fn p649_leaked_gate(e: &ExprRef) -> Option { /// statements, recursively — conditional annotations (`if TYPE_CHECKING: /// x: T`) leave their markers inside the nested block. A marker directly /// after `name = value` merges into `name: T = value`. -fn p649_restore_gates(stmts: &mut Vec, entries: &mut Vec) { +fn p649_restore_gates(stmts: &mut Vec, entries: &mut Vec, class_scope: bool) { let mut i = 0; while i < stmts.len() { let gate = match &stmts[i] { @@ -48700,6 +48868,20 @@ fn p649_restore_gates(stmts: &mut Vec, entries: &mut Vec) { annotation: ent.ann.clone(), value: merged, }; + // A class's annotation registration gate is compiler-visible + // even when its condition was constant-folded. An unconditional + // source block retains that registration without emitting a + // runtime condition. Plain AnnAssign would lose the cell and + // the annotation function's guarded entries on recompilation. + let ann_stmt = if class_scope { + Stmt::If { + cond: Rc::new(Expr::Const(Rc::new(PyObject::True))), + body: vec![ann_stmt], + orelse: vec![], + } + } else { + ann_stmt + }; if has_value { stmts.remove(i - 1); stmts[i - 1] = ann_stmt; @@ -48714,26 +48896,47 @@ fn p649_restore_gates(stmts: &mut Vec, entries: &mut Vec) { Stmt::If { body, orelse, .. } | Stmt::While { body, orelse, .. } | Stmt::For { body, orelse, .. } => { - p649_restore_gates(body, entries); - p649_restore_gates(orelse, entries); + p649_restore_gates(body, entries, class_scope); + p649_restore_gates(orelse, entries, class_scope); } Stmt::Try { body, handlers, orelse, finalbody, .. } => { - p649_restore_gates(body, entries); + p649_restore_gates(body, entries, class_scope); for h in handlers.iter_mut() { - p649_restore_gates(&mut h.body, entries); + p649_restore_gates(&mut h.body, entries, class_scope); } - p649_restore_gates(orelse, entries); - p649_restore_gates(finalbody, entries); + p649_restore_gates(orelse, entries, class_scope); + p649_restore_gates(finalbody, entries, class_scope); } _ => {} } i += 1; } + // Keep one readable registration block for consecutive gated fields. + // Each field still has its own compiler gate index in __annotate__. + if class_scope { + fn annotation_block(stmt: &Stmt) -> bool { + matches!(stmt, Stmt::If { cond, body, orelse } + if matches!(&**cond, Expr::Const(value) if matches!(&**value, PyObject::True)) + && orelse.is_empty() && !body.is_empty() + && body.iter().all(|stmt| matches!(stmt, Stmt::AnnAssign { .. }))) + } + let mut index = 1; + while index < stmts.len() { + if annotation_block(&stmts[index - 1]) && annotation_block(&stmts[index]) { + let Stmt::If { body: current, .. } = stmts.remove(index) else { unreachable!() }; + if let Stmt::If { body, .. } = &mut stmts[index - 1] { + body.extend(current); + } + } else { + index += 1; + } + } + } } /// 3.14 PEP 649: rebuild module/class-scope annotation statements from /// the compiler-generated `__annotate__` machinery (see P649Entry). -fn restore_pep649_annotations(body: &mut Vec) { +fn restore_pep649_annotations(body: &mut Vec, code: &CodeObject) { // 1) locate the pseudo def and extract its entries let mut entries: Vec = Vec::new(); let mut def_pos: Option = None; @@ -48814,7 +49017,7 @@ fn restore_pep649_annotations(body: &mut Vec) { // 3) gated (module-scope) entries: replace their `{IDX}` markers, // recursively through nested blocks - p649_restore_gates(body, &mut entries); + p649_restore_gates(body, &mut entries, code.name != "" && code.flags & 1 == 0); // gated entries whose `{IDX}` marker never appeared were dead in the // executable flow (e.g. folded `if False:` bodies — the compiler drops @@ -49023,7 +49226,7 @@ fn postprocess_body(mut body: Vec, code: &CodeObject, version: PythonVersi // `__annotate_func__`) instead of inline `__annotations__` subscript // stores. Restore them to AnnAssign statements and strip the // protocol remnants (see restore_pep649_annotations). - restore_pep649_annotations(&mut body); + restore_pep649_annotations(&mut body, code); // __module__ / __qualname__ / __doc__ handling for class bodies let mut idx = 0; while idx < body.len() { @@ -50985,6 +51188,49 @@ impl<'a> Ctx<'a> { // ===================== comprehensions ===================== +// The main statement parser already understands PEP 709's nested inline +// comprehensions. Reuse it for generator elements instead of flattening an +// inner dict/list/set comprehension into the generator's own clauses. +fn generator_parts_from_body( + body: &[Stmt], generators: &mut Vec, +) -> Option { + let mut result = None; + for statement in body { + match statement { + Stmt::For { target, iter, body, orelse, is_async } if orelse.is_empty() => { + if result.is_some() { return None; } + generators.push(Comprehension { + target: target.clone(), iter: iter.clone(), ifs: Vec::new(), + is_async: *is_async, if_line_break: false, + }); + result = generator_parts_from_body(body, generators); + if result.is_none() { return None; } + } + Stmt::If { cond, body, orelse } if orelse.is_empty() => { + if result.is_some() { return None; } + generators.last_mut()?.ifs.push(cond.clone()); + result = generator_parts_from_body(body, generators); + if result.is_none() { return None; } + } + Stmt::Expr(value) => { + match &**value { + Expr::Yield(Some(value)) => { + if result.is_some() { return None; } + result = Some(value.clone()); + } + Expr::Const(_) => {}, // Docstring. + _ => return None, + } + } + Stmt::Pass | Stmt::Return(None) => {}, + Stmt::Return(Some(value)) + if matches!(&**value, Expr::Const(object) if matches!(&**object, PyObject::None)) => {}, + _ => return None, + } + } + result +} + impl<'a> Ctx<'a> { /// Try to turn a completed call into a comprehension. The callee is a /// Function whose code object is named `` etc. The iterable is @@ -51156,10 +51402,16 @@ fn genexpr_ternary_merge( | Op::POP_JUMP_IF_TRUE | Op::POP_JUMP_FORWARD_IF_FALSE | Op::POP_JUMP_FORWARD_IF_TRUE + | Op::POP_JUMP_IF_NONE + | Op::POP_JUMP_IF_NOT_NONE + | Op::POP_JUMP_FORWARD_IF_NONE + | Op::POP_JUMP_FORWARD_IF_NOT_NONE ) }; let mut jf_target = None; - for x in instrs.iter().skip(ii + 1) { + let mut j = ii + 1; + while j < instrs.len() { + let x = instrs[j]; if x.offset >= t { break; } @@ -51176,6 +51428,18 @@ fn genexpr_ternary_merge( break; } if is_cj(x.op) && x.target == Some(t) && !x.is_backward { + j += 1; + continue; + } + // A then-arm value may itself contain a conditional dictionary or + // another ternary. Consume its complete, pure value region before + // searching for this arm's final jump, just as the else-arm does. + if is_cj(x.op) { + let nested_merge = Self::genexpr_ternary_merge(instrs, j)?; + if nested_merge > t { + return None; + } + j = instrs.iter().position(|item| item.offset == nested_merge)?; continue; } // 3.12+: TO_BOOL sits between the value run and the cond jump @@ -51183,11 +51447,13 @@ fn genexpr_ternary_merge( // element rendered as a plain `x`, silently dropping the else // arm on EVERY version with the folded ternary shape) if matches!(x.op, Op::TO_BOOL | Op::NOP | Op::NOT_TAKEN | Op::CACHE) { + j += 1; continue; } if !Self::comp_arm_value_op(x.op) { return None; } + j += 1; } let m = jf_target?; let ti = instrs.iter().position(|x| x.offset == t)?; @@ -51203,13 +51469,7 @@ fn genexpr_ternary_merge( j += 1; continue; } - if matches!( - x.op, - Op::POP_JUMP_IF_FALSE - | Op::POP_JUMP_IF_TRUE - | Op::POP_JUMP_FORWARD_IF_FALSE - | Op::POP_JUMP_FORWARD_IF_TRUE - ) { + if is_cj(x.op) { let nm = Self::genexpr_ternary_merge(instrs, j)?; let nmi = instrs.iter().position(|x2| x2.offset == nm)?; if nmi > mi { @@ -51232,6 +51492,20 @@ fn genexpr_ternary_merge( let table = table_for(self.version).ok()?; let instrs = crate::bytecode::decode_instructions(code, table, self.version); + if self.version.at_least(3, 12) && matches!(_kind, CompKind::Generator) + && instrs.iter().any(|instruction| instruction.op == Op::LOAD_FAST_AND_CLEAR) + { + if let Some(statements) = decompile(code, self.version).ok().filter(|result| result.clean) { + let mut generators = Vec::new(); + if let Some(element) = generator_parts_from_body(&statements.body, &mut generators) { + if !generators.is_empty() { + generators[0].iter = outer_iter.clone(); + return Some((element, None, generators)); + } + } + } + } + struct PartialGen { target: Option, iter: ExprRef, @@ -51408,7 +51682,7 @@ fn genexpr_ternary_merge( _ => Vec::new(), }; match &*other { - Expr::Dict(d) => entries.extend(d.iter().cloned()), + Expr::Dict(d) if inst.op == Op::DICT_UPDATE => entries.extend(d.iter().cloned()), o => entries.push(( Rc::new(Expr::Starred(Rc::new(o.clone()))), Rc::new(Expr::Name(String::new())), @@ -51879,8 +52153,19 @@ fn genexpr_ternary_merge( let v = stack.pop().unwrap_or_else(|| Rc::new(Expr::Name("?".to_string()))); (k, v) }; - key.get_or_insert(k); - elt.get_or_insert(v); + if self.version.at_least(3, 10) && inst.arg == 1 { + // Large dict displays use MAP_ADD 1 even inside a + // comprehension. They build its element, rather + // than setting the enclosing comprehension's key. + let dictionary = stack.pop()?; + let Expr::Dict(entries) = &*dictionary else { return None; }; + let mut entries = entries.clone(); + entries.push((k, v)); + stack.push(Rc::new(Expr::Dict(entries))); + } else { + key.get_or_insert(k); + elt.get_or_insert(v); + } } Op::YIELD_VALUE => { if let Some(p) = pending_or_filter.take() { @@ -52911,20 +53196,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, + })); + } } } @@ -53162,3 +53449,66 @@ mod comparison_tests; #[cfg(test)] #[path = "decompiler_stack_tests.rs"] mod stack_tests; + +#[cfg(test)] +mod generator_parts_tests { + use super::*; + + fn yielded_loop() -> Stmt { + Stmt::For { + target: Rc::new(Expr::Name("row".into())), + iter: Rc::new(Expr::Name("rows".into())), + body: vec![Stmt::Expr(Rc::new(Expr::Yield(Some(Rc::new( + Expr::Name("row".into()), + )))))], + orelse: vec![], + is_async: false, + } + } + + #[test] + fn extraction_preserves_one_yield_path_and_rejects_siblings() { + let body = vec![yielded_loop()]; + let mut generators = Vec::new(); + let element = generator_parts_from_body(&body, &mut generators).unwrap(); + assert!(matches!(&*element, Expr::Name(name) if name == "row")); + assert_eq!(generators.len(), 1); + + let mut generators = Vec::new(); + assert!(generator_parts_from_body( + &[yielded_loop(), yielded_loop()], &mut generators, + ).is_none()); + + let branch = Stmt::If { + cond: Rc::new(Expr::Name("ready".into())), + body: vec![Stmt::Expr(Rc::new(Expr::Yield(Some(Rc::new( + Expr::Name("row".into()), + )))))], + orelse: vec![], + }; + let mut body = yielded_loop(); + if let Stmt::For { body, .. } = &mut body { + *body = vec![branch.clone(), branch]; + } + assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + } + + #[test] + fn extraction_does_not_discard_assignments_or_loop_else() { + for name in ["saved", "__assert_armored__"] { + let assignment = Stmt::Assign { + targets: vec![Rc::new(Expr::Name(name.into()))], + value: Rc::new(Expr::Const(Rc::new(PyObject::None))), + wrap_before: vec![], + }; + assert!(generator_parts_from_body( + &[assignment, yielded_loop()], &mut Vec::new(), + ).is_none()); + } + let mut body = yielded_loop(); + if let Stmt::For { orelse, .. } = &mut body { + *orelse = vec![Stmt::Pass]; + } + assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + } +} From 8f3b0c0dd1145d20c168e27907495cf976d17bf8 Mon Sep 17 00:00:00 2001 From: ejfkdev <152914458+ejfkdev@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:10:40 +0800 Subject: [PATCH 22/25] PR25 src --- src/decompiler.rs | 73 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/decompiler.rs b/src/decompiler.rs index d923f258..d088bbb5 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -26407,18 +26407,18 @@ impl<'a> Ctx<'a> { )) } - /// 3.14 value-position `A1 and ... An or C`: every operand carries its + /// 3.13+ value-position `A1 and ... An or C1 or ... Cm`: each link carries its /// own COPY/TO_BOOL value-preserving idiom; the and-links PJIF to the /// or-continuation (this jump's target) and the LAST and-operand PJITs /// to the merge M; [target, M) evaluates C (t_pb f1/g_val 3.14: the /// per-jump value_merge blocks mis-nest the chain into /// `a and (b or c)`). Read-only; returns (merged value, merge offset). - fn try_value_and_or_314( + fn try_value_and_or( &self, cond: &ExprRef, target: usize, ) -> Option<(ExprRef, usize)> { - if !self.version.at_least(3, 14) { + if !self.version.at_least(3, 13) { return None; } let is_pjif = |o: Op| { @@ -26499,8 +26499,9 @@ impl<'a> Ctx<'a> { if ands.len() < 2 { return None; } - // [target, m) is the or's second operand: a pure value region - // (its leading POP_TOP discards the arriving operand copy) + // [target, m) contains the remaining OR operands. Each non-final + // operand must use the same COPY/TO_BOOL/PJIT-to-M value protocol; + // arbitrary statement branches or other merge targets do not qualify. let mi = *self.idx_of.get(&m)?; if mi <= ti { return None; @@ -26515,14 +26516,42 @@ impl<'a> Ctx<'a> { if cstart >= mi { return None; } - if !self.instrs[cstart..mi].iter().all(|x| { - x.target.is_none() - && (is_pure_value_op(x.op) - || matches!(x.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::POP_TOP)) - }) { + let mut or_tail = Vec::new(); + let mut operand_start = cstart; + let mut k = cstart; + while k < mi { + let ins = self.instrs[k]; + if is_pjit(ins.op) && idiom_link(k) { + if ins.target != Some(m) + || self.instrs[k - 2].arg != 1 + || k - 2 <= operand_start + { + return None; + } + or_tail.push(self.sim_value_region(operand_start, k - 2)?); + k += 1; + while k < mi && pad(self.instrs[k].op) { + k += 1; + } + if k >= mi || self.instrs[k].op != Op::POP_TOP { + return None; + } + k += 1; + operand_start = k; + continue; + } + if ins.target.is_some() + || !(is_pure_value_op(ins.op) + || matches!(ins.op, Op::COPY | Op::TO_BOOL | Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + { + return None; + } + k += 1; + } + if operand_start >= mi { return None; } - let c = self.sim_value_region(cstart, mi)?; + or_tail.push(self.sim_value_region(operand_start, mi)?); let mut and_flat = Vec::new(); for v in ands { flatten_boolop(v, BoolOpKind::And, &mut and_flat); @@ -26533,7 +26562,9 @@ impl<'a> Ctx<'a> { }) as ExprRef; let mut or_flat = Vec::new(); flatten_boolop(and, BoolOpKind::Or, &mut or_flat); - flatten_boolop(c, BoolOpKind::Or, &mut or_flat); + for value in or_tail { + flatten_boolop(value, BoolOpKind::Or, &mut or_flat); + } Some(( Rc::new(Expr::BoolOp { op: BoolOpKind::Or, @@ -34928,10 +34959,20 @@ if split_cond { // (the per-jump value_merge blocks below would mis-nest it). // !jump_if_true path only, where `c` is `cond` un-negated. if !jump_if_true { - if let Some((merged, m)) = self.try_value_and_or_314(&c, target) { - self.push(merged); - self.skip_until = Some(m); - return; + if let Some((merged, m)) = self.try_value_and_or(&c, target) { + // COPY kept this exact operand below its consumed truth + // test. The folded expression already owns that value; + // leaving it live creates an extra call argument (or steals + // the caller's callee/NULL prefix). Consume only this saved + // operand, preserving every unrelated stack entry. + if self.stack.last().map_or(false, |value| { + matches!(value, Sv::E(saved) if expr_eq(saved, &c)) + }) { + self.stack.pop(); + self.push(merged); + self.skip_until = Some(m); + return; + } } } // 3.12+ chained comparison: the then arm recomputes the next link From 457eb8f41173cc1ce8487f6dba90aeb8ebbef69b Mon Sep 17 00:00:00 2001 From: ejfkdev <152914458+ejfkdev@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:10:40 +0800 Subject: [PATCH 23/25] PR26 src --- src/decompiler.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/decompiler.rs b/src/decompiler.rs index d088bbb5..f22428f2 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -30905,6 +30905,100 @@ return None; return; } } + // A 3.12+ lookup rejection guard has exactly one fall-through back + // edge before its success target. Preserve it as `if: continue` only + // when the success span assigns the local checked by an independent + // None guard and returned after that guard. Other suites retain their + // existing handling; changing protected suites also changes their + // exception-region boundaries. + // Treating the success region as an If suite bounded by the NEXT + // back edge closes that suite at an independent later guard and + // ejects the return that relies on its assignments (a coordinator + // lookup then returns an unbound local for non-dictionary rows). + // Inline comprehension filters retain their own bookkeeping. A + // nested while loop is not this for loop's rejection trampoline. + if self.version.at_least(3, 12) && self.inline_comp.is_none() + && self.code.exception_entries().is_empty() + && raw_target == target && target > self.cur_next + { + let current_loop = self.blocks.iter().rev() + .find(|block| matches!(block.kind, BlockType::For | BlockType::While)) + .filter(|block| block.kind == BlockType::For) + .map(|block| (block.start, block.end)); + let trampoline = self.idx_of.get(&self.cur_next).map(|&index| { + self.instrs[index..].iter().take_while(|item| item.offset < target) + .filter(|item| !matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .collect::>() + }); + if let (Some((top, loop_end)), Some(steps)) = (current_loop, trampoline) { + let dependent_return = self.idx_of.get(&target).map_or(false, |&index| { + let body = self.instrs[index..].iter().take_while(|item| item.offset < loop_end) + .filter(|item| !matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .collect::>(); + let guard = match body.iter().position(|item| item.target.is_some() + || matches!(item.op, Op::RETURN_VALUE | Op::RETURN_CONST | Op::RAISE_VARARGS | Op::RERAISE)) { + Some(index) => index, None => return false, + }; + if guard < 2 || !matches!(body[guard].op, Op::POP_JUMP_IF_NONE | Op::POP_JUMP_IF_NOT_NONE) { + return false; + } + let stored = body[..guard].iter().rev().find_map(|item| match item.op { + Op::STORE_FAST => Some(item.arg), + Op::STORE_FAST_LOAD_FAST => Some(item.arg >> 4), + Op::STORE_FAST_STORE_FAST => Some(item.arg & 15), + _ => None, + }); + let local = match stored { Some(local) => local, None => return false }; + let tested = body[guard - 1]; + if !(matches!(tested.op, Op::LOAD_FAST | Op::LOAD_FAST_BORROW | Op::LOAD_FAST_CHECK) + && tested.arg == local + || tested.op == Op::STORE_FAST_LOAD_FAST && tested.arg & 15 == local) { + return false; + } + let back = match body.get(guard + 1) { Some(back) => back, None => return false }; + if !matches!(back.op, Op::JUMP_BACKWARD | Op::JUMP_BACKWARD_NO_INTERRUPT) + || back.target != Some(top) || body[guard].target != Some(back.end()) { + return false; + } + let returned = match body.get(guard + 2) { Some(value) => value, None => return false }; + if !matches!(returned.op, Op::LOAD_FAST | Op::LOAD_FAST_BORROW | Op::LOAD_FAST_CHECK) + || returned.arg != local { return false; } + let mut tail = guard + 3; + while tail + 1 < body.len() && body[tail].op == Op::SWAP + && body[tail].arg == 2 && body[tail + 1].op == Op::POP_TOP { + tail += 2; + } + body.get(tail).map_or(false, |item| item.op == Op::RETURN_VALUE) + }); + if steps.len() == 1 + && dependent_return + && matches!(steps[0].op, Op::JUMP_BACKWARD | Op::JUMP_BACKWARD_NO_INTERRUPT) + && steps[0].target == Some(top) && steps[0].end() == target + { + self.push_stmt(Stmt::If { + // This layout negates a comparison guard by its jump, not + // its operator. Folding `<` to `>=` changes NaN and + // user comparison hooks. None-identity guards use + // their dedicated jump spelling and can invert IS. + cond: if jump_if_true { + match &*cond { + Expr::Compare { operands, ops } + if ops.len() == 1 + && matches!(ops[0], CmpOp::Is | CmpOp::IsNot) + && operands.len() == 2 + && operands.iter().any(|value| matches!(&**value, + Expr::Const(object) if matches!(&**object, PyObject::None))) => + { negate_cond(cond) } + _ => simplify_not_or_wrap(cond), + } + } else { cond }, + body: vec![Stmt::Continue], orelse: Vec::new(), + }); + self.skip_until = Some(target); + return; + } + } + } if self.version.at_least(3, 12) { if let Some((merged, body_end, tramp_end)) = self.try_or_join(&cond, jump_if_true, target) From 297187b8a1cd1d609b00cd2eeb4ae790550b6b9a Mon Sep 17 00:00:00 2001 From: ejfkdev <152914458+ejfkdev@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:19:36 +0800 Subject: [PATCH 24/25] test fixtures and behavior cases from all PRs --- .../cases/b_awaited_synchronous_iteration.py | 14 ++ .../cases/b_boolean_call_arguments.py | 31 +++++ .../cases/b_chained_comprehension_filters.py | 25 ++++ .../cases/b_class_annotation_registration.py | 31 +++++ .../cases/b_comprehension_boolean_values.py | 27 ++++ .../b_comprehension_dictionary_displays.py | 25 ++++ .../cases/b_conditional_loop_prefix.py | 63 +++++++++ .../behavior/cases/b_extended_loop_prefix.py | 50 +++++++ tests/behavior/cases/b_guarded_else_merge.py | 29 +++++ .../cases/b_keyword_conditional_calls.py | 25 ++++ tests/behavior/cases/b_loop_lookup_guards.py | 37 ++++++ .../cases/b_nested_conditional_elements.py | 18 +++ .../b_nested_generator_comprehensions.py | 26 ++++ .../cases/b_ordered_keyword_expansions.py | 32 +++++ .../cases/b_repeated_unpack_bindings.py | 31 +++++ .../cases/b_slice_constants_and_calls.py | 35 +++++ .../cases/b_unpacked_comprehension_cleanup.py | 33 +++++ .../3.11.14/handler_return_padding_3.py | 15 +++ .../3.11.14/handler_return_padding_3.pyc | Bin 0 -> 542 bytes .../3.11.14/out/handler_return_padding_3.py | 12 ++ .../3.11.14/out/padded_value_returns_3.py | 14 ++ .../corpus/3.11.14/padded_value_returns_3.py | 19 +++ .../corpus/3.11.14/padded_value_returns_3.pyc | Bin 0 -> 681 bytes tests/corpus/3.11.14/report.json | 17 +++ .../corpus/3.12.3/handler_return_padding_3.py | 15 +++ .../3.12.3/handler_return_padding_3.pyc | Bin 0 -> 516 bytes .../3.12.3/out/handler_return_padding_3.py | 12 ++ .../3.12.3/out/padded_value_returns_3.py | 14 ++ tests/corpus/3.12.3/padded_value_returns_3.py | 19 +++ .../corpus/3.12.3/padded_value_returns_3.pyc | Bin 0 -> 645 bytes tests/corpus/3.12.3/report.json | 17 +++ tests/corpus/3.13.15/out/code.py | 4 +- .../corpus/3.13.7/handler_return_padding_3.py | 15 +++ .../3.13.7/handler_return_padding_3.pyc | Bin 0 -> 520 bytes .../3.13.7/out/handler_return_padding_3.py | 12 ++ .../3.13.7/out/padded_value_returns_3.py | 14 ++ tests/corpus/3.13.7/padded_value_returns_3.py | 19 +++ .../corpus/3.13.7/padded_value_returns_3.pyc | Bin 0 -> 649 bytes tests/corpus/3.13.7/report.json | 17 +++ .../corpus/3.14.2/handler_return_padding_3.py | 15 +++ .../3.14.2/handler_return_padding_3.pyc | Bin 0 -> 557 bytes .../3.14.2/out/handler_return_padding_3.py | 12 ++ .../3.14.2/out/padded_value_returns_3.py | 14 ++ tests/corpus/3.14.2/padded_value_returns_3.py | 19 +++ .../corpus/3.14.2/padded_value_returns_3.pyc | Bin 0 -> 657 bytes tests/corpus/3.14.2/report.json | 17 +++ tests/corpus/3.14.7/out/annotationlib.py | 5 +- tests/corpus/3.14.7/out/code.py | 4 +- tests/corpus/summary.json | 24 ++++ .../fixtures/awaited_synchronous_iteration.py | 23 ++++ tests/fixtures/boolean_call_arguments.py | 35 +++++ .../fixtures/chained_comprehension_filters.py | 61 +++++++++ .../fixtures/class_annotation_registration.py | 30 +++++ .../fixtures/comprehension_boolean_values.py | 38 ++++++ .../comprehension_dictionary_displays.py | 51 ++++++++ tests/fixtures/conditional_loop_prefix.py | 20 +++ tests/fixtures/extended_loop_prefix.py | 123 ++++++++++++++++++ tests/fixtures/guarded_else_merge.py | 67 ++++++++++ tests/fixtures/handler_return_padding.py | 15 +++ tests/fixtures/keyword_call_arm_boundary.py | 5 + tests/fixtures/keyword_conditional_calls.py | 22 ++++ tests/fixtures/loop_lookup_guards.py | 33 +++++ tests/fixtures/nested_conditional_elements.py | 20 +++ .../nested_generator_comprehensions.py | 22 ++++ tests/fixtures/ordered_keyword_expansions.py | 30 +++++ tests/fixtures/padded_value_returns.py | 19 +++ tests/fixtures/repeated_unpack_bindings.py | 35 +++++ tests/fixtures/slice_constants_and_calls.py | 23 ++++ tests/fixtures/sunk_value_return_control.py | 10 ++ .../unpacked_comprehension_cleanup.py | 75 +++++++++++ tests/integration.rs | 2 - .../awaited_synchronous_iteration.3.10.pyc | Bin 0 -> 595 bytes .../awaited_synchronous_iteration.3.11.pyc | Bin 0 -> 989 bytes .../awaited_synchronous_iteration.3.12.pyc | Bin 0 -> 915 bytes .../awaited_synchronous_iteration.3.13.pyc | Bin 0 -> 908 bytes .../awaited_synchronous_iteration.3.14.pyc | Bin 0 -> 917 bytes .../pyc/awaited_synchronous_iteration.3.8.pyc | Bin 0 -> 579 bytes .../pyc/awaited_synchronous_iteration.3.9.pyc | Bin 0 -> 579 bytes tests/pyc/boolean_call_arguments.3.11.pyc | Bin 0 -> 2347 bytes tests/pyc/boolean_call_arguments.3.12.pyc | Bin 0 -> 2177 bytes tests/pyc/boolean_call_arguments.3.13.pyc | Bin 0 -> 2503 bytes tests/pyc/boolean_call_arguments.3.14.pyc | Bin 0 -> 2661 bytes .../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 .../class_annotation_registration.3.14.pyc | Bin 0 -> 2944 bytes .../pyc/comprehension_boolean_values.3.12.pyc | Bin 0 -> 2373 bytes .../pyc/comprehension_boolean_values.3.13.pyc | Bin 0 -> 2553 bytes .../pyc/comprehension_boolean_values.3.14.pyc | Bin 0 -> 2656 bytes ...comprehension_dictionary_displays.3.10.pyc | Bin 0 -> 3312 bytes ...comprehension_dictionary_displays.3.11.pyc | Bin 0 -> 4793 bytes ...comprehension_dictionary_displays.3.12.pyc | Bin 0 -> 4188 bytes ...comprehension_dictionary_displays.3.13.pyc | Bin 0 -> 4244 bytes ...comprehension_dictionary_displays.3.14.pyc | Bin 0 -> 4490 bytes tests/pyc/conditional_loop_prefix.3.11.pyc | Bin 0 -> 949 bytes tests/pyc/conditional_loop_prefix.3.12.pyc | Bin 0 -> 1224 bytes tests/pyc/conditional_loop_prefix.3.13.pyc | Bin 0 -> 1276 bytes tests/pyc/conditional_loop_prefix.3.14.pyc | Bin 0 -> 1285 bytes tests/pyc/extended_loop_prefix.3.11.pyc | Bin 0 -> 4315 bytes tests/pyc/extended_loop_prefix.3.12.pyc | Bin 0 -> 3100 bytes tests/pyc/extended_loop_prefix.3.13.pyc | Bin 0 -> 3107 bytes tests/pyc/extended_loop_prefix.3.14.pyc | Bin 0 -> 3115 bytes tests/pyc/guarded_else_merge.3.11.pyc | Bin 0 -> 2285 bytes tests/pyc/guarded_else_merge.3.12.pyc | Bin 0 -> 2104 bytes tests/pyc/guarded_else_merge.3.13.pyc | Bin 0 -> 2212 bytes tests/pyc/guarded_else_merge.3.14.pyc | Bin 0 -> 2291 bytes tests/pyc/handler_return_padding_1.3.11.pyc | Bin 0 -> 530 bytes tests/pyc/handler_return_padding_1.3.12.pyc | Bin 0 -> 516 bytes tests/pyc/handler_return_padding_1.3.13.pyc | Bin 0 -> 520 bytes tests/pyc/handler_return_padding_1.3.14.pyc | Bin 0 -> 545 bytes tests/pyc/handler_return_padding_3.3.11.pyc | Bin 0 -> 542 bytes tests/pyc/handler_return_padding_3.3.12.pyc | Bin 0 -> 516 bytes tests/pyc/handler_return_padding_3.3.13.pyc | Bin 0 -> 520 bytes tests/pyc/handler_return_padding_3.3.14.pyc | Bin 0 -> 557 bytes tests/pyc/handler_return_padding_9.3.11.pyc | Bin 0 -> 579 bytes tests/pyc/handler_return_padding_9.3.12.pyc | Bin 0 -> 516 bytes tests/pyc/handler_return_padding_9.3.13.pyc | Bin 0 -> 520 bytes tests/pyc/handler_return_padding_9.3.14.pyc | Bin 0 -> 594 bytes tests/pyc/keyword_call_arm_boundary.3.13.pyc | Bin 0 -> 416 bytes tests/pyc/keyword_call_arm_boundary.3.14.pyc | Bin 0 -> 426 bytes tests/pyc/keyword_conditional_calls.3.13.pyc | Bin 0 -> 1128 bytes tests/pyc/keyword_conditional_calls.3.14.pyc | Bin 0 -> 1149 bytes tests/pyc/loop_lookup_guards.3.11.pyc | Bin 0 -> 1443 bytes tests/pyc/loop_lookup_guards.3.12.pyc | Bin 0 -> 1195 bytes tests/pyc/loop_lookup_guards.3.13.pyc | Bin 0 -> 1246 bytes tests/pyc/loop_lookup_guards.3.14.pyc | Bin 0 -> 1266 bytes .../pyc/nested_conditional_elements.3.12.pyc | Bin 0 -> 1483 bytes .../pyc/nested_conditional_elements.3.13.pyc | Bin 0 -> 1602 bytes .../pyc/nested_conditional_elements.3.14.pyc | Bin 0 -> 1671 bytes .../nested_generator_comprehensions.3.11.pyc | Bin 0 -> 3698 bytes .../nested_generator_comprehensions.3.12.pyc | Bin 0 -> 2604 bytes .../nested_generator_comprehensions.3.13.pyc | Bin 0 -> 2700 bytes .../nested_generator_comprehensions.3.14.pyc | Bin 0 -> 3144 bytes tests/pyc/ordered_keyword_expansions.3.10.pyc | Bin 0 -> 1243 bytes tests/pyc/ordered_keyword_expansions.3.11.pyc | Bin 0 -> 1717 bytes tests/pyc/ordered_keyword_expansions.3.12.pyc | Bin 0 -> 1513 bytes tests/pyc/ordered_keyword_expansions.3.13.pyc | Bin 0 -> 1513 bytes tests/pyc/ordered_keyword_expansions.3.14.pyc | Bin 0 -> 1497 bytes tests/pyc/ordered_keyword_expansions.3.9.pyc | Bin 0 -> 1309 bytes tests/pyc/padded_value_returns_1.3.11.pyc | Bin 0 -> 665 bytes tests/pyc/padded_value_returns_1.3.12.pyc | Bin 0 -> 629 bytes tests/pyc/padded_value_returns_1.3.13.pyc | Bin 0 -> 633 bytes tests/pyc/padded_value_returns_1.3.14.pyc | Bin 0 -> 641 bytes tests/pyc/padded_value_returns_3.3.11.pyc | Bin 0 -> 681 bytes tests/pyc/padded_value_returns_3.3.12.pyc | Bin 0 -> 645 bytes tests/pyc/padded_value_returns_3.3.13.pyc | Bin 0 -> 649 bytes tests/pyc/padded_value_returns_3.3.14.pyc | Bin 0 -> 657 bytes tests/pyc/padded_value_returns_9.3.11.pyc | Bin 0 -> 729 bytes tests/pyc/padded_value_returns_9.3.12.pyc | Bin 0 -> 701 bytes tests/pyc/padded_value_returns_9.3.13.pyc | Bin 0 -> 705 bytes tests/pyc/padded_value_returns_9.3.14.pyc | Bin 0 -> 713 bytes tests/pyc/repeated_unpack_bindings.3.12.pyc | Bin 0 -> 1561 bytes tests/pyc/repeated_unpack_bindings.3.13.pyc | Bin 0 -> 1553 bytes tests/pyc/repeated_unpack_bindings.3.14.pyc | Bin 0 -> 1549 bytes tests/pyc/slice_constants_and_calls.3.10.pyc | Bin 0 -> 768 bytes tests/pyc/slice_constants_and_calls.3.11.pyc | Bin 0 -> 1055 bytes tests/pyc/slice_constants_and_calls.3.12.pyc | Bin 0 -> 921 bytes tests/pyc/slice_constants_and_calls.3.13.pyc | Bin 0 -> 921 bytes tests/pyc/slice_constants_and_calls.3.14.pyc | Bin 0 -> 945 bytes tests/pyc/slice_constants_and_calls.3.8.pyc | Bin 0 -> 770 bytes tests/pyc/slice_constants_and_calls.3.9.pyc | Bin 0 -> 770 bytes tests/pyc/sunk_value_return_control.3.11.pyc | Bin 0 -> 460 bytes tests/pyc/sunk_value_return_control.3.12.pyc | Bin 0 -> 444 bytes tests/pyc/sunk_value_return_control.3.13.pyc | Bin 0 -> 438 bytes tests/pyc/sunk_value_return_control.3.14.pyc | Bin 0 -> 441 bytes .../unpacked_comprehension_cleanup.3.11.pyc | Bin 0 -> 4364 bytes .../unpacked_comprehension_cleanup.3.12.pyc | Bin 0 -> 3386 bytes .../unpacked_comprehension_cleanup.3.13.pyc | Bin 0 -> 3220 bytes .../unpacked_comprehension_cleanup.3.14.pyc | Bin 0 -> 3278 bytes tools/compile_padded_returns.py | 81 ++++++++++++ tools/compile_padded_value_returns.py | 81 ++++++++++++ 172 files changed, 1790 insertions(+), 8 deletions(-) create mode 100644 tests/behavior/cases/b_awaited_synchronous_iteration.py create mode 100644 tests/behavior/cases/b_boolean_call_arguments.py create mode 100644 tests/behavior/cases/b_chained_comprehension_filters.py create mode 100644 tests/behavior/cases/b_class_annotation_registration.py create mode 100644 tests/behavior/cases/b_comprehension_boolean_values.py create mode 100644 tests/behavior/cases/b_comprehension_dictionary_displays.py create mode 100644 tests/behavior/cases/b_conditional_loop_prefix.py create mode 100644 tests/behavior/cases/b_extended_loop_prefix.py create mode 100644 tests/behavior/cases/b_guarded_else_merge.py create mode 100644 tests/behavior/cases/b_keyword_conditional_calls.py create mode 100644 tests/behavior/cases/b_loop_lookup_guards.py create mode 100644 tests/behavior/cases/b_nested_conditional_elements.py create mode 100644 tests/behavior/cases/b_nested_generator_comprehensions.py create mode 100644 tests/behavior/cases/b_ordered_keyword_expansions.py create mode 100644 tests/behavior/cases/b_repeated_unpack_bindings.py create mode 100644 tests/behavior/cases/b_slice_constants_and_calls.py create mode 100644 tests/behavior/cases/b_unpacked_comprehension_cleanup.py create mode 100644 tests/corpus/3.11.14/handler_return_padding_3.py create mode 100644 tests/corpus/3.11.14/handler_return_padding_3.pyc create mode 100644 tests/corpus/3.11.14/out/handler_return_padding_3.py create mode 100644 tests/corpus/3.11.14/out/padded_value_returns_3.py create mode 100644 tests/corpus/3.11.14/padded_value_returns_3.py create mode 100644 tests/corpus/3.11.14/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.11.14/report.json create mode 100644 tests/corpus/3.12.3/handler_return_padding_3.py create mode 100644 tests/corpus/3.12.3/handler_return_padding_3.pyc create mode 100644 tests/corpus/3.12.3/out/handler_return_padding_3.py create mode 100644 tests/corpus/3.12.3/out/padded_value_returns_3.py create mode 100644 tests/corpus/3.12.3/padded_value_returns_3.py create mode 100644 tests/corpus/3.12.3/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.12.3/report.json create mode 100644 tests/corpus/3.13.7/handler_return_padding_3.py create mode 100644 tests/corpus/3.13.7/handler_return_padding_3.pyc create mode 100644 tests/corpus/3.13.7/out/handler_return_padding_3.py create mode 100644 tests/corpus/3.13.7/out/padded_value_returns_3.py create mode 100644 tests/corpus/3.13.7/padded_value_returns_3.py create mode 100644 tests/corpus/3.13.7/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.13.7/report.json create mode 100644 tests/corpus/3.14.2/handler_return_padding_3.py create mode 100644 tests/corpus/3.14.2/handler_return_padding_3.pyc create mode 100644 tests/corpus/3.14.2/out/handler_return_padding_3.py create mode 100644 tests/corpus/3.14.2/out/padded_value_returns_3.py create mode 100644 tests/corpus/3.14.2/padded_value_returns_3.py create mode 100644 tests/corpus/3.14.2/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.14.2/report.json create mode 100644 tests/fixtures/awaited_synchronous_iteration.py create mode 100644 tests/fixtures/boolean_call_arguments.py create mode 100644 tests/fixtures/chained_comprehension_filters.py create mode 100644 tests/fixtures/class_annotation_registration.py create mode 100644 tests/fixtures/comprehension_boolean_values.py create mode 100644 tests/fixtures/comprehension_dictionary_displays.py create mode 100644 tests/fixtures/conditional_loop_prefix.py create mode 100644 tests/fixtures/extended_loop_prefix.py create mode 100644 tests/fixtures/guarded_else_merge.py create mode 100644 tests/fixtures/handler_return_padding.py create mode 100644 tests/fixtures/keyword_call_arm_boundary.py create mode 100644 tests/fixtures/keyword_conditional_calls.py create mode 100644 tests/fixtures/loop_lookup_guards.py create mode 100644 tests/fixtures/nested_conditional_elements.py create mode 100644 tests/fixtures/nested_generator_comprehensions.py create mode 100644 tests/fixtures/ordered_keyword_expansions.py create mode 100644 tests/fixtures/padded_value_returns.py create mode 100644 tests/fixtures/repeated_unpack_bindings.py create mode 100644 tests/fixtures/slice_constants_and_calls.py create mode 100644 tests/fixtures/sunk_value_return_control.py create mode 100644 tests/fixtures/unpacked_comprehension_cleanup.py create mode 100644 tests/pyc/awaited_synchronous_iteration.3.10.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.11.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.12.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.13.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.14.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.8.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.9.pyc create mode 100644 tests/pyc/boolean_call_arguments.3.11.pyc create mode 100644 tests/pyc/boolean_call_arguments.3.12.pyc create mode 100644 tests/pyc/boolean_call_arguments.3.13.pyc create mode 100644 tests/pyc/boolean_call_arguments.3.14.pyc 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 create mode 100644 tests/pyc/class_annotation_registration.3.14.pyc create mode 100644 tests/pyc/comprehension_boolean_values.3.12.pyc create mode 100644 tests/pyc/comprehension_boolean_values.3.13.pyc create mode 100644 tests/pyc/comprehension_boolean_values.3.14.pyc create mode 100644 tests/pyc/comprehension_dictionary_displays.3.10.pyc create mode 100644 tests/pyc/comprehension_dictionary_displays.3.11.pyc create mode 100644 tests/pyc/comprehension_dictionary_displays.3.12.pyc create mode 100644 tests/pyc/comprehension_dictionary_displays.3.13.pyc create mode 100644 tests/pyc/comprehension_dictionary_displays.3.14.pyc create mode 100644 tests/pyc/conditional_loop_prefix.3.11.pyc create mode 100644 tests/pyc/conditional_loop_prefix.3.12.pyc create mode 100644 tests/pyc/conditional_loop_prefix.3.13.pyc create mode 100644 tests/pyc/conditional_loop_prefix.3.14.pyc create mode 100644 tests/pyc/extended_loop_prefix.3.11.pyc create mode 100644 tests/pyc/extended_loop_prefix.3.12.pyc create mode 100644 tests/pyc/extended_loop_prefix.3.13.pyc create mode 100644 tests/pyc/extended_loop_prefix.3.14.pyc create mode 100644 tests/pyc/guarded_else_merge.3.11.pyc create mode 100644 tests/pyc/guarded_else_merge.3.12.pyc create mode 100644 tests/pyc/guarded_else_merge.3.13.pyc create mode 100644 tests/pyc/guarded_else_merge.3.14.pyc create mode 100644 tests/pyc/handler_return_padding_1.3.11.pyc create mode 100644 tests/pyc/handler_return_padding_1.3.12.pyc create mode 100644 tests/pyc/handler_return_padding_1.3.13.pyc create mode 100644 tests/pyc/handler_return_padding_1.3.14.pyc create mode 100644 tests/pyc/handler_return_padding_3.3.11.pyc create mode 100644 tests/pyc/handler_return_padding_3.3.12.pyc create mode 100644 tests/pyc/handler_return_padding_3.3.13.pyc create mode 100644 tests/pyc/handler_return_padding_3.3.14.pyc create mode 100644 tests/pyc/handler_return_padding_9.3.11.pyc create mode 100644 tests/pyc/handler_return_padding_9.3.12.pyc create mode 100644 tests/pyc/handler_return_padding_9.3.13.pyc create mode 100644 tests/pyc/handler_return_padding_9.3.14.pyc create mode 100644 tests/pyc/keyword_call_arm_boundary.3.13.pyc create mode 100644 tests/pyc/keyword_call_arm_boundary.3.14.pyc create mode 100644 tests/pyc/keyword_conditional_calls.3.13.pyc create mode 100644 tests/pyc/keyword_conditional_calls.3.14.pyc create mode 100644 tests/pyc/loop_lookup_guards.3.11.pyc create mode 100644 tests/pyc/loop_lookup_guards.3.12.pyc create mode 100644 tests/pyc/loop_lookup_guards.3.13.pyc create mode 100644 tests/pyc/loop_lookup_guards.3.14.pyc create mode 100644 tests/pyc/nested_conditional_elements.3.12.pyc create mode 100644 tests/pyc/nested_conditional_elements.3.13.pyc create mode 100644 tests/pyc/nested_conditional_elements.3.14.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.11.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.12.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.13.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.14.pyc create mode 100644 tests/pyc/ordered_keyword_expansions.3.10.pyc create mode 100644 tests/pyc/ordered_keyword_expansions.3.11.pyc create mode 100644 tests/pyc/ordered_keyword_expansions.3.12.pyc create mode 100644 tests/pyc/ordered_keyword_expansions.3.13.pyc create mode 100644 tests/pyc/ordered_keyword_expansions.3.14.pyc create mode 100644 tests/pyc/ordered_keyword_expansions.3.9.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.11.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.12.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.13.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.14.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.11.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.12.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.13.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.14.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.11.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.12.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.13.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.14.pyc create mode 100644 tests/pyc/repeated_unpack_bindings.3.12.pyc create mode 100644 tests/pyc/repeated_unpack_bindings.3.13.pyc create mode 100644 tests/pyc/repeated_unpack_bindings.3.14.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.10.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.11.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.12.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.13.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.14.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.8.pyc create mode 100644 tests/pyc/slice_constants_and_calls.3.9.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.11.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.12.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.13.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.14.pyc create mode 100644 tests/pyc/unpacked_comprehension_cleanup.3.11.pyc create mode 100644 tests/pyc/unpacked_comprehension_cleanup.3.12.pyc create mode 100644 tests/pyc/unpacked_comprehension_cleanup.3.13.pyc create mode 100644 tests/pyc/unpacked_comprehension_cleanup.3.14.pyc create mode 100644 tools/compile_padded_returns.py create mode 100644 tools/compile_padded_value_returns.py diff --git a/tests/behavior/cases/b_awaited_synchronous_iteration.py b/tests/behavior/cases/b_awaited_synchronous_iteration.py new file mode 100644 index 00000000..ccf15d1a --- /dev/null +++ b/tests/behavior/cases/b_awaited_synchronous_iteration.py @@ -0,0 +1,14 @@ +# MIN_VERSION: 3.9 +import asyncio + +async def awaited(read): + result = [] + for item in await read(): + result.append(item) + return result + + +async def read(): + print("read") + return [1, 2, 3] +print(asyncio.run(awaited(read))) diff --git a/tests/behavior/cases/b_boolean_call_arguments.py b/tests/behavior/cases/b_boolean_call_arguments.py new file mode 100644 index 00000000..173d1eed --- /dev/null +++ b/tests/behavior/cases/b_boolean_call_arguments.py @@ -0,0 +1,31 @@ +# MIN_VERSION: 3.13 + + +def select(call, before, first, second, last, after): + return call(before(), (first() and second()) or last(), after()) + + +def alternatives(a, b, c, d, e): + return bool((a and b) or c or d or e) + + +def before(): + print("before") + return 1 +def first(): + print("first") + return 0 +def second(): + print("second") + return 2 +def last(): + print("last") + return 3 +def after(): + print("after") + return 4 +def call(*args): + return args +print(select(call, before, first, second, last, after)) +print(alternatives(1, 0, 0, 0, 5)) +print(alternatives(1, 2, 0, 0, 0)) 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)) diff --git a/tests/behavior/cases/b_class_annotation_registration.py b/tests/behavior/cases/b_class_annotation_registration.py new file mode 100644 index 00000000..b69c745f --- /dev/null +++ b/tests/behavior/cases/b_class_annotation_registration.py @@ -0,0 +1,31 @@ +# MIN_VERSION: 3.14 + + +class Always: + "Documented class." + if True: + amount: int = 3 + label: str = "ready" + + def read(self) -> int: + return self.amount + + +def select(enabled, values, default): + class Result: + if True: + amount: int = 3 + if enabled: + label: str = "ready" + for value in values: + limit: int = default(value) + return Result + + +print(Always.__annotate__.__code__.co_freevars) +print(sorted(Always.__annotations__)) +def default(value): + print("default", value) + return value +print(sorted(select(False, [], default).__annotations__)) +print(sorted(select(True, [3, 4], default).__annotations__)) 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/behavior/cases/b_comprehension_dictionary_displays.py b/tests/behavior/cases/b_comprehension_dictionary_displays.py new file mode 100644 index 00000000..68c35a62 --- /dev/null +++ b/tests/behavior/cases/b_comprehension_dictionary_displays.py @@ -0,0 +1,25 @@ +# MIN_VERSION: 3.10 + + +def list17(rows): + return [{ + "k0": row + 0, "k1": row + 1, "k2": row + 2, "k3": row + 3, + "k4": row + 4, "k5": row + 5, "k6": row + 6, "k7": row + 7, + "k8": row + 8, "k9": row + 9, "k10": row + 10, "k11": row + 11, + "k12": row + 12, "k13": row + 13, "k14": row + 14, "k15": row + 15, + "k16": row + 16, + } for row in rows] + + +def generator20(rows): + return ({ + "k0": row + 0, "k1": row + 1, "k2": row + 2, "k3": row + 3, + "k4": row + 4, "k5": row + 5, "k6": row + 6, "k7": row + 7, + "k8": row + 8, "k9": row + 9, "k10": row + 10, "k11": row + 11, + "k12": row + 12, "k13": row + 13, "k14": row + 14, "k15": row + 15, + "k16": row + 16, "k17": row + 17, "k18": row + 18, "k19": row + 19, + } for row in rows) + + +print(list17([0, 10])) +print(tuple(generator20([1, 20]))) diff --git a/tests/behavior/cases/b_conditional_loop_prefix.py b/tests/behavior/cases/b_conditional_loop_prefix.py new file mode 100644 index 00000000..2500953a --- /dev/null +++ b/tests/behavior/cases/b_conditional_loop_prefix.py @@ -0,0 +1,63 @@ +# MIN_VERSION: 3.11 + +# Attribute loads make FOR_ITER require EXTENDED_ARG on Python 3.12+. +# Conditional branches lead to adjacent back edges targeting that prefix. +def process(rows, work): + success = True + for row in rows: + if row.is_file: + if not work( + "file", row.ok, + row.a, row.b, row.c, row.d, row.e, row.f, + row.g, row.h, row.i, row.j, row.k, row.l, + ): + success = False + else: + if not work( + "directory", row.ok, + row.a, row.b, row.c, row.d, row.e, row.f, + row.g, row.h, row.i, row.j, row.k, row.l, + ): + success = False + return success + + +class Row: + a = 1 + b = 2 + c = 3 + d = 4 + e = 5 + f = 6 + g = 7 + h = 8 + i = 9 + j = 10 + k = 11 + l = 12 + + def __init__(self, is_file, ok): + self.is_file = is_file + self.ok = ok + + +def work(kind, ok, *values): + calls.append((kind, ok, values)) + return ok + + +for outcomes in [ + [], + [(True, True)], + [(True, False)], + [(False, True)], + [(False, False)], + [(True, True), (False, True)], + [(False, True), (True, True)], + [(True, False), (False, True), (True, True)], + [(False, False), (True, True), (False, True)], +]: + calls = [] + rows = [Row(is_file, ok) for is_file, ok in outcomes] + print(process(rows, work)) + print(calls) diff --git a/tests/behavior/cases/b_extended_loop_prefix.py b/tests/behavior/cases/b_extended_loop_prefix.py new file mode 100644 index 00000000..f8203427 --- /dev/null +++ b/tests/behavior/cases/b_extended_loop_prefix.py @@ -0,0 +1,50 @@ +# MIN_VERSION: 3.11 + + +def size_30(rows, work): + result = [] + for row in rows: + try: + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + except ValueError: + continue + result.append(row) + return result + + +calls = [] +def work(row): + calls.append(row) + if row == 2: + raise ValueError(row) +print(size_30([1, 2, 3], work)) +print(calls) diff --git a/tests/behavior/cases/b_guarded_else_merge.py b/tests/behavior/cases/b_guarded_else_merge.py new file mode 100644 index 00000000..a2692415 --- /dev/null +++ b/tests/behavior/cases/b_guarded_else_merge.py @@ -0,0 +1,29 @@ +# MIN_VERSION: 3.11 +import asyncio + +async def awaited(owner): + value = None + active = owner.active + if active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = float(row[0]) + owner.record(value) + return value + + +class Owner: + def __init__(self, active, row): + self.active = active + self.row = row + async def fetch(self): + print("fetch", self.row) + return self.row + def record(self, value): + print("record", value) +print(asyncio.run(awaited(Owner(True, (9,))))) +print(asyncio.run(awaited(Owner(False, (9,))))) +print(asyncio.run(awaited(Owner(False, (None,))))) +print(asyncio.run(awaited(Owner(False, None)))) diff --git a/tests/behavior/cases/b_keyword_conditional_calls.py b/tests/behavior/cases/b_keyword_conditional_calls.py new file mode 100644 index 00000000..ad3f42fe --- /dev/null +++ b/tests/behavior/cases/b_keyword_conditional_calls.py @@ -0,0 +1,25 @@ +# MIN_VERSION: 3.13 + + +def case_0(value, build, other): + return build("reject" if value else "accept", amount=0) + + +def case_1(value, build, other): + return build("prefix", amount=0 if value else 1) + + +def case_9(value, build, other): + return build("prefix", amount=1 if value else 2, last=other(value)) + + +def build(*args, **kwargs): + print("build", args, sorted(kwargs.items())) + return args +def other(value): + print("other", value) + return 8 +print(case_0(False, build, other)) +print(case_0(True, build, other)) +print(case_1(False, build, other)) +print(case_9(True, build, other)) diff --git a/tests/behavior/cases/b_loop_lookup_guards.py b/tests/behavior/cases/b_loop_lookup_guards.py new file mode 100644 index 00000000..558b76a2 --- /dev/null +++ b/tests/behavior/cases/b_loop_lookup_guards.py @@ -0,0 +1,37 @@ +# MIN_VERSION: 3.12 + + +def rating_lookup(values): + for value in values: + if not value.rating < 10: + continue + coordinator = value.coordinator + if coordinator is None: + continue + return coordinator + + +def dictionary_lookup(rows): + for row in rows: + if not isinstance(row, dict): + continue + coordinator = row.get("coordinator") + if coordinator is None: + continue + return coordinator + + +class Rating: + def __init__(self, value): + self.value = value + def __lt__(self, limit): + print("lt", self.value, limit) + return self.value < limit + def __ge__(self, limit): + raise AssertionError("comparison must not be inverted") +class Row: + def __init__(self, rating, coordinator): + self.rating = Rating(rating) + self.coordinator = coordinator +print(rating_lookup([Row(float("nan"), 8), Row(3, None), Row(4, 0)])) +print(dictionary_lookup([1, {}, {"coordinator": False}])) diff --git a/tests/behavior/cases/b_nested_conditional_elements.py b/tests/behavior/cases/b_nested_conditional_elements.py new file mode 100644 index 00000000..f3578c49 --- /dev/null +++ b/tests/behavior/cases/b_nested_conditional_elements.py @@ -0,0 +1,18 @@ +# MIN_VERSION: 3.12 + + +def select(rows): + return [{ + "excluded": ({"count": row["count"], + "reasons": loads(row["reasons"]) if row["reasons"] else {}} + if row["count"] is not None else {}), + "archived": str(row["archived"]) if row["archived"] else None, + } for row in rows] + + +def loads(value): + print("loads", value) + return {"decoded": value} +print(select([{"count": 1, "reasons": "why", "archived": 3}, + {"count": None, "reasons": "skip", "archived": 0}, + {"count": 2, "reasons": "", "archived": None}])) diff --git a/tests/behavior/cases/b_nested_generator_comprehensions.py b/tests/behavior/cases/b_nested_generator_comprehensions.py new file mode 100644 index 00000000..33da158d --- /dev/null +++ b/tests/behavior/cases/b_nested_generator_comprehensions.py @@ -0,0 +1,26 @@ +# MIN_VERSION: 3.12 + + +def dictionary(rows): + return tuple({key: row[key] for key in row} for row in rows) + + +def call_arguments(rows, collect): + return tuple(collect([value * 2 for value in row]) for row in rows) + + +def filtered(rows): + return tuple({key: row[key] for key in row if key} for row in rows if row) + + +def unpacked(rows, columns): + return tuple({key: value for key, value in zip(columns, row) if key} for row in rows) + + +def collect(values): + print("collect", values) + return sum(values) +print(dictionary([{"a": 1, "b": 2}, {}, {"c": 3}])) +print(call_arguments([[1, 2], [], [3]], collect)) +print(filtered([{}, {"": 0, "a": 1}])) +print(unpacked([[1, 2], [3, 4]], ["a", "b"])) diff --git a/tests/behavior/cases/b_ordered_keyword_expansions.py b/tests/behavior/cases/b_ordered_keyword_expansions.py new file mode 100644 index 00000000..1d45533c --- /dev/null +++ b/tests/behavior/cases/b_ordered_keyword_expansions.py @@ -0,0 +1,32 @@ +# MIN_VERSION: 3.9 + + +def direct(fn, first, second): + return fn(1, fixed=2, **first, **second) + + +def dictionary(fn, first, second): + return fn(1, **{"fixed": 2, **first, **second}) + + +def effectful(fn, get_first, get_second): + return fn(1, fixed=2, **get_first(), **get_second()) + + +def fn(*args, **kwargs): + print("call", args, sorted(kwargs.items())) +def first(): + print("first") + return {"fixed": 7} +def second(): + print("second") + return {"extra": 8} +try: + direct(fn, {"fixed": 7}, {"extra": 8}) +except TypeError: + print("duplicate") +dictionary(fn, {"fixed": 7}, {"extra": 8}) +try: + effectful(fn, first, second) +except TypeError: + print("duplicate before second") diff --git a/tests/behavior/cases/b_repeated_unpack_bindings.py b/tests/behavior/cases/b_repeated_unpack_bindings.py new file mode 100644 index 00000000..40f05f9b --- /dev/null +++ b/tests/behavior/cases/b_repeated_unpack_bindings.py @@ -0,0 +1,31 @@ +# MIN_VERSION: 3.13 + + +def underscore_loop(values): + result = [] + for value, _, discarded, discarded in values: + result.append((value, discarded, _)) + return result + + +def underscore_list(values): + return [(value, discarded, _) for value, _, discarded, discarded in values] + + +def nested_loop(values): + result = [] + for value, (discarded, discarded) in values: + result.append((value, discarded)) + return result + + +def nested_list(values): + return [(value, discarded) for value, (discarded, discarded) in values] + + +rows = [(1, 2, 3, 4), (5, 6, 7, 8)] +print(underscore_loop(rows)) +print(underscore_list(rows)) +nested = [(1, (2, 3)), (4, (5, 6))] +print(nested_loop(nested)) +print(nested_list(nested)) diff --git a/tests/behavior/cases/b_slice_constants_and_calls.py b/tests/behavior/cases/b_slice_constants_and_calls.py new file mode 100644 index 00000000..d8301971 --- /dev/null +++ b/tests/behavior/cases/b_slice_constants_and_calls.py @@ -0,0 +1,35 @@ +# MIN_VERSION: 3.9 + + +def dynamic_axes(data, stop): + return data[:stop, :] + + +def constant_axes(data, stop): + return data[:, :] + + +def mixed_axes(data, stop): + return data[1:stop:2, :, 7] + + +def call(data, stop): + return data[slice(None, stop, None)] + + +def call_axes(data, stop): + return data[slice(None, stop, None), :] + + +class Data: + def __getitem__(self, key): + return repr(key) +def slice(*args): + print("custom slice", args) + return args +data = Data() +print(dynamic_axes(data, 4)) +print(constant_axes(data, 4)) +print(mixed_axes(data, 4)) +print(call(data, 4)) +print(call_axes(data, 4)) diff --git a/tests/behavior/cases/b_unpacked_comprehension_cleanup.py b/tests/behavior/cases/b_unpacked_comprehension_cleanup.py new file mode 100644 index 00000000..2b30f9b5 --- /dev/null +++ b/tests/behavior/cases/b_unpacked_comprehension_cleanup.py @@ -0,0 +1,33 @@ +# MIN_VERSION: 3.12 + + +def unpack4(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [a + b + c + d for rows in groups for a, b, c, d in rows] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def unpack_list(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c) for a, b in groups for c, d in b] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def unpack6(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c, e) for a, b in groups for c, d in b for e, f in d] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +print(unpack4([[], [(1, 2, 3, 4)], [(5, 6, 7, 8)]])) +print(unpack_list([(1, [(3, 4), (5, 6)]), (7, [])])) +print(unpack6([(1, [(3, [(5, 6), (7, 8)])]), (9, [])])) diff --git a/tests/corpus/3.11.14/handler_return_padding_3.py b/tests/corpus/3.11.14/handler_return_padding_3.py new file mode 100644 index 00000000..7e7278b7 --- /dev/null +++ b/tests/corpus/3.11.14/handler_return_padding_3.py @@ -0,0 +1,15 @@ +# The .pyc variants insert 1, 3 or 9 NOPs before each RETURN_VALUE. +# Regenerate with the matching CPython: python tools/compile_padded_returns.py +# Jump targets and exception-table boundaries are relocated with the padding. + +async def handled(read, log): + try: + await read() + log("saved") + except ValueError: + log("locked") + return False + except Exception as error: + log("failed", error) + return False + return True diff --git a/tests/corpus/3.11.14/handler_return_padding_3.pyc b/tests/corpus/3.11.14/handler_return_padding_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff2d6ead4fb36b4b5b6052154b74ad698692a636 GIT binary patch literal 542 zcmZWlO-lk%6us{~#~RTJ!G#7gP>bB;ABez(Z3-a_!8S7ZUN$t%$eS^Zw2jhVaNEid z|ADRC_Y2-G`U8SHSKaZes0a7Vxtw$E9qwB!7GVG|9w#S>%Ztx12O=fiWJy8mAc8f<X>Xw>`mU99%LrTNEoG>R`C|7O$1hI}8?)XqI3&($M8&%=> z3`?&`b=6cVHQ%*#Y01M2qixx!%fm)l2wjw#YI_NFN}BSZ(QzWOV^*%PZq{vy5!$>) z;ZlH%67<3`xKF0}n5BQ)(pSwei{?jH@eWRZs!!e+EP(g@b zX$gjD;Y7>d8(ycB3RJy%4G-c;ct~dXBg^vKMDr{`C7CQl-YN6oQLQfQs(y?K`lTM% Z-6xoF#@HY5S`i0CFaZ-KUxg|Elgud8xd0|f?y>=Y{HRUHF)O2-o@nb3sl5Hv@?Zb zVHbjkrIn=iA8hLnP)KLxTs-2z&b~ME<_$adVOb^zfK%&q>g)Y#^^@@6!wB_E0xb}5 z0b3AYj+NO!17Hw7r_+0)um(uVBrrIFP9!>~&O;h<$6Nf;Fi1gF6xfOU5dvc#uB~bC zqYi)>P{Waq{p*SlaDOnHG!w_H?-e^@ohXk`q3-O+{&%U-y* zY&QEd>r2n~^4pcBUsux0@7h)2NL5mjc(qisg{V|_=4ydX{FQ_nq1JG3z3{=_5`{A? zr18Sczzt5uZc`b!4-*fVfyc2zioY;ouS101l7$5CGJ|+GVNjx*;)K`R*mrG%N|T+k eDEtg9?7O1w$VD|yG+cN8SAKIu?3pHx_O-u(Ty8f2 literal 0 HcmV?d00001 diff --git a/tests/corpus/3.12.3/out/handler_return_padding_3.py b/tests/corpus/3.12.3/out/handler_return_padding_3.py new file mode 100644 index 00000000..2704aa0c --- /dev/null +++ b/tests/corpus/3.12.3/out/handler_return_padding_3.py @@ -0,0 +1,12 @@ +async def handled(read, log): + try: + await read() + log('saved') + except ValueError: + log('locked') + return False + except Exception as error: + log('failed', error) + return False + return True + diff --git a/tests/corpus/3.12.3/out/padded_value_returns_3.py b/tests/corpus/3.12.3/out/padded_value_returns_3.py new file mode 100644 index 00000000..f79afb3c --- /dev/null +++ b/tests/corpus/3.12.3/out/padded_value_returns_3.py @@ -0,0 +1,14 @@ +def synchronous(read, build): + try: + item = read() + except ValueError: + return build('missing') + return build(item) + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build('missing') + return build(item) + diff --git a/tests/corpus/3.12.3/padded_value_returns_3.py b/tests/corpus/3.12.3/padded_value_returns_3.py new file mode 100644 index 00000000..fb1d66fb --- /dev/null +++ b/tests/corpus/3.12.3/padded_value_returns_3.py @@ -0,0 +1,19 @@ +# Generate .pyc files with the matching CPython 3.11–3.14 interpreter: +# python tools/compile_padded_value_returns.py +# Each RETURN_VALUE receives 1, 3 or 9 NOPs, with jumps and exception +# boundaries relocated. Padding never constructs a new return value. + +def synchronous(read, build): + try: + item = read() + except ValueError: + return build("missing") + return build(item) + + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build("missing") + return build(item) diff --git a/tests/corpus/3.12.3/padded_value_returns_3.pyc b/tests/corpus/3.12.3/padded_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02223be668b60160f32b93b150eacc456be163ca GIT binary patch literal 645 zcmbVJJxc>Y5S_WbGv8kz*vWw+2EmkpAa+(l8bMG5IkTr8Y?u3mR6cqINcu+M@lku%`X zg+<^ulFHAhOPzScuOK5tb$dq#+c$TMFTw#9v8#dE6M*B#fcNmZ0- znr~ySEnQhyby}_`Y+2Z_IJf!orV3vcHXYXG7+ZU76Ae__RBO_HO<&iqMiY(F^~VAF1oP8OW@)rcX61lYa?)N@i!< zwqEPXs2gp#?W=B2$!MeH1j3iFtY9^nAIi7KkPzAAjdJN5t5&aBi&q0a_ks*#H0l literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/out/handler_return_padding_3.py b/tests/corpus/3.13.7/out/handler_return_padding_3.py new file mode 100644 index 00000000..2704aa0c --- /dev/null +++ b/tests/corpus/3.13.7/out/handler_return_padding_3.py @@ -0,0 +1,12 @@ +async def handled(read, log): + try: + await read() + log('saved') + except ValueError: + log('locked') + return False + except Exception as error: + log('failed', error) + return False + return True + diff --git a/tests/corpus/3.13.7/out/padded_value_returns_3.py b/tests/corpus/3.13.7/out/padded_value_returns_3.py new file mode 100644 index 00000000..f79afb3c --- /dev/null +++ b/tests/corpus/3.13.7/out/padded_value_returns_3.py @@ -0,0 +1,14 @@ +def synchronous(read, build): + try: + item = read() + except ValueError: + return build('missing') + return build(item) + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build('missing') + return build(item) + diff --git a/tests/corpus/3.13.7/padded_value_returns_3.py b/tests/corpus/3.13.7/padded_value_returns_3.py new file mode 100644 index 00000000..fb1d66fb --- /dev/null +++ b/tests/corpus/3.13.7/padded_value_returns_3.py @@ -0,0 +1,19 @@ +# Generate .pyc files with the matching CPython 3.11–3.14 interpreter: +# python tools/compile_padded_value_returns.py +# Each RETURN_VALUE receives 1, 3 or 9 NOPs, with jumps and exception +# boundaries relocated. Padding never constructs a new return value. + +def synchronous(read, build): + try: + item = read() + except ValueError: + return build("missing") + return build(item) + + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build("missing") + return build(item) diff --git a/tests/corpus/3.13.7/padded_value_returns_3.pyc b/tests/corpus/3.13.7/padded_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66632e32df7bebeda6c62331540f0d90744504b1 GIT binary patch literal 649 zcmbVJze~eF6n-z4*xJUQ2!cb6Rm6hmR1~Ui4lZ3vT?8eXqz2lg+%=+()maDaDo#Rl zbJ5k!(K)OCfS7;4cd;#kxOm5Xe0TS~ckg`{4-I7)0IbyuZ}%^+ckjge1G_99CDMTk zOaX@q5@Jm$umh#91W<{~MDcE-%|R9NQ0xjkrHXl|D#dQb9LPBbGqBTD0HnYi1~5P> zE7(&CWjfJHwFB#H8XOFja+GSgLbzTn#3-|E)&qN!bDtMjq;lJ|B5gl#>sF+?ExRFP zS%zUm^DSGn#IoZaw*qd9<)&#_wzYR8!}qvNhn`q!oI9!lv+kkDN<8k@37r5*%P&-wt0 zinl(1Ab6^eu~%O}s1M+5jWw{tKQsR~yG#~R<2D0;x5tP4#nVRdll1VzSoOz9TtXEx z5J44ZVcuvMLl=Na`kGGvgF*=kbCIliF)RWnOW z7G_}tb|xLzi%9?rcp4x_OieS!Ka+CR@Nk)V*tK?*8M%jGV!E0SZ*>%@%i%ls5sgj$WhG<1z(41!~ zA%)8o^MgZW3f&I%W*x^-bCC$6aOuG7L~SX;r6ae=eIfUypuD-?ayj>!hl{OqHS*iy zGDWD~YhN!S+@^4al>*+_CAdW=aW__idy@*!Sb~@7N{+o+l)aCV>LpM|Gg-;7UMflD wUdGgw93#Ek#li3{$O)P%$0?}M)zg5teX%B|DAj+ZZ&UbUi1=;Wm>zh40DbssH~;_u literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/out/handler_return_padding_3.py b/tests/corpus/3.14.2/out/handler_return_padding_3.py new file mode 100644 index 00000000..2704aa0c --- /dev/null +++ b/tests/corpus/3.14.2/out/handler_return_padding_3.py @@ -0,0 +1,12 @@ +async def handled(read, log): + try: + await read() + log('saved') + except ValueError: + log('locked') + return False + except Exception as error: + log('failed', error) + return False + return True + diff --git a/tests/corpus/3.14.2/out/padded_value_returns_3.py b/tests/corpus/3.14.2/out/padded_value_returns_3.py new file mode 100644 index 00000000..f79afb3c --- /dev/null +++ b/tests/corpus/3.14.2/out/padded_value_returns_3.py @@ -0,0 +1,14 @@ +def synchronous(read, build): + try: + item = read() + except ValueError: + return build('missing') + return build(item) + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build('missing') + return build(item) + diff --git a/tests/corpus/3.14.2/padded_value_returns_3.py b/tests/corpus/3.14.2/padded_value_returns_3.py new file mode 100644 index 00000000..fb1d66fb --- /dev/null +++ b/tests/corpus/3.14.2/padded_value_returns_3.py @@ -0,0 +1,19 @@ +# Generate .pyc files with the matching CPython 3.11–3.14 interpreter: +# python tools/compile_padded_value_returns.py +# Each RETURN_VALUE receives 1, 3 or 9 NOPs, with jumps and exception +# boundaries relocated. Padding never constructs a new return value. + +def synchronous(read, build): + try: + item = read() + except ValueError: + return build("missing") + return build(item) + + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build("missing") + return build(item) diff --git a/tests/corpus/3.14.2/padded_value_returns_3.pyc b/tests/corpus/3.14.2/padded_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d984de4943fdce47ee3dddaded4a7f9d05835eb GIT binary patch literal 657 zcmaKqze~eF6vyAoCAOy3iWMAcHB}IcXqP&O!A)FBhf+ETwKh>B{l#4(792`-)B<0M*; zgGq29heXUNbC6_ZW#9!+Ny}2*Z9)rTP=ExaNz2tx@|b|UlK!k2qwg`80|U1D2I3G{ z5r8R}0qM=bm)BF`J-75bu*TxxUs;O>c?G)C^U4tMEv(_>`xv3O F;|F~tZ#)11 literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/report.json b/tests/corpus/3.14.2/report.json new file mode 100644 index 00000000..7dad997e --- /dev/null +++ b/tests/corpus/3.14.2/report.json @@ -0,0 +1,17 @@ +{ + "version": "3.14.2", + "interp": "/opt/homebrew/bin/python3.14", + "counts": { + "PASS": 2 + }, + "results": [ + { + "module": "handler_return_padding_3", + "verdict": "PASS" + }, + { + "module": "padded_value_returns_3", + "verdict": "PASS" + } + ] +} \ No newline at end of file diff --git a/tests/corpus/3.14.7/out/annotationlib.py b/tests/corpus/3.14.7/out/annotationlib.py index e272c210..4cebcc1b 100644 --- a/tests/corpus/3.14.7/out/annotationlib.py +++ b/tests/corpus/3.14.7/out/annotationlib.py @@ -548,7 +548,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): if isinstance(result, ForwardRef): return result.evaluate(format=Format.FORWARDREF) return result - return {val.evaluate(format=Format.FORWARDREF): val for key, val in result.items() if not isinstance(val, ForwardRef)} + return result.items() + val if format == Format.VALUE: raise RuntimeError('annotate function does not support VALUE format') raise ValueError(f'Invalid format: {format!r}') @@ -704,7 +705,7 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Fo obj_locals = None unwrap = obj else: - obj_locals = unwrap = (obj_globals := None) + obj_globals = obj_locals = unwrap = None if unwrap is not None: _seen_ids = {id(unwrap)} while True: diff --git a/tests/corpus/3.14.7/out/code.py b/tests/corpus/3.14.7/out/code.py index 26dba2f7..25ee4e64 100644 --- a/tests/corpus/3.14.7/out/code.py +++ b/tests/corpus/3.14.7/out/code.py @@ -107,7 +107,7 @@ def showsyntaxerror(self, filename=None, **kwargs): source = kwargs.pop('source', '') self._showtraceback(typ, value, None, source) finally: - value = tb = (typ := None) + typ = value = tb = None def showtraceback(self): '''Display the exception that just occurred. @@ -122,7 +122,7 @@ def showtraceback(self): typ, value, tb = sys.exc_info() self._showtraceback(typ, value, tb.tb_next, '') finally: - value = tb = (typ := None) + typ = value = tb = None def _showtraceback(self, typ, value, tb, source): sys.last_type = typ diff --git a/tests/corpus/summary.json b/tests/corpus/summary.json index 43f97016..70dbb5df 100644 --- a/tests/corpus/summary.json +++ b/tests/corpus/summary.json @@ -20,6 +20,12 @@ "AST-PASS": 8 } }, + "3.11.14": { + "interp": "~/.local/share/uv/python/cpython-3.11-macos-aarch64-none/bin/python3.11", + "counts": { + "PASS": 2 + } + }, "3.11.16": { "interp": "~/.local/share/uv/python/cpython-3.11-macos-aarch64-none/bin/python3.11", "counts": { @@ -34,6 +40,12 @@ "AST-PASS": 10 } }, + "3.12.3": { + "interp": "~/.local/share/uv/python/cpython-3.12-macos-aarch64-none/bin/python3.12", + "counts": { + "PASS": 2 + } + }, "3.13.15": { "interp": "/opt/homebrew/bin/python3.13", "counts": { @@ -41,6 +53,18 @@ "AST-PASS": 7 } }, + "3.13.7": { + "interp": "/opt/homebrew/bin/python3.13", + "counts": { + "PASS": 2 + } + }, + "3.14.2": { + "interp": "/opt/homebrew/bin/python3.14", + "counts": { + "PASS": 2 + } + }, "3.14.7": { "interp": "/opt/homebrew/bin/python3.14", "counts": { diff --git a/tests/fixtures/awaited_synchronous_iteration.py b/tests/fixtures/awaited_synchronous_iteration.py new file mode 100644 index 00000000..6d5438be --- /dev/null +++ b/tests/fixtures/awaited_synchronous_iteration.py @@ -0,0 +1,23 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# async/await syntax starts in Python 3.5. + +async def awaited(read): + result = [] + for item in await read(): + result.append(item) + return result + + +async def asynchronous(read): + result = [] + async for item in read(): + result.append(item) + return result + + +async def sync(read): + result = [] + for item in read(): + result.append(item) + return result diff --git a/tests/fixtures/boolean_call_arguments.py b/tests/fixtures/boolean_call_arguments.py new file mode 100644 index 00000000..de66ce33 --- /dev/null +++ b/tests/fixtures/boolean_call_arguments.py @@ -0,0 +1,35 @@ +def select(call, before, first, second, last, after): + return call(before(), (first() and second()) or last(), after()) + + +def repeated_prefix(call, value, second, last, after): + return call(value, (value and second()) or last(), after()) + + +def alternatives(a, b, c, d, e): + return bool((a and b) or c or d or e) + + +def or_prefix(a, b, c, d, e): + return bool(c or d or (a and b) or e) + + +def method(a, b, c, d, e): + return bool((a and a.get("available")) or b or c or d or e) + + +def assignment(a, b, c, d, e): + value = (a and a.get("available")) or b or c or d or e + return bool(value) + + +def pure_or(a, b, c, d, e): + return bool(a or b or c or d or e) + + +def pure_and(a, b, c, d, e): + return bool(a and b and c and d and e) + + +def following_branch(call, a, b, c, d, e): + return call((a and b) or c, d if e else 0) 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/fixtures/class_annotation_registration.py b/tests/fixtures/class_annotation_registration.py new file mode 100644 index 00000000..60ae6d60 --- /dev/null +++ b/tests/fixtures/class_annotation_registration.py @@ -0,0 +1,30 @@ +if True: + module_count: int = 5 + + +class Always: + "Documented class." + if True: + amount: int = 3 + label: str = "ready" + + def read(self) -> int: + return self.amount + + +def select(enabled, values, default): + class Result: + if True: + amount: int = 3 + if enabled: + label: str = "ready" + for value in values: + limit: int = default(value) + return Result + + +def loop_only(values, default): + class Result: + for value in values: + amount: int = default(value) + return Result 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/fixtures/comprehension_dictionary_displays.py b/tests/fixtures/comprehension_dictionary_displays.py new file mode 100644 index 00000000..edf421b6 --- /dev/null +++ b/tests/fixtures/comprehension_dictionary_displays.py @@ -0,0 +1,51 @@ +# Compile with each named CPython version; the committed .pyc files are +# exercised by the existing Rust integration suite. + +# Python 3.10+ uses MAP_ADD 1 for large dictionary displays. + +def list17(rows): + return [{ + "k0": row + 0, "k1": row + 1, "k2": row + 2, "k3": row + 3, + "k4": row + 4, "k5": row + 5, "k6": row + 6, "k7": row + 7, + "k8": row + 8, "k9": row + 9, "k10": row + 10, "k11": row + 11, + "k12": row + 12, "k13": row + 13, "k14": row + 14, "k15": row + 15, + "k16": row + 16, + } for row in rows] + + +def generator17(rows): + return ({ + "k0": row + 0, "k1": row + 1, "k2": row + 2, "k3": row + 3, + "k4": row + 4, "k5": row + 5, "k6": row + 6, "k7": row + 7, + "k8": row + 8, "k9": row + 9, "k10": row + 10, "k11": row + 11, + "k12": row + 12, "k13": row + 13, "k14": row + 14, "k15": row + 15, + "k16": row + 16, + } for row in rows) + + +def list20(rows): + return [{ + "k0": row + 0, "k1": row + 1, "k2": row + 2, "k3": row + 3, + "k4": row + 4, "k5": row + 5, "k6": row + 6, "k7": row + 7, + "k8": row + 8, "k9": row + 9, "k10": row + 10, "k11": row + 11, + "k12": row + 12, "k13": row + 13, "k14": row + 14, "k15": row + 15, + "k16": row + 16, "k17": row + 17, "k18": row + 18, "k19": row + 19, + } for row in rows] + + +def generator20(rows): + return ({ + "k0": row + 0, "k1": row + 1, "k2": row + 2, "k3": row + 3, + "k4": row + 4, "k5": row + 5, "k6": row + 6, "k7": row + 7, + "k8": row + 8, "k9": row + 9, "k10": row + 10, "k11": row + 11, + "k12": row + 12, "k13": row + 13, "k14": row + 14, "k15": row + 15, + "k16": row + 16, "k17": row + 17, "k18": row + 18, "k19": row + 19, + } for row in rows) + + +def dictionary_comprehension(rows): + return {row: row + 1 for row in rows} + + +def small_display(rows): + return [{"value": row} for row in rows] diff --git a/tests/fixtures/conditional_loop_prefix.py b/tests/fixtures/conditional_loop_prefix.py new file mode 100644 index 00000000..1a209679 --- /dev/null +++ b/tests/fixtures/conditional_loop_prefix.py @@ -0,0 +1,20 @@ +# Attribute loads make FOR_ITER require EXTENDED_ARG on Python 3.12+. +# Conditional branches lead to adjacent back edges targeting that prefix. +def process(rows, work): + success = True + for row in rows: + if row.is_file: + if not work( + "file", row.ok, + row.a, row.b, row.c, row.d, row.e, row.f, + row.g, row.h, row.i, row.j, row.k, row.l, + ): + success = False + else: + if not work( + "directory", row.ok, + row.a, row.b, row.c, row.d, row.e, row.f, + row.g, row.h, row.i, row.j, row.k, row.l, + ): + success = False + return success diff --git a/tests/fixtures/extended_loop_prefix.py b/tests/fixtures/extended_loop_prefix.py new file mode 100644 index 00000000..706a8340 --- /dev/null +++ b/tests/fixtures/extended_loop_prefix.py @@ -0,0 +1,123 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# The long functions require EXTENDED_ARG before FOR_ITER. + +def size_1(rows, work): + result = [] + for row in rows: + try: + work(row) + except ValueError: + continue + result.append(row) + return result + + +def size_30(rows, work): + result = [] + for row in rows: + try: + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + except ValueError: + continue + result.append(row) + return result + + +def size_60(rows, work): + result = [] + for row in rows: + try: + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + work(row) + except ValueError: + continue + result.append(row) + return result diff --git a/tests/fixtures/guarded_else_merge.py b/tests/fixtures/guarded_else_merge.py new file mode 100644 index 00000000..96521e49 --- /dev/null +++ b/tests/fixtures/guarded_else_merge.py @@ -0,0 +1,67 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# The finite else-span guard applies to Python 3.11+. + +async def awaited(owner): + value = None + active = owner.active + if active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = float(row[0]) + owner.record(value) + return value + + +async def synchronous(owner): + value = None + active = owner.active + if active: + value = float(1) + else: + row = owner.fetch() + if row and row[0] is not None: + value = 2 + owner.record(value) + return value + + +async def attribute(owner): + value = None + active = owner.active + if active: + value = float(active['total']) + else: + row = owner.row + if row and row[0] is not None: + value = float(row[0]) + owner.record(value) + return value + + +async def following_else_work(owner): + value = None + active = owner.active + if active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = 2 + owner.record('queried') + owner.record(value) + return value + + +async def independent(owner): + value = None + row = None + if owner.active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = 2 + return value diff --git a/tests/fixtures/handler_return_padding.py b/tests/fixtures/handler_return_padding.py new file mode 100644 index 00000000..7e7278b7 --- /dev/null +++ b/tests/fixtures/handler_return_padding.py @@ -0,0 +1,15 @@ +# The .pyc variants insert 1, 3 or 9 NOPs before each RETURN_VALUE. +# Regenerate with the matching CPython: python tools/compile_padded_returns.py +# Jump targets and exception-table boundaries are relocated with the padding. + +async def handled(read, log): + try: + await read() + log("saved") + except ValueError: + log("locked") + return False + except Exception as error: + log("failed", error) + return False + return True diff --git a/tests/fixtures/keyword_call_arm_boundary.py b/tests/fixtures/keyword_call_arm_boundary.py new file mode 100644 index 00000000..1dab94ec --- /dev/null +++ b/tests/fixtures/keyword_call_arm_boundary.py @@ -0,0 +1,5 @@ +# Resolving owner.build belongs after the condition. Reconstructing this as +# outer(owner.build(amount=1 if value else 2)) changes descriptor evaluation +# order. The bounded merger must decline rather than report a clean result. +def select(value, owner, outer): + return outer(owner.build(amount=1) if value else owner.build(amount=2)) diff --git a/tests/fixtures/keyword_conditional_calls.py b/tests/fixtures/keyword_conditional_calls.py new file mode 100644 index 00000000..044becfc --- /dev/null +++ b/tests/fixtures/keyword_conditional_calls.py @@ -0,0 +1,22 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# CALL_KW starts in Python 3.13. + +def case_0(value, build, other): + return build("reject" if value else "accept", amount=0) + + +def case_1(value, build, other): + return build("prefix", amount=0 if value else 1) + + +def case_2(value, build, other): + return build(0 if value else 1, amount=value) + + +def case_9(value, build, other): + return build("prefix", amount=1 if value else 2, last=other(value)) + + +def case_11(value, build, other): + return other(build(amount=1) if value else build(amount=2, other=3)) diff --git a/tests/fixtures/loop_lookup_guards.py b/tests/fixtures/loop_lookup_guards.py new file mode 100644 index 00000000..ed3f08d6 --- /dev/null +++ b/tests/fixtures/loop_lookup_guards.py @@ -0,0 +1,33 @@ +def rating_lookup(values): + for value in values: + if not value.rating < 10: + continue + coordinator = value.coordinator + if coordinator is None: + continue + return coordinator + + +def dictionary_lookup(rows): + for row in rows: + if not isinstance(row, dict): + continue + coordinator = row.get("coordinator") + if coordinator is None: + continue + return coordinator + + +def none_lookup(rows): + for row in rows: + if row is None: + continue + coordinator = row.coordinator + if coordinator is None: + continue + return coordinator + + +def comprehension(rows): + return [row.get("coordinator") for row in rows if isinstance(row, dict)] + diff --git a/tests/fixtures/nested_conditional_elements.py b/tests/fixtures/nested_conditional_elements.py new file mode 100644 index 00000000..001331f8 --- /dev/null +++ b/tests/fixtures/nested_conditional_elements.py @@ -0,0 +1,20 @@ +def select(rows): + return [{ + "excluded": ({"count": row["count"], + "reasons": loads(row["reasons"]) if row["reasons"] else {}} + if row["count"] is not None else {}), + "archived": str(row["archived"]) if row["archived"] else None, + } for row in rows] + + +def then_arm(rows, build): + return [build(1 if row.inner else 2) if row.outer else 3 for row in rows] + + +def else_arm(rows, build): + return [1 if row.outer else build(2 if row.inner else 3) for row in rows] + + +def both_arms(rows, build): + return [build(1 if row.inner is None else 2) if row.outer + else build(3 if row.inner is not None else 4) for row in rows] diff --git a/tests/fixtures/nested_generator_comprehensions.py b/tests/fixtures/nested_generator_comprehensions.py new file mode 100644 index 00000000..77aefa9a --- /dev/null +++ b/tests/fixtures/nested_generator_comprehensions.py @@ -0,0 +1,22 @@ +def dictionary(rows): + return tuple({key: row[key] for key in row} for row in rows) + + +def list_elements(rows): + return tuple([value * 2 for value in row] for row in rows) + + +def set_elements(rows): + return tuple({value * 2 for value in row} for row in rows) + + +def call_arguments(rows, collect): + return tuple(collect([value * 2 for value in row]) for row in rows) + + +def filtered(rows): + return tuple({key: row[key] for key in row if key} for row in rows if row) + + +def unpacked(rows, columns): + return tuple({key: value for key, value in zip(columns, row) if key} for row in rows) diff --git a/tests/fixtures/ordered_keyword_expansions.py b/tests/fixtures/ordered_keyword_expansions.py new file mode 100644 index 00000000..306e8715 --- /dev/null +++ b/tests/fixtures/ordered_keyword_expansions.py @@ -0,0 +1,30 @@ +def direct(fn, first, second): + return fn(1, fixed=2, **first, **second) + + +def interleaved(fn, first, second): + return fn(1, **first, fixed=2, **second) + + +def mappings(fn, first, second): + return fn(1, **first, **second) + + +def dictionary(fn, first, second): + return fn(1, **{"fixed": 2, **first, **second}) + + +def non_identifier(fn, first, second): + return fn(1, **{"my col": 2, **first, **second}) + + +def reserved_word(fn, first, second): + return fn(1, **{"class": 2, **first, **second}) + + +def repeated_literal(fn, first, second): + return fn(1, **{"fixed": 2, "fixed": 3, **first, **second}) + + +def effectful(fn, get_first, get_second): + return fn(1, fixed=2, **get_first(), **get_second()) diff --git a/tests/fixtures/padded_value_returns.py b/tests/fixtures/padded_value_returns.py new file mode 100644 index 00000000..fb1d66fb --- /dev/null +++ b/tests/fixtures/padded_value_returns.py @@ -0,0 +1,19 @@ +# Generate .pyc files with the matching CPython 3.11–3.14 interpreter: +# python tools/compile_padded_value_returns.py +# Each RETURN_VALUE receives 1, 3 or 9 NOPs, with jumps and exception +# boundaries relocated. Padding never constructs a new return value. + +def synchronous(read, build): + try: + item = read() + except ValueError: + return build("missing") + return build(item) + + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build("missing") + return build(item) diff --git a/tests/fixtures/repeated_unpack_bindings.py b/tests/fixtures/repeated_unpack_bindings.py new file mode 100644 index 00000000..2b97c72e --- /dev/null +++ b/tests/fixtures/repeated_unpack_bindings.py @@ -0,0 +1,35 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# CPython 3.13 starts replacing overwritten stores with POP_TOP. + +def repeated_loop(values): + result = [] + for value, discarded, discarded, discarded in values: + result.append(value is not None) + return result + + +def repeated_list(values): + return [value is not None for value, discarded, discarded, discarded in values] + + +def underscore_loop(values): + result = [] + for value, _, discarded, discarded in values: + result.append((value, discarded, _)) + return result + + +def underscore_list(values): + return [(value, discarded, _) for value, _, discarded, discarded in values] + + +def nested_loop(values): + result = [] + for value, (discarded, discarded) in values: + result.append((value, discarded)) + return result + + +def nested_list(values): + return [(value, discarded) for value, (discarded, discarded) in values] diff --git a/tests/fixtures/slice_constants_and_calls.py b/tests/fixtures/slice_constants_and_calls.py new file mode 100644 index 00000000..1ed1a6e9 --- /dev/null +++ b/tests/fixtures/slice_constants_and_calls.py @@ -0,0 +1,23 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# Marshal format 5 adds slice constants in 3.14; runtime calls must +# retain their global lookup on every supported Python version. + +def dynamic_axes(data, stop): + return data[:stop, :] + + +def constant_axes(data, stop): + return data[:, :] + + +def mixed_axes(data, stop): + return data[1:stop:2, :, 7] + + +def call(data, stop): + return data[slice(None, stop, None)] + + +def call_axes(data, stop): + return data[slice(None, stop, None), :] diff --git a/tests/fixtures/sunk_value_return_control.py b/tests/fixtures/sunk_value_return_control.py new file mode 100644 index 00000000..3b28c204 --- /dev/null +++ b/tests/fixtures/sunk_value_return_control.py @@ -0,0 +1,10 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +def select(value): + try: + entry = value.entry + except AttributeError: + kind = "missing" + else: + kind = "present" + return kind, value diff --git a/tests/fixtures/unpacked_comprehension_cleanup.py b/tests/fixtures/unpacked_comprehension_cleanup.py new file mode 100644 index 00000000..948c5ae3 --- /dev/null +++ b/tests/fixtures/unpacked_comprehension_cleanup.py @@ -0,0 +1,75 @@ +# 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 unpack4(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [a + b + c + d for rows in groups for a, b, c, d in rows] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def unpack_list(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c) for a, b in groups for c, d in b] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def unpack_set(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = {(a, c) for a, b in groups for c, d in b} + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def unpack_dict(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = {a: c for a, b in groups for c, d in b} + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def unpack6(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c, e) for a, b in groups for c, d in b for e, f in d] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def simple(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c) for a in groups for c in a] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def mixed(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c) for key, a in groups for c in a] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after + + +def mixed_inner(groups): + a, b, c, d, e, f = 10, 20, 30, 40, 50, 60 + result = [(a, c) for a in groups for key, c in a] + after = 0 + for number in (1, 2, 3): + after += number + return result, (a, b, c, d, e, f), after diff --git a/tests/integration.rs b/tests/integration.rs index 8e99a853..857cc85c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -276,6 +276,4 @@ fn empty_defaults_keep_the_comprehension_level() { assert_contains(&src, &[&expected], version); } } -} - } } diff --git a/tests/pyc/awaited_synchronous_iteration.3.10.pyc b/tests/pyc/awaited_synchronous_iteration.3.10.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cc60d846151739ce8d20633e54619d69783f41f GIT binary patch literal 595 zcma)&&q~8U5XNUV+azsM5WRZvB(w($3Z6tfh_shpdJqIDp&PZ(CUrMKD=jGc0=|JB z^Bi7%f}8~3!i&zN+CTK-!p!c>EW_{nqDrM?0l<6m+-TN5PCn_qbOcpL6lRl>0)s*I>Fo;yr4-Q$bu_?LIbv)+>hgz`$^H?S1oEwTEs#2D0IR5z6{30c1>PNMUGUh+;@#1d@y?OhA$;g*li( zlO-9Z4y2p~h?{}CTqq33bi6okQ9NU zsfZ0oz(S~q3&^>}lZX@`MSMUZ9*~*{Ac4*62=C%#b);YohY!pQ;<67wfl&-{N2KczG$)vibgs2G&ai*10!2WCb_#t&R9j6xq6-~=C>1r|f& J!qtII2LRBX$$|g? literal 0 HcmV?d00001 diff --git a/tests/pyc/awaited_synchronous_iteration.3.12.pyc b/tests/pyc/awaited_synchronous_iteration.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d87c32b418c5082748cfcfbb98746accdc7c967 GIT binary patch literal 915 zcmb7?&ubGw6vyA&ncapo+nToK(54VXkcF5){dEyf;>FsF2SJp$bT=AllQ6TP22%*B zh+?3TO7kbkQv6Fi3w2L|7cW9c4z{_}H`_#lP5m)2UuNIDH?!Z*ypN_CWdL9v-J5^Y z&VBz%>t6#0t6qW@ZE)ZP+y@6q(a}h?Dzo_NFiRzKnp-q`k4SCapq_li_ajr==4-R%e2Oe| z7=^w}#_M-FI>ZpHjwkkjvc?lzf=feY?3md<(OMu*iK`(oMN23Y zLOqZ$NQPuYlsD#jZJQ8Excg7 zWtR4CHjVGm(tMUag~2jajc4%xUGi7YTv-;=bS3p4$crQ+UeCiE$(-yVS?m&A0Gq1H5MUk7!xBZ*xAkgY~Yq-;KGSs!DLDYt2dPatN)^QHfP(6Ez`Pj#5A0ypqwY{;WTYagd(p8GG&XXX~)OOI_^kFB2jA$|IQLH5k2 zOt!OpkRtC$npD)aD!7d$-aA*p(Td%stkH^1K<*dK{zf=BoR1{k3tgj#Pk2G=;e!5@6||jv z5no|Jd(Db}pnsF9M$`Yl0r@LouK0>^x^|LG&J+9~I+1^&|Ng7f1BOb%?%YMJF0y1a u61kFAeTeA{jwMZB-e|h*b?=JEQA~}oq)CT;hKPp-$K2rEIi)23RfhQKCZy@@BO2Wu%oa+7xHZZOg&;k^yEw~$^c z2#s3w?#bZAUn6x-f+ufnBS>$4FHJRSMDf9V%+8ycx8FCDjoX$^M2+nSZ_L;4u6=_4 z{cz9gOu}hUfhH)R0t30A0kwiYOBrMBG*6`)ux(h!16-yw>Kv+ZkuK3ZBI?{hr$=~n zlN#l5#8w<>E7MYU3`%M}EK z8#`r{vVf z;bi09VFcc>jG?G$=XsYMqm|C{V8vFEHCVAYC4SK?^>2DjkzQ27)4BNv&6#FfzHfFmcgS(aljV^B8iJ{qUEMW literal 0 HcmV?d00001 diff --git a/tests/pyc/awaited_synchronous_iteration.3.8.pyc b/tests/pyc/awaited_synchronous_iteration.3.8.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f88c010c2d333a182e6faee94e60531b8f439517 GIT binary patch literal 579 zcma)&!AiqG5Qb-xG>I`SML|4BPaZ6S0R>Mg9z57fFF^!BO6W!{v`O4esFn1j52D9B zhu}lx>RWhnb~PoX7Z+xBc4nCQ_P=U29UTDnd#6*s^SS*+@zT-gbxtOB$T48}3OB$s zrqhkgU=6x8X8O>zhDa^dkwXo#4>GY!?jGit!wd9i$|YRDZELRO_y)Q5jtGzK9DOLT zUtnnlNfJh^&`X1d0V_=&ifo*g#wZOZ1#p|DeS2vrQsLc=p3;o!UN8-)%m!i>4Ig+M z#hDmTf(Pj+jt-JpY5jFHB#;~dXkZ&%=q>z`yK@q`yo7U9Vz~6SfY21LGp8{ z0GE<+`FBP7>(=O5xS5XWbdw284TMZuGJ@?b9p6g-J|@SvAoDhPs+-nSKq>uv#TvBy|^&5vopiYZ~v^__H6)Q=Yx;;r&xVayl^af9h1pTavV6mz$I{t z+jJv$c!O?>HwVyjM@TKRqlY?VpJZ~I+zm{zfM+^WuF#rGn literal 0 HcmV?d00001 diff --git a/tests/pyc/boolean_call_arguments.3.11.pyc b/tests/pyc/boolean_call_arguments.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82e30327b3535b139276bf0ad25611d8cf24f0ab GIT binary patch literal 2347 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 Gmi-qNN{$%- literal 0 HcmV?d00001 diff --git a/tests/pyc/boolean_call_arguments.3.14.pyc b/tests/pyc/boolean_call_arguments.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98fbb201a1023d14819c62dea1247829b2819d71 GIT binary patch literal 2661 zcmcIm&2Jk;6rZto9e>1$O`#4+1vQqFtRrF~#fp?xDM1NbP$8?z;-ezkcsGuvjX!4B zZn*ixg^ME&F(5&z9!TKQs+S=CiA6pbR@^voLkWKX@6E35O=PPoDkJ&##Fl9E^b4oNo0~BCXp#1m3@Fd*$?QK1wcU-0Yy0g z7?8&R$K)VjP!6St6fOk%UJ1cIE_zYre(#Ya$l|LT-@PN!~x~ zX))+N6$Vf9;-J5b>p@GAQ+oH5NC8_YD7tP3cC_6J(`<3K#0=987+RrHR&7C7;Gd}M zni?~3yR*F6^QLB)Ms~M!VAh#tWOphRT~o^2=xbYH#rnQhHjTyV=jch-3MLzemr+M! z0ia2Kk4zp0z6ySw`*x)}leK2D4;zmTy3?!H^y-t1r|}}qt!qkhDtF3KoS|QZ;K5@uWJkxCM<*y1~Bju7llQj zXHPP%swt+XZdaKG`Y;F=!8t}6fD>c9X7ExS-$M_rQG|7kWv~enho7+q&?Gz`x^c;hORd^b{jlEtw3Cp!u{A5UcA_}; z@seZDKu;V$+ZuH`PzkQwcij1F&jy!y0Rk(4s zD~w@1#X4iW$=M?hNR_k|OXJZ)+fKT+=Gf!)PGr^+X8$8mbqG$wyo@ygj`a zhT6MCJba41z5DZG|0`q+`Q(LVAGm>*BA<~#U!s5EpmTc$`0Y6|%?BAi>tuLrx1#Hn z2c>dxdxt6I!XC?juQ%Z5xkBCBlpYhTEgN9L>MjSeOC(c&klYC^DBvA zx#q=)pB40bu- Te;-Yphbf)I!G(VSvfS;zIef4` literal 0 HcmV?d00001 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 diff --git a/tests/pyc/class_annotation_registration.3.14.pyc b/tests/pyc/class_annotation_registration.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c17f634801eade8738a83a15b76263f08e5abda7 GIT binary patch literal 2944 zcmbVO-ESL35TCs-+p*K6Y2(zfTbxvFV+u~IRun{31-0o5kb;Z+0Bug!^O;&)d}VJ9 zgbK+*m2lJth{Ri8`r7}7P$8KUQYDa(cuV4vw0&Y`@9a2kDgi5byq~i(v%lZWojx!; zqyoUg@0Y(R8slgGz<#f3QLl3mI~Fvdq)&j*&wzj>U9cOl1fQ{2U>v5P#3rDmoL9mV zLMf@ZQ!QyIwUTih3bBvuo?h%k8=+H?sx8)p8z5K-go3KoG^lL1< ztfUA7`;0Cw!#PG5xpBJ49Tl-Ml-W6j%yi=-UNS_Y3}UDhLj&T0@s9hYwc>w$@Umw& zmYuqADtX(re7_jz+_5SvHbZ0RM~y6`$aL~3=Fq+}EGci^1*qU`4nAbJ(7J2v6PRVF zUHOal2L^XUgV!IjK&{n9K?#hi$CoV;44bCBs#3FsY5KHzx*5z0#~1!=we}UBa{O7j z#kA^mPgtVn)lKevTJr^$gW~O#AZ2z}9n%aBg$@<_CzGhzKaApO6Lx^<*-a8_w*2eh zJV-C8Lyf#ef8w1;2o zSYf!L&qh~YHu?BUOLT-P)Uiu9ak$PL2d{CwyjdCN&i2TxmM+Zjeb?}GGHp0-Ivw~;CucV0zdfyFF*Y04XA zMN(ERfuu3(W{}%zStVN8T@Cb4SfSSW0mYn3div7F=x*AODMRa~j5K_#o{|T<2*R{N z2}LFmT-8S5$|!&^u&+$P4dZ;21VjvXDU@p79Ro&Kx0_f=hJGaOY48@DN<^O?xNg1F65k-yYpzYE@VhA}=TdoN{ zfkyO`A%p}y*B;OZ$M<({Px`?yf}iCN z5)T5NM124j>-$HQp(E9Sye#| zbIJg#vb(Gz9z{&iBBp3Prt}K*08{Y0086ofWgCk#mSkeJ7Gzw{yKQ=PcV&<)`}5?y zH0{6ggk0^zd>obh8=Rkfsd0OtP9XI$Ep(wivvz4cHM2!n??-cDUo;o+TnOnr^>z`g z?IJjY3js&Mg%BHNXbG;fI4?~g;7hcIu$-KFaN>Tkb>)}TObAPLUs$Sxh$O!E0Ga58 zfK%uR$P$y!2$PCl_#04c7=4(ej2UH3nWts8pU26UJ=%3#3!AL$Sc-FSy@%{>eeXSC zkcx_>Ek8jNnWSVfFEClh<=4n`UyY_hO&0l-NaV*5o5ZgmHu+e~{H$euJie}Fwp1L; z*IFzXp~^RKw=7_ijnaKt^YI#4DoJHnDTDZ2XwyZ00Vm~Ml$4%spjP%oLDlUendWPx# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..44b3d7f97e72365fb333c4e5e112b9d1c25d35dc GIT binary patch literal 2373 zcmb_d&rcgi7@gT&^TTTbDNU7<6q%9+ZwfeIS`0-IgvwF^;(~gqR9UWf0Y{4+W|jmb zka{RmNs!{uN-^RPq+X1;^#4c?W~*A2uQ_px;!~p@`ewarH`tcis4MN;&+Bhz-ur&d zi;fPzObD^Rc<^i2&R;7pVOKBeTIFdN&xlUGCtnkt0;w(mC0z!}x&l;mAJC__09*7{ zV5{B+Y}4C;?VEm0O?vBzWmVi_=tx}>Jnp~1s6B{Mpl9@?Z zC3)Gfj-`^qG7ou47AO`A2Hy-@2Df;4DYap(GlPednbrHuSTWK(l}X3H&16g?k&Zt| znCk`~zQ0*&;f84>EhaPzo;(crj{Gq-T~ud&S=^h=%0GRTi##7c7(a>}PT5z6^O4^! zg?|s+wAGn{GV>=MT){Ux;c?}yD2iA(QJjQvE{fP~n{H8^eu)3vxJZaYM|~WQ<%Kgr z>vc>2B=NTsE;gXRD!t z5(4c|*`eKa`JHJqkWIT-tH-AOO^_g^H?{TXd<< zXV-S;ZB;8M8mMcm8&3Zw+AE@F8%BG-VYFEX7H&e<&kf719CjIQj_Z5Ezm81{-a5Iz z4WpL(lwi~O&)lO1q)H7q!IXcL_Ab`ffTk=kd`6szvP;5h5>Y*sta9vupC|}E06;_s zZx+>&BR8Vi*&KZu+n>*^od$xoI#N(Z5ZIAQDZhWUNllnosV2u62-hld?o~$uT-@u# zeWJj-D8YEBjtb9%5YGq2f4yG#FIaD%i7+1k?#_ zN2A0mV*BAB3da*W7A>k1c_h~pi?)A$_u%f~{AnO$s}luf0%9jvAHHeaIJfZ6x!j_{ zYh#Mjy>_o$k*&@K*+S3P%CvEXcJNes*)-xyDbq3-yN*qTRqv_x-#i zdjE0ER)-485XcYlHhAv%dz5TzPm^rL>P4Uh8jJAGBZ%p)7sEcmQcc?ILWRAdSH=Mg z!Bqs73pwuC9nH<=Bl)$H&)*)tqXlIYT1H(hy-HT?ZK7owi=9gdCTdL!^Lgn=<0OHK zuK%W@6A4Hs*k&AEWwDIOMBfP2TmWQ*BSm#AFK3gvzI`Vdk77rCr-6Q39V;kfP%%~+ z7u{Es1rdBihx@p{EgLo$f p3XcL_$&}L95~a84>o!U?0XM4FsIVpkObQql5GeybW1r&VzX2Qv)GYu2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b59a580097c03ee2692172b35dd5adb3a5b6d48e GIT binary patch literal 2553 zcmcIlOK%%h7`=C9oF*QpB@N|~LUA4tyLsCVaiRbXj-s|nOw}Q5x-iBb2TP8pxpx97 zt8OZ_Nu?TzwhlpRS_#RnghVQ_fM4On5*6OE$_~{lcAPt7Cn>f=rHYZ}`18y+_kQPm zk6UhSb;^X0>m9%Ru>EPM|4-Qa7p++3b1?1_jdT!3Gzz4e1e7!xC~FE((HuaB)&y+Q zjscHp&A?`@1=zCa^s2GydSY1>x7fIR6L6O-kaqGJ<C_POM|A)O+J)Ju9_=M=R+}L=_=E|)YCj^ zq$6J#MoN#SBVR>RD>@&%dacyN^^_hnnb1H)&HaGu7TXRp^W_9Le~Fa zY-4Q8zd8Bz^kB|kJT>&YYxvjc9d)XpO#Qv9PH_d_-5C2wc}`iOj-~8*7&XeiLWt3( zQQ=ghlmul?)>kGkhxbINd$>s+$;D5FK6An@IYDVjPn!{gu{Jyq8e1-eq=V14YR>oMdbCv zcXHjV)EavOZk8*uN@`fojt86;qK~j!5&6sBSMQVRk}n zEA(ck1k?{$jVELK5qH2(ln?g;P&)m?MRnxy^p>S}CX}V$&fK5NF7LYBdB{FeP(~p8 zh}{UMyBk!{H(2X{US-5!1EcM-sOf)Qqm}=M+5>^R2?o#p5IXpz>W$Xvt1m>JhA}l9 zi6rBuX#JRhx~l0PM=Y93apuO#LMz@60Du2jQ4Kx`fJ^Y1q(_L5(Mog8V>$X5=3800`?Zzu(t)&5N|=7MQrljaCj4Um3$yrRL7r8=d>z$ zPd4=6;>N|zxm}lM8~GnEDB~bM&N}f`{k99^O-=66rPU#L_^_Bqsa8uJ`9vP-6*flU zp~9|T)2%$2UQFqcL^5USjJ<~-1bf~CKvvIm71e?7;~Ado{4udUa}$H{_RL0nM;$0A z10XcOo8fq)wM)6z($yfNpTnbu7N-o+*j+V;53O^@5dh6y1> z8u(d2W$63F(9bRZ^#1y+;COiW`wEH=OnnumeagL)eGQntXbPhkQPuQ)LN&pwK?;xJ zbpi=XN7=Oq`qX4biU~eK2i|KF8}b*`z+*EfXFKmF1g*81t%`DfJ3CyKnh~H7;O6z@Pws88FH)sK`H>uf{O| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bf57c91758e134ecfa96ced4352d7bfebf34ad72 GIT binary patch literal 2656 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 literal 0 HcmV?d00001 diff --git a/tests/pyc/comprehension_dictionary_displays.3.13.pyc b/tests/pyc/comprehension_dictionary_displays.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1494d265f6cf4efabbdd54a17176b17b9e1496c GIT binary patch literal 4244 zcmeH~OH3O_9L8sNZDV7bmkH*L!MqHKvCYF!kw_+KlS8qpu4t-WB5E9hVlbU0X;M{H zR4Ec^FZ6&!ZzQLnUdS!ErMDcD=F<8Usi(a;SXGrv)$czuI5>n}dP;;PfBwy`_w}Fo z=9?|~{nZ9ztn-V#jn*fpe)$tz{%DFygMdwDvR1~KsX;YUhw7#QHB1N8VLGACMOUOM zvws{NUWG4Rp!5%9lg+UJ`%JU+5oYNDcHt^xD_RWaoWmJU;UAYY(*QdZJHal+RbaPb z54c*f7wl8)2iGXB1=lIA2RA4VfEyK`05>TPf}0h$fLj%}f!h^#fIAg;fkTSB!99{c z(Px(#^Rf`0a7F8dO1jJ<;dDX28nf}33=p@ePJo21OBl7OQrI$PJ0y(TP6-pXOG477 zisC<$wp+rK?IAF(#$$H1qPXo3gF?fB0Z zWvhSa{@LB`o%6CaKJ{?)as8vDXuT*O=frU@%EyUT`8d%kA4jdCi+TL&idC}R+^(*v z+5;!=o4bjf&x@)(dHP}I@yw$uvh8M`nssaA(>phc9y;F5TD!K5?Jw`T_uIp<-M}t? z(7fNCPHB(BPxN00<%a6=2})j^;3XB~C6%SfT+ZODJqEO>Znsm3)CZ0VQ&G1FQ#Ar$ zOjwePHI5d2dRatIR~$g2XcXVl9wdyhDV=x$BUSj1Vf<=x{F*x0Q0xFZ6}!Myirrw3 z;%cy0u@CH5Tm!CETnDaK+yD+JZUmoD+yo9PZU(m~ZUwg~ZU=WL?gV!!4uQKRUuT=n z5Sz_Q4=(m3jmugj%&A;qqO}xs4JOVCw zrWdm-*&D0;OrevA&#&cqJQrAw~pKjG^hvjUT4rA)rZ~KP*i~J@gpI5?Q z->1e?Nllhr;{#MTl#PXC#k~j0%oEs71EECyjk!!mS%q`OgeII@!X8#(J{n=dt<)Qb zBJi8{F9Ko4Fqsj*z3R7*e$D=<@u7x*{Pu`|97w-AKDEhUHq8Y#%>_FKC2*2tl4MGZ zC*F@;6$Tcmn27Py*g+G7bpeCoI28}4Vv$^0tY+Q46sj^xWqu0({zqQUXfvMwnU@p~ zqxhEU&^rh65(D>r%S)J4tX$tTt=UIrRcZR2O4ELkrUUPeOZfzPktazeNv0q&VL8o6 z2d&9Ru;DRmHOR^(yvgzyAGZ-p)8?TrzKYoa%{k9!^`#ktuAa zB@rhp@-aw7u98Vbu2Rpw&ea%t6|a^^-8{Ya$z`oRi`Cs*U&{2fTntjr9-gk754_w| zy!G(2_-0VPXyLjV8( literal 0 HcmV?d00001 diff --git a/tests/pyc/comprehension_dictionary_displays.3.14.pyc b/tests/pyc/comprehension_dictionary_displays.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..340d85462659701a2c5f29616bd9a36f1f0a200c GIT binary patch literal 4490 zcmeHLO-vg{6rS1j{~y@=m{J5JP6)=xKY#o zGtiu{1X$3TI&4WDSYh;t@~2>z*>%>)-sTHRm@Oz*zh*4YBk1`$e~p5WlP zfm6UK22KU18aNX;lYujXGaEPyIE#U^g0mVp8#tSRvxBo6I0ra~fpda$8aNj?mw|JG za~n7hIFEsA0M}sP8o@QzbJNPyH;ma?C-R)<0brN(GA!sVDtDr=7;FGG2pfXMVHaV; zuo2j3QN0t362u4w2nGp;2;u}6AFu~X(Ij#oX+@P#!2tGj7Y7Szt)N9d$Sf8XL|Tia za&vi+zMY=eGP(KpXEUh+{wBo|TrHnXE@|QXQqhvlXoc9Ycm;h}U|Lwtm^}~kwU*r$ zmw(R%ZzWK2Till4Y_65m>gD^lN-9PESJ!6NV_)9Tqn^Ia$*nUF-_WD@==R`__tCJ7 zo>tqL(e7!ros6pOWK?ZOQOUyW?#dIbdfe7gd1fWP(7!peHSqAAQY9X_w4K_ScywEj zZOraj(N+`p7fW_(Z)5FU>+1T=uWW~LD7w|OCBAPvjK?nXoxo4ZkG(qKsCH0xq63+X zJxWW7SiZw5ICD}Rh3ILo@w z<}G#=6Y8{FDPLHg{^xRCWXt9*HubGuvI%d1#nlWC`UH)dFcHw02pV5OORBID*a;j2 zP68K!o4`ZRK+s6wCGZh60j32_0~D?jjtW2AO4*7w_(l8Jt@M0)F)zl8o#csRAtz$P z;jwHkmCS16;p%njRqMbEd4~CcS$JBZhb8_eRcM)(s{>h*pG=HSyvFskbJ0q9L)uu6 zo^fYQn#CFLG-hSGYnkmShiR2dpse*WuCTg#rO}u5{R6r+i0sd&RZICgiKEg7U zPp1-Wl;9E-c*#-K@XhnOc5laZ$$tJ4iJtULw*NPYrlns>^miDdszhT_ok}z^a{?0m z?~A=6?zm3y4H&hxV4ooIziOv`#|&s`c6G*W0+p2`YyNZ zM?3JBmH%Hx{hw@|Y}55$bW>K5)PJf!7GBbcb1-q1AONV-dD{L-ogasDr1KFvOuHDn zOL|m)XjZ1M6@1|_kUyiBI7SHCYR+hNhc^*HqcBYC1}wAR&-FfW1~;y(&8#-9-u=S= z*|m);kDb9?Q}EzdXHYZYrh7iaQb#*k`4x2jIQk0tF9Gar^PK)65Fw4kBKP8VFPcA0 zW*5@OyuDkSOJ=i&hf6Vl$B=0>>G8@veNUXBjoCGA<;H62^XZM*$Ij5MDTEge=`ZYV zF--?8@*pbrV^R<0*vQY99qNy!5z;qtwz0X~>_Rp@E@)Mz)M|9Gq;k&pT~_YjZ#Dqu Mea~&2_se1a8%M@O=Kufz literal 0 HcmV?d00001 diff --git a/tests/pyc/conditional_loop_prefix.3.11.pyc b/tests/pyc/conditional_loop_prefix.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a205329d5ebb6d2b9186367eda0dc3fe796ca138 GIT binary patch literal 949 zcmdUtJ8#oa6vxl;BW@CG+6GZjB;?f#9e}A73$HB_i!eXG+AmN@2uCiOmvMN^F&Qp~Te^*GgP3aXwUGbeDa?uN;7P^c|bj0)40* zu;SeY)%{=Z{yflVS7vKN8Qm~qH&rzV1@mJr_B}^gq3q;grSWZrn+hK)^c4mQnZmBZ zM+zS+3>9uEq=LvZlp*+@RK^|`+scxC-)B-PJrCx~a>4a=LhStWPTGV&l+E?Fq5?oh zxq127m-xmH(&NCP>PJX4!G;&`07_^omNNDL%#)T=^bd)W3oA+)HldCWQvemUtnQvM zk0g7mKpAa?M$gVQ944xboRgktc#@42(V3B+^hD*R&jr!hbBI!+Mj(v_(uF{}7)X}_ z>6Jiw?QeQA+!w>zk*!vg+R3158twRh)u>!lhksQ~sbg(Z){Xjm8kCg`n4$E2q2*l9 zxg##vE_1Ac&q}5tbeCr(7K*f@^=KbXG{4I5_<=u&b1^I5np;x1RcT(Q|II1f}wq#O2HGsfgGQ J&tq)h{R6uA+}i*E literal 0 HcmV?d00001 diff --git a/tests/pyc/conditional_loop_prefix.3.13.pyc b/tests/pyc/conditional_loop_prefix.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4ca9f1e926c273607984e3f8fc72e407bc33c14 GIT binary patch literal 1276 zcmeH{KX21O7{=eTowy{eN$aRBjX)7%zzZEvrXm(p!jcFP93e48!EuXeh@-nirCUa} zQV?n~rLaIJU_ku}e1!0-QU@lsL@8qBy^udu@sJO|4R_D|{P?}|qx1FW<{~Np{5Y%h?0v{6dO!PvwM#KDypqMIAb2k z>qAj|4`8#akWW25CR_Cr*b^J=nt9gEB?_=#|*7@70@5+ikfAxexX``{SKY>xb)~Zyu$-q^2_c*0s3N1M0dFNF-nDy{H5R z#Fsmc^lifO2=dmWW- z89?e*3PY1AAR(X=Fjc$(6LYv?;{_7JpOtf?IH^j8JOCc9zw`6wWBb$by=0tL@x>fS-Rsy&XP0(nS60C{o&(u%G0TmTcOWs~+7q|)ar7MPGF%Xa~M)lzRB zGl%kGS2(W$X22rnu`@c4_w726YT;e|Gty|k3Ki0kQ1{PBWBn>rNacr}IFK4A5SB>I zfpl^pO$?+{1L^cYdiGy>rTboCavKquTwd{&+z~+T6TYQPQRe+WMT`$~7$QXnXKMy$ zHFpO(S)#fJFbcC^DLCocEuj_N5?vOfj?1VWcx)$a2;JociG|{A3bHQ9eL-wN96_ic zML{+Mc_4@@$U{NUP)ch;W8O9w+P23wh0e>iO*t2;gmSv)=jY}`I-oq@3q|*FP-c`b z*xr`o2ClcYUMiG4&oB6l;>FAUj?jJP$qr18kNo-gZ(J(SFeWQEzMYQms*N#YFSD1~ z&3ug~8|GA1tw(Foh8aJI>_?vQgYtg4YS(FvzFd8~_G<0j_4nDs?1#mprH@NPnf~Zn z!uSEWG-HsOd#<;}P`3<7OgGF_b+vBQtf3kWW$HZbSMO;r^wv2r6U|u|k5^XO_~o#Y qCY>w}ft|sUC2e-mJl25# literal 0 HcmV?d00001 diff --git a/tests/pyc/extended_loop_prefix.3.11.pyc b/tests/pyc/extended_loop_prefix.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c805c5c27817f7cd0ae7bda0a15f9b440c6f1a46 GIT binary patch literal 4315 zcmeHK&1(}u6n`_DO_Ob6l4dK~NNMT^6vejqfuI!8MohpW6oiT)lr!h+@QoiqNEjcevpTR%UDAYTSrd@YCK39Bcyd4u6M+Pp({T3TSJ$+aEmp9{V5p zYfJAuBkw&UZ>?`*FjT&#`S3xNL76_2qT(8fioG-N8S}CVoeb1SGT%Q3y))xjG0^yi zI&t{7ama*rDS(SBK#dr;eqRG{Re`Gj<2uARt=RyAO>oBPV-8sCJvGGwI12LnTc|PJ z^c@!?p8$c=ckBoH?%=XBsKoaw2K{ui`V-wm0sX3*Lzw1jOp3{LLzZIW;zpDp_Eygt zN_v-i(&Q^gJu!S%a}?2u!_GaNQz7{q;qaovi@p_|8tZ>h(W&wJS1bD2DHg(CaSks{ z#T`I*MZZXRN$1FG`ixW4`AKB2D!22T|2x)yzq3lniBY;aDkV;co5u;_+=O7Aj;%em zMaw$0xI|c6@NLh9ds!u)R%e)uHh#j_E+OCnn2@IAAwx~DVgFG+wBb6rOt_&pvM{>& E0=Nl$S^xk5 literal 0 HcmV?d00001 diff --git a/tests/pyc/extended_loop_prefix.3.12.pyc b/tests/pyc/extended_loop_prefix.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65423109bbea1f59350bfc4706c8d8a2cf227084 GIT binary patch literal 3100 zcmeHJ&ubGw6n<}ZH_65R!EiSQ3TOlW(@^+QeWLPYIdBn_q8+nK$3}z4@qVK?wl- zAJ(>BJq)gVrM6eF(W>`TV-wQw5}rUB$zqz3WvkM#yvRI%0EOCOyZW-b;u);7A|zpc z5MW(Wfk6?A()-4~2t~Fmyj#jqkMen$ydJ|vusp9zz$Jt^C|}7#L7Ia+R&l1v?BMJ| zCZ|twZgAVTkj?5V>8fnY+{l}@oHzJ$+sAb?m$7U~jLEcNoXY-?rJI&H@^q}$zkl6vY( zC{l@)?>RnD!*8_>t+MzjFyNdZ2PXufXz9V;cRxf1YJkGG&ylF!H$$}FMqr4&Q%998 z997zfcANsf35TJr;5?nBU2t22-Rn1luhZgjJXtDO4z#6j@sFUPR2FA9Wogw$j%2Xd9&n zy_rZPBWydOCeJC`gGjbLdE*rGCi4F1krx1e%M6`H&fa-DTU`J5{r*av_x>QxtnB|6 zalVn@CrNg03UU6{BF^J@pw466sLtJxVVdvIl5cJ^@qW%7c1*=nTY1M0M^fBH?W8w>Lif=uAJrQfc07h&5moZv@gcFES H9;2zB<@P%r literal 0 HcmV?d00001 diff --git a/tests/pyc/extended_loop_prefix.3.13.pyc b/tests/pyc/extended_loop_prefix.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1f773a3e4d022977cb881eda63696015b07d5ff GIT binary patch literal 3107 zcmeH}&u5E3f-r}-^W)8A_wD!d-q?yFO91fl z&6m&XwO#%Z`%WX>o;`(~EyzO@7~~0BlBa0tLuuBvOdWp!H}=JGHtXz2K#dkagX~#= znsg2dP@wmOcV9c|k&6KYKzLq&X?WmxdZFY3oR2{+2~dHXQVc4jMV4BW z%lAvwhQ7#{!MLYXuj{pP%f(&HST(t8)nJdfhv{acYH>--;W-e;#xGgAX_<-2%9_U+D7->uR{AC95s14r?eI=*d zuSv>%a&)s8^DX!j-3-ZkPKj@SyvO5S{S8|@2J(_2Isd7|pjZVl=yT?f872s10s`hm z>m!q9(Tmv)%++tj&S=Hxm_dz+%)MYch4bL#o#=f zPcEo*Ta_24-P>`3_GEr%Fx)--ejoEIw0W#P3>%;0_Z{y1`oI70Z}Ricf%%!{0{@nu zQ%N>~>F2u0&!3Lv=ebeL&!bRddcUEs;@qO5;@qYKDo!SzrF4hN*$MYfn4n#nl{z=m zPQ~uXq6Ibz0}cwh@5W=J+^FhvEP`{W6)?r^9KInA5(XY8&_C}QCB?|=vXLCS-6347iaF{oI7*ReBXEO)iYkV002Ms z9?yMDzq-ALeXEgXuX+wUB}l_=bW2Qi(8Sq0Q z1fU7$Aw$L?BV2Fp9ELPq;ofa6eZ(Uf25`Xz$j}Lhz$433gdDzW48jnC)2j4`kNfIlm7u>m@kMZ=58=&KYjc_+Z?Mz+TJ8>5Pe!r0Yb^AW9mXL9elY ztYMsmjMIQQ&{%|E9E_%;j`nLY=EISnuLqF9O}Kw+z&&EYy@~pMcpPEwXJl@iaOx&V zJ9l?2?#h^)Hw&>vnQqIT#f$dsae}rpgRfw^TNwOnz+gZ6yiUnn!1#ynw$Mq33cm*Y7-|Zyw{>UXF N*H*{^AuglP-wd{yQLbJScPaM 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 literal 0 HcmV?d00001 diff --git a/tests/pyc/guarded_else_merge.3.14.pyc b/tests/pyc/guarded_else_merge.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ea01b6c1346ef0a9f8b725781308b2d561c8ae2 GIT binary patch literal 2291 zcmbuA&u<$=6vttF`r zp)Dvmw10tdBHH)}5OFClxe*5c3GjDWVdLg#@%Qjf`0~cSI7`z1SM^_u@&vn3IP15_yyys%g6+P)Q8ztX5aQUh^WU z4-WaDI5H#HaP#uq8}#Qfh$r^q_&Blu3&x3J544@J&qylm%$C+!2ZmGAu3c!4!paFT@nqEQ{-(l`O^upix}rO~?x&E$`jDZ~T^=Y>?8c zx|h@OsdHosXEtZ&F4AAgMW_du2!;df4;Uu`1_aI0c=Z)Z6vI;=_1zSxmVOX@@koR*;PtlB=qSO?0LMjuqawg@dXR)Hq!g#8Ic3yIQ-`|a z!#+8L1Ek?$=*pUrE9Kc$E_%D@*(Qn)c_vdzpGP9J@A;{=L}}kYr^B?5k6_BblR<)} literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_1.3.11.pyc b/tests/pyc/handler_return_padding_1.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f1660ad70c02470b891672075a51ecfefca979b GIT binary patch literal 530 zcmZWlK}!Nb6n-<~S}R%-ER;bAb;wKpfeJj>r4Ygp>>`UZnzZf8?3zZ}Md>ei?PQ3* zAW!)><`(?{!JDVvx++lv@0;)Oeczkmy~kn^1_0yn-o&`R`uaw`K4i&eQ6zof(1Tmx zBo8TCrfU~~hP;b4`$V_|f?gdcv|)h#2rD>Uobb#F&}Xk9;_HNy0lqO>y@N^n>=3+F z0PiD^@uipk^%j9M9rED)`V0DZJ9IHS)TRSeMr)M-CZI!KXpjqdlQf~liqIrn^QR5N zIi|tB`L7COLdd;nq57qKdFsC;&Kb4{#IvL;3g1y z`$>2+G70vfBTQ&CW3Y*#AY5S`i0CFaZ-KUxg|Elgud8xd0|f?y>=Y{HRUHF)O2-o@nb3sl5Hv@?Zb zVHbjkrIn=iA8hLnP)KLxTs-2z&b~ME<_$adVOb^zfK%&q>g)Y#^^@@6!wB_E0xb}5 z0b3AYj+NO!17Hw7r_+0)um(uVBrrIFP9!>~&O;h<$6Nf;Fi1gF6xfOU5dvc#uB~bC zqYi)>P{Waq{p*SlaDOnHG!w_H?-e^@ohXk`q3-O+{&%U-y* zY&QEd>r2n~^4pcBUsux0@7h)2NL5mjc(qisg{V|_=4ydX{FQ_nq1JG3z3{=_5`{A? zr18Sczzt5uZc`b!4-*fVfyc2zioY;ouS101l7$5CGJ|+GVNjx*;)K`R*mrG%N|T+k eDEtg9?7O1w$VD|yG+cN8SAKIu?3pHx_O-u(Ty8f2 literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_1.3.13.pyc b/tests/pyc/handler_return_padding_1.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f68db615df47527485ea117ad3b75cf5ceaa0f2 GIT binary patch literal 520 zcmYLGJx{|h5WTaTLPA@7+ZU76Ae__RBO_HO<&iqMiY(F^~VAF1oP8OW@)rcX61lYa?)N@i!< zwqEPXs2gp#?W=B2$!MeH1j3iFtY9^nAIi7KkPzAAjdJN5t5&aBi&q0a_ks*#H0l literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_1.3.14.pyc b/tests/pyc/handler_return_padding_1.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be6567e2a55419f9b7b7c5d153027770ef9b1cac GIT binary patch literal 545 zcmYL`&q^Cn6vn?ZHP*N^*^KddJYWokIX z;s$jrAw`|mu+2?e;UzwsHqa9Bl|*=00QK{bO8*?Ym)^yo$$xgHZus_>+0c0(C; zcNuFa;Aq~*N#+9bkaNHa^9{>8|M?Q?!a7bS@4;-*vK?o{<{C~l>y^PY*Y}*UXRJdC zu_Hp*9@YrI?y`SX8NGlQk^u6+v~#Q zvvPbSk$#MD*%;v@l}-0cXT%7ytkO literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_3.3.11.pyc b/tests/pyc/handler_return_padding_3.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff2d6ead4fb36b4b5b6052154b74ad698692a636 GIT binary patch literal 542 zcmZWlO-lk%6us{~#~RTJ!G#7gP>bB;ABez(Z3-a_!8S7ZUN$t%$eS^Zw2jhVaNEid z|ADRC_Y2-G`U8SHSKaZes0a7Vxtw$E9qwB!7GVG|9w#S>%Ztx12O=fiWJy8mAc8f<Y5S`i0CFaZ-KUxg|Elgud8xd0|f?y>=Y{HRUHF)O2-o@nb3sl5Hv@?Zb zVHbjkrIn=iA8hLnP)KLxTs-2z&b~ME<_$adVOb^zfK%&q>g)Y#^^@@6!wB_E0xb}5 z0b3AYj+NO!17Hw7r_+0)um(uVBrrIFP9!>~&O;h<$6Nf;Fi1gF6xfOU5dvc#uB~bC zqYi)>P{Waq{p*SlaDOnHG!w_H?-e^@ohXk`q3-O+{&%U-y* zY&QEd>r2n~^4pcBUsux0@7h)2NL5mjc(qisg{V|_=4ydX{FQ_nq1JG3z3{=_5`{A? zr18Sczzt5uZc`b!4-*fVfyc2zioY;ouS101l7$5CGJ|+GVNjx*;)K`R*mrG%N|T+k eDEtg9?7O1w$VD|yG+cN8SAKIu?3pHx_O-u(Ty8f2 literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_3.3.13.pyc b/tests/pyc/handler_return_padding_3.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f68db615df47527485ea117ad3b75cf5ceaa0f2 GIT binary patch literal 520 zcmYLGJx{|h5WTaTLPA@7+ZU76Ae__RBO_HO<&iqMiY(F^~VAF1oP8OW@)rcX61lYa?)N@i!< zwqEPXs2gp#?W=B2$!MeH1j3iFtY9^nAIi7KkPzAAjdJN5t5&aBi&q0a_ks*#H0l literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_3.3.14.pyc b/tests/pyc/handler_return_padding_3.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f1c0628e0c747b19ff2b319b8e85c527ee0669d GIT binary patch literal 557 zcmYL`!AiqG5Qb;6O|(I4)kB50G_4m!&`UijN(J>I9!lv+kkDN<8k@37r5*%P&-wt0 zinl(1Ab6^eu~%O}s1M+5jWw{tKQsR~yG#~R<2D0;x5tP4#nVRdll1VzSoOz9TtXEx z5J44ZVcuvMLl=Na`kGGvgF*=kbCIliF)RWnOW z7G_}tb|xLzi%9?rcp4x_OieS!Ka+CR@Nk)V*tK?*8M%jGV!E0SZ*>%@%i%ls5sgj$WhG<1z(41!~ zA%)8o^MgZW3f&I%W*x^-bCC$6aOuG7L~SX;r6ae=eIfUypuD-?ayj>!hl{OqHS*iy zGDWD~YhN!S+@^4al>*+_CAdW=aW__idy@*!Sb~@7N{+o+l)aCV>LpM|Gg-;7UMflD wUdGgw93#Ek#li3{$O)P%$0?}M)zg5teX%B|DAj+ZZ&UbUi1=;Wm>zh40DbssH~;_u literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_9.3.11.pyc b/tests/pyc/handler_return_padding_9.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a90c4f1262a32a0385a309d3b69e92d1c8cb12b GIT binary patch literal 579 zcmZuu%}PQ+6h1TKwY+FWAQVDeC?Q+q3q;_;HkA;DU>mtOqe-`WW$rbNw2jg$xNT*K zZ{W_?m|gS&f;(58>mOOoaK1T*@B7XfI3JOS&H!LI+#f}6u0FpoUmdc@CSgpvz@Yh&W|ZcdPlBtU&`+f)rTWg{sMU#V(&#pVh{k;&KbE z{uU+};KmR-F$3PCsS`FLAs_W{D$Yi64f9N&%nW^y+o+Tggz4tlqr+>KIG4Lv`Wu-X SAdDFyWUT9CXAFN~=Kls*cW;LP literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_9.3.12.pyc b/tests/pyc/handler_return_padding_9.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aae4bf0eaa3f1a800afe27d58da3fefe4d7945d8 GIT binary patch literal 516 zcmYLGJxc>Y5S`i0CFaZ-KUxg|Elgud8xd0|f?y>=Y{HRUHF)O2-o@nb3sl5Hv@?Zb zVHbjkrIn=iA8hLnP)KLxTs-2z&b~ME<_$adVOb^zfK%&q>g)Y#^^@@6!wB_E0xb}5 z0b3AYj+NO!17Hw7r_+0)um(uVBrrIFP9!>~&O;h<$6Nf;Fi1gF6xfOU5dvc#uB~bC zqYi)>P{Waq{p*SlaDOnHG!w_H?-e^@ohXk`q3-O+{&%U-y* zY&QEd>r2n~^4pcBUsux0@7h)2NL5mjc(qisg{V|_=4ydX{FQ_nq1JG3z3{=_5`{A? zr18Sczzt5uZc`b!4-*fVfyc2zioY;ouS101l7$5CGJ|+GVNjx*;)K`R*mrG%N|T+k eDEtg9?7O1w$VD|yG+cN8SAKIu?3pHx_O-u(Ty8f2 literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_9.3.13.pyc b/tests/pyc/handler_return_padding_9.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f68db615df47527485ea117ad3b75cf5ceaa0f2 GIT binary patch literal 520 zcmYLGJx{|h5WTaTLPA@7+ZU76Ae__RBO_HO<&iqMiY(F^~VAF1oP8OW@)rcX61lYa?)N@i!< zwqEPXs2gp#?W=B2$!MeH1j3iFtY9^nAIi7KkPzAAjdJN5t5&aBi&q0a_ks*#H0l literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_return_padding_9.3.14.pyc b/tests/pyc/handler_return_padding_9.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57241865b96a0e64aa45e4c40a837bc947407292 GIT binary patch literal 594 zcmZvZ%}VP)6vxlWG;NLb8x-``wznWHLW@vWic-PrU5JZPx)LOGMvca%Oj4-}LDaQA zfTBCGZy>npYs~h7YoR`XXKJVjH$(pX&&TgfI9VJWu^0fnJlrSGA2%}p$PXS2T`xiA z0xB>A0aP#xd82L&x&Tb_SM>JYNPa_#dgq`987PHKTf`pGHEq0L+onYwHO$bYQg}lO zD&`z9KTj57usxH8-Eay(6!IZcA)R=}9%T z*{+Lb;CPKZQxPd_u9)i`C{yUNueYC0r&TTxe&84Pop#WY!Y>}$4ekoLCk4fgy{65% z(>N$J&s6NI#N{ZVdiOoOApbAPC6<@)+RDKVE<|ngz`#(qd=CHI__ex^fowQngt9(!0U1*nf*GV3iWt%vHJOq@T3~<;$N=#_ zCjg15453U)4517fKsE>@3NtW-G6um+f(U}iV8&o3OGZ1?$pqoDCY5I9q-ZkVVl7L|DNVh_nqQumS_Gs@OHzx9K@L<=C7=40qL-YkxFNn}*paup% yO-8U0D;bJ-fZQTbXx`$m$<0qG%}KQ@;sbI)UMdDz^MRR>k?}JJBcl%^STO(!IafRY literal 0 HcmV?d00001 diff --git a/tests/pyc/keyword_call_arm_boundary.3.14.pyc b/tests/pyc/keyword_call_arm_boundary.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6506f42ce968ae8ed913909438e98320baf82daa GIT binary patch literal 426 zcmdPqTZlX-=wL5igJn@=Y;F@&hv?BjXniMn)e-uwnpWJ665` literal 0 HcmV?d00001 diff --git a/tests/pyc/keyword_conditional_calls.3.13.pyc b/tests/pyc/keyword_conditional_calls.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d453af6544c579390f0c25189549fc9f45879051 GIT binary patch literal 1128 zcma)5J#W)c6n)QM4KV=%swiI~sRF_Rk)~>fs8yv7C}9agRwjnXI1dQ8arA5h-S`0v z=}^Rk%2G8FLLG}(`V$f{;gu;HYE}mBeRk5IrWJ2EKJVk+ckX@1!&E9M6GHyb_g|mX zzpVWNJ{~k?!+9WE#3WYXK8Y&Q+5AOhMks6&RHG&+6 z1-m9SA+{vthzXe@W~kLr4=Zs7{kkRH{IwQe5X`m2J*I zBF9Sy*lDgIBoNL3jLP9Dn8Y#*{1QyW?iCw{DdXKvJfpyZm#lBwXDTVDZJ1%erqJ&!(fK2J4gk5|D zj;TrLqhjn$=&PI~;2NHWI3aWZM~NYE%or+;<@u*KwZyTP81DVk)X&+y?6+G7>i2tn zz1&mEr)XYyAsYFM5bI}_3z*I}p(W;?UC%SwY@(r*XaT)w9@RO>T z5e_aC_;omh58&bND!j4tU~~4PwR!W0Ug;^7Zm_?0@VK8ZM^jNx)lh}?g|Wo*iEfBo w(S>NG$b*TMb+-{XY?a@DHGBXs1Mpj>lpZE2T{%opT0~exsL&zcLn`k13qKIe>i_@% literal 0 HcmV?d00001 diff --git a/tests/pyc/keyword_conditional_calls.3.14.pyc b/tests/pyc/keyword_conditional_calls.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..891c547ce7aac86653bf9b4481c64e94b075e038 GIT binary patch literal 1149 zcma)5&1(}u6rb6zrd?Yau(WDGV>Gf>d zs`mIN*u#NitA7cM7BbNy@{x%Fu_*ycrVJ>X3ZP=D8DwY<-)t~cFu32p0nkEEaT=A; zl{i|RMkob)TF8`cO3}{@>^|uma;Q=Ak~n^-*^WaReg}iK=>>JagF14kzP8SvyjZ0vvU9~^h2Njw9#JLOZN1&uClh17+8G>qoeCBh)iCuIbLysR}A&xQN0!( z_Y5i28k7KE${Elp4v>Wg712}R#7y%v2@Z4-u`>XIX?tlq*h=^GLRTs5#xtFR=+Qin zzZVO5k2ACIq;E(Euq*lxnk)1)EaXicHS_~%CwEqRdcLdVw}#fgM0x!}K)1k%1T@i5 zh(SZ~Lu^T9iHoZcOBSf}JQyHKiOz!olLk|$xHj`o?(A*42rGGaCp-H&y_^1Wd0+i{ zwWk-mO7R5340X}vSKxkz9-n)g5_WSeS^-@ziIRaLFCYp9|;6E&%+NF~)~87_T2DFka=b!l8u!0N%&qoWD^|#=-yq literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_lookup_guards.3.11.pyc b/tests/pyc/loop_lookup_guards.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c1bfc22827fdec62dcee33edfb685169056fd57 GIT binary patch literal 1443 zcmbtUO=uHA6rR~lVw+96_AmX3KWi;WptglV5iAu{sL&sTiU*;TZFY^@rn_NgqgE2^ zK_p1PAB6NG^i(Z9c=F=OgWeO#AuM?EtlTQVMn1Qi z$*V-8 zQbS5qr88_n9C z{=BWlMh)+QmjmCEN9^P=VmVj)q7yZ!VNzQ))4CJVjI`~D8Qq4B5V0OmM+6MeN;=>S zr<-XSmZ_4hHgmnOETloW7w-=2d2Avz8dE zC&vEDBVox9;E^%Fd7K2nHv$+S$p9a{pn z`#a*BvIkHUPT65}CTmbTZEfaeADv?Z(eLRd zg)|wT$y#YOOJ_l7t0>HT3Sb*mr3Lrfr61Nm^?uQ6)ARM|c~~o5dp`GU?!{s)Hd2p` z_5&7@>qI zb5oLJ63aR_!j73uOUq~VS#lcI*s=5`KvTpRH-!@z{u4g*`4V3{KE-$uLmm7U`H4RP DgoZQh literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_lookup_guards.3.12.pyc b/tests/pyc/loop_lookup_guards.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8230f1de4a4268097aa63c38700f8b8e66172057 GIT binary patch literal 1195 zcmbtT&rcIU7@e8jmda8aY$-yGHb{h31Hpp_Azn1$AoL)KCM1TG-F00kyUom2Y_cRA zG${!Qa)Lw;TnShI01n=~U=?jP-aK(rdXkgh%x;^u;b7t<`}SqOAG7bxH=pA1ZV4gu zIJS3d{_yF^PvFgkLsq{6WC!W!HJV2{2E;l6B)SABm1Q-eHF9|gZ^)ne6M!A$p@-<% zFhXnC!%Og`bLn`QAjI%y65_x**BbH2I^G=eh(<7)LozBOmE=ykAyFlMl;&N_Hho2N z9I9J(-gT%d`O%m8Ldjr$M7(JPk{SQ0Y&Lr~>>A8vT86mymA9>6-P_4c2lXe3jK)Do#JZ^PSNtR}Lxo>VQJ`JNv6oF1tt zH6?wdT;<;qgVosJ?>OYwp>>#eYrMwFo4%>n69$ZJO#~w`t5l$iT1^7{;qRmW~g? z%Fw#eR_pdjPPnlz`P~+?Z06=|&G2R2(p+CM4Hp73byk@#0k#=OW3ZFo%QG#<&eL+k zn-qLk;9-*h;7uYKcq9Et4D4Qi?|w}T3ugv)%x!abc5$%BuX&gW$^b+Su0LCrNW&`1I z%~>f@Vl_mMbSvFTZCYQ@n@1>-&)gt)jFNrB=$%of!i?`qFmo{Qnay!#OFgoAveY9p??;GL3_&j;=w=2ncLg7I6$L@5k-Gd{jqjd&>+Gw^rkWI_;%`dZrQJ#^Hgg=TQ&Gx~_XsR* zUckDAfe&lKYcSCyxS!)&t!F>pZ(k=_od{QS1dDbX#IvSYwqp`F z+`Q(SvA6Ai^)ZMQgpU^(cs}kECZ&K$cx5jB|Kq*yP^jucdgCmQ2^lc z{lJ^!$n@e*^iLM4d({DS3Xp_uupmj$k|ae-lL}fT=^BQpJJX03NkURQ)nzmVNI?K* zt^(vqirm95=hvd=W(PnHGcLhg2mqt#`54TmDY{&Xxq>m&rh_mHtuTOR!V}P69?VHZ zsnf*LmTu%6HDj7As~c&{WKqRwdYWFbHSW0Oo4GNXi$jit!%oD~xW&hp_2-t&G(Ns! zn)xL(Pwf0s&Q7x|kLA}KUro5a!CEm{RL!rWDZr2R?xMTm4{ok+tZzTu^M?<-!Ahuq zKQy`%8ZD|7HB?py#5aFmxuvh_gN~jmCc+tV;#88{3#lo|O$u@gCwx<6;e^g)-5V7` zxati1B}1WgZY5!|u5*g!#6>hEpF~MJ4dU{57L`MP@AmL}Yu_I!`y-N9@4MVqZhLh%GRQ=jx*X literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_conditional_elements.3.12.pyc b/tests/pyc/nested_conditional_elements.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71b448527f9189d9ce61dc65fd59436993bd2094 GIT binary patch literal 1483 zcmb_cT}V?=96#sWd*^hU=GUequ~mel@&n0K%n*}W=7Q=e2$}cXay9SObMMlDhC)6F zK`jC&1Sv$QJtVy}dh#*oMN>leB=qFl+Cb2!{^z#Y6!RfEaDV6jb^ho6zs^^Oqd-6i zy>mTUIeB^`{sZW}g)>yT3WzC0(NlC6Q4EMF0VGra6e6NmlC!q_h!sBWG#vw&LL=x3 zdX8oC0LkQD3qqrK1XElIy7A*#3U{+P641NdyXOr>H}u;3!C@oJ6unjjR-rwzrqZAh z)YL$zM+qtYifZUJ1CfM92TnGa9ZvIL^$@@qO4?D0bK54_UC9DuD@;nLvMP3U+P+*- z2ekXt%*E-E^_ENV;OCmRilA6*e!h zd=Mw3-f%EPSpm>Gw{yGQ%*GW~qfb$L0<51^0Bh=NfEYvT*KT~bbuX39b$swG_$E7F z=u?rGk*H@Tux780*RR-HzuCH%#qJ&0(!3oe&Nlp?)bQ}jk4z8rHx;t!IXpG22!4r` za&Q712wDm0iW&5A&=qq1{-9r!PF23FoNS+!<{CaUFEq~v;+|D^>%O2FAFjiDA!n$w zy}*b1A9`WzU@shzm~nV)ATeT2VtxM4LcL2uFW~Xmk!1gY@-qxNJ(1oT?zK@wciAI)P)lttJv`XDZu`|~3#y9O-uBwj? zE)K31H~$o{qa}s-*(AbJ7lsMr40C{EzgIMe-0Vlj>tkL#kJ3OQvuSs&t^FDehml$n~1p*F~stf4>OD&K_2_b@3G^NF%HWe!>RXrg848@Y!O=i{! zNL7_fq~unlM!=~c^#D>O=TIr!l3RaH;pY(c)T$?L#+yrSdC#$3tOnd=gvl(VF1HEYzpk`ywX|4}HYN@vYzN_c0c`sCFrr01@ zT0f^Aefmo)X$O^7^Zvim0?X*AsMLRMrY?Eoi?ypzf*PgAbMkuIH^I3VQcH%;#&S14Bv z%;gr{f)r}N#-Yg0W*j@4b90WJ%Fl|tSP-_W`mZf{0fnQCE5o>&j;<8KEjS88Ylj~) z!qIXwT6fNt4Cw4${c z&&5kmJ{-8;T%7v(%Br?{b?wyt>Apv&pEenhz!7CUqTD4gjOep6ps51*-`iu6v35N} zC#f}>CT9FQ2-RZqda(xqoZjhL?6JOHn(% zHf8zU_0n;gP@eolxSj zo_Mw!O86w0+e4qeS<+t6(e;|d;T^HG8gVC+Ch@+3Y$a$%XrM#nm8j6sg+7W@jdZMZ zu5{iV+b}lI7l(_(8>#J{J#(ai4tWMwzFI%Oq30$2e1?w&M{S_5vDLMK^*Nac`d5Q; zI!=yGQ?!P3>Bs2${M8zZ8A*8xHznU8XhArnTAxHASdx{4gnp2-NhaUNfaf+|Ef4?z literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_conditional_elements.3.14.pyc b/tests/pyc/nested_conditional_elements.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a5fc236a238f3b73f8ccd7ae0207be7d0f153ad GIT binary patch literal 1671 zcmb_cO>7%Q6n?Wm!H%8KCXHQ_kj9M&u?lemq0}fsilR`KV7FS)TtJB&?^Lnmtdp5F z)Dl9zAkm%@slrAgB9%aLL?wq)eL$kz=#4r#G<#~r0U`C^*j&nqH(T4atfu0?Nc-)Z znKy6VeD9k%)7#T613>M3)dHT}G63WfVec zqUkLbdGHfq>yHp}umH#4GRa62kdfX?0IU%m7Klz>dkLo-jfMe+FnZ^>F5`&Y?)hqC zh0!loeA3#t({HLV6D45?O9FKTeU#_K5;;4q)9-04T?$JvX`TYOVHnS|Mn(}&IaU&R zuwC?OB9$?yVXKLB#)+C}9F) zBlhbSTb2C%L#ud zHmZWE`XAQ-O@0iM_%bvzsgzzp_;nzFm=FhSwk|XrpthHuLccNl_25+#e$(?TxFSjMSV8KqVkr6lZ-zXXXpSf+HwqRdv4 zrb*e3%-lF^tIL^W%3$4SbCLbn*a#Dnw_Vl>HJ9RSl?`Cmna3UQU3QBRAva z;NHmeGpRX)to-CXlA2xIp+it}`0_|76`bj8EvW>aFR3d{^4g{3KblDT#9u}Nl{Nw| zJX>}l5<7}M_j1}=5%h6}DMBB9{9}Y1JeR*ZK&Fk(x{3-!6&F J6E=tr{{lfXJL&)c literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.11.pyc b/tests/pyc/nested_generator_comprehensions.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a803036fd0a78065b70237494a8fafdd84ade27 GIT binary patch literal 3698 zcmdT`O>7%Q6rNezjhk)4x@jqCNSl!U8EDdyHXsy?AxMb?GKU~U3MfS0ZEMxBqgk7_ zmLv60sievw1*(KwxU{JvlpJ&Hu}53llO;}E%B@l^QBHhsW_P_#-2@OIv6}sMc7EP_ zyYHL#-ov)G)(~UN{_^x{`?>DhzoGA6-0f|?j?N-8*==@-nH|)zY6lSRD(bgHi%mayu&|c z9AeAt>w|asclt-z^^4D!LtEcDI_W6Qk7wmz{nHzdk|j{~-Du}W+}wdIk8z6E<}@W&he&?c6!l(Jb9EqD?80XFULNs3lHw)guA zO5MBamrCEkmaV7kDT87oiMBrCYeQ#c5KxT`I50|12T>fr=1061DHLb27CK@2cQ(1Z zN7_fnE2s`6!sif2aQ)bX_g-?NqjCDd(vl2KHn#w#%M3 zs5@z8t!XP)u$vOdU%U^pH(v!1f6l7599cNI)O#=P56Hi&*dyAoHmF&&W1!TtmrAMU zJeGQtNmPu)dY@9U(t&3+cGiIf22BK;R4!h^RuLoJ0hY8iXcfmWO{a2`4une-618y% zP&E<`P)|5|q7qL0pNVpH2T{J4L=i{u)siNHqE*mGING-5k|U1Z?%eQ}Qcw^?jlJD} zd=H^gRJSLpxib$0Wr!14Oc6?uFO{?C7e+$xyI(S<|n){Rk=Cl=-)l>ySPzw!K%`m$OhTTDRHIvCyqBAPTM`+ofj*z ztwfeiF8BTvS8u?8f8(Kga;UYW^VzJGE_hi@hGuo%d-pU#Y95AqKS_*MjM0t3g(C|` zl;-sgJgc1-9bjz8i49?XN0XLGy2Lec5W>V6`XFLL&o?Slv^QEJ;+T^$>J&O#ZWO4? zeS88cs>Y}T)JGkCv=Saw^zw@h?DOgMkC!WyHUs6#p}TyJuZ8z4Sq?X$6OmP>5BgHS zWND)WvKDSqwzQAC;ej>^3doI))iF{!bvctQSi&-!rVYRDAYa&YM)8L$@SJSRBNWhw z>cxjU9^h9XeWSjsfv->7<|%)hp!tQDmjKcCefN;t261jEE4sU zi?h%ob4(zCkQ5?Ks%oShpq_H{R3)5Jr1=kG5;;o7q)*TZ#H3@Y;s^#ls#G7qq*dRn z7gWHS)UAJ8zk;M$J`1^PEoENTQ2pvb8hP;|iap{ez@?O`N`#_RF*h@n zzP1;oUZ&lB0AkdbYCj)c?pU@Sqp}hLZ!@%X9J}R8MBJhIlGbCvPS9J z#SW>wAzDpTjEN`XO5-o~`r8{L)q$ilkibk2?wZ8IQm2-JmG%!K3NjWa!2M#8S^B0rBqb|P>HST`sna< z-YjOVgrI09E9vX#JPUEopJ|+bz@M=IMmc|<23&albxf}J5eq(*BR*o}k{oG<@*DmD Dzjqzd literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.12.pyc b/tests/pyc/nested_generator_comprehensions.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c39d374acbbaaddf79feb70d58f8fea9c9ada6a GIT binary patch literal 2604 zcmc&$O>7%Q6rR~#JB}TeI-2dZFsH>p*}wq|Vd zBS(r_Bq)t&LPZmSKyD8uQ9z0~`G`0nxHxf?N)|T`zzx-xIw#&6dp9;QRS#6G5-}mqQ2EO&ss#kguoDHIr56KkKDOjpYU?p7!E9(ka zMYn;qE!ure5%W9zT!9}RQ0fP>K~h!@_sNh8WC!B->{K%siBHn%wrGOvDEvwqm^TQ8 zZ8GH2vxF?u481^ZN)ahaBGM%XM5K(Ap&6+iR^F0UoeIR4rQ39aQnK2ikVSZ>A~sLH zljrF+)wapDT$F}JiZV;g7v(F)BFFoBz22jjQwC4*;hE?{D$NW&9EVp!pPn`11`DSW zY&w#dOETlK5$DlFoDU@zkDA9ZyO3o5rL%f8l7e`cEe?&x5|MC>`-iNw1ZQxp$sgq5 z3fWf!yAxZZho0f=#9Gf9-yB_ESRaJfJvC6?_Y9YL3oO0b+(&P?z4lFaAKbiXOQn-B z0~`fz&N05wb3=HrD~x4uH{r04Dv3mD3ok2jCcTg8}fQX!{@>OB*J3 zq7KXji6w6=%2U%u%$PIcDc%4Iylm-tFjXl0yFFX2hn~T#ZLM#!`<4&fJ=G5|43+h~Rl662D?^~*Yz@KY7Q2hm$Kd<Tl$?}cm^tr_DSqCY{I(1aICcV{fe@js=oolwsGSkBn1c= z$V)*D{?L1%1`DoWUI{)CZd(%pVX{C1lHf$(bk%OcB<2FB0E&!=*G>m_o!EF!P6vh% zRG5w$(SYflGtpSeV20j6R-VF7&Vi{Sq2)$@?m>+o%T9dXQ>*d-h6eI7Ru)yf+B85v zQ2>ype*0}lzx``>6_gV)<1kNWeQ*qRMXktdh=NvdX?aC^f-PfVk%aSWTIK~r<9OU( zS2A>`+Os&bZ^{wbMCUz-FH6g^z^EA}xD2x!(UF&BYkK=Ed_=S|zXd`*T9lWfNuRBz zm4sMW{gj@IbB+oO{Z!Xbo#}Wo9J$gEKOt@-@R8X{p`R{(%XeSRO%#R$_dSz$IGg9E*i%F0f@iYKapYcW8>1V}5yxHIh~t~nWA$z2g9UYMd1kouf&iosxh8=ML literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.13.pyc b/tests/pyc/nested_generator_comprehensions.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5eb814e44905bd74f3349053b804ad2b29ca8291 GIT binary patch literal 2700 zcmc&$O>7%Q6rR~#JC2>C-Z}-+P~pT8#Hk!7ICiBF8CN+Zml!15l>-vNvNyrhv8@@q zB%Gp_8wb*#h#-L!IMoG-6fSLURH#DY;$)*%T5;pl8>%n0xbWVt?X1&~f<(niewux6 zc4qv(_kC|_-Q9MP5ORO&&(Ghey?+097`GnU7}bVhut8$vG+`u0!P1xjR)~pU#h3(E zidn!~maN{6bn`p>T!J5NPDPFOqZO zBCXJ9g;YdM7#-6@R`^I0FDk1XPwjTQwega|OWePhT`85B!u@%8RWb`pN?u{9Qh_a` z3(G~Oe4ylcwvgwe#Z|3y3ZpAU7S_&Wvgs1|Q*3o~DpyFSay&fR3Jb6Y$ILt;YozLo z>`ZJ=-wdd-|5jjYdYxZgx#p_`9?1TkiTa-Guh+U@>gCR1`e~2H`o)Q1IJssim5VtA z1`?c{bIhWf*}-b|gRR*~aA1hLz^swq9L`;5^l9{^{CHJ<^T z-vnHY_yD=Lj{}Y#0#0~@8H_SSydpBJ6lMdXSuUk=Wu=Lso)6~0fIFAvr3EFYEGzjE zKLQdwbZrQX0ZDkLciVH*rOLirt}V;vzzuK3^+5LR^a3EhI_rjstOtz~j5+(=Z5y$d zA&jT2nE(ji?Nyy&)e){rVTS(y0f6zR0LcCe0N4v~YCZexB^Vu%?^n2TY`U>h8Ss)E zlgcjh8f*Y21GNb|gGNsq7D4lA)$J&F-lJy8Nze(3vh$(sp9v3El3u?V7=)=wdfbHM zX}S2`$W20}-)j&vcHQ_>}le-LzO%~aF_AS=0r z6k96)T~%X=avzu{Mp@6?8@dy?`r>Vu+86x6b*Yqu}>P!3h)(7mDhUSF`z zQE#qvjnU6~&f33njZwYSy#kZL&yXY>Q}2G?5S*U&V=xppBna;X^F&vjedo+iL#lH| zb<9+yndegUt#+{jyP7aNi|qnC89o^}_Z$d96Te0S0TLGm56?*sB!osw0U15iQXr@3 zIowhhqGS*Q1?320^e<*}C50)OBQyxrnva7qz=|SRzj{IkAfgCWRCPqFQuMhJ ztp(X9aDq0X`H^S`mxj2Mpuq;)k$4wGE*?NvhDgzq5ZVBQ%1?UWHqeAl%!{r20HoLi zo*pr71Uh_Ps)-+Gi(ZR~in{Q$_*h=fbB?6}d~73Re>q=Fr7s={E=nd3foV{2elMQ< zCH5WPK6ht&-yhkRXZQS(uTO8eexANQvhKMyvg?mL1ZO;1SLN9{$Ktqh>O7&F-IKP8 zb<*~|3@>fWMCof|rmcA%HU^TK4_ zL|?0^P?^YpQe(l=XXR)2qs*1-D?)%W}h Dq+bC{ literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.14.pyc b/tests/pyc/nested_generator_comprehensions.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a00b18f46968fe1184f4a03a208ab38d24710b0f GIT binary patch literal 3144 zcmdT`L2MgE6#cVnJ5Jn`-8fLvrcGRzASaF8;y6ela#BS~A|e^#woX%3M8?^srjBi9 zY?^Y4S`HOLkXAvB#G!#xEFnn6rBx3I!2yX&(v4cl7Y>|yL8#$S-}rym-q?*(;DX9Z z{+>TGJAZcG`|nTB-sUD50IqAle?QZnY4{D>*2BuJt!`{AKos^t4x$V#i%MvvsEk&Q z+R)mfcC_}W!wbHKMCDCdz=l5xv2_;B0-Qr@K5wwMr8i-w6`&nDz;CP{0|olZXi zM4GSzG{PYW;mV#JbNb4WFeHPz_-X6P@ZG+(k=3BC1`5HIkwx+O z{Egm1@PQh**H^l)21;9daOxFj5Bpf{a(tom;LeNoTt1u9uwlcU3qeQO#hcI;o3=yq z7VMxQaRki*{OVFZ32jh3T^ar?v~C-|X1Tg+82v4r2Q^^Wu;ag{fCHmGlLCH=0`Evi zk!F<#Nw+%%*bWLvj|hthrxgk^C+2Y{nxg%Kcq*?|*kGJ2N5(<1GnEv%X)UGAY3ZD( z11@cSYa86~-PSels#{lk3+@&Bvgf96(N}OkP@5A%!~gDO#Tvnu7Mk!&Ap`-la z2gvLW%AV>qxCbXU(hCR480SrdsytTmT)>_Ji7?JJ&JzGdHS_eNXj zq1wN$_WyXKbiXZB5+tKnT|xG#>TkN$8e~S+`6RAlKc}P;gk9HpQ%CpKZiG`x6D866wb!Ck5Z|qvA;_9w3^q-gfv+C&;+_{DSF{-DpVh=sDDDmwufhSa9gHdB& zq#C<(&ZH+MJymy+$5iR7>h5d1)m=`gn|VrFStm~qoKL258rNp($SajJaU6{aeuTjP z#k;BY7qEb$SsD4xyVSerT@F^tsDZ!QUlMp*fh&zeY|%EvmKp~fUo{UnzEy@y9_&zG zXJr$du2960+A4~XbtOW;M@V>0S9e9we?Eo4qQOV_EmcB*sgz~s%9k;KleQS^D~lKj z!)ZA$8)U#ExP<-O8;9@McLO~Q`ljUCgO7#mo}r=8`Z_o9fN|#s%G9$ wp)pm;IXIV@$)~h1r<^eEdKB{ynKAZQV(bKa>||`nGUJB-7W)%>&oU$CU!1i)z5oCK literal 0 HcmV?d00001 diff --git a/tests/pyc/ordered_keyword_expansions.3.10.pyc b/tests/pyc/ordered_keyword_expansions.3.10.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54ae9dcc555199d7c4bd4da627f88c63250704e7 GIT binary patch literal 1243 zcma)5J#Q015Z%2mpFiV75JVIt+LI6w5(Nc9NKhg->D*YSn{~9}d}qy`NsQb2zKOpiw0SqZy8WD6vCkCu^Vn zv{@gK!&z&=8CSV)GDi+nCDT}YHn%ptmTjFC3R4w-P{0wd8S+SCm&^|2x=;snN{~(k zSr;jC2eu-}f&L8{PU7iQXJ@8K*DS>YiF-HU)fBHc6l-a&NSZ66%i`QYu?0FQsJb!E znHBbU+rJGD`*a%l6`PO2Wd1nGQ)KpcQ0rxRHqxN7LXWkwAQc=JY^R(xy)C%XPtw?! z|3T|gGbtqDM%+gCIX%nm-c4XExowwYa!5O0+c5&~o3oIARx=8ItH^v;JRgyRY^v>c zZTqOEDlRCcR8wEY=|lR^YJ4n_$7`nu>z=ALt`+_yxQTh+);Mx8q(H;wnJPwh}^SCb{x#-0P5)u+Z;=xPPbf#ou+1A+w!Svw4 zgYjT`qb3|UfR^aN|G|G?y_sG;aUe_o^(BT4Eww_CEc-#5>iQ#e<3$tLC(vKYQb^sisOz}H(N1b zD_QhZKX%g(-8+Cc?5CA@H||>_81}+ z6Xz7IQ*nY`K!>?XKxtXF<4IMPg>@;^04x+ScNoykY9ipW!`t5zZt4;icX11Yeibuq zZbrbZ6Yz9oQAbzxV99yh;;V&f)pk}~HHcN3`z;6|v6OG6>L8YC%mHYask3a(iBu;E z_K*a7h#IBAg^}tLcG1H<7SyJ?0vZ&)qkDhy$@o^L{-m+w5038)0h*Hl*l3uUe`1!7 z2Jw0wTir|+D`iDqT^PK6r{d&oAsx>y*;1)%*g*H|c2UmyR<{1MQSb*dI}?EB8~`>N zX7;~OZbggbg6saBaRTKd74k4og`;kQEX7$-7M*26iK7c3389^2QJc}V3Uk}2@jov% z9c!l>h3$a0r42=C8o)(Kcv;D+EO=m5woxgCvZ9K+j@P{p2$ONvw`Lnl%>jRK2BRqf z-_}X7XvQ~Z&$5ljw!eNASerRz1;E;t7 l4<&7bdY5L~05wZ$8=y{*+J>{Uk2p<-hy@SHa2m9OzX4m~Y1;q* literal 0 HcmV?d00001 diff --git a/tests/pyc/ordered_keyword_expansions.3.12.pyc b/tests/pyc/ordered_keyword_expansions.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aaa035b731c9511c573bb93d7f035fc9006d82b0 GIT binary patch literal 1513 zcmbtUOKTHR7@a$rq)CRTB`7{ngV9AGwqO#CQW`f_MFoK_<&)>#93xc3pYCn@1Z;>!Jg5=DW{1-}xTDB9X99)3nXpr{9k@ z_X2-lPaZb125A^u8rQyPZ#B+o z>wqmy*BTo4aolCG2HR#0eVct{ueGj`@B7QvhT!`;Hg?%Q5$S4OUtcfVv?Lw3A8B!N@p7zcL;!QFv06y7uC2y?6CB1lD9 zm-KtmXmI<8N2>X}wd_S7wC@2r4*@+hQ?Qu0ilF4$GOU~yr) zR@o9#Uc~{5n{$k1y7{72^rESD2GCgo^vtwpq|eK;?=Mw~j`M$#5mbPXNT~wlzPtf` zq^6|wI7wm#vnrQkFh<#9Sg3T43jd)u*zxcs{CV~NeL9iQrGd{7&dRP!Qq)Az1)++C zd@oic)y*}$$92oKmOHb(=we5B(Z!$ZfS#H2jND1elYE=Q731<7W*gcH+r=zB+YBT9 zXf&OF37-sOQ$MsK+`_P{gvp%lEWQ1uyA-m1D(YOBmqp;20>Ylp>2<*ojD7+hGb^0kUslyS_dMnw+) d^)bc{bjI!<1V?a}tTaJpVm)O8j2|l!{{UmYuW6n$?PC@`Vg5RKNTSn7fV+eV;4)3h$ysxcvHCb%UN=0PS-2fQ<_#g#vS zO%se8mJm1o2!Ft^bhfU#GiBw@dmkT#G_8xiWX|08eeSvUzF{H}4>87`czyW0`?LNV z?P=p6 zS(&XcJ*4AV8@$YSd0E-zn|zh^)lv|yStr6gQ*dFQpAnHh>xTlR=Fk+lUJ!9bwc(g9 z9hgd`f~+T8&t1oT-Vq3Qy#b~GVOBnf)+e+p;-qOfuGMhdwPq`bm==ht2aC9yPIrmY z7xqg_476;>!jjKSmy1_ye>Kq69I731diL zC(ta%6JQJDSeUScBP7y1n*ws^a)L1%xP@!x*i4nZi!)<3e8gs=YDOsmk-myZ^w4NF zEvN265?4v0chDGXl5Q7#E#284XlY+fUnX#c9FySgVQ}|Q8Vc{Va>Pxm>S2%!XaQYM z8Vw&`@W{1acMkmI^X^lG-X_98%lT^VMvO9Q>2u_;m8v#uASJKWY0+so70VQkXVokL za1S?7+}s(K`SzQR;U{z50zz*eVW8!GHGflWPKcgKP1yuZv;JWO(4WcOw4++^8=DT47 z)y;kUW_8=?9Q2k3$?cx-liNRz5eC|hukKu?Jk7T?JYrIQlWbdI1stVDML64=&%aCv z;fgc5Z$$%ubMM`M z{jIF5ggKuHU*zEvf1fjvZ*ZG&W>?>`)_bt4?67Hh!fAM7wb=^ew!>M?=V|yF?-CKC z?|OQm=i2WAJ=b{((5=8S&~igKLMB3q!-fpy78v7D@&v<(0y+za<2jx{NCJ!*^GmP@ zb;t7=cbRp-Y(5J+n4?KHCE(Dd2qQK;17{}KoF?`v{4-)hBR1otW)+!6j>P{&Cb?%d zn~t|D_+>bif+-fHLzx$&T?%xgvop|*Kr=3q_(Mv@_1%X0ZbN#o!c$W+V>>k;3T5#Y zzXI1|jfS_+cof>tIWR(qU?s`PJwVnL%^%A&J^D z#p-o$lk0m8d*5Xnd>QuSyMzJ%R4Jt=3Z)NDk|Vf_RuQ3eYCWO9VEssv F_zjmz8SwxB literal 0 HcmV?d00001 diff --git a/tests/pyc/ordered_keyword_expansions.3.9.pyc b/tests/pyc/ordered_keyword_expansions.3.9.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3905ae4d3d1020be99b162b57036adf65c79463 GIT binary patch literal 1309 zcmb_cOK;Oa5Z?7GuO@AINg$B8T0-;>AOr%gx%8GxWO>%r3dgo)9cWN)<%YyB;DXdj z~wUFebbpbrD`KJ3Aed;lZZC*OgCV-X(4oUnNqd$eBA zGIz)<1x2O!S}x_eTsr6SqdXQXf4r8oTltATRj_7Sp+t5ov2rUX$#@G*Sp`PLc?Rgy zo;5!nmCBUnh)$MsROTp0P^e(~S}ooZo~qL#%8br4^SD@40cccl$=M9Y7ZL|#w^(z0 z%laMxW~~J?uDM?_M-Ehz2{y=b>(gsB&{?T4RnZ#-{B4tRkMLH?O`&ekE~RwplueOB zcWf*2t1y2~+MPv3p|cayYh@f>m+_iQHqE)9=3LN)xjj2B*#{k$RNn{}oTa(r=pQnM zetp+>!EEs`&Qs*t?7a9W%d@Elm6bZt3J+Uk_WuK!%8%2?n7`vPq)}8zZJ+WG-fmH0 zT3fy6;j!a(p8)mV*H(t$eR7tv-HR}&D`Q!#Szn=p?5x>Yq61Wgib|rDYMRU_y-P=% zj@=}@#cVT`jS!j%ITI`cwpV8)bg@H$CcP6?POWGseTKga7p+2iDoIF>$vl0~VseY{ uIP6+!B8vrivXDm#xs|rJ$b6B1diifqBOAHK(WYhi3o=Cd zC#_q&cJ&8@w{sEQdFHFAZ3FkrnLB6Z&OP&xNJJR`Jh!%=-fv!C-jMr)d@^?p$q+aU z;1D=LB%Br@%WBlu0tAq&tK2hB9RUhG7$Kiv%*Q1)Ha5XpEOFifu|kxRa5BPrlSNIa z(K1A!2yN)FGPH@(^yp8G+8@(`6Kd#E>c+{KIMGtIZfpBOm?F!ZfY7Sk2_D&cojU>D z(i&1dF`Z7&GL|MSnLpM$mMt`yZ&o?ic%`L|SA>R1L*|-YCnmc_?NpeCX-g5q!%G-0 z2FN(VRWt#&sSLX(ncsHwmGm6p00vOWbLl0N&&DdyGu0PhxiQUCw| literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_1.3.12.pyc b/tests/pyc/padded_value_returns_1.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d55d035af45cb113f4e45dfbb17d2141625b4d6c GIT binary patch literal 629 zcmb7BJxc>Y5S_WbGv8`#Y$OB}HH{X5*od8#V511ygfn~ULB80%GYKk460Af)K^qZl zgeW5D&+s2yD_D!yNGfMzB8oOH?AzJhH?#BJKE+~T1^`#Fmz(!}6P_vl8rY5eK?=@+ zLlf43BO%tb6w7H}OaPr?RmflNa6_QMF5Gqrdm8OkRLvxl z$@aKoNk>j^*j2|BmYgn|oLfA*tMLC*jYri~Xhzsu{VBQ=FmCMOB%n89W^0Y^BZAL|J?4Q22G6L1+%vwN1o2TZd^ uS{GYd#~h*3Tb~6wFSLWRg%Wr3)|`mbmHKFZfZ&xO;#<_fVSkXI^Wq1a^ma7> literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_1.3.13.pyc b/tests/pyc/padded_value_returns_1.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ed911c6de4e66a2c9cafdf4432827c94e5540ec GIT binary patch literal 633 zcmb7BJx{_=6uq~v$hRVki7^Gy7+7SHu%MfR3k$);!BktY2^4c5C9v46n(01b&QHhHI~@11+!J*Po38500dX%*fcUS99sng2XEVL^(?0BW!Z z1T|#DrdAM#+K&=IXRaCryCbaub;vXCLIk}g=N^AoC1WwOaNp-scQJG zl3E6ovH_bS0}UK%RX^Nzq;%Zo5dGM$+4Agdq7D^=uaj+Be(1n+T9&UnUArySIN5C0 zU+dbkD~k>1r0bC_i^ryA+17qfmG6_yo330x?)!1scMp%K<90kraW0$YLW6L^9-k0#`Bl~7r3Nf>IPg#%XotZ+@?3gqbTDOZir`o sSBKgt9#ZAQaf9&|l=JLm=X3B7_S;ieYr)7Zy}*bN~PV literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_1.3.14.pyc b/tests/pyc/padded_value_returns_1.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6ccbc43a82f9cb5eb698044a9c42fb425a502b2 GIT binary patch literal 641 zcmaJY6r6W^N%TyjXknqo#DEwC(-;USc48wILBv7`F?Z@ozSzB!*afv#&{nLC zjR@NNJFc~{6yp!@Z4$*oJ=nR~z1g>KW^X3K)ln z0x?p-NFyW4#l)|R5*snQ(G?Liqe+9y@W56V)p>+j( z>3m}fTI*s-Ou?l#1(B+&lv~?pYHgJ&zOyQ2U1mgJN~dH8pp99(**&-E`O;C@M#;7vyLhCx7p23CHMP(<4u;fmt$ZNswYsn55N{6i=;$t0l;I>Y zip#N#xTRmbpK7VcFvBid-1SfYYwYQeyJ=EOtcNW8mPZ8Zv41k5zcR@Q&c)b}`T}H# zAtPi&^8pQm3|~1&izh*}^wZ@qFU7S`8&q=)CzPi%D}i&GG%w5qt_JgTO$pqL<-~)? z(<9}?6TdgwMmHm=OSF3hdM6AlSL?Q4aaN@MbM%#WwB}6^(R(yR;~ygo-4j0nH^y!Z literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_3.3.11.pyc b/tests/pyc/padded_value_returns_3.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d8162a77b8647e829a48fcddfff44b264d58f7d GIT binary patch literal 681 zcmcIhyG{Z@6uq+p@-|>X>Xw>`mU99%LrTNEoG>R`C|7O$1hI}8?)XqI3&($M8&%=> z3`?&`b=6cVHQ%*#Y01M2qixx!%fm)l2wjw#YI_NFN}BSZ(QzWOV^*%PZq{vy5!$>) z;ZlH%67<3`xKF0}n5BQ)(pSwei{?jH@eWRZs!!e+EP(g@b zX$gjD;Y7>d8(ycB3RJy%4G-c;ct~dXBg^vKMDr{`C7CQl-YN6oQLQfQs(y?K`lTM% Z-6xoF#@HY5S_WbGv8kz*vWw+2EmkpAa+(l8bMG5IkTr8Y?u3mR6cqINcu+M@lku%`X zg+<^ulFHAhOPzScuOK5tb$dq#+c$TMFTw#9v8#dE6M*B#fcNmZ0- znr~ySEnQhyby}_`Y+2Z_IJf!orV3vcHXYXG zE7(&CWjfJHwFB#H8XOFja+GSgLbzTn#3-|E)&qN!bDtMjq;lJ|B5gl#>sF+?ExRFP zS%zUm^DSGn#IoZaw*qd9<)&#_wzYR8!}qvNhn`q!oB{l#4(792`-)B<0M*; zgGq29heXUNbC6_ZW#9!+Ny}2*Z9)rTP=ExaNz2tx@|b|UlK!k2qwg`80|U1D2I3G{ z5r8R}0qM=bm)BF`J-75bu*TxxUs;O>c?G)C^U4tMEv(_>`xv3O F;|F~tZ#)11 literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_9.3.11.pyc b/tests/pyc/padded_value_returns_9.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adb6012757c7654420d502f3e4ea24fb79978c66 GIT binary patch literal 729 zcmc(dPfG$p7{=e3(fl74h*u#AgwmU*=+-IlA_#U7bGBO0Rc3b8$Sx@nLB|mK0LCI6 z`xc!$Y`6LX!rnYY@2qKs-karp_LV=NYC0Px!0dHK9~fBQhc806czn`nl> zVE{+K2`b^V2pLwRi!6W-z1qsX_0l1r(1Q{BG1hEcQX{9ZoNy5np&l}Ir zgc>bFXz3|J2fC~b9in=A^l#6qyiE&EprK2t8>cpL!li26(hh`ZiVSmnLaTDee{AV> z?)Y?5Ye@AAsZ?sZZEDh#xf8u>T0)b#R+V#&SK6w*A~akyWVUtgMC7?qI}=T#X-N^m z<~1xA3uF}KDjb8mM3y~~>@u#tiawwm0zts5ko6J!1uMajUHkq6pHMu6A0@*?K>47O z5Xu~t2;;UHvw}@MR6<4US;*UE8r*9%xmDNp#Rh&- bm%-gdnJ_}gBY5S`h*GvAtkB3K2KKpJD0Mg$8h!A20Yku!VhLB80%GX@nzgO#Wt7_bvW zh+?a?wg2E+!CJgVtelN8sA&ZE*vFf>nYXi#eM}^x8~`p7&)09e7CaH}F6^)%Nu&t^ z8n6Tc39-(yyuiAZ00wa_6u9l$2r$@&n@+(FqX@UzO-C^TzrT4H1>O0%#y}3DRzQJY zJq5Pd(-nbvsKF**gc`QkoF6JVN;&1V7W&ZztLWJ?QdVS^`-ZeF;hQU-Qxv}8xOPeD zVbkgKd&aeut8%N(rt3*t<*JqtwpiZK?aR`pWLb?@>wZkt%Y}7WDOWrtW7OP7JJ9p0 zIKf_M1kO`=euw#9WA=6F3BfEN8Tr$keKK~7ra9wHeqfsL7qjwkIP_n`Zg~x*c0rm1 zSuR7Ghc!e9z6_F43jDbB{rdfs(p&1As|kWVobJO$Gy_LCW*i$CI89OF5;Jg>n&$jI zM$rR9q7R%CpKGzUiBik7K84yRy@w}D72y@_DVd^Q`oRPP1TP#BU*jeY2T6kV$sZ*1 Bc*Ot! literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_9.3.13.pyc b/tests/pyc/padded_value_returns_9.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7812335a6fa7074016568ee4b729b8bf006606df GIT binary patch literal 705 zcmbtSJx>Bb5S_U_JOuo}XiO{sk&s-YHbe?Ep|G%_A=p@ulRNMdILIzXq(y5@G&I)6 z1pk1xbe7iF`Ui~n2b@J9L>d!!voCMw-ptOtoWHabVgQh<>+jFQx2F%{(}iQ^uM+7) z5w?Is5ec!T=&Yhll>jPnS;+50~oRs(2YtOF!K zW)*RgC6xGFUJO!Dg+msHDh`x_r!{OLY^OFrFLY|uUF(?hCfAv#a?3Eiz`1MJO;5Gk zRzt{k6N!YEZd;-)_Nw+}+vS$nYZ<0#ndOd5FLR56j@WH=y|Cyyl?&cuY!&;WTV0PY`D=EDr*rBodG{&qwCuHpfb<@E=Uc>898g4M9-l}Q1k5b?f)9|#G zSJ`WrZbODdhbkjp7$_qfxhymIDfIJFFxP0BZrwWKQTil*8h?f0lOf_)B!HX#D#7UG EZ-+p0qyPW_ literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_9.3.14.pyc b/tests/pyc/padded_value_returns_9.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64d17f012cdbbe19ec5f2f849620d16f45a6abb9 GIT binary patch literal 713 zcma)4Jxc>Y5PfrdXW}J(BZ5(kMgl=l3oS$uEW}1Ef>;R>bElr#69-$c6c*8^KK_PJ8S{qIlQs+54YwM^RGoM*63wYLly(@ zktIgT8YyH%-uTi2CUae?QEjRd*uW@eT7rtPPNP^iW?D59`1z%f#AZtkw87~IhA;x1 zm&IpZU=07tOdtgh^I{kt)hUb=J*B+TE>n9WR|wo?Da$e=LQ}dqC$zQ$ui%8H=etFv z`%Na3VcK_eNDQfSP7FqNOOE*lC>7xOrx#ao)p$ zD@x94=O*Y}$5_w)Ppo5#~A5<=d6 zO+NqKlWqA4dG(;>Y_|)NWulY!JmgrcR+N^In@TOvJRI!;KC2= z4nbHZ(`24nWROg@5;7qXGB3=Fcv|#D?J9T|4@=NVihe22P_lSZA{GsiJRQr^k~5M> z7;!x>m7K35v6Nw!oXjv&v1G}?pGz)1YHAUt8@dUfBB$Zl4<-#WY4%P<-zHP=b}us$ zMkHzI;Z!^k(O!n1MdNxjK5h0S=1QLGWNc<8!Q8M1$}#&OERb!Fcj3$^w54V(1v12F~SUItJcWo|xJKM7EZr@Ns1u3)#H}2(!=2mB&5GSnW zLWUu!;oMfkS(=v);%rHlI3?DQZnsHuNetNFy9Q9c@08^TksNc>n_Tjz-@L(%~rXkve6Y^!GB>>zKGEi<0i5@Wo^>RPR;2HC(L zk944#fURb5cuN^6o*l^b6@vpS{i{(Vv+h2%p^Oxy5$LoLyVHDa&N0==|317~8P+o= z&CnpeFE1Hhquj=Q`NpdmJry%17 literal 0 HcmV?d00001 diff --git a/tests/pyc/repeated_unpack_bindings.3.13.pyc b/tests/pyc/repeated_unpack_bindings.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de82713859dd366dbc8961d5b03bb71c3d5563b6 GIT binary patch literal 1553 zcmai!O-vI(6vyA}ZkL6nARj?RFnmLcp@1ck#25~w0m@fcOYlT0T}WfyHZxr#H%^Kt zxsY&d;)QzR$ia)=;M58dtOrfJc~W}fVtg}ON}GsovM;Z*JJbIEzxQw6?XHpm;On~| zZ-2j;n*WY{@gjvT--pdM7|;w941$#y5?0BOvC4*mRWV#xU2CeRV&!5V-|oVje?fj4 z%QpDHA|YsiiCX}+0$@pP&<2(qA`7Lv@Lhg|Bm<_&Na0Bv%(MdN_?O9IA)tHr&5#Iv1HCAKIbYUQMMAM5i`Q@>!1yp6GvVJsd`&}uw`OJajqHsI7mQ&3*+mko#Kg5Dk{p7 zv<}y}FxG=*13phL9P7qmAlosK>fXJ%H@UyQ(|AqR z`-Pk-Y)W7^J2DcTh+6qO>ZPub?3YVWw_KtYqA17baXGrUZ$ey-EIax*6x4U2u-3LB zCS@yeYAy?KjrjZ;-fRHN1vu|uR_obxZFzQz*zDc9OjG}fb_cQb4+(4({VvDGZ528v zu(`yxC^Mx14n61Nov24_LrcSc8L^?oe0Z5+m4YjLDxjmac%nxsq1S*5jzn(26$&}U zCVYGyZ`OqcDNf9O(&rAXYTcWzY;E^e z<<96u%cWifbFUrB(evjb_W|Sa?E9iu)p;evrsNx?- zB$V JtJ6fx`5zn^F24W( literal 0 HcmV?d00001 diff --git a/tests/pyc/repeated_unpack_bindings.3.14.pyc b/tests/pyc/repeated_unpack_bindings.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b94832cd1faec0c69d2b2c8894692c04b343d39e GIT binary patch literal 1549 zcmai!%TE(g6voe;nGREi@{)kcoAPQgw53f%o6O1Wy`3}tzTf#~pxRX_0>I}_Z{JLe z!^0oAE`CUUb>YQD0yJoXIA{bb(FCl5CSnye39FPz!-(fK3U&5Za*~3^7C&O7FtovQJ3RV1|t5Pud~a3gCn`2oX1=$Xtr#zu5ng?<@-!NYL}SlQpRtv56%W_e*9}^QJ(whO2ulJ^T{Vfow~>1p#kcL) z-rDsfpPVYyA6*|@J9=_7-Pm`c^#2mUKlWX%OFX*3d!Wo61vj`m6TBUBW{E5bmgoaZ z2&j3F2=i#8lX)>G1YNcVmS_kD`e31h_Bslb^-4iXq!qZJPF5mu z*5MwLW%go8z}c*JtS%k}GF?;2p1oW9(+69-4cV&Jqrew+F)b}#X4OS*<-59OxlD|V z_j55byipPLZCeI)BWUUGpqINtqF*cl-Esw*i=qsk$6@PYu5ocWvSRCFNEq6@!Wt_U z(W$w*PW2UztpU$#@iKi_3fR0u8KrmIvE$h-LbLYj(v1Tr${mC@FwCJ*^t&7yvz6-% z@MZ;QOQI!lZ0I-}ZCa1ehL=bDB0@uh+2C4|QHm}9Ef0>?U_Xyk!tMrc*aDe+CeS8W zrjIdf#N#Hs%x)~maBA+WI)5Z*dMA_mzVl#gXF6Nec_e4m`Lr~Dd92QJg70fvlrn<< z3ah}vx{3ArxGuZ>LJ7l?uwF5wY)XnzwB@KV%p=B+qq>Rb_;LVqkV~-><{8W=J==~< zP0x;VcdQUGE8WGXZtU(*|a@W?!R1*SZn$N%FJVLOrGbTA|d3wQX=)|)r34I=SqIL NU@w2;?kwRm{s*A^y5xf_qH!mS%QV66;*-fE^+_Y!^Lg?MAKaz_F zPd$6{`@UxjAs*j*x36!XA72>%Sr)$C<2Ki5K!AJ^PXa8k z(MwR^pts-%YUpinp^jdGHxQlUh;rA9Vukwww`rj%v05qT7S60L*EBTAwPB^HcIB^GFq}s>Trzbuhr!x-iZaMjf;G2S za9|H@lA@#fOTt_d?In?3VeUisM#(e=hCacI+-X~CALAWx2S4%*ni!O#;3D^O zAuETa-P@@mh0ih95V5AzXXp;2tXkZl>}q<7`FoM-gv3o<&=u-T6L7!d@DN#CZjw3h d+16b6zv1Dyy-^O6G`=G}4GqV2Tt!bq{Q^-pe$D^@ literal 0 HcmV?d00001 diff --git a/tests/pyc/slice_constants_and_calls.3.11.pyc b/tests/pyc/slice_constants_and_calls.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef6510a8b88e9556ef18ffe6d3a45e6847ca6576 GIT binary patch literal 1055 zcma))&ubGw6vt#e=8Z0=e~2{Jz<3EGAWVGT*$n^XBo{_ul7hHf1x$-kty0d2{~i z^-tK7hbPuJ595SM_MYuA$)Q|YP)piSTMDR<&Mqs=RjkQv#sq9iKE7w}caWjOyOzX% z*Q#1;lh9q(U32$P*HOoETj>Os+XY60x zL_CXI(f29(P~|`pX63>jWG@(Fm<-;fDCRc|4{1F$OB_Fs#EPPb*)aEUa>7tV-a+}h zc8nwKs(rbT1yLGO--cAZ;XLFDx>Tdq_GOH=jIc#)eHRkfiHCXA$UWUKYbRXnr`IE~ zKFvq8io|;Dqw@ z<;VQDr3Zcd(wmXk{0rJM-1{H2t2=xH8K&_T2rn+-Ed;D$YiiRB#xKjXqNRuZbTJY| z16wLMW<(W@hN32zPgRsF?ix*l8KyJ$MWZDT>i)K(@u3pyJ;=!BoR2b`-x;xqxSX$& Wn+V(-xrx9nkedkHN96t~hVU1EqR6%Y literal 0 HcmV?d00001 diff --git a/tests/pyc/slice_constants_and_calls.3.12.pyc b/tests/pyc/slice_constants_and_calls.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63c9d7a860196c95e92469515502c77a72c0aec7 GIT binary patch literal 921 zcmaiz&ubGw6vtmOa9f|kQ zX*g?`eaXhWHfCcd71EFVROqO?r>RJ}d{T`<9qFA$vls2FQ19qgGYIQJx1%G!6Y09& zk@diDwe-Wiep-045Jz z3n1NwYYnT2+LZb-C6}gGAeZpu+Ra`lEzAmMZBNFskOBe|$y*O4<%vp_V~pBvafH2OB=Z={b)NojkMuO^~uxM~&8Trfo~ zR%ER?lAAmS7{={8Uz_Hmq|g>x5Dh3&n+8!4l?9mJ5Z#26#FWrQ(3#6 zA}Uo(tl^(vVqm+71>L~HlnwR%0iM@xD?wVYr672L%C}~E!T$Hu7HX+PHoP&E^`?Zh-vHWj?Qi&zr`Dti*Li) zwH%CA*_b!SY;2_>@FFi2DjMu7+N4rR>rtp8wez|&j1FX|c2u|Hhi!k*SCQ9`RNLzZ zZQtv5)#Lrcv=|)rykPxdz${_eBDz|G7(aMijp4?J2d6LN#Ytf!5gW5q zpc;>f%Z0V!G@p&7v7LZO@pejxN=#MS{vDSFrvjx>|*3nDU-IWspbul-~sE BrNaOK literal 0 HcmV?d00001 diff --git a/tests/pyc/slice_constants_and_calls.3.14.pyc b/tests/pyc/slice_constants_and_calls.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..376bceb9fc6a47c40e815493107f56dd97c78a3e GIT binary patch literal 945 zcmaKqziZo25Xaw1PpNI$u2a&4q@_uTN$of!gmfw;Q)fZnpeeNxk_ra@NZ(Vy-m(T7 z(9pqS$NFFBUm+-lqOEl5R^m>bdiRtc*rpNOSC7-XyN~X!Q7#os#@M@y^skry@$n~~ zZy&fmO{;h~XM(Mh@y_*2pQVKksS9PyGLMe5biTe=Kcu%s@~94Y8%K zx~vHt=3K=Z+oIS=%UMs*)aq1zsWL55MmyJa-Bu5(jQAg)qJM$boYbJ^^ ziCs&0?H-`v@P!W){+saty8B#l{8 zll5;&N|5abO*vbP-0N;XIO~MRat(ddM)eHkw+R4OMSzW~9Kb5c7D@dN%AT(B58vve AC;$Ke literal 0 HcmV?d00001 diff --git a/tests/pyc/slice_constants_and_calls.3.8.pyc b/tests/pyc/slice_constants_and_calls.3.8.pyc new file mode 100644 index 0000000000000000000000000000000000000000..258bdc8c385e42e94d4258d4983520fcd401d96d GIT binary patch literal 770 zcmaiy!AiqG5QcX*8=J;PMWyHyNKxzqC?a_C(1S$}UP8#EP)O6VyM-2V(>_dIDOXQ@ zg`S+*5E^OKgx#6!X7>AMW?zHAcNk-LZ)aC8o9^=$`ad3lUynHS4Nky-f3kN50vxm) zB)Dh=96$@L10J-|O7MrQcNkM_{g5wlJmJviIOTX+&Q}7@WGgWemu$!iJ}6irN(qsP zN~z6srYO)(n`@J3qx+-r!pv2o`#K%RNf=KvZKBNRFv=i|qcqjGvt`+VWfo1waTqNU zeLzsFhN~9(|0^R+O=XJLG8C0-t4h(?Wl*cII3Jg4oQMAJxC`9HLhzh}SUD9CoRLGt z=*hvFsB03wCfOBrAK#yh7YSJS5&l@iJLvDgU0k$XI6)_8#`Xc&hlSca3tqamxO;Pz zRO8bI<@irZmExANTh##L_mbK~vPpFtIFdc+xwest%Bv_%MJg2SHF{(3j%|>4KbgWj NO&;u3peRpP?>8d&dw~D| literal 0 HcmV?d00001 diff --git a/tests/pyc/slice_constants_and_calls.3.9.pyc b/tests/pyc/slice_constants_and_calls.3.9.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60518fac679c236667ad207111ed03f045f00cde GIT binary patch literal 770 zcmaiyu};G<5Qc5Xp=oFUAymO5R7La!5JF<_(v2mua;X$4O)EQK+{=2gZ0^ebbg>Rep*N^`26a62Lz^_Lf<`ySlz+c!i0|5?N z4ia3n0#2ZV)&UQ?XeIa)HaLwawt2{xIG%8rE1U{Et>9aMXR@uBiW@dzB_Efp6qN*( zt4f-DzR(nCr>|iio7{}1$tqv!*o;h?L~$6+Gn1<BSTZU_Np|UT?LH_i}P`*!FlNaj=RQPECeq&h^fkMaFkvWmgN&+x|v-b4QY?&6~D!UZ}7%WWTkZ7kI0S@O!Y#XXv< zq#9qgDCd7t>J;~s!>R@tf0WcEl1-}L!jbIpz_pFkR9;PKYEq$TuhkoScWi^a``H|p OY5ZWX0!4YUe!l@qOnp57 literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.11.pyc b/tests/pyc/sunk_value_return_control.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96c6d1a85a09c697e5017c83fd9e97636abfe452 GIT binary patch literal 460 zcmZXQ&r1S96vyAp?z(ELNnm9{2zikp|9~P2>d?hYC%Z_wqin9L%*>{OLPUo>bm$NW z{R7dz;9t>E_S&(_f&`1F-dfh7f%)=%Kj!m>_mWB_6#z(oG$)gzV)7mR?;$^KW}q2^ z3HNXTCOL-^tuJd)i;6db%uZ0okbogvqx(K1Vv-Sc)^tbzJ@U$_~~OQxpn1AcFK8&mlR*SxwKuzH5}lp zEz2=Q^q)eZFyEC-NKtNBed!{wEL^8mTXfer!<192cM#gK_qqc=CYa6Yk|*(T1Q2H^ zw-8WG%S|)crxi~N;*iOCs;7pZIv8645H0s7)78zW=Ba^#`7jKz-N-yFW$Ws_bYZlum2qYwk>0~2-|H32m7s+*N AP5=M^ literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.12.pyc b/tests/pyc/sunk_value_return_control.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58db421e78e9e63bfd11358b6ef7445be05c7bc7 GIT binary patch literal 444 zcmZ8dK}!Nb6n-7)VxbF*?&d^6^s$p4BgS(rpJ0UPe& z3c3OMgfgcmB#p;qrFOLGmF~ThbRMT=( zGdnS!jIkCZz|4)+sgZfij`u&*jh}BFQrllZ(3IJFP=gqx%%v9SIGu?U=G>*$;n^rC sFQlN9EwZ@e6+%>i>j<;NLAzsnEq2J$*hZf%qD2CQ5E91XWRpiRzhSdxtN;K2 literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.13.pyc b/tests/pyc/sunk_value_return_control.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68224696b4b55b851933f1f99abab0b88b7d65fd GIT binary patch literal 438 zcmZ9IyGsK>5XNWkk;~&^0^%bn%n4}@!8CThu&_;G5Cn@5lTA2txs%;JNjkBLrAV;X zO8h(QEyK1}b_Zu6R?bFa;lK{x%s2dYc9VQQs{_FB<^1_Qeu&>t{{{I~NfyNf8c+rX zJ>qKZxuXUQJPMK2F->3%j^PN^{|HEf1lozSrBz`aPPMsjLCXSgprHj?1%CW4?CKTh z5?8NF>w-~E10k(}$GI2uTuqu-vyogl5Q2GUk)Vf+h0N8ZdD-$K%1neyqwfVBt~PTV zM{Wqp1+R6zp@C2LxhI@=EX>+IQ3~;F!gQ);QN@aAl6K?G%YUO z=WcUjGfqRXG&ZKW!g%}DSpNLTfL%_2;7DQh#)wm37iUhK!QL0Qk#TJ=!kt~!A4yfO oyJU936C~&W%V3h}ok7@%e7eW-SgLR4YKd4Ugd{18tT2W71~oBbdH?_b literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.14.pyc b/tests/pyc/sunk_value_return_control.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f8c9e4dfb4f8cb62004a630efb9059997bf98a6 GIT binary patch literal 441 zcmZ8dyGjF55S_cPyfzOIjZuROL177)!ZPBcuoX$+1B9T-RhA^X%e{BSbipc?V&M0H~N@4Gt%RAnhbMTN| tQ=iD1dSH|IgJ+l^11yCpOm6kOcF?8UEQ_VQGnY$5Dj{TQBuJ4-n;+e^VfFw3 literal 0 HcmV?d00001 diff --git a/tests/pyc/unpacked_comprehension_cleanup.3.11.pyc b/tests/pyc/unpacked_comprehension_cleanup.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5f5cc50a81020ae9a557e530bac780d2de453bb GIT binary patch literal 4364 zcmds4%}*Og6yNoJ_=^n~D520av6{4X%ZF(JwG~COMBo+)(NjbOj(43{@#n1FCTJ~j zs8l2QVA=!A&8OB@FRpr_>Lq^ymN=lj_QVZ|8<+OIne}!ZTw2sN2dwo^GqXGMX5a7k z-n{)mQ33+TaXBU2=YfTEN-yH=#jSDn`0Ug|I+uv2^+jlOZu-E8{wcsVcc?70 zt1iAAX04w-Yt6Lc2cd&lC0+~fXtnrG$_(#hoJXN?29voHgr;zU+?{w3kTv>#&4498 zA-PgsCpU1cA7KC?1Yq%+#V0Kp-l~)?T2ee&)QBZ#OPR+S(UB460z@*16TJ4@kTCSn zNBSnfHg|k;vMmm8$F>WVaAmc|*Jh6C3pHfeWLsH2QkJ)ukNcGE`^SUhryMVhQmDws zKBYFY(_fJ)(s3|Ui`5Ed*j%mi^_jz=d-c*`aN?=?n2T7nhR71*BB#=7K!x1bZ~=GnDBY=+EOE5>@wrADk#s1$19O44}*8q~Gq4VX$s5HKjn7(h?@gRXs$ zci}12)7d6KZwcU5>LUPcW&TK+zpfH^iYe$)34D7&0^}}u?G>`m{R_Hg=Z#yot6{*! zc;_}UMD8JAX#8XT0)H+_K7@&*IQhNKGF>WveaZil2>SrGw#TK=y#rkl=5t5bcS6-) z4Y}j1r*i`fYARuSw-Zn};&QL^z&l`Ap9a8ynq}8+5tCCZ%zDkcjc{WXGIPjz02)`L zqZ3YOmY_Avs+VL`+=%%lKwFtTQf9ABIHA^PMqPx4ol(cS_#Sll#Kv^0Ef~HUvobSH(9XwbYm?gQRaT)16VC8xTZtYi(d$Pn))h9f zH@cF5To$mTx9RQgqF1E!TEa8!1*@tk$096+(d{Dn%}naKruO0(laD@0{eGy-v1@JZ%=X$<{)vV*QY5TyJ5-&2l}{U1^O zAmXE+X|zzMfj!7DFe*mzB*z5oR literal 0 HcmV?d00001 diff --git a/tests/pyc/unpacked_comprehension_cleanup.3.12.pyc b/tests/pyc/unpacked_comprehension_cleanup.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb9e343fbf9bfb484c1c32461c3c88be6438ccf4 GIT binary patch literal 3386 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 literal 0 HcmV?d00001 diff --git a/tests/pyc/unpacked_comprehension_cleanup.3.14.pyc b/tests/pyc/unpacked_comprehension_cleanup.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08964896ceee90da73a53605b07c1205859c6348 GIT binary patch literal 3278 zcmcguO>7%Q6rS;}*XuZ&#&Jm6QWC5rZQbV2BsCJMkQTKLQlv<+@H!_V*IC1=v9q4_ zHto$f?6zsm1=&bQ2@+Dwi8Pn?ShyfgDfEx!01~I%pe`V8ycz$*PMwmJpOyC89ea27 z&G&up%{$#8wQ&fc&ClNY@mA@BPkx5``hlx=`kk1*b%|a(@hm(G;{0#uih?fO~NEewTih?>(*b&BITbK*;rZ9?3 zTW}De)N&^Z9z$rEuRIy)8isdkhr%_D_{Px;H{FMj6ZN6#YQ%VvF_0McaiQ4 z*Zt-b~$ar8)IftkH#*l3-ht1 zgih4=)uf>_}%bl0=se-pKyNb6wL#yVNYj}$v{_}Sy>X^cY9X@&Ulzgnc$tOr&p?Zqd(PYFkVFM3P8(P)*axCELqFNMPZ2KCO4l;j8uEmuXH`9;l0 zlULvuI%7EouuCBCiHA~Pt*6*saBs?`bL(<8buGRj=i+NUnc#O);DECH2C}?frmEaH zB0Jk&OqFq(yU$VO;{U7)f%?&!=C&%$hMFc`c!=sjXzG%FfYz&Dg@0PquDU13=;}f7 z22VP6Jb8he26r<@f*0r&s!^pX{U?sxd+jxgEoiYc8GuDm0*n?G>mPb3O%^YGn9ip* zy`{l*Z#H^$VZ)n?7B5kYO}>D|&alM4aab0cEk<*QCR6!qfW1i$+rxtCV95_UH4qJy zveBf%Ln#2F*+_|a6#E1>!%suiWYj|jvMVgPDnUJ+b>X0eVa`4$z~-=18XUTy*XL_2 z4||JIEye*d1Pea}r(rS%nu_t@sYlY(mNc~%ES@XKo2gQIJ@wV>op)}}W>fjuYueq} zoVJn5X=}ku@PRb74_#LMjWziXNdC7gH90>dAMI%JKX|wz&2<)7i<-=t9FqhpV9E{j z!=94zD;%rBP?Z_sP!p?!W1ZVS(F*Lrk@HG3%^c(n-XFb!p(XT1m7IXsC*U;Z*&>?Q z?Hjx=jThqu_vg`dciwv~UW(@8#ds!oPa6MwDw~Kb1Cf4a@$20(5}J4fCMrA@SP0MX zNP8g8qxf~aUfI2^ z?dnh}`Oj%mI~n z3Q7c+5-Yv3U2LtyN3E5>(2v%X0N*lC*SZU1S9byRzN&)*82mSMfO#5qAU;Z0OHN&k z(|IkKR0$b@$IjD7TQJ)<2+UH4-}0Aw);+9K0A{Jj{}Ho2GB3{fMAk^MW`*sgY)@hJ zjHMmZoQ20ar82~bDxMI}CYBb=MfFWG2ESn%JYB^v9LD%byMRZYbUhEA(tpV)yzv+F C!Ji)h literal 0 HcmV?d00001 diff --git a/tools/compile_padded_returns.py b/tools/compile_padded_returns.py new file mode 100644 index 00000000..bdb472d9 --- /dev/null +++ b/tools/compile_padded_returns.py @@ -0,0 +1,81 @@ +"""Regenerate the padded handler-return fixtures with CPython 3.11–3.14. + +Run once per interpreter from any directory. This prepares bytecode inputs +for tests/integration.rs; it is not a separate test runner. Only RETURN_VALUE +is padded, leaving versions that emit RETURN_CONST as compatibility controls. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +def pad_return_values(code, count=3): + """Add NOP padding while retaining jumps and exact handler boundaries.""" + import dis + instructions = list(dis.get_instructions(code)) + # This compact fixture needs no extended operands. NOPs + # reproduce the padded return layout, preserving the return's value. + assert not any(item.opname == 'EXTENDED_ARG' for item in instructions) + starts, sizes, cursor = {}, {}, 0 + for index, item in enumerate(instructions): + starts[item.offset] = cursor + end = instructions[index + 1].offset if index + 1 < len(instructions) else len(code.co_code) + sizes[item.offset] = end - item.offset + cursor += sizes[item.offset] + (2 * count if item.opname == 'RETURN_VALUE' else 0) + starts[len(code.co_code)] = cursor + output = bytearray() + for item in instructions: + if item.opname == 'RETURN_VALUE': + output.extend(bytes((dis.opmap['NOP'], 0)) * count) + actual = len(output) + raw = bytearray(code.co_code[item.offset:item.offset + sizes[item.offset]]) + if item.opcode in getattr(dis, "hasjump", dis.hasjrel + dis.hasjabs): + delta = (starts[item.argval] - (actual + sizes[item.offset])) // 2 + operand = -delta if 'BACKWARD' in item.opname else delta + assert 0 <= operand <= 255 + raw[1] = operand + output.extend(raw) + def varint(value, marker=False): + digits = [value & 63] + while value >> 6: + value >>= 6 + digits.append(value & 63) + digits.reverse() + for index in range(len(digits) - 1): + digits[index] |= 64 + if marker: + digits[0] |= 128 + return bytes(digits) + table = b''.join(varint(starts[row.start] // 2, True) + + varint((starts[row.end] - starts[row.start]) // 2) + + varint(starts[row.target] // 2) + + varint((row.depth << 1) | row.lasti) + for row in dis.Bytecode(code).exception_entries) + padded = code.replace(co_code=bytes(output), co_exceptiontable=table, co_linetable=b'') + return padded + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 14): + raise SystemExit("Use CPython 3.11, 3.12, 3.13 or 3.14") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/fixtures/handler_return_padding.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (1, 3, 9): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + path = root / f"tests/pyc/handler_return_padding_{count}.{version}.pyc" + path.write_bytes(header + marshal.dumps(padded)) + if count == 3: + # Also exercise a representative padded input in verify_corpus.py. + # Keep the directory named after the interpreter producing the pyc. + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "handler_return_padding_3.py").write_bytes(source) + (corpus / "handler_return_padding_3.pyc").write_bytes(header + marshal.dumps(padded)) diff --git a/tools/compile_padded_value_returns.py b/tools/compile_padded_value_returns.py new file mode 100644 index 00000000..52e1f539 --- /dev/null +++ b/tools/compile_padded_value_returns.py @@ -0,0 +1,81 @@ +"""Regenerate the padded value-return fixtures with CPython 3.11–3.14. + +Run once per interpreter from any directory. This prepares bytecode inputs +for tests/integration.rs; it is not a separate test runner. Only RETURN_VALUE +is padded, leaving versions that emit RETURN_CONST as compatibility controls. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +def pad_return_values(code, count=3): + """Add NOP padding while retaining jumps and exact handler boundaries.""" + import dis + instructions = list(dis.get_instructions(code)) + # This compact fixture needs no extended operands. NOPs + # reproduce the padded return layout, preserving the return's value. + assert not any(item.opname == 'EXTENDED_ARG' for item in instructions) + starts, sizes, cursor = {}, {}, 0 + for index, item in enumerate(instructions): + starts[item.offset] = cursor + end = instructions[index + 1].offset if index + 1 < len(instructions) else len(code.co_code) + sizes[item.offset] = end - item.offset + cursor += sizes[item.offset] + (2 * count if item.opname == 'RETURN_VALUE' else 0) + starts[len(code.co_code)] = cursor + output = bytearray() + for item in instructions: + if item.opname == 'RETURN_VALUE': + output.extend(bytes((dis.opmap['NOP'], 0)) * count) + actual = len(output) + raw = bytearray(code.co_code[item.offset:item.offset + sizes[item.offset]]) + if item.opcode in getattr(dis, "hasjump", dis.hasjrel + dis.hasjabs): + delta = (starts[item.argval] - (actual + sizes[item.offset])) // 2 + operand = -delta if 'BACKWARD' in item.opname else delta + assert 0 <= operand <= 255 + raw[1] = operand + output.extend(raw) + def varint(value, marker=False): + digits = [value & 63] + while value >> 6: + value >>= 6 + digits.append(value & 63) + digits.reverse() + for index in range(len(digits) - 1): + digits[index] |= 64 + if marker: + digits[0] |= 128 + return bytes(digits) + table = b''.join(varint(starts[row.start] // 2, True) + + varint((starts[row.end] - starts[row.start]) // 2) + + varint(starts[row.target] // 2) + + varint((row.depth << 1) | row.lasti) + for row in dis.Bytecode(code).exception_entries) + padded = code.replace(co_code=bytes(output), co_exceptiontable=table, co_linetable=b'') + return padded + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 14): + raise SystemExit("Use CPython 3.11, 3.12, 3.13 or 3.14") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/fixtures/padded_value_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (1, 3, 9): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + path = root / f"tests/pyc/padded_value_returns_{count}.{version}.pyc" + path.write_bytes(header + marshal.dumps(padded)) + if count == 3: + # Also exercise a representative padded input in verify_corpus.py. + # Keep the directory named after the interpreter producing the pyc. + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "padded_value_returns_3.py").write_bytes(source) + (corpus / "padded_value_returns_3.pyc").write_bytes(header + marshal.dumps(padded)) From adb4a87ce02da5b460069c8fc73b17f09e4a9cd0 Mon Sep 17 00:00:00 2001 From: ejfkdev <152914458+ejfkdev@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:57:50 +0800 Subject: [PATCH 25/25] Integrate community PRs #1-#26 Merges the 26 targeted decompiler fixes from @EiNSTeiN- (3.11-3.14 bytecode families) on top of main. Three textual conflicts were resolved by keeping both sides: PR #6 stacks on #5, all PRs append tests at the same point in tests/integration.rs, and #4/#5/#6/#24 append test-module declarations at the end of decompiler.rs. Verification with the standard release profile: - real-world corpus 6411/6411 recompile (was 6408/6411) - stdlib corpus 528/528 semantic pass (520 + 8 new padded fixtures) - behavior matrix: all cases pass on 3.9-3.14 (56 failures remain on 3.15, all pre-existing; main had 61 there) - cargo test: 37 integration + 8 + 27 unit tests pass README/CHANGELOG/LIMITATIONS updated (100% real-world, AST-equal 2493). --- CHANGELOG.md | 22 +- README.md | 25 +- README.zh-CN.md | 22 +- benchmarks/realworld.json | 82 +++--- docs/LIMITATIONS.md | 74 ++--- tests/integration.rs | 548 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 648 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c3dc8e5..2acefd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ cargo tests, on 13 interpreters (2.6–3.14). ## Unreleased +### Community fixes (PRs #1–#26) +- merged 26 targeted fixes from @EiNSTeiN- covering 3.11–3.14 bytecode + families: chained assignments and comparison chains; COPY/SWAP and + walrus in the bounded expression simulator; keyword calls and ordered + `**` expansions; comprehension filter polarity, Boolean elements, + nested conditional elements and nested comprehensions inside generator + expressions; `for x in await f()` iteration protocol; padded handler + returns; EXTENDED_ARG loop back edges; guarded `else` suites; split + try/except/else/finally printing; marshalled slice constants vs runtime + `slice()` calls; and PEP 649 class-annotation registration gates. +- the 26 branches and their regression tests were merged and verified + together. Three textual conflicts were resolved by keeping both sides: + PR #6 stacks on #5, all PRs append tests at the same point in + `tests/integration.rs`, and #4/#5/#6/#24 append test-module + declarations at the end of `decompiler.rs`. + ### Fixed - value-position and-chains in a ternary: `try_dup_tail_ternary` now absorbs leading same-target cond jumps into the condition, so @@ -16,9 +32,9 @@ cargo tests, on 13 interpreters (2.6–3.14). merge, so `d[a if c and e else b]` keeps the shared lookup. ### Benchmarks -- real-world corpus: 6408/6411 modules recompile (99.95%); pandas is at - 1419/1420 with only the nested-comprehension family left (sklearn ×2, - pandas to_dict) +- real-world corpus: 6411/6411 modules recompile (100%); the former + nested-comprehension family (pandas `to_dict`, sklearn `_logistic`, + `pairwise`) is closed, and strict AST equality rises to 2493/6411 ### Fixed diff --git a/README.md b/README.md index e3f8f8c1..765316d8 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,14 @@ seven projects **at its latest release tag**, compiles every `.py` to | Project (tag) | modules | serial `-j 1` | parallel (default) | recompiles | AST-identical¹ | |---|---|---|---|---|---| -| yt-dlp (2026.08.19) | 1045 | 0.93 s | 0.12 s | **1045/1045** | 500/1045 | -| matplotlib (v3.11.2) | 253 | 0.47 s | 0.09 s | **253/253** | 59/253 | -| pandas (v3.0.5) | 1420 | 1.35 s | 0.21 s | 1419/1420 | 582/1420 | -| django (6.1.1) | 907 | 0.67 s | 0.16 s | **907/907** | 467/907 | -| sympy (1.14.0) | 1532 | 3.68 s | 1.85 s | **1532/1532** | 479/1532 | -| scikit-learn (1.9.1) | 671 | 0.71 s | 0.10 s | 669/671 | 216/671 | -| ansible (v2.21.4) | 583 | 0.35 s | 0.06 s | **583/583** | 185/583 | -| **total** | **6411** | **8.16 s** | **2.59 s** | **6408/6411 (99.95 %)** | 2488/6411 | +| yt-dlp (2026.08.19) | 1045 | 0.46 s | 0.08 s | **1045/1045** | 501/1045 | +| matplotlib (v3.11.2) | 253 | 0.33 s | 0.06 s | **253/253** | 60/253 | +| pandas (v3.0.5) | 1420 | 0.90 s | 0.14 s | **1420/1420** | 584/1420 | +| django (6.1.1) | 907 | 0.38 s | 0.10 s | **907/907** | 468/907 | +| sympy (1.14.0) | 1532 | 3.03 s | 1.63 s | **1532/1532** | 479/1532 | +| scikit-learn (1.9.1) | 671 | 0.48 s | 0.07 s | **671/671** | 216/671 | +| ansible (v2.21.4) | 583 | 0.27 s | 0.05 s | **583/583** | 185/583 | +| **total** | **6411** | **5.85 s** | **2.13 s** | **6411/6411 (100 %)** | 2493/6411 | ¹ strict: docstring-stripped AST equal to the original source — equivalent normalizations (an if/else instead of a ternary, a redundant @@ -118,10 +118,11 @@ equivalent normalizations (an if/else instead of a ternary, a redundant differences, so treat it as a shape-fidelity floor rather than an error rate. The parse/recompile column is the acceptance bar. -One pycdc process per project: ~1.0 ms per module serial, peak RSS -≈99 MB for the whole 6.4 k-module batch. The three remaining modules are one shape: a dict comprehension -nested inside a generator expression's element (call arguments -included) — marked incomplete, never a crash. Raw data: [`benchmarks/realworld.json`](benchmarks/realworld.json), +One pycdc process per project: ~0.9 ms per module serial, peak RSS +≈81 MB for the whole 6.4 k-module batch. **All 6411 modules recompile**; +the last family (a dict comprehension nested inside a generator +expression's element, call arguments included) was closed by the +community PR batch. Raw data: [`benchmarks/realworld.json`](benchmarks/realworld.json), harness: [`tools/bench_realworld.py`](tools/bench_realworld.py). diff --git a/README.zh-CN.md b/README.zh-CN.md index b7de1870..ab8fab4c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -99,23 +99,23 @@ Apple Silicon 实测,526 个真实 `.pyc`(约 9.5MB,2.6–3.14 标准库 | 项目(tag) | 模块数 | 串行 `-j 1` | 并行(默认) | 可重编译 | AST 完全一致¹ | |---|---|---|---|---|---| -| yt-dlp (2026.08.19) | 1045 | 0.93 s | 0.12 s | **1045/1045** | 500/1045 | -| matplotlib (v3.11.2) | 253 | 0.47 s | 0.09 s | **253/253** | 59/253 | -| pandas (v3.0.5) | 1420 | 1.35 s | 0.21 s | 1419/1420 | 582/1420 | -| django (6.1.1) | 907 | 0.67 s | 0.16 s | **907/907** | 467/907 | -| sympy (1.14.0) | 1532 | 3.68 s | 1.85 s | **1532/1532** | 479/1532 | -| scikit-learn (1.9.1) | 671 | 0.71 s | 0.10 s | 669/671 | 216/671 | -| ansible (v2.21.4) | 583 | 0.35 s | 0.06 s | **583/583** | 185/583 | -| **合计** | **6411** | **8.16 s** | **2.59 s** | **6408/6411(99.95%)** | 2488/6411 | +| yt-dlp (2026.08.19) | 1045 | 0.46 s | 0.08 s | **1045/1045** | 501/1045 | +| matplotlib (v3.11.2) | 253 | 0.33 s | 0.06 s | **253/253** | 60/253 | +| pandas (v3.0.5) | 1420 | 0.90 s | 0.14 s | **1420/1420** | 584/1420 | +| django (6.1.1) | 907 | 0.38 s | 0.10 s | **907/907** | 468/907 | +| sympy (1.14.0) | 1532 | 3.03 s | 1.63 s | **1532/1532** | 479/1532 | +| scikit-learn (1.9.1) | 671 | 0.48 s | 0.07 s | **671/671** | 216/671 | +| ansible (v2.21.4) | 583 | 0.27 s | 0.05 s | **583/583** | 185/583 | +| **合计** | **6411** | **5.85 s** | **2.13 s** | **6411/6411(100%)** | 2493/6411 | ¹ 严格口径:去掉 docstring 后 AST 与原源码完全一致。等价归一化 (if/else 代替三元、解包外多余的 `*()`/`**{}`、关键字参数顺序调整)也 会被计为差异,因此它是「形状保真度下限」而非错误率;验收口径是左侧的 可重编译列。 -每个项目一个 pycdc 进程:串行约 1.0 ms/模块,整批 6.4k 模块峰值 -RSS ≈98 MB。剩余 3 个模块属一种形态:生成器表达式元素(含调用参数)内嵌 -dict 推导式——标记为不完整,不崩溃。 +每个项目一个 pycdc 进程:串行约 0.9 ms/模块,整批 6.4k 模块峰值 +RSS ≈81 MB。**6411 个模块全部可重编译**;最后一族(生成器表达式元素 / +调用参数里的 dict 推导式)已由社区 PR 批次修复。 [`benchmarks/realworld.json`](benchmarks/realworld.json), 脚本见 [`tools/bench_realworld.py`](tools/bench_realworld.py)。 diff --git a/benchmarks/realworld.json b/benchmarks/realworld.json index 7f5b778e..6ed311bd 100644 --- a/benchmarks/realworld.json +++ b/benchmarks/realworld.json @@ -3,17 +3,17 @@ "pycdc": "target/release/pycdc", "interp": "~/.local/share/uv/python/cpython-3.12.14-macos-aarch64-none/bin/python3.12", "host": "Darwin 25.6.0 arm64", - "generated": "2026-09-15 13:38:23", + "generated": "2026-10-23 14:52:30", "note": "one pycdc process per project (batch directory mode); serial = -j 1, parallel = default worker count; recompiles = decompiled output parses under the corpus interpreter", "projects": [ { "project": "ansible", "files": 583, "source_bytes": 5372361, - "serial_wall_s": 0.35, - "serial_rss_mb": 9.0, - "parallel_wall_s": 0.06, - "parallel_rss_mb": 18.3, + "serial_wall_s": 0.27, + "serial_rss_mb": 8.5, + "parallel_wall_s": 0.05, + "parallel_rss_mb": 18.6, "decompiled": 583, "recompiles": 583, "ast_equal": 185, @@ -23,51 +23,51 @@ "project": "django", "files": 907, "source_bytes": 5914265, - "serial_wall_s": 0.67, - "serial_rss_mb": 8.8, - "parallel_wall_s": 0.16, - "parallel_rss_mb": 23.5, + "serial_wall_s": 0.38, + "serial_rss_mb": 8.3, + "parallel_wall_s": 0.1, + "parallel_rss_mb": 23.9, "decompiled": 907, "recompiles": 907, - "ast_equal": 467, + "ast_equal": 468, "ast_checked": 907 }, { "project": "matplotlib", "files": 253, "source_bytes": 7415404, - "serial_wall_s": 0.47, - "serial_rss_mb": 20.9, - "parallel_wall_s": 0.09, - "parallel_rss_mb": 38.0, + "serial_wall_s": 0.33, + "serial_rss_mb": 18.5, + "parallel_wall_s": 0.06, + "parallel_rss_mb": 35.9, "decompiled": 253, "recompiles": 253, - "ast_equal": 59, + "ast_equal": 60, "ast_checked": 253 }, { "project": "pandas", "files": 1420, "source_bytes": 21287834, - "serial_wall_s": 1.35, - "serial_rss_mb": 13.5, - "parallel_wall_s": 0.21, - "parallel_rss_mb": 39.1, + "serial_wall_s": 0.9, + "serial_rss_mb": 12.8, + "parallel_wall_s": 0.14, + "parallel_rss_mb": 38.9, "decompiled": 1420, - "recompiles": 1419, - "ast_equal": 582, + "recompiles": 1420, + "ast_equal": 584, "ast_checked": 1420 }, { "project": "sklearn", "files": 671, "source_bytes": 13444062, - "serial_wall_s": 0.71, - "serial_rss_mb": 10.8, - "parallel_wall_s": 0.1, - "parallel_rss_mb": 25.0, + "serial_wall_s": 0.48, + "serial_rss_mb": 10.6, + "parallel_wall_s": 0.07, + "parallel_rss_mb": 30.1, "decompiled": 671, - "recompiles": 669, + "recompiles": 671, "ast_equal": 216, "ast_checked": 671 }, @@ -75,10 +75,10 @@ "project": "sympy", "files": 1532, "source_bytes": 26169877, - "serial_wall_s": 3.68, - "serial_rss_mb": 49.6, - "parallel_wall_s": 1.85, - "parallel_rss_mb": 78.8, + "serial_wall_s": 3.03, + "serial_rss_mb": 48.6, + "parallel_wall_s": 1.63, + "parallel_rss_mb": 80.6, "decompiled": 1532, "recompiles": 1532, "ast_equal": 479, @@ -88,13 +88,13 @@ "project": "yt_dlp", "files": 1045, "source_bytes": 9208581, - "serial_wall_s": 0.93, - "serial_rss_mb": 15.5, - "parallel_wall_s": 0.12, - "parallel_rss_mb": 23.2, + "serial_wall_s": 0.46, + "serial_rss_mb": 11.8, + "parallel_wall_s": 0.08, + "parallel_rss_mb": 21.0, "decompiled": 1045, "recompiles": 1045, - "ast_equal": 500, + "ast_equal": 501, "ast_checked": 1045 } ], @@ -102,13 +102,13 @@ "project": "TOTAL", "files": 6411, "source_bytes": 88812384, - "serial_wall_s": 8.16, - "parallel_wall_s": 2.59, - "serial_rss_mb": 49.6, - "parallel_rss_mb": 78.8, + "serial_wall_s": 5.85, + "parallel_wall_s": 2.13, + "serial_rss_mb": 48.6, + "parallel_rss_mb": 80.6, "decompiled": 6411, - "recompiles": 6408, - "ast_equal": 2488, + "recompiles": 6411, + "ast_equal": 2493, "ast_checked": 6411 } } \ No newline at end of file diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 26b59d71..b4caccb8 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -143,64 +143,22 @@ rendering bugs: (300+ regression-driven special cases), so it is left documented rather than patched blind. -### Nested comprehension inside a genexp (3 modules, deliberately not patched) - -`({c: v for c, v in zip(cols, row) if c} for row in rows)` — a genexp whose -*element* (or a call argument inside it) is an inlined (PEP 709) dict -comprehension. The genexp keeps its own code object and is decompiled by -the comprehension walker, which has no notion of a nested level: the -inner `BUILD_MAP 0` accumulator lands on the walker's stack, `SWAP` is -not modeled, and the following `FOR_ITER` treats it as a second `for` -clause (`... for c, v in {}`), so the output does not parse. - -Simple expanded calls in a Python 3.13 or 3.14 generator, such as -`(factory(*row) for row in rows)` and `(factory(**dict(row)) for row in rows)`, -now respect `CALL_FUNCTION_EX`'s callable/NULL order introduced in 3.13. In -3.14 they also consume its unconditional keyword slot and treat a NULL marker -as absent keyword arguments. The nested-comprehension stack issues below -remain. - -A frame-stack prototype (one frame per comprehension level, `SWAP` -modeled, the accumulator placeholder replaced by Rc identity when a level -ends, `pending_or_filter` flushed per level) was implemented **twice** and -reverted both times. What it achieved, verified by runtime-equivalence -checks against the original sources: the whole-element shape and the -call-argument shapes *as minimal repros* became semantically correct -(`({c: v for c, v in zip(cols, t) if c} for t in rows)`, -`(f(**d) for x in xs)`, `(f(x, y=1) for x in xs)`; the last two were -fixed for real and are committed). - -It still produced *plausible but semantically wrong* output for the three -real modules (`pandas/core/methods/to_dict.py`, -`sklearn/linear_model/_logistic.py`, `metrics/pairwise.py`), e.g. - - fd(func, ret, s, X, Y[s, ...], **{k: (v[s] if k == "Y" else v) for k, v in kwds.items()}) - -> (fd if k == 'Y' else (func, ret, s, X, Y[s, ...])(*{}, **{**k}) for s in ...) - -The blocker is not the nesting itself: the walker's stack model is a -heuristic that happens to agree with the VM on the shapes the corpus -exercises, and a nested level exposes several concrete divergences at -once (all observed with probes during the third attempt, 2026-09-15): - - * `GET_ITER` is a no-op, so the 3.12 prologue's `SWAP`s reorder the - wrong slots (the VM's is `[saved…, iter]` -> `BUILD_* 0` -> `SWAP 2`; - the walker has no `iter` on the stack there); - * the `LOAD_FAST_AND_CLEAR` save slots are pushed but never popped — - the epilogue's `STORE_FAST`s restore them in the VM, while the walker - treats stores as target assignments, so they linger on the stack; - * `PUSH_NULL` markers then sit between the level's value and the call - slot, and the `CALL_FUNCTION_EX` pops pick up the wrong values - (`fd(...)` came out as `fd if k == 'Y' else (func, ret, X, Y)(*{}, **{**k})`). - -Fixing one of these shifts the divergence to the next (truncating the -level's stack to its base fixed the kwargs slot but stripped the outer -operands, and excluding the save slots then leaked the `PUSH_NULL` -marker into the output), which is exactly why this needs the call model -reworked rather than another local patch. A correct fix -therefore starts with making that call model faithful (or by delegating -the enclosing expression to the main engine's `exec`), and only then -re-adding the nesting. Emitting a visible syntax error stays preferable -to silently wrong code, so the modules remain in the failing set. +### ~~Nested comprehension inside a genexp~~ (fixed 2026-10-23) + +PR #24 recovers the inlined comprehension by decompiling the generator's +code object with the main statement parser and extracting its +For/If/Yield tree; the outer iterable is restored at the call site. This +closes the whole-element and call-argument shapes, including +`({c: v for c, v in zip(cols, row) if c} for row in rows)`. + +The three full-module cases also needed the keyword/dictionary handling +from #10 and #22: a large `MAP_ADD 1` kwargs dictionary must not be +mistaken for the comprehension's own key/value, and `DICT_MERGE` +expansions must keep their boundary. With the consolidated merge all +6411 real-world modules recompile. The earlier frame-stack analysis +(kept in git history) confirmed the right fix was to delegate the +enclosing expression to the statement engine rather than to model every +nested level in the comprehension walker. ### ~~Multi-link ternary on the value path~~ (fixed 2026-09-15) diff --git a/tests/integration.rs b/tests/integration.rs index 857cc85c..9733ee15 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -277,3 +277,551 @@ fn empty_defaults_keep_the_comprehension_level() { } } } + +#[test] +fn unpacked_targets_keep_iterator_cleanup_and_saved_locals() { + // 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!("unpacked_comprehension_cleanup.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + for (name, expression) in [ + ("unpack4", "[a + b + c + d for rows in groups for a, b, c, d in rows]"), + ("unpack_list", "[(a, c) for a, b in groups for c, d in b]"), + ("unpack_set", "{(a, c) for a, b in groups for c, d in b}"), + ("unpack_dict", "{a: c for a, b in groups for c, d in b}"), + ("unpack6", "[(a, c, e) for a, b in groups for c, d in b for e, f in d]"), + ("simple", "[(a, c) for a in groups for c in a]"), + ("mixed", "[(a, c) for key, a in groups for c in a]"), + ("mixed_inner", "[(a, c) for a in groups for key, c in a]"), + ] { + let expected = format!( + "def {name}(groups):\n a, b, c, d, e, f = 10, 20, 30, 40, 50, 60\n result = {expression}\n after = 0\n for number in (1, 2, 3):\n after += number\n return result, (a, b, c, d, e, f), after"); + assert_contains(&src, &[&expected], version); + } + } +} + +#[test] +fn padded_handler_returns_keep_false_before_the_success_tail() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture( + &format!("handler_return_padding_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, {padding} NOPs: incomplete:\n{src}"); + assert_contains(&src, &[ + "async def handled(read, log):\n try:\n await read()\n log('saved')\n except ValueError:\n log('locked')\n return False\n except Exception as error:\n log('failed', error)\n return False\n return True", + ], version); + } + } +} + +#[test] +fn large_dictionary_displays_remain_comprehension_elements() { + // CPython 3.10 starts using MAP_ADD 1 for large displays. Before 3.12, + // list comprehensions also use the separate code-object walker. + for version in ["3.10", "3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture( + &format!("comprehension_dictionary_displays.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + for size in [17, 20] { + let fields = (0..size).map(|index| format!("'k{index}': row + {index}")) + .collect::>().join(", "); + for (name, open, close) in [("list", "[", "]"), ("generator", "(", ")")] { + let expected = format!( + "def {name}{size}(rows):\n return {open}{{{fields}}} for row in rows{close}"); + assert_contains(&src, &[&expected], version); + } + } + assert_contains(&src, &[ + "def dictionary_comprehension(rows):\n return {row: row + 1 for row in rows}", + "def small_display(rows):\n return [{'value': row} for row in rows]", + ], version); + } +} + +#[test] +fn extended_loop_prefix_preserves_handler_continue() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("extended_loop_prefix.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + for count in [1, 30, 60] { + let body = " work(row)\n".repeat(count); + let expected = format!( + "def size_{count}(rows, work):\n result = []\n for row in rows:\n try:\n{body} except ValueError:\n continue\n result.append(row)"); + assert_contains(&src, &[&expected], version); + } + } +} + +#[test] +fn conditional_loop_prefix_keeps_both_arms_inside_loop() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("conditional_loop_prefix.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + let args = "row.ok, row.a, row.b, row.c, row.d, row.e, row.f, row.g, row.h, row.i, row.j, row.k, row.l"; + let file_condition = if version == "3.12" { "" } else { "not " }; + let directory_condition = if version == "3.11" { "elif not" } else { "if" }; + assert_contains( + &src, + &[ + &format!(" for row in rows:\n if row.is_file:\n if {file_condition}work('file', {args}):"), + &format!(" {directory_condition} work('directory', {args}):"), + " return success", + ], + version, + ); + assert!(!src.contains("\n success = False"), "Python {version}: failure assignment escaped the loop:\n{src}"); + if version == "3.13" || version == "3.14" { + assert_contains( + &src, + &[" success = False\n continue\n continue"], + version, + ); + } + } +} + +#[test] +fn awaited_collections_use_synchronous_iteration() { + for version in ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("awaited_synchronous_iteration.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"async def awaited(read): + result = [] + for item in await read(): + result.append(item) + return result"#, + r#"async def asynchronous(read): + result = [] + async for item in read(): + result.append(item) + return result"#, + r#"async def sync(read): + result = [] + for item in read(): + result.append(item) + return result"#, + ], version); + } +} + +#[test] +fn real_value_build_still_matches_the_sunk_tail() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("sunk_value_return_control.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def select(value): + try: + entry = value.entry + except AttributeError: + kind = 'missing' + else: + kind = 'present' + return kind, value"#, + ], version); + } +} + +#[test] +fn padding_does_not_match_a_mainline_value_build() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture( + &format!("padded_value_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, {padding} NOPs: incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def synchronous(read, build): + try: + item = read() + except ValueError: + return build('missing') + return build(item)"#, + r#"async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build('missing') + return build(item)"#, + ], version); + } + } +} + +#[test] +fn finite_else_span_keeps_its_inner_guard() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("guarded_else_merge.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"async def awaited(owner): + value = None + active = owner.active + if active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = float(row[0]) + owner.record(value) + return value"#, + r#"async def synchronous(owner): + value = None + active = owner.active + if active: + value = float(1) + else: + row = owner.fetch() + if row and row[0] is not None: + value = 2 + owner.record(value) + return value"#, + r#"async def attribute(owner): + value = None + active = owner.active + if active: + value = float(active['total']) + else: + row = owner.row + if row and row[0] is not None: + value = float(row[0]) + owner.record(value) + return value"#, + r#"async def following_else_work(owner): + value = None + active = owner.active + if active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = 2 + owner.record('queried') + owner.record(value) + return value"#, + r#"async def independent(owner): + value = None + row = None + if owner.active: + value = 1 + else: + row = await owner.fetch() + if row and row[0] is not None: + value = 2 + return value"#, + ], version); + } +} + +#[test] +fn split_exception_suites_generate_one_complete_try_statement() { + use pycdc::ast::{ExceptHandler, Expr, Stmt}; + use pycdc::version::PythonVersion; + use std::rc::Rc; + let name = |value: &str| Rc::new(Expr::Name(value.into())); + let inner = Stmt::Try { + body: vec![Stmt::Expr(name("work")), Stmt::Expr(name("more_work"))], + handlers: vec![ExceptHandler { + type_: Some(name("ValueError")), name: Some(name("error")), + body: vec![Stmt::Expr(name("handled"))], is_star: false, + }], + orelse: vec![], finalbody: vec![], + }; + let outer = Stmt::Try { + body: vec![inner.clone()], handlers: vec![], + orelse: vec![Stmt::Expr(name("success"))], + finalbody: vec![Stmt::Expr(name("cleanup")), Stmt::Expr(name("more_cleanup"))], + }; + let module = PycModule::from_bytes(include_bytes!("pyc/hello.3.14.pyc")).unwrap(); + // Unified try/except/finally syntax starts in Python 2.5. The repair + // concerns the recovered AST shape, rather than one opcode version. + for (major, minor) in [(2, 5), (2, 7), (3, 8), (3, 11), (3, 12), (3, 13), (3, 14)] { + let version = PythonVersion { major, minor, ..module.version() }; + let binding = if major == 2 { ", error" } else { " as error" }; + let expected = format!("try:\n work\n more_work\nexcept ValueError{binding}:\n handled\nelse:\n success\nfinally:\n cleanup\n more_cleanup\n"); + assert_eq!(generate(&[outer.clone()], version, true), expected); + + // A legitimate outer try/finally has no success suite to reunite. + let nested = Stmt::Try { + body: vec![inner.clone()], handlers: vec![], orelse: vec![], + finalbody: vec![Stmt::Expr(name("cleanup"))], + }; + let expected = format!("try:\n try:\n work\n more_work\n except ValueError{binding}:\n handled\nfinally:\n cleanup\n"); + assert_eq!(generate(&[nested], version, true), expected); + } +} + +#[test] +fn keyword_calls_survive_conditional_value_reconstruction() { + for version in ["3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("keyword_conditional_calls.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def case_0(value, build, other): + return build('reject' if value else 'accept', amount=0)"#, + r#"def case_1(value, build, other): + return build('prefix', amount=0 if value else 1)"#, + r#"def case_2(value, build, other): + return build(0 if value else 1, amount=value)"#, + r#"def case_9(value, build, other): + return build('prefix', amount=1 if value else 2, last=other(value))"#, + r#"def case_11(value, build, other): + return other(build(amount=1) if value else build(amount=2, other=3))"#, + ], version); + } +} + +#[test] +fn keyword_call_merge_does_not_hoist_an_arm_local_callable() { + for version in ["3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("keyword_call_arm_boundary.{version}.pyc")); + assert!(!clean, "Python {version}: an unproved call merge must remain incomplete:\n{src}"); + assert!(!src.contains("owner.build(amount=1 if value else 2)"), + "Python {version}: callable resolution moved before the condition:\n{src}"); + } +} + +#[test] +fn overwritten_unpack_slots_reuse_the_surviving_binding() { + for version in ["3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("repeated_unpack_bindings.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def repeated_loop(values): + result = [] + for value, discarded, discarded, discarded in values: + result.append(value is not None) + return result"#, + r#"def repeated_list(values): + return [value is not None for value, discarded, discarded, discarded in values]"#, + r#"def underscore_loop(values): + result = [] + for value, _, discarded, discarded in values: + result.append((value, discarded, _)) + return result"#, + r#"def underscore_list(values): + return [(value, discarded, _) for value, _, discarded, discarded in values]"#, + r#"def nested_loop(values): + result = [] + for value, (discarded, discarded) in values: + result.append((value, discarded)) + return result"#, + r#"def nested_list(values): + return [(value, discarded) for value, (discarded, discarded) in values]"#, + ], version); + } +} + +#[test] +fn slice_constants_preserve_axes_and_runtime_calls() { + for version in ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("slice_constants_and_calls.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def dynamic_axes(data, stop): + return data[:stop, :]"#, + r#"def constant_axes(data, stop): + return data[:, :]"#, + r#"def mixed_axes(data, stop): + return data[1:stop:2, :, 7]"#, + r#"def call(data, stop): + return data[slice(None, stop, None)]"#, + r#"def call_axes(data, stop): + return data[slice(None, stop, None), :]"#, + ], version); + } +} + +#[test] +fn nested_conditional_comprehension_arms_keep_their_values() { + for version in ["3.12", "3.13", "3.14"] { + let fixture = format!("nested_conditional_elements.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "return [{'excluded': {'count': row['count'], 'reasons': loads(row['reasons']) if row['reasons'] else {}} if row['count'] is not None else {}, 'archived': str(row['archived']) if row['archived'] else None} for row in rows]", + "return [build(1 if row.inner else 2) if row.outer else 3 for row in rows]", + "return [1 if row.outer else build(2 if row.inner else 3) for row in rows]", + "return [build(1 if row.inner is None else 2) if row.outer else build(3 if row.inner is not None else 4) for row in rows]", + ], + &fixture, + ); + } +} + +#[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, + ); + } +} + +#[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, + ); + } +} + +#[test] +fn keyword_expansions_keep_merge_checks_and_dictionary_boundaries() { + for version in ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("ordered_keyword_expansions.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "return fn(1, fixed=2, **first, **second)", + "return fn(1, **first, **{'fixed': 2}, **second)", + "return fn(1, **first, **second)", + "return fn(1, **{'fixed': 2, **first, **second})", + "return fn(1, **{'my col': 2, **first, **second})", + "return fn(1, **{'class': 2, **first, **second})", + "return fn(1, **{'fixed': 2, 'fixed': 3, **first, **second})", + "return fn(1, fixed=2, **get_first(), **get_second())", + ], + &fixture, + ); + } +} + +#[test] +fn deferred_class_annotations_keep_registration_gates() { + let fixture = "class_annotation_registration.3.14.pyc"; + let (src, clean) = decompile_fixture(fixture); + assert!(clean, "output marked incomplete:\n{src}"); + assert!(src.starts_with("module_count: int = 5\n"), "{src}"); + assert_contains( + &src, + &[ + "class Always:", + "'''Documented class.'''", + " if True:\n amount: int = 3\n label: str = 'ready'", + " if enabled:\n if True:\n label: str = 'ready'", + " for value in values:\n if True:\n limit: int = default(value)", + " for value in values:\n if True:\n amount: int = default(value)", + "def read(self) -> int:", + ], + fixture, + ); + assert!(!src.contains("__conditional_annotations__"), "{src}"); +} + +#[test] +fn nested_comprehensions_remain_generator_elements() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("nested_generator_comprehensions.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "return tuple(({key: row[key] for key in row} for row in rows))", + "return tuple(([value * 2 for value in row] for row in rows))", + "return tuple(({value * 2 for value in row} for row in rows))", + "return tuple((collect([value * 2 for value in row]) for row in rows))", + "return tuple(({key: row[key] for key in row if key} for row in rows if row))", + "return tuple(({key: value for key, value in zip(columns, row) if key} for row in rows))", + ], + &fixture, + ); + } +} + +#[test] +fn boolean_value_chains_preserve_call_arguments_and_prefixes() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("boolean_call_arguments.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "return call(before(), first() and second() or last(), after())", + "return call(value, value and second() or last(), after())", + "return bool(a and b or c or d or e)", + "return bool(c or d or a and b or e)", + "return bool(a and a.get('available') or b or c or d or e)", + "value = a and a.get('available') or b or c or d or e\n return bool(value)", + "return bool(a or b or c or d or e)", + "return bool(a and b and c and d and e)", + "return call(a and b or c, d if e else 0)", + ], + &fixture, + ); + } +} + +#[test] +fn independent_lookup_guards_keep_assignment_and_return_inside_the_loop() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("loop_lookup_guards.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + // The existing None-jump printer uses either equivalent spelling. + let src = src.replace("not coordinator is not None", "coordinator is None"); + assert_contains( + &src, + &[ + "def rating_lookup(values):\n for value in values:\n if not value.rating < 10:\n continue\n coordinator = value.coordinator\n if coordinator is None:\n continue\n return coordinator", + "def dictionary_lookup(rows):\n for row in rows:\n if not isinstance(row, dict):\n continue\n coordinator = row.get('coordinator')\n if coordinator is None:\n continue\n return coordinator", + "def none_lookup(rows):\n for row in rows:\n if row is None:\n continue\n coordinator = row.coordinator\n if coordinator is None:\n continue\n return coordinator", + "return [row.get('coordinator') for row in rows if isinstance(row, dict)]", + ], + &fixture, + ); + assert!(!src.contains("value.rating >="), "{fixture}: comparison inverted"); + } +}