From 2d168269d6c6ee31138d618fb0f53bb85e3baaa1 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:05:43 -0400 Subject: [PATCH 1/2] Keep large dictionary displays inside comprehensions --- src/decompiler.rs | 16 +++++- .../comprehension_dictionary_displays.py | 51 ++++++++++++++++++ tests/integration.rs | 24 +++++++++ ...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 8 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/comprehension_dictionary_displays.py 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 diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..bce2fb97 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -14797,6 +14797,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 { @@ -51833,8 +51834,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() { 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/integration.rs b/tests/integration.rs index f4af5082..b4ba9c03 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,27 @@ fn py314_hello() { "py3.14 hello", ); } + +#[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); + } +} diff --git a/tests/pyc/comprehension_dictionary_displays.3.10.pyc b/tests/pyc/comprehension_dictionary_displays.3.10.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7587b2efb8a66ead14f34e434985cb13c4dce5d GIT binary patch literal 3312 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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 From f1556f4b2f7468f739b4a913da4a67386841da1c Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:32:58 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../b_comprehension_dictionary_displays.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/behavior/cases/b_comprehension_dictionary_displays.py 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])))