diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..bedd968b 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -42442,33 +42442,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, } } 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/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/integration.rs b/tests/integration.rs index f4af5082..c34d3bfb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,23 @@ fn py314_hello() { "py3.14 hello", ); } + +#[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); + } +} diff --git a/tests/pyc/slice_constants_and_calls.3.10.pyc b/tests/pyc/slice_constants_and_calls.3.10.pyc new file mode 100644 index 00000000..6d170997 Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.10.pyc differ 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 00000000..ef6510a8 Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.11.pyc differ 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 00000000..63c9d7a8 Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.12.pyc differ diff --git a/tests/pyc/slice_constants_and_calls.3.13.pyc b/tests/pyc/slice_constants_and_calls.3.13.pyc new file mode 100644 index 00000000..8383befd Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.13.pyc differ 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 00000000..376bceb9 Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.14.pyc differ 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 00000000..258bdc8c Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.8.pyc differ 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 00000000..60518fac Binary files /dev/null and b/tests/pyc/slice_constants_and_calls.3.9.pyc differ