From 4108e74017bb93778d4e4a9c54e3f2bb463d1f23 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:35:45 -0400 Subject: [PATCH 1/2] Preserve synchronous iteration over awaited collections --- src/decompiler.rs | 18 +++---------- .../fixtures/awaited_synchronous_iteration.py | 23 ++++++++++++++++ tests/integration.rs | 25 ++++++++++++++++++ .../awaited_synchronous_iteration.3.10.pyc | Bin 0 -> 595 bytes .../awaited_synchronous_iteration.3.11.pyc | Bin 0 -> 989 bytes .../awaited_synchronous_iteration.3.12.pyc | Bin 0 -> 915 bytes .../awaited_synchronous_iteration.3.13.pyc | Bin 0 -> 908 bytes .../awaited_synchronous_iteration.3.14.pyc | Bin 0 -> 917 bytes .../pyc/awaited_synchronous_iteration.3.8.pyc | Bin 0 -> 579 bytes .../pyc/awaited_synchronous_iteration.3.9.pyc | Bin 0 -> 579 bytes 10 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/awaited_synchronous_iteration.py create mode 100644 tests/pyc/awaited_synchronous_iteration.3.10.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.11.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.12.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.13.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.14.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.8.pyc create mode 100644 tests/pyc/awaited_synchronous_iteration.3.9.pyc diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..0586915f 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -19314,20 +19314,10 @@ let reopen = self return true; } let target = inst.target.unwrap_or(inst.end()); - // async for: the iterable expression is an Await, or we are - // inside an async def using GET_AITER - let is_async = matches!( - self.stack.last(), - Some(Sv::E(e)) if matches!(&**e, Expr::Await(_)) - ); - if is_async { - if let Some(Sv::E(e)) = self.stack.last_mut() { - if let Expr::Await(inner) = &**e { - *e = inner.clone(); - } - } - } - self.handle_for_iter(target, is_async); + // FOR_ITER iterates synchronously, even when the collection + // was produced by `await fetch()`. Genuine async iteration + // uses GET_AITER/GET_ANEXT and its separate decoder path. + self.handle_for_iter(target, false); true } Op::FOR_LOOP => { diff --git a/tests/fixtures/awaited_synchronous_iteration.py b/tests/fixtures/awaited_synchronous_iteration.py new file mode 100644 index 00000000..6d5438be --- /dev/null +++ b/tests/fixtures/awaited_synchronous_iteration.py @@ -0,0 +1,23 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +# async/await syntax starts in Python 3.5. + +async def awaited(read): + result = [] + for item in await read(): + result.append(item) + return result + + +async def asynchronous(read): + result = [] + async for item in read(): + result.append(item) + return result + + +async def sync(read): + result = [] + for item in read(): + result.append(item) + return result diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..439d4cfc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,28 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn awaited_collections_use_synchronous_iteration() { + for version in ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("awaited_synchronous_iteration.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"async def awaited(read): + result = [] + for item in await read(): + result.append(item) + return result"#, + r#"async def asynchronous(read): + result = [] + async for item in read(): + result.append(item) + return result"#, + r#"async def sync(read): + result = [] + for item in read(): + result.append(item) + return result"#, + ], version); + } +} diff --git a/tests/pyc/awaited_synchronous_iteration.3.10.pyc b/tests/pyc/awaited_synchronous_iteration.3.10.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cc60d846151739ce8d20633e54619d69783f41f GIT binary patch literal 595 zcma)&&q~8U5XNUV+azsM5WRZvB(w($3Z6tfh_shpdJqIDp&PZ(CUrMKD=jGc0=|JB z^Bi7%f}8~3!i&zN+CTK-!p!c>EW_{nqDrM?0l<6m+-TN5PCn_qbOcpL6lRl>0)s*I>Fo;yr4-Q$bu_?LIbv)+>hgz`$^H?S1oEwTEs#2D0IR5z6{30c1>PNMUGUh+;@#1d@y?OhA$;g*li( zlO-9Z4y2p~h?{}CTqq33bi6okQ9NU zsfZ0oz(S~q3&^>}lZX@`MSMUZ9*~*{Ac4*62=C%#b);YohY!pQ;<67wfl&-{N2KczG$)vibgs2G&ai*10!2WCb_#t&R9j6xq6-~=C>1r|f& J!qtII2LRBX$$|g? literal 0 HcmV?d00001 diff --git a/tests/pyc/awaited_synchronous_iteration.3.12.pyc b/tests/pyc/awaited_synchronous_iteration.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d87c32b418c5082748cfcfbb98746accdc7c967 GIT binary patch literal 915 zcmb7?&ubGw6vyA&ncapo+nToK(54VXkcF5){dEyf;>FsF2SJp$bT=AllQ6TP22%*B zh+?3TO7kbkQv6Fi3w2L|7cW9c4z{_}H`_#lP5m)2UuNIDH?!Z*ypN_CWdL9v-J5^Y z&VBz%>t6#0t6qW@ZE)ZP+y@6q(a}h?Dzo_NFiRzKnp-q`k4SCapq_li_ajr==4-R%e2Oe| z7=^w}#_M-FI>ZpHjwkkjvc?lzf=feY?3md<(OMu*iK`(oMN23Y zLOqZ$NQPuYlsD#jZJQ8Excg7 zWtR4CHjVGm(tMUag~2jajc4%xUGi7YTv-;=bS3p4$crQ+UeCiE$(-yVS?m&A0Gq1H5MUk7!xBZ*xAkgY~Yq-;KGSs!DLDYt2dPatN)^QHfP(6Ez`Pj#5A0ypqwY{;WTYagd(p8GG&XXX~)OOI_^kFB2jA$|IQLH5k2 zOt!OpkRtC$npD)aD!7d$-aA*p(Td%stkH^1K<*dK{zf=BoR1{k3tgj#Pk2G=;e!5@6||jv z5no|Jd(Db}pnsF9M$`Yl0r@LouK0>^x^|LG&J+9~I+1^&|Ng7f1BOb%?%YMJF0y1a u61kFAeTeA{jwMZB-e|h*b?=JEQA~}oq)CT;hKPp-$K2rEIi)23RfhQKCZy@@BO2Wu%oa+7xHZZOg&;k^yEw~$^c z2#s3w?#bZAUn6x-f+ufnBS>$4FHJRSMDf9V%+8ycx8FCDjoX$^M2+nSZ_L;4u6=_4 z{cz9gOu}hUfhH)R0t30A0kwiYOBrMBG*6`)ux(h!16-yw>Kv+ZkuK3ZBI?{hr$=~n zlN#l5#8w<>E7MYU3`%M}EK z8#`r{vVf z;bi09VFcc>jG?G$=XsYMqm|C{V8vFEHCVAYC4SK?^>2DjkzQ27)4BNv&6#FfzHfFmcgS(aljV^B8iJ{qUEMW literal 0 HcmV?d00001 diff --git a/tests/pyc/awaited_synchronous_iteration.3.8.pyc b/tests/pyc/awaited_synchronous_iteration.3.8.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f88c010c2d333a182e6faee94e60531b8f439517 GIT binary patch literal 579 zcma)&!AiqG5Qb-xG>I`SML|4BPaZ6S0R>Mg9z57fFF^!BO6W!{v`O4esFn1j52D9B zhu}lx>RWhnb~PoX7Z+xBc4nCQ_P=U29UTDnd#6*s^SS*+@zT-gbxtOB$T48}3OB$s zrqhkgU=6x8X8O>zhDa^dkwXo#4>GY!?jGit!wd9i$|YRDZELRO_y)Q5jtGzK9DOLT zUtnnlNfJh^&`X1d0V_=&ifo*g#wZOZ1#p|DeS2vrQsLc=p3;o!UN8-)%m!i>4Ig+M z#hDmTf(Pj+jt-JpY5jFHB#;~dXkZ&%=q>z`yK@q`yo7U9Vz~6SfY21LGp8{ z0GE<+`FBP7>(=O5xS5XWbdw284TMZuGJ@?b9p6g-J|@SvAoDhPs+-nSKq>uv#TvBy|^&5vopiYZ~v^__H6)Q=Yx;;r&xVayl^af9h1pTavV6mz$I{t z+jJv$c!O?>HwVyjM@TKRqlY?VpJZ~I+zm{zfM+^WuF#rGn literal 0 HcmV?d00001 From bf7368f646075c6150c23aa095314a72a938e7bc Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:33:17 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../cases/b_awaited_synchronous_iteration.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/behavior/cases/b_awaited_synchronous_iteration.py diff --git a/tests/behavior/cases/b_awaited_synchronous_iteration.py b/tests/behavior/cases/b_awaited_synchronous_iteration.py new file mode 100644 index 00000000..ccf15d1a --- /dev/null +++ b/tests/behavior/cases/b_awaited_synchronous_iteration.py @@ -0,0 +1,14 @@ +# MIN_VERSION: 3.9 +import asyncio + +async def awaited(read): + result = [] + for item in await read(): + result.append(item) + return result + + +async def read(): + print("read") + return [1, 2, 3] +print(asyncio.run(awaited(read)))