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
tpyc/sema/analyzer.py ADDED
@@ -0,0 +1,3625 @@
1
+ """
2
+ TurboPython Semantic Analyzer
3
+
4
+ Main orchestrator that wires all components together.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from dataclasses import replace as dc_replace
9
+ from typing import Optional
10
+
11
+ from ..typesys import (
12
+ TpyType, TypeRegistry, NominalType, AliasRef, UnionType, FinalType, STR, LiteralType, VoidType, VOID,
13
+ NoneType, INT32, ReadonlyType, unwrap_readonly, unwrap_optional_own, OwnType, OptionalType, RecordInfo, FieldInfo,
14
+ RecursiveUnionInfo, RecursiveAliasInstanceType,
15
+ FunctionInfo, ParamInfo, MethodSignature, is_any_str_type, BIGINT, FLOAT,
16
+ make_ref, unwrap_ref_type, RefType, TypeParamKind, TypeParamRef, TupleType, PtrType,
17
+ TypeAliasInfo,
18
+ is_integer_type, is_void_like_type,
19
+ span_as_const, span_is_readonly,
20
+ _contains_self_reference,
21
+ contains_type_param,
22
+ del_suppresses_default_ctor,
23
+ )
24
+ from ..type_def_registry import is_span
25
+ from ..compilation_context import get_current_compiler
26
+ from ..namespace import Namespace, NameBinding, BindingKind
27
+ from ..parse import TpyModule, TpyRecord, TpyFunction, TpyExpr, TpyStmt, TpyVarDecl, is_docstring, is_super_del_call, is_base_init_call, ParseError
28
+ from ..parse.nodes import RecordLinkage
29
+ from .registration import build_record_self_type, _vararg_span_type
30
+ from ..parse.nodes import (
31
+ TpyStrLiteral, TpyAssign, TpyIf, TpyWhile, TpyForEach, TpyFieldAccess, TpyName, TpyCall,
32
+ TpyMethodCall, TpyExprStmt, TpyRaise, TpyTry, TpyMatch, TpyNestedDef,
33
+ expr_contains_self_method_call,
34
+ )
35
+ from .expressions import _collect_body_name_refs
36
+
37
+ from ..diagnostics import Scope, Diagnostic, SemanticError
38
+ from .context import SemanticContext, RecordContext, MODULE_INIT_CONTEXT
39
+ from .type_ops import TypeOperations
40
+ from .operators import OperatorResolver
41
+ from .compatibility import TypeCompatibility
42
+ from .list_literals import IterableHelper
43
+ from .local_deduction import LocalTypeDeduction
44
+ from .protocols import ProtocolChecker
45
+ from .registration import TypeRegistrar
46
+ from .narrowing import NarrowingTracker
47
+ from .expressions import ExpressionAnalyzer
48
+ from .calls import CallAnalyzer
49
+ from .methods import MethodAnalyzer
50
+ from .statements import StatementAnalyzer
51
+
52
+ from ..prescan import ScanResult, scan_reassigned_vars
53
+ from ..liveness import analyze_last_uses
54
+ from .mutation_propagation import propagate_mutation_facts, infer_method_const
55
+ from tpyc import modules as builtin_modules
56
+ from ..cycle_detection import detect_type_cycles
57
+ from ..parse import SourceLocation, is_parser_keyword
58
+ from ..type_def_registry import (
59
+ is_str_type, is_str_view_type, enum_info_of,
60
+ is_array, is_enum_type, is_list, is_dict, is_set, is_span,
61
+ )
62
+ from ..parse.resolve_refs import (
63
+ _walk_body, _merged_method_scope, _record_scope,
64
+ promote_bare_nominals,
65
+ )
66
+ from .macros import _promote_method_signature
67
+ from .builder_trace import BuilderTraceExpander
68
+ from ..symbol_binding import (
69
+ SymbolKind, install_binding, lookup_imported, protocol_kind_for,
70
+ is_macro_kind, is_kind, walk_attribute_chain, resolve_definer,
71
+ )
72
+
73
+
74
+
75
+
76
+ def _body_has_raise(stmts: list[TpyStmt], exception_type: str) -> bool:
77
+ """Return True if any path in *stmts* contains ``raise <exception_type>``.
78
+
79
+ Called after body analysis, so TpyRaise.exception_type is already
80
+ qualified (e.g. 'builtins.StopIteration').
81
+ """
82
+ for stmt in stmts:
83
+ if isinstance(stmt, TpyRaise) and stmt.exception_type == exception_type:
84
+ return True
85
+ if isinstance(stmt, TpyIf):
86
+ if _body_has_raise(stmt.then_body, exception_type) or _body_has_raise(stmt.else_body, exception_type):
87
+ return True
88
+ elif isinstance(stmt, (TpyWhile, TpyForEach)):
89
+ if _body_has_raise(stmt.body, exception_type) or _body_has_raise(stmt.orelse, exception_type):
90
+ return True
91
+ elif isinstance(stmt, TpyTry):
92
+ if (_body_has_raise(stmt.try_body, exception_type)
93
+ or _body_has_raise(stmt.else_body, exception_type)
94
+ or _body_has_raise(stmt.finally_body, exception_type)):
95
+ return True
96
+ for handler in stmt.handlers:
97
+ if _body_has_raise(handler.body, exception_type):
98
+ return True
99
+ elif isinstance(stmt, TpyMatch):
100
+ for case in stmt.cases:
101
+ if _body_has_raise(case.body, exception_type):
102
+ return True
103
+ return False
104
+
105
+
106
+ class SemanticAnalyzer:
107
+ """Semantic analyzer for TurboPython."""
108
+
109
+ def __init__(self, default_int_type: TpyType = INT32,
110
+ shared_modules: 'dict[str, object] | None' = None):
111
+ # Create shared context. `shared_modules`, when provided, gives
112
+ # this analyzer's TypeRegistry the workspace-wide ModuleInfo dict
113
+ # owned by the Compiler -- every analyzer in one compilation
114
+ # reads/writes the same dict, so cross-module qname lookups
115
+ # (`get_record_for_type`, recursive-union detection, builder-trace
116
+ # type rendering) see every peer module's contribution. Per-module
117
+ # short-name bindings (records / functions / protocols /
118
+ # type_aliases / enums) stay strictly per-analyzer; only the
119
+ # `modules` surface is shared. None preserves the legacy
120
+ # per-analyzer behavior (used by REPL, tests, ad-hoc callers).
121
+ self.ctx = SemanticContext(
122
+ registry=TypeRegistry(shared_modules=shared_modules),
123
+ global_scope=Scope(),
124
+ default_int_type=default_int_type,
125
+ builtins_ns=Namespace(),
126
+ macro_ns=None, # Set below
127
+ global_ns=None, # Set below
128
+ )
129
+ # Chain: global_ns -> macro_ns -> builtins_ns
130
+ self.ctx.macro_ns = Namespace(parent=self.ctx.builtins_ns)
131
+ self.ctx.global_ns = Namespace(parent=self.ctx.macro_ns)
132
+
133
+ # Layer 1: No dependencies on other analyzers
134
+ self.type_ops = TypeOperations(self.ctx)
135
+ self.compat = TypeCompatibility(self.ctx)
136
+ self.iterable = IterableHelper(self.ctx)
137
+ self.deduction = LocalTypeDeduction(self.ctx, self.compat)
138
+
139
+ # Layer 2: Depends on type_ops
140
+ self.protocols = ProtocolChecker(self.ctx, self.type_ops)
141
+ self.registrar = TypeRegistrar(self.ctx, self.type_ops, self.protocols)
142
+ self.operators = OperatorResolver(self.ctx, self.type_ops, self.protocols)
143
+
144
+ # Wire up compatibility's deferred dependencies
145
+ self.compat.type_ops = self.type_ops
146
+ self.compat.protocols = self.protocols
147
+ self.compat.deduction = self.deduction
148
+ # validate_hashable_container_elem needs Hashable conformance
149
+ self.type_ops.protocols = self.protocols
150
+
151
+ # Narrowing tracker (depends on type_ops, protocols)
152
+ self.narrowing = NarrowingTracker(self.ctx, self.type_ops, self.protocols)
153
+
154
+ # Layer 3: Analyzers ordered by dependencies
155
+ # Cycle: expr <-> calls <-> methods. Break by creating calls/methods
156
+ # first, then expr (which takes them), then setting back-references.
157
+ self.calls = CallAnalyzer(
158
+ self.ctx, self.type_ops, self.protocols, self.compat, self.deduction
159
+ )
160
+ self.methods = MethodAnalyzer(
161
+ self.ctx, self.type_ops, self.protocols, self.compat, self.deduction
162
+ )
163
+ self.expr = ExpressionAnalyzer(
164
+ self.ctx, self.type_ops, self.operators, self.protocols, self.compat,
165
+ self.narrowing, self.calls, self.methods,
166
+ )
167
+ # Complete the back-references
168
+ self.calls.expr = self.expr
169
+ self.calls.methods = self.methods
170
+ self.methods.expr = self.expr
171
+ self.methods.calls = self.calls
172
+ self.compat.methods = self.methods
173
+
174
+ # stmts and match: one-way deps, passed through constructors
175
+ self.stmts = StatementAnalyzer(
176
+ self.ctx, self.type_ops, self.compat, self.deduction, self.iterable,
177
+ self.protocols, self.narrowing, self.expr,
178
+ )
179
+ self.expr.set_scopes(self.stmts.scopes)
180
+
181
+ # Per-function/method pre-scan results (shared with codegen)
182
+ self.function_scan_results: dict[int, ScanResult] = {}
183
+ self.top_level_scan_result: ScanResult | None = None
184
+
185
+ # Per-function/method hoisted vars (scope escape phase 2)
186
+ self.function_hoisted_vars: dict[int, set[str]] = {}
187
+ self.top_level_hoisted_vars: set[str] = set()
188
+
189
+ # Per-function/method move-through vars (lvalue alias promoted to rvalue)
190
+ self.function_move_through_vars: dict[int, set[str]] = {}
191
+ self.top_level_move_through_vars: set[str] = set()
192
+
193
+ # Per-function movable locals (owned, not hoisted/loop/lvalue-reassigned)
194
+ self.function_movable_locals: dict[int, set[str]] = {}
195
+
196
+ # Per-function `global x` declarations (for codegen)
197
+ self.function_global_decls: dict[int, set[str]] = {}
198
+
199
+ # Branch-declared vars that need pre-declaration before if-statements
200
+ self.if_branch_decls: dict[int, dict[str, TpyType]] = {}
201
+
202
+ # @overload dispatch groups: implementation func id -> list of stub TpyFunctions
203
+ # Used by codegen to emit per-overload specialized C++ functions.
204
+ self.overload_groups: dict[int, list[TpyFunction]] = {}
205
+
206
+ # Convenience aliases for public API
207
+ self.registry = self.ctx.registry
208
+ self.global_scope = self.ctx.global_scope
209
+ self.diagnostics = self.ctx.diagnostics
210
+
211
+ # Populate builtins_ns with Python builtins (always available without import)
212
+ # These are like CPython's builtins module - int, str, list, len, print, etc.
213
+ self._register_python_builtins()
214
+
215
+ # Public API compatibility properties
216
+ @property
217
+ def current_scope(self) -> Optional[Scope]:
218
+ return self.ctx.func.current_scope
219
+
220
+ @property
221
+ def current_function(self) -> Optional[TpyFunction]:
222
+ return self.ctx.func.current_function
223
+
224
+ @property
225
+ def expr_types(self) -> dict[int, TpyType]:
226
+ return self.ctx.expr_types
227
+
228
+ @property
229
+ def var_types(self) -> dict[int, TpyType]:
230
+ return self.ctx.var_types
231
+
232
+ @property
233
+ def imports(self) -> dict:
234
+ return self.ctx.imports
235
+
236
+ @property
237
+ def imported_names(self) -> dict:
238
+ return self.ctx.imported_names
239
+
240
+ @property
241
+ def list_literals(self) -> dict:
242
+ return self.ctx.list_literals
243
+
244
+ @property
245
+ def global_ns(self):
246
+ return self.ctx.global_ns
247
+
248
+ @property
249
+ def builtins_ns(self):
250
+ return self.ctx.builtins_ns
251
+
252
+ def _error(self, message: str, node: TpyExpr | TpyStmt | None = None) -> SemanticError:
253
+ """Create a SemanticError with location from a node."""
254
+ return self.ctx.error(message, node)
255
+
256
+ def _warning(self, message: str, node: TpyExpr | TpyStmt | TpyRecord | None = None) -> None:
257
+ """Record a warning diagnostic."""
258
+ self.ctx.warning(message, node)
259
+
260
+ def _register_python_builtins(self) -> None:
261
+ """Register Python builtins in builtins_ns (always available without import).
262
+
263
+ This mirrors CPython's builtins module. Names like int, str, len, print
264
+ are available in every scope without explicit import.
265
+ """
266
+ # Python builtin types (as constructors)
267
+ # These are registered as IMPORTED_NAME from "builtins" module
268
+ # Generic types (list) are also registered - they're handled by generic type
269
+ # inference code but need namespace entry to distinguish from tpy types
270
+ python_builtin_types = ["int", "str", "bool", "float", "bytes", "bytearray",
271
+ "list", "dict", "set",
272
+ "slice", "super",
273
+ "BaseException", "Exception", "StopIteration",
274
+ "TextIO"]
275
+ for name in python_builtin_types:
276
+ self.ctx.builtins_ns.bind_imported_name(name, "builtins", name)
277
+
278
+ # Python builtin functions -- available without import (like CPython).
279
+ # Registered in both builtins_ns (for sema lookup) and imported_names
280
+ # (for codegen to resolve to the lib/tpy/builtins.py module functions).
281
+ python_builtin_functions = ["len", "repr", "hash", "chr", "ord", "abs",
282
+ "min", "max", "pow", "round", "divmod",
283
+ "next", "print", "input", "range", "enumerate",
284
+ "zip", "isinstance", "iter",
285
+ "all", "any", "sum", "sorted",
286
+ "bin", "hex", "oct", "reversed",
287
+ "map", "filter",
288
+ "open", "getattr", "setattr", "delattr", "hasattr"]
289
+ for name in python_builtin_functions:
290
+ self.ctx.builtins_ns.bind_imported_name(name, "builtins", name)
291
+ self.ctx.imported_names[name] = ("builtins", name)
292
+
293
+ # Sub-phase indices used by the phase-counter assertion. The five
294
+ # public methods below must be called in this exact order; calling
295
+ # them out of order raises AssertionError. Compiler still drives
296
+ # all five through `analyze()` today; future phases (Phase 3+) will
297
+ # call individual sub-phases workspace-wide.
298
+ _PHASE_NONE = 0
299
+ _PHASE_BIND_IMPORTS = 1
300
+ _PHASE_REGISTER_RECORDS_AND_PROTOCOLS = 2
301
+ _PHASE_REGISTER_SIGNATURES = 3
302
+ _PHASE_ANALYZE_BODIES = 4
303
+ _PHASE_PHASE2_FIXPOINT = 5
304
+
305
+ def _advance_phase(self, expected_prev: int, completed: int) -> None:
306
+ """Phase-counter guard. `expected_prev` is the phase that must
307
+ have just completed; `completed` is the phase the caller is
308
+ finishing now. Raises AssertionError on out-of-order calls.
309
+ """
310
+ current = getattr(self, "_completed_phase", self._PHASE_NONE)
311
+ assert current == expected_prev, (
312
+ f"sub-phase ordering violated: expected previous phase "
313
+ f"{expected_prev}, got {current} (trying to complete "
314
+ f"{completed})"
315
+ )
316
+ self._completed_phase = completed
317
+
318
+ def analyze(self, module: TpyModule, module_name: str = "__main__",
319
+ cpp_module_name: str | None = None) -> None:
320
+ """Analyze a module for semantic correctness.
321
+
322
+ Args:
323
+ module: The parsed module AST.
324
+ module_name: Name of this module ("__main__" for entry point).
325
+ cpp_module_name: File-derived structural name used by codegen
326
+ for namespace identity. Defaults to `module_name`; callers
327
+ analyzing an entry-point module should pass the file name
328
+ so `union_wrapper_index.origin` matches what codegen
329
+ compares against. See `bind_imports` for details.
330
+
331
+ Note: User module dependencies should be registered in registry.modules
332
+ before calling this method (via register_module).
333
+
334
+ This drives the five public sub-phases in order. Workspace-wide
335
+ callers (Phase 3+) will eventually call the sub-phases directly
336
+ across all modules; for now `analyze()` is a thin wrapper that
337
+ preserves the legacy per-module-with-publishing pipeline.
338
+ """
339
+ self.bind_imports(module, module_name, cpp_module_name=cpp_module_name)
340
+ self.register_records_and_protocols(module)
341
+ self.register_signatures(module)
342
+ self.analyze_bodies(module)
343
+ self.run_phase2_fixpoint(module)
344
+
345
+ def bind_imports(self, module: TpyModule, module_name: str = "__main__",
346
+ cpp_module_name: str | None = None) -> None:
347
+ """Sub-phase 1: set module context, bind imports + bare modules.
348
+
349
+ Establishes `ctx.module_name`, `ctx.parser_resolver`, the imports
350
+ dict, star-import registrations, tpy type-alias bindings, and
351
+ bare-module bindings. Macro registries and resolved type refs
352
+ are expected to already be in place (compiler-driven).
353
+
354
+ `module_name` is the runtime/`__name__` flavor ("__main__" for
355
+ the entry point). `cpp_module_name` (when given) is the
356
+ file-derived structural name that codegen uses for namespace
357
+ identity -- needed by `_register_union_wrappers` so the
358
+ wrapper-index origin matches what codegen compares against.
359
+ Defaults to `module_name` for non-entry callers that already
360
+ pass the structural name.
361
+ """
362
+ # Set module context
363
+ self.ctx.module_name = module_name
364
+ self.ctx.cpp_module_name = cpp_module_name or module_name
365
+ self.ctx.module_cpp_namespace = getattr(module.directives, 'cpp_namespace', None) if hasattr(module, 'directives') else None
366
+ # Builder-trace expansion (phase 7) needs to splice synthesized
367
+ # records/functions into the module while bodies are being analyzed.
368
+ self._module = module
369
+ # Module resolver -- consumed by `_infer_field_type_from_default`
370
+ # (same-module record lookup) and the macro post-resolve step in
371
+ # `register_record` (body TypeRefNodes on macro-added methods).
372
+ # See `SemanticContext.parser_resolver` docstring.
373
+ self.ctx.parser_resolver = module.resolver
374
+
375
+ # Convert parse warnings to diagnostics
376
+ for warning in module.parse_warnings:
377
+ self.ctx.warning_from_loc(warning.message, warning.loc)
378
+
379
+ # Process imports
380
+ self.ctx.imports = module.imports
381
+ self.ctx.bare_module_imports = module.bare_module_imports
382
+ if module.tpy_star_import:
383
+ self.registrar.register_tpy_star_import()
384
+ self._register_all_tpy_type_aliases()
385
+ for import_module_name, names in self.ctx.imports.items():
386
+ if isinstance(names, set):
387
+ if module.tpy_star_import and import_module_name == "tpy":
388
+ continue # already fully registered above
389
+ # "from X import Y" or "from X import Y as Z"
390
+ # Stored as (original_name, local_name) tuples to support aliases
391
+ is_star = import_module_name in module.star_imports
392
+ for original_name, local_name in names:
393
+ is_user_module = import_module_name in module.user_module_imports
394
+ # Generic IMPORTED_NAME goes in first so the
395
+ # kind-specific binding installed by
396
+ # `_register_user_module_import` (bind_variable /
397
+ # bind_enum / ...) lands on top for callers that
398
+ # consult the namespace by kind.
399
+ self.ctx.imported_names[local_name] = (import_module_name, original_name)
400
+ self.ctx.global_ns.bind_imported_name(local_name, import_module_name, original_name)
401
+
402
+ if is_star and is_user_module:
403
+ if not self._register_user_module_import(
404
+ import_module_name, original_name, local_name,
405
+ from_star_import=True):
406
+ # Re-resolve a name absent from this source's
407
+ # exports against any module that does export
408
+ # it (typical case: `Int32` star-imported via
409
+ # `from utils import *` where utils itself
410
+ # re-imports it from tpy). `_bind_star_reexport`
411
+ # rewrites `imported_names` and the namespace
412
+ # binding to point at the actual definer.
413
+ self._bind_star_reexport(original_name, local_name)
414
+ continue
415
+
416
+ if is_user_module:
417
+ self._register_user_module_import(import_module_name, original_name, local_name)
418
+ # Register tpy type aliases from .py stubs (no-op for non-alias names)
419
+ elif import_module_name == "tpy":
420
+ self._register_tpy_type_alias(original_name, local_name)
421
+ # Re-exported module-level variables from tpy/__init__.py
422
+ # need explicit promotion to VARIABLE bindings (the implicit
423
+ # path otherwise leaves them as IMPORTED_NAME, which use
424
+ # sites reject). Records/functions/factories already resolve
425
+ # through other machinery -- leave them alone here.
426
+ self._register_implicit_module_variable(
427
+ import_module_name, original_name, local_name)
428
+ elif names is None:
429
+ # "import X" for special modules (tpy, typing, etc.)
430
+ alias = module.module_aliases.get(import_module_name)
431
+ self.ctx.global_ns.bind_module(import_module_name, alias)
432
+ # Register tpy type aliases for qualified annotation resolution
433
+ # (e.g. import tpy; x: tpy.Float64)
434
+ if import_module_name == "tpy":
435
+ self._register_all_tpy_type_aliases()
436
+
437
+ # Bind modules for bare `import X` statements (user modules)
438
+ for mod_name in module.bare_module_imports:
439
+ alias = module.module_aliases.get(mod_name)
440
+ self.ctx.global_ns.bind_module(mod_name, alias)
441
+
442
+ # TypeRefNodes were already resolved before `analyze()` was
443
+ # called (by `Compiler._resolve_module_refs` between
444
+ # canonicalization and sema). Everything downstream can read
445
+ # TpyType uniformly.
446
+ self._advance_phase(self._PHASE_NONE, self._PHASE_BIND_IMPORTS)
447
+
448
+ def register_records_and_protocols(self, module: TpyModule) -> None:
449
+ """Sub-phase 2: enums, records (incl. macro application), then
450
+ protocols, inheritance + value-type validation, recursive-union
451
+ detection, and type-alias registration. Everything declaration-
452
+ level except function signatures.
453
+ """
454
+ # Method-linkage validation (stubs allowed/required per record
455
+ # linkage, @native decorator restrictions) runs here so any
456
+ # base-resolution errors from the resolve phase fire first.
457
+ self._validate_record_method_linkage(module)
458
+
459
+ # Resolve imported type aliases in AST type annotations.
460
+ # The parser creates NominalType("Shape") for imported aliases since it
461
+ # doesn't know about cross-module aliases at parse time. Substitute them
462
+ # with the resolved types before registration/analysis.
463
+ if self.ctx.registry.imported_type_alias_info:
464
+ self._resolve_imported_aliases(module)
465
+
466
+ # First pass: register all enums then records. Method expansion
467
+ # (self-flag derivation, wrapping, cloning, validation) runs
468
+ # inside `register_record` after `_apply_class_macros` so macro-
469
+ # added methods flow through the same pipeline as regular ones.
470
+ # Enums are registered first so record field-type resolution
471
+ # (inside `register_record`) can substitute parser-level
472
+ # `NominalType("Color")` placeholders with the registered enum
473
+ # NominalType that carries `_module_qname`. Records never depend
474
+ # on each other at registration time, and enums are self-
475
+ # contained, so this ordering is safe even for nested enums --
476
+ # nested enum names are already dotted ("Message.Kind") when
477
+ # `register_enum` sees them, so the parent record need not
478
+ # exist in the sema registry yet.
479
+ for enum in module.all_enums():
480
+ self.registrar.register_enum(enum)
481
+ for record in module.all_records():
482
+ self.registrar.register_record(record)
483
+
484
+ # Populate macro_ns with exports from macro dep modules.
485
+ # Macros have run during register_record, so we know which macro
486
+ # modules were used and can import their dependencies.
487
+ self._populate_macro_deps(module)
488
+
489
+ # Promote bare NominalTypes that macros emitted via
490
+ # `types.named(...)` to qname-bearing form now that macro_deps
491
+ # are in ctx.registry. Macros like `@model` reference types
492
+ # (JsonReader, JsonWriter) that main.py doesn't import
493
+ # explicitly; they land in the registry only after
494
+ # `_populate_macro_deps`, so promotion has to run here rather
495
+ # than inside `register_record`.
496
+ self._promote_macro_generated_types(module)
497
+
498
+ # Register protocols (two phases to allow forward references)
499
+ for protocol in module.protocols:
500
+ self.registrar.register_protocol(protocol)
501
+ # Populate ProtocolInfo.transitive_supertypes (single source of
502
+ # truth for subtype queries) before any record-side closure or
503
+ # subtype-using check reads it.
504
+ for protocol in module.protocols:
505
+ self.registrar.finalize_protocol_closure(protocol)
506
+ for protocol in module.protocols:
507
+ self.registrar.validate_protocol_parents(protocol)
508
+
509
+ # Validate inheritance relationships (after all records and protocols
510
+ # are registered). Sets `implemented_protocols` / MRO on each record.
511
+ for record in module.all_records():
512
+ self.registrar.validate_record_inheritance(record)
513
+
514
+ # Re-validate record field types now that protocols are registered AND
515
+ # inheritance is finalized. Optional[@dynamic] / container-element /
516
+ # union-protocol checks rely on protocol_info_of, which only resolves
517
+ # once protocols are in the registry; the Own[Optional[Polymorphic]]
518
+ # check needs implemented_protocols/MRO populated. Runs after
519
+ # inheritance so both prerequisites hold.
520
+ for record in module.all_records():
521
+ self.registrar.validate_record_field_protocols(record)
522
+
523
+ # Validate @error_return(E) on methods (ReturnException markers are now set)
524
+ for record in module.all_records():
525
+ self.registrar.validate_method_error_returns(record)
526
+
527
+ # Validate ValueType fields in a second pass (all ValueType flags are set now)
528
+ for record in module.all_records():
529
+ self.registrar.validate_value_type_fields(record)
530
+
531
+ # Validate default_factory fields conform to Default protocol
532
+ self._validate_factory_defaults(module)
533
+
534
+ # Propagate @nocopy from fields to containing records.
535
+ # Done after inheritance validation so parent types are resolved.
536
+ # Definition order handles transitive propagation naturally.
537
+ self._propagate_nocopy(module)
538
+
539
+ # Validate that every recursive path in a recursive union alias goes
540
+ # through an indirecting container. Runs here -- after record/protocol
541
+ # registration -- so the field-walk in `validate_recursive_union_paths`
542
+ # can resolve same-module RecordInfo / TypeDef entries for user
543
+ # indirecting records (otherwise we'd false-positive on `class
544
+ # MyBox[T]: _ptr: Ptr[T]; type Bad = Lit | MyBox[Bad]`).
545
+ self._validate_recursive_union_paths(module)
546
+
547
+ # Detect mutual recursion cycles and tag recursive union aliases
548
+ self._detect_recursive_unions(module)
549
+ self.ctx.recursive_union_names = module.recursive_union_names
550
+ self._register_union_wrappers(module)
551
+
552
+ # The parser eagerly expands same-module union aliases, so
553
+ # RecursiveAlias | None becomes UnionType(NoneType, member1, member2, ...)
554
+ # instead of OptionalType(NominalType("RecursiveAlias")).
555
+ # Now that recursive aliases are identified, fix up those annotations.
556
+ if module.recursive_union_names:
557
+ self._fix_recursive_optional_annotations(module)
558
+
559
+ # Transfer type aliases from parser to sema registry, validating members
560
+ compiler = get_current_compiler()
561
+ display_names = compiler.union_display_names if compiler is not None else None
562
+ for name, entry in module.type_aliases.items():
563
+ typ, loc, type_params, type_param_kinds = entry
564
+ is_recursive = name in module.recursive_union_names
565
+ self._validate_type_alias_members(name, typ, loc)
566
+ info = TypeAliasInfo(
567
+ body=typ,
568
+ type_params=list(type_params),
569
+ type_param_kinds=list(type_param_kinds),
570
+ loc=loc,
571
+ is_recursive=is_recursive,
572
+ )
573
+ self.ctx.registry.register_type_alias(name, typ, info=info)
574
+ install_binding(
575
+ self.ctx.module_attributes, name,
576
+ SymbolKind.TYPE_ALIAS, typ,
577
+ )
578
+ # Register the defining-module's canonical short name for
579
+ # `UnionType.__str__` so diagnostics print "Shape" instead
580
+ # of "Circle | Rect". First write wins via `setdefault` --
581
+ # the defining module's name takes precedence over any
582
+ # import aliasing in downstream modules. Skip generic aliases:
583
+ # their display name is only meaningful parameterized
584
+ # (`Either[Int32]`), and keying on the unsubstituted members
585
+ # would mislabel an unrelated concrete union of the same shape.
586
+ if (display_names is not None and not type_params
587
+ and isinstance(typ, UnionType)):
588
+ display_names.setdefault(typ.members, name)
589
+ # Convert the parser's AliasRef placeholders for generic recursive
590
+ # aliases (self-refs in bodies + use sites in annotations) into
591
+ # semantic RecursiveAliasInstanceType nodes, now that the alias
592
+ # registry carries each alias's TypeAliasInfo.
593
+ self._finalize_generic_recursive_aliases(module)
594
+ self._advance_phase(
595
+ self._PHASE_BIND_IMPORTS,
596
+ self._PHASE_REGISTER_RECORDS_AND_PROTOCOLS,
597
+ )
598
+
599
+ def register_signatures(self, module: TpyModule) -> None:
600
+ """Sub-phase 3: register function signatures with @overload
601
+ grouping, normalize FunctionInfo refs (`make_ref` over params /
602
+ returns now that records/protocols/value-type flags are final),
603
+ inject synthetic `__name__` Final[str] for non-private modules.
604
+ """
605
+ # Second pass: register all functions (with @overload grouping)
606
+ self._register_functions_with_overloads(module.functions)
607
+
608
+ # Normalize FunctionInfo types: wrap non-value params/returns with Ref[T].
609
+ # Must run after ALL types are registered (records, protocols, value types,
610
+ # AND free functions) so make_ref correctly identifies which types need wrapping.
611
+ self._normalize_function_info_refs()
612
+
613
+ # Inject synthetic __name__: Final[str] before registration so it flows
614
+ # through the same path as user-defined Finals (single source of truth).
615
+ # Skip for private submodules (containing "._") that may share their
616
+ # parent's C++ namespace -- they'd cause duplicate __name__ definitions.
617
+ # This applies to both stdlib (tpy._core._types, tpy._builtins._list) and user private
618
+ # submodules (myapp._internal), which is fine since __name__ is rarely
619
+ # needed in submodules compiled to C++.
620
+ module_name = self.ctx.module_name
621
+ is_private_submodule = "._" in module_name
622
+ if not is_private_submodule:
623
+ name_decl = TpyVarDecl(
624
+ name="__name__",
625
+ type=FinalType(STR),
626
+ init=TpyStrLiteral(value=module_name),
627
+ )
628
+ if module.top_level_stmts is None:
629
+ module.top_level_stmts = []
630
+ module.top_level_stmts.insert(0, name_decl)
631
+
632
+ # Register top-level variable declarations (globals) here, in
633
+ # sub-phase 3, rather than waiting for body sema. The
634
+ # registration only inspects each TpyVarDecl's name + declared
635
+ # type (no init-expression analysis); doing it now lets peer
636
+ # modules' `from X import VAR` resolve at decl-time, which the
637
+ # workspace-wide two-pass sema needs (Phase 5 split).
638
+ self.registrar.register_globals(module.top_level_stmts)
639
+ # Top-level statement analysis also moves into the declaration
640
+ # sub-phases. It populates `global_scope` with names assigned via
641
+ # top-level expressions, including tuple-unpacks
642
+ # (`lo, hi = get_bounds()`) whose types only emerge from
643
+ # analyzing the RHS. Without this, peer modules' `from X import lo`
644
+ # would fail because `lo` would not yet be in
645
+ # `X.exports.variables` when the peer's declarations pass runs.
646
+ # Cross-module function calls in top-level statements resolve
647
+ # against decl-finalized peer ModuleInfos (deps run first in
648
+ # topo order in the declarations pass).
649
+ if module.top_level_stmts:
650
+ self._analyze_top_level(module.top_level_stmts)
651
+ self._advance_phase(
652
+ self._PHASE_REGISTER_RECORDS_AND_PROTOCOLS,
653
+ self._PHASE_REGISTER_SIGNATURES,
654
+ )
655
+
656
+ def analyze_bodies(self, module: TpyModule) -> None:
657
+ """Sub-phase 4: analyze class-constant initializers, builder-trace
658
+ expansion, record method bodies, free function bodies. Top-level
659
+ statement analysis moved into `register_signatures` so peer
660
+ modules' decl pass sees globals defined via tuple-unpack
661
+ assignments.
662
+ """
663
+ # Fifth pass: analyze class-constant initializers. Runs after
664
+ # `_analyze_top_level` so module-level Final globals are in scope, and
665
+ # before `_analyze_record_methods` so methods see typed constants.
666
+ for record in module.all_records():
667
+ self._analyze_class_constants(record)
668
+
669
+ # Pass 5.5: see `_expand_builder_traces`.
670
+ self._expand_builder_traces(module)
671
+
672
+ # Sixth pass: analyze record methods (including nested records)
673
+ for record in module.all_records():
674
+ self._analyze_record_methods(record)
675
+
676
+ # Seventh pass: analyze function bodies (skip bodyless @overload stubs and @inline)
677
+ for func in module.functions:
678
+ if func.is_inline and not func.is_stub:
679
+ func.skip_codegen = True
680
+ continue
681
+ # Bodied @overload stubs (mode b) carry their own body that needs
682
+ # analysis just like a regular function; bodyless @overload stubs
683
+ # are handled via their trailing implementation.
684
+ if func.is_overload_stub and func.is_stub:
685
+ continue
686
+ self._analyze_function(func)
687
+ self._advance_phase(
688
+ self._PHASE_REGISTER_SIGNATURES,
689
+ self._PHASE_ANALYZE_BODIES,
690
+ )
691
+
692
+ def run_phase2_fixpoint(self, module: TpyModule) -> None:
693
+ """Sub-phase 5: call-graph mutation-fact propagation + readonly
694
+ inference. Borrow-check resolution is deferred to a post-pass
695
+ (`finalize_borrow_checks`) that the Compiler runs after every
696
+ module's propagation has completed so cross-module mutation
697
+ facts are settled before borrow warnings are emitted.
698
+ """
699
+ self._propagate_mutation_facts()
700
+ self._sync_inferred_const(module)
701
+ self._sync_inferred_vararg_readonly(module)
702
+ self._advance_phase(
703
+ self._PHASE_ANALYZE_BODIES,
704
+ self._PHASE_PHASE2_FIXPOINT,
705
+ )
706
+
707
+ def finalize_borrow_checks(self) -> None:
708
+ """Emit / suppress deferred borrow warnings using fully-propagated
709
+ cross-module facts. Compiler runs this once per analyzer in a
710
+ third workspace-wide pass after every module has completed
711
+ `run_phase2_fixpoint`, guaranteeing that
712
+ `resolve_pending_borrow_checks` reads finalized
713
+ `mutated_params` / `structural_mutated_params` regardless of
714
+ body-sema iteration order. This is what makes the suite pass
715
+ byte-identical when body sema runs in reverse-topo order
716
+ (Phase 6 acceptance criterion).
717
+ """
718
+ self.calls.resolve_pending_borrow_checks()
719
+
720
+ def _normalize_function_info_refs(self) -> None:
721
+ """Apply make_ref to all registered FunctionInfo param/return types.
722
+
723
+ Wraps non-value types with RefType so codegen and type inference see
724
+ explicit reference semantics. Called after all types (including value
725
+ type markers) are registered, so make_ref correctly identifies which
726
+ types need wrapping.
727
+
728
+ Stamps `originating_module` on every FunctionInfo registered
729
+ in this module's analyzer. Imported peer FIs already carry
730
+ their defining module's stamp, so we skip when set. Opaque
731
+ FIs (builtin / @builtin_decorator stubs) keep
732
+ `originating_module=None` so mutation propagation treats
733
+ them as unowned and skips them.
734
+ """
735
+ current_module = self.ctx.module_name
736
+
737
+ def _ref_params(params: list) -> list:
738
+ result = []
739
+ for p in params:
740
+ if isinstance(p, ParamInfo):
741
+ result.append(dc_replace(p, type=make_ref(p.type)))
742
+ else:
743
+ # Legacy tuple (name, type) form
744
+ result.append((p[0], make_ref(p[1])))
745
+ return result
746
+
747
+ for overloads in self.ctx.registry.functions.values():
748
+ for fi in overloads:
749
+ if fi.originating_module is None and not fi.is_builtin_function and not fi.builtin_decorator_key:
750
+ fi.originating_module = current_module
751
+ if isinstance(fi.return_type, RefType):
752
+ continue
753
+ fi.return_type = make_ref(fi.return_type)
754
+ fi.params = _ref_params(fi.params)
755
+ for rec in self.ctx.registry.records.values():
756
+ for method_list in rec.methods.values():
757
+ for fi in method_list:
758
+ if fi.originating_module is None and not fi.is_builtin_function and not fi.builtin_decorator_key:
759
+ # Method's defining module = the record's defining module,
760
+ # not the current module. Records that flow in via cross-
761
+ # module registration carry RecordInfo.module set during
762
+ # their owning module's sema; trust that when present.
763
+ fi.originating_module = rec.module or current_module
764
+ if isinstance(fi.return_type, RefType):
765
+ continue
766
+ fi.return_type = make_ref(fi.return_type)
767
+ fi.params = _ref_params(fi.params)
768
+
769
+ def _propagate_mutation_facts(self) -> None:
770
+ """Run call-graph mutation-fact propagation, taking advantage of
771
+ the workspace-shared `registry.modules` dict to pull peer
772
+ modules' FunctionInfos in addition to this module's own.
773
+
774
+ Each per-analyzer call walks the workspace once. After the
775
+ first call clears `call_edges` on its propagated FIs, the
776
+ gate (`call_edges is not None`) excludes them from later
777
+ analyzers' collections, so subsequent calls only process
778
+ whatever module just finished body sema -- effectively
779
+ incremental rather than re-doing all modules' work.
780
+
781
+ The `originating_module` distinction from Phase 6 stays
782
+ (gates body-sema-time fact production); cross-module
783
+ mutating calls propagate facts uniformly through the
784
+ workspace call graph.
785
+ """
786
+ all_fis: list = []
787
+ seen: set[int] = set()
788
+
789
+ def _collect_from(reg_or_mod) -> None:
790
+ for overloads in reg_or_mod.functions.values():
791
+ for fi in overloads:
792
+ if id(fi) in seen:
793
+ continue
794
+ if fi.direct_mutated_params is not None and fi.call_edges is not None:
795
+ seen.add(id(fi))
796
+ all_fis.append(fi)
797
+ for rec in reg_or_mod.records.values():
798
+ for method_overloads in rec.methods.values():
799
+ for fi in method_overloads:
800
+ if id(fi) in seen:
801
+ continue
802
+ if fi.direct_mutated_params is not None and fi.call_edges is not None:
803
+ seen.add(id(fi))
804
+ all_fis.append(fi)
805
+
806
+ _collect_from(self.ctx.registry)
807
+ for mod_info in self.ctx.registry.modules.values():
808
+ if not mod_info.is_builtin:
809
+ _collect_from(mod_info)
810
+ propagate_mutation_facts(all_fis)
811
+ infer_method_const(all_fis)
812
+
813
+ def _sync_inferred_const(self, module: TpyModule) -> None:
814
+ """Copy inferred is_readonly=True from FunctionInfo back to TpyFunction nodes.
815
+
816
+ infer_method_const() sets FunctionInfo.is_readonly on the registry objects,
817
+ but codegen reads method.is_readonly from the TpyFunction AST nodes.
818
+ This pass syncs the two representations.
819
+ """
820
+ for record in module.all_records():
821
+ record_info = self.ctx.registry.get_record(record.name)
822
+ if record_info is None:
823
+ continue
824
+ for method in record.methods:
825
+ if method.is_readonly:
826
+ continue # already readonly, no need to sync
827
+ # Phase 1 facts for @overload methods land on overloads[0]'s FI
828
+ # (see comment at get_method() call below). Syncing stub nodes
829
+ # would re-read via get_method() and hit the same FI repeatedly --
830
+ # skip them and let the non-stub TpyFunction node do the sync.
831
+ if method.is_overload_stub:
832
+ continue
833
+ # @auto_readonly mutable clones are paired with a const clone --
834
+ # keep them mutable so the pair generates both overloads correctly.
835
+ if method.is_auto_readonly_mutable_clone:
836
+ continue
837
+ # @readonly(False) is an explicit opt-out -- respect it.
838
+ if method.readonly_opt_out:
839
+ continue
840
+ fi = record_info.get_method(method.name)
841
+ if fi is None or not fi.is_readonly:
842
+ continue
843
+ # For @dynamic protocol overrides, the const-ness of the concrete
844
+ # method must match the virtual base declaration. If the protocol
845
+ # declares the method as non-const, don't infer const here --
846
+ # it would produce a different C++ signature and break the override.
847
+ if self._dynamic_proto_requires_nonconst(record_info, method.name):
848
+ continue
849
+ method.is_readonly = True
850
+
851
+ def _dynamic_proto_requires_nonconst(self, record_info: RecordInfo, method_name: str) -> bool:
852
+ """Return True if method_name must remain non-const due to a @dynamic protocol override.
853
+
854
+ A @dynamic protocol generates C++ pure virtual methods that concrete implementations
855
+ must override with matching (non-const) signatures. The method may be declared in a
856
+ non-dynamic ancestor, but still ends up in the dynamic vtable.
857
+
858
+ Walks the MRO so a class inheriting a @dynamic protocol transitively (via a
859
+ concrete-class parent) still has its overrides pinned to the protocol's
860
+ const-ness -- otherwise auto-readonly inference would emit a const signature
861
+ that mismatches the virtual slot.
862
+ """
863
+ visited: set[str] = set()
864
+ for proto_type, _ in self.ctx.registry.iter_dynamic_protocols(record_info):
865
+ # Search this dynamic protocol and ALL its ancestors for method_name,
866
+ # regardless of whether ancestors are themselves dynamic.
867
+ if self._proto_hierarchy_has_nonconst(proto_type.name, method_name, visited):
868
+ return True
869
+ return False
870
+
871
+ def _proto_hierarchy_has_nonconst(self, proto_name: str, method_name: str, visited: set[str]) -> bool:
872
+ """Search method_name in this protocol and all ancestors (ignoring dynamic flag)."""
873
+ if proto_name in visited:
874
+ return False
875
+ visited.add(proto_name)
876
+ proto_info = self.ctx.registry.scan_by_short_name(proto_name)
877
+ if proto_info is None:
878
+ return False
879
+ for sig in proto_info.methods:
880
+ if sig.name == method_name and not sig.is_readonly:
881
+ return True
882
+ for parent in proto_info.parent_protocols:
883
+ if self._proto_hierarchy_has_nonconst(parent.name, method_name, visited):
884
+ return True
885
+ return False
886
+
887
+ def _sync_inferred_vararg_readonly(self, module: TpyModule) -> None:
888
+ """Flip vararg slots `Span[T]` -> `Span[readonly[T]]` for bodies that
889
+ don't mutate the vararg pack. Parallels `_sync_inferred_const` for
890
+ methods and the per-index `mutated_params` gate that `decide_param_const`
891
+ consumes for non-vararg ref params -- both extend the same body-mutation
892
+ inference to the vararg case so non-mutating callees collapse to
893
+ `varargs<const T>` and accept const arg sources without caller-side
894
+ marking workarounds. Skip rules mirror `_sync_inferred_const`: explicit
895
+ readonly slot, @native, overload stubs, and @dynamic-protocol overrides
896
+ whose virtual slot is non-const.
897
+ """
898
+ for func in module.functions:
899
+ if func.is_overload_stub or func.native_function:
900
+ continue
901
+ if func.vararg_name is None:
902
+ continue
903
+ # For @overload groups, mutation facts land on the implementation's
904
+ # FI (last in overload list); see analyzer.py:1142-1143 where
905
+ # Phase 1 writes them. Plain defs have a single entry.
906
+ overloads = self.ctx.registry.get_function(func.name)
907
+ if not overloads:
908
+ continue
909
+ self._maybe_flip_vararg_readonly(func, overloads[-1], dyn_pin_nonconst=False)
910
+
911
+ for record in module.all_records():
912
+ rec_info = self.ctx.registry.get_record(record.name)
913
+ if rec_info is None:
914
+ continue
915
+ for method in record.methods:
916
+ if method.is_overload_stub or method.native_function:
917
+ continue
918
+ if method.vararg_name is None:
919
+ continue
920
+ # @auto_readonly methods are cloned into mutable + const pairs;
921
+ # the mutable clone exists precisely to offer the non-const
922
+ # form, so its vararg slot must stay mutable even when the
923
+ # body doesn't mutate the pack. The const clone gets the slot
924
+ # via its own ReadonlyType wrapping at clone time.
925
+ if method.is_auto_readonly_mutable_clone:
926
+ continue
927
+ method_fi = rec_info.get_method(method.name)
928
+ if method_fi is None:
929
+ continue
930
+ pin_nonconst = self._dynamic_proto_pins_vararg_nonconst(
931
+ rec_info, method.name)
932
+ self._maybe_flip_vararg_readonly(method, method_fi,
933
+ dyn_pin_nonconst=pin_nonconst)
934
+
935
+ def _maybe_flip_vararg_readonly(
936
+ self, func: TpyFunction, fi: 'FunctionInfo', *, dyn_pin_nonconst: bool,
937
+ ) -> None:
938
+ """Apply the slot flip when body analysis shows the vararg isn't
939
+ mutated. Mutates both `fi.params` and `func.params` so codegen
940
+ (which reads from the AST) and downstream sema (which reads from FI)
941
+ agree on the resolved slot type.
942
+ """
943
+ va_idx = next((i for i, p in enumerate(fi.params) if p.is_variadic), -1)
944
+ if va_idx < 0:
945
+ return
946
+ va_param = fi.params[va_idx]
947
+ bare_va = unwrap_ref_type(va_param.type)
948
+ if not is_span(bare_va) or span_is_readonly(bare_va):
949
+ return
950
+ # mutated_params=None means no body was analyzed (native, stub,
951
+ # builtin). The signature is the source of truth in those cases --
952
+ # don't infer-flip what the user (or C++ binding) declared.
953
+ if fi.mutated_params is None or va_idx in fi.mutated_params:
954
+ return
955
+ if dyn_pin_nonconst:
956
+ return
957
+ new_span = span_as_const(bare_va)
958
+ new_param_type = make_ref(new_span) if isinstance(va_param.type, RefType) else new_span
959
+ fi.params[va_idx] = ParamInfo(
960
+ name=va_param.name,
961
+ type=new_param_type,
962
+ requires_mutable_lvalue=va_param.requires_mutable_lvalue,
963
+ default_expr=va_param.default_expr,
964
+ keyword_only=va_param.keyword_only,
965
+ is_variadic=True,
966
+ )
967
+ # AST mirror: codegen reads from `func.params` directly.
968
+ for i, (pname, _) in enumerate(func.params):
969
+ if pname == func.vararg_name:
970
+ func.params[i] = (pname, new_param_type)
971
+ break
972
+
973
+ def _dynamic_proto_pins_vararg_nonconst(
974
+ self, record_info: 'RecordInfo', method_name: str,
975
+ ) -> bool:
976
+ """Parallel to `_dynamic_proto_requires_nonconst`, but for the vararg
977
+ slot: an override of a @dynamic protocol method must keep the vararg
978
+ mutable when the protocol declares it mutable, since the C++ vtable
979
+ slot is monomorphized to `varargs<T>` vs `varargs<const T>`.
980
+
981
+ Reachable but unexercised today: `MethodSignature` (typesys.py:4127)
982
+ does not carry `is_variadic`, so protocol methods can't declare a
983
+ `*args` slot; `_proto_hierarchy_has_vararg_nonconst` therefore never
984
+ finds a mutable-vararg sig. Kept as a forward-compat guard so the
985
+ invariant is in place the day protocols gain vararg method support.
986
+ """
987
+ visited: set[str] = set()
988
+ for proto_type, _ in self.ctx.registry.iter_dynamic_protocols(record_info):
989
+ if self._proto_hierarchy_has_vararg_nonconst(
990
+ proto_type.name, method_name, visited):
991
+ return True
992
+ return False
993
+
994
+ def _proto_hierarchy_has_vararg_nonconst(
995
+ self, proto_name: str, method_name: str, visited: set[str],
996
+ ) -> bool:
997
+ if proto_name in visited:
998
+ return False
999
+ visited.add(proto_name)
1000
+ proto_info = self.ctx.registry.scan_by_short_name(proto_name)
1001
+ if proto_info is None:
1002
+ return False
1003
+ for sig in proto_info.methods:
1004
+ if sig.name != method_name:
1005
+ continue
1006
+ for _, ptype in sig.params:
1007
+ bare = unwrap_ref_type(ptype) if isinstance(ptype, TpyType) else None
1008
+ if bare is not None and is_span(bare) and not span_is_readonly(bare):
1009
+ return True
1010
+ for parent in proto_info.parent_protocols:
1011
+ if self._proto_hierarchy_has_vararg_nonconst(
1012
+ parent.name, method_name, visited):
1013
+ return True
1014
+ return False
1015
+
1016
+ def _is_type_nocopy(self, typ: TpyType) -> bool:
1017
+ """Delegate to canonical is_type_nocopy on context."""
1018
+ return self.ctx.is_type_nocopy(typ)
1019
+
1020
+ def _validate_factory_defaults(self, module: TpyModule) -> None:
1021
+ """Validate that field(default_factory=X) fields have Default-constructible types."""
1022
+ default_proto = NominalType("Default", (), is_protocol=True)
1023
+ for record in module.all_records():
1024
+ for fld in record.fields:
1025
+ if not fld.is_factory_default:
1026
+ continue
1027
+ if not self.protocols.type_conforms_to_protocol(fld.type, default_proto):
1028
+ raise SemanticError(
1029
+ f"Field '{fld.name}' in '{record.name}' uses "
1030
+ f"default_factory but type '{fld.type}' is not default-constructible",
1031
+ fld.loc or record.loc,
1032
+ )
1033
+ # Validate factory name matches field type
1034
+ expr = fld.default_expr
1035
+ if isinstance(expr, TpyCall) and isinstance(fld.type, NominalType):
1036
+ if expr.func_name != fld.type.name:
1037
+ raise SemanticError(
1038
+ f"default_factory '{expr.func_name}' does not match "
1039
+ f"field type '{fld.type}'",
1040
+ fld.loc or record.loc,
1041
+ )
1042
+
1043
+ def _propagate_nocopy(self, module: TpyModule) -> None:
1044
+ """Propagate nocopy from fields/parents to containing records.
1045
+
1046
+ Processes records in definition order. If any field's type is nocopy,
1047
+ the containing record becomes nocopy too. Also checks parent type.
1048
+ Skips records that define __copy__ (opt-out escape hatch).
1049
+ """
1050
+ for record in module.all_records():
1051
+ info = self.ctx.registry.get_record(record.name)
1052
+ if info is None or info.is_nocopy:
1053
+ continue
1054
+ # __copy__ opts out of propagation
1055
+ if info.has_copy:
1056
+ continue
1057
+ # Check parents (any nocopy parent propagates to the child)
1058
+ if any(self._is_type_nocopy(p) for p in info.parents):
1059
+ info.is_nocopy = True
1060
+ continue
1061
+ # Check fields
1062
+ for f in info.fields:
1063
+ if self._is_type_nocopy(f.type):
1064
+ info.is_nocopy = True
1065
+ break
1066
+
1067
+ def _normalize_param_type(self, ptype: TpyType, is_readonly_ctx: bool) -> TpyType:
1068
+ """Normalize a parameter type for readonly context.
1069
+
1070
+ Strips ReadonlyType from value types (copies are always safe).
1071
+ Wraps non-value types with ReadonlyType in @readonly contexts.
1072
+ """
1073
+ if isinstance(ptype, ReadonlyType) and ptype.wrapped.is_value_type():
1074
+ return ptype.wrapped
1075
+ if is_readonly_ctx and not isinstance(ptype, ReadonlyType):
1076
+ if not ptype.is_value_type():
1077
+ return ReadonlyType(ptype)
1078
+ return ptype
1079
+
1080
+ def _warn_unconsumed_own_params(self, func: TpyFunction) -> None:
1081
+ """Warn when Own[T] params are never consumed (stored, forwarded, or returned)."""
1082
+ if func.is_stub:
1083
+ return
1084
+ for pname, ptype in func.params:
1085
+ own = unwrap_optional_own(unwrap_readonly(ptype))
1086
+ if own is None:
1087
+ continue
1088
+ if pname in self.ctx.func.current_consumed_own_params:
1089
+ continue
1090
+ # Optional[Own[T]]: None branch has nothing to consume, making
1091
+ # flow-sensitive intersection unreliable
1092
+ if isinstance(unwrap_readonly(ptype), OptionalType):
1093
+ continue
1094
+ # Value types: copy == move, no semantic difference
1095
+ if own.wrapped.is_value_type():
1096
+ continue
1097
+ # Generic T bounded to ValueType: same as value type
1098
+ if isinstance(own.wrapped, TypeParamRef):
1099
+ bound = self.type_ops.get_type_param_bound(own.wrapped.name)
1100
+ if (bound is not None and isinstance(bound, NominalType)
1101
+ and bound.qualified_name() == "tpy.ValueType"):
1102
+ continue
1103
+ # @nocopy types: Own is the only way to pass them
1104
+ if self.ctx.is_type_nocopy(own.wrapped):
1105
+ continue
1106
+ self.ctx.warning(
1107
+ f"Own[{own.wrapped}] param '{pname}' is never consumed "
1108
+ f"(not stored in a field, forwarded to another Own[T], or returned)",
1109
+ func,
1110
+ )
1111
+
1112
+ def _analyze_function(self, func: TpyFunction) -> None:
1113
+ """Analyze a function body."""
1114
+ # Stub functions (extern imports with ... body) have no body to analyze
1115
+ if func.is_stub:
1116
+ return
1117
+
1118
+ self.ctx.reset_function_tracking()
1119
+
1120
+ self.ctx.func.current_function = func
1121
+ # Async def bodies are analyzed normally. The await-expression
1122
+ # analyzer (sema/expressions.py:_analyze_await) handles the supported
1123
+ # v1 forms (direct call to async def, Task[T], Future[T], structural
1124
+ # awaitable) and rejects unsupported shapes with a clear diagnostic.
1125
+ # Resolve return type (sets is_protocol for cross-module imports)
1126
+ func.return_type = make_ref(self.type_ops.resolve_type(func.return_type))
1127
+ scope = Scope(parent=self.ctx.global_scope)
1128
+ self.ctx.func.current_scope = scope
1129
+
1130
+ # Resolve and normalize params (@readonly wraps all non-value params).
1131
+ # Write back to AST so codegen sees Ref/ReadonlyType (codegen reads func.params directly, not FI).
1132
+ local_ns = Namespace(parent=self.ctx.global_ns)
1133
+ self.ctx.func.current_ns = local_ns
1134
+ resolved_params: list[tuple[str, TpyType]] = []
1135
+ for i, (pname, ptype) in enumerate(func.params):
1136
+ resolved_ptype = self._normalize_param_type(
1137
+ self.type_ops.resolve_type(ptype), func.is_readonly)
1138
+ func.params[i] = (pname, make_ref(resolved_ptype))
1139
+ resolved_params.append((pname, resolved_ptype))
1140
+
1141
+ # Add *args parameter as Span[readonly[T]] to func.params and resolved_params.
1142
+ # Insert before keyword-only params to match FunctionInfo param order.
1143
+ if func.vararg_name is not None and func.vararg_type is not None:
1144
+ va_type = _vararg_span_type(self.type_ops.resolve_type(func.vararg_type))
1145
+ kw_start = func.keyword_only_start
1146
+ if kw_start is not None and kw_start < len(func.params):
1147
+ func.params.insert(kw_start, (func.vararg_name, make_ref(va_type)))
1148
+ resolved_params.insert(kw_start, (func.vararg_name, va_type))
1149
+ if func.defaults:
1150
+ func.defaults.insert(kw_start, None)
1151
+ # Adjust keyword_only_start since we inserted before it
1152
+ func.keyword_only_start = kw_start + 1
1153
+ else:
1154
+ func.params.append((func.vararg_name, make_ref(va_type)))
1155
+ resolved_params.append((func.vararg_name, va_type))
1156
+ if func.defaults:
1157
+ func.defaults.append(None)
1158
+ # Clear vararg_type to prevent re-processing; keep vararg_name for codegen
1159
+ func.vararg_type = None
1160
+
1161
+ # Add **kwargs parameter as TypedDict type to func.params and resolved_params.
1162
+ if func.kwarg_name is not None and func.kwarg_type is not None:
1163
+ kw_type = self.type_ops.resolve_type(func.kwarg_type)
1164
+ func.params.append((func.kwarg_name, make_ref(kw_type)))
1165
+ resolved_params.append((func.kwarg_name, kw_type))
1166
+ if func.defaults:
1167
+ func.defaults.append(None)
1168
+ # Clear kwarg_type to prevent re-processing; keep kwarg_name for codegen
1169
+ func.kwarg_type = None
1170
+
1171
+ # Track consuming method for ownership propagation through fields
1172
+ prev_consuming = self.ctx.in_consuming_method
1173
+ self.ctx.in_consuming_method = func.is_consuming
1174
+
1175
+ self._validate_named_defaults(func)
1176
+
1177
+ # Shared core: bind params, prescan, analyze body
1178
+ scan = self.stmts._prescan_and_analyze_body(func, resolved_params, scope, local_ns)
1179
+ self.deduction.resolve_all()
1180
+
1181
+ # Collect generator/async local variables for struct field generation.
1182
+ # Same hoisting policy: every function-level local becomes a struct
1183
+ # field. Async coros use the same `func.generator_locals` slot; the
1184
+ # field-rewrite path in expressions/statements is shared via the
1185
+ # in_generator_body flag (the `in_async_coro_body` flag steers only
1186
+ # the return-statement rewrite).
1187
+ if func.is_generator or func.is_async:
1188
+ param_names = {pname for pname, _ in func.params}
1189
+ locals_dict: dict[str, 'TpyType'] = {}
1190
+ for name, binding in local_ns.all_bindings().items():
1191
+ if name not in param_names and binding.type is not None:
1192
+ locals_dict[name] = binding.type
1193
+ for name, (vtype, _, _) in self.ctx.func.pending_loop_vars.items():
1194
+ if name not in param_names and vtype is not None:
1195
+ locals_dict[name] = vtype
1196
+ func.generator_locals = list(locals_dict.items())
1197
+
1198
+ # Finalize nested def escape analysis
1199
+ self._finalize_nested_def_escapes()
1200
+
1201
+ # Store Phase 1 local mutation facts (resolved by Phase 2 propagation).
1202
+ # Phase 6 (mutual-imports) ownership gate: only mutate body-sema
1203
+ # fields on FunctionInfos that originate in this module. The
1204
+ # `direct_mutated_params is None` check covers the legacy case
1205
+ # (FI not yet body-analyzed); the strict `originating_module ==
1206
+ # self.ctx.module_name` check enforces the doc's ownership rule
1207
+ # -- declaration-time `_normalize_function_info_refs` stamps
1208
+ # `originating_module` on every non-opaque FI, so by the time a
1209
+ # body sema reaches this gate, any reachable FI should carry
1210
+ # its defining module's name. A None at this point indicates a
1211
+ # missing-origin-stamping bug, not a reason to write through.
1212
+ func_overloads = self.ctx.registry.get_function(func.name)
1213
+ func_info = func_overloads[-1] if func_overloads else None
1214
+ if (func_info is not None
1215
+ and func_info.direct_mutated_params is None
1216
+ and func_info.originating_module == self.ctx.module_name):
1217
+ param_list = [pname for pname, _ in func.params]
1218
+ direct = frozenset(
1219
+ i for i, pname in enumerate(param_list)
1220
+ if pname in self.ctx.func.current_mutated_param_names
1221
+ )
1222
+ func_info.direct_mutated_params = direct
1223
+ func_info.call_edges = list(self.ctx.func.current_call_edges)
1224
+ func_info.representational_type_params = frozenset(self.ctx.func.current_representational_params)
1225
+ # Set mutated_params to direct facts as initial estimate;
1226
+ # Phase 2 propagation will replace with the complete transitive set.
1227
+ func_info.mutated_params = direct
1228
+ # Structural mutation facts (append/insert/clear/del/etc.)
1229
+ direct_struct = frozenset(
1230
+ i for i, pname in enumerate(param_list)
1231
+ if pname in self.ctx.func.current_struct_mutated_param_names
1232
+ )
1233
+ func_info.direct_structural_mutated_params = direct_struct
1234
+ func_info.structural_mutated_params = direct_struct
1235
+ # 8b: Return borrow facts -- which params does the return value borrow from?
1236
+ func_info.return_borrows_from = frozenset(
1237
+ i for i, pname in enumerate(param_list)
1238
+ if pname in self.ctx.func.current_returned_param_names
1239
+ )
1240
+ func_info.addr_escapes_params = frozenset(
1241
+ i for i, pname in enumerate(param_list)
1242
+ if pname in self.ctx.func.current_addr_escape_param_names
1243
+ )
1244
+ # Generator functions: the returned struct stores non-value params
1245
+ # as T& references (or &ref lambda captures), and str params as
1246
+ # string_view, so the result borrows from those params
1247
+ if func.is_generator:
1248
+ gen_borrows = frozenset(
1249
+ i for i, (_, ptype) in enumerate(func.params)
1250
+ if not ptype.is_value_type() or is_str_type(ptype) or is_str_view_type(ptype)
1251
+ )
1252
+ if gen_borrows:
1253
+ func_info.return_borrows_from = func_info.return_borrows_from | gen_borrows
1254
+
1255
+ self._warn_unconsumed_own_params(func)
1256
+ self._store_analysis_results(func, scan)
1257
+
1258
+ self.ctx.in_consuming_method = prev_consuming
1259
+ self.ctx.func.current_function = None
1260
+ self.ctx.func.current_scope = None
1261
+ self.ctx.func.current_ns = None
1262
+
1263
+ def _store_analysis_results(self, func: TpyFunction, scan: ScanResult) -> None:
1264
+ """Store prescan/liveness results for codegen consumption."""
1265
+ self.function_scan_results[id(func)] = scan
1266
+ if self.ctx.func.hoisted_vars:
1267
+ self.function_hoisted_vars[id(func)] = self.ctx.func.hoisted_vars.copy()
1268
+ if self.ctx.func.move_through_vars:
1269
+ self.function_move_through_vars[id(func)] = self.ctx.func.move_through_vars.copy()
1270
+ # Compute movable locals from ever_owned_locals (survives FlowFacts restores)
1271
+ movable = set()
1272
+ for name in self.ctx.func.ever_owned_locals:
1273
+ if name in self.ctx.func.hoisted_vars:
1274
+ continue
1275
+ if name in self.ctx.func.current_reassigned_vars and name in self.ctx.func.current_lvalue_reassigned:
1276
+ continue
1277
+ movable.add(name)
1278
+ if movable:
1279
+ self.function_movable_locals[id(func)] = movable
1280
+ if self.ctx.func.global_declarations:
1281
+ self.function_global_decls[id(func)] = self.ctx.func.global_declarations.copy()
1282
+ self.if_branch_decls.update(self.ctx.if_branch_decls)
1283
+
1284
+ def _validate_named_defaults(self, func: TpyFunction) -> None:
1285
+ """Validate `def f(x: T = NAME)` defaults: NAME must be a Final[T] global.
1286
+
1287
+ The parser accepts any TpyName in default position; binding shape is
1288
+ deferred here so module-level Finals (declared after functions in the
1289
+ registration order) and imported Finals are visible.
1290
+ """
1291
+ for default in func.defaults:
1292
+ if not isinstance(default, TpyName):
1293
+ continue
1294
+ name = default.name
1295
+ if name in self.ctx.final_globals:
1296
+ continue
1297
+ imp = lookup_imported(
1298
+ self.ctx.module_attributes, name, SymbolKind.VARIABLE)
1299
+ if imp is not None:
1300
+ source_module, original_name = imp
1301
+ source_info = self.ctx.registry.get_module(source_module)
1302
+ var_info = source_info.variables.get(original_name) if source_info else None
1303
+ if var_info is not None and var_info.is_final:
1304
+ continue
1305
+ raise self.ctx.error(
1306
+ f"Default parameter value '{name}' must be a module-level "
1307
+ f"Final[T] constant", default)
1308
+
1309
+ def _finalize_nested_def_escapes(self) -> None:
1310
+ """Finalize escape analysis for nested defs after the enclosing function is analyzed."""
1311
+ # Outer function's parameter names and types
1312
+ outer_params: dict[str, TpyType] = {}
1313
+ if self.ctx.func.current_function:
1314
+ for pname, ptype in self.ctx.func.current_function.params:
1315
+ outer_params[pname] = ptype
1316
+ outer_param_names = set(outer_params.keys())
1317
+ # Get the enclosing function body for "used after" analysis
1318
+ body = self.ctx.func.current_function.body if self.ctx.func.current_function else []
1319
+ for name in self.ctx.func.nested_def_escapes:
1320
+ node = self.ctx.func.nested_def_nodes.get(name)
1321
+ if node is None:
1322
+ continue
1323
+ node.escapes = True
1324
+ # Reject nonlocal in escaping closures
1325
+ if node.nonlocal_names:
1326
+ nl_list = ", ".join(f"'{n}'" for n in sorted(node.nonlocal_names))
1327
+ self.ctx.emit_error(
1328
+ f"nonlocal {nl_list} in escaping closure '{name}' is not supported"
1329
+ f" (the closure is returned or stored; use a class instead)",
1330
+ node)
1331
+ # Collect names referenced after the nested def for move analysis
1332
+ names_used_after = self._names_used_after(body, node)
1333
+ # Per-capture analysis: determine ref vs value vs move capture mode
1334
+ for cap_name in node.captured_names:
1335
+ cap_type = self.ctx.func.current_scope.lookup(cap_name) if self.ctx.func.current_scope else None
1336
+ if cap_type is None:
1337
+ continue
1338
+ raw_type = unwrap_readonly(cap_type)
1339
+ is_own_param = isinstance(raw_type, OwnType)
1340
+ if isinstance(raw_type, OwnType):
1341
+ raw_type = raw_type.wrapped
1342
+ check_type = raw_type.inner if isinstance(raw_type, OptionalType) else raw_type
1343
+ if cap_name in outer_param_names:
1344
+ # Reject str/StrView parameter captures (string_view dangles)
1345
+ if is_str_type(check_type) or is_str_view_type(check_type):
1346
+ self.ctx.emit_error(
1347
+ f"Escaping closure '{name}' captures str parameter"
1348
+ f" '{cap_name}' which would dangle (string_view into"
1349
+ f" caller's storage). Use String for owned capture",
1350
+ node)
1351
+ elif not raw_type.is_value_type():
1352
+ if is_own_param:
1353
+ # Own[T] param is destroyed on return -- must move
1354
+ node.move_captures.add(cap_name)
1355
+ self.ctx.mark_own_param_consumed(cap_name)
1356
+ else:
1357
+ # Regular const-ref param: caller's object outlives closure
1358
+ node.ref_captures.add(cap_name)
1359
+ else:
1360
+ # Local variable: must copy or move (local dies on return).
1361
+ # Only emit std::move for types where move is cheaper than
1362
+ # copy (string, BigInt, etc.) -- for trivial types like
1363
+ # Int32 it's just noise.
1364
+ wants_move = (not raw_type.is_value_type()
1365
+ or raw_type.is_expensive_copy())
1366
+ if wants_move and cap_name not in names_used_after:
1367
+ # Last use -- move into the closure, no warning
1368
+ node.move_captures.add(cap_name)
1369
+ elif wants_move:
1370
+ # Used after the closure -- must copy, warn
1371
+ self.ctx.warning(
1372
+ f"Escaping closure '{name}' copies local"
1373
+ f" '{cap_name}' (used after closure definition,"
1374
+ f" preventing move). Reorder code so the closure"
1375
+ f" is the last use, or use copy() to make the"
1376
+ f" copy explicit",
1377
+ node)
1378
+
1379
+ @staticmethod
1380
+ def _names_used_after(body: list[TpyStmt], nested_node: TpyNestedDef) -> set[str]:
1381
+ """Collect names referenced in top-level statements after the nested def."""
1382
+ found = False
1383
+ after_stmts: list[TpyStmt] = []
1384
+ target_name = nested_node.func.name
1385
+ for stmt in body:
1386
+ if found:
1387
+ after_stmts.append(stmt)
1388
+ elif isinstance(stmt, TpyNestedDef) and stmt.func.name == target_name:
1389
+ found = True
1390
+ if not found:
1391
+ # Nested def not at top level of body -- conservatively assume all used
1392
+ return set(nested_node.captured_names or [])
1393
+ return _collect_body_name_refs(after_stmts)
1394
+
1395
+ def _collect_method_overload_groups(self, record: TpyRecord) -> None:
1396
+ """Identify and validate @overload groups among a record's methods.
1397
+
1398
+ For each group, stores the mapping from implementation -> stubs
1399
+ in self.overload_groups. Also validates exhaustiveness.
1400
+ """
1401
+ pending_stubs: dict[str, list[TpyFunction]] = {}
1402
+ for method in record.methods:
1403
+ if method.is_overload_stub:
1404
+ pending_stubs.setdefault(method.name, []).append(method)
1405
+ continue
1406
+ stubs = pending_stubs.pop(method.name, None)
1407
+ if stubs:
1408
+ self._reject_bodied_overloads_with_impl(
1409
+ stubs, method, f"{record.name}.{method.name}")
1410
+ self._validate_method_overload_group(method, stubs, record.name)
1411
+ self.overload_groups[id(method)] = stubs
1412
+ for name, stubs in pending_stubs.items():
1413
+ # All stubs must be self-contained: each is @native, @cpp_template,
1414
+ # or carries its own body (mode b). Mixed native + bodied groups OK.
1415
+ if all(s.has_implementation for s in stubs):
1416
+ continue
1417
+ raise SemanticError(
1418
+ f"@overload stubs for '{record.name}.{name}' have no implementation method",
1419
+ stubs[0].loc or record.loc,
1420
+ )
1421
+
1422
+ def _validate_method_overload_group(
1423
+ self, impl: TpyFunction, stubs: list[TpyFunction], record_name: str,
1424
+ ) -> None:
1425
+ """Validate exhaustiveness of method overload stubs."""
1426
+ self._validate_overload_signatures(
1427
+ impl, stubs, f"{record_name}.{impl.name}")
1428
+
1429
+ def _validate_overload_signatures(
1430
+ self, impl: TpyFunction, stubs: list[TpyFunction], display_name: str,
1431
+ ) -> None:
1432
+ """Shared @overload stub validation (functions + methods).
1433
+
1434
+ Rules:
1435
+ - Stub arity must be <= impl arity. When shorter, every missing trailing
1436
+ impl param must have a default.
1437
+ - Stub params must prefix-match impl params by name; stub types must be
1438
+ compatible with impl types at each shared position (exact match or
1439
+ LiteralType over the impl base for non-unions; subset of members for
1440
+ unions).
1441
+ - When any stub has fewer params than the impl, the impl may not use
1442
+ keyword-only params, *args, or **kwargs.
1443
+ - Union-typed impl params: stubs that include the param must only
1444
+ reference members of the union; when every stub includes the param,
1445
+ all members must be covered.
1446
+ """
1447
+ impl_defaults = impl.defaults if impl.defaults else []
1448
+ has_short_arity = any(len(s.params) < len(impl.params) for s in stubs)
1449
+ if has_short_arity:
1450
+ if impl.keyword_only_start is not None:
1451
+ raise SemanticError(
1452
+ f"@overload implementation '{display_name}' cannot have "
1453
+ f"keyword-only parameters when stubs have different arities",
1454
+ impl.loc,
1455
+ )
1456
+ if impl.vararg_name is not None:
1457
+ raise SemanticError(
1458
+ f"@overload implementation '{display_name}' cannot have "
1459
+ f"*args when stubs have different arities",
1460
+ impl.loc,
1461
+ )
1462
+ if impl.kwarg_name is not None:
1463
+ raise SemanticError(
1464
+ f"@overload implementation '{display_name}' cannot have "
1465
+ f"**kwargs when stubs have different arities",
1466
+ impl.loc,
1467
+ )
1468
+
1469
+ for stub in stubs:
1470
+ if len(stub.params) > len(impl.params):
1471
+ raise SemanticError(
1472
+ f"@overload stub for '{display_name}' has "
1473
+ f"{len(stub.params)} parameter(s), more than the "
1474
+ f"implementation's {len(impl.params)}",
1475
+ stub.loc,
1476
+ )
1477
+ if len(stub.params) < len(impl.params):
1478
+ for i in range(len(stub.params), len(impl.params)):
1479
+ impl_pname = impl.params[i][0]
1480
+ has_default = (i < len(impl_defaults)
1481
+ and impl_defaults[i] is not None)
1482
+ if not has_default:
1483
+ raise SemanticError(
1484
+ f"@overload stub for '{display_name}' omits "
1485
+ f"parameter '{impl_pname}', but the implementation "
1486
+ f"has no default value for it",
1487
+ stub.loc,
1488
+ )
1489
+ for (impl_pname, _), (stub_pname, _) in zip(impl.params, stub.params):
1490
+ if impl_pname != stub_pname:
1491
+ raise SemanticError(
1492
+ f"@overload stub parameter '{stub_pname}' does not match "
1493
+ f"implementation parameter '{impl_pname}'",
1494
+ stub.loc,
1495
+ )
1496
+
1497
+ for param_idx, (pname, ptype) in enumerate(impl.params):
1498
+ resolved = self.type_ops.resolve_type(ptype)
1499
+ param_stubs = [s for s in stubs if param_idx < len(s.params)]
1500
+ if not param_stubs:
1501
+ # Every stub skips this param; impl default covers all call sites.
1502
+ continue
1503
+ if isinstance(resolved, OptionalType):
1504
+ inner_covered = False
1505
+ none_covered = False
1506
+ for stub in param_stubs:
1507
+ stub_ptype = self.type_ops.resolve_type(stub.params[param_idx][1])
1508
+ if stub_ptype == resolved:
1509
+ inner_covered = True
1510
+ none_covered = True
1511
+ continue
1512
+ if stub_ptype == resolved.inner:
1513
+ inner_covered = True
1514
+ continue
1515
+ if is_void_like_type(stub_ptype):
1516
+ # `None` as a type annotation parses to VoidType; a
1517
+ # `None` literal's type is NoneType -- both mean "None".
1518
+ none_covered = True
1519
+ continue
1520
+ if isinstance(stub_ptype, LiteralType):
1521
+ if stub_ptype.base_type == resolved.inner:
1522
+ inner_covered = True
1523
+ continue
1524
+ if stub_ptype.is_int_base() and is_integer_type(resolved.inner):
1525
+ inner_covered = True
1526
+ continue
1527
+ raise SemanticError(
1528
+ f"@overload stub type '{stub_ptype}' for parameter '{pname}' "
1529
+ f"is not compatible with implementation type '{resolved}'",
1530
+ stub.loc,
1531
+ )
1532
+ # Exhaustiveness only when every stub includes this param; when
1533
+ # some stubs skip it the impl default covers the missing path.
1534
+ if len(param_stubs) == len(stubs):
1535
+ missing_parts = []
1536
+ if not inner_covered:
1537
+ missing_parts.append(str(resolved.inner))
1538
+ if not none_covered:
1539
+ missing_parts.append("None")
1540
+ if missing_parts:
1541
+ raise SemanticError(
1542
+ f"@overload stubs for '{display_name}' don't cover "
1543
+ f"all variants of parameter '{pname}': missing "
1544
+ f"{', '.join(missing_parts)}",
1545
+ impl.loc,
1546
+ )
1547
+ continue
1548
+ if not isinstance(resolved, UnionType):
1549
+ for stub in param_stubs:
1550
+ stub_ptype = self.type_ops.resolve_type(stub.params[param_idx][1])
1551
+ if stub_ptype != resolved:
1552
+ # LiteralType is compatible with its base type family
1553
+ if isinstance(stub_ptype, LiteralType):
1554
+ if stub_ptype.base_type == resolved:
1555
+ continue
1556
+ if stub_ptype.is_int_base() and is_integer_type(resolved):
1557
+ continue
1558
+ raise SemanticError(
1559
+ f"@overload stub type '{stub_ptype}' for parameter '{pname}' "
1560
+ f"does not match implementation type '{resolved}'",
1561
+ stub.loc,
1562
+ )
1563
+ continue
1564
+ covered: set[TpyType] = set()
1565
+ for stub in param_stubs:
1566
+ stub_ptype = self.type_ops.resolve_type(stub.params[param_idx][1])
1567
+ if isinstance(stub_ptype, UnionType):
1568
+ stub_members = set(stub_ptype.members)
1569
+ else:
1570
+ stub_members = {stub_ptype}
1571
+ extra = [m for m in stub_members if m not in resolved.members]
1572
+ if extra:
1573
+ extra_names = ", ".join(str(m) for m in extra)
1574
+ raise SemanticError(
1575
+ f"@overload stub type for parameter '{pname}' includes "
1576
+ f"{extra_names} which is not in the implementation's "
1577
+ f"union type '{resolved}'",
1578
+ stub.loc,
1579
+ )
1580
+ covered.update(stub_members)
1581
+ # Only require full union coverage when every stub includes the param.
1582
+ # If some stubs skip it, the impl default handles those call sites.
1583
+ if len(param_stubs) == len(stubs):
1584
+ missing = [m for m in resolved.members if m not in covered]
1585
+ if missing:
1586
+ missing_names = ", ".join(str(m) for m in missing)
1587
+ raise SemanticError(
1588
+ f"@overload stubs for '{display_name}' don't cover all "
1589
+ f"variants of parameter '{pname}': missing {missing_names}",
1590
+ impl.loc,
1591
+ )
1592
+
1593
+ def _register_functions_with_overloads(self, functions: list[TpyFunction]) -> None:
1594
+ """Register functions, grouping @overload stubs with their implementations.
1595
+
1596
+ @overload stubs precede their implementation function (same name).
1597
+ Stubs are registered as callable overloads; the implementation is
1598
+ registered for body analysis only (not directly callable).
1599
+ """
1600
+ pending_stubs: dict[str, list[TpyFunction]] = {}
1601
+
1602
+ for func in functions:
1603
+ if func.builtin_decorator_key:
1604
+ # Register minimal FunctionInfo so the key flows through exports.
1605
+ # return_type comes from func (already VOID-substituted by
1606
+ # parse.resolve_refs); this FunctionInfo is used for
1607
+ # decorator-key lookup, not call resolution.
1608
+ self.ctx.registry.register_function(FunctionInfo(
1609
+ name=func.name, params=[], return_type=func.return_type,
1610
+ builtin_decorator_key=func.builtin_decorator_key,
1611
+ ))
1612
+ # Refresh the attribute-table binding to point at the
1613
+ # registry's list -- pre-pop installed a placeholder
1614
+ # FunctionInfo without builtin_decorator_key, which
1615
+ # exports.functions does NOT use here (the registry's
1616
+ # special one wins).
1617
+ install_binding(
1618
+ self.ctx.module_attributes, func.name,
1619
+ SymbolKind.FUNCTION,
1620
+ self.ctx.registry.get_function(func.name),
1621
+ )
1622
+ continue
1623
+ if func.is_overload_stub:
1624
+ pending_stubs.setdefault(func.name, []).append(func)
1625
+ continue
1626
+
1627
+ # Non-stub function: check if there are pending stubs for this name
1628
+ stubs = pending_stubs.pop(func.name, None)
1629
+ if stubs:
1630
+ self._reject_bodied_overloads_with_impl(stubs, func, func.name)
1631
+ self._register_overload_group(func, stubs)
1632
+ else:
1633
+ self.registrar.register_function(func)
1634
+
1635
+ # Stubs left without an implementation: each stub must be self-contained
1636
+ # (native, cpp_template, or carries its own body).
1637
+ for name, stubs in pending_stubs.items():
1638
+ if all(s.has_implementation for s in stubs):
1639
+ self.registrar.register_overload_group(stubs)
1640
+ else:
1641
+ raise SemanticError(
1642
+ f"@overload stubs for '{name}' have no implementation function",
1643
+ stubs[0].loc,
1644
+ )
1645
+
1646
+ @staticmethod
1647
+ def _reject_bodied_overloads_with_impl(
1648
+ stubs: list[TpyFunction], impl: TpyFunction, display_name: str,
1649
+ ) -> None:
1650
+ """Disallow mixing bodied @overload with a trailing implementation.
1651
+
1652
+ A bodied @overload variant is itself the implementation of that
1653
+ signature. Pairing it with another trailing impl would make dispatch
1654
+ ambiguous; require the author to pick one mode per group.
1655
+ """
1656
+ for stub in stubs:
1657
+ if stub.is_overload_stub and not stub.is_stub:
1658
+ raise SemanticError(
1659
+ f"@overload '{display_name}' has a body and cannot be "
1660
+ f"paired with a trailing implementation; either remove "
1661
+ f"the body or remove the trailing implementation",
1662
+ stub.loc or impl.loc,
1663
+ )
1664
+
1665
+ def _register_overload_group(
1666
+ self, impl: TpyFunction, stubs: list[TpyFunction],
1667
+ ) -> None:
1668
+ """Validate and register an @overload group.
1669
+
1670
+ - Validates stub parameter types cover all union variants in the implementation
1671
+ - Registers each stub as a callable FunctionInfo overload
1672
+ - Stores the group mapping for codegen
1673
+ """
1674
+ self._validate_overload_signatures(impl, stubs, impl.name)
1675
+
1676
+ # Register each stub as a callable overload via a single binding
1677
+ self.registrar.register_overload_group(stubs)
1678
+
1679
+ # The implementation is NOT registered in the namespace/registry --
1680
+ # callers resolve against stubs only. The body is still analyzed
1681
+ # via _analyze_function (which works on the TpyFunction directly).
1682
+
1683
+ # Store the group mapping for codegen
1684
+ self.overload_groups[id(impl)] = stubs
1685
+
1686
+ def _validate_recursive_union_paths(self, module: TpyModule) -> None:
1687
+ """For every alias tagged as recursive in `resolve_refs`, verify that
1688
+ every self-reference goes through an indirecting container. Runs
1689
+ post-registration so the field walk in `validate_recursive_union_paths`
1690
+ can see same-module RecordInfo / TypeDef entries.
1691
+ """
1692
+ if not module.recursive_union_names:
1693
+ return
1694
+ from ..cycle_detection import validate_recursive_union_paths
1695
+ for alias_name in module.recursive_union_names:
1696
+ entry = module.type_aliases.get(alias_name)
1697
+ if entry is None:
1698
+ continue
1699
+ alias_type, alias_loc = entry[0], entry[1]
1700
+ if not isinstance(alias_type, UnionType):
1701
+ continue
1702
+ err = validate_recursive_union_paths(
1703
+ alias_name, alias_type.members, type_params=entry[2])
1704
+ if err is not None:
1705
+ raise SemanticError(err, loc=alias_loc)
1706
+
1707
+ def _detect_recursive_unions(self, module: TpyModule) -> None:
1708
+ """Detect type cycles and tag union aliases as recursive.
1709
+
1710
+ Runs after all records are registered but before type alias registration.
1711
+ Tags aliases in module.recursive_union_names (codegen emits wrapper structs).
1712
+ """
1713
+ # Build inputs: record fields and non-recursive union aliases
1714
+ record_fields: dict[str, list[tuple[str, TpyType]]] = {}
1715
+ for record in module.all_records():
1716
+ record_fields[record.name] = [(f.name, f.type) for f in record.fields]
1717
+
1718
+ union_aliases: dict[str, tuple[TpyType, ...]] = {}
1719
+ alias_locs: dict[str, object] = {}
1720
+ for name, entry in module.type_aliases.items():
1721
+ typ, loc = entry[0], entry[1]
1722
+ if isinstance(typ, UnionType) and name not in module.recursive_union_names:
1723
+ union_aliases[name] = typ.members
1724
+ alias_locs[name] = loc
1725
+
1726
+ if not union_aliases:
1727
+ return
1728
+
1729
+ cycles = detect_type_cycles(record_fields, union_aliases)
1730
+
1731
+ for cycle in cycles:
1732
+ # Validate indirection: cycle must have at least one indirected edge
1733
+ if cycle.is_fully_unindirected():
1734
+ edge = cycle.first_unindirected_edge()
1735
+ assert edge is not None
1736
+ if edge.field_name:
1737
+ msg = (
1738
+ f"Types form an infinite-size cycle: "
1739
+ f"field '{edge.field_name}' in '{edge.source}' references "
1740
+ f"'{edge.target}' without indirection -- "
1741
+ f"use Box[{edge.target}] or another indirecting container"
1742
+ )
1743
+ else:
1744
+ msg = (
1745
+ f"Types form an infinite-size cycle through "
1746
+ f"'{edge.source}' and '{edge.target}' -- "
1747
+ f"use Box or another indirecting container to break the cycle"
1748
+ )
1749
+ # At least one node must be an alias (cycles are reachable
1750
+ # from alias nodes only), so loc should never be None here.
1751
+ loc = alias_locs.get(edge.source) or alias_locs.get(edge.target)
1752
+ raise SemanticError(msg, loc)
1753
+
1754
+ # Tag union aliases in this cycle as recursive. A *generic* alias
1755
+ # reaching this point is recursive transitively (through a record
1756
+ # or another alias) rather than via a direct self-ref -- direct
1757
+ # generic self-refs are tagged at parse-resolution and excluded
1758
+ # from the cycle graph above. v1 supports only direct identity
1759
+ # recursion, so reject the transitive case with a clear diagnostic.
1760
+ for alias_name in cycle.alias_names:
1761
+ entry = module.type_aliases.get(alias_name)
1762
+ if entry is not None and entry[2]:
1763
+ others = [n for n in cycle.path if n != alias_name]
1764
+ through = f" (through {', '.join(others)})" if others else ""
1765
+ raise SemanticError(
1766
+ f"alias '{alias_name}' participates in a type "
1767
+ f"cycle{through}; mutual recursion across generic "
1768
+ f"aliases is not supported in v1",
1769
+ alias_locs.get(alias_name),
1770
+ )
1771
+ module.recursive_union_names.add(alias_name)
1772
+
1773
+ def _register_union_wrappers(self, module: TpyModule) -> None:
1774
+ """Register this module's recursive union aliases in the compiler-wide
1775
+ `union_wrapper_index`. The index is keyed by the canonical member tuple
1776
+ (and, when None is a direct member, also by the non-None subset) so a
1777
+ narrowed-by-None type still resolves to the same wrapper.
1778
+
1779
+ Sema is the authoritative source: each module's pass writes its own
1780
+ entries with `setdefault` (idempotent), accumulating across the
1781
+ compilation. Downstream lookups go through `UnionType.wrapper_info()`
1782
+ / `UnionType.needs_wrapper()`.
1783
+ """
1784
+ if not module.recursive_union_names:
1785
+ return
1786
+ compiler = get_current_compiler()
1787
+ if compiler is None:
1788
+ return
1789
+ index = compiler.union_wrapper_index
1790
+ # Use the codegen-flavor (file-derived) name -- the entry point's
1791
+ # ctx.module_name is "__main__" for runtime semantics, but
1792
+ # codegen filters `info.origin == self.module_name` against the
1793
+ # file name. They must agree.
1794
+ module_name = self.ctx.cpp_module_name
1795
+ for alias_name in module.recursive_union_names:
1796
+ entry = module.type_aliases.get(alias_name)
1797
+ if entry is None:
1798
+ continue
1799
+ typ = entry[0]
1800
+ if not isinstance(typ, UnionType):
1801
+ continue
1802
+ # Generic recursive aliases are keyed by their on-type alias_info
1803
+ # (RecursiveAliasInstanceType), not the member-tuple index -- their
1804
+ # members carry unbound TypeParamRefs that must not leak into the
1805
+ # concrete-union index.
1806
+ if entry[2]:
1807
+ continue
1808
+ info = RecursiveUnionInfo(
1809
+ name=alias_name, full_members=typ.members, origin=module_name,
1810
+ )
1811
+ index.setdefault(typ.members, info)
1812
+ non_none = tuple(m for m in typ.members if not is_void_like_type(m))
1813
+ if len(non_none) < len(typ.members):
1814
+ index.setdefault(non_none, info)
1815
+
1816
+ def _validate_record_method_linkage(self, module: TpyModule) -> None:
1817
+ """Check per-record linkage rules against each method:
1818
+ @native classes require stub bodies; regular classes disallow
1819
+ stub bodies (except @overload stubs) and @native("...") decorators.
1820
+
1821
+ Runs after `parse.resolve_refs` so base-resolution
1822
+ errors fire first (previously these checks ran at parse time
1823
+ and could mask those errors).
1824
+ """
1825
+ for record in module.all_records():
1826
+ for method in record.methods:
1827
+ # Use record.loc so the diagnostic points at the class header.
1828
+ if record.linkage != RecordLinkage.DEFAULT:
1829
+ if not method.is_stub:
1830
+ raise SemanticError(
1831
+ "Methods on @native classes must have '...' "
1832
+ "body (stub declaration)",
1833
+ loc=record.loc,
1834
+ )
1835
+ else:
1836
+ if method.is_stub and not method.is_overload_stub:
1837
+ raise SemanticError(
1838
+ f"Method '{method.name}' cannot have '...' body "
1839
+ f"on a regular class (only allowed on @native classes)",
1840
+ loc=record.loc,
1841
+ )
1842
+ if method.native_name is not None:
1843
+ raise SemanticError(
1844
+ f"@native(\"...\") decorator on method '{method.name}' "
1845
+ f"is only allowed on @native classes",
1846
+ loc=record.loc,
1847
+ )
1848
+
1849
+
1850
+ def _fix_recursive_optional_annotations(self, module: TpyModule) -> None:
1851
+ """Fix annotations where a recursive union alias + None was flattened.
1852
+
1853
+ The parser eagerly expands same-module aliases, so `Expr | None`
1854
+ (where `Expr = Lit | BinOp`) becomes UnionType(NoneType, Lit, BinOp).
1855
+ This pass converts that back to OptionalType(AliasRef("Expr")) by
1856
+ matching the non-None members against the alias definitions.
1857
+
1858
+ When the alias's own definition already includes None (e.g.
1859
+ `JsonValue = None | bool | ... | list[JsonValue]`), a bare reference
1860
+ expands the same way -- but the input is the alias itself, not
1861
+ `Alias | None`. The type stays as the full UnionType so downstream
1862
+ passes (is-None narrowing, match dispatch with `case None:`) keep
1863
+ the NoneType member visible.
1864
+
1865
+ Only covers module-level declarations (function signatures, record
1866
+ fields, top-level vars); local annotations are resolved during body
1867
+ analysis.
1868
+ """
1869
+ # Build reverse map: frozenset(alias non-None members) -> (alias name, alias_has_none)
1870
+ alias_by_members: dict[frozenset, tuple[str, bool]] = {}
1871
+ for name in module.recursive_union_names:
1872
+ entry = module.type_aliases.get(name)
1873
+ if entry is not None:
1874
+ # Generic recursive aliases have parameterized use sites
1875
+ # (RecursiveAliasInstanceType), never a bare expanded union, so
1876
+ # the member-set reverse map does not apply -- and their members
1877
+ # carry TypeParamRefs that would mis-key it.
1878
+ if entry[2]:
1879
+ continue
1880
+ typ = entry[0] # entry[0] is the body type
1881
+ if isinstance(typ, UnionType):
1882
+ alias_has_none = any(
1883
+ is_void_like_type(m) for m in typ.members
1884
+ )
1885
+ non_none = frozenset(
1886
+ m for m in typ.members
1887
+ if not is_void_like_type(m)
1888
+ )
1889
+ alias_by_members[non_none] = (name, alias_has_none)
1890
+
1891
+ if not alias_by_members:
1892
+ return
1893
+
1894
+ def _fix(typ: TpyType) -> TpyType:
1895
+ if not isinstance(typ, UnionType):
1896
+ return typ.map_inner_types(lambda t: _fix(t))
1897
+ non_none = [m for m in typ.members if not is_void_like_type(m)]
1898
+ if len(non_none) == len(typ.members):
1899
+ return typ.map_inner_types(lambda t: _fix(t))
1900
+ entry = alias_by_members.get(frozenset(non_none))
1901
+ if entry is None:
1902
+ return typ.map_inner_types(lambda t: _fix(t))
1903
+ alias_name, alias_has_none = entry
1904
+ if alias_has_none:
1905
+ return typ
1906
+ return OptionalType(AliasRef(alias_name, module=self.ctx.module_name))
1907
+
1908
+ def _fix_func(func: TpyFunction) -> None:
1909
+ for i, (pname, typ) in enumerate(func.params):
1910
+ fixed = _fix(typ)
1911
+ if fixed is not typ:
1912
+ func.params[i] = (pname, fixed)
1913
+ if func.return_type is not None:
1914
+ fixed = _fix(func.return_type)
1915
+ if fixed is not func.return_type:
1916
+ func.return_type = fixed
1917
+
1918
+ for func in module.functions:
1919
+ _fix_func(func)
1920
+ for record in module.all_records():
1921
+ for f in record.fields:
1922
+ fixed = _fix(f.type)
1923
+ if fixed is not f.type:
1924
+ f.type = fixed
1925
+ for method in record.methods:
1926
+ _fix_func(method)
1927
+ if module.top_level_stmts:
1928
+ for stmt in module.top_level_stmts:
1929
+ if isinstance(stmt, TpyVarDecl) and stmt.type is not None:
1930
+ fixed = _fix(stmt.type)
1931
+ if fixed is not stmt.type:
1932
+ stmt.type = fixed
1933
+
1934
+ def _validate_type_alias_members(
1935
+ self, alias_name: str, typ: TpyType, loc: 'SourceLocation | None'
1936
+ ) -> None:
1937
+ """Validate that all NominalType members in a type alias are registered."""
1938
+ # Recursive union aliases have self-referencing NominalType placeholders
1939
+ # inside their members -- safety was already validated.
1940
+ if alias_name in self.ctx.recursive_union_names:
1941
+ return
1942
+ members: list[TpyType] = []
1943
+ if isinstance(typ, UnionType):
1944
+ members = list(typ.members)
1945
+ elif isinstance(typ, NominalType):
1946
+ members = [typ]
1947
+ for m in members:
1948
+ # Bare NominalType reference in an alias body -- the registry
1949
+ # lookup is the actual check. Exclude resolved types (which have
1950
+ # a _module_qname) and protocols.
1951
+ if (isinstance(m, NominalType) and not m.is_protocol
1952
+ and not m._module_qname):
1953
+ if self.ctx.registry.get_record(m.name) is None:
1954
+ raise SemanticError(
1955
+ f"Type alias '{alias_name}' references unknown type '{m.name}'",
1956
+ loc,
1957
+ )
1958
+
1959
+ @staticmethod
1960
+ def _resolve_alias(typ: TpyType, aliases: dict[str, TpyType],
1961
+ _seen: frozenset[str] = frozenset(),
1962
+ _skip: frozenset[str] = frozenset()) -> TpyType:
1963
+ """Recursively substitute alias NominalTypes with their resolved types.
1964
+
1965
+ Uses _seen to prevent infinite recursion on self-referencing aliases.
1966
+ _skip contains recursive union alias names that must not be expanded
1967
+ (their NominalType placeholders are structural).
1968
+ """
1969
+ # Alias placeholders are bare parser NominalTypes (no _module_qname,
1970
+ # no TypeDef entry). Exclude protocols and anything already resolved.
1971
+ if (isinstance(typ, NominalType) and not typ.is_protocol
1972
+ and not typ._module_qname):
1973
+ if typ.name in _seen or typ.name in _skip:
1974
+ return typ
1975
+ resolved = aliases.get(typ.name)
1976
+ if resolved is not None:
1977
+ new_seen = _seen | {typ.name}
1978
+ return resolved.map_inner_types(
1979
+ lambda t: SemanticAnalyzer._resolve_alias(t, aliases, new_seen, _skip)
1980
+ )
1981
+ return typ.map_inner_types(
1982
+ lambda t: SemanticAnalyzer._resolve_alias(t, aliases, _seen, _skip)
1983
+ )
1984
+
1985
+ def _resolve_imported_aliases(self, module: TpyModule) -> None:
1986
+ """Substitute imported alias NominalTypes in module AST type annotations."""
1987
+ # Exclude generic aliases from the substitution pool: their bodies
1988
+ # contain unbound TypeParamRefs that would leak into annotations if
1989
+ # _resolve_alias rewrote a bare NominalType(name) placeholder.
1990
+ # Generic alias use sites go through the parse-resolution path
1991
+ # (`_resolve_generic_alias_use`) which substitutes correctly with
1992
+ # the supplied type args.
1993
+ aliases = {
1994
+ n: info.body
1995
+ for n, info in self.ctx.registry.type_aliases.items()
1996
+ if not info.type_params
1997
+ }
1998
+ skip = frozenset(module.recursive_union_names)
1999
+ for func in module.functions:
2000
+ self._resolve_func_aliases(func, aliases, skip)
2001
+ for record in module.all_records():
2002
+ for f in record.fields:
2003
+ f.type = self._resolve_alias(f.type, aliases, _skip=skip)
2004
+ for method in record.methods:
2005
+ self._resolve_func_aliases(method, aliases, skip)
2006
+ for stmt in module.top_level_stmts:
2007
+ if isinstance(stmt, TpyVarDecl) and stmt.type is not None:
2008
+ stmt.type = self._resolve_alias(stmt.type, aliases, _skip=skip)
2009
+
2010
+ @staticmethod
2011
+ def _resolve_func_aliases(func: TpyFunction, aliases: dict[str, TpyType],
2012
+ skip: frozenset[str] = frozenset()) -> None:
2013
+ """Resolve alias types in a function's signature."""
2014
+ if func.return_type is not None:
2015
+ func.return_type = SemanticAnalyzer._resolve_alias(func.return_type, aliases, _skip=skip)
2016
+ for i, (name, typ) in enumerate(func.params):
2017
+ resolved = SemanticAnalyzer._resolve_alias(typ, aliases, _skip=skip)
2018
+ if resolved is not typ:
2019
+ func.params[i] = (name, resolved)
2020
+
2021
+ def _alias_lookup_for_finalize(
2022
+ self, name: str,
2023
+ ) -> 'tuple[TypeAliasInfo, str, str] | None':
2024
+ """Resolve a referenced alias name to (info, qname, short_name),
2025
+ covering both local and imported aliases, or None if not an alias.
2026
+
2027
+ The qname is the defining module + the alias's original name --
2028
+ collision-proof across modules that define same-short-named aliases,
2029
+ and consistent whether the alias is referenced from its own module or
2030
+ an importer (both resolve to the defining module's name).
2031
+
2032
+ Imported aliases are checked first: `from m import Tree` also registers
2033
+ a local entry, so a local-first lookup would mis-stamp the importer's
2034
+ module name onto the qname (diverging from the defining module's body,
2035
+ which the importer inherits via the shared alias_info)."""
2036
+ imp = self.ctx.registry.imported_type_alias_info.get(name)
2037
+ if imp is not None:
2038
+ decl_mod, orig = imp
2039
+ mod_info = self.ctx.registry.modules.get(decl_mod)
2040
+ iinfo = mod_info.type_aliases.get(orig) if mod_info is not None else None
2041
+ if iinfo is None:
2042
+ iinfo = self.ctx.registry.get_type_alias_info(name)
2043
+ if iinfo is not None:
2044
+ return iinfo, f"{decl_mod}.{orig}", orig
2045
+ info = self.ctx.registry.get_type_alias_info(name)
2046
+ if info is not None:
2047
+ return info, f"{self.ctx.module_name}.{name}", name
2048
+ return None
2049
+
2050
+ def _finalize_alias_refs(self, typ: TpyType) -> TpyType:
2051
+ """Rewrite a generic-recursive `AliasRef` placeholder into the
2052
+ semantic `RecursiveAliasInstanceType`, recursing through inner types.
2053
+
2054
+ Non-generic recursive aliases keep their bare `AliasRef` self-ref
2055
+ (their use sites stay UnionType + union_wrapper_index)."""
2056
+ if isinstance(typ, AliasRef):
2057
+ looked = self._alias_lookup_for_finalize(typ.name)
2058
+ if (looked is not None and looked[0].type_params
2059
+ and looked[0].is_recursive):
2060
+ info, qname, _short = looked
2061
+ new_args = tuple(
2062
+ self._finalize_alias_refs(a) if isinstance(a, TpyType) else a
2063
+ for a in typ.args
2064
+ )
2065
+ # Render via the *local* reference name (`typ.name`) -- codegen
2066
+ # keys native_cpp_names by the importing name (e.g. `from m
2067
+ # import Tree as MyTree`), exactly as cross-module records do,
2068
+ # so two same-short-named aliases imported into one module do
2069
+ # not collide on rendering. Identity stays the qname.
2070
+ return RecursiveAliasInstanceType(qname, new_args, typ.name, info)
2071
+ return typ.map_inner_types(self._finalize_alias_refs)
2072
+
2073
+ def _finalize_generic_recursive_aliases(self, module: TpyModule) -> None:
2074
+ """Convert the parser's generic-recursive `AliasRef` placeholders into
2075
+ `RecursiveAliasInstanceType` across alias bodies and all annotation
2076
+ slots, using the sema-registry alias_info (parse-resolve runs against
2077
+ a separate registry and cannot mint the semantic type).
2078
+
2079
+ Single conversion point so use-site instances and recursive children
2080
+ share one alias_info source. Runs both for locally-defined generic
2081
+ recursive aliases (whose bodies get finalized) and for modules that
2082
+ merely *use* an imported one (use sites still need conversion). See
2083
+ docs/GENERIC_RECURSIVE_ALIASES_DESIGN.md."""
2084
+ generic_rec = {
2085
+ n for n in module.recursive_union_names
2086
+ if (gi := self.ctx.registry.get_type_alias_info(n)) is not None
2087
+ and gi.type_params
2088
+ }
2089
+ imports_generic_rec = any(
2090
+ (lk := self._alias_lookup_for_finalize(n)) is not None
2091
+ and lk[0].type_params and lk[0].is_recursive
2092
+ for n in self.ctx.registry.imported_type_alias_info
2093
+ )
2094
+ if not generic_rec and not imports_generic_rec:
2095
+ return
2096
+ # Alias bodies: the registered TypeAliasInfo (read by alternatives
2097
+ # expansion) and the cross-phase tuple (read by codegen + export).
2098
+ for n in generic_rec:
2099
+ info = self.ctx.registry.get_type_alias_info(n)
2100
+ info.body = self._finalize_alias_refs(info.body)
2101
+ entry = module.type_aliases.get(n)
2102
+ if entry is not None:
2103
+ module.type_aliases[n] = (
2104
+ (self._finalize_alias_refs(entry[0]),) + tuple(entry[1:])
2105
+ )
2106
+ # Use sites in annotations.
2107
+ for func in module.functions:
2108
+ self._finalize_func_aliases(func)
2109
+ for record in module.all_records():
2110
+ for f in record.fields:
2111
+ f.type = self._finalize_alias_refs(f.type)
2112
+ for method in record.methods:
2113
+ self._finalize_func_aliases(method)
2114
+ # RecordInfo carries registration-time snapshots of the ctor and
2115
+ # method signatures, built before this pass, so their alias
2116
+ # placeholders are stale and must be finalized here too. (Free
2117
+ # functions register after this pass, so their FunctionInfo is
2118
+ # already finalized; protocol signatures are finalized below.)
2119
+ info = self.ctx.registry.get_record(record.name)
2120
+ if info is not None:
2121
+ if info.init_params:
2122
+ info.init_params = [
2123
+ (n, self._finalize_alias_refs(t), d)
2124
+ for (n, t, d) in info.init_params
2125
+ ]
2126
+ for overloads in info.methods.values():
2127
+ for fi in overloads:
2128
+ if fi.return_type is not None:
2129
+ fi.return_type = self._finalize_alias_refs(fi.return_type)
2130
+ for pi in fi.params:
2131
+ pi.type = self._finalize_alias_refs(pi.type)
2132
+ # Protocol method signatures are another registration-time snapshot:
2133
+ # the registry ProtocolInfo (shared with the TypeDef payload) is read
2134
+ # by conformance + protocol-call dispatch; the AST copy is separate.
2135
+ for protocol in module.protocols:
2136
+ self._finalize_method_signatures(protocol.methods)
2137
+ pinfo = self.ctx.registry.scan_by_short_name(protocol.name)
2138
+ if pinfo is not None:
2139
+ self._finalize_method_signatures(pinfo.methods)
2140
+ if module.top_level_stmts:
2141
+ self._finalize_stmts_aliases(module.top_level_stmts)
2142
+
2143
+ def _finalize_method_signatures(self, methods: list[MethodSignature]) -> None:
2144
+ for msig in methods:
2145
+ for i, (name, typ) in enumerate(msig.params):
2146
+ new = self._finalize_alias_refs(typ)
2147
+ if new is not typ:
2148
+ msig.params[i] = (name, new)
2149
+ if msig.return_type is not None:
2150
+ msig.return_type = self._finalize_alias_refs(msig.return_type)
2151
+
2152
+ def _finalize_func_aliases(self, func: TpyFunction) -> None:
2153
+ if func.return_type is not None:
2154
+ func.return_type = self._finalize_alias_refs(func.return_type)
2155
+ for i, (name, typ) in enumerate(func.params):
2156
+ new = self._finalize_alias_refs(typ)
2157
+ if new is not typ:
2158
+ func.params[i] = (name, new)
2159
+ if func.vararg_type is not None:
2160
+ func.vararg_type = self._finalize_alias_refs(func.vararg_type)
2161
+ if func.kwarg_type is not None:
2162
+ func.kwarg_type = self._finalize_alias_refs(func.kwarg_type)
2163
+ if func.body:
2164
+ self._finalize_stmts_aliases(func.body)
2165
+
2166
+ def _finalize_stmts_aliases(self, stmts: 'list') -> None:
2167
+ """Convert generic-recursive AliasRef placeholders in local variable
2168
+ annotations. Local annotations are resolved at parse-resolution (via
2169
+ resolve_refs `_walk_body`), so they carry the same placeholders as
2170
+ signatures and must be finalized too."""
2171
+ for stmt in stmts:
2172
+ if isinstance(stmt, TpyVarDecl) and stmt.type is not None:
2173
+ stmt.type = self._finalize_alias_refs(stmt.type)
2174
+ if isinstance(stmt, TpyNestedDef):
2175
+ self._finalize_func_aliases(stmt.func)
2176
+ continue
2177
+ for body in stmt.sub_bodies():
2178
+ self._finalize_stmts_aliases(body)
2179
+
2180
+ def _analyze_class_constants(self, record: TpyRecord) -> None:
2181
+ """Analyze each class constant's initializer and validate it is a
2182
+ compile-time constant. Earlier constants in declaration order are
2183
+ bound into the class-body namespace so later constants can reference
2184
+ them (e.g. `B: Final[int] = A + 1`).
2185
+ """
2186
+ record_info = self.ctx.registry.get_record(record.name)
2187
+ if record_info is None or not record_info.class_constants:
2188
+ return
2189
+
2190
+ # Class-body context: synthetic module-init scope so the const-validity
2191
+ # check (`_find_nonconstant_leaf`) accepts Final names via
2192
+ # `analyzed_finals`, and so name resolution inherits module-level
2193
+ # bindings. Class-constant names get added to scope/ns as analysis
2194
+ # progresses (so later constants can forward-ref earlier ones).
2195
+ self.ctx.reset_function_tracking()
2196
+ self.ctx.func.current_function = MODULE_INIT_CONTEXT
2197
+ self.ctx.func.current_scope = Scope(parent=self.ctx.global_scope)
2198
+ self.ctx.func.current_ns = Namespace(parent=self.ctx.global_ns)
2199
+ self.ctx.is_top_level = True
2200
+
2201
+ # Class-constant names get added to `analyzed_finals` so the
2202
+ # const-validity check accepts forward refs. Track adds and roll
2203
+ # back at the end -- avoids cloning the full module Finals set.
2204
+ added_finals: list[str] = []
2205
+
2206
+ try:
2207
+ for cc_name, cc_fld in record_info.class_constants.items():
2208
+ if cc_fld.default_expr is None:
2209
+ # @native extern binding: no initializer to analyze.
2210
+ continue
2211
+ self.expr.analyze_expr_with_hint(cc_fld.default_expr, cc_fld.type)
2212
+ self.stmts.validate_compile_time_constant(
2213
+ cc_fld.default_expr, cc_fld.type,
2214
+ f"class constant '{record.name}.{cc_name}'",
2215
+ cc_fld.loc,
2216
+ )
2217
+ # Bind so later class constants in the same body can reference it.
2218
+ self.ctx.func.current_scope.define(cc_name, cc_fld.type)
2219
+ self.ctx.func.current_ns.bind_variable(cc_name, cc_fld.type)
2220
+ self.ctx.analyzed_finals.add(cc_name)
2221
+ added_finals.append(cc_name)
2222
+ finally:
2223
+ self.ctx.analyzed_finals.difference_update(added_finals)
2224
+ self.ctx.func.current_function = None
2225
+ self.ctx.func.current_scope = None
2226
+ self.ctx.func.current_ns = None
2227
+ self.ctx.is_top_level = False
2228
+
2229
+ def _analyze_record_methods(self, record: TpyRecord) -> None:
2230
+ """Analyze all methods of a record."""
2231
+ # Set up record context for super() support and generic type params
2232
+ self.ctx.record_ctx.record = record
2233
+ self.ctx.record_ctx.type_params = record.type_params if record.type_params else None
2234
+ self.ctx.record_ctx.type_param_kinds = record.type_param_kinds if record.type_param_kinds else None
2235
+ self.ctx.record_ctx.type_param_bounds = record.type_param_bounds if record.type_param_bounds else None
2236
+
2237
+ # Collect method overload groups for this record
2238
+ self._collect_method_overload_groups(record)
2239
+
2240
+ for method in record.methods:
2241
+ # Skip bodyless @overload stubs -- their trailing impl is analyzed
2242
+ # instead. Bodied @overload stubs (mode b) need body analysis just
2243
+ # like regular methods.
2244
+ if method.is_overload_stub and method.is_stub:
2245
+ continue
2246
+
2247
+ self.ctx.reset_function_tracking()
2248
+ self.ctx.func.current_function = method
2249
+ # Resolve return type (sets is_protocol for cross-module imports)
2250
+ method.return_type = make_ref(self.type_ops.resolve_type(method.return_type))
2251
+ scope = Scope(parent=self.ctx.global_scope)
2252
+ self.ctx.func.current_scope = scope
2253
+
2254
+ local_ns = Namespace(parent=self.ctx.global_ns)
2255
+ self.ctx.func.current_ns = local_ns
2256
+ if not method.is_staticmethod:
2257
+ info = self.ctx.registry.get_record(record.name)
2258
+ self_named = build_record_self_type(
2259
+ record,
2260
+ qname=info.qualified_name() if info is not None else None,
2261
+ )
2262
+ self_type = self._normalize_param_type(self_named, method.is_readonly)
2263
+ scope.define("self", self_type)
2264
+ self.ctx.func.var_scope_depth["self"] = scope.depth
2265
+ self.ctx.func.definitely_assigned.add("self")
2266
+ local_ns.bind_variable("self", self_type)
2267
+
2268
+ # Resolve and normalize params (@readonly wraps all non-value params).
2269
+ # Write back to AST so codegen sees Ref/ReadonlyType (codegen reads func.params directly, not FI).
2270
+ # For auto_readonly_params_resolved methods, the parser clone already applied
2271
+ # ReadonlyType to the params that need it -- skip blanket wrapping.
2272
+ readonly_ctx = method.is_readonly and not method.auto_readonly_params_resolved
2273
+ resolved_params: list[tuple[str, TpyType]] = []
2274
+ for i, (pname, ptype) in enumerate(method.params):
2275
+ resolved_ptype = self._normalize_param_type(
2276
+ self.type_ops.resolve_type(ptype), readonly_ctx)
2277
+ method.params[i] = (pname, make_ref(resolved_ptype))
2278
+ resolved_params.append((pname, resolved_ptype))
2279
+
2280
+ # Add *args parameter as Span[readonly[T]] to method.params and resolved_params
2281
+ if method.vararg_name is not None and method.vararg_type is not None:
2282
+ va_type = _vararg_span_type(self.type_ops.resolve_type(method.vararg_type))
2283
+ kw_start = method.keyword_only_start
2284
+ if kw_start is not None and kw_start < len(method.params):
2285
+ method.params.insert(kw_start, (method.vararg_name, make_ref(va_type)))
2286
+ resolved_params.insert(kw_start, (method.vararg_name, va_type))
2287
+ if method.defaults:
2288
+ method.defaults.insert(kw_start, None)
2289
+ method.keyword_only_start = kw_start + 1
2290
+ else:
2291
+ method.params.append((method.vararg_name, make_ref(va_type)))
2292
+ resolved_params.append((method.vararg_name, va_type))
2293
+ if method.defaults:
2294
+ method.defaults.append(None)
2295
+ method.vararg_type = None
2296
+
2297
+ # Add **kwargs parameter as TypedDict type
2298
+ if method.kwarg_name is not None and method.kwarg_type is not None:
2299
+ kw_type = self.type_ops.resolve_type(method.kwarg_type)
2300
+ method.params.append((method.kwarg_name, make_ref(kw_type)))
2301
+ resolved_params.append((method.kwarg_name, kw_type))
2302
+ if method.defaults:
2303
+ method.defaults.append(None)
2304
+ method.kwarg_type = None
2305
+
2306
+ # __next__ must have an explicit non-void return type annotation
2307
+ if method.name == "__next__" and isinstance(method.return_type, VoidType):
2308
+ raise self._error(
2309
+ "__next__ method must have a return type annotation",
2310
+ method
2311
+ )
2312
+
2313
+ # @inline methods: skip body analysis. The body is a template that
2314
+ # gets cloned and substituted at each call site (see methods.py
2315
+ # _inline_method_call).
2316
+ if method.is_inline and not method.is_stub:
2317
+ method.skip_codegen = True
2318
+ continue
2319
+
2320
+ # Track consuming method for ownership propagation through fields
2321
+ prev_consuming = self.ctx.in_consuming_method
2322
+ self.ctx.in_consuming_method = method.is_consuming
2323
+
2324
+ self._validate_named_defaults(method)
2325
+
2326
+ # Shared core: bind params, prescan, analyze body
2327
+ scan = self.stmts._prescan_and_analyze_body(method, resolved_params, scope, local_ns)
2328
+
2329
+ # Collect generator / async-coro local variables for struct
2330
+ # field generation (locals that may live across yield / await
2331
+ # suspensions are hoisted to the resumable-frame struct).
2332
+ if method.is_generator or method.is_async:
2333
+ param_names = {pname for pname, _ in method.params}
2334
+ locals_dict: dict[str, 'TpyType'] = {}
2335
+ for name, binding in local_ns.all_bindings().items():
2336
+ if name not in param_names and name != "self" and binding.type is not None:
2337
+ locals_dict[name] = binding.type
2338
+ for name, (vtype, _, _) in self.ctx.func.pending_loop_vars.items():
2339
+ if name not in param_names and name != "self" and vtype is not None:
2340
+ locals_dict[name] = vtype
2341
+ method.generator_locals = list(locals_dict.items())
2342
+
2343
+ # Finalize nested def escape analysis (same as _analyze_function)
2344
+ self._finalize_nested_def_escapes()
2345
+
2346
+ self.ctx.in_consuming_method = prev_consuming
2347
+
2348
+ # Warn if __next__ has no raise StopIteration (likely infinite loop)
2349
+ # Skip for native methods (cpp_template set) -- StopIteration handled in C++
2350
+ if (method.name == "__next__" and not method.cpp_template
2351
+ and not _body_has_raise(method.body, "builtins.StopIteration")):
2352
+ self.ctx.warning(
2353
+ "__next__() has no 'raise StopIteration' -- "
2354
+ "iterator will loop forever if caller exhausts it",
2355
+ method
2356
+ )
2357
+
2358
+ # Check for field assignments inside control flow in __init__
2359
+ if method.name == "__init__":
2360
+ self._check_init_field_assignments(method, record)
2361
+
2362
+ # Validate super().__init__() position in __init__ methods
2363
+ if method.name == "__init__" and self.ctx.func.super_init_call is not None:
2364
+ # super().__init__() must be the first non-docstring statement
2365
+ first_real_stmt = MethodAnalyzer.find_first_non_docstring_stmt(method.body)
2366
+ if first_real_stmt is not None:
2367
+ # Check if first real statement contains the super().__init__() call
2368
+ if not MethodAnalyzer.stmt_contains_super_init(first_real_stmt, self.ctx.func.super_init_call):
2369
+ raise self._error(
2370
+ "super().__init__() must be the first statement in __init__",
2371
+ self.ctx.func.super_init_call
2372
+ )
2373
+
2374
+ # Require an explicit base-init call (super().__init__(...) or
2375
+ # Base.__init__(self, ...)) when the single user-record base has
2376
+ # no synthesizable default constructor. Otherwise the C++ MIL
2377
+ # would try to implicit-default-construct the base subobject and
2378
+ # the build fails with a cryptic "no matching function for call
2379
+ # to Base::Base()" error from inside the subclass ctor.
2380
+ if method.name == "__init__":
2381
+ self._require_super_init_for_non_default_base(method, record)
2382
+
2383
+ # Validate __del__ methods
2384
+ if method.name == "__del__":
2385
+ record_info = self.ctx.registry.get_record(record.name)
2386
+ if self.ctx.func.super_del_call is not None:
2387
+ # super().__del__() must be the last non-docstring statement
2388
+ last_real_stmt = MethodAnalyzer.find_last_non_docstring_stmt(method.body)
2389
+ if last_real_stmt is not None and not is_super_del_call(last_real_stmt):
2390
+ raise self._error(
2391
+ "super().__del__() must be the last statement in __del__",
2392
+ self.ctx.func.super_del_call
2393
+ )
2394
+ elif record_info and record_info.parents:
2395
+ # No super().__del__() but parent(s) may have __del__. Any parent
2396
+ # with __del__ runs automatically after this destructor.
2397
+ parent_has_del = False
2398
+ for p in record_info.parents:
2399
+ if not hasattr(p, 'name'):
2400
+ continue
2401
+ parent_info = self.ctx.registry.get_record(p.name)
2402
+ if parent_info and parent_info.get_method("__del__") is not None:
2403
+ parent_has_del = True
2404
+ break
2405
+ if parent_has_del:
2406
+ self.ctx.warning(
2407
+ "Parent class has __del__() which will be called automatically by C++ "
2408
+ "after this destructor runs. Unlike Python, you do not need "
2409
+ "super().__del__() -- but it also means the parent destructor "
2410
+ "always runs even without an explicit call.",
2411
+ method
2412
+ )
2413
+
2414
+ self.deduction.resolve_all()
2415
+
2416
+ # Store Phase 1 local mutation facts on method FunctionInfo.
2417
+ # For @overload methods, get_method() returns overloads[0] (the first
2418
+ # stub). The implementation's FI is not separately registered, so all
2419
+ # Phase 1 facts are stored on stub[0] and Phase 2 / const inference
2420
+ # work through it. This is consistent with _sync_inferred_const, which
2421
+ # also reads back via get_method() and skips is_overload_stub nodes.
2422
+ record_info = self.ctx.registry.get_record(record.name)
2423
+ if record_info is not None and not method.is_stub:
2424
+ method_fi = record_info.get_method(method.name)
2425
+ # Property methods are not in the methods dict (popped during
2426
+ # registration) -- look them up through the properties registry
2427
+ # so their return_borrows_from facts are recorded.
2428
+ if method_fi is None and method.is_property_getter:
2429
+ prop = record_info.properties.get(method.name)
2430
+ if prop is not None:
2431
+ method_fi = prop.getter
2432
+ elif method_fi is None and method.is_property_setter:
2433
+ prop = record_info.properties.get(method.property_name)
2434
+ if prop is not None:
2435
+ method_fi = prop.setter
2436
+ if (method_fi is not None
2437
+ and method_fi.direct_mutated_params is None
2438
+ and method_fi.originating_module == self.ctx.module_name):
2439
+ param_list = [pname for pname, _ in method.params]
2440
+ direct = frozenset(
2441
+ i for i, pname in enumerate(param_list)
2442
+ if pname in self.ctx.func.current_mutated_param_names
2443
+ )
2444
+ method_fi.direct_mutated_params = direct
2445
+ method_fi.direct_self_mutated = self.ctx.func.current_self_mutated
2446
+ method_fi.call_edges = list(self.ctx.func.current_call_edges)
2447
+ method_fi.mutated_params = direct
2448
+ method_fi.representational_type_params = frozenset(self.ctx.func.current_representational_params)
2449
+ # Structural mutation facts (append/insert/clear/del/etc.)
2450
+ direct_struct = frozenset(
2451
+ i for i, pname in enumerate(param_list)
2452
+ if pname in self.ctx.func.current_struct_mutated_param_names
2453
+ )
2454
+ if self.ctx.func.current_self_struct_mutated:
2455
+ direct_struct = direct_struct | frozenset({-1})
2456
+ method_fi.direct_structural_mutated_params = direct_struct
2457
+ method_fi.structural_mutated_params = direct_struct
2458
+ # 8b: Return borrow facts
2459
+ returned = frozenset(
2460
+ i for i, pname in enumerate(param_list)
2461
+ if pname in self.ctx.func.current_returned_param_names
2462
+ )
2463
+ if "self" in self.ctx.func.current_returned_param_names:
2464
+ returned = returned | frozenset([-1])
2465
+ method_fi.return_borrows_from = returned
2466
+ method_fi.addr_escapes_params = frozenset(
2467
+ i for i, pname in enumerate(param_list)
2468
+ if pname in self.ctx.func.current_addr_escape_param_names
2469
+ )
2470
+
2471
+ self._warn_unconsumed_own_params(method)
2472
+ self._store_analysis_results(method, scan)
2473
+
2474
+ self.ctx.func.current_scope = None
2475
+ self.ctx.func.current_function = None
2476
+ self.ctx.func.current_ns = None
2477
+
2478
+ # Multi-base init-call coverage check. Runs after all method bodies
2479
+ # are analyzed so expr.unbound_self_parent_type is set on any
2480
+ # BaseN.__init__(self, ...) calls in the child __init__.
2481
+ record_info_for_init = self.ctx.registry.get_record(record.name)
2482
+ if record_info_for_init is not None:
2483
+ self.registrar.validate_multi_base_init_calls(record, record_info_for_init)
2484
+
2485
+ self.ctx.record_ctx = RecordContext()
2486
+
2487
+ def _check_init_field_assignments(self, method: TpyFunction, record: TpyRecord) -> None:
2488
+ """Enforce the two-section __init__ model.
2489
+
2490
+ The init section is the leading prefix of super().__init__() +
2491
+ self.field = expr (own fields only, each at most once). Everything
2492
+ after the first statement that breaks this pattern is the body section.
2493
+
2494
+ At the split point:
2495
+ - Own fields not initialized + no default ctor -> error.
2496
+ - Own fields not initialized + has default ctor -> warning (CPython gap).
2497
+ - self.method() call with uninitialized fields in RHS -> warning.
2498
+
2499
+ In the body section (depth > 0 branches):
2500
+ - @nocopy field assigned -> error (copy assignment deleted).
2501
+ - __del__ field assigned -> error (destructor on default-constructed value).
2502
+ - Other fields: silently allowed (e.g. accumulation in a loop body).
2503
+ """
2504
+ record_info = self.ctx.registry.get_record(record.name)
2505
+ if record_info is None:
2506
+ return
2507
+ # Native types manage their own construction in C++
2508
+ if record_info.is_native:
2509
+ return
2510
+ # Own fields for split-point and missing-field checks
2511
+ own_fields: dict[str, FieldInfo] = {f.name: f for f in record_info.fields}
2512
+ # All fields (incl. inherited) for branch-body safety checks
2513
+ all_fields = self._collect_all_fields(record_info)
2514
+ if not own_fields and not all_fields:
2515
+ return
2516
+
2517
+ # --- Find split point ---
2518
+ init_section_fields: set[str] = set()
2519
+ split_idx = len(method.body)
2520
+
2521
+ for i, stmt in enumerate(method.body):
2522
+ if is_base_init_call(stmt):
2523
+ continue
2524
+ if is_docstring(stmt):
2525
+ continue
2526
+ if (isinstance(stmt, TpyAssign)
2527
+ and isinstance(stmt.target, TpyFieldAccess)
2528
+ and isinstance(stmt.target.obj, TpyName)
2529
+ and stmt.target.obj.name == "self"
2530
+ and stmt.target.field in all_fields
2531
+ and stmt.target.field not in init_section_fields):
2532
+ field_name = stmt.target.field
2533
+ # Warn if RHS calls self.method() while fields are still uninitialized
2534
+ uninit = set(own_fields.keys()) - init_section_fields
2535
+ if uninit and expr_contains_self_method_call(stmt.value):
2536
+ self._warning(
2537
+ f"instance method called in __init__ before all fields are initialized; "
2538
+ f"the method may access uninitialized fields",
2539
+ stmt
2540
+ )
2541
+ init_section_fields.add(field_name)
2542
+ continue
2543
+ # Not a valid init-section statement: this is the split point
2544
+ split_idx = i
2545
+ break
2546
+
2547
+ # Collect all fields assigned unconditionally (depth 0) in the full body.
2548
+ # Fields in this set are OK even if not in init section: they will be
2549
+ # assigned in the constructor body before any branch or method call uses them.
2550
+ depth0_assigned: set[str] = {
2551
+ stmt.target.field
2552
+ for stmt in method.body
2553
+ if (isinstance(stmt, TpyAssign)
2554
+ and isinstance(stmt.target, TpyFieldAccess)
2555
+ and isinstance(stmt.target.obj, TpyName)
2556
+ and stmt.target.obj.name == "self")
2557
+ }
2558
+
2559
+ # --- Check fields at split point ---
2560
+ first_error: SemanticError | None = None
2561
+
2562
+ if own_fields:
2563
+ split_node = method.body[split_idx] if split_idx < len(method.body) else None
2564
+ for field_name, field_info in own_fields.items():
2565
+ if field_name in init_section_fields:
2566
+ continue
2567
+ if field_name in depth0_assigned:
2568
+ # Field is assigned unconditionally in the body (just not in the
2569
+ # init section), so it will be initialized before first use.
2570
+ continue
2571
+ if field_info.default_value is not None or field_info.default_expr is not None:
2572
+ continue
2573
+ field_type = field_info.type
2574
+ # In a generic record, fields whose type involves a type parameter
2575
+ # cannot be checked here -- C++ handles the constraint at instantiation.
2576
+ if record.type_params and contains_type_param(field_type):
2577
+ continue
2578
+ if self.protocols._is_default_constructible(field_type):
2579
+ self._warning(
2580
+ f"field '{field_name}' is not initialized before the constructor body; "
2581
+ f"it will be default-constructed in C++ "
2582
+ f"(in CPython, the attribute would not exist)",
2583
+ split_node
2584
+ )
2585
+ else:
2586
+ err = self._error(
2587
+ f"field '{field_name}' of type '{str(field_type)}' has no default "
2588
+ f"constructor and is not initialized before the constructor body",
2589
+ split_node
2590
+ )
2591
+ if first_error is None:
2592
+ first_error = err
2593
+
2594
+ # --- Check branch assignments in body (all fields incl. inherited) ---
2595
+ def walk_body(stmts: list[TpyStmt], depth: int) -> None:
2596
+ nonlocal first_error
2597
+ for stmt in stmts:
2598
+ if isinstance(stmt, TpyAssign):
2599
+ target = stmt.target
2600
+ if (depth > 0
2601
+ and isinstance(target, TpyFieldAccess)
2602
+ and isinstance(target.obj, TpyName)
2603
+ and target.obj.name == "self"
2604
+ and target.field in all_fields):
2605
+ field_name = target.field
2606
+ field_type = all_fields[field_name].type
2607
+ if self._type_has_del_or_nocopy(field_type):
2608
+ type_name = str(field_type)
2609
+ err = self._error(
2610
+ f"field '{field_name}' of type '{type_name}' assigned inside "
2611
+ f"control flow in __init__; '{type_name}' is move-only or has "
2612
+ f"'__del__', so it must be initialized unconditionally before "
2613
+ f"any branch",
2614
+ stmt
2615
+ )
2616
+ if first_error is None:
2617
+ first_error = err
2618
+ elif isinstance(stmt, TpyIf):
2619
+ walk_body(stmt.then_body, depth + 1)
2620
+ walk_body(stmt.else_body, depth + 1)
2621
+ elif isinstance(stmt, (TpyWhile, TpyForEach)):
2622
+ walk_body(stmt.body, depth + 1)
2623
+ walk_body(stmt.orelse, depth + 1)
2624
+
2625
+ walk_body(method.body, 0)
2626
+
2627
+ if first_error is not None:
2628
+ raise first_error
2629
+
2630
+ def _collect_all_fields(self, record_info: RecordInfo) -> dict[str, FieldInfo]:
2631
+ """Collect all fields from a record and its ancestors, walking MRO base-first.
2632
+
2633
+ Child fields override ancestor fields with the same name (last-write-wins
2634
+ in dict semantics, consistent with Python attribute shadowing).
2635
+ """
2636
+ result: dict[str, FieldInfo] = {}
2637
+ for anc_rec in self.ctx.registry.iter_ancestor_records(record_info, reverse=True):
2638
+ for f in anc_rec.fields:
2639
+ result[f.name] = f
2640
+ for f in record_info.fields:
2641
+ result[f.name] = f
2642
+ return result
2643
+
2644
+ def _require_super_init_for_non_default_base(
2645
+ self, method: TpyFunction, record: TpyRecord,
2646
+ ) -> None:
2647
+ """Reject `class Child(Base): def __init__(self, ...): ...` without an
2648
+ explicit base-init call when Base's C++ default constructor would be
2649
+ implicitly deleted -- C++ would otherwise fail to synthesize the
2650
+ base subobject default-construction in the child's MIL.
2651
+ `super().__init__(...)` and `Base.__init__(self, ...)` are both
2652
+ accepted (mirrors `is_base_init_call`). Multi-base inheritance is
2653
+ already covered by `validate_multi_base_init_calls`.
2654
+
2655
+ The predicate matches the codegen-level "would `Base() = default;`
2656
+ succeed?" not the user-level "is Base default-constructible from
2657
+ TPy?". A base with a required-arg `__init__` but all-default-ctor
2658
+ fields synthesizes `Base()` fine at C++ level (leaves fields
2659
+ default-initialized), so it does NOT trigger the rule.
2660
+ """
2661
+ record_info = self.ctx.registry.get_record(record.name)
2662
+ if record_info is None or record_info.is_native:
2663
+ return
2664
+ if len(record_info.parents) != 1:
2665
+ return
2666
+ parent = record_info.parents[0]
2667
+ if not isinstance(parent, NominalType) or parent.is_protocol:
2668
+ return
2669
+ parent_rec = self.ctx.registry.get_record_for_type(parent)
2670
+ if parent_rec is None or parent_rec.is_native or parent_rec.builtin_type_key:
2671
+ return
2672
+ if not self._record_default_ctor_is_deleted(parent_rec):
2673
+ return
2674
+ # Accept either super().__init__(...) or Base.__init__(self, ...).
2675
+ if self.ctx.func.super_init_call is not None:
2676
+ return
2677
+ for stmt in method.body:
2678
+ if is_base_init_call(stmt):
2679
+ return
2680
+ reason = self._explain_default_ctor_deletion(parent_rec)
2681
+ if method.is_macro_generated:
2682
+ origin = (f"'@{method.macro_origin}'" if method.macro_origin
2683
+ else "a macro")
2684
+ raise self._error(
2685
+ f"the '__init__' synthesized by {origin} for '{record.name}' "
2686
+ f"does not initialize parent class '{parent_rec.name}', but "
2687
+ f"'{parent_rec.name}' cannot be constructed without arguments "
2688
+ f"({reason}). Either give '{parent_rec.name}' a zero-argument "
2689
+ f"form, or write an explicit '__init__' on '{record.name}' "
2690
+ f"that calls 'super().__init__(...)'.",
2691
+ method,
2692
+ )
2693
+ raise self._error(
2694
+ f"'{record.name}.__init__' must call 'super().__init__(...)' "
2695
+ f"(or '{parent_rec.name}.__init__(self, ...)') as its first "
2696
+ f"statement: '{parent_rec.name}' cannot be constructed without "
2697
+ f"arguments ({reason}), so the inherited fields are left "
2698
+ f"uninitialized.",
2699
+ method,
2700
+ )
2701
+
2702
+ def _explain_default_ctor_deletion(
2703
+ self, rec: 'RecordInfo', _visited: set[str] | None = None,
2704
+ ) -> str:
2705
+ """Produce a one-clause user-facing explanation of why `rec` cannot
2706
+ be constructed without arguments. Walks the same chain as
2707
+ `_record_default_ctor_is_deleted` and reports the FIRST source it
2708
+ finds, so the message attributes the actual cause (a specific
2709
+ field on `rec` itself, or on an ancestor) rather than blaming
2710
+ `rec` generically. `_visited` mirrors the cycle guard in
2711
+ `_record_default_ctor_is_deleted`.
2712
+ """
2713
+ if _visited is None:
2714
+ _visited = set()
2715
+ if rec.name in _visited:
2716
+ return f"'{rec.name}' (cycle in ancestor/field chain)"
2717
+ _visited = _visited | {rec.name}
2718
+ if del_suppresses_default_ctor(rec):
2719
+ return (f"'{rec.name}' has '__del__' and required '__init__' "
2720
+ f"parameters")
2721
+ for fld in rec.fields:
2722
+ if self._field_type_blocks_default_ctor(fld.type):
2723
+ inner = self._explain_field_type_blocks_default_ctor(
2724
+ fld.type, _visited)
2725
+ return (f"field '{fld.name}' has type '{str(fld.type)}' "
2726
+ f"({inner})")
2727
+ for p in rec.parents:
2728
+ if not isinstance(p, NominalType) or p.is_protocol:
2729
+ continue
2730
+ p_rec = self.ctx.registry.get_record_for_type(p)
2731
+ if p_rec is None or p_rec.is_native or p_rec.builtin_type_key:
2732
+ continue
2733
+ if self._record_default_ctor_is_deleted(p_rec):
2734
+ inner = self._explain_default_ctor_deletion(p_rec, _visited)
2735
+ return (f"ancestor '{p_rec.name}' (inherited by "
2736
+ f"'{rec.name}') has the same restriction: {inner}")
2737
+ return "a field or ancestor has no zero-argument constructor"
2738
+
2739
+ def _record_default_ctor_is_deleted(
2740
+ self, rec: 'RecordInfo', _visited: set[str] | None = None,
2741
+ ) -> bool:
2742
+ """True if the C++ `Base() = default;` for this user record would
2743
+ be implicitly deleted -- either codegen explicitly suppresses it
2744
+ (via `del_suppresses_default_ctor`) or some field / ancestor's
2745
+ type lacks a C++ default ctor.
2746
+
2747
+ Mirrors codegen's `_all_fields_default_constructible` but inverted
2748
+ (returns True when default-ctor is unavailable). `_visited` guards
2749
+ cyclic shapes (parent reachable via own generic instantiation,
2750
+ mutual-import edge cases).
2751
+ """
2752
+ if _visited is None:
2753
+ _visited = set()
2754
+ if rec.name in _visited:
2755
+ return False
2756
+ _visited = _visited | {rec.name}
2757
+ if del_suppresses_default_ctor(rec):
2758
+ return True
2759
+ for fld in rec.fields:
2760
+ if self._field_type_blocks_default_ctor(fld.type, _visited):
2761
+ return True
2762
+ # Route parents through the field-type predicate so generic
2763
+ # parents (e.g. `class C(Box[Int32])`) dispatch through the
2764
+ # `type_args` branch and consult the base template's
2765
+ # `del_suppresses_default_ctor`.
2766
+ for p in rec.parents:
2767
+ if self._field_type_blocks_default_ctor(p, _visited):
2768
+ return True
2769
+ return False
2770
+
2771
+ def _field_type_blocks_default_ctor(
2772
+ self, typ: TpyType, _visited: set[str] | None = None,
2773
+ ) -> bool:
2774
+ """True if a field of this type forces the enclosing record's C++
2775
+ default ctor to be deleted. Mirrors codegen's
2776
+ `_fld_type_cpp_default_constructible` in `codegen_cpp/records.py`
2777
+ (this is the inverse).
2778
+ """
2779
+ typ = unwrap_readonly(typ)
2780
+ # Storage-form Own[T] field stores T inline; the wrapped type's
2781
+ # default-ctor is what matters.
2782
+ if isinstance(typ, OwnType):
2783
+ return self._field_type_blocks_default_ctor(typ.wrapped, _visited)
2784
+ # Shapes with usable C++ default ctors regardless of T.
2785
+ if isinstance(typ, (OptionalType, PtrType)):
2786
+ return False
2787
+ if is_enum_type(typ):
2788
+ return False
2789
+ if is_list(typ) or is_dict(typ) or is_set(typ) or is_span(typ):
2790
+ return False
2791
+ # Tuple / Array: default-ctorable iff every element is.
2792
+ if isinstance(typ, TupleType):
2793
+ return any(self._field_type_blocks_default_ctor(et, _visited)
2794
+ for et in typ.element_types)
2795
+ if is_array(typ):
2796
+ elem = typ.get_element_type()
2797
+ return elem is not None and self._field_type_blocks_default_ctor(elem, _visited)
2798
+ # Union field lowers to std::variant<...>; codegen's
2799
+ # `_fld_type_cpp_default_constructible` does not special-case
2800
+ # UnionType and falls through to `_is_default_constructible`
2801
+ # which also returns False for it -- so codegen never emits
2802
+ # `Outer() = default;` for a class with a union field. Mirror
2803
+ # that: every union field blocks the enclosing default ctor.
2804
+ # (Optional[T] is OptionalType, not UnionType, and stays
2805
+ # default-ctorable via the OptionalType branch above.)
2806
+ if isinstance(typ, UnionType):
2807
+ return True
2808
+ if isinstance(typ, NominalType) and not typ.is_protocol:
2809
+ # Generic instantiation: codegen emits `= default;` for the
2810
+ # template unless the base template itself suppresses default
2811
+ # construction (Box/Rc/Weak, __del__-shapes). Mirror that.
2812
+ if typ.type_args:
2813
+ base_rec = self.ctx.registry.get_record(typ.name)
2814
+ if base_rec is None:
2815
+ return False
2816
+ return del_suppresses_default_ctor(base_rec)
2817
+ rec = self.ctx.registry.get_record_for_type(typ)
2818
+ if rec is None or rec.builtin_type_key or rec.is_native:
2819
+ return False
2820
+ return self._record_default_ctor_is_deleted(rec, _visited)
2821
+ return False
2822
+
2823
+ def _explain_field_type_blocks_default_ctor(
2824
+ self, typ: TpyType, _visited: set[str] | None = None,
2825
+ ) -> str:
2826
+ """Mirror of `_field_type_blocks_default_ctor`: name the cause for
2827
+ the user instead of just returning bool. Caller has already
2828
+ verified that the predicate is True for this type. `_visited`
2829
+ threads through to `_explain_default_ctor_deletion` to guard
2830
+ against cyclic ancestor/field chains.
2831
+ """
2832
+ typ = unwrap_readonly(typ)
2833
+ if isinstance(typ, OwnType):
2834
+ return self._explain_field_type_blocks_default_ctor(
2835
+ typ.wrapped, _visited)
2836
+ if isinstance(typ, TupleType):
2837
+ for i, et in enumerate(typ.element_types):
2838
+ if self._field_type_blocks_default_ctor(et):
2839
+ inner = self._explain_field_type_blocks_default_ctor(
2840
+ et, _visited)
2841
+ return f"tuple element {i} ({inner})"
2842
+ return "a tuple element cannot be constructed without arguments"
2843
+ if is_array(typ):
2844
+ elem = typ.get_element_type()
2845
+ if elem is not None:
2846
+ inner = self._explain_field_type_blocks_default_ctor(
2847
+ elem, _visited)
2848
+ return f"array element type ({inner})"
2849
+ return "the array element type cannot be constructed without arguments"
2850
+ if isinstance(typ, UnionType):
2851
+ return "union fields have no default in C++"
2852
+ if isinstance(typ, NominalType) and not typ.is_protocol:
2853
+ if typ.type_args:
2854
+ base_rec = self.ctx.registry.get_record(typ.name)
2855
+ if base_rec is not None and del_suppresses_default_ctor(base_rec):
2856
+ return (f"'{typ.name}' has '__del__' and required "
2857
+ f"'__init__' parameters")
2858
+ rec = self.ctx.registry.get_record_for_type(typ)
2859
+ if rec is not None and not rec.builtin_type_key and not rec.is_native:
2860
+ return self._explain_default_ctor_deletion(rec, _visited)
2861
+ return "the type has no zero-argument constructor"
2862
+
2863
+ def _type_has_del_or_nocopy(self, field_type: TpyType) -> bool:
2864
+ """Check if a type (or any ancestor in its MRO) has __del__ or is @nocopy."""
2865
+ inner = field_type
2866
+ if isinstance(inner, OwnType):
2867
+ inner = inner.wrapped
2868
+ inner = unwrap_readonly(inner)
2869
+ rec = self.ctx.registry.get_record_for_type(inner)
2870
+ if rec is None:
2871
+ return False
2872
+ if rec.is_nocopy or rec.has_del:
2873
+ return True
2874
+ for anc_rec in self.ctx.registry.iter_ancestor_records(rec):
2875
+ if anc_rec.has_del or anc_rec.is_nocopy:
2876
+ return True
2877
+ return False
2878
+
2879
+ def _analyze_top_level(self, stmts: list[TpyStmt]) -> None:
2880
+ """Analyze top-level statements (for generated main()).
2881
+
2882
+ Treats top-level code like a function body so list literals and other
2883
+ constructs go through the same analysis path.
2884
+ """
2885
+ self.ctx.reset_function_tracking()
2886
+ self.ctx.func.current_function = MODULE_INIT_CONTEXT
2887
+ self.ctx.func.current_scope = Scope(parent=self.ctx.global_scope)
2888
+ self.ctx.is_top_level = True
2889
+
2890
+ # Set up namespace - use global_ns for top-level (globals are visible)
2891
+ # New local variables will be added to global_ns as they're declared
2892
+ self.ctx.func.current_ns = self.ctx.global_ns
2893
+
2894
+ # Top-level builder-trace expansion runs here (not in pass 5.5)
2895
+ # because pass 4 is itself the body-analysis pass for module
2896
+ # code -- the rewrite has to land before the analyze loop below.
2897
+ self._expand_builder_trace_top_level(stmts)
2898
+
2899
+ # Pre-scan for codegen
2900
+ self.top_level_scan_result = scan_reassigned_vars(stmts)
2901
+ # Last-use analysis for auto-move (shared with codegen)
2902
+ self.ctx.all_last_uses |= analyze_last_uses(
2903
+ stmts, self.top_level_scan_result.alias_sources)
2904
+ self.ctx.func.current_reassigned_vars = self.top_level_scan_result.reassigned.copy()
2905
+ self.ctx.func.current_lvalue_reassigned = self.top_level_scan_result.lvalue_reassigned.copy()
2906
+ self.ctx.func.current_aug_assigned_vars = self.top_level_scan_result.aug_assigned.copy()
2907
+
2908
+ for stmt in stmts:
2909
+ self.stmts.analyze_stmt(stmt)
2910
+ self.deduction.resolve_all()
2911
+
2912
+ if self.ctx.func.hoisted_vars:
2913
+ self.top_level_hoisted_vars = self.ctx.func.hoisted_vars.copy()
2914
+ if self.ctx.func.move_through_vars:
2915
+ self.top_level_move_through_vars = self.ctx.func.move_through_vars.copy()
2916
+ self.if_branch_decls.update(self.ctx.if_branch_decls)
2917
+
2918
+ self.ctx.func.current_function = None
2919
+ self.ctx.func.current_scope = None
2920
+ self.ctx.func.current_ns = None
2921
+ self.ctx.is_top_level = False
2922
+
2923
+ def get_expr_type(self, expr: TpyExpr) -> Optional[TpyType]:
2924
+ """Get the cached type of an expression."""
2925
+ return self.ctx.get_expr_type(expr)
2926
+
2927
+ def _register_implicit_module_variable(self, module_name: str,
2928
+ original_name: str,
2929
+ local_name: str) -> bool:
2930
+ """Promote an IMPORTED_NAME to VARIABLE when the implicit module
2931
+ re-exports a module-level constant.
2932
+
2933
+ The parser's special handling for ``tpy`` / ``typing`` / ``builtins``
2934
+ skips ``user_module_imports``, so the regular
2935
+ ``_register_user_module_import`` path doesn't run on imports from these
2936
+ modules. Records / functions / type aliases already have dedicated
2937
+ resolution paths in the implicit-module branch; module-level variables
2938
+ do not. This helper covers the variable-only case so re-exported
2939
+ constants (e.g. ``__version__`` from a ``# tpy: native_module``
2940
+ ``tpy/__init__.py``) become first-class VARIABLE bindings instead of
2941
+ being rejected at use site as "not a variable".
2942
+ """
2943
+ module_info = self.ctx.registry.get_module(module_name)
2944
+ if module_info is None or module_info.is_builtin:
2945
+ return False
2946
+ if not module_info.variables or original_name not in module_info.variables:
2947
+ return False
2948
+ var_info = module_info.variables[original_name]
2949
+ self.ctx.global_scope.define(local_name, var_info.type)
2950
+ self.ctx.global_ns.bind_variable(local_name, var_info.type)
2951
+ # Install into the attribute table so use-site checks that read
2952
+ # `module_attributes` (e.g. `is_indirect_name`, `pointer_globals`)
2953
+ # find non-value imports and emit the right deref / pointer
2954
+ # qualification. Without this, a non-value-typed implicit-stdlib
2955
+ # variable would lose its `(*x)` indirection at use sites.
2956
+ install_binding(
2957
+ self.ctx.module_attributes, local_name,
2958
+ SymbolKind.VARIABLE, var_info.type,
2959
+ defining_module=module_name, canonical_name=original_name,
2960
+ )
2961
+ return True
2962
+
2963
+ def _register_tpy_type_alias(self, original_name: str, local_name: str) -> None:
2964
+ """Register a single tpy type alias from the compiled tpy module_info."""
2965
+ # Compile-time-only types (e.g. FStr) take priority
2966
+ self.registrar._register_compile_time_type_alias(local_name, "tpy", original_name)
2967
+
2968
+ # AST-level type aliases (e.g. Float64 = float)
2969
+ tpy_info = self.ctx.registry.get_module("tpy")
2970
+ if not tpy_info or not tpy_info.type_aliases:
2971
+ return
2972
+ alias_info = tpy_info.type_aliases.get(original_name)
2973
+ if alias_info is not None:
2974
+ self.ctx.registry.register_type_alias(local_name, alias_info.body,
2975
+ imported_from=("tpy", original_name),
2976
+ info=alias_info)
2977
+ if local_name != original_name:
2978
+ self.ctx.registry.register_type_alias(original_name, alias_info.body,
2979
+ info=alias_info)
2980
+
2981
+ def _register_all_tpy_type_aliases(self) -> None:
2982
+ """Register all tpy type aliases (for bare 'import tpy' and star imports)."""
2983
+ tpy_info = self.ctx.registry.get_module("tpy")
2984
+ if not tpy_info or not tpy_info.type_aliases:
2985
+ return
2986
+ for name, alias_info in tpy_info.type_aliases.items():
2987
+ self.ctx.registry.register_type_alias(name, alias_info.body,
2988
+ imported_from=("tpy", name),
2989
+ info=alias_info)
2990
+
2991
+ def _bind_star_reexport(self, original_name: str, local_name: str) -> None:
2992
+ """Try to bind a star-imported re-export from a known module.
2993
+
2994
+ When 'from utils import *' brings in a name like Int32 that utils
2995
+ imported from tpy, we need to find the original source and bind it
2996
+ correctly so the name is usable.
2997
+ """
2998
+ for mod_name, mod_info in self.ctx.registry.modules.items():
2999
+ if mod_info.is_builtin:
3000
+ continue
3001
+ if mod_info.has_export(original_name):
3002
+ self.ctx.imported_names[local_name] = (mod_name, original_name)
3003
+ self.ctx.global_ns.bind_imported_name(local_name, mod_name, original_name)
3004
+ self._register_user_module_import(mod_name, original_name, local_name)
3005
+ return
3006
+ # Fallback: tpy type aliases (e.g. Int32, Float64)
3007
+ self._register_tpy_type_alias(original_name, local_name)
3008
+
3009
+ def _register_nested_with_alias(self, items, register_fn,
3010
+ original_name: str, local_name: str) -> None:
3011
+ """Register nested children of `original_name` from `items` (a name -> info
3012
+ dict) under both the canonical key (`Outer.Inner`) and -- when the import
3013
+ is aliased -- the alias-prefixed key (`L.Inner`). Mirrors the pre-register
3014
+ pattern used for the flat record above.
3015
+ """
3016
+ if not items:
3017
+ return
3018
+ prefix = original_name + "."
3019
+ aliased = local_name != original_name
3020
+ for canonical, info in items.items():
3021
+ if not canonical.startswith(prefix):
3022
+ continue
3023
+ register_fn(info, canonical)
3024
+ if aliased:
3025
+ register_fn(info, local_name + canonical[len(original_name):])
3026
+
3027
+ def _register_from_attribute_binding(
3028
+ self, src_binding, local_name: str, module_name: str,
3029
+ original_name: str,
3030
+ ) -> bool:
3031
+ """Register an imported name when the source's per-kind dicts
3032
+ haven't materialized yet but its attribute-table binding has
3033
+ (cycle re-export). Dispatches on `src_binding.kind` to install
3034
+ the same registry/global_ns/attribute-table state the per-kind
3035
+ branches in `_register_user_module_import` would, so downstream
3036
+ consumers see the import. Returns True on success.
3037
+ """
3038
+ ult_mod = src_binding.defining_module or module_name
3039
+ ult_name = src_binding.canonical_name
3040
+ kind = src_binding.kind
3041
+ info = src_binding.info
3042
+ if kind == SymbolKind.FUNCTION:
3043
+ func_infos = info if isinstance(info, list) else [info]
3044
+ self.ctx.registry.register_function_group(local_name, func_infos)
3045
+ if len(func_infos) > 1:
3046
+ self.ctx.global_ns.bind(NameBinding(
3047
+ kind=BindingKind.FUNCTION,
3048
+ name=local_name,
3049
+ func_infos=func_infos,
3050
+ ))
3051
+ install_binding(
3052
+ self.ctx.module_attributes, local_name,
3053
+ SymbolKind.FUNCTION, func_infos,
3054
+ defining_module=ult_mod, canonical_name=ult_name,
3055
+ )
3056
+ return True
3057
+ if kind == SymbolKind.RECORD:
3058
+ self.ctx.registry.register_record(info, local_name)
3059
+ if local_name != original_name:
3060
+ self.ctx.registry.register_record(info, original_name)
3061
+ install_binding(
3062
+ self.ctx.module_attributes, local_name,
3063
+ SymbolKind.RECORD, info,
3064
+ defining_module=ult_mod, canonical_name=ult_name,
3065
+ )
3066
+ return True
3067
+ if kind in (SymbolKind.PROTOCOL_STATIC, SymbolKind.PROTOCOL_DYNAMIC):
3068
+ self.ctx.registry.register_protocol(info, local_name)
3069
+ self.ctx.global_ns.bind_imported_name(local_name, module_name, original_name)
3070
+ install_binding(
3071
+ self.ctx.module_attributes, local_name,
3072
+ kind, info,
3073
+ defining_module=ult_mod, canonical_name=ult_name,
3074
+ )
3075
+ return True
3076
+ if kind == SymbolKind.ENUM:
3077
+ self.ctx.registry.register_enum(info, local_name)
3078
+ if local_name != original_name:
3079
+ self.ctx.registry.register_enum(info, original_name)
3080
+ self.ctx.global_ns.bind_enum(info, name=local_name)
3081
+ install_binding(
3082
+ self.ctx.module_attributes, local_name,
3083
+ SymbolKind.ENUM, info,
3084
+ defining_module=ult_mod, canonical_name=ult_name,
3085
+ )
3086
+ return True
3087
+ if kind in (SymbolKind.OPAQUE, SymbolKind.PARSER_KEYWORD):
3088
+ # Parser keywords / builtin types have no analyzer registry
3089
+ # registration (the parser resolves them via TypeDef /
3090
+ # decorator schemas) and no `global_ns` counterpart (use
3091
+ # sites for these names go through the parser's
3092
+ # `_name_index` route, not the namespace lookup). We only
3093
+ # propagate the per-module attribute binding so consumers'
3094
+ # `from M import X` (explicit or star) and the cycle
3095
+ # re-export pre-pop both find the chain entry. The lack of
3096
+ # a `global_ns.bind_imported_name` here is intentional --
3097
+ # adding it would shadow the parser-keyword fallback path
3098
+ # in `_analyze_name` and break the bootstrap that lets
3099
+ # `@builtin_decorator` resolve before sema runs.
3100
+ install_binding(
3101
+ self.ctx.module_attributes, local_name,
3102
+ kind, info,
3103
+ defining_module=ult_mod, canonical_name=ult_name,
3104
+ )
3105
+ return True
3106
+ return False
3107
+
3108
+ def _register_user_module_import(self, module_name: str, original_name: str, local_name: str,
3109
+ from_star_import: bool = False) -> bool:
3110
+ """Register an imported item from a user module.
3111
+
3112
+ Looks up the item in the unified registry (user modules are registered
3113
+ as ModuleInfo before analysis) and registers it in the appropriate
3114
+ namespace (function, record, or protocol).
3115
+
3116
+ Returns True if the name was found and registered, False if not found
3117
+ (only possible when from_star_import is True).
3118
+
3119
+ When from_star_import is True, names not found in the module's exports
3120
+ are silently skipped (they may be re-imported names visible in the
3121
+ source but not in the compiled module's ModuleInfo).
3122
+ """
3123
+ module_info = self.ctx.registry.get_module(module_name)
3124
+ if module_info is None:
3125
+ # Module not in registry - may be a macro module (loaded
3126
+ # via macro_registry but never registered as a compiled
3127
+ # module). Install a macro binding when applicable so
3128
+ # downstream re-export chases find the macro.
3129
+ macro_chain = self._resolve_macro_chain(module_name, original_name)
3130
+ if macro_chain is not None:
3131
+ ult_mod, ult_name, kind = macro_chain
3132
+ install_binding(
3133
+ self.ctx.module_attributes, local_name,
3134
+ kind, None,
3135
+ defining_module=ult_mod, canonical_name=ult_name,
3136
+ )
3137
+ return True
3138
+ if module_info.is_builtin:
3139
+ # Builtin module (no user file shadowing it), skip to let builtin handling work
3140
+ return True
3141
+
3142
+ # Check for function (list of overloads in ModuleInfo)
3143
+ if module_info.functions and original_name in module_info.functions:
3144
+ func_infos = module_info.functions[original_name]
3145
+ # Skip callable registration for special_handling builtins
3146
+ # (e.g. native_global in tpy.extern). Statement interception in
3147
+ # statements.py looks up the IMPORTED_NAME binding's import_source;
3148
+ # registering a function group here would clobber that binding.
3149
+ # Still record the import so re-export works.
3150
+ if func_infos and func_infos[0].special_handling:
3151
+ install_binding(
3152
+ self.ctx.module_attributes, local_name,
3153
+ SymbolKind.FUNCTION, func_infos,
3154
+ defining_module=module_name, canonical_name=original_name,
3155
+ )
3156
+ return True
3157
+ self.ctx.registry.register_function_group(local_name, func_infos)
3158
+ if len(func_infos) > 1:
3159
+ self.ctx.global_ns.bind(NameBinding(
3160
+ kind=BindingKind.FUNCTION,
3161
+ name=local_name,
3162
+ func_infos=func_infos,
3163
+ ))
3164
+ # Attribution matches `_extract_declaration_exports`'s
3165
+ # function re-export logic: defining_module is the ultimate
3166
+ # definer (originating_module); canonical_name is the
3167
+ # function's name in that module.
3168
+ ult_mod = func_infos[0].originating_module if func_infos else None
3169
+ ult_name = func_infos[0].name if func_infos else original_name
3170
+ install_binding(
3171
+ self.ctx.module_attributes, local_name,
3172
+ SymbolKind.FUNCTION, func_infos,
3173
+ defining_module=ult_mod or module_name,
3174
+ canonical_name=ult_name,
3175
+ )
3176
+ return True
3177
+
3178
+ # Check for record
3179
+ if module_info.records and original_name in module_info.records:
3180
+ record_info = module_info.records[original_name]
3181
+ # Register with local name for lookup (and original name for type resolution)
3182
+ self.ctx.registry.register_record(record_info, local_name)
3183
+ if local_name != original_name:
3184
+ self.ctx.registry.register_record(record_info, original_name)
3185
+ # Also register nested types so Outer.Inner resolves in the importing module.
3186
+ self._register_nested_with_alias(
3187
+ module_info.records, self.ctx.registry.register_record,
3188
+ original_name, local_name)
3189
+ self._register_nested_with_alias(
3190
+ module_info.enums, self.ctx.registry.register_enum,
3191
+ original_name, local_name)
3192
+ # Install with the chain-flattened attribution: defining_module
3193
+ # is the *ultimate* definer (record_info.defining_module),
3194
+ # canonical_name is the record's name in that module.
3195
+ install_binding(
3196
+ self.ctx.module_attributes, local_name,
3197
+ SymbolKind.RECORD, record_info,
3198
+ defining_module=record_info.defining_module,
3199
+ canonical_name=record_info.name,
3200
+ )
3201
+ return True
3202
+
3203
+ # Check for protocol
3204
+ if module_info.protocols and original_name in module_info.protocols:
3205
+ protocol_info = module_info.protocols[original_name]
3206
+ # Register with local name for lookup (supports aliases)
3207
+ self.ctx.registry.register_protocol(protocol_info, local_name)
3208
+ # Also bind in namespace so it can be resolved as a type
3209
+ self.ctx.global_ns.bind_imported_name(local_name, module_name, original_name)
3210
+ # Install with chain-flattened attribution: defining_module
3211
+ # is the *ultimate* definer (protocol_info.module), canonical
3212
+ # name is the protocol's name in that module. Mirrors the
3213
+ # records / enums / functions paths above; the prior
3214
+ # `defining_module=module_name` (immediate import source)
3215
+ # caused chain re-exports through `pkg/__init__.py` to
3216
+ # qualify protocols via the intermediate package, which
3217
+ # broke `RefAdapter` instantiation when the package's own
3218
+ # `using` was suppressed for sibling-cycle reasons.
3219
+ ult_mod = protocol_info.module if protocol_info.module else module_name
3220
+ install_binding(
3221
+ self.ctx.module_attributes, local_name,
3222
+ protocol_kind_for(protocol_info.is_dynamic), protocol_info,
3223
+ defining_module=ult_mod, canonical_name=protocol_info.name,
3224
+ )
3225
+ return True
3226
+
3227
+ # Check for type alias
3228
+ if module_info.type_aliases and original_name in module_info.type_aliases:
3229
+ alias_info = module_info.type_aliases[original_name]
3230
+ typ = alias_info.body
3231
+ self.ctx.registry.register_type_alias(local_name, typ,
3232
+ imported_from=(module_name, original_name),
3233
+ info=alias_info)
3234
+ # Implicitly import member record types so codegen can qualify them
3235
+ if isinstance(typ, UnionType) and module_info.records:
3236
+ for member in typ.members:
3237
+ if isinstance(member, NominalType) and member.name in module_info.records:
3238
+ if self.ctx.registry.get_record(member.name) is None:
3239
+ rec = module_info.records[member.name]
3240
+ self.ctx.registry.register_record(rec, member.name)
3241
+ install_binding(
3242
+ self.ctx.module_attributes, local_name,
3243
+ SymbolKind.TYPE_ALIAS, typ,
3244
+ defining_module=module_name, canonical_name=original_name,
3245
+ )
3246
+ return True
3247
+
3248
+ # Check for enum
3249
+ if module_info.enums and original_name in module_info.enums:
3250
+ enum_type = module_info.enums[original_name]
3251
+ self.ctx.registry.register_enum(enum_type, local_name)
3252
+ # Also register under original name: the parser resolves aliases
3253
+ # back to original names for type annotations (NominalType("Color")
3254
+ # even when the alias is "C")
3255
+ if local_name != original_name:
3256
+ self.ctx.registry.register_enum(enum_type, original_name)
3257
+ self.ctx.global_ns.bind_enum(enum_type, name=local_name)
3258
+ # Install with chain-flattened attribution: defining_module
3259
+ # is the ultimate declaring module (EnumInfo.module_name),
3260
+ # canonical_name is the enum's name in that module.
3261
+ einfo = enum_info_of(enum_type)
3262
+ ult_mod = einfo.module_name if (einfo and einfo.module_name) else module_name
3263
+ install_binding(
3264
+ self.ctx.module_attributes, local_name,
3265
+ SymbolKind.ENUM, enum_type,
3266
+ defining_module=ult_mod, canonical_name=enum_type.name,
3267
+ )
3268
+ return True
3269
+
3270
+ # Check for variable
3271
+ if module_info.variables and original_name in module_info.variables:
3272
+ var_info = module_info.variables[original_name]
3273
+ self.ctx.global_scope.define(local_name, var_info.type)
3274
+ self.ctx.global_ns.bind_variable(local_name, var_info.type)
3275
+ # Install with the chain-flattened ultimate defining module
3276
+ # so the consumer's `.hpp` renders the definer's qname
3277
+ # directly (matters when the immediate source is a
3278
+ # native_module facade with no .hpp).
3279
+ ult_mod, ult_name = resolve_definer(
3280
+ self.ctx.registry, module_name, original_name,
3281
+ SymbolKind.VARIABLE)
3282
+ install_binding(
3283
+ self.ctx.module_attributes, local_name,
3284
+ SymbolKind.VARIABLE, var_info.type,
3285
+ defining_module=ult_mod, canonical_name=ult_name,
3286
+ )
3287
+ return True
3288
+
3289
+ # @builtin_type/@builtin_decorator stubs and parser keywords are handled
3290
+ # at parse time, not exported by .py files -- silently skip them here.
3291
+ if is_parser_keyword(module_name, original_name):
3292
+ return True
3293
+ if (self.ctx.registry.get_builtin_type_key(original_name) or
3294
+ self.ctx.registry.get_builtin_decorator_key(original_name)):
3295
+ return True
3296
+
3297
+ # `from pkg import submod` where submod is itself a registered user
3298
+ # module: bind it as a MODULE so consumer code can do
3299
+ # `submod.X(...)` / `submod.Type` qualified access (mirrors CPython
3300
+ # which treats the imported name as the submodule namespace object).
3301
+ submodule_qname = f"{module_name}.{original_name}"
3302
+ if self.ctx.registry.get_module(submodule_qname) is not None:
3303
+ self.ctx.global_ns.bind_module(submodule_qname, alias=local_name)
3304
+ return True
3305
+
3306
+ # Macro re-export (Phase 6): the source module may itself be a
3307
+ # macro module that registers `original_name`, or it may be a
3308
+ # plain module that re-exports a macro. Walk the chain to find
3309
+ # the ultimate macro source and install a binding so consumer
3310
+ # sites pick up the macro under the right registry key.
3311
+ macro_chain = self._resolve_macro_chain(module_name, original_name)
3312
+ if macro_chain is not None:
3313
+ ult_mod, ult_name, kind = macro_chain
3314
+ install_binding(
3315
+ self.ctx.module_attributes, local_name,
3316
+ kind, None,
3317
+ defining_module=ult_mod, canonical_name=ult_name,
3318
+ )
3319
+ return True
3320
+
3321
+ # Cycle re-export fallback: the per-kind dicts on the source
3322
+ # ModuleInfo may not yet hold `original_name` because the source
3323
+ # is a cycle peer whose bind_imports hasn't run. Consult the
3324
+ # source's pre-populated attribute table, and if it has a
3325
+ # re-export binding, dispatch on kind to register the imported
3326
+ # name in the analyzer's registry just as the per-kind branches
3327
+ # above would.
3328
+ if module_info is not None and module_info.module_attributes is not None:
3329
+ src_cell = module_info.module_attributes.get(original_name)
3330
+ if src_cell is not None:
3331
+ if self._register_from_attribute_binding(
3332
+ src_cell.binding, local_name, module_name, original_name):
3333
+ return True
3334
+
3335
+ # Star imports include all public names from the source (matching
3336
+ # CPython), but not all of them have entries in ModuleInfo (e.g.
3337
+ # re-imported names like Int32 from tpy). Skip those so the caller
3338
+ # doesn't bind them as coming from this module.
3339
+ if from_star_import:
3340
+ return False
3341
+
3342
+ raise self._error(f"'{original_name}' not found in module '{module_name}'")
3343
+
3344
+ def _resolve_macro_chain(
3345
+ self, module_name: str, name: str,
3346
+ ) -> 'tuple[str, str, SymbolKind] | None':
3347
+ """Find the ultimate macro source for `(module_name, name)` by
3348
+ first probing the macro registry directly (handles macro
3349
+ modules that aren't registered as compiled modules) then
3350
+ walking the binding chain.
3351
+
3352
+ Returns (defining_module, canonical_name, SymbolKind), or None
3353
+ if no chain reaches a macro. Used by `_register_user_module_import`
3354
+ when the per-kind dict branches miss -- macros aren't tracked
3355
+ in the per-kind dicts on `ModuleInfo`.
3356
+ """
3357
+ registry = self.ctx.macro_registry
3358
+ if registry is None:
3359
+ return None
3360
+ for kind, getter in (
3361
+ (SymbolKind.CLASS_MACRO, registry.get_macro),
3362
+ (SymbolKind.CALL_MACRO, registry.get_call_macro),
3363
+ (SymbolKind.BUILDER_MACRO, registry.get_builder_macro),
3364
+ ):
3365
+ if getter(module_name, name) is not None:
3366
+ return (module_name, name, kind)
3367
+ result = walk_attribute_chain(
3368
+ self.ctx.registry, module_name, name, is_macro_kind)
3369
+ if result is None:
3370
+ return None
3371
+ ult_mod, ult_name, bd = result
3372
+ return (ult_mod, ult_name, bd.kind)
3373
+
3374
+ def _expand_builder_traces(self, module: TpyModule) -> None:
3375
+ """Pass 5.5: walk every record-method body and free-function body
3376
+ in the module, running the builder-trace expander on each.
3377
+ Splices synthesized records / functions into ``module`` before
3378
+ passes 6/7 iterate, so emitted method bodies get sema-analyzed
3379
+ like user-written ones.
3380
+
3381
+ Top-level statements are expanded later, inside
3382
+ ``_analyze_top_level`` (pass 4 is itself the body-analysis pass
3383
+ for module-level code, so expansion has to run there).
3384
+
3385
+ No-op when no @builder_macro classes are registered.
3386
+ """
3387
+ if (self.ctx.macro_registry is None
3388
+ or not self.ctx.macro_registry.has_builder_macros()):
3389
+ return
3390
+ # Both loops iterate snapshots: `all_records()` already returns a
3391
+ # fresh list, and `list(module.functions)` is explicit because that
3392
+ # one is live. Records and functions synthesized during expansion
3393
+ # need no further expansion -- they are built from quoted fragments
3394
+ # / AST primitives, not user source containing more builder traces.
3395
+ for record in module.all_records():
3396
+ for method in record.methods:
3397
+ self._expand_builder_trace_body(
3398
+ method, f"{record.name}.{method.name}")
3399
+ for func in list(module.functions):
3400
+ self._expand_builder_trace_body(func, func.name)
3401
+
3402
+ def _expand_builder_trace_body(self, func: TpyFunction, function_qname: str) -> None:
3403
+ """Run BuilderTraceExpander on a function/method body in place.
3404
+
3405
+ No-op when the macro registry has no @builder_macro classes or
3406
+ the body is empty.
3407
+ """
3408
+ if not func.body:
3409
+ return
3410
+ if (self.ctx.macro_registry is None
3411
+ or not self.ctx.macro_registry.has_builder_macros()):
3412
+ return
3413
+ expander = BuilderTraceExpander(
3414
+ ctx=self.ctx, registrar=self.registrar,
3415
+ module=self._module, function_being_traced=function_qname,
3416
+ )
3417
+ new_body = expander.expand(func.body)
3418
+ if new_body is not None:
3419
+ func.body = new_body
3420
+
3421
+ def _expand_builder_trace_top_level(self, stmts: list[TpyStmt]) -> None:
3422
+ """Run BuilderTraceExpander on module top-level statements in place.
3423
+
3424
+ Mutates ``stmts`` directly (the caller passed in module.top_level_stmts).
3425
+ """
3426
+ if not stmts:
3427
+ return
3428
+ if (self.ctx.macro_registry is None
3429
+ or not self.ctx.macro_registry.has_builder_macros()):
3430
+ return
3431
+ expander = BuilderTraceExpander(
3432
+ ctx=self.ctx, registrar=self.registrar,
3433
+ module=self._module, function_being_traced="<module>",
3434
+ )
3435
+ new_stmts = expander.expand(stmts)
3436
+ if new_stmts is not None:
3437
+ stmts[:] = new_stmts
3438
+
3439
+ def _promote_macro_generated_types(self, module: TpyModule) -> None:
3440
+ """Promote bare `NominalType` placeholders on macro-generated
3441
+ method signatures / bodies / registered `FunctionInfo` to
3442
+ qname-bearing form using the post-macro-deps `ctx.registry`.
3443
+
3444
+ Macros may reference types (JsonReader, JsonWriter, ...) that
3445
+ main.py doesn't import explicitly -- those land in the registry
3446
+ only after `_populate_macro_deps`, so promotion must run here
3447
+ rather than inside `register_record`.
3448
+ """
3449
+ resolver = self.ctx.parser_resolver
3450
+ if resolver is None:
3451
+ return
3452
+ registry = self.ctx.registry
3453
+ for record in module.all_records():
3454
+ # AST-level method (TpyFunction) signatures + bodies.
3455
+ for method in record.methods:
3456
+ _promote_method_signature(method, registry)
3457
+ scope = _merged_method_scope(_record_scope(record), method)
3458
+ _walk_body(method.body, scope, resolver, promote_registry=registry)
3459
+ # Registered FunctionInfo (captured before promotion) --
3460
+ # rebuild ParamInfo / return_type in place so overload
3461
+ # resolution sees the promoted param types.
3462
+ info = registry.get_record(record.name)
3463
+ if info is None:
3464
+ continue
3465
+ for overloads in info.methods.values():
3466
+ for i, finfo in enumerate(overloads):
3467
+ new_params = [
3468
+ dc_replace(p, type=promote_bare_nominals(p.type, registry))
3469
+ for p in finfo.params
3470
+ ]
3471
+ new_return = (
3472
+ promote_bare_nominals(finfo.return_type, registry)
3473
+ if finfo.return_type is not None else finfo.return_type
3474
+ )
3475
+ overloads[i] = dc_replace(
3476
+ finfo,
3477
+ params=new_params,
3478
+ return_type=new_return,
3479
+ )
3480
+ # Top-level functions: walk bodies so nested-def signatures
3481
+ # (resolved at parse time, before F.5 canonicalization) get
3482
+ # their bare NominalTypes promoted.
3483
+ for func in module.functions:
3484
+ _walk_body(func.body, None, resolver, promote_registry=registry)
3485
+
3486
+ def _resolve_builder_macro_chain(
3487
+ self, module_name: str, name: str,
3488
+ ) -> str | None:
3489
+ """Return the defining module of the builder macro reachable via
3490
+ the binding chain from `(module_name, name)`, or None if the
3491
+ chain doesn't end at a BUILDER_MACRO binding. Used by
3492
+ `_populate_macro_deps` to fold a re-exported builder macro's
3493
+ MACRO_DEPS into the consumer's macro_ns.
3494
+ """
3495
+ result = walk_attribute_chain(
3496
+ self.ctx.registry, module_name, name,
3497
+ is_kind(SymbolKind.BUILDER_MACRO))
3498
+ return result[0] if result is not None else None
3499
+
3500
+ def _populate_macro_deps(self, module: TpyModule) -> None:
3501
+ """Populate macro_ns with exports from MACRO_DEPS of used macro modules.
3502
+
3503
+ Collects dependency modules from all macro modules referenced by records
3504
+ in this module, then binds their public exports (records, functions,
3505
+ enums) into macro_ns -- a shadow namespace below global_ns so user
3506
+ bindings always take priority.
3507
+ """
3508
+ macro_reg = self.ctx.macro_registry
3509
+ if macro_reg is None:
3510
+ return
3511
+
3512
+ # Collect all macro dep modules from records that have macros.
3513
+ # Maps dep_module -> name_filter (None = all exports, list = specific names).
3514
+ dep_modules: dict[str, list[str] | None] = {}
3515
+
3516
+ def _merge(mod_name: str) -> None:
3517
+ for dep, names in macro_reg.get_deps(mod_name).items():
3518
+ if dep not in dep_modules:
3519
+ dep_modules[dep] = names
3520
+ elif dep_modules[dep] is None or names is None:
3521
+ dep_modules[dep] = None # None wins (all exports)
3522
+ else:
3523
+ dep_modules[dep] = list(set(dep_modules[dep]) | set(names))
3524
+
3525
+ # Class macros: triggered by records carrying pending_macros.
3526
+ for record in module.all_records():
3527
+ if not record.pending_macros:
3528
+ continue
3529
+ for qname, _kwargs in record.pending_macros:
3530
+ mod_name = qname.rsplit(".", 1)[0] if "." in qname else ""
3531
+ _merge(mod_name)
3532
+
3533
+ # Builder-trace macros: expanded later (during body analysis), so
3534
+ # there's no equivalent of pending_macros to inspect here. Use the
3535
+ # import set as a proxy -- if this module imports a module that
3536
+ # has any @builder_macro classes registered, eagerly bring its
3537
+ # deps into macro_ns so synthesized code can reference them.
3538
+ # Walks re-export chains via ModuleInfo.module_attributes so
3539
+ # `from utils import ArgumentParser` (where utils re-exports
3540
+ # from argparse) pulls argparse's deps into main's macro_ns.
3541
+ builder_macro_modules = macro_reg.builder_macro_modules()
3542
+ for imported, names in module.imports.items():
3543
+ if imported in builder_macro_modules:
3544
+ _merge(imported)
3545
+ continue
3546
+ if not isinstance(names, set):
3547
+ continue
3548
+ for original_name, _local in names:
3549
+ ult = self._resolve_builder_macro_chain(imported, original_name)
3550
+ if ult is not None and ult in builder_macro_modules:
3551
+ _merge(ult)
3552
+
3553
+ if not dep_modules:
3554
+ return
3555
+
3556
+ self.ctx.macro_dep_modules = set(dep_modules.keys())
3557
+
3558
+ # Bind exports from each dep module into macro_ns
3559
+ for dep_mod_name, name_filter in dep_modules.items():
3560
+ module_info = self.ctx.registry.get_module(dep_mod_name)
3561
+ if module_info is None:
3562
+ continue
3563
+
3564
+ # Bind the module name itself so synthesized code can use
3565
+ # the qualified form (e.g. ``sys.argv`` / ``tpy.copy``).
3566
+ # Module attribute access then resolves through ModuleInfo
3567
+ # without polluting the user's namespace with every export.
3568
+ if name_filter is None and dep_mod_name not in self.ctx.macro_ns:
3569
+ self.ctx.macro_ns.bind_module(dep_mod_name)
3570
+
3571
+ # Macro-dep symbols flow into the consumer's per-module
3572
+ # attribute table as imports so the re-export emit picks
3573
+ # them up (the macro-expanded code references these names,
3574
+ # so the consumer's `.hpp` needs to alias them in its
3575
+ # namespace just like any other `from M import X`).
3576
+ if module_info.records:
3577
+ for name, record_info in module_info.records.items():
3578
+ if name_filter is not None and name not in name_filter:
3579
+ continue
3580
+ if self.ctx.registry.get_record(name) is None:
3581
+ self.ctx.registry.register_record(record_info, name)
3582
+ self.ctx.macro_ns.bind_imported_name(name, dep_mod_name, name)
3583
+ self.ctx.imported_names.setdefault(name, (dep_mod_name, name))
3584
+ install_binding(
3585
+ self.ctx.module_attributes, name,
3586
+ SymbolKind.RECORD, record_info,
3587
+ defining_module=(record_info.defining_module or dep_mod_name),
3588
+ canonical_name=record_info.name,
3589
+ )
3590
+
3591
+ if module_info.functions:
3592
+ for name, func_infos in module_info.functions.items():
3593
+ if name_filter is not None and name not in name_filter:
3594
+ continue
3595
+ is_special = func_infos and func_infos[0].special_handling
3596
+ if not is_special:
3597
+ if self.ctx.registry.get_function(name) is None:
3598
+ self.ctx.registry.register_function_group(name, func_infos)
3599
+ self.ctx.macro_ns.bind_imported_name(name, dep_mod_name, name)
3600
+ self.ctx.imported_names.setdefault(name, (dep_mod_name, name))
3601
+ ult_mod = (func_infos[0].originating_module
3602
+ if func_infos else None) or dep_mod_name
3603
+ ult_name = func_infos[0].name if func_infos else name
3604
+ install_binding(
3605
+ self.ctx.module_attributes, name,
3606
+ SymbolKind.FUNCTION, func_infos,
3607
+ defining_module=ult_mod, canonical_name=ult_name,
3608
+ )
3609
+
3610
+ if module_info.enums:
3611
+ for name, enum_type in module_info.enums.items():
3612
+ if name_filter is not None and name not in name_filter:
3613
+ continue
3614
+ if self.ctx.registry.get_enum(name) is None:
3615
+ self.ctx.registry.register_enum(enum_type, name)
3616
+ self.ctx.macro_ns.bind_enum(enum_type, name=name)
3617
+ self.ctx.imported_names.setdefault(name, (dep_mod_name, name))
3618
+ einfo = enum_info_of(enum_type)
3619
+ ult_mod = (einfo.module_name if einfo and einfo.module_name
3620
+ else dep_mod_name)
3621
+ install_binding(
3622
+ self.ctx.module_attributes, name,
3623
+ SymbolKind.ENUM, enum_type,
3624
+ defining_module=ult_mod, canonical_name=enum_type.name,
3625
+ )