From fc5d7404b7505e5807731353c2ff49b667d47ba4 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:05:42 -0400 Subject: [PATCH 1/2] Exclude frame StopIteration cleanup from exception chains --- src/decompiler.rs | 11 +++++++ .../fixtures/sequential_generator_handlers.py | 29 ++++++++++++++++++ tests/integration.rs | 16 ++++++++++ .../sequential_generator_handlers.3.11.pyc | Bin 0 -> 1167 bytes .../sequential_generator_handlers.3.12.pyc | Bin 0 -> 1015 bytes .../sequential_generator_handlers.3.13.pyc | Bin 0 -> 1021 bytes .../sequential_generator_handlers.3.14.pyc | Bin 0 -> 1015 bytes 7 files changed, 56 insertions(+) create mode 100644 tests/fixtures/sequential_generator_handlers.py create mode 100644 tests/pyc/sequential_generator_handlers.3.11.pyc create mode 100644 tests/pyc/sequential_generator_handlers.3.12.pyc create mode 100644 tests/pyc/sequential_generator_handlers.3.13.pyc create mode 100644 tests/pyc/sequential_generator_handlers.3.14.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..7e1768f5 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -8108,6 +8108,17 @@ impl<'a> Ctx<'a> { loop { let mut grew = false; for e in &self.raw_exc_entries { + // The generator/coroutine StopIteration handler spans the + // whole frame, not this source-level except/finally chain. + // Following its redirects would swallow later main-flow + // statements into the current handler region. + let frame_handler = self.idx_of.get(&e.target) + .and_then(|&index| self.instrs.get(index)) + .map_or(false, |instruction| instruction.op == Op::CALL_INTRINSIC_1 + && instruction.arg == 3); + if frame_handler { + continue; + } let seed = e.start == from || (e.target == from && e.start < from); let inside = e.start >= from diff --git a/tests/fixtures/sequential_generator_handlers.py b/tests/fixtures/sequential_generator_handlers.py new file mode 100644 index 00000000..ffc228ba --- /dev/null +++ b/tests/fixtures/sequential_generator_handlers.py @@ -0,0 +1,29 @@ +# Compile with each named CPython version; the committed .pyc files are +# exercised by the existing Rust integration suite. + +# The frame-wide StopIteration intrinsic is emitted starting in Python 3.12. + +def synchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 + + +async def asynchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..7e54b7fc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,19 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn sequential_generator_handlers_keep_the_shared_mainline() { + // CALL_INTRINSIC_1 3 starts in 3.12; 3.11 has no frame-wide handler. + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture( + &format!("sequential_generator_handlers.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + let body = " try:\n yield work('first')\n except ValueError:\n yield 2\n yield 3\n try:\n yield work('last')\n except KeyError:\n yield 5\n yield 6"; + for definition in ["def synchronous(work):", "async def asynchronous(work):"] { + let expected = format!("{definition}\n{body}"); + assert_contains(&src, &[&expected], version); + } + assert!(!src.contains("else:"), "Python {version}: spurious handler else:\n{src}"); + } +} diff --git a/tests/pyc/sequential_generator_handlers.3.11.pyc b/tests/pyc/sequential_generator_handlers.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..008ddc55f84d67317e746ae8e2c877d886280670 GIT binary patch literal 1167 zcmah|F=*RB6#b`@Y)7_aW5-Lf6x<9MT5m0dwveH82xJLONtYlZr7@MI=1#^jbjXmw zfefV`vX_uz0;P28P-w;uh0rMo3WYAsrsBn9>OaYnW9-^G>GAdN|0mtOckeY#r2z2p zyY}ttk0*bBW1L*1A9afu9Dxb@@C;0XNK9!J7U^bOAqr)DD)@9y5SD>p#|%jD3Oi1) z?lh@ggCjz~nXQ0_mGN~GSek$-MYHSR$W7R#>(C@7jdFc9`M+#Z%mvVAQ^v5RqMQk< z&`GcO%2thW?^5($ik@!WDOW_T{!*ft!~wiv(tvgK!a%h}v!uH!a1DJxd^;VFfZ|-Q*ZZYr_a2LJ zjqJ{WeQ2L&=8_x9N&}uSnoK4L`8@vUzE*@cdWF6t6?&@OgU|YsO3!8p;)pXSZgBS# zHcfKIQ;4*2$|K&$2yevm!|sT)NGvaat<`rOYk?Z5(W)o`lP%mOdsRsiEjffO;kQ@p<3KgL&&9)70vpTaO33!k; zx7I(PwkM%@5P}p@`~%){_Lf|9uU_oQ(DYEM2j3){C8k-Jx8HvA&D)**&9_8CX84b8|Jp? zHX)E_TSVT&S!HN8)|;(jGaozkuJgd-xRUld%){nn87gcZ$^=t%D5XZJi~ZCtE0cPB zE>M>WT=;u(#h%=Cptv?ZtMWO9rA`C=w(XXjTbz45&}N-#SJg?Nls$e=$eo-$O8buR zMS7`F@k`thY2iF5IYqx)s(c!l5@r4z;n0Fm6F5j;PhNu=7SGZU7N`lLuDPJb; zV)+G!kDyJNn)+dlJ#pYx50YS>-Up(lzFW!7c$=a6 zbD)DcK4ktCH}M5)B&(5{eh!`}D4y$A;FXHvbz+*mqbS~!Y4(Ak_>{=1tU;yMP>9s3 zw2wh%`1d+H*m*SMU8Rp1)kx4$BGykFgRM!}{T$+sKn~%SIdvd+X%IM4JWc@!Ntq@3U2*~46KteMDVv(A00iLQ^i@Y?h!%hsv8!N*; zQWT#_hJ9fuzFJv>H8klpbRxAO?YSx1JwjZ5b{#rcp}VUi?Z&E?FS*VPKZTw=szUA- RPY5CVrcTVx1QtGy-ao0S+jal| literal 0 HcmV?d00001 diff --git a/tests/pyc/sequential_generator_handlers.3.13.pyc b/tests/pyc/sequential_generator_handlers.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c083b3a4ece3b16cb8a473828e6973ded609066 GIT binary patch literal 1021 zcmZva%WD%s9LImNvyZUZJg8M6J{nsQqhb!lrr-l}kRF6wf?hn7rQJkhY&O&+3-jCWe&@Th`kp53>jchCRj;D%F&f*Uc_V^b2x$*eUZidakv4qn7=T{-Gvs( zLYd-S$wssC$gCR9R^SBO!j-~H15*RQccpNR87Q5!;WC{8n_x~F%PGTLF3J_CP1;(? zSL`a|URS)+6`lLCW8$+So>eSu$=7e1PTji3n9Kawb*s@=ZQ{%KT(-_dH(5XLS={4! zyL#WNGmGcBb*FCCysGIeSFD=FOwVP@Yi4cLu^2ypyWz)qqqeff+?reGOl+w57#BTu z;Q{m{s-%A;r=AtIH+Eu2dMdGxg=j6(qsCH*~_dZhIzsy}-= z7}F!>Uw#$W-y}tyH1%0{BBOY&FThI$#p~oeeJ`Q-K<4R3isEy!sL_rrypBqQ)`UIA znd0UJZ*b)~WHQdVAcwnOrhPSm^)R7Ei80=>kav_s;-@2$BgDg`vM+fqN($yoKxVoq z2`nK#mO|+p=6)tqXdMuVNlbPJOt$`GvO8eX9AWZ!5|e>BWRfP$_$hd*6w{;?zX-2X z6mL_D^pk|*3t6OJDT;5YVuE&b;dKlmbVAsroYX%=O#F2l0;~`=6}z3|j7rsF4$_0tLxf(Gl%?6)#cft+HrU)s zji>qr;!zPiNIZ(-2YAT8(0b6l_T(*?90Wc1PBt5Ax(oB}!!yskJ3GHQkxr=;0A{sA z-*!%0*LP449@1BxQ55S?f`i~eiC`rqDF@SZDNKl93`Y(}=Ovb7ki`|_umDGK-I;!F z4c18($`s#nHk>6xvq~^4zHkI?Vz1y)pppPMjD5>e&}Ra!P1NU)yk1>eVQ$r}ai(Dp1qZ)@YyFy0+EgHY5ifmO;P>ns-W#3 zGXL_+*nfi*Q>3Bh;BgGaGxa>YP*A+q3-q0Y;yo$Q4-~~Gy{OVwTzIV!BD5;(J!B~! zZ|5y`jwV}(1t(pmJrzNBkWj)z1mTg8aF|BM$3t>MjKi$FE1M6qf-(~qn~5W5n1ui$ zOQQ5Ob2phZS{aduBD38mbN@dw+kG+*hR8gMBGWeqWHO|YoPsBEF+-jwFTtxgiZ{j# z{V1XMOlIg8isGwLG-xX&yp~RcHiSJtE%mMv6T5Cg2P*_WmAJU-R%(tl%l4rst}NHF S;8h{ym!=Y}JBEcB==}p1pU1-h literal 0 HcmV?d00001 From 583d2cdbbb08ef46367586d6e5da8d0d3f9dff0f Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:32:17 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../cases/b_sequential_generator_handlers.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/behavior/cases/b_sequential_generator_handlers.py diff --git a/tests/behavior/cases/b_sequential_generator_handlers.py b/tests/behavior/cases/b_sequential_generator_handlers.py new file mode 100644 index 00000000..54ae3d4c --- /dev/null +++ b/tests/behavior/cases/b_sequential_generator_handlers.py @@ -0,0 +1,41 @@ +# MIN_VERSION: 3.12 +import asyncio + +def synchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 + + +async def asynchronous(work): + try: + yield work("first") + except ValueError: + yield 2 + yield 3 + try: + yield work("last") + except KeyError: + yield 5 + yield 6 + + +def work(stage): + print(stage) + if stage == "first": + raise ValueError(stage) + return 9 +print(tuple(synchronous(work))) +async def consume(): + result = [] + async for value in asynchronous(work): + result.append(value) + return result +print(asyncio.run(consume()))