tpy-lang 0.3.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. tpy_lang-0.3.0.dev0.dist-info/METADATA +151 -0
  2. tpy_lang-0.3.0.dev0.dist-info/RECORD +333 -0
  3. tpy_lang-0.3.0.dev0.dist-info/WHEEL +4 -0
  4. tpy_lang-0.3.0.dev0.dist-info/entry_points.txt +3 -0
  5. tpyc/__init__.py +104 -0
  6. tpyc/__main__.py +6 -0
  7. tpyc/_buildinfo.py +1 -0
  8. tpyc/_data/docs/LANGUAGE_FEATURES.md +6278 -0
  9. tpyc/_data/docs/STDLIB_ROADMAP.md +1258 -0
  10. tpyc/_data/docs/TPY_FOR_AGENTS.md +556 -0
  11. tpyc/_data/lib/tpy/_bindings/__init__.py +6 -0
  12. tpyc/_data/lib/tpy/_bindings/pcre2.py +173 -0
  13. tpyc/_data/lib/tpy/_bindings/posix_socket.py +161 -0
  14. tpyc/_data/lib/tpy/_functools_macros.py +80 -0
  15. tpyc/_data/lib/tpy/_macro_helpers.py +161 -0
  16. tpyc/_data/lib/tpy/argparse.py +2062 -0
  17. tpyc/_data/lib/tpy/asyncio/__init__.py +744 -0
  18. tpyc/_data/lib/tpy/asyncio/_executor.py +515 -0
  19. tpyc/_data/lib/tpy/base64.py +410 -0
  20. tpyc/_data/lib/tpy/bisect.py +39 -0
  21. tpyc/_data/lib/tpy/builtins.py +38 -0
  22. tpyc/_data/lib/tpy/dataclasses.py +354 -0
  23. tpyc/_data/lib/tpy/enum.py +23 -0
  24. tpyc/_data/lib/tpy/functools.py +33 -0
  25. tpyc/_data/lib/tpy/hashlib.py +206 -0
  26. tpyc/_data/lib/tpy/heapq.py +118 -0
  27. tpyc/_data/lib/tpy/io.py +395 -0
  28. tpyc/_data/lib/tpy/json.py +221 -0
  29. tpyc/_data/lib/tpy/math.py +406 -0
  30. tpyc/_data/lib/tpy/random.py +597 -0
  31. tpyc/_data/lib/tpy/re.py +467 -0
  32. tpyc/_data/lib/tpy/socket.py +379 -0
  33. tpyc/_data/lib/tpy/struct.py +178 -0
  34. tpyc/_data/lib/tpy/sys.py +40 -0
  35. tpyc/_data/lib/tpy/time.py +39 -0
  36. tpyc/_data/lib/tpy/tpy/__init__.py +78 -0
  37. tpyc/_data/lib/tpy/tpy/_bootstrap/__init__.py +10 -0
  38. tpyc/_data/lib/tpy/tpy/_bootstrap/_decorators.py +37 -0
  39. tpyc/_data/lib/tpy/tpy/_bootstrap/_extern.py +64 -0
  40. tpyc/_data/lib/tpy/tpy/_builtins/__init__.py +11 -0
  41. tpyc/_data/lib/tpy/tpy/_builtins/_bytes.py +378 -0
  42. tpyc/_data/lib/tpy/tpy/_builtins/_dict.py +151 -0
  43. tpyc/_data/lib/tpy/tpy/_builtins/_exceptions.py +125 -0
  44. tpyc/_data/lib/tpy/tpy/_builtins/_funcs.py +681 -0
  45. tpyc/_data/lib/tpy/tpy/_builtins/_io.py +97 -0
  46. tpyc/_data/lib/tpy/tpy/_builtins/_list.py +127 -0
  47. tpyc/_data/lib/tpy/tpy/_builtins/_range.py +52 -0
  48. tpyc/_data/lib/tpy/tpy/_builtins/_set.py +139 -0
  49. tpyc/_data/lib/tpy/tpy/_builtins/_super.py +11 -0
  50. tpyc/_data/lib/tpy/tpy/_builtins/_types.py +661 -0
  51. tpyc/_data/lib/tpy/tpy/_core/__init__.py +23 -0
  52. tpyc/_data/lib/tpy/tpy/_core/_bytes_view.py +129 -0
  53. tpyc/_data/lib/tpy/tpy/_core/_containers.py +137 -0
  54. tpyc/_data/lib/tpy/tpy/_core/_functions.py +40 -0
  55. tpyc/_data/lib/tpy/tpy/_core/_types.py +2061 -0
  56. tpyc/_data/lib/tpy/tpy/_typing/__init__.py +77 -0
  57. tpyc/_data/lib/tpy/tpy/_version.py +29 -0
  58. tpyc/_data/lib/tpy/tpy/bits.py +28 -0
  59. tpyc/_data/lib/tpy/tpy/coro/__init__.py +127 -0
  60. tpyc/_data/lib/tpy/tpy/extern.py +8 -0
  61. tpyc/_data/lib/tpy/tpy/mem.py +49 -0
  62. tpyc/_data/lib/tpy/tpy/unsafe.py +195 -0
  63. tpyc/_data/lib/tpy/tpy/version.py +21 -0
  64. tpyc/_data/lib/tpy/typing.py +13 -0
  65. tpyc/_data/runtime/cpp/include/tpy/any.hpp +461 -0
  66. tpyc/_data/runtime/cpp/include/tpy/as_ostream.hpp +117 -0
  67. tpyc/_data/runtime/cpp/include/tpy/async.hpp +76 -0
  68. tpyc/_data/runtime/cpp/include/tpy/bigint.hpp +1343 -0
  69. tpyc/_data/runtime/cpp/include/tpy/builtins.hpp +400 -0
  70. tpyc/_data/runtime/cpp/include/tpy/bytes_ops.hpp +469 -0
  71. tpyc/_data/runtime/cpp/include/tpy/container_ops.hpp +487 -0
  72. tpyc/_data/runtime/cpp/include/tpy/copy_iter.hpp +82 -0
  73. tpyc/_data/runtime/cpp/include/tpy/core.hpp +558 -0
  74. tpyc/_data/runtime/cpp/include/tpy/dict_ops.hpp +289 -0
  75. tpyc/_data/runtime/cpp/include/tpy/dunder.hpp +750 -0
  76. tpyc/_data/runtime/cpp/include/tpy/dynamic.hpp +44 -0
  77. tpyc/_data/runtime/cpp/include/tpy/enum.hpp +40 -0
  78. tpyc/_data/runtime/cpp/include/tpy/file.hpp +245 -0
  79. tpyc/_data/runtime/cpp/include/tpy/fixed_int.hpp +317 -0
  80. tpyc/_data/runtime/cpp/include/tpy/format.hpp +954 -0
  81. tpyc/_data/runtime/cpp/include/tpy/frame_slot.hpp +120 -0
  82. tpyc/_data/runtime/cpp/include/tpy/generator.hpp +47 -0
  83. tpyc/_data/runtime/cpp/include/tpy/iterable_ops.hpp +122 -0
  84. tpyc/_data/runtime/cpp/include/tpy/itertools.hpp +749 -0
  85. tpyc/_data/runtime/cpp/include/tpy/next_iter.hpp +82 -0
  86. tpyc/_data/runtime/cpp/include/tpy/ordered_map.hpp +518 -0
  87. tpyc/_data/runtime/cpp/include/tpy/ordered_set.hpp +337 -0
  88. tpyc/_data/runtime/cpp/include/tpy/own_iter.hpp +54 -0
  89. tpyc/_data/runtime/cpp/include/tpy/pascal_graph_sdl.hpp +192 -0
  90. tpyc/_data/runtime/cpp/include/tpy/printing.hpp +302 -0
  91. tpyc/_data/runtime/cpp/include/tpy/protocols.hpp +61 -0
  92. tpyc/_data/runtime/cpp/include/tpy/range.hpp +115 -0
  93. tpyc/_data/runtime/cpp/include/tpy/ranges.hpp +212 -0
  94. tpyc/_data/runtime/cpp/include/tpy/set_ops.hpp +265 -0
  95. tpyc/_data/runtime/cpp/include/tpy/slice.hpp +47 -0
  96. tpyc/_data/runtime/cpp/include/tpy/span_iter.hpp +42 -0
  97. tpyc/_data/runtime/cpp/include/tpy/stdlib/math.hpp +41 -0
  98. tpyc/_data/runtime/cpp/include/tpy/stdlib/pcre2_h.hpp +96 -0
  99. tpyc/_data/runtime/cpp/include/tpy/stdlib/random.hpp +25 -0
  100. tpyc/_data/runtime/cpp/include/tpy/stdlib/socket_h.hpp +145 -0
  101. tpyc/_data/runtime/cpp/include/tpy/stdlib/time.hpp +62 -0
  102. tpyc/_data/runtime/cpp/include/tpy/system.hpp +121 -0
  103. tpyc/_data/runtime/cpp/include/tpy/throwable.hpp +55 -0
  104. tpyc/_data/runtime/cpp/include/tpy/tpy.hpp +156 -0
  105. tpyc/_data/runtime/cpp/include/tpy/type_name.hpp +77 -0
  106. tpyc/_data/runtime/cpp/include/tpy/type_traits.hpp +240 -0
  107. tpyc/_data/runtime/cpp/include/tpy/uninit_array_storage.hpp +250 -0
  108. tpyc/_data/runtime/cpp/include/tpy/uninit_heap_storage.hpp +277 -0
  109. tpyc/_data/runtime/cpp/include/tpy/varargs.hpp +174 -0
  110. tpyc/_data/runtime/cpp/include/tpy/variant_ref.hpp +118 -0
  111. tpyc/_data/runtime/cpp/src/stdlib/socket_impl.cpp +104 -0
  112. tpyc/_data/runtime/cpp/third_party/README.md +58 -0
  113. tpyc/_data/runtime/cpp/third_party/pcre2/AUTHORS +36 -0
  114. tpyc/_data/runtime/cpp/third_party/pcre2/CMakeLists.txt +1233 -0
  115. tpyc/_data/runtime/cpp/third_party/pcre2/COPYING +5 -0
  116. tpyc/_data/runtime/cpp/third_party/pcre2/ChangeLog +3097 -0
  117. tpyc/_data/runtime/cpp/third_party/pcre2/HACKING +853 -0
  118. tpyc/_data/runtime/cpp/third_party/pcre2/INSTALL +368 -0
  119. tpyc/_data/runtime/cpp/third_party/pcre2/LICENCE +94 -0
  120. tpyc/_data/runtime/cpp/third_party/pcre2/NEWS +492 -0
  121. tpyc/_data/runtime/cpp/third_party/pcre2/NON-AUTOTOOLS-BUILD +430 -0
  122. tpyc/_data/runtime/cpp/third_party/pcre2/README +956 -0
  123. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/COPYING-CMAKE-SCRIPTS +22 -0
  124. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindEditline.cmake +16 -0
  125. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindPackageHandleStandardArgs.cmake +58 -0
  126. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindReadline.cmake +29 -0
  127. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config-version.cmake.in +15 -0
  128. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config.cmake.in +148 -0
  129. tpyc/_data/runtime/cpp/third_party/pcre2/config-cmake.h.in +56 -0
  130. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-16.pc.in +13 -0
  131. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-32.pc.in +13 -0
  132. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-8.pc.in +13 -0
  133. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-posix.pc.in +13 -0
  134. tpyc/_data/runtime/cpp/third_party/pcre2/pcre2-config.in +121 -0
  135. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h +483 -0
  136. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.generic +483 -0
  137. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.in +460 -0
  138. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h +1010 -0
  139. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.generic +1010 -0
  140. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.in +1010 -0
  141. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_auto_possess.c +1371 -0
  142. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c +196 -0
  143. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c.dist +196 -0
  144. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chkdint.c +96 -0
  145. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_compile.c +11001 -0
  146. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_config.c +252 -0
  147. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_context.c +510 -0
  148. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_convert.c +1189 -0
  149. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dfa_match.c +4119 -0
  150. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dftables.c +297 -0
  151. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_error.c +345 -0
  152. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_extuni.c +162 -0
  153. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_find_bracket.c +219 -0
  154. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_fuzzsupport.c +792 -0
  155. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_internal.h +2084 -0
  156. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_intmodedep.h +940 -0
  157. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_compile.c +14972 -0
  158. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_match.c +200 -0
  159. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_misc.c +234 -0
  160. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_neon_inc.h +354 -0
  161. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_simd_inc.h +2355 -0
  162. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_test.c +2528 -0
  163. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_maketables.c +165 -0
  164. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match.c +7777 -0
  165. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match_data.c +185 -0
  166. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_newline.c +243 -0
  167. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ord2utf.c +120 -0
  168. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_pattern_info.c +432 -0
  169. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_printint.c +886 -0
  170. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_script_run.c +344 -0
  171. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_serialize.c +286 -0
  172. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_string_utils.c +237 -0
  173. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_study.c +1915 -0
  174. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substitute.c +1009 -0
  175. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substring.c +550 -0
  176. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_tables.c +234 -0
  177. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucd.c +5460 -0
  178. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucp.h +396 -0
  179. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucptables.c +1533 -0
  180. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_valid_utf.c +398 -0
  181. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_xclass.c +308 -0
  182. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2demo.c +497 -0
  183. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2grep.c +4606 -0
  184. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.c +425 -0
  185. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.h +187 -0
  186. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix_test.c +209 -0
  187. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2test.c +9708 -0
  188. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorApple.c +137 -0
  189. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorCore.c +327 -0
  190. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorFreeBSD.c +89 -0
  191. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorPosix.c +62 -0
  192. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorWindows.c +40 -0
  193. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorNetBSD.c +72 -0
  194. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorPosix.c +172 -0
  195. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorPosix.c +141 -0
  196. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorWindows.c +102 -0
  197. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfig.h +142 -0
  198. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigCPU.h +188 -0
  199. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigInternal.h +907 -0
  200. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.c +3561 -0
  201. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.h +2466 -0
  202. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_32.c +4636 -0
  203. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_64.c +3491 -0
  204. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_T2_32.c +4302 -0
  205. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeLOONGARCH_64.c +3765 -0
  206. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_32.c +472 -0
  207. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_64.c +387 -0
  208. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_common.c +4259 -0
  209. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_32.c +485 -0
  210. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_64.c +719 -0
  211. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_common.c +3161 -0
  212. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_32.c +142 -0
  213. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_64.c +222 -0
  214. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_common.c +3121 -0
  215. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeS390X.c +4526 -0
  216. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_32.c +1685 -0
  217. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_64.c +1398 -0
  218. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_common.c +5001 -0
  219. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitSerialize.c +516 -0
  220. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitUtils.c +344 -0
  221. tpyc/_data/runtime/cpp/third_party/pcre2.sources.txt +54 -0
  222. tpyc/_data/runtime/cpp/third_party/pcre2.vendor.json +7 -0
  223. tpyc/build/__init__.py +7 -0
  224. tpyc/build/pcre2.py +122 -0
  225. tpyc/build/third_party.py +413 -0
  226. tpyc/cli.py +822 -0
  227. tpyc/codegen_cpp/__init__.py +18 -0
  228. tpyc/codegen_cpp/builtins.py +484 -0
  229. tpyc/codegen_cpp/context.py +2064 -0
  230. tpyc/codegen_cpp/expressions.py +5940 -0
  231. tpyc/codegen_cpp/functions.py +1913 -0
  232. tpyc/codegen_cpp/gen_async.py +3258 -0
  233. tpyc/codegen_cpp/gen_generators.py +657 -0
  234. tpyc/codegen_cpp/generator.py +2258 -0
  235. tpyc/codegen_cpp/match.py +1997 -0
  236. tpyc/codegen_cpp/param_const.py +172 -0
  237. tpyc/codegen_cpp/protocols.py +907 -0
  238. tpyc/codegen_cpp/records.py +1654 -0
  239. tpyc/codegen_cpp/resumable_cfg.py +1651 -0
  240. tpyc/codegen_cpp/statements.py +4963 -0
  241. tpyc/codegen_cpp/string_dispatch.py +76 -0
  242. tpyc/codegen_cpp/test_context.py +46 -0
  243. tpyc/codegen_cpp/test_param_const.py +113 -0
  244. tpyc/codegen_cpp/test_resumable_cfg.py +182 -0
  245. tpyc/codegen_cpp/type_resolution.py +53 -0
  246. tpyc/codegen_cpp/types.py +436 -0
  247. tpyc/codegen_cpp/variant_access.py +135 -0
  248. tpyc/coercions.py +749 -0
  249. tpyc/compilation_context.py +57 -0
  250. tpyc/compiler.py +3945 -0
  251. tpyc/cycle_detection.py +358 -0
  252. tpyc/diagnostics.py +135 -0
  253. tpyc/dump_types.py +353 -0
  254. tpyc/frontend_diagnostics.py +47 -0
  255. tpyc/frontend_ir/__init__.py +140 -0
  256. tpyc/frontend_ir/lower.py +1098 -0
  257. tpyc/frontend_ir/nodes.py +718 -0
  258. tpyc/frontend_ir/resolver_adapter.py +151 -0
  259. tpyc/frontend_plugin.py +209 -0
  260. tpyc/install_docs.py +81 -0
  261. tpyc/liveness.py +756 -0
  262. tpyc/macro_api.py +1724 -0
  263. tpyc/macro_loader.py +497 -0
  264. tpyc/module_names.py +64 -0
  265. tpyc/modules/__init__.py +31 -0
  266. tpyc/modules/defs.py +89 -0
  267. tpyc/modules/registry.py +36 -0
  268. tpyc/modules/resolver.py +192 -0
  269. tpyc/modules/type_resolution.py +629 -0
  270. tpyc/namespace.py +172 -0
  271. tpyc/parse/__init__.py +84 -0
  272. tpyc/parse/imports.py +490 -0
  273. tpyc/parse/nodes.py +1732 -0
  274. tpyc/parse/parser.py +4043 -0
  275. tpyc/parse/resolve_refs.py +466 -0
  276. tpyc/parse/type_resolver.py +1060 -0
  277. tpyc/prescan.py +254 -0
  278. tpyc/qnames.py +149 -0
  279. tpyc/repl.py +529 -0
  280. tpyc/repl_backends.py +848 -0
  281. tpyc/sema/__init__.py +21 -0
  282. tpyc/sema/analyzer.py +3625 -0
  283. tpyc/sema/bound_check.py +72 -0
  284. tpyc/sema/builder_trace.py +684 -0
  285. tpyc/sema/calls.py +5406 -0
  286. tpyc/sema/compatibility.py +2107 -0
  287. tpyc/sema/context.py +1243 -0
  288. tpyc/sema/expressions.py +3737 -0
  289. tpyc/sema/flow_facts.py +199 -0
  290. tpyc/sema/init_tracker.py +150 -0
  291. tpyc/sema/list_literals.py +69 -0
  292. tpyc/sema/literal_utils.py +27 -0
  293. tpyc/sema/local_deduction.py +1088 -0
  294. tpyc/sema/macros.py +179 -0
  295. tpyc/sema/match.py +1177 -0
  296. tpyc/sema/method_expansion.py +347 -0
  297. tpyc/sema/methods.py +2197 -0
  298. tpyc/sema/mutation_propagation.py +268 -0
  299. tpyc/sema/narrowing.py +857 -0
  300. tpyc/sema/numeric_lattice.py +160 -0
  301. tpyc/sema/operators.py +402 -0
  302. tpyc/sema/overloads.py +841 -0
  303. tpyc/sema/protocols.py +1209 -0
  304. tpyc/sema/reach_analysis.py +202 -0
  305. tpyc/sema/registration.py +3156 -0
  306. tpyc/sema/scope_tracker.py +193 -0
  307. tpyc/sema/statements.py +4426 -0
  308. tpyc/sema/type_ops.py +1879 -0
  309. tpyc/sema/value_range.py +181 -0
  310. tpyc/symbol_binding.py +259 -0
  311. tpyc/test_c3_mro.py +208 -0
  312. tpyc/test_cli_argv.py +52 -0
  313. tpyc/test_compiler.py +559 -0
  314. tpyc/test_contains_type_param.py +101 -0
  315. tpyc/test_cycle_detection.py +221 -0
  316. tpyc/test_dump_types.py +225 -0
  317. tpyc/test_install_docs.py +65 -0
  318. tpyc/test_local_cpp_form.py +135 -0
  319. tpyc/test_macro_loader.py +76 -0
  320. tpyc/test_method_expansion.py +254 -0
  321. tpyc/test_nominal_identity.py +182 -0
  322. tpyc/test_overloads.py +410 -0
  323. tpyc/test_parse.py +303 -0
  324. tpyc/test_parse_type_ref.py +506 -0
  325. tpyc/test_parse_version_info.py +58 -0
  326. tpyc/test_reach_analysis.py +72 -0
  327. tpyc/test_ref_type.py +216 -0
  328. tpyc/test_send_sync_substitution.py +276 -0
  329. tpyc/test_tuple_mutation_propagation.py +206 -0
  330. tpyc/test_type_def_registry.py +1729 -0
  331. tpyc/test_union_types.py +195 -0
  332. tpyc/type_def_registry.py +975 -0
  333. tpyc/typesys.py +5104 -0
