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/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) 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 00000000..d3f73a5a Binary files /dev/null and b/tests/pyc/assignment_identity.3.11.pyc differ diff --git a/tests/pyc/assignment_identity.3.12.pyc b/tests/pyc/assignment_identity.3.12.pyc new file mode 100644 index 00000000..2b9520b5 Binary files /dev/null and b/tests/pyc/assignment_identity.3.12.pyc differ diff --git a/tests/pyc/assignment_identity.3.13.pyc b/tests/pyc/assignment_identity.3.13.pyc new file mode 100644 index 00000000..1d19476a Binary files /dev/null and b/tests/pyc/assignment_identity.3.13.pyc differ diff --git a/tests/pyc/assignment_identity.3.14.pyc b/tests/pyc/assignment_identity.3.14.pyc new file mode 100644 index 00000000..b3cd8796 Binary files /dev/null and b/tests/pyc/assignment_identity.3.14.pyc differ