Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions src/decompiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
20 changes: 20 additions & 0 deletions tests/behavior/cases/b_generator_expanded_calls.py
Original file line number Diff line number Diff line change
@@ -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))
12 changes: 12 additions & 0 deletions tests/fixtures/generator_expanded_calls.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions tests/fixtures/generator_positional_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def select(rows, factory):
return tuple(factory(*row) for row in rows)
19 changes: 19 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added tests/pyc/generator_expanded_calls.3.11.pyc
Binary file not shown.
Binary file added tests/pyc/generator_expanded_calls.3.12.pyc
Binary file not shown.
Binary file added tests/pyc/generator_expanded_calls.3.13.pyc
Binary file not shown.
Binary file added tests/pyc/generator_expanded_calls.3.14.pyc
Binary file not shown.