From 5dd9985e50189796218920cc081a0eda38024693 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:35:46 -0400 Subject: [PATCH 1/2] Distinguish slice constants from runtime slice calls --- src/decompiler.rs | 41 +++++++++---------- tests/fixtures/slice_constants_and_calls.py | 23 +++++++++++ tests/integration.rs | 20 +++++++++ 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 10 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 tests/fixtures/slice_constants_and_calls.py 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 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/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 0000000000000000000000000000000000000000..6d17099701aab9b1374bbc242985139698e215ac GIT binary patch literal 768 zcmah{!AiqG5Zz5SHjRx|DN;Y7C>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 From 1c064d7d84d2fa24fa2c7df8bfc39b3208f2f1aa Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:34:16 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../cases/b_slice_constants_and_calls.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/behavior/cases/b_slice_constants_and_calls.py 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))