From de6be7c3335fa1707a8e8eb1357e71de983d1566 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 16:24:13 -0400 Subject: [PATCH 1/2] Recover nested generator comprehensions through statement parsing --- docs/LIMITATIONS.md | 73 ++++------- src/decompiler.rs | 120 ++++++++++++++++++ .../nested_generator_comprehensions.py | 22 ++++ tests/integration.rs | 21 +++ .../nested_generator_comprehensions.3.11.pyc | Bin 0 -> 3698 bytes .../nested_generator_comprehensions.3.12.pyc | Bin 0 -> 2604 bytes .../nested_generator_comprehensions.3.13.pyc | Bin 0 -> 2700 bytes .../nested_generator_comprehensions.3.14.pyc | Bin 0 -> 3144 bytes 8 files changed, 185 insertions(+), 51 deletions(-) create mode 100644 tests/fixtures/nested_generator_comprehensions.py create mode 100644 tests/pyc/nested_generator_comprehensions.3.11.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.12.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.13.pyc create mode 100644 tests/pyc/nested_generator_comprehensions.3.14.pyc diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 6c90274a..52cc0d71 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -143,57 +143,28 @@ rendering bugs: (300+ regression-driven special cases), so it is left documented rather than patched blind. -### Nested comprehension inside a genexp (3 modules, deliberately not patched) - -`({c: v for c, v in zip(cols, row) if c} for row in rows)` — a genexp whose -*element* (or a call argument inside it) is an inlined (PEP 709) dict -comprehension. The genexp keeps its own code object and is decompiled by -the comprehension walker, which has no notion of a nested level: the -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. - -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 -reverted both times. What it achieved, verified by runtime-equivalence -checks against the original sources: the whole-element shape and the -call-argument shapes *as minimal repros* became semantically correct -(`({c: v for c, v in zip(cols, t) if c} for t in rows)`, -`(f(**d) for x in xs)`, `(f(x, y=1) for x in xs)`; the last two were -fixed for real and are committed). - -It still produced *plausible but semantically wrong* output for the three -real modules (`pandas/core/methods/to_dict.py`, -`sklearn/linear_model/_logistic.py`, `metrics/pairwise.py`), e.g. - - fd(func, ret, s, X, Y[s, ...], **{k: (v[s] if k == "Y" else v) for k, v in kwds.items()}) - -> (fd if k == 'Y' else (func, ret, s, X, Y[s, ...])(*{}, **{**k}) for s in ...) - -The blocker is not the nesting itself: the walker's stack model is a -heuristic that happens to agree with the VM on the shapes the corpus -exercises, and a nested level exposes several concrete divergences at -once (all observed with probes during the third attempt, 2026-09-15): - - * `GET_ITER` is a no-op, so the 3.12 prologue's `SWAP`s reorder the - wrong slots (the VM's is `[saved…, iter]` -> `BUILD_* 0` -> `SWAP 2`; - the walker has no `iter` on the stack there); - * the `LOAD_FAST_AND_CLEAR` save slots are pushed but never popped — - the epilogue's `STORE_FAST`s restore them in the VM, while the walker - treats stores as target assignments, so they linger on the stack; - * `PUSH_NULL` markers then sit between the level's value and the call - slot, and the `CALL_FUNCTION_EX` pops pick up the wrong values - (`fd(...)` came out as `fd if k == 'Y' else (func, ret, X, Y)(*{}, **{**k})`). - -Fixing one of these shifts the divergence to the next (truncating the -level's stack to its base fixed the kwargs slot but stripped the outer -operands, and excluding the save slots then leaked the `PUSH_NULL` -marker into the output), which is exactly why this needs the call model -reworked rather than another local patch. A correct fix -therefore starts with making that call model faithful (or by delegating -the enclosing expression to the main engine's `exec`), and only then -re-adding the nesting. Emitting a visible syntax error stays preferable -to silently wrong code, so the modules remain in the failing set. +### Nested comprehensions inside generator expressions + +Python 3.12+ generators containing an inlined list, set or dictionary +comprehension now reuse the statement parser to recover the nested element. +This fixes whole-element and call-argument shapes, including the former +minimal reproduction: + +```python +({c: v for c, v in zip(cols, row) if c} for row in rows) +``` + +The extractor accepts a completely recovered For/If/yield tree, preserves +its yielded expression, and restores the actual outer iterable at the call +site. It rejects unsupported statements and multiple sibling yield paths. +The legacy comprehension walker remains the fallback when extraction does +not prove that shape, and for generators without an inlined comprehension. + +The full-module cases previously listed here (`pandas/core/methods/to_dict.py`, +`sklearn/linear_model/_logistic.py`, and `metrics/pairwise.py`) also exercise +complex call, closure and control-flow reconstruction. They remain on the +full-module verification backlog; repairing the nested-comprehension shape +alone does not establish that every construct in those files is recovered. ### ~~Multi-link ternary on the value path~~ (fixed 2026-09-15) diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..0d36a3db 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -50939,6 +50939,49 @@ impl<'a> Ctx<'a> { // ===================== comprehensions ===================== +// The main statement parser already understands PEP 709's nested inline +// comprehensions. Reuse it for generator elements instead of flattening an +// inner dict/list/set comprehension into the generator's own clauses. +fn generator_parts_from_body( + body: &[Stmt], generators: &mut Vec, +) -> Option { + let mut result = None; + for statement in body { + match statement { + Stmt::For { target, iter, body, orelse, is_async } if orelse.is_empty() => { + if result.is_some() { return None; } + generators.push(Comprehension { + target: target.clone(), iter: iter.clone(), ifs: Vec::new(), + is_async: *is_async, if_line_break: false, + }); + result = generator_parts_from_body(body, generators); + if result.is_none() { return None; } + } + Stmt::If { cond, body, orelse } if orelse.is_empty() => { + if result.is_some() { return None; } + generators.last_mut()?.ifs.push(cond.clone()); + result = generator_parts_from_body(body, generators); + if result.is_none() { return None; } + } + Stmt::Expr(value) => { + match &**value { + Expr::Yield(Some(value)) => { + if result.is_some() { return None; } + result = Some(value.clone()); + } + Expr::Const(_) => {}, // Docstring. + _ => return None, + } + } + Stmt::Pass | Stmt::Return(None) => {}, + Stmt::Return(Some(value)) + if matches!(&**value, Expr::Const(object) if matches!(&**object, PyObject::None)) => {}, + _ => return None, + } + } + result +} + impl<'a> Ctx<'a> { /// Try to turn a completed call into a comprehension. The callee is a /// Function whose code object is named `` etc. The iterable is @@ -51186,6 +51229,20 @@ fn genexpr_ternary_merge( let table = table_for(self.version).ok()?; let instrs = crate::bytecode::decode_instructions(code, table, self.version); + if self.version.at_least(3, 12) && matches!(_kind, CompKind::Generator) + && instrs.iter().any(|instruction| instruction.op == Op::LOAD_FAST_AND_CLEAR) + { + if let Some(statements) = decompile(code, self.version).ok().filter(|result| result.clean) { + let mut generators = Vec::new(); + if let Some(element) = generator_parts_from_body(&statements.body, &mut generators) { + if !generators.is_empty() { + generators[0].iter = outer_iter.clone(); + return Some((element, None, generators)); + } + } + } + } + struct PartialGen { target: Option, iter: ExprRef, @@ -53096,3 +53153,66 @@ impl<'a> Ctx<'a> { } } } + +#[cfg(test)] +mod generator_parts_tests { + use super::*; + + fn yielded_loop() -> Stmt { + Stmt::For { + target: Rc::new(Expr::Name("row".into())), + iter: Rc::new(Expr::Name("rows".into())), + body: vec![Stmt::Expr(Rc::new(Expr::Yield(Some(Rc::new( + Expr::Name("row".into()), + )))))], + orelse: vec![], + is_async: false, + } + } + + #[test] + fn extraction_preserves_one_yield_path_and_rejects_siblings() { + let body = vec![yielded_loop()]; + let mut generators = Vec::new(); + let element = generator_parts_from_body(&body, &mut generators).unwrap(); + assert!(matches!(&*element, Expr::Name(name) if name == "row")); + assert_eq!(generators.len(), 1); + + let mut generators = Vec::new(); + assert!(generator_parts_from_body( + &[yielded_loop(), yielded_loop()], &mut generators, + ).is_none()); + + let branch = Stmt::If { + cond: Rc::new(Expr::Name("ready".into())), + body: vec![Stmt::Expr(Rc::new(Expr::Yield(Some(Rc::new( + Expr::Name("row".into()), + )))))], + orelse: vec![], + }; + let mut body = yielded_loop(); + if let Stmt::For { body, .. } = &mut body { + *body = vec![branch.clone(), branch]; + } + assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + } + + #[test] + fn extraction_does_not_discard_assignments_or_loop_else() { + for name in ["saved", "__assert_armored__"] { + let assignment = Stmt::Assign { + targets: vec![Rc::new(Expr::Name(name.into()))], + value: Rc::new(Expr::Const(Rc::new(PyObject::None))), + wrap_before: vec![], + }; + assert!(generator_parts_from_body( + &[assignment, yielded_loop()], &mut Vec::new(), + ).is_none()); + } + let mut body = yielded_loop(); + if let Stmt::For { orelse, .. } = &mut body { + *orelse = vec![Stmt::Pass]; + } + assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + } +} diff --git a/tests/fixtures/nested_generator_comprehensions.py b/tests/fixtures/nested_generator_comprehensions.py new file mode 100644 index 00000000..77aefa9a --- /dev/null +++ b/tests/fixtures/nested_generator_comprehensions.py @@ -0,0 +1,22 @@ +def dictionary(rows): + return tuple({key: row[key] for key in row} for row in rows) + + +def list_elements(rows): + return tuple([value * 2 for value in row] for row in rows) + + +def set_elements(rows): + return tuple({value * 2 for value in row} for row in rows) + + +def call_arguments(rows, collect): + return tuple(collect([value * 2 for value in row]) for row in rows) + + +def filtered(rows): + return tuple({key: row[key] for key in row if key} for row in rows if row) + + +def unpacked(rows, columns): + return tuple({key: value for key, value in zip(columns, row) if key} for row in rows) diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..9689314c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,24 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn nested_comprehensions_remain_generator_elements() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let fixture = format!("nested_generator_comprehensions.{version}.pyc"); + let (src, clean) = decompile_fixture(&fixture); + assert!(clean, "{fixture}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "return tuple(({key: row[key] for key in row} for row in rows))", + "return tuple(([value * 2 for value in row] for row in rows))", + "return tuple(({value * 2 for value in row} for row in rows))", + "return tuple((collect([value * 2 for value in row]) for row in rows))", + "return tuple(({key: row[key] for key in row if key} for row in rows if row))", + "return tuple(({key: value for key, value in zip(columns, row) if key} for row in rows))", + ], + &fixture, + ); + } +} diff --git a/tests/pyc/nested_generator_comprehensions.3.11.pyc b/tests/pyc/nested_generator_comprehensions.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a803036fd0a78065b70237494a8fafdd84ade27 GIT binary patch literal 3698 zcmdT`O>7%Q6rNezjhk)4x@jqCNSl!U8EDdyHXsy?AxMb?GKU~U3MfS0ZEMxBqgk7_ zmLv60sievw1*(KwxU{JvlpJ&Hu}53llO;}E%B@l^QBHhsW_P_#-2@OIv6}sMc7EP_ zyYHL#-ov)G)(~UN{_^x{`?>DhzoGA6-0f|?j?N-8*==@-nH|)zY6lSRD(bgHi%mayu&|c z9AeAt>w|asclt-z^^4D!LtEcDI_W6Qk7wmz{nHzdk|j{~-Du}W+}wdIk8z6E<}@W&he&?c6!l(Jb9EqD?80XFULNs3lHw)guA zO5MBamrCEkmaV7kDT87oiMBrCYeQ#c5KxT`I50|12T>fr=1061DHLb27CK@2cQ(1Z zN7_fnE2s`6!sif2aQ)bX_g-?NqjCDd(vl2KHn#w#%M3 zs5@z8t!XP)u$vOdU%U^pH(v!1f6l7599cNI)O#=P56Hi&*dyAoHmF&&W1!TtmrAMU zJeGQtNmPu)dY@9U(t&3+cGiIf22BK;R4!h^RuLoJ0hY8iXcfmWO{a2`4une-618y% zP&E<`P)|5|q7qL0pNVpH2T{J4L=i{u)siNHqE*mGING-5k|U1Z?%eQ}Qcw^?jlJD} zd=H^gRJSLpxib$0Wr!14Oc6?uFO{?C7e+$xyI(S<|n){Rk=Cl=-)l>ySPzw!K%`m$OhTTDRHIvCyqBAPTM`+ofj*z ztwfeiF8BTvS8u?8f8(Kga;UYW^VzJGE_hi@hGuo%d-pU#Y95AqKS_*MjM0t3g(C|` zl;-sgJgc1-9bjz8i49?XN0XLGy2Lec5W>V6`XFLL&o?Slv^QEJ;+T^$>J&O#ZWO4? zeS88cs>Y}T)JGkCv=Saw^zw@h?DOgMkC!WyHUs6#p}TyJuZ8z4Sq?X$6OmP>5BgHS zWND)WvKDSqwzQAC;ej>^3doI))iF{!bvctQSi&-!rVYRDAYa&YM)8L$@SJSRBNWhw z>cxjU9^h9XeWSjsfv->7<|%)hp!tQDmjKcCefN;t261jEE4sU zi?h%ob4(zCkQ5?Ks%oShpq_H{R3)5Jr1=kG5;;o7q)*TZ#H3@Y;s^#ls#G7qq*dRn z7gWHS)UAJ8zk;M$J`1^PEoENTQ2pvb8hP;|iap{ez@?O`N`#_RF*h@n zzP1;oUZ&lB0AkdbYCj)c?pU@Sqp}hLZ!@%X9J}R8MBJhIlGbCvPS9J z#SW>wAzDpTjEN`XO5-o~`r8{L)q$ilkibk2?wZ8IQm2-JmG%!K3NjWa!2M#8S^B0rBqb|P>HST`sna< z-YjOVgrI09E9vX#JPUEopJ|+bz@M=IMmc|<23&albxf}J5eq(*BR*o}k{oG<@*DmD Dzjqzd literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.12.pyc b/tests/pyc/nested_generator_comprehensions.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c39d374acbbaaddf79feb70d58f8fea9c9ada6a GIT binary patch literal 2604 zcmc&$O>7%Q6rR~#JB}TeI-2dZFsH>p*}wq|Vd zBS(r_Bq)t&LPZmSKyD8uQ9z0~`G`0nxHxf?N)|T`zzx-xIw#&6dp9;QRS#6G5-}mqQ2EO&ss#kguoDHIr56KkKDOjpYU?p7!E9(ka zMYn;qE!ure5%W9zT!9}RQ0fP>K~h!@_sNh8WC!B->{K%siBHn%wrGOvDEvwqm^TQ8 zZ8GH2vxF?u481^ZN)ahaBGM%XM5K(Ap&6+iR^F0UoeIR4rQ39aQnK2ikVSZ>A~sLH zljrF+)wapDT$F}JiZV;g7v(F)BFFoBz22jjQwC4*;hE?{D$NW&9EVp!pPn`11`DSW zY&w#dOETlK5$DlFoDU@zkDA9ZyO3o5rL%f8l7e`cEe?&x5|MC>`-iNw1ZQxp$sgq5 z3fWf!yAxZZho0f=#9Gf9-yB_ESRaJfJvC6?_Y9YL3oO0b+(&P?z4lFaAKbiXOQn-B z0~`fz&N05wb3=HrD~x4uH{r04Dv3mD3ok2jCcTg8}fQX!{@>OB*J3 zq7KXji6w6=%2U%u%$PIcDc%4Iylm-tFjXl0yFFX2hn~T#ZLM#!`<4&fJ=G5|43+h~Rl662D?^~*Yz@KY7Q2hm$Kd<Tl$?}cm^tr_DSqCY{I(1aICcV{fe@js=oolwsGSkBn1c= z$V)*D{?L1%1`DoWUI{)CZd(%pVX{C1lHf$(bk%OcB<2FB0E&!=*G>m_o!EF!P6vh% zRG5w$(SYflGtpSeV20j6R-VF7&Vi{Sq2)$@?m>+o%T9dXQ>*d-h6eI7Ru)yf+B85v zQ2>ype*0}lzx``>6_gV)<1kNWeQ*qRMXktdh=NvdX?aC^f-PfVk%aSWTIK~r<9OU( zS2A>`+Os&bZ^{wbMCUz-FH6g^z^EA}xD2x!(UF&BYkK=Ed_=S|zXd`*T9lWfNuRBz zm4sMW{gj@IbB+oO{Z!Xbo#}Wo9J$gEKOt@-@R8X{p`R{(%XeSRO%#R$_dSz$IGg9E*i%F0f@iYKapYcW8>1V}5yxHIh~t~nWA$z2g9UYMd1kouf&iosxh8=ML literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.13.pyc b/tests/pyc/nested_generator_comprehensions.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5eb814e44905bd74f3349053b804ad2b29ca8291 GIT binary patch literal 2700 zcmc&$O>7%Q6rR~#JC2>C-Z}-+P~pT8#Hk!7ICiBF8CN+Zml!15l>-vNvNyrhv8@@q zB%Gp_8wb*#h#-L!IMoG-6fSLURH#DY;$)*%T5;pl8>%n0xbWVt?X1&~f<(niewux6 zc4qv(_kC|_-Q9MP5ORO&&(Ghey?+097`GnU7}bVhut8$vG+`u0!P1xjR)~pU#h3(E zidn!~maN{6bn`p>T!J5NPDPFOqZO zBCXJ9g;YdM7#-6@R`^I0FDk1XPwjTQwega|OWePhT`85B!u@%8RWb`pN?u{9Qh_a` z3(G~Oe4ylcwvgwe#Z|3y3ZpAU7S_&Wvgs1|Q*3o~DpyFSay&fR3Jb6Y$ILt;YozLo z>`ZJ=-wdd-|5jjYdYxZgx#p_`9?1TkiTa-Guh+U@>gCR1`e~2H`o)Q1IJssim5VtA z1`?c{bIhWf*}-b|gRR*~aA1hLz^swq9L`;5^l9{^{CHJ<^T z-vnHY_yD=Lj{}Y#0#0~@8H_SSydpBJ6lMdXSuUk=Wu=Lso)6~0fIFAvr3EFYEGzjE zKLQdwbZrQX0ZDkLciVH*rOLirt}V;vzzuK3^+5LR^a3EhI_rjstOtz~j5+(=Z5y$d zA&jT2nE(ji?Nyy&)e){rVTS(y0f6zR0LcCe0N4v~YCZexB^Vu%?^n2TY`U>h8Ss)E zlgcjh8f*Y21GNb|gGNsq7D4lA)$J&F-lJy8Nze(3vh$(sp9v3El3u?V7=)=wdfbHM zX}S2`$W20}-)j&vcHQ_>}le-LzO%~aF_AS=0r z6k96)T~%X=avzu{Mp@6?8@dy?`r>Vu+86x6b*Yqu}>P!3h)(7mDhUSF`z zQE#qvjnU6~&f33njZwYSy#kZL&yXY>Q}2G?5S*U&V=xppBna;X^F&vjedo+iL#lH| zb<9+yndegUt#+{jyP7aNi|qnC89o^}_Z$d96Te0S0TLGm56?*sB!osw0U15iQXr@3 zIowhhqGS*Q1?320^e<*}C50)OBQyxrnva7qz=|SRzj{IkAfgCWRCPqFQuMhJ ztp(X9aDq0X`H^S`mxj2Mpuq;)k$4wGE*?NvhDgzq5ZVBQ%1?UWHqeAl%!{r20HoLi zo*pr71Uh_Ps)-+Gi(ZR~in{Q$_*h=fbB?6}d~73Re>q=Fr7s={E=nd3foV{2elMQ< zCH5WPK6ht&-yhkRXZQS(uTO8eexANQvhKMyvg?mL1ZO;1SLN9{$Ktqh>O7&F-IKP8 zb<*~|3@>fWMCof|rmcA%HU^TK4_ zL|?0^P?^YpQe(l=XXR)2qs*1-D?)%W}h Dq+bC{ literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_generator_comprehensions.3.14.pyc b/tests/pyc/nested_generator_comprehensions.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a00b18f46968fe1184f4a03a208ab38d24710b0f GIT binary patch literal 3144 zcmdT`L2MgE6#cVnJ5Jn`-8fLvrcGRzASaF8;y6ela#BS~A|e^#woX%3M8?^srjBi9 zY?^Y4S`HOLkXAvB#G!#xEFnn6rBx3I!2yX&(v4cl7Y>|yL8#$S-}rym-q?*(;DX9Z z{+>TGJAZcG`|nTB-sUD50IqAle?QZnY4{D>*2BuJt!`{AKos^t4x$V#i%MvvsEk&Q z+R)mfcC_}W!wbHKMCDCdz=l5xv2_;B0-Qr@K5wwMr8i-w6`&nDz;CP{0|olZXi zM4GSzG{PYW;mV#JbNb4WFeHPz_-X6P@ZG+(k=3BC1`5HIkwx+O z{Egm1@PQh**H^l)21;9daOxFj5Bpf{a(tom;LeNoTt1u9uwlcU3qeQO#hcI;o3=yq z7VMxQaRki*{OVFZ32jh3T^ar?v~C-|X1Tg+82v4r2Q^^Wu;ag{fCHmGlLCH=0`Evi zk!F<#Nw+%%*bWLvj|hthrxgk^C+2Y{nxg%Kcq*?|*kGJ2N5(<1GnEv%X)UGAY3ZD( z11@cSYa86~-PSels#{lk3+@&Bvgf96(N}OkP@5A%!~gDO#Tvnu7Mk!&Ap`-la z2gvLW%AV>qxCbXU(hCR480SrdsytTmT)>_Ji7?JJ&JzGdHS_eNXj zq1wN$_WyXKbiXZB5+tKnT|xG#>TkN$8e~S+`6RAlKc}P;gk9HpQ%CpKZiG`x6D866wb!Ck5Z|qvA;_9w3^q-gfv+C&;+_{DSF{-DpVh=sDDDmwufhSa9gHdB& zq#C<(&ZH+MJymy+$5iR7>h5d1)m=`gn|VrFStm~qoKL258rNp($SajJaU6{aeuTjP z#k;BY7qEb$SsD4xyVSerT@F^tsDZ!QUlMp*fh&zeY|%EvmKp~fUo{UnzEy@y9_&zG zXJr$du2960+A4~XbtOW;M@V>0S9e9we?Eo4qQOV_EmcB*sgz~s%9k;KleQS^D~lKj z!)ZA$8)U#ExP<-O8;9@McLO~Q`ljUCgO7#mo}r=8`Z_o9fN|#s%G9$ wp)pm;IXIV@$)~h1r<^eEdKB{ynKAZQV(bKa>||`nGUJB-7W)%>&oU$CU!1i)z5oCK literal 0 HcmV?d00001 From c98e72f3ea02aaff3236ee2d85f4010a2bbbacdb Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:35:17 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../b_nested_generator_comprehensions.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/behavior/cases/b_nested_generator_comprehensions.py diff --git a/tests/behavior/cases/b_nested_generator_comprehensions.py b/tests/behavior/cases/b_nested_generator_comprehensions.py new file mode 100644 index 00000000..33da158d --- /dev/null +++ b/tests/behavior/cases/b_nested_generator_comprehensions.py @@ -0,0 +1,26 @@ +# MIN_VERSION: 3.12 + + +def dictionary(rows): + return tuple({key: row[key] for key in row} for row in rows) + + +def call_arguments(rows, collect): + return tuple(collect([value * 2 for value in row]) for row in rows) + + +def filtered(rows): + return tuple({key: row[key] for key in row if key} for row in rows if row) + + +def unpacked(rows, columns): + return tuple({key: value for key, value in zip(columns, row) if key} for row in rows) + + +def collect(values): + print("collect", values) + return sum(values) +print(dictionary([{"a": 1, "b": 2}, {}, {"c": 3}])) +print(call_arguments([[1, 2], [], [3]], collect)) +print(filtered([{}, {"": 0, "a": 1}])) +print(unpacked([[1, 2], [3, 4]], ["a", "b"]))