@@ -0,0 +1,3258 @@
1
+ """Code generation for `async def` functions (state-machine struct conforming
2
+ to Awaitable[T]).
3
+
4
+ Mirrors gen_generators.py but with the resumable-frame shape from
5
+ docs/ASYNC_DESIGN.md: each async def lowers to a struct with a
6
+ `poll(Waker) -> Poll<T>` method instead of `__next__() -> std::expected`.
7
+
8
+ Implementation status:
9
+ - Commit 1: no-await async defs.
10
+ - Commit 3 (this): single/multi-await, statically-resolved sub-coroutines.
11
+ Awaits are restricted to top-level statement positions:
12
+ * `x = await sub()` (assign / vardecl)
13
+ * `await sub()` (statement-level expression)
14
+ * `return await sub()` (return value)
15
+ - Subsequent commits: try/finally re-establishment, cancellation, await
16
+ inside loops/conditionals.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import contextlib
21
+ import copy
22
+ from dataclasses import dataclass, fields, is_dataclass
23
+ from enum import IntEnum
24
+ from typing import TYPE_CHECKING
25
+
26
+ from ..namespace import Namespace
27
+ from ..parse.nodes import (
28
+ TpyFunction, TpyAwait, TpyStmt, TpyAssign, TpyVarDecl, TpyReturn,
29
+ TpyExprStmt, TpyName, TpyExpr, TpyTry, TpyExceptHandler, TpyCall,
30
+ TpyMethodCall, TpyFieldAccess, TpyForEach, TpyWith,
31
+ TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBoolLiteral,
32
+ TpyNoneLiteral, TpyCoerce,
33
+ is_stable_address_lvalue,
34
+ )
35
+ from ..typesys import IntLiteralType, NominalType, OptionalType, OwnType, TypeParamRef, unwrap_readonly, unwrap_ref_type, unwrap_own, VoidType
36
+ from .gen_generators import GeneratorCodegen, GeneratorForInfo
37
+ from ..type_def_registry import is_str_type, is_str_category, is_big_int_type
38
+ from .context import INDENT, escape_cpp_name, CodeGenError, FinallyContext, module_to_cpp_namespace, qualified_cpp_name
39
+ from . import resumable_cfg as rcfg
40
+
41
+
42
+ def _stmts_have_return(stmts: "list[TpyStmt]") -> bool:
43
+ """Recursively check whether any TpyReturn appears in a statement list."""
44
+ for stmt in stmts:
45
+ if isinstance(stmt, TpyReturn):
46
+ return True
47
+ if hasattr(stmt, "sub_bodies"):
48
+ for body in stmt.sub_bodies():
49
+ if _stmts_have_return(body):
50
+ return True
51
+ return False
52
+
53
+
54
+ def _regions_have_pending_cleanup(regions: tuple) -> bool:
55
+ """Return True if any region in the tuple will emit cleanup code.
56
+
57
+ Used by _emit_exit_region_finallies to decide whether to defer the
58
+ __finally_stop check: if the destination region stack still has pending
59
+ cleanup (with.__exit__ or finally helpers), the stop-check must wait
60
+ until those later states have run their cleanup."""
61
+ for r in regions:
62
+ if isinstance(r, rcfg.WithRegion):
63
+ return True
64
+ if isinstance(r, rcfg.FinallyRegion):
65
+ # BBs inside a CFG-based finally body: the finally body itself
66
+ # is the pending cleanup; don't stop before it runs.
67
+ return True
68
+ if isinstance(r, rcfg.TryRegion) and r.finally_helper_name is not None:
69
+ return True
70
+ if isinstance(r, rcfg.ExceptRegion) and r.parent_finally is not None:
71
+ return True
72
+ return False
73
+
74
+
75
+ if TYPE_CHECKING:
76
+ from io import TextIO
77
+ from .context import CodeGenContext
78
+ from .types import TypeMapper
79
+ from .expressions import ExpressionGenerator
80
+ from .statements import StatementGenerator
81
+ from .functions import FunctionGenerator
82
+
83
+
84
+ class _CoroParamKind(IntEnum):
85
+ """Storage form for a coro-struct captured param.
86
+
87
+ REF: non-value type bound by reference; field is `T&`, ctor takes `T&`,
88
+ init binds the reference. Caller must keep the source alive across
89
+ polls.
90
+ VALUE: value type (or string view) stored by value; field is `T`,
91
+ ctor takes `T x_` and the init moves into the field.
92
+ POINTER: pointer-form Optional[NonValue] param; field is `T*` (or
93
+ `const T*`), ctor takes the same. Init is a direct copy -- raw
94
+ pointers are trivially copyable, so std::move would just add
95
+ noise.
96
+ TYPE_PARAM: TypeParamRef param whose value-vs-reference resolution
97
+ happens at instantiation; field is `::tpy::val_or_ref_t<T>` (T for
98
+ value types, T& for object types), ctor takes
99
+ `::tpy::param_val_or_ref_t<T>` (const T& or T&), init directly
100
+ binds/copies. No std::move (the param is already a reference).
101
+ STATIC_PROTOCOL: static-protocol-typed param (e.g. `Own[Awaitable[T]]`)
102
+ whose concrete type is deduced as an extra template arg `T_<pname>`
103
+ with a concept constraint -- mirrors `gen_params_with_protocols`
104
+ in `functions.py`. Field stores by value (the concrete deduced
105
+ type); ctor takes `T_<pname>&&` (forwarding ref) and moves in;
106
+ the factory forwards the same way.
107
+ OWNED_VALUE: `Own[T]` non-value, non-static-protocol type (e.g.
108
+ `Own[Cancellable[T]]` -- a @dynamic protocol, so the C++ shape is
109
+ `unique_ptr<P>`, not a deduced template arg). Field stores by
110
+ value; ctor takes `T&&` and moves in; the factory forwards via
111
+ `std::move(name)`. Parallels STATIC_PROTOCOL minus the
112
+ template-arg dance.
113
+ """
114
+ REF = 0
115
+ VALUE = 1
116
+ POINTER = 2
117
+ TYPE_PARAM = 3
118
+ STATIC_PROTOCOL = 4
119
+ OWNED_VALUE = 5
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class _CoroParam:
124
+ cpp_name: str
125
+ field_type: str # type spelling for the frame-field declaration
126
+ ctor_param_type: str # type spelling for the constructor's parameter
127
+ kind: _CoroParamKind
128
+
129
+ def field_decl(self) -> str:
130
+ if self.kind is _CoroParamKind.REF:
131
+ return f"{self.field_type}& {self.cpp_name}"
132
+ return f"{self.field_type} {self.cpp_name}"
133
+
134
+ def factory_param_decl(self) -> str:
135
+ # Factory function signature param: same C++ type as the ctor's
136
+ # param (matters for TYPE_PARAM, where it's `param_val_or_ref_t<T>`
137
+ # rather than the field's `val_or_ref_t<T>`), but with the bare
138
+ # name -- the factory body forwards by bare name.
139
+ if self.kind is _CoroParamKind.REF:
140
+ return f"{self.ctor_param_type}& {self.cpp_name}"
141
+ if self.kind is _CoroParamKind.STATIC_PROTOCOL:
142
+ return f"{self.ctor_param_type}&& {self.cpp_name}"
143
+ if self.kind is _CoroParamKind.OWNED_VALUE:
144
+ return f"{self.ctor_param_type} {self.cpp_name}"
145
+ return f"{self.ctor_param_type} {self.cpp_name}"
146
+
147
+ def ctor_param_decl(self) -> str:
148
+ if self.kind is _CoroParamKind.REF:
149
+ return f"{self.ctor_param_type}& {self.cpp_name}"
150
+ if self.kind is _CoroParamKind.STATIC_PROTOCOL:
151
+ return f"{self.ctor_param_type}&& {self.cpp_name}_"
152
+ if self.kind is _CoroParamKind.OWNED_VALUE:
153
+ return f"{self.ctor_param_type}&& {self.cpp_name}_"
154
+ # VALUE / POINTER / TYPE_PARAM: `_` suffix disambiguates from the
155
+ # field name in the init list.
156
+ return f"{self.ctor_param_type} {self.cpp_name}_"
157
+
158
+ def ctor_init(self) -> str:
159
+ if self.kind is _CoroParamKind.REF:
160
+ return f"{self.cpp_name}({self.cpp_name})"
161
+ if self.kind in (_CoroParamKind.POINTER, _CoroParamKind.TYPE_PARAM):
162
+ # POINTER: raw pointer, trivially copyable -- std::move is noise.
163
+ # TYPE_PARAM: `param_val_or_ref_t<T>` is already a reference type;
164
+ # std::move on it yields an rvalue that won't bind to the field
165
+ # type for non-value Ts. Direct bind/copy is uniformly correct.
166
+ return f"{self.cpp_name}({self.cpp_name}_)"
167
+ return f"{self.cpp_name}(std::move({self.cpp_name}_))"
168
+
169
+
170
+ class _StateKind(IntEnum):
171
+ INITIAL = 0
172
+ RESUME = 1
173
+ JOIN = 2
174
+
175
+
176
+ @dataclass(frozen=True, order=True)
177
+ class _StateLabel:
178
+ """Typed switch-case label. Orders naturally (INITIAL < RESUME < JOIN
179
+ by `IntEnum` value, then by `idx`); formats to its C++ identifier
180
+ via `cpp_name()`."""
181
+ kind: _StateKind
182
+ idx: int = 0
183
+
184
+ def cpp_name(self) -> str:
185
+ if self.kind is _StateKind.INITIAL:
186
+ return "S_INITIAL"
187
+ if self.kind is _StateKind.RESUME:
188
+ # Resume point after suspension `idx`. Shape-neutral name: the
189
+ # same emitter serves both `await` and `yield` suspensions.
190
+ return f"S_RESUME_{self.idx}"
191
+ return f"S_JOIN_{self.idx}"
192
+
193
+
194
+ # Single C++ spelling of `Ready(unit)` for void-returning async defs.
195
+ # `async def -> None` lowers Poll[None]'s type-arg slot to std::monostate
196
+ # (the value-bearing-None unit type); the `ready` factory needs an actual
197
+ # value of that type, so we construct `std::monostate{}` explicitly.
198
+ # Centralised here so the 3 emit sites in this module + 1 in statements.py
199
+ # can't drift apart.
200
+ POLL_VOID_READY_RETURN = (
201
+ "return ::tpystd::tpy::Poll<::std::monostate>::ready(::std::monostate{});"
202
+ )
203
+
204
+
205
+ def _same_elements(a: list, b: list) -> bool:
206
+ """True iff `a` and `b` have identical length and element-by-element
207
+ identity. Used by the async lift pre-pass to decide whether a
208
+ sub-body actually changed -- distinct list objects with all-`is`
209
+ elements are treated as unchanged so the parent stmt's id stays
210
+ stable."""
211
+ return len(a) == len(b) and all(x is y for x, y in zip(a, b))
212
+
213
+
214
+ class AsyncCoroCodegen:
215
+ """Generates C++ code for `async def` functions as state-machine structs."""
216
+
217
+ def __init__(
218
+ self,
219
+ ctx: "CodeGenContext",
220
+ types: "TypeMapper",
221
+ expressions: "ExpressionGenerator",
222
+ statements: "StatementGenerator",
223
+ functions: "FunctionGenerator",
224
+ ):
225
+ self.ctx = ctx
226
+ self.types = types
227
+ self.expressions = expressions
228
+ self.statements = statements
229
+ self.functions = functions
230
+ # Set by CodeGenerator after init; the resumable for-loop emit reuses
231
+ # the legacy strategy analysis (`_analyze_for_strategy`).
232
+ self.gen_generators: GeneratorCodegen
233
+ # Resumable-shape discriminator read by the `_resumable_*` policy
234
+ # seams. ASYNC (await -> __poll__ -> Poll<T>) is the default; the
235
+ # generator migration sets GENERATOR (yield -> __next__ ->
236
+ # expected<T, StopIteration>) transiently via `_resumable_shape`.
237
+ self._shape: rcfg.ResumableShape = rcfg.ResumableShape.ASYNC
238
+
239
+ @contextlib.contextmanager
240
+ def _resumable_shape(self, shape: 'rcfg.ResumableShape'):
241
+ """Temporarily select the resumable shape (async / generator) the
242
+ policy seams emit for, restoring the prior shape on exit."""
243
+ prev = self._shape
244
+ self._shape = shape
245
+ try:
246
+ yield
247
+ finally:
248
+ self._shape = prev
249
+
250
+ def _is_generator_shape(self) -> bool:
251
+ return self._shape is rcfg.ResumableShape.GENERATOR
252
+
253
+ def gen_struct_name(self, func: TpyFunction,
254
+ record_name: str | None = None) -> str:
255
+ """Resumable-frame struct name for the CURRENT function. Shape-aware:
256
+ `__gen_<funcname>` (/ `__gen_<Record>_<funcname>`) for the generator
257
+ shape, `__coro_<funcname>` (/ `__coro_<Record>_<funcname>`) for the
258
+ async shape -- so the struct name reflects what it is (the
259
+ `operator<<` repr and the legacy `GeneratorCodegen.gen_struct_name`
260
+ use the same `__gen_` convention). Awaited-callee sub-coro structs
261
+ are named via `_sub_struct_name` (always `__coro_`)."""
262
+ prefix = "__gen_" if self._is_generator_shape() else "__coro_"
263
+ return AsyncCoroCodegen._sub_struct_name(func.name, record_name, prefix)
264
+
265
+ @staticmethod
266
+ def _sub_struct_name(name: str, owner_record: str | None = None,
267
+ prefix: str = "__coro_") -> str:
268
+ """Resumable-frame struct name from raw strings: `<prefix><name>` for
269
+ free functions, `<prefix><Owner>_<name>` for methods. Single source
270
+ of truth for `gen_struct_name`, the await payload factory (sub-
271
+ coroutine of a statically-resolved await -- always `__coro_`), and
272
+ the async-with prescan (which only has the CM's `NominalType.name`)."""
273
+ if owner_record:
274
+ return (f"{prefix}{escape_cpp_name(owner_record)}_"
275
+ f"{escape_cpp_name(name)}")
276
+ return f"{prefix}{escape_cpp_name(name)}"
277
+
278
+ def _classify_params(self, func: TpyFunction,
279
+ record_name: str | None = None
280
+ ) -> list[_CoroParam]:
281
+ """Classify async-def params for the coro struct field/ctor shape.
282
+
283
+ Returns a list of `_CoroParam` records, one per captured param.
284
+ When `record_name` is set, prepends `__self: <Record>&` (or
285
+ `const <Record>&` for @readonly methods) so async methods
286
+ capture their receiver -- parallels `GeneratorCodegen`
287
+ self-capture.
288
+
289
+ Generic params (`T` as TypeParamRef) use the
290
+ `param_val_or_ref_t<T>` / `val_or_ref_t<T>` trait so each
291
+ instantiation picks the right value-vs-reference shape -- a
292
+ value-typed T (e.g. Int32) stores by value (so literal /
293
+ rvalue call-site args don't dangle), while an object-typed T
294
+ stores by reference (matching Python semantics and the
295
+ non-template ref path).
296
+
297
+ v1 conservative rule for str: passes by string_view (caller's
298
+ storage, same lifetime model as generators -- coros that
299
+ escape via Task will need to copy out, but until that lands
300
+ the borrow holds across await boundaries within a single
301
+ asyncio.run).
302
+ """
303
+ out: list[_CoroParam] = []
304
+ if record_name:
305
+ # Match the factory-site spelling (generator.py:1177, :1252):
306
+ # convert dotted nested-class names to C++ scope syntax (`Outer.Inner`
307
+ # -> `Outer::Inner`) before escaping, then template-qualify for a
308
+ # generic class (`Box[T]` -> `Box<T>&`) -- the bare class name in
309
+ # a type position outside the class body is ill-formed for
310
+ # templates.
311
+ cpp_record = escape_cpp_name(record_name.replace(".", "::"))
312
+ record_tps = self._record_template_args(record_name)
313
+ if record_tps:
314
+ cpp_record = f"{cpp_record}<{', '.join(record_tps)}>"
315
+ const_prefix = "const " if func.is_readonly else ""
316
+ recv_type = f"{const_prefix}{cpp_record}"
317
+ out.append(_CoroParam(
318
+ cpp_name="__self",
319
+ field_type=recv_type,
320
+ ctor_param_type=recv_type,
321
+ kind=_CoroParamKind.REF,
322
+ ))
323
+ for pname, ptype in func.params:
324
+ cpp_name = escape_cpp_name(pname)
325
+ ptype_inner = unwrap_ref_type(ptype)
326
+ actual = unwrap_readonly(ptype_inner)
327
+ if is_str_type(ptype_inner):
328
+ out.append(_CoroParam(
329
+ cpp_name=cpp_name,
330
+ field_type="std::string_view",
331
+ ctor_param_type="std::string_view",
332
+ kind=_CoroParamKind.VALUE,
333
+ ))
334
+ elif isinstance(ptype_inner, TypeParamRef):
335
+ # to_cpp_return / to_cpp_param_type already encode the
336
+ # val_or_ref_t<T> / param_val_or_ref_t<T> traits (and
337
+ # collapse to std::size_t for INT-kind params).
338
+ out.append(_CoroParam(
339
+ cpp_name=cpp_name,
340
+ field_type=ptype_inner.to_cpp_return(),
341
+ ctor_param_type=ptype_inner.to_cpp_param_type(),
342
+ kind=_CoroParamKind.TYPE_PARAM,
343
+ ))
344
+ elif self.functions.protocols.is_static_protocol_param(ptype):
345
+ # Static-protocol param (e.g. `Own[Awaitable[T]]`): the
346
+ # concrete operand type is deduced as an extra template
347
+ # arg `T_<pname>` with a concept constraint, declared in
348
+ # `_emit_template_header`. Field stores by value;
349
+ # ctor/factory forward via T_<pname>&&. Checked before
350
+ # the OptionalType-pointer-repr branch below so
351
+ # `Own[Awaitable[T]] | None` doesn't get mis-routed to
352
+ # POINTER -- `_protocol_template_parts` will reject the
353
+ # nullable shape with a clear diagnostic.
354
+ #
355
+ # `T_{pname}` (raw, not escaped) keeps the template-arg
356
+ # name aligned with `_protocol_template_parts` (which
357
+ # declares the template arg) -- `_struct_name_templated`
358
+ # reads the same field back to spell instantiations.
359
+ # Using `cpp_name` here would diverge when `pname`
360
+ # collides with a C++ keyword (e.g. `class` -> `class_`).
361
+ template_arg = f"T_{pname}"
362
+ out.append(_CoroParam(
363
+ cpp_name=cpp_name,
364
+ field_type=template_arg,
365
+ ctor_param_type=template_arg,
366
+ kind=_CoroParamKind.STATIC_PROTOCOL,
367
+ ))
368
+ elif isinstance(actual, OptionalType) and actual.uses_pointer_repr():
369
+ cpp_type = ptype_inner.to_cpp_param_type()
370
+ out.append(_CoroParam(
371
+ cpp_name=cpp_name,
372
+ field_type=cpp_type,
373
+ ctor_param_type=cpp_type,
374
+ kind=_CoroParamKind.POINTER,
375
+ ))
376
+ elif self.ctx.is_ptr_variant_union(actual):
377
+ # Non-value union: the pointer-variant borrow form
378
+ # (`std::variant<A*, B*>`) is the shape every other param
379
+ # boundary uses -- ordinary functions, simple generators,
380
+ # plain locals. Storing it by value in the frame keeps the
381
+ # factory's signature in step with what the call site (and
382
+ # the emplace path) already build; a value-variant-by-ref
383
+ # field would be the lone divergence. Deep-const for a
384
+ # readonly union, matching `_gen_union_arg`.
385
+ is_readonly_param = actual is not ptype_inner
386
+ cpp_type = (self.types.type_to_cpp_const_ptr_variant(actual)
387
+ if is_readonly_param
388
+ else self.types.type_to_cpp_ptr_variant(actual))
389
+ out.append(_CoroParam(
390
+ cpp_name=cpp_name,
391
+ field_type=cpp_type,
392
+ ctor_param_type=cpp_type,
393
+ kind=_CoroParamKind.POINTER,
394
+ ))
395
+ else:
396
+ cpp_type = self.types.type_to_cpp(ptype_inner)
397
+ if isinstance(ptype_inner, OwnType):
398
+ # Owned non-value (e.g. `Own[Cancellable[T]]` -- a
399
+ # @dynamic protocol whose C++ shape is unique_ptr<P>,
400
+ # so the param can't be re-bound through REF and the
401
+ # factory can't forward by bare name without
402
+ # tripping the deleted copy ctor). Store by value;
403
+ # ctor moves in; factory moves out. Checked before
404
+ # `is_value_type()` because `OwnType.is_value_type()`
405
+ # returns True (Own[T] uses T&& at param boundaries
406
+ # so it's value-like for *most* purposes) but the
407
+ # move-only semantics still need explicit forwarding.
408
+ out.append(_CoroParam(
409
+ cpp_name=cpp_name,
410
+ field_type=cpp_type,
411
+ ctor_param_type=cpp_type,
412
+ kind=_CoroParamKind.OWNED_VALUE,
413
+ ))
414
+ elif ptype_inner.is_value_type():
415
+ out.append(_CoroParam(
416
+ cpp_name=cpp_name,
417
+ field_type=cpp_type,
418
+ ctor_param_type=cpp_type,
419
+ kind=_CoroParamKind.VALUE,
420
+ ))
421
+ else:
422
+ out.append(_CoroParam(
423
+ cpp_name=cpp_name,
424
+ field_type=cpp_type,
425
+ ctor_param_type=cpp_type,
426
+ kind=_CoroParamKind.REF,
427
+ ))
428
+ return out
429
+
430
+ def _protocol_template_parts(self, func: TpyFunction) -> list[str]:
431
+ """Return template-header parts for any static-protocol-typed
432
+ params on `func`. Each part has the form `<Concept> T_<pname>`
433
+ (or `<Concept><type_args> T_<pname>` when the protocol is
434
+ generic). Mirrors `gen_combined_template_header` in protocols.py
435
+ for the single-required-protocol case; multi-protocol / nullable
436
+ shapes are deferred until a concrete need surfaces.
437
+ """
438
+ parts: list[str] = []
439
+ for pname, ptype in func.params:
440
+ if not self.functions.protocols.is_static_protocol_param(ptype):
441
+ continue
442
+ infos = self.functions.protocols.get_all_protocol_params(
443
+ [(pname, ptype)])
444
+ if not infos:
445
+ continue
446
+ info = infos[0]
447
+ if len(info.protocols) != 1 or info.has_none:
448
+ raise CodeGenError(
449
+ f"async def param {pname!r}: multi-protocol or "
450
+ "optional-protocol shape is not yet supported in "
451
+ "async-def coro codegen (only single required "
452
+ "protocols like `Own[Awaitable[T]]`)",
453
+ loc=func.loc)
454
+ proto = info.protocols[0]
455
+ concept_name = self.functions.protocols.get_concept_name(proto)
456
+ if proto.type_args:
457
+ targs = ", ".join(t.to_cpp() for t in proto.type_args)
458
+ parts.append(f"{concept_name}<{targs}> T_{pname}")
459
+ else:
460
+ parts.append(f"{concept_name} T_{pname}")
461
+ return parts
462
+
463
+ def _record_template_args(self, record_name: str | None) -> tuple[str, ...]:
464
+ """Type params of the enclosing record for a method, or () for a free
465
+ function / method on a non-generic class. A method on `class Box[T]`
466
+ contributes T to the coro struct's template header so the struct can
467
+ reference `Box<T>` in the self-capture field."""
468
+ if not record_name:
469
+ return ()
470
+ info = self.ctx.analyzer.registry.get_record(record_name)
471
+ if info is None or not info.type_params:
472
+ return ()
473
+ return tuple(info.type_params)
474
+
475
+ def _record_template_parts(self, record_name: str | None) -> list[str]:
476
+ """Rendered template-header parts for the enclosing record's type
477
+ params, mirroring `protocols.gen_record_template_parts` so out-of-class
478
+ member-def headers spell the same constrained form as the class
479
+ declaration (`Iterable<int32_t> T`, not bare `typename T`). Without
480
+ this, an out-of-class definition's parameter-list is non-equivalent to
481
+ the in-class declaration's and C++ rejects with a constraint
482
+ mismatch."""
483
+ if not record_name:
484
+ return []
485
+ info = self.ctx.analyzer.registry.get_record(record_name)
486
+ if info is None or not info.type_params:
487
+ return []
488
+ return self.functions.protocols.gen_record_template_parts(
489
+ list(info.type_params),
490
+ info.type_param_bounds or {},
491
+ info.type_param_kinds)
492
+
493
+ def _is_templated_coro(self, func: TpyFunction,
494
+ record_name: str | None = None) -> bool:
495
+ """True iff the coro/generator struct is a C++ template -- because
496
+ of explicit `[T, ...]` type params, a static-protocol-typed param
497
+ (`T_<pname>` template arg), OR (for a method) the enclosing record's
498
+ type params. Templated structs must emit their poll/__next__ body +
499
+ factory inline in the header; a non-template struct's body lands in
500
+ the .cpp. Single source of truth for the header-vs-cpp placement
501
+ decision across both async and generator shapes."""
502
+ return (bool(func.type_params)
503
+ or bool(self._protocol_template_parts(func))
504
+ or bool(self._record_template_parts(record_name)))
505
+
506
+ def _emit_template_header(self, out: "TextIO", func: TpyFunction,
507
+ *, indent: str = "",
508
+ record_name: str | None = None) -> bool:
509
+ proto_parts = self._protocol_template_parts(func)
510
+ record_parts = self._record_template_parts(record_name)
511
+ if not func.type_params and not proto_parts and not record_parts:
512
+ return False
513
+ # Record's [T...] first so an outer Box<T> reads naturally; then the
514
+ # method's own [U...]; then protocol-typed-param template args. The
515
+ # combined-flat form is correct for free types (structs at namespace
516
+ # scope) -- the coro/gen struct itself is a free `template<typename
517
+ # T, typename U> struct __coro_...`. For an out-of-class member
518
+ # function definition of a class template (`Box<T>::method`) where
519
+ # the method is itself a template, C++ requires NESTED headers
520
+ # instead -- see `_emit_member_template_headers`.
521
+ parts = list(record_parts)
522
+ parts.extend(f"typename {tp}" for tp in func.type_params)
523
+ parts.extend(proto_parts)
524
+ out.write(f"{indent}template <{', '.join(parts)}>\n")
525
+ return True
526
+
527
+ def _emit_member_template_headers(self, out: "TextIO",
528
+ func: TpyFunction,
529
+ *, record_name: str,
530
+ indent: str = "") -> None:
531
+ """Emit C++ template headers for the out-of-class definition of a
532
+ member function of a class template: `template <record_tps>` then
533
+ `template <method_type_params + proto_parts>`. Either level may
534
+ collapse to no-op if that level has no params (e.g. a non-template
535
+ method on a class template emits only the class header; a method
536
+ template on a non-generic class emits only the method header). The
537
+ nested form is the **only** C++-legal spelling for "member function
538
+ template of a class template"; the flat form `_emit_template_header`
539
+ produces declares a different entity and triggers
540
+ no-declaration-matches at the in-class declaration."""
541
+ record_parts = self._record_template_parts(record_name)
542
+ if record_parts:
543
+ out.write(f"{indent}template <{', '.join(record_parts)}>\n")
544
+ proto_parts = self._protocol_template_parts(func)
545
+ if func.type_params or proto_parts:
546
+ parts_list = [f"typename {tp}" for tp in func.type_params]
547
+ parts_list.extend(proto_parts)
548
+ out.write(f"{indent}template <{', '.join(parts_list)}>\n")
549
+
550
+ def _struct_name_templated(self, func: TpyFunction,
551
+ record_name: str | None = None) -> str:
552
+ """Return the coro struct name suffixed with `<T1, T2, ...>` when the
553
+ function is generic, else the bare name. Use this whenever the
554
+ struct name appears in a type-name position (return types,
555
+ out-of-line method qualifiers, parameter types) -- C++ rejects
556
+ the injected-class-name there. The bare `gen_struct_name` is
557
+ still correct inside the struct body (constructors) and for
558
+ forward decls (`struct X;`).
559
+
560
+ Template-arg order matches `_emit_template_header`: record's type
561
+ params first (for a method on a generic class), then the function's
562
+ own type params, then per-param `T_<pname>` static-protocol args.
563
+ """
564
+ bare = self.gen_struct_name(func, record_name)
565
+ extras = [p.field_type for p in self._classify_params(func, record_name)
566
+ if p.kind is _CoroParamKind.STATIC_PROTOCOL]
567
+ all_args = (list(self._record_template_args(record_name))
568
+ + list(func.type_params) + extras)
569
+ if not all_args:
570
+ return bare
571
+ return f"{bare}<{', '.join(all_args)}>"
572
+
573
+ def _emit_params_decl(self, func: TpyFunction) -> str:
574
+ return ", ".join(p.factory_param_decl() for p in self._classify_params(func))
575
+
576
+ def _ret_cpp(self, func: TpyFunction) -> str:
577
+ return self.types.type_to_cpp(unwrap_ref_type(func.return_type))
578
+
579
+ def _is_void_return(self, func: TpyFunction) -> bool:
580
+ """True iff func's declared return is `None` -- i.e. the top-level
581
+ `-> None` annotation that lowers to C++ `void`. Distinguished from
582
+ `Task[None]` / `Poll[None]` consumers, where the inner None is at
583
+ a type-arg position and routes to `std::monostate`."""
584
+ return isinstance(unwrap_ref_type(func.return_type), VoidType)
585
+
586
+ def _poll_ret_cpp(self, func: TpyFunction) -> str:
587
+ # The state-machine's __poll__ returns Poll[T], a type-arg slot,
588
+ # so a `-> None` async def flips its inner T to the unit type --
589
+ # matching how Task[None] / Future[None] / Awaitable[None] lower
590
+ # elsewhere. Without the flip, `Task<std::monostate>::poll_frame()`
591
+ # would call a `Poll<void> __poll__()` and the C++ compiler would
592
+ # reject the return-type mismatch.
593
+ if self._is_void_return(func):
594
+ return "::tpystd::tpy::Poll<::std::monostate>"
595
+ return f"::tpystd::tpy::Poll<{self._ret_cpp(func)}>"
596
+
597
+ # -- Forward declarations -------------------------------------------------
598
+
599
+ def gen_coro_forward_decl(self, out: "TextIO", func: TpyFunction,
600
+ record_name: str | None = None) -> bool:
601
+ struct_name = self.gen_struct_name(func, record_name)
602
+ self._emit_template_header(out, func, record_name=record_name)
603
+ out.write(f"struct {struct_name};\n")
604
+ return True
605
+
606
+ def gen_factory_forward_decl(self, out: "TextIO", func: TpyFunction) -> bool:
607
+ return_type_name = self._struct_name_templated(func)
608
+ self._emit_template_header(out, func)
609
+ params = self._emit_params_decl(func)
610
+ out.write(f"{return_type_name} {escape_cpp_name(func.name)}({params});\n")
611
+ return True
612
+
613
+ # -- Body partitioning ----------------------------------------------------
614
+
615
+ def _effective_body(self, func: TpyFunction) -> list[TpyStmt]:
616
+ """Return the function body with the await-lift pass applied so
617
+ awaits embedded in expressions become top-level vardecls. The
618
+ CFG builder treats compound statements (including any
619
+ wrapping try/finally) uniformly, so no unwrap is needed here.
620
+
621
+ Cached on the TpyFunction node so multiple codegen passes
622
+ (struct emission + poll body) see the same rewrite.
623
+ """
624
+ state = rcfg.resumable_state(func)
625
+ if state.lifted_body is not None:
626
+ return state.lifted_body
627
+ lifted = self._lift_nested_awaits(func, func.body)
628
+ state.lifted_body = lifted
629
+ return lifted
630
+
631
+ def _lift_nested_awaits(self, func: TpyFunction,
632
+ body: list[TpyStmt]) -> list[TpyStmt]:
633
+ """Rewrite each statement that has awaits buried in expressions
634
+ into a sequence of statements where every await is at top level.
635
+
636
+ Recurses into compound statement bodies (if/while/for/try/with)
637
+ so awaits buried inside loop/branch bodies are also lifted.
638
+
639
+ Example:
640
+ x = (await a()) + (await b())
641
+ becomes:
642
+ __await_lift_0 = await a()
643
+ __await_lift_1 = await b()
644
+ x = __await_lift_0 + __await_lift_1
645
+
646
+ Each lifted name is registered as a hoisted local so the bind
647
+ slot is a frame field.
648
+ """
649
+ out: list[TpyStmt] = []
650
+ for stmt in body:
651
+ top_await = rcfg._top_level_await_in(stmt)
652
+ new_stmts = self._lift_awaits_in_stmt(func, stmt, top_await)
653
+ # Recurse compound bodies AFTER the surface lift so a
654
+ # substituted TpyName isn't re-scanned. The surface lift
655
+ # mutates `stmt` in place (see BUGS.md "Await lifter
656
+ # mutates parse AST"); the compound recurse returns a
657
+ # shallow copy so nested bodies don't.
658
+ new_stmts = [
659
+ self._lift_compound_subbodies(func, s) for s in new_stmts
660
+ ]
661
+ out.extend(new_stmts)
662
+ return out
663
+
664
+ def _lift_compound_subbodies(self, func: TpyFunction,
665
+ stmt: TpyStmt) -> TpyStmt:
666
+ """If `stmt` is a compound (if/while/for/try/with) and any of
667
+ its sub-bodies needs lifting, return a shallow copy with the
668
+ sub-body lists replaced by lifted versions. Otherwise return
669
+ `stmt` unchanged. Does not mutate the input."""
670
+ if not hasattr(stmt, "sub_bodies"):
671
+ return stmt
672
+ if not is_dataclass(stmt):
673
+ return stmt
674
+ # Preserve the original stmt object (and its id) when sub-body
675
+ # lifting produced no actual changes -- any analyzer-side
676
+ # id(stmt)-keyed dict (e.g. if_branch_decls) would otherwise be
677
+ # orphaned by a spurious clone.
678
+ replacements: dict[str, list[TpyStmt]] = {}
679
+ for f in fields(stmt):
680
+ v = getattr(stmt, f.name, None)
681
+ if isinstance(v, list) and v and isinstance(v[0], TpyStmt):
682
+ lifted = self._lift_nested_awaits(func, v)
683
+ if not _same_elements(lifted, v):
684
+ replacements[f.name] = lifted
685
+ # TpyTry: handlers list isn't TpyStmt-typed but each handler
686
+ # carries its own body. Build new handler instances if any
687
+ # handler body needed lifting.
688
+ new_handlers: list[TpyExceptHandler] | None = None
689
+ if isinstance(stmt, TpyTry) and stmt.handlers:
690
+ rebuilt: list[TpyExceptHandler] = []
691
+ any_changed = False
692
+ for h in stmt.handlers:
693
+ lifted_body = self._lift_nested_awaits(func, h.body) if h.body else h.body
694
+ if not _same_elements(lifted_body, h.body):
695
+ rebuilt.append(TpyExceptHandler(
696
+ exception_type=h.exception_type,
697
+ binding=h.binding,
698
+ body=lifted_body,
699
+ loc=h.loc,
700
+ ))
701
+ any_changed = True
702
+ else:
703
+ rebuilt.append(h)
704
+ if any_changed:
705
+ new_handlers = rebuilt
706
+ if not replacements and new_handlers is None:
707
+ return stmt
708
+ new_stmt = copy.copy(stmt)
709
+ for fname, lifted in replacements.items():
710
+ setattr(new_stmt, fname, lifted)
711
+ if new_handlers is not None:
712
+ new_stmt.handlers = new_handlers
713
+ return new_stmt
714
+
715
+ def _lift_awaits_in_stmt(self, func: TpyFunction, stmt: TpyStmt,
716
+ top_await: 'TpyAwait | None') -> list[TpyStmt]:
717
+ """Lift any TpyAwait inside `stmt`'s expressions out into preceding
718
+ VarDecls. The top-level await (if any) stays in place. Returns the
719
+ new statement list; if no rewrite needed, returns [stmt].
720
+ """
721
+ # Collect nested awaits (excluding the top-level one).
722
+ lifts: list[TpyAwait] = []
723
+ self._collect_nested_awaits(stmt, top_await, lifts)
724
+ if not lifts:
725
+ return [stmt]
726
+ new_stmts: list[TpyStmt] = []
727
+ for await_node in lifts:
728
+ name = f"__await_lift_{self._next_lift_id(func)}"
729
+ self._bump_lift_id(func)
730
+ await_t = self.ctx.get_expr_type(await_node)
731
+ if await_t is None:
732
+ raise CodeGenError(
733
+ "await result has no analyzed type (lift)", loc=stmt.loc)
734
+ await_t = unwrap_ref_type(await_t)
735
+ new_decl = TpyVarDecl(
736
+ name=name, type=await_t, init=await_node, loc=stmt.loc)
737
+ new_stmts.append(new_decl)
738
+ # Register as hoisted local; codegen emits a frame field.
739
+ # Append in place to avoid an O(N) copy per lift (which,
740
+ # paired with _next_lift_id's linear scan, would otherwise
741
+ # be O(N^2) over nested awaits in one function).
742
+ if func.generator_locals is None:
743
+ func.generator_locals = []
744
+ func.generator_locals.append((name, await_t))
745
+ # Replace the await in `stmt`'s expression tree with a
746
+ # TpyName referring to the lifted local. Use the analyzer's
747
+ # set_expr_type so codegen's get_expr_type sees it.
748
+ replacement = TpyName(name=name, loc=await_node.loc)
749
+ self.ctx.analyzer.ctx.set_expr_type(replacement, await_t)
750
+ self._replace_expr_in_stmt(stmt, await_node, replacement)
751
+ new_stmts.append(stmt)
752
+ return new_stmts
753
+
754
+ def _collect_nested_awaits(self, stmt: TpyStmt, top_await: 'TpyAwait | None',
755
+ out: list[TpyAwait]) -> None:
756
+ """Walk stmt's expressions; collect TpyAwait nodes that are NOT the
757
+ top-level await (which is handled by region partitioning). Skips
758
+ nested defs / sub_bodies."""
759
+
760
+ def walk_expr(e):
761
+ if e is None:
762
+ return
763
+ if isinstance(e, TpyAwait):
764
+ if e is not top_await:
765
+ out.append(e)
766
+ # Don't recurse into the await's own value; that becomes
767
+ # the call processed by the lifted VarDecl.
768
+ return
769
+ for c in (e.children() if hasattr(e, "children") else ()):
770
+ walk_expr(c)
771
+
772
+ if hasattr(stmt, "exprs"):
773
+ for e in stmt.exprs():
774
+ walk_expr(e)
775
+
776
+ def _next_lift_id(self, func: TpyFunction) -> int:
777
+ """Return the next free index for __await_lift_<n> names.
778
+ Tracked as a per-function counter so the lift pass is linear
779
+ in the total number of lifts."""
780
+ return rcfg.resumable_state(func).next_lift_id
781
+
782
+ def _bump_lift_id(self, func: TpyFunction) -> None:
783
+ rcfg.resumable_state(func).next_lift_id += 1
784
+
785
+ def _replace_expr_in_stmt(self, stmt: TpyStmt, old_expr,
786
+ new_expr) -> None:
787
+ """Walk stmt's dataclass fields and replace `old_expr` with
788
+ `new_expr` (by identity). Catches both primary-expression slots
789
+ (init/value/expr/condition for if/while/etc.) and nested
790
+ expression children."""
791
+ if not is_dataclass(stmt):
792
+ return
793
+ for f in fields(stmt):
794
+ v = getattr(stmt, f.name, None)
795
+ if v is old_expr:
796
+ setattr(stmt, f.name, new_expr)
797
+ return
798
+ if isinstance(v, list):
799
+ for i, item in enumerate(v):
800
+ if item is old_expr:
801
+ v[i] = new_expr
802
+ return
803
+ if hasattr(item, "children"):
804
+ self._replace_in_expr(item, old_expr, new_expr)
805
+ elif hasattr(v, "children"):
806
+ self._replace_in_expr(v, old_expr, new_expr)
807
+
808
+ def _replace_in_expr(self, expr, old_expr, new_expr) -> None:
809
+ """Recursively replace `old_expr` with `new_expr` inside `expr`."""
810
+ if not hasattr(expr, "children") or not is_dataclass(expr):
811
+ return
812
+ for f in fields(expr):
813
+ v = getattr(expr, f.name, None)
814
+ if v is old_expr:
815
+ setattr(expr, f.name, new_expr)
816
+ return
817
+ if isinstance(v, list):
818
+ for i, item in enumerate(v):
819
+ if item is old_expr:
820
+ v[i] = new_expr
821
+ return
822
+ if hasattr(item, "children"):
823
+ self._replace_in_expr(item, old_expr, new_expr)
824
+ elif hasattr(v, "children"):
825
+ self._replace_in_expr(v, old_expr, new_expr)
826
+
827
+ # -- Struct definition ----------------------------------------------------
828
+
829
+ def gen_coro_struct(self, out: "TextIO", func: TpyFunction,
830
+ record_name: str | None = None) -> None:
831
+ """Emit the full `__FCoro` struct definition. When `record_name`
832
+ is given, the struct captures `__self: <Record>&` and the struct
833
+ name is `__coro_<Record>_<func>` (mirrors generator-method
834
+ codegen).
835
+ """
836
+ struct_name = self.gen_struct_name(func, record_name)
837
+ ctor_params = self._classify_params(func, record_name)
838
+ cfg = self._build_resumable_cfg(func)
839
+ yields = cfg.yield_sites
840
+
841
+ label = f"{record_name}.{func.name}" if record_name else func.name
842
+ kind = "Generator" if self._is_generator_shape() else "Async coroutine"
843
+ out.write(f"// {kind}: {label}\n")
844
+ self._emit_template_header(out, func, record_name=record_name)
845
+ out.write(f"struct {struct_name} {{\n")
846
+
847
+ # State integer (shared) + per-shape extra state (async adds the
848
+ # cancel flag).
849
+ out.write(f"{INDENT}int32_t __state;\n")
850
+ self._emit_resumable_extra_state_fields(out)
851
+
852
+ # Captured param fields
853
+ for p in ctor_params:
854
+ out.write(f"{INDENT}{p.field_decl()};\n")
855
+
856
+ state = rcfg.resumable_state(func)
857
+ # Hoisted local fields (mirrors generator behavior).
858
+ if func.generator_locals:
859
+ owning_str = state.with_owning_str_targets
860
+ pointer_form_names: set[str] = set()
861
+ for info in state.for_loop_info.values():
862
+ if info.pointer_form_loop_var is not None:
863
+ pointer_form_names.add(info.pointer_form_loop_var)
864
+ pointer_form_names.update(info.pointer_form_unpack_targets)
865
+ for lname, ltype in func.generator_locals:
866
+ ltype_inner = unwrap_ref_type(ltype)
867
+ cpp_name = escape_cpp_name(lname)
868
+ if lname in owning_str:
869
+ # `with X() as label:` -- `__enter__` returns by
870
+ # value; storing the view across suspensions
871
+ # would dangle. Use owning storage. See
872
+ # `_prescan_with_stmts`.
873
+ out.write(f"{INDENT}std::string {cpp_name};\n")
874
+ elif lname in pointer_form_names:
875
+ # Pointer-form alias: a for-loop var (non-value element
876
+ # over a stable source) or a tuple-unpack target bound to
877
+ # a non-value container member. Stored as `T*` (alias the
878
+ # live element) rather than `frame_slot<T>` (value-copy),
879
+ # so mutations propagate and resume preserves aliasing;
880
+ # also works for @nocopy / move-only elements. Bound by
881
+ # address in `_emit_async_for_advance` / the tuple-unpack
882
+ # emit. Checked before is_value_type because the loop var
883
+ # of a tuple-unpack (`__for_tup`) is itself a value tuple.
884
+ inner_cpp = self.types.type_to_cpp(ltype_inner)
885
+ out.write(f"{INDENT}{inner_cpp}* {cpp_name} = nullptr;\n")
886
+ elif ltype_inner.is_value_type():
887
+ cpp_type = self.types.type_to_cpp(ltype_inner)
888
+ out.write(f"{INDENT}{cpp_type} {cpp_name};\n")
889
+ elif (isinstance(ltype_inner, OptionalType)
890
+ and ltype_inner.uses_pointer_repr()):
891
+ # Pointer-repr Optional: bare `T* = nullptr` aliases
892
+ # the source and uses nullptr as both "uninitialized"
893
+ # and "None"; no outer `std::optional<...>` wrap.
894
+ inner_cpp = self.types.type_to_cpp(ltype_inner.inner)
895
+ out.write(f"{INDENT}{inner_cpp}* {cpp_name} = nullptr;\n")
896
+ else:
897
+ cpp_type = self.types.type_to_cpp(ltype_inner)
898
+ out.write(f"{INDENT}::tpy::frame_slot<{cpp_type}> {cpp_name};\n")
899
+
900
+ # Synthetic fields for CFG-decomposed for-loops: one
901
+ # iterator + one __next__-result slot per for-with-await,
902
+ # stored as frame_slot<T> -- same memory shape as the hoisted
903
+ # user-local fields, so the whole frame uses one slot type.
904
+ for fname, ftype in state.for_fields:
905
+ out.write(f"{INDENT}::tpy::frame_slot<{ftype}> {fname};\n")
906
+
907
+ # Context-manager fields for CFG-decomposed `with`-with-await
908
+ # bodies. One frame_slot<T> per WithItem.
909
+ for fname, ftype in state.with_fields:
910
+ out.write(f"{INDENT}::tpy::frame_slot<{ftype}> {fname};\n")
911
+
912
+ # In-flight exception slots for CFG-decomposed try-finally-with-
913
+ # await bodies. std::exception_ptr default-constructs
914
+ # to null; the catch arm sets it via std::current_exception().
915
+ # bool pending-return flags need explicit init: NSDMI
916
+ # = false. Value slots default-init via their own type's ctor.
917
+ is_gen = self._is_generator_shape()
918
+ for fname, ftype in state.try_finally_fields:
919
+ # Generators always return StopIteration; no pending-return value
920
+ # slot is needed. The field may be allocated by the prescan when
921
+ # the trial build ran under the async shape -- skip it here.
922
+ if is_gen and fname.startswith("__finally_ret_"):
923
+ continue
924
+ if ftype == "bool":
925
+ out.write(f"{INDENT}{ftype} {fname} = false;\n")
926
+ else:
927
+ out.write(f"{INDENT}{ftype} {fname};\n")
928
+
929
+ self._emit_resumable_sub_future_fields(out, func, yields)
930
+
931
+ # Generators with helper-based finallies that contain a `return` need a
932
+ # stop flag: the helper sets it so `__next__()` emits StopIteration
933
+ # instead of re-throwing (Python: `return` in `finally` suppresses exc).
934
+ if is_gen and any(_stmts_have_return(body)
935
+ for _, body in cfg.finally_helpers):
936
+ out.write(f"{INDENT}bool __finally_stop = false;\n")
937
+
938
+ out.write(f"\n")
939
+
940
+ # State enum: S_INITIAL, S_RESUME_<i>, S_JOIN_<n>, S_DONE.
941
+ # State numbering matches _compute_case_entries' assignment.
942
+ case_entries = self._compute_case_entries(cfg)
943
+ ordered = sorted(case_entries.items(), key=lambda kv: kv[1])
944
+ out.write(f"{INDENT}enum : int32_t {{\n")
945
+ next_val = 0
946
+ for _bb_id, label in ordered:
947
+ out.write(f"{INDENT}{INDENT}{label.cpp_name()} = {next_val},\n")
948
+ next_val += 1
949
+ out.write(f"{INDENT}{INDENT}S_DONE = {next_val},\n")
950
+ out.write(f"{INDENT}}};\n\n")
951
+
952
+ # Constructor.
953
+ ctor_param_list = ", ".join(p.ctor_param_decl() for p in ctor_params)
954
+ init_parts = ["__state(S_INITIAL)", *self._resumable_extra_ctor_inits()]
955
+ init_parts.extend(p.ctor_init() for p in ctor_params)
956
+ out.write(f"{INDENT}{struct_name}({ctor_param_list})\n")
957
+ out.write(f"{INDENT}{INDENT}: {', '.join(init_parts)} {{}}\n\n")
958
+
959
+ # Body-method forward declaration + per-shape extra methods.
960
+ out.write(f"{INDENT}{self._resumable_body_method_fwd_decl(func)};\n")
961
+ self._emit_resumable_extra_methods(out, struct_name)
962
+
963
+ # Finally-helper forward declarations: one per TryRegion with a
964
+ # finally body.
965
+ for helper_name, _body in cfg.finally_helpers:
966
+ out.write(f"{INDENT}void {helper_name}();\n")
967
+
968
+ repr_label = (f"{record_name}.{func.name}" if record_name
969
+ else func.name)
970
+ repr_kind = "generator" if self._is_generator_shape() else "coroutine"
971
+ param_struct_name = self._struct_name_templated(func, record_name)
972
+ out.write(f"\n{INDENT}friend std::ostream& operator<<("
973
+ f"std::ostream& os, const {param_struct_name}&) {{\n")
974
+ out.write(f"{INDENT}{INDENT}return os << "
975
+ f"\"<{repr_kind} {repr_label}>\";\n")
976
+ out.write(f"{INDENT}}}\n")
977
+ out.write(f"}};\n")
978
+
979
+ # -- Factory function -----------------------------------------------------
980
+
981
+ def gen_factory(self, out: "TextIO", func: TpyFunction) -> None:
982
+ """Emit the factory function: `__FCoro f(args) { return __FCoro(args); }`.
983
+
984
+ Uses the templated struct name explicitly so zero-param generic
985
+ async defs (no ctor args for CTAD to deduce T from) compile."""
986
+ struct_name = self._struct_name_templated(func)
987
+ self.ctx.emit_source_comment(out, func.loc)
988
+ self._emit_template_header(out, func)
989
+ params = self._emit_params_decl(func)
990
+ out.write(f"{struct_name} {escape_cpp_name(func.name)}({params}) {{\n")
991
+ args = self._factory_args_forwarded(func)
992
+ out.write(f"{INDENT}return {struct_name}({args});\n")
993
+ out.write(f"}}\n")
994
+
995
+ def _factory_args_forwarded(
996
+ self, func: TpyFunction,
997
+ *, receiver: tuple[str, str] | None = None) -> str:
998
+ """Format the arg list for a coro-struct factory call.
999
+
1000
+ Static-protocol params (`Own[Awaitable[T]]` etc.) take a
1001
+ forwarding-ref `T_<pname>&&` -- which, inside the factory body,
1002
+ is an lvalue -- so the call to the struct ctor (also `T_<pname>&&`)
1003
+ must wrap the name in `std::move(...)` to bind. Other kinds pass
1004
+ by bare name.
1005
+
1006
+ `receiver=(record_name, recv_expr)` supports async methods: the
1007
+ receiver is prepended to the arg list as `recv_expr` (typically
1008
+ `"*this"`), and `__self` is filtered out of the classified
1009
+ params so it isn't double-emitted. None for free async fns.
1010
+ """
1011
+ record_name, recv_expr = receiver if receiver is not None else (None, None)
1012
+ parts: list[str] = []
1013
+ if recv_expr is not None:
1014
+ parts.append(recv_expr)
1015
+ for cparam in self._classify_params(func, record_name):
1016
+ if cparam.cpp_name == "__self":
1017
+ continue
1018
+ if cparam.kind in (_CoroParamKind.STATIC_PROTOCOL,
1019
+ _CoroParamKind.OWNED_VALUE):
1020
+ parts.append(f"std::move({cparam.cpp_name})")
1021
+ else:
1022
+ parts.append(cparam.cpp_name)
1023
+ return ", ".join(parts)
1024
+
1025
+ # -- poll() body ----------------------------------------------------------
1026
+
1027
+ @contextlib.contextmanager
1028
+ def _resumable_frame_ctx(self, func: TpyFunction, record_name: str | None):
1029
+ """Set up + tear down resumable-frame ctx state for an async body.
1030
+
1031
+ Routes through `StatementGenerator.setup_body_scope` so async bodies
1032
+ get the same per-scope state setup as sync (reassigned_vars,
1033
+ aliased_vars, movable_locals, etc. populated from sema scan).
1034
+ Layers the resumable-frame fields (`generator_field_names`,
1035
+ `generator_self_ref`, etc.) plus `in_generator_body=True` on top.
1036
+ """
1037
+ old_in_gen = self.ctx.in_generator_body
1038
+ old_field_names = self.ctx.generator_field_names
1039
+ old_optional_fields = self.ctx.generator_optional_fields
1040
+ old_frame_slot_locals = self.ctx.generator_frame_slot_locals
1041
+ old_borrow_form_loop_vars = self.ctx.generator_borrow_form_loop_vars
1042
+ old_for_info = self.ctx.generator_for_loop_info
1043
+ old_self_ref = self.ctx.generator_self_ref
1044
+ old_movable_locals = self.ctx.movable_locals
1045
+ old_in_method = self.ctx.in_method
1046
+ old_method_record = self.ctx.current_method_record_type
1047
+ # `setup_body_scope` -> `reset_scope` wipes `pointer_locals` and
1048
+ # `current_type_param_bounds` on entry, so the inner body sees a
1049
+ # clean state. Save/restore here so a future caller that nests sync
1050
+ # body emission around a coro doesn't see inner-coro state leak out
1051
+ # on exit.
1052
+ old_pointer_locals = self.ctx.pointer_locals
1053
+ old_type_param_bounds = self.ctx.current_type_param_bounds
1054
+
1055
+ # Reset frame-specific fields before setup_body_scope, since the
1056
+ # `setup_resumable_frame_locals` call inside it reads
1057
+ # `generator_for_loop_info` and writes to `generator_optional_fields`.
1058
+ self.ctx.generator_field_names = set()
1059
+ self.ctx.generator_optional_fields = set()
1060
+ self.ctx.generator_frame_slot_locals = set()
1061
+ self.ctx.generator_borrow_form_loop_vars = set()
1062
+ # Populated by `_prescan_resumable_for_loops`; carries
1063
+ # `pointer_form_loop_var` so `setup_resumable_frame_locals` (called
1064
+ # inside `setup_body_scope` below) seeds non-value loop vars into
1065
+ # `pointer_locals`. Empty for bodies with no CFG-decomposed for-loop.
1066
+ self.ctx.generator_for_loop_info = rcfg.resumable_state(func).for_loop_info
1067
+
1068
+ local_ns = Namespace(parent=self.ctx.analyzer.global_ns)
1069
+ for pname, ptype in func.params:
1070
+ local_ns.bind_variable(pname, ptype)
1071
+
1072
+ crp, dcbp = self.functions.compute_body_const_sets(func, record_name)
1073
+ # Mirror sync `_gen_method`: a generator/async method on a generic
1074
+ # record needs the record's type-param bounds in scope so for-loop
1075
+ # over `self.items: T` resolves T to its protocol bound (otherwise
1076
+ # falls back to the universal `::tpy::__iter__` path, missing the
1077
+ # direct-iterator / native-iterable peepholes and breaking move-only
1078
+ # `Iterator[X]` params).
1079
+ record_bounds = None
1080
+ if record_name:
1081
+ rec_info = self.ctx.analyzer.registry.get_record(record_name)
1082
+ if rec_info is not None and rec_info.type_param_bounds:
1083
+ record_bounds = rec_info.type_param_bounds
1084
+ self.statements.setup_body_scope(
1085
+ func.params, func.return_type, func, local_ns,
1086
+ indent_level=1, is_method=bool(record_name),
1087
+ const_ref_params=crp, deep_const_borrow_params=dcbp,
1088
+ owning_record_name=record_name,
1089
+ record_type_param_bounds=record_bounds,
1090
+ )
1091
+
1092
+ self.ctx.in_generator_body = True
1093
+ # Belt-and-suspenders: `setup_body_scope` registers Own[T] params
1094
+ # as movable when `T.is_value_type()` is False, which already
1095
+ # covers most static-protocol shapes. Static-protocol frame
1096
+ # fields (e.g. `coro: Own[Awaitable[T]]` stored as the deduced
1097
+ # `T_coro`) are move-only by construction; force-register them
1098
+ # so the call-arg generator emits `std::move(coro)` rather than
1099
+ # a copy-into-temp + move, independent of how the protocol's
1100
+ # `is_value_type` resolves.
1101
+ for cparam in self._classify_params(func, record_name):
1102
+ if cparam.kind in (_CoroParamKind.STATIC_PROTOCOL,
1103
+ _CoroParamKind.OWNED_VALUE):
1104
+ self.ctx.movable_locals.add(cparam.cpp_name)
1105
+ if record_name:
1106
+ self.ctx.generator_self_ref = "__self"
1107
+ self.ctx.generator_field_names.add("__self")
1108
+ else:
1109
+ self.ctx.generator_self_ref = None
1110
+ for pname, _ in func.params:
1111
+ self.ctx.generator_field_names.add(pname)
1112
+ if func.generator_locals:
1113
+ for lname, _ltype in func.generator_locals:
1114
+ self.ctx.generator_field_names.add(lname)
1115
+
1116
+ try:
1117
+ yield
1118
+ finally:
1119
+ self.ctx.in_generator_body = old_in_gen
1120
+ self.ctx.generator_field_names = old_field_names
1121
+ self.ctx.generator_optional_fields = old_optional_fields
1122
+ self.ctx.generator_frame_slot_locals = old_frame_slot_locals
1123
+ self.ctx.generator_borrow_form_loop_vars = old_borrow_form_loop_vars
1124
+ self.ctx.generator_for_loop_info = old_for_info
1125
+ self.ctx.generator_self_ref = old_self_ref
1126
+ self.ctx.movable_locals = old_movable_locals
1127
+ self.ctx.in_method = old_in_method
1128
+ self.ctx.current_method_record_type = old_method_record
1129
+ self.ctx.pointer_locals = old_pointer_locals
1130
+ self.ctx.current_type_param_bounds = old_type_param_bounds
1131
+
1132
+ def gen_coro_finally_top_def(self, out: "TextIO", func: TpyFunction,
1133
+ record_name: str | None = None) -> None:
1134
+ """Emit member-function bodies for every `__finally_<n>()` helper
1135
+ the CFG produced (one per TryRegion with a finally body). No-op
1136
+ if the function has no finally bodies.
1137
+ """
1138
+ cfg = self._build_resumable_cfg(func)
1139
+ if not cfg.finally_helpers:
1140
+ return
1141
+ struct_name = self._struct_name_templated(func, record_name)
1142
+
1143
+ with self._resumable_frame_ctx(func, record_name):
1144
+ for helper_name, body_stmts in cfg.finally_helpers:
1145
+ self._emit_template_header(out, func, record_name=record_name)
1146
+ out.write(f"void {struct_name}::{helper_name}() {{\n")
1147
+ self.ctx.indent_level = 1
1148
+ with self._generator_finally_helper_scope():
1149
+ for stmt in body_stmts:
1150
+ self.statements.gen_stmt(out, stmt)
1151
+ self.ctx.indent_level = 0
1152
+ out.write(f"}}\n")
1153
+
1154
+ # =====================================================================
1155
+ # Resumable-shape policy seam. These methods isolate the decisions
1156
+ # that differ between the async (`await` -> `__poll__`) shape and the
1157
+ # future generator (`yield` -> `__next__`) shape so the generator
1158
+ # migration can plug in without forking the state-machine emitter.
1159
+ # Each seam branches on `self._shape`; async output must stay
1160
+ # byte-identical (async never enters the generator branch).
1161
+ # =====================================================================
1162
+
1163
+ def _resumable_ret_type_cpp(self, func: TpyFunction) -> str:
1164
+ """The frame body method's return type. Async: `Poll<T>`.
1165
+ Generator: `std::expected<T_slot, ::tpy::StopIteration>` where
1166
+ T_slot is the iterator slot type from
1167
+ `gen_generators._iter_slot_for_yield` -- borrow form
1168
+ (`std::tuple<T&, ...>` / `std::tuple<P*, ...>`) for tuple yields,
1169
+ bare cpp type otherwise. `gen_yield_value` already bridges
1170
+ storage-form sources into the borrow-form slot when
1171
+ `ctx.current_yield_type` is set (done by
1172
+ `_resumable_return_lowering`)."""
1173
+ if self._is_generator_shape():
1174
+ yt = func.generator_yield_type
1175
+ elem_cpp = self.types.type_to_cpp(yt)
1176
+ slot_cpp = GeneratorCodegen._iter_slot_for_yield(yt, elem_cpp)
1177
+ return f"std::expected<{slot_cpp}, ::tpy::StopIteration>"
1178
+ return self._poll_ret_cpp(func)
1179
+
1180
+ def _resumable_body_method_decl(self, func: TpyFunction,
1181
+ struct_name: str) -> str:
1182
+ """Full declarator of the frame's body method: return type +
1183
+ qualified name + params. Async: `Poll<T> <struct>::__poll__(Waker
1184
+ waker)`. Generator: `expected<T, StopIteration> <struct>::__next__()`
1185
+ (no waker)."""
1186
+ if self._is_generator_shape():
1187
+ return (f"{self._resumable_ret_type_cpp(func)} "
1188
+ f"{struct_name}::__next__()")
1189
+ return (f"{self._poll_ret_cpp(func)} {struct_name}::__poll__("
1190
+ f"::tpystd::coro::Waker waker)")
1191
+
1192
+ def _emit_resumable_body_prelude(self, out: "TextIO",
1193
+ has_suspensions: bool) -> None:
1194
+ """Per-shape body prelude. Async silences the unused `waker`
1195
+ param when the frame has no suspensions; the generator shape has
1196
+ no waker param, so it emits nothing."""
1197
+ if self._is_generator_shape():
1198
+ return
1199
+ if not has_suspensions:
1200
+ out.write(f"{INDENT}(void)waker;\n")
1201
+
1202
+ def _emit_resumable_done_case(self, out: "TextIO", inner: str) -> None:
1203
+ """The terminal `S_DONE` switch case. Async panics on a re-poll
1204
+ after Ready; the generator returns `StopIteration` (a repeat
1205
+ `__next__()` after exhaustion is well-defined in Python)."""
1206
+ if self._is_generator_shape():
1207
+ out.write(f"{inner}case S_DONE: return "
1208
+ f"::tpy::make_unexpected(::tpy::StopIteration{{}});\n")
1209
+ return
1210
+ out.write(f"{inner}case S_DONE: "
1211
+ f"::tpy::tpy_panic(\"poll after Ready\");\n")
1212
+
1213
+ @contextlib.contextmanager
1214
+ def _resumable_return_lowering(self, func: TpyFunction):
1215
+ """Configure how a source-level `return` lowers inside the frame
1216
+ body, then restore (policy seam). Async: the statement emitter
1217
+ consults `ctx.in_async_coro_body` to rewrite `return v` into
1218
+ `__state = S_DONE; return Poll::ready(v)`. The generator shape
1219
+ lowers a bare `return` to the StopIteration/done path and rejects
1220
+ return-with-value."""
1221
+ if self._is_generator_shape():
1222
+ old_in_gen = self.ctx.in_generator_resumable_body
1223
+ old_done = self.ctx.generator_resumable_done_state
1224
+ old_yt = self.ctx.current_yield_type
1225
+ self.ctx.in_generator_resumable_body = True
1226
+ self.ctx.generator_resumable_done_state = "S_DONE"
1227
+ self.ctx.current_yield_type = func.generator_yield_type
1228
+ try:
1229
+ yield
1230
+ finally:
1231
+ self.ctx.in_generator_resumable_body = old_in_gen
1232
+ self.ctx.generator_resumable_done_state = old_done
1233
+ self.ctx.current_yield_type = old_yt
1234
+ return
1235
+ old_in_async = getattr(self.ctx, "in_async_coro_body", False)
1236
+ old_async_ret_cpp = getattr(self.ctx, "async_coro_return_cpp", None)
1237
+ old_async_done_label = getattr(self.ctx, "async_coro_done_state", None)
1238
+ self.ctx.in_async_coro_body = True
1239
+ self.ctx.async_coro_return_cpp = self._ret_cpp(func)
1240
+ self.ctx.async_coro_done_state = "S_DONE"
1241
+ self.ctx.current_return_type = func.return_type
1242
+ try:
1243
+ yield
1244
+ finally:
1245
+ self.ctx.in_async_coro_body = old_in_async
1246
+ self.ctx.async_coro_return_cpp = old_async_ret_cpp
1247
+ self.ctx.async_coro_done_state = old_async_done_label
1248
+
1249
+ @contextlib.contextmanager
1250
+ def _generator_finally_helper_scope(self):
1251
+ """While emitting a `__finally_<n>()` helper body of a generator,
1252
+ `ctx.in_generator_finally_helper` makes `return` lower to
1253
+ `this->__finally_stop = true; return;` (void) instead of `goto __done`.
1254
+ No-op for the async shape (helpers there return Poll)."""
1255
+ old_in_helper = self.ctx.in_generator_finally_helper
1256
+ if self._is_generator_shape():
1257
+ self.ctx.in_generator_finally_helper = True
1258
+ try:
1259
+ yield
1260
+ finally:
1261
+ self.ctx.in_generator_finally_helper = old_in_helper
1262
+
1263
+ @contextlib.contextmanager
1264
+ def _generator_finally_stop_scope(self, has_finally_stop: bool):
1265
+ """While emitting a generator state-machine body, expose whether any
1266
+ finally helper contains a `return` (so the helper call sites append the
1267
+ `if (this->__finally_stop) return StopIteration;` check), then restore."""
1268
+ old_has_finally_stop = self.ctx.generator_has_finally_stop
1269
+ self.ctx.generator_has_finally_stop = has_finally_stop
1270
+ try:
1271
+ yield
1272
+ finally:
1273
+ self.ctx.generator_has_finally_stop = old_has_finally_stop
1274
+
1275
+ def _emit_resumable_extra_state_fields(self, out: "TextIO") -> None:
1276
+ """Per-shape state fields beyond the shared `int32_t __state`.
1277
+ Async adds the cancellation flag; the generator shape adds
1278
+ nothing."""
1279
+ if self._is_generator_shape():
1280
+ return
1281
+ out.write(f"{INDENT}bool __cancel_pending;\n")
1282
+
1283
+ def _resumable_extra_ctor_inits(self) -> list[str]:
1284
+ """Per-shape ctor member-init entries beyond `__state(S_INITIAL)`.
1285
+ Async initializes the cancel flag; the generator shape adds none.
1286
+ """
1287
+ if self._is_generator_shape():
1288
+ return []
1289
+ return ["__cancel_pending(false)"]
1290
+
1291
+ def _emit_resumable_sub_future_fields(
1292
+ self, out: "TextIO", func: TpyFunction,
1293
+ yields: 'list[rcfg.Yield]') -> None:
1294
+ """Sub-future frame fields -- async-only (policy seam). One per
1295
+ Yield (suspension_index = field ordinal): Inline ->
1296
+ optional<sub-coro struct>, Erased -> optional<value awaitable>,
1297
+ Borrowed -> raw pointer. Async-with's synthetic yields override
1298
+ sub_field_cpp_type via the state.async_with_struct_names map (the CM's
1299
+ __aenter__/__aexit__ coro struct, computed at prescan time). The
1300
+ generator shape stores nothing at a `yield`, so it emits nothing.
1301
+ """
1302
+ if self._is_generator_shape():
1303
+ return
1304
+ state = rcfg.resumable_state(func)
1305
+ struct_names = state.async_with_struct_names
1306
+ for_struct_names = state.async_for_struct_names
1307
+ for y in yields:
1308
+ p = y.payload
1309
+ sub_cpp = p.sub_field_cpp_type
1310
+ if p.async_with_kind is not None and p.async_with_ctx_n is not None:
1311
+ entry = struct_names.get(p.async_with_ctx_n)
1312
+ if entry is not None:
1313
+ sub_cpp = entry[0] if p.async_with_kind is rcfg.AsyncWithKind.AENTER else entry[1]
1314
+ elif p.async_for_uid is not None:
1315
+ entry = for_struct_names.get(p.async_for_uid)
1316
+ if entry is not None:
1317
+ sub_cpp = entry
1318
+ if p.mode is rcfg.AwaitMode.BORROWED:
1319
+ out.write(f"{INDENT}{sub_cpp}* "
1320
+ f"__sub_{y.suspension_index} = nullptr;\n")
1321
+ else:
1322
+ out.write(f"{INDENT}std::optional<{sub_cpp}> "
1323
+ f"__sub_{y.suspension_index};\n")
1324
+
1325
+ def _resumable_body_method_fwd_decl(self, func: TpyFunction) -> str:
1326
+ """In-struct forward declaration of the body method (unqualified).
1327
+ Async: `Poll<T> __poll__(Waker waker)`. Generator:
1328
+ `expected<T, StopIteration> __next__()`."""
1329
+ if self._is_generator_shape():
1330
+ return f"{self._resumable_ret_type_cpp(func)} __next__()"
1331
+ return (f"{self._poll_ret_cpp(func)} "
1332
+ f"__poll__(::tpystd::coro::Waker waker)")
1333
+
1334
+ def _emit_resumable_extra_methods(self, out: "TextIO",
1335
+ struct_name: str) -> None:
1336
+ """Per-shape inline methods on the frame struct. Async emits
1337
+ `cancel()`; the generator emits `__iter__()` (returns `*this`, so
1338
+ the frame is its own iterator -- parallels gen_generators)."""
1339
+ if self._is_generator_shape():
1340
+ out.write(f"{INDENT}{struct_name}& __iter__() "
1341
+ f"{{ return *this; }}\n")
1342
+ return
1343
+ out.write(f"{INDENT}void cancel() {{ __cancel_pending = true; }}\n")
1344
+
1345
+ def gen_coro_poll_def(self, out: "TextIO", func: TpyFunction,
1346
+ record_name: str | None = None) -> None:
1347
+ """Emit the `poll()` method body in the .cpp file (or inline-in-hpp
1348
+ for templates -- the caller handles placement). When `record_name`
1349
+ is given, the struct is `__coro_<Record>_<func>` and the body
1350
+ sees `self.X` as `__self.X` (parallels generator methods).
1351
+ """
1352
+ struct_name = self._struct_name_templated(func, record_name)
1353
+ cfg = self._build_resumable_cfg(func)
1354
+ has_yields = bool(cfg.yield_sites)
1355
+
1356
+ self.ctx.emit_source_comment(out, func.loc)
1357
+ self._emit_template_header(out, func, record_name=record_name)
1358
+ out.write(f"{self._resumable_body_method_decl(func, struct_name)} {{\n")
1359
+ self._emit_resumable_body_prelude(out, has_yields)
1360
+
1361
+ # Return-rewrite setup (the C++ Poll<T> type and DONE state
1362
+ # label so `return v` lowers correctly inside the state machine)
1363
+ # is the resumable-shape policy's responsibility.
1364
+ has_finally_stop = (
1365
+ self._is_generator_shape()
1366
+ and any(_stmts_have_return(body) for _, body in cfg.finally_helpers))
1367
+ with self._generator_finally_stop_scope(has_finally_stop):
1368
+ with self._resumable_frame_ctx(func, record_name):
1369
+ with self._resumable_return_lowering(func):
1370
+ self._emit_state_machine(out, func, cfg)
1371
+
1372
+ out.write(f"}}\n")
1373
+
1374
+ # =====================================================================
1375
+ # CFG-based state-machine emitter (replaces _emit_switch_body).
1376
+ # =====================================================================
1377
+
1378
+ def _build_resumable_cfg(self, func: TpyFunction) -> 'rcfg.CFG':
1379
+ """Apply the await-lift pre-pass, then build the CFG. The CFG
1380
+ builder handles any wrapping try/finally uniformly with all
1381
+ other compound statements -- no special unwrap-and-rewrap pass
1382
+ is needed.
1383
+
1384
+ A `_CFGNotYetSupported` (a shape the resumable lowering can't yet
1385
+ handle) is turned into a `CodeGenError` at the offending location.
1386
+ This is the authoritative backstop so no unsupported shape silently
1387
+ miscompiles. The result is cached on the func for the emit pass."""
1388
+ state = rcfg.resumable_state(func)
1389
+ if state.cfg is not None:
1390
+ return state.cfg
1391
+ body = self._effective_body(func)
1392
+ try:
1393
+ for_uid_map = self._prescan_resumable_for_loops(func, body)
1394
+ with_uid_map = self._prescan_with_stmts(func, body)
1395
+ try_finally_uid_map = self._prescan_resumable_try_finally(func, body)
1396
+ builder = rcfg.CFGBuilder(
1397
+ payload_factory=self._make_await_payload,
1398
+ for_uid_map=for_uid_map,
1399
+ with_uid_map=with_uid_map,
1400
+ try_finally_uid_map=try_finally_uid_map,
1401
+ func_returns_void=self._is_void_return(func),
1402
+ )
1403
+ cfg = builder.build(body)
1404
+ except rcfg._CFGNotYetSupported as e:
1405
+ raise CodeGenError(e.msg, loc=e.loc)
1406
+ # Stash the builder so callers (emit) can look up handler
1407
+ # entries via builder.get_handler_entry().
1408
+ state.cfg_builder = builder
1409
+ state.cfg = cfg
1410
+ return cfg
1411
+
1412
+ def _prescan_resumable_for_loops(
1413
+ self, func: TpyFunction,
1414
+ body: list[TpyStmt]) -> dict[int, int]:
1415
+ """Find for-loops whose body contains await OR are `async for`
1416
+ (M6). For each, allocate a uid, register frame-field declarations
1417
+ and register the loop variable as a hoisted local so it persists
1418
+ across suspensions.
1419
+
1420
+ Sync for-with-await (M3.1): two slots,
1421
+ `(__for_itr_N: decltype(::tpy::__iter__(it)),
1422
+ __for_r_N: decltype(itr.__next__()))`.
1423
+
1424
+ Async-for (M6): one slot,
1425
+ `__for_itr_N: decltype(it.__aiter__())`. No `__for_r_N` slot --
1426
+ the advance is a Yield(await __anext__()) and the unwrapped
1427
+ value goes straight to the loop var via the standard resume-bind
1428
+ path. Also populates `state.async_for_struct_names[uid]` with
1429
+ the C++ name of the `__anext__` sub-coro struct so the Yield
1430
+ emit can size `__sub_<i>` and emplace it.
1431
+
1432
+ Returns {id(TpyForEach) -> uid} for the CFG builder. Frame fields
1433
+ live on `state.for_fields` as `[(name, cpp_type)]` consumed
1434
+ by gen_coro_struct (see `ResumableFuncState`).
1435
+ """
1436
+ state = rcfg.resumable_state(func)
1437
+ if state.for_prescanned:
1438
+ return state.for_uid_map
1439
+ uid_map: dict[int, int] = {}
1440
+ fields_out: list[tuple[str, str]] = []
1441
+ struct_names_out: dict[int, str] = {}
1442
+ # {id(TpyForEach) -> GeneratorForInfo}: carries pointer_form_loop_var
1443
+ # so `setup_resumable_frame_locals` + the struct field emit render a
1444
+ # non-value loop var as an aliasing `T*` rather than a `frame_slot<T>`
1445
+ # value copy.
1446
+ for_loop_info: dict[int, GeneratorForInfo] = {}
1447
+ info_by_uid: dict[int, GeneratorForInfo] = {}
1448
+ counter = [0]
1449
+ # Loop-var hoists are accumulated here and applied to
1450
+ # func.generator_locals only after the walk completes -- if the walk
1451
+ # raises `_CFGNotYetSupported` mid-way (an unsupported for-loop
1452
+ # shape) the func falls back to the legacy emitter, which must not
1453
+ # inherit partially-appended loop-var entries (they would emit as
1454
+ # duplicate frame fields alongside the legacy for-loop fields).
1455
+ local_hoists: list[tuple[str, "TpyType"]] = []
1456
+
1457
+ def walk(stmts: list[TpyStmt]) -> None:
1458
+ for s in stmts:
1459
+ if isinstance(s, TpyForEach) and (
1460
+ s.is_async
1461
+ or rcfg._stmts_have_any_suspension(s.body)
1462
+ or rcfg._stmts_have_any_suspension(s.orelse)):
1463
+ cur_uid = counter[0]
1464
+ counter[0] += 1
1465
+ uid_map[id(s)] = cur_uid
1466
+ if s.is_async:
1467
+ # async-for (M6): one `__aiter__` iterator slot; the
1468
+ # advance is a Yield(await __anext__), not a strategy.
1469
+ iter_t = self.types.get_resolved_type(s.iterable)
1470
+ if iter_t is None:
1471
+ raise CodeGenError(
1472
+ "for-loop iterable has no resolved type "
1473
+ "(async for pre-scan)", loc=s.loc)
1474
+ src_cpp = self.types.type_to_cpp(unwrap_ref_type(iter_t))
1475
+ iter_field_type = (
1476
+ f"std::decay_t<decltype(std::declval<"
1477
+ f"{src_cpp}&>().__aiter__())>")
1478
+ fields_out.append(
1479
+ (f"__for_itr_{cur_uid}", iter_field_type))
1480
+ if s.async_aiter_type is None:
1481
+ raise CodeGenError(
1482
+ "async-for missing resolved aiter type "
1483
+ "(internal: sema didn't populate "
1484
+ "stmt.async_aiter_type)", loc=s.loc)
1485
+ struct_names_out[cur_uid] = self._sub_struct_qualname(
1486
+ s.async_aiter_type, "__anext__")
1487
+ info = GeneratorForInfo(
1488
+ uid=cur_uid, strategy="async_for", fields=[])
1489
+ else:
1490
+ # Sync for-loop: reuse the legacy strategy analysis so
1491
+ # range / begin_end peepholes (a plain counter / begin-
1492
+ # end iterators) carry over instead of the slower
1493
+ # universal `__iter__`/`__next__` shape. The fields it
1494
+ # returns are emitted as `tpy::frame_slot<T>` in
1495
+ # gen_coro_struct (matching hoisted user locals).
1496
+ info = self.gen_generators._analyze_for_strategy(
1497
+ s, cur_uid)
1498
+ if info is None:
1499
+ raise rcfg._CFGNotYetSupported(
1500
+ "yield inside this for-loop shape is not yet "
1501
+ "supported on the resumable path.", loc=s.loc)
1502
+ fields_out.extend(info.fields)
1503
+ for_loop_info[id(s)] = info
1504
+ info_by_uid[cur_uid] = info
1505
+ elem_t = (unwrap_ref_type(s.elem_type)
1506
+ if s.elem_type else None)
1507
+ if elem_t is None:
1508
+ raise CodeGenError(
1509
+ "for-with-suspension: loop var has no "
1510
+ "resolved elem_type", loc=s.loc)
1511
+ local_hoists.append((s.var, elem_t))
1512
+ if hasattr(s, "sub_bodies"):
1513
+ for b in s.sub_bodies():
1514
+ walk(b)
1515
+
1516
+ walk(body)
1517
+ # Walk succeeded: commit the loop-var hoists.
1518
+ if local_hoists:
1519
+ if func.generator_locals is None:
1520
+ func.generator_locals = []
1521
+ existing = {n for n, _ in func.generator_locals}
1522
+ for name, elem_t in local_hoists:
1523
+ if name not in existing:
1524
+ func.generator_locals.append((name, elem_t))
1525
+ existing.add(name)
1526
+ state.for_uid_map = uid_map
1527
+ state.for_fields = fields_out
1528
+ state.async_for_struct_names = struct_names_out
1529
+ state.for_loop_info = for_loop_info
1530
+ state.for_info_by_uid = info_by_uid
1531
+ state.for_prescanned = True
1532
+ return uid_map
1533
+
1534
+ def _sub_struct_qualname(
1535
+ self, owner: 'NominalType | None', method: str,
1536
+ inferred_type_args: 'tuple[TpyType, ...] | None' = None,
1537
+ *, module_qual: str | None = None,
1538
+ extra_template_args: 'list[str] | None' = None) -> str:
1539
+ """Build the C++ name of the sub-coro struct generated for a
1540
+ statically-resolved await (free function or method).
1541
+
1542
+ Free function (owner=None, same module): `__coro_<name>[<inferred_args>]`.
1543
+ Free function (owner=None, cross-module via module_qual):
1544
+ `<callee_ns>::__coro_<name>[<inferred_args>]`.
1545
+ Method on non-generic class: `<ns>::__coro_<Record>_<name>
1546
+ [<inferred_args>]`.
1547
+ Method on generic class: `<ns>::__coro_<Record>_<name>
1548
+ <owner_type_args>`. (The class-generic + method-generic
1549
+ case is currently rejected at sema, so owner_type_args and
1550
+ inferred_type_args are not composed today.)
1551
+
1552
+ `extra_template_args` appends concrete C++ types (typically
1553
+ `std::remove_cvref_t<decltype(arg)>`) for each static-protocol
1554
+ param on the callee -- these correspond to the `T_<pname>`
1555
+ template args declared on the callee's struct.
1556
+ """
1557
+ ns_qual = ""
1558
+ owner_name = None
1559
+ owner_args_suffix = ""
1560
+ if owner is not None:
1561
+ owner_cpp = self.types.type_to_cpp(owner)
1562
+ ns_prefix = owner_cpp.split("<", 1)[0]
1563
+ ns_qual = (ns_prefix.rsplit("::", 1)[0] + "::"
1564
+ if "::" in ns_prefix else "")
1565
+ owner_name = owner.name
1566
+ if owner.type_args:
1567
+ inner_cpps = [self.types.type_to_cpp(ta)
1568
+ for ta in owner.type_args]
1569
+ owner_args_suffix = "<" + ", ".join(inner_cpps) + ">"
1570
+ elif module_qual is not None:
1571
+ # Cross-module free-function await: qualify with the
1572
+ # callee module's C++ namespace.
1573
+ ns_qual = f"::{module_to_cpp_namespace(module_qual)}::"
1574
+ bare = AsyncCoroCodegen._sub_struct_name(method, owner_name)
1575
+ # Combined template-arg list: callee's explicit `[T1, ...]` from
1576
+ # the call's inferred substitution, followed by any
1577
+ # `T_<pname>` extras deduced from static-protocol args.
1578
+ all_args: list[str] = []
1579
+ if inferred_type_args and not (owner is not None and owner.type_args):
1580
+ all_args.extend(self.types.type_to_cpp(ta)
1581
+ for ta in inferred_type_args)
1582
+ if extra_template_args:
1583
+ all_args.extend(extra_template_args)
1584
+ suffix = ("<" + ", ".join(all_args) + ">") if all_args else ""
1585
+ return f"{ns_qual}{bare}{owner_args_suffix}{suffix}"
1586
+
1587
+ def _extra_template_args_for_await(self, call: TpyExpr) -> list[str]:
1588
+ """Compute the per-static-protocol `T_<pname>` template-arg
1589
+ spellings for the sub-coro struct of an inline await.
1590
+
1591
+ Each callee static-protocol param gets one extra template arg
1592
+ on the struct (see `_protocol_template_parts`); at the call
1593
+ site that arg is the concrete type of the corresponding
1594
+ argument, spelled as `std::remove_cvref_t<decltype(<arg>)>` so
1595
+ the compiler deduces it without us having to spell it.
1596
+ Temps queued by gen_expr are discarded -- decltype doesn't
1597
+ evaluate, and the same arg's gen_expr will re-run at emplace
1598
+ time when the temps are actually needed.
1599
+ """
1600
+ if not isinstance(call, (TpyCall, TpyMethodCall)):
1601
+ return []
1602
+ fi = call.resolved_function_info
1603
+ if fi is None or not fi.params:
1604
+ return []
1605
+ if not any(
1606
+ self.functions.protocols.is_static_protocol_param(p.type)
1607
+ for p in fi.params):
1608
+ return []
1609
+ out: list[str] = []
1610
+ checkpoint = self.ctx.temps.checkpoint()
1611
+ for i, pinfo in enumerate(fi.params):
1612
+ if not self.functions.protocols.is_static_protocol_param(pinfo.type):
1613
+ continue
1614
+ if i >= len(call.args):
1615
+ # Defensive: callee param without a corresponding arg
1616
+ # at the call site (defaults aren't supported on async
1617
+ # static-protocol params today; bail rather than emit
1618
+ # a malformed template arg).
1619
+ self.ctx.temps.rollback_to(checkpoint)
1620
+ return []
1621
+ arg_cpp = self.expressions.gen_expr(call.args[i])
1622
+ out.append(f"std::remove_cvref_t<decltype({arg_cpp})>")
1623
+ self.ctx.temps.rollback_to(checkpoint)
1624
+ return out
1625
+
1626
+
1627
+ def _prescan_with_stmts(
1628
+ self, func: TpyFunction,
1629
+ body: list[TpyStmt]) -> dict[int, list[int]]:
1630
+ """Find `with` stmts whose body contains await. For each, allocate
1631
+ a ctx_n per WithItem, declare a `__with_ctx_<n>` frame field of
1632
+ the context-manager's C++ type, and register any `as` target
1633
+ names as hoisted locals.
1634
+
1635
+ Returns {id(TpyWith) -> [ctx_n_per_item]}; frame fields land on
1636
+ `state.with_fields` as `[(name, cpp_type)]`.
1637
+ """
1638
+ state = rcfg.resumable_state(func)
1639
+ if state.with_prescanned:
1640
+ return state.with_uid_map
1641
+ uid_map: dict[int, list[int]] = {}
1642
+ fields_out: list[tuple[str, str]] = []
1643
+ counter = [0]
1644
+
1645
+ # Async-with sub-coro struct names by ctx_n. Populated alongside
1646
+ # the per-ctx frame field. Emit uses this to size __sub_<i>
1647
+ # slots and to write the inline factory call.
1648
+ struct_names_out: dict[int, tuple[str, str]] = {}
1649
+
1650
+ def walk(stmts: list[TpyStmt]) -> None:
1651
+ for s in stmts:
1652
+ # Sync `with` only needs the frame slot when its body
1653
+ # contains an await (M3.2). `async with` always needs
1654
+ # one because `__aenter__` / `__aexit__` are themselves
1655
+ # the suspensions, regardless of whether the body has
1656
+ # any other await.
1657
+ if (isinstance(s, TpyWith)
1658
+ and (s.is_async
1659
+ or rcfg._stmts_have_any_suspension(s.body))):
1660
+ per_item: list[int] = []
1661
+ for item in s.items:
1662
+ cur_n = counter[0]
1663
+ counter[0] += 1
1664
+ per_item.append(cur_n)
1665
+ ctx_t = self.types.get_resolved_type(item.context_expr)
1666
+ if ctx_t is None:
1667
+ raise CodeGenError(
1668
+ "with-stmt context manager has no resolved "
1669
+ "type (async with-with-await pre-scan)",
1670
+ loc=s.loc)
1671
+ ctx_cpp = self.types.type_to_cpp(unwrap_ref_type(ctx_t))
1672
+ fields_out.append((f"__with_ctx_{cur_n}", ctx_cpp))
1673
+ # For async-with, compute the qualified C++ names
1674
+ # of the __aenter__ / __aexit__ coro structs so
1675
+ # struct-emit and _emit_suspend can look them up
1676
+ # by ctx_n without needing the types helper at
1677
+ # the CFG layer.
1678
+ if s.is_async:
1679
+ ctx_inner = unwrap_ref_type(ctx_t)
1680
+ if not isinstance(ctx_inner, NominalType):
1681
+ raise CodeGenError(
1682
+ "async-with context manager type is "
1683
+ "not a record (internal)",
1684
+ loc=s.loc)
1685
+ # Build the aenter/aexit sub-coro struct
1686
+ # names using the canonical struct namer
1687
+ # (same shape as M4 async-method coro
1688
+ # structs). The C++ namespace is derived
1689
+ # from the CM's qualified type by
1690
+ # stripping any template args first (so
1691
+ # `::ns::Foo<T>` yields a `::ns::` prefix,
1692
+ # not `::ns::Foo<T>::`). Type args are
1693
+ # re-attached as a suffix on the coro
1694
+ # struct reference -- the struct itself is
1695
+ # templated over the same T as the CM.
1696
+ # Generic CMs are rejected at sema today (see
1697
+ # registration.py); the helper handles type_args
1698
+ # so this prescan stays robust if that lifts.
1699
+ struct_names_out[cur_n] = (
1700
+ self._sub_struct_qualname(ctx_inner, "__aenter__"),
1701
+ self._sub_struct_qualname(ctx_inner, "__aexit__"),
1702
+ )
1703
+ if item.target is not None:
1704
+ enter_t = (unwrap_ref_type(item.enter_type)
1705
+ if item.enter_type else None)
1706
+ if enter_t is None:
1707
+ raise CodeGenError(
1708
+ "with-stmt target has no resolved "
1709
+ "enter_type", loc=s.loc)
1710
+ if func.generator_locals is None:
1711
+ func.generator_locals = []
1712
+ if not any(n == item.target
1713
+ for n, _ in func.generator_locals):
1714
+ func.generator_locals.append(
1715
+ (item.target, enter_t))
1716
+ # For str-typed as-targets, the C++
1717
+ # `__enter__()` / `__aenter__()` returns
1718
+ # `std::string` by value but sema's `str`
1719
+ # lowers to `std::string_view`. Tracking
1720
+ # the name here promotes the frame field's
1721
+ # storage type to `std::string` (owning)
1722
+ # so the field doesn't alias a temporary
1723
+ # that dies at the assignment's semicolon.
1724
+ # Applies to both sync `with` and async
1725
+ # `with`: the Poll<std::string>::value()
1726
+ # extraction in async-with's resume case
1727
+ # produces the same temporary shape as the
1728
+ # sync __enter__() return.
1729
+ resolved_enter_t = self.types.resolve_type(enter_t)
1730
+ if is_str_category(resolved_enter_t):
1731
+ state.with_owning_str_targets.add(item.target)
1732
+ uid_map[id(s)] = per_item
1733
+ if hasattr(s, "sub_bodies"):
1734
+ for b in s.sub_bodies():
1735
+ walk(b)
1736
+
1737
+ walk(body)
1738
+ state.with_uid_map = uid_map
1739
+ state.with_fields = fields_out
1740
+ state.async_with_struct_names = struct_names_out
1741
+ state.with_prescanned = True
1742
+ return uid_map
1743
+
1744
+ def _prescan_resumable_try_finally(
1745
+ self, func: TpyFunction,
1746
+ body: list[TpyStmt]) -> dict[int, int]:
1747
+ """Find try-stmts whose finally body contains an await. For
1748
+ each, allocate a uid and declare:
1749
+ * `__finally_exc_<n>` -- `std::exception_ptr` (always).
1750
+ * `__finally_pending_<n>` -- `bool` (only when try / handler /
1751
+ finally body contains a reachable `return`).
1752
+ * `__finally_ret_<n>` -- function's return type, for the
1753
+ same reason and only when the async def is non-void.
1754
+ Returns {id(TpyTry) -> uid}; field declarations land on
1755
+ `state.try_finally_fields`."""
1756
+ state = rcfg.resumable_state(func)
1757
+ if state.try_finally_prescanned:
1758
+ return state.try_finally_uid_map
1759
+ uid_map: dict[int, int] = {}
1760
+ fields_out: list[tuple[str, str]] = []
1761
+ counter = [0]
1762
+ is_void = self._is_void_return(func)
1763
+ ret_cpp = self._ret_cpp(func) if not is_void else None
1764
+
1765
+ def walk(stmts: list[TpyStmt]) -> None:
1766
+ for s in stmts:
1767
+ if (isinstance(s, TpyTry)
1768
+ and s.finally_body
1769
+ and rcfg._stmts_have_any_suspension(s.finally_body)):
1770
+ cur_uid = counter[0]
1771
+ counter[0] += 1
1772
+ uid_map[id(s)] = cur_uid
1773
+ fields_out.append(
1774
+ (f"__finally_exc_{cur_uid}", "std::exception_ptr"))
1775
+ has_return = (rcfg._stmts_have_any_return(s.try_body)
1776
+ or any(rcfg._stmts_have_any_return(h.body)
1777
+ for h in s.handlers)
1778
+ or rcfg._stmts_have_any_return(s.finally_body))
1779
+ if has_return:
1780
+ fields_out.append(
1781
+ (f"__finally_pending_{cur_uid}", "bool"))
1782
+ # Generator pending returns are always StopIteration;
1783
+ # no value slot needed (_ret_cpp would give Iterator[T]).
1784
+ if ret_cpp is not None and not self._is_generator_shape():
1785
+ fields_out.append(
1786
+ (f"__finally_ret_{cur_uid}", ret_cpp))
1787
+ # `async with` desugars to a try/finally where the
1788
+ # finally body is `await __cm.__aexit__(...)`. The
1789
+ # synthetic finally needs the same set of frame slots
1790
+ # as a user-written try/finally-with-await; allocate
1791
+ # them in the same shared uid pool here.
1792
+ if isinstance(s, TpyWith) and s.is_async:
1793
+ cur_uid = counter[0]
1794
+ counter[0] += 1
1795
+ uid_map[id(s)] = cur_uid
1796
+ fields_out.append(
1797
+ (f"__finally_exc_{cur_uid}", "std::exception_ptr"))
1798
+ if rcfg._stmts_have_any_return(s.body):
1799
+ fields_out.append(
1800
+ (f"__finally_pending_{cur_uid}", "bool"))
1801
+ if ret_cpp is not None:
1802
+ fields_out.append(
1803
+ (f"__finally_ret_{cur_uid}", ret_cpp))
1804
+ if hasattr(s, "sub_bodies"):
1805
+ for b in s.sub_bodies():
1806
+ walk(b)
1807
+
1808
+ walk(body)
1809
+ state.try_finally_uid_map = uid_map
1810
+ state.try_finally_fields = fields_out
1811
+ state.try_finally_prescanned = True
1812
+ return uid_map
1813
+
1814
+ def _make_await_payload(self, await_node: TpyAwait, host_stmt: TpyStmt,
1815
+ kind, bind_target, return_stmt) -> 'rcfg.AwaitPayload':
1816
+ """CFGBuilder payload factory: derives mode + sub_field_cpp_type
1817
+ from the await's sema annotations."""
1818
+ if await_node.awaited_async_func_name is not None:
1819
+ mode = rcfg.AwaitMode.INLINE
1820
+ inferred_type_args = getattr(
1821
+ await_node.value, "inferred_type_args", None)
1822
+ # Cross-module free-function await spelled `mod.func(...)`:
1823
+ # operand is a TpyMethodCall whose receiver is the module
1824
+ # (no class owner). Use the module qualifier to namespace
1825
+ # the sub-coro struct name.
1826
+ module_qual = None
1827
+ if (isinstance(await_node.value, TpyMethodCall)
1828
+ and await_node.awaited_method_owner_type is None):
1829
+ module_qual = (await_node.value.user_module_call
1830
+ or await_node.value.builtin_module_call)
1831
+ extra_template_args = self._extra_template_args_for_await(
1832
+ await_node.value)
1833
+ sub_cpp = self._sub_struct_qualname(
1834
+ await_node.awaited_method_owner_type,
1835
+ await_node.awaited_async_func_name,
1836
+ inferred_type_args,
1837
+ module_qual=module_qual,
1838
+ extra_template_args=extra_template_args)
1839
+ elif await_node.awaited_task_inner is not None:
1840
+ operand_type = self.ctx.get_expr_type(await_node.value)
1841
+ if operand_type is None:
1842
+ raise CodeGenError(
1843
+ "await operand has no analyzed type",
1844
+ loc=host_stmt.loc)
1845
+ operand_inner = unwrap_own(unwrap_ref_type(operand_type))
1846
+ sub_cpp = self.types.type_to_cpp(operand_inner)
1847
+ if operand_inner.is_value_type():
1848
+ mode = rcfg.AwaitMode.ERASED
1849
+ elif is_stable_address_lvalue(await_node.value):
1850
+ mode = rcfg.AwaitMode.BORROWED
1851
+ else:
1852
+ mode = rcfg.AwaitMode.ERASED
1853
+ else:
1854
+ raise CodeGenError(
1855
+ "await reached codegen without sema-resolved sub-future shape",
1856
+ loc=host_stmt.loc)
1857
+ return rcfg.AwaitPayload(
1858
+ mode=mode,
1859
+ sub_field_cpp_type=sub_cpp,
1860
+ operand_expr=await_node.value,
1861
+ kind=kind,
1862
+ bind_target=bind_target,
1863
+ return_stmt=return_stmt,
1864
+ host_stmt=host_stmt,
1865
+ await_node=await_node,
1866
+ )
1867
+
1868
+ def _compute_case_entries(self, cfg: 'rcfg.CFG') -> dict[int, _StateLabel]:
1869
+ """Return mapping bb_id -> StateLabel for every BB that needs its
1870
+ own case label in the emitted switch. Cached on the CFG so the
1871
+ struct-emit and poll-emit passes share the result.
1872
+
1873
+ Rules: a BB is a case entry iff it is
1874
+ - the entry BB (S_INITIAL),
1875
+ - the resume_bb of a Yield (S_RESUME_<i>),
1876
+ - reached via Fall/Branch by 2+ predecessors (multi-pred join), or
1877
+ - reached via Fall/Branch from a predecessor with a different
1878
+ region_stack (region-crossing edge).
1879
+
1880
+ Handler entries (reached only via C++ catch) are inlined into
1881
+ their enclosing try-region's catch and never get a case label.
1882
+ """
1883
+ if cfg._case_entries_cache is not None:
1884
+ return cfg._case_entries_cache
1885
+ case_entries: dict[int, _StateLabel] = {
1886
+ cfg.entry_bb: _StateLabel(_StateKind.INITIAL)
1887
+ }
1888
+ for y in cfg.yield_sites:
1889
+ case_entries[y.resume_bb] = _StateLabel(
1890
+ _StateKind.RESUME, y.suspension_index)
1891
+ # Compute predecessors via Fall/Branch/Yield-resume edges.
1892
+ # Yield.resume_bb already a case entry; tracking helps detect
1893
+ # multi-pred / region-crossing.
1894
+ preds: dict[int, list[int]] = {bid: [] for bid in cfg.blocks}
1895
+ for bid, bb in cfg.blocks.items():
1896
+ t = bb.terminator
1897
+ if isinstance(t, rcfg.Fall):
1898
+ preds[t.next_bb].append(bid)
1899
+ elif isinstance(t, rcfg.Branch):
1900
+ preds[t.then_bb].append(bid)
1901
+ preds[t.else_bb].append(bid)
1902
+ elif isinstance(t, rcfg.AsyncForAdvance):
1903
+ preds[t.has_value_bb].append(bid)
1904
+ preds[t.exhausted_bb].append(bid)
1905
+ elif isinstance(t, rcfg.Yield):
1906
+ preds[t.resume_bb].append(bid)
1907
+ # Multi-pred (excluding yield-resume, which is already case entry).
1908
+ join_idx = 0
1909
+ for bid, ps in preds.items():
1910
+ if bid in case_entries:
1911
+ continue
1912
+ if len(ps) >= 2:
1913
+ case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
1914
+ join_idx += 1
1915
+ # Region-crossing predecessors.
1916
+ for bid, bb in cfg.blocks.items():
1917
+ if bid in case_entries:
1918
+ continue
1919
+ for p in preds.get(bid, ()):
1920
+ p_bb = cfg.blocks[p]
1921
+ if p_bb.region_stack != bb.region_stack:
1922
+ case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
1923
+ join_idx += 1
1924
+ break
1925
+ # WithRegion suppression targets: any with whose __exit__ may
1926
+ # suppress an exception transitions state directly to
1927
+ # post_with_bb from inside the catch (not a normal CFG edge).
1928
+ # Mark every such post_with_bb as a case entry so the catch's
1929
+ # `__state = ...; continue;` lands on a labeled case.
1930
+ with_post_bbs: set[int] = set()
1931
+ for bb in cfg.blocks.values():
1932
+ for r in bb.region_stack:
1933
+ if (isinstance(r, rcfg.WithRegion)
1934
+ and r.item.exit_can_suppress):
1935
+ with_post_bbs.add(r.post_with_bb)
1936
+ for bid in with_post_bbs:
1937
+ if bid not in case_entries:
1938
+ case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
1939
+ join_idx += 1
1940
+ # CFG-based finally entry BBs: the TryRegion's catch-all
1941
+ # transitions state directly to `finally_entry_bb` from inside
1942
+ # the catch (not a normal CFG edge). The Fall on the normal
1943
+ # exit path region-crosses out of the TryRegion which already
1944
+ # makes this BB a case entry, but mark explicitly to cover the
1945
+ # "try body always raises" case (no Fall predecessor).
1946
+ # Same for CFG-based finally EXIT BBs: a `return` inside the
1947
+ # finally body transitions state directly here without a CFG
1948
+ # edge, so the BB must have a case label even if the
1949
+ # Fall-from-finally-end region-crossing already picked it up.
1950
+ finally_entry_bbs: set[int] = set()
1951
+ finally_exit_bbs: set[int] = set()
1952
+ for bb in cfg.blocks.values():
1953
+ for r in bb.region_stack:
1954
+ if (isinstance(r, rcfg.TryRegion)
1955
+ and r.finally_entry_bb is not None):
1956
+ finally_entry_bbs.add(r.finally_entry_bb)
1957
+ elif (isinstance(r, rcfg.FinallyRegion)
1958
+ and r.finally_exit_bb is not None):
1959
+ finally_exit_bbs.add(r.finally_exit_bb)
1960
+ for bid in finally_entry_bbs | finally_exit_bbs:
1961
+ if bid not in case_entries:
1962
+ case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
1963
+ join_idx += 1
1964
+ # MatchDispatch join BBs: the dispatch transitions to its join
1965
+ # directly (a non-CFG edge from the suspension-free dispatch's
1966
+ # fall-through), and each arm body Falls there. The arm bodies
1967
+ # are INLINED by the dispatch (not their own cases), so arm_bbs
1968
+ # are deliberately NOT marked here; only the shared join needs a
1969
+ # label.
1970
+ match_join_bbs: set[int] = set()
1971
+ for bb in cfg.blocks.values():
1972
+ t = bb.terminator
1973
+ if isinstance(t, rcfg.MatchDispatch) and t.join_bb is not None:
1974
+ match_join_bbs.add(t.join_bb)
1975
+ for bid in match_join_bbs:
1976
+ if bid not in case_entries:
1977
+ case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
1978
+ join_idx += 1
1979
+ cfg._case_entries_cache = case_entries
1980
+ return case_entries
1981
+
1982
+ def _emit_state_machine(self, out: "TextIO", func: TpyFunction,
1983
+ cfg: 'rcfg.CFG') -> None:
1984
+ """Emit `while (true) switch (state) { ... }` for the CFG.
1985
+ For zero-yield async defs the loop is omitted (no state
1986
+ transitions can fire, so the switch runs once)."""
1987
+ case_entries = self._compute_case_entries(cfg)
1988
+ self.ctx.indent_level = 1
1989
+ inner = self.ctx.indent()
1990
+ if cfg.yield_sites:
1991
+ out.write(f"{inner}while (true) switch (__state) {{\n")
1992
+ else:
1993
+ out.write(f"{inner}switch (__state) {{\n")
1994
+ # Case-label order matches the enum in gen_coro_struct.
1995
+ order = sorted(case_entries.items(), key=lambda kv: kv[1])
1996
+ for bb_id, label in order:
1997
+ out.write(f"{inner}case {label.cpp_name()}: {{\n")
1998
+ self.ctx.indent_level = 2
1999
+ self._emit_case(out, cfg, bb_id, case_entries, func)
2000
+ self.ctx.indent_level = 1
2001
+ out.write(f"{inner}}}\n")
2002
+ self._emit_resumable_done_case(out, inner)
2003
+ out.write(f"{inner}}}\n")
2004
+ out.write(f"{inner}__builtin_unreachable();\n")
2005
+ self.ctx.indent_level = 0
2006
+
2007
+ def _case_is_no_throw(self, cfg: 'rcfg.CFG', entry_bb: int) -> bool:
2008
+ """True iff the case body that starts at `entry_bb` provably
2009
+ cannot throw anything an in-scope handler would catch.
2010
+ Conservative: requires the BB to consist solely of a Yield
2011
+ terminator with literal / simple-name emplace args, with no
2012
+ user stmts and not a resume entry (resume cases emit a cancel
2013
+ check + sub poll that can both throw)."""
2014
+ bb = cfg.blocks[entry_bb]
2015
+ if bb.stmts:
2016
+ return False
2017
+ if cfg.resume_to_yield().get(entry_bb) is not None:
2018
+ return False
2019
+ t = bb.terminator
2020
+ if not isinstance(t, rcfg.Yield):
2021
+ return False
2022
+ return self._payload_args_no_throw(t.payload)
2023
+
2024
+ def _payload_args_no_throw(self,
2025
+ payload: 'rcfg.SuspensionPayload') -> bool:
2026
+ """True if emitting the Yield cannot throw. For a generator yield,
2027
+ the emitted action is `return <value>;` -- no-throw iff the value
2028
+ is a literal / simple name."""
2029
+ if isinstance(payload, rcfg.YieldPayload):
2030
+ return (payload.value_expr is None
2031
+ or self._expr_is_simple(payload.value_expr))
2032
+ # Async-with synthetic yields emplace from the CM frame slot
2033
+ # (`*__with_ctx_<n>`) plus monostate literals -- provably
2034
+ # no-throw and never touch the payload's `operand_expr`.
2035
+ if payload.async_with_kind is not None:
2036
+ return True
2037
+ if payload.mode is rcfg.AwaitMode.BORROWED:
2038
+ return self._expr_is_simple(payload.operand_expr)
2039
+ if payload.mode is rcfg.AwaitMode.ERASED:
2040
+ return self._expr_is_simple(payload.operand_expr)
2041
+ # INLINE: emplace args are the call's args.
2042
+ call = payload.operand_expr
2043
+ if not isinstance(call, TpyCall):
2044
+ return False
2045
+ return all(self._expr_is_simple(a) for a in call.args)
2046
+
2047
+ @staticmethod
2048
+ def _expr_is_simple(expr: TpyExpr) -> bool:
2049
+ """True for literals, bare names, and shallow coercions of
2050
+ either. These can be emitted without invoking constructors /
2051
+ function calls that could throw."""
2052
+ if isinstance(expr, TpyCoerce):
2053
+ return AsyncCoroCodegen._expr_is_simple(expr.expr)
2054
+ return isinstance(expr, (TpyIntLiteral, TpyFloatLiteral,
2055
+ TpyStrLiteral, TpyBoolLiteral,
2056
+ TpyNoneLiteral, TpyName))
2057
+
2058
+ def _emit_case(self, out: "TextIO", cfg: 'rcfg.CFG',
2059
+ entry_bb: int, case_entries: dict[int, _StateLabel],
2060
+ func: TpyFunction) -> None:
2061
+ """Emit the body of one switch case: wrap in region_stack, then
2062
+ walk the BB graph inline until hitting another case entry or a
2063
+ terminator that exits poll()."""
2064
+ bb = cfg.blocks[entry_bb]
2065
+ body_indent = self.ctx.indent()
2066
+ # Push FinallyContext entries so a `return` inside this case
2067
+ # body walks the right finally chain via _emit_finally_chain.
2068
+ # ExceptRegion contributes its parent try's finally because
2069
+ # Python runs finally after the handler completes.
2070
+ pushed_finally = self._push_finally_helpers(
2071
+ self._finally_helpers_for_region_stack(bb.region_stack))
2072
+ # Pending-return ctx: when this case is inside the try
2073
+ # body or handler body of a CFG-based finally, install ctx
2074
+ # state so `return` inside the body routes through the
2075
+ # pending-return slot.
2076
+ prev_pending = self._snapshot_pending_return_ctx()
2077
+ pending = self._pending_return_info_for_region_stack(bb.region_stack)
2078
+ if pending is not None:
2079
+ flag, slot, finally_entry_bb, boundary = pending
2080
+ target_label = case_entries[finally_entry_bb].cpp_name()
2081
+ self.ctx.async_pending_return_flag = flag
2082
+ self.ctx.async_pending_return_slot = slot
2083
+ self.ctx.async_pending_return_target_state = target_label
2084
+ self.ctx.async_pending_return_boundary = boundary
2085
+ try:
2086
+ # Emit `try {` for each TryRegion / WithRegion. For each
2087
+ # TryRegion, collect the list of "inter-region finally
2088
+ # helpers" between THIS TryRegion and the next-inner
2089
+ # try-emitting region (TryRegion or WithRegion): each
2090
+ # ExceptRegion's parent_finally goes in the TryRegion's
2091
+ # catch because its source-level try frame is no longer on
2092
+ # the C++ try stack but is still logically active.
2093
+ # WithRegions inside the gap get their own C++ try and
2094
+ # don't contribute extras.
2095
+ tryctx_stack: list[tuple[rcfg.Region, tuple[str, ...]]] = []
2096
+ # Skip the try/catch wrap entirely for case bodies provably
2097
+ # no-throw (typical setup case: empty stmts + Yield with
2098
+ # literal/name emplace args). The matching live catch sits
2099
+ # on the resume case where the sub-future's poll runs and
2100
+ # CAN throw.
2101
+ no_throw = self._case_is_no_throw(cfg, entry_bb)
2102
+ try_emitting = ([] if no_throw else
2103
+ [i for i, r in enumerate(bb.region_stack)
2104
+ if isinstance(r, (rcfg.TryRegion,
2105
+ rcfg.WithRegion))])
2106
+ for j, i in enumerate(try_emitting):
2107
+ region = bb.region_stack[i]
2108
+ if isinstance(region, rcfg.TryRegion):
2109
+ next_emit = (try_emitting[j + 1]
2110
+ if j + 1 < len(try_emitting)
2111
+ else len(bb.region_stack))
2112
+ extras: list[str] = []
2113
+ for k in range(i + 1, next_emit):
2114
+ mid = bb.region_stack[k]
2115
+ if isinstance(mid, rcfg.ExceptRegion):
2116
+ if mid.parent_finally is not None:
2117
+ extras.append(mid.parent_finally)
2118
+ extras.reverse()
2119
+ else:
2120
+ extras = []
2121
+ out.write(f"{body_indent}try {{\n")
2122
+ tryctx_stack.append((region, tuple(extras)))
2123
+ self.ctx.indent_level += 1
2124
+ body_indent = self.ctx.indent()
2125
+
2126
+ # Emit the case body: resume step if this is a yield-resume,
2127
+ # then BB statements, then terminator.
2128
+ self._emit_case_body(out, cfg, entry_bb, case_entries, func)
2129
+
2130
+ # Close regions innermost first.
2131
+ for region, extras in reversed(tryctx_stack):
2132
+ self.ctx.indent_level -= 1
2133
+ body_indent = self.ctx.indent()
2134
+ out.write(f"{body_indent}}}")
2135
+ if isinstance(region, rcfg.TryRegion):
2136
+ self._emit_try_region_catches(
2137
+ out, body_indent, region, cfg, case_entries,
2138
+ entry_bb, func, extras)
2139
+ else:
2140
+ self._emit_with_region_catches(
2141
+ out, body_indent, region, case_entries)
2142
+ out.write("\n")
2143
+ finally:
2144
+ for _ in range(pushed_finally):
2145
+ self.ctx.finally_stack.pop()
2146
+ self._restore_pending_return_ctx(prev_pending)
2147
+
2148
+ def _snapshot_pending_return_ctx(self) -> tuple:
2149
+ return (self.ctx.async_pending_return_flag,
2150
+ self.ctx.async_pending_return_slot,
2151
+ self.ctx.async_pending_return_target_state,
2152
+ self.ctx.async_pending_return_boundary)
2153
+
2154
+ def _restore_pending_return_ctx(self, snap: tuple) -> None:
2155
+ (self.ctx.async_pending_return_flag,
2156
+ self.ctx.async_pending_return_slot,
2157
+ self.ctx.async_pending_return_target_state,
2158
+ self.ctx.async_pending_return_boundary) = snap
2159
+
2160
+ def _finally_helpers_for_region_stack(
2161
+ self, region_stack: tuple) -> list:
2162
+ """Compute the finally-emit closures for BBs with this
2163
+ region_stack. Each TryRegion / ExceptRegion contributes its
2164
+ finally helper name (called via `this->name()`); each WithRegion
2165
+ contributes a closure that emits `(*__with_ctx_<n>).__exit__(
2166
+ {}, nullptr/{}, {})`. Order: outermost first (innermost ends up
2167
+ on top of the stack)."""
2168
+ helpers: list = []
2169
+ for region in region_stack:
2170
+ if isinstance(region, rcfg.TryRegion):
2171
+ if region.finally_helper_name is not None:
2172
+ helpers.append(("helper", region.finally_helper_name))
2173
+ elif isinstance(region, rcfg.ExceptRegion):
2174
+ if region.parent_finally is not None:
2175
+ helpers.append(("helper", region.parent_finally))
2176
+ elif isinstance(region, rcfg.WithRegion):
2177
+ helpers.append(("with", region))
2178
+ return helpers
2179
+
2180
+ def _pending_return_info_for_region_stack(
2181
+ self, region_stack: tuple) -> 'tuple[str, str | None, int, int] | None':
2182
+ """If a CFG-based finally is active for this region_stack, return
2183
+ `(flag, slot, target_bb, boundary)` for the INNERMOST one --
2184
+ boundary is the count of finally_stack frames contributed by
2185
+ helper-based regions BELOW it (those defer to AsyncFinallyExit
2186
+ instead of running at the return site). Returns None otherwise.
2187
+
2188
+ Walking innermost-first is what makes nested CFG finally work:
2189
+ a return inside the inner try body parks into the inner's slot;
2190
+ the inner's AsyncFinallyExit later forwards into the outer.
2191
+ At the inner's `finally_exit_bb`, the inner FinallyRegion is no
2192
+ longer on the stack, so the same lookup naturally finds the
2193
+ outer (the new innermost) -- which is how AsyncFinallyExit
2194
+ learns whether to replay locally or forward outward.
2195
+
2196
+ For try/handler-body returns: target_bb = finally_entry_bb (the
2197
+ return parks + jumps to the finally entry).
2198
+ For finally-body returns: target_bb = finally_exit_bb (the return
2199
+ parks + jumps straight to AsyncFinallyExit; running the finally
2200
+ body again would re-execute it from the top)."""
2201
+ innermost: 'tuple[str, str | None, int, int] | None' = None
2202
+ boundary = 0
2203
+ for region in region_stack:
2204
+ if isinstance(region, rcfg.TryRegion):
2205
+ if region.pending_return_flag is not None:
2206
+ innermost = (region.pending_return_flag,
2207
+ region.pending_return_slot,
2208
+ region.finally_entry_bb,
2209
+ boundary)
2210
+ elif region.finally_helper_name is not None:
2211
+ boundary += 1
2212
+ elif isinstance(region, rcfg.ExceptRegion):
2213
+ if region.parent_pending_return_flag is not None:
2214
+ innermost = (region.parent_pending_return_flag,
2215
+ region.parent_pending_return_slot,
2216
+ region.parent_finally_entry_bb,
2217
+ boundary)
2218
+ elif region.parent_finally is not None:
2219
+ boundary += 1
2220
+ elif isinstance(region, rcfg.FinallyRegion):
2221
+ if region.pending_return_flag is not None:
2222
+ innermost = (region.pending_return_flag,
2223
+ region.pending_return_slot,
2224
+ region.finally_exit_bb,
2225
+ boundary)
2226
+ elif isinstance(region, rcfg.WithRegion):
2227
+ boundary += 1
2228
+ return innermost
2229
+
2230
+ def _emit_finally_helper_call(self, out: "TextIO", indent: str,
2231
+ helper_name: str) -> None:
2232
+ """Emit `this->helper_name();`.
2233
+
2234
+ The stop-check (`if __finally_stop`) is NOT emitted here. Callers
2235
+ that need to act on __finally_stop (e.g. to suppress a rethrow or
2236
+ skip a state transition) call _emit_generator_stop_check AFTER all
2237
+ cleanup for the current exit event has run, so that outer region
2238
+ cleanup (with.__exit__, outer finallies) is never skipped."""
2239
+ out.write(f"{indent}this->{helper_name}();\n")
2240
+
2241
+ def _emit_generator_stop_check(self, out: "TextIO", indent: str) -> None:
2242
+ """Emit `if (this->__finally_stop) { return StopIteration; }`.
2243
+
2244
+ Only emitted when in generator shape and the struct has __finally_stop.
2245
+ Call AFTER all cleanup for an exit event (region loop, catch handler)
2246
+ has run, so outer cleanups are never skipped by an early return."""
2247
+ if not (self._is_generator_shape() and self.ctx.generator_has_finally_stop):
2248
+ return
2249
+ done_state = self.ctx.generator_resumable_done_state or "S_DONE"
2250
+ out.write(f"{indent}if (this->__finally_stop) {{\n")
2251
+ out.write(f"{indent}{INDENT}__state = {done_state};\n")
2252
+ out.write(f"{indent}{INDENT}return ::tpy::make_unexpected("
2253
+ f"::tpy::StopIteration{{}});\n")
2254
+ out.write(f"{indent}}}\n")
2255
+
2256
+ def _push_finally_helpers(self, helpers: list) -> int:
2257
+ """Push FinallyContext entries for each helper. Returns the
2258
+ count pushed for matching pop in a `finally:` clause."""
2259
+ count = 0
2260
+ for kind, payload in helpers:
2261
+ if kind == "helper":
2262
+ helper_name = payload
2263
+ def _emit_finally(o: "TextIO", ind: str,
2264
+ n=helper_name) -> None:
2265
+ self._emit_finally_helper_call(o, ind, n)
2266
+ else: # "with"
2267
+ region = payload
2268
+ def _emit_finally(o: "TextIO", ind: str,
2269
+ r=region) -> None:
2270
+ self._emit_with_exit(o, ind, r, on_exception=False)
2271
+ fctx = FinallyContext(
2272
+ emit_finally=_emit_finally, terminates=False, loop_depth=0)
2273
+ self.ctx.finally_stack.append(fctx)
2274
+ count += 1
2275
+ return count
2276
+
2277
+ def _emit_with_region_catches(self, out: "TextIO", indent: str,
2278
+ region: 'rcfg.WithRegion',
2279
+ case_entries: dict[int, _StateLabel]
2280
+ ) -> None:
2281
+ """Emit `} catch (...) { __exit__(...); throw; }` for a
2282
+ WithRegion at the close of a case body. When
2283
+ `item.exit_can_suppress` is True, a `catch (BaseException&)`
2284
+ clause first calls __exit__ with the exception and, on a True
2285
+ return, transitions to the WithRegion's post_with_bb state
2286
+ (suppressing the exception). The foreign catch-all always
2287
+ cleanup-calls __exit__ with `nullptr` exc and rethrows."""
2288
+ item = region.item
2289
+ n = region.ctx_n
2290
+ can_suppress = item.exit_can_suppress
2291
+ takes_exc_val = item.exit_takes_exc_val
2292
+ emit_tpy_catch = can_suppress or takes_exc_val
2293
+ # post_with_bb is only marked a case entry when this WithRegion
2294
+ # may suppress; without suppression the catch unconditionally
2295
+ # rethrows and never needs the label.
2296
+ post_label = (case_entries[region.post_with_bb].cpp_name()
2297
+ if can_suppress else None)
2298
+ if emit_tpy_catch:
2299
+ out.write(f" catch (::tpy::BaseException& __exc_{n}) {{\n")
2300
+ self.ctx.indent_level += 1
2301
+ catch_ind = self.ctx.indent()
2302
+ if can_suppress:
2303
+ out.write(
2304
+ f"{catch_ind}if (!(*__with_ctx_{n}).__exit__("
2305
+ f"{{}}, &__exc_{n}, {{}})) throw;\n")
2306
+ out.write(f"{catch_ind}__state = {post_label};\n")
2307
+ out.write(f"{catch_ind}continue;\n")
2308
+ else:
2309
+ self._emit_with_exit(out, catch_ind, region,
2310
+ on_exception=True)
2311
+ out.write(f"{catch_ind}throw;\n")
2312
+ self.ctx.indent_level -= 1
2313
+ out.write(f"{indent}}} catch (...) {{\n")
2314
+ else:
2315
+ out.write(f" catch (...) {{\n")
2316
+ self.ctx.indent_level += 1
2317
+ catch_ind = self.ctx.indent()
2318
+ # Foreign exception (or non-suppressing path): cleanup-only call,
2319
+ # then rethrow. Don't pass &exc since the C++ side hasn't bound
2320
+ # one in this catch arm.
2321
+ out.write(f"{catch_ind}(*__with_ctx_{n}).__exit__({{}}, "
2322
+ f"{'nullptr' if takes_exc_val else '{}'}, {{}});\n")
2323
+ out.write(f"{catch_ind}throw;\n")
2324
+ self.ctx.indent_level -= 1
2325
+ out.write(f"{indent}}}")
2326
+
2327
+ def _emit_try_region_catches(self, out: "TextIO", indent: str,
2328
+ region: 'rcfg.TryRegion',
2329
+ cfg: 'rcfg.CFG',
2330
+ case_entries: dict[int, _StateLabel],
2331
+ case_entry_bb: int,
2332
+ func: TpyFunction,
2333
+ extra_finallies: tuple[str, ...] = ()) -> None:
2334
+ """Emit `} catch (...) { ... }` clauses for a TryRegion at the
2335
+ close of a case body. Each handler's body is emitted inline
2336
+ inside its catch (walks the handler entry BB).
2337
+
2338
+ `case_entry_bb` is the case-entry BB this region wraps; used to
2339
+ determine which __sub_<n> to reset (the in-flight sub-future
2340
+ for this resume case).
2341
+
2342
+ `extra_finallies` are helper-fn names (innermost first) for any
2343
+ ExceptRegion / FinallyRegion in the case's region_stack that
2344
+ sit *between* this TryRegion and the next-inner TryRegion --
2345
+ their Python-level try frames are no longer C++ try wraps here
2346
+ but are still logically active. They run in the catch-all (and
2347
+ each handler's body, if the handler completes normally is the
2348
+ Fall-edge case handled by `_emit_exit_region_finallies`; the
2349
+ throw escape is what we cover here)."""
2350
+ builder = rcfg.resumable_state(func).cfg_builder
2351
+ yield_for_case = self._yield_at_resume(cfg, case_entry_bb)
2352
+ # Helpers to invoke on throw from inside the handler body
2353
+ # (innermost first): each extra_finally + this region's own
2354
+ # finally. Mirrors the catch-all unwind order.
2355
+ handler_throw_finallies: tuple[str, ...] = tuple(extra_finallies)
2356
+ if region.finally_helper_name is not None:
2357
+ handler_throw_finallies = handler_throw_finallies + (region.finally_helper_name,)
2358
+ for handler in region.handlers:
2359
+ self.statements._emit_except_handler_header(out, handler)
2360
+ self.ctx.indent_level += 1
2361
+ catch_indent = self.ctx.indent()
2362
+ # Reset the in-flight sub-future first action in catch.
2363
+ # Generators have no sub-futures; only reset for async shape.
2364
+ if (yield_for_case is not None
2365
+ and isinstance(yield_for_case.payload, rcfg.AwaitPayload)):
2366
+ self._emit_sub_reset(out, catch_indent,
2367
+ yield_for_case.payload,
2368
+ yield_for_case.suspension_index)
2369
+ # Wrap handler body in `try { ... } catch (...) {
2370
+ # finallies; throw; }` so a `raise` from inside the
2371
+ # handler runs this try's finally (and any inter-region
2372
+ # finallies) before propagating to the outer try. For
2373
+ # CFG-based finally, the inner catch saves
2374
+ # `std::current_exception()` to the parent's captured-exc
2375
+ # slot and transitions state to its finally entry instead
2376
+ # of plain rethrow -- mirrors the outer catch-all path.
2377
+ cfg_finally = region.captured_exc_field is not None
2378
+ has_throw_unwind = bool(handler_throw_finallies) or cfg_finally
2379
+ if has_throw_unwind:
2380
+ out.write(f"{catch_indent}try {{\n")
2381
+ self.ctx.indent_level += 1
2382
+ # Swap ctx.finally_stack to match the handler entry BB's
2383
+ # region_stack. The case-entry's stack contains finallies
2384
+ # for regions that are no longer active inside the handler
2385
+ # body (the try whose handler we're in is gone). Without
2386
+ # the swap, a return inside the handler walks finallies
2387
+ # that should not apply (e.g. a sibling inner try's finally
2388
+ # that has already run on the throw path).
2389
+ old_finally_stack = self.ctx.finally_stack
2390
+ handler_entry: int | None = None
2391
+ if builder is not None:
2392
+ handler_entry = builder.get_handler_entry(region, handler)
2393
+ handler_stack_helpers: list = []
2394
+ if handler_entry is not None:
2395
+ handler_bb = cfg.blocks[handler_entry]
2396
+ handler_stack_helpers = self._finally_helpers_for_region_stack(
2397
+ handler_bb.region_stack)
2398
+ self.ctx.finally_stack = []
2399
+ self._push_finally_helpers(handler_stack_helpers)
2400
+ old_except_tier = self.ctx.in_except_tier
2401
+ self.ctx.in_except_tier = "throw"
2402
+ # Pending-return ctx for the handler body: a return inside
2403
+ # the handler routes through the parent CFG-based finally's
2404
+ # pending-return slot.
2405
+ prev_pending = self._snapshot_pending_return_ctx()
2406
+ if handler_entry is not None:
2407
+ pending = self._pending_return_info_for_region_stack(
2408
+ cfg.blocks[handler_entry].region_stack)
2409
+ if pending is not None:
2410
+ flag, slot, finally_entry_bb, boundary = pending
2411
+ target_label = case_entries[finally_entry_bb].cpp_name()
2412
+ self.ctx.async_pending_return_flag = flag
2413
+ self.ctx.async_pending_return_slot = slot
2414
+ self.ctx.async_pending_return_target_state = target_label
2415
+ self.ctx.async_pending_return_boundary = boundary
2416
+ try:
2417
+ if handler_entry is not None:
2418
+ self._walk_inline(out, cfg, handler_entry,
2419
+ case_entries, func)
2420
+ finally:
2421
+ self.ctx.in_except_tier = old_except_tier
2422
+ self.ctx.finally_stack = old_finally_stack
2423
+ self._restore_pending_return_ctx(prev_pending)
2424
+ if has_throw_unwind:
2425
+ self.ctx.indent_level -= 1
2426
+ inner_close = self.ctx.indent()
2427
+ out.write(f"{inner_close}}} catch (...) {{\n")
2428
+ self.ctx.indent_level += 1
2429
+ inner_catch = self.ctx.indent()
2430
+ for helper in handler_throw_finallies:
2431
+ self._emit_finally_helper_call(out, inner_catch, helper)
2432
+ if cfg_finally:
2433
+ assert region.finally_entry_bb is not None
2434
+ fe_label = case_entries[region.finally_entry_bb].cpp_name()
2435
+ out.write(f"{inner_catch}this->{region.captured_exc_field}"
2436
+ f" = std::current_exception();\n")
2437
+ out.write(f"{inner_catch}__state = {fe_label};\n")
2438
+ out.write(f"{inner_catch}continue;\n")
2439
+ else:
2440
+ self._emit_generator_stop_check(out, inner_catch)
2441
+ out.write(f"{inner_catch}throw;\n")
2442
+ self.ctx.indent_level -= 1
2443
+ out.write(f"{inner_close}}}\n")
2444
+ self.ctx.indent_level -= 1
2445
+ out.write(f"{indent}}}")
2446
+ # Catch-all: reset sub, run inter-region finallies (innermost
2447
+ # first). For helper-based finally: call helper, re-throw. For
2448
+ # CFG-based finally: save current exception to the
2449
+ # captured-exc field and transition state to the finally entry
2450
+ # so the finally body runs in the state machine (with possible
2451
+ # suspensions); the saved exception is rethrown by the
2452
+ # AsyncFinallyExit stmt at the finally tail.
2453
+ out.write(" catch (...) {\n")
2454
+ self.ctx.indent_level += 1
2455
+ catch_indent = self.ctx.indent()
2456
+ if (yield_for_case is not None
2457
+ and isinstance(yield_for_case.payload, rcfg.AwaitPayload)):
2458
+ self._emit_sub_reset(out, catch_indent,
2459
+ yield_for_case.payload,
2460
+ yield_for_case.suspension_index)
2461
+ for helper in extra_finallies:
2462
+ self._emit_finally_helper_call(out, catch_indent, helper)
2463
+ if region.captured_exc_field is not None:
2464
+ assert region.finally_entry_bb is not None
2465
+ label = case_entries[region.finally_entry_bb].cpp_name()
2466
+ out.write(f"{catch_indent}this->{region.captured_exc_field} "
2467
+ f"= std::current_exception();\n")
2468
+ out.write(f"{catch_indent}__state = {label};\n")
2469
+ out.write(f"{catch_indent}continue;\n")
2470
+ else:
2471
+ if region.finally_helper_name is not None:
2472
+ self._emit_finally_helper_call(
2473
+ out, catch_indent, region.finally_helper_name)
2474
+ # All cleanup done; Python `return` in finally suppresses the
2475
+ # exception (return StopIteration rather than rethrowing).
2476
+ self._emit_generator_stop_check(out, catch_indent)
2477
+ out.write(f"{catch_indent}throw;\n")
2478
+ self.ctx.indent_level -= 1
2479
+ out.write(f"{indent}}}")
2480
+
2481
+ def _resume_index_for_case(self, cfg: 'rcfg.CFG',
2482
+ case_entry_bb: int) -> int | None:
2483
+ y = cfg.resume_to_yield().get(case_entry_bb)
2484
+ return y.suspension_index if y is not None else None
2485
+
2486
+ def _yield_at_resume(self, cfg: 'rcfg.CFG',
2487
+ resume_bb: int) -> 'rcfg.Yield | None':
2488
+ return cfg.resume_to_yield().get(resume_bb)
2489
+
2490
+ def _emit_case_body(self, out: "TextIO", cfg: 'rcfg.CFG',
2491
+ entry_bb: int, case_entries: dict[int, _StateLabel],
2492
+ func: TpyFunction) -> None:
2493
+ """Emit the body of a case starting at entry_bb. Begins with the
2494
+ resume step (if entry_bb is a yield-resume), then walks BBs
2495
+ inline until a terminator exits the case."""
2496
+ body_indent = self.ctx.indent()
2497
+ # Resume step. Generators have no sub-future to poll and bind
2498
+ # nothing back in, so their resume case is a bare continuation.
2499
+ y = self._yield_at_resume(cfg, entry_bb)
2500
+ if y is not None and not self._is_generator_shape():
2501
+ self._emit_resume_core(out, body_indent, y.payload,
2502
+ y.suspension_index, func)
2503
+ if y.payload.kind is rcfg.AwaitKind.RETURN:
2504
+ # Resume core already emitted the return.
2505
+ return
2506
+ # Walk BBs inline from entry_bb.
2507
+ self._walk_inline(out, cfg, entry_bb, case_entries, func)
2508
+
2509
+ def _emit_exit_region_finallies(self, out: "TextIO", indent: str,
2510
+ from_regions: tuple,
2511
+ to_regions: tuple) -> None:
2512
+ """Emit cleanup (finally helpers + with __exit__ calls) for each
2513
+ region that exists in `from_regions` but not in `to_regions`,
2514
+ in innermost-first order. Used when control transitions from a
2515
+ deeper region stack to a shallower one (normal exit from
2516
+ try/with) -- C++ try/catch doesn't run finally on normal exit,
2517
+ so we run them explicitly here."""
2518
+ if not from_regions:
2519
+ return
2520
+ # Identify the common prefix length.
2521
+ common = 0
2522
+ while (common < len(from_regions) and common < len(to_regions)
2523
+ and from_regions[common] is to_regions[common]):
2524
+ common += 1
2525
+ exited = list(from_regions[common:])
2526
+ # Innermost first.
2527
+ for region in reversed(exited):
2528
+ if isinstance(region, rcfg.TryRegion):
2529
+ if region.finally_helper_name is not None:
2530
+ self._emit_finally_helper_call(
2531
+ out, indent, region.finally_helper_name)
2532
+ elif isinstance(region, rcfg.ExceptRegion):
2533
+ # Leaving an except handler normally: run the parent
2534
+ # try's finally body (Python semantics).
2535
+ if region.parent_finally is not None:
2536
+ self._emit_finally_helper_call(
2537
+ out, indent, region.parent_finally)
2538
+ elif isinstance(region, rcfg.WithRegion):
2539
+ # Leaving a with-region normally: __exit__(None, None, None).
2540
+ self._emit_with_exit(out, indent, region,
2541
+ on_exception=False)
2542
+ # Emit stop-check only when to_regions has no pending cleanup of its
2543
+ # own. If to_regions still has with/__exit__ or finally helpers, the
2544
+ # state machine will run those in subsequent states; the stop-check
2545
+ # in those later transitions fires after ALL cleanup is done.
2546
+ if not _regions_have_pending_cleanup(to_regions):
2547
+ self._emit_generator_stop_check(out, indent)
2548
+
2549
+ def _walk_inline(self, out: "TextIO", cfg: 'rcfg.CFG',
2550
+ start_bb: int, case_entries: dict[int, _StateLabel],
2551
+ func: TpyFunction) -> None:
2552
+ """Walk BBs starting from start_bb, emitting their statements
2553
+ and following Fall/Branch terminators inline. Stops when the
2554
+ terminator is Yield/Return/Raise/Unreachable, or when a
2555
+ Fall/Branch target is a case_entry (then emits a state
2556
+ transition)."""
2557
+ body_indent = self.ctx.indent()
2558
+ cur = start_bb
2559
+ while True:
2560
+ bb = cfg.blocks[cur]
2561
+ # Emit BB statements.
2562
+ for stmt in bb.stmts:
2563
+ if isinstance(stmt, rcfg.AsyncForIterSetup):
2564
+ self._emit_async_for_iter_setup(out, body_indent, stmt, func)
2565
+ elif isinstance(stmt, rcfg.WithEnter):
2566
+ self._emit_with_enter(out, body_indent, stmt)
2567
+ elif isinstance(stmt, rcfg.AsyncWithSetup):
2568
+ self._emit_async_with_setup(out, body_indent, stmt)
2569
+ elif isinstance(stmt, rcfg.AsyncFinallyExit):
2570
+ self._emit_async_finally_exit(out, body_indent, stmt)
2571
+ else:
2572
+ self.statements.gen_stmt(out, stmt)
2573
+ t = bb.terminator
2574
+ if isinstance(t, rcfg.Yield):
2575
+ self._emit_yield_terminator(out, body_indent, t, func)
2576
+ return
2577
+ if isinstance(t, rcfg.ReturnT):
2578
+ # gen_stmt routes a TpyReturn inside async-coro context
2579
+ # through _make_async_return, which walks the active
2580
+ # finally chain (ctx.finally_stack) before emitting the
2581
+ # Poll<T>::ready(...).
2582
+ self.statements.gen_stmt(out, t.return_stmt)
2583
+ return
2584
+ if isinstance(t, rcfg.RaiseT):
2585
+ # Emit the raise as an ordinary TpyRaise statement; the
2586
+ # finally chain is run via C++ exception unwinding.
2587
+ self.statements.gen_stmt(out, t.raise_stmt)
2588
+ return
2589
+ if isinstance(t, rcfg.Unreachable):
2590
+ self._emit_unreachable_tail(out, body_indent, func)
2591
+ return
2592
+ if isinstance(t, rcfg.Fall):
2593
+ if t.next_bb in case_entries:
2594
+ self._emit_exit_region_finallies(
2595
+ out, body_indent,
2596
+ cfg.blocks[cur].region_stack,
2597
+ cfg.blocks[t.next_bb].region_stack)
2598
+ out.write(f"{body_indent}__state = "
2599
+ f"{case_entries[t.next_bb].cpp_name()};\n")
2600
+ out.write(f"{body_indent}continue;\n")
2601
+ return
2602
+ cur = t.next_bb
2603
+ continue
2604
+ if isinstance(t, rcfg.Branch):
2605
+ cond_cpp = self.expressions.gen_expr(t.cond)
2606
+ self.ctx.temps.flush(out, body_indent)
2607
+ out.write(f"{body_indent}if ({cond_cpp}) {{\n")
2608
+ self.ctx.indent_level += 1
2609
+ self._walk_inline_or_jump(out, cfg, t.then_bb, case_entries,
2610
+ func, from_bb=cur)
2611
+ self.ctx.indent_level -= 1
2612
+ out.write(f"{body_indent}}} else {{\n")
2613
+ self.ctx.indent_level += 1
2614
+ self._walk_inline_or_jump(out, cfg, t.else_bb, case_entries,
2615
+ func, from_bb=cur)
2616
+ self.ctx.indent_level -= 1
2617
+ out.write(f"{body_indent}}}\n")
2618
+ return
2619
+ if isinstance(t, rcfg.AsyncForAdvance):
2620
+ self._emit_async_for_advance(
2621
+ out, body_indent, cfg, t, case_entries, func, from_bb=cur)
2622
+ return
2623
+ if isinstance(t, rcfg.MatchDispatch):
2624
+ self._emit_match_dispatch(
2625
+ out, cfg, t, case_entries, func, from_bb=cur)
2626
+ return
2627
+ raise CodeGenError(
2628
+ f"internal: unknown terminator {type(t).__name__}",
2629
+ loc=None)
2630
+
2631
+ def _emit_match_dispatch(self, out: "TextIO", cfg: 'rcfg.CFG',
2632
+ t: 'rcfg.MatchDispatch',
2633
+ case_entries: dict[int, _StateLabel],
2634
+ func: TpyFunction, from_bb: int) -> None:
2635
+ """Emit a suspending `match` (H1) by reusing the ordinary
2636
+ `gen_match` for the type-aware dispatch, with each arm body routed
2637
+ back through `_walk_inline` via the `_emit_case_body` hook. Arm
2638
+ bodies are inlined here (not their own cases); their internal
2639
+ suspensions split out resume cases as usual. After the dispatch,
2640
+ the (non-exhaustive) fall-through transitions to `join_bb`."""
2641
+ body_indent = self.ctx.indent()
2642
+ # The arm emitter inlines one arm body in place. `match` adds no
2643
+ # region, so the arm BB shares `from_bb`'s region stack -- the
2644
+ # inline walk handles its own fall-to-join / suspension exits.
2645
+ def emit_arm(arm_bb: int) -> None:
2646
+ self._walk_inline(out, cfg, arm_bb, case_entries, func)
2647
+ old_emitter = self.ctx.resumable_arm_emitter
2648
+ old_map = self.ctx.resumable_arm_bb_by_body
2649
+ self.ctx.resumable_arm_emitter = emit_arm
2650
+ self.ctx.resumable_arm_bb_by_body = {
2651
+ id(case.body): arm_bb
2652
+ for case, arm_bb in zip(t.match_stmt.cases, t.arm_bbs)
2653
+ }
2654
+ try:
2655
+ self.statements.match.gen_match(out, t.match_stmt, body_indent)
2656
+ finally:
2657
+ self.ctx.resumable_arm_emitter = old_emitter
2658
+ self.ctx.resumable_arm_bb_by_body = old_map
2659
+ # The dispatch case MUST end in a terminating statement: a switch
2660
+ # whose cases all return/continue still "may fall through" to the
2661
+ # GCC eye (no default), so without this the next state's `case`
2662
+ # label trips -Werror=implicit-fallthrough. When a fall-through is
2663
+ # reachable (`join_bb` set) emit the state transition to join;
2664
+ # otherwise the match is exhaustive AND every arm terminates, so
2665
+ # the post-dispatch point is genuinely unreachable.
2666
+ if t.join_bb is not None:
2667
+ self._walk_inline_or_jump(
2668
+ out, cfg, t.join_bb, case_entries, func, from_bb=from_bb)
2669
+ else:
2670
+ out.write(f"{body_indent}__builtin_unreachable();\n")
2671
+
2672
+ def _walk_inline_or_jump(self, out: "TextIO", cfg: 'rcfg.CFG',
2673
+ target_bb: int,
2674
+ case_entries: dict[int, _StateLabel],
2675
+ func: TpyFunction,
2676
+ from_bb: int) -> None:
2677
+ body_indent = self.ctx.indent()
2678
+ if target_bb in case_entries:
2679
+ self._emit_exit_region_finallies(
2680
+ out, body_indent,
2681
+ cfg.blocks[from_bb].region_stack,
2682
+ cfg.blocks[target_bb].region_stack)
2683
+ out.write(f"{body_indent}__state = "
2684
+ f"{case_entries[target_bb].cpp_name()};\n")
2685
+ out.write(f"{body_indent}continue;\n")
2686
+ else:
2687
+ self._walk_inline(out, cfg, target_bb, case_entries, func)
2688
+
2689
+ def _emit_async_finally_exit(self, out: "TextIO", indent: str,
2690
+ stmt: 'rcfg.AsyncFinallyExit') -> None:
2691
+ """Emit the deferred-return check + saved-exception rethrow at
2692
+ the dedicated finally exit BB. Both fields are cleared before
2693
+ extraction so a re-entry to the same try (e.g. in a loop)
2694
+ starts with no carried-over state.
2695
+
2696
+ Order matters: the pending-return check runs FIRST so a `return`
2697
+ in the finally body wins over an in-flight exception captured
2698
+ from the try (Python: return-in-finally swallows). The deferred
2699
+ return clears the captured-exc field on the way out so the
2700
+ rethrow check is skipped automatically.
2701
+
2702
+ The pending branch has two outcomes depending on whether an
2703
+ enclosing CFG-finally region is active at this exit site (set
2704
+ in `ctx.async_pending_return_*` by `_emit_case` from this BB's
2705
+ region_stack):
2706
+ * Not active: replay locally -- walk outer helpers, then
2707
+ emit Poll::ready / StopIteration as the coro's exit.
2708
+ * Active: forward -- move this exit's parked value into the
2709
+ outer's slot, set the outer's flag, walk helpers down to
2710
+ the outer boundary, transition to the outer's
2711
+ `finally_entry_bb`. The outer's `AsyncFinallyExit`
2712
+ eventually performs the local replay (or forwards again
2713
+ for deeper nesting)."""
2714
+ f = stmt.captured_exc_field
2715
+ inner = indent + INDENT
2716
+ if stmt.pending_return_flag is not None:
2717
+ flag = stmt.pending_return_flag
2718
+ out.write(f"{indent}if (this->{flag}) {{\n")
2719
+ out.write(f"{inner}this->{flag} = false;\n")
2720
+ # Swallow any in-flight exception captured by the try
2721
+ # region's catch: Python semantics say return-in-finally
2722
+ # wins, including over a propagating raise. For a return
2723
+ # from the try body (no in-flight exception), this is a
2724
+ # no-op.
2725
+ out.write(f"{inner}this->{f} = nullptr;\n")
2726
+ # If an enclosing CFG-based finally is active (set in ctx
2727
+ # by `_emit_case` from this BB's region_stack), forward the
2728
+ # parked state into the outer's slots and transition to its
2729
+ # finally_entry instead of replaying locally -- otherwise
2730
+ # the outer finally body would be skipped.
2731
+ outer_flag = self.ctx.async_pending_return_flag
2732
+ if outer_flag is not None:
2733
+ outer_slot = self.ctx.async_pending_return_slot
2734
+ outer_target = self.ctx.async_pending_return_target_state
2735
+ outer_boundary = self.ctx.async_pending_return_boundary
2736
+ inner_slot = stmt.pending_return_slot
2737
+ if outer_slot is not None and inner_slot is not None:
2738
+ out.write(f"{inner}this->{outer_slot} = "
2739
+ f"std::move(this->{inner_slot});\n")
2740
+ out.write(f"{inner}this->{outer_flag} = true;\n")
2741
+ # Walk helpers between this exit and the outer finally
2742
+ # entry; helpers BELOW the outer (`outer_boundary`) stay
2743
+ # on the stack to run inside the outer's finally region.
2744
+ self.statements._emit_finally_chain(out, inner,
2745
+ stop_at=outer_boundary)
2746
+ out.write(f"{inner}__state = {outer_target};\n")
2747
+ out.write(f"{inner}continue;\n")
2748
+ else:
2749
+ # No outer CFG finally: replay locally. Walk any outer
2750
+ # helpers (an enclosing helper-based finally outside
2751
+ # this CFG-based one) before emitting the actual return.
2752
+ self.statements._emit_finally_chain(out, inner)
2753
+ if self._is_generator_shape():
2754
+ done_state = self.ctx.generator_resumable_done_state or "S_DONE"
2755
+ out.write(f"{inner}__state = {done_state};\n")
2756
+ out.write(f"{inner}return ::tpy::make_unexpected("
2757
+ f"::tpy::StopIteration{{}});\n")
2758
+ else:
2759
+ done_state = self.ctx.async_coro_done_state or "S_DONE"
2760
+ out.write(f"{inner}__state = {done_state};\n")
2761
+ slot = stmt.pending_return_slot
2762
+ ret_cpp = self.ctx.async_coro_return_cpp
2763
+ if slot is None or ret_cpp == "void":
2764
+ out.write(f"{inner}{POLL_VOID_READY_RETURN}\n")
2765
+ else:
2766
+ out.write(f"{inner}return ::tpystd::tpy::Poll<{ret_cpp}>::ready"
2767
+ f"(std::move(this->{slot}));\n")
2768
+ out.write(f"{indent}}}\n")
2769
+ out.write(f"{indent}if (this->{f}) {{\n")
2770
+ out.write(f"{inner}std::exception_ptr __tmp = this->{f};\n")
2771
+ out.write(f"{inner}this->{f} = nullptr;\n")
2772
+ out.write(f"{inner}std::rethrow_exception(__tmp);\n")
2773
+ out.write(f"{indent}}}\n")
2774
+
2775
+ def _emit_with_enter(self, out: "TextIO", indent: str,
2776
+ stmt: 'rcfg.WithEnter') -> None:
2777
+ """Emit the with-stmt setup sequence:
2778
+ __with_ctx_<n>.emplace(<context_expr>);
2779
+ <target> = (*__with_ctx_<n>).__enter__(); # if target
2780
+ (*__with_ctx_<n>).__enter__(); # else
2781
+ """
2782
+ ctx_n = stmt.ctx_n
2783
+ item = stmt.item
2784
+ ctx_expr = self.expressions.gen_expr(item.context_expr)
2785
+ self.ctx.temps.flush(out, indent)
2786
+ out.write(f"{indent}__with_ctx_{ctx_n}.emplace({ctx_expr});\n")
2787
+ if item.target is not None:
2788
+ target = escape_cpp_name(item.target)
2789
+ enter_call = f"(*__with_ctx_{ctx_n}).__enter__()"
2790
+ if item.target in self.ctx.generator_frame_slot_locals:
2791
+ # A non-value `as`-target hoisted across a yield is backed by
2792
+ # `tpy::frame_slot<T>` (deleted operator=); construct via
2793
+ # emplace, mirroring the var-decl / reassign frame-slot paths.
2794
+ # Pointer-form Optional targets stay `T*` and bind with `=`.
2795
+ out.write(f"{indent}{target}.emplace({enter_call});\n")
2796
+ else:
2797
+ out.write(f"{indent}{target} = {enter_call};\n")
2798
+ else:
2799
+ out.write(f"{indent}(*__with_ctx_{ctx_n}).__enter__();\n")
2800
+
2801
+ def _emit_async_with_setup(self, out: "TextIO", indent: str,
2802
+ stmt: 'rcfg.AsyncWithSetup') -> None:
2803
+ """Emit the async-with frame-slot population:
2804
+ __with_ctx_<n>.emplace(<context_expr>);
2805
+ (Construction into a `tpy::frame_slot<CM>` frame field;
2806
+ `frame_slot::operator=` is deleted, so writes go through
2807
+ `.emplace(...)`.) Subsequent Yield BBs (aenter/aexit) emplace
2808
+ `__sub_<i>` with `(*__with_ctx_<n>, ...)`."""
2809
+ ctx_n = stmt.ctx_n
2810
+ item = stmt.item
2811
+ ctx_expr = self.expressions.gen_expr(item.context_expr)
2812
+ self.ctx.temps.flush(out, indent)
2813
+ out.write(f"{indent}__with_ctx_{ctx_n}.emplace({ctx_expr});\n")
2814
+
2815
+ def _emit_with_exit(self, out: "TextIO", indent: str,
2816
+ region: 'rcfg.WithRegion',
2817
+ on_exception: bool) -> None:
2818
+ """Emit a single `__exit__` call. `on_exception=True` passes the
2819
+ catch-bound `__exc_<n>` (or `nullptr` when sema marked
2820
+ exit_takes_exc_val=False); otherwise passes the "normal exit"
2821
+ args (all empty / nullptr)."""
2822
+ item = region.item
2823
+ n = region.ctx_n
2824
+ if on_exception and item.exit_takes_exc_val:
2825
+ exc_arg = f"&__exc_{n}"
2826
+ elif item.exit_takes_exc_val:
2827
+ exc_arg = "nullptr"
2828
+ else:
2829
+ exc_arg = "{}"
2830
+ out.write(f"{indent}(*__with_ctx_{n}).__exit__("
2831
+ f"{{}}, {exc_arg}, {{}});\n")
2832
+
2833
+ def _for_info(self, func: TpyFunction, uid: int) -> 'GeneratorForInfo | None':
2834
+ return rcfg.resumable_state(func).for_info_by_uid.get(uid)
2835
+
2836
+ def _for_src_access(self, out: "TextIO", indent: str,
2837
+ iterable_expr: 'TpyExpr', uid: int,
2838
+ info: 'GeneratorForInfo') -> str:
2839
+ """Resolve the for-loop source expression for the begin_end / next
2840
+ strategies. When the iterable is a temporary (the pre-scan allocated
2841
+ `__for_src_<uid>`), store it once here and return the stored access;
2842
+ otherwise return the (re-evaluable) named expression."""
2843
+ src_cpp = self.expressions.gen_expr(iterable_expr)
2844
+ self.ctx.temps.flush(out, indent)
2845
+ if any(fn == f"__for_src_{uid}" for fn, _ in info.fields):
2846
+ out.write(f"{indent}__for_src_{uid}.emplace({src_cpp});\n")
2847
+ return f"(*__for_src_{uid})"
2848
+ return src_cpp
2849
+
2850
+ def _emit_async_for_iter_setup(self, out: "TextIO", indent: str,
2851
+ stmt: 'rcfg.AsyncForIterSetup',
2852
+ func: TpyFunction) -> None:
2853
+ """Initialize for-loop iteration state into the frame, per strategy
2854
+ (mirrors the legacy `_gen_generator_for_*` peephole init so range /
2855
+ begin_end generators keep their fast shape):
2856
+ async_for: `__for_itr.emplace((it).__aiter__())`
2857
+ range: `__for_i`/`__for_stop`[/`__for_step`] counters
2858
+ begin_end: `[__for_src.emplace(...);] __for_it.emplace(src.begin()); __for_end.emplace(...end())`
2859
+ next: `[__for_src.emplace(...);]` (the source IS the iterator)
2860
+ iter_next: `__for_itr.emplace(::tpy::__iter__(src))`
2861
+ """
2862
+ uid = stmt.uid
2863
+ if stmt.is_async:
2864
+ iter_cpp = self.expressions.gen_expr(stmt.iterable_expr)
2865
+ self.ctx.temps.flush(out, indent)
2866
+ out.write(f"{indent}__for_itr_{uid}.emplace(({iter_cpp}).__aiter__());\n")
2867
+ return
2868
+ info = self._for_info(func, uid)
2869
+ strat = info.strategy if info else "iter_next"
2870
+ if strat == "range":
2871
+ self._emit_for_range_setup(out, indent, stmt, uid)
2872
+ elif strat == "begin_end":
2873
+ src = self._for_src_access(out, indent, stmt.iterable_expr, uid, info)
2874
+ out.write(f"{indent}__for_it_{uid}.emplace(({src}).begin());\n")
2875
+ out.write(f"{indent}__for_end_{uid}.emplace(({src}).end());\n")
2876
+ elif strat == "next":
2877
+ # Source IS the iterator; just stash a temporary if needed.
2878
+ self._for_src_access(out, indent, stmt.iterable_expr, uid, info)
2879
+ else: # iter_next (universal)
2880
+ iter_cpp = self.expressions.gen_expr(stmt.iterable_expr)
2881
+ self.ctx.temps.flush(out, indent)
2882
+ out.write(f"{indent}__for_itr_{uid}.emplace(::tpy::__iter__({iter_cpp}));\n")
2883
+
2884
+ def _emit_for_range_setup(self, out: "TextIO", indent: str,
2885
+ stmt: 'rcfg.AsyncForIterSetup', uid: int) -> None:
2886
+ """range() counter init into the frame counters."""
2887
+ range_call = stmt.iterable_expr
2888
+ elem_type = stmt.elem_type
2889
+ if elem_type and isinstance(elem_type, IntLiteralType):
2890
+ elem_type = self.ctx.analyzer.ctx.default_int_type
2891
+ cpp_elem = self.types.type_to_cpp(elem_type) if elem_type else "int32_t"
2892
+ gen_args = self.statements.builtins.gen_range_args(range_call)
2893
+ self.ctx.temps.flush(out, indent)
2894
+ nargs = len(gen_args)
2895
+ ci, st = f"__for_i_{uid}", f"__for_stop_{uid}"
2896
+ if nargs == 1:
2897
+ out.write(f"{indent}{ci}.emplace({cpp_elem}(0));\n")
2898
+ out.write(f"{indent}{st}.emplace(static_cast<{cpp_elem}>({gen_args[0]}));\n")
2899
+ else:
2900
+ out.write(f"{indent}{ci}.emplace(static_cast<{cpp_elem}>({gen_args[0]}));\n")
2901
+ out.write(f"{indent}{st}.emplace(static_cast<{cpp_elem}>({gen_args[1]}));\n")
2902
+ if nargs == 3:
2903
+ sp = f"__for_step_{uid}"
2904
+ out.write(f"{indent}{sp}.emplace(static_cast<{cpp_elem}>({gen_args[2]}));\n")
2905
+ if self.statements._extract_int_literal(range_call.args[2]) is None:
2906
+ out.write(f"{indent}::tpy::range_check_step_nonzero(*{sp});\n")
2907
+ if not is_big_int_type(elem_type):
2908
+ self.statements._gen_range_overflow_check(
2909
+ out, indent, f"*{ci}", f"*{st}", f"*{sp}", elem_type)
2910
+
2911
+ def _for_advance_parts(self, func: TpyFunction,
2912
+ t: 'rcfg.AsyncForAdvance') -> tuple[str, str, list[str]]:
2913
+ """Return (pre, exhausted_test, bind_post) for the AsyncForAdvance,
2914
+ per strategy. `pre` runs before the exhaustion test; the loop exits
2915
+ when `exhausted_test` is true; `bind_post` is the list of statements
2916
+ that bind the loop var (and advance the cursor) on the live path."""
2917
+ uid = t.uid
2918
+ stmt = t.stmt
2919
+ info = self._for_info(func, uid)
2920
+ strat = info.strategy if info else "iter_next"
2921
+ cpp_var = escape_cpp_name(stmt.var)
2922
+ if strat == "range":
2923
+ ci, st = f"(*__for_i_{uid})", f"(*__for_stop_{uid})"
2924
+ range_call = stmt.iterable
2925
+ nargs = len(range_call.args)
2926
+ if nargs == 3:
2927
+ sp = f"(*__for_step_{uid})"
2928
+ step_lit = self.statements._extract_int_literal(range_call.args[2])
2929
+ if step_lit is not None and step_lit > 0:
2930
+ cont = f"{ci} < {st}"
2931
+ elif step_lit is not None and step_lit < 0:
2932
+ cont = f"{ci} > {st}"
2933
+ else:
2934
+ cont = f"({sp} > 0 ? {ci} < {st} : {ci} > {st})"
2935
+ bind_post = [f"{cpp_var} = {ci};", f"{ci} += {sp};"]
2936
+ else:
2937
+ cont = f"{ci} < {st}"
2938
+ bind_post = [f"{cpp_var} = ({ci})++;"]
2939
+ return ("", f"!({cont})", bind_post)
2940
+ if strat == "begin_end":
2941
+ it, end = f"(*__for_it_{uid})", f"(*__for_end_{uid})"
2942
+ if info.pointer_form_loop_var == stmt.var:
2943
+ bind_post = [f"{cpp_var} = &(*({it})++);"]
2944
+ else:
2945
+ bind_post = [f"{cpp_var} = *({it})++;"]
2946
+ return ("", f"{it} == {end}", bind_post)
2947
+ # next / iter_next: __next__() into __for_r, exhaust on !has_value.
2948
+ r = f"(*__for_r_{uid})"
2949
+ if strat == "next":
2950
+ src = self._for_src_expr(stmt, uid, info)
2951
+ pre = f"__for_r_{uid}.emplace({src}.__next__());"
2952
+ else: # iter_next
2953
+ pre = f"__for_r_{uid}.emplace((*__for_itr_{uid}).__next__());"
2954
+ elem = f"::tpy::unwrap_ref(*{r})"
2955
+ # Bind form mirrors the loop var's frame storage shape (D2a):
2956
+ # pointer-form `T*` (alias), frame_slot `.emplace`, or value assign.
2957
+ if info is not None and info.pointer_form_loop_var == stmt.var:
2958
+ bind_post = [f"{cpp_var} = &({elem});"]
2959
+ elif stmt.var in self.ctx.generator_frame_slot_locals:
2960
+ bind_post = [f"{cpp_var}.emplace({elem});"]
2961
+ else:
2962
+ bind_post = [f"{cpp_var} = {elem};"]
2963
+ return (pre, f"!{r}.has_value()", bind_post)
2964
+
2965
+ def _for_src_expr(self, stmt: 'TpyForEach', uid: int,
2966
+ info: 'GeneratorForInfo') -> str:
2967
+ """The iterator source for the `next` strategy advance: the stored
2968
+ `__for_src` (temporary) or the re-referencable named expression."""
2969
+ if any(fn == f"__for_src_{uid}" for fn, _ in info.fields):
2970
+ return f"(*__for_src_{uid})"
2971
+ return self.expressions.gen_expr(stmt.iterable)
2972
+
2973
+ def _emit_async_for_advance(self, out: "TextIO", indent: str,
2974
+ cfg: 'rcfg.CFG',
2975
+ t: 'rcfg.AsyncForAdvance',
2976
+ case_entries: dict[int, _StateLabel],
2977
+ func: TpyFunction,
2978
+ from_bb: int) -> None:
2979
+ """Emit the per-iteration advance for an AsyncForAdvance terminator
2980
+ (strategy-specific check + bind), then transfer to the body
2981
+ (`has_value_bb`) or loop exit (`exhausted_bb`)."""
2982
+ pre, exhausted_test, bind_post = self._for_advance_parts(func, t)
2983
+ if pre:
2984
+ out.write(f"{indent}{pre}\n")
2985
+ out.write(f"{indent}if ({exhausted_test}) {{\n")
2986
+ self.ctx.indent_level += 1
2987
+ inner = self.ctx.indent()
2988
+ self._emit_exit_region_finallies(
2989
+ out, inner,
2990
+ cfg.blocks[from_bb].region_stack,
2991
+ cfg.blocks[t.exhausted_bb].region_stack)
2992
+ if t.exhausted_bb in case_entries:
2993
+ out.write(f"{inner}__state = "
2994
+ f"{case_entries[t.exhausted_bb].cpp_name()};\n")
2995
+ out.write(f"{inner}continue;\n")
2996
+ else:
2997
+ # Exit BB isn't a case entry (shouldn't happen given
2998
+ # _compute_case_entries counts AsyncForAdvance successors,
2999
+ # but stay defensive: walk inline).
3000
+ self._walk_inline(out, cfg, t.exhausted_bb, case_entries, func)
3001
+ self.ctx.indent_level -= 1
3002
+ out.write(f"{indent}}}\n")
3003
+ # has_value path -- bind loop var (and advance the cursor) then body.
3004
+ for line in bind_post:
3005
+ out.write(f"{indent}{line}\n")
3006
+ self._walk_inline_or_jump(
3007
+ out, cfg, t.has_value_bb, case_entries, func, from_bb=from_bb)
3008
+
3009
+ def _emit_unreachable_tail(self, out: "TextIO", indent: str,
3010
+ func: TpyFunction) -> None:
3011
+ """Tail emission for a BB whose end is statically unreachable
3012
+ (no explicit return/raise). Generators that fall off the end stop
3013
+ iterating (StopIteration). For void async defs, emit Ready(unit);
3014
+ otherwise panic."""
3015
+ if self._is_generator_shape():
3016
+ # Fell off the end of the generator body -> StopIteration.
3017
+ self.statements._emit_finally_chain(out, indent)
3018
+ out.write(f"{indent}__state = S_DONE;\n")
3019
+ out.write(f"{indent}return ::tpy::make_unexpected("
3020
+ f"::tpy::StopIteration{{}});\n")
3021
+ return
3022
+ if self._is_void_return(func):
3023
+ # Walk any active finally frames before returning.
3024
+ self.statements._emit_finally_chain(out, indent)
3025
+ out.write(f"{indent}__state = S_DONE;\n")
3026
+ out.write(f"{indent}{POLL_VOID_READY_RETURN}\n")
3027
+ else:
3028
+ out.write(f"{indent}::tpy::tpy_panic(\"async def fell "
3029
+ f"through without returning a value\");\n")
3030
+
3031
+ @staticmethod
3032
+ def _sub_field_name(suspension_index: int) -> str:
3033
+ return f"__sub_{suspension_index}"
3034
+
3035
+ def _gen_coro_emplace_arg(self, arg: 'TpyExpr', arg_index: int,
3036
+ call: 'TpyCall | TpyMethodCall') -> str:
3037
+ """Generate one arg for `__sub_N.emplace(...)` constructing a
3038
+ sub-coroutine. Mirrors the param-type-driven coercions sync call
3039
+ codegen applies in `_gen_call`: pointer-form `Optional[NonValue]`
3040
+ lift (P -> &P) when the callee param is the `T*` shape, the
3041
+ @dynamic-protocol Adapter wrap when the callee takes `Own[P]`
3042
+ (needed for `await wait_for(coro, ...)` so the concrete coro
3043
+ gets boxed into `unique_ptr<Cancellable<T>>`), plus the full
3044
+ `Own[T]` move-out machinery (`std::move(name)` for last-use
3045
+ movable lvalues bound to `Own[T]` params) via `gen_call_arg`.
3046
+ """
3047
+ fi = call.resolved_function_info
3048
+ if fi is None or arg_index >= len(fi.params):
3049
+ return self.expressions.gen_expr(arg)
3050
+ ptype = fi.params[arg_index].type
3051
+ opt_arg = self.expressions._gen_optional_ptr_arg(arg, ptype)
3052
+ if opt_arg is not None:
3053
+ return opt_arg
3054
+ dynamic_arg = self.expressions._gen_dynamic_protocol_arg(arg, ptype)
3055
+ if dynamic_arg is not None:
3056
+ return dynamic_arg
3057
+ union_arg = self.expressions._gen_union_arg(
3058
+ arg, ptype, is_readonly_target=fi.is_readonly)
3059
+ if union_arg is not None:
3060
+ return union_arg
3061
+ return self.expressions.gen_call_arg(arg, ptype)
3062
+
3063
+ def _emit_sub_reset(self, out: "TextIO", indent: str,
3064
+ payload: 'rcfg.AwaitPayload',
3065
+ suspension_index: int) -> None:
3066
+ sub = self._sub_field_name(suspension_index)
3067
+ if payload.mode is rcfg.AwaitMode.BORROWED:
3068
+ out.write(f"{indent}{sub} = nullptr;\n")
3069
+ elif (payload.mode is rcfg.AwaitMode.INLINE
3070
+ or payload.mode is rcfg.AwaitMode.ERASED):
3071
+ out.write(f"{indent}{sub}.reset();\n")
3072
+ else:
3073
+ raise CodeGenError(f"unknown await mode {payload.mode!r}",
3074
+ loc=None)
3075
+
3076
+ def _emit_resume_core(self, out: "TextIO", indent: str,
3077
+ payload: 'rcfg.AwaitPayload',
3078
+ suspension_index: int,
3079
+ func: TpyFunction) -> None:
3080
+ """Emit the cancel check + poll + bind step at the start of a
3081
+ resume case body. Returns: caller continues with post-resume
3082
+ statements; for RETURN-kind, this function fully terminates the
3083
+ case body (walks finally chain and returns Ready)."""
3084
+ ret_cpp = self._poll_ret_cpp(func)
3085
+ sub = self._sub_field_name(suspension_index)
3086
+ # `::tpy::poll_with_cancel` propagates the outer's cancel into
3087
+ # the in-flight sub before polling (so the sub observes the
3088
+ # cancel at its own suspension point and can run
3089
+ # `finally`-with-await cleanup) and throws CancelledError if
3090
+ # the sub races past the cancel. See runtime/cpp/include/tpy/
3091
+ # async.hpp for the full semantics.
3092
+ out.write(f"{indent}auto __r{suspension_index} = "
3093
+ f"::tpy::poll_with_cancel({sub}, __cancel_pending, "
3094
+ f"waker);\n")
3095
+ out.write(f"{indent}if (__r{suspension_index}.is_pending()) "
3096
+ f"return {ret_cpp}::pending();\n")
3097
+ moved = f"std::move(__r{suspension_index}).value()"
3098
+ if (payload.kind is rcfg.AwaitKind.ASSIGN
3099
+ or payload.kind is rcfg.AwaitKind.VARDECL):
3100
+ target = escape_cpp_name(payload.bind_target)
3101
+ if payload.bind_target in self.ctx.generator_optional_fields:
3102
+ out.write(f"{indent}{target}.emplace({moved});\n")
3103
+ else:
3104
+ out.write(f"{indent}{target} = {moved};\n")
3105
+ elif payload.kind is rcfg.AwaitKind.RETURN:
3106
+ out.write(f"{indent}auto __ret{suspension_index} = {moved};\n")
3107
+ self._emit_sub_reset(out, indent, payload, suspension_index)
3108
+ # When a CFG-based finally is active, route the
3109
+ # `return await X` through the pending-return slot + flag
3110
+ # and transition to the finally entry instead of emitting
3111
+ # Poll::ready directly; AsyncFinallyExit at the finally
3112
+ # tail will emit the deferred Poll::ready after the
3113
+ # rethrow check.
3114
+ pending_flag = self.ctx.async_pending_return_flag
3115
+ if pending_flag is not None:
3116
+ pending_slot = self.ctx.async_pending_return_slot
3117
+ target_state = self.ctx.async_pending_return_target_state
3118
+ boundary = self.ctx.async_pending_return_boundary
3119
+ assert target_state is not None
3120
+ if pending_slot is not None and not self._is_void_return(func):
3121
+ out.write(f"{indent}this->{pending_slot} = "
3122
+ f"std::move(__ret{suspension_index});\n")
3123
+ else:
3124
+ out.write(f"{indent}(void)__ret{suspension_index};\n")
3125
+ out.write(f"{indent}this->{pending_flag} = true;\n")
3126
+ terminated = self.statements._emit_finally_chain(
3127
+ out, indent, stop_at=boundary)
3128
+ if not terminated:
3129
+ out.write(f"{indent}__state = {target_state};\n")
3130
+ out.write(f"{indent}continue;\n")
3131
+ return
3132
+ self.statements._emit_finally_chain(out, indent)
3133
+ if self._is_void_return(func):
3134
+ out.write(f"{indent}(void)__ret{suspension_index};\n")
3135
+ out.write(f"{indent}__state = S_DONE;\n")
3136
+ out.write(f"{indent}{POLL_VOID_READY_RETURN}\n")
3137
+ else:
3138
+ out.write(f"{indent}__state = S_DONE;\n")
3139
+ out.write(f"{indent}return ::tpystd::tpy::Poll<"
3140
+ f"{self._ret_cpp(func)}>::ready("
3141
+ f"std::move(__ret{suspension_index}));\n")
3142
+ return
3143
+ elif payload.kind is rcfg.AwaitKind.DISCARD:
3144
+ out.write(f"{indent}(void){moved};\n")
3145
+ else:
3146
+ raise CodeGenError(f"unknown await kind {payload.kind!r}",
3147
+ loc=None)
3148
+ self._emit_sub_reset(out, indent, payload, suspension_index)
3149
+
3150
+ def _emit_yield_terminator(self, out: "TextIO", indent: str,
3151
+ t: 'rcfg.Yield', func: TpyFunction) -> None:
3152
+ """Emit the full suspend action for a Yield terminator (policy
3153
+ seam). Async stores the sub-future + advances state, then
3154
+ `continue`s to re-enter the switch at the new state -- the resume
3155
+ case's poll() returns Pending iff the sub-future is genuinely
3156
+ not-yet-ready; continuing (rather than returning Pending
3157
+ unconditionally) lets a synchronously-ready sub-future complete
3158
+ in one poll. The generator shape emits the yielded value +
3159
+ advances state + returns the value to the caller (the next
3160
+ `__next__()` call resumes at the new state)."""
3161
+ if self._is_generator_shape():
3162
+ self._emit_generator_yield(out, indent, t)
3163
+ return
3164
+ self._emit_suspend(out, indent, t.payload, t.suspension_index, func)
3165
+ out.write(f"{indent}continue;\n")
3166
+
3167
+ def _emit_generator_yield(self, out: "TextIO", indent: str,
3168
+ t: 'rcfg.Yield') -> None:
3169
+ """Generator suspension: compute the yielded value, advance to the
3170
+ resume state, and return it to the caller. The next `__next__()`
3171
+ call re-enters the switch at the resume state, whose case header
3172
+ is a no-op (no poll/bind) and simply continues after the yield."""
3173
+ payload = t.payload
3174
+ assert isinstance(payload, rcfg.YieldPayload), \
3175
+ "generator shape requires a YieldPayload terminator"
3176
+ ys = payload.yield_stmt
3177
+ assert ys is not None, \
3178
+ "YieldPayload built from a generator body always carries yield_stmt"
3179
+ if ys.loc is not None:
3180
+ self.ctx.emit_source_comment(out, ys.loc, indent)
3181
+ yield_expr = self.statements.gen_yield_value(ys)
3182
+ self.ctx.temps.flush(out, indent)
3183
+ resume = _StateLabel(_StateKind.RESUME, t.suspension_index).cpp_name()
3184
+ out.write(f"{indent}__state = {resume};\n")
3185
+ out.write(f"{indent}return {yield_expr};\n")
3186
+
3187
+ def _emit_suspend(self, out: "TextIO", indent: str,
3188
+ payload: 'rcfg.AwaitPayload',
3189
+ suspension_index: int,
3190
+ func: TpyFunction) -> None:
3191
+ """Emit the emplace + state-advance step at a Yield terminator.
3192
+
3193
+ Inline mode: emplace the sub-coro struct directly via its ctor.
3194
+ Erased mode: move the operand value into the optional field.
3195
+ Borrowed mode: store the operand's address in the pointer field.
3196
+ """
3197
+ if payload.host_stmt is not None and payload.host_stmt.loc is not None:
3198
+ self.ctx.emit_source_comment(out, payload.host_stmt.loc, indent)
3199
+ sub = self._sub_field_name(suspension_index)
3200
+ if payload.mode is rcfg.AwaitMode.INLINE:
3201
+ if payload.async_with_kind is not None:
3202
+ # Async-with synthetic yield. Receiver is the CM frame
3203
+ # slot; args differ by kind.
3204
+ ctx_n = payload.async_with_ctx_n
3205
+ recv = f"(*__with_ctx_{ctx_n})"
3206
+ if payload.async_with_kind is rcfg.AsyncWithKind.AENTER:
3207
+ out.write(f"{indent}{sub}.emplace({recv});\n")
3208
+ else: # AEXIT -- cleanup-only call with all-None args
3209
+ out.write(f"{indent}{sub}.emplace({recv}, "
3210
+ f"::std::monostate{{}}, "
3211
+ f"::std::monostate{{}}, "
3212
+ f"::std::monostate{{}});\n")
3213
+ elif payload.async_for_uid is not None:
3214
+ uid = payload.async_for_uid
3215
+ out.write(f"{indent}{sub}.emplace(*__for_itr_{uid});\n")
3216
+ else:
3217
+ call = payload.operand_expr
3218
+ if isinstance(call, TpyCall):
3219
+ args = [self._gen_coro_emplace_arg(arg, i, call)
3220
+ for i, arg in enumerate(call.args)]
3221
+ self.ctx.temps.flush(out, indent)
3222
+ out.write(f"{indent}{sub}.emplace({', '.join(args)});\n")
3223
+ elif isinstance(call, TpyMethodCall):
3224
+ is_module_call = (call.user_module_call is not None
3225
+ or call.builtin_module_call is not None)
3226
+ if is_module_call:
3227
+ # `module.func(...)` -- receiver is a namespace,
3228
+ # not a value, so the sub-coro ctor takes only
3229
+ # the function args.
3230
+ args = [self._gen_coro_emplace_arg(a, i, call)
3231
+ for i, a in enumerate(call.args)]
3232
+ self.ctx.temps.flush(out, indent)
3233
+ out.write(f"{indent}{sub}.emplace({', '.join(args)});\n")
3234
+ else:
3235
+ # Bound async method: prepend receiver as __self ctor arg.
3236
+ recv_cpp = self.expressions.gen_expr(call.obj)
3237
+ arg_cpps = [self._gen_coro_emplace_arg(a, i, call)
3238
+ for i, a in enumerate(call.args)]
3239
+ self.ctx.temps.flush(out, indent)
3240
+ joined = ", ".join([recv_cpp] + arg_cpps)
3241
+ out.write(f"{indent}{sub}.emplace({joined});\n")
3242
+ else:
3243
+ raise CodeGenError(
3244
+ "internal: inline-mode await operand is not a call",
3245
+ loc=None)
3246
+ elif payload.mode is rcfg.AwaitMode.ERASED:
3247
+ operand_cpp = self.expressions.gen_expr(payload.operand_expr)
3248
+ self.ctx.temps.flush(out, indent)
3249
+ out.write(f"{indent}{sub}.emplace(std::move({operand_cpp}));\n")
3250
+ elif payload.mode is rcfg.AwaitMode.BORROWED:
3251
+ operand_cpp = self.expressions.gen_expr(payload.operand_expr)
3252
+ self.ctx.temps.flush(out, indent)
3253
+ out.write(f"{indent}{sub} = &({operand_cpp});\n")
3254
+ else:
3255
+ raise CodeGenError(f"unknown await mode {payload.mode!r}",
3256
+ loc=None)
3257
+ out.write(f"{indent}__state = "
3258
+ f"{_StateLabel(_StateKind.RESUME, suspension_index).cpp_name()};\n")