From c2c5152385bcbc69d60ebd26ac4b5d70992a211b Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 05:10:23 -0400 Subject: [PATCH 01/22] Preserve disjunctive guards inside existing for loops ```python def select(values, test): result = [] for value in values: if not test(value) or value < 0.2: result.append("dry") else: result.append("wet") return result ``` The OR-join recognizer rejects any body back edge reaching before its condition because that normally identifies a rotated while loop. Inside an already open for loop, the ordinary iteration edge satisfies that check too. Rejecting the join can lose the first operand's true path or separate the else branch from its condition. Exempt the nearest enclosing for loop's exact top, or its proved EXTENDED_ARG prefix, from that rejection. Keep rejecting other backward edges, including the edges belonging to an enclosing or nested while. This preserves short-circuit evaluation and both outcomes of the guard, including negated calls, truth tests, None comparisons, and long bodies. --- src/decompiler.rs | 10 +- .../cases/b_for_loop_disjunction_legacy.py | 22 +++ .../behavior/cases/b_for_loop_disjunctions.py | 161 ++++++++++++++++++ tests/corpus/3.14.2/for_loop_disjunctions.py | 131 ++++++++++++++ tests/corpus/3.14.2/for_loop_disjunctions.pyc | Bin 0 -> 4713 bytes tests/corpus/3.15.2/for_loop_disjunctions.py | 131 ++++++++++++++ tests/corpus/3.15.2/for_loop_disjunctions.pyc | Bin 0 -> 4758 bytes tests/fixtures/for_loop_disjunctions.py | 99 +++++++++++ tests/integration.rs | 22 +++ tests/pyc/for_loop_disjunctions.3.10.pyc | Bin 0 -> 1747 bytes tests/pyc/for_loop_disjunctions.3.11.pyc | Bin 0 -> 4401 bytes tests/pyc/for_loop_disjunctions.3.12.pyc | Bin 0 -> 3609 bytes tests/pyc/for_loop_disjunctions.3.13.pyc | Bin 0 -> 3697 bytes tests/pyc/for_loop_disjunctions.3.14.pyc | Bin 0 -> 3695 bytes tests/pyc/for_loop_disjunctions.3.15.pyc | Bin 0 -> 3724 bytes tests/pyc/for_loop_disjunctions.3.8.pyc | Bin 0 -> 1759 bytes tests/pyc/for_loop_disjunctions.3.9.pyc | Bin 0 -> 1759 bytes 17 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 tests/behavior/cases/b_for_loop_disjunction_legacy.py create mode 100644 tests/behavior/cases/b_for_loop_disjunctions.py create mode 100644 tests/corpus/3.14.2/for_loop_disjunctions.py create mode 100644 tests/corpus/3.14.2/for_loop_disjunctions.pyc create mode 100644 tests/corpus/3.15.2/for_loop_disjunctions.py create mode 100644 tests/corpus/3.15.2/for_loop_disjunctions.pyc create mode 100644 tests/fixtures/for_loop_disjunctions.py create mode 100644 tests/pyc/for_loop_disjunctions.3.10.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.11.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.12.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.13.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.14.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.15.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.8.pyc create mode 100644 tests/pyc/for_loop_disjunctions.3.9.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 05a5036f..c6892813 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -23216,9 +23216,17 @@ impl<'a> Ctx<'a> { // rotated `while a or b:` — the body region loops back to this // condition; leave it to the while machinery if let (Some(&bi), Some(&ei)) = (self.idx_of.get(&target), self.idx_of.get(&exit)) { + let outer_for_top = self.blocks.iter().rev() + .find(|block| matches!(block.kind, BlockType::For | BlockType::While)) + .filter(|block| block.kind == BlockType::For && block.start < self.cur_offset) + .map(|block| block.start); if self.instrs[bi..ei] .iter() - .any(|x| x.is_backward && x.target.map_or(false, |t| t <= self.cur_offset)) + .any(|x| x.is_backward && x.target.map_or(false, |t| { + t <= self.cur_offset && !outer_for_top.map_or(false, |top| { + t == top || self.is_loop_top_prefix(t, top) + }) + })) { return None; } diff --git a/tests/behavior/cases/b_for_loop_disjunction_legacy.py b/tests/behavior/cases/b_for_loop_disjunction_legacy.py new file mode 100644 index 00000000..fec52108 --- /dev/null +++ b/tests/behavior/cases/b_for_loop_disjunction_legacy.py @@ -0,0 +1,22 @@ +# MIN_VERSION: 3.8 +# MAX_VERSION: 3.9 + + +def select(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +for values in ([], [0.1, 0.3, 0.1], [None, 0.1, 0.3], ["dry", 0.3]): + events = [] + + def test(value): + events.append(value) + return isinstance(value, (int, float)) + + print(select(values, test), events) diff --git a/tests/behavior/cases/b_for_loop_disjunctions.py b/tests/behavior/cases/b_for_loop_disjunctions.py new file mode 100644 index 00000000..b8ad1ede --- /dev/null +++ b/tests/behavior/cases/b_for_loop_disjunctions.py @@ -0,0 +1,161 @@ +# MIN_VERSION: 3.10 + +def call_only(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + return result + + +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_only(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_only(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_only(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def continue_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + continue + result.append(value) + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result + + +for function in ( + call_only, call_else, truth_only, truth_else, none_only, none_else, + positive_only, positive_else, rotated_while, break_loop, continue_loop, + extended_loop, +): + for values in ([], [0.1, 0.3, 0.1], [None, 0.1, 0.3], ["dry", 0.3]): + events = [] + + def test(value): + events.append(value) + return isinstance(value, (int, float)) + + try: + result = ("return", function(values, test)) + except TypeError as error: + result = ("error", type(error).__name__) + print(function.__name__, result, events) diff --git a/tests/corpus/3.14.2/for_loop_disjunctions.py b/tests/corpus/3.14.2/for_loop_disjunctions.py new file mode 100644 index 00000000..795af584 --- /dev/null +++ b/tests/corpus/3.14.2/for_loop_disjunctions.py @@ -0,0 +1,131 @@ +def call_only(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + return result + + +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_only(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_only(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_only(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result diff --git a/tests/corpus/3.14.2/for_loop_disjunctions.pyc b/tests/corpus/3.14.2/for_loop_disjunctions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eec355fedef221ab28a56e9df265d4a773e01ee0 GIT binary patch literal 4713 zcmeHL&u`mQ9RJ!*(!@!cCMzl%g{CU)6gpN3CMfFI4kHyPN;UZqaX<|r@w%*DVm&+C zx^W0xVC6@1;1Ez&<=Pvk{SUiWBQr=KaX{R*l|O**d$Ap-iQN)}#G#h_&2#+x{Jx*> z=Y7AQ=f&};C;&jA{xJ9H)Wb)=W7|GBvvu%3HtHb36zCvvXgP^T%S!@ULGq#XNq)3` zDS$R0jiDWrf@p(M2yI9b(TY-d4pNaKZ@&S+hiy861BJcgJg9sQCSY|DKoxS_EaZd> z<~+`tkCjA#Y5ZS;-ibTybch8SFqq$1a1|SnyeW0siX=*Jb`g>lO(v)CIz)S1 zM&pW5s}G^yoM?q8S|US>#Bhw^Xno8^x6ztsXbGMiL!4avcXAAeusB92vt4}_byCx> zcWLngcBGDL$k0Bge~nEx{cT}4>_!L=7e8lX*)G0y$i>k}fIzu`;3UDa1B5tE<0ueV zGl&YU z1B1oDiA<{xnd7sAre}H{=xFLd6T?Tggvfza)KG{B>pV*iCPwSRGUk#J#tnrCf0d4L z=;+Co8I&sP{5e|Y%tIA2uhE5tVu<^bilG#52#Q=GW7r;69iybG7j(IlzkR)|$ON+v ze1NA$Ib&AHUm~$a;HykM-4SDr*{@TLTATasZYMat6TKg8$os+RPB7Yterpm-WTVoNGNe2PIi+^GPH=NUaEjVhrLN`@z|cPy8bLS+=pT?>l`I z%Bs(zTn_banqNJ~GWBr9i+ZT1c+-3dNyCU|lND!f9!|aMf}Laep7iM7A)MfGf`_$| ztl$6Ric0et6iqH{b>v3uhw{?ik9e0}`Hqg;Uo10p6H8L)e;!Zm0cIJi$_(sOA8gv9 zWIvKTY2%Llzrf+ANYbu7*nW0}qnq6KIH2DISS?>mh1tgiwtpscKS}6*iqLHap#qyw zl}V_eVgiDdJr?9WU;}hF;=zkItEHMEUm{rdyI(5sLjpV%IF5VH`?1G&~D@BU$ue2I_{8N$d>vMvAi z?KiA_f7w;CbY)v|1L;CR&u6!NZeTfW6m`q>*}7%B0jgU?!*>1EUwxQ&e0*fYJBmapZOogj+JKD?ycS{>sy)4rAj2emU@sXkNprC+4Yn7@jnA35_eL2xQ(=?_!X`= z5P{;w4itkT#moTz)}0{sr446U;CxBotWdVclu>h)JGxETw$mOc%Ogo6>|&LYZdf|S zzG9;=;xbfQO6yIOeiey@7%`O-qrI4;HL)zNI1qstoD6=abo4c_YQsSEwbKm-lzDwy zyH%SO?VDBiz|`az#Db?b8U9ObpzPmLHpA^4ql1~BU@iwDm^t;BnTK-*P-+80bcWXi z5PeuiHxS$dp2ooEAlB%|vIh%Z?96PS9kVd{i( zz2W$6O%CPFyk6b<<5&!HJ8Ke3w73%PTYaNEyOr3ydWg;c2(vj5!OjacJH1Wkc6N*a z;&y^k8xr+MjY#{7K*A@I=*L>R;NT8`fKJ#$U`<1{#IfFl;}9-zu(Y%2xy8RqJDo#8 zv|w5}JGWeOi6NN&Dr2xOS%hOH|JwBGba`y+NCkxs! zJ~ZfX4|yJBSbo@_Bt$yvO>~~zOIXrkvbPN7K-dHHUcJDWBVM{=>0(~yp`u1wT#|lgiDN=`6+_R{0Q#-+W|W$C+SaoR zcW&kkou0t3Hz5}D9Ph!2#7cqBuR608HC7t^Vxm;slD_`166{@zK8Tj|o#0R<7%laG zsjm7f;eIF8A(?!KSsjQFPtNgp(z;e_b{ro=GK{}#+e2qt;niy__Wy4G`PLe%0o ztCffw%$TeP?DWpYUDB|A*Tyj!35Oka>5IU$0I@cZ9BZNR=Y64Z zW2s)G>_kS6`Z(GD7aV?!jMVcF`nw5<9fW5j!rmS5tUeLu?;!XQo3i63Wk)T_P9K!j z+>{lal+{+elHi$-@9{R__gByA!Hc)eY|+p!QPe8c7aDliKz0>Lk{-!^DgLOh2Wp^m SFMuI&Rf_*LcuI=%+5Z83Va!4R literal 0 HcmV?d00001 diff --git a/tests/fixtures/for_loop_disjunctions.py b/tests/fixtures/for_loop_disjunctions.py new file mode 100644 index 00000000..2ee0f33f --- /dev/null +++ b/tests/fixtures/for_loop_disjunctions.py @@ -0,0 +1,99 @@ +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result diff --git a/tests/integration.rs b/tests/integration.rs index 62a3b6f0..75ca4bc6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -25,6 +25,28 @@ fn assert_contains(haystack: &str, needles: &[&str], ctx: &str) { } } +#[test] +fn disjunction_inside_for_preserves_each_operand_and_else() { + for version in ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("for_loop_disjunctions.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "for value in values:\n if not test(value) or value < 0.2:", + "for value in values:\n if not value or value < 0.2:", + "for value in values:\n if value is None or value < 0.2:", + "for value in values:\n if test(value) or value < 0.2:", + "else:\n result.append('wet')", + "while value < 2 or value == 3:", + "result.append('complete')", + "result.append('tail')", + ], + version, + ); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/for_loop_disjunctions.3.10.pyc b/tests/pyc/for_loop_disjunctions.3.10.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da87c741b0de403c1bb4a3dbdd55a5fb5bb56dd6 GIT binary patch literal 1747 zcmeHHL2uJA6t-ixOVea4u)%Rh)C-dk2o4iNFmdcDKL1H_R;e(%M0?9YDR_tGHn7$Ib&KELeV?)V=e{^@bx z>pnES2}Te>Uyv>lj$q)a;GzcJ5p}Tyo(Wepz;oe=Cit50MF74omUl>drH^Y(9}WSw zhtTw0Fi**`lap=oc#r`NCW`LG2D z)KQlmNHeBIJsb{YBF4O^kHR>Uy5L4?Q`D8zS!{}$?Hv>4!R}wLN}h&>{xu#TQFLk^a`Am zDSvXx%jca^;bsamrD|vpb7`6o6`oO<*|VtdTTf8|rV`aM^e#p_7I zwGMd$h{#ABxkG-P0W%%oKkJ;L08Vk;wo|ZJCJGfWH=%(_O=zU(9qdK1Y{N8-DJ+@h zA>Y1nrdFYAaaOlswBp*O8CA6?Nt(#H68l71R%K@zv&eZzA@Z)7-vHXvKcvXR%7tI#U3q%{p zLj$D~=!Ye63sd;0N})&p_MOzCYp_(Oy<{t;LRQ;?ppYb%(UutsXZ=v-Y{9d&&fVNk UMHb6@SQ4!&T;h6@x8Voh0eD$GOaK4? literal 0 HcmV?d00001 diff --git a/tests/pyc/for_loop_disjunctions.3.11.pyc b/tests/pyc/for_loop_disjunctions.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcfa57b1bf3f4cbb0dfdeb62d17fafdd3a91da59 GIT binary patch literal 4401 zcmeHK&1)M+6rcUnTGB?g>L$1~RNH(M)zFmE^EFgq_nVl`4IREU_-_Z9P*Y53p3Y|wtM?a$Pkj?>eIuFR}0-&IafTAt| zO1cav>!B+ssbqLx6G9^N=|;O%rt3Od<4fpEv~+Pz;8BSy2~S|R-}iPzL@o(BmtzXZ z@e8I?gR3}HO$N$#?EtcKCH@N96c_)gKc?-^IZx2ZT~D}!?M=>;(}e=g>zkq{ z-%cAvYVF_rjKca7~v)8gKb`eC*UNgzEVVZ?yJ!{=8<}-HI z%v+xn?s{XHv|%h`!@?v6i|IF8O8{l$svBIO^Wu77Fltah)|KQ0fNYyY>^~2gnFbF8ImM24w?^7 z(*2_v>02ggEN|vSfOjcJmxDQqj8|rB^NmaOh1$Z~Bj|AV+Bzy_L*MJccBK|f zD{E(Ow`n^LY<)m!8*mRJvC8L;w0@y_p=DyH8z<{CwVD6SAhrK4&uII85TOz}=;P?^ z-1Cf#O+Lr`EI9)=uVWo(ZMSaU$|0&@RYj;%*YgC-wsk!RUEslm#g|m|OZxAUyeAkq zPZa3)yQnHL?X->c{DAQZYw5zDj#Co_QH6;`oKPcUlQ8Zpo4Y;mz>a5~3=4WU;289dha z0G&=!mOgAEfI$&uqB7q%g1l&-&dqH_+flSFMRUdQPpKbL&Y5j-YDb*f5~o;{8E_)s zNS!C*@Jqi2oJnZkz8#*d1aj9C`$v*+2<^=rU~)ezSWJdkxl3pnM`)cwXx7a#MV7TN)7(TXg@Awnm5` zY^wnWeK1BJ@>={$#Mdh0;Dg|U_!gRdSo*Z*PQGTdNo`d2sTbyOa&zvPIlnpQch0<4 z)sP4Pf1RAVa^Kwg`y=Y@MVeZD8kMJ@!F`wmji4l&fKt#zl%gh~lr$NotSKm!<)K6% zEwudrNT~DM)R#O@*Ws>EfOBy5IKW*|fC4FqFPnE`azWI{BA#Zkg0Rqf2A?xcXxS1d zP4uy8`$L-(5)_2XurAGhsyyTC)oEAM*s?3$rq()fl~gWAv)a1kDz{R4o|>*?QPXl2 zM$No#xpHHd2QiLo>u3J2dF)zd$;zY2v1WmwqizRS#8u z`8r5=W_Hnz)h3hzv}lCaM}u-aV1&gZn9Dq9ezt3Mh>8F|wW2oat4Ud;G={qk4}_D` zWk`s#pE(zP=H3#5E9x}M0;u;Y7nK=S%A(r*?Hd`LvIG1GUgiZ9&6If`iQ3A;i^Uly z6t&|I5_Y~y9{fU|vGuuRlee=%V0vk&%BhFPhXM5R7^lFKh(jjuiScc`s=n-jIHB!Xw4xHmJ_w zBX>}>g{!@ARa!Y{3+(Ih^0mf3i0)e8+wUy@FtLjmz%Cx0+~C^(kB2Rk`VGE5Hbkqb zoy=beTwlxwitp-88hkPEWBzXz6`* f;6wv8kr1*e43XI8XqQlMFBT%P`u<50^E~|n9t&=n literal 0 HcmV?d00001 diff --git a/tests/pyc/for_loop_disjunctions.3.13.pyc b/tests/pyc/for_loop_disjunctions.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63fa3266d2c8a42baf23a2134e8fbccbae392765 GIT binary patch literal 3697 zcmeHK-D?wB6hHHkX}-p^Y4<~2G_|#B6zw9qu&u6$TU(Hc;80lDhcYJDH9F0NJCpha zl7$7$igaKpM7s-lE%=Z4=0nkR6cKz7->Ufs_S~6CGS;M|SSy?(X~iNb`jPE=!E^a;4 z_VxtA(OyA_6pRA#Fc`vD`~oj?5=9V$daS>4vN}~evwZFoQRqDhb6ZDd%SpI-c*sS= z%v<>dADu4|6vVzm(!>&WXmPSKS>0b7Sw8gN5MqJq($w#$cyaPei*#n8Ni|4WJ~8?D z@Do2HNsYNN!RBumZE{1}!I_6;FYbJDL!wmn;9NoV{ILJ7uTe+~nYWBb-!iXh-Vti!{5}yvOQ3V-VLpXAl7dW-*;`6Zmvr%(QJ}ik8qhlZsF2BQ9FKK3eJypruQIn z%Asa9{aYdE>`M(40Y?65F+)XsXD6IB=8HP92$H;(*Sj;-MM|`*F0WG-gP7f5W>5sP zbOU*>#Rop->^MXcF5p*I!^3qkdPn(9vBy@$o%Ps2S=j_)9pASN@iaeka1)S`aGPN| z&1)FOU0?}R&~1wkJBn>Qw(-FH{$K<<&i_w*O!Lalt_%<$MzlNJq}fsI?eWvQ@}W$w zTi7IH!C|c~9Y$N$%9?*8*EH+RNB!(y?S6JOo@$lpZQbkfKNk+q0%rx(-<{#uVc{pP zUBol#KKBShS<_8f*GXA(<+crOd2=^(1>ADvwd&y$^G2?ulOO2_TH-E)$<7h$0>^Rh Y`93cBerSV`@Ln>?B^$d&xuomq9|wFnBLDyZ literal 0 HcmV?d00001 diff --git a/tests/pyc/for_loop_disjunctions.3.14.pyc b/tests/pyc/for_loop_disjunctions.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bba080ff19fb7cf3c78613bcd9e5d018e1851901 GIT binary patch literal 3695 zcmeH~%WuEI-!M7pra9jqB3Ox=_Zp3c@fQG)G8zw2$hf18e1CSa4$aMCSTwW6Ze%OniaafT5`TZ$B{yb&Nd3M8=uTg7(*xsyR&|-eU zV|%Y=wGuULk3~((_A+XgG|N^RclnbI565Eme*UdFdcJVoDxu5hd7aH@x^B$m3+AO# zF=rL@qItx)Vf%AgO`D;bNm&FZ^9M3dp(#T>94Sw}>fL@%c^TPKNzUDxy;Y^P-2CNw zC_0zEpRNwQ2*uvG#_#Pr z@uM*va;zJ#?105e*2M-EeBTh{E+^2qvAmA-Rh61NAKVBK)`PP<1Gy>IZ2|eI0fagc zN7juvcPtIA3ZfO$7?(%eAQGM_#vh(3+<3WJjf%gh7ij|uCID>3(;P?RhEfmq%>GKiBL+A$M0bhBU;t~Lp6LofrJ(28sdMk>nO`0RLfs1~0e`8-NI zdWD2pftwJ=j}LuaQZAh*evegzCFNJx2`v__or+ zoz>cnBz4%yo%Phe^IEDaTLnm2q@V4}=~oPmS`?K%ThLZb!oWF3vzLXKv%UDdC>LN( zwQ+V*?3bb5l|mf?e?%a@wCEeGdjfaUx6_s3MbFOlC>tmjWpF4brO88Ua7J-$*UhAq z$DqOM&Nb0hIX68%?j$yOZ1TVy|LAacn%_@+B&F2uReKbdHlp9CqqK2Yr}Wb67Iw#2 zW_XXwVu*=XUGq7XM5oHEXk?$a8`;HBtW}0L$BpLxYjId|Z4z+&{xspcgfm2J6GSba zN_2_62jiVKLSq( UeV-UF-iroE^xs|kNK~x!KXBnNzyJUM literal 0 HcmV?d00001 diff --git a/tests/pyc/for_loop_disjunctions.3.15.pyc b/tests/pyc/for_loop_disjunctions.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..672ee873aa784346cbd4d2615c263de3615d84da GIT binary patch literal 3724 zcmeH~TW=dh6vxl(`hJOPH&tn9)L4*!+p3YODg~vq4^H}!;=+O=RYFl@?1}1j>$PUr z=_L}{f&?^xVpV-GD3$!0FTi);Jp!`=wQs26g*1Es{Drvl8^J!MfV!StvH9Mn}jbjX^R^fmAdVQq=;G0WAm_)IyM< z_rqDrg-c5J1o;ufl(*V^2w`KMCPVpBTKxC-1>;%oKXS;VD#kw4&2+?2md*)*qA=>6E1l5C1GRx zK48DqgDs?Eh>#R`yzm_eNlzr9I5A7qt?L~$X)MI0$=eWqG*424&0tGijqeAFNMUz> zD4*bpjp>5|cZWBsWzX~2o}TCTsqBt0wtO4@d9DW|7q}{9UVk*sKMvI0eJ5En%!*Z6>{611PDo1IA}gM1 z1Xm`PC!3?K*H%ycGg9)YxhZWCij66q=%cOFG)rEpk?u67NH_8*&8ZXD;F81LbS6)E zVf0erFw)cH5&hUtWAsWWtyJP*6iXY6(8843ZstEnh(tMHY;#A#GbRs3=R!!#?N476k z^c@2-_Oal_n{I*^c$S3JLFQEm4{Dz*?j+LNlXrrmDHrsO^+>iIh<#DGUucZ22afH< z+5#^dH|(*_D^t7Cj{56qUXfW5^|gPk^i|XY3-mt+Jm6`)aloU;LTdj{9(iTv)g5~l zUw(w$xR-6Sv_~1{cPw>k$Grx3Ju$LXk`MGw)BkUA z_{sP30DU9RfgM7`EdyOS5ED6G$A8KxhKG!-fzNJxmM<#4(qNOQlhT+GC+vk~z*kM0y%O zU{|;R$Nm)lLarRp{sJf7*J-;>Z8s)y#Fp(hwiQ42`#jGZ&vO_dBwOvMR`K!cPlzuR z1HN8B%Qm3ngws!?O}N1sjFg+a2BX0(z5pZRHm}2Ia)&R%SmQ4DV6^zs7HQt-;=e}4 zgaF%1Xju!o_oQzW>|wxxCrXmE!^7-D9(Prk#MxSUT-Lim6m>+D35w8N=m<5bO&ioTTq+mgd;U$I!s_^@ z58$Ru-X7j^<+58!JWOJyWDPsSTy+jarK4o7b~P$Ie*%?Tp+cDRQBA!+OhstFgriHuVHn}xs@q(-3L{*DqU4*hZ@s*J6D@VZu%0Dx9x7SmqtQ~(o{hhT|&%v zQUpD%q$dcy0o_P0mf;#2{!N&h5C1Jte+2)OZu{d4+^Zh^luG|xI{zb}+t@B=Mbj2~ zg@iqEq`*jlf@p}Fm`0=&rqQ9xnt_sRfUXbXOGLHnjeo04b=5~ZhkEs-4(cTJd{ Ywm8MVtE{3DdY5$@D9N*0M&q828m){}_}l;G{%jW7~TST>g3 z26dpx%P=}UyDwpO#JPo2>beE0TB>wap&e*U>+M)=8oB99MBcJH$yOQ(B|uX_7@a}P zH&O&$t)zzty#Zz@7t3%B4Ih~?9sX;e{t*5lUGqm5xLG~;A(j5IbpA&`w^6yA6rC=i z=SbKSdkP9EP!J2^3Z@Y$g=uu?CCxxg8p60bgc1T<` aR`hiF)0NL&-b#2LiKnA#Y&(Nix4l1 Date: Thu, 29 Oct 2026 05:29:55 -0400 Subject: [PATCH 02/22] Keep padded None returns separate from calculated return tails ```python def parse_timestamp(value): if isinstance(value, (int, float)) and not isinstance(value, bool): try: stamp = datetime.fromtimestamp(value, timezone.utc) except (OverflowError, OSError, ValueError): return return stamp if isinstance(value, str): try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return return parsed ``` When inert instructions appear before RETURN_VALUE, the return-tail matcher counts that padding as part of a calculated-value sequence. A bare None return can then look like a duplicated value-return tail, causing the following string-conversion branch to disappear. Ignore NOP, NOT_TAKEN, and CACHE when classifying the tail, and require an actual return after the None load. Keep the complete instruction sequence for genuine value-tail matching. Recognize both LOAD_CONST None and Python 3.15's LOAD_COMMON_CONSTANT None through the same predicate. Use it in the function-tail pair matcher as well, so implicit None copies still delimit a real else suite when the general value-tail matcher rejects them. Other common constants retain their existing treatment. --- src/decompiler.rs | 41 ++++++------ .../cases/b_none_return_sibling_branches.py | 60 ++++++++++++++++++ tests/corpus/3.11.14/padded_none_returns_3.py | 37 +++++++++++ .../corpus/3.11.14/padded_none_returns_3.pyc | Bin 0 -> 1765 bytes tests/corpus/3.12.3/padded_none_returns_3.py | 37 +++++++++++ tests/corpus/3.12.3/padded_none_returns_3.pyc | Bin 0 -> 1612 bytes tests/corpus/3.13.7/padded_none_returns_3.py | 37 +++++++++++ tests/corpus/3.13.7/padded_none_returns_3.pyc | Bin 0 -> 1656 bytes tests/corpus/3.14.2/padded_none_returns_3.py | 37 +++++++++++ tests/corpus/3.14.2/padded_none_returns_3.pyc | Bin 0 -> 1738 bytes tests/corpus/3.15.2/padded_none_returns_3.py | 37 +++++++++++ tests/corpus/3.15.2/padded_none_returns_3.pyc | Bin 0 -> 1736 bytes tests/data/padded_none_returns.py | 37 +++++++++++ tests/integration.rs | 25 ++++++++ tests/pyc/padded_none_returns_0.3.11.pyc | Bin 0 -> 1705 bytes tests/pyc/padded_none_returns_0.3.12.pyc | Bin 0 -> 1570 bytes tests/pyc/padded_none_returns_0.3.13.pyc | Bin 0 -> 1614 bytes tests/pyc/padded_none_returns_0.3.14.pyc | Bin 0 -> 1666 bytes tests/pyc/padded_none_returns_0.3.15.pyc | Bin 0 -> 1664 bytes tests/pyc/padded_none_returns_1.3.11.pyc | Bin 0 -> 1725 bytes tests/pyc/padded_none_returns_1.3.12.pyc | Bin 0 -> 1584 bytes tests/pyc/padded_none_returns_1.3.13.pyc | Bin 0 -> 1628 bytes tests/pyc/padded_none_returns_1.3.14.pyc | Bin 0 -> 1690 bytes tests/pyc/padded_none_returns_1.3.15.pyc | Bin 0 -> 1688 bytes tests/pyc/padded_none_returns_3.3.11.pyc | Bin 0 -> 1765 bytes tests/pyc/padded_none_returns_3.3.12.pyc | Bin 0 -> 1612 bytes tests/pyc/padded_none_returns_3.3.13.pyc | Bin 0 -> 1656 bytes tests/pyc/padded_none_returns_3.3.14.pyc | Bin 0 -> 1738 bytes tests/pyc/padded_none_returns_3.3.15.pyc | Bin 0 -> 1736 bytes tools/compile_padded_none_returns.py | 35 ++++++++++ 30 files changed, 364 insertions(+), 19 deletions(-) create mode 100644 tests/behavior/cases/b_none_return_sibling_branches.py create mode 100644 tests/corpus/3.11.14/padded_none_returns_3.py create mode 100644 tests/corpus/3.11.14/padded_none_returns_3.pyc create mode 100644 tests/corpus/3.12.3/padded_none_returns_3.py create mode 100644 tests/corpus/3.12.3/padded_none_returns_3.pyc create mode 100644 tests/corpus/3.13.7/padded_none_returns_3.py create mode 100644 tests/corpus/3.13.7/padded_none_returns_3.pyc create mode 100644 tests/corpus/3.14.2/padded_none_returns_3.py create mode 100644 tests/corpus/3.14.2/padded_none_returns_3.pyc create mode 100644 tests/corpus/3.15.2/padded_none_returns_3.py create mode 100644 tests/corpus/3.15.2/padded_none_returns_3.pyc create mode 100644 tests/data/padded_none_returns.py create mode 100644 tests/pyc/padded_none_returns_0.3.11.pyc create mode 100644 tests/pyc/padded_none_returns_0.3.12.pyc create mode 100644 tests/pyc/padded_none_returns_0.3.13.pyc create mode 100644 tests/pyc/padded_none_returns_0.3.14.pyc create mode 100644 tests/pyc/padded_none_returns_0.3.15.pyc create mode 100644 tests/pyc/padded_none_returns_1.3.11.pyc create mode 100644 tests/pyc/padded_none_returns_1.3.12.pyc create mode 100644 tests/pyc/padded_none_returns_1.3.13.pyc create mode 100644 tests/pyc/padded_none_returns_1.3.14.pyc create mode 100644 tests/pyc/padded_none_returns_1.3.15.pyc create mode 100644 tests/pyc/padded_none_returns_3.3.11.pyc create mode 100644 tests/pyc/padded_none_returns_3.3.12.pyc create mode 100644 tests/pyc/padded_none_returns_3.3.13.pyc create mode 100644 tests/pyc/padded_none_returns_3.3.14.pyc create mode 100644 tests/pyc/padded_none_returns_3.3.15.pyc create mode 100644 tools/compile_padded_none_returns.py diff --git a/src/decompiler.rs b/src/decompiler.rs index c6892813..b61145ba 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -44392,6 +44392,19 @@ impl<'a> Ctx<'a> { } } + fn loads_none(&self, op: Op, arg: u32) -> bool { + match op { + Op::LOAD_CONST => matches!( + self.code.consts.get(arg as usize).map(|o| &**o), + Some(PyObject::None) + ), + Op::LOAD_COMMON_CONSTANT => common_constant_expr(arg, self.version) + .map_or(false, |value| matches!(&*value, + Expr::Const(object) if matches!(&**object, PyObject::None))), + _ => false, + } + } + /// 3.11+ function-tail try/except[/else] whose implicit `return None` /// the compiler SANK into both exits: the success path ends with a /// RETURN None right before the out-of-line chain, and the chain's @@ -44491,15 +44504,15 @@ impl<'a> Ctx<'a> { // the mainline and claimed the else arm's own return as the // mirror, losing it. A bare `LOAD None; RETURN` tail is // try_tail_sunk_pair's stricter domain - skip it here. - if tail.len() < 2 { + let meaningful_tail: Vec<_> = tail.iter().filter(|(op, _)| { + !matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE) + }).collect(); + if meaningful_tail.len() < 2 { return None; } - if tail.len() == 2 - && tail[0].0 == Op::LOAD_CONST - && matches!( - self.code.consts.get(tail[0].1 as usize).map(|o| &**o), - Some(PyObject::None) - ) + if meaningful_tail.len() == 2 + && meaningful_tail[1].0 == Op::RETURN_VALUE + && self.loads_none(meaningful_tail[0].0, meaningful_tail[0].1) { return None; } @@ -44616,11 +44629,7 @@ impl<'a> Ctx<'a> { ), Op::RETURN_VALUE => ri > 0 && { let p = &self.instrs[ri - 1]; - p.op == Op::LOAD_CONST - && matches!( - self.code.consts.get(p.arg as usize).map(|o| &**o), - Some(PyObject::None) - ) + self.loads_none(p.op, p.arg) }, _ => false, }; @@ -44642,13 +44651,7 @@ impl<'a> Ctx<'a> { self.idx_of .get(&x.offset) .and_then(|&xi| xi.checked_sub(1).map(|p| &self.instrs[p])) - .map(|p| { - p.op == Op::LOAD_CONST - && matches!( - self.code.consts.get(p.arg as usize).map(|o| &**o), - Some(PyObject::None) - ) - }) + .map(|p| self.loads_none(p.op, p.arg)) .unwrap_or(false) }, _ => false, diff --git a/tests/behavior/cases/b_none_return_sibling_branches.py b/tests/behavior/cases/b_none_return_sibling_branches.py new file mode 100644 index 00000000..a244a81c --- /dev/null +++ b/tests/behavior/cases/b_none_return_sibling_branches.py @@ -0,0 +1,60 @@ +# MIN_VERSION: 3.11 + +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") + + +for value in ( + None, True, False, 0, 123456789, float("inf"), float("nan"), "invalid", + "2026-10-12T12:00:00", "2026-10-12T12:00:00Z", + datetime(2026, 10, 12), datetime(2026, 10, 12, tzinfo=timezone.utc), +): + print(parse_timestamp(value)) + + +def read(where): + print("read", where) + if where == "outside": + raise ValueError("outside the protected body") + + +for function in (unprotected_return, else_return): + try: + function(read) + except ValueError as error: + print(function.__name__, str(error)) diff --git a/tests/corpus/3.11.14/padded_none_returns_3.py b/tests/corpus/3.11.14/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.11.14/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.11.14/padded_none_returns_3.pyc b/tests/corpus/3.11.14/padded_none_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e08825748b315fe4645feefa419a4c34ec4647c8 GIT binary patch literal 1765 zcmbtUO=uHA6n?XlY|^bsYtdG$n)HXZ!D>^FA|A9NBD9Bs&>+?|vw@UkH_lF4Eh&fy z-n{l8h^NX@@#?`_?N#tRSMlI2kXui_*>sa^()7|v=G(VB@4b0H-^`ap!k_@~_uJ#| zKW_ax_=9q5Xvb1x4uxG{z(@(okb^SG5dutm2RV&VM9LzF5f)vAjQ&rVd76wfQ&^%{ zwq#u4wqsj)a_mw)bewAMMjbcW1<|oq0}*VF3VSH;x8C`q8f>)Q-lS{rS$XLhBHsSA z5e?+8WDob*Z|!AYLwglT&p6pWY&`(#x7r^EqF!OV2jj9@TDv6D{cGm zzWjYx+(Ut8zpgPPRM_3H5Kpu-@Doa`wd1;t#aIhpT+CoMPCC3yp z^1^XSGEv}8S*@e1az!RqHZ6|pTMsyQxQwl=HU;B}S*lu1nYw~E?TW5baCq4i87iZj zN<&7Qe)NjTU5mMjjp=myXhPry?gE~5bD?4~X0de}Px?ByM3vj_&B|*zdZB8f;)|Ha6WZ!5e^o|?hnzQNuuz^+|no0ZdRf8Jd zPhqkU@X3N2!RSvG-(Cyb2Xy!7vedCBMP|vEQx!_^2)~G4v566Gnaou;N}7)&%8{yF z;f}EK0*leC16N6o;pA5l+;a$T2b1u@NYPIu)hD%QsMSBU(*nj5g0{ivpEOUQ4xdEG z5I%*E&NK$Ck{k4q&)}l3+!6mcd^G0iKAES>UbIP7MNYrHe!{+$*UZ^3%~AYL*@yd-t#z{w~XuR{xlb&{$>FZkvD5Axq1!T-Q$)KkeDG1G_teFX@Z#Is z!WYExutsQj4Z$ujV59{u)W9V*LV#J%p=L3Pl(GcUL@!ygzv@E+OQo43xoo;zr8V95 zJzggxg?h5_*OrGL(abK$nAZns48z_Ci3feyOz2~i>{xy4FWIvGNw%z$XR8N`AkP`^HOXV8EcQqeXxowqO-QoiI z-Kr42P^s0m2xK2RtqzYc4Wx+9H3Prl3)hh)s*+n;LM05kVHzT|a@PDlO=i zPhXSy|KNg{)5aH&l!#c=clhPPr+6zcnGyP-4uy}r>B=A(6`wA*Fkzu<;i*C7T^NMJ r^ljH?off|%N+_v~4RzUnr-YCnHjwKF_7r(c4i=JRc9^%xTo{?3D=_~` literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/padded_none_returns_3.py b/tests/corpus/3.13.7/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.13.7/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.13.7/padded_none_returns_3.pyc b/tests/corpus/3.13.7/padded_none_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e33a197b4aba00719b26d87504d70f14f1868ea GIT binary patch literal 1656 zcmcgs%Wl&^6uo1|PGp)=RkTGdG;UhjhDtON?SiM+WD}%TiWL@Q5t(>WSxW3Mo)pm~ z|Bw%0nco2QCuYlv4MGhPTkhDNq$!~+kg(-DXFT`)m{C4&=>YJXR>s$B&kx z(KdnsIN*?%Pz456!iWuywhByhbfmhIL7F)+O6FH7G}J`Sb2;UHmx`REXJJ5FM4qLO ziL10c`i5o(&={Z9p`i@JVTyKBA@>-d4z*0Wy|lFK_#}JjR|*?B^{-|rG+7<;h^&Kr zx1{F$0GB zP6JXOM?U5C9i{E6efRW={dL7TS*88jj4C+TC`zBoS)kRmKh{#4rRDm_rfRAEdo533 z0~*UJw#Mx-T2<&k&w`6xY(oJ~pbgLT0<;NAjX7qW4>Lz4B20ef2klUh*JtKlx%{|X z9xe#WkNhCwZqTAkb~%&HE3%CCIxhY)-fD5DD30*ESZ^E!6@@*D& zF$2d^*rh|1Dx|FqrA_8=t1wjxEYHy$G$MxU2^tE_(8OV8*~wm0do`L^ z7UWN!nPt(Bgo)YrJu0$cpGz<#YzZZ>Tp31PFOt)4vo)k*u^;qU$Z3mX*h$1phO>en zyNe{6L-HZF1Ouz6eUNg>Y7FIimd AD*ylh literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/padded_none_returns_3.py b/tests/corpus/3.14.2/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.14.2/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.14.2/padded_none_returns_3.pyc b/tests/corpus/3.14.2/padded_none_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abf44cf64e72e061ca2a3ac5890e7f2218e1e96f GIT binary patch literal 1738 zcmbtUUuzRV5TCtEE+I9s6x)hPnxs|RSh0Z?>Yw6+5UCHQ;VgNOU^sIZIY{o#y-mgX zQv43$HxL4T3Z;GkANRo*pL~->@YUJ7Z2q*hD&DcPv%531^P8ES)l@Q}0>EnH%jdV7 z58r=9e)uT^uD^oJ0XzYnJb-OruuT}rfv(I0Q*;%nsz;DU^ym^4wQoYLr|`IGFpJq8 zi^m1scO9!q1}4R%;ShKC7m*%7eRx)by1W`T2HdjHz|Y(T$UqIMk#IduTbBoCvGLuI zugB$HH6gjttdq^b?P(DIlDHu`I+xtpRyK~=m_U>o+L)Hyx^e>DJ%M0CZ$?TVmwk3! z$fn0)RlOi(M{@y@{K4x%ggsIt68(e+8ENgf2j;629xO=N(JVZuo{|wAOGjFik@J*Y z<@cGzvs3JxjqYS-(aL{kRu8H~)JY!da}sO);*h%}xph?o6KgL6Imp6D&xSi{7Hoq0 zL)o(>dyz_!aEnTbdltp>7iCIa%40i^LMBl&+FdK; zRiDvN&-UGxOFITDsyw0>caZHS7+?nrE1%Wi}k_4Gn6s zE_M7H-b;QyP-zT(p`r#wzv$xtAKy8A$X+RD;N!y8@)jIs=E}F!!<$6Vt*QGd^;41{ zooF{AFhRS)4gv?5z#0@VA+q#xuq)PzT+63`2}1esn@G^`vB*^RRIXtfY}2annA>H7 z>k&GOx|p~KwG7i29U@QDNcl|H@uV$!bZ*^R=+qUk~YbYrq))!CYyGjW8sR3Cz(o*C(Ia-X8^p9zceU)lL?u0T z9aI>jn^FRSx$vAc`ul3b`fE;S-0?4Snl+eR}nFbtIH_# zn&XLZweDJ;2)%UOx`0G7%|03qWk&s<&u>b?&i1ug2Y~ z%NmxK(?$45N{Db^$7ouNQ=7}%p3P>(f`x!112L=k#(R1z$2Ng>5)Z%@(%w(@i4j_Qaz}{cD0C9eQwNV*CeZ zAHfOEIc&@fn4p?373_)H50n_CgK(v2r0sEt|_HBFmCU z#dOPQGS{OO4>J%1&tzoRaIzEm)wDLoo+e}dMz zDjHisrY}6w?{+w7OWBBiDg8KorFIZ%kdf&9hHJO#^a0DEq&%x5FUK#P5c0`ZsmiMAOz)|T@|R}Pz9*^ak$!+i$ zQ>C(sZ|rh``+ELC2ONp z6*yU$crZ+V#E8bl<+V7R#;vt!dOAfALl=WCjDImaMd1*J!GJ2>J>k(H;Ex7s1gqa@ z_)H_r#0JrIWnv!+jH@BHBDGK>;tE>A#zh3TnWt}vq?krj;}xeYT*-413((A6=)@-Q z5!VpBO9*d967bHNp&!W1fOMXq*5Gvdhl5c+NOlk{1&W}bdI&SG~;E~^yzU6DdfU8lwtLY)#q j>L!ro-*BV%S2q*nIjKVj&PT~~9a=DElDRtcf?wkQ^;;d& literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_0.3.12.pyc b/tests/pyc/padded_none_returns_0.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba1f0bddc75e0dd260fe73aca758fdf679373723 GIT binary patch literal 1570 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 IkgT7YKWK;Qr~m)} literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_1.3.11.pyc b/tests/pyc/padded_none_returns_1.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25d134e70779d5853824846cb80f954ffec76beb GIT binary patch literal 1725 zcmbtUO=uHA6n?Xl-KJa9hJseDZDLz&f?$G25f8TFL1+(!(&kdu>;zJh-8egGwWJ{G z*=rFzcxnk1uO7VBUImXbSMlI2kXui_*>#g_eoh^--@fd;|M$(DBoZbCfInZJef@U- z$JuX`J4^d7wKFIjfekjPKots5B?Usj*4{xuvne9AV~8bsqdHM*ox$>K>syKoIJl&5f?FCb(IU&L2$`V*|; zg>&O~aMM@r$@>O;Ff;UoWawH`52&fg=#SGc)VFE|wfZf&h(D~lcD=$L@hobpUbVLo mT9gpdGJs@$!t&s+WlWM6qy;^=pp)4abRcPvR0{^dZ}UIrvmST= literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_1.3.12.pyc b/tests/pyc/padded_none_returns_1.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2d2792dd1bcd9279c10951015108a97a73f52d6 GIT binary patch literal 1584 zcmcIky>AmS6n{SFE+3Fmm9`)Xq+g&$NHiltz|@F|cBq8ZEY%S^QmW)G*iK8TR6+>; zf)3r-qcSlvR&?S&@P?QW3u*?WPCVbmmqwH>2uptco!^(=*L}(7a})sfpMITx_v6## zH^j+u2B?1*!2vK}qybH+K$BDm0p`4gio+;U%2G&E9k=LxH%dK6XPB?JsyDgL7`ht< zyh;WJ&EerTp8Frs%>k%{*9B!A!{GplXI%dJgZ?qj%;@W$fKn&Z6P@fgBFJ zWnmrALVn1z=wb_J^+#SlmgtXLRCAIj9*oFNSP9FQOIc7RC(+uPv}kx@ZZ3?>TG}IB zVqzQdchlOjQbLaSyKVWy^Ze{GEV{K}GqLA%oxfDb`R58b|5l+6EP$G}QK{<*x?xd` zE`p#0HlPDL@QQB04nduS|0lPXS}hXosoi=|3pJ_idiP4Dhow?)T<7Yt9!TW}RW3}= z3R5zj7QEH)@yaz{w5_T-=Sw>pn$`oQ-C85`l}^0~!-me+MA$TZG}UbB{Mt4b$loao z5el7NTaQ8Rx!-8>7*nZ4tXG#|Efh^(6{&W&jf8e%J6_8dk~3-gY<70`bWWiIxs225 zFSmTgINJcmuY4!2+GFmoPy9*?P1>QjO%pR zRtEHlm_m6hR7!B4Ni+4cVg{+6Xa_A3Dqd9>a2%^Jfn38aZXuDEk-W)Vh5c-Sz9)tM z{CrU`j^~jSiC8fH&ph@LZ%A7aVSdJu@VT#C?YVK&nKp|G6@?XP_2S5)1e|6bHAB{J m@F${(lEzuzwEIs=2>Im!Svv8?$V+lE?~)tX^RI|ULrc+c z1Hn1iV3Pyb2M+s$6ANr@3Akocq}0wJ&7A0Er25sSPo6FEuG0-v>ryi13H ztJFLcsOB6Rqq{mZlw#OT(Pk>-901gzmPxl`OKUa0$&MYRu#r<|HB+Ih)h18LDy+*_ z^H-+|T`#+v+AQTK?4KLS7Zlpje)DAyCZ*;T?=+`d23Q@7O6`u~yjn}>!C$ORW9gH6 z?D2%f57bN3x*a~H8{<4Zr0&{`$lF*P>OB=JU#n|>%%3*vmFvA4V+dSyXdwiCxq)rKF>d@YVf77 zQC)!xZK1kEFYXgbSIp`by(kl;6W@dkO#Dr}LU4|4P=^x!L#mvtWUr~cY(-27@+M`#l~+tQDcliML$3vWSzfAPH|G z`H;H}A5DvXBG!L*fET6e5|T3E52XcT=H6hPP-TV7k2_|bY3JcUPYO?#TAZxdu~=%5 v_?Cnq_o5rP{SMpX_!EItHk3vGof1NR8bCHLjXCm`T$XgQJS-Tb7{}!o{zvKA literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_1.3.14.pyc b/tests/pyc/padded_none_returns_1.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48e6587e4ca9cc341083a7911b3406f2295d5433 GIT binary patch literal 1690 zcmbtU%WD%s82@HB*@V>8P_?b5HA&;6X`l_XQ1DT_1S)76){=t+!QH4>i3X=`b47aRW@^7Xj< zS4~K6wCiMZ;5`j_VLdszlibA?HpcWPkW3HtQ<7U(PN8}vBA?KkkM`}c%p76dPEuHjk?w|60Q_7BZ;k|lB6?AMDX;Eg(Q+Ac}tBH4K z*trS(7IWUyd4A zZHMt_tL++$M_#&an?+w2k zs5FMLP|UQv*n_Cv_b@3ohomsM-K_oiRVNlCU{Ps zAajUCt3eJ6A=@kiyW*_K^Ps3G8-5do2?j!zucvYi({Gzrg~!}36I_qbYp90>i%`oj zeKGx6nnucJx{gO(W;GcC4>3~#Xbu-$MB-mU@-}`2-c9Az4DSz?Ysa0 literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_1.3.15.pyc b/tests/pyc/padded_none_returns_1.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c89e1c385c77e3d00e3982848f9042044f70907d GIT binary patch literal 1688 zcmcgsPj3=I6o0b|3!zeJ(^df~w6={{BeaPJjj3KpJ(Z%tgTyo|JER!c#hImzJvA|Y z1HXYMzJ`V`F;`FO$ztNwH@iFhX)7K~oMhhoX5RmKZ|3Q2Jf;Hx?JvK7ZGZdz1@Ulb z0UF#!aEkLKRLB$91rED}lMGa}Mc`URN2*ssNJIOP708FbO2wWgqL%4Wx7ns5D(Q)1 z(>js7U>LYc?}HH1Qv|gkw+c1o9<~FtjuLXz_W@E+g>uN>k5jrD@FL@_2vok%j zbJf)Tt(yB0c{|HcTT=Pet`AX4p)K8jI`(u5GLQye=Me7eX*eXPJCysgR4=rbC&F+~ zns&nxWGH zY{D#5Ks8#%-xIYD%8HWym{J-|7+8R&MMcEvx)SsVTSPUiScp;6;_~_DSQ@FA@7f*a zxU}wK=6=#lW^@fF%OK%bki3g7z=xSN{UcfXZ+Wn+3;};hjGNt*k&J~sLt9)GiESX$ z6TURwUf6F#*?3;*y!s0^% literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_3.3.11.pyc b/tests/pyc/padded_none_returns_3.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e08825748b315fe4645feefa419a4c34ec4647c8 GIT binary patch literal 1765 zcmbtUO=uHA6n?XlY|^bsYtdG$n)HXZ!D>^FA|A9NBD9Bs&>+?|vw@UkH_lF4Eh&fy z-n{l8h^NX@@#?`_?N#tRSMlI2kXui_*>sa^()7|v=G(VB@4b0H-^`ap!k_@~_uJ#| zKW_ax_=9q5Xvb1x4uxG{z(@(okb^SG5dutm2RV&VM9LzF5f)vAjQ&rVd76wfQ&^%{ zwq#u4wqsj)a_mw)bewAMMjbcW1<|oq0}*VF3VSH;x8C`q8f>)Q-lS{rS$XLhBHsSA z5e?+8WDob*Z|!AYLwglT&p6pWY&`(#x7r^EqF!OV2jj9@TDv6D{cGm zzWjYx+(Ut8zpgPPRM_3H5Kpu-@Doa`wd1;t#aIhpT+CoMPCC3yp z^1^XSGEv}8S*@e1az!RqHZ6|pTMsyQxQwl=HU;B}S*lu1nYw~E?TW5baCq4i87iZj zN<&7Qe)NjTU5mMjjp=myXhPry?gE~5bD?4~X0de}Px?ByM3vj_&B|*zdZB8f;)|Ha6WZ!5e^o|?hnzQNuuz^+|no0ZdRf8Jd zPhqkU@X3N2!RSvG-(Cyb2Xy!7vedCBMP|vEQx!_^2)~G4v566Gnaou;N}7)&%8{yF z;f}EK0*leC16N6o;pA5l+;a$T2b1u@NYPIu)hD%QsMSBU(*nj5g0{ivpEOUQ4xdEG z5I%*E&NK$Ck{k4q&)}l3+!6mcd^G0iKAES>UbIP7MNYrHe!{+$*UZ^3%~AYL*@yd-t#z{w~XuR{xlb&{$>FZkvD5Axq1!T-Q$)KkeDG1G_teFX@Z#Is z!WYExutsQj4Z$ujV59{u)W9V*LV#J%p=L3Pl(GcUL@!ygzv@E+OQo43xoo;zr8V95 zJzggxg?h5_*OrGL(abK$nAZns48z_Ci3feyOz2~i>{xy4FWIvGNw%z$XR8N`AkP`^HOXV8EcQqeXxowqO-QoiI z-Kr42P^s0m2xK2RtqzYc4Wx+9H3Prl3)hh)s*+n;LM05kVHzT|a@PDlO=i zPhXSy|KNg{)5aH&l!#c=clhPPr+6zcnGyP-4uy}r>B=A(6`wA*Fkzu<;i*C7T^NMJ r^ljH?off|%N+_v~4RzUnr-YCnHjwKF_7r(c4i=JRc9^%xTo{?3D=_~` literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_3.3.13.pyc b/tests/pyc/padded_none_returns_3.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e33a197b4aba00719b26d87504d70f14f1868ea GIT binary patch literal 1656 zcmcgs%Wl&^6uo1|PGp)=RkTGdG;UhjhDtON?SiM+WD}%TiWL@Q5t(>WSxW3Mo)pm~ z|Bw%0nco2QCuYlv4MGhPTkhDNq$!~+kg(-DXFT`)m{C4&=>YJXR>s$B&kx z(KdnsIN*?%Pz456!iWuywhByhbfmhIL7F)+O6FH7G}J`Sb2;UHmx`REXJJ5FM4qLO ziL10c`i5o(&={Z9p`i@JVTyKBA@>-d4z*0Wy|lFK_#}JjR|*?B^{-|rG+7<;h^&Kr zx1{F$0GB zP6JXOM?U5C9i{E6efRW={dL7TS*88jj4C+TC`zBoS)kRmKh{#4rRDm_rfRAEdo533 z0~*UJw#Mx-T2<&k&w`6xY(oJ~pbgLT0<;NAjX7qW4>Lz4B20ef2klUh*JtKlx%{|X z9xe#WkNhCwZqTAkb~%&HE3%CCIxhY)-fD5DD30*ESZ^E!6@@*D& zF$2d^*rh|1Dx|FqrA_8=t1wjxEYHy$G$MxU2^tE_(8OV8*~wm0do`L^ z7UWN!nPt(Bgo)YrJu0$cpGz<#YzZZ>Tp31PFOt)4vo)k*u^;qU$Z3mX*h$1phO>en zyNe{6L-HZF1Ouz6eUNg>Y7FIimd AD*ylh literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_none_returns_3.3.14.pyc b/tests/pyc/padded_none_returns_3.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abf44cf64e72e061ca2a3ac5890e7f2218e1e96f GIT binary patch literal 1738 zcmbtUUuzRV5TCtEE+I9s6x)hPnxs|RSh0Z?>Yw6+5UCHQ;VgNOU^sIZIY{o#y-mgX zQv43$HxL4T3Z;GkANRo*pL~->@YUJ7Z2q*hD&DcPv%531^P8ES)l@Q}0>EnH%jdV7 z58r=9e)uT^uD^oJ0XzYnJb-OruuT}rfv(I0Q*;%nsz;DU^ym^4wQoYLr|`IGFpJq8 zi^m1scO9!q1}4R%;ShKC7m*%7eRx)by1W`T2HdjHz|Y(T$UqIMk#IduTbBoCvGLuI zugB$HH6gjttdq^b?P(DIlDHu`I+xtpRyK~=m_U>o+L)Hyx^e>DJ%M0CZ$?TVmwk3! z$fn0)RlOi(M{@y@{K4x%ggsIt68(e+8ENgf2j;629xO=N(JVZuo{|wAOGjFik@J*Y z<@cGzvs3JxjqYS-(aL{kRu8H~)JY!da}sO);*h%}xph?o6KgL6Imp6D&xSi{7Hoq0 zL)o(>dyz_!aEnTbdltp>7iCIa%40i^LMBl&+FdK; zRiDvN&-UGxOFITDsyw0>caZHS7+?nrE1%Wi}k_4Gn6s zE_M7H-b;QyP-zT(p`r#wzv$xtAKy8A$X+RD;N!y8@)jIs=E}F!!<$6Vt*QGd^;41{ zooF{AFhRS)4gv?5z#0@VA+q#xuq)PzT+63`2}1esn@G^`vB*^RRIXtfY}2annA>H7 z>k&GOx|p~KwG7i29U@QDNcl|H@uV$!bZ*^R=+qUk~YbYrq))!CYyGjW8sR3Cz(o*C(Ia-X8^p9zceU)lL?u0T z9aI>jn^FRSx$vAc`ul3b`fE;S-0?4Snl+eR}nFbtIH_# zn&XLZweDJ;2)%UOx`0G7%|03qWk&s<&u>b?&i1ug2Y~ z%NmxK(?$45N{Db^$7ouNQ=7}%p3P>(f`x!112L=k#(R1z$2Ng>5)Z%@(%w(@i4j_Qaz}{cD0C9eQwNV*CeZ zAHfOEIc&@fn4p?373_)H50n_CgK(v2r0sEt|_HBFmCU z#dOPQGS{OO4>J%1&tzoRaIzEm)wDLoo+e}dMz zDjHisrY}6w?{+w7OWBBiDg8KorFIZ%kdf&9hHJO#^a0DEq&%x5FUK#P5c0 Date: Thu, 29 Oct 2026 05:36:16 -0400 Subject: [PATCH 03/22] Preserve calculated values across padded protected return boundaries ```python def select(value, make, fallback, default): if value: try: return {"value": make(value)} except Exception: pass return fallback.read().result or default.result ``` When padding separates RETURN_VALUE from its protected value construction, pycdc replaces the calculated return with a bare return. This discards both the value and its evaluation, including calls and exceptions. Calls, attributes, tuples, and dictionaries can all reach this boundary. Use the existing expression stack when it contains exactly one expression and the already bounded return-only tail contains RETURN_VALUE. Consume that expression once instead of starting an empty region walk. Leave a real bare return and its preceding side effects unchanged. Apply the check to the Python 3.11+ exception-table path. The affected padded shape also occurs in 3.12 and 3.13, so restricting the check to 3.14 leaves valid earlier inputs broken. Python 3.11 remains a compatibility control, and the current 3.15 encoding follows the same bounded rule. --- src/decompiler.rs | 22 ++++- .../behavior/cases/b_carried_value_returns.py | 87 ++++++++++++++++++ .../corpus/3.11.14/carried_value_returns_3.py | 47 ++++++++++ .../3.11.14/carried_value_returns_3.pyc | Bin 0 -> 1637 bytes .../corpus/3.12.3/carried_value_returns_3.py | 47 ++++++++++ .../corpus/3.12.3/carried_value_returns_3.pyc | Bin 0 -> 1626 bytes .../corpus/3.13.7/carried_value_returns_3.py | 47 ++++++++++ .../corpus/3.13.7/carried_value_returns_3.pyc | Bin 0 -> 1744 bytes .../corpus/3.14.2/carried_value_returns_3.py | 47 ++++++++++ .../corpus/3.14.2/carried_value_returns_3.pyc | Bin 0 -> 1790 bytes .../corpus/3.15.2/carried_value_returns_3.py | 47 ++++++++++ .../corpus/3.15.2/carried_value_returns_3.pyc | Bin 0 -> 1801 bytes tests/data/carried_value_returns.py | 47 ++++++++++ tests/integration.rs | 20 ++++ tests/pyc/carried_value_returns_1.3.11.pyc | Bin 0 -> 1593 bytes tests/pyc/carried_value_returns_1.3.12.pyc | Bin 0 -> 1590 bytes tests/pyc/carried_value_returns_1.3.13.pyc | Bin 0 -> 1708 bytes tests/pyc/carried_value_returns_1.3.14.pyc | Bin 0 -> 1746 bytes tests/pyc/carried_value_returns_1.3.15.pyc | Bin 0 -> 1757 bytes tests/pyc/carried_value_returns_3.3.11.pyc | Bin 0 -> 1637 bytes tests/pyc/carried_value_returns_3.3.12.pyc | Bin 0 -> 1626 bytes tests/pyc/carried_value_returns_3.3.13.pyc | Bin 0 -> 1744 bytes tests/pyc/carried_value_returns_3.3.14.pyc | Bin 0 -> 1790 bytes tests/pyc/carried_value_returns_3.3.15.pyc | Bin 0 -> 1801 bytes tests/pyc/carried_value_returns_9.3.11.pyc | Bin 0 -> 1769 bytes tests/pyc/carried_value_returns_9.3.12.pyc | Bin 0 -> 1740 bytes tests/pyc/carried_value_returns_9.3.13.pyc | Bin 0 -> 1852 bytes tests/pyc/carried_value_returns_9.3.14.pyc | Bin 0 -> 1922 bytes tests/pyc/carried_value_returns_9.3.15.pyc | Bin 0 -> 1933 bytes tools/compile_carried_value_returns.py | 35 +++++++ 30 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 tests/behavior/cases/b_carried_value_returns.py create mode 100644 tests/corpus/3.11.14/carried_value_returns_3.py create mode 100644 tests/corpus/3.11.14/carried_value_returns_3.pyc create mode 100644 tests/corpus/3.12.3/carried_value_returns_3.py create mode 100644 tests/corpus/3.12.3/carried_value_returns_3.pyc create mode 100644 tests/corpus/3.13.7/carried_value_returns_3.py create mode 100644 tests/corpus/3.13.7/carried_value_returns_3.pyc create mode 100644 tests/corpus/3.14.2/carried_value_returns_3.py create mode 100644 tests/corpus/3.14.2/carried_value_returns_3.pyc create mode 100644 tests/corpus/3.15.2/carried_value_returns_3.py create mode 100644 tests/corpus/3.15.2/carried_value_returns_3.pyc create mode 100644 tests/data/carried_value_returns.py create mode 100644 tests/pyc/carried_value_returns_1.3.11.pyc create mode 100644 tests/pyc/carried_value_returns_1.3.12.pyc create mode 100644 tests/pyc/carried_value_returns_1.3.13.pyc create mode 100644 tests/pyc/carried_value_returns_1.3.14.pyc create mode 100644 tests/pyc/carried_value_returns_1.3.15.pyc create mode 100644 tests/pyc/carried_value_returns_3.3.11.pyc create mode 100644 tests/pyc/carried_value_returns_3.3.12.pyc create mode 100644 tests/pyc/carried_value_returns_3.3.13.pyc create mode 100644 tests/pyc/carried_value_returns_3.3.14.pyc create mode 100644 tests/pyc/carried_value_returns_3.3.15.pyc create mode 100644 tests/pyc/carried_value_returns_9.3.11.pyc create mode 100644 tests/pyc/carried_value_returns_9.3.12.pyc create mode 100644 tests/pyc/carried_value_returns_9.3.13.pyc create mode 100644 tests/pyc/carried_value_returns_9.3.14.pyc create mode 100644 tests/pyc/carried_value_returns_9.3.15.pyc create mode 100644 tools/compile_carried_value_returns.py diff --git a/src/decompiler.rs b/src/decompiler.rs index b61145ba..28fe63ba 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -6061,7 +6061,27 @@ impl<'a> Ctx<'a> { }); if !has_with { if bare_tail_span { - body.push(Stmt::Return(None)); + // A padded RETURN_VALUE can consume the + // value that the protected body already + // calculated. The handler parser preserves + // that live stack; a fresh region walk has + // no value and used to fabricate a bare + // return instead. Keep only this exact + // single-expression stack and return tail. + let carried_return = self.version.at_least(3, 11) + && self.stack.len() == 1 + && matches!(self.stack.last(), Some(Sv::E(_))) + && self.instrs.iter().any(|x| { + x.offset >= span_start + && x.offset < else_end + && x.op == Op::RETURN_VALUE + }); + let value = if carried_return { + Some(self.pop_expr()) + } else { + None + }; + body.push(Stmt::Return(value)); } else { // 3.14 narrows protected ranges at // value-construction boundaries: the diff --git a/tests/behavior/cases/b_carried_value_returns.py b/tests/behavior/cases/b_carried_value_returns.py new file mode 100644 index 00000000..13f67652 --- /dev/null +++ b/tests/behavior/cases/b_carried_value_returns.py @@ -0,0 +1,87 @@ +# MIN_VERSION: 3.11 +from types import SimpleNamespace + +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result + +for function in (call_return, attribute_return, tuple_return, mapping_return, bare_return): + for truth in (False, True): + for result in (None, 0, "success"): + for failure in ("", "truth", "attribute", "make", "base", "fallback"): + events = [] + + class Value: + def __bool__(self): + events.append("truth") + if failure == "truth": + raise ValueError("truth failed") + return truth + + class Result: + @property + def result(self): + events.append("attribute") + if failure == "attribute": + raise AttributeError("attribute failed") + return result + + class Fallback: + def read(self): + events.append("fallback") + if failure == "fallback": + raise RuntimeError("fallback failed") + return SimpleNamespace(result="fallback-result") + + def make(value): + events.append("make") + if failure == "make": + raise KeyError("make failed") + if failure == "base": + raise KeyboardInterrupt("owned interrupt") + return Result() if function is attribute_return else result + + try: + outcome = function(Value(), make, Fallback(), SimpleNamespace(result="default")) + print(function.__name__, truth, result, failure, outcome, events) + except BaseException as error: + print(function.__name__, truth, result, failure, type(error).__name__, str(error), events) diff --git a/tests/corpus/3.11.14/carried_value_returns_3.py b/tests/corpus/3.11.14/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.11.14/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.11.14/carried_value_returns_3.pyc b/tests/corpus/3.11.14/carried_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f16c355a07971fff54855a3d34db40cbdacba53 GIT binary patch literal 1637 zcmchXzi-n(6vyBBoL^4TmWp2kQiz&JWT6Wqf{8Utr*^3>xeKIqQtNEfQf2JKz@0uz`t%#d zwz11-*1%v45{_UCBtvD=L3JcY<u0Y9(Zn*D8$tsO2WPm%@)#;ts^FN{T(<`yJ8UO$t)=1ZIS^uKU=|*CG{0 z;kp!&Snmq0`$}#b@3vM^tcT&l;8jxY;sloix{6k^&*&Ay3d)hU3U9p{f6r<>o8B>I zjREO3vgz&6T&ATfM2?mLKqkkz`>7j#=V)&jj<%ORMbo#Pa9zNQ&XMF%TCOfqTq5yE z$|#nZrZp!kB8qgs6Gtiqa|4G7j$Wc{17`USjJ*c`z#3P8d4s72Og$TnWIZQ+*=s5- zr~TdFpl`eoiAod)zNK&rZMX41N1<{Pj=efRVf6(lNS3v|`s~^%T-3nA6qW`e2>Qe4 zX*JwM2DN|W@DB4$H8iuUf#CsmC5HkXG}3bV&j*gvP6{u?Q2I*hg_)E)LfgW)hl_jg zE08i3Mw5SJP2Qg5%*mo@7ilUYO$0T$CKttI_T72$@nBEJzIvjGE}f;bEtDx|j7^Jq V_Gk)LR&8X){?aXw_fIi}`~xCt^0fc} literal 0 HcmV?d00001 diff --git a/tests/corpus/3.12.3/carried_value_returns_3.py b/tests/corpus/3.12.3/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.12.3/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.12.3/carried_value_returns_3.pyc b/tests/corpus/3.12.3/carried_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5fb5bfb4ce2017749cad3c56e3f0469fc01f0db GIT binary patch literal 1626 zcmc(fzi-n(6vyBBoY>B<{*b5`5FpAxmUgKSsw$)mh%E~XWvMQ?3nX>Y=-H-;lrd}- z0|WgJ5K$#07K9KI%FKV@m5B{CTPNN*u|?cSgoR$@Z|?5hd$vFR-hD3?a~uG^efaeF z)zgpfe^5VZY}T9Hq0T8t7{W7>XUf^SndAcKzAim^6J-wo3l11C zU;{XtRdzuCp1{Cp>6NK=HXFD8tU8o<=&p@~Y!FpJ6yT)%9FnU}&1RA{!vp$hGWaNU}yABL7x4M8hB{ZzN9 zU2OWw^Sx~;e6d{@h)pFAqTB;iet;cstJ@Frbt*8Mz#^sU^of2xZ-r!N?ZOwQlI7zJ zBV7#g9W6Ws0=ba}+0unlEyh4_GpP^_6S$W{kicCKi>ne`BMx7Ni~4hpI9l|F`tFz| zDVu2PzN00lB~Oxo!~%`NRpI;CtOdS`X|2(;CWa-wT91Zn6%)p$Q(5KX6-K(4*IQZ| z@hYWxN!BppH@On?8P%T=8|{LnuoV2D>n2F9QQ8JwQzVgroIUvJRBS%B80p`ENH)J0 zo9v2#Ouex*kWyQ8yUot?*d^Bq$E|-y@|sd2kL1!GVbK4N2UDLV2a{0^trwyrT_tg8 z9Qm5S#2z>3rS8qMuphu@r(*IkXQb6pCOU*r?UT9`BZY`1rB&H0pGBK1(dS;9)Snw z8!!;5;t5#%8eW;$Q2hdY=h#+~dI&7#hJVg?-}l}5=l@;eTFsyUaCrUwJ=Nbn|B3y) zk+c^dVdn}gH~;|_K_!-gs#p}2S}LkKEp@fAn&$$@zAPPoOR|UXBAsM=04*4jBN)O- zx<(5}TJQ{p%0T99wUhLDuwFkMJ0n?L8|!vpNQ!jn7f zV2hsm-1EI-oB8Z`%!HV6`#i}#7u*lT!h7j{j;doUkPe6%rpokK?$B$Xe9=u6Le(%E zG~6es#ejcc=oP}uK(L3m0S{!+t(HMS=%uYGe9WQWo`M4C1yNajgsx=MMbL%(IBtQ+QI8XgeT zVv5%ozd;cTY}?c#JtcdqJGZLR|CJDvp4kc3o*%eQ4(b7>p;6XBRZI;+%`orNut`w= z29!lMPxLGDAAwpqX=hONIdk3FVw!pC5n^ip3#zY}wBafA@wTTrFukK|Z%XoOPw7Yc z2ez;sZi}5URYy$Zc6x%ZDlee5b5}MM2o=M;N5eWn?JrBoB`4&m6w-e}k`*#_xk_A} m>hkIQ(heN{T1e{hFCuQC{HBDEo3ciFH_Z)+n>y(w3;zM?F4Ls| literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/carried_value_returns_3.py b/tests/corpus/3.14.2/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.14.2/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.14.2/carried_value_returns_3.pyc b/tests/corpus/3.14.2/carried_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44fa1cc615a980b00eb6a38574608907af2d3459 GIT binary patch literal 1790 zcmd6ozi-n(6vyBB9NUT8kfyYSCTWU-1Q8M$$byP0d0<0PZH-xMzD3} zKgq(t$Y%Zw4=hYfpoTwycaCi}!4iRmp5(9Z$9wP2pL=(HRNgF604P1xzIVPOxOb-OoB>G2~{#FDm7(P*(|i6t&HT%8$i@W@cD<>e*h1XL2DDB4mB8% zdoX}*GEWVAYS4m#)E8bWrJJ<-*Jnujjz}8$ERNiS0nM{ZXE=nHS#$*~j97;zG~mUt z14pz04w;eOj7G)hZ%6jjcP=K8%xz{x3b(z$_1kh(c*ESlj^r^rwb zPZ?TxVwHzjqLy!azSpss&pIQzL)1QGu|c z8wWIO5Y&8@AJKk^<*huHuhb_}`eipGZNJ6XME0#kY{L(xZboYd-FET0MyqIO5Nf)y zOT!(4`ggRl4<)iyi1II+M5;PtB{LR%SCE(!J7-$ s*&;<3r(SzDzOVwljA6vyBBoH(i5Kq}Ibgruzyhzw9sb%}&3d0;{`HBu&2RhRfeqB?1Gwh?UE z82AvKvoRnh7E62zuS|?Id;s1#wiSXU0t-Fq{e8ZB@9zA&cjp&{jWr4YpDv%jJ8fMy zzhXTpB&yBsV`YM7LJ1r&2`VuaRK=vI)KpQ`6KxL~nXVc|$%m}V;b$LECTMNAhbl{V z1E2zBXpnw~t6WsO0$ayq=8|YmeAZif!2_d|h zT%%J&d9N87_?m8%sDF!~ri1>1t`oEc(A#OCTj;Y~Vl)Upv)l)e!2i7qQsJlg#WNs$ zCGJj#FXZ-5snGBtRcH+HCBC#dN(KcfExZGq;EG@7T{Vyf(OLU1wzS7NsAdSg4`Rl=|>v;|&y zBM*LFH@2z2MNt2im$`J%ACl|EYo1pt^C}G4csv-riZkps(%ShKVxKW+g7&8jtHFu( z--zLvn_rG*vTMmDsT>DdpN%Y=TViE=dWVT5+wS1!KR{c^wp^5LRq9uWQKi#Ncq0mV zbcM`LA%P1SIi1JNkACjS@WS$Jek9~($bZVLj`E!nLauUIa(K1*i~QKs$YD6|Pn+=6 AasU7T literal 0 HcmV?d00001 diff --git a/tests/data/carried_value_returns.py b/tests/data/carried_value_returns.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/data/carried_value_returns.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/integration.rs b/tests/integration.rs index 179ff120..9cef5b1c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -72,6 +72,26 @@ fn padded_none_return_does_not_absorb_a_following_branch() { } } +#[test] +fn padded_return_consumes_the_existing_calculated_value_once() { + for version in ["3.11", "3.12", "3.13", "3.14", "3.15"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture( + &format!("carried_value_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + for value in ["make(value)", "make(value).result", "make(value), 7", + "{'value': make(value)}"] { + assert!(src.contains(&format!("try:\n return {value}\n")), + "Python {version}, padding {padding}: lost protected value {value}:\n{src}"); + } + assert_eq!(src.matches("make(value)").count(), 5, "{version}: {src}"); + assert_eq!(src.matches("return fallback.read().result or default.result").count(), 5, + "{version}: {src}"); + assert_contains(&src, &["try:\n make(value)\n", "return 'handled'"], version); + } + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/carried_value_returns_1.3.11.pyc b/tests/pyc/carried_value_returns_1.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81e07361d773695692cc7bf5a118d21b6d6414e1 GIT binary patch literal 1593 zcmcgsJ#Q015S_g}f1T|lL<*w7GImnnhBTB2Dq1(Gk|tVv*Wl#roc8WWK(0_kfhZ6X zN`p{nf=>Azh?4$VDmqKCL3s+fP`b%0m%@VbPyfM5xH~`U76d0R=(@nXaKS)`Q*8YAAzowz=07P!5QX0 zpXG-VY{5E2bDJWb18OfWsrRSc!@3H`mWf^h7uz-zC@-?19cR_zsf zodBxV%vNoWa+x*N&vFOI)Nk(nEE0az&%QAF*&TgBQ!Nv&3;57Ef;>XY)kU&P1Rg;d z!D^+6Yq}z$NcTH&q-KI#!($RfFOjzqWO)tVdQJX;HUA*U3v@Mt)UySVtmj-&D>?0N zMh2?sLJCx(IPfj|H4M9szd81mRe0mo`FmDhfW2f{*Qn2~jQwTJ7iPaS5JAu%KAKhk z26C_eBmbA^Z>rzOu0F#7mB~LxgG3r_{^Kxl+G*jj7)oDBy|51Dj?lIVZX%)EKZ{VN zCTQ?e*5K_VXQqaxo1&>>!LeV{BZ^v%6!k Suxc%H_LpseVt)tIwBG?+{qcPO literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_1.3.12.pyc b/tests/pyc/carried_value_returns_1.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85a51d2929d66b205ac84557f5e9c3f867eb252b GIT binary patch literal 1590 zcmc(fzi-n(6vyB5IX|2qg;szV5FpAx7P>@;0U>2TY*|<+OLfUzAgPmD&o)h@jA22D zwHq^0B_x)L4Q1v(@XEx7nynM>9LFMQ5MiMw`RjLg?>(PBe)qnW%LM@dUq78Zee?YD z$M0;X0~2d{hm9jpFoGALkQJ51YAL}gl+9`nGL>x8PGbR#ZBkF)#?eEdf&&%|aR|pr zW(S_Z(CQekrE#3hJHKa4*81p|5Qbvap2@fkL7MF@kU-dlchy7ai(NRxG1kW-a-Q^? z+6&qTy~rjlRg@*|hi*`@qs(jRhB~sla$iUJmUP{QZ0<)nrCX9cd~>4PJkGWQ?Fasj zl7ZZ5N}{$_dvWX@=^&(@f8QHK#U>|67O2djCVXnn=U-wOZ0@G!m$prDdL`P|t5c3U$R^0@a#torg;17mfwx_(kT+lx3;dlsC3xq+q1T zk^-a>&!WpR2&ml%13jm-$;*ZmZ5VGe8*SDACg9XcVp2rb)tuh5Ys{&%%t>L(x{zYC z>OW#8=C7}XqZEXmo1(bNVb}PXp@<8{c?e&enk^;*S^o|~;o_ZZ;{}13)xz2!HoDUD z+P#dPseo zi5m@>n?t68$gm?5IG$BfeDluokGcmcboFC0Tun=!-embH5b-ph#p>zSf=st^SdA<3 E7o~FJi2wiq literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_1.3.13.pyc b/tests/pyc/carried_value_returns_1.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b248c87d6161dc4e8c915aaca871214a153012ff GIT binary patch literal 1708 zcmd6n&2G~`5XWb|PMkO&v=tPDP?EL?kqhOJUJym{0f|!%v2x;2Ht{Azb<${TSMUft zK<_ulSoG?oG6dd3tYdcN4ZPk1~Sse;#@|BfpFcP=$}W1BL{*!Jhi(9 zjp)dip6?wy!WYMVfjE-R**x|Pr61tLdoleSmHLz*nZOE#s_XVpsqnm{x^{ zWk#9}_6NDHfxUklY)2*C7zym$#xy;quy4PD4A>c732bfoDhoEM-#p`J&<)itFi+Cd zv7qdt5-JhE5=jac=?_D+BYYo6y}*}CdWx4Kh*jl1r?+dF5Spg-kcaz>G@IU6@?WDz z3v8C9&05->m!{VKH7BKNWGhHzKbX2Hpa>PuB{dqNPEonbtiXRvGEv0J7Qo$$iQG z13-()*&0xFET+@ZWVo!MM}(*TFNm%wXsw1aT3Z8#chK5~ENyLCJ2F0siQ`HKw?t48p&_Y U7`rLtS?gwRP2xR+wdM=|0i4g!`v3p{ literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_1.3.14.pyc b/tests/pyc/carried_value_returns_1.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f555cf555f5a46c67d3901d8cc8574edd3da17c7 GIT binary patch literal 1746 zcmd6o&2G~`5XWb|PVB^KXwvf8mJ|gEA|!Gk7t{*L2W}{ee8>q=ZDO}rb<${UBe?a< zgCK=oIni^z>MQqO|V4ZfVxut-jA8d=I8%z9#uEW901Bs_3!O3^{ubi z&l*d6v-{XN2McaO02V`KmV&BS9F>l)?lYY+*G@-BbC}X8{(sA!{^>prlEvlYuP}K(Xd0re<--nl3X2^eEf?VV|aVc_Tv6PQo(hHw* zso_J?PGk|KrAnRpC6Y1;dImDQvVx;{Qv`u@2VvkW7!oz&D615mFszvsh$@D8hex*= zYCgk{7{A0YG1IC=UujR2t6|e`2`+NmTBIsLIPo${=nuGopG%ZVrV5d2m~|f280z0q zvR00An_Ue`a}TXesX7vqi90@7xak%$YWxeQPnh&)H#P7|{qLOcaLq40D|tuR#iw** z?X?)&o@2|kff)lKGgs~6kAApJOHW<3&7&=5Zu9Q6kX&<;PNpOqN@6T2bvn1>{ABBg kqcc17oM)2w(;vj_F3Jzi7`xDmtaDLclAm>rbrL)O0=m%Aj{pDw literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_1.3.15.pyc b/tests/pyc/carried_value_returns_1.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2d2df740de3b23f2362e0c65a0c6972a6e9c239 GIT binary patch literal 1757 zcmd6oF>ljA6vyBB>^P~LstE+qhPG4)LLL}FlKV)57T%EU;+2jD%&u|lv!U_d?T{e3yVd%pbcfA-6@;tB_V&zCRWoir}% zU$LJymh>hMurtE2paLFP43$|5s$y|eZmFp1vAzp6t*s`r=t6xeKly+%!f3*MRBCMl zU=uc=$#!sbKY7*$>_Ss%(42*~pZspj*H6akY*w{Xq^euc}brR!m~GRXXy7X29YWqVMjUXc!BHJ)JS_R+`x&{zBqTHd`Gx$OSI1; z-F7+xGdx=1U2M@k-|>8J&lbMeYYQoRj(r->J(Z3h$bt8G_%<@ySRfgYxcnH}gx2Vl zP~Ph%hmad)iHGY9H5>33OdVm&0j^{LZeY%0OQ36u;{Yl2-)})C^o+a&x-z?!4PDZf zkGa(GB55Tuhf-5zp85rn6%up}S`CE&%T??|>%#YCuNC;tj3RL(l5!1YO0i@XAuJkZ zm4}-QHJjo`%pYOQQH%|>Xwwt@iE=%x`Yi@VK$c>)>IXwN<%AA_TNrbk3Z@33W|%j4 zSZ1hy%gI_e(2v;l;57Bo(wx@%VmRy#&Ss9fjg)r&h14gUG{X3kqqg8!|L>&mh)rL9 zR=i8;3s599(2q9n+Y tW{#)xrpG&f*uSs?*Lf;w1L!+3*+%)!8Dm%Z96Puw{~|xiIy;D;`x9{V(V_qV literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_3.3.11.pyc b/tests/pyc/carried_value_returns_3.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f16c355a07971fff54855a3d34db40cbdacba53 GIT binary patch literal 1637 zcmchXzi-n(6vyBBoL^4TmWp2kQiz&JWT6Wqf{8Utr*^3>xeKIqQtNEfQf2JKz@0uz`t%#d zwz11-*1%v45{_UCBtvD=L3JcY<u0Y9(Zn*D8$tsO2WPm%@)#;ts^FN{T(<`yJ8UO$t)=1ZIS^uKU=|*CG{0 z;kp!&Snmq0`$}#b@3vM^tcT&l;8jxY;sloix{6k^&*&Ay3d)hU3U9p{f6r<>o8B>I zjREO3vgz&6T&ATfM2?mLKqkkz`>7j#=V)&jj<%ORMbo#Pa9zNQ&XMF%TCOfqTq5yE z$|#nZrZp!kB8qgs6Gtiqa|4G7j$Wc{17`USjJ*c`z#3P8d4s72Og$TnWIZQ+*=s5- zr~TdFpl`eoiAod)zNK&rZMX41N1<{Pj=efRVf6(lNS3v|`s~^%T-3nA6qW`e2>Qe4 zX*JwM2DN|W@DB4$H8iuUf#CsmC5HkXG}3bV&j*gvP6{u?Q2I*hg_)E)LfgW)hl_jg zE08i3Mw5SJP2Qg5%*mo@7ilUYO$0T$CKttI_T72$@nBEJzIvjGE}f;bEtDx|j7^Jq V_Gk)LR&8X){?aXw_fIi}`~xCt^0fc} literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_3.3.12.pyc b/tests/pyc/carried_value_returns_3.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5fb5bfb4ce2017749cad3c56e3f0469fc01f0db GIT binary patch literal 1626 zcmc(fzi-n(6vyBBoY>B<{*b5`5FpAxmUgKSsw$)mh%E~XWvMQ?3nX>Y=-H-;lrd}- z0|WgJ5K$#07K9KI%FKV@m5B{CTPNN*u|?cSgoR$@Z|?5hd$vFR-hD3?a~uG^efaeF z)zgpfe^5VZY}T9Hq0T8t7{W7>XUf^SndAcKzAim^6J-wo3l11C zU;{XtRdzuCp1{Cp>6NK=HXFD8tU8o<=&p@~Y!FpJ6yT)%9FnU}&1RA{!vp$hGWaNU}yABL7x4M8hB{ZzN9 zU2OWw^Sx~;e6d{@h)pFAqTB;iet;cstJ@Frbt*8Mz#^sU^of2xZ-r!N?ZOwQlI7zJ zBV7#g9W6Ws0=ba}+0unlEyh4_GpP^_6S$W{kicCKi>ne`BMx7Ni~4hpI9l|F`tFz| zDVu2PzN00lB~Oxo!~%`NRpI;CtOdS`X|2(;CWa-wT91Zn6%)p$Q(5KX6-K(4*IQZ| z@hYWxN!BppH@On?8P%T=8|{LnuoV2D>n2F9QQ8JwQzVgroIUvJRBS%B80p`ENH)J0 zo9v2#Ouex*kWyQ8yUot?*d^Bq$E|-y@|sd2kL1!GVbK4N2UDLV2a{0^trwyrT_tg8 z9Qm5S#2z>3rS8qMuphu@r(*IkXQb6pCOU*r?UT9`BZY`1rB&H0pGBK1(dS;9)Snw z8!!;5;t5#%8eW;$Q2hdY=h#+~dI&7#hJVg?-}l}5=l@;eTFsyUaCrUwJ=Nbn|B3y) zk+c^dVdn}gH~;|_K_!-gs#p}2S}LkKEp@fAn&$$@zAPPoOR|UXBAsM=04*4jBN)O- zx<(5}TJQ{p%0T99wUhLDuwFkMJ0n?L8|!vpNQ!jn7f zV2hsm-1EI-oB8Z`%!HV6`#i}#7u*lT!h7j{j;doUkPe6%rpokK?$B$Xe9=u6Le(%E zG~6es#ejcc=oP}uK(L3m0S{!+t(HMS=%uYGe9WQWo`M4C1yNajgsx=MMbL%(IBtQ+QI8XgeT zVv5%ozd;cTY}?c#JtcdqJGZLR|CJDvp4kc3o*%eQ4(b7>p;6XBRZI;+%`orNut`w= z29!lMPxLGDAAwpqX=hONIdk3FVw!pC5n^ip3#zY}wBafA@wTTrFukK|Z%XoOPw7Yc z2ez;sZi}5URYy$Zc6x%ZDlee5b5}MM2o=M;N5eWn?JrBoB`4&m6w-e}k`*#_xk_A} m>hkIQ(heN{T1e{hFCuQC{HBDEo3ciFH_Z)+n>y(w3;zM?F4Ls| literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_3.3.14.pyc b/tests/pyc/carried_value_returns_3.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44fa1cc615a980b00eb6a38574608907af2d3459 GIT binary patch literal 1790 zcmd6ozi-n(6vyBB9NUT8kfyYSCTWU-1Q8M$$byP0d0<0PZH-xMzD3} zKgq(t$Y%Zw4=hYfpoTwycaCi}!4iRmp5(9Z$9wP2pL=(HRNgF604P1xzIVPOxOb-OoB>G2~{#FDm7(P*(|i6t&HT%8$i@W@cD<>e*h1XL2DDB4mB8% zdoX}*GEWVAYS4m#)E8bWrJJ<-*Jnujjz}8$ERNiS0nM{ZXE=nHS#$*~j97;zG~mUt z14pz04w;eOj7G)hZ%6jjcP=K8%xz{x3b(z$_1kh(c*ESlj^r^rwb zPZ?TxVwHzjqLy!azSpss&pIQzL)1QGu|c z8wWIO5Y&8@AJKk^<*huHuhb_}`eipGZNJ6XME0#kY{L(xZboYd-FET0MyqIO5Nf)y zOT!(4`ggRl4<)iyi1II+M5;PtB{LR%SCE(!J7-$ s*&;<3r(SzDzOVwljA6vyBBoH(i5Kq}Ibgruzyhzw9sb%}&3d0;{`HBu&2RhRfeqB?1Gwh?UE z82AvKvoRnh7E62zuS|?Id;s1#wiSXU0t-Fq{e8ZB@9zA&cjp&{jWr4YpDv%jJ8fMy zzhXTpB&yBsV`YM7LJ1r&2`VuaRK=vI)KpQ`6KxL~nXVc|$%m}V;b$LECTMNAhbl{V z1E2zBXpnw~t6WsO0$ayq=8|YmeAZif!2_d|h zT%%J&d9N87_?m8%sDF!~ri1>1t`oEc(A#OCTj;Y~Vl)Upv)l)e!2i7qQsJlg#WNs$ zCGJj#FXZ-5snGBtRcH+HCBC#dN(KcfExZGq;EG@7T{Vyf(OLU1wzS7NsAdSg4`Rl=|>v;|&y zBM*LFH@2z2MNt2im$`J%ACl|EYo1pt^C}G4csv-riZkps(%ShKVxKW+g7&8jtHFu( z--zLvn_rG*vTMmDsT>DdpN%Y=TViE=dWVT5+wS1!KR{c^wp^5LRq9uWQKi#Ncq0mV zbcM`LA%P1SIi1JNkACjS@WS$Jek9~($bZVLj`E!nLauUIa(K1*i~QKs$YD6|Pn+=6 AasU7T literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_9.3.11.pyc b/tests/pyc/carried_value_returns_9.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14952439130782d709e7353c47758cab7e67fab9 GIT binary patch literal 1769 zcmd6nzi-n(6vyBBoL^4T0urb~AduQ9O$Mq+j0h&yES=h=y5ug9)=90iO$*A{|Dgy8 z2_Y0@m4TTZF|vDQVvCfm6YpJ{BTys4fZ(Eh^X~lKv;E2U?rpW|aRB&OdDdC}^!dRT zjO}3O(R2-iV~}tJ+aMV#lMbpQIVzVfsw;Dw(8zaPTMa-lHJ`pV^&`-=5;!noBN$`v zlUaW#UHELmDnxUw)@0UJ55Z2ep!jB#>e7j~Ul801I%`z-5@32P@j zEj>K!svzp`4^vkwA=A88VeCf@H_bf}eymb=Aof&R>0jV~#sqRuQGqTUqDc2UainIzF5@ym(@T_XfGyvImtK=6toaLIWDNtXo()X0o|E<9 z|GVl3o>+)bC5i*z(z%Ld*YG<>r*a)$cy&Hz^#$limbFd$?7yLN=9z`*EDb~u z^oNgT?pZ-LxBigN2h2C_X=S^I;Q_f2pDG<>GLz+BJlvdiT6ipm(pOS1%&puJ+M4PH z0$BeMTA57M;_q3Dx09SX#Wd|qO$O9NSyNSVW<;}>PmA{k`!e>`eNF!Wo#E4MlnG~y YO^SJTcLG+f*vPE?W?G=yi()pxH?*PlT>t<8 literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_9.3.12.pyc b/tests/pyc/carried_value_returns_9.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26acf48a285432baf448bbdbd7390eb76edf1bb4 GIT binary patch literal 1740 zcmd6ozi-n(6vyBB9LLU&l7xbghyYOrve2btRmlTl%hH9iRF|9sNu4x$wrL_|3>$kT z{sKa!N-T^;p7{^FGO?j%>%==Zv0B#>5lcDCU%&Ic_k8~N-T6nkT;Krk{q)oT3Yyu~X z%oe1NU6^P+6_siyi+=Cw6BhSI%quaN@T{?PehYSC1NQ#b#R|b{)AN$L^!!2RQIv498KiO$in^uuP#Ue6Hr_RY|6K+knup>>3ZN zj5HhZ8Mz(%Ys7Kb166KCfIKdL4rMFEb9q`U0b@|#E$Bt&p77kf@&sc zkTi5Om2D|0C@GR8h_OVys3LqHyRE=?mK+s1#S+()cg(SBS0JodcAbY08EH1hQ}T~F zuBSOl*3q)w>ekCuYyOIvsk&JSSIG~CZi4C_h25v~3M$JsAv7&}oriZB>EBUFc70uU z*?)@avYJ*#wbmEIVR!I$SyvAT)Z>4F^@)Pwx+5Qb;H1{)J1f|9C2DUdWu%BAUn+?qaBQK@85066;m&o zGRR^U9pmH{EU1D2i=YxqK~*e@N-Y&t9p;+(zMA*~$hq7)`i61~U3h|5Iamdt23>ND zYZr?cHC#!5PN1uFr2pEuSloA3V=dM$$ufq$1JHmj&9X|zk#{F>1aFc!8ZeC8fI75a z4&aj3VL)c2J)>da)rUTxdZU|3s0z;PFfX|4J6=-_bMKks^H3eLYaZ$Y<~TjpzYaB< z4;Wgw^0W;c;bV`xo_lUHkDd3K5F>71#lBa9d%l>sPp2QlVjnkHEQk`iO7}>f(A`7% zq?u(1%7*!f28RSS8~hJ6O@Tk$1mBUHu8k}}n2g+-8Qvzz*!&4Hl96GR6$_K0EKReL zA!J$1sL=2tzMAIS%si# zm}fLNC8*hSzoUJM?*1lSi?o!ie|WD-TWSA_Ny-}A3gV9EPn`td0lGcF?+Ux5-M$>81{WZkPu&*e9s%SuSpzLO#(#)~Jw_ tg>;?GYjxfc^ylLn+jsa2Au}LfM^O#sHzkDJ>3Pz+tE42VXrvVf{sRf;+oJ#g literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_9.3.14.pyc b/tests/pyc/carried_value_returns_9.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25b2ec82b02f2c0586abcabb2dc356b732644ab8 GIT binary patch literal 1922 zcmdUw&u`N(6vv;ZNzz=pdJfK5<|okPvp6qVWll6Ugm_Bs20jjVI zeX<9A=%jvjac23`gnq6kcUWniblkh{EuA|e3ooae(n}rsRAsm9NsgM(fH#?^0ywz4 zTwjCNFI_mIHE_w4bf+{fzWOlalfXS6$C}{Gi3@`Jp%=8YIRBn`A&<2YJL9o&!aQ%l zhG((v@Dqj>zC3XQTigt|ANXyD1*|<}Lb%*HPSzd^9)x1-Kbd@t%^_}(P6)hCf^;U& z=x?EX(ya?PECcsC%0W`Do7ZT{E8V4vylA$2$UgF*r~i> zNOB8Pbx4FvkSP^9{s?(8Q9x;F!jS!q1 zT00Q!nASdx_6X|VQM6Y|MVtH|6z4g(HpTLYO(yR6bdinskYW8_I9}qSKWAedOZtuD zh%er}=-SE{XN5S|jrDhI?0DP}YXde1Ok`PFMM#g9=^AO49@6N5SckMb%_n77$Vwn& yS_>HyA(u|)={>I`#`Dp+6MFoGknxg#XtVn$KPVyO$|#V|Rb@$jR&>%yUXNcB>fL_; literal 0 HcmV?d00001 diff --git a/tests/pyc/carried_value_returns_9.3.15.pyc b/tests/pyc/carried_value_returns_9.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..760729aa45b143744ee1d6636279f9febcd758b9 GIT binary patch literal 1933 zcmdT_!EVz)5S{fpaniIEZ4^W)X`5Cc0)&dHmq-Z75}XiCjg%9rs*UZCs7@NKZ3MU6 zIPeiY=Lfj^#(^)eI3RK2NW%v(lQ>oqED<=Bjplj1nc3NWZ!&qgy0XLp;N!)M_b0WB z>NlLngGIC99h~&h3@CsL218_qf~XiAksB(adaRY8oHErYiXJql?T4Qb`e=2yjYz}H z1KflSsIzTcJ&1B`;L7Z$1a+lGd(4f4=)E?bFIuazNtWgcWhp|PC;820qPG%k!K*k^ z8t66@zX{KuwxG&4p~VL5Xu!klv$v+*^V;X#P?ff@!nCwq-|@<7n0g}|-wxG|IJ3h{ zQ#ejTm}jA8*-e2SE}eJ>N4V$NuIKJs!V~+Zkg{!Cr=!?YX?woxx(|Er!kmd4L<b%wWO4gnnEZ-C zeY8JSWD$zX1Dz+~NQL literal 0 HcmV?d00001 diff --git a/tools/compile_carried_value_returns.py b/tools/compile_carried_value_returns.py new file mode 100644 index 00000000..1e9c3d5a --- /dev/null +++ b/tools/compile_carried_value_returns.py @@ -0,0 +1,35 @@ +"""Generate Carried-value return fixtures with CPython 3.11–3.15. + +Python 3.11 is a compatibility control for the protected-range boundary. +This reuses the existing relocation helper rather than changing return values. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +from compile_padded_value_returns import pad_return_values + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 15): + raise SystemExit("Use CPython 3.11 through 3.15") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/data/carried_value_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (1, 3, 9): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + payload = header + marshal.dumps(padded) + (root / f"tests/pyc/carried_value_returns_{count}.{version}.pyc").write_bytes(payload) + if count == 3: + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "carried_value_returns_3.py").write_bytes(source) + (corpus / "carried_value_returns_3.pyc").write_bytes(payload) From 8bffea1a25ae94f54401de654cc26652e3b533ae Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 05:40:05 -0400 Subject: [PATCH 04/22] Keep padded calculated returns inside their bounded try region ```python def parse_timestamp(value): if isinstance(value, (int, float)) and not isinstance(value, bool): try: return datetime.fromtimestamp(value, timezone.utc) except (OverflowError, OSError, ValueError): return if isinstance(value, str): try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return return parsed.astimezone(timezone.utc) ``` With padding before RETURN_VALUE, pycdc can emit an empty try and move fromtimestamp outside its handler. Values such as infinity then raise instead of taking the intended bare return. Scanning farther for a return can also pull the following string branch into the wrong scope. Recover the live expression only for an except-only region whose pending body and else are empty. Require exactly one expression on the stack, an unconsumed RETURN_VALUE before the handler, no incoming jump from outside the bounded tail, and no context-manager protocol. Record the return as consumed and skip only through that instruction. Use the Python 3.11+ exception-table path; the padded failure occurs on 3.12 through 3.15, while 3.11 remains compatible. Returns after the try and in its else suite keep their original exception scope, and timezone conversion after parsing remains outside the parsing handler. --- src/decompiler.rs | 44 +++++++++++++ tests/behavior/cases/b_bounded_try_returns.py | 59 ++++++++++++++++++ tests/corpus/3.11.14/bounded_try_returns_3.py | 36 +++++++++++ .../corpus/3.11.14/bounded_try_returns_3.pyc | Bin 0 -> 1751 bytes tests/corpus/3.12.3/bounded_try_returns_3.py | 36 +++++++++++ tests/corpus/3.12.3/bounded_try_returns_3.pyc | Bin 0 -> 1600 bytes tests/corpus/3.13.7/bounded_try_returns_3.py | 36 +++++++++++ tests/corpus/3.13.7/bounded_try_returns_3.pyc | Bin 0 -> 1644 bytes tests/corpus/3.14.2/bounded_try_returns_3.py | 36 +++++++++++ tests/corpus/3.14.2/bounded_try_returns_3.pyc | Bin 0 -> 1726 bytes tests/corpus/3.15.2/bounded_try_returns_3.py | 36 +++++++++++ tests/corpus/3.15.2/bounded_try_returns_3.pyc | Bin 0 -> 1724 bytes tests/data/bounded_try_returns.py | 36 +++++++++++ tests/integration.rs | 21 +++++++ tests/pyc/bounded_try_returns_0.3.11.pyc | Bin 0 -> 1691 bytes tests/pyc/bounded_try_returns_0.3.12.pyc | Bin 0 -> 1558 bytes tests/pyc/bounded_try_returns_0.3.13.pyc | Bin 0 -> 1602 bytes tests/pyc/bounded_try_returns_0.3.14.pyc | Bin 0 -> 1654 bytes tests/pyc/bounded_try_returns_0.3.15.pyc | Bin 0 -> 1652 bytes tests/pyc/bounded_try_returns_1.3.11.pyc | Bin 0 -> 1711 bytes tests/pyc/bounded_try_returns_1.3.12.pyc | Bin 0 -> 1572 bytes tests/pyc/bounded_try_returns_1.3.13.pyc | Bin 0 -> 1616 bytes tests/pyc/bounded_try_returns_1.3.14.pyc | Bin 0 -> 1678 bytes tests/pyc/bounded_try_returns_1.3.15.pyc | Bin 0 -> 1676 bytes tests/pyc/bounded_try_returns_3.3.11.pyc | Bin 0 -> 1751 bytes tests/pyc/bounded_try_returns_3.3.12.pyc | Bin 0 -> 1600 bytes tests/pyc/bounded_try_returns_3.3.13.pyc | Bin 0 -> 1644 bytes tests/pyc/bounded_try_returns_3.3.14.pyc | Bin 0 -> 1726 bytes tests/pyc/bounded_try_returns_3.3.15.pyc | Bin 0 -> 1724 bytes tools/compile_bounded_try_returns.py | 35 +++++++++++ 30 files changed, 375 insertions(+) create mode 100644 tests/behavior/cases/b_bounded_try_returns.py create mode 100644 tests/corpus/3.11.14/bounded_try_returns_3.py create mode 100644 tests/corpus/3.11.14/bounded_try_returns_3.pyc create mode 100644 tests/corpus/3.12.3/bounded_try_returns_3.py create mode 100644 tests/corpus/3.12.3/bounded_try_returns_3.pyc create mode 100644 tests/corpus/3.13.7/bounded_try_returns_3.py create mode 100644 tests/corpus/3.13.7/bounded_try_returns_3.pyc create mode 100644 tests/corpus/3.14.2/bounded_try_returns_3.py create mode 100644 tests/corpus/3.14.2/bounded_try_returns_3.pyc create mode 100644 tests/corpus/3.15.2/bounded_try_returns_3.py create mode 100644 tests/corpus/3.15.2/bounded_try_returns_3.pyc create mode 100644 tests/data/bounded_try_returns.py create mode 100644 tests/pyc/bounded_try_returns_0.3.11.pyc create mode 100644 tests/pyc/bounded_try_returns_0.3.12.pyc create mode 100644 tests/pyc/bounded_try_returns_0.3.13.pyc create mode 100644 tests/pyc/bounded_try_returns_0.3.14.pyc create mode 100644 tests/pyc/bounded_try_returns_0.3.15.pyc create mode 100644 tests/pyc/bounded_try_returns_1.3.11.pyc create mode 100644 tests/pyc/bounded_try_returns_1.3.12.pyc create mode 100644 tests/pyc/bounded_try_returns_1.3.13.pyc create mode 100644 tests/pyc/bounded_try_returns_1.3.14.pyc create mode 100644 tests/pyc/bounded_try_returns_1.3.15.pyc create mode 100644 tests/pyc/bounded_try_returns_3.3.11.pyc create mode 100644 tests/pyc/bounded_try_returns_3.3.12.pyc create mode 100644 tests/pyc/bounded_try_returns_3.3.13.pyc create mode 100644 tests/pyc/bounded_try_returns_3.3.14.pyc create mode 100644 tests/pyc/bounded_try_returns_3.3.15.pyc create mode 100644 tools/compile_bounded_try_returns.py diff --git a/src/decompiler.rs b/src/decompiler.rs index 28fe63ba..f33bd343 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -5095,6 +5095,50 @@ impl<'a> Ctx<'a> { } } let mut body = self.pending_try_body.pop().unwrap_or_default(); + // A narrowed except-only range can end on padding immediately before + // RETURN_VALUE, while its calculated value is still live. Preserve + // that exact carried return inside the try without scanning onward + // into a following sibling branch or an out-of-line handler. + if self.version.at_least(3, 11) + && self.code.name != "" + && tc.except_handler.is_some() + && tc.finally_handler.is_none() + && tc.region_end == tc.body_end + && body.is_empty() + && orelse.is_empty() + && self.stack.len() == 1 + && matches!(self.stack.last(), Some(Sv::E(_))) + { + let tail = self.instrs.iter() + .skip_while(|instruction| instruction.offset < tc.body_end) + .find(|instruction| !matches!(instruction.op, + Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .copied(); + if let Some(ret) = tail { + let before_handler = tc.except_handler.map_or(false, |handler| ret.offset < handler); + let entered_from_outside = self.instrs.iter().any(|instruction| { + (instruction.offset < tc.start || instruction.offset > ret.offset) + && instruction.target.map_or(false, |target| { + target >= tc.body_end && target <= ret.offset + }) + }); + let has_context_protocol = self.instrs.iter().any(|instruction| { + instruction.offset >= tc.start && instruction.offset <= tc.body_end + && matches!(instruction.op, Op::BEFORE_WITH | Op::BEFORE_ASYNC_WITH + | Op::WITH_EXCEPT_START | Op::LOAD_SPECIAL) + }); + if ret.op == Op::RETURN_VALUE && before_handler + && !entered_from_outside && !has_context_protocol + && !self.return_offsets.contains(&ret.offset) + { + body.push(Stmt::Return(Some(self.pop_expr()))); + self.return_offsets.insert(ret.offset); + if self.skip_until.map_or(true, |skip| skip < ret.end()) { + self.skip_until = Some(ret.end()); + } + } + } + } // 3.12+ with-wrapped try: the body's tail `return None` sits in // the gap between body_end and the chain head, AFTER the inline // with success-exit sequence (POP_EXCEPT; LOAD None ×3; CALL; diff --git a/tests/behavior/cases/b_bounded_try_returns.py b/tests/behavior/cases/b_bounded_try_returns.py new file mode 100644 index 00000000..11f3c2b5 --- /dev/null +++ b/tests/behavior/cases/b_bounded_try_returns.py @@ -0,0 +1,59 @@ +# MIN_VERSION: 3.11 + +# Regenerate with: python tools/compile_bounded_try_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") + + +for value in ( + None, True, False, 0, 123456789, float("inf"), float("nan"), "invalid", + "2026-10-12T12:00:00", "2026-10-12T12:00:00Z", + datetime(2026, 10, 12), datetime(2026, 10, 12, tzinfo=timezone.utc), +): + print(parse_timestamp(value)) + + +def read(where): + print("read", where) + if where == "outside": + raise ValueError("outside the protected body") + + +for function in (unprotected_return, else_return): + try: + function(read) + except ValueError as error: + print(function.__name__, str(error)) diff --git a/tests/corpus/3.11.14/bounded_try_returns_3.py b/tests/corpus/3.11.14/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.11.14/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.11.14/bounded_try_returns_3.pyc b/tests/corpus/3.11.14/bounded_try_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1bbf9f1592a02f78e904429e128a16c1502af0a GIT binary patch literal 1751 zcmbtVJ#5oJ6n^JRoYW;WZTS;LZAx3wKt&@}VnB!i!oboFm5`Ew;>H)rm1BqVrKMGn z5JF;O?SO=silVZ!mo99~w=%&LDO)8Z-Z^y=Ck;z4@$<9qeed3T_r3F%RLYT~_1r~9j0!65hA|YUD@1Uqz6eYD{C}UQ99kTjw1#4(BX_(DoGlh(8dY&Uw zW$snfJi4k>Wom7kah%_I!nwy~Vr{*x7|%?n#@agd1#jbQ-z$5(Vv4LLQ$5b z%<@&!WiltT0v-Fy*xlyxrdM+bMuS#bm;(38gd$zl>gS^J1Ktmaoo`3|D-V6E(NWJ;6!>YtgO) zpTh~&Y+Ut-e*wkY(TniGn4zD@%#hTcqt)=#{ueNI24cf8JZV0SHhdOEmhhW+>rG>n zIX(;HU%<)0xF`N8_-HKAL$X9y8}T+p6)A)54HD)G)oAA&l;$M)pyF9Ihdt!#E23)F pDxhdmLP%2wviK8j4gQ;Yio7IE=#iN?nQB5uq;)dWgh3IM`yYh%8IS+~ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.12.3/bounded_try_returns_3.py b/tests/corpus/3.12.3/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.12.3/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.12.3/bounded_try_returns_3.pyc b/tests/corpus/3.12.3/bounded_try_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e839f77cb8d8fc195ecef3b58b094920058557c GIT binary patch literal 1600 zcmcgs&1(}u6n}3w*?ef)5?T?fH8HI=2sX!};>m!g7AXkLrEIek*plqV*-2wc1rh%R z>D6nfe}TtpPhLIE)sr`A4nn>7W;e5Owf3So%f9)|ysvrh&6|&fLf!;`>i6|qbD!?L z`%E|<#u$xm5;y<{9Be}e8qmQ8A~?n?Xc&%3)O1orQ-gHX`l6JEhRiy)V8ZP%nN@Vp z_gE9h2GwNIuRM>A$jt$Wn70o?JBGtC;*a-XC#BSq<3)w`Gs!)*5?Q^slh~?96I3nJGu-MyM_6 z_!9oEYCBqr_Y?jWG=DOmon?oLuWjgTJjZ?fL#6PaR0{vTQUj=fxTK>Kmt%BAqn5b{ z+(g)f0rcUSxe0wFotXbScbHnMqO`=G>$QA|jXmpjt#-Fo8_vkQ8@OH|Y_G|lW+V=gnEVQ`aQG7Dh>B_?TF5|@S*E#pO z%&c!jApgW}_gI9@K=4S<4g8kRJGQ7AGQF!Lq}8=~z??v}SgCYc7GyYBp{})8w*8*x zFlS5f!4_wt$GzZAcOa)jjji|$e4bQQACKsVV3EjCuJUjd-s4ie2p?8w>(l1(6e?QG z)fdd;Ye+Ql45);J&%h%B2h@xPHKP+ZC`M~TUoU;3sTd?5jWdd2u1Sk>?mA3n{GL!? zhI^>}_yTJOOrLa#n4$2QCtVw+qf4i*Sy-?zG_JIUQFLJvPP6wrzSC>7 k2Yi`W%GpRI=c|c`-z>o86RUtv@uZZ-`O%Dp*Tc;G0B~sfdjJ3c literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/bounded_try_returns_3.py b/tests/corpus/3.13.7/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.13.7/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.13.7/bounded_try_returns_3.pyc b/tests/corpus/3.13.7/bounded_try_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da87932a91ad256b34ba9aaa729f86ded5e59293 GIT binary patch literal 1644 zcmcgsPfrs;6o1ogx6MkG5FiE&Y%35N6R8Fc8s*jtCXjSf5B5No?v!lO?$)=n7&!V7 zO!@&l_G@VP5_9#WCqoZ<^i6lCrC9M`;x>Krn|Xic{heX4Xc++T^FxUh<~pq}gvVbd zMZ+xueQ>~GuiyX>4j3{UoXiSf#xaN*PL^o)G*>adDy2b27CcvQ;di+#D0&eFyu}nR z{Y+Zb&BJeWO&^+LUIUu?V%SU3cB1Wa-LAiv`mJqD#c_9RV_u`(%x^L6!*uiuTK1--R{`>KlAZdZmYl3*isCOG zr#YSJ@dIt?=H2eujl0*hClTXAVyH$zH9OH9k@tlq z=v!suBP;)xAW+RBmWWgsmX#ORT)x9Q5!b$M5_2Rx)7Qg6E@?c~YNMi!fNa9P< sPTup<>!aNK0(TbZM*si- literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/bounded_try_returns_3.py b/tests/corpus/3.14.2/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.14.2/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.14.2/bounded_try_returns_3.pyc b/tests/corpus/3.14.2/bounded_try_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0c8297b178f77ed03dfbe203f706d07f0ff6b2f GIT binary patch literal 1726 zcmbtUPiqrF6o0duY(mo1Qf!qbZ4z7B#)1vBSg0aiBK4Anwd5edvdvCpA=!iY1Y<6)V4U~PJ@c5 zq{p61tEA^rP6qxmc4qw_jCbo_8sIt~1d*t_3McE~O zu0}RqRrK=TRW-XEB6}nQm05+ga^VCTR+eI6Ww_I=%}HQS+L zjtGMvJ8sPrr~XSlYy>tubD(o}{SlL1LQiGNdH1i!)12Yhs%C zKlm>g;ve8$Q_r5~>P3$h6R*CA+5WtUk(_htZiaQ_L*6fIOQ*ftZ_apM(6arRycb4Pqlk^8E$XbfY>SPlDjQ{I zd}Jf4%Kp8on_-=)1t`twSWB0BD6Y|pVSOn zDpaYtmZyTdu3J;Fig4@1M^p8NiWT;`K>q8z5Ux<+!ZRO4x2;-}``D14@b#+Ft+=9Y zdD(yp?URHu8hBe>TTj=a@4P!~EHJH0cA z1gg?2(Z(Mn-fj52u#`{0m*n;QIvgkF@^_8nJB;Z1#G{z;b&??+dSoaeokwOH!4dxR z*pQ1*1l@E=f6erIptLCMk8?^BP$m|jTIMS3Ha!9@CT39;D;5;oDoc76Qi4jYW|~ez zxE{AXOn@J}xQYzwBSDuZ{ N#1g-!O}5xc(_bFx>L36B literal 0 HcmV?d00001 diff --git a/tests/data/bounded_try_returns.py b/tests/data/bounded_try_returns.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/data/bounded_try_returns.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/integration.rs b/tests/integration.rs index 9cef5b1c..c6f0d79f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -92,6 +92,27 @@ fn padded_return_consumes_the_existing_calculated_value_once() { } } +#[test] +fn bounded_try_return_keeps_its_handler_and_following_sibling() { + for version in ["3.11", "3.12", "3.13", "3.14", "3.15"] { + for padding in [0, 1, 3] { + let (src, clean) = decompile_fixture( + &format!("bounded_try_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + assert_contains(&src, &[ + "try:\n return datetime.fromtimestamp(value, timezone.utc)", + "except (OverflowError, OSError, ValueError):\n return\n if isinstance(value, str):", + "try:\n parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))", + "parsed.astimezone(timezone.utc)", + "def unprotected_return(read):", "def else_return(read):", + "read('inside')", "return read('outside')", + ], version); + assert_eq!(src.matches("datetime.fromtimestamp(value, timezone.utc)").count(), 1, + "{version}: {src}"); + } + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/bounded_try_returns_0.3.11.pyc b/tests/pyc/bounded_try_returns_0.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ead619dac7a84457cb6eaa4e69ce4af1673f9bc GIT binary patch literal 1691 zcmbtVJ#5oJ6n^J(oYW<4DS}qCDk=S;5h~5ZfKUf0jIBfvQnOTUe34wmb~s;JS_KIZ zGi#ApSSX6h&R!}cHs)KI*dk@Cgv2|iPU0k;da|FN{oeQP{hq(3(D>81aiqvAeZR`mAAUSo+Z){2aZ?rB`F*jcXPQ1xm8krre55 zZ|pLT^1UaV`&=eB@-4=EVU??_#nV9W7S0L$lFwaBWHp)CRT|Q$SUg~Mpav?FIhz$| zI9S2SwN|$Ms%JBMOYr?I&P0`a!M)1991Rg$Z6Ej)Le&H{@rGa?$;rsfqh&Z*o_L(1 zKaxbz*`+lTPEB)dlAcZwq_LwwB_uowo+EPzL!dzoPn&p45C~fWHKH}BB|@YYM`B^< zx->BVj?HA!uL{M~3BQh(uwXi8mK~@wA;VS2$?>XJ;l5x+f$_Iu=W{4fgT&Po1Xqx} z8yk$>LK_T)Pl=4|5S;BAQqucdiRt{L?^8yM(DhgswLVE}Z&82L!6WEgMrf(;$DHTNg z3%vB?%}c0%fydIGyn33eCvVamgnIGKZf0Y!_EH?!H@}_V`)w zua7=|Bb+R2fci@W4!{MMHK7GHXt5e&;9BpXX1SascT*%&-E_(Ru8n$@%DRpe(rXEo z)p9TJMV$>C>dmH4NA7>5Fb5zLZXcux42J`RFZN+8rOigynUyg|eL z6FVG4s{+{B2n7YbGOkJwZ1NYVZX> z&R`w7unVvFI_xsCN%+6nUTU?(lr8r>zY!=_+q3VM%MZ)t-h|3~q34Iv@#_NhD@ENX zl|j*NI&_rQf4n2>D({457!>z>snU&R;7FC)41%UAG%#pso?Nxss<5^rkoesSVt^{M zR*gaarPJ()81qnKZ0ChRBfyp;OO{ISXbokz9SnsV>LDr?&*mgK4p(R{ot4d?t>5oGbdWu8=3w~jR9bxs@jsAjd=o@wiC}KSrH`zD8nfKnj|1(Ubk_G_$JkPVlVyE$y@c4^_ zXt+V(0xYoDE7%8weTFOtR%8V*Vi`mYD@rulij~Z7N@)<03C9*(w7Xm;6g~4?-e8Is zekQHT^6(3pxq$kZSA)7X3_Bs(43+Gc05zya!|l}4crfC`r;b8cPw2C1QlrUgu_vsk z?o3V2G`d-HC-k^@-S=B#?X*VQk>BFjh3i@wEjugeQ0~{JGE%*(B`0fv{QrxuVIF6E zy|3?>wQBcftgdOv$*QbY=j7X>meBX<9J#E<{#Z3O@0Oe639H8brD`q6Lw#AtQ@=Mx zD;jMZaj>aJbC87=G~v0Cg(f4dvBW(7AbL<@(iCTHx9Lgt=FEIlE<6+{nZ{Ll(O^0nRR^8g#Zz(B9_kaOvuC5v9bYv`E+_MXF7~%* ze(kMeukSkCITCn!gk1E|^&j_6-iL;Arl68V@e?(M>-g^kKGeBNEE z+%YaQOwr|JW!<7f||Yt1$uFG3*~}+MDL5Ld?Xosa|>Y{NR#s2 zcDRgteWAdB;Sy<3fuhK5$5%6)Lsd_i>AOAj1aAn6H;5J2iJ@8q)ui}~MBXPdaGuN> zA6WK3yN_z-Fi)h!u%PTuIlI6+KG$A8B<4tXu9pXcSkP>$y+(x^g@sCM5JVOvAn~m0 oIsFcQf$F(c#)c{}-wnpt4-?qtm6>91*;OvaR)=YmWk*^01wRSpN&o-= literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_0.3.14.pyc b/tests/pyc/bounded_try_returns_0.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c9fbd29d493370edc50d6982152356f5d734eb6 GIT binary patch literal 1654 zcmbtUOK;Oa5T3OYCrU}F3bcr}#BSQss1l9TN=UqJk;)~FOqBzQsN8siEG2eWuZ!AK zg~Wj?zkqB02B_jc?3D{#D%4cs%50p?qYoZpCC`5L%(t`i^4erJqXWR%mv`^K?U^TE z5f7HuN8MEfNAMDC@(i|s!xrHr2evi~T(fnQx}89ou##) zG71fMn>^@y&&#v3Lybbe=Whq$)J)etYqZv3{ zB=&-!DY6X~w4@%7YPCgnd!I7YzbP>mFp=7R9)nEPX+|{0ddOMK^TMDJu$IFMnn>#QgAn`#@apF?I)cU~@>7GvCDr z!X*@+W|vB7_HRXc5FFtkRKddJtES4q zj(peDd#{XW8LzQokYT|(x-c;1o=eLj6+~R34q>;Dg+)p*>bRktZ1p^dVd)=<^^YO+LLz?=f;Jig%SrbpiZZiM76@C!ZS>+3 zv>8%$;}ulO5x)G!EFM VSv@tfKy<8 literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_0.3.15.pyc b/tests/pyc/bounded_try_returns_0.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a2549dc87e45153038f0bfe5d85feac05a2cb35 GIT binary patch literal 1652 zcmcgsPft@p5TEU9UrVt_5C~9#wjhEgQivQR>aD?epjLwi8$%xLHZ*D9tGoL!a4;l( z1V4et#t-0K!`<^lT7ToG+ok9>E4k*kF=PLm@N>h#rvFeR?BC`m}82Cdp2o$WMOM z$oAG*jNvc-cCwo8w|m{(aoTRlKVx^s$o5vUn?EP_1#A;@qsf%Vw%8>#War&;c}#sZ zWdBk_YUCLxj+@Ae6J0cA(28Y4DMDG9h9r>2F5I({u**=hD|L}fGrYOPw5<-RZpG8= z#esc0n_bUlo5MO@l~q?N$1QO|JuawmbyV<1&A}ry3EJ{iNyi;&9+tMMuF{c8&2y9v z?|5EKCo00L6CbatH*_Mu#|83V=Y;Ttj^>{QApYE``8>duR3gx;O0VLHx}%mtIP^0K7HNu_jJ;l*+p-?+293FH)++4$qgCRzX6* z%vvEaFtv)x&R)8(A;w#o*dk@Cgv2|iPU8GbJ=xFCzW2R*@7?##N6Dnc0U-bV--9bl zKNtUy-X6xe)=ZOf01h~;1ZCKOGTUGb9OENw7!D`NofyfO6JLdl`B%g0hDtcL6tY+r zDxu}R=ZZW#cc~vH-FofjZ(8U8WY=5`WVkpP9Fl(Ae#aLz*lxeQOV!}3_R=?GqVs7x z9_ruNA+2-VUdx>&ypE)A48}*%qp;c=_vMZwInmJzMo$(md-9#^*hVe=C!-*{GTgR8 zORCbq9HV$0<2=Y~Aqqg==+bX>^a5W1j5AoLt=6brc*ECWm-R$C-9bcG!Vf)>j)#`R z_Q)anM14f-`X?rra;O1y8Y!ee+%}_b`{M zPbjPC7hPZ4ZeC!FYJ_?l)d-48$)-=5tcq7Z zl}eq>NpkFGsf+FGmREHh;cQCW+e9I&==%37dulvDY_?;-DT3;5YSIn<0*RCH*{4}J z$zFdp#(!8$%Z2n(5>AuW(lkGvVk9HSf-XpKEW9G+fQV>7jZU5@F&G5IAd7GeCPsiY zqF7DsI$u^M?LpBIYQ(Ebt<)J_C2zE8F%-7r>oX#SntN)p>Q>N`A}^^1t^9GGCUjGw z?g#%8iT7id;gdDPzp$AhDXo#!@RWuHOC3PCa12fqXK5biNMsn^p+|T4W1{4XFz!W~ z42-+tz6qbLG(Tc#zFd#DD5*&pY-f-tYE-?IYfzH2#G|t3R7>J9>c4}oRdbz0gEPh& jCb0A`xZ8g<%p`lw8qg(EaW>U}wwN&4?FRIVpv?aPC+!$M literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_1.3.12.pyc b/tests/pyc/bounded_try_returns_1.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a5877ba237f9613dcd770ceab8cec4ffa3a46aa GIT binary patch literal 1572 zcmcIjyKmD#82`?Wod*P}J`kdaru0E0q&5=+MNG{|J5)kymdcGUlB>iH-(6Z#6$v5! z1BPzwQ4xOtV?`%crdyfVP%|Kvh41W~(}>aqagx9L`M&q-OR-q60igVA$NEgNFA9c0BNw zhfN?0#Uan2t1VpAABp@%sy}8>!%DMwG$K20Bn)3H=0WP3#AkQXpy5rmxiT|jN#`}< z>#nh3Bv6d_x@Y*q`|`^A)No_NRN)mnWPj-n`{(Ykf9p;c=0M&uS;^Zex?@noo(HrU zY(W?H;ibI=`;2r_{-4}lX1&amD-VLO5h+$Xa35AGk1Lhlm?{J!2!-^*I!8UPsN1Eo z$lFbio*eyWcVt}^JYm*C*+D3k(`ZJXRGFP9YN}!bqn75Asa9JRH}*IZf4_k=5E6K1}p$+v`cA`$`bAMZ6 zcN@9vU?`T`U3D|o*iK`>X;RfY+ouo2JdqP;ZfzM(Ru`%{_`Ez>b?nm&(=?Z_PTHpv zjOfV9>w?59zeeB)@?g>O`ROvU)J{?57KC>C7#%7woL$u<8Do%Vy%L{05pI`Hlbp literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_1.3.13.pyc b/tests/pyc/bounded_try_returns_1.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e31953b98513b2007d7745c2f75e745a6c51fb8 GIT binary patch literal 1616 zcmcIkPfrs;6o1ogw@sx;P?S&uTR@;OksjcnL2vZ{2_)UrgFTR?J0+X6yY=l914loC zhO_5>4Gmwx!(2UiGxVTG-*k7DQYs#dlkA({?0awC{}~htc>@4`eJHWqRJ-|&aI#V{ z8mtjG2ODg*2fKi<%aB=MrxpNHwn5ae(?rw9nX>s^DfLq_=Q@H5uft_d(X+tkO{RG9 zGHF+q2Vcp|IW!Vp9U9s&Y{zILR&w?L>QGC^$C0hMIOJqUe#Nkn)BkFDjjnc^J!6Zo zs%|Y@{nhAp-R;ng>Tbqz|n;Jvkr}N~qI{Rbo*rZ!-k0_}f`?uO1LkSuWbYzYB z1TAQ^Wn{sjZna(n=&I@%!ohGLxn_^ZZsI*_$)-Nu~0+1ZQFSw_CY4#_-L^x&GU{aXL{sLSP>+u^JYUF6s~tE(rjLT4EekCSI*LzMb-J=} zaKe%J@2ZFa%Itw1f&6Qy-Qy89LV=N<7Y3~WJB}!)WadOkNVDr;$lXx2ie<@?po_yz znq+75DCqevcMk=g9wHY#^uwp!Q+ao&ahUjkMN(C>J)jT5RU)5fSE~SDrq`-PxPZCp zym9f6DO$`|H;jvQM)ca#pb|2C8eS1Nr`E5-GQB;zjjZUL(r4AsR0@)f*2Gd6`_iP0 zdoGvRpeGdQGn^w0Dp4A_o(LK-r|ImjE|q*_-KQQP8_J1r`rk{)_4 zt&@RCxf%FL-@RYx=m@GqUInVkHLMNLL#ctwE(4^X0_9L}9;b~5172kOZ-A>&^{*UL zXtbN;@xXf$a?*Ny#8c>M3u|Ng6PYB2`qK)nYUi@rl#x&9%_!-!vahfkDM*ghsC-k& zj`l2O@rQ^9m6aJ!cupxRXFc2d=RDt6vZGykE?>|A6Guf^RMl#e9ddamk#~WetFxNy zBwF~dPMTQf9Auz6tMaSf8KQd%t?CBYIL#?Y!yE)U4y@~Q;1JXu%3Uti4VCgl82r$2 z8=fF94vnQk;Yp#;ofa|2cU+%aZk;kY^Gpu1h%nl2TKFq0PkM*fMa=TmH50nyauIGc zJ&TLbuIDvHyurMdnw~KwR%@)P^yzB&y0c zpobs$^GH74oi8Tf)9gZV8IBXP#k_vJNF-gJDsJe<>jde<6Cw-Je?p!iIKpOCAd3x9 zywR4RnHnL^oQV-D%QImPeMXR8JP_ bhfWCjX#iO~G2-MkImv`c@^soD>At_;YBKBL literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_1.3.15.pyc b/tests/pyc/bounded_try_returns_1.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60f484412f0f2e17f4e27b11ec7d6a0bc9653408 GIT binary patch literal 1676 zcmcgsOHWfl6h712-ZsSoA%M~fv|0_tAce$*1lcvXqEv$m8$&MbG&E`Ntus>yEDVW% z!GGYY@dvoquy;LMH@cLXxb>XgP9K1EVd5ln=9@XMIggoV(-<&TSAHE{) zjX)Q*ml2%dd<6ye6t+RaHj``~3V|7rfr5q9Dg==Rcf%>jguatvE1)A~M{!lHa~&ai z>bbnch}Ru9uH?N<^bA3<$K8XXaS!WVbPpwHly(8)um|~|zaOOhPL~%RJoRxgV$SkW zgZgWMJ?eU&gA`eh^?3%3wy-{+KNQJ$Pk+*&wZN6Aw&>^eK6qq9v^&r+@6PKIe)omSn7 zr`gL>`$0Clk4dR6IFJW+R4CZIz{ zB%$qwBP1_NYNBLPrxY5NtN7-f)dR2TmU($!iIaW7Ra3a~VdF$kbTsyRcMu6wrC*?p z6J#36r+7Ly0iSQ)&#l1u#B6TaI?phoD`UAQ*7+twI`oN9LOP$w4uUhBbJ%=~Pyo|l z^ZuTheYcz_>5ma531}M=UoCST_L_=7i-{Rj!%77Ocgm8Ud{R((^;FYs2v6~n!odBY z#a(1j-w1k8@&=N(k!g50mbTur^nZ&(&=VCYBpD{~gM`6rkMWx1=DzgBc7(6JZ!6@t zA2nR3be;GFDYg6%{Y*qQ>t4B8<68p%DWDX!DMLRj#@H_#*wTd^W3SmoGQ<);CvCRa GNzZS1E$4** literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_3.3.11.pyc b/tests/pyc/bounded_try_returns_3.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1bbf9f1592a02f78e904429e128a16c1502af0a GIT binary patch literal 1751 zcmbtVJ#5oJ6n^JRoYW;WZTS;LZAx3wKt&@}VnB!i!oboFm5`Ew;>H)rm1BqVrKMGn z5JF;O?SO=silVZ!mo99~w=%&LDO)8Z-Z^y=Ck;z4@$<9qeed3T_r3F%RLYT~_1r~9j0!65hA|YUD@1Uqz6eYD{C}UQ99kTjw1#4(BX_(DoGlh(8dY&Uw zW$snfJi4k>Wom7kah%_I!nwy~Vr{*x7|%?n#@agd1#jbQ-z$5(Vv4LLQ$5b z%<@&!WiltT0v-Fy*xlyxrdM+bMuS#bm;(38gd$zl>gS^J1Ktmaoo`3|D-V6E(NWJ;6!>YtgO) zpTh~&Y+Ut-e*wkY(TniGn4zD@%#hTcqt)=#{ueNI24cf8JZV0SHhdOEmhhW+>rG>n zIX(;HU%<)0xF`N8_-HKAL$X9y8}T+p6)A)54HD)G)oAA&l;$M)pyF9Ihdt!#E23)F pDxhdmLP%2wviK8j4gQ;Yio7IE=#iN?nQB5uq;)dWgh3IM`yYh%8IS+~ literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_3.3.12.pyc b/tests/pyc/bounded_try_returns_3.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e839f77cb8d8fc195ecef3b58b094920058557c GIT binary patch literal 1600 zcmcgs&1(}u6n}3w*?ef)5?T?fH8HI=2sX!};>m!g7AXkLrEIek*plqV*-2wc1rh%R z>D6nfe}TtpPhLIE)sr`A4nn>7W;e5Owf3So%f9)|ysvrh&6|&fLf!;`>i6|qbD!?L z`%E|<#u$xm5;y<{9Be}e8qmQ8A~?n?Xc&%3)O1orQ-gHX`l6JEhRiy)V8ZP%nN@Vp z_gE9h2GwNIuRM>A$jt$Wn70o?JBGtC;*a-XC#BSq<3)w`Gs!)*5?Q^slh~?96I3nJGu-MyM_6 z_!9oEYCBqr_Y?jWG=DOmon?oLuWjgTJjZ?fL#6PaR0{vTQUj=fxTK>Kmt%BAqn5b{ z+(g)f0rcUSxe0wFotXbScbHnMqO`=G>$QA|jXmpjt#-Fo8_vkQ8@OH|Y_G|lW+V=gnEVQ`aQG7Dh>B_?TF5|@S*E#pO z%&c!jApgW}_gI9@K=4S<4g8kRJGQ7AGQF!Lq}8=~z??v}SgCYc7GyYBp{})8w*8*x zFlS5f!4_wt$GzZAcOa)jjji|$e4bQQACKsVV3EjCuJUjd-s4ie2p?8w>(l1(6e?QG z)fdd;Ye+Ql45);J&%h%B2h@xPHKP+ZC`M~TUoU;3sTd?5jWdd2u1Sk>?mA3n{GL!? zhI^>}_yTJOOrLa#n4$2QCtVw+qf4i*Sy-?zG_JIUQFLJvPP6wrzSC>7 k2Yi`W%GpRI=c|c`-z>o86RUtv@uZZ-`O%Dp*Tc;G0B~sfdjJ3c literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_3.3.13.pyc b/tests/pyc/bounded_try_returns_3.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da87932a91ad256b34ba9aaa729f86ded5e59293 GIT binary patch literal 1644 zcmcgsPfrs;6o1ogx6MkG5FiE&Y%35N6R8Fc8s*jtCXjSf5B5No?v!lO?$)=n7&!V7 zO!@&l_G@VP5_9#WCqoZ<^i6lCrC9M`;x>Krn|Xic{heX4Xc++T^FxUh<~pq}gvVbd zMZ+xueQ>~GuiyX>4j3{UoXiSf#xaN*PL^o)G*>adDy2b27CcvQ;di+#D0&eFyu}nR z{Y+Zb&BJeWO&^+LUIUu?V%SU3cB1Wa-LAiv`mJqD#c_9RV_u`(%x^L6!*uiuTK1--R{`>KlAZdZmYl3*isCOG zr#YSJ@dIt?=H2eujl0*hClTXAVyH$zH9OH9k@tlq z=v!suBP;)xAW+RBmWWgsmX#ORT)x9Q5!b$M5_2Rx)7Qg6E@?c~YNMi!fNa9P< sPTup<>!aNK0(TbZM*si- literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_try_returns_3.3.14.pyc b/tests/pyc/bounded_try_returns_3.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0c8297b178f77ed03dfbe203f706d07f0ff6b2f GIT binary patch literal 1726 zcmbtUPiqrF6o0duY(mo1Qf!qbZ4z7B#)1vBSg0aiBK4Anwd5edvdvCpA=!iY1Y<6)V4U~PJ@c5 zq{p61tEA^rP6qxmc4qw_jCbo_8sIt~1d*t_3McE~O zu0}RqRrK=TRW-XEB6}nQm05+ga^VCTR+eI6Ww_I=%}HQS+L zjtGMvJ8sPrr~XSlYy>tubD(o}{SlL1LQiGNdH1i!)12Yhs%C zKlm>g;ve8$Q_r5~>P3$h6R*CA+5WtUk(_htZiaQ_L*6fIOQ*ftZ_apM(6arRycb4Pqlk^8E$XbfY>SPlDjQ{I zd}Jf4%Kp8on_-=)1t`twSWB0BD6Y|pVSOn zDpaYtmZyTdu3J;Fig4@1M^p8NiWT;`K>q8z5Ux<+!ZRO4x2;-}``D14@b#+Ft+=9Y zdD(yp?URHu8hBe>TTj=a@4P!~EHJH0cA z1gg?2(Z(Mn-fj52u#`{0m*n;QIvgkF@^_8nJB;Z1#G{z;b&??+dSoaeokwOH!4dxR z*pQ1*1l@E=f6erIptLCMk8?^BP$m|jTIMS3Ha!9@CT39;D;5;oDoc76Qi4jYW|~ez zxE{AXOn@J}xQYzwBSDuZ{ N#1g-!O}5xc(_bFx>L36B literal 0 HcmV?d00001 diff --git a/tools/compile_bounded_try_returns.py b/tools/compile_bounded_try_returns.py new file mode 100644 index 00000000..30c4ba9e --- /dev/null +++ b/tools/compile_bounded_try_returns.py @@ -0,0 +1,35 @@ +"""Generate Bounded try-return fixtures with CPython 3.11–3.15. + +The zero-padding inputs and RETURN_CONST versions are compatibility controls. +This reuses the existing relocation helper rather than changing return values. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +from compile_padded_value_returns import pad_return_values + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 15): + raise SystemExit("Use CPython 3.11 through 3.15") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/data/bounded_try_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (0, 1, 3): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + payload = header + marshal.dumps(padded) + (root / f"tests/pyc/bounded_try_returns_{count}.{version}.pyc").write_bytes(payload) + if count == 3: + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "bounded_try_returns_3.py").write_bytes(source) + (corpus / "bounded_try_returns_3.pyc").write_bytes(payload) From 9fe8e94533c3c535b5f156cbead9e6ba08a6d0f3 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 05:44:12 -0400 Subject: [PATCH 05/22] Keep chained comparisons as values inside inline comprehensions ```python def select(values): return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] ``` The chained-comparison recognizer opens a statement-level if for this expression. That loses the ternary's values and can replace an entire dictionary comprehension with an empty list. The same problem occurs in dictionary keys and values, including expressions with calls before the comparison and at its endpoints. Inside an inline comprehension, recognize the two value-building arms, their forward merge, and the failed comparison's cleanup. Verify that the stack still holds the exact middle operand retained by COPY, remove that retained value once, and schedule a ternary value merge. Ordinary comprehension filters keep their existing path. This applies to the Python 3.12+ inline-comprehension implementation, including 3.15. Conditional expressions date to 2.5; dictionary comprehensions date to 3.0 and were backported to 2.7. Those earlier versions use a different comprehension parser and are not changed here. --- src/decompiler.rs | 45 ++++++++++++++ .../b_comprehension_chained_ternaries.py | 56 ++++++++++++++++++ .../3.12.3/comprehension_chained_ternaries.py | 18 ++++++ .../comprehension_chained_ternaries.pyc | Bin 0 -> 1550 bytes .../3.13.7/comprehension_chained_ternaries.py | 18 ++++++ .../comprehension_chained_ternaries.pyc | Bin 0 -> 1618 bytes .../3.14.2/comprehension_chained_ternaries.py | 18 ++++++ .../comprehension_chained_ternaries.pyc | Bin 0 -> 1610 bytes .../3.15.2/comprehension_chained_ternaries.py | 18 ++++++ .../comprehension_chained_ternaries.pyc | Bin 0 -> 1627 bytes tests/data/comprehension_chained_ternaries.py | 18 ++++++ tests/integration.rs | 16 +++++ .../comprehension_chained_ternaries.3.12.pyc | Bin 0 -> 1550 bytes .../comprehension_chained_ternaries.3.13.pyc | Bin 0 -> 1618 bytes .../comprehension_chained_ternaries.3.14.pyc | Bin 0 -> 1610 bytes .../comprehension_chained_ternaries.3.15.pyc | Bin 0 -> 1627 bytes 16 files changed, 207 insertions(+) create mode 100644 tests/behavior/cases/b_comprehension_chained_ternaries.py create mode 100644 tests/corpus/3.12.3/comprehension_chained_ternaries.py create mode 100644 tests/corpus/3.12.3/comprehension_chained_ternaries.pyc create mode 100644 tests/corpus/3.13.7/comprehension_chained_ternaries.py create mode 100644 tests/corpus/3.13.7/comprehension_chained_ternaries.pyc create mode 100644 tests/corpus/3.14.2/comprehension_chained_ternaries.py create mode 100644 tests/corpus/3.14.2/comprehension_chained_ternaries.pyc create mode 100644 tests/corpus/3.15.2/comprehension_chained_ternaries.py create mode 100644 tests/corpus/3.15.2/comprehension_chained_ternaries.pyc create mode 100644 tests/data/comprehension_chained_ternaries.py create mode 100644 tests/pyc/comprehension_chained_ternaries.3.12.pyc create mode 100644 tests/pyc/comprehension_chained_ternaries.3.13.pyc create mode 100644 tests/pyc/comprehension_chained_ternaries.3.14.pyc create mode 100644 tests/pyc/comprehension_chained_ternaries.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index f33bd343..e3391348 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -31595,6 +31595,51 @@ return None; if let Some((merged, body_start, body_end)) = self.try_stmt_chain_compare(&cond, target) { + // A chain can guard a comprehension element's ternary. + // Its success/else arms build values; opening a statement + // If here drops those values and the collection itself. + let ternary_merge = (|| { + let comp = self.inline_comp.as_ref()?; + let start = *self.idx_of.get(&body_start)?; + let end = *self.idx_of.get(&body_end)?; + let then = self.instrs[start..end].iter() + .filter(|item| !matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .collect::>(); + let hop = then.last()?; + let merge = hop.target?; + if hop.op != Op::JUMP_FORWARD || hop.is_backward + || merge <= body_end || merge >= comp.end + || !then[..then.len() - 1].iter().all(|item| is_pure_value_op(item.op)) + { return None; } + let merge_index = *self.idx_of.get(&merge)?; + if !self.instrs[end..merge_index].iter().all(|item| { + is_pure_value_op(item.op) || matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE) + }) { return None; } + let cleanup = *self.idx_of.get(&target)?; + let mut failed = self.instrs[cleanup..start].iter() + .filter(|item| !matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)); + if failed.next()?.op != Op::POP_TOP { return None; } + let failed_hop = failed.next()?; + if failed_hop.op != Op::JUMP_FORWARD || failed_hop.target != Some(body_end) + || failed.next().is_some() + { return None; } + let Expr::Compare { operands, .. } = &*cond else { return None; }; + if !matches!(self.stack.last(), Some(Sv::E(value)) + if Rc::ptr_eq(value, &operands[1])) + { return None; } + Some(merge) + })(); + if let Some(merge) = ternary_merge { + // COPY retained this one middle operand for later links. + // The chain scan consumed those links without executing + // their stack operations; remove only that same value. + self.stack.pop(); + let comp = self.inline_comp.as_mut().unwrap(); + comp.pending_merges.push((merge, merged, None)); + comp.tern_chain_t = None; + self.skip_until = Some(body_start); + return; + } let mut blk = Block::new(BlockType::If, body_start, body_end); blk.cond = Some(merged); blk.cond_set = true; diff --git a/tests/behavior/cases/b_comprehension_chained_ternaries.py b/tests/behavior/cases/b_comprehension_chained_ternaries.py new file mode 100644 index 00000000..1536345d --- /dev/null +++ b/tests/behavior/cases/b_comprehension_chained_ternaries.py @@ -0,0 +1,56 @@ +# MIN_VERSION: 3.12 +# This case exercises the inline-comprehension reconstruction path. + +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} + + +for values in ([], list(range(24)), [0, 6, 8, 9, float("inf")]): + for function in (mapping_value, mapping_key, arithmetic, filtered): + print(function.__name__, function(values)) + +for failure in (None, "base", "coefficient", "lower", "probe", "upper"): + events = [] + + def record(label, value=None): + events.append((label, value)) + if label == failure: + raise ValueError("owned " + label) + return {"base": 0.6, "coefficient": 0.4, "lower": 4, "upper": 8, "probe": value}[label] + + def base(value): + return record("base", value) + + def coefficient(value): + return record("coefficient", value) + + def lower(): + return record("lower") + + def probe(value): + return record("probe", value) + + def upper(): + return record("upper") + + try: + print(failure, observed(range(10), base, coefficient, lower, probe, upper)) + except ValueError as error: + print(failure, str(error)) + print(events) diff --git a/tests/corpus/3.12.3/comprehension_chained_ternaries.py b/tests/corpus/3.12.3/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/corpus/3.12.3/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.12.3/comprehension_chained_ternaries.pyc b/tests/corpus/3.12.3/comprehension_chained_ternaries.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75ee69b7090b26b99f4e2c7a10a54f55c246115b GIT binary patch literal 1550 zcmaJ>O>7%Q6rLH+Zd|WT)rJ5{X>c4>$VhFek`z@$4k2}m1S~%$haw=^>`t7`&#sxZ z+oo~}5=2U+NF*(&ZUvILx#}g{f_mu@)Qj!37VNc$$_J#XInfhuX6>}8Qbw9@-_FmQ z`M&qw+=;~^A%xKBf4+WSKlokgKG@q0x2&xM7*~;nzDJ)U3j<<{01_(%7_ulJT~Ly$ z+1?>Gc0d=dw{(E3sEAIYAFxSmWRfqt5W0wq*n)17^JKOgA+A`huR}0$z_)`fdWEdK zOku}GoB_nrI){>q-c-SBGb}(H+Jug7pc}+j&l+CWdkn`Vw@yUw^dZP*U8#`BCj2Dh1aB)8zl=8as=cCzEr36Fvkafu)s0nG@&1L)8m z4nGK1ZVo?3mI#DCKRwU#w#i?F9ebdA{Q#iK2cI^y%PPz+DmbCBfB;*e-42EComZys-G>2hJ?(CuTl-dOD_^YBM(9M=Fn4wQ*wv!_Q<4Su?@K&_a1VfsTcA7v@oRA`^+i!&mZv_;^e(=vb1&E?Z zc5o41pH3Pc^EK0DQ&YBSGbisWbM86DeI>`;N#-ksT#j)r2zp{;JZM27h22Sy@v|W3 zL70LFBJVu_$d~iqG_=L4n_o_^4V08CZ~uCrGPc><|2w;t{j=+j!kt&|rY34KS*I-OOS$$wU3}K8P5sXjbM`4UVlK7d#Q+NwT-XXjP*8K= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.13.7/comprehension_chained_ternaries.pyc b/tests/corpus/3.13.7/comprehension_chained_ternaries.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45cc60e2539bcdb04b480a0885b369ff55efd035 GIT binary patch literal 1618 zcmah}O>7%Q7@b+~I&Ri(m70QRp*VJ_a3M{r#6?YuLof~;8mF*qr6NJHvDc16;sz`;MXQuo?Jqjx%1QA*MEG3xYKAmY6TH&5``Qjj3^XH6#*zHE}%;hfnr7qyOTRB z_`*JPzIE#YV3VlCPcBeXI6+LoPdNQI+=M7jBoQ(#1ke@NG*!eI zVd`Zy296bVbqBIFhi6)kjriTMRyopY3kQ;*KUe*?ix@k$yuYmDDz#1y(X2Bdz zX$5UKX)NWLKCkD@tdW~Z&THA6o|-A>ET^%oZXVBHvV2QgKA+8HX6#Lv7n|@?%rM|9 z^6>0?4LSDfLHOU!T2$ZUxM+RFfNwq=)q*fmb}CA3;hz?ixDJ% zm_qSwIrgB8RsV@PBBVUb;9xM7UGA$;>&o)@N$_F(TxWJc`vXgqWM z8gCEBo3<1jn*D&s=x9&IA3HM}<2_{Rhb;>8OL`%jWG`UF9(40<08~2naYI)AoV*?R zI<^ri)3SO$Fj~<7Tfql`(QE!&q1yiFKi=p)_(G|Q3iMJOz1|$@){ARz>vbN%b9&)i zUN*d7Qb^$x2&fcg^E=eRw^K0Cw&@tk&eUs<;O2 z*Gw+=`UIKqh>5UdKVEiq+vc@XYM*VBYGEpA-|Xy?kt#0gQT7_f?B_0o;E9V;`c$Cw Xefrcxp&rLiY@DPm#P161Fy{RSS!Y%n literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/comprehension_chained_ternaries.py b/tests/corpus/3.14.2/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/corpus/3.14.2/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.14.2/comprehension_chained_ternaries.pyc b/tests/corpus/3.14.2/comprehension_chained_ternaries.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6169659aac56138645da7083cb8eb1d6c652443a GIT binary patch literal 1610 zcma)6O-vg{6rNeHH-*Q(92x^29Lh)>nY_Q$6O{95Hgn zKvK=A0||XTOUVq$7@D4$Ow6cSh9oC*gl1H#5u-nQ-E5mzvso>Zp5#twD>z}G3@?Z! zw0rTxnjF{&tqm1$A^LNBQT?U;)?n%5sFL%EsHW9)e$_e?5 z`)VQt;AQduA!GuFg&6jt7zR?_csVJQ-9qh5usaQl3EBd>DL3$aPOeiYtS~dk!ia^??QKQ~L9`Hl#r-u9 z_Op-=FoX&uP+U9^_6{rTFQh^1@G(e<18JZLVXWBBu(#I7nT7)2h%h$zawfcD`WzpR zM$&g~v#~q!*gE-iOhyp=xHSAHP{dcP8UYesJA?%Mgazghl#Y}(2Ngh)vb~D!1&Jj znjBo0iuujKjko`H_O2W+xPLftbF|o5b@r}H6**WIgMcb%F!#%jKJmNuJ~6>kt}$5o zaAd!MQtts$y$1vUSt^FzC?@&peIq6`{n^~eSN9YX5AN)z3q03_V)p;+UhE$Vg2cgx zB@UEOaGx`2zpqFd@(T1QTus}VVDa%Dp{${q;k4mC_O#$>dD?oc<-Draepb zX=2***({-yQDU$Yr}*fxs?VWM8-y+Z*(YGDli6Gb0c2mhQIi)d*KU;c+$~?h{{4kt zPnM!J_qh%7M|$h{gZy8vhyJO`)p+gd)Q*2@-6*HDZTIYU*KE}}3&$$*Vp&{-U@RJp z|K-t^(M~bqwetqX>zt=Bk1@ZSgW3aARDapUc|Dn*BYx_FHQw?(pggr= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.15.2/comprehension_chained_ternaries.pyc b/tests/corpus/3.15.2/comprehension_chained_ternaries.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91931c0aff994eff5bab8c69813729e1b002f8ea GIT binary patch literal 1627 zcma)6O-vg{6rNe{E)Hu#38HFEXfQ}rrXhp`T&0cdQpc`@9NLYul_J%J#fUQj*YJ*o&^{GcU( zf>L_#6g%t%W1rvo2wV{?iLPQ>7(%uXhhLXoMJV5ql9RA^K?uEtP#RA7QOe6>nZ*gA z2P}mXIE8f2L^ndm&`CIN;sE#_9%a!weg@fS9jyzFyreG_h~-G`Q-J+}UT4TAmTe8s z>b5?dHW%}hWJ%64%-mEus~b5oJ7p7^)2TtMGx_UI+oGP&8@ahD{t)*ED9;v8x-b`13j-aa*#9?O_iEX55I~-yNEzm4uD{-ZvBp=5gj@^u| z^_Ov3d(t~v(ZOx>ZTF7e>Ac^+bt1g&3s-&NM<9@7RZIePNte1eBL}HtO6Jsk2tg6- z5Opu}uHikBfjmE&fZ}N&nb40w6W?+B1&Da%2qN$^CV?Z6dNalXbOV{Cz&mOKvrdBp zoM%G~CHB2sE<7CB9Q@VSx7Jk(eCfY6Uhb&-`tHd!B~}$2uWos@bGLCEm zf+83ollRSoC?Lyd_!QFQbGvgy6At|HoI>Z|99x>W|4maea9uyrr2p~`&k_}Zc<@o; zfmVt&Ig^_+M$V$3Kp7SEICHF{f-RJRbHo|lB94+aNhV{Y4U)4RX~F!IP)EvBbA~ul zA)hCdvc|WV8{n(g?Lx0PV-dOpWWzK>9V{mS4#-|!X(+3W<&~;w-ySVVU%dTYcSUOi z&Tok2lTY>ql&iT4;zP7wlQ&y|uDimYYV%NMe zu8enzL5a^Y-tJt*yu|#kIi?-RM7tNexM= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/integration.rs b/tests/integration.rs index c6f0d79f..d34c494b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -113,6 +113,22 @@ fn bounded_try_return_keeps_its_handler_and_following_sibling() { } } +#[test] +fn chained_comparison_ternaries_keep_collection_values_and_filters_distinct() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture( + &format!("comprehension_chained_ternaries.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "return {value: 1 if 6 <= value <= 8 else 0 for value in values}", + "return {value if 6 <= value <= 8 else -value: value for value in values}", + "return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values]", + "return [1 if value >= 6 else 0 for value in values if value <= 8]", + "base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3)", + ], version); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/comprehension_chained_ternaries.3.12.pyc b/tests/pyc/comprehension_chained_ternaries.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75ee69b7090b26b99f4e2c7a10a54f55c246115b GIT binary patch literal 1550 zcmaJ>O>7%Q6rLH+Zd|WT)rJ5{X>c4>$VhFek`z@$4k2}m1S~%$haw=^>`t7`&#sxZ z+oo~}5=2U+NF*(&ZUvILx#}g{f_mu@)Qj!37VNc$$_J#XInfhuX6>}8Qbw9@-_FmQ z`M&qw+=;~^A%xKBf4+WSKlokgKG@q0x2&xM7*~;nzDJ)U3j<<{01_(%7_ulJT~Ly$ z+1?>Gc0d=dw{(E3sEAIYAFxSmWRfqt5W0wq*n)17^JKOgA+A`huR}0$z_)`fdWEdK zOku}GoB_nrI){>q-c-SBGb}(H+Jug7pc}+j&l+CWdkn`Vw@yUw^dZP*U8#`BCj2Dh1aB)8zl=8as=cCzEr36Fvkafu)s0nG@&1L)8m z4nGK1ZVo?3mI#DCKRwU#w#i?F9ebdA{Q#iK2cI^y%PPz+DmbCBfB;*e-42EComZys-G>2hJ?(CuTl-dOD_^YBM(9M=Fn4wQ*wv!_Q<4Su?@K&_a1VfsTcA7v@oRA`^+i!&mZv_;^e(=vb1&E?Z zc5o41pH3Pc^EK0DQ&YBSGbisWbM86DeI>`;N#-ksT#j)r2zp{;JZM27h22Sy@v|W3 zL70LFBJVu_$d~iqG_=L4n_o_^4V08CZ~uCrGPc><|2w;t{j=+j!kt&|rY34KS*I-OOS$$wU3}K8P5sXjbM`4UVlK7d#Q+NwT-XXjP*8K7%Q7@b+~I&Ri(m70QRp*VJ_a3M{r#6?YuLof~;8mF*qr6NJHvDc16;sz`;MXQuo?Jqjx%1QA*MEG3xYKAmY6TH&5``Qjj3^XH6#*zHE}%;hfnr7qyOTRB z_`*JPzIE#YV3VlCPcBeXI6+LoPdNQI+=M7jBoQ(#1ke@NG*!eI zVd`Zy296bVbqBIFhi6)kjriTMRyopY3kQ;*KUe*?ix@k$yuYmDDz#1y(X2Bdz zX$5UKX)NWLKCkD@tdW~Z&THA6o|-A>ET^%oZXVBHvV2QgKA+8HX6#Lv7n|@?%rM|9 z^6>0?4LSDfLHOU!T2$ZUxM+RFfNwq=)q*fmb}CA3;hz?ixDJ% zm_qSwIrgB8RsV@PBBVUb;9xM7UGA$;>&o)@N$_F(TxWJc`vXgqWM z8gCEBo3<1jn*D&s=x9&IA3HM}<2_{Rhb;>8OL`%jWG`UF9(40<08~2naYI)AoV*?R zI<^ri)3SO$Fj~<7Tfql`(QE!&q1yiFKi=p)_(G|Q3iMJOz1|$@){ARz>vbN%b9&)i zUN*d7Qb^$x2&fcg^E=eRw^K0Cw&@tk&eUs<;O2 z*Gw+=`UIKqh>5UdKVEiq+vc@XYM*VBYGEpA-|Xy?kt#0gQT7_f?B_0o;E9V;`c$Cw Xefrcxp&rLiY@DPm#P161Fy{RSS!Y%n literal 0 HcmV?d00001 diff --git a/tests/pyc/comprehension_chained_ternaries.3.14.pyc b/tests/pyc/comprehension_chained_ternaries.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6169659aac56138645da7083cb8eb1d6c652443a GIT binary patch literal 1610 zcma)6O-vg{6rNeHH-*Q(92x^29Lh)>nY_Q$6O{95Hgn zKvK=A0||XTOUVq$7@D4$Ow6cSh9oC*gl1H#5u-nQ-E5mzvso>Zp5#twD>z}G3@?Z! zw0rTxnjF{&tqm1$A^LNBQT?U;)?n%5sFL%EsHW9)e$_e?5 z`)VQt;AQduA!GuFg&6jt7zR?_csVJQ-9qh5usaQl3EBd>DL3$aPOeiYtS~dk!ia^??QKQ~L9`Hl#r-u9 z_Op-=FoX&uP+U9^_6{rTFQh^1@G(e<18JZLVXWBBu(#I7nT7)2h%h$zawfcD`WzpR zM$&g~v#~q!*gE-iOhyp=xHSAHP{dcP8UYesJA?%Mgazghl#Y}(2Ngh)vb~D!1&Jj znjBo0iuujKjko`H_O2W+xPLftbF|o5b@r}H6**WIgMcb%F!#%jKJmNuJ~6>kt}$5o zaAd!MQtts$y$1vUSt^FzC?@&peIq6`{n^~eSN9YX5AN)z3q03_V)p;+UhE$Vg2cgx zB@UEOaGx`2zpqFd@(T1QTus}VVDa%Dp{${q;k4mC_O#$>dD?oc<-Draepb zX=2***({-yQDU$Yr}*fxs?VWM8-y+Z*(YGDli6Gb0c2mhQIi)d*KU;c+$~?h{{4kt zPnM!J_qh%7M|$h{gZy8vhyJO`)p+gd)Q*2@-6*HDZTIYU*KE}}3&$$*Vp&{-U@RJp z|K-t^(M~bqwetqX>zt=Bk1@ZSgW3aARDapUc|Dn*BYx_FHQw?(pggrUQj*YJ*o&^{GcU( zf>L_#6g%t%W1rvo2wV{?iLPQ>7(%uXhhLXoMJV5ql9RA^K?uEtP#RA7QOe6>nZ*gA z2P}mXIE8f2L^ndm&`CIN;sE#_9%a!weg@fS9jyzFyreG_h~-G`Q-J+}UT4TAmTe8s z>b5?dHW%}hWJ%64%-mEus~b5oJ7p7^)2TtMGx_UI+oGP&8@ahD{t)*ED9;v8x-b`13j-aa*#9?O_iEX55I~-yNEzm4uD{-ZvBp=5gj@^u| z^_Ov3d(t~v(ZOx>ZTF7e>Ac^+bt1g&3s-&NM<9@7RZIePNte1eBL}HtO6Jsk2tg6- z5Opu}uHikBfjmE&fZ}N&nb40w6W?+B1&Da%2qN$^CV?Z6dNalXbOV{Cz&mOKvrdBp zoM%G~CHB2sE<7CB9Q@VSx7Jk(eCfY6Uhb&-`tHd!B~}$2uWos@bGLCEm zf+83ollRSoC?Lyd_!QFQbGvgy6At|HoI>Z|99x>W|4maea9uyrr2p~`&k_}Zc<@o; zfmVt&Ig^_+M$V$3Kp7SEICHF{f-RJRbHo|lB94+aNhV{Y4U)4RX~F!IP)EvBbA~ul zA)hCdvc|WV8{n(g?Lx0PV-dOpWWzK>9V{mS4#-|!X(+3W<&~;w-ySVVU%dTYcSUOi z&Tok2lTY>ql&iT4;zP7wlQ&y|uDimYYV%NMe zu8enzL5a^Y-tJt*yu|#kIi?-RM7tNexM Date: Thu, 29 Oct 2026 06:01:51 -0400 Subject: [PATCH 06/22] Reuse statement reconstruction for eligible modern generator expressions ```python def select(values): return tuple((value, discarded, _) for value, _, discarded, discarded in values) ``` The legacy generator simulator replaces optimized unpacking targets with underscores. Here it emits `for value, _, _, _ in values` while leaving `discarded` in the yielded expression, so the recovered function raises NameError. The statement parser already reconstructs those targets, but its generator route requires LOAD_FAST_AND_CLEAR, an unrelated marker for an inlined comprehension inside the generator. Remove that marker requirement for Python 3.12+ generator code objects. Reuse the existing complete For/If/Yield extraction, restore the outer iterable at the call site, and retain the legacy fallback when the statement result is incomplete or contains unsupported statements. Keep the existing rejection of arbitrary statements in a generator tree. This also lets nested calls and multiple captured closure cells use the same parser without requiring an inner inline comprehension. Generator expressions date to Python 2.4; this change applies to the modern route already established for 3.12 and later. For the 3.12 layout, retain a multiline filter hint when its conditional jumps to the same loop's shared back edge. The statement-derived clauses must preserve this detail rather than changing already-correct bytecode layout in generators such as `value for value in values\n if condition`. Expanded calls now use their original direct spelling; the structural expectations follow that recovered form. --- src/decompiler.rs | 32 ++++++++++--- .../b_generator_statement_reconstruction.py | 42 ++++++++++++++++++ .../3.12.3/generator_multiline_filter_312.py | 3 ++ .../3.12.3/generator_multiline_filter_312.pyc | Bin 0 -> 538 bytes .../generator_statement_reconstruction.py | 17 +++++++ .../generator_statement_reconstruction.pyc | Bin 0 -> 2189 bytes .../generator_statement_reconstruction.py | 17 +++++++ .../generator_statement_reconstruction.pyc | Bin 0 -> 2215 bytes .../generator_statement_reconstruction.py | 17 +++++++ .../generator_statement_reconstruction.pyc | Bin 0 -> 2369 bytes .../generator_statement_reconstruction.py | 17 +++++++ .../generator_statement_reconstruction.pyc | Bin 0 -> 2439 bytes tests/data/generator_multiline_filter_312.py | 3 ++ .../generator_statement_reconstruction.py | 17 +++++++ tests/integration.rs | 35 ++++++++++++++- .../generator_multiline_filter_312.3.12.pyc | Bin 0 -> 538 bytes ...enerator_statement_reconstruction.3.12.pyc | Bin 0 -> 2189 bytes ...enerator_statement_reconstruction.3.13.pyc | Bin 0 -> 2215 bytes ...enerator_statement_reconstruction.3.14.pyc | Bin 0 -> 2369 bytes ...enerator_statement_reconstruction.3.15.pyc | Bin 0 -> 2439 bytes 20 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 tests/behavior/cases/b_generator_statement_reconstruction.py create mode 100644 tests/corpus/3.12.3/generator_multiline_filter_312.py create mode 100644 tests/corpus/3.12.3/generator_multiline_filter_312.pyc create mode 100644 tests/corpus/3.12.3/generator_statement_reconstruction.py create mode 100644 tests/corpus/3.12.3/generator_statement_reconstruction.pyc create mode 100644 tests/corpus/3.13.7/generator_statement_reconstruction.py create mode 100644 tests/corpus/3.13.7/generator_statement_reconstruction.pyc create mode 100644 tests/corpus/3.14.2/generator_statement_reconstruction.py create mode 100644 tests/corpus/3.14.2/generator_statement_reconstruction.pyc create mode 100644 tests/corpus/3.15.2/generator_statement_reconstruction.py create mode 100644 tests/corpus/3.15.2/generator_statement_reconstruction.pyc create mode 100644 tests/data/generator_multiline_filter_312.py create mode 100644 tests/data/generator_statement_reconstruction.py create mode 100644 tests/pyc/generator_multiline_filter_312.3.12.pyc create mode 100644 tests/pyc/generator_statement_reconstruction.3.12.pyc create mode 100644 tests/pyc/generator_statement_reconstruction.3.13.pyc create mode 100644 tests/pyc/generator_statement_reconstruction.3.14.pyc create mode 100644 tests/pyc/generator_statement_reconstruction.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index e3391348..9af853b0 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -51711,9 +51711,10 @@ 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. +// The main statement parser understands nested inline comprehensions, +// optimized unpack targets, and nested closure calls. Reuse its For/If/Yield +// tree for eligible 3.12+ generators, retaining the legacy simulator as a +// fallback when the statement result is incomplete or not a generator tree. fn generator_parts_from_body( body: &[Stmt], generators: &mut Vec, ) -> Option { @@ -52016,13 +52017,32 @@ 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 self.version.at_least(3, 12) && matches!(_kind, CompKind::Generator) { 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() { + // In 3.12, a filter on a separate source line jumps + // to the shared back edge instead of to the yield. + // Preserve the legacy walker's layout hint when the + // decoded loops map one-to-one to generator clauses. + if !self.version.at_least(3, 13) { + let loops: Vec<_> = instrs.iter() + .filter(|ins| ins.op == Op::FOR_ITER).collect(); + if loops.len() == generators.len() { + for (clause, loop_head) in generators.iter_mut().zip(loops.iter()) { + clause.if_line_break = !clause.ifs.is_empty() && instrs.iter().any(|ins| { + ins.op == Op::POP_JUMP_IF_FALSE + && ins.offset > loop_head.offset + && loop_head.target.map_or(false, |end| ins.offset < end) + && ins.target.and_then(|target| instrs.iter() + .find(|tail| tail.offset == target)) + .map_or(false, |tail| tail.op == Op::JUMP_BACKWARD + && tail.target == Some(loop_head.offset)) + }); + } + } + } generators[0].iter = outer_iter.clone(); return Some((element, None, generators)); } diff --git a/tests/behavior/cases/b_generator_statement_reconstruction.py b/tests/behavior/cases/b_generator_statement_reconstruction.py new file mode 100644 index 00000000..7f9e10f9 --- /dev/null +++ b/tests/behavior/cases/b_generator_statement_reconstruction.py @@ -0,0 +1,42 @@ +# MIN_VERSION: 3.12 +# Exercise the modern generator statement-parser route. + +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) + + +for values in ([], [(None, 1, 2, 3)], [(1, 2, 3, 4), (None, 5, 6, 7)], [(1, 2)], [None]): + for function in (repeated_targets, real_underscore): + try: + print(function.__name__, function(values)) + except (TypeError, ValueError) as error: + print(function.__name__, type(error).__name__, str(error)) + +for rows in ([], [[1, 2, 3], [5, 9], [3]]): + print(nested_calls(rows)) +print(captured_cells([1, 2, 4])) + + +def observed_rows(): + print("outer called") + for value in (1, None, 3): + print("yield", value) + yield (value, 1, 2, 3) + + +print(repeated_targets(observed_rows())) + diff --git a/tests/corpus/3.12.3/generator_multiline_filter_312.py b/tests/corpus/3.12.3/generator_multiline_filter_312.py new file mode 100644 index 00000000..b6d92ec8 --- /dev/null +++ b/tests/corpus/3.12.3/generator_multiline_filter_312.py @@ -0,0 +1,3 @@ +def multiline_filter(values, excluded): + return tuple(value for value in values + if value not in excluded) diff --git a/tests/corpus/3.12.3/generator_multiline_filter_312.pyc b/tests/corpus/3.12.3/generator_multiline_filter_312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..daf244908fb739caf434f680e8a444d2b8959b01 GIT binary patch literal 538 zcmZ8eJxc>Y5S_V8G-@Kz2r5_vQk{`R4O)d%B8ZU2PLO!qZ6vVyVDCLZ{8bbA0v@b6#(9}*hb>zVfCH3ztCfyQ6g1gF>r`@as(u!ttMLJe4hJFn!`?kKHt<~8M-gb?6}A!KA{>A^+^YwuVg430)m2i9Zg5}c4s%27(BO{AOiMNv zk2iY@HnVIiZR)n3-eVOebh|1FWry1(UaGJ{vBYgA3Yn$lRIT0{$x6*(P2`RegPK&9 zR}Gz8xs)%G)3D+M-4EzC;Tv!wdA;&HHG4DjG^PEhFgg7t?|v;gtCMJqdUk`hQ{zli z+@XHYmKc^U(ki|yLbSQj!DtbtYdJA%j)0P0$D$o+e6#)|H{#*sCt#2mt-OZ)3L#TP gM1u6~g|g+U>2Q`4<86hZNk}M_&pupRz3kB-WZL3b9lwff7TY!QRpBA*h2f~1B5QZZDJ>WVAu zPBm@uhzq{xw0;r97BQV?c(y=h$vxP|bIi3gI+@QHjEhv3vl9}Henbd_o#hm0;%`FfF=&b#oNJN`tlFL^Uw+i@Nvj&z^^Yw<=oH4&c)3Gz>i{vK-zA zG#3K5GpDZ|lVi_g%R02tz}3NQO^a5&(XuPrO&eseoR^<~a+}^(aM<0UCj4uxJ4&mv z>vUaNP+*dJOeCjVhSIEdHz#1Ws?V9NU%=t-6{p}AI!U&!tg8S-Zh1D8Znv#~HXQIa zs*OE|YB&I#x!#7|hu8|)P+Q(?()zlIw`&g%>`SZ1U};kq)|mx81tWaLo+8DfoSTg) zGM>_NW}YJ(*=cM+IZ{8$tZa){m6kX4J6jFXL>nnWS6Az{H OQhq?|9+f7jJpL~iMcPOJ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/generator_statement_reconstruction.py b/tests/corpus/3.13.7/generator_statement_reconstruction.py new file mode 100644 index 00000000..30975bc9 --- /dev/null +++ b/tests/corpus/3.13.7/generator_statement_reconstruction.py @@ -0,0 +1,17 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) + diff --git a/tests/corpus/3.13.7/generator_statement_reconstruction.pyc b/tests/corpus/3.13.7/generator_statement_reconstruction.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef7a1450b6c5a10480b6cd2178c7c60a8ea0e1ce GIT binary patch literal 2215 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 K4v{WVbNPS&rre?c literal 0 HcmV?d00001 diff --git a/tests/data/generator_multiline_filter_312.py b/tests/data/generator_multiline_filter_312.py new file mode 100644 index 00000000..b6d92ec8 --- /dev/null +++ b/tests/data/generator_multiline_filter_312.py @@ -0,0 +1,3 @@ +def multiline_filter(values, excluded): + return tuple(value for value in values + if value not in excluded) diff --git a/tests/data/generator_statement_reconstruction.py b/tests/data/generator_statement_reconstruction.py new file mode 100644 index 00000000..30975bc9 --- /dev/null +++ b/tests/data/generator_statement_reconstruction.py @@ -0,0 +1,17 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) + diff --git a/tests/integration.rs b/tests/integration.rs index d34c494b..7b9ede13 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -129,6 +129,29 @@ fn chained_comparison_ternaries_keep_collection_values_and_filters_distinct() { } } +#[test] +fn generator_statement_route_preserves_repeated_targets_and_closure_calls() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture( + &format!("generator_statement_reconstruction.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "value is not None for value, discarded, discarded, discarded in values", + "(value, discarded, _) for value, _, discarded, discarded in values", + "sum((value for value in row)) / len(row) for row in rows", + "sum((factory(deltas) for _ in deltas)) / len(deltas) for _ in range(3)", + ], version); + } +} + +#[test] +fn generator_statement_route_keeps_python312_multiline_filter_layout() { + let (src, clean) = decompile_fixture("generator_multiline_filter_312.3.12.pyc"); + assert!(clean, "{src}"); + assert_contains(&src, &["value for value in values\n if value not in excluded"], + "3.12 multiline filter"); +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); @@ -321,8 +344,16 @@ fn generator_expanded_calls() { &src, &[ "def positional(rows, factory):\n return tuple((factory(*row) for row in rows))", - "def keywords(rows, factory):\n return tuple((factory(*(), **{**dict(row)}) for row in rows))", - "def both(rows, factory):\n return tuple((factory(*row[0], **{**row[1]}) for row in rows))", + if version == "3.11" { + "def keywords(rows, factory):\n return tuple((factory(*(), **{**dict(row)}) for row in rows))" + } else { + "def keywords(rows, factory):\n return tuple((factory(**dict(row)) for row in rows))" + }, + if version == "3.11" { + "def both(rows, factory):\n return tuple((factory(*row[0], **{**row[1]}) for row in rows))" + } else { + "def both(rows, factory):\n return tuple((factory(*row[0], **row[1]) for row in rows))" + }, ], &fixture, ); diff --git a/tests/pyc/generator_multiline_filter_312.3.12.pyc b/tests/pyc/generator_multiline_filter_312.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..daf244908fb739caf434f680e8a444d2b8959b01 GIT binary patch literal 538 zcmZ8eJxc>Y5S_V8G-@Kz2r5_vQk{`R4O)d%B8ZU2PLO!qZ6vVyVDCLZ{8bbA0v@b6#(9}*hb>zVfCH3ztCfyQ6g1gF>r`@as(u!ttMLJe4hJFn!`?kKHt<~8M-gb?6}A!KA{>A^+^YwuVg430)m2i9Zg5}c4s%27(BO{AOiMNv zk2iY@HnVIiZR)n3-eVOebh|1FWry1(UaGJ{vBYgA3Yn$lRIT0{$x6*(P2`RegPK&9 zR}Gz8xs)%G)3D+M-4EzC;Tv!wdA;&HHG4DjG^PEhFgg7t?|v;gtCMJqdUk`hQ{zli z+@XHYmKc^U(ki|yLbSQj!DtbtYdJA%j)0P0$D$o+e6#)|H{#*sCt#2mt-OZ)3L#TP gM1u6~g|g+U>2Q`4<86hZNk}M_&pupRz3kB-WZL3b9lwff7TY!QRpBA*h2f~1B5QZZDJ>WVAu zPBm@uhzq{xw0;r97BQV?c(y=h$vxP|bIi3gI+@QHjEhv3vl9}Henbd_o#hm0;%`FfF=&b#oNJN`tlFL^Uw+i@Nvj&z^^Yw<=oH4&c)3Gz>i{vK-zA zG#3K5GpDZ|lVi_g%R02tz}3NQO^a5&(XuPrO&eseoR^<~a+}^(aM<0UCj4uxJ4&mv z>vUaNP+*dJOeCjVhSIEdHz#1Ws?V9NU%=t-6{p}AI!U&!tg8S-Zh1D8Znv#~HXQIa zs*OE|YB&I#x!#7|hu8|)P+Q(?()zlIw`&g%>`SZ1U};kq)|mx81tWaLo+8DfoSTg) zGM>_NW}YJ(*=cM+IZ{8$tZa){m6kX4J6jFXL>nnWS6Az{H OQhq?|9+f7jJpL~iMcPOJ literal 0 HcmV?d00001 diff --git a/tests/pyc/generator_statement_reconstruction.3.13.pyc b/tests/pyc/generator_statement_reconstruction.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef7a1450b6c5a10480b6cd2178c7c60a8ea0e1ce GIT binary patch literal 2215 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 K4v{WVbNPS&rre?c literal 0 HcmV?d00001 From 21d8f67ee1dba1754f6222c4cecafa52b9421ffb Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 06:06:30 -0400 Subject: [PATCH 07/22] Preserve Boolean generator values behind continue-shaped filters ```python def select(rows): return tuple(row[1] or 0 for row in rows if row[0]) ``` A generator filter can appear in the statement tree as an inverted `if condition: continue`, with the yield following it. Rejecting that tree sends the element through a fallback that confuses its Boolean value with another filter. Rows that should be rejected can produce values, and `and` can return the wrong operand. Recognize exactly one early Continue with no else as a rejection guard for the current generator clause, and invert its condition. Continue to reject guards outside a clause, after a yield, or mixed with extra statements. The yielded expression stays in the statement parser. When reconstructing a retained Boolean operand, look past NOP, NOT_TAKEN, and CACHE before checking its conditional jump. Inert padding must not hide the POP that consumes the retained operand. Generator expressions and these Boolean combinations date to Python 2.4. The affected modern route is available from 3.12; 3.14 adds NOT_TAKEN padding, and 3.15 uses the same reconstruction. Calls and truth hooks in a None-filtered getter expression retain their evaluation order. --- src/decompiler.rs | 42 +++++++++++- .../b_filtered_generator_boolean_values.py | 60 ++++++++++++++++++ .../filtered_generator_boolean_values.py | 18 ++++++ .../filtered_generator_boolean_values.pyc | Bin 0 -> 2091 bytes .../filtered_generator_boolean_values.py | 18 ++++++ .../filtered_generator_boolean_values.pyc | Bin 0 -> 2234 bytes .../filtered_generator_boolean_values.py | 18 ++++++ .../filtered_generator_boolean_values.pyc | Bin 0 -> 2722 bytes .../filtered_generator_boolean_values.py | 18 ++++++ .../filtered_generator_boolean_values.pyc | Bin 0 -> 2754 bytes .../data/filtered_generator_boolean_values.py | 18 ++++++ tests/integration.rs | 16 +++++ ...filtered_generator_boolean_values.3.12.pyc | Bin 0 -> 2091 bytes ...filtered_generator_boolean_values.3.13.pyc | Bin 0 -> 2234 bytes ...filtered_generator_boolean_values.3.14.pyc | Bin 0 -> 2722 bytes ...filtered_generator_boolean_values.3.15.pyc | Bin 0 -> 2754 bytes 16 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 tests/behavior/cases/b_filtered_generator_boolean_values.py create mode 100644 tests/corpus/3.12.3/filtered_generator_boolean_values.py create mode 100644 tests/corpus/3.12.3/filtered_generator_boolean_values.pyc create mode 100644 tests/corpus/3.13.7/filtered_generator_boolean_values.py create mode 100644 tests/corpus/3.13.7/filtered_generator_boolean_values.pyc create mode 100644 tests/corpus/3.14.2/filtered_generator_boolean_values.py create mode 100644 tests/corpus/3.14.2/filtered_generator_boolean_values.pyc create mode 100644 tests/corpus/3.15.2/filtered_generator_boolean_values.py create mode 100644 tests/corpus/3.15.2/filtered_generator_boolean_values.pyc create mode 100644 tests/data/filtered_generator_boolean_values.py create mode 100644 tests/pyc/filtered_generator_boolean_values.3.12.pyc create mode 100644 tests/pyc/filtered_generator_boolean_values.3.13.pyc create mode 100644 tests/pyc/filtered_generator_boolean_values.3.14.pyc create mode 100644 tests/pyc/filtered_generator_boolean_values.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 9af853b0..e50062c7 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -43172,7 +43172,12 @@ impl<'a> Ctx<'a> { && self .idx_of .get(&self.cur_offset) - .and_then(|&pi| (pi > 0).then(|| &self.instrs[pi - 1])) + // 3.14 inserts NOT_TAKEN after the conditional jump. + // It does not consume the retained operand: the first + // meaningful instruction in the arm is still its pop. + .and_then(|&pi| self.instrs[..pi].iter().rev().find(|p| { + !matches!(p.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE) + })) .map_or(false, |p| { matches!(p.op, Op::POP_JUMP_IF_FALSE | Op::POP_JUMP_IF_TRUE) && !p.is_backward @@ -51730,6 +51735,14 @@ fn generator_parts_from_body( result = generator_parts_from_body(body, generators); if result.is_none() { return None; } } + // A rejected iteration can be an inverted continue guard, + // followed by the yielded value. Keep the statement parser's + // expression instead of losing its COPY/TO_BOOL value merges. + Stmt::If { cond, body, orelse } + if orelse.is_empty() && matches!(body.as_slice(), [Stmt::Continue]) => { + if result.is_some() { return None; } + generators.last_mut()?.ifs.push(negate_cond(cond.clone())); + } Stmt::If { cond, body, orelse } if orelse.is_empty() => { if result.is_some() { return None; } generators.last_mut()?.ifs.push(cond.clone()); @@ -54103,6 +54116,33 @@ mod generator_parts_tests { } } + #[test] + fn extraction_accepts_only_an_early_continue_guard() { + let guard = Stmt::If { + cond: Rc::new(Expr::Name("skip".into())), + body: vec![Stmt::Continue], orelse: vec![], + }; + let mut accepted = yielded_loop(); + if let Stmt::For { body, .. } = &mut accepted { body.insert(0, guard.clone()); } + let mut clauses = Vec::new(); + assert!(generator_parts_from_body(&[accepted], &mut clauses).is_some()); + assert_eq!(clauses[0].ifs.len(), 1); + assert!(matches!(&*clauses[0].ifs[0], Expr::Unary { op: UnaryOp::Not, .. })); + assert!(generator_parts_from_body(&[guard.clone()], &mut Vec::new()).is_none()); + let mut late = yielded_loop(); + if let Stmt::For { body, .. } = &mut late { body.push(guard.clone()); } + assert!(generator_parts_from_body(&[late], &mut Vec::new()).is_none()); + for add_else in [false, true] { + let mut rejected = guard.clone(); + if let Stmt::If { body, orelse, .. } = &mut rejected { + if add_else { orelse.push(Stmt::Pass); } else { body.push(Stmt::Pass); } + } + let mut enclosing = yielded_loop(); + if let Stmt::For { body, .. } = &mut enclosing { body.insert(0, rejected); } + assert!(generator_parts_from_body(&[enclosing], &mut Vec::new()).is_none()); + } + } + #[test] fn extraction_preserves_one_yield_path_and_rejects_siblings() { let body = vec![yielded_loop()]; diff --git a/tests/behavior/cases/b_filtered_generator_boolean_values.py b/tests/behavior/cases/b_filtered_generator_boolean_values.py new file mode 100644 index 00000000..44c53633 --- /dev/null +++ b/tests/behavior/cases/b_filtered_generator_boolean_values.py @@ -0,0 +1,60 @@ +# MIN_VERSION: 3.12 +# The modern statement route preserves filters and yielded Boolean values. + +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) + + +for rows in ([], [(False, 3, 4)], [(True, 0, 2)], + [(True, 2, 3), (True, None, 4), (False, 5, 6)], + [(True, "", 7), (True, "kept", 8)]): + for function in (or_value, and_value, chained_or, mixed_value): + print(function.__name__, function(rows)) + + +events = [] + + +class Value: + def __init__(self, tag, truth): + self.tag = tag + self.truth = truth + + def __bool__(self): + events.append(("bool", self.tag)) + return self.truth + + +class Row: + def __init__(self, tag, value): + self.tag = tag + self.value = value + + def get(self, key): + events.append(("get", self.tag, key)) + return self.value + + +rows = [Row("missing", None), Row("zero", 0), Row("false", Value("false", False)), + Row("true", Value("true", True))] +result = [] +for value in none_filter(rows): + result.append(value.tag if isinstance(value, Value) else value) +print(result) +print(events) diff --git a/tests/corpus/3.12.3/filtered_generator_boolean_values.py b/tests/corpus/3.12.3/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/corpus/3.12.3/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/corpus/3.12.3/filtered_generator_boolean_values.pyc b/tests/corpus/3.12.3/filtered_generator_boolean_values.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b67ed1c9d14e2fc34aee9ecc7a737d57ff5d6c11 GIT binary patch literal 2091 zcmd5-L1-IS5dE{eaxHHhcH`PfqeM7z5r`7IMNpOEHC3Xdg@PbBBqeGv8|}t*khSjL zbz=t?Ovup&xrJE1=_nBbf?Qm3XwL0L()uKN>!rSx=u-RCnbk^BjMYGCpbPu>`{)0e zzwgb=|8;nHNMwvXuKW`EJlxcN$8q-of7FWLV4E525xd0l;MQ+`<1)Lfm zKb&$*+p(t%-O;D+F611Oo5tL{nK!xaSbXk|W#vpgKlh!UTQ=>W`v=j@ruu94i^;|PDSzt&kpNt8yaVm0|}pfFOnwl(%csQA&3p}LYdum^F` z!=Vsto(w?+ngM5dDQDt9qEp&*@^L44ro+8S5_z4*)}f1QN&><_roWk}wsC(~)=pVV}54^8xyP2f>%d`)MxY z#DjBs-ssCT?XxMXfFD9)DDof{1`Rt8s%)s@r%r!qS}pXHut{5&ojKEiUUdVNnxxpJx| zrQQn5RO@kkTlWyU8I6BP?ff2<-jT=X-VN)pG5V%8z7B%lWiDdQL%aPq2lPd2Iq#(B zI{{+Q6z5H+Pp1jzt-Lwcz6g$~f>Y?loF9m>4UJe4x5L^PnE6_sx8oWS%11RC9 Ag8%>k literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/filtered_generator_boolean_values.py b/tests/corpus/3.13.7/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/corpus/3.13.7/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/corpus/3.13.7/filtered_generator_boolean_values.pyc b/tests/corpus/3.13.7/filtered_generator_boolean_values.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25782ac0e5ffd9908735abbe30d5221bdd6972bf GIT binary patch literal 2234 zcmd6o&ui0Q7{}i?N$0jOZ7iF^>CApmSAT5kruEPcs#Wx0K{J0~g%O*q4!UN(>HGm6 z#FNL3Iqc9wkL&QDgMl7I|Aa~ggW%26ZemZn`8-KlGhGEi!9X6~yicCzdC4c==c%@} zwep0J)u#t8zN8(mzQDSEQEyf4g~b9<$X>#TLcvmn1IsBqSY8pp3fVxUDN~EUKml${ zT@8a-AnCdfGh8JhauW9OI_Y`(NrX#nNrB(fAFQ4~fu#aF86k2A(L>hfA>$EQSzvfrhW-uwiYeBF&pW_)(45b~ zMA)$Rsd=p-1B^2df^iwdm$j1lwr6$1l6qYT>r!v=Y>6(OUz2*5wHk`UbiVU2Wo;11 zc3}!1gO++d)(I{=q#r`OtXaXhCB`d4ob3kZKZB5ijp^}dHv9NY1 z_&XB&Gk4UGGNsUg_Vg zy3!b-^F1RCc(neg%JxJ07Odk_MdD4r6--!Sq9P>zUm#mm@t1&fPfb1m9E} z$(6CC%tz_{HA{-j_RXqu;q~2pMWrMyUR#$UOLR?&tc-24eGs+1GDKfF20ekGe*7qs zZ^o=((h`#uA^A^GVB2t}$s<_!v~dEpaRo*E)1}l$QwFC?2TqsYzxN9o&TC;Ko>r(k zb?hc}%9zO)QrTL`2(|gFUTDz9y7NX}A9v@^HtNAnVjczKHnKFb`f&Z-^^Z5KcnX1q zp6{94Q|yGs2CzyQ7nVC&+GF%ZPppA_L|;gry3}H?X)AaR{8qpWtedkoM9;Pq|fbFWfq_yLRlvM5m1Mgc9_;<~6(>S9WX1V|pY=Je87-y|x zdSL-Jq2TC+nUBrA#hD*QY%8oHJfqcO6{rZh5W1n;c3bQbC3QsQninM-v~8v z6yq8Wh89Nb*zh0f;32{{bTIg4I!NKyA;sRL+Cdc&1eSv~A~vb4W+BKb^yPbFVf@|C_Qv+XHt+)LGI9z&YP*iPpd z;}>xSewo&4;@8yx=2x)`GwmgU5lwT4iQq61Y=)W`#+Y~F=(maBA81kM8&H4j;ai)u~sh~9S9N{&0lw=Sk8JM9a zMlt?h%OHgsZ8!SY@lM)jeUbi~r!lSKL?k#2^XIEnGwLOK;z zviVrL z4jE*+Ev@9wsrx!EYNy6o=oe*KQrt%9~J9;wEVvccq0YX={R06bBhhB_ct)G))mEiWYmFMsmDi z)}iI*a!G|EVI)F*iR7c*UJ`m}b4ve&MVlznUO4wi&BX^K-t5}OPV7pkNK|5_eV(0p z``&oox4)UsPM+v-0l?|u_3!`OA25>GZeFC`s*2di<1ND}&_O0>iOiwpWEWbO%%kP6 z9UB43oou|tUgq&;;j3Su$>W{C+qo{2Yuv|>d>vo_2I0IN`vzR9j}Ai|-}}Th;|B1; z83=nuVEB;FrFtgJN{(a=W1wmdO&;fP9j;*OEau7MZ(IUc5$55o0eq-m=jy*{%sK!w zeE>JeJZbf~Oxk@0VIFQn-U}FY=51$|Yz4xuTckpsv3M1za27Xq!E#aUw$5RXc-9)v zsd`QyPboQNJiV03sZ>oZEUHiD}WC6 zz}QqqODY+CdaOCf!s{V4wFmq&xC<4&)VCFT?AiAG6e#o+VgSZ;GM7I-_J$U5jkLavt4k4^noKQziY$$m8^OdQS*CRXz}ch7c@%SR$KO_J!cX zRK3`f@4M-1EbU2_wx!D6WXr|BUnL zQ0DhuuFUDWP8}}u3sqt2AC`VFc}rQ!q8wLuQ%U45Lmk;?+fCiP_4?yS&MNoZx;%B{?1LAE;0dV#~`*9GW{_JgBZiW z=`ejUgOB}BFi7@x{@VXl42G7M?jQu+gx>ys9S&w~4ra^Ar^4>s&fK0L6-EonhIr6B zYV?j4ua|_)xdTBm1gT8+1?kajjlPL>?W_a7mb7#T-A)`qAigzg`r{4)afX1?Vfx|* zAAeO4U;<|E{_EK2pac_439d2$Jaoy>I8mt1DWIR70$%KSwD2-%N`IvN`f{~DLNJuaeoDS2MF!+Q`g5W!rSx=u-RCnbk^BjMYGCpbPu>`{)0e zzwgb=|8;nHNMwvXuKW`EJlxcN$8q-of7FWLV4E525xd0l;MQ+`<1)Lfm zKb&$*+p(t%-O;D+F611Oo5tL{nK!xaSbXk|W#vpgKlh!UTQ=>W`v=j@ruu94i^;|PDSzt&kpNt8yaVm0|}pfFOnwl(%csQA&3p}LYdum^F` z!=Vsto(w?+ngM5dDQDt9qEp&*@^L44ro+8S5_z4*)}f1QN&><_roWk}wsC(~)=pVV}54^8xyP2f>%d`)MxY z#DjBs-ssCT?XxMXfFD9)DDof{1`Rt8s%)s@r%r!qS}pXHut{5&ojKEiUUdVNnxxpJx| zrQQn5RO@kkTlWyU8I6BP?ff2<-jT=X-VN)pG5V%8z7B%lWiDdQL%aPq2lPd2Iq#(B zI{{+Q6z5H+Pp1jzt-Lwcz6g$~f>Y?loF9m>4UJe4x5L^PnE6_sx8oWS%11RC9 Ag8%>k literal 0 HcmV?d00001 diff --git a/tests/pyc/filtered_generator_boolean_values.3.13.pyc b/tests/pyc/filtered_generator_boolean_values.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25782ac0e5ffd9908735abbe30d5221bdd6972bf GIT binary patch literal 2234 zcmd6o&ui0Q7{}i?N$0jOZ7iF^>CApmSAT5kruEPcs#Wx0K{J0~g%O*q4!UN(>HGm6 z#FNL3Iqc9wkL&QDgMl7I|Aa~ggW%26ZemZn`8-KlGhGEi!9X6~yicCzdC4c==c%@} zwep0J)u#t8zN8(mzQDSEQEyf4g~b9<$X>#TLcvmn1IsBqSY8pp3fVxUDN~EUKml${ zT@8a-AnCdfGh8JhauW9OI_Y`(NrX#nNrB(fAFQ4~fu#aF86k2A(L>hfA>$EQSzvfrhW-uwiYeBF&pW_)(45b~ zMA)$Rsd=p-1B^2df^iwdm$j1lwr6$1l6qYT>r!v=Y>6(OUz2*5wHk`UbiVU2Wo;11 zc3}!1gO++d)(I{=q#r`OtXaXhCB`d4ob3kZKZB5ijp^}dHv9NY1 z_&XB&Gk4UGGNsUg_Vg zy3!b-^F1RCc(neg%JxJ07Odk_MdD4r6--!Sq9P>zUm#mm@t1&fPfb1m9E} z$(6CC%tz_{HA{-j_RXqu;q~2pMWrMyUR#$UOLR?&tc-24eGs+1GDKfF20ekGe*7qs zZ^o=((h`#uA^A^GVB2t}$s<_!v~dEpaRo*E)1}l$QwFC?2TqsYzxN9o&TC;Ko>r(k zb?hc}%9zO)QrTL`2(|gFUTDz9y7NX}A9v@^HtNAnVjczKHnKFb`f&Z-^^Z5KcnX1q zp6{94Q|yGs2CzyQ7nVC&+GF%ZPppA_L|;gry3}H?X)AaR{8qpWtedkoM9;Pq|fbFWfq_yLRlvM5m1Mgc9_;<~6(>S9WX1V|pY=Je87-y|x zdSL-Jq2TC+nUBrA#hD*QY%8oHJfqcO6{rZh5W1n;c3bQbC3QsQninM-v~8v z6yq8Wh89Nb*zh0f;32{{bTIg4I!NKyA;sRL+Cdc&1eSv~A~vb4W+BKb^yPbFVf@|C_Qv+XHt+)LGI9z&YP*iPpd z;}>xSewo&4;@8yx=2x)`GwmgU5lwT4iQq61Y=)W`#+Y~F=(maBA81kM8&H4j;ai)u~sh~9S9N{&0lw=Sk8JM9a zMlt?h%OHgsZ8!SY@lM)jeUbi~r!lSKL?k#2^XIEnGwLOK;z zviVrL z4jE*+Ev@9wsrx!EYNy6o=oe*KQrt%9~J9;wEVvccq0YX={R06bBhhB_ct)G))mEiWYmFMsmDi z)}iI*a!G|EVI)F*iR7c*UJ`m}b4ve&MVlznUO4wi&BX^K-t5}OPV7pkNK|5_eV(0p z``&oox4)UsPM+v-0l?|u_3!`OA25>GZeFC`s*2di<1ND}&_O0>iOiwpWEWbO%%kP6 z9UB43oou|tUgq&;;j3Su$>W{C+qo{2Yuv|>d>vo_2I0IN`vzR9j}Ai|-}}Th;|B1; z83=nuVEB;FrFtgJN{(a=W1wmdO&;fP9j;*OEau7MZ(IUc5$55o0eq-m=jy*{%sK!w zeE>JeJZbf~Oxk@0VIFQn-U}FY=51$|Yz4xuTckpsv3M1za27Xq!E#aUw$5RXc-9)v zsd`QyPboQNJiV03sZ>oZEUHiD}WC6 zz}QqqODY+CdaOCf!s{V4wFmq&xC<4&)VCFT?AiAG6e#o+VgSZ;GM7I-_J$U5jkLavt4k4^noKQziY$$m8^OdQS*CRXz}ch7c@%SR$KO_J!cX zRK3`f@4M-1EbU2_wx!D6WXr|BUnL zQ0DhuuFUDWP8}}u3sqt2AC`VFc}rQ!q8wLuQ%U45Lmk;?+fCiP_4?yS&MNoZx;%B{?1LAE;0dV#~`*9GW{_JgBZiW z=`ejUgOB}BFi7@x{@VXl42G7M?jQu+gx>ys9S&w~4ra^Ar^4>s&fK0L6-EonhIr6B zYV?j4ua|_)xdTBm1gT8+1?kajjlPL>?W_a7mb7#T-A)`qAigzg`r{4)afX1?Vfx|* zAAeO4U;<|E{_EK2pac_439d2$Jaoy>I8mt1DWIR70$%KSwD2-%N`IvN`f{~DLNJuaeoDS2MF!+Q`g5W Date: Thu, 29 Oct 2026 06:15:19 -0400 Subject: [PATCH 08/22] Reconstruct generator expressions in bounded expression simulation ```python def select(values, offset): return collect(value + offset for value in values) ``` The main parser can recover this generator, but the bounded expression simulator cannot build its function or instantiate it with the iterator. A guard or return-value reconstruction that delegates this expression to the simulator therefore fails even though the generator body is supported. Model MAKE_FUNCTION only for a code object named . For closure attributes, require a function and a tuple whose length matches its free variables. Keep the iterator expression, recognize the generator's call with zero explicit arguments, and extract its clauses from a complete statement-parser result. Substitute the actual outer iterable before returning the generator expression to the surrounding call. Ordinary function creation, malformed closures, and incomplete generator bodies remain rejected by this restricted simulator. GET_ITER also requires a value on the stack. Generator expressions date to Python 2.4. This implementation uses the MAKE_FUNCTION/SET_FUNCTION_ATTRIBUTE protocol introduced in Python 3.13, which is the earliest applicable encoding; it also handles 3.14 and 3.15. --- src/decompiler.rs | 38 ++++++++++ src/decompiler_stack_tests.rs | 65 ++++++++++++++++++ .../cases/b_bounded_generator_expressions.py | 38 ++++++++++ .../3.13.7/bounded_generator_expressions.py | 14 ++++ .../3.13.7/bounded_generator_expressions.pyc | Bin 0 -> 1461 bytes .../3.14.2/bounded_generator_expressions.py | 14 ++++ .../3.14.2/bounded_generator_expressions.pyc | Bin 0 -> 1488 bytes .../3.15.2/bounded_generator_expressions.py | 14 ++++ .../3.15.2/bounded_generator_expressions.pyc | Bin 0 -> 1527 bytes tests/data/bounded_generator_expressions.py | 14 ++++ .../bounded_generator_expressions.3.13.pyc | Bin 0 -> 1461 bytes .../bounded_generator_expressions.3.14.pyc | Bin 0 -> 1488 bytes .../bounded_generator_expressions.3.15.pyc | Bin 0 -> 1527 bytes 13 files changed, 197 insertions(+) create mode 100644 tests/behavior/cases/b_bounded_generator_expressions.py create mode 100644 tests/corpus/3.13.7/bounded_generator_expressions.py create mode 100644 tests/corpus/3.13.7/bounded_generator_expressions.pyc create mode 100644 tests/corpus/3.14.2/bounded_generator_expressions.py create mode 100644 tests/corpus/3.14.2/bounded_generator_expressions.pyc create mode 100644 tests/corpus/3.15.2/bounded_generator_expressions.py create mode 100644 tests/corpus/3.15.2/bounded_generator_expressions.pyc create mode 100644 tests/data/bounded_generator_expressions.py create mode 100644 tests/pyc/bounded_generator_expressions.3.13.pyc create mode 100644 tests/pyc/bounded_generator_expressions.3.14.pyc create mode 100644 tests/pyc/bounded_generator_expressions.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index e50062c7..486a65ec 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -29284,6 +29284,27 @@ return None; Op::PUSH_NULL => { st.push(self.name_expr("\u{0}null")); } + Op::MAKE_FUNCTION if self.version.at_least(3, 13) => { + let value = st.pop()?; + let Expr::Const(value) = &*value else { return None; }; + let PyObject::Code(code) = &**value else { return None; }; + if code.name != "" { return None; } + st.push(Rc::new(Expr::Function(Rc::new(FunctionDef { + name: code.name.clone(), code: code.clone(), params: Parameters::empty(), + decorators: Vec::new(), returns: None, is_async: false, type_params: Vec::new(), + })))); + } + Op::SET_FUNCTION_ATTRIBUTE if ins.arg == 8 => { + let function = st.pop()?; + let closure = st.pop()?; + let Expr::Function(definition) = &*function else { return None; }; + let Expr::Tuple(cells) = &*closure else { return None; }; + if cells.len() != definition.code.freevars.len() { return None; } + // Generator captures refer to the enclosing lexical + // cells; retain the function after checking their count. + st.push(function); + } + Op::GET_ITER => { st.last()?; } Op::COPY => { if arg == 0 || arg > st.len() { return None; @@ -29340,6 +29361,23 @@ return None; } else { arg }; + if self.version.at_least(3, 13) && ins.op == Op::CALL && n == 0 && st.len() >= 2 { + if let Expr::Function(definition) = &*st[st.len() - 2] { + if definition.code.name == "" { + let code = definition.code.clone(); + let outer = st.pop()?; + st.pop(); + let decoded = decompile(&code, self.version).ok().filter(|result| result.clean)?; + let mut generators = Vec::new(); + let element = generator_parts_from_body(&decoded.body, &mut generators)?; + generators.first_mut()?.iter = outer; + st.push(Rc::new(Expr::Comprehension { + kind: CompKind::Generator, elt: element, key: None, generators, + })); + continue; + } + } + } let mut args = Vec::new(); for _ in 0..n { args.push(pop1(&mut st)); diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index 236a612e..c1350058 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -249,3 +249,68 @@ fn store_fast_rejects_separate_evaluations_and_standalone_assignments() { assert!(ctx.sim_stack_region(0, 1, stack).is_none()); } } + +fn nested_code(code: &CodeObject, name: &str) -> Rc { + code.consts.iter().find_map(|value| match &**value { + PyObject::Code(code) if code.name == name => Some(code.clone()), + _ => None, + }).unwrap_or_else(|| panic!("missing code object {name}")) +} + +#[test] +fn bounded_generator_regions_preserve_iterables_and_captured_cells() { + for minor in 13..=15 { + let path = format!("{}/tests/pyc/bounded_generator_expressions.3.{minor}.pyc", + env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let version = module.version(); + for (name, expected) in [ + ("plain", "return collect((value for value in values))"), + ("captured", "return collect((value + offset for value in values))"), + ("multiple", "return collect(((value + first) * second for value in values))"), + ] { + let code = nested_code(&module.code, name); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let start = instrs.iter().position(|ins| ins.op == Op::LOAD_GLOBAL).unwrap(); + let end = instrs.iter().position(|ins| ins.op == Op::RETURN_VALUE).unwrap(); + let ctx = context(&code, version, instrs); + let result = ctx.sim_stack_region(start, end, vec![]) + .unwrap_or_else(|| panic!("3.{minor} {name}: generator construction rejected")); + assert_eq!(result.len(), 1, "3.{minor} {name}: {result:?}"); + let source = crate::codegen::generate(&[Stmt::Return(Some(result[0].clone()))], version, true); + assert_eq!(source.trim(), expected, "3.{minor} {name}"); + } + let ordinary = nested_code(&module.code, "ordinary_function"); + let instrs = crate::bytecode::decode_instructions(&ordinary, table_for(version).unwrap(), version); + let start = instrs.iter().position(|ins| ins.op == Op::LOAD_CONST).unwrap(); + let end = instrs.iter().position(|ins| ins.op == Op::RETURN_VALUE).unwrap(); + let ctx = context(&ordinary, version, instrs); + assert!(ctx.sim_stack_region(start, end, vec![]).is_none()); + } +} + +#[test] +fn bounded_generator_construction_rejects_invalid_closure_slots() { + let module = PycModule::from_bytes(include_bytes!( + "../tests/pyc/bounded_generator_expressions.3.13.pyc")).unwrap(); + let parent = nested_code(&module.code, "captured"); + let generator = nested_code(&parent, ""); + assert_eq!(generator.freevars, ["offset"]); + let constant = Rc::new(Expr::Const(Rc::new(PyObject::Code(generator)))); + let name = Rc::new(Expr::Name("offset".into())); + for closure in [name.clone(), Rc::new(Expr::Tuple(vec![])), + Rc::new(Expr::Tuple(vec![name.clone(), name.clone()]))] { + let ctx = context(&parent, module.version(), vec![ + instruction(Op::MAKE_FUNCTION, 0), instruction(Op::SET_FUNCTION_ATTRIBUTE, 8), + ]); + assert!(ctx.sim_stack_region(0, 2, vec![closure, constant.clone()]).is_none()); + } + let ctx = context(&parent, module.version(), vec![ + instruction(Op::MAKE_FUNCTION, 0), instruction(Op::SET_FUNCTION_ATTRIBUTE, 8), + ]); + let result = ctx.sim_stack_region(0, 2, + vec![Rc::new(Expr::Tuple(vec![name])), constant]).unwrap(); + assert!(matches!(&*result[0], Expr::Function(_))); + let ctx = context(&parent, module.version(), vec![instruction(Op::GET_ITER, 0)]); + assert!(ctx.sim_stack_region(0, 1, vec![]).is_none()); +} diff --git a/tests/behavior/cases/b_bounded_generator_expressions.py b/tests/behavior/cases/b_bounded_generator_expressions.py new file mode 100644 index 00000000..b601762d --- /dev/null +++ b/tests/behavior/cases/b_bounded_generator_expressions.py @@ -0,0 +1,38 @@ +# MIN_VERSION: 3.13 + +collect = tuple + +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) + + +for values in ([], [1, 2, 3], ["x"]): + for function, args in ((plain, (values,)), (captured, (values, 5)), + (multiple, (values, 5, 3))): + try: + print(function.__name__, function(*args)) + except TypeError as error: + print(function.__name__, type(error).__name__) + + +def values(): + print("outer") + for value in (1, 2): + print("yield", value) + yield value + + +print(captured(values(), 5)) +print(ordinary_function("unchanged")) diff --git a/tests/corpus/3.13.7/bounded_generator_expressions.py b/tests/corpus/3.13.7/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/corpus/3.13.7/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/corpus/3.13.7/bounded_generator_expressions.pyc b/tests/corpus/3.13.7/bounded_generator_expressions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..283b3e2712afb99b6d03d4d40f066c843fd3b837 GIT binary patch literal 1461 zcmbW1K~EDw6vtnOb zwrrPwz`Q$XIPRw~P2vc{hyo9a`gg$Wkf+@>j9nqa zWDfi|=E9Xpl986qNl38z03Lr5%mhh;3dlB-olm&l?IvGXHFbNg|laUz|gs>hkUfeNFZnzwE z)9I!-C<7_tjq6&?tuv!?0Zfb+$3F>Z<_{Zt#%}&i@vsr_d^t2i8SD?mWtZT^kX2CI z^#Ep~BhRkD66VE6z?=x6pVj<9qpjxK$$U%6Gc22q8Foe&=zZ|4(pBj^(iLe|`Y)&` zSCk@9`_G^*%UQ@WL*5GnC4x3)RvCA_I5%|1u81}R!sifg)h)MLvy2N6_dzOr z6ikS?bd>rK+kNzEaqr33;pwAP$a&;y$XO0JV+6Z~v7(~u9on6p*f^|VeFg1PLHmnZ z`j~2~rFOE^Qc3}BZ1B!NC477=5d2Y~U2nm~{`-FbgR$T~_6hz2CP{=56`dEoSc!tA zZEaMvqBr5NO4Zib_L^C@br&vp1Wou6_LHF5ByVqvY(-nM0rW?%>(C!sEDfKC&(Ji9 z*MDO#)lN>elqsRJlu^X(7A+}AiHJZRn=owM!j@C1TSk%H1Rw6m$H4rODW%5}rMcq- OrSoVOs1G(H#D4=)(-sf_ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/bounded_generator_expressions.py b/tests/corpus/3.14.2/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/corpus/3.14.2/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/corpus/3.14.2/bounded_generator_expressions.pyc b/tests/corpus/3.14.2/bounded_generator_expressions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..740ccff9d381ec83b3751c823661382f2fd1857a GIT binary patch literal 1488 zcmbW1&rcIU6vtHudNFb++LH-c_J?V*bj!?^z-^;v zFGeHYFrJ79y{i92wHlL+H&5OS^?*0uo82}=Au&#}-%j4leCPEuZzeM|I3N>3R<_0# zUO$?8_YL~pMuT2^3OX(+lXJu&WeQ5m5~x&`LFKZNBv=Po^NNBw3cL^nk7& zkhJ7UxvO4iTeTVnM~~FBmvId4@OfQz)cFm&QPT~5{k~B%nCjSU-Pox!gY&9gO@B?g;wwVUX{TRWp`-VeZ|1vi#Eg#T5uX7#fCTygtpg zEJJgk8}`%T_+L{D@+MwLc(b$6$0#2Kamm+c>;u}hV%b(S+f=e`2onwW>onX!kj+6y zrM+w`bde%oEAmf-(M1r(vnbFm<3VJ+Ce1;#(*oj@ z2!0qs^AFkdsYW^*Fm%Gq_!!8MAp0o>o~>BE70ox5Jj3(TFvm{ol3jp^N>%AN+lsU( z{SV#i;8u{jbRnNV>8{Chm}<}h*6~wWD?O}3W|eWr3v)xWYq}^uKz@w*X2Wu-b;~$` z`2cLmCqV+{rMK}np}iYVZ|_&WjAlNLX5PmG@)r{UdD$mF0gKpqTrN)8?C|ew#?Ted zT=4%T_YF3YNOLp{qqNX*0cAQ`zpi*{Ep_T>Tz6!6Ym&!P6zruZUztr8Br4q}O%iFE2I? zQ^YhBmw0Cq`|(zEwyDetpVhP??!Bl{0ZOd!?XfC@S}ttbdc!h`YzAWDk+=YzA2Ox% UP@*(<7@_nU$_o7nnil520je?-)&Kwi literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/bounded_generator_expressions.py b/tests/corpus/3.15.2/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/corpus/3.15.2/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/corpus/3.15.2/bounded_generator_expressions.pyc b/tests/corpus/3.15.2/bounded_generator_expressions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de3d049579d3ed02458cd2676feef26201e154e3 GIT binary patch literal 1527 zcmb7E&2JJx6rWi>N=s`kwb-b&G^V7qfqq3a4I!AQ2NN5q!Hb7>yX-1)%aWM|?QJ>f z-89z31L{TUMQ?um6Bslm8~*{177w-;eQ#J010Fib{PyL&ncuwqy*IOVY-qqm2zfiW z_j~H%(lZ6@_Mnc{2!Ub2Op`IfNty!Ev;-)nU4X6|eN!ajR$A*}C2_+<;zk(2f_b|g zMV1U%0w0bg#}5p&i(wfSG!_6X;JZn#f)yeYg70!4A%&6q#Of#Dxdzxz&JfIG*>#sz z>1vfkBwLCt*q)NC7ga97=5WZKGgWT#`K)Zp^P5I7pH;K#chtPfWYb{l>VpDPInNn+ z9xH6y{Rs?#FKLe#bUBxgC3HiPb)Jm1Bgo5-101~2zLPR}uI*-a&cDE*soERn-aWN0t zUI5!`Xc|{dj?r+)693M87&)!T1+&Q1Y!7{LD?b5nxK~1btEGy%8-J2~oP1qsYWyg8 z*24y1MMPpl@L>VmBI>O*1@^W084_Veg7KFS`Ewxts#Fie>;8Dn9cRN3RWQZYXp4+3 zyFoW33%;E)XQV~x=t8do%Zk)P=&^30*Icuhv@_bWpZu@v?he^KT8?qk_HtD*@>$W9 zp#DGdZxwYjSJ2fS@}sW&6u{w!Aj~&;o1OY6VDElP7 zCeEs^R|Nfdof_C@7R(HpA|3rnMQe6mMqfggw>GnK(mrRfY%VXe?RBk~S4?;mI`nZC z;hhzdmdVp;k+A4{Hi&*a+_~Z-U0iGuqS&q+p-k+-$x5*9pQ*WL1W&{*9t6>`LMySM r$&NkhbarCP$QE@q$wJ`614XfZxG1Ij5~ZnOb zwrrPwz`Q$XIPRw~P2vc{hyo9a`gg$Wkf+@>j9nqa zWDfi|=E9Xpl986qNl38z03Lr5%mhh;3dlB-olm&l?IvGXHFbNg|laUz|gs>hkUfeNFZnzwE z)9I!-C<7_tjq6&?tuv!?0Zfb+$3F>Z<_{Zt#%}&i@vsr_d^t2i8SD?mWtZT^kX2CI z^#Ep~BhRkD66VE6z?=x6pVj<9qpjxK$$U%6Gc22q8Foe&=zZ|4(pBj^(iLe|`Y)&` zSCk@9`_G^*%UQ@WL*5GnC4x3)RvCA_I5%|1u81}R!sifg)h)MLvy2N6_dzOr z6ikS?bd>rK+kNzEaqr33;pwAP$a&;y$XO0JV+6Z~v7(~u9on6p*f^|VeFg1PLHmnZ z`j~2~rFOE^Qc3}BZ1B!NC477=5d2Y~U2nm~{`-FbgR$T~_6hz2CP{=56`dEoSc!tA zZEaMvqBr5NO4Zib_L^C@br&vp1Wou6_LHF5ByVqvY(-nM0rW?%>(C!sEDfKC&(Ji9 z*MDO#)lN>elqsRJlu^X(7A+}AiHJZRn=owM!j@C1TSk%H1Rw6m$H4rODW%5}rMcq- OrSoVOs1G(H#D4=)(-sf_ literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_generator_expressions.3.14.pyc b/tests/pyc/bounded_generator_expressions.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..740ccff9d381ec83b3751c823661382f2fd1857a GIT binary patch literal 1488 zcmbW1&rcIU6vtHudNFb++LH-c_J?V*bj!?^z-^;v zFGeHYFrJ79y{i92wHlL+H&5OS^?*0uo82}=Au&#}-%j4leCPEuZzeM|I3N>3R<_0# zUO$?8_YL~pMuT2^3OX(+lXJu&WeQ5m5~x&`LFKZNBv=Po^NNBw3cL^nk7& zkhJ7UxvO4iTeTVnM~~FBmvId4@OfQz)cFm&QPT~5{k~B%nCjSU-Pox!gY&9gO@B?g;wwVUX{TRWp`-VeZ|1vi#Eg#T5uX7#fCTygtpg zEJJgk8}`%T_+L{D@+MwLc(b$6$0#2Kamm+c>;u}hV%b(S+f=e`2onwW>onX!kj+6y zrM+w`bde%oEAmf-(M1r(vnbFm<3VJ+Ce1;#(*oj@ z2!0qs^AFkdsYW^*Fm%Gq_!!8MAp0o>o~>BE70ox5Jj3(TFvm{ol3jp^N>%AN+lsU( z{SV#i;8u{jbRnNV>8{Chm}<}h*6~wWD?O}3W|eWr3v)xWYq}^uKz@w*X2Wu-b;~$` z`2cLmCqV+{rMK}np}iYVZ|_&WjAlNLX5PmG@)r{UdD$mF0gKpqTrN)8?C|ew#?Ted zT=4%T_YF3YNOLp{qqNX*0cAQ`zpi*{Ep_T>Tz6!6Ym&!P6zruZUztr8Br4q}O%iFE2I? zQ^YhBmw0Cq`|(zEwyDetpVhP??!Bl{0ZOd!?XfC@S}ttbdc!h`YzAWDk+=YzA2Ox% UP@*(<7@_nU$_o7nnil520je?-)&Kwi literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_generator_expressions.3.15.pyc b/tests/pyc/bounded_generator_expressions.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de3d049579d3ed02458cd2676feef26201e154e3 GIT binary patch literal 1527 zcmb7E&2JJx6rWi>N=s`kwb-b&G^V7qfqq3a4I!AQ2NN5q!Hb7>yX-1)%aWM|?QJ>f z-89z31L{TUMQ?um6Bslm8~*{177w-;eQ#J010Fib{PyL&ncuwqy*IOVY-qqm2zfiW z_j~H%(lZ6@_Mnc{2!Ub2Op`IfNty!Ev;-)nU4X6|eN!ajR$A*}C2_+<;zk(2f_b|g zMV1U%0w0bg#}5p&i(wfSG!_6X;JZn#f)yeYg70!4A%&6q#Of#Dxdzxz&JfIG*>#sz z>1vfkBwLCt*q)NC7ga97=5WZKGgWT#`K)Zp^P5I7pH;K#chtPfWYb{l>VpDPInNn+ z9xH6y{Rs?#FKLe#bUBxgC3HiPb)Jm1Bgo5-101~2zLPR}uI*-a&cDE*soERn-aWN0t zUI5!`Xc|{dj?r+)693M87&)!T1+&Q1Y!7{LD?b5nxK~1btEGy%8-J2~oP1qsYWyg8 z*24y1MMPpl@L>VmBI>O*1@^W084_Veg7KFS`Ewxts#Fie>;8Dn9cRN3RWQZYXp4+3 zyFoW33%;E)XQV~x=t8do%Zk)P=&^30*Icuhv@_bWpZu@v?he^KT8?qk_HtD*@>$W9 zp#DGdZxwYjSJ2fS@}sW&6u{w!Aj~&;o1OY6VDElP7 zCeEs^R|Nfdof_C@7R(HpA|3rnMQe6mMqfggw>GnK(mrRfY%VXe?RBk~S4?;mI`nZC z;hhzdmdVp;k+A4{Hi&*a+_~Z-U0iGuqS&q+p-k+-$x5*9pQ*WL1W&{*9t6>`LMySM r$&NkhbarCP$QE@q$wJ`614XfZxG1Ij5~Z Date: Thu, 29 Oct 2026 06:22:08 -0400 Subject: [PATCH 09/22] Keep generator assignment expressions bound to their enclosing cells ```python def select(rows): return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) ``` The generator statement tree contains a nonlocal declaration for change. Rejecting that declaration discards the otherwise reconstructed Boolean expression and falls back to a parser that loses its short-circuiting. A non-dictionary row can then reach row.get(), raising an exception that the original expression avoids. The outer cell must also retain the last assigned value after the generator is consumed. Pass the code object's actual free-variable names to the generator-body extractor. Omit a nonlocal declaration only when every name belongs to those enclosing cells; the assignment expression still retains its store. Preserve the existing rejection of globals and unrelated declarations. Apply the same scope check to ordinary generator reconstruction and to the bounded expression simulator. Assignment expressions start in Python 3.8. This repair applies to the modern generator route from 3.12 onward; the simulator uses the same check on its supported construction encodings. --- src/decompiler.rs | 48 +++++++++++++----- .../cases/b_generator_walrus_cells.py | 36 +++++++++++++ tests/corpus/3.12.3/generator_walrus_cells.py | 8 +++ .../corpus/3.12.3/generator_walrus_cells.pyc | Bin 0 -> 1074 bytes tests/corpus/3.13.7/generator_walrus_cells.py | 8 +++ .../corpus/3.13.7/generator_walrus_cells.pyc | Bin 0 -> 1110 bytes tests/corpus/3.14.2/generator_walrus_cells.py | 8 +++ .../corpus/3.14.2/generator_walrus_cells.pyc | Bin 0 -> 1269 bytes tests/corpus/3.15.2/generator_walrus_cells.py | 8 +++ .../corpus/3.15.2/generator_walrus_cells.pyc | Bin 0 -> 1285 bytes tests/data/generator_walrus_cells.py | 8 +++ tests/integration.rs | 14 +++++ tests/pyc/generator_walrus_cells.3.12.pyc | Bin 0 -> 1074 bytes tests/pyc/generator_walrus_cells.3.13.pyc | Bin 0 -> 1110 bytes tests/pyc/generator_walrus_cells.3.14.pyc | Bin 0 -> 1269 bytes tests/pyc/generator_walrus_cells.3.15.pyc | Bin 0 -> 1285 bytes 16 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 tests/behavior/cases/b_generator_walrus_cells.py create mode 100644 tests/corpus/3.12.3/generator_walrus_cells.py create mode 100644 tests/corpus/3.12.3/generator_walrus_cells.pyc create mode 100644 tests/corpus/3.13.7/generator_walrus_cells.py create mode 100644 tests/corpus/3.13.7/generator_walrus_cells.pyc create mode 100644 tests/corpus/3.14.2/generator_walrus_cells.py create mode 100644 tests/corpus/3.14.2/generator_walrus_cells.pyc create mode 100644 tests/corpus/3.15.2/generator_walrus_cells.py create mode 100644 tests/corpus/3.15.2/generator_walrus_cells.pyc create mode 100644 tests/data/generator_walrus_cells.py create mode 100644 tests/pyc/generator_walrus_cells.3.12.pyc create mode 100644 tests/pyc/generator_walrus_cells.3.13.pyc create mode 100644 tests/pyc/generator_walrus_cells.3.14.pyc create mode 100644 tests/pyc/generator_walrus_cells.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 486a65ec..d83d047c 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -29369,7 +29369,7 @@ return None; st.pop(); let decoded = decompile(&code, self.version).ok().filter(|result| result.clean)?; let mut generators = Vec::new(); - let element = generator_parts_from_body(&decoded.body, &mut generators)?; + let element = generator_parts_from_body(&decoded.body, &mut generators, &code.freevars)?; generators.first_mut()?.iter = outer; st.push(Rc::new(Expr::Comprehension { kind: CompKind::Generator, elt: element, key: None, generators, @@ -51759,7 +51759,7 @@ impl<'a> Ctx<'a> { // tree for eligible 3.12+ generators, retaining the legacy simulator as a // fallback when the statement result is incomplete or not a generator tree. fn generator_parts_from_body( - body: &[Stmt], generators: &mut Vec, + body: &[Stmt], generators: &mut Vec, freevars: &[String], ) -> Option { let mut result = None; for statement in body { @@ -51770,7 +51770,7 @@ fn generator_parts_from_body( target: target.clone(), iter: iter.clone(), ifs: Vec::new(), is_async: *is_async, if_line_break: false, }); - result = generator_parts_from_body(body, generators); + result = generator_parts_from_body(body, generators, freevars); if result.is_none() { return None; } } // A rejected iteration can be an inverted continue guard, @@ -51784,7 +51784,7 @@ fn generator_parts_from_body( 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); + result = generator_parts_from_body(body, generators, freevars); if result.is_none() { return None; } } Stmt::Expr(value) => { @@ -51797,6 +51797,10 @@ fn generator_parts_from_body( _ => return None, } } + // Generator walrus targets bind enclosing cells. Their Named + // expressions retain the stores; the parent supplies the scope. + // Never discard global or unrelated nonlocal declarations. + Stmt::Nonlocal(names) if names.iter().all(|name| freevars.contains(name)) => {}, Stmt::Pass | Stmt::Return(None) => {}, Stmt::Return(Some(value)) if matches!(&**value, Expr::Const(object) if matches!(&**object, PyObject::None)) => {}, @@ -52071,7 +52075,7 @@ fn genexpr_ternary_merge( if self.version.at_least(3, 12) && matches!(_kind, CompKind::Generator) { 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 let Some(element) = generator_parts_from_body(&statements.body, &mut generators, &code.freevars) { if !generators.is_empty() { // In 3.12, a filter on a separate source line jumps // to the shared back edge instead of to the yield. @@ -54154,6 +54158,22 @@ mod generator_parts_tests { } } + #[test] + fn extraction_omits_only_declarations_for_real_enclosing_cells() { + let declaration = Stmt::Nonlocal(vec!["change".into()]); + let body = vec![declaration.clone(), yielded_loop()]; + assert!(generator_parts_from_body(&body, &mut Vec::new(), &["change".into()]).is_some()); + assert!(generator_parts_from_body(&body, &mut Vec::new(), &[]).is_none()); + for declaration in [ + Stmt::Global(vec!["change".into()]), + Stmt::Nonlocal(vec!["change".into(), "unrelated".into()]), + ] { + assert!(generator_parts_from_body( + &[declaration, yielded_loop()], &mut Vec::new(), &["change".into()], + ).is_none()); + } + } + #[test] fn extraction_accepts_only_an_early_continue_guard() { let guard = Stmt::If { @@ -54163,13 +54183,13 @@ mod generator_parts_tests { let mut accepted = yielded_loop(); if let Stmt::For { body, .. } = &mut accepted { body.insert(0, guard.clone()); } let mut clauses = Vec::new(); - assert!(generator_parts_from_body(&[accepted], &mut clauses).is_some()); + assert!(generator_parts_from_body(&[accepted], &mut clauses, &[]).is_some()); assert_eq!(clauses[0].ifs.len(), 1); assert!(matches!(&*clauses[0].ifs[0], Expr::Unary { op: UnaryOp::Not, .. })); - assert!(generator_parts_from_body(&[guard.clone()], &mut Vec::new()).is_none()); + assert!(generator_parts_from_body(&[guard.clone()], &mut Vec::new(), &[]).is_none()); let mut late = yielded_loop(); if let Stmt::For { body, .. } = &mut late { body.push(guard.clone()); } - assert!(generator_parts_from_body(&[late], &mut Vec::new()).is_none()); + assert!(generator_parts_from_body(&[late], &mut Vec::new(), &[]).is_none()); for add_else in [false, true] { let mut rejected = guard.clone(); if let Stmt::If { body, orelse, .. } = &mut rejected { @@ -54177,7 +54197,7 @@ mod generator_parts_tests { } let mut enclosing = yielded_loop(); if let Stmt::For { body, .. } = &mut enclosing { body.insert(0, rejected); } - assert!(generator_parts_from_body(&[enclosing], &mut Vec::new()).is_none()); + assert!(generator_parts_from_body(&[enclosing], &mut Vec::new(), &[]).is_none()); } } @@ -54185,14 +54205,14 @@ mod generator_parts_tests { 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(); + 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()); + &[]).is_none()); let branch = Stmt::If { cond: Rc::new(Expr::Name("ready".into())), @@ -54205,7 +54225,7 @@ mod generator_parts_tests { if let Stmt::For { body, .. } = &mut body { *body = vec![branch.clone(), branch]; } - assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + assert!(generator_parts_from_body(&[body], &mut Vec::new(), &[]).is_none()); } #[test] @@ -54218,12 +54238,12 @@ mod generator_parts_tests { }; assert!(generator_parts_from_body( &[assignment, yielded_loop()], &mut Vec::new(), - ).is_none()); + &[]).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()); + assert!(generator_parts_from_body(&[body], &mut Vec::new(), &[]).is_none()); } } diff --git a/tests/behavior/cases/b_generator_walrus_cells.py b/tests/behavior/cases/b_generator_walrus_cells.py new file mode 100644 index 00000000..a8202cf0 --- /dev/null +++ b/tests/behavior/cases/b_generator_walrus_cells.py @@ -0,0 +1,36 @@ +# MIN_VERSION: 3.12 +# Walrus syntax starts in 3.8; this tests the modern generator route. + +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change + + +for rows in ([], [None], [{}], [{"change": None}], [{"change": -1}], + [{"change": 0}, {"change": 7}, {"change": -3}, None], + [{"change": "invalid"}]): + try: + print(select(rows)) + except TypeError as error: + print(type(error).__name__, str(error)) + +for rows in ([], [{}], [{"change": 1}, {"change": 7}], [{"change": None}]): + print(last_binding(rows)) + + +events = [] + + +class Row(dict): + def get(self, key): + events.append(key) + return dict.get(self, key) + + +print(select([None, Row(change=None), Row(change=0), Row(change=7)])) +print(events) diff --git a/tests/corpus/3.12.3/generator_walrus_cells.py b/tests/corpus/3.12.3/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.12.3/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.12.3/generator_walrus_cells.pyc b/tests/corpus/3.12.3/generator_walrus_cells.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c8c69c9b3442b69fe4f1fa39050dcc47eff57d8 GIT binary patch literal 1074 zcmZuw&rcIU6rML*p!_Ilsc1kAjWH&Tq%>*_i3GKhaxiI3L=%msS$CJRy0*>Ct}Ub_ zO*8?F2M#KD(t`&HF~O7gH+WfC4rIg4i{6aAAQyeJT?!~KnYVBEd-L9#`M&)c3bjdu zklIT6%b(J%=5H7`4E(4T#$bV{?R;{RHcvZ5%L-q0LY6A0Wt|6r4qy?w^^Bx^EoZ^LzKezHl6Ev zZpwK@a|5cbn68x5%q7XCiKHvBLWzk>;NliyvFgrgQ{yHdP-Rmd$Z2_v$!39NOR~W% zo>er%;ECeoPV11s%0)Ij)x$MIQ_RGWQBY)q4<{OsI3K@?Zu>jgu9EezW$VVv-ok)ND`JDTdTb*z&j@tdBAJX=XG5hQo0`{G(t*C2vR41aYg{|LQjQDi$5-fq+ zWLiZ-i&Hm%v-A4!qEqY&>%fQGT%U(U@I}zYyU}6$t@f~k6BNyju;sK{cEGA3Kn}3j zki#G%GZe*xx4y^TEJaDs!zD2d6_}&r6r~|$+!j5ro4RcLSKZUd2v*;xkz+z5$bUn6 zVy?O!61}clFvrR{@x5v zN`%Vm-Ku0WbhMh@>Wn(k)auwqH05-rs-v6jUGwSL%XXlbbzyEMPCYKY%}d1l<{fS) hIP|!nT81{vdN9x9T8#5k0)Sr>;0e@%5*+t-`~^IW^YH)x literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/generator_walrus_cells.py b/tests/corpus/3.13.7/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.13.7/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.13.7/generator_walrus_cells.pyc b/tests/corpus/3.13.7/generator_walrus_cells.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a99f589253e50ecad8d47a07ee7bfbc158792c2 GIT binary patch literal 1110 zcmZ`(K~EDw6rP!Fp-_q~6%BBpATczW(nQb%p-4%9gEoe?B!q-!-CdV8%eI->rEqJ) z#Ulqu^rQz5l6b(A%CA^#6Ul~~Ubqo^ftzo(rD;)LGH>6`_uiY?_kG!FAkZlwght07 z|Jp`F%ReD*7_LXvAO!m;jrtKqX$&5#Bq~T(I8XDy;*F{?aBFxUtv1js9>=`ul`o6J zXaaC<6Low9MMnKY}tOO(s7fw2Ah;5%g>ZAwTS@_>u4uM+K)%S{DsfNuBY= z@ZPC0&0wZzNQ&c;HOX`YRWT0)hs5HJK=Vb)2gLYv7DsnI*S#yosKDvZ53$3>0^qnNM>wG&r71t60ruvznjg0yEsE7WCyM91=H5_ zz5X|IA8wXIcYlQBL+Ok9S^c_Rez;)YU-&q0KhN0rG62{sIa`vCSL8}ahO$f627PvB zy|X@ifE}-C6?7#^91q-|)joiq7I{Lu;Ni5@?nHaw1;b*!;4Vt&)E}$_t>c1O4q9cu zWqYhD0H}rIhFm5AStB4W;Gv0G0+J#BlZ=70I77w>NJmIH?V6#PnyCNP-__6}^l#D7 zZG`fnC4S#))~bm*?|m)26L$!yD5gFrrXAp}D0N3d1F+)^>LN39FEm5e4D}z3wezwF zxEf>O^`AhcUeYK~$h2csT7;VU}*z38^Wn z&PZ4D$rSF6xc1pFIN;?ZaH}+b+8wThlixDsaI)N;EG?Y)dv@nv&)A-R8iuq~l(>Al o#RtgUtFi5y+Q`e6t|aIH_oG5$?3-o#a(fCt=)KlOR$ssI20 literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/generator_walrus_cells.py b/tests/corpus/3.14.2/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.14.2/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.14.2/generator_walrus_cells.pyc b/tests/corpus/3.14.2/generator_walrus_cells.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37df60df591bc58085c2e56e6a9d0b1d7b3b4613 GIT binary patch literal 1269 zcmaKr-Afcv6u{4&S=Vpdbu;mUwls{zSQm67aM3haOG#{976eJAnPDB6cDFk-X)iG# zfq|e81@>eQJ!JG?PZjk?jEs=Xiw`~Z7HtpuROil&o0fTD&OLMQIrp4%&+qm&HP-O} z5O}lu{IT%s$yd}XgZNsx85I*U&@>9|2O${nthIB)cSI8#k1jTuwke4T8yi=!WNdkt4*36tGO4~EiPFS)_`hj)^Ph093sUsVmC_Xo#*d$Js zZj@9x#yJ5jNAv1o!F#UL2ArIrmm+1Du^wrx6Z>OYCO zaNfkkj=+h};z4+-0w*iT8^=-eVQM=L6J_Pb2(7`1)}lBh#l2eEh$~K16*PVBo>GvM zLjD90s+rpb6dnkX(T~|Jabw}d!plWFny^|DYq_14#HZ|m#)2(Hy9i~Cj`1{%lT%G) z%}{l}u^y;?XB&=2rkFaoFegyK{_ZxOGt+xbkrjD|#k3=%tMb!|Za0mZW4rauYw6`- zE6`3O=rkGQTuR)dHo;x<7^+F#FG_|g577>^b14d;@|`Dy{NM= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.15.2/generator_walrus_cells.pyc b/tests/corpus/3.15.2/generator_walrus_cells.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d325ca8def56403d2b9fb638c146a8322915c197 GIT binary patch literal 1285 zcmaKr&1(}u6u{r?=Brj?Vr|u^q@osslBO*!R7%w-sTHM7O#~?t*WD#qTsL86V(M+Z z1rhY%K~H+{V8Mf)`~v@m)hNnddiJRCU_JRJo5V(n1M}wXn>RD>vrI562)+#)!D@dMFR$ZJ#)t!Sfg7bAEt6*as7V0}$WTbG-d7#z1;v2;fx{IGayV z949u$bP+PB596ygu4hgHl*w{8?mStR(A*m1ZEczyZd=rYUc7m-fHD0$ve1cRnpo0e zaGq>9iIIp*m!vB&^&FrEb|2vPxwMy?sNaNKBCEs=s3pZJ>IvE1VlcyET(MN$^=XF6 zUAd_9RoRu2DOaZD8WlBWqR1n|wtI-{jI&Wq;mT-Hw{)uTGA+~;lU7+l)lHKnE4SRf z8L_ugp|kEDrklFTlQU*nRZKRU>~4w7**uEhA1nMD+=dSo?il8S*u&FLcQ&FUU!s~5 z)n2HtiZ6?=OO2^H=h)n{jI)??j^?~Q=W@YOwGWrI&rz*W;pJH0N&9he172h_iXWmkEP>+JZCLA%+@1UssX7(jh*a-Q)+Zfbwf@)A z=&z!oiO$f&fv8i9MUKi<$wrw4OZpFw67q;-|DVV6+mXjjJUbkeNuDf8W0Lqc+@N6@ z+)&J(zywAuKy#>U3g~WtN_3$MD7v*rFK%6+V!c37k#>U6ATBh7qJvAT&~z(W+_3^m#D#F1OA^nd`$x{4|V8 zLZ%U1+#uCi)#U9id$6|KymjNl6f*@;50*lK&HlP=$j+>V2kz>h2GXx{je+z%{Z3bJ zgwyui*Zv*%GxZaWFGhFZp@h^TrghUQv`ijPp_v= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/integration.rs b/tests/integration.rs index 04edab9c..a64c18f7 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -168,6 +168,20 @@ fn filtered_generators_keep_boolean_values_separate_from_rejection_guards() { } } +#[test] +fn generator_walrus_keeps_its_parent_cell_and_short_circuit_value() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("generator_walrus_cells.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "isinstance(row, dict) and (change := row.get('change')) is not None and change >= 0", + "change = 'initial'", "return results, change", + ], version); + assert_eq!(src.matches("change :=").count(), 2, "{version}: {src}"); + assert!(!src.contains("nonlocal "), "{version}: declaration escaped its generator:\n{src}"); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/generator_walrus_cells.3.12.pyc b/tests/pyc/generator_walrus_cells.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c8c69c9b3442b69fe4f1fa39050dcc47eff57d8 GIT binary patch literal 1074 zcmZuw&rcIU6rML*p!_Ilsc1kAjWH&Tq%>*_i3GKhaxiI3L=%msS$CJRy0*>Ct}Ub_ zO*8?F2M#KD(t`&HF~O7gH+WfC4rIg4i{6aAAQyeJT?!~KnYVBEd-L9#`M&)c3bjdu zklIT6%b(J%=5H7`4E(4T#$bV{?R;{RHcvZ5%L-q0LY6A0Wt|6r4qy?w^^Bx^EoZ^LzKezHl6Ev zZpwK@a|5cbn68x5%q7XCiKHvBLWzk>;NliyvFgrgQ{yHdP-Rmd$Z2_v$!39NOR~W% zo>er%;ECeoPV11s%0)Ij)x$MIQ_RGWQBY)q4<{OsI3K@?Zu>jgu9EezW$VVv-ok)ND`JDTdTb*z&j@tdBAJX=XG5hQo0`{G(t*C2vR41aYg{|LQjQDi$5-fq+ zWLiZ-i&Hm%v-A4!qEqY&>%fQGT%U(U@I}zYyU}6$t@f~k6BNyju;sK{cEGA3Kn}3j zki#G%GZe*xx4y^TEJaDs!zD2d6_}&r6r~|$+!j5ro4RcLSKZUd2v*;xkz+z5$bUn6 zVy?O!61}clFvrR{@x5v zN`%Vm-Ku0WbhMh@>Wn(k)auwqH05-rs-v6jUGwSL%XXlbbzyEMPCYKY%}d1l<{fS) hIP|!nT81{vdN9x9T8#5k0)Sr>;0e@%5*+t-`~^IW^YH)x literal 0 HcmV?d00001 diff --git a/tests/pyc/generator_walrus_cells.3.13.pyc b/tests/pyc/generator_walrus_cells.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a99f589253e50ecad8d47a07ee7bfbc158792c2 GIT binary patch literal 1110 zcmZ`(K~EDw6rP!Fp-_q~6%BBpATczW(nQb%p-4%9gEoe?B!q-!-CdV8%eI->rEqJ) z#Ulqu^rQz5l6b(A%CA^#6Ul~~Ubqo^ftzo(rD;)LGH>6`_uiY?_kG!FAkZlwght07 z|Jp`F%ReD*7_LXvAO!m;jrtKqX$&5#Bq~T(I8XDy;*F{?aBFxUtv1js9>=`ul`o6J zXaaC<6Low9MMnKY}tOO(s7fw2Ah;5%g>ZAwTS@_>u4uM+K)%S{DsfNuBY= z@ZPC0&0wZzNQ&c;HOX`YRWT0)hs5HJK=Vb)2gLYv7DsnI*S#yosKDvZ53$3>0^qnNM>wG&r71t60ruvznjg0yEsE7WCyM91=H5_ zz5X|IA8wXIcYlQBL+Ok9S^c_Rez;)YU-&q0KhN0rG62{sIa`vCSL8}ahO$f627PvB zy|X@ifE}-C6?7#^91q-|)joiq7I{Lu;Ni5@?nHaw1;b*!;4Vt&)E}$_t>c1O4q9cu zWqYhD0H}rIhFm5AStB4W;Gv0G0+J#BlZ=70I77w>NJmIH?V6#PnyCNP-__6}^l#D7 zZG`fnC4S#))~bm*?|m)26L$!yD5gFrrXAp}D0N3d1F+)^>LN39FEm5e4D}z3wezwF zxEf>O^`AhcUeYK~$h2csT7;VU}*z38^Wn z&PZ4D$rSF6xc1pFIN;?ZaH}+b+8wThlixDsaI)N;EG?Y)dv@nv&)A-R8iuq~l(>Al o#RtgUtFi5y+Q`e6t|aIH_oG5$?3-o#a(fCt=)KlOR$ssI20 literal 0 HcmV?d00001 diff --git a/tests/pyc/generator_walrus_cells.3.14.pyc b/tests/pyc/generator_walrus_cells.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37df60df591bc58085c2e56e6a9d0b1d7b3b4613 GIT binary patch literal 1269 zcmaKr-Afcv6u{4&S=Vpdbu;mUwls{zSQm67aM3haOG#{976eJAnPDB6cDFk-X)iG# zfq|e81@>eQJ!JG?PZjk?jEs=Xiw`~Z7HtpuROil&o0fTD&OLMQIrp4%&+qm&HP-O} z5O}lu{IT%s$yd}XgZNsx85I*U&@>9|2O${nthIB)cSI8#k1jTuwke4T8yi=!WNdkt4*36tGO4~EiPFS)_`hj)^Ph093sUsVmC_Xo#*d$Js zZj@9x#yJ5jNAv1o!F#UL2ArIrmm+1Du^wrx6Z>OYCO zaNfkkj=+h};z4+-0w*iT8^=-eVQM=L6J_Pb2(7`1)}lBh#l2eEh$~K16*PVBo>GvM zLjD90s+rpb6dnkX(T~|Jabw}d!plWFny^|DYq_14#HZ|m#)2(Hy9i~Cj`1{%lT%G) z%}{l}u^y;?XB&=2rkFaoFegyK{_ZxOGt+xbkrjD|#k3=%tMb!|Za0mZW4rauYw6`- zE6`3O=rkGQTuR)dHo;x<7^+F#FG_|g577>^b14d;@|`Dy{NMRD>vrI562)+#)!D@dMFR$ZJ#)t!Sfg7bAEt6*as7V0}$WTbG-d7#z1;v2;fx{IGayV z949u$bP+PB596ygu4hgHl*w{8?mStR(A*m1ZEczyZd=rYUc7m-fHD0$ve1cRnpo0e zaGq>9iIIp*m!vB&^&FrEb|2vPxwMy?sNaNKBCEs=s3pZJ>IvE1VlcyET(MN$^=XF6 zUAd_9RoRu2DOaZD8WlBWqR1n|wtI-{jI&Wq;mT-Hw{)uTGA+~;lU7+l)lHKnE4SRf z8L_ugp|kEDrklFTlQU*nRZKRU>~4w7**uEhA1nMD+=dSo?il8S*u&FLcQ&FUU!s~5 z)n2HtiZ6?=OO2^H=h)n{jI)??j^?~Q=W@YOwGWrI&rz*W;pJH0N&9he172h_iXWmkEP>+JZCLA%+@1UssX7(jh*a-Q)+Zfbwf@)A z=&z!oiO$f&fv8i9MUKi<$wrw4OZpFw67q;-|DVV6+mXjjJUbkeNuDf8W0Lqc+@N6@ z+)&J(zywAuKy#>U3g~WtN_3$MD7v*rFK%6+V!c37k#>U6ATBh7qJvAT&~z(W+_3^m#D#F1OA^nd`$x{4|V8 zLZ%U1+#uCi)#U9id$6|KymjNl6f*@;50*lK&HlP=$j+>V2kz>h2GXx{je+z%{Z3bJ zgwyui*Zv*%GxZaWFGhFZp@h^TrghUQv`ijPp_v Date: Thu, 29 Oct 2026 06:32:49 -0400 Subject: [PATCH 10/22] Rebuild async exception regions across suspension cleanup ```python async def select(manager, work, record): async with manager: try: await work("a") finally: record("cleanup") await work("b") return "tail" ``` The exception table splits this source region around await suspension instructions and out-of-line CLEANUP_THROW trampolines. Treating those trampolines as source handlers can empty the async-with body, move the try/finally outside it, and place later awaited work inside the finally suite. That changes cleanup order and exception suppression. For the Python 3.14+ fragmented layout, remove suspension-cleanup targets and entries that start at their trampolines from the source-region view. Merge equal-handler, equal-depth, equal-lasti fragments only across the bounded suspension protocol. Retain the original table for interpreter cleanup processing. Recognize awaited exit sequences and intervening protected fragments when rebuilding a context's region. Keep cleanup-target filtering tied to that modern reconstruction path; applying it independently to 3.12 or 3.13 changes otherwise-correct context boundaries. Adjacent async contexts and an enclosing source handler keep their own scope, including when __aexit__ suppresses a body exception. Async functions and async context managers date to Python 3.5; this fix addresses the fragmented exception-table encoding used in 3.14 and 3.15. --- src/decompiler.rs | 58 +++++++++++++- .../cases/b_await_context_controls.py | 72 ++++++++++++++++++ .../cases/b_await_exception_regions.py | 65 ++++++++++++++++ tests/corpus/3.12.3/await_context_controls.py | 26 +++++++ .../corpus/3.12.3/await_context_controls.pyc | Bin 0 -> 2119 bytes tests/corpus/3.13.7/await_context_controls.py | 26 +++++++ .../corpus/3.13.7/await_context_controls.pyc | Bin 0 -> 2108 bytes tests/corpus/3.14.2/await_context_controls.py | 26 +++++++ .../corpus/3.14.2/await_context_controls.pyc | Bin 0 -> 2258 bytes .../corpus/3.14.2/await_exception_regions.py | 19 +++++ .../corpus/3.14.2/await_exception_regions.pyc | Bin 0 -> 1431 bytes tests/corpus/3.15.2/await_context_controls.py | 26 +++++++ .../corpus/3.15.2/await_context_controls.pyc | Bin 0 -> 2332 bytes .../corpus/3.15.2/await_exception_regions.py | 19 +++++ .../corpus/3.15.2/await_exception_regions.pyc | Bin 0 -> 1468 bytes tests/data/await_context_controls.py | 26 +++++++ tests/data/await_exception_regions.py | 19 +++++ tests/integration.rs | 26 +++++++ tests/pyc/await_context_controls.3.12.pyc | Bin 0 -> 2119 bytes tests/pyc/await_context_controls.3.13.pyc | Bin 0 -> 2108 bytes tests/pyc/await_context_controls.3.14.pyc | Bin 0 -> 2258 bytes tests/pyc/await_context_controls.3.15.pyc | Bin 0 -> 2332 bytes tests/pyc/await_exception_regions.3.14.pyc | Bin 0 -> 1431 bytes tests/pyc/await_exception_regions.3.15.pyc | Bin 0 -> 1468 bytes 24 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 tests/behavior/cases/b_await_context_controls.py create mode 100644 tests/behavior/cases/b_await_exception_regions.py create mode 100644 tests/corpus/3.12.3/await_context_controls.py create mode 100644 tests/corpus/3.12.3/await_context_controls.pyc create mode 100644 tests/corpus/3.13.7/await_context_controls.py create mode 100644 tests/corpus/3.13.7/await_context_controls.pyc create mode 100644 tests/corpus/3.14.2/await_context_controls.py create mode 100644 tests/corpus/3.14.2/await_context_controls.pyc create mode 100644 tests/corpus/3.14.2/await_exception_regions.py create mode 100644 tests/corpus/3.14.2/await_exception_regions.pyc create mode 100644 tests/corpus/3.15.2/await_context_controls.py create mode 100644 tests/corpus/3.15.2/await_context_controls.pyc create mode 100644 tests/corpus/3.15.2/await_exception_regions.py create mode 100644 tests/corpus/3.15.2/await_exception_regions.pyc create mode 100644 tests/data/await_context_controls.py create mode 100644 tests/data/await_exception_regions.py create mode 100644 tests/pyc/await_context_controls.3.12.pyc create mode 100644 tests/pyc/await_context_controls.3.13.pyc create mode 100644 tests/pyc/await_context_controls.3.14.pyc create mode 100644 tests/pyc/await_context_controls.3.15.pyc create mode 100644 tests/pyc/await_exception_regions.3.14.pyc create mode 100644 tests/pyc/await_exception_regions.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index d83d047c..0e31b790 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -872,6 +872,13 @@ pub fn decompile_in_scope( let Some(&hi) = idx_of.get(&e.target) else { return true; }; + if version.at_least(3, 14) + && instrs.get(hi).map(|instruction| instruction.op) == Some(Op::CLEANUP_THROW) + { + // Await/yield suspension cleanup is interpreter protocol, + // even when followed by a backward jump and user handlers. + return false; + } let mut saw_reraise = false; for ins in instrs.iter().skip(hi).take(14) { if matches!(ins.op, Op::RERAISE) { @@ -888,6 +895,44 @@ pub fn decompile_in_scope( }); } + if version.at_least(3, 14) { + // Await suspension splits one source try into fragments, and its + // out-of-line CLEANUP_THROW trampoline can itself be protected by + // the same source handler. Rebuild the lexical await region before + // deriving try contexts; keep the original table for VM glue. + exc_entries.retain(|entry| { + idx_of.get(&entry.start).and_then(|index| instrs.get(*index)) + .map_or(true, |instruction| instruction.op != Op::CLEANUP_THROW) + }); + let mut grouped: std::collections::BTreeMap<(usize, u32, bool), Vec> = + std::collections::BTreeMap::new(); + for entry in exc_entries.drain(..) { + grouped.entry((entry.target, entry.depth, entry.lasti)).or_default().push(entry); + } + for (_, mut entries) in grouped { + entries.sort_by_key(|entry| entry.start); + let mut merged: Vec = Vec::new(); + for entry in entries { + let join = merged.last().map_or(false, |previous| { + entry.start >= previous.end + && instrs.iter().filter(|instruction| { + instruction.offset >= previous.end && instruction.offset < entry.start + }).all(|instruction| matches!(instruction.op, + Op::SEND | Op::YIELD_VALUE | Op::RESUME | Op::RESUME_CHECK + | Op::JUMP_BACKWARD_NO_INTERRUPT | Op::END_SEND + | Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + }); + if join { + merged.last_mut().unwrap().end = entry.end; + } else { + merged.push(entry); + } + } + exc_entries.extend(merged); + } + exc_entries.sort_by_key(|entry| entry.start); + } + // classify exception-table targets: handlers starting with PUSH_EXC_INFO // are out-of-line handler regions; CHECK_EXC_MATCH inside => except // dispatch, otherwise => finally handler. @@ -1049,11 +1094,22 @@ pub fn decompile_in_scope( | Op::CALL_FUNCTION | Op::RETURN_VALUE | Op::RETURN_CONST + | Op::LOAD_SMALL_INT + | Op::GET_AWAITABLE + | Op::SEND + | Op::END_SEND + | Op::YIELD_VALUE + | Op::YIELD_FROM + | Op::RESUME + | Op::RESUME_CHECK + | Op::JUMP_BACKWARD_NO_INTERRUPT ) || (x.op == Op::LOAD_CONST && matches!( code.consts.get(x.arg as usize).map(|o| &**o), Some(PyObject::None) - )) + )) || (version.at_least(3, 14) && exc_entries.iter().any(|entry| { + entry.start <= x.offset && x.offset < entry.end && entry.target != t + })) }) && gap.iter().any(|x| { matches!(x.op, Op::RETURN_VALUE | Op::RETURN_CONST) }); diff --git a/tests/behavior/cases/b_await_context_controls.py b/tests/behavior/cases/b_await_context_controls.py new file mode 100644 index 00000000..af81b5dc --- /dev/null +++ b/tests/behavior/cases/b_await_context_controls.py @@ -0,0 +1,72 @@ +# MIN_VERSION: 3.12 +import asyncio + +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def exercise(function, failure, suppress): + events = [] + + async def work(label): + events.append(label) + await asyncio.sleep(0) + if failure == label: + raise ValueError(label) + return 1 if label == "a" else 2 + + def record(label): + events.append(label) + + class Manager: + async def __aenter__(self): + events.append("enter") + await asyncio.sleep(0) + if failure == "enter": + raise ValueError("enter") + return self + + async def __aexit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + await asyncio.sleep(0) + if failure == "exit": + raise ValueError("exit") + return suppress + + try: + result = await function(Manager(), work, record, False) + print("return", result) + except BaseException as error: + print(type(error).__name__, str(error)) + print(events) + + +for function in (sequential_contexts, awaited_then_context, outer_handler): + for failure in ("", "a", "b", "cleanup", "enter", "exit"): + for suppress in (False, True): + print(function.__name__, failure, suppress) + asyncio.run(exercise(function, failure, suppress)) diff --git a/tests/behavior/cases/b_await_exception_regions.py b/tests/behavior/cases/b_await_exception_regions.py new file mode 100644 index 00000000..fea42d06 --- /dev/null +++ b/tests/behavior/cases/b_await_exception_regions.py @@ -0,0 +1,65 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" + + +async def exercise(function, failure, suppress): + events = [] + + async def work(label): + events.append(label) + await asyncio.sleep(0) + if failure == label: + raise ValueError(label) + return 1 if label == "a" else 2 + + def record(label): + events.append(label) + + class Manager: + async def __aenter__(self): + events.append("enter") + await asyncio.sleep(0) + if failure == "enter": + raise ValueError("enter") + return self + + async def __aexit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + await asyncio.sleep(0) + if failure == "exit": + raise ValueError("exit") + return suppress + + try: + result = await function(Manager(), work, record, False) + print("return", result) + except BaseException as error: + print(type(error).__name__, str(error)) + print(events) + + +for function in (awaited_finally, awaited_else): + for failure in ("", "a", "b", "cleanup", "enter", "exit"): + for suppress in (False, True): + print(function.__name__, failure, suppress) + asyncio.run(exercise(function, failure, suppress)) diff --git a/tests/corpus/3.12.3/await_context_controls.py b/tests/corpus/3.12.3/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.12.3/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.12.3/await_context_controls.pyc b/tests/corpus/3.12.3/await_context_controls.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed6a6878c4ef444d714a234199b7276edd90c418 GIT binary patch literal 2119 zcmcJQ-)j_C6vxk*duJxIJ8QPdW@&6}Rw9(Op{b}uBm{1X^g&1;go1qu)6A%8*WLEc zhBY)LVx&ZCt00(GL1JdkK3e>B?|4icXyN37?&a)IA^~z z=f|BNpL6$*L?XrjVEpb!KP-Ls=%0tAHxF#6wKR!E@ZlnS06vmO-ymbCoB04X z>3OoXx2g6xAXUx6A(%%WKBHxB7>n!+)1>PL%%YD=jTPppw~n)V{B!Qp6(5$2dhdDT z+4>iBHFoT3w(B~_XU$W_2^fYeC&-SG4G+Kp(CgE4-q80q3?_tFG7wJElOSAq_Z&M3 zC8X?k$PSae(Y|Vq&*`=5wLh{FX4!ZJy$XI!N6cvUTbzuD(W_&wd<1$!)wmr(4nE;CYJxnh8Qi=yAU z$w}Rg)3q{+z5Gs$)Xyl6Sih*eUtKZEB97vy6`5RVN2Nw76eB$9ag^>y>JaTkr3Wl= z*p9L&2Hj}Ct@MEob^R!++7X>IYK?W<_c)H(?mg31+Et4_eh&=s~jmWPN12o}yKE+s63obxsGMNKa8e_JL$-24+|ZwkR(1 z@hvZ z4b$vxL2|i!H?ibo2vsMulHUEz!Ru3toeP3cbuv0q?Ep8?Krs%9zFO6wP;EKcuYe?_ zv$mQ%eiZLnCGvV#I$u@9_C5(O(drja4B^GN$iP(;j9-h9x{1QHZZT4~T@hz@P}tTI zC-sxc`wfM|f9Is`a;l9Ol8j94ZKSNo(K>F4J$Cd8ij*65*-H1?Dl_AVK^A2VRr@B9 z$f7|;x;0%%!>fA}0qQq{p zXYkR?7>XHkEzGpYb(2EtDqfAX;6vvc91V|3A$XdJy4&NwEpXF2;$w*nTqb<1FBLRj zD`@Hrb^Zr@2%ML|XFSXFj<07+zaeSTD_SE1J&GG0BWDZ#bS@Z`>aSn_GpH9yvw*m2 Rn3!Bm+L&BRk&`_X_!oAQ&OHDC literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/await_context_controls.py b/tests/corpus/3.13.7/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.13.7/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.13.7/await_context_controls.pyc b/tests/corpus/3.13.7/await_context_controls.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a3f5effdb793da519a33685a4a2f69d31bb23f2 GIT binary patch literal 2108 zcmcJQ&u<$=6vyA&nO*-{V^g~Z5`rBPQH&xgXo{tZQjOI}L?CmpLXbE_OT2a>TpMN` z47aLqFhSsoDrzZG58PZ1TtGtQfHwby_filC1QJq+k&vJV-t0PwgHko&$4YPZ>v=zB zXXZ2GK|CI105ISE<^0dr9y5O+z5C!$4KgG)pbTRmp^VgGS)*2~u{>YqyYT=a((jTD z&QtCbTeoxWP)gN&O4>C{;Q=9ATbnkNO+DOg&{dtp&B`wf%m|zk5~f zu2uDWRbAu@`m8n$VJMZ!=Ew?@Fa!ShG|%As!(hVW!%0_Y230KwS(;N0N-v&eS*RlA zJx_LqZ00EWi+s@^Kd4e*Rm`)Jhn9!0^`K{Xp$k^Ss@c-x?WTO)Go-!TlogLx>sC$D z`hrxO;TY89{KuPa*-pzjS+QEyNvmzGww9Kgjh6j(@RrTG^W+=vcqzwzbIoqFR;~Ju z-;pL&QTyYpQ1cU9gxiVCdb@utbJ@9)@4nQ*-x(wy=(c}6xcj@9*|9pa7hm`_LScP# zd>GzsciMk^pU8~%?8e@4KBK`dg^D?RAH{^R&PaV|iYfL9imY*+le!V3z8;E6eltqy zOB82~uT#lp)T9+&qdI(|y8J`kAom zKZlgNi7IB38O4dvW(13-xfwN;&Zt2ThJKsAJ%Ij^djkId0Q}0_5Z|W>y*$}CS^Ovn zF6={a5?-Y_1y2YpLQJ7vV^!Egw8u+(?0KOTt5K=jl^%N0m#zAmJtt*TN`>SImoi4b zy|hoHU0G_a*o__Vko}^cRMxqaiO0XyLRfz>eWO?5LoH)tA#B( zmvJAs7{nj$r^fop?9jF2^m+XAHyzB(0Fa6%9^7K|zT<4rSeH-~@V%Hg0bLXtzZ@lX z9R)W&Wu$JHBFb)}FpSSRsV`LAEfit?4JY+2qg+=*l8QiT4WuYH?CYo@rbBKLMap#V zHrxRr3&}(J4Lp1?ykH|Kh6LC9UvJN!x$f9fmY{ zAMKJ%s^&nR;?1>|EtdiUrA$*`iflhff2U^@Bs~Brx(8BvoG4Nt1ug=~cOC{LgLZhG zKr%j1`}>i?C!i^D1ds@!{|KZ~p7~e0f4=;+kvc8TfDD4;7=HFbv$9sVpOfnU-oN_Q X64D7l+}3nVZYM*S9Hgm{$I0;*f;pM3 literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/await_context_controls.py b/tests/corpus/3.14.2/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.14.2/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.14.2/await_context_controls.pyc b/tests/corpus/3.14.2/await_context_controls.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d4103b16644fcd0480264514fc8eac605eefd21 GIT binary patch literal 2258 zcmb`I-*4Mg6vxl;kHm4DA5GG%Z8}<5*%T&PMGaJJg<(rmB`8ITrcw!{$TX(QmL=sT zQN~*}_E31~)(XlK5=ENSH##18c3$`!;+KJVK)jW;5)#`3=h|78K%=%2w(jvaK0mJG z`#IMa!y#1!fFJI@`TCQe-`@L;bmzdfIv66c201tbHOL`Z%n4+LoOlk>(z4Xa08mJO zPUE0N*)PEu26pUCayFcIX`D;08^68`s2nS*5r|e35Er+jGv*Y~B7q|p)4`8-Vb|yQZ<%Ft z#bS=ssIZ%kk6FtVR&bwZxuO*{Oszff8~F0waxk^Vl+)7MI+Q^ z6>pr7_iH_@j>AW^WZvO}g3P;v_XgifiqOTg~uD(s@8E}Esfb(OIS zW1QbEh*^++I|&zGtB|iA)q|$M27Z7x<0LbOY_}9 zL&Up5Jp&u4tJ1v~sU6fc<$ICT15J;~4^a;*KT4#2;&HZsdQ`GSiEF<|>9$G?gRQwX zj1*``T^;l3lL31O^;Fm%R=J)KxIORYz9RA5Nj{IA2^t{Uvu?F3NV*AJACF@n+-kNpbAmI!}({!t;HTg=tM$gqtG?3{~nii{(9L8H-aO zAHC@Pm$z>8WDh;{IeLi{amUf~TrPU9`y}XvY56w^J!Ol7@HGc+$K@|_I`jy-{}sK3 zwCH|5-1c-|6r&vh4rqYa&C2YpN}*n|-eQAv&;86KNKkVjJ{Dw*K92e^dYB-?xZ@wm C2d=>Y literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/await_exception_regions.py b/tests/corpus/3.14.2/await_exception_regions.py new file mode 100644 index 00000000..383e0fa6 --- /dev/null +++ b/tests/corpus/3.14.2/await_exception_regions.py @@ -0,0 +1,19 @@ +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" diff --git a/tests/corpus/3.14.2/await_exception_regions.pyc b/tests/corpus/3.14.2/await_exception_regions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caf0d367d229cb1d9f536951d2424cc1cb106778 GIT binary patch literal 1431 zcmb`H&u(BJNH=mN;Z@8znvm{ob0)xbl{ zz2|@Xho}O7CV6YrW~2`*R+JKGF#9$2i6Wf;puP`Navpr7?5ET}DZ*H{2JN@|-WnJ% z+AR)?62LI{QUUU`8~#+Fv{&H^*+*NQi_wj>g}`&`t)&<*#!A!mgF9d=v3A+5yNjNP zm3BjXA18#j&=9p)xfHmIAs_DuwYc2$!e)4^<~H49Zrk;nRd0F0TWb1^dR2If^c9XT zt;AL*=hdp0{JI+iE257I@TrFL6rRy9@|Ts{X#doHF3HKmTiM)Zc-7uGw|4G{ncY0~ z(9Cb8vzsT^{S9l)`qMhN+XtD!jrMx`i8=JDV|ire-)!lFyQxlsQ*Xl`sPvE4w1PjQ zlhuEbNnJx{RK1>Za>^}q2J~MQQn&kQ>^F2u`tK^K2vTH>Qs|VG$kbR5Alagv;S{x~ zsIaGuJU*J3#$@z4I>*z|xWW3A#BnM~+5bjx;D1JNN$DWEkKjij_ay;kQy(hA1v|6nrOyN+H(1b?ddjtBC|X*%%x1ZqV|+5uzb%Rq%Zl zNe%{w6bGIg@?55}DCnIbHha){LFkDCG{n}>rSKkcYI1lhcj(5+tNwLs&3U0i|6mV4 zX3grcmMKuKZ}`=Y2AM-UfCMVj`Pj@qm+9fnE{zkFUHH~%6@pJ^_zZ?mXZXe#z6`@> zGJF~3c8bRCp_A6{tE3*J$QQ}z*h-{mtW6|Wl;){Xrr#OSqLReAjI2jw);6+pEHSMn zqX~2-)6ofo^=XOY*(7D>Y^ihSI>#V1dbw@t&^;*qtILgAEAY;Uej4jsON#{U3h||+ NVQyzY$DH6l+jsrU6Z-%F literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/await_context_controls.py b/tests/corpus/3.15.2/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.15.2/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.15.2/await_context_controls.pyc b/tests/corpus/3.15.2/await_context_controls.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f98af220709248e29d52f58a6e0ffd5ae19b0c9 GIT binary patch literal 2332 zcmb`J&u<$=6vyA}&v@;%H?`wnnD4hq+s+E_7=!!d+j_t9a9q~ zWp<3Rjk9^Ur(^oyge!7Kt|%U7#c5$m9O2G_3==*pNK=rdc&_ zSkx2hHTs?>QL9*^B~Q3sF>g@Z6;O7AZ3*2uZ0WQ&wr;*Jy44Ummgpdk|@s5QF6OveMPKTR;VG3dt($(Oi5<0WDYYmJw~ zOkWIQ%;EKvK2T4IR-M3+)Cwo8fupPLST%4h->v{g?JH<~XzH`jd$oh!es~8TUaLt^ z#9B&XrKF&M^;U+1gzuB<$z`)zs#v8B;wjh6%Dk1Qv_>f=AEcTp_}h#5JX@t%r)*Vs zv85UgvGZUs#!2i!4VLbp$e&TFWkHH@hID`ltp$ zeZCn@HnoBF;97R2J?yjm_qGV3_%;wR;YFU??uj8t>IU`Iy4e2>pCVEu*Du(JF z(R-Bd1yuJ{JtaOQdav@6i0T&>=PE=`NN!MMTH`U@jUwUT_V~V+V4$1ubxPL90`4Hu z&uQ)pVWvmetnTXq%NpaE&IOGi-;min4FzfD8$qUz!|18-VjKNMI#a$qkeIR2gq>%yH z8uWqmhmQkf1kqSUAeFBfFyAl?TU2(R3`j)rlz`0e{zt~IS^qv!_OE0M4O%7MmAhFh h%~z~(8pA&SW5mQ`o)Gec6N&ajlZmz+Lr2*?{s*}Qv&sMf literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/await_exception_regions.py b/tests/corpus/3.15.2/await_exception_regions.py new file mode 100644 index 00000000..383e0fa6 --- /dev/null +++ b/tests/corpus/3.15.2/await_exception_regions.py @@ -0,0 +1,19 @@ +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" diff --git a/tests/corpus/3.15.2/await_exception_regions.pyc b/tests/corpus/3.15.2/await_exception_regions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8ad789015479aa559868efa31a10d7cfeb23bea GIT binary patch literal 1468 zcmcJPQE$^$6vxl89XoN|IBnWQX>dy?70C$NDg}+Ih{=^Y9xAOAR9+@gnZ#|PnWVZ- z3B17&5(1`n>I8@nz%+>uFrFs&Gi=l9mx;%{0j<;uO&aGoO@XbC>{|Ew?{oj>p4^+C zeKFmw%K-4GKK0M}BAzUhK4`eFw);rjqCOA(5Wqarh!?bBu%)uH@8MgD`W@=qpHb#f zu;QFcFo{>>rqq0foKFew+IiZpwidSMXk&N_+r`_Mp# zk#-SSk^lyvDp^pZ-SNd|)?Q^ouA-gzDnd8X%D(5;S63pu8YxY;=Kln?5^2}my1VR& zNNF|1H<2p5azj)iWyyD!13vU))S{857c_&hiraL@+?HEwE_yf0-b%C9s4og{nQp=8 z%FW1(=e)||QmyX#{!L-f0({EBWg7m1+wiBETWdXRy^`eA;8t&LJ=n0<=I+nEGJSFbnp7agW8&T-~7Yu-|dD>ey#Og>!nfLi!Hwy*4wRQezz;0;HkfZ>$LQ@J<}#` zpi|U;P)I#NXFT!LaE6p;=vexX2~yAdC>8=bC-e|i)|x@qFpJKp8ulewTS&%mNOx>4 zv=#P@BFD$IX(JVWj?PpjJd-B9sir7?IF!f#Hz*rQ4DJDxp8&#hgxE>y)*yUM=h$v4 zl51?njX2(AObmWu3DTrSX%vz*X-Fn6MDuPf48zcR)>#uqT|#!A z(xx-1a0;C-GU3@Y=}kRFaa)RC!1x4&O22BGI<(IydG=bPvg&*1L@&kSchn^DT1Lb- Pl7_jRz9i;E55?XAZR-JdkK3e>B?|4icXyN37?&a)IA^~z z=f|BNpL6$*L?XrjVEpb!KP-Ls=%0tAHxF#6wKR!E@ZlnS06vmO-ymbCoB04X z>3OoXx2g6xAXUx6A(%%WKBHxB7>n!+)1>PL%%YD=jTPppw~n)V{B!Qp6(5$2dhdDT z+4>iBHFoT3w(B~_XU$W_2^fYeC&-SG4G+Kp(CgE4-q80q3?_tFG7wJElOSAq_Z&M3 zC8X?k$PSae(Y|Vq&*`=5wLh{FX4!ZJy$XI!N6cvUTbzuD(W_&wd<1$!)wmr(4nE;CYJxnh8Qi=yAU z$w}Rg)3q{+z5Gs$)Xyl6Sih*eUtKZEB97vy6`5RVN2Nw76eB$9ag^>y>JaTkr3Wl= z*p9L&2Hj}Ct@MEob^R!++7X>IYK?W<_c)H(?mg31+Et4_eh&=s~jmWPN12o}yKE+s63obxsGMNKa8e_JL$-24+|ZwkR(1 z@hvZ z4b$vxL2|i!H?ibo2vsMulHUEz!Ru3toeP3cbuv0q?Ep8?Krs%9zFO6wP;EKcuYe?_ zv$mQ%eiZLnCGvV#I$u@9_C5(O(drja4B^GN$iP(;j9-h9x{1QHZZT4~T@hz@P}tTI zC-sxc`wfM|f9Is`a;l9Ol8j94ZKSNo(K>F4J$Cd8ij*65*-H1?Dl_AVK^A2VRr@B9 z$f7|;x;0%%!>fA}0qQq{p zXYkR?7>XHkEzGpYb(2EtDqfAX;6vvc91V|3A$XdJy4&NwEpXF2;$w*nTqb<1FBLRj zD`@Hrb^Zr@2%ML|XFSXFj<07+zaeSTD_SE1J&GG0BWDZ#bS@Z`>aSn_GpH9yvw*m2 Rn3!Bm+L&BRk&`_X_!oAQ&OHDC literal 0 HcmV?d00001 diff --git a/tests/pyc/await_context_controls.3.13.pyc b/tests/pyc/await_context_controls.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a3f5effdb793da519a33685a4a2f69d31bb23f2 GIT binary patch literal 2108 zcmcJQ&u<$=6vyA&nO*-{V^g~Z5`rBPQH&xgXo{tZQjOI}L?CmpLXbE_OT2a>TpMN` z47aLqFhSsoDrzZG58PZ1TtGtQfHwby_filC1QJq+k&vJV-t0PwgHko&$4YPZ>v=zB zXXZ2GK|CI105ISE<^0dr9y5O+z5C!$4KgG)pbTRmp^VgGS)*2~u{>YqyYT=a((jTD z&QtCbTeoxWP)gN&O4>C{;Q=9ATbnkNO+DOg&{dtp&B`wf%m|zk5~f zu2uDWRbAu@`m8n$VJMZ!=Ew?@Fa!ShG|%As!(hVW!%0_Y230KwS(;N0N-v&eS*RlA zJx_LqZ00EWi+s@^Kd4e*Rm`)Jhn9!0^`K{Xp$k^Ss@c-x?WTO)Go-!TlogLx>sC$D z`hrxO;TY89{KuPa*-pzjS+QEyNvmzGww9Kgjh6j(@RrTG^W+=vcqzwzbIoqFR;~Ju z-;pL&QTyYpQ1cU9gxiVCdb@utbJ@9)@4nQ*-x(wy=(c}6xcj@9*|9pa7hm`_LScP# zd>GzsciMk^pU8~%?8e@4KBK`dg^D?RAH{^R&PaV|iYfL9imY*+le!V3z8;E6eltqy zOB82~uT#lp)T9+&qdI(|y8J`kAom zKZlgNi7IB38O4dvW(13-xfwN;&Zt2ThJKsAJ%Ij^djkId0Q}0_5Z|W>y*$}CS^Ovn zF6={a5?-Y_1y2YpLQJ7vV^!Egw8u+(?0KOTt5K=jl^%N0m#zAmJtt*TN`>SImoi4b zy|hoHU0G_a*o__Vko}^cRMxqaiO0XyLRfz>eWO?5LoH)tA#B( zmvJAs7{nj$r^fop?9jF2^m+XAHyzB(0Fa6%9^7K|zT<4rSeH-~@V%Hg0bLXtzZ@lX z9R)W&Wu$JHBFb)}FpSSRsV`LAEfit?4JY+2qg+=*l8QiT4WuYH?CYo@rbBKLMap#V zHrxRr3&}(J4Lp1?ykH|Kh6LC9UvJN!x$f9fmY{ zAMKJ%s^&nR;?1>|EtdiUrA$*`iflhff2U^@Bs~Brx(8BvoG4Nt1ug=~cOC{LgLZhG zKr%j1`}>i?C!i^D1ds@!{|KZ~p7~e0f4=;+kvc8TfDD4;7=HFbv$9sVpOfnU-oN_Q X64D7l+}3nVZYM*S9Hgm{$I0;*f;pM3 literal 0 HcmV?d00001 diff --git a/tests/pyc/await_context_controls.3.14.pyc b/tests/pyc/await_context_controls.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d4103b16644fcd0480264514fc8eac605eefd21 GIT binary patch literal 2258 zcmb`I-*4Mg6vxl;kHm4DA5GG%Z8}<5*%T&PMGaJJg<(rmB`8ITrcw!{$TX(QmL=sT zQN~*}_E31~)(XlK5=ENSH##18c3$`!;+KJVK)jW;5)#`3=h|78K%=%2w(jvaK0mJG z`#IMa!y#1!fFJI@`TCQe-`@L;bmzdfIv66c201tbHOL`Z%n4+LoOlk>(z4Xa08mJO zPUE0N*)PEu26pUCayFcIX`D;08^68`s2nS*5r|e35Er+jGv*Y~B7q|p)4`8-Vb|yQZ<%Ft z#bS=ssIZ%kk6FtVR&bwZxuO*{Oszff8~F0waxk^Vl+)7MI+Q^ z6>pr7_iH_@j>AW^WZvO}g3P;v_XgifiqOTg~uD(s@8E}Esfb(OIS zW1QbEh*^++I|&zGtB|iA)q|$M27Z7x<0LbOY_}9 zL&Up5Jp&u4tJ1v~sU6fc<$ICT15J;~4^a;*KT4#2;&HZsdQ`GSiEF<|>9$G?gRQwX zj1*``T^;l3lL31O^;Fm%R=J)KxIORYz9RA5Nj{IA2^t{Uvu?F3NV*AJACF@n+-kNpbAmI!}({!t;HTg=tM$gqtG?3{~nii{(9L8H-aO zAHC@Pm$z>8WDh;{IeLi{amUf~TrPU9`y}XvY56w^J!Ol7@HGc+$K@|_I`jy-{}sK3 zwCH|5-1c-|6r&vh4rqYa&C2YpN}*n|-eQAv&;86KNKkVjJ{Dw*K92e^dYB-?xZ@wm C2d=>Y literal 0 HcmV?d00001 diff --git a/tests/pyc/await_context_controls.3.15.pyc b/tests/pyc/await_context_controls.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f98af220709248e29d52f58a6e0ffd5ae19b0c9 GIT binary patch literal 2332 zcmb`J&u<$=6vyA}&v@;%H?`wnnD4hq+s+E_7=!!d+j_t9a9q~ zWp<3Rjk9^Ur(^oyge!7Kt|%U7#c5$m9O2G_3==*pNK=rdc&_ zSkx2hHTs?>QL9*^B~Q3sF>g@Z6;O7AZ3*2uZ0WQ&wr;*Jy44Ummgpdk|@s5QF6OveMPKTR;VG3dt($(Oi5<0WDYYmJw~ zOkWIQ%;EKvK2T4IR-M3+)Cwo8fupPLST%4h->v{g?JH<~XzH`jd$oh!es~8TUaLt^ z#9B&XrKF&M^;U+1gzuB<$z`)zs#v8B;wjh6%Dk1Qv_>f=AEcTp_}h#5JX@t%r)*Vs zv85UgvGZUs#!2i!4VLbp$e&TFWkHH@hID`ltp$ zeZCn@HnoBF;97R2J?yjm_qGV3_%;wR;YFU??uj8t>IU`Iy4e2>pCVEu*Du(JF z(R-Bd1yuJ{JtaOQdav@6i0T&>=PE=`NN!MMTH`U@jUwUT_V~V+V4$1ubxPL90`4Hu z&uQ)pVWvmetnTXq%NpaE&IOGi-;min4FzfD8$qUz!|18-VjKNMI#a$qkeIR2gq>%yH z8uWqmhmQkf1kqSUAeFBfFyAl?TU2(R3`j)rlz`0e{zt~IS^qv!_OE0M4O%7MmAhFh h%~z~(8pA&SW5mQ`o)Gec6N&ajlZmz+Lr2*?{s*}Qv&sMf literal 0 HcmV?d00001 diff --git a/tests/pyc/await_exception_regions.3.14.pyc b/tests/pyc/await_exception_regions.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caf0d367d229cb1d9f536951d2424cc1cb106778 GIT binary patch literal 1431 zcmb`H&u(BJNH=mN;Z@8znvm{ob0)xbl{ zz2|@Xho}O7CV6YrW~2`*R+JKGF#9$2i6Wf;puP`Navpr7?5ET}DZ*H{2JN@|-WnJ% z+AR)?62LI{QUUU`8~#+Fv{&H^*+*NQi_wj>g}`&`t)&<*#!A!mgF9d=v3A+5yNjNP zm3BjXA18#j&=9p)xfHmIAs_DuwYc2$!e)4^<~H49Zrk;nRd0F0TWb1^dR2If^c9XT zt;AL*=hdp0{JI+iE257I@TrFL6rRy9@|Ts{X#doHF3HKmTiM)Zc-7uGw|4G{ncY0~ z(9Cb8vzsT^{S9l)`qMhN+XtD!jrMx`i8=JDV|ire-)!lFyQxlsQ*Xl`sPvE4w1PjQ zlhuEbNnJx{RK1>Za>^}q2J~MQQn&kQ>^F2u`tK^K2vTH>Qs|VG$kbR5Alagv;S{x~ zsIaGuJU*J3#$@z4I>*z|xWW3A#BnM~+5bjx;D1JNN$DWEkKjij_ay;kQy(hA1v|6nrOyN+H(1b?ddjtBC|X*%%x1ZqV|+5uzb%Rq%Zl zNe%{w6bGIg@?55}DCnIbHha){LFkDCG{n}>rSKkcYI1lhcj(5+tNwLs&3U0i|6mV4 zX3grcmMKuKZ}`=Y2AM-UfCMVj`Pj@qm+9fnE{zkFUHH~%6@pJ^_zZ?mXZXe#z6`@> zGJF~3c8bRCp_A6{tE3*J$QQ}z*h-{mtW6|Wl;){Xrr#OSqLReAjI2jw);6+pEHSMn zqX~2-)6ofo^=XOY*(7D>Y^ihSI>#V1dbw@t&^;*qtILgAEAY;Uej4jsON#{U3h||+ NVQyzY$DH6l+jsrU6Z-%F literal 0 HcmV?d00001 diff --git a/tests/pyc/await_exception_regions.3.15.pyc b/tests/pyc/await_exception_regions.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8ad789015479aa559868efa31a10d7cfeb23bea GIT binary patch literal 1468 zcmcJPQE$^$6vxl89XoN|IBnWQX>dy?70C$NDg}+Ih{=^Y9xAOAR9+@gnZ#|PnWVZ- z3B17&5(1`n>I8@nz%+>uFrFs&Gi=l9mx;%{0j<;uO&aGoO@XbC>{|Ew?{oj>p4^+C zeKFmw%K-4GKK0M}BAzUhK4`eFw);rjqCOA(5Wqarh!?bBu%)uH@8MgD`W@=qpHb#f zu;QFcFo{>>rqq0foKFew+IiZpwidSMXk&N_+r`_Mp# zk#-SSk^lyvDp^pZ-SNd|)?Q^ouA-gzDnd8X%D(5;S63pu8YxY;=Kln?5^2}my1VR& zNNF|1H<2p5azj)iWyyD!13vU))S{857c_&hiraL@+?HEwE_yf0-b%C9s4og{nQp=8 z%FW1(=e)||QmyX#{!L-f0({EBWg7m1+wiBETWdXRy^`eA;8t&LJ=n0<=I+nEGJSFbnp7agW8&T-~7Yu-|dD>ey#Og>!nfLi!Hwy*4wRQezz;0;HkfZ>$LQ@J<}#` zpi|U;P)I#NXFT!LaE6p;=vexX2~yAdC>8=bC-e|i)|x@qFpJKp8ulewTS&%mNOx>4 zv=#P@BFD$IX(JVWj?PpjJd-B9sir7?IF!f#Hz*rQ4DJDxp8&#hgxE>y)*yUM=h$v4 zl51?njX2(AObmWu3DTrSX%vz*X-Fn6MDuPf48zcR)>#uqT|#!A z(xx-1a0;C-GU3@Y=}kRFaa)RC!1x4&O22BGI<(IydG=bPvg&*1L@&kSchn^DT1Lb- Pl7_jRz9i;E55?XAZR- Date: Thu, 29 Oct 2026 06:47:33 -0400 Subject: [PATCH 11/22] Preserve context exits across early returns and finally cleanup async def select(manager, work, record, flag): async with manager: if flag: return "early" try: return await work("a") finally: record("cleanup") The parser consumes the context-exit slot for the early-return branch, then lacks that slot when reconstructing the remaining branch. It can emit a call to None instead of retaining the awaited return and cleanup. Keep exit accounting for branch-local copies while their context is still live. Bound the finally collector at a proven context-return tail and retain the carried value through that tail's actual return endpoint. The protected-tail changes apply to Python 3.14 and later. Python 3.15 also encodes None with LOAD_COMMON_CONSTANT. Use the shared constant predicate in the return-tail matcher so both synchronous and asynchronous context bodies keep their returned values. Remove the corresponding known limitation. Include native fixture checks and executable examples covering early, computed and attribute returns, entry/body/cleanup/exit errors, actual async suspension, and exception suppression. Earlier modern exit layouts remain covered as controls. --- docs/LIMITATIONS.md | 7 +- src/decompiler.rs | 40 ++++++++-- .../cases/b_context_exit_accounting.py | 73 ++++++++++++++++++ .../cases/b_context_manager_returns.py | 72 +++++++++++++++++ .../corpus/3.10.19/context_manager_returns.py | 18 +++++ .../3.10.19/context_manager_returns.pyc | Bin 0 -> 827 bytes .../corpus/3.11.14/context_manager_returns.py | 18 +++++ .../3.11.14/context_manager_returns.pyc | Bin 0 -> 1649 bytes .../corpus/3.12.3/context_manager_returns.py | 18 +++++ .../corpus/3.12.3/context_manager_returns.pyc | Bin 0 -> 1277 bytes .../corpus/3.13.7/context_manager_returns.py | 18 +++++ .../corpus/3.13.7/context_manager_returns.pyc | Bin 0 -> 1234 bytes .../corpus/3.14.2/context_exit_accounting.py | 26 +++++++ .../corpus/3.14.2/context_exit_accounting.pyc | Bin 0 -> 1931 bytes .../corpus/3.14.2/context_manager_returns.py | 18 +++++ .../corpus/3.14.2/context_manager_returns.pyc | Bin 0 -> 1370 bytes .../corpus/3.15.2/context_exit_accounting.py | 26 +++++++ .../corpus/3.15.2/context_exit_accounting.pyc | Bin 0 -> 1992 bytes .../corpus/3.15.2/context_manager_returns.py | 18 +++++ .../corpus/3.15.2/context_manager_returns.pyc | Bin 0 -> 1393 bytes .../corpus/3.9.25/context_manager_returns.py | 18 +++++ .../corpus/3.9.25/context_manager_returns.pyc | Bin 0 -> 809 bytes tests/data/context_exit_accounting.py | 26 +++++++ tests/fixtures/context_manager_returns.py | 18 +++++ tests/integration.rs | 27 +++++++ tests/pyc/context_exit_accounting.3.14.pyc | Bin 0 -> 1931 bytes tests/pyc/context_exit_accounting.3.15.pyc | Bin 0 -> 1992 bytes tests/pyc/context_manager_returns.3.10.pyc | Bin 0 -> 827 bytes tests/pyc/context_manager_returns.3.11.pyc | Bin 0 -> 1649 bytes tests/pyc/context_manager_returns.3.12.pyc | Bin 0 -> 1277 bytes tests/pyc/context_manager_returns.3.13.pyc | Bin 0 -> 1234 bytes tests/pyc/context_manager_returns.3.14.pyc | Bin 0 -> 1370 bytes tests/pyc/context_manager_returns.3.15.pyc | Bin 0 -> 1393 bytes tests/pyc/context_manager_returns.3.9.pyc | Bin 0 -> 809 bytes 34 files changed, 428 insertions(+), 13 deletions(-) create mode 100644 tests/behavior/cases/b_context_exit_accounting.py create mode 100644 tests/behavior/cases/b_context_manager_returns.py create mode 100644 tests/corpus/3.10.19/context_manager_returns.py create mode 100644 tests/corpus/3.10.19/context_manager_returns.pyc create mode 100644 tests/corpus/3.11.14/context_manager_returns.py create mode 100644 tests/corpus/3.11.14/context_manager_returns.pyc create mode 100644 tests/corpus/3.12.3/context_manager_returns.py create mode 100644 tests/corpus/3.12.3/context_manager_returns.pyc create mode 100644 tests/corpus/3.13.7/context_manager_returns.py create mode 100644 tests/corpus/3.13.7/context_manager_returns.pyc create mode 100644 tests/corpus/3.14.2/context_exit_accounting.py create mode 100644 tests/corpus/3.14.2/context_exit_accounting.pyc create mode 100644 tests/corpus/3.14.2/context_manager_returns.py create mode 100644 tests/corpus/3.14.2/context_manager_returns.pyc create mode 100644 tests/corpus/3.15.2/context_exit_accounting.py create mode 100644 tests/corpus/3.15.2/context_exit_accounting.pyc create mode 100644 tests/corpus/3.15.2/context_manager_returns.py create mode 100644 tests/corpus/3.15.2/context_manager_returns.pyc create mode 100644 tests/corpus/3.9.25/context_manager_returns.py create mode 100644 tests/corpus/3.9.25/context_manager_returns.pyc create mode 100644 tests/data/context_exit_accounting.py create mode 100644 tests/fixtures/context_manager_returns.py create mode 100644 tests/pyc/context_exit_accounting.3.14.pyc create mode 100644 tests/pyc/context_exit_accounting.3.15.pyc create mode 100644 tests/pyc/context_manager_returns.3.10.pyc create mode 100644 tests/pyc/context_manager_returns.3.11.pyc create mode 100644 tests/pyc/context_manager_returns.3.12.pyc create mode 100644 tests/pyc/context_manager_returns.3.13.pyc create mode 100644 tests/pyc/context_manager_returns.3.14.pyc create mode 100644 tests/pyc/context_manager_returns.3.15.pyc create mode 100644 tests/pyc/context_manager_returns.3.9.pyc diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 6860fd1b..c1c364e4 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -23,16 +23,11 @@ constant handling. On real code compiled with CPython 3.15.0 (pandas + scikit-learn, 2091 modules), **all 2091 recompile (100%)**. -One 3.15 edge remains outside that corpus: a `with` body whose only -statement is `return ` (`with c as x: return x`) renders the body as -`pass` and loses the returned value (the exit protocol is not sunk into -the body). Attribute and subscript as-targets (`with c as h.attr:`, +Attribute and subscript as-targets (`with c as h.attr:`, `with c as d[k]:`) are recovered on 3.14 and 3.15. 3.15 的 opcode 表由本机解释器直接生成,2.6–3.15 的行为用例全部通过。 真实代码(pandas + scikit-learn,2091 模块)**全部 2091 个可重编译(100%)**。 -语料之外仍有一个 3.15 边角:`with` 体只有 `return ` 时 -(`with c as x: return x`)体会渲染成 `pass` 并丢失返回值。 ## Class bodies with conditional method definitions (3.14/3.15) diff --git a/src/decompiler.rs b/src/decompiler.rs index 0e31b790..a53a4f1d 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -4804,6 +4804,18 @@ impl<'a> Ctx<'a> { // handler chain — statements AFTER the copy (post-try code inside // a loop iteration) belong to the main walk, not the finally let mut fin_span_stop = stop; + if self.version.at_least(3, 14) + && self.blocks.iter().any(|block| block.kind == BlockType::With) + { + if let Some(tail) = self.instrs.iter().enumerate().find_map(|(index, instruction)| { + if instruction.offset >= pos && instruction.offset < fin_span_stop + && instruction.op == Op::SWAP && self.with_return_tail_end(index).is_some() + { Some(instruction.offset) } else { None } + }) { + stop = tail; + fin_span_stop = tail; + } + } // the inline-copy mirror verdict: false means the span at `pos` // does NOT opcode-mirror the finally chain's copy — there is no // inline copy here (an always-raising try body has no normal @@ -5127,6 +5139,8 @@ impl<'a> Ctx<'a> { let r = &self.instrs[ri]; if r.op == Op::RETURN_VALUE { Some(r.end()) + } else if self.version.at_least(3, 14) && r.op == Op::SWAP { + self.with_return_tail_end(ri) } else { None } @@ -41941,11 +41955,8 @@ if split_cond { k += 1; } Op::POP_FINALLY => k += 1, - Op::LOAD_CONST - if matches!( - self.code.consts.get(x.arg as usize).map(|o| &**o), - Some(PyObject::None) - ) => + Op::LOAD_CONST | Op::LOAD_COMMON_CONSTANT + if self.loads_none(x.op, x.arg) => { nones += 1; k += 1; @@ -47292,7 +47303,16 @@ impl<'a> Ctx<'a> { && star_args_none(&args) && args.iter().all(|a| matches!(&**a, Expr::Const(o) if matches!(&**o, PyObject::None))) { - self.with_exits -= 1; + // A constant early return has no SWAP-based return tail. Its + // copied __aexit__ call must not consume the enclosing with's + // bookkeeping needed by later branches and the final exit. + let early_exit = self.blocks.iter().any(|block| { + block.kind == BlockType::With && block.end > self.cur_offset + }) && matches!(self.blocks.last().map(|block| block.kind), + Some(BlockType::If) | Some(BlockType::Else)); + if !early_exit { + self.with_exits -= 1; + } let next_awaitable = self .idx_of .get(&self.cur_offset) @@ -47717,7 +47737,13 @@ impl<'a> Ctx<'a> { && none_args && (func_is_none || func_is_underflow) { - self.with_exits -= 1; + let early_exit = self.blocks.iter().any(|block| { + block.kind == BlockType::With && block.end > self.cur_offset + }) && matches!(self.blocks.last().map(|block| block.kind), + Some(BlockType::If) | Some(BlockType::Else)); + if !early_exit { + self.with_exits -= 1; + } // async with (3.11+): the __aexit__ result is awaited right // after the swallowed call — the protocol has no awaitable on // our modeled stack, so drop it cleanly (no underflow) diff --git a/tests/behavior/cases/b_context_exit_accounting.py b/tests/behavior/cases/b_context_exit_accounting.py new file mode 100644 index 00000000..c9f43438 --- /dev/null +++ b/tests/behavior/cases/b_context_exit_accounting.py @@ -0,0 +1,73 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") + + +async def exercise(function, failure, suppress, flag): + events = [] + + async def work(label): + events.append(label) + await asyncio.sleep(0) + if failure == label: + raise ValueError(label) + return 1 if label == "a" else 2 + + def record(label): + events.append(label) + + class Manager: + async def __aenter__(self): + events.append("enter") + await asyncio.sleep(0) + if failure == "enter": + raise ValueError("enter") + return self + + async def __aexit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + await asyncio.sleep(0) + if failure == "exit": + raise ValueError("exit") + return suppress + + try: + result = await function(Manager(), work, record, flag) + print("return", result) + except BaseException as error: + print(type(error).__name__, str(error)) + print(events) + + +for function in (guarded_return, computed_return, none_guard): + for failure in ("", "a", "cleanup", "enter", "exit"): + for suppress in (False, True): + for flag in (False, True, None): + print(function.__name__, failure, suppress, flag) + asyncio.run(exercise(function, failure, suppress, flag)) diff --git a/tests/behavior/cases/b_context_manager_returns.py b/tests/behavior/cases/b_context_manager_returns.py new file mode 100644 index 00000000..9e9228e2 --- /dev/null +++ b/tests/behavior/cases/b_context_manager_returns.py @@ -0,0 +1,72 @@ +# MIN_VERSION: 3.9 +# Controls for the modern exit protocol; Python 3.8 async returns have a separate gap. +import asyncio + +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result + + +events = [] + + +class Manager: + def __repr__(self): + return "manager" + + def __enter__(self): + events.append("enter") + if failure == "enter": + raise ValueError("enter") + return self + + def __exit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + if failure == "exit": + raise ValueError("exit") + return suppress + + async def __aenter__(self): + await asyncio.sleep(0) + return self.__enter__() + + async def __aexit__(self, typ, value, traceback): + await asyncio.sleep(0) + return self.__exit__(typ, value, traceback) + + @property + def result(self): + events.append("read") + if failure == "read": + raise ValueError("read") + return 7 + + +for failure in ("", "enter", "exit", "read"): + for suppress in (False, True): + for function in (synchronous, synchronous_attribute, asynchronous, asynchronous_attribute): + events.clear() + print(function.__name__, failure, suppress) + try: + result = function(Manager()) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except ValueError as error: + print("error", str(error)) + print(events) diff --git a/tests/corpus/3.10.19/context_manager_returns.py b/tests/corpus/3.10.19/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.10.19/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.10.19/context_manager_returns.pyc b/tests/corpus/3.10.19/context_manager_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c4af4f79134da35edf8faeed388318f6e6e0e7a GIT binary patch literal 827 zcmZ{iJ5K^Z5XWcl0mn;ew9uA@gn%ZN#u&BGn9xudX~@Z*ih(<`yQh#~qS2n-((XQo zpTUOK4`6SDGj}K_9J`s>ow>d@uN*Np@9eJc2s$)$-(}@UC2NgKf)T?|Ir#m pOZ@e+5mr1DH{=h~u>yq;EUc2X+w-{Jb@sw1LqBeTWm;z5`~hQ0jxYcK literal 0 HcmV?d00001 diff --git a/tests/corpus/3.11.14/context_manager_returns.py b/tests/corpus/3.11.14/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.11.14/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.11.14/context_manager_returns.pyc b/tests/corpus/3.11.14/context_manager_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d183ede80f532bd174cb157dac841cfda303426 GIT binary patch literal 1649 zcmbtUL2DC182x5v({0m+)N13QK?2f4f*1t7N%0~Gg(3 z9>jyui+E89;>DjK{(zE`;Khp&iHF#eZzjoRHCc`AEc5v0n{Q@!-+MD}6Nxw{qR-#1 zl%M6!{rrMBXsjP~&LC)zO%Le?*$l{R18CSB$Zd0-va!;jS}mGFSKD@001c|qCA!aQ ztYp~O#ilm~(L3~BW3*0Z=>kb}*QoLh+GUJ3h~cp@(`0K+Y8owTyXY2gIieN2TP*vI zRLe7&!>s2>Pv$qbs-7<#nJ-mbKaesw`O$h-3V&R%O)?zyGgqT*J3DQ2)-eX%S9 z9?-$ee4AKo@yXSLpwC=^PHCTmTBAiwO17>0fR#5Cg8Vu+bS(euRCP2Y1w0&_}9=>h_d(wd{k zHN{d|{Z|MSOKHVYDECK+9`;#^uIjO5o#qX5$$H6_Mp>%kn>w3-Rp_Xgx=3|q(>;Ej z8vKzNZMQm1IH(#}FPs~1L(QQu@xD>|=87TyL=rW#=<*}rD;_YHObrbX89iSQnGNd10`~g_M&eWP literal 0 HcmV?d00001 diff --git a/tests/corpus/3.12.3/context_manager_returns.py b/tests/corpus/3.12.3/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.12.3/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.12.3/context_manager_returns.pyc b/tests/corpus/3.12.3/context_manager_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddd21f3e7d258879300586d97dcf9c32d36c7f9e GIT binary patch literal 1277 zcmbVMO>0v@6utLN-h2H>N&QH*@W9s6MUj38YD!R{3pWbgxT>2H^PXR{iOjs%1S3cz zLZzjytVMTHA=KhRbmuSd50ot2xN%tMIVWPFg(!SfV1Grh8oE zwB;j*PpcNTF_ADtw{GZxbX)=UM;iwT#jkQMt*YAP2feYK}& zdms!{sD`iI%!fG{sNr-r69)NkvK~Dt1MFwi(CtE{T@^Cv8#0^8=9FwTKTD*Jf!wF{ z*3J!LJO22>dhGC)0Nc{bO%2w%xRtB^0+YMi)0h^G7gUU1W0}K@OpFB*<=B z_BAoX#s-N{+V4O#)nI{)A*LNuodbNP8nnl{Oa)Cb~W1B&`}2v-HEBj0FB0a+CI-1+oZFMxt=lid%tOOy&W}*EVJ%d1n!=I zyC4hEVX+(qS>ZwFr313YfY;6#yKi|k`pyM;udj>`M9f!fy4+M;>ere6sDhCBG`3sL mPG|kxRB&D%!k+#!Qr&z^=M7cN}6Gjw4>TzKBJKnwU0eUo=`-@6|(=iED$*4B_gM3bwr zt@oc^-Twl6XK~G{^uSmnhmMjYhk@(}pl}SJG0`*-NN>mL6cbw)ty}}F(I^SZ@+d3e zpoT*i1g^27bdq9pmQGiXG>#e>5v5*uqBKEQj9$uciIXKNiLy15%BChtK2}5wT0}22Oj|zfqQ2yb_m95Xw(` zGJs=^uhd^v?yO@nmhwC~bt~_=vK2n9y*dKeTd zf#$|E!VXR1i)MMvNCxZ!TRDQ(*2n191}PcAwsebqf75o2AlZLt`b8tdz6ugav|j)_ zRi}UqBc=sg9S3{`^UvX~!ra6DYCqc|#kuNpZ|;XWkB}uR!3B<)cZ~$Eh<>AJC-@Qf znUBo`Kj8##m_ybx9<-m^L)KfwHgtbSr3a|=dzJoXB7UJ09`FWGNw81ID6 zNe(pmKcGLce*2_;zu)W%462TKaA+nso}YHF%EP$x|JH68-wbBn6oNZ9LxOuWbXK$_ H55fBrd6VGS literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/context_exit_accounting.py b/tests/corpus/3.14.2/context_exit_accounting.py new file mode 100644 index 00000000..aa8d62ca --- /dev/null +++ b/tests/corpus/3.14.2/context_exit_accounting.py @@ -0,0 +1,26 @@ +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") diff --git a/tests/corpus/3.14.2/context_exit_accounting.pyc b/tests/corpus/3.14.2/context_exit_accounting.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a4e842000d2a7d4a15779ea2c37cc95134d1b0a GIT binary patch literal 1931 zcmcJP-D@0G7{4IxDXafb8q%)HNg z=FFMjyr&Xzn-M~e-rDT%dn5hJuh6}NcGcDp#0^p;gCrnDib#tJqEci9GNzW*UIig0 z^ev379IEqV6m^%$Ia*?&60(vq)wRyhB6%ARUZOYHCC!F!j=k8;RnmlvkRy1WJg4OH zaLg#XD~hxL*Bm`cipo@Xtuv_DIq#f;EZfeGk`U|ow|NW1AeSy;oWaOrI5~28*E}_H zj%7%V=E$ke%CHBWcQJ@fw%??x$Tz7eqa=ejlxL9?x|J`n8XZ$-Bh7RD`RfsNBcnX; zx%F^ym8`0fvEbI-s^>>)tKq*B>AqKP_?1Y#Ht$xOa>I|~h+YW1X3(6dxPd!SZqx&B zDJXeMwV>pd%Z;!e)aupo#p_X`8oGYPtCV~%2>rSrN4?yz<`snB@rfQM4=uNDw;nsm zwbbxajr0vJx8859>&(jjd^qz-`QG8X7oNlh?wtKGmU$X)r$20Mm?T};uyFNkBSzw> zJ7<52W&Z4@W&K~x_b~s>T8D|Qy)^EGk;)E zf|~FMD~^;nM0s8pgVH9lRK^^Yo_t#$v-!9o#+md_>eG%TUZMQ8gm~44e#8_RH49aH zL;4%mw;xEmxY2E(_Qb6R<34bk0A@K1tBcx>ISvN3RW|EOYp!Fn`-;fkN10y{!I>`Ab?rYkr?=1=|2JCR1Sz|l(}5G?C-IG*ONw1@)V)%>ulNHn zrZAdU5q3bb<|J>QUrVJAu%Wz*C!nF&lA9sfP;9!Q^V8Zr2kJiM!|E40)B}g-)Nf?Z z`IbRFG+_~192qfyuA_^Tbb%EaN6>gw*;}A(hoM&QShxLQL6GUr-Pq zQ47x5G1hLQzWhVlJJ8;$EnJb#zhs_YXjH;^?{y!@blR;H>x@#msTef7nbv6bk3J}0 G?%v;MplCV( literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/context_manager_returns.py b/tests/corpus/3.14.2/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.14.2/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.14.2/context_manager_returns.pyc b/tests/corpus/3.14.2/context_manager_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ba11d2340897259036fe225f401d6cdd68af6cf GIT binary patch literal 1370 zcmb_c&ubGw82x58yUA{nrmd!_(Att>ORTg9;}58)IY=UcKxT3vHx-~_`uZrLPBpUSB0r|wwssRxqDdNI zY36cQNmCMEs*rcAc_&@ZP74d)^Y+l} zLN#bFs> zTzbx1epu-@5ZyceO%5R?tgxzvV%?l0RJ_);5c0IuXj7HxjN^#xE!VL`7%@%eb^)5} zEiW!F?yor%S6>9r?ygyGe;>2<3=l_?N;Dx`j`pJEywUPeX3{;Ja-NlxG1Y(tCJwN` zC@j&OB`x8nu})0Cw1yY=HFoinaU`>xQ?krcNMI@L7blcyQb$CP6U4ORfUi=ONJqK2 zTwFV~?Ss}>x!8Cf7Ju7UkhT;_1=(YEI`BjRS>;3O(=ff@l9ij1r&%5s4 z9q^hi#{h8Z{EM*1jsjqF}B4yi*6a5bxDZ+kW92A`UeYj B^yUBn literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/context_exit_accounting.py b/tests/corpus/3.15.2/context_exit_accounting.py new file mode 100644 index 00000000..aa8d62ca --- /dev/null +++ b/tests/corpus/3.15.2/context_exit_accounting.py @@ -0,0 +1,26 @@ +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") diff --git a/tests/corpus/3.15.2/context_exit_accounting.pyc b/tests/corpus/3.15.2/context_exit_accounting.pyc new file mode 100644 index 0000000000000000000000000000000000000000..544bfe7baf91741185124a12eb88515326591234 GIT binary patch literal 1992 zcmcJP&1)M+7{+IISK1G+SF&Zhv5l14t`kH^LT#|InHZXu;h}$f1YaqBsx}a_BR%teDh>8j@M&)wBCP zGn$?K&HIVdX{m&ef4+ZMTKQw+BM-XWXiIGmK-@$xl3o&!B1NP{1yPyRhe%%aw3bu; znCQ3Bw3VecT;AuFliEb~#S?E?-=U zsT=FwqVHDg%Nt}vi}fY9>dyH*)}k8!G){8gtMN*#U0-zP0{IzEpv9x1AB4f#iW|CT zy;?Q&Z-iz4#zI(jJ+D@;h6~lXGs`QnGgo(c#jljPAJ%!5+ZZo5FDN5CA-Bk*RNq?k zdGx3wwUrroks#^!)}p)7)1+$V{_fANd*AhcbK%PikL~VzQ-9jo7oAPRT68CB7^G*U zVd8$LVUy0xy{W(K?8~;{ZuBZx#eb}J75eE{l#N_L;XtG$>eW3)bBPs ztqGN~yd^YEYQum_q#YzhjDsTL98MybcAhjpz{p90z#A=w=JPee=9M_%_tbT%I*9Y2qN5@FYST zF5ZE=IOWxrmh0hbc2amMGr=We%7Gw8+i7>K~_-8Y(prz)D}ff*i>rAV#joGOodSl z>TJ>!!<3y!ixEfa4^4Ou=HU!Z#nE{vss0g4*j2S%!rmQ0mq5>(CG4G0#2tLKL+P=q zwW?okRv+F4V+K8N5%z(z)sgz_+*YRNEtj2J_!pF&UP;rCEIYk)J;^2#KiE*eP&Sgh zuR}esSzdc6E0Z7^q|gkQgzX?F22h<05G3^om2njHNs|=_I&ZOYU5u;JKcN@8P4O`; z(6l)3KyTV6JbATQYq>l$%?)Vo)Hd$O?(s6tE!8UZMgKgPTLbz?(1XxWDW%U8o#vkP NBxvq=8j5!#@h^KZZkhl9 literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/context_manager_returns.py b/tests/corpus/3.15.2/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.15.2/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.15.2/context_manager_returns.pyc b/tests/corpus/3.15.2/context_manager_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa391bddd30183ec93fc148a533c176c4e896f84 GIT binary patch literal 1393 zcmb_cK~EDw6n<~E%XYW56tq;JD4<~KK_Yl*M3Nr3NTO82#RH_&#gag4X15j+4pmPC zlA7oNz3PD;)r%25@Gnd+Uc7jenwS_4ep3n-iUy;zd0*emd^_Fuy>Itc;Dn_SQSfc= z^$Rl{pHs;92iLMnH|?j2r42RD6ss>Y+0frM}%M%?hFpN{cS)#Gbm;|HZUUN^^;Cl&B=i z#zHcaylcy{em^;%vmM+*{oUoLYdfwpFgv&C=A`Wmq_P>;UUa8x>}hFZJ>y)uw^a5y zOPN$!W;5BGBfYp?RpXonJfKfza3%bB^keg>?;2Sx#c(ma8vfxZ)|zP7pZPADM;D&~ zqk#Bb@i(z|c>J3nQdM5!audqU&AZZrX-y5OdQuM6)j6GXT{$tM;GMU-+&ZtlGq#j}Vm&KsVY{!0&`=J6epki@mmw@>#TS4v&`S0-8S7WWfp( zM_6H0G%}Sv3>xb|7|JF!j{fU3gREnjXGlwPRWGTW*C>@gS*Mi@fa60ry?`%Dqvp0^ zel@?|x9f{wxR~F&*77?}477(W@m7Aq1J+Xyl65hpKMx?;;LF~Uf#ekf-Wc&_^Bs=_ zHv>qvOcQGd?6fcJdmQ=q!cOw>Z1xAS&;L)cEmYAcc3ZvJb;b;Wt-|RC9G1Oi&co6V zg9_;*kUplQkMO$xn*K6?TE@`_45ft8d}SX;;GnOPkJgX4~0c{!_11DwqH;gX-JpW9EG0eqk6GHF1eUG!!U#g$_`I zn&=6&sEyvB4$YxAsoRE1zDMxr|4{c`To<@R70ndhx1Y(BT!Vr2ESxVUfi@h2*gpcI z=M7qlxrnqPh>T8d==t6~;7c}^-M3UM~{%t6kBAIjLF#ET`$_8g4CcD(Y6n(mm!%J5A12I@J>W Wpmbl{(_uiv0Xx%AMD@e9)%*ogGKLEP literal 0 HcmV?d00001 diff --git a/tests/data/context_exit_accounting.py b/tests/data/context_exit_accounting.py new file mode 100644 index 00000000..aa8d62ca --- /dev/null +++ b/tests/data/context_exit_accounting.py @@ -0,0 +1,26 @@ +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") diff --git a/tests/fixtures/context_manager_returns.py b/tests/fixtures/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/fixtures/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/integration.rs b/tests/integration.rs index 6cc172bd..b98e3fd7 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -208,6 +208,33 @@ fn await_region_reconstruction_preserves_adjacent_contexts_and_outer_handlers() } } +#[test] +fn early_context_returns_preserve_finally_and_remaining_exit_bookkeeping() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("context_exit_accounting.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "async with manager:\n if flag:\n return 'early'\n try:\n return await work('a')\n finally:\n record('cleanup')", + "async with manager:\n try:\n return await work('a')\n finally:\n record('cleanup')", + "async with manager:\n if flag is None:\n return 3\n try:\n return await work('a')\n finally:\n record('cleanup')", + ], version); + } +} + +#[test] +fn context_managers_keep_return_values_inside_the_body() { + for version in ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("context_manager_returns.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "def synchronous(manager):\n with manager as value:\n return value", + "def synchronous_attribute(manager):\n with manager as value:\n return value.result", + "async def asynchronous(manager):\n async with manager as value:\n return value", + "async def asynchronous_attribute(manager):\n async with manager as value:\n return value.result", + ], version); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/context_exit_accounting.3.14.pyc b/tests/pyc/context_exit_accounting.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a4e842000d2a7d4a15779ea2c37cc95134d1b0a GIT binary patch literal 1931 zcmcJP-D@0G7{4IxDXafb8q%)HNg z=FFMjyr&Xzn-M~e-rDT%dn5hJuh6}NcGcDp#0^p;gCrnDib#tJqEci9GNzW*UIig0 z^ev379IEqV6m^%$Ia*?&60(vq)wRyhB6%ARUZOYHCC!F!j=k8;RnmlvkRy1WJg4OH zaLg#XD~hxL*Bm`cipo@Xtuv_DIq#f;EZfeGk`U|ow|NW1AeSy;oWaOrI5~28*E}_H zj%7%V=E$ke%CHBWcQJ@fw%??x$Tz7eqa=ejlxL9?x|J`n8XZ$-Bh7RD`RfsNBcnX; zx%F^ym8`0fvEbI-s^>>)tKq*B>AqKP_?1Y#Ht$xOa>I|~h+YW1X3(6dxPd!SZqx&B zDJXeMwV>pd%Z;!e)aupo#p_X`8oGYPtCV~%2>rSrN4?yz<`snB@rfQM4=uNDw;nsm zwbbxajr0vJx8859>&(jjd^qz-`QG8X7oNlh?wtKGmU$X)r$20Mm?T};uyFNkBSzw> zJ7<52W&Z4@W&K~x_b~s>T8D|Qy)^EGk;)E zf|~FMD~^;nM0s8pgVH9lRK^^Yo_t#$v-!9o#+md_>eG%TUZMQ8gm~44e#8_RH49aH zL;4%mw;xEmxY2E(_Qb6R<34bk0A@K1tBcx>ISvN3RW|EOYp!Fn`-;fkN10y{!I>`Ab?rYkr?=1=|2JCR1Sz|l(}5G?C-IG*ONw1@)V)%>ulNHn zrZAdU5q3bb<|J>QUrVJAu%Wz*C!nF&lA9sfP;9!Q^V8Zr2kJiM!|E40)B}g-)Nf?Z z`IbRFG+_~192qfyuA_^Tbb%EaN6>gw*;}A(hoM&QShxLQL6GUr-Pq zQ47x5G1hLQzWhVlJJ8;$EnJb#zhs_YXjH;^?{y!@blR;H>x@#msTef7nbv6bk3J}0 G?%v;MplCV( literal 0 HcmV?d00001 diff --git a/tests/pyc/context_exit_accounting.3.15.pyc b/tests/pyc/context_exit_accounting.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..544bfe7baf91741185124a12eb88515326591234 GIT binary patch literal 1992 zcmcJP&1)M+7{+IISK1G+SF&Zhv5l14t`kH^LT#|InHZXu;h}$f1YaqBsx}a_BR%teDh>8j@M&)wBCP zGn$?K&HIVdX{m&ef4+ZMTKQw+BM-XWXiIGmK-@$xl3o&!B1NP{1yPyRhe%%aw3bu; znCQ3Bw3VecT;AuFliEb~#S?E?-=U zsT=FwqVHDg%Nt}vi}fY9>dyH*)}k8!G){8gtMN*#U0-zP0{IzEpv9x1AB4f#iW|CT zy;?Q&Z-iz4#zI(jJ+D@;h6~lXGs`QnGgo(c#jljPAJ%!5+ZZo5FDN5CA-Bk*RNq?k zdGx3wwUrroks#^!)}p)7)1+$V{_fANd*AhcbK%PikL~VzQ-9jo7oAPRT68CB7^G*U zVd8$LVUy0xy{W(K?8~;{ZuBZx#eb}J75eE{l#N_L;XtG$>eW3)bBPs ztqGN~yd^YEYQum_q#YzhjDsTL98MybcAhjpz{p90z#A=w=JPee=9M_%_tbT%I*9Y2qN5@FYST zF5ZE=IOWxrmh0hbc2amMGr=We%7Gw8+i7>K~_-8Y(prz)D}ff*i>rAV#joGOodSl z>TJ>!!<3y!ixEfa4^4Ou=HU!Z#nE{vss0g4*j2S%!rmQ0mq5>(CG4G0#2tLKL+P=q zwW?okRv+F4V+K8N5%z(z)sgz_+*YRNEtj2J_!pF&UP;rCEIYk)J;^2#KiE*eP&Sgh zuR}esSzdc6E0Z7^q|gkQgzX?F22h<05G3^om2njHNs|=_I&ZOYU5u;JKcN@8P4O`; z(6l)3KyTV6JbATQYq>l$%?)Vo)Hd$O?(s6tE!8UZMgKgPTLbz?(1XxWDW%U8o#vkP NBxvq=8j5!#@h^KZZkhl9 literal 0 HcmV?d00001 diff --git a/tests/pyc/context_manager_returns.3.10.pyc b/tests/pyc/context_manager_returns.3.10.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c4af4f79134da35edf8faeed388318f6e6e0e7a GIT binary patch literal 827 zcmZ{iJ5K^Z5XWcl0mn;ew9uA@gn%ZN#u&BGn9xudX~@Z*ih(<`yQh#~qS2n-((XQo zpTUOK4`6SDGj}K_9J`s>ow>d@uN*Np@9eJc2s$)$-(}@UC2NgKf)T?|Ir#m pOZ@e+5mr1DH{=h~u>yq;EUc2X+w-{Jb@sw1LqBeTWm;z5`~hQ0jxYcK literal 0 HcmV?d00001 diff --git a/tests/pyc/context_manager_returns.3.11.pyc b/tests/pyc/context_manager_returns.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d183ede80f532bd174cb157dac841cfda303426 GIT binary patch literal 1649 zcmbtUL2DC182x5v({0m+)N13QK?2f4f*1t7N%0~Gg(3 z9>jyui+E89;>DjK{(zE`;Khp&iHF#eZzjoRHCc`AEc5v0n{Q@!-+MD}6Nxw{qR-#1 zl%M6!{rrMBXsjP~&LC)zO%Le?*$l{R18CSB$Zd0-va!;jS}mGFSKD@001c|qCA!aQ ztYp~O#ilm~(L3~BW3*0Z=>kb}*QoLh+GUJ3h~cp@(`0K+Y8owTyXY2gIieN2TP*vI zRLe7&!>s2>Pv$qbs-7<#nJ-mbKaesw`O$h-3V&R%O)?zyGgqT*J3DQ2)-eX%S9 z9?-$ee4AKo@yXSLpwC=^PHCTmTBAiwO17>0fR#5Cg8Vu+bS(euRCP2Y1w0&_}9=>h_d(wd{k zHN{d|{Z|MSOKHVYDECK+9`;#^uIjO5o#qX5$$H6_Mp>%kn>w3-Rp_Xgx=3|q(>;Ej z8vKzNZMQm1IH(#}FPs~1L(QQu@xD>|=87TyL=rW#=<*}rD;_YHObrbX89iSQnGNd10`~g_M&eWP literal 0 HcmV?d00001 diff --git a/tests/pyc/context_manager_returns.3.12.pyc b/tests/pyc/context_manager_returns.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddd21f3e7d258879300586d97dcf9c32d36c7f9e GIT binary patch literal 1277 zcmbVMO>0v@6utLN-h2H>N&QH*@W9s6MUj38YD!R{3pWbgxT>2H^PXR{iOjs%1S3cz zLZzjytVMTHA=KhRbmuSd50ot2xN%tMIVWPFg(!SfV1Grh8oE zwB;j*PpcNTF_ADtw{GZxbX)=UM;iwT#jkQMt*YAP2feYK}& zdms!{sD`iI%!fG{sNr-r69)NkvK~Dt1MFwi(CtE{T@^Cv8#0^8=9FwTKTD*Jf!wF{ z*3J!LJO22>dhGC)0Nc{bO%2w%xRtB^0+YMi)0h^G7gUU1W0}K@OpFB*<=B z_BAoX#s-N{+V4O#)nI{)A*LNuodbNP8nnl{Oa)Cb~W1B&`}2v-HEBj0FB0a+CI-1+oZFMxt=lid%tOOy&W}*EVJ%d1n!=I zyC4hEVX+(qS>ZwFr313YfY;6#yKi|k`pyM;udj>`M9f!fy4+M;>ere6sDhCBG`3sL mPG|kxRB&D%!k+#!Qr&z^=M7cN}6Gjw4>TzKBJKnwU0eUo=`-@6|(=iED$*4B_gM3bwr zt@oc^-Twl6XK~G{^uSmnhmMjYhk@(}pl}SJG0`*-NN>mL6cbw)ty}}F(I^SZ@+d3e zpoT*i1g^27bdq9pmQGiXG>#e>5v5*uqBKEQj9$uciIXKNiLy15%BChtK2}5wT0}22Oj|zfqQ2yb_m95Xw(` zGJs=^uhd^v?yO@nmhwC~bt~_=vK2n9y*dKeTd zf#$|E!VXR1i)MMvNCxZ!TRDQ(*2n191}PcAwsebqf75o2AlZLt`b8tdz6ugav|j)_ zRi}UqBc=sg9S3{`^UvX~!ra6DYCqc|#kuNpZ|;XWkB}uR!3B<)cZ~$Eh<>AJC-@Qf znUBo`Kj8##m_ybx9<-m^L)KfwHgtbSr3a|=dzJoXB7UJ09`FWGNw81ID6 zNe(pmKcGLce*2_;zu)W%462TKaA+nso}YHF%EP$x|JH68-wbBn6oNZ9LxOuWbXK$_ H55fBrd6VGS literal 0 HcmV?d00001 diff --git a/tests/pyc/context_manager_returns.3.14.pyc b/tests/pyc/context_manager_returns.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ba11d2340897259036fe225f401d6cdd68af6cf GIT binary patch literal 1370 zcmb_c&ubGw82x58yUA{nrmd!_(Att>ORTg9;}58)IY=UcKxT3vHx-~_`uZrLPBpUSB0r|wwssRxqDdNI zY36cQNmCMEs*rcAc_&@ZP74d)^Y+l} zLN#bFs> zTzbx1epu-@5ZyceO%5R?tgxzvV%?l0RJ_);5c0IuXj7HxjN^#xE!VL`7%@%eb^)5} zEiW!F?yor%S6>9r?ygyGe;>2<3=l_?N;Dx`j`pJEywUPeX3{;Ja-NlxG1Y(tCJwN` zC@j&OB`x8nu})0Cw1yY=HFoinaU`>xQ?krcNMI@L7blcyQb$CP6U4ORfUi=ONJqK2 zTwFV~?Ss}>x!8Cf7Ju7UkhT;_1=(YEI`BjRS>;3O(=ff@l9ij1r&%5s4 z9q^hi#{h8Z{EM*1jsjqF}B4yi*6a5bxDZ+kW92A`UeYj B^yUBn literal 0 HcmV?d00001 diff --git a/tests/pyc/context_manager_returns.3.15.pyc b/tests/pyc/context_manager_returns.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa391bddd30183ec93fc148a533c176c4e896f84 GIT binary patch literal 1393 zcmb_cK~EDw6n<~E%XYW56tq;JD4<~KK_Yl*M3Nr3NTO82#RH_&#gag4X15j+4pmPC zlA7oNz3PD;)r%25@Gnd+Uc7jenwS_4ep3n-iUy;zd0*emd^_Fuy>Itc;Dn_SQSfc= z^$Rl{pHs;92iLMnH|?j2r42RD6ss>Y+0frM}%M%?hFpN{cS)#Gbm;|HZUUN^^;Cl&B=i z#zHcaylcy{em^;%vmM+*{oUoLYdfwpFgv&C=A`Wmq_P>;UUa8x>}hFZJ>y)uw^a5y zOPN$!W;5BGBfYp?RpXonJfKfza3%bB^keg>?;2Sx#c(ma8vfxZ)|zP7pZPADM;D&~ zqk#Bb@i(z|c>J3nQdM5!audqU&AZZrX-y5OdQuM6)j6GXT{$tM;GMU-+&ZtlGq#j}Vm&KsVY{!0&`=J6epki@mmw@>#TS4v&`S0-8S7WWfp( zM_6H0G%}Sv3>xb|7|JF!j{fU3gREnjXGlwPRWGTW*C>@gS*Mi@fa60ry?`%Dqvp0^ zel@?|x9f{wxR~F&*77?}477(W@m7Aq1J+Xyl65hpKMx?;;LF~Uf#ekf-Wc&_^Bs=_ zHv>qvOcQGd?6fcJdmQ=q!cOw>Z1xAS&;L)cEmYAcc3ZvJb;b;Wt-|RC9G1Oi&co6V zg9_;*kUplQkMO$xn*K6?TE@`_45ft8d}SX;;GnOPkJgX4~0c{!_11DwqH;gX-JpW9EG0eqk6GHF1eUG!!U#g$_`I zn&=6&sEyvB4$YxAsoRE1zDMxr|4{c`To<@R70ndhx1Y(BT!Vr2ESxVUfi@h2*gpcI z=M7qlxrnqPh>T8d==t6~;7c}^-M3UM~{%t6kBAIjLF#ET`$_8g4CcD(Y6n(mm!%J5A12I@J>W Wpmbl{(_uiv0Xx%AMD@e9)%*ogGKLEP literal 0 HcmV?d00001 From cd53c9ba5be3680fb4996a40dab5706a564e3e9c Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 06:52:36 -0400 Subject: [PATCH 12/22] Keep computed returns inside contexts containing handler loops def select(lock, rows, parse): with lock: result = [] for row in rows: try: result.append(parse(row)) except ValueError: continue return {"rows": result} The loop's out-of-line exception fragments can keep the context open beyond its computed-return tail. The existing late-return path expects an already closed context statement, so it can leak exit protocol calls or move the return outside the context. Moving that return also changes the result when the context suppresses an exception. When the exact exit/return protocol is proved and the top block is the still-live context, fold the carried value into that block before simulating cleanup slots. Preserve result evaluation inside the context and exit before the actual function return. Cover synchronous and asynchronous contexts returning a variable, a call or a dictionary after handler loops. The exception-table regressions occur on Python 3.12 and later; earlier layouts remain controls. Behavior examples include rejected rows, cancellation, entry and exit failures, and exception suppression. --- src/decompiler.rs | 11 ++ .../behavior/cases/b_loop_context_returns.py | 117 ++++++++++++++++++ tests/corpus/3.10.19/loop_context_returns.py | 66 ++++++++++ tests/corpus/3.10.19/loop_context_returns.pyc | Bin 0 -> 1526 bytes tests/corpus/3.11.14/loop_context_returns.py | 66 ++++++++++ tests/corpus/3.11.14/loop_context_returns.pyc | Bin 0 -> 3836 bytes tests/corpus/3.12.3/loop_context_returns.py | 66 ++++++++++ tests/corpus/3.12.3/loop_context_returns.pyc | Bin 0 -> 3041 bytes tests/corpus/3.13.7/loop_context_returns.py | 66 ++++++++++ tests/corpus/3.13.7/loop_context_returns.pyc | Bin 0 -> 3025 bytes tests/corpus/3.14.2/loop_context_returns.py | 66 ++++++++++ tests/corpus/3.14.2/loop_context_returns.pyc | Bin 0 -> 3230 bytes tests/corpus/3.15.2/loop_context_returns.py | 66 ++++++++++ tests/corpus/3.15.2/loop_context_returns.pyc | Bin 0 -> 3273 bytes tests/data/loop_context_returns.py | 66 ++++++++++ tests/integration.rs | 18 +++ tests/pyc/loop_context_returns.3.10.pyc | Bin 0 -> 1526 bytes tests/pyc/loop_context_returns.3.11.pyc | Bin 0 -> 3836 bytes tests/pyc/loop_context_returns.3.12.pyc | Bin 0 -> 3041 bytes tests/pyc/loop_context_returns.3.13.pyc | Bin 0 -> 3025 bytes tests/pyc/loop_context_returns.3.14.pyc | Bin 0 -> 3230 bytes tests/pyc/loop_context_returns.3.15.pyc | Bin 0 -> 3273 bytes 22 files changed, 608 insertions(+) create mode 100644 tests/behavior/cases/b_loop_context_returns.py create mode 100644 tests/corpus/3.10.19/loop_context_returns.py create mode 100644 tests/corpus/3.10.19/loop_context_returns.pyc create mode 100644 tests/corpus/3.11.14/loop_context_returns.py create mode 100644 tests/corpus/3.11.14/loop_context_returns.pyc create mode 100644 tests/corpus/3.12.3/loop_context_returns.py create mode 100644 tests/corpus/3.12.3/loop_context_returns.pyc create mode 100644 tests/corpus/3.13.7/loop_context_returns.py create mode 100644 tests/corpus/3.13.7/loop_context_returns.pyc create mode 100644 tests/corpus/3.14.2/loop_context_returns.py create mode 100644 tests/corpus/3.14.2/loop_context_returns.pyc create mode 100644 tests/corpus/3.15.2/loop_context_returns.py create mode 100644 tests/corpus/3.15.2/loop_context_returns.pyc create mode 100644 tests/data/loop_context_returns.py create mode 100644 tests/pyc/loop_context_returns.3.10.pyc create mode 100644 tests/pyc/loop_context_returns.3.11.pyc create mode 100644 tests/pyc/loop_context_returns.3.12.pyc create mode 100644 tests/pyc/loop_context_returns.3.13.pyc create mode 100644 tests/pyc/loop_context_returns.3.14.pyc create mode 100644 tests/pyc/loop_context_returns.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index a53a4f1d..969b2f8f 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -42142,6 +42142,17 @@ if split_cond { } return true; } + // A loop's out-of-line except fragments can extend the With's + // region beyond its normal computed-return tail. The With is then + // still open here although its loop has closed. This pure exit + // protocol carries the body's existing result, not a user call. + // Close that With at the tail before simulating its cleanup slots. + if self.blocks.last().map_or(false, |block| { + block.kind == BlockType::With && block.end > inst.offset + }) { + self.fold_with_return(inst.offset, after); + return true; + } // probe the target shape BEFORE popping so a failed fold leaves // the stack untouched if !matches!( diff --git a/tests/behavior/cases/b_loop_context_returns.py b/tests/behavior/cases/b_loop_context_returns.py new file mode 100644 index 00000000..cc2db758 --- /dev/null +++ b/tests/behavior/cases/b_loop_context_returns.py @@ -0,0 +1,117 @@ +# MIN_VERSION: 3.9 +import asyncio + +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +events = [] + +class Lock: + def __enter__(self): + events.append("enter") + if mode == "open_error": + raise RuntimeError("enter") + return self + + def __exit__(self, kind, value, traceback): + events.append(("exit", kind.__name__ if kind else None)) + if mode == "exit_error": + raise RuntimeError("exit") + return mode == "suppressed" + + async def __aenter__(self): + await asyncio.sleep(0) + return self.__enter__() + + async def __aexit__(self, kind, value, traceback): + await asyncio.sleep(0) + return self.__exit__(kind, value, traceback) + + +def parse(value): + events.append(("parse", value)) + if mode == "rejected" and value <= 1: + raise ValueError("row") + if mode in ("failed", "suppressed"): + raise RuntimeError("body") + if mode == "cancelled": + raise asyncio.CancelledError("cancelled") + return value * 2 + + +for rows in ([], [1, 2], [-1, 0, 3]): + for mode in ("normal", "rejected", "failed", "cancelled", "open_error", "exit_error", "suppressed"): + for function in (sync_value, sync_length, sync_mapping, async_value, async_length, async_mapping): + events.clear() + print(function.__name__, rows, mode) + try: + result = function(Lock(), rows, parse) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except (RuntimeError, asyncio.CancelledError) as error: + print(type(error).__name__, str(error)) + print(events) diff --git a/tests/corpus/3.10.19/loop_context_returns.py b/tests/corpus/3.10.19/loop_context_returns.py new file mode 100644 index 00000000..cd32d3a7 --- /dev/null +++ b/tests/corpus/3.10.19/loop_context_returns.py @@ -0,0 +1,66 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + diff --git a/tests/corpus/3.10.19/loop_context_returns.pyc b/tests/corpus/3.10.19/loop_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5396b517187549bb5609c872e7805e368df3dad0 GIT binary patch literal 1526 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 JZsk7g_b-vrQUU+~ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/loop_context_returns.py b/tests/corpus/3.13.7/loop_context_returns.py new file mode 100644 index 00000000..cd32d3a7 --- /dev/null +++ b/tests/corpus/3.13.7/loop_context_returns.py @@ -0,0 +1,66 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + diff --git a/tests/corpus/3.13.7/loop_context_returns.pyc b/tests/corpus/3.13.7/loop_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09f6bd722f912abc24812d974a3c113e2bccad70 GIT binary patch literal 3025 zcmcIm&u<$=82x5ve>j_U;?%AkHzaYK92#1hst`&cAVv`qMQWsCxtAP*m26UqYe%!2 zK*a^oOWINuB*G7ILCOV7yfew(Q0t2#w2Gj~1$O}49FBm{$ z$sD)lRl;VoK-6{p2f#WNNuwN1P=yy+lHML5s^~*hNJowr&^fzUC;L>7D|3;qFk*m&&^jWzWb3sCtZI+P??`wRTIb_+=<5_ z?=;{O3cKQW`;u$sugT2X^p?is*}IAK*YPjn-@4yVe|PSe#Q0wZrTVvQN~9n3lHGGD z_0QinrH1On;@L3Kzr}HW_@Eo1tEq?HII>=h3C=&u9Md-pkn6S}>n9F&hk*s8XuxgJ z*FwXaNU^&b&Z^H9wIXjS+<~fZcA~0OKCE%6rj#ZiEvyb(@0)jDZGP#0^^{pVi?aZm<6V7)3QvS};1v3Un6E zTV?Bf#&FQTSJm$%s^QZ!jZutdUTskk#bd27s$W&bCYNH*^Vj@G-aPVuYKa%wB_?$9G6%aF z6IuRMMwt4h0e0O%-)$x`<{c9(Fl~{tf&@n>NHWD!5zjCc&6?t<6$~?xcY>Uw_+gd( znSZ)S|3{d7@AYq|a<*{9bPGqkvzv}c;4AFBs2h&hF0NS&djW?x2k6KWPZroVZQ)`L z*kVfABCc#PrEHN`w&>M2d%$iv=nE_+a%K=S74NmhpcP~}g28i4@xh4an2P32al#4? zGI7WW#vR2cRsNlg@h7I=dt>Zev|CtVs)ZG%cgYGVymIT}5N}waS>Ul7q*riw?*TgU z!c#@QGc!yoGnmQ@lgbPO$_z1m(*nEUpf4aMvSz?c#bdTeTfsh#V6dMlo{o5yspx7%Q6n?Yg9edYV+o>%(X@2a)Y9c3TB0)l_LW(w_?V(Xrt%@a3QDhPm(z>=a zyM|U=BDh2eQYZoeLPA`laLt9nVSC`r0inu0aNxu(Z6Xy_9C&YSw~ZQ0wv?)C`R%v! zo@ZwLee=zXh76qpz=IFc%7vd!NB&0qpQ_?M{5#uY$7&--^VtictSW{a$%RQMuH)@zb54rQKY5xCF;Q1>yr4?u%{{8fcl6bol+!DM|7Nk~Hqt}2ag1T3ony^w@HxO5g^6{dL~ ztg;l0!f?H41*>~uRiT=s_pz{-THQ8V*jB0{S1#v^%T+y_D^&95r7X#9wdyOBmabQQ zvh=ZCRm(YP=c_{I?MlI^a%#kZVQ6S5m8uRpdE2okmUB*SqEIT87ne#!Cx6pflzFEj zi}rZ=R#mre6_*x2q5)(Ge@U-xzl~uH{?d$%*uBo^y0{-rtk3N!+~~d+iGO3n3D#W_>bcnI0y7oBUA3q9WYAd!Ldu)$l&zk|$ zP`^q{iyalgouH{}uHXn=jS<#8Yy{1I?P!kw@TT`7IlfANqIYF^Lji5FKHEO)lGL=# zB@HeGhAJ=Qi_(wiDXY?;M9`xRvn$Sr@&vYYP|-UW+TabTibTJ7ZS#}XxI2x#BX@D+ z&h&|$5F*zXG^3gu=7di$!ch;$gXVA(_I~pw`6Ah~aE(qdk1uB7r=?Xo-7;USu}U+F z)bM2%qz~hfGrJ-C=T#l+ zj>lZD9AT~((QFTol?Fe|Pd7E(xu;ZOT{oGo*9X#6o39`AmTY*6uQG2@HfK1Pd&WTH z_{R+r!S7-ChbRt5x10^99TSbjww%wMCN@gREoamD&%TZ$d>WnwcAJ?-;7g9^>wuY7 ze@K{Nv8y8b*}yi}&&&bg1_WVbyI*rpa!k5?jBvk)2N-43!kp3E3^T_}e$aQZ&+)|a*r+}lN zbTpDqoTQ_XbjC0_P9lY` zlVaX)UonSvlNKdYcN>Ky6DG+hB$-nrnNG3mNA!zI0&md#D6h{9!!6dMKnQ)E*D$QTAfp|LPqpBme*a literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/loop_context_returns.py b/tests/corpus/3.15.2/loop_context_returns.py new file mode 100644 index 00000000..cd32d3a7 --- /dev/null +++ b/tests/corpus/3.15.2/loop_context_returns.py @@ -0,0 +1,66 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + diff --git a/tests/corpus/3.15.2/loop_context_returns.pyc b/tests/corpus/3.15.2/loop_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42e4e0ce6c7976b5df4c26254489dfe98d15b45f GIT binary patch literal 3273 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 JZsk7g_b-vrQUU+~ literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_context_returns.3.13.pyc b/tests/pyc/loop_context_returns.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09f6bd722f912abc24812d974a3c113e2bccad70 GIT binary patch literal 3025 zcmcIm&u<$=82x5ve>j_U;?%AkHzaYK92#1hst`&cAVv`qMQWsCxtAP*m26UqYe%!2 zK*a^oOWINuB*G7ILCOV7yfew(Q0t2#w2Gj~1$O}49FBm{$ z$sD)lRl;VoK-6{p2f#WNNuwN1P=yy+lHML5s^~*hNJowr&^fzUC;L>7D|3;qFk*m&&^jWzWb3sCtZI+P??`wRTIb_+=<5_ z?=;{O3cKQW`;u$sugT2X^p?is*}IAK*YPjn-@4yVe|PSe#Q0wZrTVvQN~9n3lHGGD z_0QinrH1On;@L3Kzr}HW_@Eo1tEq?HII>=h3C=&u9Md-pkn6S}>n9F&hk*s8XuxgJ z*FwXaNU^&b&Z^H9wIXjS+<~fZcA~0OKCE%6rj#ZiEvyb(@0)jDZGP#0^^{pVi?aZm<6V7)3QvS};1v3Un6E zTV?Bf#&FQTSJm$%s^QZ!jZutdUTskk#bd27s$W&bCYNH*^Vj@G-aPVuYKa%wB_?$9G6%aF z6IuRMMwt4h0e0O%-)$x`<{c9(Fl~{tf&@n>NHWD!5zjCc&6?t<6$~?xcY>Uw_+gd( znSZ)S|3{d7@AYq|a<*{9bPGqkvzv}c;4AFBs2h&hF0NS&djW?x2k6KWPZroVZQ)`L z*kVfABCc#PrEHN`w&>M2d%$iv=nE_+a%K=S74NmhpcP~}g28i4@xh4an2P32al#4? zGI7WW#vR2cRsNlg@h7I=dt>Zev|CtVs)ZG%cgYGVymIT}5N}waS>Ul7q*riw?*TgU z!c#@QGc!yoGnmQ@lgbPO$_z1m(*nEUpf4aMvSz?c#bdTeTfsh#V6dMlo{o5yspx7%Q6n?Yg9edYV+o>%(X@2a)Y9c3TB0)l_LW(w_?V(Xrt%@a3QDhPm(z>=a zyM|U=BDh2eQYZoeLPA`laLt9nVSC`r0inu0aNxu(Z6Xy_9C&YSw~ZQ0wv?)C`R%v! zo@ZwLee=zXh76qpz=IFc%7vd!NB&0qpQ_?M{5#uY$7&--^VtictSW{a$%RQMuH)@zb54rQKY5xCF;Q1>yr4?u%{{8fcl6bol+!DM|7Nk~Hqt}2ag1T3ony^w@HxO5g^6{dL~ ztg;l0!f?H41*>~uRiT=s_pz{-THQ8V*jB0{S1#v^%T+y_D^&95r7X#9wdyOBmabQQ zvh=ZCRm(YP=c_{I?MlI^a%#kZVQ6S5m8uRpdE2okmUB*SqEIT87ne#!Cx6pflzFEj zi}rZ=R#mre6_*x2q5)(Ge@U-xzl~uH{?d$%*uBo^y0{-rtk3N!+~~d+iGO3n3D#W_>bcnI0y7oBUA3q9WYAd!Ldu)$l&zk|$ zP`^q{iyalgouH{}uHXn=jS<#8Yy{1I?P!kw@TT`7IlfANqIYF^Lji5FKHEO)lGL=# zB@HeGhAJ=Qi_(wiDXY?;M9`xRvn$Sr@&vYYP|-UW+TabTibTJ7ZS#}XxI2x#BX@D+ z&h&|$5F*zXG^3gu=7di$!ch;$gXVA(_I~pw`6Ah~aE(qdk1uB7r=?Xo-7;USu}U+F z)bM2%qz~hfGrJ-C=T#l+ zj>lZD9AT~((QFTol?Fe|Pd7E(xu;ZOT{oGo*9X#6o39`AmTY*6uQG2@HfK1Pd&WTH z_{R+r!S7-ChbRt5x10^99TSbjww%wMCN@gREoamD&%TZ$d>WnwcAJ?-;7g9^>wuY7 ze@K{Nv8y8b*}yi}&&&bg1_WVbyI*rpa!k5?jBvk)2N-43!kp3E3^T_}e$aQZ&+)|a*r+}lN zbTpDqoTQ_XbjC0_P9lY` zlVaX)UonSvlNKdYcN>Ky6DG+hB$-nrnNG3mNA!zI0&md#D6h{9!!6dMKnQ)E*D$QTAfp|LPqpBme*a literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_context_returns.3.15.pyc b/tests/pyc/loop_context_returns.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42e4e0ce6c7976b5df4c26254489dfe98d15b45f GIT binary patch literal 3273 zcmc&%&u<$=6n?Yo_5O<2wrs~q^UFzFS4$EaT&M`pmQ4wjS}DYYAVEbVx)amyipKn{SW2M#@PLXm~Ui37K^iBwc^;Jvj?i(;2pYSgvn?YHx0 zXJ+^N=9~4aR>+V6;E7w${IMQ;t(Zsr@4@P5YXm_BOBNE~LY8451bwc zSb%Z42NqaA*l@7k=sdRG2MZFl>GOZ|Y_)c)Y-&}i26Kyxh0=W0cs*BKDok=-=Br9I zP%P))tp<4cvRhRabKEUdgS_A_6}_rVl{he@)9F;II^-2x&pkY!^Kyra%g`0eA@Sq{nt=F?!GXhjc9B+_**Qwd~#EgL){yZ z&g-EsLf;g=JNfN9zeeoa3dFlM4TyB!4TE7_jsLZ6K)in&Sg5-i>DrX>eDAJ_sg?M0 z{GL0EJzvwEu<~{Cn6{=M_`!5KOrgmLg~c;YmJs$49Tco5NjW_brCOrJW;Uho!p6l=q2(!fq;9_LjjcuGLZ2CTGvL z&za4*h7ZHZ44N|4;WKp!m-EHa1s=m@Ni6Q07}}r;msvDA*IE?0I-#wp zPE`5HbSzUCGC~nzge@OOO=oB)Qy=5>w&G|ADRid_UifUG_o1&!4PTvP12C;#M7dsO z6?_3_f(EvJR9-vN#A_$fpLdQp2r8U8-qM37@3V^Sy2*#@{oopF%v=RySMGd7USR$@ zZOyoud%#x{`0j?VxNl+jlPC|zR=jIoJKh?JuXvw%E!>roE8eHx-);d%_$WG|u`ihu zGrkHSx}iI^@_o`tXzL21A1$mC9n3kTiI7S-VLAs)k&-bf_A|lbDJU5qj3 z>4ft0nzd)v`Oseu#cfwbakl-smTcbTxO3nttl$PHsWY>*g@DDWC@p zewQLTkE5W7q9meT5>b>ylpzuIYwH#wp;}le63iLXM5ju)-*iSzVat>qVT4D0JjN)S zQ70^2j5Ft0L_B99zNzV0-&{pqk5xrk)_m7^h;CkNshd-eS~s>d^*@_Us3x7pZqtoq zK1V&F`40Db~#1RYeO{QbL9rV9g{ZAi%Q{`l|oE8hGr(T6Ej8OYn(q6b!o?j{! mj`LouLxF+2AH!{#F?L7NSm&KijrCASQK(&R( Date: Thu, 29 Oct 2026 06:57:42 -0400 Subject: [PATCH 13/22] Preserve early async exits beyond the final protected fragment async def select(lock, pending, release): async with lock: record("body") if pending: return False if release: return False record("outside") return True Python 3.14 and later can place an early-return exit just past the context's last protected fragment. The parser consumes its exit counter too soon and exposes the later normal exit as an invalid call to None. Recognize a bounded, otherwise pure exit/return protocol owned by an open conditional arm, and keep the normal-exit slot available. Include Python 3.15's LOAD_COMMON_CONSTANT encoding and extra null SEND slot. Retain the existing 3.14 boundary; earlier superficially similar failures use a different context-exit path. When the exact three-None async exit intentionally lacks its modeled callable, restore only the clean state from before that call. Existing incomplete diagnostics remain set; other underflows stay diagnosed. Cover constant, bare and variable early returns, consecutive guards, ordinary body work, and user-authored awaits. Exercise suspension, entry/body/exit failures, cancellation and exception suppression. --- src/decompiler.rs | 36 ++++++- src/decompiler_stack_tests.rs | 32 ++++++ tests/behavior/cases/b_sunk_context_exits.py | 100 +++++++++++++++++++ tests/corpus/3.14.2/sunk_context_exits.py | 54 ++++++++++ tests/corpus/3.14.2/sunk_context_exits.pyc | Bin 0 -> 3158 bytes tests/corpus/3.15.2/sunk_context_exits.py | 54 ++++++++++ tests/corpus/3.15.2/sunk_context_exits.pyc | Bin 0 -> 3232 bytes tests/data/sunk_context_exits.py | 54 ++++++++++ tests/integration.rs | 17 ++++ tests/pyc/sunk_context_exits.3.14.pyc | Bin 0 -> 3158 bytes tests/pyc/sunk_context_exits.3.15.pyc | Bin 0 -> 3232 bytes 11 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 tests/behavior/cases/b_sunk_context_exits.py create mode 100644 tests/corpus/3.14.2/sunk_context_exits.py create mode 100644 tests/corpus/3.14.2/sunk_context_exits.pyc create mode 100644 tests/corpus/3.15.2/sunk_context_exits.py create mode 100644 tests/corpus/3.15.2/sunk_context_exits.pyc create mode 100644 tests/data/sunk_context_exits.py create mode 100644 tests/pyc/sunk_context_exits.3.14.pyc create mode 100644 tests/pyc/sunk_context_exits.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 969b2f8f..58dbdced 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -47507,6 +47507,7 @@ impl<'a> Ctx<'a> { /// method is already `Attribute { value: receiver }`, so the marker is /// only validated, not consumed into the AST. fn call_311(&mut self, argc: usize, _is_method: bool) { + let clean_before_call = self.clean; // 3.11+ assert-with-message: the assertion class and the message // sit as [cls, msg] with NO null slot, and CALL 0 consumes msg as // the self_or_null position (an implicit first argument). Without @@ -47548,6 +47549,7 @@ impl<'a> Ctx<'a> { // the iterable sits ABOVE the function and becomes the marker // Peek before popping to choose the right order. let n = self.stack.len(); + let missing_callable_slot = n == 0; let genexpr_case = n >= 2 && matches!(&self.stack[n - 1], Sv::E(_)) && matches!(&self.stack[n - 2], Sv::E(f) if is_comp_callable(f)); @@ -47748,10 +47750,34 @@ impl<'a> Ctx<'a> { && none_args && (func_is_none || func_is_underflow) { - let early_exit = self.blocks.iter().any(|block| { + let protected_early_exit = self.blocks.iter().any(|block| { block.kind == BlockType::With && block.end > self.cur_offset }) && matches!(self.blocks.last().map(|block| block.kind), Some(BlockType::If) | Some(BlockType::Else)); + // CPython 3.14+ can sink a constant-return protocol call + // immediately past the With's final protected fragment. The + // arm still owns it, and a later normal exit needs this slot. + let sunk_return_exit = self.version.at_least(3, 14) + && pos_args.len() == 3 + && self.blocks.iter().any(|block| block.kind == BlockType::With) + && self.blocks.last().filter(|block| { + matches!(block.kind, BlockType::If | BlockType::Else) + && block.end > self.cur_offset + }).zip(self.idx_of.get(&self.cur_offset)).map_or(false, |(arm, &index)| { + for item in self.instrs[index + 1..].iter().take_while(|item| item.offset < arm.end) { + if item.op == Op::RETURN_VALUE { return true; } + // 3.15 adds a null SEND slot after GET_AWAITABLE. + if item.op == Op::PUSH_NULL && self.version.at_least(3, 15) { continue; } + if !matches!(item.op, + Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::GET_AWAITABLE + | Op::LOAD_CONST | Op::LOAD_SMALL_INT | Op::LOAD_COMMON_CONSTANT + | Op::SEND | Op::YIELD_VALUE + | Op::RESUME | Op::JUMP_BACKWARD_NO_INTERRUPT | Op::END_SEND | Op::POP_TOP) + { return false; } + } + false + }); + let early_exit = protected_early_exit || sunk_return_exit; if !early_exit { self.with_exits -= 1; } @@ -47769,6 +47795,14 @@ impl<'a> Ctx<'a> { }); if next_awaitable { self.with_exit_await_drop = true; + // The exit callable is deliberately absent from this + // modeled stack. Its exact three-None async protocol + // has been consumed, so that one expected underflow + // does not indicate missing user code. Preserve every + // incomplete diagnostic that preceded this call. + if missing_callable_slot && argc == 3 { + self.clean = clean_before_call; + } } return; } diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index c1350058..cf4c6842 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -314,3 +314,35 @@ fn bounded_generator_construction_rejects_invalid_closure_slots() { let ctx = context(&parent, module.version(), vec![instruction(Op::GET_ITER, 0)]); assert!(ctx.sim_stack_region(0, 1, vec![]).is_none()); } + +#[test] +fn expected_async_exit_underflow_preserves_prior_diagnostics() { + let module = fixture(); + for minor in [14, 15] { + for initial_clean in [true, false] { + let mut next = instruction(Op::GET_AWAITABLE, 2); + next.offset = 2; + let mut ctx = context(&module.code, PythonVersion { minor, ..module.version() }, + vec![instruction(Op::CALL, 3), next]); + ctx.idx_of.insert(0, 0); + ctx.with_exits = 1; + ctx.clean = initial_clean; + ctx.stack = vec![Sv::E(Rc::new(Expr::Const(Rc::new(PyObject::None)))); 3]; + ctx.call_311(3, false); + assert!(ctx.with_exit_await_drop); + assert_eq!(ctx.clean, initial_clean, "prior diagnostics must survive"); + assert_eq!(ctx.with_exits, 0); + } + for (argc, awaited) in [(2, true), (3, false)] { + let mut next = instruction(if awaited { Op::GET_AWAITABLE } else { Op::POP_TOP }, 2); + next.offset = 2; + let mut ctx = context(&module.code, PythonVersion { minor, ..module.version() }, + vec![instruction(Op::CALL, argc as u32), next]); + ctx.idx_of.insert(0, 0); + ctx.with_exits = 1; + ctx.stack = vec![Sv::E(Rc::new(Expr::Const(Rc::new(PyObject::None)))); argc]; + ctx.call_311(argc, false); + assert!(!ctx.clean, "only the exact three-None async exit forgives its absent callable"); + } + } +} diff --git a/tests/behavior/cases/b_sunk_context_exits.py b/tests/behavior/cases/b_sunk_context_exits.py new file mode 100644 index 00000000..fc179ff4 --- /dev/null +++ b/tests/behavior/cases/b_sunk_context_exits.py @@ -0,0 +1,100 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True + + +events = [] + +class Lock: + async def __aenter__(self): + await asyncio.sleep(0) + events.append("enter") + if mode == "enter_error": + raise RuntimeError("enter") + return self + + async def __aexit__(self, kind, value, traceback): + await asyncio.sleep(0) + events.append(("exit", kind.__name__ if kind else None)) + if mode == "exit_error": + raise RuntimeError("exit") + return mode == "suppressed" + + +def record(value): + events.append(value) + if value == "body" and mode in ("body_error", "suppressed"): + raise ValueError("body") + if value == "body" and mode == "cancelled": + raise asyncio.CancelledError("cancelled") + + +async def user_call(*args): + await asyncio.sleep(0) + events.append(("user_call", args)) + if mode == "user_error": + raise RuntimeError("user") + + +for pending, release in ((False, False), (True, False), (False, True), (True, True)): + for mode in ("normal", "body_error", "suppressed", "cancelled", "enter_error", "exit_error", "user_error"): + for function in (false, bare, twice, value, user_await, normal): + events.clear() + print(function.__name__, pending, release, mode) + try: + print("return", asyncio.run(function(Lock(), pending, release, user_call))) + except (ValueError, RuntimeError, asyncio.CancelledError) as error: + print(type(error).__name__, str(error)) + print(events) diff --git a/tests/corpus/3.14.2/sunk_context_exits.py b/tests/corpus/3.14.2/sunk_context_exits.py new file mode 100644 index 00000000..65d9c33b --- /dev/null +++ b/tests/corpus/3.14.2/sunk_context_exits.py @@ -0,0 +1,54 @@ +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True + + diff --git a/tests/corpus/3.14.2/sunk_context_exits.pyc b/tests/corpus/3.14.2/sunk_context_exits.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7c0328831e6461585978c11f34d011ef342175e GIT binary patch literal 3158 zcmd5;T}vES7(Qpdc6L{1*IB<3UDsqA&6*OawW&stPAIh(B6N|ILO~|G)3D9P4QFPf zLN7vkV}dlaAmp;QN@*`j(-ca})JJ5-*bIC0$G3~0E&U9>`Quf#+yiorWmyaJSe65MRsu>{87OBJppsRA zYF10LOem*p&cT=t{x;h3BfxQ%!b}rvh)>F8sVq-Q{neqPJj>q1R%ZA$c}$I9-0K_b zW6l05PqFj7yZS`Fk+F8xhJDf1Pp^UBsPyr}#J*AbIByI6GTvv%QE>GT}eb9?i) zXZO10sY{c&(v;`Sc$3ad!E;YvnXRaA*hM^N;0gM4?g-#leEJ8m#4Ycp_oHsiU7a@` zM9m)(t*apxZ@lHvb;DcHnHrrN{6=ZIAC0X>t3B^{Ys;@;it90Jh`-CNBl>$X*g}() zQkSA&pKz;9xzhmVhq#qgeBC78#tD-FrZj6H?4RJ)NyG1o5PwD@d{)Pp|5?beeT9rv z6EgB4hKw>UNOGY9CWJ5z2U9utS&+B-SBAO z-nv@%#x{LD=dHPKVb<%hbFJjxe)ty*{vC#Y!QkIB${h{t^APU&3MW*l%a3zwK=B(I zh#%+HS;bGA#LsZTfdJ12IINv(3g46XIn}r)M~E8|VO+<^og-=uyE@c}YN9MWf!b0= zC3tdWIH1X2MpQh7cA&Z-8t?&jkBqB!vFwOujOjpog1Hk%Ys`If_cZQS)A%rj!BI-% z!xRRc>QX(}r`&4S?#97A2x}%BO18h5n!ig4Tk+LZeiqVkJpwJ$|E!*opqSg!cEJ;^_$rjq+%dp*yWcljZY#I* z@4xu9k@;TFe5Ger)xggyT8+nl598iXn|)N<*W6bzAu4qrNgOAM`}o2Ut6BY!Tc-Yz z0`_qf_rBm(qw=K*=9?Th{YFl_S+jb>Bzem4lM&)4CBhyZy*ms19Tq&eiFXla4-?ls zwf*S!JmgI5J8}1HCGL#6UV|&DaP0i-9;H~DDv8T>QMBQE^rE?gfbG)K>1%pEwv(Q# zq$i!k=Aq*#o`#MY(lJarX81yjRj+;!1%p+i%3Tx8kHU=Huh*=uu%CdEhTj+=o{|WU z>F6ELXu)AHI41;^CP*ql5C+>_WXo8Mtsg1$;mf6bx#)}t$~wUTG7a*Z%sGE3X*~YW S3_u5K4w%a0D{+8GV%(o6(jAik literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/sunk_context_exits.py b/tests/corpus/3.15.2/sunk_context_exits.py new file mode 100644 index 00000000..65d9c33b --- /dev/null +++ b/tests/corpus/3.15.2/sunk_context_exits.py @@ -0,0 +1,54 @@ +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True + + diff --git a/tests/corpus/3.15.2/sunk_context_exits.pyc b/tests/corpus/3.15.2/sunk_context_exits.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2b77fed439068b2bb965b3ae30d7ee0551b4804 GIT binary patch literal 3232 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 S3_u5K4w%a0D{+8GV%(o6(jAik literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_context_exits.3.15.pyc b/tests/pyc/sunk_context_exits.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2b77fed439068b2bb965b3ae30d7ee0551b4804 GIT binary patch literal 3232 zcmd5;&rcj>82-NbJ6WcCLn*gf zFR0ia^uW<8hsH!NTDc5=f|}HBOd1m}UQAj@NDK#`cM7{S9S|$6&g6af+3)x6^FHtQ zzGE~eM8?=3J*5>tdrvO{*B`!x){X<*fE8FP^I3tz^MU{`6hwHjAi+xo8D5@HyID>t zN?UX3Lx+6?TT5caF4h^XHOWT!MKKTp@uDzHqwt*PudoTVjJb3C297nRBry8j?Q>w~ z;Vs#6JN6VBdrs)xOl4zZuzpzU5tiHD zM{b;1WeK)@wo3w?ZCdDL8T=A@#|B2k44dXB#c94Qj@P93s-^jwTAdC2a>-rhHN|s_ zRj;%x)uc+bc)6xtb!STDnfGg|=T=k3>y@CQpy;0*e+ zGnc1|)tSJ(9!$B{%Yi>|b-pHFaw?eDj2ZMN`lE2avk%#W7VCDf82q9c3)dEn2TiG; zt(P`bW*)vB(EC}it}(f3VdN*N^?p;!MpM0KA=q4-#3DBj!KVkM_!pB2! zF%OZ)JQc1U3|ya7%0<^pfeZ`g7j~t#uH;XH4UHu&l6FJDkalvXv|H-Z{v_D+ zKftPxc9x`lhNPW^wDU4aJ4@0&tK3zALaw7ROi*js#`9bW(-z@Zd0te)*CKqDqh*yx zUW~k~=DYOOB+r}XYO_H&qhU;Za2d}hxYCrHCvgJ>!hS@30MdUO!O;i%*Eh%C1jsd~ zGV6MW!Qc@JkWsjJ?OPo?w0~=_RPS%4dYR&(o8m#HRNpV(F@V13c2-+S0fh#)dzG-o zBAn&qdn4bk<^??*;P$W?4jP2dXy}b+{7;F`ZbObl{&BFb?2!IL9j(9C zd#N7S+#=jQMTDn8IM7mu-6?;~?e@esGSIh4w1pD4GfF61gxfiq!we_fsoHPoxaaL- zX4q>GKB=KSp77reknmkd`e*ctPnCTmpw@b(%znI)*>lpkM-^&K_FShl?@lLod zRJ|)s#cRi)L70CE?r9-Y={C365>M?0QA>r~El_G>{>?-75K(Fnr9*rvZKvd~l0eX~ zsjOH)p~=aoRQrS;QEG-A2H|5G+H*=&L`J1WbO}X?CJM#qq|0m#+pxz~h<5r)wKQ9C m-}GKa8}c~wvv3bZ&iNxj;pU@M Date: Thu, 29 Oct 2026 07:02:04 -0400 Subject: [PATCH 14/22] Carry computed returns through nested context exits async def select(contexts, value): async with contexts[0]: async with contexts[1]: async with contexts[2]: return await value() The return value is evaluated while every context remains open, then carried across each exit before one function return. The parser matches only a single exit group, so nested groups leak protocol calls or lose the returned value. Extend the return-tail matcher to report complete exit-group counts. Accept another exact SWAP group only after the previous call result is discarded, and never exceed the known context-exit slots. When the inner context has closed, place the return in that inner body while retaining the contiguous outer contexts and their suppression-path exit slots. The multi-group SWAP layout applies from Python 3.11. Cover synchronous and asynchronous nesting, calls, tuple and dictionary values, awaited values, single-context controls and returns after a synchronous inner context. Exercise entry and exit failures, cancellation, suppression at each nesting level, and evaluation while the correct contexts are open. --- src/decompiler.rs | 54 +++++++- src/decompiler_stack_tests.rs | 35 ++++++ .../cases/b_nested_context_returns.py | 116 ++++++++++++++++++ .../corpus/3.11.14/nested_context_returns.py | 45 +++++++ .../corpus/3.11.14/nested_context_returns.pyc | Bin 0 -> 5893 bytes tests/corpus/3.12.3/nested_context_returns.py | 45 +++++++ .../corpus/3.12.3/nested_context_returns.pyc | Bin 0 -> 4155 bytes tests/corpus/3.13.7/nested_context_returns.py | 45 +++++++ .../corpus/3.13.7/nested_context_returns.pyc | Bin 0 -> 3991 bytes tests/corpus/3.14.2/nested_context_returns.py | 45 +++++++ .../corpus/3.14.2/nested_context_returns.pyc | Bin 0 -> 4563 bytes tests/corpus/3.15.2/nested_context_returns.py | 45 +++++++ .../corpus/3.15.2/nested_context_returns.pyc | Bin 0 -> 4678 bytes tests/data/nested_context_returns.py | 45 +++++++ tests/integration.rs | 17 +++ tests/pyc/nested_context_returns.3.11.pyc | Bin 0 -> 5893 bytes tests/pyc/nested_context_returns.3.12.pyc | Bin 0 -> 4155 bytes tests/pyc/nested_context_returns.3.13.pyc | Bin 0 -> 3991 bytes tests/pyc/nested_context_returns.3.14.pyc | Bin 0 -> 4563 bytes tests/pyc/nested_context_returns.3.15.pyc | Bin 0 -> 4678 bytes 20 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 tests/behavior/cases/b_nested_context_returns.py create mode 100644 tests/corpus/3.11.14/nested_context_returns.py create mode 100644 tests/corpus/3.11.14/nested_context_returns.pyc create mode 100644 tests/corpus/3.12.3/nested_context_returns.py create mode 100644 tests/corpus/3.12.3/nested_context_returns.pyc create mode 100644 tests/corpus/3.13.7/nested_context_returns.py create mode 100644 tests/corpus/3.13.7/nested_context_returns.pyc create mode 100644 tests/corpus/3.14.2/nested_context_returns.py create mode 100644 tests/corpus/3.14.2/nested_context_returns.pyc create mode 100644 tests/corpus/3.15.2/nested_context_returns.py create mode 100644 tests/corpus/3.15.2/nested_context_returns.pyc create mode 100644 tests/data/nested_context_returns.py create mode 100644 tests/pyc/nested_context_returns.3.11.pyc create mode 100644 tests/pyc/nested_context_returns.3.12.pyc create mode 100644 tests/pyc/nested_context_returns.3.13.pyc create mode 100644 tests/pyc/nested_context_returns.3.14.pyc create mode 100644 tests/pyc/nested_context_returns.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 58dbdced..da41b960 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -41920,6 +41920,10 @@ if split_cond { } fn with_return_tail_end(&self, k0: usize) -> Option { + self.with_return_tail_info(k0).map(|(after, _)| after) + } + + fn with_return_tail_info(&self, k0: usize) -> Option<(usize, usize)> { let first = self.instrs.get(k0)?; if !(first.op == Op::ROT_TWO || (first.op == Op::SWAP && (first.arg == 2 || first.arg == 3))) @@ -41946,6 +41950,8 @@ if split_cond { let mut call = false; let mut pop_after_call = false; let mut cleanup = false; + let mut remaining_exits = self.with_exits.saturating_sub(1); + let mut exit_groups = 1; while k < self.instrs.len() { let x = &self.instrs[k]; match x.op { @@ -41998,9 +42004,27 @@ if split_cond { pop_after_call = true; k += 1; } + // Nested modern contexts carry the same computed value + // across another complete exit group before one shared + // return. Only consume the exact SWAP pair after a finished + // exit, and never exceed the known context-exit slots. + Op::SWAP if (x.arg == 2 || x.arg == 3) && call && pop_after_call + && self.version.at_least(3, 11) + && remaining_exits > 0 + && (x.arg == 2 || matches!(self.instrs.get(k + 1).map(|next| (next.op, next.arg)), + Some((Op::SWAP, 2)))) => + { + remaining_exits -= 1; + exit_groups += 1; + nones = 0; + call = false; + pop_after_call = false; + cleanup = false; + k += if x.arg == 3 { 2 } else { 1 }; + } Op::RETURN_VALUE | Op::RETURN_CONST => { if cleanup || (call && pop_after_call) { - return Some(x.end()); + return Some((x.end(), exit_groups)); } return None; } @@ -42100,7 +42124,7 @@ if split_cond { let Some(&ki) = self.idx_of.get(&inst.offset) else { return false; }; - let Some(after) = self.with_return_tail_end(ki) else { + let Some((after, exit_groups)) = self.with_return_tail_info(ki) else { return false; }; // 3.14 guard-arm sunk exits: the With is still OPEN (its merged @@ -42142,6 +42166,32 @@ if split_cond { } return true; } + // The innermost context may have closed at the first exit group, + // leaving its With statement last in a still-open outer context. + // A proved multi-exit return belongs in that closed inner body. + // Contiguous outer contexts prove its nesting. Keep them open: + // their exit slots are still needed on the suppression path. + if exit_groups > 1 + && self.blocks.iter().rev().take(exit_groups - 1) + .all(|block| block.kind == BlockType::With) + && self.blocks.len() >= exit_groups + && matches!(self.blocks.last().and_then(|block| block.stmts.last()), Some(Stmt::With { .. })) + { + let value = self.pop_expr(); + let ret = match &*value { + Expr::Const(object) if matches!(&**object, PyObject::None) => Stmt::Return(None), + _ => Stmt::Return(Some(value)), + }; + if let Some(Stmt::With { body, .. }) = self.blocks.last_mut().and_then(|block| block.stmts.last_mut()) { + body.push(ret); + } + self.with_exits = self.with_exits.saturating_sub(1); + let skip_end = self.with_handler_skip_end(after); + if self.skip_until.map_or(true, |current| current < skip_end) { + self.skip_until = Some(skip_end); + } + return true; + } // A loop's out-of-line except fragments can extend the With's // region beyond its normal computed-return tail. The With is then // still open here although its loop has closed. This pure exit diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index cf4c6842..855bdb4b 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -346,3 +346,38 @@ fn expected_async_exit_underflow_preserves_prior_diagnostics() { } } } + +#[test] +fn nested_context_return_matcher_requires_complete_exit_groups_and_slots() { + let module = fixture(); + let none = module.code.consts.iter().position(|o| matches!(&**o, PyObject::None)).unwrap() as u32; + for minor in 11..=15 { + for groups in [1, 2, 3] { + let mut instrs = Vec::new(); + for _ in 0..groups { + instrs.extend([ + instruction(Op::SWAP, 3), instruction(Op::SWAP, 2), + instruction(Op::LOAD_CONST, none), instruction(Op::LOAD_CONST, none), + instruction(Op::LOAD_CONST, none), instruction(Op::CALL, 3), + instruction(Op::POP_TOP, 0), + ]); + } + instrs.push(instruction(Op::RETURN_VALUE, 0)); + for (i, inst) in instrs.iter_mut().enumerate() { inst.offset = i * 2; } + let end = instrs.last().unwrap().end(); + let mut ctx = context(&module.code, PythonVersion { minor, ..module.version() }, instrs); + ctx.with_exits = groups; + assert_eq!(ctx.with_return_tail_info(0), Some((end, groups))); + if groups > 1 { + ctx.with_exits = groups - 1; + assert!(ctx.with_return_tail_info(0).is_none(), "must not consume unknown exits"); + ctx.with_exits = groups; + ctx.instrs[8].arg = 1; + assert!(ctx.with_return_tail_info(0).is_none(), "second SWAP pair must be exact"); + ctx.instrs[8].arg = 2; + ctx.instrs[13].op = Op::NOP; + assert!(ctx.with_return_tail_info(0).is_none(), "each exit must discard its call result"); + } + } + } +} diff --git a/tests/behavior/cases/b_nested_context_returns.py b/tests/behavior/cases/b_nested_context_returns.py new file mode 100644 index 00000000..4426dd52 --- /dev/null +++ b/tests/behavior/cases/b_nested_context_returns.py @@ -0,0 +1,116 @@ +# MIN_VERSION: 3.11 +import asyncio + +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + +events = [] +opened = set() + +class Context: + def __init__(self, index): + self.index = index + + def __enter__(self): + events.append(("enter", self.index)) + if failure == "enter-" + str(self.index): + raise ValueError("entry") + opened.add(self.index) + return self + + def __exit__(self, kind, error, traceback): + events.append(("exit", self.index, kind.__name__ if kind else None)) + opened.remove(self.index) + if failure == "exit-" + str(self.index): + raise ValueError("exit") + return self.index == suppression and kind is ValueError + + async def __aenter__(self): + await asyncio.sleep(0) + return self.__enter__() + + async def __aexit__(self, kind, error, traceback): + await asyncio.sleep(0) + return self.__exit__(kind, error, traceback) + + +def value(): + events.append(("value", tuple(sorted(opened)))) + if failure == "body": + raise ValueError("body") + if failure == "cancelled": + raise asyncio.CancelledError("cancelled") + return 7 + + +async def async_value(): + await asyncio.sleep(0) + return value() + + +cases = [ + (sync_call, 2, False), + (sync_mapping, 3, False), + (async_call, 2, False), + (async_await, 3, True), + (async_tuple, 2, False), + (async_single, 1, True), + (sync_outer_return, 2, False), +] +for function, depth, isawait in cases: + for failure in ("", "body", "cancelled", "enter-0", "enter-1", "enter-2", "exit-0", "exit-1", "exit-2"): + for suppression in (-1, 0, 1, 2): + events.clear() + opened.clear() + print(function.__name__, failure, suppression) + contexts = [Context(index) for index in range(depth)] + try: + result = function(contexts, async_value if isawait else value) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except (ValueError, asyncio.CancelledError, UnboundLocalError) as error: + print(type(error).__name__, str(error)) + print(events, tuple(sorted(opened))) diff --git a/tests/corpus/3.11.14/nested_context_returns.py b/tests/corpus/3.11.14/nested_context_returns.py new file mode 100644 index 00000000..391f7397 --- /dev/null +++ b/tests/corpus/3.11.14/nested_context_returns.py @@ -0,0 +1,45 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + diff --git a/tests/corpus/3.11.14/nested_context_returns.pyc b/tests/corpus/3.11.14/nested_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df1823b0efb486046cca8099e5f7e0ace7ea61f2 GIT binary patch literal 5893 zcmb_gU1%It6ux)vOeV9t$!?OSY0@U9nraqXtC+M@OJpK#AB4Wt8fulYWL7Dpn=rF6 zEp0)Fh(eG}2{dBWDAE#(Xz>RGMP_{#d|V<0r7!KNARmx7i9rFWApuGT17wB_ zC>si(VzdFa8R`j=Zl9KfB|=o7aiYy%xUEZIr(cN22_?Xhyc~&KqSry^rqG*~4DiAr z*TM2lVV#nC{1fCU@&d8s1?fCHMHVO}r(h;ZG;Sqm#~}4apk$pLzlW~e!+lvN>(WO1 z^lUDlJ)gHWl=m_-^H~dh8XkT$lFwRsYrm1nXZGiS%o=AZnrF;xe%{Pk&&^%f(5wr& z=`+)rnHe(<8p<8BEGp@n=X!?P$?+B!zsfQ0Ax_14P*KVju-wfg}w;OcJbLjcO>jiS?^ZuUJB@mwssiIqpchE^D4$x(`W-7=8Zo?4o2Wv zS})dG<=2a}da;>{hsSbwMl_D(v~t{(VT()!;2s9;hHTC*SY{hCF;y#(Y{pQQ;DLHB zGdFiO_pX@)7FSvF#wt_NWda~#RnmX~Q0nSm-MxZw+W`!K(vG1uWli}?{YL$}ZxkZ9 z!gn!BqjP+TsQ|K)7k3A_S~DKX-z|eRSsmg3Bw*!l*Fs1bKBX!FJs7PVJx7 zo!?72@m<*f*Xgp?%TNb>{))fcq<1Ys#@A<^h9?6lbC_Mx^bzH=gq~1uB!K!c0X#b) z>#4Y%V!*I_DALBI6QQHzWMdNK?nU=eO|j*Hq4YR6FM7*Q;tmWh?8PAbJR zZqe@owNCh1|G*aBg)bETWZx=Xx(ESNDg2G8QqRs+edT2c)Jj`uOtsWB8r$@ie(q&U z_1{@nNW*Eo0w+`JEY|DOW4IG>*L}~#N%aQL%X$$W@^VriV{0)zseIcFl%0&}{hHm+ zklsa+9^~{`OrKDTlmdG44Lcgox39M(-@M(fc{VOW{8#fXO4ep1LaFbGHM#=zhIW8@Qz#8Sxq9-`scTb(%kCHZw|_A9?>*SR{esX*pvC%~#XbuL zf`ZURL01I>9>CTL20T2UivKU13?jKJmgQ8VI)l86YHdAH*X`AF3Q=Gy@yt=PJCCt3 zE$yOE=I(f=_HDTJ7KHk8a&m&LB_{`z?^F5#^^b0#?5=KL+6Q8jBwi#8IQ#$-FUp*5 zM`>{&HaVgdM;Ox2P^3pWJrR4gUHvmo0X+p&99U9YQb23wWN`=bL&&_1bUU+0Xu|nMrJF^U7Yit6dLQxGgn?h+@f8ogTv6W+mm+-3VAA(oi zA-wAP1)-+lgy8oMj|pHPD5$>_IJHEYO?c(hi8_P4K0SszE%`2#onjH#4tLenQ_DD8 zi|Z-nX16}h>{Lu2(Ch(*^dLogh|}XS{S~z+Q9w`9;eC>yT0Q|H$}J(7R;`{qq9hL{ zMp~+L=sI^^r!QM@o!2mi#euj$8NS=yzHPFMu9fyKIfxYuN)?qV5!0fTg&4DA5^ME%}B2BZ?dajT8AsxRCmb|s!xK2PWpTbK7~-!UM5p!>92G7z>T>usv7QNZJA zmA|*-FN93I%<}UqZxK9U_~!9r?CA3P|BtBLJjOl?5au3OgtuCR5m;Aeh+9{s)zds!#v` literal 0 HcmV?d00001 diff --git a/tests/corpus/3.12.3/nested_context_returns.py b/tests/corpus/3.12.3/nested_context_returns.py new file mode 100644 index 00000000..391f7397 --- /dev/null +++ b/tests/corpus/3.12.3/nested_context_returns.py @@ -0,0 +1,45 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + diff --git a/tests/corpus/3.12.3/nested_context_returns.pyc b/tests/corpus/3.12.3/nested_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..705c3539ee4413b1413c0672c90cea5b8628c277 GIT binary patch literal 4155 zcmd57)R7Ov{*>YASEvEwBEi!+0hcoGH#jxk{pVh8nD0_`GliL_DR5DlIIMKX5O zJ;qr4DnWrrUadnAjtB~WyFiq1Akl7C+ALC!_qG=#E{??ki3_%_V_WKdR zXcBRXCJ{j!2%;#k^frSinw43KW!VO?v@s2_tSyUVsm+)D$MFiMEO)z@vfQYA&rYfH zHo)I)sIEzuMXoD61|~fRQR%kX!A3=RyL2C18~l@Sx40GA_%u>-B>^5~CwZ2=iq!bP zm=UL>#B7C=HFKgeSDO;6gMAX5)t;)>4K1pB9bzdqwI8DwV+%%vQb2n`6hAO&-xQPU zvb;Vis%x}wTEkf>O{z2cy;ggx)Pb+X1owy{4KgL1Ss``{%$hL#cOZUdOGuUG_H+F7 zt1IN3+thW=J+r?^j9$fZty@Lt1!DEmEh7Aqo?5y^Wnf8{fE6c56KM4=D`Jt>-tlMf zE+@iihfi@gWc^hDO^4l-_>jBd(0KuLLAe{`KOCTMxa)+jm_Yw>U>6wNZL$Jk5e{>x z14qjyPMQr!Lpx~WZf8@_jvTXtxFVZGgl2bhx8Dr<^xBk_wpB35)65ADDByxru&FCW zHr`Wbnr)bu)zu9x?l!ldM^66*q zuHS2QW>&gcgFLThGx}a@oj3h2?$X;muqJ8Nlw2>0@HU`Oq(q_2MCMrD+XTDK82wUL z?-SvnZsKDv_y{x|9*Phj`@lz_3*mH+{0j65Ulc%>BkrKO#@#*Pe+!`hIM4+OJPZdR zld$7pQP9ENeW9S!Bph?#HAr%IzZE=S5$@xp@8@K{hr>;f(s-ZN@8@JQXr-GY1;NwS zhJvXGM-=P}8zWd-i1Bo{Rft=Jd#v}oNu zz0TAgO_D~r$Tm2~mP)ISQ!J9A9e-vUhW`{qeQo73hTzHAjn*w^5C4rSr;H&7WNd-N z(p>Xas)s;YsfD(`<39;sC&DhBFZvKsfayp|oag=4Me>jpYzJMKW_`>cVhT|f~dPav{i2p z+%A3jcb_Eo#eQsVEh-_~sNJ>ESuaL>i|Mw~c8iqDeh0jrviikTXqSB7?iXLm-S%bG zFRsb~bug*)EmPlIJUW)@zN)K&&I0{Xx*acKlIeF}4h_6CmK`6-9q_h7hg!`1`ymi_ f&KC^9yA~XiCl{K^C_yN`;21o%7&dvENA>0x5@;>G literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/nested_context_returns.py b/tests/corpus/3.13.7/nested_context_returns.py new file mode 100644 index 00000000..391f7397 --- /dev/null +++ b/tests/corpus/3.13.7/nested_context_returns.py @@ -0,0 +1,45 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + diff --git a/tests/corpus/3.13.7/nested_context_returns.pyc b/tests/corpus/3.13.7/nested_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5f791edbd2e003cfd0b4a3c18e3709f9c7522ed GIT binary patch literal 3991 zcmdT{%}*Og6rb^|cfISy#sSAR=EHm>!6^#lgP6onX^oUBl?qa=GzSi;3l5D$0PUKj zkyWWpr6^TMutJHWQYEdLrbW%6N!k*rYNa**K#!OnQniPOTMLIssE58cHemA+NYg`W zYsNpndGoxU_xsJfSu8107$M~0=$qe4?U%g2173O2JX+ibV1h(R1<^^ALQkU>=q*tO zJ&Ov^3sEcd)~E=**e?Zby_S_Ygh+t#XBO}9NGBkQ6_AAl2w=+!rgfwh?D#S)QTW9u zY0bw8Y+z|1wWNtO<|dFnc;3^N0I7kf0n*JnSPeN%t9g{dIQ`F<)lYJ>rdZcJhMk9) zWQt5#=4`z~gNgV%31iNBE;cX{H}JUvfw_i6+(;PBeX&HWc@SW{?^NFNlparv=!3?- z;R|!Jabd9cRBvoxK)1s@jB4ONeUHq_u2k(6|7?jn6}!@tE%T0to-*R_UG9Mo#r>iG zaf$b8&r<&DnE#r8$*6*vmqd-SQE9hzTn2DgIV6800WhheIi;e-w}Cfb{3QSrxZoQv zK8dpBL_~!X1*vGE+eyZHpMRE~Zr9dHT}zGaq%>&l(gj!n9slG!W^Yc=ht3rXjZ#PCHQX2XfSsuT0k}{ zCB#jNc65EdKY)0PEkLt+kiu%tDLGD~z?{N@laipz1 z8i;g9jG~+es=8+Ok&rLd(ypo(P^WW6c;qmaYmdk6>;ZzusUFyg2U&^ZgMEtm{Ivu_d zMi$^e?tMA=myw2u7nR6PI!ZO0c$EQ4DOx-Gm}-h`k^#CUX{FXLR6zHsrrI6|fWBA3 z&$Lj@A(~7?tWYqUD(@W>Ev2HiM>cm-&F3&ZD&l$xefHXMw3TXYf_a$FZ5JbDQpQ3f zRyD&^#7+r(iuM&5o|E(Jw-@JGe(%|egU4YrFXY_TjQck{IBVYmw!Q)FRcIa1kih|H ze7D*z)0PjVtr^^1@n2I$P$<|x^%6+_CNi<-WD+|cJCo4Mz^@#d@ji4L>txnb^C?Xy z$CKHry2+F2cR>)|`gAgt1h7Us*|O@ar!MPh00nvLp)0T9T6*HCODAt6QA;=@_fBn~ zEece}--|{Is)LovBn2H%eeHQZZHsT*U`oT*U>a#i4aI1J_8=$+NXKx$@dUWi8PD zcjrZ32#Jy5fw=C5b@rnWx;GUoWK$i*i%e4ETK5r?gH3GX;MI_=imA{c^ZH?9%%COY^Q|wew^cBFOR!2cU5EIq zb>OouUW4l{UMDSUl^P(=s_nacxl7dMb^P)IuU1<@rsGbeV^J|D>TzRaAYs660avU* zt{`{u;%jIm5!drKE4>27cSAD{LASOl=PQ2s7_73x_p8(WR4TYlfl+Z^g`1QQCSDTT z*r;uf^&`2BeW|p{_oX)WNYO;8D=6{X8n6C!toZnG#-+?t%^!fDf{QRna?fdNKReVn vG7x`RuLK)xs>Hyu^AAQTU9d3fUr+@aTyW&kE=1J}s)bf9Dgxc9<0kPp;FP-~ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/nested_context_returns.py b/tests/corpus/3.14.2/nested_context_returns.py new file mode 100644 index 00000000..391f7397 --- /dev/null +++ b/tests/corpus/3.14.2/nested_context_returns.py @@ -0,0 +1,45 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + diff --git a/tests/corpus/3.14.2/nested_context_returns.pyc b/tests/corpus/3.14.2/nested_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a123f6e82ae3becb5cdf23e3d5ead4b0c541a42 GIT binary patch literal 4563 zcmdT|U2GIp6ux(6cXobuw%y%!yW83B_K!j-6bmTIVqvC4q5&S(h$J;O8`zkdTAG<% zf3h(l_<}#l7I-K|9yAh_;FBheKB$=~(Kq8`TOTxPVti9DAt9Rho!QcMX|YvAp4Ec%R*TkVLKO;Sq2-v zrVZ~{+)))Dmo=++wnrFux%W$ zczk5Ra>Oo{%J#U^Kk>r6;=C|^Y~)yBY|K`%kKE$qAWpJbB|6zrHLAv}-aJ{T4p)cg z;>mNnu8S;^dU+T>)aF-=Z}en!_{QnluVnm+al`4r{aHde z&S>T~rOtyIcK9AViWfLZ-uY)d$et9|@^YRZS?1?$-1n-G$GFAQ>(H&`!DZgR<+MZb z_3?(@Yx$eyg|(hZ7B$xNigp0s^L)TQ@tkwm4g%W}!~$V#3Zwb3&xdCU6BDE3PuV(D zAu@RaVl_(4MpNfIri>|LHko-V_;%>4mQ7$rtnG|Z&yydc=_%vJWKBz4Iz4>Dr_fE# z4B`Q2V^%VFF=pnJX~oRwpQ%8uqC!cO>5sa-GHJFeq{(TPrhAeM>?Jj;x1LbM<(Dw8 zO@L6H2)tEEg`h@S5N@W00|?2dLBr?yN%3G%#jaJ-#M*n7(P?FhIDj-#|BUt6bioS; zAI2QOG%z)yi+>heT6^CRQ`eDr>eJ536S7E5HA<~qyM0<)N=^5Qqui!Yz74%Zd$_qH<_@TY_Xs2hWmtNW1{*KRt)_q%3lUC9 z)(*wXa;r1qwX1~t1d>}7tkz}2Fkagy?>?mRf0?{1Gv;3+`~lEsIlb3I?tjY050V!V z97W7SnENm(U^+0PcQ0TLrSIO^cQ+WdS}+!#D~y)xC|K8o>AVba`)HhP$-c>_Dm3QW zI^TGF>P3WIvTLexrUGzFjLyX~b1j{7E!~LJWEVtJ){ogATRQ20$qL0R?Wv8ktHf{$ z3H(%Ie+DsZB@n|v7ZUq9V%SO|hJmINLovk-bE~z{)kVUMn$@kl2Dj3UZdxRq)k(jF zTbYQvH9&Yk$5wZcTkWd5HvsI3I?nbQ5ulP6l?W#}>03GMy)?%;UdG4mzP^)_O}At{ z81*)DYiq*mQwiTEki1{Ut{JA`gc*$wMdX>VL^vi5_31N7K4ipZQYztYfn<+NtNj|z zoOyr`?Fr264gp``(tbm^65{*S$Q4;792ZD7D_E^d8;f_I{h!`>J8C6VU3q&4x85TN z-tCzG>sxOj&`RZrv7#MEd>IsIkNs(&%{8UY9hls`GN{&n@$I6EuZu3eU3Bra(#5xd zF22^_v~2b%pXxxa#?6hY8x9a|)M4tjakE!)GZNtqh^wHx(U-lPG<}j4(A@!U?u@zH zRl*MnB=^d&^g2E{Uo9)#SeZS}0sgzYZ#G(4rZCa|fo=4{=NWoojAA_jr{uZLRj1 zYe+BPYUNcFI!CbORKo0lzX#!NfD4)#2)@frCch^FUDl!dh?|1+u>|BY4y7w_!U4(5 zC{+IF5m!UArnm`#WKzLuoqrY^?Jx1XRV(T{ZnP&*AJ;d=M^U4%Y`)iOdk&^+O%Hi* zJ$36+&5AaOS{|FoI4{{nr#x12sG{xZX|MH2QNM&eQ7#qjg;v^5(mJNI8)CIa$@k#t zhv2nH>f46q>!{Wj+h-`%=BjaesdbJAFUn}0-&f3daH(|$j%#MOa=16-H|u_TW>XdI zU*3LV@oD70j$eRZ!y>D_wY=w<6UWPA#X+0?H-amIIh!DU7CGlv1(6$9!vWrVHL`#< T5XxK)3%u=DHNX>g3dVl`5&sPK literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/nested_context_returns.py b/tests/corpus/3.15.2/nested_context_returns.py new file mode 100644 index 00000000..391f7397 --- /dev/null +++ b/tests/corpus/3.15.2/nested_context_returns.py @@ -0,0 +1,45 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + diff --git a/tests/corpus/3.15.2/nested_context_returns.pyc b/tests/corpus/3.15.2/nested_context_returns.pyc new file mode 100644 index 0000000000000000000000000000000000000000..531c1f77d179ef7d529d2fb88e41037371c7f64c GIT binary patch literal 4678 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 literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_context_returns.3.12.pyc b/tests/pyc/nested_context_returns.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..705c3539ee4413b1413c0672c90cea5b8628c277 GIT binary patch literal 4155 zcmd57)R7Ov{*>YASEvEwBEi!+0hcoGH#jxk{pVh8nD0_`GliL_DR5DlIIMKX5O zJ;qr4DnWrrUadnAjtB~WyFiq1Akl7C+ALC!_qG=#E{??ki3_%_V_WKdR zXcBRXCJ{j!2%;#k^frSinw43KW!VO?v@s2_tSyUVsm+)D$MFiMEO)z@vfQYA&rYfH zHo)I)sIEzuMXoD61|~fRQR%kX!A3=RyL2C18~l@Sx40GA_%u>-B>^5~CwZ2=iq!bP zm=UL>#B7C=HFKgeSDO;6gMAX5)t;)>4K1pB9bzdqwI8DwV+%%vQb2n`6hAO&-xQPU zvb;Vis%x}wTEkf>O{z2cy;ggx)Pb+X1owy{4KgL1Ss``{%$hL#cOZUdOGuUG_H+F7 zt1IN3+thW=J+r?^j9$fZty@Lt1!DEmEh7Aqo?5y^Wnf8{fE6c56KM4=D`Jt>-tlMf zE+@iihfi@gWc^hDO^4l-_>jBd(0KuLLAe{`KOCTMxa)+jm_Yw>U>6wNZL$Jk5e{>x z14qjyPMQr!Lpx~WZf8@_jvTXtxFVZGgl2bhx8Dr<^xBk_wpB35)65ADDByxru&FCW zHr`Wbnr)bu)zu9x?l!ldM^66*q zuHS2QW>&gcgFLThGx}a@oj3h2?$X;muqJ8Nlw2>0@HU`Oq(q_2MCMrD+XTDK82wUL z?-SvnZsKDv_y{x|9*Phj`@lz_3*mH+{0j65Ulc%>BkrKO#@#*Pe+!`hIM4+OJPZdR zld$7pQP9ENeW9S!Bph?#HAr%IzZE=S5$@xp@8@K{hr>;f(s-ZN@8@JQXr-GY1;NwS zhJvXGM-=P}8zWd-i1Bo{Rft=Jd#v}oNu zz0TAgO_D~r$Tm2~mP)ISQ!J9A9e-vUhW`{qeQo73hTzHAjn*w^5C4rSr;H&7WNd-N z(p>Xas)s;YsfD(`<39;sC&DhBFZvKsfayp|oag=4Me>jpYzJMKW_`>cVhT|f~dPav{i2p z+%A3jcb_Eo#eQsVEh-_~sNJ>ESuaL>i|Mw~c8iqDeh0jrviikTXqSB7?iXLm-S%bG zFRsb~bug*)EmPlIJUW)@zN)K&&I0{Xx*acKlIeF}4h_6CmK`6-9q_h7hg!`1`ymi_ f&KC^9yA~XiCl{K^C_yN`;21o%7&dvENA>0x5@;>G literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_context_returns.3.13.pyc b/tests/pyc/nested_context_returns.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5f791edbd2e003cfd0b4a3c18e3709f9c7522ed GIT binary patch literal 3991 zcmdT{%}*Og6rb^|cfISy#sSAR=EHm>!6^#lgP6onX^oUBl?qa=GzSi;3l5D$0PUKj zkyWWpr6^TMutJHWQYEdLrbW%6N!k*rYNa**K#!OnQniPOTMLIssE58cHemA+NYg`W zYsNpndGoxU_xsJfSu8107$M~0=$qe4?U%g2173O2JX+ibV1h(R1<^^ALQkU>=q*tO zJ&Ov^3sEcd)~E=**e?Zby_S_Ygh+t#XBO}9NGBkQ6_AAl2w=+!rgfwh?D#S)QTW9u zY0bw8Y+z|1wWNtO<|dFnc;3^N0I7kf0n*JnSPeN%t9g{dIQ`F<)lYJ>rdZcJhMk9) zWQt5#=4`z~gNgV%31iNBE;cX{H}JUvfw_i6+(;PBeX&HWc@SW{?^NFNlparv=!3?- z;R|!Jabd9cRBvoxK)1s@jB4ONeUHq_u2k(6|7?jn6}!@tE%T0to-*R_UG9Mo#r>iG zaf$b8&r<&DnE#r8$*6*vmqd-SQE9hzTn2DgIV6800WhheIi;e-w}Cfb{3QSrxZoQv zK8dpBL_~!X1*vGE+eyZHpMRE~Zr9dHT}zGaq%>&l(gj!n9slG!W^Yc=ht3rXjZ#PCHQX2XfSsuT0k}{ zCB#jNc65EdKY)0PEkLt+kiu%tDLGD~z?{N@laipz1 z8i;g9jG~+es=8+Ok&rLd(ypo(P^WW6c;qmaYmdk6>;ZzusUFyg2U&^ZgMEtm{Ivu_d zMi$^e?tMA=myw2u7nR6PI!ZO0c$EQ4DOx-Gm}-h`k^#CUX{FXLR6zHsrrI6|fWBA3 z&$Lj@A(~7?tWYqUD(@W>Ev2HiM>cm-&F3&ZD&l$xefHXMw3TXYf_a$FZ5JbDQpQ3f zRyD&^#7+r(iuM&5o|E(Jw-@JGe(%|egU4YrFXY_TjQck{IBVYmw!Q)FRcIa1kih|H ze7D*z)0PjVtr^^1@n2I$P$<|x^%6+_CNi<-WD+|cJCo4Mz^@#d@ji4L>txnb^C?Xy z$CKHry2+F2cR>)|`gAgt1h7Us*|O@ar!MPh00nvLp)0T9T6*HCODAt6QA;=@_fBn~ zEece}--|{Is)LovBn2H%eeHQZZHsT*U`oT*U>a#i4aI1J_8=$+NXKx$@dUWi8PD zcjrZ32#Jy5fw=C5b@rnWx;GUoWK$i*i%e4ETK5r?gH3GX;MI_=imA{c^ZH?9%%COY^Q|wew^cBFOR!2cU5EIq zb>OouUW4l{UMDSUl^P(=s_nacxl7dMb^P)IuU1<@rsGbeV^J|D>TzRaAYs660avU* zt{`{u;%jIm5!drKE4>27cSAD{LASOl=PQ2s7_73x_p8(WR4TYlfl+Z^g`1QQCSDTT z*r;uf^&`2BeW|p{_oX)WNYO;8D=6{X8n6C!toZnG#-+?t%^!fDf{QRna?fdNKReVn vG7x`RuLK)xs>Hyu^AAQTU9d3fUr+@aTyW&kE=1J}s)bf9Dgxc9<0kPp;FP-~ literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_context_returns.3.14.pyc b/tests/pyc/nested_context_returns.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a123f6e82ae3becb5cdf23e3d5ead4b0c541a42 GIT binary patch literal 4563 zcmdT|U2GIp6ux(6cXobuw%y%!yW83B_K!j-6bmTIVqvC4q5&S(h$J;O8`zkdTAG<% zf3h(l_<}#l7I-K|9yAh_;FBheKB$=~(Kq8`TOTxPVti9DAt9Rho!QcMX|YvAp4Ec%R*TkVLKO;Sq2-v zrVZ~{+)))Dmo=++wnrFux%W$ zczk5Ra>Oo{%J#U^Kk>r6;=C|^Y~)yBY|K`%kKE$qAWpJbB|6zrHLAv}-aJ{T4p)cg z;>mNnu8S;^dU+T>)aF-=Z}en!_{QnluVnm+al`4r{aHde z&S>T~rOtyIcK9AViWfLZ-uY)d$et9|@^YRZS?1?$-1n-G$GFAQ>(H&`!DZgR<+MZb z_3?(@Yx$eyg|(hZ7B$xNigp0s^L)TQ@tkwm4g%W}!~$V#3Zwb3&xdCU6BDE3PuV(D zAu@RaVl_(4MpNfIri>|LHko-V_;%>4mQ7$rtnG|Z&yydc=_%vJWKBz4Iz4>Dr_fE# z4B`Q2V^%VFF=pnJX~oRwpQ%8uqC!cO>5sa-GHJFeq{(TPrhAeM>?Jj;x1LbM<(Dw8 zO@L6H2)tEEg`h@S5N@W00|?2dLBr?yN%3G%#jaJ-#M*n7(P?FhIDj-#|BUt6bioS; zAI2QOG%z)yi+>heT6^CRQ`eDr>eJ536S7E5HA<~qyM0<)N=^5Qqui!Yz74%Zd$_qH<_@TY_Xs2hWmtNW1{*KRt)_q%3lUC9 z)(*wXa;r1qwX1~t1d>}7tkz}2Fkagy?>?mRf0?{1Gv;3+`~lEsIlb3I?tjY050V!V z97W7SnENm(U^+0PcQ0TLrSIO^cQ+WdS}+!#D~y)xC|K8o>AVba`)HhP$-c>_Dm3QW zI^TGF>P3WIvTLexrUGzFjLyX~b1j{7E!~LJWEVtJ){ogATRQ20$qL0R?Wv8ktHf{$ z3H(%Ie+DsZB@n|v7ZUq9V%SO|hJmINLovk-bE~z{)kVUMn$@kl2Dj3UZdxRq)k(jF zTbYQvH9&Yk$5wZcTkWd5HvsI3I?nbQ5ulP6l?W#}>03GMy)?%;UdG4mzP^)_O}At{ z81*)DYiq*mQwiTEki1{Ut{JA`gc*$wMdX>VL^vi5_31N7K4ipZQYztYfn<+NtNj|z zoOyr`?Fr264gp``(tbm^65{*S$Q4;792ZD7D_E^d8;f_I{h!`>J8C6VU3q&4x85TN z-tCzG>sxOj&`RZrv7#MEd>IsIkNs(&%{8UY9hls`GN{&n@$I6EuZu3eU3Bra(#5xd zF22^_v~2b%pXxxa#?6hY8x9a|)M4tjakE!)GZNtqh^wHx(U-lPG<}j4(A@!U?u@zH zRl*MnB=^d&^g2E{Uo9)#SeZS}0sgzYZ#G(4rZCa|fo=4{=NWoojAA_jr{uZLRj1 zYe+BPYUNcFI!CbORKo0lzX#!NfD4)#2)@frCch^FUDl!dh?|1+u>|BY4y7w_!U4(5 zC{+IF5m!UArnm`#WKzLuoqrY^?Jx1XRV(T{ZnP&*AJ;d=M^U4%Y`)iOdk&^+O%Hi* zJ$36+&5AaOS{|FoI4{{nr#x12sG{xZX|MH2QNM&eQ7#qjg;v^5(mJNI8)CIa$@k#t zhv2nH>f46q>!{Wj+h-`%=BjaesdbJAFUn}0-&f3daH(|$j%#MOa=16-H|u_TW>XdI zU*3LV@oD70j$eRZ!y>D_wY=w<6UWPA#X+0?H-amIIh!DU7CGlv1(6$9!vWrVHL`#< T5XxK)3%u=DHNX>g3dVl`5&sPK literal 0 HcmV?d00001 diff --git a/tests/pyc/nested_context_returns.3.15.pyc b/tests/pyc/nested_context_returns.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..531c1f77d179ef7d529d2fb88e41037371c7f64c GIT binary patch literal 4678 zcmds5&2JM|5Z`Bey}SO{_$%1K*v9$9fg~iMAmS9XPM`;n8dpN7O@Ug$El44WcAZjW zA*J+^Ab|ss8dMInsyLuhsi$&4YPFmE0UVR`Ku<+nDv?w|idw0^Sv!PaLqY+ys#i1l zd3N4=>z&`syqEW6x5LVDT=1h2_ocu3ejfmS@?j;k)B!LCNpsbl#-$lFOIx5VX)Clf zZG*O@d1yXuhqm_#aV{YYSV}X{9|=NBE&T>H206kVtj9T8k3JFd zVe9&^*pm@CYtGUq(A7a}nR8UK#knY#;2Mf9+{bv%&D<$VgbQO$x(-|STEpBaR>M6z z8~w5uk0p=d=v(X|t#FIGWmymhhKF+5&vM!V|7m7$G^^3OX>44m&t20dd7eWG}=dPa26#5JO z^ZwA(p2t>Bu9@h^A4m1Y@O^iv(7!xh{7TLj!^>J6C-E*0QIFlI}IX(1=>Vioimk*&TabnI}b1gm{o(|82 zqTle}3ipEBKn-8yN_Z_%9(cD*hnFXdRwC1Q|FX%U71Hd-0#m+J4S&<8bW3+6CFa&S z2cYh7z)GZP^XknaSvn=MbURZnw;_nYMvXJoTu!MkFz3=`P--hOZwy~5&{vmvzYQz}O1aVq@c;B33zt4p=I!wNbw~0(0nH$Xk#I#7P7y&55wC zJiov>Hc*e&(yH<-5xkiq!7JZ?e$`v8T%FcaOr;9-9Qrvjs|87h8;SNl)I*|QRqz@s zi3od3lieMI=aKT3XAlf*f?wQ`N;|J1Hy z_6wmNo_)oX?T+gvU&82&J!kc9H>h(-%7xe~# za0?@Qo2lCwSvrN3?AE)OvM-<~Wne=R;bVARxNAgMDsDHTBH>1%$08Y+q1|J&%7nWm z*w++FH=fmGHNC@sjn(Bv^`*1n09VfajWYjr`nsXV|l zNj>k6&IfDfgY`)5P#shtW+wTAU@c8@HBU)SV~W?86_Z{-SGW-B-Gii0H6ZDMZUlQn zNcvP0k{;-8u%|)NeJqvm>3$pGpexns*6UbmyHBsT5#A<~eK$+Bc=SCy;cgj24F^lL zxeO;y*dt?ZBZ{;ZjhH~ViIF|Q;BRbam-F1bJtEmFW5E%~MAU=O?HT7xz;0Ra;89kiUd&QyRlAgp_6qJ&(`>-Eb*R{yU}!j@ve% zA7t2mVu~dGECA^OgHe|lVW*%pQFT=|d33)_I4;3n7BFnVxutthV~`5l_g6mH50@S5 zkK#a3e%N2{WOryw@n}vV+{i|FbrYPhs&@Q1mCjiUb~USw4(2ra-HnY~i)S*gxhEbT z&1Kcn*Dhh4A?5Sv|nt6meGnUQz;2 zoz;N@PNhLg2 Date: Thu, 29 Oct 2026 07:09:18 -0400 Subject: [PATCH 15/22] Keep nested per-row handlers owned by their loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit async def select(owner): try: for row in owner.rows: try: await owner.arecord(row) except ValueError: owner.failed(row) owner.finish() except RuntimeError: owner.fallback() The bytecode can place an inner dispatch after the enclosing mainline but before the outer dispatch. Treating that physical order as lexical ownership moves the inner try out of the loop, processing only the last row, or adopts the outer handler as one of the inner clauses. Record an intervening live For as the owner when the nested try opens. Use that ownership to avoid a flat enclosing rebuild, nested-handler adoption, and premature clause truncation at POP_EXCEPT fragments protected by the outer dispatch. Keep genuine nested-chain bounds. Cover named, multiple, sibling and same-type clauses; synchronous and async bodies with zero, one or two awaits; post-loop work; and nonloop and outer-finally controls. Python 3.12–3.15 examples preserve every row, outer fault scope, cancellation and failures raised by a handler itself. --- src/decompiler.rs | 32 ++- src/decompiler_stack_tests.rs | 1 + .../cases/b_loop_handler_ownership.py | 184 ++++++++++++++++++ tests/corpus/3.12.3/loop_handler_ownership.py | 121 ++++++++++++ .../corpus/3.12.3/loop_handler_ownership.pyc | Bin 0 -> 5811 bytes tests/corpus/3.13.7/loop_handler_ownership.py | 121 ++++++++++++ .../corpus/3.13.7/loop_handler_ownership.pyc | Bin 0 -> 5856 bytes tests/corpus/3.14.2/loop_handler_ownership.py | 121 ++++++++++++ .../corpus/3.14.2/loop_handler_ownership.pyc | Bin 0 -> 5938 bytes tests/corpus/3.15.2/loop_handler_ownership.py | 121 ++++++++++++ .../corpus/3.15.2/loop_handler_ownership.pyc | Bin 0 -> 5979 bytes tests/data/loop_handler_ownership.py | 121 ++++++++++++ tests/integration.rs | 23 +++ tests/pyc/loop_handler_ownership.3.12.pyc | Bin 0 -> 5811 bytes tests/pyc/loop_handler_ownership.3.13.pyc | Bin 0 -> 5856 bytes tests/pyc/loop_handler_ownership.3.14.pyc | Bin 0 -> 5938 bytes tests/pyc/loop_handler_ownership.3.15.pyc | Bin 0 -> 5979 bytes 17 files changed, 842 insertions(+), 3 deletions(-) create mode 100644 tests/behavior/cases/b_loop_handler_ownership.py create mode 100644 tests/corpus/3.12.3/loop_handler_ownership.py create mode 100644 tests/corpus/3.12.3/loop_handler_ownership.pyc create mode 100644 tests/corpus/3.13.7/loop_handler_ownership.py create mode 100644 tests/corpus/3.13.7/loop_handler_ownership.pyc create mode 100644 tests/corpus/3.14.2/loop_handler_ownership.py create mode 100644 tests/corpus/3.14.2/loop_handler_ownership.pyc create mode 100644 tests/corpus/3.15.2/loop_handler_ownership.py create mode 100644 tests/corpus/3.15.2/loop_handler_ownership.pyc create mode 100644 tests/data/loop_handler_ownership.py create mode 100644 tests/pyc/loop_handler_ownership.3.12.pyc create mode 100644 tests/pyc/loop_handler_ownership.3.13.pyc create mode 100644 tests/pyc/loop_handler_ownership.3.14.pyc create mode 100644 tests/pyc/loop_handler_ownership.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index da41b960..72938b8e 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -572,6 +572,9 @@ struct Ctx<'a> { /// chains, rendering `if AttributeError:` garbage and skipping the /// outer body end so the outer tail never fired) nested_tail_defer: bool, + /// An intervening live For owns this nested try. Its out-of-line + /// handler must not adopt the outer dispatch or enter a flat rebuild. + lexical_loop_handlers: FastMap, pending_try_handlers: Vec>, pending_loop: Vec<(Option, Option, Option, Vec, bool)>, pending_with: Vec>, @@ -1225,6 +1228,7 @@ pub fn decompile_in_scope( sunk_pair_drop: None, sunk_while_lift: None, nested_tail_defer: false, + lexical_loop_handlers: FastMap::default(), pending_try_handlers: Vec::new(), pending_loop: Vec::new(), pending_with: Vec::new(), @@ -3518,7 +3522,7 @@ impl<'a> Ctx<'a> { // interact) and its skip_until strands the enclosing // region's tail if let Some(h) = tc.except_handler { - if self.try_ctxs.values().any(|r| { + if let Some(enclosing) = self.try_ctxs.values().filter(|r| { r.start < tc.start && r.region_end > tc.region_end && r.finally_handler.is_none() @@ -3526,8 +3530,20 @@ impl<'a> Ctx<'a> { && r.except_handler.map_or(false, |ho| { h >= r.body_end && h < ho }) - }) { - self.nested_tail_defer = true; + }).max_by_key(|r| r.start) { + let intervening_for = self.blocks.iter().any(|b| { + b.kind == BlockType::For + && b.start > enclosing.start + && b.start <= tc.start + && b.end >= tc.region_end + }); + if intervening_for { + self.lexical_loop_handlers.insert( + h, enclosing.except_handler.unwrap(), + ); + } else { + self.nested_tail_defer = true; + } } } let mut blk = Block::new(BlockType::Try, pos, tc.body_end); @@ -4206,6 +4222,9 @@ impl<'a> Ctx<'a> { && r.finally_handler.is_none() && r.except_handler .map_or(false, |h| h >= tc.body_end && h < h_outer) + && !r.except_handler.map_or(false, |h| { + self.lexical_loop_handlers.contains_key(&h) + }) }) .cloned(); if let Some(inner) = nested { @@ -6691,6 +6710,7 @@ impl<'a> Ctx<'a> { None => false, }; let nested = region_end > from && adj_reraise && (extent == region_end || tramp_gap) + && !self.lexical_loop_handlers.contains_key(&from) // the exception table must confirm the enclosure: a chain // genuinely nested inside the adjacent one has (part of) its // own region protected by an entry targeting the adjacent @@ -8514,6 +8534,12 @@ impl<'a> Ctx<'a> { fn dispatch_region_end(&self, from: usize) -> usize { let mut end = self.handler_region_end(from); for e in &self.exc_entries { + // A per-row clause's POP_EXCEPT can be protected by the + // enclosing dispatch. It does not introduce a nested clause + // body. All other genuine nested-chain bounds remain active. + if self.lexical_loop_handlers.get(&from) == Some(&e.target) { + continue; + } if e.start > from && e.start < end && e.target > from { // only a NESTED chain head bounds the region — an // except* clause body's lasti redirect targets in-chain diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index 855bdb4b..a7b86685 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -74,6 +74,7 @@ fn context(code: &CodeObject, version: PythonVersion, instrs: Vec) sunk_pair_drop: None, sunk_while_lift: None, nested_tail_defer: false, + lexical_loop_handlers: FastMap::default(), pending_try_handlers: Vec::new(), pending_loop: Vec::new(), pending_with: Vec::new(), diff --git a/tests/behavior/cases/b_loop_handler_ownership.py b/tests/behavior/cases/b_loop_handler_ownership.py new file mode 100644 index 00000000..c629bd7a --- /dev/null +++ b/tests/behavior/cases/b_loop_handler_ownership.py @@ -0,0 +1,184 @@ +# MIN_VERSION: 3.12 +import asyncio + +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + +class Owner: + def __init__(self): + self.rows = [] if case == "empty" else [1, 2, 3] + self.events = [] + self.row = 2 + + def record(self, row): + self.events.append(("record", row)) + if case in ("row-error", "handler-error") and row == 2: + raise ValueError("row") + if case == "key-error" and row == 2: + raise KeyError("key") + if case == "runtime-error": + raise RuntimeError("runtime") + if case == "cancel": + raise asyncio.CancelledError("cancelled") + + async def arecord(self, row): + await asyncio.sleep(0) + self.record(row) + + def second(self, row): + self.events.append(("second", row)) + if case == "second-error" and row == 2: + raise KeyError("second") + + async def asecond(self, row): + await asyncio.sleep(0) + self.second(row) + + def failed(self, row, *details): + self.events.append(("failed", row, details)) + if case == "handler-error": + raise RuntimeError("handler") + + def other(self, row, *details): + self.events.append(("other", row, details)) + + def finish(self): + self.events.append("finish") + if case == "finish-error": + raise RuntimeError("finish") + + def fallback(self, *details): + self.events.append(("fallback", details)) + + +functions = (named, multiple, siblings, same_type, async_0, async_1, async_2, outer_finally, nonloop) +for function in functions: + for case in ("normal", "empty", "row-error", "key-error", "second-error", "handler-error", "finish-error", "runtime-error", "cancel"): + owner = Owner() + print(function.__name__, case) + try: + result = function(owner) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events) diff --git a/tests/corpus/3.12.3/loop_handler_ownership.py b/tests/corpus/3.12.3/loop_handler_ownership.py new file mode 100644 index 00000000..c7d86ef8 --- /dev/null +++ b/tests/corpus/3.12.3/loop_handler_ownership.py @@ -0,0 +1,121 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + diff --git a/tests/corpus/3.12.3/loop_handler_ownership.pyc b/tests/corpus/3.12.3/loop_handler_ownership.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc8af3240e39143d53966b3c7ec64dcfcd506f16 GIT binary patch literal 5811 zcmchbO>7&-6~||GIICTe6jvf8k(8_tny#rNQj|2V8uf-3d5}txtGR(9i)PrW1ZfD<+T?Zi6eo?tvU3_NZ0WIa#yl_l$?PtaY_26wDNuXyhs;$k6ZR2Z~Z;W=!<#1B0(GP0d4<;;@#ayC=SygXYh&YwM> zDP(7J##vFtJfEMRm|v>!LS`*6+;zjedE^g2Usn?MmBfZWv{U79Z`(WuPjfk>A11%={+4vR z2aJ2Fp5&hm=)>HKbGWvKIXJ8&L;J9rjxcL7o({2ch=c7-?fcK0S`R+{Kh%~w;RA_O z9}Pjr&G#Ci$Sf_!5+TfVUnBj;vP-H5~o4F->VDMt; zJOGZv+Q7FqP$~=@s}Au7K59joTbM27=K=Iyxa`9FA47&rcQ!CBtK-Nt4~{o84gP9Z zE7PGTGyPR~0ErhZ`LFwLp1c#j_0H}1y7tlo?Il6{c-zFEhG)GTN{4I2gK^b8^?oP-*Y@P)&E&T{q|@_vP5c_TiVcm zn|8k!?G@RCxuB3WaBvwgYm!#daX5J4ZsyO;<_kYIjVKg20EhV($Po8H1MZ4CYD;*c zj{6rN;W7)3)*YznK`2=5Y2`m$ouOQnUN+eo2tc4N@&rsq3qUD7h1Q5M#1Q1j3 z3=lx{IRQi;xfj>N+|nfQSv?f(FhjdfO*3W<$MqA;I>EtiVh87@D{yRWLiQ$vO3yXw z{onJ`sEc8tt94f3g=)!V_6)?@4t<}nMQSTo10fCbv|yt?BdV$6q16hcdkEMxyMzXP3L zrur1U8q%ZWM)$X+k9xrP6kUId>%H9Sk>J{jP_%m)+R>Q)8na%JposNNiuP*^Y;RCx z5Se{J8EkKoy#mXKU%<5efp3+)&pF_?G9)I{eZZL_EB!4?rgp{WwDbF+{&R$<(vh(j zJ$nLVh&jxIoL7ud_-Efma(=TfIei1GbJyo?o&3BnwXUTeXenH+?2?#jkaUmz`>XJi zRAz64&Ip9k6cI|08xdWS{!Dcuuld0Gqz8PTQMhXn3fGnt$2H5K1a51HqCL#ePQ*}g zj!IbaI2G!{!1jjfSUXT1c~+<(+CrUcNp2kHf#Fm;t9HO}5{?+4gz=|HFpt?NdLZof zDB`<(1B#7nb6dxr7*l%dm}1X#Bk&-=V=eG<7aq-mwkMCT8cAs29XQNY$ZATaa#zW? zDwNCz6Ie5k_q9}u2MO(f^3_0d>{Sf`p^rDT)H72$2hVnyrU#MI<4EZNq%hL;H9v9b@G_3CfqD7T7Tc_AYY8 zdSe3~PFKFQ?w%t?fxEptT__GNL2jM`(WT775@TssWrjZtL?Wn-f>r6yZ+G7 zp6k7RT525SpfMPtiUb;CYlPqNxM%k-HrBTPq-<|RU7TJVyDMcvCSv;MZ{`R zgz6$56H}|uGr^*GjWx0bjS~^QZ7av-irIzP+zA8O!8bQwg$&ktlx|9$RNIUPARFp} WEFmu4%n!anm11b^5xzD literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/loop_handler_ownership.py b/tests/corpus/3.13.7/loop_handler_ownership.py new file mode 100644 index 00000000..c7d86ef8 --- /dev/null +++ b/tests/corpus/3.13.7/loop_handler_ownership.py @@ -0,0 +1,121 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + diff --git a/tests/corpus/3.13.7/loop_handler_ownership.pyc b/tests/corpus/3.13.7/loop_handler_ownership.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5737b67507115f23cd1d3ad04c4843823ba4057 GIT binary patch literal 5856 zcmdT|TW=f36`tAQ^0FkQRYYA#>Ox&2OOfrQ2IM$OY+$LTP1{HXLMRV43WJuY3o4Pq zF4a_N9||WAHPFI!E5q(X5uBF?>-Ww<&^s}pm7J?E(XFK3R$ zeKTzFnXzEmXBOVE%6>C9S1{+x+S}Qc_1x>GSuo4~h3xW5ZoVw6BAqPfm#w98-F6OMF_2PWCn0;}jP*}UP zl+Dktqo#IQYkVj$RF3zabZs+(lJqrsqnqAyNQ4K%(3f%kN+QbFKT#G zveo59)QP#j$IgrQJo7?#Mykt;b|>ckK5br@1luvqq$D?0GJZ0Y-zE`HIry#IMkPhm zLU9RDrzAJ&znerICgrVzeUl!x99&&rDK4)8^57%ZLf3x-L+I8T(5*&K?MHV@GrC(3 zNB5`gNy5GyDSSPAYwB+M?b$ntZT;kZ{iMYHsbgdR+jz_?t?76b`*7}Q=}s5>2-rhW zioBFex2dJDACXdD zNCWIebg%~d2l~K{UqlCL0b=Jw?|B zj*b1d@vOjp6xcU3?L1zqfOocvb=d~dMmV4EYs z0?t0p^648`Tr8oCf1lqFGin5%Zo0o#V2@y+Ae)IdEvb&rLRj@V0Kgc}6hP%bwz4QX z5JaY|XddYKiaCI<2Zm%8YWRWZ=}g{Sy}o*T>Mxz6+xqBzebkd`Q2W}(9)H??7~32S zqwh!5w;mKe9A}MQ2bjyc)yP!W^<0qw!#sxeR4_DFDO39X?Zgw{Siq$OhM0* z!X*s}_}<9++HsI|_Gys?@kNB!)Le1rfyHw*EPB)=mrfS3uP}24M-o^fVJaIcnuA9V zB)jvHxSC#Vz+?Ra-XXL@<)$~G1uJzgg~tIXt6I!mlQc5hfuKBN4r72>9M;caDwGyd z>nY8LYSswKkM+@}2*ET!YAn*I#y)EWO#h~jJ~dMD>Ogqu0U$Na390>r)TDo_8y-Mv z^!jbZN4`IX4t9e2+DT54UTOQ2#s$@$6m<1GC;tNZC7ww4iBeKQ|57g}zfZv52VS`j z`8yut%UM5>7#;w1;q4G*rAn%VsF2(k3`9T{TxClIAtojTG%aB-=4J_j@#GxEje}Cme`YU@Bq-5$o zWKHReajA4H`rnSm$rtjp4QGxbR={ChhM^DXSL4`W3k^Svv|W4i>YKMZK8XJ+{va}h z4K~_U3o$(bUE6g16I`cVx1A8K?ENd;Mbg(p(>$?GV4C-m{p^G1jexLy+|Zcq=i~~EOu`TS obL$KYR)3s-u4r8UJQ0EEYk?V*r>+Sj))V6TZg-ID<_oy(zn9vOApigX literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/loop_handler_ownership.py b/tests/corpus/3.14.2/loop_handler_ownership.py new file mode 100644 index 00000000..c7d86ef8 --- /dev/null +++ b/tests/corpus/3.14.2/loop_handler_ownership.py @@ -0,0 +1,121 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + diff --git a/tests/corpus/3.14.2/loop_handler_ownership.pyc b/tests/corpus/3.14.2/loop_handler_ownership.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6820e6e51e44536ed7875f928c7d498c99f39d8 GIT binary patch literal 5938 zcmdT|-)|dP6}~gqGyd&NHcsQ%j^iZFueRwzw+p+Qmc@>Ci%3+hOj;yVRJn^yse;vG$6 zulF;yDv9gVzoVU(PBumzX7dA#d0B!+@^^H@>B&ZD82@36CD{d6vyaM_CmD;gRe6!U z;ciB)-$m(yw|TZGH|IMk*JHzM2qUh_ugde^W(_Yd<=HGBVtEPk1e*6^o{82xJo`m6-!*5Ur?1av&9Wh#o%U2+dg(2rs_MC=lD=H^ozE85a{IF#^q`tvsNtUR~lVBZ)OXHOWCC#Z1Ad5daIbzs~qE1l^l&=(5s(FCaZ_bIiqYm zznm>+pD&b3s~0b4i_3+aeo@?FT+Xi^UtO;%#q3IMS?|S7^!&zqFjrVj_9$Zy`y#hq zx%tXZ=4vtz9)GCy-wNCa-2T?zwUI4_g%dR&^M> zj$sFce6bk5;KSPz&Bl;mGiv>I&5-vU8KN~uvN5Eu8MS_&o*_B`c-p5E>2-ySN`~03 zlN41w^SSjpS(H-wGC)t1*Xa%DBqlnsxXA*Y9=PgRSu2$Ds{lU~iiPO`p^{I}=s_1!slH@c}Ed!QW?ESTw<1?TXXS9{X&1`FWa z_ooLf79e1b2PkqvBKtxt9abtK74b?~L8LOs$&dTz0=zOAopbZQx)t~~cF+qJ3jh8= z?11`qr|=>Rw6qxGH<~RAOt-SY=VcJSW1J1My|O@4`AP8?UT;(QcAgw+jp4|X_A%yv z%vjqf-Lru3CR$rI>MBpvS>ShKK~*)7H;T(T(M}JO>0kiS#EAw*{!$@d{GOpl&|wIN z@dp^9LBK`>9vp4a;3I9Mrt;uOD?s`mB>jJ`lNWsrURXDyFRA|M?v96r<&Fa-_DvNQNFB=^8%V!t11agJXf~;KD7P-O z;Qw?Wbz2~dcf$6>q!<5Yv#aJ6?0 zGDJAD`fj!`h{~hCi^}7xm>!i!Q*+_mkEfum{T!|tk;oque%zl9aC0)6cJqo`fo}s6 z<*s*eY`XxYQK;23uRFdtFtzKZrM)fS;9oYH1!k&($6BiSBF{^8O>2WoTV}T_*oLgr z*t$s7RJ~6m>zUl0HP(wu7r&$@5S+zf=rI3jz-7=@vRV*0xMa2Z+TbDz)xk!g`mra1 z`jpDk!T%a(Fzr<-&7}8wQVDjmcUJy=7|wkep})jaF~uCZ_zgnQ5BEXQq1dgJ8!LC_{&r|;Q=59AO*u*ra$@iF zqfh(xQ=h%z^qo4Kd<3T{b~Bt(QZr`$WFM;#a^I;MX1#mWMHERuHSGqmHc|t#o z0nXttehyQYIIq$!aV}&zJ2*eoraB@7S2tB_0eiK!)dRSGqD?(DVps4`5WsODHq8mK zNkVK~-H5<5B`K9qP1!}h8b${*DxZ_1arNYRs@K}13R}vik zD-vZgk`^|R1p<0xjZ$|u);<NU&R6xa2k+$>nDa4>*%(w(Y<+7>_VyDh~_+g;; z`ogt^+x@>C{N>r9+$(825s#$lM{wK! E0MHYVjsO4v literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/loop_handler_ownership.py b/tests/corpus/3.15.2/loop_handler_ownership.py new file mode 100644 index 00000000..c7d86ef8 --- /dev/null +++ b/tests/corpus/3.15.2/loop_handler_ownership.py @@ -0,0 +1,121 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + diff --git a/tests/corpus/3.15.2/loop_handler_ownership.pyc b/tests/corpus/3.15.2/loop_handler_ownership.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6924565869467ab2379b4b11963d9c95fa18c070 GIT binary patch literal 5979 zcmdT|OK%(36~4pEnc?uI(bMKbGAS#P7)w^-!Yyh?4oJmmD+f&(!6^`4AF zSp=RLWUMHO5p+#kJ1ZTl%{thtQIKfz$6O5@+vSE0MFu4k`uQ~GYgQ*j%`i)KYJvWrDysp5SxlV8i8HjJWCQC`mE^4X;d zH%oMqE9A`OivNYRLMgXW8{&C6lh0qsEdJ;wugJxVg{)EGn6DydX$F%X{dg=^87pPY zk~y=KDP?Bz#p3Gue1Epid@L7WS0yb7t#HjufqI{ZMi)1;k*3@ zubueaiC@lbxp?5{U48JH|EmA`!r%4yw#)`bw!BOWUh!Y{|2nv>Fu#6($A{<+lbXi9 zclbv3-N!fe>D&7B?iNSa?heKt#3;<~30w(Y4!&VtKmPhFo1W2+JfolZdUm&D^gS?V zaU+-X#_>D< zD;ztZTsEfF7&|@$ttJIlb(5tCbpMoUjL1=?}j(^soVOLV8V3QOrUgoNl%T`m;mRN zFBP$wfPgg-q{#6|YDg{X3L@np8If{?lRx5{3-WRzJg4ywH5q;jH|VK~9RI6sxPkoJ zU5*!ZpplKSMy<8ffkdMYd{O?O)`ZzG>r@Boxu0_WnP;1Fd^1ai8gn?Zqt;y^_)Q8@}rMjxg&h+91IlJ-(&zK)ey43J{k^%Q0dy_NnzYO5i3Ph<8BwXYlk*Y<3{F!K~M=kp}{Blqtzeo6e zzBv~!4~J8;ygVzzcP7MWH-vZ;(c?gieT#(+RC=*(ZJ|k6McDj&3s9S-lPzf+MAx|n z1s)BkstweNcKXMQ+0|06Sg59Kk(@%zGeO(Mr0;Bc+5M1Dm0BfeCSTZwQ=<7G{TzXwd0 zFxNc~z!U@LHgNK#tLU!|P|f+%&cO}v;Lp{X4QSHC@kaLi8ef*GxvdE@P4&IClHYV0 zq{B$y#~x@HOPco#H$U1hl^KxYzt(BWDV6M>&Kph zP^u(UL;p6vg=K&4O1Y%h^kj^^**oige*n%c3jH;j99FFm$GH`i$R8GdERuYhTPHb& zSf?cNpC;cKII(VlbEv!0e3~7?3&|lfT$DCY_|GU_z;Zs`D8&y2lgiG7+dj*_NQOch zZz`oDMS66P#{cUJ+Q=hz}PdcU{ z)X3h%{ZIS%%S4@F^%B+xtbzoqDfZ@IQgi=FN~+!uWO#obK+KY<+fpS2EG?Q$sMceg z;*%2j3E@vgQa)Z*IYySf68Wfx!461!savF-_-~MgkA70~ZOxX29q{>71D_7n%T6|w zYJkt`)NH|M^TvRk^!AmkosyoXS|`C(2B6k-oc(pvn*BNg z^_X!O6P(3i{uZVRR84M9du~*}R-oS2C)*+fU6quazg5X?_W-(|=#!6(-WA*w(0c^v zO=*PQaYFB?_k%DzOOcXeUQ3n9_YYuz6{E4%s6>%5VaKB>MJ>BHddspzz9M`rlA7e@ zF^=)&35onk^7o`p@kR45+{Olu|KECx?;8iO+HkKY(rQJOZ4uU<{Pv{0Kyj^v-5_i+ zVLNHvcnpte5r_E$7}8y;eBfb4)J6@;;r$(LkNR(v&@H6~u-^n9m`AXJEv?-9Nvpo( z6RmQJ^sIQb|A4cBBUF94$Xx9Wd8W4U*W6=&)H!S}#u^`lPCEM`^R+S_xrewc8gh@F zHTKP3t#edgs#4tnAqCX*QVA7euOlG|g=Eqb7_VugLa{(we#Qi1MI7b@7<#FS9Kvp% zHg?zFd*!*y&s`sUEA;!&9sl8Nuh_xUm>z~UZtMRT{j|fkrJ-l{pS}YmpB=Q&1C!Q! zH4v?)jK>k9KW-8uwz(uppS!%0{(0CBGZKUu6{oiGAr{pnednM@(v1Wz F`wzS@j>Z50 literal 0 HcmV?d00001 diff --git a/tests/data/loop_handler_ownership.py b/tests/data/loop_handler_ownership.py new file mode 100644 index 00000000..c7d86ef8 --- /dev/null +++ b/tests/data/loop_handler_ownership.py @@ -0,0 +1,121 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + diff --git a/tests/integration.rs b/tests/integration.rs index 2ea735df..704dee84 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -287,6 +287,29 @@ fn nested_contexts_carry_one_return_through_every_exit() { } } +#[test] +fn per_row_handlers_remain_owned_by_their_loop_and_outer_try() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("loop_handler_ownership.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "try:\n for row in owner.rows:\n try:\n owner.record(row)\n except ValueError as error:", + " except KeyError as error:\n owner.other(row, str(error))", + " try:\n owner.second(row)\n except KeyError:", + " except Exception:\n owner.failed(row)", + " owner.finish()\n except RuntimeError as error:", + " owner.finish()\n except Exception as error:", + " await owner.arecord(row)\n await owner.asecond(row)\n except ValueError:", + " finally:\n owner.finish()", + "def nonloop(owner):\n try:\n try:\n owner.record(owner.row)", + " except ValueError:\n owner.failed(owner.row)", + ], version); + // The compiler may sink implicit None returns into either arm. + let nonloop = src.split("def nonloop(owner):").nth(1).unwrap(); + assert!(nonloop.contains(" except RuntimeError:\n owner.fallback()"), "{src}"); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/loop_handler_ownership.3.12.pyc b/tests/pyc/loop_handler_ownership.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc8af3240e39143d53966b3c7ec64dcfcd506f16 GIT binary patch literal 5811 zcmchbO>7&-6~||GIICTe6jvf8k(8_tny#rNQj|2V8uf-3d5}txtGR(9i)PrW1ZfD<+T?Zi6eo?tvU3_NZ0WIa#yl_l$?PtaY_26wDNuXyhs;$k6ZR2Z~Z;W=!<#1B0(GP0d4<;;@#ayC=SygXYh&YwM> zDP(7J##vFtJfEMRm|v>!LS`*6+;zjedE^g2Usn?MmBfZWv{U79Z`(WuPjfk>A11%={+4vR z2aJ2Fp5&hm=)>HKbGWvKIXJ8&L;J9rjxcL7o({2ch=c7-?fcK0S`R+{Kh%~w;RA_O z9}Pjr&G#Ci$Sf_!5+TfVUnBj;vP-H5~o4F->VDMt; zJOGZv+Q7FqP$~=@s}Au7K59joTbM27=K=Iyxa`9FA47&rcQ!CBtK-Nt4~{o84gP9Z zE7PGTGyPR~0ErhZ`LFwLp1c#j_0H}1y7tlo?Il6{c-zFEhG)GTN{4I2gK^b8^?oP-*Y@P)&E&T{q|@_vP5c_TiVcm zn|8k!?G@RCxuB3WaBvwgYm!#daX5J4ZsyO;<_kYIjVKg20EhV($Po8H1MZ4CYD;*c zj{6rN;W7)3)*YznK`2=5Y2`m$ouOQnUN+eo2tc4N@&rsq3qUD7h1Q5M#1Q1j3 z3=lx{IRQi;xfj>N+|nfQSv?f(FhjdfO*3W<$MqA;I>EtiVh87@D{yRWLiQ$vO3yXw z{onJ`sEc8tt94f3g=)!V_6)?@4t<}nMQSTo10fCbv|yt?BdV$6q16hcdkEMxyMzXP3L zrur1U8q%ZWM)$X+k9xrP6kUId>%H9Sk>J{jP_%m)+R>Q)8na%JposNNiuP*^Y;RCx z5Se{J8EkKoy#mXKU%<5efp3+)&pF_?G9)I{eZZL_EB!4?rgp{WwDbF+{&R$<(vh(j zJ$nLVh&jxIoL7ud_-Efma(=TfIei1GbJyo?o&3BnwXUTeXenH+?2?#jkaUmz`>XJi zRAz64&Ip9k6cI|08xdWS{!Dcuuld0Gqz8PTQMhXn3fGnt$2H5K1a51HqCL#ePQ*}g zj!IbaI2G!{!1jjfSUXT1c~+<(+CrUcNp2kHf#Fm;t9HO}5{?+4gz=|HFpt?NdLZof zDB`<(1B#7nb6dxr7*l%dm}1X#Bk&-=V=eG<7aq-mwkMCT8cAs29XQNY$ZATaa#zW? zDwNCz6Ie5k_q9}u2MO(f^3_0d>{Sf`p^rDT)H72$2hVnyrU#MI<4EZNq%hL;H9v9b@G_3CfqD7T7Tc_AYY8 zdSe3~PFKFQ?w%t?fxEptT__GNL2jM`(WT775@TssWrjZtL?Wn-f>r6yZ+G7 zp6k7RT525SpfMPtiUb;CYlPqNxM%k-HrBTPq-<|RU7TJVyDMcvCSv;MZ{`R zgz6$56H}|uGr^*GjWx0bjS~^QZ7av-irIzP+zA8O!8bQwg$&ktlx|9$RNIUPARFp} WEFmu4%n!anm11b^5xzD literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_handler_ownership.3.13.pyc b/tests/pyc/loop_handler_ownership.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5737b67507115f23cd1d3ad04c4843823ba4057 GIT binary patch literal 5856 zcmdT|TW=f36`tAQ^0FkQRYYA#>Ox&2OOfrQ2IM$OY+$LTP1{HXLMRV43WJuY3o4Pq zF4a_N9||WAHPFI!E5q(X5uBF?>-Ww<&^s}pm7J?E(XFK3R$ zeKTzFnXzEmXBOVE%6>C9S1{+x+S}Qc_1x>GSuo4~h3xW5ZoVw6BAqPfm#w98-F6OMF_2PWCn0;}jP*}UP zl+Dktqo#IQYkVj$RF3zabZs+(lJqrsqnqAyNQ4K%(3f%kN+QbFKT#G zveo59)QP#j$IgrQJo7?#Mykt;b|>ckK5br@1luvqq$D?0GJZ0Y-zE`HIry#IMkPhm zLU9RDrzAJ&znerICgrVzeUl!x99&&rDK4)8^57%ZLf3x-L+I8T(5*&K?MHV@GrC(3 zNB5`gNy5GyDSSPAYwB+M?b$ntZT;kZ{iMYHsbgdR+jz_?t?76b`*7}Q=}s5>2-rhW zioBFex2dJDACXdD zNCWIebg%~d2l~K{UqlCL0b=Jw?|B zj*b1d@vOjp6xcU3?L1zqfOocvb=d~dMmV4EYs z0?t0p^648`Tr8oCf1lqFGin5%Zo0o#V2@y+Ae)IdEvb&rLRj@V0Kgc}6hP%bwz4QX z5JaY|XddYKiaCI<2Zm%8YWRWZ=}g{Sy}o*T>Mxz6+xqBzebkd`Q2W}(9)H??7~32S zqwh!5w;mKe9A}MQ2bjyc)yP!W^<0qw!#sxeR4_DFDO39X?Zgw{Siq$OhM0* z!X*s}_}<9++HsI|_Gys?@kNB!)Le1rfyHw*EPB)=mrfS3uP}24M-o^fVJaIcnuA9V zB)jvHxSC#Vz+?Ra-XXL@<)$~G1uJzgg~tIXt6I!mlQc5hfuKBN4r72>9M;caDwGyd z>nY8LYSswKkM+@}2*ET!YAn*I#y)EWO#h~jJ~dMD>Ogqu0U$Na390>r)TDo_8y-Mv z^!jbZN4`IX4t9e2+DT54UTOQ2#s$@$6m<1GC;tNZC7ww4iBeKQ|57g}zfZv52VS`j z`8yut%UM5>7#;w1;q4G*rAn%VsF2(k3`9T{TxClIAtojTG%aB-=4J_j@#GxEje}Cme`YU@Bq-5$o zWKHReajA4H`rnSm$rtjp4QGxbR={ChhM^DXSL4`W3k^Svv|W4i>YKMZK8XJ+{va}h z4K~_U3o$(bUE6g16I`cVx1A8K?ENd;Mbg(p(>$?GV4C-m{p^G1jexLy+|Zcq=i~~EOu`TS obL$KYR)3s-u4r8UJQ0EEYk?V*r>+Sj))V6TZg-ID<_oy(zn9vOApigX literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_handler_ownership.3.14.pyc b/tests/pyc/loop_handler_ownership.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6820e6e51e44536ed7875f928c7d498c99f39d8 GIT binary patch literal 5938 zcmdT|-)|dP6}~gqGyd&NHcsQ%j^iZFueRwzw+p+Qmc@>Ci%3+hOj;yVRJn^yse;vG$6 zulF;yDv9gVzoVU(PBumzX7dA#d0B!+@^^H@>B&ZD82@36CD{d6vyaM_CmD;gRe6!U z;ciB)-$m(yw|TZGH|IMk*JHzM2qUh_ugde^W(_Yd<=HGBVtEPk1e*6^o{82xJo`m6-!*5Ur?1av&9Wh#o%U2+dg(2rs_MC=lD=H^ozE85a{IF#^q`tvsNtUR~lVBZ)OXHOWCC#Z1Ad5daIbzs~qE1l^l&=(5s(FCaZ_bIiqYm zznm>+pD&b3s~0b4i_3+aeo@?FT+Xi^UtO;%#q3IMS?|S7^!&zqFjrVj_9$Zy`y#hq zx%tXZ=4vtz9)GCy-wNCa-2T?zwUI4_g%dR&^M> zj$sFce6bk5;KSPz&Bl;mGiv>I&5-vU8KN~uvN5Eu8MS_&o*_B`c-p5E>2-ySN`~03 zlN41w^SSjpS(H-wGC)t1*Xa%DBqlnsxXA*Y9=PgRSu2$Ds{lU~iiPO`p^{I}=s_1!slH@c}Ed!QW?ESTw<1?TXXS9{X&1`FWa z_ooLf79e1b2PkqvBKtxt9abtK74b?~L8LOs$&dTz0=zOAopbZQx)t~~cF+qJ3jh8= z?11`qr|=>Rw6qxGH<~RAOt-SY=VcJSW1J1My|O@4`AP8?UT;(QcAgw+jp4|X_A%yv z%vjqf-Lru3CR$rI>MBpvS>ShKK~*)7H;T(T(M}JO>0kiS#EAw*{!$@d{GOpl&|wIN z@dp^9LBK`>9vp4a;3I9Mrt;uOD?s`mB>jJ`lNWsrURXDyFRA|M?v96r<&Fa-_DvNQNFB=^8%V!t11agJXf~;KD7P-O z;Qw?Wbz2~dcf$6>q!<5Yv#aJ6?0 zGDJAD`fj!`h{~hCi^}7xm>!i!Q*+_mkEfum{T!|tk;oque%zl9aC0)6cJqo`fo}s6 z<*s*eY`XxYQK;23uRFdtFtzKZrM)fS;9oYH1!k&($6BiSBF{^8O>2WoTV}T_*oLgr z*t$s7RJ~6m>zUl0HP(wu7r&$@5S+zf=rI3jz-7=@vRV*0xMa2Z+TbDz)xk!g`mra1 z`jpDk!T%a(Fzr<-&7}8wQVDjmcUJy=7|wkep})jaF~uCZ_zgnQ5BEXQq1dgJ8!LC_{&r|;Q=59AO*u*ra$@iF zqfh(xQ=h%z^qo4Kd<3T{b~Bt(QZr`$WFM;#a^I;MX1#mWMHERuHSGqmHc|t#o z0nXttehyQYIIq$!aV}&zJ2*eoraB@7S2tB_0eiK!)dRSGqD?(DVps4`5WsODHq8mK zNkVK~-H5<5B`K9qP1!}h8b${*DxZ_1arNYRs@K}13R}vik zD-vZgk`^|R1p<0xjZ$|u);<NU&R6xa2k+$>nDa4>*%(w(Y<+7>_VyDh~_+g;; z`ogt^+x@>C{N>r9+$(825s#$lM{wK! E0MHYVjsO4v literal 0 HcmV?d00001 diff --git a/tests/pyc/loop_handler_ownership.3.15.pyc b/tests/pyc/loop_handler_ownership.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6924565869467ab2379b4b11963d9c95fa18c070 GIT binary patch literal 5979 zcmdT|OK%(36~4pEnc?uI(bMKbGAS#P7)w^-!Yyh?4oJmmD+f&(!6^`4AF zSp=RLWUMHO5p+#kJ1ZTl%{thtQIKfz$6O5@+vSE0MFu4k`uQ~GYgQ*j%`i)KYJvWrDysp5SxlV8i8HjJWCQC`mE^4X;d zH%oMqE9A`OivNYRLMgXW8{&C6lh0qsEdJ;wugJxVg{)EGn6DydX$F%X{dg=^87pPY zk~y=KDP?Bz#p3Gue1Epid@L7WS0yb7t#HjufqI{ZMi)1;k*3@ zubueaiC@lbxp?5{U48JH|EmA`!r%4yw#)`bw!BOWUh!Y{|2nv>Fu#6($A{<+lbXi9 zclbv3-N!fe>D&7B?iNSa?heKt#3;<~30w(Y4!&VtKmPhFo1W2+JfolZdUm&D^gS?V zaU+-X#_>D< zD;ztZTsEfF7&|@$ttJIlb(5tCbpMoUjL1=?}j(^soVOLV8V3QOrUgoNl%T`m;mRN zFBP$wfPgg-q{#6|YDg{X3L@np8If{?lRx5{3-WRzJg4ywH5q;jH|VK~9RI6sxPkoJ zU5*!ZpplKSMy<8ffkdMYd{O?O)`ZzG>r@Boxu0_WnP;1Fd^1ai8gn?Zqt;y^_)Q8@}rMjxg&h+91IlJ-(&zK)ey43J{k^%Q0dy_NnzYO5i3Ph<8BwXYlk*Y<3{F!K~M=kp}{Blqtzeo6e zzBv~!4~J8;ygVzzcP7MWH-vZ;(c?gieT#(+RC=*(ZJ|k6McDj&3s9S-lPzf+MAx|n z1s)BkstweNcKXMQ+0|06Sg59Kk(@%zGeO(Mr0;Bc+5M1Dm0BfeCSTZwQ=<7G{TzXwd0 zFxNc~z!U@LHgNK#tLU!|P|f+%&cO}v;Lp{X4QSHC@kaLi8ef*GxvdE@P4&IClHYV0 zq{B$y#~x@HOPco#H$U1hl^KxYzt(BWDV6M>&Kph zP^u(UL;p6vg=K&4O1Y%h^kj^^**oige*n%c3jH;j99FFm$GH`i$R8GdERuYhTPHb& zSf?cNpC;cKII(VlbEv!0e3~7?3&|lfT$DCY_|GU_z;Zs`D8&y2lgiG7+dj*_NQOch zZz`oDMS66P#{cUJ+Q=hz}PdcU{ z)X3h%{ZIS%%S4@F^%B+xtbzoqDfZ@IQgi=FN~+!uWO#obK+KY<+fpS2EG?Q$sMceg z;*%2j3E@vgQa)Z*IYySf68Wfx!461!savF-_-~MgkA70~ZOxX29q{>71D_7n%T6|w zYJkt`)NH|M^TvRk^!AmkosyoXS|`C(2B6k-oc(pvn*BNg z^_X!O6P(3i{uZVRR84M9du~*}R-oS2C)*+fU6quazg5X?_W-(|=#!6(-WA*w(0c^v zO=*PQaYFB?_k%DzOOcXeUQ3n9_YYuz6{E4%s6>%5VaKB>MJ>BHddspzz9M`rlA7e@ zF^=)&35onk^7o`p@kR45+{Olu|KECx?;8iO+HkKY(rQJOZ4uU<{Pv{0Kyj^v-5_i+ zVLNHvcnpte5r_E$7}8y;eBfb4)J6@;;r$(LkNR(v&@H6~u-^n9m`AXJEv?-9Nvpo( z6RmQJ^sIQb|A4cBBUF94$Xx9Wd8W4U*W6=&)H!S}#u^`lPCEM`^R+S_xrewc8gh@F zHTKP3t#edgs#4tnAqCX*QVA7euOlG|g=Eqb7_VugLa{(we#Qi1MI7b@7<#FS9Kvp% zHg?zFd*!*y&s`sUEA;!&9sl8Nuh_xUm>z~UZtMRT{j|fkrJ-l{pS}YmpB=Q&1C!Q! zH4v?)jK>k9KW-8uwz(uppS!%0{(0CBGZKUu6{oiGAr{pnednM@(v1Wz F`wzS@j>Z50 literal 0 HcmV?d00001 From d1c7d88d4090dab454f410f37c66dcbcb8101463 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 07:21:08 -0400 Subject: [PATCH 16/22] Close normal async exits without consuming return values or outer scopes async def select(owner): async with owner: async with owner.query("row0") as query: first = await query.read() async with owner.query("row1") as query: last = await query.read() value = owner.build(first, last) return value The parser recognizes synchronous context-exit calls but not their awaited counterparts, so work after an async context can stay inside it. Recognize GET_AWAITABLE 2 followed by a bounded SEND loop with its own backward edge, END_SEND and POP_TOP. User-authored awaits use operand 0 and remain visible. Enable this path for Python 3.14 and later, retaining the earlier exception-layout handling. Keep carried and constant returns out of normal-exit consumption, including padding around RETURN. Check exception-table ownership before consuming an inner exit protected by an outer context, and preserve the original protocol in displaced handler walks that have lost their lexical Try and With blocks. These safeguards are part of the recognizer so it never discards a return value or an enclosing context's exit slot. Python 3.15 adds a null SEND slot and shared None constants. Recognize both while scanning the header and the exit: otherwise the header scan mistakes protocol setup for the body and assigns an inner context its outer handler. Share the ownership check across both constant-loading entry points. Cover sequential and nested contexts, post-context work, returns within the outer context, user awaits, branch returns, padded return protocols, malformed suspension loops and displaced ownership. Behavior examples exercise actual suspension, cancellation, suppression, and failures while entering, reading, building and exiting. --- src/decompiler.rs | 123 +++++++++++- src/decompiler_stack_tests.rs | 94 +++++++++ .../cases/b_nested_context_returns.py | 8 + tests/behavior/cases/b_normal_async_exits.py | 189 ++++++++++++++++++ .../corpus/3.11.14/nested_context_returns.py | 6 + .../corpus/3.11.14/nested_context_returns.pyc | Bin 5893 -> 6726 bytes tests/corpus/3.12.3/nested_context_returns.py | 6 + .../corpus/3.12.3/nested_context_returns.pyc | Bin 4155 -> 4814 bytes tests/corpus/3.13.7/nested_context_returns.py | 6 + .../corpus/3.13.7/nested_context_returns.pyc | Bin 3991 -> 4619 bytes .../3.14.2/async_context_return_padding_1.py | 12 ++ .../3.14.2/async_context_return_padding_1.pyc | Bin 0 -> 1480 bytes .../3.14.2/async_context_return_padding_3.py | 12 ++ .../3.14.2/async_context_return_padding_3.pyc | Bin 0 -> 1496 bytes .../3.14.2/async_context_return_padding_9.py | 12 ++ .../3.14.2/async_context_return_padding_9.pyc | Bin 0 -> 1544 bytes tests/corpus/3.14.2/nested_context_returns.py | 6 + .../corpus/3.14.2/nested_context_returns.pyc | Bin 4563 -> 5273 bytes tests/corpus/3.14.2/normal_async_exits.py | 87 ++++++++ tests/corpus/3.14.2/normal_async_exits.pyc | Bin 0 -> 10314 bytes .../3.15.2/async_context_return_padding_1.py | 12 ++ .../3.15.2/async_context_return_padding_1.pyc | Bin 0 -> 1517 bytes .../3.15.2/async_context_return_padding_3.py | 12 ++ .../3.15.2/async_context_return_padding_3.pyc | Bin 0 -> 1533 bytes .../3.15.2/async_context_return_padding_9.py | 12 ++ .../3.15.2/async_context_return_padding_9.pyc | Bin 0 -> 1581 bytes tests/corpus/3.15.2/nested_context_returns.py | 6 + .../corpus/3.15.2/nested_context_returns.pyc | Bin 4678 -> 5414 bytes tests/corpus/3.15.2/normal_async_exits.py | 87 ++++++++ tests/corpus/3.15.2/normal_async_exits.pyc | Bin 0 -> 10660 bytes tests/data/async_context_return_padding.py | 12 ++ tests/data/nested_context_returns.py | 6 + tests/data/normal_async_exits.py | 87 ++++++++ tests/integration.rs | 34 ++++ .../async_context_return_padding_1.3.14.pyc | Bin 0 -> 1480 bytes .../async_context_return_padding_1.3.15.pyc | Bin 0 -> 1517 bytes .../async_context_return_padding_3.3.14.pyc | Bin 0 -> 1496 bytes .../async_context_return_padding_3.3.15.pyc | Bin 0 -> 1533 bytes .../async_context_return_padding_9.3.14.pyc | Bin 0 -> 1544 bytes .../async_context_return_padding_9.3.15.pyc | Bin 0 -> 1581 bytes tests/pyc/nested_context_returns.3.11.pyc | Bin 5893 -> 6726 bytes tests/pyc/nested_context_returns.3.12.pyc | Bin 4155 -> 4814 bytes tests/pyc/nested_context_returns.3.13.pyc | Bin 3991 -> 4619 bytes tests/pyc/nested_context_returns.3.14.pyc | Bin 4563 -> 5273 bytes tests/pyc/nested_context_returns.3.15.pyc | Bin 4678 -> 5414 bytes tests/pyc/normal_async_exits.3.14.pyc | Bin 0 -> 10314 bytes tests/pyc/normal_async_exits.3.15.pyc | Bin 0 -> 10660 bytes 47 files changed, 826 insertions(+), 3 deletions(-) create mode 100644 tests/behavior/cases/b_normal_async_exits.py create mode 100644 tests/corpus/3.14.2/async_context_return_padding_1.py create mode 100644 tests/corpus/3.14.2/async_context_return_padding_1.pyc create mode 100644 tests/corpus/3.14.2/async_context_return_padding_3.py create mode 100644 tests/corpus/3.14.2/async_context_return_padding_3.pyc create mode 100644 tests/corpus/3.14.2/async_context_return_padding_9.py create mode 100644 tests/corpus/3.14.2/async_context_return_padding_9.pyc create mode 100644 tests/corpus/3.14.2/normal_async_exits.py create mode 100644 tests/corpus/3.14.2/normal_async_exits.pyc create mode 100644 tests/corpus/3.15.2/async_context_return_padding_1.py create mode 100644 tests/corpus/3.15.2/async_context_return_padding_1.pyc create mode 100644 tests/corpus/3.15.2/async_context_return_padding_3.py create mode 100644 tests/corpus/3.15.2/async_context_return_padding_3.pyc create mode 100644 tests/corpus/3.15.2/async_context_return_padding_9.py create mode 100644 tests/corpus/3.15.2/async_context_return_padding_9.pyc create mode 100644 tests/corpus/3.15.2/normal_async_exits.py create mode 100644 tests/corpus/3.15.2/normal_async_exits.pyc create mode 100644 tests/data/async_context_return_padding.py create mode 100644 tests/data/normal_async_exits.py create mode 100644 tests/pyc/async_context_return_padding_1.3.14.pyc create mode 100644 tests/pyc/async_context_return_padding_1.3.15.pyc create mode 100644 tests/pyc/async_context_return_padding_3.3.14.pyc create mode 100644 tests/pyc/async_context_return_padding_3.3.15.pyc create mode 100644 tests/pyc/async_context_return_padding_9.3.14.pyc create mode 100644 tests/pyc/async_context_return_padding_9.3.15.pyc create mode 100644 tests/pyc/normal_async_exits.3.14.pyc create mode 100644 tests/pyc/normal_async_exits.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 72938b8e..c8f8befc 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -11312,7 +11312,7 @@ impl<'a> Ctx<'a> { } /// 3.12+ inline with success-exit span: `LOAD None; LOAD None; - /// LOAD None; CALL; POP_TOP` starting at `off` — returns the offset + /// LOAD None; CALL; [async exit await;] POP_TOP` starting at `off` — returns the offset /// just past the POP_TOP. The callable is the VM-managed bound /// __exit__ (never modeled); the sequence is pure machinery. fn inline_with_exit_span(&self, off: usize) -> Option { @@ -11366,9 +11366,74 @@ impl<'a> Ctx<'a> { while matches!(self.instrs.get(k).map(|x| x.op), Some(op) if matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) { k += 1; } + let mut async_exit = false; + if self.version.at_least(3, 14) + && self.instrs.get(k).map(|x| (x.op, x.arg)) == Some((Op::GET_AWAITABLE, 2)) + { + // The compiler marks an __aexit__ await with operand 2. A + // user-authored await uses operand 0 and must remain visible. + // Consume only a complete SEND loop ending in END_SEND. + k += 1; + if self.version.at_least(3, 15) + && self.instrs.get(k).map(|x| x.op) == Some(Op::PUSH_NULL) + { + k += 1; + } + let none = self.instrs.get(k)?; + if !self.loads_none(none.op, none.arg) { + return None; + } + k += 1; + let send = self.instrs.get(k)?; + if send.op != Op::SEND { + return None; + } + let end = *self.idx_of.get(&send.target?)?; + if end <= k || end - k > 12 || self.instrs[end].op != Op::END_SEND { + return None; + } + let loop_body = &self.instrs[k + 1..end]; + if !loop_body.iter().any(|x| x.op == Op::YIELD_VALUE) + || !loop_body.iter().any(|x| { + x.op == Op::JUMP_BACKWARD_NO_INTERRUPT && x.target == Some(send.offset) + }) + || loop_body.iter().any(|x| { + !matches!(x.op, Op::YIELD_VALUE | Op::RESUME | Op::CLEANUP_THROW + | Op::JUMP_BACKWARD_NO_INTERRUPT | Op::NOP | Op::NOT_TAKEN | Op::CACHE) + || (x.op == Op::JUMP_BACKWARD_NO_INTERRUPT && x.target != Some(send.offset)) + }) + { + return None; + } + k = end + 1; + async_exit = true; + while matches!(self.instrs.get(k).map(|x| x.op), Some(op) if matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) { + k += 1; + } + } if self.instrs.get(k).map(|x| x.op) != Some(Op::POP_TOP) { return None; } + let mut tail = k + 1; + while self.instrs.get(tail).map_or(false, |x| is_pad(x)) { + tail += 1; + } + let mut return_at = tail + 1; + while self.instrs.get(return_at).map_or(false, |x| is_pad(x)) { + return_at += 1; + } + if async_exit && self.instrs.get(tail).map_or(false, |x| { + matches!(x.op, Op::RETURN_VALUE | Op::RETURN_CONST) + || (matches!(x.op, Op::LOAD_CONST | Op::LOAD_SMALL_INT | Op::LOAD_COMMON_CONSTANT) + && self.instrs.get(return_at).map_or(false, |next| { + matches!(next.op, Op::RETURN_VALUE | Op::RETURN_CONST) + })) + }) { + // A calculated return can carry its value through this same + // exit await. Keep the historical return protocol; closing the + // With here can discard that value or eject its source Return. + return None; + } Some(self.instrs[k].end()) } @@ -13843,6 +13908,54 @@ fn short(sv: &Sv) -> String { use crate::opcode::Op; impl<'a> Ctx<'a> { + /// Recognize only an exit owned by the current lexical walk. Both + /// constant encodings must preserve nested and displaced return slots. + fn owned_inline_with_exit_span(&self) -> Option { + self.inline_with_exit_span(self.cur_offset).filter(|after| { + let is_async_exit = self.instrs.iter().any(|instruction| { + instruction.offset >= self.cur_offset + && instruction.offset < *after + && instruction.op == Op::GET_AWAITABLE + && instruction.arg == 2 + }); + let split_protected_walk = self.blocks.iter().any(|block| block.kind == BlockType::Try); + if !is_async_exit { + return true; + } + if !split_protected_walk { + // A split handler walk can lose both its Try and + // lexical With blocks while the original exception + // table still protects a nested exit. Preserve the + // existing protocol in that displaced fragment; + // ordinary open-With walks keep their established + // success-exit handling. + let has_open_with = self.blocks.iter().any(|block| block.kind == BlockType::With); + let protected_by_with = self.exc_entries.iter().any(|entry| { + entry.start <= self.cur_offset + && self.cur_offset < entry.end + && self.with_region_handlers.values().any(|handler| *handler == entry.target) + }); + return has_open_with || !protected_by_with; + } + let Some(owner) = self.blocks.iter().rfind(|block| block.kind == BlockType::With) else { + // A displaced return must retain its protocol + // value for the source fidelity reconstruction. + return false; + }; + // An exit protected by the open With belongs to a + // nested context. Keep its existing CALL/accounting + // path; consuming it here also consumes the outer + // return's exit slot in a split protected walk. + !owner.with_handler.map_or(false, |handler| { + self.exc_entries.iter().any(|entry| { + entry.start <= self.cur_offset + && self.cur_offset < entry.end + && entry.target == handler + }) + }) + }) + } + /// Execute one instruction. Returns false when the instruction stream /// should stop being processed (unconditional exit). /// 3.15 spells the inline with's three None exit arguments as @@ -13852,7 +13965,7 @@ impl<'a> Ctx<'a> { if !self.version.at_least(3, 15) || self.with_exits == 0 { return false; } - let Some(after) = self.inline_with_exit_span(self.cur_offset) else { + let Some(after) = self.owned_inline_with_exit_span() else { return false; }; let nested_end_here = self.with_regions.iter().any(|(k, v)| { @@ -13918,7 +14031,7 @@ impl<'a> Ctx<'a> { // _maybe_compile: the leak rendered `None(None, None)` // and trapped the post-with tail return inside the with) if self.version.at_least(3, 11) && self.with_exits > 0 { - if let Some(after) = self.inline_with_exit_span(self.cur_offset) { + if let Some(after) = self.owned_inline_with_exit_span() { // close the With ONLY when it is topmost: a sunk // exit inside an open branch arm (`if quitting: // return` — bdb 3.12 trace_dispatch) must keep @@ -14558,6 +14671,10 @@ impl<'a> Ctx<'a> { | Op::END_SEND | Op::CLEANUP_THROW | Op::LOAD_CONST + // 3.15's await setup has a null SEND + // slot and a shared None constant. + | Op::PUSH_NULL + | Op::LOAD_COMMON_CONSTANT | Op::NOP | Op::NOT_TAKEN | Op::END_ASYNC_FOR diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index a7b86685..14365fa2 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -382,3 +382,97 @@ fn nested_context_return_matcher_requires_complete_exit_groups_and_slots() { } } } + +#[test] +fn normal_async_exit_spans_recognize_actual_suspension_protocols() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/normal_async_exits.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let code = nested_code(&module.code, "normal_1"); + let version = module.version(); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + let starts: Vec<_> = ctx.instrs.windows(3).enumerate() + .filter(|(_, group)| group.iter().all(|x| ctx.loads_none(x.op, x.arg))) + .map(|(i, group)| (i, group[0].offset)).collect(); + assert_eq!(starts.len(), 2); + for (i, off) in starts { + assert!(ctx.inline_with_exit_span(off).is_some(), "3.{minor} exit at {off}: {:?}", &ctx.instrs[i..i+16]); + } + } +} + +#[test] +fn async_normal_exit_rejects_user_awaits_broken_loops_and_return_tails() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/normal_async_exits.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let code = nested_code(&module.code, "normal_1"); + let version = module.version(); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let make = |instrs| { + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx + }; + let ctx = make(instrs.clone()); + let start = ctx.instrs.windows(3).position(|group| group.iter().all(|x| ctx.loads_none(x.op, x.arg))).unwrap(); + let off = ctx.instrs[start].offset; + let after = ctx.inline_with_exit_span(off).unwrap(); + let end = ctx.idx_of[&after]; + for mutation in 0..4 { + let mut bad = instrs.clone(); + let span = &mut bad[start..end]; + match mutation { + 0 => span.iter_mut().find(|x| x.op == Op::GET_AWAITABLE).unwrap().arg = 0, + 1 => span.iter_mut().find(|x| x.op == Op::JUMP_BACKWARD_NO_INTERRUPT).unwrap().target = Some(off), + 2 => span.iter_mut().find(|x| x.op == Op::END_SEND).unwrap().op = Op::NOP, + _ => span.iter_mut().find(|x| x.op == Op::RESUME).unwrap().op = Op::LOAD_GLOBAL, + } + assert!(make(bad).inline_with_exit_span(off).is_none(), "3.{minor}: mutation {mutation}"); + } + for padding in [0, 1, 3, 9] { + for constant in [false, true] { + let mut tail = instrs[..end].to_vec(); + tail.extend((0..padding).map(|_| instruction(Op::NOP, 0))); + if constant { + tail.push(instruction(Op::LOAD_SMALL_INT, 4)); + tail.extend((0..padding).map(|_| instruction(Op::NOP, 0))); + } + tail.push(instruction(Op::RETURN_VALUE, 0)); + for (i, item) in tail[end..].iter_mut().enumerate() { item.offset = after + i * 2; } + assert!(make(tail).inline_with_exit_span(off).is_none(), "3.{minor}: carried return with {padding} pads"); + } + } + } +} + +#[test] +fn async_exit_ownership_preserves_displaced_and_nested_slots() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/normal_async_exits.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let code = nested_code(&module.code, "normal_1"); + let version = module.version(); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx.cur_offset = ctx.instrs.windows(3).find(|group| group.iter().all(|x| ctx.loads_none(x.op, x.arg))).unwrap()[0].offset; + let after = ctx.inline_with_exit_span(ctx.cur_offset).unwrap(); + assert!(ctx.owned_inline_with_exit_span().is_some()); + ctx.exc_entries.push(crate::code::ExceptionEntry { start: ctx.cur_offset, end: after, target: 999, depth: 2, lasti: true }); + ctx.with_region_handlers.insert(1, 999); + assert!(ctx.owned_inline_with_exit_span().is_none(), "lost Try and With must retain the protected exit"); + ctx.blocks.push(Block::new(BlockType::Try, 0, after)); + assert!(ctx.owned_inline_with_exit_span().is_none(), "a displaced Try has no owning With"); + let mut owner = Block::new(BlockType::With, 0, after); + owner.with_handler = Some(999); + ctx.blocks.push(owner); + assert!(ctx.owned_inline_with_exit_span().is_none(), "the open owner's handler protects an inner exit"); + ctx.blocks.last_mut().unwrap().with_handler = Some(1001); + assert!(ctx.owned_inline_with_exit_span().is_some()); + ctx.blocks.remove(1); + assert!(ctx.owned_inline_with_exit_span().is_some(), "ordinary open-With walks keep their exit handling"); + } +} diff --git a/tests/behavior/cases/b_nested_context_returns.py b/tests/behavior/cases/b_nested_context_returns.py index 4426dd52..544de2cb 100644 --- a/tests/behavior/cases/b_nested_context_returns.py +++ b/tests/behavior/cases/b_nested_context_returns.py @@ -46,6 +46,13 @@ def sync_outer_return(contexts, value): +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result + + events = [] opened = set() @@ -98,6 +105,7 @@ async def async_value(): (async_tuple, 2, False), (async_single, 1, True), (sync_outer_return, 2, False), + (async_outer_return, 2, True), ] for function, depth, isawait in cases: for failure in ("", "body", "cancelled", "enter-0", "enter-1", "enter-2", "exit-0", "exit-1", "exit-2"): diff --git a/tests/behavior/cases/b_normal_async_exits.py b/tests/behavior/cases/b_normal_async_exits.py new file mode 100644 index 00000000..8998d35e --- /dev/null +++ b/tests/behavior/cases/b_normal_async_exits.py @@ -0,0 +1,189 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 + + +class Owner: + active = False + + def __init__(self): + self.events = [] + self.ready = ready + + async def __aenter__(self): + await asyncio.sleep(0) + self.events.append('outer-enter') + if fault == 'outer-enter': + raise ValueError('owned outer enter') + self.active = True + return self + + async def __aexit__(self, kind, value, trace): + await asyncio.sleep(0) + self.events.append(('outer-exit', None if kind is None else kind.__name__)) + self.active = False + if fault == 'outer-exit': + raise ValueError('owned outer exit') + return suppression == 'outer' + + def query(self, name): + self.events.append(('query', name)) + return Query(self, name) + + def build(self, first, last): + self.events.append(('build', self.active, first, last)) + if fault == 'build': + raise ValueError('owned build') + if fault == 'build-cancel': + raise asyncio.CancelledError('owned build cancellation') + return first + last + + async def user(self, *values): + await asyncio.sleep(0) + self.events.append(('user', self.active, values)) + if fault == 'read': + raise ValueError('owned user') + if fault == 'read-cancel': + raise asyncio.CancelledError('owned user cancellation') + + async def write(self, first, last): + await asyncio.sleep(0) + await self.user(first, last) + +class Query: + + def __init__(self, owner, name): + self.owner, self.name = (owner, name) + self.selected = name == f'row{count // 2}' + + async def __aenter__(self): + await asyncio.sleep(0) + self.owner.events.append(('query-enter', self.name, self.owner.active)) + if self.selected and fault == 'query-enter': + raise ValueError('owned query enter') + return self + + async def read(self): + await asyncio.sleep(0) + self.owner.events.append(('read', self.name, self.owner.active)) + if self.selected and fault == 'read': + raise ValueError('owned read') + if self.selected and fault == 'read-cancel': + raise asyncio.CancelledError('owned read cancellation') + return int(self.name[3:]) + + async def __aexit__(self, kind, value, trace): + await asyncio.sleep(0) + self.owner.events.append(('query-exit', self.name, None if kind is None else kind.__name__, self.owner.active)) + if self.selected and fault == 'query-exit': + raise ValueError('owned query exit') + return suppression == 'query' + +functions = [ + (normal_1, 1), + (normal_2, 2), + (normal_4, 4), + (constant_one, 1), + (constant_two, 2), + (carried_one, 1), + (computed_two, 2), + (mapping_two, 2), + (user_await, 1), + (branch_return, 1), +] +for function, count in functions: + for ready in (False, True): + for fault in ("", "outer-enter", "outer-exit", "query-enter", "query-exit", "read", "read-cancel", "build", "build-cancel"): + for suppression in ("", "outer", "query"): + owner = Owner() + print(function.__name__, ready, fault, suppression) + try: + print("return", asyncio.run(function(owner))) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events, owner.active) diff --git a/tests/corpus/3.11.14/nested_context_returns.py b/tests/corpus/3.11.14/nested_context_returns.py index 391f7397..f3ba23ec 100644 --- a/tests/corpus/3.11.14/nested_context_returns.py +++ b/tests/corpus/3.11.14/nested_context_returns.py @@ -42,4 +42,10 @@ def sync_outer_return(contexts, value): return result +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result + diff --git a/tests/corpus/3.11.14/nested_context_returns.pyc b/tests/corpus/3.11.14/nested_context_returns.pyc index df1823b0efb486046cca8099e5f7e0ace7ea61f2..ba3d2cdecef2ed41728a6960887a0f77fac49da2 100644 GIT binary patch delta 549 zcmZqGJ7yxVoR^o2nSp`f-Jk6f**?_XnFtJCM-~#Gk)S&JzcsJIwpr-Fh)5f_lYC6riPnU@@&Us{q{6kn8DQd*Q(Bny<#1*u%f z$iVOd2pbq~@Qcr2?5UpoQB-o07}q2**7_U5(lZ>VSbtz(h%qt{MIR9kKq&d~;|GF+U`<{nY7h1s(0d3p`K73ml`$|dCh|LK zFfR~vWMW*n%t|wg#7@5&JjA9=cFo_~Lpc(*ujfmI) delta 99 zcmX?R(yAx0oR^o2nSp`9E%wF65ZANwCklu$8cb9-WC>=_zh{!CTbR=A*?wD`XQSSuCYQ_A+W8f#xbOd>|ZF)PTlEBE;wwgG4a;g($ubE?%U z;*|{=%=dk1;{Nl*LSu7vael42UYGN-zTT9paug3Ll%xrMv-lC~I^GVvcravKE%b@H z`O=Q)TpBV+pY9X=DEYg!?Yqyf4>?we|I2ZQI%bou1L|tUy%e6tT{nn(l#7$#|62}w z9AoGnwmmJkAQ%vzUAo9j8NA=DUMRgB^YkW{peYDW~48qgC zsHpZc*b!MR5LGS8XjJ3@{m2CjM_!_fnwU&S;~;`;3{>=$PS~#6_h(?I=L@tp>R3f4;m0Ey2K(OTR4~@$!YfH`L`ivY$4-kz?Xywj2 O2c`mrK^94lq5Bsj*N6ZB delta 114 zcmX@7x?4fuG%qg~GXn!drmgxS-~8_>69vQ=^(Lz8vsAKca%@c4&&Q}aS%Ke#QEGB1 ze-X2vCi`SxA$bicppGIoAn_T98H%`o#4iq;-29Z%oK(9a86cMth>JBQ&lFPQ`pm_` KC{`p56aWAdtM?TVnhvcC)t^l1t}z9p+>!7p;-tU`4PHl zin3=xmTb8G2Ai?)<1bKVp=9AaaqD~Ur|dL7OL!sXLUqekuc=MJ*oGN`@|b@ z#IipS8lpfcfT5fJ; zd39ypopI+^R^6PN#6p{w92HA6zFtb$r#;1~?cq;VskVygR~G%$lyKspuv;ia>Pk`P zuTV@MroWKA(m`Rn@SQ)#DVutR!5;8Rvacc?>bzS$0Un{JD$-ey#{=iUqx8a$bQwhN z26&9z29WNI44VKM1l~oYL9ks$0DTz+49nOr#|iJKkQQ*ql(yk%u(%CgRHYVuvF=|epz-PiJHTfuC p9J8M$`{XV`c_44Spe&aRP#Yr<7i&zuCaA{snTv%{tVkLt0083|6hQz0 diff --git a/tests/corpus/3.14.2/async_context_return_padding_1.py b/tests/corpus/3.14.2/async_context_return_padding_1.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.14.2/async_context_return_padding_1.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.14.2/async_context_return_padding_1.pyc b/tests/corpus/3.14.2/async_context_return_padding_1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dbd40f4afcf7fa07f8b56dd921609cce89c57b6 GIT binary patch literal 1480 zcmds%-A)rx5XWcEessJ2;6sgsQrcRh;X?{Z!9qdOO$;{;Asa)yay!u4>w9!?7k8-VpW|{(+WYXD9br<$gz}(fi=Kz)bC8MV|CRAV&Oi}j zP=85gkj;}#=V1<_Zt}$mWAwv39Bgb~08Bws#DR5mv#nSK6pN#4`)56>vc;F+p|k{ z_oTIF?X_E;bL?5J zR@OdBq&`g>lgbyYtt7unq`uv*t;hWY)+$LqDYG6;P}t94ZB6s16xM|V)%o*iOvwHt zjr9t0yn>v+7U^}>xRVM58p?Pe>8#U&{isHn!30uOFsT~L@gR>zF&z|i*7Jh>f=;u4 zhxYA%0IeyWixT$e!d;Jdgswx_JtZrV6>f@Oy>f}}>`iO0pet=hT!zXU`+&$*2&Wmz z)#&Rgv~HZjVVcQw8rGrR=@6jq2nD131c?hCtIXqL=CR5=W|_xT<}u4WZbm#-BOXZ; zkJ*UF4dOAzx|pIme-Vw0>MzAuuOi2*$h8}h-c*f=RG^@tjR&gEdQ7mtrBP;(L8=O} zs!@ptMKm6ygZso_JSYkFOFGSN_eFG>q7(GN<^7)@QCmN3H+l!o6PN#hqszvp{2?LY OPa)&jue6N0sNgpT-02tq literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/async_context_return_padding_3.py b/tests/corpus/3.14.2/async_context_return_padding_3.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.14.2/async_context_return_padding_3.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.14.2/async_context_return_padding_3.pyc b/tests/corpus/3.14.2/async_context_return_padding_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6973a93320740f5f922fa506c43d68526c98a254 GIT binary patch literal 1496 zcmdUv-%ry}6vxlGZMW_xI5i|}vy%lC`5jS#4VE<~yexz?hWKRD(1y&wHoHzB@qyr9 zpoxEj*|R1-w|_=o{RfQr2Y5~yWKkmFk-MC`Z_hpF^xXDyJDtrWVQBmq(tv}D(;SIo zFklQkFpy?6L>Z=}o%09|~(XEs0Z%i ztj1HRiG8sd%5M8$p)W$^ZO?I!=+d$5DJfLi2Q9~SW7LaF-9eWhK&ey;AA3&M>(1Mj zXU$vPqt=eO({6drp=Y{|*K=ED$Fl8aYj>`56sBpZ>sc+&Y*R6Q?fjxn$)Nj%rjrr9 ziXXB5O#LL0`g~6>C||L@oc<<}`hKUrp72vxUrqaIne}Lj+eWtmg#J z3u#)t9NrV!%T0&Z5U<2M_UPbUjrj$_yM*5c*&qk1gbki&L}=b;+)GGbGgD?)|g9;xvVgk8gZ#( z#N`@u$(p#-A}(vhWt??6LwWuJ>cyD9m|$H&_OBtw*H{M_)5kMG9Q9-_P?M~03!d+) zyjn!c3bdGBO$3vuKgtFVi0?$OD0qI7rq#{9hz?b>gFd+2|JfGR>VDhq?K$f%e~_a? U$GiL~A>uC~(5!hze3A_>g z3pD!TKVbH(iO=ny(O3TgBmM!NQwAAmB)r<@+>=x zr?^#E8Lo2>M(?Q&yrVqYvrir3zr>5E8x)pNaFdUTyeMMi3SyXAUbg8K5PQ|R)>7={tu^xdxC zoo+b3GwpPDTWj`OyXCt(zU{ew&uiHor_pG()~7nVQHG|vzSHvUHWgzW_yg{fLH9Ea zZ$_3he20tk+Ivar;}vs4{ep`NnXi)6xAUfw3espoCdeqPN7LjE3TO%~7*knaO;cVl ziKd|hw?u1I=nKtmv$3 zg2(%Ln*BS}pZ)`=E%8E3V~-Bx>G&T9n?WKZx5%E6&5~8lif8@t5!!dontLkRayjNU zQeQipM7Bz}RivlU&!13xDRM_SCf9A)TTZ7#0DB|kjN{WJ4tY&8uaB75H1k>}UQJ|P z%gk#s=2gbLk|tiuF|R7|nq*zdQeH5FW+@Te9w8kfl0TG4xuF{CWWqGEVHVBNe3;W& zUlu$TG>$AGXO$C6S}LrfIh6}1b=I?j#|L?uUG9tcu*ECrgU9_JEK!@^YBzeD?t;gE X;PGMPRsN8O_){o&?pIF1Vw~_B=x67g literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/nested_context_returns.py b/tests/corpus/3.14.2/nested_context_returns.py index 391f7397..f3ba23ec 100644 --- a/tests/corpus/3.14.2/nested_context_returns.py +++ b/tests/corpus/3.14.2/nested_context_returns.py @@ -42,4 +42,10 @@ def sync_outer_return(contexts, value): return result +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result + diff --git a/tests/corpus/3.14.2/nested_context_returns.pyc b/tests/corpus/3.14.2/nested_context_returns.pyc index 7a123f6e82ae3becb5cdf23e3d5ead4b0c541a42..32ab8fa60147550f6d35e28c08feaa57817614b8 100644 GIT binary patch delta 456 zcmcbtJX2FZn~#@^nSp`fxqYM9nbx)MCklu$noU$U03gH#g%!<@%g1CsYUTcsU@XF zc}3Dd30;uNkjdI?a`n;*v(2Z+cDQ|KW)P8|ZvB8?^gBC)fOLO#PxVbJc* zldOLM*&wO@>h9`4#mj+)G>JLsGR|Oh)Z&=Q3}np_a}r?P#OSEQvzZyl+Q#i@#CedB z(NT}*AQKCS&BhHBJ}AiOXuxq$h!x1rJSff!)Ob*Z(TR`kpeh@Pt-}aXq{j%-U;t(t zu{m+`9<*R|v=%yO$pvH|VqpT2Y#cz9hxizQx(*4jIcaeo5@U3d-pm5NgyjW0;{}UGrqu3WdAW#BP?8a(Kx1->h#J=yE*3_yB1xbC06-!Z AWB>pF diff --git a/tests/corpus/3.14.2/normal_async_exits.py b/tests/corpus/3.14.2/normal_async_exits.py new file mode 100644 index 00000000..bb75f5ab --- /dev/null +++ b/tests/corpus/3.14.2/normal_async_exits.py @@ -0,0 +1,87 @@ +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 + + diff --git a/tests/corpus/3.14.2/normal_async_exits.pyc b/tests/corpus/3.14.2/normal_async_exits.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1589907aadfefdf010f8c19f528b58e8c6b4faf4 GIT binary patch literal 10314 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 N;XRMSyk-aH{uez%4jKRe literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/async_context_return_padding_1.py b/tests/corpus/3.15.2/async_context_return_padding_1.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.15.2/async_context_return_padding_1.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.15.2/async_context_return_padding_1.pyc b/tests/corpus/3.15.2/async_context_return_padding_1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..431ffd48bf6c56baca8767ce429f0414db1e2c87 GIT binary patch literal 1517 zcmds%Ur*Cu6vof#pS7z{V1XDUlOZm!g@2ByK~ZA~iOCWe8se2K4Xc8IZMH?^j_5Z~ zFG&0Z`viL7O<%u7Z~X*{AHe4=0|_&caOvBe?&;}s-uJZqy?bnTTXbrCKc$J=7l-{2 z`-l}9pnz5wXRa6{R8ZEBJ<@s!@eXnL9@!jalbRD%6?RusS z(g+z;eu;R5m_g*OQi0mD>VKZvAdB+tMxQH0S7<}Y(h%N-Jj!!(>JV)(>L)K!Uom7S zM{~@o8{ATsV$E;t&a{+Rf3@TJd${!6T0xEV#%|s7{Sy3Koy$P$n7w-7=YSXRNHtR2@ntO284pf6Q3W(qf98Qm>e{rM_n&|96ZJ z(EtC3@rv?PndA^#@;o@VVcrIWWyCCE^dc~p=TBgsxJZ2m%<57?YOKF@x4<%X6%|z9 z#7`&2ej4f6maKZU>bBc#0$INY$;2=Jw~h(5K&YcgLM=&XQ4*?2LW`16DuwoJf*62xFq!i|eCG{h@e8dik_*4Y-3JED)E z7e;Tq-4p1AH~oBz-TDNI58ydvLn0&+E^TwVe^3AC^tAnbdt~*PaBBO0N&|LIj)7_4(bhmORd$B$quH!28{ChjOH=MnOANyhy${|i3-ihlY$4T8U8cUO zcc(e-Gox%_ORTh2-`k&ViMIA)&-D-J)OG3wrLB4U4cGS*G>=bT2LgT}`Fy@z4Ba3M zrs_`UOgX_pW3#sDHA44QSo7U*&u`S4PQAX}cs|)YXs78?5IT*p=20`oi$CBI1q2`I z@H(@U!nbIn`A#MEK4%xS&uA;w7nRi4emj|rGH7d7)Tgl?NK-nxfwpc&V+q#9G}T3S z(4I&{b1BkNLCQ-F$uIR-Cv|(QNA{wf&B~0$I%lwF0@+{CmyD#GMtinb-m+NV=bq=| z{E6oO|8QOtPsB|OX=z>r>n=tFqDS$N;x@(jWuUAqo9vNCJ29_3nDMD~~T zrBqVRpgq?sXD!m-M4Iv~5wEOx=oa`q^6^s{_jbMd-j2KI_falhdHi5M6h!@U>6`Duv;yT-tT`5%GbZmErZf&bsmP6IqZrQFIquE?~YvA#NDwRs% zgl~JkS8ZCpRkggG)>>oDY5Dd$zv0?`$89y*RAw__8D3&Q^DQ;Z_*4ndU*zaAX{zBO0g^1)( zeQRwJ>$D4@EW2^~IE%TPpmeA+%XYJ|WwqOcwA)X~$j|?KM+6s`;3N}VV1f-MIK>1T zOmH?LSdR!MO#~Ye!5Jbr&ib}ab-@gplks3SLt3Pe@^HQzd_qj4`8XqHbkh6rEakgWwBw?oyWsN3hrcTRXv=AKHti>FmU7XKw?xnSp`9_|tE#qZ-V+Cklu$T1-?ow;eoF|`Wah|NeA;OqHIi5q6DV%8%zb2^ev&p;>x__ z`25n6)S~#J)RNMoydo)}gf2*B%w#_{rFvWCUrj2D5G1og{bw?xnSp^pJfVQM_iwEy rIbAS^Nt1o@QW1F|=ah&nmn2XlBM=vBO#UdM#`T4Zg;A_X0w@3g|F0CJ diff --git a/tests/corpus/3.15.2/normal_async_exits.py b/tests/corpus/3.15.2/normal_async_exits.py new file mode 100644 index 00000000..bb75f5ab --- /dev/null +++ b/tests/corpus/3.15.2/normal_async_exits.py @@ -0,0 +1,87 @@ +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 + + diff --git a/tests/corpus/3.15.2/normal_async_exits.pyc b/tests/corpus/3.15.2/normal_async_exits.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72aaf6a3a86c895edbea439989a28eba0dffb318 GIT binary patch literal 10660 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 bU4@GbMol*=+*Eg?!b#!(dS&vN+cEb4iv}U7 literal 0 HcmV?d00001 diff --git a/tests/data/async_context_return_padding.py b/tests/data/async_context_return_padding.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/data/async_context_return_padding.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/data/nested_context_returns.py b/tests/data/nested_context_returns.py index 391f7397..f3ba23ec 100644 --- a/tests/data/nested_context_returns.py +++ b/tests/data/nested_context_returns.py @@ -42,4 +42,10 @@ def sync_outer_return(contexts, value): return result +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result + diff --git a/tests/data/normal_async_exits.py b/tests/data/normal_async_exits.py new file mode 100644 index 00000000..bb75f5ab --- /dev/null +++ b/tests/data/normal_async_exits.py @@ -0,0 +1,87 @@ +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 + + diff --git a/tests/integration.rs b/tests/integration.rs index 704dee84..a06ec990 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -283,6 +283,7 @@ fn nested_contexts_carry_one_return_through_every_exit() { "async with contexts[0]:\n async with contexts[1]:\n return (value(),)", "async with contexts[0]:\n return await value()", "result = value()\n return result", + "result = await value()\n return result", ], version); } } @@ -310,6 +311,39 @@ fn per_row_handlers_remain_owned_by_their_loop_and_outer_try() { } } +#[test] +fn normal_async_exits_close_only_their_own_context() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("normal_async_exits.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "row0 = await query.read()\n value = owner.build(row0, row0)", + "row3 = await query.read()\n value = owner.build(row0, row3)", + "row0 = await query.read()\n async with owner.query('row1') as query:", + "row0 = await query.read()\n return 4", + "row0 = await query.read()\n return row0", + "row1 = await query.read()\n return owner.build(row0, row0)", + "row1 = await query.read()\n return {'value': row0}", + "await owner.user(None, None, None)\n return owner.build(1, 2)", + "if owner.ready:\n return 4\n await owner.write(1, 2)\n return 2", + ], version); + } +} + +#[test] +fn async_return_padding_never_turns_a_carried_exit_into_a_normal_exit() { + for version in ["3.14", "3.15"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture(&format!("async_context_return_padding_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + assert_contains(&src, &[ + "row0 = await query.read()\n return 4", + "row0 = await query.read()\n return {'value': row0}", + ], version); + } + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/async_context_return_padding_1.3.14.pyc b/tests/pyc/async_context_return_padding_1.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dbd40f4afcf7fa07f8b56dd921609cce89c57b6 GIT binary patch literal 1480 zcmds%-A)rx5XWcEessJ2;6sgsQrcRh;X?{Z!9qdOO$;{;Asa)yay!u4>w9!?7k8-VpW|{(+WYXD9br<$gz}(fi=Kz)bC8MV|CRAV&Oi}j zP=85gkj;}#=V1<_Zt}$mWAwv39Bgb~08Bws#DR5mv#nSK6pN#4`)56>vc;F+p|k{ z_oTIF?X_E;bL?5J zR@OdBq&`g>lgbyYtt7unq`uv*t;hWY)+$LqDYG6;P}t94ZB6s16xM|V)%o*iOvwHt zjr9t0yn>v+7U^}>xRVM58p?Pe>8#U&{isHn!30uOFsT~L@gR>zF&z|i*7Jh>f=;u4 zhxYA%0IeyWixT$e!d;Jdgswx_JtZrV6>f@Oy>f}}>`iO0pet=hT!zXU`+&$*2&Wmz z)#&Rgv~HZjVVcQw8rGrR=@6jq2nD131c?hCtIXqL=CR5=W|_xT<}u4WZbm#-BOXZ; zkJ*UF4dOAzx|pIme-Vw0>MzAuuOi2*$h8}h-c*f=RG^@tjR&gEdQ7mtrBP;(L8=O} zs!@ptMKm6ygZso_JSYkFOFGSN_eFG>q7(GN<^7)@QCmN3H+l!o6PN#hqszvp{2?LY OPa)&jue6N0sNgpT-02tq literal 0 HcmV?d00001 diff --git a/tests/pyc/async_context_return_padding_1.3.15.pyc b/tests/pyc/async_context_return_padding_1.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..431ffd48bf6c56baca8767ce429f0414db1e2c87 GIT binary patch literal 1517 zcmds%Ur*Cu6vof#pS7z{V1XDUlOZm!g@2ByK~ZA~iOCWe8se2K4Xc8IZMH?^j_5Z~ zFG&0Z`viL7O<%u7Z~X*{AHe4=0|_&caOvBe?&;}s-uJZqy?bnTTXbrCKc$J=7l-{2 z`-l}9pnz5wXRa6{R8ZEBJ<@s!@eXnL9@!jalbRD%6?RusS z(g+z;eu;R5m_g*OQi0mD>VKZvAdB+tMxQH0S7<}Y(h%N-Jj!!(>JV)(>L)K!Uom7S zM{~@o8{ATsV$E;t&a{+Rf3@TJd${!6T0xEV#%|s7{Sy3Koy$P$n7w-7=YSXRNHtR2@ntO284pf6Q3W(qf98Qm>e{rM_n&|96ZJ z(EtC3@rv?PndA^#@;o@VVcrIWWyCCE^dc~p=TBgsxJZ2m%<57?YOKF@x4<%X6%|z9 z#7`&2ej4f6maKZU>bBc#0$INY$;2=Jw~h(5K&YcgLM=&XQ4*?2LW`16DZ=}o%09|~(XEs0Z%i ztj1HRiG8sd%5M8$p)W$^ZO?I!=+d$5DJfLi2Q9~SW7LaF-9eWhK&ey;AA3&M>(1Mj zXU$vPqt=eO({6drp=Y{|*K=ED$Fl8aYj>`56sBpZ>sc+&Y*R6Q?fjxn$)Nj%rjrr9 ziXXB5O#LL0`g~6>C||L@oc<<}`hKUrp72vxUrqaIne}Lj+eWtmg#J z3u#)t9NrV!%T0&Z5U<2M_UPbUjrj$_yM*5c*&qk1gbki&L}=b;+)GGbGgD?)|g9;xvVgk8gZ#( z#N`@u$(p#-A}(vhWt??6LwWuJ>cyD9m|$H&_OBtw*H{M_)5kMG9Q9-_P?M~03!d+) zyjn!c3bdGBO$3vuKgtFVi0?$OD0qI7rq#{9hz?b>gFd+2|JfGR>VDhq?K$f%e~_a? U$GiL~A>uC~uwoJf*62xFq!i|eCG{h@e8dik_*4Y-3JED)E z7e;Tq-4p1AH~oBz-TDNI58ydvLn0&+E^TwVe^3AC^tAnbdt~*PaBBO0N&|LIj)7_4(bhmORd$B$quH!28{ChjOH=MnOANyhy${|i3-ihlY$4T8U8cUO zcc(e-Gox%_ORTh2-`k&ViMIA)&-D-J)OG3wrLB4U4cGS*G>=bT2LgT}`Fy@z4Ba3M zrs_`UOgX_pW3#sDHA44QSo7U*&u`S4PQAX}cs|)YXs78?5IT*p=20`oi$CBI1q2`I z@H(@U!nbIn`A#MEK4%xS&uA;w7nRi4emj|rGH7d7)Tgl?NK-nxfwpc&V+q#9G}T3S z(4I&{b1BkNLCQ-F$uIR-Cv|(QNA{wf&B~0$I%lwF0@+{CmyD#GMtinb-m+NV=bq=| z{E6oO|8QOtPsB|OX=z>r>n=tFqDS$N;x@(jWuUAqo9vNCJ29_3nDMD~~T zrBqVRpgq?sXD!m-M4Iv~5wEOx=oa`q^6^s{_jbMd-j2KI_falhdHi5M6h!(5!hze3A_>g z3pD!TKVbH(iO=ny(O3TgBmM!NQwAAmB)r<@+>=x zr?^#E8Lo2>M(?Q&yrVqYvrir3zr>5E8x)pNaFdUTyeMMi3SyXAUbg8K5PQ|R)>7={tu^xdxC zoo+b3GwpPDTWj`OyXCt(zU{ew&uiHor_pG()~7nVQHG|vzSHvUHWgzW_yg{fLH9Ea zZ$_3he20tk+Ivar;}vs4{ep`NnXi)6xAUfw3espoCdeqPN7LjE3TO%~7*knaO;cVl ziKd|hw?u1I=nKtmv$3 zg2(%Ln*BS}pZ)`=E%8E3V~-Bx>G&T9n?WKZx5%E6&5~8lif8@t5!!dontLkRayjNU zQeQipM7Bz}RivlU&!13xDRM_SCf9A)TTZ7#0DB|kjN{WJ4tY&8uaB75H1k>}UQJ|P z%gk#s=2gbLk|tiuF|R7|nq*zdQeH5FW+@Te9w8kfl0TG4xuF{CWWqGEVHVBNe3;W& zUlu$TG>$AGXO$C6S}LrfIh6}1b=I?j#|L?uUG9tcu*ECrgU9_JEK!@^YBzeD?t;gE X;PGMPRsN8O_){o&?pIF1Vw~_B=x67g literal 0 HcmV?d00001 diff --git a/tests/pyc/async_context_return_padding_9.3.15.pyc b/tests/pyc/async_context_return_padding_9.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00258ee264aaa2b20b93eea6ecb6802542a5ada3 GIT binary patch literal 1581 zcmds%T~E_s6vxl$%esxNjAghmG8s`4hR%`tf`FP>@U>6`Duv;yT-tT`5%GbZmErZf&bsmP6IqZrQFIquE?~YvA#NDwRs% zgl~JkS8ZCpRkggG)>>oDY5Dd$zv0?`$89y*RAw__8D3&Q^DQ;Z_*4ndU*zaAX{zBO0g^1)( zeQRwJ>$D4@EW2^~IE%TPpmeA+%XYJ|WwqOcwA)X~$j|?KM+6s`;3N}VV1f-MIK>1T zOmH?LSdR!MO#~Ye!5Jbr&ib}ab-@gplks3SLt3Pe@^HQzd_qj4`8XqHbkh6rEakgWwBw?oyWsN3hrcTRXv=AKHti>FmU7XKFtJCM-~#Gk)S&JzcsJIwpr-Fh)5f_lYC6riPnU@@&Us{q{6kn8DQd*Q(Bny<#1*u%f z$iVOd2pbq~@Qcr2?5UpoQB-o07}q2**7_U5(lZ>VSbtz(h%qt{MIR9kKq&d~;|GF+U`<{nY7h1s(0d3p`K73ml`$|dCh|LK zFfR~vWMW*n%t|wg#7@5&JjA9=cFo_~Lpc(*ujfmI) delta 99 zcmX?R(yAx0oR^o2nSp`9E%wF65ZANwCklu$8cb9-WC>=_zh{!CTbR=A*?wD`XQSSuCYQ_A+W8f#xbOd>|ZF)PTlEBE;wwgG4a;g($ubE?%U z;*|{=%=dk1;{Nl*LSu7vael42UYGN-zTT9paug3Ll%xrMv-lC~I^GVvcravKE%b@H z`O=Q)TpBV+pY9X=DEYg!?Yqyf4>?we|I2ZQI%bou1L|tUy%e6tT{nn(l#7$#|62}w z9AoGnwmmJkAQ%vzUAo9j8NA=DUMRgB^YkW{peYDW~48qgC zsHpZc*b!MR5LGS8XjJ3@{m2CjM_!_fnwU&S;~;`;3{>=$PS~#6_h(?I=L@tp>R3f4;m0Ey2K(OTR4~@$!YfH`L`ivY$4-kz?Xywj2 O2c`mrK^94lq5Bsj*N6ZB delta 114 zcmX@7x?4fuG%qg~GXn!drmgxS-~8_>69vQ=^(Lz8vsAKca%@c4&&Q}aS%Ke#QEGB1 ze-X2vCi`SxA$bicppGIoAn_T98H%`o#4iq;-29Z%oK(9a86cMth>JBQ&lFPQ`pm_` KC{`p56aWAdtM?TVnhvcC)t^l1t}z9p+>!7p;-tU`4PHl zin3=xmTb8G2Ai?)<1bKVp=9AaaqD~Ur|dL7OL!sXLUqekuc=MJ*oGN`@|b@ z#IipS8lpfcfT5fJ; zd39ypopI+^R^6PN#6p{w92HA6zFtb$r#;1~?cq;VskVygR~G%$lyKspuv;ia>Pk`P zuTV@MroWKA(m`Rn@SQ)#DVutR!5;8Rvacc?>bzS$0Un{JD$-ey#{=iUqx8a$bQwhN z26&9z29WNI44VKM1l~oYL9ks$0DTz+49nOr#|iJKkQQ*ql(yk%u(%CgRHYVuvF=|epz-PiJHTfuC p9J8M$`{XV`c_44Spe&aRP#Yr<7i&zuCaA{snTv%{tVkLt0083|6hQz0 diff --git a/tests/pyc/nested_context_returns.3.14.pyc b/tests/pyc/nested_context_returns.3.14.pyc index 7a123f6e82ae3becb5cdf23e3d5ead4b0c541a42..32ab8fa60147550f6d35e28c08feaa57817614b8 100644 GIT binary patch delta 456 zcmcbtJX2FZn~#@^nSp`fxqYM9nbx)MCklu$noU$U03gH#g%!<@%g1CsYUTcsU@XF zc}3Dd30;uNkjdI?a`n;*v(2Z+cDQ|KW)P8|ZvB8?^gBC)fOLO#PxVbJc* zldOLM*&wO@>h9`4#mj+)G>JLsGR|Oh)Z&=Q3}np_a}r?P#OSEQvzZyl+Q#i@#CedB z(NT}*AQKCS&BhHBJ}AiOXuxq$h!x1rJSff!)Ob*Z(TR`kpeh@Pt-}aXq{j%-U;t(t zu{m+`9<*R|v=%yO$pvH|VqpT2Y#cz9hxizQx(*4jIcaeo5@U3d-pm5NgyjW0;{}UGrqu3WdAW#BP?8a(Kx1->h#J=yE*3_yB1xbC06-!Z AWB>pF diff --git a/tests/pyc/nested_context_returns.3.15.pyc b/tests/pyc/nested_context_returns.3.15.pyc index 531c1f77d179ef7d529d2fb88e41037371c7f64c..7e28382be766256f0852ba3b5fe5c696c189da9d 100644 GIT binary patch delta 470 zcmX@6vP?@Lh>w?xnSp`9_|tE#qZ-V+Cklu$T1-?ow;eoF|`Wah|NeA;OqHIi5q6DV%8%zb2^ev&p;>x__ z`25n6)S~#J)RNMoydo)}gf2*B%w#_{rFvWCUrj2D5G1og{bw?xnSp^pJfVQM_iwEy rIbAS^Nt1o@QW1F|=ah&nmn2XlBM=vBO#UdM#`T4Zg;A_X0w@3g|F0CJ diff --git a/tests/pyc/normal_async_exits.3.14.pyc b/tests/pyc/normal_async_exits.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1589907aadfefdf010f8c19f528b58e8c6b4faf4 GIT binary patch literal 10314 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 N;XRMSyk-aH{uez%4jKRe literal 0 HcmV?d00001 diff --git a/tests/pyc/normal_async_exits.3.15.pyc b/tests/pyc/normal_async_exits.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72aaf6a3a86c895edbea439989a28eba0dffb318 GIT binary patch literal 10660 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 bU4@GbMol*=+*Eg?!b#!(dS&vN+cEb4iv}U7 literal 0 HcmV?d00001 From 0bba2c11b08ceac8e3b8c486d232112b1db12240 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 07:27:07 -0400 Subject: [PATCH 17/22] Follow typed mismatch edges past private handler-body bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit async def select(owner): accepted = False for candidate in owner.candidates: try: for value in (candidate, "keeper"): await owner.check(value) accepted = bool(await owner.reconsider(candidate)) or accepted except Incompatible as error: details = "reviewable" if owner.can_close: try: changed = bool(await owner.close(candidate)) details = "closed" if changed else "terminal" except RuntimeError as inner: await owner.log("close-failed", str(inner)) await owner.log("incompatible", details) except ValueError as error: await owner.log("invalid", str(error)) continue except (OSError, asyncio.TimeoutError) as error: await owner.log("transient", str(error)) continue except Exception as error: await owner.log("unavailable", str(error)) continue await owner.log("finished", accepted) return accepted A nested try inside one handler body can bound that body's private walk before the remaining clauses. The parser treats that bound as the end of the whole dispatch and drops later typed handlers, allowing their exceptions to escape instead of taking the corresponding branch. Keep the private body bound, but follow an explicit mismatch edge when it reaches a verified typed-pattern load, CHECK_EXC_MATCH and forward false jump. Extend only the dispatch scan to that clause's own boundary. Reject scans crossing cleanup, a new handler, a call or an unrelated jump, and retain a fixed lookahead limit. Cover synchronous and asynchronous handlers, named exceptions, tuple patterns, attribute patterns from global and local namespaces, and single-clause/no-private-try controls. Python 3.12–3.15 examples preserve the later clauses and post-loop work, including cancellation and errors raised inside handler bodies. --- src/decompiler.rs | 42 ++- src/decompiler_stack_tests.rs | 33 +++ .../cases/b_private_handler_clauses.py | 241 ++++++++++++++++++ .../corpus/3.12.3/private_handler_clauses.py | 165 ++++++++++++ .../corpus/3.12.3/private_handler_clauses.pyc | Bin 0 -> 10121 bytes .../corpus/3.13.7/private_handler_clauses.py | 165 ++++++++++++ .../corpus/3.13.7/private_handler_clauses.pyc | Bin 0 -> 10387 bytes .../corpus/3.14.2/private_handler_clauses.py | 165 ++++++++++++ .../corpus/3.14.2/private_handler_clauses.pyc | Bin 0 -> 10712 bytes .../corpus/3.15.2/private_handler_clauses.py | 165 ++++++++++++ .../corpus/3.15.2/private_handler_clauses.pyc | Bin 0 -> 10736 bytes tests/data/private_handler_clauses.py | 165 ++++++++++++ tests/integration.rs | 21 ++ tests/pyc/private_handler_clauses.3.12.pyc | Bin 0 -> 10121 bytes tests/pyc/private_handler_clauses.3.13.pyc | Bin 0 -> 10387 bytes tests/pyc/private_handler_clauses.3.14.pyc | Bin 0 -> 10712 bytes tests/pyc/private_handler_clauses.3.15.pyc | Bin 0 -> 10736 bytes 17 files changed, 1161 insertions(+), 1 deletion(-) create mode 100644 tests/behavior/cases/b_private_handler_clauses.py create mode 100644 tests/corpus/3.12.3/private_handler_clauses.py create mode 100644 tests/corpus/3.12.3/private_handler_clauses.pyc create mode 100644 tests/corpus/3.13.7/private_handler_clauses.py create mode 100644 tests/corpus/3.13.7/private_handler_clauses.pyc create mode 100644 tests/corpus/3.14.2/private_handler_clauses.py create mode 100644 tests/corpus/3.14.2/private_handler_clauses.pyc create mode 100644 tests/corpus/3.15.2/private_handler_clauses.py create mode 100644 tests/corpus/3.15.2/private_handler_clauses.pyc create mode 100644 tests/data/private_handler_clauses.py create mode 100644 tests/pyc/private_handler_clauses.3.12.pyc create mode 100644 tests/pyc/private_handler_clauses.3.13.pyc create mode 100644 tests/pyc/private_handler_clauses.3.14.pyc create mode 100644 tests/pyc/private_handler_clauses.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index c8f8befc..6542f018 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -6631,6 +6631,32 @@ impl<'a> Ctx<'a> { stmts } + /// Bound a typed clause reached by the preceding match's false edge. + /// Only ordinary pattern loads followed by CHECK_EXC_MATCH and its + /// forward mismatch jump qualify; never cross a new handler or cleanup. + fn following_except_clause_end(&self, from: usize) -> Option { + let start = *self.idx_of.get(&from)?; + for (index, inst) in self.instrs.iter().enumerate().skip(start).take(32) { + if inst.op == Op::CHECK_EXC_MATCH { + let jump = self.instrs.get(index + 1)?; + if matches!(jump.op, Op::POP_JUMP_IF_FALSE | Op::POP_JUMP_FORWARD_IF_FALSE) + && jump.target.map_or(false, |target| target > jump.offset) + { + return Some(jump.end()); + } + return None; + } + if !matches!(inst.op, + Op::LOAD_GLOBAL | Op::LOAD_NAME | Op::LOAD_FAST | Op::LOAD_DEREF + | Op::LOAD_ATTR | Op::LOAD_METHOD | Op::BUILD_TUPLE + | Op::NOP | Op::NOT_TAKEN | Op::CACHE) + { + return None; + } + } + None + } + /// Parse an out-of-line except-dispatch handler region (3.11+): /// PUSH_EXC_INFO followed by `[pattern; CHECK_EXC_MATCH; PJIF next]*` /// clause bodies, ending in RERAISE. @@ -6639,6 +6665,11 @@ impl<'a> Ctx<'a> { let mut clause_offsets: Vec = Vec::new(); let end = self.dispatch_region_end(from); let region_end = self.handler_region_end(from); + let private_body_cutoff = end < region_end + && self.exc_entries.iter().any(|entry| { + entry.start == end && entry.target == region_end + }); + let mut dispatch_limit = end; // nesting: this chain's own cleanup region ends exactly where the // next chain begins — that chain handles re-raises FROM this one, // i.e. it is the ENCLOSING try's dispatch. Adjacency must be exact @@ -6781,7 +6812,7 @@ impl<'a> Ctx<'a> { let mut eg_chain = false; while pc < self.instrs.len() { let inst = self.instrs[pc]; - if inst.offset >= end + if inst.offset >= dispatch_limit && !handlers.is_empty() && !(nested && inst.offset < self.chain_extent(from)) { @@ -6832,6 +6863,15 @@ impl<'a> Ctx<'a> { | Op::POP_JUMP_FORWARD_IF_NONE => { at_clause_head = false; let next = inst.target.unwrap_or(end); + if private_body_cutoff && pattern.is_some() && next > dispatch_limit { + // A private try bounds this clause's body subwalk, + // not the parent match's next typed clause. Retain + // that body bound while following only the explicit + // mismatch edge and its verified pattern protocol. + if let Some(limit) = self.following_except_clause_end(next) { + dispatch_limit = limit; + } + } clause_offsets.push(inst.offset); pc += 1; // 3.14 pads the clause head with NOT_TAKEN diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index 14365fa2..4041810a 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -476,3 +476,36 @@ fn async_exit_ownership_preserves_displaced_and_nested_slots() { assert!(ctx.owned_inline_with_exit_span().is_some(), "ordinary open-With walks keep their exit handling"); } } + +#[test] +fn following_exception_clauses_require_bounded_typed_mismatch_protocols() { + let module = fixture(); + let build = |mut prefix: Vec, jump_op, forward| { + prefix.push(instruction(Op::CHECK_EXC_MATCH, 0)); + let mut jump = instruction(jump_op, 0); + jump.target = Some(if forward { 1000 } else { 0 }); + prefix.push(jump); + for (i, x) in prefix.iter_mut().enumerate() { x.offset = i * 2; } + let mut ctx = context(&module.code, module.version(), prefix); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx + }; + for prefix in [ + vec![instruction(Op::LOAD_GLOBAL, 0)], + vec![instruction(Op::LOAD_NAME, 0)], + vec![instruction(Op::LOAD_FAST, 0), instruction(Op::LOAD_ATTR, 0)], + vec![instruction(Op::LOAD_DEREF, 0), instruction(Op::LOAD_ATTR, 0)], + vec![instruction(Op::LOAD_GLOBAL, 0), instruction(Op::LOAD_GLOBAL, 1), instruction(Op::BUILD_TUPLE, 2)], + ] { + let ctx = build(prefix.clone(), Op::POP_JUMP_IF_FALSE, true); + assert_eq!(ctx.following_except_clause_end(0), Some(ctx.instrs.last().unwrap().end())); + assert!(build(prefix.clone(), Op::POP_JUMP_IF_FALSE, false).following_except_clause_end(0).is_none()); + assert!(build(prefix, Op::POP_JUMP_IF_TRUE, true).following_except_clause_end(0).is_none()); + } + for barrier in [Op::PUSH_EXC_INFO, Op::POP_EXCEPT, Op::RERAISE, Op::CALL, Op::JUMP_FORWARD] { + let ctx = build(vec![instruction(Op::LOAD_GLOBAL, 0), instruction(barrier, 0)], Op::POP_JUMP_IF_FALSE, true); + assert!(ctx.following_except_clause_end(0).is_none(), "must stop at {barrier:?}"); + } + let ctx = build(vec![instruction(Op::NOP, 0); 32], Op::POP_JUMP_IF_FALSE, true); + assert!(ctx.following_except_clause_end(0).is_none()); +} diff --git a/tests/behavior/cases/b_private_handler_clauses.py b/tests/behavior/cases/b_private_handler_clauses.py new file mode 100644 index 00000000..a2129022 --- /dev/null +++ b/tests/behavior/cases/b_private_handler_clauses.py @@ -0,0 +1,241 @@ +# MIN_VERSION: 3.12 +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +class Incompatible(ValueError): + pass + +class Owner: + + def __init__(self): + self.candidates = [] if case == 'empty' else ['first', 'second'] + self.can_close = case != 'no-close' + self.events = [] + + def check(self, value): + self.events.append(('check', value)) + if value == 'first': + failures = {'incompatible': Incompatible, 'no-close': Incompatible, 'close-failed': Incompatible, 'terminal': Incompatible, 'log-error': Incompatible, 'invalid': ValueError, 'io': OSError, 'timeout': asyncio.TimeoutError, 'unexpected': LookupError, 'cancel': asyncio.CancelledError} + if case in failures: + raise failures[case]('owned fault') + + def reconsider(self, candidate): + self.events.append(('reconsider', candidate)) + return True + + def close(self, candidate): + self.events.append(('close', candidate)) + if case == 'close-failed': + raise RuntimeError('owned close') + return case != 'terminal' + + def log(self, *values): + self.events.append(('log', *values)) + if case == 'log-error' and values[0] == 'incompatible': + raise KeyError('owned log') + if case == 'finish-error' and values[0] == 'finished': + raise RuntimeError('owned finish') + +class AsyncOwner(Owner): + async def check(self, value): + await asyncio.sleep(0) + return super().check(value) + + async def reconsider(self, candidate): + await asyncio.sleep(0) + return super().reconsider(candidate) + + async def close(self, candidate): + await asyncio.sleep(0) + return super().close(candidate) + + async def log(self, *values): + await asyncio.sleep(0) + return super().log(*values) + + +from types import SimpleNamespace +faults = SimpleNamespace(incompatible=Incompatible, invalid=ValueError, + operating=OSError, timeout=asyncio.TimeoutError, ordinary=Exception) +functions = [ + (named, True, False), + (synchronous, False, False), + (attributes, True, False), + (local_attributes, True, True), + (single_clause, True, False), + (no_private_try, True, False), +] +for function, asynchronous, local in functions: + for case in ("normal", "empty", "incompatible", "no-close", "close-failed", "terminal", "invalid", "io", "timeout", "unexpected", "cancel", "log-error", "finish-error"): + owner = AsyncOwner() if asynchronous else Owner() + print(function.__name__, case) + try: + result = function(owner, faults) if local else function(owner) + if asynchronous: + result = asyncio.run(result) + print("return", result) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events) diff --git a/tests/corpus/3.12.3/private_handler_clauses.py b/tests/corpus/3.12.3/private_handler_clauses.py new file mode 100644 index 00000000..70c9b2c3 --- /dev/null +++ b/tests/corpus/3.12.3/private_handler_clauses.py @@ -0,0 +1,165 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.12.3/private_handler_clauses.pyc b/tests/corpus/3.12.3/private_handler_clauses.pyc new file mode 100644 index 0000000000000000000000000000000000000000..679862a49c4e5f598ef2c5dcce4dbddcd402c42b GIT binary patch literal 10121 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 OYFueEQ=_;8y8aLOD?U2_ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/private_handler_clauses.py b/tests/corpus/3.14.2/private_handler_clauses.py new file mode 100644 index 00000000..70c9b2c3 --- /dev/null +++ b/tests/corpus/3.14.2/private_handler_clauses.py @@ -0,0 +1,165 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.14.2/private_handler_clauses.pyc b/tests/corpus/3.14.2/private_handler_clauses.pyc new file mode 100644 index 0000000000000000000000000000000000000000..263a91f1bdad74cffa4522fb6de67e6d4b3035cc GIT binary patch literal 10712 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 QLR1jL-6kV3h`Ye_zh-cAA^-pY literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/private_handler_clauses.py b/tests/corpus/3.15.2/private_handler_clauses.py new file mode 100644 index 00000000..70c9b2c3 --- /dev/null +++ b/tests/corpus/3.15.2/private_handler_clauses.py @@ -0,0 +1,165 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.15.2/private_handler_clauses.pyc b/tests/corpus/3.15.2/private_handler_clauses.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96fc35522ffba8a900525860307cc5a57a41bed4 GIT binary patch literal 10736 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 OYFueEQ=_;8y8aLOD?U2_ literal 0 HcmV?d00001 diff --git a/tests/pyc/private_handler_clauses.3.14.pyc b/tests/pyc/private_handler_clauses.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..263a91f1bdad74cffa4522fb6de67e6d4b3035cc GIT binary patch literal 10712 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 QLR1jL-6kV3h`Ye_zh-cAA^-pY literal 0 HcmV?d00001 diff --git a/tests/pyc/private_handler_clauses.3.15.pyc b/tests/pyc/private_handler_clauses.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96fc35522ffba8a900525860307cc5a57a41bed4 GIT binary patch literal 10736 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 Date: Thu, 29 Oct 2026 07:37:47 -0400 Subject: [PATCH 18/22] Preserve handler breaks after iterator disposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit async def select(owner): accepted = False for candidate in owner.candidates: try: for value in (candidate, "keeper"): await owner.check(value) accepted = bool(await owner.reconsider(candidate)) or accepted except Incompatible as error: details = "reviewable" if owner.can_close: try: changed = bool(await owner.close(candidate)) details = "closed" if changed else "terminal" except RuntimeError as inner: await owner.log("close-failed", str(inner)) await owner.log("incompatible", details) except ValueError as error: await owner.log("invalid", str(error)) continue except (OSError, asyncio.TimeoutError) as error: await owner.log("transient", str(error)) break except Exception as error: await owner.log("unavailable", str(error)) break await owner.log("finished", accepted) return accepted After POP_EXCEPT and named-exception cleanup, a for-handler break drops the live iterator before jumping to the loop exit. The parser stops at that discard and loses the break, continuing with later candidates and changing both the accumulated result and post-loop observations. Consume iterator disposal only when the following jump targets a registered For exit. Leave arbitrary body POP_TOPs alone. Python 3.15 uses two iterator slots and shared None constants during named cleanup; recognize those encodings without widening the accepted jump target. Cover synchronous and asynchronous handlers, named and unnamed tuple and generic clauses, nested handler tries and direct controls. The Python 3.12–3.15 examples retain handler logging before the break and post-loop work exactly once, while preserving failures and cancellation. --- src/decompiler.rs | 25 ++ .../cases/b_handler_iterator_breaks.py | 217 ++++++++++++++++++ .../corpus/3.12.3/handler_iterator_breaks.py | 142 ++++++++++++ .../corpus/3.12.3/handler_iterator_breaks.pyc | Bin 0 -> 8276 bytes .../corpus/3.13.7/handler_iterator_breaks.py | 142 ++++++++++++ .../corpus/3.13.7/handler_iterator_breaks.pyc | Bin 0 -> 8508 bytes .../corpus/3.14.2/handler_iterator_breaks.py | 142 ++++++++++++ .../corpus/3.14.2/handler_iterator_breaks.pyc | Bin 0 -> 8804 bytes .../corpus/3.15.2/handler_iterator_breaks.py | 142 ++++++++++++ .../corpus/3.15.2/handler_iterator_breaks.pyc | Bin 0 -> 8831 bytes tests/data/handler_iterator_breaks.py | 142 ++++++++++++ tests/integration.rs | 18 ++ tests/pyc/handler_iterator_breaks.3.12.pyc | Bin 0 -> 8276 bytes tests/pyc/handler_iterator_breaks.3.13.pyc | Bin 0 -> 8508 bytes tests/pyc/handler_iterator_breaks.3.14.pyc | Bin 0 -> 8804 bytes tests/pyc/handler_iterator_breaks.3.15.pyc | Bin 0 -> 8831 bytes 16 files changed, 970 insertions(+) create mode 100644 tests/behavior/cases/b_handler_iterator_breaks.py create mode 100644 tests/corpus/3.12.3/handler_iterator_breaks.py create mode 100644 tests/corpus/3.12.3/handler_iterator_breaks.pyc create mode 100644 tests/corpus/3.13.7/handler_iterator_breaks.py create mode 100644 tests/corpus/3.13.7/handler_iterator_breaks.pyc create mode 100644 tests/corpus/3.14.2/handler_iterator_breaks.py create mode 100644 tests/corpus/3.14.2/handler_iterator_breaks.pyc create mode 100644 tests/corpus/3.15.2/handler_iterator_breaks.py create mode 100644 tests/corpus/3.15.2/handler_iterator_breaks.pyc create mode 100644 tests/data/handler_iterator_breaks.py create mode 100644 tests/pyc/handler_iterator_breaks.3.12.pyc create mode 100644 tests/pyc/handler_iterator_breaks.3.13.pyc create mode 100644 tests/pyc/handler_iterator_breaks.3.14.pyc create mode 100644 tests/pyc/handler_iterator_breaks.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 6542f018..a96459db 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -7747,6 +7747,31 @@ impl<'a> Ctx<'a> { | Op::DELETE_DEREF => { k2 += 1; } + Op::LOAD_COMMON_CONSTANT if self.loads_none(c.op, c.arg) => { + k2 += 1; + } + Op::POP_TOP => { + // A For-handler break drops the live iterator after + // POP_EXCEPT and any named cleanup, immediately before + // its jump to the registered loop exit. The iterator + // discard is machinery, but that jump still represents + // a source Break. Do not skip arbitrary body POP_TOPs. + // 3.15 keeps a second iterator slot on this path. + let pop_count = if self.version.at_least(3, 15) + && self.instrs.get(k2 + 1).map(|x| x.op) == Some(Op::POP_TOP) + { 2 } else { 1 }; + let next = self.instrs.get(k2 + pop_count); + let iterator_exit = next.map_or(false, |jump| { + matches!(jump.op, Op::JUMP_FORWARD | Op::JUMP_BACKWARD + | Op::JUMP_ABSOLUTE | Op::JUMP) + && jump.target.and_then(|target| self.find_loop_exit(target)) + .map_or(false, |index| self.blocks[index].kind == BlockType::For) + }); + if !iterator_exit { + break; + } + k2 += pop_count; + } Op::RETURN_VALUE | Op::RETURN_CONST => { // a sunk implicit tail `return None` (3.12+ // function-tail try: POP_EXCEPT; RETURN_CONST None — diff --git a/tests/behavior/cases/b_handler_iterator_breaks.py b/tests/behavior/cases/b_handler_iterator_breaks.py new file mode 100644 index 00000000..f69f7d2e --- /dev/null +++ b/tests/behavior/cases/b_handler_iterator_breaks.py @@ -0,0 +1,217 @@ +# MIN_VERSION: 3.12 +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +class Incompatible(ValueError): + pass + +class Owner: + + def __init__(self): + self.candidates = [] if case == 'empty' else ['first', 'second'] + self.can_close = case != 'no-close' + self.events = [] + + def check(self, value): + self.events.append(('check', value)) + if value == 'first': + failures = {'incompatible': Incompatible, 'no-close': Incompatible, 'close-failed': Incompatible, 'terminal': Incompatible, 'log-error': Incompatible, 'invalid': ValueError, 'io': OSError, 'timeout': asyncio.TimeoutError, 'unexpected': LookupError, 'cancel': asyncio.CancelledError} + if case in failures: + raise failures[case]('owned fault') + + def reconsider(self, candidate): + self.events.append(('reconsider', candidate)) + return True + + def close(self, candidate): + self.events.append(('close', candidate)) + if case == 'close-failed': + raise RuntimeError('owned close') + return case != 'terminal' + + def log(self, *values): + self.events.append(('log', *values)) + if case == 'log-error' and values[0] == 'incompatible': + raise KeyError('owned log') + if case == 'finish-error' and values[0] == 'finished': + raise RuntimeError('owned finish') + +class AsyncOwner(Owner): + async def check(self, value): + await asyncio.sleep(0) + return super().check(value) + + async def reconsider(self, candidate): + await asyncio.sleep(0) + return super().reconsider(candidate) + + async def close(self, candidate): + await asyncio.sleep(0) + return super().close(candidate) + + async def log(self, *values): + await asyncio.sleep(0) + return super().log(*values) + + +from types import SimpleNamespace +faults = SimpleNamespace(incompatible=Incompatible, invalid=ValueError, + operating=OSError, timeout=asyncio.TimeoutError, ordinary=Exception) +functions = [ + (named_break, True, False), + (sync_break, False, False), + (direct_break, True, False), + (unnamed_break, True, False), + (direct_unnamed_break, True, False), +] +for function, asynchronous, local in functions: + for case in ("normal", "empty", "incompatible", "no-close", "close-failed", "terminal", "invalid", "io", "timeout", "unexpected", "cancel", "log-error", "finish-error"): + owner = AsyncOwner() if asynchronous else Owner() + print(function.__name__, case) + try: + result = function(owner, faults) if local else function(owner) + if asynchronous: + result = asyncio.run(result) + print("return", result) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events) diff --git a/tests/corpus/3.12.3/handler_iterator_breaks.py b/tests/corpus/3.12.3/handler_iterator_breaks.py new file mode 100644 index 00000000..38105d90 --- /dev/null +++ b/tests/corpus/3.12.3/handler_iterator_breaks.py @@ -0,0 +1,142 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.12.3/handler_iterator_breaks.pyc b/tests/corpus/3.12.3/handler_iterator_breaks.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e133c8b773ec0d3de55647b169745ecaff1cd6e9 GIT binary patch literal 8276 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 Rhz__tz%5yU6ZXQW{{re~nREaE literal 0 HcmV?d00001 diff --git a/tests/corpus/3.13.7/handler_iterator_breaks.py b/tests/corpus/3.13.7/handler_iterator_breaks.py new file mode 100644 index 00000000..38105d90 --- /dev/null +++ b/tests/corpus/3.13.7/handler_iterator_breaks.py @@ -0,0 +1,142 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.13.7/handler_iterator_breaks.pyc b/tests/corpus/3.13.7/handler_iterator_breaks.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf77e184248569a7d910ba011cf6bea8178c9dd3 GIT binary patch literal 8508 zcmeHMT}&I<6~1H7^^EOd0b}FXfcZ7p1e^^R2(UmBzz#_vQe3y20>9 z5pBlDUvN$OJTZAtaPa}3mk-92)4b~fD96pQV?N#&825VS3u6kmi}(0EuAny%!|rjf zdm=`k_4_A@f43iIdmt8WDBZpsrksiUhZ!WW7i z#xN{y+8e{(xHHD%4Z^wtF~&Xa;?IF9_VF+`KypP@@`@Q@dBMN|Sf=X$sCg#6!WkcI zrYqze)<3o&WzjYBRrAd8SvI0DM``l|+PtnTxqR};epGD!OL@<; zjl!}`#LC;&OKYy!A&_eyUiS>swBaq1TvMcQ9$qq^ob zUGsxN3sF~z>L>1(%sKDr6DHR<>)*IyFu5;7XZU{W-1NP!h^jrxwXbpQMCUOEBhR18 z5N`ZefQF~=vC7^--6%WCN*1W2H2b@1kQP zk}Te`3j0C4q$(L`!AteJeht0UNA*j%WuyjqSxrIzvPLmbz${l923TgfhZ;bN3X5TMrC=YCjnsgNU8#@v?`Oc*N=pIQU+FUpHsVkPHCV-l44BBZWHi)R0i$8Y zU~9!;BV{wO;d)Hu{Va@zTglv2WUx2kRRd+OW>+gQk?Y9lY7-`-2WgOBDi{dZZ}vjF zL$S~gX*9i9fRw*;u>i#Zr0Q2I4n$bUSqu#BwiqDALmkQj{i_5Db^-bTHLn8tV#IkN zz9kFdjS$D4ApZLSdU7_v=`d7}Il;Su?9g7oJ7pVy5kPs%`l~=S)JJ)}Zwu8a?{Q#; z!e+~VIxuf%e_LSAvVSCHUxZ`%!i0 znz}Qd`n#Tg!xdi}x;iw&&z_5NmajNVD(5Fhe0NJ`op>VsbDBHvJ>=d_W%z&>cJfz!M;axIjS5>iUbL3nMKLC#2? zi^Q!- zV?4QRGkdM~A&r)$`P*Vxqgx5})E4&Bl` zX=+JRM;ZWkB-BF_An{Zst^@>d9v{%6U=jolz;G|o8vzi=%@9;S3_~#GX9&i0hJZ5} z!iL^3>->E5)6oT8#Mm9J?0!($9nrrt!#t{QzHM6QnqkBeJ-y;jrT+h7x&LR_*N;`U z&K=5qP=IpZ)=tWOkh98tD6f}PP5oB9)JyeK+)|$zXsC_M4ND9*3l3LNwpun^ zjfq@OM#GkPiQh_toPj-CCw;$vJ+UX($rQIo?D=e$*z@M3FJ~71A3EuSYz(w7_qnFr zq1gUhGmdY4P=BNohV3jWcV%%Lp$y#mX&ko-^57PEud~4Gxsn`ad8)=-)04pzl5I;h z3a0t=qo@|J`Yr*Qi+O?O%~90uj-uu&BC3uk*RjTRY;eUdokGp^L{yzou5*p+gyD<> zsAq9RvTJuhBv2p+Q0n*rlr|fY?8X1F11N3$0IE380hAUFpjyCtxSX<8vf&C$#tOAcoV!e&R6}VXs^6U z%=WpSAnTCt0zV+Xj;8aX?JO#GWf6WB&cF{{96#Wfp55>s_~|Q+{0PnDU!d4In29>c zOfrGP3cAxoe;_Y#{9iJZy`#Vq>?ZIM-3}-K3WS%G#(9Y@8wG47yWl0dI4>#5!%K9) zOIpBA$UqHNv!O~%>t`rwO%0`OWO_|1r-{)*muD$`Tyr(N+z3z8M2 L%`T9HcF_7aM`id! literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/handler_iterator_breaks.py b/tests/corpus/3.14.2/handler_iterator_breaks.py new file mode 100644 index 00000000..38105d90 --- /dev/null +++ b/tests/corpus/3.14.2/handler_iterator_breaks.py @@ -0,0 +1,142 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.14.2/handler_iterator_breaks.pyc b/tests/corpus/3.14.2/handler_iterator_breaks.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d10894d2447b965f56e07847c7e98126f4f6fe4d GIT binary patch literal 8804 zcmeHMZ%kX)6~E8+^?SA-V2r`B4L0FlFu{=UCjAH2n|#QUFFWVi5C6ds z!#Y*!@`TSj+X3RpZDSOP^KWHidey;i%c@3 z&BVandLy@yud`Nrg*3Nk_;jwbYZkOLOt~(bAl@KCmxH*-6?+=KA0nB=(3U zY9g(0QaPer_baZ^r8vpB=p;RP28CePJ!uQkW1iz%j^~{*+|0?D$QZc2c z$--Pt{J352V{E)Lt~~GcPI`smxLWXD^m*TOzdqrOE5|1SL9Zvyg}lNApWi+4gcn=v zZ@7IE(9Zk(-EeQ z#nt0(zsKishrGc!9Y58{2|{3 zuR{<5LR>x(I1`r#Lqc3V>YkWNwz4n0nCNER!MFY6zCfHGfgypZP@-Aocx&7{8S(}E zvs|18-t9;-J@{r9&ZTN6^ygvXWjlYU`l;{7#qZsqpmV> z#cN@CpDpLT%Su(tTxYbZCsy7QH5`r^jxQUI|Ed1x$G%dLvbqONM5Dc8`_Ojv z_%s()+hPjavck5iDY^8*S9?jZ?a$>skJpOKj|it~UoES;JOFdK^6r&)FTeXhrYJtb ztXZmNN`GgbHP5{gwe`lTdzY(wqn0CA2R?7ufAj3zJJ$vtuNkTz5e95mP1fJqZrY}Y z|Je5l{loF7sW+zY{Y>Bc^no0^pM0S+T(@1b-58!3i0ay6JpL{7ZBN&VjE{&+Rl1t& zAG{r$X}|SORM!&YTUPj%zw-L0Yg)r^3^Aj9#b{qHYC?0JWIlAaWagziri9BCzU_0q z7+mhkusM0Rb!O^LS5$W(#vfSW51`Eh3QnGUtt7PJ-@z()6z}NcV8k%g#3d9^hnalyi`*w1GU(p??ADD0ARF9C4UY?F8>r-T_G= zpIrC99dx&C8GlQ3PsJCp+>kbdbS&RG!j@Q`72#Maf`r_F*8(DhfO)i_M1fhffY386 zU~(^DmMUma@F*||6)~S3N36VYrMz+Gm1ucK)Nn9n=wCMUe{w8^I#m6$!JiCXK6j)3zM@*ho5zp#5N$ak zP4h;i=>njcZvVLZqwblvqt%^JOIHqv=~~qruMb`uyy3q)K399^Tuk4&qVE*prR&KT zeDRgR4+pRMr_aRrrce2%6s{zDe78%cU%F+w&o^#{C`rTfw_B&DZgt(~n=-H@*?Zyk zi!-ISesG`PmqL-?k6=50b?AG0W%smllzTPK8hY=5zORSQYwC`+DCYMwsJCePs@VA+ zb6*9!u!};=LIs7Eg{o52dw3YNsAf=V%>8z{XfFpnEVhI-W=9hZH=#|q8*Rde%uXwf zSSjckM>Bp;-iOb_58!D2PSU?3SY6iTQty!dol6`RdCAI?6}H#^H`3k95_ zfX`4s)-S-Ch=5FAz+DzfQ2;m*!H5EPS-_Nh7WwdsU~W)qe*lV$p)k6-=h z)j4CdwmW9+Ubc2eO^2?spV!%M*3WfaWhKUtPC!y9@h!5Cf5PSh`ylzT4}$E&*(0(K zlFdFKx1ZPB`wr3thUruA3oEBNmYnc8$ z++r<7y@3Ur#r@^b8!kkfaFNB)O~ZD^v5yNkQPf*GunBjgp9qIG5tYT+P9u95X9E|h zr>NV}CekkQjxGi08DND!0THa`zpG%Ct7^yZ5UghN1}i^WZijWMsQg zxzk!D9NA(8Ja_$9LNcK4H4*9xHi0@!O0&4j6AhN* zJe6;_rzS#q5}AT2e|j+uD$C?$_ym8rDK_?wuKbc6=3bs>dKe#E<{Qt6`ox{c#u%++moRxXO2ACoDsX*8OJ?qVj z)08jwfw9PaOd|IInTz{?4RRk7aGx@iHH>2q7p|qKH&EyeH*=^T#NG(-qhc;nguM|x z_C|~h>Lvx~Hf4gA*TY%`tiB3JsRhhk1pHdfl7}Ekf_rm^E(APN6W+tZ0ceBo8VT+O g0WZ-Q<}al1Yo&?Nf88Yt188laJ?a8YXa}qR0Pvh8M*si- literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/handler_iterator_breaks.py b/tests/corpus/3.15.2/handler_iterator_breaks.py new file mode 100644 index 00000000..38105d90 --- /dev/null +++ b/tests/corpus/3.15.2/handler_iterator_breaks.py @@ -0,0 +1,142 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/corpus/3.15.2/handler_iterator_breaks.pyc b/tests/corpus/3.15.2/handler_iterator_breaks.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39790a6e79656b734a98417a5914e79e46a1c943 GIT binary patch literal 8831 zcmeHMU2GHC6~5Q@%*6KK7{`u@ox~x*Y|M`noF8yPl3kM6NuV1{nhG}EBI6k1CD>@} zK-m{q_Mz;m72%-*)RmeQNSg>$kh+!ju_Up>Qw2D!QVG%Z@ww@fIfdoQrZ(MC}-gFKH1B4 zksxtV9SKsG1f;|z1u1pOK+0SUNM#7Scy3^nfXpQcFny#KS2O1l^YI91S&}hNuwRp74FC4!&J)%} zTrZ9&h&a;#+sDrPPR!LM-9ufn0JEnRa#V?)A;)cr965-WZ}!YUk!ah;iQaS8BPWM4 zVuU9d;99sn11M5Vs*C1-?`!)$=)v#z*1*^%b4BlW^kDp{@Rw_ncGP3+Ti2SQ` zWbp}ekkRg+*GY%ygwhdbk9xhwy+T|ecu)AeC*6mJy>Zqv90+;`;{_qFaLng-4?pJ< zEn5$}eZ$bs`~053vE%NL53M<$|Ac$kHyD?P1h+rv^ZG+^<%r*X0*n*x3J&}HzTl8| zaH24-@VNbhzCm}$8;moaA+P6XTy`iB7{>UX0E`}lT=?!x|8f{|FcBX{U}AB8--tit zJLc^agn$s24hN3JrNI!);YaS_kyInR3VtW=R8Ah{}C`{e0~@k#7$*m zMe>T9VS1rp4NTKr17?H6Ug4k*7SkOH2nP=dUiZPJ#ysQJ6zIA)Gj+U{1`ZrY=jEwR#;yQM8reQQkLb4TCv`|3}-9?41Z#)np-RG+te zVj1ljD~KvAF`4C_%rd7eI@9}TGg)i-b7{-7`-R4*q(I&h(+?%G zwQtk=<(1>Q8^#Ia%>z+OTdb<>UR7JP{OwWagH78m58eFmg7evZebrM!!FJAI`nBb< zW$fKA+rMId+Y>dk#kBZ$N89$|p%g8if34A9v|O-UdUxCz)zrs${JY23zqnsm@{~yA zy17(;_toHd!<7%C8e5FFMS0ubcmTrI&vk;+by-$FxL!9ta;+(Qgf4K+D?|6v>Dnr zuv0v3SF5Mi6r)~6p?zwDvcn{wsxfsKJzCoGLETN}Uou(#C>5!^oj`MulT% zN=;o#Zl;+!+xVHCEa<4)UlqCv}&X)=g0EeXu4=6HV zJ6~CmA;QN31%-3D1w|Rb;vuJK_(c*hmQghP7%f*)G>CaGQXbUhlm{CV*}PJDu)0DZ zH5$OT=bveq$pU=X)Ls?v)7}97mu+hh`$u0 zCmlnCYXQGu0@i~9l<6c3XoR<*C9Xh#OQ8rPL)N2NH*8o-V~ARJQF%dmsq;_jN)bIS zb;e4!+%4TQejr-9J*wXk(|6p_cYM{A#tlaCv+j?&&wX&IdRA7IK#XTk-y-T##EP1i zv7!l}Vyxkd=FgkQPerTj(ekD|Akj3ZExFizq5G2my62|(+6OVM{jS!YK!>L1U-N6v zcYo47>K{82(ZS(ca)AvxSCUskJPe5q}@EF-gdhL^>y@B>rb5`6v26eyG^cocYyFrgAKPXQmPfIMH&pg?Xf z;DQS}6nYdTC~(0AoUdR+fq*2yQjiHw0I(!N1qwt0p%O(EiVY~tAcB}mimfCugdg+) z0Sl#2#0uT5X!ky#NPaG&`7XIg^}<}loXJI)TrP6oP(IfG#lYtSH%p@C=9sDZo~b!% z*g48Q*tqp__06VHPGka^#NzJ%5V80JEc02dQ?~@M00STvP7M+Z)U%0&vk-`dwS(cN zl(fB2J;iT9eFtsd$xqo?)S=kd(P>5nmeWP1PCz6Z?P%nu>sZw7=rc^pp*LJ^a;)LP zE!08za4QRXL?(yc$Qm3QF`AqTF49CDt$d`J1$~BM?~H8A1NrN{CzpOI96K2u4Z8~K?kY@e|t z+G{B4wKA~D!oYbJ=YKsgFkijBn&0T&usHZ8;pO^ovA8r&o8n2?&;!3~k`c7;T@ZR+cfR!M zI5G(fdsgdcSOQ1D02q?PhB$(HHjX%ImS#vW3K$Yt=4D9G8z%M88?L9FJNR$|i+VE) z?Gai5dSpFynE6O0wnwTH?N*9Op(j1eyRpWt0B^bh{MZ literal 0 HcmV?d00001 diff --git a/tests/data/handler_iterator_breaks.py b/tests/data/handler_iterator_breaks.py new file mode 100644 index 00000000..38105d90 --- /dev/null +++ b/tests/data/handler_iterator_breaks.py @@ -0,0 +1,142 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + diff --git a/tests/integration.rs b/tests/integration.rs index f7b472b4..c3d05f5b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -365,6 +365,24 @@ fn private_handler_bodies_do_not_hide_later_typed_clauses() { } } +#[test] +fn handler_iterator_disposal_preserves_break_and_post_loop_work() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("handler_iterator_breaks.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "except (OSError, asyncio.TimeoutError) as error:\n await owner.log('transient', str(error))\n break", + "except Exception as error:\n await owner.log('unavailable', str(error))\n break", + "except (OSError, asyncio.TimeoutError) as error:\n owner.log('transient', str(error))\n break", + "except (OSError, asyncio.TimeoutError):\n await owner.log('transient', 'owned fault')\n break", + "except Exception:\n await owner.log('unavailable', 'owned fault')\n break", + " await owner.log('finished', accepted)\n return accepted", + " owner.log('finished', accepted)\n return accepted", + ], version); + assert_eq!(src.matches("\n break").count(), 10, "Python {version}: {src}"); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); diff --git a/tests/pyc/handler_iterator_breaks.3.12.pyc b/tests/pyc/handler_iterator_breaks.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e133c8b773ec0d3de55647b169745ecaff1cd6e9 GIT binary patch literal 8276 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 Rhz__tz%5yU6ZXQW{{re~nREaE literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_iterator_breaks.3.13.pyc b/tests/pyc/handler_iterator_breaks.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf77e184248569a7d910ba011cf6bea8178c9dd3 GIT binary patch literal 8508 zcmeHMT}&I<6~1H7^^EOd0b}FXfcZ7p1e^^R2(UmBzz#_vQe3y20>9 z5pBlDUvN$OJTZAtaPa}3mk-92)4b~fD96pQV?N#&825VS3u6kmi}(0EuAny%!|rjf zdm=`k_4_A@f43iIdmt8WDBZpsrksiUhZ!WW7i z#xN{y+8e{(xHHD%4Z^wtF~&Xa;?IF9_VF+`KypP@@`@Q@dBMN|Sf=X$sCg#6!WkcI zrYqze)<3o&WzjYBRrAd8SvI0DM``l|+PtnTxqR};epGD!OL@<; zjl!}`#LC;&OKYy!A&_eyUiS>swBaq1TvMcQ9$qq^ob zUGsxN3sF~z>L>1(%sKDr6DHR<>)*IyFu5;7XZU{W-1NP!h^jrxwXbpQMCUOEBhR18 z5N`ZefQF~=vC7^--6%WCN*1W2H2b@1kQP zk}Te`3j0C4q$(L`!AteJeht0UNA*j%WuyjqSxrIzvPLmbz${l923TgfhZ;bN3X5TMrC=YCjnsgNU8#@v?`Oc*N=pIQU+FUpHsVkPHCV-l44BBZWHi)R0i$8Y zU~9!;BV{wO;d)Hu{Va@zTglv2WUx2kRRd+OW>+gQk?Y9lY7-`-2WgOBDi{dZZ}vjF zL$S~gX*9i9fRw*;u>i#Zr0Q2I4n$bUSqu#BwiqDALmkQj{i_5Db^-bTHLn8tV#IkN zz9kFdjS$D4ApZLSdU7_v=`d7}Il;Su?9g7oJ7pVy5kPs%`l~=S)JJ)}Zwu8a?{Q#; z!e+~VIxuf%e_LSAvVSCHUxZ`%!i0 znz}Qd`n#Tg!xdi}x;iw&&z_5NmajNVD(5Fhe0NJ`op>VsbDBHvJ>=d_W%z&>cJfz!M;axIjS5>iUbL3nMKLC#2? zi^Q!- zV?4QRGkdM~A&r)$`P*Vxqgx5})E4&Bl` zX=+JRM;ZWkB-BF_An{Zst^@>d9v{%6U=jolz;G|o8vzi=%@9;S3_~#GX9&i0hJZ5} z!iL^3>->E5)6oT8#Mm9J?0!($9nrrt!#t{QzHM6QnqkBeJ-y;jrT+h7x&LR_*N;`U z&K=5qP=IpZ)=tWOkh98tD6f}PP5oB9)JyeK+)|$zXsC_M4ND9*3l3LNwpun^ zjfq@OM#GkPiQh_toPj-CCw;$vJ+UX($rQIo?D=e$*z@M3FJ~71A3EuSYz(w7_qnFr zq1gUhGmdY4P=BNohV3jWcV%%Lp$y#mX&ko-^57PEud~4Gxsn`ad8)=-)04pzl5I;h z3a0t=qo@|J`Yr*Qi+O?O%~90uj-uu&BC3uk*RjTRY;eUdokGp^L{yzou5*p+gyD<> zsAq9RvTJuhBv2p+Q0n*rlr|fY?8X1F11N3$0IE380hAUFpjyCtxSX<8vf&C$#tOAcoV!e&R6}VXs^6U z%=WpSAnTCt0zV+Xj;8aX?JO#GWf6WB&cF{{96#Wfp55>s_~|Q+{0PnDU!d4In29>c zOfrGP3cAxoe;_Y#{9iJZy`#Vq>?ZIM-3}-K3WS%G#(9Y@8wG47yWl0dI4>#5!%K9) zOIpBA$UqHNv!O~%>t`rwO%0`OWO_|1r-{)*muD$`Tyr(N+z3z8M2 L%`T9HcF_7aM`id! literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_iterator_breaks.3.14.pyc b/tests/pyc/handler_iterator_breaks.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d10894d2447b965f56e07847c7e98126f4f6fe4d GIT binary patch literal 8804 zcmeHMZ%kX)6~E8+^?SA-V2r`B4L0FlFu{=UCjAH2n|#QUFFWVi5C6ds z!#Y*!@`TSj+X3RpZDSOP^KWHidey;i%c@3 z&BVandLy@yud`Nrg*3Nk_;jwbYZkOLOt~(bAl@KCmxH*-6?+=KA0nB=(3U zY9g(0QaPer_baZ^r8vpB=p;RP28CePJ!uQkW1iz%j^~{*+|0?D$QZc2c z$--Pt{J352V{E)Lt~~GcPI`smxLWXD^m*TOzdqrOE5|1SL9Zvyg}lNApWi+4gcn=v zZ@7IE(9Zk(-EeQ z#nt0(zsKishrGc!9Y58{2|{3 zuR{<5LR>x(I1`r#Lqc3V>YkWNwz4n0nCNER!MFY6zCfHGfgypZP@-Aocx&7{8S(}E zvs|18-t9;-J@{r9&ZTN6^ygvXWjlYU`l;{7#qZsqpmV> z#cN@CpDpLT%Su(tTxYbZCsy7QH5`r^jxQUI|Ed1x$G%dLvbqONM5Dc8`_Ojv z_%s()+hPjavck5iDY^8*S9?jZ?a$>skJpOKj|it~UoES;JOFdK^6r&)FTeXhrYJtb ztXZmNN`GgbHP5{gwe`lTdzY(wqn0CA2R?7ufAj3zJJ$vtuNkTz5e95mP1fJqZrY}Y z|Je5l{loF7sW+zY{Y>Bc^no0^pM0S+T(@1b-58!3i0ay6JpL{7ZBN&VjE{&+Rl1t& zAG{r$X}|SORM!&YTUPj%zw-L0Yg)r^3^Aj9#b{qHYC?0JWIlAaWagziri9BCzU_0q z7+mhkusM0Rb!O^LS5$W(#vfSW51`Eh3QnGUtt7PJ-@z()6z}NcV8k%g#3d9^hnalyi`*w1GU(p??ADD0ARF9C4UY?F8>r-T_G= zpIrC99dx&C8GlQ3PsJCp+>kbdbS&RG!j@Q`72#Maf`r_F*8(DhfO)i_M1fhffY386 zU~(^DmMUma@F*||6)~S3N36VYrMz+Gm1ucK)Nn9n=wCMUe{w8^I#m6$!JiCXK6j)3zM@*ho5zp#5N$ak zP4h;i=>njcZvVLZqwblvqt%^JOIHqv=~~qruMb`uyy3q)K399^Tuk4&qVE*prR&KT zeDRgR4+pRMr_aRrrce2%6s{zDe78%cU%F+w&o^#{C`rTfw_B&DZgt(~n=-H@*?Zyk zi!-ISesG`PmqL-?k6=50b?AG0W%smllzTPK8hY=5zORSQYwC`+DCYMwsJCePs@VA+ zb6*9!u!};=LIs7Eg{o52dw3YNsAf=V%>8z{XfFpnEVhI-W=9hZH=#|q8*Rde%uXwf zSSjckM>Bp;-iOb_58!D2PSU?3SY6iTQty!dol6`RdCAI?6}H#^H`3k95_ zfX`4s)-S-Ch=5FAz+DzfQ2;m*!H5EPS-_Nh7WwdsU~W)qe*lV$p)k6-=h z)j4CdwmW9+Ubc2eO^2?spV!%M*3WfaWhKUtPC!y9@h!5Cf5PSh`ylzT4}$E&*(0(K zlFdFKx1ZPB`wr3thUruA3oEBNmYnc8$ z++r<7y@3Ur#r@^b8!kkfaFNB)O~ZD^v5yNkQPf*GunBjgp9qIG5tYT+P9u95X9E|h zr>NV}CekkQjxGi08DND!0THa`zpG%Ct7^yZ5UghN1}i^WZijWMsQg zxzk!D9NA(8Ja_$9LNcK4H4*9xHi0@!O0&4j6AhN* zJe6;_rzS#q5}AT2e|j+uD$C?$_ym8rDK_?wuKbc6=3bs>dKe#E<{Qt6`ox{c#u%++moRxXO2ACoDsX*8OJ?qVj z)08jwfw9PaOd|IInTz{?4RRk7aGx@iHH>2q7p|qKH&EyeH*=^T#NG(-qhc;nguM|x z_C|~h>Lvx~Hf4gA*TY%`tiB3JsRhhk1pHdfl7}Ekf_rm^E(APN6W+tZ0ceBo8VT+O g0WZ-Q<}al1Yo&?Nf88Yt188laJ?a8YXa}qR0Pvh8M*si- literal 0 HcmV?d00001 diff --git a/tests/pyc/handler_iterator_breaks.3.15.pyc b/tests/pyc/handler_iterator_breaks.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39790a6e79656b734a98417a5914e79e46a1c943 GIT binary patch literal 8831 zcmeHMU2GHC6~5Q@%*6KK7{`u@ox~x*Y|M`noF8yPl3kM6NuV1{nhG}EBI6k1CD>@} zK-m{q_Mz;m72%-*)RmeQNSg>$kh+!ju_Up>Qw2D!QVG%Z@ww@fIfdoQrZ(MC}-gFKH1B4 zksxtV9SKsG1f;|z1u1pOK+0SUNM#7Scy3^nfXpQcFny#KS2O1l^YI91S&}hNuwRp74FC4!&J)%} zTrZ9&h&a;#+sDrPPR!LM-9ufn0JEnRa#V?)A;)cr965-WZ}!YUk!ah;iQaS8BPWM4 zVuU9d;99sn11M5Vs*C1-?`!)$=)v#z*1*^%b4BlW^kDp{@Rw_ncGP3+Ti2SQ` zWbp}ekkRg+*GY%ygwhdbk9xhwy+T|ecu)AeC*6mJy>Zqv90+;`;{_qFaLng-4?pJ< zEn5$}eZ$bs`~053vE%NL53M<$|Ac$kHyD?P1h+rv^ZG+^<%r*X0*n*x3J&}HzTl8| zaH24-@VNbhzCm}$8;moaA+P6XTy`iB7{>UX0E`}lT=?!x|8f{|FcBX{U}AB8--tit zJLc^agn$s24hN3JrNI!);YaS_kyInR3VtW=R8Ah{}C`{e0~@k#7$*m zMe>T9VS1rp4NTKr17?H6Ug4k*7SkOH2nP=dUiZPJ#ysQJ6zIA)Gj+U{1`ZrY=jEwR#;yQM8reQQkLb4TCv`|3}-9?41Z#)np-RG+te zVj1ljD~KvAF`4C_%rd7eI@9}TGg)i-b7{-7`-R4*q(I&h(+?%G zwQtk=<(1>Q8^#Ia%>z+OTdb<>UR7JP{OwWagH78m58eFmg7evZebrM!!FJAI`nBb< zW$fKA+rMId+Y>dk#kBZ$N89$|p%g8if34A9v|O-UdUxCz)zrs${JY23zqnsm@{~yA zy17(;_toHd!<7%C8e5FFMS0ubcmTrI&vk;+by-$FxL!9ta;+(Qgf4K+D?|6v>Dnr zuv0v3SF5Mi6r)~6p?zwDvcn{wsxfsKJzCoGLETN}Uou(#C>5!^oj`MulT% zN=;o#Zl;+!+xVHCEa<4)UlqCv}&X)=g0EeXu4=6HV zJ6~CmA;QN31%-3D1w|Rb;vuJK_(c*hmQghP7%f*)G>CaGQXbUhlm{CV*}PJDu)0DZ zH5$OT=bveq$pU=X)Ls?v)7}97mu+hh`$u0 zCmlnCYXQGu0@i~9l<6c3XoR<*C9Xh#OQ8rPL)N2NH*8o-V~ARJQF%dmsq;_jN)bIS zb;e4!+%4TQejr-9J*wXk(|6p_cYM{A#tlaCv+j?&&wX&IdRA7IK#XTk-y-T##EP1i zv7!l}Vyxkd=FgkQPerTj(ekD|Akj3ZExFizq5G2my62|(+6OVM{jS!YK!>L1U-N6v zcYo47>K{82(ZS(ca)AvxSCUskJPe5q}@EF-gdhL^>y@B>rb5`6v26eyG^cocYyFrgAKPXQmPfIMH&pg?Xf z;DQS}6nYdTC~(0AoUdR+fq*2yQjiHw0I(!N1qwt0p%O(EiVY~tAcB}mimfCugdg+) z0Sl#2#0uT5X!ky#NPaG&`7XIg^}<}loXJI)TrP6oP(IfG#lYtSH%p@C=9sDZo~b!% z*g48Q*tqp__06VHPGka^#NzJ%5V80JEc02dQ?~@M00STvP7M+Z)U%0&vk-`dwS(cN zl(fB2J;iT9eFtsd$xqo?)S=kd(P>5nmeWP1PCz6Z?P%nu>sZw7=rc^pp*LJ^a;)LP zE!08za4QRXL?(yc$Qm3QF`AqTF49CDt$d`J1$~BM?~H8A1NrN{CzpOI96K2u4Z8~K?kY@e|t z+G{B4wKA~D!oYbJ=YKsgFkijBn&0T&usHZ8;pO^ovA8r&o8n2?&;!3~k`c7;T@ZR+cfR!M zI5G(fdsgdcSOQ1D02q?PhB$(HHjX%ImS#vW3K$Yt=4D9G8z%M88?L9FJNR$|i+VE) z?Gai5dSpFynE6O0wnwTH?N*9Op(j1eyRpWt0B^bh{MZ literal 0 HcmV?d00001 From 05db4010be3db21b8662fe3e11717bb737f1bb0b Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 07:43:23 -0400 Subject: [PATCH 19/22] Recover shared None-led rejection guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit def select(value): if value is None or not 0.0 <= value <= 15.0: return False return True def collect(rows): result = [] for a, b, c, d in rows: if a is None or b is None or c is None or d is None or d < 0.01: continue result.append(a + b + c + d) return result The Python 3.14–3.15 compiler shares rejection blocks among predicate edges and jumps over them on success. The parser reads these as nested positive conditions, allowing missing values through. A copied middle operand in a negated comparison chain also needs to survive each link. Prove the shared constant return or registered loop continue, simulate predicate expressions and merge only comparison links with a carried middle value. Preserve independent repeated calls and wrap the original comparison in not: reversing inequalities changes NaN and rich-comparison behavior. Resume at the proven success continuation. Accept constant-pool, small-integer and shared-constant return encodings. Require iterator/value cleanup before consuming a duplicate return, and exclude the success continuation from rejection-target matching even when it returns the same constant. This keeps a real following return separate from compiler-generated copies. Cover None in every loop operand, endpoint and NaN comparisons, longer chains, side-effecting and failing calls, rich comparisons, and equal rejection/success values. These are existing Python constructs; the new reconstruction route targets the 3.14+ bytecode layout. --- src/decompiler.rs | 153 ++++++++++++++++++ .../behavior/cases/b_none_rejection_guards.py | 66 ++++++++ tests/corpus/3.14.2/none_rejection_guards.py | 62 +++++++ tests/corpus/3.14.2/none_rejection_guards.pyc | Bin 0 -> 2524 bytes tests/corpus/3.15.2/none_rejection_guards.py | 62 +++++++ tests/corpus/3.15.2/none_rejection_guards.pyc | Bin 0 -> 2528 bytes tests/data/none_rejection_guards.py | 62 +++++++ tests/integration.rs | 19 +++ tests/pyc/none_rejection_guards.3.14.pyc | Bin 0 -> 2524 bytes tests/pyc/none_rejection_guards.3.15.pyc | Bin 0 -> 2528 bytes 10 files changed, 424 insertions(+) create mode 100644 tests/behavior/cases/b_none_rejection_guards.py create mode 100644 tests/corpus/3.14.2/none_rejection_guards.py create mode 100644 tests/corpus/3.14.2/none_rejection_guards.pyc create mode 100644 tests/corpus/3.15.2/none_rejection_guards.py create mode 100644 tests/corpus/3.15.2/none_rejection_guards.pyc create mode 100644 tests/data/none_rejection_guards.py create mode 100644 tests/pyc/none_rejection_guards.3.14.pyc create mode 100644 tests/pyc/none_rejection_guards.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index a96459db..0443037c 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -30477,9 +30477,162 @@ return None; Some(body_start) } + /// Recover a shared rejection block reached by a None-led OR guard. + /// CPython 3.14 puts the last test's success jump over the reject block. + /// Treating earlier None jumps as nested positive ifs loses rejection for + /// missing values. A negated comparison chain also has a POP_TOP cleanup + /// edge; retain its copied middle value to avoid evaluating it twice. + fn try_none_rejection_guard( + &self, first: &ExprRef, first_jump_true: bool, target: usize, + ) -> Option<(ExprRef, Vec, usize)> { + if !self.version.at_least(3, 14) || !self.stack.is_empty() || target <= self.cur_offset { + return None; + } + if !matches!(&**first, Expr::Compare { operands, ops } + if operands.len() == 2 && ops.len() == 1 + && matches!(ops[0], CmpOp::Is | CmpOp::IsNot) + && matches!(&*operands[1], Expr::Const(value) if matches!(&**value, PyObject::None))) { + return None; + } + let padding = |op| matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::EXTENDED_ARG); + let skip_padding = |mut index: usize| { + while self.instrs.get(index).map_or(false, |item| padding(item.op)) { index += 1; } + index + }; + let constant_at = |index: usize| -> Option { + let item = self.instrs.get(index)?; + match item.op { + Op::LOAD_CONST => self.code.consts.get(item.arg as usize).cloned(), + Op::LOAD_SMALL_INT => Some(Rc::new(PyObject::Int(item.arg as i32))), + Op::LOAD_COMMON_CONSTANT => match &*common_constant_expr(item.arg, self.version)? { + Expr::Const(value) => Some(value.clone()), + _ => None, + }, + _ => None, + } + }; + let reject_index = skip_padding(*self.idx_of.get(&target)?); + let reject = self.instrs.get(reject_index)?; + let (body, end_index) = if reject.is_backward && matches!(reject.op, Op::JUMP_BACKWARD | Op::JUMP_BACKWARD_NO_INTERRUPT) { + let top = reject.target?; + if !self.blocks.iter().any(|block| matches!(block.kind, BlockType::For | BlockType::While) + && (block.start == top || block.cond_end == top)) { return None; } + (vec![Stmt::Continue], reject_index + 1) + } else if let Some(value) = constant_at(reject_index) { + let ret = skip_padding(reject_index + 1); + if self.instrs.get(ret)?.op != Op::RETURN_VALUE { return None; } + (vec![Stmt::Return(Some(Rc::new(Expr::Const(value))))], ret + 1) + } else { return None; }; + let equivalent_return = |index: usize| -> Option { + let Some(Stmt::Return(Some(expected))) = body.first() else { return None; }; + let Expr::Const(expected) = &**expected else { return None; }; + let actual = constant_at(index)?; + if !Rc::ptr_eq(expected, &actual) && !const_value_eq(expected, &actual) { return None; } + let ret = skip_padding(index + 1); + if self.instrs.get(ret)?.op != Op::RETURN_VALUE { return None; } + Some(ret + 1) + }; + let mut body_index = skip_padding(end_index); + // A failed chained link may pop its retained middle value before a + // compiler-generated duplicate return. The successful jump skips + // both return copies, not merely the first one. + if matches!(body.first(), Some(Stmt::Return(_))) { + loop { + let mut copy = body_index; + if !self.instrs.get(copy).map_or(false, |item| item.op == Op::POP_TOP) { + break; + } + copy = skip_padding(copy + 1); + let Some(after_copy) = equivalent_return(copy) else { break; }; + body_index = skip_padding(after_copy); + } + } + let body_start = self.instrs.get(body_index)?.offset; + let reaches_reject = |offset: usize| -> bool { + let Some(&start) = self.idx_of.get(&offset) else { return false; }; + let mut index = skip_padding(start); + // A real success continuation may return the same constant. + // Equality alone does not make that statement a rejection copy. + if index >= body_index { return false; } + if self.instrs.get(index).map_or(false, |item| item.op == Op::POP_TOP) { + index = skip_padding(index + 1); + } + if index == reject_index { return true; } + if equivalent_return(index).is_some() { return true; } + if self.instrs.get(index).map_or(false, |item| + self.dup_copy_redirect.get(&item.offset).copied() == Some(reject.offset)) { return true; } + self.instrs.get(index).map_or(false, |item| item.op == Op::JUMP_FORWARD + && item.target.map_or(false, |dest| skip_padding(*self.idx_of.get(&dest).unwrap_or(&usize::MAX)) == reject_index)) + }; + let failure = |condition: ExprRef, truth: bool| -> ExprRef { + if truth { return condition; } + if let Expr::Compare { operands, ops } = &*condition { + if ops.len() == 1 && matches!(ops[0], CmpOp::Is | CmpOp::IsNot) { + return Rc::new(Expr::Compare { + operands: operands.clone(), ops: vec![if ops[0] == CmpOp::Is { CmpOp::IsNot } else { CmpOp::Is }], + }); + } + } + // Do not flip inequalities: NaN and custom rich comparisons + // make `not (a <= b)` different from `a > b`. + Rc::new(Expr::Unary { op: UnaryOp::Not, operand: condition }) + }; + let mut values = vec![failure(first.clone(), first_jump_true)]; + let mut start = *self.idx_of.get(&self.cur_offset)? + 1; + let mut carried = Vec::new(); + let mut complete = false; + while start < reject_index { + let mut end = start; + while end < reject_index && !matches!(self.instrs[end].op, + Op::POP_JUMP_IF_NONE | Op::POP_JUMP_IF_NOT_NONE | Op::POP_JUMP_IF_TRUE | Op::POP_JUMP_IF_FALSE) { + end += 1; + } + if end == reject_index { return None; } + let was_chain = !carried.is_empty(); + carried = self.sim_stack_region(start, end, carried)?; + let value = carried.pop()?; + let jump = self.instrs[end]; + let (condition, jump_true) = match jump.op { + Op::POP_JUMP_IF_NONE | Op::POP_JUMP_IF_NOT_NONE => (Rc::new(Expr::Compare { + operands: vec![value, Rc::new(Expr::Const(Rc::new(PyObject::None)))], + ops: vec![if jump.op == Op::POP_JUMP_IF_NONE { CmpOp::Is } else { CmpOp::IsNot }], + }), true), + Op::POP_JUMP_IF_TRUE => (value, true), + _ => (value, false), + }; + let destination = jump.target?; + let member = if reaches_reject(destination) { + failure(condition, jump_true) + } else if destination == body_start && reaches_reject(self.instrs.get(end + 1)?.offset) { + complete = true; + failure(condition, !jump_true) + } else { return None; }; + let merged = if was_chain { + match (values.last().map(|value| &**value), &*member) { + (Some(Expr::Unary { op: UnaryOp::Not, operand: left }), + Expr::Unary { op: UnaryOp::Not, operand: right }) => { + merge_chain_compare(left, right).map(|operand| Rc::new(Expr::Unary { op: UnaryOp::Not, operand })) + } + _ => None, + } + } else { None }; + if let Some(merged) = merged { values.pop(); values.push(merged); } + else { values.push(member); } + if complete { break; } + start = end + 1; + } + if !complete || !carried.is_empty() || values.len() < 2 { return None; } + Some((Rc::new(Expr::BoolOp { op: BoolOpKind::Or, values }), body, body_start)) + } + fn handle_cond_jump(&mut self, cond: ExprRef, jump_if_true: bool, target: usize) { // tail-duplicated body copy: aim the jump at the original body let target = self.dup_copy_redirect.get(&target).copied().unwrap_or(target); + if let Some((guard, body, resume)) = self.try_none_rejection_guard(&cond, jump_if_true, target) { + self.push_stmt(Stmt::If { cond: guard, body, orelse: Vec::new() }); + self.skip_until = Some(resume); + return; + } let raw_target = target; // 3.8+ `continue` threading: the jump may land ON the loop's back // edge instruction instead of the loop top (3.10 `if a or b: diff --git a/tests/behavior/cases/b_none_rejection_guards.py b/tests/behavior/cases/b_none_rejection_guards.py new file mode 100644 index 00000000..a0ee0677 --- /dev/null +++ b/tests/behavior/cases/b_none_rejection_guards.py @@ -0,0 +1,66 @@ +# MIN_VERSION: 3.14 +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + +# Keep the driver out of the source being reconstructed. +exec("trace = []\ndef outcome(function, *args):\n try:\n return ('return', function(*args))\n except Exception as error:\n return ('raise', type(error).__name__, str(error))\n\nclass Number:\n def __init__(self, label, value, fail=None):\n self.label, self.value, self.fail = label, value, fail\n def compare(self, op, other):\n trace.append((self.label, op, getattr(other, 'label', other)))\n if self.fail == op:\n raise RuntimeError(self.label + ':' + op)\n other = getattr(other, 'value', other)\n return self.value <= other if op == 'le' else self.value >= other\n def __le__(self, other): return self.compare('le', other)\n def __ge__(self, other): return self.compare('ge', other)\n def __lt__(self, other): raise AssertionError('rewritten lt')\n def __gt__(self, other): raise AssertionError('rewritten gt')\n def __bool__(self): raise AssertionError('unexpected truth conversion')\n\ndef reader(label, values):\n pending = iter(values)\n def read():\n trace.append(label)\n result = next(pending)\n if isinstance(result, Exception): raise result\n return result\n return read\n\nfor function in (in_range, same_tail, integer_tail, string_tail):\n for value in (None, -1.0, 0.0, 10.0, 15.0, 16.0, float('nan'), float('inf')):\n print(function.__name__, repr(value), outcome(function, value))\n for value in (-1.0, 3.0, 17.0, float('nan')):\n for fail in (None, 'ge', 'le'):\n trace.clear()\n result = outcome(function, Number('value', value, fail))\n print(function.__name__, repr(value), fail, result, trace)\nfor function in (called, separate):\n print(function.__name__, 'none', outcome(function, None))\n for values in ((1.0, 16.0), (-1.0, 5.0), (float('nan'), 2.0), (2.0, float('nan')), (ValueError('first'),), (2.0, ValueError('second'))):\n trace.clear()\n result = outcome(function, reader('read', values))\n print(function.__name__, repr(values), result, trace)\nfor a, b in ((1.0, 2.0), (-1.0, 2.0), (3.0, 2.0), (2.0, 16.0), (float('nan'), 2.0), (2.0, float('nan')), (ValueError('first'), 2.0), (2.0, ValueError('second'))):\n trace.clear()\n result = outcome(multi, reader('first', (a,)), reader('second', (b,)))\n print('multi', repr((a,b)), result, trace)\ntrace.clear()\nprint('multi-none', outcome(multi, None, reader('unused', (1,))), trace)\nclass Row(dict):\n def get(self, key):\n trace.append(key)\n return super().get(key)\nrows = [(1,2,3,4), (1,2,3,0), (None,2,3,4), (1,None,3,4), (1,2,None,4), (1,2,3,None), (4,5,6,7)]\nfor values in ([], rows, rows[::-1]):\n trace.clear()\n mapped = [Row(zip('abcd', value)) for value in values]\n print('collect', outcome(collect, mapped), trace)\n print('unpacked', outcome(unpacked, values))\n") diff --git a/tests/corpus/3.14.2/none_rejection_guards.py b/tests/corpus/3.14.2/none_rejection_guards.py new file mode 100644 index 00000000..c0c4e441 --- /dev/null +++ b/tests/corpus/3.14.2/none_rejection_guards.py @@ -0,0 +1,62 @@ +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result diff --git a/tests/corpus/3.14.2/none_rejection_guards.pyc b/tests/corpus/3.14.2/none_rejection_guards.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b95f7ac7a3870a6d68afc190eb4397873a0adba GIT binary patch literal 2524 zcmb_e&2Jk;6o0c{{;o}8uxMP8;9z8e(gH37s8U2qa%dZsRpkl^H7x5%FgR;7yQX{; z$*MpkaA*$%MX0J0a=|J>h*J(6_$L}Ai#Q+-@Lk1ks2_n%YL`jzsWnDp3bQMw6HAGDh zA%^rYVpxylpb#~8zr{bvH{`#aQG_OxfTjVm_~rA^ke-)Dp93gI#fliuhWNM-bU6q8 z0(B(^Ep_TPzAld!+uJ8?IQ>%&=xD!;OKzXb&PZVIvg3^5N3>^6~ZTv9C|v${yQHrnh7`@&xvq zvE&XKL1*V)6z`*R4|>j(U=WJj9NWR&Wc;9*hJb!4j2dOnEf&EgS2K-jm6ll&CwNL= z=MXsQ*p_x zY{%7XYSyf>%?ai6o@?q{qv}*xFD@nUXFn!*`EslrUwvjhJJy=H`381tL$|VHzAh&{ zT|5Un23>PQ8|RB6u{mB;k+U4&+?jH`VmTCZ)nm;W_jhHT@-sHI8H@jKtoN3qf*N4$ zYF>?THN!M%-RbN_%ywACn)RIE5%Ne^Co;Yhnax=5-A-_Uc@FOkptMaGfjTKc3MM`V zs7od3Trd^Tj5i&M{%T#`$AYvilzl9uwuQQnh1|B#O0rk!OA1YiBvWZz^coF&y{?S* zVD%TNcKS8I8gq_&^|yAR_;_HYr0$t%!P)NK{c!pdfCxN3z9rbu^buoIKV%(Yz86 zGHY0uO@|fm$uKv$jDUqXk!()kVPYybX#Xo&^iF_GZe)+3c zz8n0$u=e(k#UI|?jHfrkhpr|T64#7Hx@0U?mIoK-*26=;Cn1&INpZmxDYwovh1MPYdg4Xr`8Z}gnJ;`jQ2|z&;fube>B~1p(ngUcb6{u<< zU`PuC!=s@r%SViz-Dp!flIr$rfEsFnnQ6v4u=m@{5zmPIm@uN=h}RwuQTr$xjb>Sw zhog}!TaiZY;bpG`jpJ{uuob~ouIi;}%ce7PZjWQxj(x(^9sNYPQntpp^`T`r#Y%Z> zVp`{>eZ2aq8!nc|xL%&Hcmyxei*^?9C0h^0uYa*HxOjRkbMnWbyP1<4$@Hegx?aL_ zBbMC8BDTMALztq8+<_ZQ=;h7zdu^MJY}OQ)A#S<4KpjNkeIkJaA0nSnr? zv%WUIiTgTUb8S5L!+|ibK{3sDMO;%y-31?L!{bEI^EjV-*tV)V*By()wOJd@b_MiF zn!%;M<^CTou9&yRSLL6||H=3-3#}tq46TCAd#L!Yv^mn zDq=yr)EEUAr=qb_!DvM~V1-a$As?_ptglcGSRvI{s0GPS^99*VdC{eqh#EwhY9g1> zQlqBb|k@EL3f4sUBPe# z^WNv_pNIea?5nfLMbd! zSNzxy85JaD!{N_i<^@`E4S>v*NY>8%k!+ih*5k<=!?VM4gWp}45f0ml#lJnXAbcNL z)Na0WJ9{U;I`wPcFQ070)9c}u8;RM(oIYzHv`S~@wUr7jk*-%UAFYsP^sGLHE=Lq)Ggo47?BuK~8<*IIcgfAtX{3@z_0kEUVk&fBuj4-3G zKk6NaSbOJY_`x0LBi)#!!(CP{PFA>bQIRF3v literal 0 HcmV?d00001 diff --git a/tests/data/none_rejection_guards.py b/tests/data/none_rejection_guards.py new file mode 100644 index 00000000..c0c4e441 --- /dev/null +++ b/tests/data/none_rejection_guards.py @@ -0,0 +1,62 @@ +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result diff --git a/tests/integration.rs b/tests/integration.rs index c3d05f5b..2bc0cc64 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1225,3 +1225,22 @@ fn python315_shapes_recover_changed_encodings() { assert!(!src.contains("/*with-result*/"), "unresolved with target:\n{src}"); assert!(!src.contains('\0'), "PUSH_NULL marker leaked:\n{src}"); } + +#[test] +fn none_rejection_guards_preserve_chains_calls_and_success_returns() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("none_rejection_guards.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "if value is None or not 0.0 <= value <= 15.0:", + "if read is None or not 0.0 <= read() <= 15.0:", + "if first is None or not 0.0 <= first() <= second() <= 15.0:", + "if read is None or not 0.0 <= read() or not read() <= 15.0:", + "def same_tail(value):\n if value is None or not 0.0 <= value <= 15.0:\n return False\n return False", + "return 3\n return 5", + "return 'reject'\n return 'accept'", + "if a is None or b is None or c is None or d is None or d < 0.01:\n continue", + ], version); + assert_eq!(src.matches(" continue").count(), 2, "{version}: {src}"); + } +} diff --git a/tests/pyc/none_rejection_guards.3.14.pyc b/tests/pyc/none_rejection_guards.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b95f7ac7a3870a6d68afc190eb4397873a0adba GIT binary patch literal 2524 zcmb_e&2Jk;6o0c{{;o}8uxMP8;9z8e(gH37s8U2qa%dZsRpkl^H7x5%FgR;7yQX{; z$*MpkaA*$%MX0J0a=|J>h*J(6_$L}Ai#Q+-@Lk1ks2_n%YL`jzsWnDp3bQMw6HAGDh zA%^rYVpxylpb#~8zr{bvH{`#aQG_OxfTjVm_~rA^ke-)Dp93gI#fliuhWNM-bU6q8 z0(B(^Ep_TPzAld!+uJ8?IQ>%&=xD!;OKzXb&PZVIvg3^5N3>^6~ZTv9C|v${yQHrnh7`@&xvq zvE&XKL1*V)6z`*R4|>j(U=WJj9NWR&Wc;9*hJb!4j2dOnEf&EgS2K-jm6ll&CwNL= z=MXsQ*p_x zY{%7XYSyf>%?ai6o@?q{qv}*xFD@nUXFn!*`EslrUwvjhJJy=H`381tL$|VHzAh&{ zT|5Un23>PQ8|RB6u{mB;k+U4&+?jH`VmTCZ)nm;W_jhHT@-sHI8H@jKtoN3qf*N4$ zYF>?THN!M%-RbN_%ywACn)RIE5%Ne^Co;Yhnax=5-A-_Uc@FOkptMaGfjTKc3MM`V zs7od3Trd^Tj5i&M{%T#`$AYvilzl9uwuQQnh1|B#O0rk!OA1YiBvWZz^coF&y{?S* zVD%TNcKS8I8gq_&^|yAR_;_HYr0$t%!P)NK{c!pdfCxN3z9rbu^buoIKV%(Yz86 zGHY0uO@|fm$uKv$jDUqXk!()kVPYybX#Xo&^iF_GZe)+3c zz8n0$u=e(k#UI|?jHfrkhpr|T64#7Hx@0U?mIoK-*26=;Cn1&INpZmxDYwovh1MPYdg4Xr`8Z}gnJ;`jQ2|z&;fube>B~1p(ngUcb6{u<< zU`PuC!=s@r%SViz-Dp!flIr$rfEsFnnQ6v4u=m@{5zmPIm@uN=h}RwuQTr$xjb>Sw zhog}!TaiZY;bpG`jpJ{uuob~ouIi;}%ce7PZjWQxj(x(^9sNYPQntpp^`T`r#Y%Z> zVp`{>eZ2aq8!nc|xL%&Hcmyxei*^?9C0h^0uYa*HxOjRkbMnWbyP1<4$@Hegx?aL_ zBbMC8BDTMALztq8+<_ZQ=;h7zdu^MJY}OQ)A#S<4KpjNkeIkJaA0nSnr? zv%WUIiTgTUb8S5L!+|ibK{3sDMO;%y-31?L!{bEI^EjV-*tV)V*By()wOJd@b_MiF zn!%;M<^CTou9&yRSLL6||H=3-3#}tq46TCAd#L!Yv^mn zDq=yr)EEUAr=qb_!DvM~V1-a$As?_ptglcGSRvI{s0GPS^99*VdC{eqh#EwhY9g1> zQlqBb|k@EL3f4sUBPe# z^WNv_pNIea?5nfLMbd! zSNzxy85JaD!{N_i<^@`E4S>v*NY>8%k!+ih*5k<=!?VM4gWp}45f0ml#lJnXAbcNL z)Na0WJ9{U;I`wPcFQ070)9c}u8;RM(oIYzHv`S~@wUr7jk*-%UAFYsP^sGLHE=Lq)Ggo47?BuK~8<*IIcgfAtX{3@z_0kEUVk&fBuj4-3G zKk6NaSbOJY_`x0LBi)#!!(CP{PFA>bQIRF3v literal 0 HcmV?d00001 From 7c7a3fdfce01256acb64f77aa58fd90da9926ae9 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Thu, 29 Oct 2026 07:47:51 -0400 Subject: [PATCH 20/22] Recover shared guards returning constructed outcomes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit def select(value, build): if not value or len(value) < 2: return build("reject", amount=0) return build("accept", amount=len(value)) def collect_result(value, rows, collect, scale): if value is None or not 0.0 <= value <= 20.0: return collect(scale * item for item in rows) return collect(rows) The guard recognizer requires a None-identity first test and a constant return. With a constructed rejection value, missing inputs can instead reach the success path. Accept arbitrary first predicates and a bounded value-building region that leaves exactly one result before RETURN_VALUE. Reject intervening stores, branches and exception cleanup so statement bodies remain available to the ordinary parser. Keep keyword names, constructor evaluation and lazy generator iteration on the original selected path. Recognize duplicated short constructed returns only when their complete instruction runs match, while retaining the cleanup and success-boundary checks used for constant return copies. Support dictionary construction in the bounded simulator with exact key/value consumption and original pair order. The Python 3.14–3.15 examples cover truth hooks, comparison predicates, factories, lists, dictionaries and captured generator outcomes. Controls retain explicit assignments and logging before returns, along with exceptions from predicates, constructors and iteration. The reconstruction route remains gated to 3.14+; these source constructs predate that layout. --- src/decompiler.rs | 52 ++++++-- src/decompiler_stack_tests.rs | 26 ++++ .../cases/b_constructed_rejection_guards.py | 124 ++++++++++++++++++ .../3.14.2/constructed_rejection_guards.py | 54 ++++++++ .../3.14.2/constructed_rejection_guards.pyc | Bin 0 -> 2714 bytes .../3.15.2/constructed_rejection_guards.py | 54 ++++++++ .../3.15.2/constructed_rejection_guards.pyc | Bin 0 -> 2740 bytes tests/data/constructed_rejection_guards.py | 54 ++++++++ tests/integration.rs | 19 +++ .../pyc/constructed_rejection_guards.3.14.pyc | Bin 0 -> 2714 bytes .../pyc/constructed_rejection_guards.3.15.pyc | Bin 0 -> 2740 bytes 11 files changed, 369 insertions(+), 14 deletions(-) create mode 100644 tests/behavior/cases/b_constructed_rejection_guards.py create mode 100644 tests/corpus/3.14.2/constructed_rejection_guards.py create mode 100644 tests/corpus/3.14.2/constructed_rejection_guards.pyc create mode 100644 tests/corpus/3.15.2/constructed_rejection_guards.py create mode 100644 tests/corpus/3.15.2/constructed_rejection_guards.pyc create mode 100644 tests/data/constructed_rejection_guards.py create mode 100644 tests/pyc/constructed_rejection_guards.3.14.pyc create mode 100644 tests/pyc/constructed_rejection_guards.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 0443037c..ef279e61 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -29795,6 +29795,13 @@ return None; }; st.push(Rc::new(e)); } + Op::BUILD_MAP => { + let count = arg.checked_mul(2)?; + if st.len() < count { return None; } + let values = st.split_off(st.len() - count); + st.push(Rc::new(Expr::Dict(values.chunks_exact(2) + .map(|pair| (pair[0].clone(), pair[1].clone())).collect()))); + } Op::RETURN_CONST => { st.push(Rc::new(Expr::Const( self.code.consts.get(arg).cloned().unwrap_or_else(|| Rc::new(PyObject::None)), @@ -30477,23 +30484,17 @@ return None; Some(body_start) } - /// Recover a shared rejection block reached by a None-led OR guard. + /// Recover a shared rejection block reached by short-circuit tests. /// CPython 3.14 puts the last test's success jump over the reject block. /// Treating earlier None jumps as nested positive ifs loses rejection for /// missing values. A negated comparison chain also has a POP_TOP cleanup /// edge; retain its copied middle value to avoid evaluating it twice. - fn try_none_rejection_guard( + fn try_shared_rejection_guard( &self, first: &ExprRef, first_jump_true: bool, target: usize, ) -> Option<(ExprRef, Vec, usize)> { if !self.version.at_least(3, 14) || !self.stack.is_empty() || target <= self.cur_offset { return None; } - if !matches!(&**first, Expr::Compare { operands, ops } - if operands.len() == 2 && ops.len() == 1 - && matches!(ops[0], CmpOp::Is | CmpOp::IsNot) - && matches!(&*operands[1], Expr::Const(value) if matches!(&**value, PyObject::None))) { - return None; - } let padding = |op| matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::EXTENDED_ARG); let skip_padding = |mut index: usize| { while self.instrs.get(index).map_or(false, |item| padding(item.op)) { index += 1; } @@ -30518,14 +30519,37 @@ return None; if !self.blocks.iter().any(|block| matches!(block.kind, BlockType::For | BlockType::While) && (block.start == top || block.cond_end == top)) { return None; } (vec![Stmt::Continue], reject_index + 1) - } else if let Some(value) = constant_at(reject_index) { - let ret = skip_padding(reject_index + 1); + } else { + // Accept only a straight value expression that leaves exactly + // one result before RETURN, without branches or statements. + let mut ret = reject_index; + while let Some(item) = self.instrs.get(ret) { + if item.op == Op::RETURN_VALUE { break; } + if item.is_jump() || matches!(item.op, Op::STORE_FAST | Op::STORE_NAME + | Op::STORE_DEREF | Op::STORE_ATTR | Op::STORE_SUBSCR | Op::POP_EXCEPT + | Op::PUSH_EXC_INFO | Op::RERAISE | Op::RAISE_VARARGS) { return None; } + ret += 1; + } if self.instrs.get(ret)?.op != Op::RETURN_VALUE { return None; } - (vec![Stmt::Return(Some(Rc::new(Expr::Const(value))))], ret + 1) - } else { return None; }; + let mut result = self.sim_stack_region(reject_index, ret, Vec::new())?; + if result.len() != 1 { return None; } + (vec![Stmt::Return(Some(result.pop()?))], ret + 1) + }; let equivalent_return = |index: usize| -> Option { let Some(Stmt::Return(Some(expected))) = body.first() else { return None; }; - let Expr::Const(expected) = &**expected else { return None; }; + let Expr::Const(expected) = &**expected else { + // Short constructed returns can be duplicated too. Compare + // the complete branch-free instruction run, not just its + // first constant or the resulting expression's spelling. + let mut actual = skip_padding(index); + for expected in &self.instrs[reject_index..end_index] { + if padding(expected.op) { continue; } + let item = self.instrs.get(actual)?; + if item.op != expected.op || item.arg != expected.arg { return None; } + actual = skip_padding(actual + 1); + } + return Some(actual); + }; let actual = constant_at(index)?; if !Rc::ptr_eq(expected, &actual) && !const_value_eq(expected, &actual) { return None; } let ret = skip_padding(index + 1); @@ -30628,7 +30652,7 @@ return None; fn handle_cond_jump(&mut self, cond: ExprRef, jump_if_true: bool, target: usize) { // tail-duplicated body copy: aim the jump at the original body let target = self.dup_copy_redirect.get(&target).copied().unwrap_or(target); - if let Some((guard, body, resume)) = self.try_none_rejection_guard(&cond, jump_if_true, target) { + if let Some((guard, body, resume)) = self.try_shared_rejection_guard(&cond, jump_if_true, target) { self.push_stmt(Stmt::If { cond: guard, body, orelse: Vec::new() }); self.skip_until = Some(resume); return; diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index 4041810a..66127d15 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -509,3 +509,29 @@ fn following_exception_clauses_require_bounded_typed_mismatch_protocols() { let ctx = build(vec![instruction(Op::NOP, 0); 32], Op::POP_JUMP_IF_FALSE, true); assert!(ctx.following_except_clause_end(0).is_none()); } + +#[test] +fn bounded_mapping_preserves_pair_order_prefix_and_rejects_underflow() { + let module = fixture(); + let values: Vec<_> = ["prefix", "key0", "value0", "key1", "value1"].iter() + .map(|name| Rc::new(Expr::Name((*name).into()))).collect(); + for minor in 8..=15 { + let version = PythonVersion { minor, ..module.version() }; + let ctx = context(&module.code, version, vec![instruction(Op::BUILD_MAP, 2)]); + let result = ctx.sim_stack_region(0, 1, values.clone()).unwrap(); + assert_eq!(result.len(), 2); + assert!(Rc::ptr_eq(&result[0], &values[0])); + let Expr::Dict(pairs) = &*result[1] else { panic!("expected dictionary"); }; + assert_eq!(pairs.len(), 2); + for (i, (key, value)) in pairs.iter().enumerate() { + assert!(Rc::ptr_eq(key, &values[1 + 2 * i])); + assert!(Rc::ptr_eq(value, &values[2 + 2 * i])); + } + assert!(ctx.sim_stack_region(0, 1, values[..3].to_vec()).is_none()); + let empty = context(&module.code, version, vec![instruction(Op::BUILD_MAP, 0)]); + let result = empty.sim_stack_region(0, 1, values[..1].to_vec()).unwrap(); + assert!(matches!(&*result[1], Expr::Dict(pairs) if pairs.is_empty())); + let oversized = context(&module.code, version, vec![instruction(Op::BUILD_MAP, u32::MAX)]); + assert!(oversized.sim_stack_region(0, 1, values.clone()).is_none()); + } +} diff --git a/tests/behavior/cases/b_constructed_rejection_guards.py b/tests/behavior/cases/b_constructed_rejection_guards.py new file mode 100644 index 00000000..86d4a2c1 --- /dev/null +++ b/tests/behavior/cases/b_constructed_rejection_guards.py @@ -0,0 +1,124 @@ +# MIN_VERSION: 3.14 +def constructed(value, build): + if value is None or not 0.0 <= value <= 20.0: + return build("reject", amount=0) + return build("accept", amount=value) + + +def truth_guard(value, build): + if not value or len(value) < 2: + return build("reject", amount=0) + return build("accept", amount=len(value)) + + +def positive_guard(value, invalid, build): + if invalid(value) or value < 2: + return build("reject", amount=value) + return build("accept", amount=value) + + +def constant_truth(value): + if not value or len(value) < 2: + return False + return True + + +def list_result(value): + if value is None or not 0.0 <= value <= 20.0: + return ["reject", value] + return ["accept", value] + + +def mapping_result(value): + if value is None or not 0.0 <= value <= 20.0: + return {"kind": "reject", "value": value} + return {"kind": "accept", "value": value} + + +def generator_result(value, rows, collect, scale): + if value is None or not 0.0 <= value <= 20.0: + return collect(scale * item for item in rows) + return collect(rows) + + +def stored_result(value, build): + if not value or len(value) < 2: + result = build("reject", amount=0) + return result + return build("accept", amount=len(value)) + + +def effectful_body(value, build, log): + if not value or len(value) < 2: + log("rejected") + return build("reject", amount=0) + return build("accept", amount=len(value)) + +# Run the observation driver unchanged. +exec(""" +trace = [] +fail = None +def build(kind, **options): + trace.append(('build', kind, options)) + if fail == 'build': raise RuntimeError('build') + return kind, options + +def log(message): + trace.append(('log', message)) + if fail == 'log': raise RuntimeError('log') + +def outcome(function, *args): + try: return 'return', function(*args) + except Exception as error: return 'raise', type(error).__name__, str(error) + +class Values: + def __init__(self, count): self.count = count + def __bool__(self): + trace.append('bool') + if fail == 'bool': raise ValueError('bool') + return self.count != 0 + def __len__(self): + trace.append('len') + if fail == 'len': raise ValueError('len') + return self.count + +for function in (constructed, list_result, mapping_result): + for value in (None, -1.0, 0.0, 10.0, 20.0, 21.0, float('nan')): + for fail in (None, 'build'): + trace.clear() + args = (value, build) if function is constructed else (value,) + print(function.__name__, repr(value), fail, outcome(function, *args), trace) +for function in (truth_guard, constant_truth, stored_result, effectful_body): + for count in (0, 1, 2, 3): + for fail in (None, 'bool', 'len', 'build', 'log'): + trace.clear() + args = (Values(count),) + if function is not constant_truth: args += (build,) + if function is effectful_body: args += (log,) + print(function.__name__, count, fail, outcome(function, *args), trace) +for invalid_result in (False, True): + for value in (0, 2, 3): + for fail in (None, 'invalid', 'build'): + trace.clear() + def invalid(value): + trace.append(('invalid', value)) + if fail == 'invalid': raise RuntimeError('invalid') + return invalid_result + print('positive', invalid_result, value, fail, outcome(positive_guard, value, invalid, build), trace) +class Rows: + def __iter__(self): + trace.append('iter') + if fail == 'iter': raise RuntimeError('iter') + for value in (2, 3): + trace.append(('next', value)) + yield value + trace.append('exhausted') +def collect(values): + trace.append('collect') + if fail == 'collect': raise RuntimeError('collect') + return list(values) +for value in (None, -1.0, 0.0, 20.0, 21.0, float('nan')): + for fail in (None, 'iter', 'collect'): + trace.clear() + print('generator', repr(value), fail, outcome(generator_result, value, Rows(), collect, 10), trace) +""") diff --git a/tests/corpus/3.14.2/constructed_rejection_guards.py b/tests/corpus/3.14.2/constructed_rejection_guards.py new file mode 100644 index 00000000..7696b0e5 --- /dev/null +++ b/tests/corpus/3.14.2/constructed_rejection_guards.py @@ -0,0 +1,54 @@ +def constructed(value, build): + if value is None or not 0.0 <= value <= 20.0: + return build("reject", amount=0) + return build("accept", amount=value) + + +def truth_guard(value, build): + if not value or len(value) < 2: + return build("reject", amount=0) + return build("accept", amount=len(value)) + + +def positive_guard(value, invalid, build): + if invalid(value) or value < 2: + return build("reject", amount=value) + return build("accept", amount=value) + + +def constant_truth(value): + if not value or len(value) < 2: + return False + return True + + +def list_result(value): + if value is None or not 0.0 <= value <= 20.0: + return ["reject", value] + return ["accept", value] + + +def mapping_result(value): + if value is None or not 0.0 <= value <= 20.0: + return {"kind": "reject", "value": value} + return {"kind": "accept", "value": value} + + +def generator_result(value, rows, collect, scale): + if value is None or not 0.0 <= value <= 20.0: + return collect(scale * item for item in rows) + return collect(rows) + + +def stored_result(value, build): + if not value or len(value) < 2: + result = build("reject", amount=0) + return result + return build("accept", amount=len(value)) + + +def effectful_body(value, build, log): + if not value or len(value) < 2: + log("rejected") + return build("reject", amount=0) + return build("accept", amount=len(value)) diff --git a/tests/corpus/3.14.2/constructed_rejection_guards.pyc b/tests/corpus/3.14.2/constructed_rejection_guards.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26f1339aa23fb11c0c63f2717e41ed3328de5e1b GIT binary patch literal 2714 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 Date: Thu, 29 Oct 2026 07:52:23 -0400 Subject: [PATCH 21/22] Recover shared validation raises and formatted error paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit def select(value, read, invalid, label, width, make_error, make_cause): if value is None or not 0 <= read(value) <= 10 or invalid(value): raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause(value) return value The shared guard recognizer accepts return and continue outcomes, but a validation raise can still lose its rejection path. A successful chained comparison also jumps over its failed-link cleanup before later predicates; reading that cleanup as part of the success path loses the compound guard. Recognize RAISE_VARARGS with one exception operand or an exception and explicit cause. Require exactly that operand count, and keep bare reraises, intervening statements, cleanup and branches outside this expression route. Skip a comparison cleanup trampoline only after proving its complete shape and its jump back to the same rejection block. Extend bounded expression simulation with conversion, simple/spec formatting and string assembly, retaining exact stack consumption. Use the split formatting protocol from Python 3.13 onward; the shared-guard reconstruction itself targets Python 3.14–3.15. F-string source syntax dates to Python 3.6, explicit causes to 3.0, and from None suppression to 3.3. Cover ordinary raises, keyword constructors, from None, computed causes, !s/!r/!a conversions and dynamic format specifications. Preserve one interior read, short-circuiting, NaN/rich comparisons, formatting and constructor order, existing exception context and suppression metadata. Reject malformed operand counts and incomplete formatting stacks without inventing values. --- src/decompiler.rs | 91 +++++++++- src/decompiler_stack_tests.rs | 96 ++++++++++ .../cases/b_shared_validation_raises.py | 166 ++++++++++++++++++ tests/corpus/3.13.7/bounded_formatting.py | 10 ++ tests/corpus/3.13.7/bounded_formatting.pyc | Bin 0 -> 568 bytes tests/corpus/3.14.2/bounded_formatting.py | 10 ++ tests/corpus/3.14.2/bounded_formatting.pyc | Bin 0 -> 568 bytes .../corpus/3.14.2/shared_validation_raises.py | 46 +++++ .../3.14.2/shared_validation_raises.pyc | Bin 0 -> 2331 bytes tests/corpus/3.15.2/bounded_formatting.py | 10 ++ tests/corpus/3.15.2/bounded_formatting.pyc | Bin 0 -> 575 bytes .../corpus/3.15.2/shared_validation_raises.py | 46 +++++ .../3.15.2/shared_validation_raises.pyc | Bin 0 -> 2348 bytes tests/data/bounded_formatting.py | 10 ++ tests/data/shared_validation_raises.py | 46 +++++ tests/integration.rs | 17 ++ tests/pyc/bounded_formatting.3.13.pyc | Bin 0 -> 568 bytes tests/pyc/bounded_formatting.3.14.pyc | Bin 0 -> 568 bytes tests/pyc/bounded_formatting.3.15.pyc | Bin 0 -> 575 bytes tests/pyc/shared_validation_raises.3.14.pyc | Bin 0 -> 2331 bytes tests/pyc/shared_validation_raises.3.15.pyc | Bin 0 -> 2348 bytes 21 files changed, 542 insertions(+), 6 deletions(-) create mode 100644 tests/behavior/cases/b_shared_validation_raises.py create mode 100644 tests/corpus/3.13.7/bounded_formatting.py create mode 100644 tests/corpus/3.13.7/bounded_formatting.pyc create mode 100644 tests/corpus/3.14.2/bounded_formatting.py create mode 100644 tests/corpus/3.14.2/bounded_formatting.pyc create mode 100644 tests/corpus/3.14.2/shared_validation_raises.py create mode 100644 tests/corpus/3.14.2/shared_validation_raises.pyc create mode 100644 tests/corpus/3.15.2/bounded_formatting.py create mode 100644 tests/corpus/3.15.2/bounded_formatting.pyc create mode 100644 tests/corpus/3.15.2/shared_validation_raises.py create mode 100644 tests/corpus/3.15.2/shared_validation_raises.pyc create mode 100644 tests/data/bounded_formatting.py create mode 100644 tests/data/shared_validation_raises.py create mode 100644 tests/pyc/bounded_formatting.3.13.pyc create mode 100644 tests/pyc/bounded_formatting.3.14.pyc create mode 100644 tests/pyc/bounded_formatting.3.15.pyc create mode 100644 tests/pyc/shared_validation_raises.3.14.pyc create mode 100644 tests/pyc/shared_validation_raises.3.15.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index ef279e61..be8f8683 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -29802,6 +29802,51 @@ return None; st.push(Rc::new(Expr::Dict(values.chunks_exact(2) .map(|pair| (pair[0].clone(), pair[1].clone())).collect()))); } + // Shared validation raises often construct an f-string + // message. Use the main parser's conversion/spec model, + // with exact stack consumption and no invented value ops. + Op::CONVERT_VALUE if self.version.at_least(3, 13) => { + let conversion = match ins.arg { + 1 => 's', 2 => 'r', 3 => 'a', _ => return None, + }; + let value = st.pop()?; + st.push(Rc::new(Expr::FString(Box::new(FString { + parts: vec![FStringPart::Value { value, conversion: Some(conversion), format_spec: None }], + })))); + } + Op::FORMAT_SIMPLE | Op::FORMAT_WITH_SPEC if self.version.at_least(3, 13) => { + let spec = if ins.op == Op::FORMAT_WITH_SPEC { Some(st.pop()?) } else { None }; + let value = st.pop()?; + let (value, conversion) = match &*value { + Expr::FString(text) if text.parts.len() == 1 => match &text.parts[0] { + FStringPart::Value { value: inner, conversion, format_spec: None } => + (inner.clone(), *conversion), + _ => (value, None), + }, + _ => (value, None), + }; + st.push(Rc::new(Expr::FString(Box::new(FString { + parts: vec![FStringPart::Value { + value, conversion, format_spec: spec.map(|value| Box::new(expr_to_fstring(value))), + }], + })))); + } + Op::BUILD_STRING if self.version.at_least(3, 13) => { + if st.len() < arg { return None; } + let values = st.split_off(st.len() - arg); + let mut parts = Vec::new(); + for value in values { + match &*value { + Expr::Const(value) => match &**value { + PyObject::Str(text) => parts.push(FStringPart::Literal(text.clone())), + _ => return None, + }, + Expr::FString(text) => parts.extend(text.parts.iter().cloned()), + _ => return None, + } + } + st.push(Rc::new(Expr::FString(Box::new(FString { parts })))); + } Op::RETURN_CONST => { st.push(Rc::new(Expr::Const( self.code.consts.get(arg).cloned().unwrap_or_else(|| Rc::new(PyObject::None)), @@ -30520,20 +30565,30 @@ return None; && (block.start == top || block.cond_end == top)) { return None; } (vec![Stmt::Continue], reject_index + 1) } else { - // Accept only a straight value expression that leaves exactly - // one result before RETURN, without branches or statements. + // A rejection can return a value or raise an exception with an + // optional explicit cause. Require exactly the terminator's + // operands, without branches, stores or exception cleanup. let mut ret = reject_index; while let Some(item) = self.instrs.get(ret) { - if item.op == Op::RETURN_VALUE { break; } + if item.op == Op::RETURN_VALUE + || (item.op == Op::RAISE_VARARGS && matches!(item.arg, 1 | 2)) { break; } if item.is_jump() || matches!(item.op, Op::STORE_FAST | Op::STORE_NAME | Op::STORE_DEREF | Op::STORE_ATTR | Op::STORE_SUBSCR | Op::POP_EXCEPT | Op::PUSH_EXC_INFO | Op::RERAISE | Op::RAISE_VARARGS) { return None; } ret += 1; } - if self.instrs.get(ret)?.op != Op::RETURN_VALUE { return None; } + let terminal = self.instrs.get(ret)?; let mut result = self.sim_stack_region(reject_index, ret, Vec::new())?; - if result.len() != 1 { return None; } - (vec![Stmt::Return(Some(result.pop()?))], ret + 1) + let statement = match terminal.op { + Op::RETURN_VALUE if result.len() == 1 => Stmt::Return(Some(result.pop()?)), + Op::RAISE_VARARGS if matches!(terminal.arg, 1 | 2) + && result.len() == terminal.arg as usize => { + let cause = if terminal.arg == 2 { Some(result.pop()?) } else { None }; + Stmt::Raise { exc: Some(result.pop()?), cause, py2_inst: None, py2_tb: None } + } + _ => return None, + }; + (vec![statement], ret + 1) }; let equivalent_return = |index: usize| -> Option { let Some(Stmt::Return(Some(expected))) = body.first() else { return None; }; @@ -30640,10 +30695,34 @@ return None; _ => None, } } else { None }; + let chain_merged = merged.is_some(); if let Some(merged) = merged { values.pop(); values.push(merged); } else { values.push(member); } if complete { break; } start = end + 1; + // A successful chained comparison can jump over its failed + // link's POP_TOP/JUMP_FORWARD cleanup into another predicate. + // Skip only that complete trampoline back to this rejection; + // simulating it on the success path consumes the middle value + // again or makes the remaining predicate region unparseable. + if chain_merged && carried.is_empty() { + let success = skip_padding(start); + if let Some(skip) = self.instrs.get(success) { + if skip.op == Op::JUMP_FORWARD { + if let Some(&destination) = skip.target.and_then(|offset| self.idx_of.get(&offset)) { + let cleanup = skip_padding(success + 1); + let jump = skip_padding(cleanup + 1); + if destination < reject_index + && self.instrs.get(cleanup).map_or(false, |item| item.op == Op::POP_TOP) + && self.instrs.get(jump).map_or(false, |item| item.op == Op::JUMP_FORWARD + && item.target.map_or(false, reaches_reject)) + && skip_padding(jump + 1) == destination { + start = destination; + } + } + } + } + } } if !complete || !carried.is_empty() || values.len() < 2 { return None; } Some((Rc::new(Expr::BoolOp { op: BoolOpKind::Or, values }), body, body_start)) diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index 66127d15..7809c6a7 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -535,3 +535,99 @@ fn bounded_mapping_preserves_pair_order_prefix_and_rejects_underflow() { assert!(oversized.sim_stack_region(0, 1, values.clone()).is_none()); } } + +#[test] +fn bounded_formatting_preserves_conversions_and_dynamic_specs() { + for minor in 13..=15 { + let path = format!("{}/tests/pyc/bounded_formatting.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let version = module.version(); + for (name, expected) in [ + ("simple", "return f'{value}'"), + ("conversions", "return f'{label!r}: {value!s:>{width}}'"), + ("ascii_spec", "return f'{label!a}: {value:{width}}'"), + ] { + let code = nested_code(&module.code, name); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let start = instrs.iter().position(|ins| ins.op == Op::RESUME).unwrap() + 1; + let end = instrs.iter().position(|ins| ins.op == Op::RETURN_VALUE).unwrap(); + let ctx = context(&code, version, instrs); + let prefix = Rc::new(Expr::Name("prefix".into())); + let result = ctx.sim_stack_region(start, end, vec![prefix.clone()]).unwrap(); + assert_eq!(result.len(), 2, "3.{minor}: {name}"); + assert!(Rc::ptr_eq(&result[0], &prefix)); + let source = crate::codegen::generate(&[Stmt::Return(Some(result[1].clone()))], version, true); + assert_eq!(source.trim(), expected, "3.{minor}: {name}"); + } + } +} + +#[test] +fn bounded_formatting_rejects_invalid_conversion_parts_and_missing_operands() { + let module = fixture(); + let value = Rc::new(Expr::Name("value".into())); + for minor in 13..=15 { + let version = PythonVersion { minor, ..module.version() }; + for (op, arg, stack) in [ + (Op::CONVERT_VALUE, 1, vec![]), + (Op::CONVERT_VALUE, 0, vec![value.clone()]), + (Op::CONVERT_VALUE, 4, vec![value.clone()]), + (Op::FORMAT_SIMPLE, 0, vec![]), + (Op::FORMAT_WITH_SPEC, 0, vec![]), + (Op::FORMAT_WITH_SPEC, 0, vec![value.clone()]), + (Op::BUILD_STRING, 2, vec![value.clone()]), + (Op::BUILD_STRING, 1, vec![value.clone()]), + (Op::BUILD_STRING, 1, vec![Rc::new(Expr::Const(Rc::new(PyObject::Int(3))))]), + ] { + let ctx = context(&module.code, version, vec![instruction(op, arg)]); + assert!(ctx.sim_stack_region(0, 1, stack).is_none(), "3.{minor}: {op:?} {arg}"); + } + for (arg, conversion) in [(1, 's'), (2, 'r'), (3, 'a')] { + let ctx = context(&module.code, version, vec![instruction(Op::CONVERT_VALUE, arg), instruction(Op::FORMAT_SIMPLE, 0)]); + let result = ctx.sim_stack_region(0, 2, vec![value.clone()]).unwrap(); + assert_eq!(result.len(), 1); + let Expr::FString(text) = &*result[0] else { panic!("expected f-string"); }; + assert!(matches!(&text.parts[..], [FStringPart::Value { conversion: Some(c), format_spec: None, .. }] if *c == conversion)); + } + } +} + +#[test] +fn shared_raise_regions_require_exact_operands_and_no_statements() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/shared_validation_raises.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let version = module.version(); + for name in ["plain", "cause", "suppress"] { + let code = nested_code(&module.code, name); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let first = instrs.iter().position(|item| item.op == Op::POP_JUMP_IF_NONE).unwrap(); + let target = instrs[first].target.unwrap(); + let terminal = instrs.iter().position(|item| item.op == Op::RAISE_VARARGS).unwrap(); + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx.cur_offset = ctx.instrs[first].offset; + let cond = Rc::new(Expr::Compare { + operands: vec![Rc::new(Expr::Name("value".into())), Rc::new(Expr::Const(Rc::new(PyObject::None)))], + ops: vec![CmpOp::Is], + }); + let (_, body, _) = ctx.try_shared_rejection_guard(&cond, true, target).unwrap(); + assert!(matches!(&body[..], [Stmt::Raise { exc: Some(_), cause, .. }] if cause.is_some() == (name != "plain"))); + let original = ctx.instrs[terminal].arg; + for arg in [0, 3, if original == 1 { 2 } else { 1 }] { + ctx.instrs[terminal].arg = arg; + assert!(ctx.try_shared_rejection_guard(&cond, true, target).is_none(), "3.{minor}: {name}, arg {arg}"); + } + ctx.instrs[terminal].arg = original; + let start = ctx.idx_of[&target]; + let original = ctx.instrs[start].op; + for op in [Op::STORE_FAST, Op::POP_EXCEPT, Op::PUSH_EXC_INFO, Op::RERAISE] { + ctx.instrs[start].op = op; + assert!(ctx.try_shared_rejection_guard(&cond, true, target).is_none(), "3.{minor}: {name}, {op:?}"); + } + ctx.instrs[start].op = original; + ctx.stack.push(Sv::E(cond.clone())); + assert!(ctx.try_shared_rejection_guard(&cond, true, target).is_none()); + } + } +} diff --git a/tests/behavior/cases/b_shared_validation_raises.py b/tests/behavior/cases/b_shared_validation_raises.py new file mode 100644 index 00000000..9b34d3c5 --- /dev/null +++ b/tests/behavior/cases/b_shared_validation_raises.py @@ -0,0 +1,166 @@ +# MIN_VERSION: 3.14 +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value + +# Run the observation driver unchanged. +exec(""" +from itertools import product + +def outcome(function, *args): + try: + # Retain an active exception to check implicit context and suppression. + try: + raise LookupError('outer context') + except LookupError: + value = function(*args) + return ('value', 'original object' if value is args[0] else value) + except Exception as error: + cause = error.__cause__ + context = error.__context__ + return (type(error).__name__, str(error), + None if cause is None else (type(cause).__name__, str(cause)), + None if context is None else (type(context).__name__, str(context)), + error.__suppress_context__) + +trace = [] +for value, bad_value, ready_value, failure in product((None, 0, 7), (False, True), (False, True), ('', 'bad', 'ready', 'constructor')): + trace.clear() + def bad(item): + trace.append(('bad', item)) + if failure == 'bad': raise KeyError('bad failed') + return bad_value + def ready(item): + trace.append(('ready', item)) + if failure == 'ready': raise KeyError('ready failed') + return ready_value + def make_error(message, **keywords): + trace.append(('error', message, keywords)) + if failure == 'constructor': raise TypeError('constructor failed') + return ValueError(message) + print('plain', value, bad_value, ready_value, failure, outcome(plain, value, bad, ready, make_error), trace) + +for value, number, invalid_value, failure in product((None, 'value'), (-1, 0, 5, 10, 11, float('nan')), (False, True), ('', 'read', 'ge', 'le', 'invalid')): + trace.clear() + class Number: + def __ge__(self, other): + trace.append(('ge', other)) + if failure == 'ge': raise TypeError('ge failed') + return number >= other + def __le__(self, other): + trace.append(('le', other)) + if failure == 'le': raise TypeError('le failed') + return number <= other + def __lt__(self, other): raise AssertionError('inverted comparison') + def __gt__(self, other): raise AssertionError('inverted comparison') + def read(item): + trace.append(('read', item)) + if failure == 'read': raise TypeError('read failed') + return Number() + def invalid(item): + trace.append(('invalid', item)) + if failure == 'invalid': raise TypeError('invalid failed') + return invalid_value + def make_error(message): + trace.append(('error', message)) + return ValueError(message) + print('chain', value, repr(number), invalid_value, failure, outcome(chained, value, read, invalid, make_error), trace) + +for function, value, rejects, cause_mode in product((cause, suppress, constant_error), (None, 0, 7), (False, True), ('none', 'exception', 'class', 'invalid', 'failure')): + trace.clear() + def reject(item): + trace.append(('reject', item)); return rejects + def make_error(message, **keywords): + trace.append(('error', message, keywords)); return ValueError(message) + def make_cause(item): + trace.append(('cause', item)) + if cause_mode == 'failure': raise KeyError('cause factory failed') + if cause_mode == 'invalid': return 'invalid cause' + if cause_mode == 'class': return RuntimeError + return RuntimeError('original cause') if cause_mode == 'exception' else None + args = (value, reject) + if function is not constant_error: args += (make_error,) + if function is cause: args += (make_cause,) + print(function.__name__, value, rejects, cause_mode, outcome(function, *args), trace) + +for function, number, rejects, failure in product((formatted, ascii_spec, formatted_plain), (None, 5, 11), (False, True), ('', 'repr', 'str', 'format', 'width', 'constructor', 'cause')): + trace.clear() + class Label: + def __repr__(self): + trace.append('repr') + if failure == 'repr': raise TypeError('repr failed') + return 'label-α' + class Value: + def __str__(self): + trace.append('str') + if failure == 'str': raise TypeError('str failed') + return 'value' + def __format__(self, spec): + trace.append(('format', spec)) + if failure == 'format': raise TypeError('format failed') + return 'formatted-' + spec + class Width: + def __format__(self, spec): + trace.append(('width', spec)) + if failure == 'width': raise TypeError('width failed') + return '6' + def read(value): + trace.append('read'); return number + def reject(value): + trace.append('reject'); return rejects + def make_error(message): + trace.append(('error', message)) + if failure == 'constructor': raise ValueError('constructor failed') + return ValueError(message) + def make_cause(): + trace.append('cause') + if failure == 'cause': raise KeyError('cause failed') + return RuntimeError('cause') + value = None if number is None else Value() + args = (value, Label(), Width(), read, reject, make_error) + if function is not formatted_plain: args += (make_cause,) + print(function.__name__, number, rejects, failure, outcome(function, *args), trace) +""") diff --git a/tests/corpus/3.13.7/bounded_formatting.py b/tests/corpus/3.13.7/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/corpus/3.13.7/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/corpus/3.13.7/bounded_formatting.pyc b/tests/corpus/3.13.7/bounded_formatting.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbb8e75963118f5ccf021c90c45f1fc5feb33121 GIT binary patch literal 568 zcmY+AKTE?v7{>4Nr?w4^R&5ax%pllWu}E=|(vJ|3P}HfUNvwgSDR*g$o1<mu5jGsEt%~gsNDdfh6+C^o9;)873F&sE0tKPZ~(n3@^9qm{L-R1gIhc> i+w=o_=s7J?gc|>D#`h&+jOPNz<#`t4(-fr2JN^Q**KN%J literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/bounded_formatting.py b/tests/corpus/3.14.2/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/corpus/3.14.2/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/corpus/3.14.2/bounded_formatting.pyc b/tests/corpus/3.14.2/bounded_formatting.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dee5da5a78b54fadf18c2587156df221296dabf GIT binary patch literal 568 zcmY+AyG{Z@6ozN^B6koKr7)4i1i2VVNNkAV5h%8RSdnFS)lC+b%%V&JgtQo)Gfr(ke+OG*QF zm$s-zyWpErhBOgc`jJ|th_)HV7jmRJ`oMCS0=r0^+NSrw@uplZGi(~A40klK7`NRK zXIVJGQ+@_ABf#@~`!bjp!eTAR2iL(xkN_K|>Y-9+G05yd;tTYM#$blnrz@G zOTd*tV<~XEC`p|gw%zAr%V7I(i&=v~G@U>-WWP hhOX&5R-5JFOnf%teaV#4r9|oRGEV8`H%L`<`~@wuYYhMZ literal 0 HcmV?d00001 diff --git a/tests/corpus/3.14.2/shared_validation_raises.py b/tests/corpus/3.14.2/shared_validation_raises.py new file mode 100644 index 00000000..eb634f66 --- /dev/null +++ b/tests/corpus/3.14.2/shared_validation_raises.py @@ -0,0 +1,46 @@ +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value diff --git a/tests/corpus/3.14.2/shared_validation_raises.pyc b/tests/corpus/3.14.2/shared_validation_raises.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44d770437ea4b4ebb9c9c213dd4f395fdaa4844e GIT binary patch literal 2331 zcmb_dOK%iM5bl}T*&PqNLm(LJunAbk#w#917Ba9wh(vNwknv%p3=$$)H9KwZIQyb| z7DS|=6^e4o4=`MCl#8897A}zFk|??4Hj<-+jB?8#h%l#|Qq{Aw4_F%xv^1rfepGjT z^;OjoD`RSekRLw&^~;ODoxS}J=$)aowar;jn#3mih)-+E9jw_UK>M$ja#2#P8OLz&P<_#~aQ0P1sEO(82p9I?K`ypaW23 z$tTa@(@@`4Pv;474Y_JceK+ct)C1&_{ z{V`Yx#EIk6I9MPZkhlqdVX9aH1mFn7illIT8blj|mG{2hv&z2N|3`MFrT&?nd9d`D zJ*qyeA{Os#SW|&9C6VB1IMatjhbQyW8XysIVIWUj={tJe&jknA(PdRhMP_isx6i*M zXUnYUOXu2bDAEQ57+pp-M2_lmlD`R)8TiB$h&a^AmB#(dy-YX650-x#-k7}*7#Di{ z!<*yL@$Wp$G~t!o`Umhpj=bQXO$o;#Z4DMY{l-y#~(eaJgguM=I)+ZeSftPOrBo5-WqPD);v&y(K8!X z{%7--bYRSH0V`&ng!Kz~QBMhY9kA~0fmK6TF9V?4!wJQKkHA|bm>l9V04S4+qURMv zofY{3*nyuo58~C4TD!bj3r1%*tW)1#`SEICeAt5zViJ!I?V2$2+G)3^2cK$Ikj7J1YQ=Wk5#Nyg-@$~)Lr=@SUUorKoUJy zbN;qQ0%N2b^M#xtyQ*v}vgpc^CG${Hj3ZvsWJFBtAW_yDXDc1 U6h(QeYRays>FvZbT$K&!e>8cm=Kufz literal 0 HcmV?d00001 diff --git a/tests/corpus/3.15.2/bounded_formatting.py b/tests/corpus/3.15.2/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/corpus/3.15.2/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/corpus/3.15.2/bounded_formatting.pyc b/tests/corpus/3.15.2/bounded_formatting.pyc new file mode 100644 index 0000000000000000000000000000000000000000..215391f201eff9591fda1cbe9fc1baaa7e4f5a66 GIT binary patch literal 575 zcmY+Ay-or_6ou#R0`iZ45M!Zefgna`Oo%Z&f)NumR%F>-b(4i9GYiBHJMAniC~0{H zUt&wMFJNUMSXp`R07fRcCue8Rz4OgnCKFMWG3LI$H9q&@Lr4F&VD6^dXa-m{mS%$0 z02OKqs&cWi#|o;xNKylN8aa)k46y1f&M*b%V-!Ew)9kJ(D3$ZGt|?qmGBj5!HJxtT zFpb8o!&{o`TJ5{z&VwHkR;y#1JcxPf5j6FYy(c%GuDw8dieF5p;}(m8^7tJz1FSl$ zC~-uuLj`J3M_*PFtgIrE@}rz)eW-E;b7R#H+FH}JIko%2zGb-gf_yHQ;{n=>=}x<6 za$z}b!4tSchlyPjN%Va4(j2C}t;5mU=wx&-ilFskMK4g~A{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value diff --git a/tests/corpus/3.15.2/shared_validation_raises.pyc b/tests/corpus/3.15.2/shared_validation_raises.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f43b00a385effd4251ce4383724e68d2be48361f GIT binary patch literal 2348 zcmb_d&u<%55PrMs^=@5z+d>0Lnl`B*Ar^&1kqAkIDyS4Hswk?FTqG#6w!52Ti({Mj z)3|6@LI%8cv+}=B?L1nxx60Pnz-g&AuP= z&G%;Bw{jVcF?QkF_g~Ndc;{~ic6%r-YkLMp15#jpOt69itrS#fwV*+3g%otEphN40 zG<3RPKpPj0(=2Z~sn$O9qeJMx?aNRNNRhcG85?9pWf=BU>&#JjXe=F(K6O{$Vd>L` zaibzDsBft8nq~Yu%{H~g$nY!I?6U9Xl}LwC_cmER#pybccExrhoqM)>J<6=wpLivY z^C}nQ9K(pl0#5{D!nFf?Laf-_b4zkBJMgQO61RQfiPvk_BfVC({R+=u9o=2zp}u5~ zjO>jsZfa}uw==ggo1;zrhcn-u`P1rO8`v2A&6*6&$w!BeHWs$=G3&vH&X@RvKQ2oY{4q-ikWk$gIR)gj8|S={5;F;%Ca}IWcy{$ z<&>%ft`s4~>0%t`A=yv_6UTlmGMp7)fF&0G8RVaUYN4?9&Yk1y-nS=yADU{ae+*6C zU4G!*uiUE;8mG3b@z5NXSa77X7mGGm7U5|?fJMxQUAf}O=qc`AJ`}-_4zre3Vh1O# z^hB8E-c`>DWOyw`ICdRL?hRF!lN?8}97K#mCFxGD)o*0`EIbnDUBVR_%Zwy6cf=KBWC!a zr7B;wg8&>Kf^UF6x&XBo$Cn-y@2}olB`VC{oLxV+UJu7+Ha>0kHH{4)W;igrWzGGP z`86Aw3wr>Y#{Lr6uV6GF&luPjx`5RP*h_%)>42h0@E36t2`85*?0_m`3&;0MqUJgL z6uupXh_|5*j@8Bo>(y{zddqtA;fFt64$b$v5F%8P(Wc%F%rqd+7$H0kG^PVkFD{`R zh_;GKI(~lOF5@Ri^|DM*6y5wf(&kqw?k6MofS!PO7wX{1?Z&d+r`u|h*fnOCWLVPR z_bETXdysu3my&8V4rLMjHFJsj>pb<)yxOTff{sfbk>yk?BCsn#+-o_NoL3=&V0xnV z{AKlrW`8H|c~iDn*{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/data/shared_validation_raises.py b/tests/data/shared_validation_raises.py new file mode 100644 index 00000000..eb634f66 --- /dev/null +++ b/tests/data/shared_validation_raises.py @@ -0,0 +1,46 @@ +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value diff --git a/tests/integration.rs b/tests/integration.rs index 6cc9495f..c4f15e3a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1263,3 +1263,20 @@ fn constructed_rejections_keep_selected_values_and_statement_boundaries() { ], version); } } + +#[test] +fn shared_validation_raises_keep_causes_and_formatted_error_paths() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("shared_validation_raises.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "if value is None or bad(value) or not ready(value):", + "if value is None or not 0 <= read(value) <= 10 or invalid(value):", + "raise make_error('invalid', value=value) from make_cause(value)", + "raise make_error('invalid', value=value) from None", + "raise make_error(f'{label!r}: {value!s:>{width}}') from make_cause()", + "raise make_error(f'{label!a}: {value:{width}}') from make_cause()", + "if value is None or reject(value):\n raise ValueError", + ], version); + } +} diff --git a/tests/pyc/bounded_formatting.3.13.pyc b/tests/pyc/bounded_formatting.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbb8e75963118f5ccf021c90c45f1fc5feb33121 GIT binary patch literal 568 zcmY+AKTE?v7{>4Nr?w4^R&5ax%pllWu}E=|(vJ|3P}HfUNvwgSDR*g$o1<mu5jGsEt%~gsNDdfh6+C^o9;)873F&sE0tKPZ~(n3@^9qm{L-R1gIhc> i+w=o_=s7J?gc|>D#`h&+jOPNz<#`t4(-fr2JN^Q**KN%J literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_formatting.3.14.pyc b/tests/pyc/bounded_formatting.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dee5da5a78b54fadf18c2587156df221296dabf GIT binary patch literal 568 zcmY+AyG{Z@6ozN^B6koKr7)4i1i2VVNNkAV5h%8RSdnFS)lC+b%%V&JgtQo)Gfr(ke+OG*QF zm$s-zyWpErhBOgc`jJ|th_)HV7jmRJ`oMCS0=r0^+NSrw@uplZGi(~A40klK7`NRK zXIVJGQ+@_ABf#@~`!bjp!eTAR2iL(xkN_K|>Y-9+G05yd;tTYM#$blnrz@G zOTd*tV<~XEC`p|gw%zAr%V7I(i&=v~G@U>-WWP hhOX&5R-5JFOnf%teaV#4r9|oRGEV8`H%L`<`~@wuYYhMZ literal 0 HcmV?d00001 diff --git a/tests/pyc/bounded_formatting.3.15.pyc b/tests/pyc/bounded_formatting.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..215391f201eff9591fda1cbe9fc1baaa7e4f5a66 GIT binary patch literal 575 zcmY+Ay-or_6ou#R0`iZ45M!Zefgna`Oo%Z&f)NumR%F>-b(4i9GYiBHJMAniC~0{H zUt&wMFJNUMSXp`R07fRcCue8Rz4OgnCKFMWG3LI$H9q&@Lr4F&VD6^dXa-m{mS%$0 z02OKqs&cWi#|o;xNKylN8aa)k46y1f&M*b%V-!Ew)9kJ(D3$ZGt|?qmGBj5!HJxtT zFpb8o!&{o`TJ5{z&VwHkR;y#1JcxPf5j6FYy(c%GuDw8dieF5p;}(m8^7tJz1FSl$ zC~-uuLj`J3M_*PFtgIrE@}rz)eW-E;b7R#H+FH}JIko%2zGb-gf_yHQ;{n=>=}x<6 za$z}b!4tSchlyPjN%Va4(j2C}t;5mU=wx&-ilFskMK4g~AE9jw_UK>M$ja#2#P8OLz&P<_#~aQ0P1sEO(82p9I?K`ypaW23 z$tTa@(@@`4Pv;474Y_JceK+ct)C1&_{ z{V`Yx#EIk6I9MPZkhlqdVX9aH1mFn7illIT8blj|mG{2hv&z2N|3`MFrT&?nd9d`D zJ*qyeA{Os#SW|&9C6VB1IMatjhbQyW8XysIVIWUj={tJe&jknA(PdRhMP_isx6i*M zXUnYUOXu2bDAEQ57+pp-M2_lmlD`R)8TiB$h&a^AmB#(dy-YX650-x#-k7}*7#Di{ z!<*yL@$Wp$G~t!o`Umhpj=bQXO$o;#Z4DMY{l-y#~(eaJgguM=I)+ZeSftPOrBo5-WqPD);v&y(K8!X z{%7--bYRSH0V`&ng!Kz~QBMhY9kA~0fmK6TF9V?4!wJQKkHA|bm>l9V04S4+qURMv zofY{3*nyuo58~C4TD!bj3r1%*tW)1#`SEICeAt5zViJ!I?V2$2+G)3^2cK$Ikj7J1YQ=Wk5#Nyg-@$~)Lr=@SUUorKoUJy zbN;qQ0%N2b^M#xtyQ*v}vgpc^CG${Hj3ZvsWJFBtAW_yDXDc1 U6h(QeYRays>FvZbT$K&!e>8cm=Kufz literal 0 HcmV?d00001 diff --git a/tests/pyc/shared_validation_raises.3.15.pyc b/tests/pyc/shared_validation_raises.3.15.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f43b00a385effd4251ce4383724e68d2be48361f GIT binary patch literal 2348 zcmb_d&u<%55PrMs^=@5z+d>0Lnl`B*Ar^&1kqAkIDyS4Hswk?FTqG#6w!52Ti({Mj z)3|6@LI%8cv+}=B?L1nxx60Pnz-g&AuP= z&G%;Bw{jVcF?QkF_g~Ndc;{~ic6%r-YkLMp15#jpOt69itrS#fwV*+3g%otEphN40 zG<3RPKpPj0(=2Z~sn$O9qeJMx?aNRNNRhcG85?9pWf=BU>&#JjXe=F(K6O{$Vd>L` zaibzDsBft8nq~Yu%{H~g$nY!I?6U9Xl}LwC_cmER#pybccExrhoqM)>J<6=wpLivY z^C}nQ9K(pl0#5{D!nFf?Laf-_b4zkBJMgQO61RQfiPvk_BfVC({R+=u9o=2zp}u5~ zjO>jsZfa}uw==ggo1;zrhcn-u`P1rO8`v2A&6*6&$w!BeHWs$=G3&vH&X@RvKQ2oY{4q-ikWk$gIR)gj8|S={5;F;%Ca}IWcy{$ z<&>%ft`s4~>0%t`A=yv_6UTlmGMp7)fF&0G8RVaUYN4?9&Yk1y-nS=yADU{ae+*6C zU4G!*uiUE;8mG3b@z5NXSa77X7mGGm7U5|?fJMxQUAf}O=qc`AJ`}-_4zre3Vh1O# z^hB8E-c`>DWOyw`ICdRL?hRF!lN?8}97K#mCFxGD)o*0`EIbnDUBVR_%Zwy6cf=KBWC!a zr7B;wg8&>Kf^UF6x&XBo$Cn-y@2}olB`VC{oLxV+UJu7+Ha>0kHH{4)W;igrWzGGP z`86Aw3wr>Y#{Lr6uV6GF&luPjx`5RP*h_%)>42h0@E36t2`85*?0_m`3&;0MqUJgL z6uupXh_|5*j@8Bo>(y{zddqtA;fFt64$b$v5F%8P(Wc%F%rqd+7$H0kG^PVkFD{`R zh_;GKI(~lOF5@Ri^|DM*6y5wf(&kqw?k6MofS!PO7wX{1?Z&d+r`u|h*fnOCWLVPR z_bETXdysu3my&8V4rLMjHFJsj>pb<)yxOTff{sfbk>yk?BCsn#+-o_NoL3=&V0xnV z{AKlrW`8H|c~iDn* Date: Thu, 29 Oct 2026 07:56:19 -0400 Subject: [PATCH 22/22] Normalize regression fixture endings and refresh source hashes Remove redundant blank lines at the ends of the newly added fixture sources and their corpus copies. Regenerate the checked-hash pyc headers with each fixture's original Python runtime so they still identify the corresponding source exactly. All 31 regenerated fixture pairs retain byte-for-byte identical marshal payloads after the 16-byte pyc header. This changes source-file formatting and source hashes without changing the compiled examples or decompiler. --- .../b_generator_statement_reconstruction.py | 1 - tests/corpus/3.10.19/loop_context_returns.py | 2 -- tests/corpus/3.10.19/loop_context_returns.pyc | Bin 1526 -> 1526 bytes tests/corpus/3.11.14/loop_context_returns.py | 2 -- tests/corpus/3.11.14/loop_context_returns.pyc | Bin 3836 -> 3836 bytes .../corpus/3.11.14/nested_context_returns.py | 2 -- .../corpus/3.11.14/nested_context_returns.pyc | Bin 6726 -> 6726 bytes .../generator_statement_reconstruction.py | 1 - .../generator_statement_reconstruction.pyc | Bin 2189 -> 2189 bytes .../corpus/3.12.3/handler_iterator_breaks.py | 2 -- .../corpus/3.12.3/handler_iterator_breaks.pyc | Bin 8276 -> 8276 bytes tests/corpus/3.12.3/loop_context_returns.py | 2 -- tests/corpus/3.12.3/loop_context_returns.pyc | Bin 3041 -> 3041 bytes tests/corpus/3.12.3/loop_handler_ownership.py | 2 -- .../corpus/3.12.3/loop_handler_ownership.pyc | Bin 5811 -> 5811 bytes tests/corpus/3.12.3/nested_context_returns.py | 2 -- .../corpus/3.12.3/nested_context_returns.pyc | Bin 4814 -> 4814 bytes .../corpus/3.12.3/private_handler_clauses.py | 2 -- .../corpus/3.12.3/private_handler_clauses.pyc | Bin 10121 -> 10121 bytes .../generator_statement_reconstruction.py | 1 - .../generator_statement_reconstruction.pyc | Bin 2215 -> 2215 bytes .../corpus/3.13.7/handler_iterator_breaks.py | 2 -- .../corpus/3.13.7/handler_iterator_breaks.pyc | Bin 8508 -> 8508 bytes tests/corpus/3.13.7/loop_context_returns.py | 2 -- tests/corpus/3.13.7/loop_context_returns.pyc | Bin 3025 -> 3025 bytes tests/corpus/3.13.7/loop_handler_ownership.py | 2 -- .../corpus/3.13.7/loop_handler_ownership.pyc | Bin 5856 -> 5856 bytes tests/corpus/3.13.7/nested_context_returns.py | 2 -- .../corpus/3.13.7/nested_context_returns.pyc | Bin 4619 -> 4619 bytes .../corpus/3.13.7/private_handler_clauses.py | 2 -- .../corpus/3.13.7/private_handler_clauses.pyc | Bin 10387 -> 10387 bytes .../generator_statement_reconstruction.py | 1 - .../generator_statement_reconstruction.pyc | Bin 2369 -> 2369 bytes .../corpus/3.14.2/handler_iterator_breaks.py | 2 -- .../corpus/3.14.2/handler_iterator_breaks.pyc | Bin 8804 -> 8804 bytes tests/corpus/3.14.2/loop_context_returns.py | 2 -- tests/corpus/3.14.2/loop_context_returns.pyc | Bin 3230 -> 3230 bytes tests/corpus/3.14.2/loop_handler_ownership.py | 2 -- .../corpus/3.14.2/loop_handler_ownership.pyc | Bin 5938 -> 5938 bytes tests/corpus/3.14.2/nested_context_returns.py | 2 -- .../corpus/3.14.2/nested_context_returns.pyc | Bin 5273 -> 5273 bytes tests/corpus/3.14.2/normal_async_exits.py | 2 -- tests/corpus/3.14.2/normal_async_exits.pyc | Bin 10314 -> 10314 bytes .../corpus/3.14.2/private_handler_clauses.py | 2 -- .../corpus/3.14.2/private_handler_clauses.pyc | Bin 10712 -> 10712 bytes tests/corpus/3.14.2/sunk_context_exits.py | 2 -- tests/corpus/3.14.2/sunk_context_exits.pyc | Bin 3158 -> 3158 bytes .../generator_statement_reconstruction.py | 1 - .../generator_statement_reconstruction.pyc | Bin 2439 -> 2439 bytes .../corpus/3.15.2/handler_iterator_breaks.py | 2 -- .../corpus/3.15.2/handler_iterator_breaks.pyc | Bin 8831 -> 8831 bytes tests/corpus/3.15.2/loop_context_returns.py | 2 -- tests/corpus/3.15.2/loop_context_returns.pyc | Bin 3273 -> 3273 bytes tests/corpus/3.15.2/loop_handler_ownership.py | 2 -- .../corpus/3.15.2/loop_handler_ownership.pyc | Bin 5979 -> 5979 bytes tests/corpus/3.15.2/nested_context_returns.py | 2 -- .../corpus/3.15.2/nested_context_returns.pyc | Bin 5414 -> 5414 bytes tests/corpus/3.15.2/normal_async_exits.py | 2 -- tests/corpus/3.15.2/normal_async_exits.pyc | Bin 10660 -> 10660 bytes .../corpus/3.15.2/private_handler_clauses.py | 2 -- .../corpus/3.15.2/private_handler_clauses.pyc | Bin 10736 -> 10736 bytes tests/corpus/3.15.2/sunk_context_exits.py | 2 -- tests/corpus/3.15.2/sunk_context_exits.pyc | Bin 3232 -> 3232 bytes .../generator_statement_reconstruction.py | 1 - tests/data/handler_iterator_breaks.py | 2 -- tests/data/loop_context_returns.py | 2 -- tests/data/loop_handler_ownership.py | 2 -- tests/data/nested_context_returns.py | 2 -- tests/data/normal_async_exits.py | 2 -- tests/data/private_handler_clauses.py | 2 -- tests/data/sunk_context_exits.py | 2 -- ...enerator_statement_reconstruction.3.12.pyc | Bin 2189 -> 2189 bytes ...enerator_statement_reconstruction.3.13.pyc | Bin 2215 -> 2215 bytes ...enerator_statement_reconstruction.3.14.pyc | Bin 2369 -> 2369 bytes ...enerator_statement_reconstruction.3.15.pyc | Bin 2439 -> 2439 bytes tests/pyc/handler_iterator_breaks.3.12.pyc | Bin 8276 -> 8276 bytes tests/pyc/handler_iterator_breaks.3.13.pyc | Bin 8508 -> 8508 bytes tests/pyc/handler_iterator_breaks.3.14.pyc | Bin 8804 -> 8804 bytes tests/pyc/handler_iterator_breaks.3.15.pyc | Bin 8831 -> 8831 bytes tests/pyc/loop_context_returns.3.10.pyc | Bin 1526 -> 1526 bytes tests/pyc/loop_context_returns.3.11.pyc | Bin 3836 -> 3836 bytes tests/pyc/loop_context_returns.3.12.pyc | Bin 3041 -> 3041 bytes tests/pyc/loop_context_returns.3.13.pyc | Bin 3025 -> 3025 bytes tests/pyc/loop_context_returns.3.14.pyc | Bin 3230 -> 3230 bytes tests/pyc/loop_context_returns.3.15.pyc | Bin 3273 -> 3273 bytes tests/pyc/loop_handler_ownership.3.12.pyc | Bin 5811 -> 5811 bytes tests/pyc/loop_handler_ownership.3.13.pyc | Bin 5856 -> 5856 bytes tests/pyc/loop_handler_ownership.3.14.pyc | Bin 5938 -> 5938 bytes tests/pyc/loop_handler_ownership.3.15.pyc | Bin 5979 -> 5979 bytes tests/pyc/nested_context_returns.3.11.pyc | Bin 6726 -> 6726 bytes tests/pyc/nested_context_returns.3.12.pyc | Bin 4814 -> 4814 bytes tests/pyc/nested_context_returns.3.13.pyc | Bin 4619 -> 4619 bytes tests/pyc/nested_context_returns.3.14.pyc | Bin 5273 -> 5273 bytes tests/pyc/nested_context_returns.3.15.pyc | Bin 5414 -> 5414 bytes tests/pyc/normal_async_exits.3.14.pyc | Bin 10314 -> 10314 bytes tests/pyc/normal_async_exits.3.15.pyc | Bin 10660 -> 10660 bytes tests/pyc/private_handler_clauses.3.12.pyc | Bin 10121 -> 10121 bytes tests/pyc/private_handler_clauses.3.13.pyc | Bin 10387 -> 10387 bytes tests/pyc/private_handler_clauses.3.14.pyc | Bin 10712 -> 10712 bytes tests/pyc/private_handler_clauses.3.15.pyc | Bin 10736 -> 10736 bytes tests/pyc/sunk_context_exits.3.14.pyc | Bin 3158 -> 3158 bytes tests/pyc/sunk_context_exits.3.15.pyc | Bin 3232 -> 3232 bytes 102 files changed, 74 deletions(-) diff --git a/tests/behavior/cases/b_generator_statement_reconstruction.py b/tests/behavior/cases/b_generator_statement_reconstruction.py index 7f9e10f9..2145bac7 100644 --- a/tests/behavior/cases/b_generator_statement_reconstruction.py +++ b/tests/behavior/cases/b_generator_statement_reconstruction.py @@ -39,4 +39,3 @@ def observed_rows(): print(repeated_targets(observed_rows())) - diff --git a/tests/corpus/3.10.19/loop_context_returns.py b/tests/corpus/3.10.19/loop_context_returns.py index cd32d3a7..703d904c 100644 --- a/tests/corpus/3.10.19/loop_context_returns.py +++ b/tests/corpus/3.10.19/loop_context_returns.py @@ -62,5 +62,3 @@ async def async_mapping(lock, rows, parse): except ValueError: continue return {"rows": result} - - diff --git a/tests/corpus/3.10.19/loop_context_returns.pyc b/tests/corpus/3.10.19/loop_context_returns.pyc index 5396b517187549bb5609c872e7805e368df3dad0..a7fcf9acb4686682970d191b330d98a3a57b0096 100644 GIT binary patch delta 25 hcmeyy{f%27pO=@5nSp_U{nV-4NjzHp8wH-R0svnX2ebeH delta 25 gcmeyy{f%27pO=@5nSp^}OOKGatUR0CMuBIn08}9c9smFU diff --git a/tests/corpus/3.11.14/loop_context_returns.py b/tests/corpus/3.11.14/loop_context_returns.py index cd32d3a7..703d904c 100644 --- a/tests/corpus/3.11.14/loop_context_returns.py +++ b/tests/corpus/3.11.14/loop_context_returns.py @@ -62,5 +62,3 @@ async def async_mapping(lock, rows, parse): except ValueError: continue return {"rows": result} - - diff --git a/tests/corpus/3.11.14/loop_context_returns.pyc b/tests/corpus/3.11.14/loop_context_returns.pyc index 787249b46c9045c833714af0a5016cb5454020f7..727ceda6eb04bcd6d4cb4e53b210bff1ad26e47e 100644 GIT binary patch delta 25 hcmew(`$tw_IWI34GXn#|G3Mkjk-Yf#8wK9*0RUxG2p#|c delta 25 hcmew(`$tw_IWI34GXn#I{Hn~iOaA=hI^&CAQh%)r1P{^GIRY4>%J8wF}P09b|xo&W#< delta 25 hcmeAb>=hI^&CAQh%)r3#Hgu_r)z6<7HVV{o003W-2&@1A diff --git a/tests/corpus/3.12.3/handler_iterator_breaks.py b/tests/corpus/3.12.3/handler_iterator_breaks.py index 38105d90..97ae7406 100644 --- a/tests/corpus/3.12.3/handler_iterator_breaks.py +++ b/tests/corpus/3.12.3/handler_iterator_breaks.py @@ -138,5 +138,3 @@ async def direct_unnamed_break(owner): break await owner.log("finished", accepted) return accepted - - diff --git a/tests/corpus/3.12.3/handler_iterator_breaks.pyc b/tests/corpus/3.12.3/handler_iterator_breaks.pyc index e133c8b773ec0d3de55647b169745ecaff1cd6e9..43585ae57c6a541d76f282c062c2b4b0a5e3ae88 100644 GIT binary patch delta 25 hcmccOaK%C3G%qg~GXn#|(FLr(Z=eZxql`1ORP72s;1( diff --git a/tests/corpus/3.13.7/loop_context_returns.py b/tests/corpus/3.13.7/loop_context_returns.py index cd32d3a7..703d904c 100644 --- a/tests/corpus/3.13.7/loop_context_returns.py +++ b/tests/corpus/3.13.7/loop_context_returns.py @@ -62,5 +62,3 @@ async def async_mapping(lock, rows, parse): except ValueError: continue return {"rows": result} - - diff --git a/tests/corpus/3.13.7/loop_context_returns.pyc b/tests/corpus/3.13.7/loop_context_returns.pyc index 09f6bd722f912abc24812d974a3c113e2bccad70..e581bea93ef61a6c263ffc8c3cf2dcb423ff3ee8 100644 GIT binary patch delta 25 hcmca8eoobWlh@n~#@^nSp`f_@#f-JA0UYHwqYX0svj22bcf= delta 25 gcmX>obWlh@n~#@^nSp_!r9ZjKC{f*gqkthN08L*7_5c6? diff --git a/tests/corpus/3.14.2/handler_iterator_breaks.py b/tests/corpus/3.14.2/handler_iterator_breaks.py index 38105d90..97ae7406 100644 --- a/tests/corpus/3.14.2/handler_iterator_breaks.py +++ b/tests/corpus/3.14.2/handler_iterator_breaks.py @@ -138,5 +138,3 @@ async def direct_unnamed_break(owner): break await owner.log("finished", accepted) return accepted - - diff --git a/tests/corpus/3.14.2/handler_iterator_breaks.pyc b/tests/corpus/3.14.2/handler_iterator_breaks.pyc index d10894d2447b965f56e07847c7e98126f4f6fe4d..e0c5484d7ca3255c72716cb2d65a6e60f6e63f8e 100644 GIT binary patch delta 25 hcmaFj^29|zn~#@^nSp_!`_Z?`4S(LoZxjeo0swR^3BLdU delta 25 gcmaFj^29|zn~#@^nSp`9?1q0cgM)X*Mu89|09#ZBz5oCK diff --git a/tests/corpus/3.14.2/loop_context_returns.py b/tests/corpus/3.14.2/loop_context_returns.py index cd32d3a7..703d904c 100644 --- a/tests/corpus/3.14.2/loop_context_returns.py +++ b/tests/corpus/3.14.2/loop_context_returns.py @@ -62,5 +62,3 @@ async def async_mapping(lock, rows, parse): except ValueError: continue return {"rows": result} - - diff --git a/tests/corpus/3.14.2/loop_context_returns.pyc b/tests/corpus/3.14.2/loop_context_returns.pyc index 6eeb06286b397be969f35d90880ae8f7fdff218e..8b24a6bc5276b9737f1bf6ae26a37d1c71378493 100644 GIT binary patch delta 25 gcmbOyIZskRn~#@^nSp`fx8I6J6^Z|88wL7!08zvTKmY&$ delta 25 gcmbOyIZskRn~#@^nSp`fW12FP+ZtVgjRJi<07oqa*Z=?k diff --git a/tests/corpus/3.14.2/loop_handler_ownership.py b/tests/corpus/3.14.2/loop_handler_ownership.py index c7d86ef8..c69d4d26 100644 --- a/tests/corpus/3.14.2/loop_handler_ownership.py +++ b/tests/corpus/3.14.2/loop_handler_ownership.py @@ -117,5 +117,3 @@ def nonloop(owner): except RuntimeError: owner.fallback() return None - - diff --git a/tests/corpus/3.14.2/loop_handler_ownership.pyc b/tests/corpus/3.14.2/loop_handler_ownership.pyc index c6820e6e51e44536ed7875f928c7d498c99f39d8..c40f2b9dfaa7b68f930516845d880fe81bc511b3 100644 GIT binary patch delta 25 hcmdm_w@FVxn~#@^nSp^}$*;Q~Y+0JZHwq|;0{~vh2crN0 delta 25 hcmdm_w@FVxn~#@^nSp^J=i{83#qP7!Hwq|;0{~gm2R{G+ diff --git a/tests/corpus/3.14.2/nested_context_returns.py b/tests/corpus/3.14.2/nested_context_returns.py index f3ba23ec..5040cd90 100644 --- a/tests/corpus/3.14.2/nested_context_returns.py +++ b/tests/corpus/3.14.2/nested_context_returns.py @@ -47,5 +47,3 @@ async def async_outer_return(contexts, value): async with contexts[1]: result = await value() return result - - diff --git a/tests/corpus/3.14.2/nested_context_returns.pyc b/tests/corpus/3.14.2/nested_context_returns.pyc index 32ab8fa60147550f6d35e28c08feaa57817614b8..97baf7455348f846b0c01cb699c43c0ec1c182ce 100644 GIT binary patch delta 25 gcmbQKIa5h($ delta 25 hcmbQKIa5Va4JARn~#@^nSp`9t6}!CHw!p7Z4|K5003jJ2n_%L delta 25 hcmX>Va4JARn~#@^nSp`fv{<>xztwkNY!tB4003sD2( delta 25 hcmcZ+d?Q#un~#@^nSp_!WUX=AjFj~NTqd++)080D@O#lD@ delta 25 hcmZn{ZWk5^;^XCFW?*30G-uhg31`f1Y!oQx1OQc{2lM~{ diff --git a/tests/corpus/3.15.2/handler_iterator_breaks.py b/tests/corpus/3.15.2/handler_iterator_breaks.py index 38105d90..97ae7406 100644 --- a/tests/corpus/3.15.2/handler_iterator_breaks.py +++ b/tests/corpus/3.15.2/handler_iterator_breaks.py @@ -138,5 +138,3 @@ async def direct_unnamed_break(owner): break await owner.log("finished", accepted) return accepted - - diff --git a/tests/corpus/3.15.2/handler_iterator_breaks.pyc b/tests/corpus/3.15.2/handler_iterator_breaks.pyc index 39790a6e79656b734a98417a5914e79e46a1c943..cf260ed11022123a082fcc3764742f0199d44da3 100644 GIT binary patch delta 25 gcmezG^4~=uh>w?xnSp^}W%KFYO5ZiE8wK)}0B}zTo&W#< delta 25 hcmezG^4~=uh>w?xnSp`fzxJFfZyrCE*(i{&1ORzw3CjQg diff --git a/tests/corpus/3.15.2/loop_context_returns.py b/tests/corpus/3.15.2/loop_context_returns.py index cd32d3a7..703d904c 100644 --- a/tests/corpus/3.15.2/loop_context_returns.py +++ b/tests/corpus/3.15.2/loop_context_returns.py @@ -62,5 +62,3 @@ async def async_mapping(lock, rows, parse): except ValueError: continue return {"rows": result} - - diff --git a/tests/corpus/3.15.2/loop_context_returns.pyc b/tests/corpus/3.15.2/loop_context_returns.pyc index 42e4e0ce6c7976b5df4c26254489dfe98d15b45f..fd2011ba83e935513e7bfddc578d0824454d2e78 100644 GIT binary patch delta 25 gcmX>pc~Vj!h>w?xnSp^p!CKFescPk^jRHG)08Fa}jsO4v delta 25 hcmX>pc~Vj!h>w?xnSp^p>y+u!ox&kG8wGaq003BJ2X6oX diff --git a/tests/corpus/3.15.2/loop_handler_ownership.py b/tests/corpus/3.15.2/loop_handler_ownership.py index c7d86ef8..c69d4d26 100644 --- a/tests/corpus/3.15.2/loop_handler_ownership.py +++ b/tests/corpus/3.15.2/loop_handler_ownership.py @@ -117,5 +117,3 @@ def nonloop(owner): except RuntimeError: owner.fallback() return None - - diff --git a/tests/corpus/3.15.2/loop_handler_ownership.pyc b/tests/corpus/3.15.2/loop_handler_ownership.pyc index 6924565869467ab2379b4b11963d9c95fa18c070..980197585df325bee16214006945b4a2e3af8cae 100644 GIT binary patch delta 25 gcmcbucUw;&h>w?xnSp^Jd3R{+6)x_(8wI?@0bWrDRsaA1 delta 25 gcmcbucUw;&h>w?xnSp`fB`aTLtEZXaMgebe09WJ&Y5)KL diff --git a/tests/corpus/3.15.2/nested_context_returns.py b/tests/corpus/3.15.2/nested_context_returns.py index f3ba23ec..5040cd90 100644 --- a/tests/corpus/3.15.2/nested_context_returns.py +++ b/tests/corpus/3.15.2/nested_context_returns.py @@ -47,5 +47,3 @@ async def async_outer_return(contexts, value): async with contexts[1]: result = await value() return result - - diff --git a/tests/corpus/3.15.2/nested_context_returns.pyc b/tests/corpus/3.15.2/nested_context_returns.pyc index 7e28382be766256f0852ba3b5fe5c696c189da9d..cbb56edcd8c219373869fabd82b9dc4e3684194a 100644 GIT binary patch delta 25 gcmZ3cwMw?xnSp`9(@?mM#h^NPqkxzw07WDPBme*a delta 25 hcmZ3cwMw?xnSp`9_|tE#qZ-V+HwuV}0svQw2MhoJ diff --git a/tests/corpus/3.15.2/normal_async_exits.py b/tests/corpus/3.15.2/normal_async_exits.py index bb75f5ab..78cc6e22 100644 --- a/tests/corpus/3.15.2/normal_async_exits.py +++ b/tests/corpus/3.15.2/normal_async_exits.py @@ -83,5 +83,3 @@ async def branch_return(owner): return 4 await owner.write(1, 2) return 2 - - diff --git a/tests/corpus/3.15.2/normal_async_exits.pyc b/tests/corpus/3.15.2/normal_async_exits.pyc index 72aaf6a3a86c895edbea439989a28eba0dffb318..456685aec2c301b4e0cead50651d45afd78ed457 100644 GIT binary patch delta 25 hcmZ1yyd+p4h>w?xnSp`9hqvfZ*`=+EHVRD91OQyg2qORh delta 25 hcmZ1yyd+p4h>w?xnSp^JMRw-0Pm9xXHwsMA1OQ&w2p<3d diff --git a/tests/corpus/3.15.2/private_handler_clauses.py b/tests/corpus/3.15.2/private_handler_clauses.py index 70c9b2c3..082f02b7 100644 --- a/tests/corpus/3.15.2/private_handler_clauses.py +++ b/tests/corpus/3.15.2/private_handler_clauses.py @@ -161,5 +161,3 @@ async def no_private_try(owner): continue await owner.log("finished", accepted) return accepted - - diff --git a/tests/corpus/3.15.2/private_handler_clauses.pyc b/tests/corpus/3.15.2/private_handler_clauses.pyc index 96fc35522ffba8a900525860307cc5a57a41bed4..1c2b8d6af9b30387ea18050379d72d80ee263198 100644 GIT binary patch delta 25 hcmewm{2^E%h>w?xnSp`fwc}2av(eKJZ4`K*2>@|i2~_|9 delta 25 gcmewm{2^E%h>w?xnSp_!%B9~=TqCw+qrd}A0Alb5mH+?% diff --git a/tests/corpus/3.15.2/sunk_context_exits.py b/tests/corpus/3.15.2/sunk_context_exits.py index 65d9c33b..37288190 100644 --- a/tests/corpus/3.15.2/sunk_context_exits.py +++ b/tests/corpus/3.15.2/sunk_context_exits.py @@ -50,5 +50,3 @@ async def normal(lock, pending, release, user_call): record("ready") record("outside") return True - - diff --git a/tests/corpus/3.15.2/sunk_context_exits.pyc b/tests/corpus/3.15.2/sunk_context_exits.pyc index e2b77fed439068b2bb965b3ae30d7ee0551b4804..4c1c856c8fe906f765a760251599b89c79a5fea9 100644 GIT binary patch delta 25 gcmZ1=xj<4Nh>w?xnSp^JHzwUL`ikS5jRF&R08iHkGynhq delta 25 hcmZ1=xj<4Nh>w?xnSp`fol?r4V&&_c8wDot002@W2LJ#7 diff --git a/tests/data/generator_statement_reconstruction.py b/tests/data/generator_statement_reconstruction.py index 30975bc9..44c9a23a 100644 --- a/tests/data/generator_statement_reconstruction.py +++ b/tests/data/generator_statement_reconstruction.py @@ -14,4 +14,3 @@ def captured_cells(rows): deltas = rows factory = sum return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) - diff --git a/tests/data/handler_iterator_breaks.py b/tests/data/handler_iterator_breaks.py index 38105d90..97ae7406 100644 --- a/tests/data/handler_iterator_breaks.py +++ b/tests/data/handler_iterator_breaks.py @@ -138,5 +138,3 @@ async def direct_unnamed_break(owner): break await owner.log("finished", accepted) return accepted - - diff --git a/tests/data/loop_context_returns.py b/tests/data/loop_context_returns.py index cd32d3a7..703d904c 100644 --- a/tests/data/loop_context_returns.py +++ b/tests/data/loop_context_returns.py @@ -62,5 +62,3 @@ async def async_mapping(lock, rows, parse): except ValueError: continue return {"rows": result} - - diff --git a/tests/data/loop_handler_ownership.py b/tests/data/loop_handler_ownership.py index c7d86ef8..c69d4d26 100644 --- a/tests/data/loop_handler_ownership.py +++ b/tests/data/loop_handler_ownership.py @@ -117,5 +117,3 @@ def nonloop(owner): except RuntimeError: owner.fallback() return None - - diff --git a/tests/data/nested_context_returns.py b/tests/data/nested_context_returns.py index f3ba23ec..5040cd90 100644 --- a/tests/data/nested_context_returns.py +++ b/tests/data/nested_context_returns.py @@ -47,5 +47,3 @@ async def async_outer_return(contexts, value): async with contexts[1]: result = await value() return result - - diff --git a/tests/data/normal_async_exits.py b/tests/data/normal_async_exits.py index bb75f5ab..78cc6e22 100644 --- a/tests/data/normal_async_exits.py +++ b/tests/data/normal_async_exits.py @@ -83,5 +83,3 @@ async def branch_return(owner): return 4 await owner.write(1, 2) return 2 - - diff --git a/tests/data/private_handler_clauses.py b/tests/data/private_handler_clauses.py index 70c9b2c3..082f02b7 100644 --- a/tests/data/private_handler_clauses.py +++ b/tests/data/private_handler_clauses.py @@ -161,5 +161,3 @@ async def no_private_try(owner): continue await owner.log("finished", accepted) return accepted - - diff --git a/tests/data/sunk_context_exits.py b/tests/data/sunk_context_exits.py index 65d9c33b..37288190 100644 --- a/tests/data/sunk_context_exits.py +++ b/tests/data/sunk_context_exits.py @@ -50,5 +50,3 @@ async def normal(lock, pending, release, user_call): record("ready") record("outside") return True - - diff --git a/tests/pyc/generator_statement_reconstruction.3.12.pyc b/tests/pyc/generator_statement_reconstruction.3.12.pyc index ac46321482a50b1d54eb2384a99f685ab3dc1f64..e23d9844889a2fcd08b8513fdf90365d1c10f789 100644 GIT binary patch delta 25 gcmeAb>=hI^&CAQh%)r1P{^GIRY4>%J8wF}P09b|xo&W#< delta 25 hcmeAb>=hI^&CAQh%)r3#Hgu_r)z6<7HVV{o003W-2&@1A diff --git a/tests/pyc/generator_statement_reconstruction.3.13.pyc b/tests/pyc/generator_statement_reconstruction.3.13.pyc index ef7a1450b6c5a10480b6cd2178c7c60a8ea0e1ce..fc1be9cf0b97f7123ec4e99cde5088e1b6559f0e 100644 GIT binary patch delta 25 hcmZ23xLi=+GcPX}GXn#|%{z&T`bvo}HVRDV003Wa2h0Ef delta 25 gcmZ23xLi=+GcPX}GXn#|9xE#~{_tMqjRMm-08?fLX8-^I diff --git a/tests/pyc/generator_statement_reconstruction.3.14.pyc b/tests/pyc/generator_statement_reconstruction.3.14.pyc index c35ded8dd30973e8ca6e64013d89ac3d03ed9544..fbd66df99312a0ae69414defe3689c514a17f2da 100644 GIT binary patch delta 25 hcmX>obWlh@n~#@^nSp`f_@#f-JA0UYHwqYX0svj22bcf= delta 25 gcmX>obWlh@n~#@^nSp_!r9ZjKC{f*gqkthN08L*7_5c6? diff --git a/tests/pyc/generator_statement_reconstruction.3.15.pyc b/tests/pyc/generator_statement_reconstruction.3.15.pyc index 0481f262bf7ca00482ef390383c3b416563df097..4ef14ad8d15089eaf548209fe2df9257a78584c7 100644 GIT binary patch delta 25 gcmZn{ZWk5^;^XCFW?*3Wlo4>jFj~NTqd++)080D@O#lD@ delta 25 hcmZn{ZWk5^;^XCFW?*30G-uhg31`f1Y!oQx1OQc{2lM~{ diff --git a/tests/pyc/handler_iterator_breaks.3.12.pyc b/tests/pyc/handler_iterator_breaks.3.12.pyc index e133c8b773ec0d3de55647b169745ecaff1cd6e9..43585ae57c6a541d76f282c062c2b4b0a5e3ae88 100644 GIT binary patch delta 25 hcmccOaK%C3G%qg~GXn#|(FLr(Z=eZxql`1ORP72s;1( diff --git a/tests/pyc/handler_iterator_breaks.3.14.pyc b/tests/pyc/handler_iterator_breaks.3.14.pyc index d10894d2447b965f56e07847c7e98126f4f6fe4d..e0c5484d7ca3255c72716cb2d65a6e60f6e63f8e 100644 GIT binary patch delta 25 hcmaFj^29|zn~#@^nSp_!`_Z?`4S(LoZxjeo0swR^3BLdU delta 25 gcmaFj^29|zn~#@^nSp`9?1q0cgM)X*Mu89|09#ZBz5oCK diff --git a/tests/pyc/handler_iterator_breaks.3.15.pyc b/tests/pyc/handler_iterator_breaks.3.15.pyc index 39790a6e79656b734a98417a5914e79e46a1c943..cf260ed11022123a082fcc3764742f0199d44da3 100644 GIT binary patch delta 25 gcmezG^4~=uh>w?xnSp^}W%KFYO5ZiE8wK)}0B}zTo&W#< delta 25 hcmezG^4~=uh>w?xnSp`fzxJFfZyrCE*(i{&1ORzw3CjQg diff --git a/tests/pyc/loop_context_returns.3.10.pyc b/tests/pyc/loop_context_returns.3.10.pyc index 5396b517187549bb5609c872e7805e368df3dad0..a7fcf9acb4686682970d191b330d98a3a57b0096 100644 GIT binary patch delta 25 hcmeyy{f%27pO=@5nSp_U{nV-4NjzHp8wH-R0svnX2ebeH delta 25 gcmeyy{f%27pO=@5nSp^}OOKGatUR0CMuBIn08}9c9smFU diff --git a/tests/pyc/loop_context_returns.3.11.pyc b/tests/pyc/loop_context_returns.3.11.pyc index 787249b46c9045c833714af0a5016cb5454020f7..727ceda6eb04bcd6d4cb4e53b210bff1ad26e47e 100644 GIT binary patch delta 25 hcmew(`$tw_IWI34GXn#|G3Mkjk-Yf#8wK9*0RUxG2p#|c delta 25 hcmew(`$tw_IWI34GXn#I{Hn~iOaApc~Vj!h>w?xnSp^p!CKFescPk^jRHG)08Fa}jsO4v delta 25 hcmX>pc~Vj!h>w?xnSp^p>y+u!ox&kG8wGaq003BJ2X6oX diff --git a/tests/pyc/loop_handler_ownership.3.12.pyc b/tests/pyc/loop_handler_ownership.3.12.pyc index bc8af3240e39143d53966b3c7ec64dcfcd506f16..a7c511ae0b395b94d172bda358cf1312b98c7481 100644 GIT binary patch delta 25 hcmdn2yIEJ@G%qg~GXnzyPxkCC7S7izHwr8k0{~k!2YLVi delta 25 hcmdn2yIEJ@G%qg~GXn!dOXm~ImHzXtZxmQ81^{SE2-E-o diff --git a/tests/pyc/loop_handler_ownership.3.13.pyc b/tests/pyc/loop_handler_ownership.3.13.pyc index a5737b67507115f23cd1d3ad04c4843823ba4057..ce130150297bd2e3bfa648af1c15daad52ac0823 100644 GIT binary patch delta 25 hcmaE$`#@LVGcPX}GXn#|Ykj+_!YL8cHVRx20|0I@2(bVF delta 25 hcmaE$`#@LVGcPX}GXn#I*W81-YyLiv*(h*93;=bF38nx5 diff --git a/tests/pyc/loop_handler_ownership.3.14.pyc b/tests/pyc/loop_handler_ownership.3.14.pyc index c6820e6e51e44536ed7875f928c7d498c99f39d8..c40f2b9dfaa7b68f930516845d880fe81bc511b3 100644 GIT binary patch delta 25 hcmdm_w@FVxn~#@^nSp^}$*;Q~Y+0JZHwq|;0{~vh2crN0 delta 25 hcmdm_w@FVxn~#@^nSp^J=i{83#qP7!Hwq|;0{~gm2R{G+ diff --git a/tests/pyc/loop_handler_ownership.3.15.pyc b/tests/pyc/loop_handler_ownership.3.15.pyc index 6924565869467ab2379b4b11963d9c95fa18c070..980197585df325bee16214006945b4a2e3af8cae 100644 GIT binary patch delta 25 gcmcbucUw;&h>w?xnSp^Jd3R{+6)x_(8wI?@0bWrDRsaA1 delta 25 gcmcbucUw;&h>w?xnSp`fB`aTLtEZXaMgebe09WJ&Y5)KL diff --git a/tests/pyc/nested_context_returns.3.11.pyc b/tests/pyc/nested_context_returns.3.11.pyc index ba3d2cdecef2ed41728a6960887a0f77fac49da2..f621cf44dc991be67ffc18fbcdf1f271018aa353 100644 GIT binary patch delta 25 gcmX?Ra?C_vIWI34GXn!dn=03*_skyF8wJdy0A5Q600000 delta 25 hcmX?Ra?C_vIWI34GXn#|yFc3}vVEw%w^6`M3IKCq31h($ delta 25 hcmbQKIa5w?xnSp`9(@?mM#h^NPqkxzw07WDPBme*a delta 25 hcmZ3cwMw?xnSp`9_|tE#qZ-V+HwuV}0svQw2MhoJ diff --git a/tests/pyc/normal_async_exits.3.14.pyc b/tests/pyc/normal_async_exits.3.14.pyc index 1589907aadfefdf010f8c19f528b58e8c6b4faf4..dd9c84359f611b0a0f8f076ea1ebe654dac954ec 100644 GIT binary patch delta 25 hcmX>Va4JARn~#@^nSp`9t6}!CHw!p7Z4|K5003jJ2n_%L delta 25 hcmX>Va4JARn~#@^nSp`fv{<>xztwkNY!tB4003sD2(w?xnSp`9hqvfZ*`=+EHVRD91OQyg2qORh delta 25 hcmZ1yyd+p4h>w?xnSp^JMRw-0Pm9xXHwsMA1OQ&w2p<3d diff --git a/tests/pyc/private_handler_clauses.3.12.pyc b/tests/pyc/private_handler_clauses.3.12.pyc index 679862a49c4e5f598ef2c5dcce4dbddcd402c42b..c2a695c57fc054bea0c891126d839e5dff522c45 100644 GIT binary patch delta 25 gcmeD5@AMZq&CAQh%)r3FZXD`=rn4btqd=uP09SYiBLDyZ delta 25 gcmeD5@AMZq&CAQh%)r2~abvtjb0E`#jRKYG0A6nga{vGU diff --git a/tests/pyc/private_handler_clauses.3.13.pyc b/tests/pyc/private_handler_clauses.3.13.pyc index dedb549bd347a08e456172c27ffb828bfab3bf6e..7a1fdb4228d88c0181762a32059f8bc8c8a9182d 100644 GIT binary patch delta 25 hcmbOnI5|+@GcPX}GXn!d delta 25 hcmcZ+d?Q#un~#@^nSp_!WUX=Aw?xnSp`fwc}2av(eKJZ4`K*2>@|i2~_|9 delta 25 gcmewm{2^E%h>w?xnSp_!%B9~=TqCw+qrd}A0Alb5mH+?% diff --git a/tests/pyc/sunk_context_exits.3.14.pyc b/tests/pyc/sunk_context_exits.3.14.pyc index e7c0328831e6461585978c11f34d011ef342175e..4751ce12f160788b7b2d9ef9e2967288e8ab40c7 100644 GIT binary patch delta 25 gcmca6aZN%%n~#@^nSp_!(adtwIhN3?8wK2W0970Y@c;k- delta 25 hcmca6aZN%%n~#@^nSp_!g5lS{t1EY|+9=@00{~*s2&4c2 diff --git a/tests/pyc/sunk_context_exits.3.15.pyc b/tests/pyc/sunk_context_exits.3.15.pyc index e2b77fed439068b2bb965b3ae30d7ee0551b4804..4c1c856c8fe906f765a760251599b89c79a5fea9 100644 GIT binary patch delta 25 gcmZ1=xj<4Nh>w?xnSp^JHzwUL`ikS5jRF&R08iHkGynhq delta 25 hcmZ1=xj<4Nh>w?xnSp`fol?r4V&&_c8wDot002@W2LJ#7