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/parse/parser.py ADDED
@@ -0,0 +1,4043 @@
1
+ """
2
+ TurboPython Parser.
3
+
4
+ Uses CPython's ast module to parse TurboPython source code.
5
+ Validates that only allowed constructs are used.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ import ast
10
+ import copy
11
+ import dataclasses
12
+ import re
13
+ import textwrap
14
+ from typing import Any, Literal, NoReturn, TYPE_CHECKING
15
+
16
+ from ..typesys import (
17
+ FieldInfo, NominalType, OptionalType, RecordInfo, TypeRegistry,
18
+ FunctionInfo, MethodSignature, ProtocolInfo, TypeParamKind, LiteralValue, LiteralTag,
19
+ bare_name,
20
+ )
21
+ from ..module_names import public_module_name
22
+ from ..type_def_registry import (
23
+ is_bool_type, is_str_type, type_def_of,
24
+ find_factory_by_simple_name, find_factory_in_module,
25
+ )
26
+ from .type_resolver import TypeResolver
27
+
28
+ if TYPE_CHECKING:
29
+ from ..typesys import TpyType
30
+ from .nodes import (
31
+ ParseError, SourceLocation, ParseWarning, RecordLinkage, FunctionLinkage,
32
+ TpyTypeRef, TpyUnionRef, TpyCallableRef, TpyLiteralRef, TpyInferFromDefaultRef,
33
+ ResolverInputNode, TypeRefNode,
34
+ TpyExpr, TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBytesLiteral,
35
+ TpyFStringValue, TpyFString, FSTRING_CONV_ASCII,
36
+ TpyBoolLiteral,
37
+ TpyNoneLiteral, TpyName, TpyBinOp, TpyChainedCompare, TpyUnaryOp, TpyTypeParamConstruct,
38
+ TpyStarUnpack, TpyCall, TpyMethodCall,
39
+ TpyFieldAccess, TpyArrayLiteral, TpyTupleLiteral, TpyDictLiteral, TpySetLiteral, TpyListRepeat,
40
+ TpyComprehensionGenerator, TpyListComprehension, TpyDictComprehension, TpySetComprehension, TpyGeneratorExpression,
41
+ TpySlice, TpySubscript, TpyCoerce,
42
+ TpyIfExpr, TpyNamedExpr, TpyAwait, TpyLambda,
43
+ TpyStmt, TpyVarDecl, TpyTupleUnpack, TpyAssign, TpyAugAssign, TpyDelItem, TpyDelVar, TpyDelAttr, TpyExprStmt, TpyReturn, TpyYield,
44
+ TpyAssert, TpyIf, TpyWhile, TpyForEach, TpyBreak, TpyContinue,
45
+ TpyPassStmt, TpyGlobal, TpyNonlocal, TpyRaise, TpyExceptHandler, TpyTry, TpyWithItem, TpyWith,
46
+ TpyNestedDef,
47
+ TpyPattern, TpyWildcardPattern, TpyCapturePattern, TpyClassPattern,
48
+ TpyLiteralPattern, TpyValuePattern, TpyOrPattern, TpyAsPattern,
49
+ TpyMatchCase, TpyMatch,
50
+ RelativeImportKey, TpyImport, TpyFunction, TpyRecord, TpyProtocol, TpyEnum, TpyModule,
51
+ ModuleDirectives,
52
+ )
53
+ from .imports import (
54
+ ImportProcessor, _PRIVATE_MODULE_PUBLIC_NAMES, _IMPLICIT_MODULES,
55
+ get_builtins_exports, get_typing_exports, get_tpy_exports, read_module_all,
56
+ NonLiteralAllError,
57
+ )
58
+ from .. import qnames
59
+
60
+ # Modules whose names the parser resolves structurally (base class detection,
61
+ # enum auto(), Unpack). A local def/class/assignment that shadows one of these
62
+ # names is warned about so the user knows the parser keyword is hidden.
63
+ # Only typing and enum -- tpy/builtins names are resolved via normal sema and
64
+ # shadowing them is routine (e.g. user-defined `copy` replacing tpy.copy).
65
+ _PARSER_KEYWORD_MODULES = frozenset({"typing", "enum"})
66
+
67
+ # Decorator names that mark stdlib stubs (builtin types, decorators, functions).
68
+ # Definitions with these decorators are excluded from _local_defs because they
69
+ # intentionally re-define imported names (e.g. @builtin_type class Protocol).
70
+ # Checked via raw AST name (_decorator_raw_name) since import resolution hasn't
71
+ # run yet during pre-scan.
72
+ _BUILTIN_DEC_NAMES = frozenset({"builtin_type", "builtin_decorator", "builtin_function"})
73
+
74
+ # Fixed-int constructor names recognized syntactically by the parser for
75
+ # default-value validation (_validate_const_default) and the C++ literal
76
+ # simplification in _get_default_value. The actual TpyType lookup happens
77
+ # in the TypeResolver via `_FIXED_INT_MAP`; here we only need name
78
+ # membership. Kept in sync with typesys.ALL_FIXED_INTS.
79
+ _FIXED_INT_NAMES: frozenset[str] = frozenset({
80
+ "Int8", "Int16", "Int32", "Int64",
81
+ "UInt8", "UInt16", "UInt32", "UInt64",
82
+ })
83
+
84
+ # Operator-to-string mappings for AST binary, comparison, and unary operators
85
+ _BINOP_TO_STR: dict[type, str] = {
86
+ ast.Add: "+", ast.Sub: "-", ast.Mult: "*",
87
+ ast.Div: "div", ast.Mod: "%", ast.FloorDiv: "//",
88
+ ast.BitAnd: "&", ast.BitOr: "|", ast.BitXor: "^",
89
+ ast.LShift: "<<", ast.RShift: ">>",
90
+ ast.Pow: "**",
91
+ }
92
+
93
+ _CMPOP_TO_STR: dict[type, str] = {
94
+ ast.Eq: "==", ast.NotEq: "!=",
95
+ ast.Lt: "<", ast.LtE: "<=",
96
+ ast.Gt: ">", ast.GtE: ">=",
97
+ ast.In: "in", ast.NotIn: "not in",
98
+ ast.Is: "is", ast.IsNot: "is not",
99
+ }
100
+
101
+ _UNARYOP_TO_STR: dict[type, str] = {
102
+ ast.UAdd: "+", ast.USub: "-", ast.Not: "!", ast.Invert: "~",
103
+ }
104
+
105
+
106
+ def _validate_fstring_format_spec(spec: str) -> str | None:
107
+ """Validate an f-string format spec against C++ std::format support.
108
+
109
+ Returns an error message for Python-only features, or None if valid.
110
+ Python features not supported by std::format: '=' alignment, 'z' option,
111
+ ',' and '_' grouping, 'n' and '%' type codes.
112
+ """
113
+ if not spec:
114
+ return None
115
+
116
+ pos = 0
117
+ n = len(spec)
118
+
119
+ # [[fill]align] -- fill can be ANY character if followed by an align char
120
+ _ALIGN = '<>^='
121
+ if n >= 2 and spec[1] in _ALIGN:
122
+ if spec[1] == '=':
123
+ return "'=' alignment is not supported"
124
+ pos = 2
125
+ elif spec[0] in _ALIGN:
126
+ if spec[0] == '=':
127
+ return "'=' alignment is not supported"
128
+ pos = 1
129
+
130
+ # [sign]
131
+ if pos < n and spec[pos] in '+- ':
132
+ pos += 1
133
+
134
+ # [z]
135
+ if pos < n and spec[pos] == 'z':
136
+ return "'z' option is not supported"
137
+
138
+ # [#]
139
+ if pos < n and spec[pos] == '#':
140
+ pos += 1
141
+
142
+ # [0]
143
+ if pos < n and spec[pos] == '0':
144
+ pos += 1
145
+
146
+ # [width]
147
+ while pos < n and spec[pos].isdigit():
148
+ pos += 1
149
+
150
+ # [grouping_option]
151
+ if pos < n and spec[pos] in ',_':
152
+ return f"'{spec[pos]}' grouping is not supported"
153
+
154
+ # [.precision]
155
+ if pos < n and spec[pos] == '.':
156
+ pos += 1
157
+ while pos < n and spec[pos].isdigit():
158
+ pos += 1
159
+
160
+ # [type]
161
+ if pos < n:
162
+ t = spec[pos]
163
+ if t == 'n':
164
+ return "'n' (locale-aware) type is not supported"
165
+ if t == '%':
166
+ return "'%' (percentage) type is not supported"
167
+
168
+ return None
169
+
170
+
171
+ def _extract_subscript_slices(node: ast.Subscript) -> list[ast.expr]:
172
+ """Extract individual type argument nodes from a subscript slice.
173
+
174
+ Handles both single-arg (X[T]) and multi-arg (X[T, U]) forms.
175
+ """
176
+ if isinstance(node.slice, ast.Tuple):
177
+ return node.slice.elts
178
+ return [node.slice]
179
+
180
+
181
+ def _attr_chain_to_dotted(node: ast.expr) -> str | None:
182
+ """Flatten ast.Name / ast.Attribute chains into a dotted string.
183
+
184
+ Returns None for any other expression shape (call result, subscript, etc.).
185
+ """
186
+ if isinstance(node, ast.Name):
187
+ return node.id
188
+ if isinstance(node, ast.Attribute):
189
+ parts: list[str] = []
190
+ cur: ast.expr = node
191
+ while isinstance(cur, ast.Attribute):
192
+ parts.append(cur.attr)
193
+ cur = cur.value
194
+ if not isinstance(cur, ast.Name):
195
+ return None
196
+ parts.append(cur.id)
197
+ parts.reverse()
198
+ return ".".join(parts)
199
+ return None
200
+
201
+
202
+ # Requires whitespace after `tpy:` to avoid matching C++ namespace comments (# tpy::Foo)
203
+ _DIRECTIVE_LINE_RE = re.compile(r'^#\s*tpy:\s+(\w.+)$')
204
+
205
+ # Schema: (positional arg types, allowed keyword arg types)
206
+ # Keys are the known directive names; unknown names produce a warning.
207
+ _DIRECTIVE_SPECS: dict[str, tuple[list[type], dict[str, type]]] = {
208
+ "native_module": ([], {}),
209
+ "macro_module": ([], {}),
210
+ "include": ([str], {"platform": str}),
211
+ # link: raw `-lfoo` by default; `managed=True` routes through the
212
+ # third-party registry (tpyc/build/third_party.py) for bundled/system/
213
+ # auto resolution via the `--<lib>=<mode>` CLI flag.
214
+ "link": ([str], {"platform": str, "managed": bool}),
215
+ "cpp_namespace": ([str], {}),
216
+ }
217
+
218
+ _CPP_NAMESPACE_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$')
219
+
220
+
221
+ def _parse_directive_call(content: str) -> tuple[str, list, dict] | None:
222
+ """Parse directive content as a bare name or Python-style call.
223
+
224
+ Returns (name, positional_args, keyword_args), or None on parse error.
225
+ """
226
+ content = content.strip()
227
+ if re.match(r'^\w+$', content):
228
+ return (content, [], {})
229
+ try:
230
+ tree = ast.parse(content, mode='eval')
231
+ expr = tree.body
232
+ if isinstance(expr, ast.Call) and isinstance(expr.func, ast.Name):
233
+ name = expr.func.id
234
+ pos_args = [ast.literal_eval(a) for a in expr.args]
235
+ kw_args = {kw.arg: ast.literal_eval(kw.value)
236
+ for kw in expr.keywords if kw.arg is not None}
237
+ return (name, pos_args, kw_args)
238
+ except (SyntaxError, ValueError):
239
+ pass
240
+ return None
241
+
242
+
243
+ def _check_directive_args(
244
+ name: str, args: list, kwargs: dict,
245
+ spec: tuple[list[type], dict[str, type]],
246
+ loc: SourceLocation, warnings: list[ParseWarning],
247
+ ) -> bool:
248
+ """Validate args/kwargs against a directive spec. Returns True if valid."""
249
+ pos_types, kw_types = spec
250
+ if len(args) != len(pos_types):
251
+ warnings.append(ParseWarning(
252
+ f"'{name}' expects {len(pos_types)} positional argument(s), got {len(args)}", loc))
253
+ return False
254
+ for i, (val, typ) in enumerate(zip(args, pos_types)):
255
+ if not isinstance(val, typ):
256
+ warnings.append(ParseWarning(
257
+ f"'{name}' argument {i + 1} must be a {typ.__name__}", loc))
258
+ return False
259
+ unknown = {k for k in kwargs if k not in kw_types}
260
+ if unknown:
261
+ warnings.append(ParseWarning(
262
+ f"'{name}' unknown keyword arguments: {sorted(unknown)}", loc))
263
+ return False
264
+ for k, val in kwargs.items():
265
+ if not isinstance(val, kw_types[k]):
266
+ warnings.append(ParseWarning(
267
+ f"'{name}' keyword '{k}' must be a {kw_types[k].__name__}", loc))
268
+ return False
269
+ return True
270
+
271
+
272
+ def _scan_directives(source_lines: list[str]) -> tuple[ModuleDirectives, list[ParseWarning]]:
273
+ """Scan all standalone # tpy: comment lines and return parsed directives."""
274
+ includes: list[tuple[str, str | None]] = []
275
+ link_libs: list[tuple[str, str | None]] = []
276
+ third_party_deps: list[tuple[str, str | None]] = []
277
+ native_module = False
278
+ cpp_namespace: str | None = None
279
+ warnings: list[ParseWarning] = []
280
+
281
+ preamble_ended = False
282
+ for lineno, line in enumerate(source_lines, start=1):
283
+ stripped = line.strip()
284
+ if stripped and not stripped.startswith('#'):
285
+ preamble_ended = True
286
+ if not stripped.startswith('#'):
287
+ continue
288
+ m = _DIRECTIVE_LINE_RE.match(stripped)
289
+ if not m:
290
+ continue
291
+ content = m.group(1).strip()
292
+ loc = SourceLocation(line=lineno)
293
+ if preamble_ended:
294
+ warnings.append(ParseWarning(
295
+ "# tpy: directives must appear before any code", loc))
296
+ continue
297
+
298
+ parsed = _parse_directive_call(content)
299
+ if parsed is None:
300
+ warnings.append(ParseWarning(f"invalid # tpy: directive syntax: {content!r}", loc))
301
+ continue
302
+
303
+ name, args, kwargs = parsed
304
+ spec = _DIRECTIVE_SPECS.get(name)
305
+ if spec is None:
306
+ warnings.append(ParseWarning(f"unknown # tpy: directive: {name!r}", loc))
307
+ continue
308
+ if not _check_directive_args(name, args, kwargs, spec, loc, warnings):
309
+ continue
310
+
311
+ if name == "native_module":
312
+ native_module = True
313
+ elif name == "include":
314
+ includes.append((args[0], kwargs.get("platform")))
315
+ elif name == "link":
316
+ # managed=True -> registry-resolved third-party dep.
317
+ # managed=False (or omitted) -> raw -lfoo linker flag.
318
+ if kwargs.get("managed", False):
319
+ third_party_deps.append((args[0], kwargs.get("platform")))
320
+ else:
321
+ link_libs.append((args[0], kwargs.get("platform")))
322
+ elif name == "cpp_namespace":
323
+ ns_value = args[0]
324
+ if not _CPP_NAMESPACE_RE.match(ns_value):
325
+ warnings.append(ParseWarning(
326
+ f"invalid namespace: {ns_value!r} (must be valid C++ namespace like 'foo::bar')", loc))
327
+ continue
328
+ if cpp_namespace is not None:
329
+ warnings.append(ParseWarning(
330
+ f"duplicate 'cpp_namespace' directive (previous: {cpp_namespace!r})", loc))
331
+ cpp_namespace = ns_value
332
+
333
+ return ModuleDirectives(
334
+ includes=includes, link_libs=link_libs,
335
+ third_party_deps=third_party_deps,
336
+ native_module=native_module,
337
+ cpp_namespace=cpp_namespace,
338
+ ), warnings
339
+
340
+
341
+ def _collect_bitor_arms(node: ast.BinOp) -> list[ast.expr]:
342
+ """Flatten a left-recursive chain of A | B | C into [A, B, C]."""
343
+ arms: list[ast.expr] = []
344
+ if isinstance(node.left, ast.BinOp) and isinstance(node.left.op, ast.BitOr):
345
+ arms.extend(_collect_bitor_arms(node.left))
346
+ else:
347
+ arms.append(node.left)
348
+ arms.append(node.right)
349
+ return arms
350
+
351
+
352
+ class _NameArg:
353
+ """Decorator argument that is a name reference (e.g. StopIteration in @error_return(StopIteration))."""
354
+ __slots__ = ("name",)
355
+
356
+ def __init__(self, name: str) -> None:
357
+ self.name = name
358
+
359
+
360
+ @dataclasses.dataclass(frozen=True)
361
+ class _DecoratorArgSchema:
362
+ """Schema for a decorator's positional and keyword arguments.
363
+
364
+ pos_type: expected type for the positional arg (str, bool, _NameArg), or None = bare only
365
+ pos_required: whether the positional arg must be provided
366
+ pos_description: human-readable type description for error messages (e.g. "bool")
367
+ kwargs: allowed keyword arg names -> expected types (None = no kwargs)
368
+ """
369
+ pos_type: type | None = None
370
+ pos_required: bool = False
371
+ pos_description: str | None = None
372
+ kwargs: dict[str, type] | None = None
373
+
374
+
375
+ # Argument schemas for all known decorators. Decorators not listed here
376
+ # (macro decorators, etc.) are validated by their own paths.
377
+ _DECORATOR_ARG_SCHEMAS: dict[str, _DecoratorArgSchema] = {
378
+ # Only decorators without @builtin_decorator stubs need explicit schemas.
379
+ # All other schemas are derived from stub signatures in .py files
380
+ # (see Parser._schema_from_stub and Parser._decorator_schemas).
381
+ qnames.STATICMETHOD: _DecoratorArgSchema(), # Python builtin, no stub
382
+ }
383
+
384
+
385
+ def _stmt_child_bodies(stmt: TpyStmt) -> list[list[TpyStmt]]:
386
+ """Return the child statement bodies of a compound statement (no nested defs)."""
387
+ if isinstance(stmt, TpyIf):
388
+ return [stmt.then_body, stmt.else_body]
389
+ elif isinstance(stmt, TpyWhile):
390
+ bodies = [stmt.body]
391
+ if stmt.orelse:
392
+ bodies.append(stmt.orelse)
393
+ return bodies
394
+ elif isinstance(stmt, TpyForEach):
395
+ bodies = [stmt.body]
396
+ if stmt.orelse:
397
+ bodies.append(stmt.orelse)
398
+ return bodies
399
+ elif isinstance(stmt, TpyWith):
400
+ return [stmt.body]
401
+ elif isinstance(stmt, TpyTry):
402
+ bodies = [stmt.try_body, stmt.else_body, stmt.finally_body]
403
+ for h in stmt.handlers:
404
+ bodies.append(h.body)
405
+ return bodies
406
+ elif isinstance(stmt, TpyMatch):
407
+ return [case.body for case in stmt.cases]
408
+ return []
409
+
410
+
411
+ def _body_contains_yield(stmts: list[TpyStmt]) -> bool:
412
+ """Check if a function body contains any yield statements (non-recursive into nested defs)."""
413
+ for stmt in stmts:
414
+ if isinstance(stmt, TpyYield):
415
+ return True
416
+ for child_body in _stmt_child_bodies(stmt):
417
+ if _body_contains_yield(child_body):
418
+ return True
419
+ return False
420
+
421
+
422
+ def _check_no_return_value_in_generator(
423
+ stmts: list[TpyStmt], func_name: str,
424
+ ) -> None:
425
+ """Reject 'return value' inside a generator function body."""
426
+ for stmt in stmts:
427
+ if isinstance(stmt, TpyReturn) and stmt.value is not None:
428
+ # Create a minimal object with lineno for ParseError
429
+ err = ParseError(
430
+ f"Generator function '{func_name}' cannot use 'return' with a value")
431
+ if stmt.loc:
432
+ err.lineno = stmt.loc.line
433
+ raise err
434
+ for child_body in _stmt_child_bodies(stmt):
435
+ _check_no_return_value_in_generator(child_body, func_name)
436
+
437
+
438
+ class Parser:
439
+ """Parser for TurboPython source code."""
440
+
441
+ FORBIDDEN_CONSTRUCTS = {
442
+ "with", "async", "await",
443
+ }
444
+
445
+ def __init__(self, decorator_schemas: dict[str, '_DecoratorArgSchema'] | None = None):
446
+ self.registry = TypeRegistry()
447
+ self.source_lines: list[str] = []
448
+ self._type_param_scope: dict[str, TypeParamKind] | None = None
449
+ self._warnings: list[ParseWarning] = []
450
+ self._imports = ImportProcessor(self._warn)
451
+ self._module_aliases: dict[str, str] = {}
452
+ self._bare_module_imports: set[str] = set()
453
+ self._reverse_module_aliases: dict[str, str] = {}
454
+ self._for_unpack_counter: int = 0
455
+ self._multi_assign_counter: int = 0
456
+ # Schemas derived from @builtin_decorator stubs (populated by compiler
457
+ # from previously-parsed modules, or from same-file definitions)
458
+ self._decorator_schemas: dict[str, _DecoratorArgSchema] = dict(decorator_schemas) if decorator_schemas else {}
459
+ # Top-level class names pre-scanned from the module body, used to
460
+ # resolve forward references in type annotations.
461
+ self._module_class_names: frozenset[str] = frozenset()
462
+ # Top-level type alias names pre-scanned, for forward references
463
+ # (e.g. Box[Expr] in a record field before `type Expr = ...` is parsed)
464
+ self._module_type_alias_names: frozenset[str] = frozenset()
465
+ # Union aliases detected as recursive (self- or mutually-referencing)
466
+ self._recursive_union_names: set[str] = set()
467
+ # All module-level definitions (def, class, assignment) pre-scanned
468
+ # to detect when local names shadow imports for parser keyword resolution.
469
+ self._local_defs: frozenset[str] = frozenset()
470
+ # Maps short nested type names to dotted names while inside a class body.
471
+ # E.g., while parsing class Message: class Kind(Enum): ..., maps "Kind" -> "Message.Kind"
472
+ self._nested_type_scope: dict[str, str] = {}
473
+ # Type-ref resolver: holds a back-reference to this parser so it
474
+ # reads live state (registry, imports, local_defs, ...) each call.
475
+ # Attached to TpyModule.resolver at end of parse(); sema delegates
476
+ # here.
477
+ self._resolver = TypeResolver(self)
478
+ # Module # tpy: directives, populated by parse() before
479
+ # _parse_module so class/protocol/enum registration can see
480
+ # cpp_namespace at registration time.
481
+ self._directives = ModuleDirectives()
482
+ # True when the module is compiled as an entry point. Overrides
483
+ # the module name used by `_public_module()` to `"__main__"`.
484
+ # Set by parse() from its `is_entry_point` argument.
485
+ self._is_entry_point: bool = False
486
+
487
+ def _loc(self, node: ast.AST) -> SourceLocation | None:
488
+ """Create a SourceLocation from an AST node."""
489
+ if hasattr(node, 'lineno'):
490
+ col = getattr(node, 'col_offset', 0)
491
+ return SourceLocation(line=node.lineno, column=col)
492
+ return None
493
+
494
+ def _warn(self, message: str, node: ast.AST | None = None) -> None:
495
+ """Record a parser warning."""
496
+ loc = self._loc(node) if node else None
497
+ self._warnings.append(ParseWarning(message, loc))
498
+
499
+ def _warn_at_loc(self, message: str, loc: SourceLocation | None) -> None:
500
+ """Record a parser warning at an already-resolved source location.
501
+
502
+ Used by callers (e.g. `type_resolver.py`) that hold a `SourceLocation`
503
+ directly rather than an `ast.AST` node.
504
+ """
505
+ self._warnings.append(ParseWarning(message, loc))
506
+
507
+ @staticmethod
508
+ def _qualify(resolved: tuple[str, str]) -> str:
509
+ """Join a (module, name) resolution to a qualified name string."""
510
+ return f"{resolved[0]}.{resolved[1]}"
511
+
512
+ def _public_module(self) -> str:
513
+ """Return the public module name for records/protocols/enums
514
+ registered from this module, collapsing private submodules via
515
+ `public_module_name`. Used to populate `RecordInfo.module`,
516
+ `ProtocolInfo.module`, and enum placeholder qnames.
517
+
518
+ Entry-point modules use `"__main__"` regardless of their on-disk
519
+ file name, matching sema's convention (`analyzer.analyze` sets
520
+ `ctx.module_name = "__main__"` for the entry point) and Python's
521
+ runtime `__name__` semantics. Parser and sema must agree on the
522
+ qname so the resolver mints authoritative `_module_qname` values
523
+ on the first pass.
524
+
525
+ Fallback: when the parser is invoked without a `module_name`
526
+ (e.g. the `test_parse_type_ref.py` unit harness parses snippets
527
+ without a module context), return `"__main__"` so qname
528
+ construction stays well-formed. Compiler-driven parses always
529
+ set `module_name` + `is_entry_point`, so this branch is only
530
+ reached from fragment-level tests."""
531
+ if self._is_entry_point:
532
+ return "__main__"
533
+ return public_module_name(
534
+ self._imports._module_name,
535
+ self._directives.cpp_namespace,
536
+ ) or "__main__"
537
+
538
+ def _resolve_type_name(self, local_name: str) -> tuple[str, str] | None:
539
+ """Resolve annotation name -> (module, original_name) or None.
540
+
541
+ Checks explicit imports, then Python builtins, then local @builtin_type
542
+ definitions. Returns None when the name is shadowed by a module-level
543
+ def/class/assignment (excluding @builtin_type/decorator/function stubs).
544
+ """
545
+ if local_name in self._local_defs:
546
+ return None
547
+ source = self._imports.get_import_source(local_name)
548
+ if source:
549
+ return source
550
+
551
+ if local_name in get_builtins_exports():
552
+ return ("builtins", local_name)
553
+
554
+ # Check for @builtin_type / @builtin_decorator defined locally in this file
555
+ builtin_key = self.registry.get_builtin_type_key(local_name) or self.registry.get_builtin_decorator_key(local_name)
556
+ if builtin_key:
557
+ parts = builtin_key.rsplit(".", 1)
558
+ if len(parts) == 2:
559
+ return (parts[0], parts[1])
560
+
561
+ # Auto-resolve builtin_decorator within tpy.extern's own module
562
+ if local_name == "builtin_decorator" and self._imports._module_name:
563
+ pub = _PRIVATE_MODULE_PUBLIC_NAMES.get(self._imports._module_name) or public_module_name(self._imports._module_name)
564
+ if pub == "tpy.extern":
565
+ return ("tpy.extern", local_name)
566
+
567
+ return None
568
+
569
+ def _resolve_parser_keyword(self, node: ast.expr) -> tuple[str, str] | None:
570
+ """Resolve a Name or Attribute node through import resolution.
571
+
572
+ Convenience wrapper that dispatches to _resolve_type_name (for bare
573
+ names) or _resolve_qualified_type_name (for module.attr). Both
574
+ underlying methods already respect _local_defs shadowing.
575
+ """
576
+ if isinstance(node, ast.Name):
577
+ return self._resolve_type_name(node.id)
578
+ elif isinstance(node, ast.Attribute):
579
+ return self._resolve_qualified_type_name(node)
580
+ return None
581
+
582
+ def _resolve_qualified_type_name(self, node: ast.Attribute) -> tuple[str, str] | None:
583
+ """Resolve module.Name -> (module, name) or None.
584
+
585
+ Only resolves if the module was bare-imported (import X or import X as Y).
586
+ 'from X import ...' does NOT put the module name in scope.
587
+ Returns None if the module prefix is shadowed by a local definition.
588
+ """
589
+ if not isinstance(node.value, ast.Name):
590
+ return None
591
+ local_module = node.value.id
592
+ if local_module in self._local_defs:
593
+ return None
594
+ canonical = self._reverse_module_aliases.get(local_module, local_module)
595
+ # Verify the module was bare-imported (imports[canonical] is None means
596
+ # whole-module import; the key being absent means no import at all).
597
+ # For parser-keyword modules, None marks whole-module import.
598
+ # For user modules, they're in bare_module_imports (checked via imports dict).
599
+ imports = self._imports.imports
600
+ if imports is None or canonical not in imports:
601
+ return None
602
+ entry = imports[canonical]
603
+ # None means whole-module import (parser-keyword modules).
604
+ # For user modules, bare import sets an empty set AND adds to bare_module_imports.
605
+ if entry is not None and not (isinstance(entry, set) and canonical in self._bare_module_imports):
606
+ return None
607
+ return (canonical, node.attr)
608
+
609
+ def _resolve_dotted_class_name(self, node: ast.Attribute) -> str | None:
610
+ """Resolve Outer.Inner (or Outer.Mid.Inner) to a dotted name string.
611
+
612
+ Returns the dotted name if the root is a known class name, else None.
613
+ """
614
+ parts: list[str] = []
615
+ cur: ast.expr = node
616
+ while isinstance(cur, ast.Attribute):
617
+ parts.append(cur.attr)
618
+ cur = cur.value
619
+ if isinstance(cur, ast.Name) and cur.id in self._module_class_names:
620
+ parts.append(cur.id)
621
+ parts.reverse()
622
+ return ".".join(parts)
623
+ return None
624
+
625
+ def _raise_unresolved_import_error(
626
+ self, raw_name: str, node: ast.expr | None = None,
627
+ *, loc: SourceLocation | None = None,
628
+ ) -> None:
629
+ """Raise a helpful error for unresolved type names with import hints."""
630
+ if raw_name in get_typing_exports():
631
+ raise ParseError(
632
+ f"'{raw_name}' requires: from typing import {raw_name}", node, loc=loc,
633
+ )
634
+ # In stdlib _core modules, unresolved names may be forward references
635
+ # to types defined later in the same file. Let them through.
636
+ mod = self._imports._module_name
637
+ if mod and "._" in mod and raw_name in self._module_class_names:
638
+ if public_module_name(mod) in _IMPLICIT_MODULES:
639
+ return
640
+ if raw_name in get_tpy_exports():
641
+ raise ParseError(
642
+ f"'{raw_name}' requires: from tpy import {raw_name}", node, loc=loc,
643
+ )
644
+
645
+ def _is_ignorable_for_import_order(self, node: ast.stmt) -> bool:
646
+ """Check if a statement should be ignored for import ordering.
647
+
648
+ Module docstrings and pass statements don't count as "code"
649
+ for the purpose of detecting late imports.
650
+ """
651
+ if isinstance(node, ast.Pass):
652
+ return True
653
+ # Module docstring (expression statement with a string literal)
654
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
655
+ return True
656
+ return False
657
+
658
+ def parse(self, source: str, module_name: str | None = None,
659
+ is_package_init: bool = False,
660
+ is_entry_point: bool = False) -> TpyModule:
661
+ """Parse TurboPython source code into a TpyModule.
662
+
663
+ `is_entry_point` mirrors the compiler's convention of treating
664
+ the entry-point module as `__main__` for qname purposes
665
+ (matching sema's `ctx.module_name = "__main__"` rename and
666
+ Python's runtime `__name__` convention). It only affects the
667
+ value returned by `_public_module()` and hence the qnames
668
+ attached to record / protocol / enum-placeholder registrations;
669
+ import resolution still uses the original `module_name`.
670
+ """
671
+ self.source_lines = source.splitlines()
672
+ self._warnings = []
673
+ self._is_entry_point = is_entry_point
674
+ self._imports = ImportProcessor(self._warn, module_name=module_name,
675
+ is_package_init=is_package_init)
676
+ self._module_aliases = {}
677
+ self._bare_module_imports = set()
678
+ self._reverse_module_aliases = {}
679
+ tree = ast.parse(source)
680
+ # Scan directives first so cpp_namespace is available while
681
+ # _parse_module registers records/protocols/enums.
682
+ directives, directive_warnings = _scan_directives(self.source_lines)
683
+ self._directives = directives
684
+ try:
685
+ module_all_result = read_module_all(tree)
686
+ except NonLiteralAllError as exc:
687
+ raise ParseError(
688
+ "__all__ is not a compile-time literal (must be a "
689
+ "list / tuple / set of string literals)",
690
+ exc.node)
691
+ module = self._parse_module(tree)
692
+ if module_all_result is not None:
693
+ module.module_all, module.module_all_loc = module_all_result
694
+ module.directives = directives
695
+ module.parse_warnings.extend(directive_warnings)
696
+ # Attach the ref resolver so sema can resolve TypeRefNodes emitted
697
+ # at annotation sites. The TypeResolver instance holds a back-ref
698
+ # to this parser and reads parser state each call, so sema sees
699
+ # live containers (registry, imports, local_defs, ...).
700
+ module.resolver = self._resolver
701
+ return module
702
+
703
+ # Names that _parse_type_annotation resolves directly (not through registry)
704
+ _BUILTIN_TYPE_NAMES = frozenset({
705
+ "int", "float", "bool", "str", "None", "tuple",
706
+ })
707
+
708
+ def _is_type_name(self, name: str) -> bool:
709
+ """Check if a name is recognizable as a type by _parse_type_annotation."""
710
+ resolved = self._resolve_type_name(name)
711
+ if resolved:
712
+ original = resolved[1]
713
+ if original in self._BUILTIN_TYPE_NAMES or original in get_tpy_exports():
714
+ return True
715
+ if original == "Self":
716
+ return True
717
+ # Also check registry directly for user-defined types
718
+ return self.registry.is_known_type(name)
719
+
720
+ def _could_be_type(self, name: str) -> bool:
721
+ """Check if a name could plausibly refer to a type.
722
+
723
+ Checks the local class pre-scan, the parser registry, and import
724
+ resolution. Used at call-parsing sites where the parser needs to
725
+ decide whether Name[args](...) could be a type instantiation.
726
+ """
727
+ return (name in self._module_class_names
728
+ or name in self._module_type_alias_names
729
+ or self.registry.is_known_type(name)
730
+ or self._resolve_type_name(name) is not None)
731
+
732
+ def _is_type_alias_assign(self, node: ast.Assign) -> bool:
733
+ """Check if an assignment is an old-style type alias (e.g., Shape = Circle | Rect).
734
+
735
+ Triggers when ALL arms are Names/None AND at least one arm is a
736
+ confirmed type (registered or builtin). Pure forward-ref aliases
737
+ (all arms are class names defined in this module) also match.
738
+ """
739
+ if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name):
740
+ return False
741
+ if not (isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.BitOr)):
742
+ return False
743
+ arms = _collect_bitor_arms(node.value)
744
+ has_confirmed_type = False
745
+ all_module_classes = True
746
+ for arm in arms:
747
+ if isinstance(arm, ast.Constant) and arm.value is None:
748
+ has_confirmed_type = True
749
+ continue
750
+ if not isinstance(arm, ast.Name):
751
+ return False
752
+ if self._is_type_name(arm.id):
753
+ has_confirmed_type = True
754
+ elif arm.id not in self._module_class_names and arm.id not in self._module_type_alias_names:
755
+ all_module_classes = False
756
+ return has_confirmed_type or all_module_classes
757
+
758
+ def _parse_alias_type_params(
759
+ self, node: ast.TypeAlias,
760
+ ) -> 'tuple[list[str], list[TypeParamKind]]':
761
+ """Parse PEP 695 type parameters on a `type X[...] = ...` alias.
762
+
763
+ v1 of generic recursive type aliases (see
764
+ `docs/GENERIC_RECURSIVE_ALIASES_DESIGN.md`) accepts only bare
765
+ `ast.TypeVar` entries. Bounds (`type Tree[T: Hashable] = ...`),
766
+ defaults (PEP 696), `TypeVarTuple`, and `ParamSpec` are rejected
767
+ outright -- silent stripping would diverge from CPython
768
+ semantics.
769
+ """
770
+ if not (hasattr(node, 'type_params') and node.type_params):
771
+ return [], []
772
+ type_params: list[str] = []
773
+ type_param_kinds: list[TypeParamKind] = []
774
+ for tp in node.type_params:
775
+ if not isinstance(tp, ast.TypeVar):
776
+ raise ParseError(
777
+ f"Type alias '{node.name.id}': only simple type "
778
+ f"parameters are supported in v1; got "
779
+ f"{type(tp).__name__}",
780
+ node,
781
+ )
782
+ if tp.bound is not None:
783
+ raise ParseError(
784
+ f"Type alias '{node.name.id}': bounds on type "
785
+ f"parameters (`{tp.name}: ...`) are not yet "
786
+ f"supported. Remove the bound or open an issue.",
787
+ node,
788
+ )
789
+ if getattr(tp, 'default_value', None) is not None:
790
+ raise ParseError(
791
+ f"Type alias '{node.name.id}': default values on "
792
+ f"type parameters (`{tp.name} = ...`) are not yet "
793
+ f"supported.",
794
+ node,
795
+ )
796
+ if tp.name in type_params:
797
+ raise ParseError(
798
+ f"Type alias '{node.name.id}': duplicate type "
799
+ f"parameter name '{tp.name}'.",
800
+ node,
801
+ )
802
+ type_params.append(tp.name)
803
+ type_param_kinds.append(TypeParamKind.TYPE)
804
+ return type_params, type_param_kinds
805
+
806
+ def _register_type_alias(
807
+ self, name: str, type_node: ast.expr,
808
+ type_aliases: 'dict[str, tuple[TpyType | TypeRefNode, SourceLocation | None, list[str], list[TypeParamKind]]]',
809
+ type_params: 'list[str] | None' = None,
810
+ type_param_kinds: 'list[TypeParamKind] | None' = None,
811
+ ) -> None:
812
+ """Parse a type alias RHS as a TypeRefNode.
813
+
814
+ `type_params` / `type_param_kinds` describe an alias's generic
815
+ parameters (`type Tree[T] = ...`); both empty for non-generic
816
+ aliases. When present, the parser temporarily binds them into
817
+ `_type_param_scope` so `T` references in the body resolve as
818
+ `TypeParamRef` instead of failing with "Unknown type: T". Sema
819
+ rejects further use until generic-alias substitution lands; see
820
+ `docs/GENERIC_RECURSIVE_ALIASES_DESIGN.md`.
821
+
822
+ The post-parse `resolve_refs` pass resolves the ref with a
823
+ `pending_alias` kwarg so same-body self-references produce a
824
+ `NominalType(name)` placeholder. Sema then detects recursive
825
+ unions and registers the resolved alias in parser.registry so
826
+ later alias bodies can reference it.
827
+ """
828
+ type_params = list(type_params or [])
829
+ type_param_kinds = list(type_param_kinds or [])
830
+ loc = SourceLocation(line=type_node.lineno) if hasattr(type_node, 'lineno') else None
831
+ if type_params:
832
+ # `_parse_type_ref` defaults its scope arg to `self._type_param_scope`,
833
+ # so setting the attribute is enough -- no explicit pass needed.
834
+ old_scope = self._type_param_scope
835
+ self._type_param_scope = dict(zip(type_params, type_param_kinds))
836
+ try:
837
+ ref = self._parse_type_ref(type_node)
838
+ finally:
839
+ self._type_param_scope = old_scope
840
+ else:
841
+ ref = self._parse_type_ref(type_node)
842
+ type_aliases[name] = (ref, loc, type_params, type_param_kinds)
843
+
844
+ def _parse_module(self, tree: ast.Module) -> TpyModule:
845
+ """Parse a module."""
846
+ self._module_class_names = frozenset(
847
+ node.name for node in tree.body if isinstance(node, ast.ClassDef)
848
+ )
849
+ self._module_type_alias_names = frozenset(
850
+ node.name.id for node in tree.body if isinstance(node, ast.TypeAlias)
851
+ )
852
+ # Pre-scan all module-level definitions (def, class, assignment) so that
853
+ # _resolve_type_name returns None for shadowed imports. This prevents
854
+ # parser keywords (Enum, Protocol, auto, decorators, type names) from
855
+ # being misresolved when shadowed by a local name.
856
+ local_defs: set[str] = set()
857
+ for node in tree.body:
858
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
859
+ if not any(self._decorator_raw_name(d) in _BUILTIN_DEC_NAMES
860
+ for d in node.decorator_list):
861
+ local_defs.add(node.name)
862
+ elif isinstance(node, ast.Assign):
863
+ for target in node.targets:
864
+ if isinstance(target, ast.Name):
865
+ local_defs.add(target.id)
866
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
867
+ local_defs.add(node.target.id)
868
+ self._local_defs = frozenset(local_defs)
869
+ records = []
870
+ functions = []
871
+ protocols = []
872
+ enums = []
873
+ top_level_stmts = []
874
+ type_aliases: 'dict[str, tuple[TpyType | TypeRefNode, SourceLocation | None, list[str], list[TypeParamKind]]]' = {}
875
+ imports: dict[str, set[tuple[str, str]] | None | str] = {}
876
+ user_module_imports: dict[str, int] = {}
877
+ module_aliases: dict[str, str] = {}
878
+ bare_module_imports: set[str] = set()
879
+ self._module_aliases = module_aliases
880
+ self._bare_module_imports = bare_module_imports
881
+ # Set imports reference early so _resolve_qualified_type_name can check it
882
+ self._imports.imports = imports
883
+ seen_non_import = False
884
+
885
+ for node in tree.body:
886
+ is_import = isinstance(node, (ast.Import, ast.ImportFrom))
887
+
888
+ # Check for late imports (imports after non-import code)
889
+ if is_import and seen_non_import:
890
+ self._warn("Import statement should be at the top of the file", node)
891
+
892
+ if isinstance(node, ast.ImportFrom):
893
+ self._imports.process_import_from(node, imports, user_module_imports, top_level_stmts, module_aliases)
894
+ # Rebuild reverse alias mapping after each import
895
+ self._reverse_module_aliases = {v: k for k, v in module_aliases.items()}
896
+ elif isinstance(node, ast.Import):
897
+ self._imports.process_import(node, imports, user_module_imports, top_level_stmts, module_aliases, bare_module_imports)
898
+ # Rebuild reverse alias mapping after each import
899
+ self._reverse_module_aliases = {v: k for k, v in module_aliases.items()}
900
+ elif isinstance(node, ast.ClassDef):
901
+ seen_non_import = True
902
+ # Warn if class shadows an imported parser keyword name
903
+ # (skip for @builtin_type classes -- shadow is intentional)
904
+ source = self._imports.get_import_source(node.name)
905
+ has_builtin_type = any(
906
+ self._decorator_local_name(d) in ("builtin_type", qnames.BUILTIN_TYPE)
907
+ for d in node.decorator_list)
908
+ if source and source[0] in _PARSER_KEYWORD_MODULES and not has_builtin_type:
909
+ self._warn(f"class '{node.name}' shadows import from '{source[0]}'", node)
910
+ result = self._parse_class(node)
911
+ if isinstance(result, TpyProtocol):
912
+ protocols.append(result)
913
+ # Register the protocol type
914
+ self.registry.register_protocol(ProtocolInfo(
915
+ name=result.name,
916
+ methods=result.methods,
917
+ type_params=result.type_params,
918
+ is_dynamic=result.is_dynamic,
919
+ module=self._public_module(),
920
+ ))
921
+ elif isinstance(result, TpyEnum):
922
+ enums.append(result)
923
+ # Register an enum placeholder so it can be used in type
924
+ # annotations within the same file. Sema re-registers with
925
+ # the fully-populated NominalType + TypeDef.enum payload.
926
+ self.registry.register_enum_placeholder(
927
+ result.name, module=self._public_module())
928
+ else:
929
+ records.append(result)
930
+ # Register the record type
931
+ self.registry.register_record(RecordInfo(
932
+ name=result.name,
933
+ fields=result.fields,
934
+ has_init=result.init_method is not None,
935
+ builtin_type_key=result.builtin_type_key,
936
+ is_indirecting=result.is_indirecting,
937
+ module=self._public_module(),
938
+ ))
939
+ # Prefix nested type names with parent chain and register
940
+ self._prefix_nested_names(result, result.name)
941
+ self._register_nested_types(result)
942
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
943
+ seen_non_import = True
944
+ # Warn if function shadows an imported parser keyword name
945
+ # (skip for @builtin_function/builtin_decorator -- shadow is intentional)
946
+ source = self._imports.get_import_source(node.name)
947
+ if source and source[0] in _PARSER_KEYWORD_MODULES:
948
+ has_builtin_dec = any(
949
+ self._decorator_local_name(d) in (
950
+ "builtin_function", "builtin_decorator",
951
+ qnames.BUILTIN_FUNCTION, qnames.BUILTIN_DECORATOR)
952
+ for d in node.decorator_list)
953
+ if not has_builtin_dec:
954
+ self._warn(f"def '{node.name}' shadows import from '{source[0]}'", node)
955
+ func = self._parse_function(node)
956
+ functions.append(func)
957
+ if func.builtin_decorator_key:
958
+ # @builtin_decorator stub signatures drive parse-time
959
+ # schema derivation for decorator argument validation
960
+ # (_schema_from_stub inspects param types). Resolve
961
+ # TypeRefNodes now so _schema_from_stub sees TpyType;
962
+ # sema's pre-pass will no-op on already-resolved fields.
963
+ # Module-level scope -- no enclosing type params.
964
+ self._finalize_function_refs(func, outer_scope=None)
965
+ # Register for decorator resolution (like @builtin_type for records)
966
+ # return_type may be None (no annotation) -- FunctionInfo
967
+ # here is used solely for decorator-key lookup; the value
968
+ # is not read for call resolution.
969
+ self.registry.register_function(FunctionInfo(
970
+ name=func.name, params=[], return_type=func.return_type,
971
+ builtin_decorator_key=func.builtin_decorator_key,
972
+ ))
973
+ # Derive arg schema from stub signature
974
+ schema = self._schema_from_stub(func)
975
+ if schema is not None:
976
+ self._decorator_schemas[func.builtin_decorator_key] = schema
977
+ elif isinstance(node, ast.TypeAlias):
978
+ seen_non_import = True
979
+ tp_names, tp_kinds = self._parse_alias_type_params(node)
980
+ self._register_type_alias(
981
+ node.name.id, node.value, type_aliases,
982
+ type_params=tp_names, type_param_kinds=tp_kinds,
983
+ )
984
+ elif isinstance(node, ast.Assign) and self._is_type_alias_assign(node):
985
+ seen_non_import = True
986
+ self._register_type_alias(node.targets[0].id, node.value, type_aliases)
987
+ else:
988
+ # Skip docstrings and pass statements for late import detection
989
+ if not self._is_ignorable_for_import_order(node):
990
+ seen_non_import = True
991
+ # All other statements go through _parse_stmt (same as function bodies)
992
+ stmt = self._parse_stmt(node)
993
+ if isinstance(stmt, list):
994
+ top_level_stmts.extend(stmt)
995
+ else:
996
+ top_level_stmts.append(stmt)
997
+
998
+ return TpyModule(records=records, functions=functions, protocols=protocols, enums=enums, top_level_stmts=top_level_stmts, source_lines=self.source_lines, imports=imports, tpy_star_import=self._imports.tpy_star_import, star_imports=self._imports.star_imports, user_module_imports=user_module_imports, module_aliases=module_aliases, bare_module_imports=bare_module_imports, type_aliases=type_aliases, parse_warnings=self._warnings, recursive_union_names=self._recursive_union_names)
999
+
1000
+ def _is_protocol_base(self, base: ast.expr) -> bool:
1001
+ """Check if a base class expression refers to typing.Protocol."""
1002
+ return self._resolve_parser_keyword(base) == ("typing", "Protocol")
1003
+
1004
+ def _is_enum_base(self, base: ast.expr) -> bool:
1005
+ """Check if a base class expression refers to enum.Enum."""
1006
+ return self._resolve_parser_keyword(base) == ("enum", "Enum")
1007
+
1008
+ def _is_int_enum_base(self, base: ast.expr) -> bool:
1009
+ """Check if a base class expression refers to enum.IntEnum."""
1010
+ return self._resolve_parser_keyword(base) == ("enum", "IntEnum")
1011
+
1012
+ def _is_typed_dict_base(self, base: ast.expr) -> bool:
1013
+ """Check if a base class expression refers to typing.TypedDict."""
1014
+ return self._resolve_parser_keyword(base) == ("typing", "TypedDict")
1015
+
1016
+ def _parse_unpack_annotation(self, annotation: ast.expr, type_param_scope, error_node) -> 'TpyType | TypeRefNode':
1017
+ """Parse Unpack[TypedDict] annotation from **kwargs.
1018
+
1019
+ Returns the inner TypedDict reference as a TypeRefNode; sema
1020
+ resolves it via `resolve_refs` and validates it
1021
+ is a TypedDict in registration.
1022
+ """
1023
+ if not isinstance(annotation, ast.Subscript):
1024
+ raise ParseError("**kwargs must have Unpack[TypedDict] annotation", error_node)
1025
+ resolved = self._resolve_parser_keyword(annotation.value)
1026
+ if resolved != ("typing", "Unpack"):
1027
+ raise ParseError("**kwargs must have Unpack[TypedDict] annotation", error_node)
1028
+ return self._parse_type_ref(annotation.slice, type_param_scope)
1029
+
1030
+ # Valid integer mixin types for IntEnum: class P(int, Enum) or class P(Int8, Enum)
1031
+ _INT_MIXIN_TYPES: dict[str, str] = {
1032
+ "int": "int",
1033
+ "Int8": "Int8", "Int16": "Int16", "Int32": "Int32", "Int64": "Int64",
1034
+ "UInt8": "UInt8", "UInt16": "UInt16", "UInt32": "UInt32", "UInt64": "UInt64",
1035
+ }
1036
+
1037
+ def _resolve_int_mixin(self, base: ast.expr) -> str | None:
1038
+ """Resolve a base class to an integer mixin type name, or None."""
1039
+ if isinstance(base, ast.Name):
1040
+ return self._INT_MIXIN_TYPES.get(base.id)
1041
+ return None
1042
+
1043
+ # Sentinels for _resolve_decorator arg_value
1044
+ _EMPTY_CALL = object() # @name() -- call with zero args
1045
+ _BAD_ARGS = object() # @name(x, y) or non-constant -- caller must error
1046
+
1047
+ def _resolve_decorator(self, dec: ast.expr) -> tuple[str, str, object] | None:
1048
+ """Resolve a decorator to (module, original_name, arg_value).
1049
+
1050
+ Handles bare (@name), qualified (@mod.name), call-with-args (@name(arg)),
1051
+ and qualified-call (@mod.name(arg)) forms.
1052
+
1053
+ arg_value meanings:
1054
+ None -- bare form (@name)
1055
+ _EMPTY_CALL -- call with zero args (@name())
1056
+ _BAD_ARGS -- invalid args (multiple or non-constant)
1057
+ <value> -- single constant arg value (str, bool, int, etc.)
1058
+
1059
+ Returns None if the decorator name cannot be resolved through imports.
1060
+ """
1061
+ # Extract the function node and args from Call decorators
1062
+ func_node = dec
1063
+ arg_value = None
1064
+ if isinstance(dec, ast.Call):
1065
+ func_node = dec.func
1066
+ if dec.keywords:
1067
+ # @native("name", function=True) -- positional + keyword args
1068
+ if len(dec.args) == 1 and isinstance(dec.args[0], ast.Constant):
1069
+ kw_dict: dict[str, object] = {}
1070
+ for kw in dec.keywords:
1071
+ if isinstance(kw.value, ast.Constant):
1072
+ kw_dict[kw.arg] = kw.value.value
1073
+ elif isinstance(kw.value, ast.Name):
1074
+ kw_dict[kw.arg] = _NameArg(kw.value.id)
1075
+ arg_value = (dec.args[0].value, kw_dict)
1076
+ elif not dec.args:
1077
+ # @type_param_default(T=int) -- kwargs only
1078
+ kw_dict = {}
1079
+ for kw in dec.keywords:
1080
+ if isinstance(kw.value, ast.Constant):
1081
+ kw_dict[kw.arg] = kw.value.value
1082
+ elif isinstance(kw.value, ast.Name):
1083
+ kw_dict[kw.arg] = _NameArg(kw.value.id)
1084
+ arg_value = (None, kw_dict) if kw_dict else self._BAD_ARGS
1085
+ else:
1086
+ arg_value = self._BAD_ARGS
1087
+ elif not dec.args:
1088
+ arg_value = self._EMPTY_CALL
1089
+ elif len(dec.args) == 1 and isinstance(dec.args[0], ast.Constant):
1090
+ arg_value = dec.args[0].value
1091
+ elif len(dec.args) == 1 and isinstance(dec.args[0], ast.Name):
1092
+ # Name arg like @error_return(StopIteration) -- store as _NameArg
1093
+ arg_value = _NameArg(dec.args[0].id)
1094
+ else:
1095
+ arg_value = self._BAD_ARGS
1096
+
1097
+ # Bare name: @name or @name(arg)
1098
+ if isinstance(func_node, ast.Name):
1099
+ name = func_node.id
1100
+ # @staticmethod and @property are Python builtins, not resolved through imports
1101
+ if name == "staticmethod":
1102
+ return ("builtins", "staticmethod", arg_value)
1103
+ if name == "property":
1104
+ return ("builtins", "property", arg_value)
1105
+ resolved = self._resolve_type_name(name)
1106
+ if resolved:
1107
+ return (resolved[0], resolved[1], arg_value)
1108
+ return None
1109
+
1110
+ # Qualified name: @mod.name or @mod.name(arg)
1111
+ if isinstance(func_node, ast.Attribute) and isinstance(func_node.value, ast.Name):
1112
+ resolved = self._resolve_qualified_type_name(func_node)
1113
+ if resolved:
1114
+ return (resolved[0], resolved[1], arg_value)
1115
+ return None
1116
+
1117
+ return None
1118
+
1119
+ @staticmethod
1120
+ def _decorator_local_name(dec: ast.expr) -> str | None:
1121
+ """Extract the local name used in the source for a decorator (for error messages)."""
1122
+ func_node = dec.func if isinstance(dec, ast.Call) else dec
1123
+ if isinstance(func_node, ast.Name):
1124
+ return func_node.id
1125
+ if isinstance(func_node, ast.Attribute):
1126
+ return f"{func_node.value.id}.{func_node.attr}" if isinstance(func_node.value, ast.Name) else None
1127
+ return None
1128
+
1129
+ @staticmethod
1130
+ def _decorator_raw_name(dec: ast.expr) -> str | None:
1131
+ """Extract the bare function name from a decorator AST node.
1132
+
1133
+ Returns just the identifier (e.g. 'builtin_type' from both
1134
+ @builtin_type(...) and @mod.builtin_type(...)). Used by pre-scan
1135
+ to detect stdlib builtin decorators before import resolution.
1136
+ """
1137
+ func_node = dec.func if isinstance(dec, ast.Call) else dec
1138
+ if isinstance(func_node, ast.Name):
1139
+ return func_node.id
1140
+ if isinstance(func_node, ast.Attribute):
1141
+ return func_node.attr
1142
+ return None
1143
+
1144
+ def _require_decorator(self, dec: ast.expr, context: str) -> tuple[str, object]:
1145
+ """Resolve a decorator or raise a helpful error. Returns (qname, arg)."""
1146
+ resolved = self._resolve_decorator(dec)
1147
+ if resolved is None:
1148
+ local_name = self._decorator_local_name(dec) or "?"
1149
+ raise ParseError(f"Unknown decorator '{local_name}' on {context}", dec)
1150
+ return self._qualify(resolved), resolved[2]
1151
+
1152
+ def _parse_type_param_default(self, dec: ast.expr) -> dict[str, str]:
1153
+ """Parse @type_param_default(T=DefaultInt) -> {"T": "tpy.extern.DefaultInt"}."""
1154
+ if not isinstance(dec, ast.Call) or not dec.keywords:
1155
+ raise ParseError("@type_param_default() requires keyword arguments, e.g. @type_param_default(T=DefaultInt)", dec)
1156
+ result: dict[str, str] = {}
1157
+ for kw in dec.keywords:
1158
+ if not isinstance(kw.value, ast.Name):
1159
+ raise ParseError(f"@type_param_default({kw.arg}=...) value must be a type name", dec)
1160
+ name = kw.value.id
1161
+ resolved = self._resolve_type_name(name)
1162
+ if resolved is None:
1163
+ raise ParseError(
1164
+ f"@type_param_default({kw.arg}={name}): unknown type '{name}'", dec)
1165
+ result[kw.arg] = self._qualify(resolved)
1166
+ return result
1167
+
1168
+ def _parse_readonly_arg(self, arg: object, dec: ast.expr) -> tuple[bool, bool]:
1169
+ """Parse @readonly validated arg -> (is_readonly, readonly_opt_out).
1170
+
1171
+ arg should be the validated positional value from _validate_decorator_args
1172
+ (None for bare/@readonly(), bool for @readonly(True/False)).
1173
+ """
1174
+ if arg is None:
1175
+ return (True, False)
1176
+ if isinstance(arg, bool):
1177
+ return (arg, not arg)
1178
+ raise ParseError("@readonly() requires a bool argument", dec)
1179
+
1180
+ def _schema_from_stub(self, func: TpyFunction) -> _DecoratorArgSchema | None:
1181
+ """Derive a _DecoratorArgSchema from a @builtin_decorator stub's signature.
1182
+
1183
+ Single param -> positional arg. Additional params with defaults -> kwargs.
1184
+ Type mapping: bool->bool, str->str, type->_NameArg (type name reference).
1185
+ """
1186
+ if not func.params:
1187
+ return _DecoratorArgSchema() # bare only
1188
+
1189
+ def _map_type(ptype: TpyType) -> tuple[type, str] | None:
1190
+ # Optional[T] kwargs default to None and accept either a value of T
1191
+ # or no value -- unwrap to T for the schema.
1192
+ if isinstance(ptype, OptionalType):
1193
+ ptype = ptype.inner
1194
+ if is_bool_type(ptype):
1195
+ return (bool, "bool")
1196
+ if is_str_type(ptype):
1197
+ return (str, "str")
1198
+ # `type` annotation -> _NameArg (type-name reference). The TYPE
1199
+ # qname has no registered TypeDef, so check by qualified name.
1200
+ if (isinstance(ptype, NominalType)
1201
+ and ptype.qualified_name() == qnames.TYPE):
1202
+ return (_NameArg, "type name")
1203
+ return None
1204
+
1205
+ # First param -> positional
1206
+ _, ptype = func.params[0]
1207
+ match = _map_type(ptype)
1208
+ if match is None:
1209
+ return None
1210
+ pos_type, pos_desc = match
1211
+ has_default = func.defaults and func.defaults[0] is not None
1212
+
1213
+ # Additional params -> kwargs
1214
+ kwargs: dict[str, type] | None = None
1215
+ for i in range(1, len(func.params)):
1216
+ pname, ptype = func.params[i]
1217
+ match = _map_type(ptype)
1218
+ if match is None:
1219
+ return None
1220
+ if kwargs is None:
1221
+ kwargs = {}
1222
+ kwargs[pname] = match[0]
1223
+
1224
+ return _DecoratorArgSchema(pos_type=pos_type, pos_required=not has_default,
1225
+ pos_description=pos_desc, kwargs=kwargs)
1226
+
1227
+ def _validate_decorator_args(
1228
+ self, qname: str, arg: object, dec: ast.expr,
1229
+ ) -> tuple[object, dict[str, object]]:
1230
+ """Validate decorator args against schema. Returns (positional, kwargs).
1231
+
1232
+ positional is the validated positional arg value, or None if not provided
1233
+ (both bare @name and empty @name() normalize to None for optional args).
1234
+ kwargs is a dict of validated keyword arg values (empty if none).
1235
+ """
1236
+ # Explicit schemas (for non-stub decorators like auto_readonly,
1237
+ # staticmethod) take precedence; then schemas derived from stubs.
1238
+ schema = _DECORATOR_ARG_SCHEMAS.get(qname) or self._decorator_schemas.get(qname)
1239
+ if schema is None:
1240
+ return (arg, {})
1241
+ dec_name = self._decorator_local_name(dec) or bare_name(qname)
1242
+
1243
+ # Handle tuple form: (positional, {kwargs}) from @native("name", function=True)
1244
+ pos_arg = arg
1245
+ raw_kwargs: dict[str, object] = {}
1246
+ if isinstance(arg, tuple) and len(arg) == 2 and isinstance(arg[1], dict):
1247
+ pos_arg, raw_kwargs = arg
1248
+
1249
+ # Validate positional arg
1250
+ _fallback_desc = {str: "string", bool: "bool", _NameArg: "type name"}
1251
+ desc = schema.pos_description or _fallback_desc.get(schema.pos_type, "valid")
1252
+ if schema.pos_type is None:
1253
+ if pos_arg is not None:
1254
+ raise ParseError(f"@{dec_name} does not take arguments", dec)
1255
+ elif schema.pos_required:
1256
+ if not isinstance(pos_arg, schema.pos_type):
1257
+ raise ParseError(f"@{dec_name}() requires a {desc} argument", dec)
1258
+ else:
1259
+ if pos_arg is not None and pos_arg is not self._EMPTY_CALL and not isinstance(pos_arg, schema.pos_type):
1260
+ raise ParseError(f"@{dec_name}() requires a {desc} argument", dec)
1261
+ if pos_arg is self._EMPTY_CALL:
1262
+ pos_arg = None
1263
+
1264
+ # Validate keyword args
1265
+ if raw_kwargs:
1266
+ if schema.kwargs is None:
1267
+ raise ParseError(f"@{dec_name} does not accept keyword arguments", dec)
1268
+ for key, val in raw_kwargs.items():
1269
+ if key not in schema.kwargs:
1270
+ raise ParseError(f"@{dec_name}() got unexpected keyword argument '{key}'", dec)
1271
+ expected_type = schema.kwargs[key]
1272
+ if not isinstance(val, expected_type):
1273
+ raise ParseError(f"@{dec_name}({key}=...) expects {expected_type.__name__}", dec)
1274
+
1275
+ validated_kwargs = raw_kwargs if schema.kwargs else {}
1276
+ return (pos_arg, validated_kwargs)
1277
+
1278
+ def _extract_decorator_kwargs(self, dec: ast.expr, arg: object, class_name: str) -> dict[str, Any]:
1279
+ """Extract keyword arguments from a macro decorator call.
1280
+
1281
+ Handles bare ``@name``, empty ``@name()``, and ``@name(k=v, ...)``.
1282
+ Positional arguments are rejected. Kwarg values must be literals.
1283
+ """
1284
+ if arg is None or arg is self._EMPTY_CALL:
1285
+ return {}
1286
+ # kwargs-only tuple from _resolve_decorator: (None, {k: v, ...})
1287
+ if isinstance(arg, tuple) and len(arg) == 2 and arg[0] is None and isinstance(arg[1], dict):
1288
+ return arg[1]
1289
+ if arg is not self._BAD_ARGS:
1290
+ dec_name = self._decorator_local_name(dec) or "?"
1291
+ raise ParseError(f"@{dec_name} does not take positional arguments", dec)
1292
+ if not isinstance(dec, ast.Call):
1293
+ return {}
1294
+ if dec.args:
1295
+ dec_name = self._decorator_local_name(dec) or "?"
1296
+ raise ParseError(f"@{dec_name} does not take positional arguments", dec)
1297
+ result: dict[str, Any] = {}
1298
+ for kw in dec.keywords:
1299
+ if kw.arg is None:
1300
+ dec_name = self._decorator_local_name(dec) or "?"
1301
+ raise ParseError(f"@{dec_name} does not support **kwargs", dec)
1302
+ try:
1303
+ result[kw.arg] = ast.literal_eval(kw.value)
1304
+ except (ValueError, TypeError):
1305
+ dec_name = self._decorator_local_name(dec) or "?"
1306
+ raise ParseError(
1307
+ f"@{dec_name}: keyword '{kw.arg}' must be a literal value", dec)
1308
+ return result
1309
+
1310
+ def _resolve_call_import(self, call: TpyExpr, node: ast.expr) -> None:
1311
+ """Resolve import origin for a call expression and set resolved_import.
1312
+
1313
+ Handles both ``field(...)`` (TpyCall) and ``dataclasses.field(...)``
1314
+ (TpyMethodCall) forms.
1315
+ """
1316
+ if not isinstance(node, ast.Call):
1317
+ return
1318
+ func = node.func
1319
+ source = None
1320
+ if isinstance(func, ast.Name):
1321
+ source = self._resolve_type_name(func.id)
1322
+ elif isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
1323
+ source = self._resolve_qualified_type_name(func)
1324
+ if source is not None:
1325
+ call.resolved_import = source
1326
+
1327
+ def _parse_class(self, node: ast.ClassDef) -> TpyRecord | TpyProtocol | TpyEnum:
1328
+ """Parse a class definition as a record, protocol, or enum."""
1329
+ if node.bases:
1330
+ # Check for IntEnum first: class P(IntEnum) or class P(int, Enum)
1331
+ has_int_enum = any(self._is_int_enum_base(base) for base in node.bases)
1332
+ if has_int_enum:
1333
+ if len(node.bases) != 1:
1334
+ raise ParseError(
1335
+ "IntEnum must be the only base class", node)
1336
+ # IntEnum without mixin defaults to Int32 (not BigInt)
1337
+ return self._parse_enum(node, is_int_enum=True, underlying_type_name="Int32")
1338
+
1339
+ # Check for mixin pattern: class P(int, Enum) or class P(Int8, Enum)
1340
+ has_enum = any(self._is_enum_base(base) for base in node.bases)
1341
+ if has_enum:
1342
+ if len(node.bases) == 2:
1343
+ # Two bases: one must be Enum, the other an int mixin
1344
+ mixin_type = None
1345
+ for base in node.bases:
1346
+ if not self._is_enum_base(base):
1347
+ mixin_type = self._resolve_int_mixin(base)
1348
+ if mixin_type is None:
1349
+ base_name = base.id if isinstance(base, ast.Name) else ast.unparse(base)
1350
+ raise ParseError(
1351
+ f"Invalid enum mixin type '{base_name}'; "
1352
+ f"expected int, Int8..Int64, or UInt8..UInt64",
1353
+ node)
1354
+ if mixin_type is not None:
1355
+ return self._parse_enum(
1356
+ node, is_int_enum=True, underlying_type_name=mixin_type)
1357
+ elif len(node.bases) > 2:
1358
+ raise ParseError(
1359
+ "Enum class must have at most 2 base classes (mixin + Enum)", node)
1360
+ return self._parse_enum(node)
1361
+ # Check if this is a Protocol definition (has Protocol as one of its bases)
1362
+ has_protocol = any(self._is_protocol_base(base) for base in node.bases)
1363
+ if has_protocol:
1364
+ return self._parse_protocol(node)
1365
+
1366
+ # Check if this is a TypedDict definition
1367
+ is_typed_dict = bool(node.bases) and any(self._is_typed_dict_base(base) for base in node.bases)
1368
+ is_total_false = False
1369
+ if is_typed_dict:
1370
+ non_td_bases = [b for b in node.bases if not self._is_typed_dict_base(b)]
1371
+ if non_td_bases:
1372
+ raise ParseError("TypedDict cannot have additional base classes", node)
1373
+ if node.decorator_list:
1374
+ raise ParseError("Decorators are not supported on TypedDict", node)
1375
+ if hasattr(node, 'type_params') and node.type_params:
1376
+ raise ParseError("Type parameters are not supported on TypedDict", node)
1377
+ # Parse total=False keyword
1378
+ for kw in node.keywords:
1379
+ if kw.arg == "total":
1380
+ if isinstance(kw.value, ast.Constant) and kw.value.value is False:
1381
+ is_total_false = True
1382
+ elif isinstance(kw.value, ast.Constant) and kw.value.value is True:
1383
+ pass # total=True is the default
1384
+ else:
1385
+ raise ParseError("total must be True or False", node)
1386
+ else:
1387
+ self._warnings.append(ParseWarning(
1388
+ f"Unknown class keyword argument '{kw.arg}' on TypedDict",
1389
+ self._loc(node)))
1390
+
1391
+ # Warn on class keyword arguments for non-TypedDict classes
1392
+ if not is_typed_dict and node.keywords:
1393
+ for kw in node.keywords:
1394
+ self._warnings.append(ParseWarning(
1395
+ f"Class keyword argument '{kw.arg}' is not supported",
1396
+ self._loc(node)))
1397
+
1398
+ # Parse record decorators (@native, @nocopy, macro decorators)
1399
+ linkage = RecordLinkage.DEFAULT
1400
+ native_name: str | None = None
1401
+ is_nocopy = False
1402
+ builtin_type_key: str | None = None
1403
+ is_indirecting = False
1404
+ pending_macros: list[tuple[str, dict[str, Any]]] = []
1405
+ for dec in node.decorator_list:
1406
+ qname, arg = self._require_decorator(dec, f"class '{node.name}'")
1407
+ if qname in self._RECORD_LINKAGE_MAP:
1408
+ pos, kw = self._validate_decorator_args(qname, arg, dec)
1409
+ new_linkage = self._RECORD_LINKAGE_MAP[qname]
1410
+ # binding="C" overrides linkage to C variant
1411
+ binding = kw.get("binding", "")
1412
+ if binding == "C":
1413
+ if new_linkage == RecordLinkage.NATIVE:
1414
+ new_linkage = RecordLinkage.NATIVE_C
1415
+ elif binding and binding != "":
1416
+ raise ParseError(
1417
+ f"@{bare_name(qname)}(binding=...) only supports binding=\"C\"", dec)
1418
+ if linkage != RecordLinkage.DEFAULT:
1419
+ raise ParseError(
1420
+ f"Class '{node.name}' cannot have both @{linkage.value} and @{new_linkage.value}", node)
1421
+ linkage = new_linkage
1422
+ native_name = pos
1423
+ if kw.get("indirecting"):
1424
+ is_indirecting = True
1425
+ elif qname == qnames.NOCOPY:
1426
+ self._validate_decorator_args(qname, arg, dec)
1427
+ is_nocopy = True
1428
+ elif qname == qnames.BUILTIN_TYPE:
1429
+ pos, _ = self._validate_decorator_args(qname, arg, dec)
1430
+ builtin_type_key = pos
1431
+ else:
1432
+ # Treat as a macro decorator -- extract kwargs and store for later
1433
+ macro_kwargs = self._extract_decorator_kwargs(dec, arg, node.name)
1434
+ pending_macros.append((qname, macro_kwargs))
1435
+
1436
+ # Extract type parameters FIRST so they're in scope when parsing bases
1437
+ # Python 3.12+ syntax: class Foo[T, U]:
1438
+ # Also extract bounds: class Foo[T: Comparable]: or class Foo[N: int]:
1439
+ type_params = []
1440
+ type_param_kinds: list[TypeParamKind] = []
1441
+ type_param_bounds: dict[str, 'TpyType | TypeRefNode'] = {}
1442
+ if hasattr(node, 'type_params') and node.type_params:
1443
+ for tp in node.type_params:
1444
+ if isinstance(tp, ast.TypeVar):
1445
+ type_params.append(tp.name)
1446
+ if tp.bound is not None:
1447
+ # Check for N: int syntax (integer type parameter)
1448
+ if isinstance(tp.bound, ast.Name) and tp.bound.id == 'int':
1449
+ type_param_kinds.append(TypeParamKind.INT)
1450
+ else:
1451
+ # Protocol bound -- resolution and protocol-shape
1452
+ # validation deferred to sema.
1453
+ type_param_kinds.append(TypeParamKind.TYPE)
1454
+ type_param_bounds[tp.name] = self._parse_type_ref(tp.bound)
1455
+ else:
1456
+ type_param_kinds.append(TypeParamKind.TYPE)
1457
+ else:
1458
+ raise ParseError(f"Only simple type parameters supported, got {type(tp).__name__}", node)
1459
+
1460
+ # Create a dict of type param names to kinds for scope during parsing
1461
+ type_param_scope = dict(zip(type_params, type_param_kinds)) if type_params else None
1462
+ # Store scope for use during method body parsing (expression parsing uses this)
1463
+ old_scope = self._type_param_scope
1464
+ self._type_param_scope = type_param_scope
1465
+
1466
+ # Parse base classes/protocols for inheritance. Classification
1467
+ # into parent class vs protocol happens above via
1468
+ # `_is_protocol_base` (keyword-matching, robust to shadowing
1469
+ # detection). TypedDict marker base is filtered out.
1470
+ #
1471
+ # Bases emit as TypeRefNode; sema re-resolves them in
1472
+ # `resolve_refs` under the record's type-param
1473
+ # scope. Class-body checks that could mask base-resolution
1474
+ # errors (stub-body validation, @native decorator validation,
1475
+ # field-type inference failure) all run in sema, so no
1476
+ # parse-time base resolution side-effect is needed.
1477
+ bases: 'list[TpyType | TypeRefNode]' = []
1478
+ for base in node.bases:
1479
+ if is_typed_dict and self._is_typed_dict_base(base):
1480
+ continue
1481
+ ref = self._parse_type_ref(base, type_param_scope)
1482
+ bases.append(ref)
1483
+
1484
+ fields = []
1485
+ methods = []
1486
+ nested_records: list[TpyRecord] = []
1487
+ nested_enums: list[TpyEnum] = []
1488
+ property_names: set[str] = set()
1489
+ # Save and extend nested type scope so short names resolve inside the class body
1490
+ old_nested_scope = self._nested_type_scope
1491
+ self._nested_type_scope = dict(old_nested_scope)
1492
+
1493
+ for item in node.body:
1494
+ if isinstance(item, ast.AnnAssign):
1495
+ # Field declaration: name: Type = default
1496
+ if not isinstance(item.target, ast.Name):
1497
+ raise ParseError("Invalid field declaration", item)
1498
+ field_name = item.target.id
1499
+ # Emit a TypeRefNode. The post-parse `resolve_refs` pass
1500
+ # pre-pass resolves it before any reader consumes fld.type.
1501
+ field_type = self._parse_type_ref(item.annotation, type_param_scope)
1502
+ default_val = None
1503
+ default_expr = None
1504
+ if item.value is not None:
1505
+ default_expr = self._parse_expr(item.value)
1506
+ # Resolve import origin for call expressions (e.g. field() or dataclasses.field())
1507
+ if isinstance(default_expr, (TpyCall, TpyMethodCall)):
1508
+ self._resolve_call_import(default_expr, item.value)
1509
+ default_val = self._get_default_value(item.value)
1510
+ fields.append(FieldInfo(field_name, field_type, default_val, default_expr=default_expr, loc=self._loc(item)))
1511
+ if is_typed_dict and default_expr is not None:
1512
+ self._warnings.append(ParseWarning(
1513
+ f"TypedDict field '{field_name}' has a default value which is "
1514
+ f"ignored by CPython at runtime; consider using total=False "
1515
+ f"for optional fields",
1516
+ self._loc(item),
1517
+ ))
1518
+ elif isinstance(item, ast.Assign):
1519
+ # Field with inferred type: name = Int32(0)
1520
+ if len(item.targets) != 1 or not isinstance(item.targets[0], ast.Name):
1521
+ raise ParseError("Invalid field declaration", item)
1522
+ field_name = item.targets[0].id
1523
+ # Emit a marker; sema's field-resolution pass runs the
1524
+ # inferrer on FieldInfo.default_expr, raising only on
1525
+ # genuine failure. Keeping inference out of the parser
1526
+ # avoids coupling parser to the full typesys surface.
1527
+ field_type: 'TpyType | TypeRefNode | TpyInferFromDefaultRef' = TpyInferFromDefaultRef(loc=self._loc(item))
1528
+ default_val = self._get_default_value(item.value)
1529
+ default_expr = self._parse_expr(item.value)
1530
+ fields.append(FieldInfo(field_name, field_type, default_val,
1531
+ default_expr=default_expr,
1532
+ loc=self._loc(item)))
1533
+ elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
1534
+ if is_typed_dict:
1535
+ raise ParseError(f"Methods are not allowed on TypedDict '{node.name}'", item)
1536
+ parsed = self._parse_method(item, node.name, type_param_scope, property_names)
1537
+ if parsed.is_property_getter:
1538
+ property_names.add(parsed.name)
1539
+ # Dual overloads (const + mutable) for correct reference
1540
+ # semantics; sema.method_expansion performs the actual
1541
+ # clone. Registration prunes the mutable clone for
1542
+ # value-type returns.
1543
+ parsed.auto_readonly = True
1544
+ methods.append(parsed)
1545
+ elif isinstance(item, ast.Pass):
1546
+ pass
1547
+ elif isinstance(item, ast.Expr) and isinstance(item.value, ast.Constant) and item.value.value is ...:
1548
+ pass # Ellipsis for opaque native types
1549
+ elif isinstance(item, ast.Expr) and isinstance(item.value, ast.Constant) and isinstance(item.value.value, str):
1550
+ pass # Docstring
1551
+ elif isinstance(item, ast.ClassDef):
1552
+ if is_typed_dict:
1553
+ raise ParseError(f"Nested classes are not allowed in TypedDict '{node.name}'", item)
1554
+ # Parse first so we can give an enum-specific diagnostic when the
1555
+ # nested entity is a @native enum (pointing the user at the
1556
+ # top-level qualified-name binding pattern). The broad-linkage
1557
+ # rejection below catches any other nested class.
1558
+ nested = self._parse_class(item)
1559
+ if isinstance(nested, TpyProtocol):
1560
+ raise ParseError("Protocols cannot be nested inside classes", item)
1561
+ if isinstance(nested, TpyEnum) and nested.is_native:
1562
+ raise ParseError(
1563
+ f"@native enum '{nested.name}' cannot be nested inside "
1564
+ f"a class; declare it at module top level with the "
1565
+ f"fully-qualified C++ name, e.g. "
1566
+ f"`@native(\"ns::Container::{nested.name}\") "
1567
+ f"class {nested.name}(Enum): ...`. TPy structure does "
1568
+ f"not need to mirror C++ structure -- the @native "
1569
+ f"qname encodes the C++ nesting.",
1570
+ item)
1571
+ if linkage != RecordLinkage.DEFAULT:
1572
+ raise ParseError(f"Nested classes are not allowed in @{linkage.value} classes", item)
1573
+ if isinstance(nested, TpyEnum):
1574
+ if type_params:
1575
+ raise ParseError(
1576
+ f"Nested enums are not supported inside generic classes "
1577
+ f"('{node.name}' has type parameters)", item)
1578
+ # Register immediately with dotted name so forward references
1579
+ # within the same class body work (e.g., kind: Container.Kind)
1580
+ dotted_name = f"{node.name}.{nested.name}"
1581
+ self.registry.register_enum_placeholder(
1582
+ dotted_name, module=self._public_module())
1583
+ self._nested_type_scope[nested.name] = dotted_name
1584
+ nested_enums.append(nested)
1585
+ else:
1586
+ if type_params:
1587
+ raise ParseError(
1588
+ f"Nested classes are not supported inside generic classes "
1589
+ f"('{node.name}' has type parameters)", item)
1590
+ # Register immediately with dotted name for forward references
1591
+ dotted_name = f"{node.name}.{nested.name}"
1592
+ self.registry.register_record(RecordInfo(
1593
+ name=dotted_name,
1594
+ fields=nested.fields,
1595
+ has_init=nested.init_method is not None,
1596
+ module=self._public_module(),
1597
+ ))
1598
+ self._nested_type_scope[nested.name] = dotted_name
1599
+ nested_records.append(nested)
1600
+ else:
1601
+ raise ParseError(f"Unsupported construct in class '{node.name}'", item)
1602
+
1603
+ # Auto-declare fields from self.f = param assignments in __init__.
1604
+ # Phase 1: only for non-native classes without bases (inheritance
1605
+ # needs parent field info to avoid shadowing, not available at parse time).
1606
+ if not bases and linkage == RecordLinkage.DEFAULT:
1607
+ init_method = None
1608
+ for m in methods:
1609
+ if m.name == "__init__":
1610
+ init_method = m
1611
+ break
1612
+ if init_method is not None:
1613
+ new_fields = self._auto_declare_fields_from_init(init_method, fields, property_names)
1614
+ fields.extend(new_fields)
1615
+ # Reorder fields to match __init__ assignment order so that
1616
+ # C++ struct layout matches the init list (avoids -Wreorder).
1617
+ fields = self._reorder_fields_by_init(init_method, fields)
1618
+
1619
+ # Method-linkage validation (stubs allowed/required per class
1620
+ # linkage, @native decorator restrictions) runs at sema time in
1621
+ # `_validate_record_method_linkage` after base resolution, so
1622
+ # base-resolution errors (e.g. "'Protocol' requires: from typing
1623
+ # import Protocol" on a class whose Protocol base is shadowed)
1624
+ # fire first instead of being masked here.
1625
+
1626
+ # Restore scopes
1627
+ self._type_param_scope = old_scope
1628
+ self._nested_type_scope = old_nested_scope
1629
+ return TpyRecord(name=node.name, fields=fields, methods=methods, type_params=type_params, type_param_kinds=type_param_kinds, type_param_bounds=type_param_bounds, bases=bases, linkage=linkage, native_name=native_name, is_nocopy=is_nocopy, builtin_type_key=builtin_type_key, is_indirecting=is_indirecting, pending_macros=pending_macros, nested_records=nested_records, nested_enums=nested_enums, is_typed_dict=is_typed_dict, is_total_false=is_total_false, loc=self._loc(node))
1630
+
1631
+ def _auto_declare_fields_from_init(
1632
+ self,
1633
+ init_method: TpyFunction,
1634
+ existing_fields: list[FieldInfo],
1635
+ property_names: set[str] | None = None,
1636
+ ) -> list[FieldInfo]:
1637
+ """Auto-declare fields from top-level `self.f = param` in __init__.
1638
+
1639
+ For CPython compatibility: fields can be created by assignment in
1640
+ __init__ without requiring class-level annotations. Only handles
1641
+ the case where the RHS is a parameter name (type taken from param).
1642
+ """
1643
+ param_types = {name: typ for name, typ in init_method.params}
1644
+ existing_names = {fld.name for fld in existing_fields}
1645
+
1646
+ new_fields: list[FieldInfo] = []
1647
+ for stmt in init_method.body:
1648
+ if not isinstance(stmt, TpyAssign):
1649
+ continue
1650
+ target = stmt.target
1651
+ if not isinstance(target, TpyFieldAccess):
1652
+ continue
1653
+ if not isinstance(target.obj, TpyName) or target.obj.name != "self":
1654
+ continue
1655
+ field_name = target.field
1656
+ if field_name in existing_names:
1657
+ continue
1658
+ if property_names and field_name in property_names:
1659
+ continue
1660
+ value = stmt.value
1661
+ if not isinstance(value, TpyName):
1662
+ continue
1663
+ if value.name not in param_types:
1664
+ continue
1665
+ new_fields.append(FieldInfo(field_name, param_types[value.name], loc=stmt.loc))
1666
+ existing_names.add(field_name)
1667
+
1668
+ return new_fields
1669
+
1670
+ @staticmethod
1671
+ def _reorder_fields_by_init(
1672
+ init_method: TpyFunction,
1673
+ fields: list[FieldInfo],
1674
+ ) -> list[FieldInfo]:
1675
+ """Reorder fields to match __init__ body assignment order.
1676
+
1677
+ C++ initializes members in struct declaration order regardless of
1678
+ init-list order. Matching the two avoids -Wreorder-ctor warnings.
1679
+ Fields not assigned in __init__ are appended at the end.
1680
+ """
1681
+ # Collect field assignment order from __init__ top-level statements
1682
+ init_order: list[str] = []
1683
+ for stmt in init_method.body:
1684
+ if not isinstance(stmt, TpyAssign):
1685
+ continue
1686
+ target = stmt.target
1687
+ if not isinstance(target, TpyFieldAccess):
1688
+ continue
1689
+ if not isinstance(target.obj, TpyName) or target.obj.name != "self":
1690
+ continue
1691
+ if target.field not in init_order:
1692
+ init_order.append(target.field)
1693
+
1694
+ field_map = {f.name: f for f in fields}
1695
+ seen: set[str] = set()
1696
+ ordered: list[FieldInfo] = []
1697
+ for name in init_order:
1698
+ if name in field_map and name not in seen:
1699
+ ordered.append(field_map[name])
1700
+ seen.add(name)
1701
+ for f in fields:
1702
+ if f.name not in seen:
1703
+ ordered.append(f)
1704
+ return ordered
1705
+
1706
+ @staticmethod
1707
+ def _prefix_nested_names(record: TpyRecord, parent_name: str) -> None:
1708
+ """Prefix immediate nested types with parent_name, then recurse."""
1709
+ for nr in record.nested_records:
1710
+ nr.name = f"{parent_name}.{nr.name}"
1711
+ Parser._prefix_nested_names(nr, nr.name)
1712
+ for ne in record.nested_enums:
1713
+ ne.name = f"{parent_name}.{ne.name}"
1714
+
1715
+ def _register_nested_types(self, record: TpyRecord) -> None:
1716
+ """Register all nested records and enums in the parser registry."""
1717
+ for nr in record.nested_records:
1718
+ self.registry.register_record(RecordInfo(
1719
+ name=nr.name,
1720
+ fields=nr.fields,
1721
+ has_init=nr.init_method is not None,
1722
+ module=self._public_module(),
1723
+ ))
1724
+ self._register_nested_types(nr)
1725
+ for ne in record.nested_enums:
1726
+ self.registry.register_enum_placeholder(
1727
+ ne.name, module=self._public_module())
1728
+
1729
+ def _parse_protocol(self, node: ast.ClassDef) -> TpyProtocol:
1730
+ """Parse a protocol definition."""
1731
+ is_dynamic = False
1732
+ cpp_concept: str | None = None
1733
+ for dec in node.decorator_list:
1734
+ qname, arg = self._require_decorator(dec, f"protocol '{node.name}'")
1735
+ pos, kw = self._validate_decorator_args(qname, arg, dec)
1736
+ if qname == qnames.DYNAMIC:
1737
+ is_dynamic = True
1738
+ continue
1739
+ if qname == qnames.NATIVE:
1740
+ if not isinstance(pos, str):
1741
+ raise ParseError("@native on protocol requires a C++ concept name string argument", dec)
1742
+ # Store the raw form; sema's `register_protocol` applies
1743
+ # `ensure_qualified()` when copying into ProtocolInfo.
1744
+ cpp_concept = pos
1745
+ continue
1746
+ dec_name = self._decorator_local_name(dec) or "?"
1747
+ raise ParseError(
1748
+ f"Unsupported decorator '@{dec_name}' on protocol '{node.name}'. "
1749
+ f"Only @dynamic (from tpy) and @native (from tpy.extern) are allowed on protocols", dec)
1750
+ # @dynamic + @native together is meaningful for runtime-defined
1751
+ # abstract bases: the C++ class (named by @native) provides the
1752
+ # vtable, and TPy treats the protocol as @dynamic for the
1753
+ # polymorphism predicate (is_polymorphic_class_type) and
1754
+ # inheritance-based conformance. Codegen suppresses concept /
1755
+ # abstract-base / Adapter emission in this case and uses the
1756
+ # @native name wherever the protocol's C++ type is needed.
1757
+ # Used by `Throwable` in lib/tpy/tpy/_core/_types.py to bridge
1758
+ # to runtime/cpp/include/tpy/throwable.hpp::tpy::Throwable.
1759
+
1760
+ # Extract type parameters from Python 3.12+ syntax: class Foo[T](Protocol):
1761
+ # Note: Protocols don't support INT type params (only TYPE).
1762
+ # Set scope before parsing bases so generic parents like `Iterable[T]`
1763
+ # can reference the protocol's own type params.
1764
+ type_params = []
1765
+ if hasattr(node, 'type_params') and node.type_params:
1766
+ for tp in node.type_params:
1767
+ if isinstance(tp, ast.TypeVar):
1768
+ type_params.append(tp.name)
1769
+ else:
1770
+ raise ParseError(f"Only simple type parameters supported in protocols, got {type(tp).__name__}", node)
1771
+
1772
+ # Set type param scope for parsing parent protocols and method signatures
1773
+ # (all TYPE kind for protocols).
1774
+ old_scope = self._type_param_scope
1775
+ self._type_param_scope = {tp: TypeParamKind.TYPE for tp in type_params} if type_params else None
1776
+
1777
+ # Extract parent protocols (excluding Protocol itself). Both bare
1778
+ # (`Sized`) and generic (`Iterable[T]`) parents are accepted. Sema
1779
+ # resolves the TypeRefNode entries to NominalType under the protocol's
1780
+ # type-param scope (see resolve_refs.py).
1781
+ parent_protocols: 'list[NominalType | TypeRefNode]' = []
1782
+ for base in node.bases:
1783
+ if self._is_protocol_base(base):
1784
+ continue
1785
+ ref = self._parse_type_ref(base, self._type_param_scope)
1786
+ if not isinstance(ref, TpyTypeRef):
1787
+ raise ParseError(
1788
+ f"Protocol parents must be simple type references "
1789
+ f"(`Name` or `Name[...]`), got {type(ref).__name__}",
1790
+ base
1791
+ )
1792
+ parent_protocols.append(ref)
1793
+
1794
+ methods = []
1795
+ fields = []
1796
+
1797
+ for item in node.body:
1798
+ if isinstance(item, ast.AsyncFunctionDef):
1799
+ raise ParseError(
1800
+ f"async methods are not allowed in protocols "
1801
+ f"(method '{item.name}' on protocol '{node.name}')", item)
1802
+ if isinstance(item, ast.FunctionDef):
1803
+ # Parse @readonly decorator
1804
+ is_readonly = False
1805
+ readonly_opt_out = False
1806
+ for dec in item.decorator_list:
1807
+ qname, arg = self._require_decorator(dec, f"protocol method '{item.name}'")
1808
+ pos, kw = self._validate_decorator_args(qname, arg, dec)
1809
+ if qname == qnames.READONLY:
1810
+ is_readonly, readonly_opt_out = self._parse_readonly_arg(pos, dec)
1811
+ else:
1812
+ dec_name = self._decorator_local_name(dec) or "?"
1813
+ raise ParseError(f"Unknown decorator '{dec_name}' on protocol method '{item.name}'", dec)
1814
+
1815
+ # Parse method signature (body should be ... or pass)
1816
+ params = []
1817
+ for i, arg in enumerate(item.args.args):
1818
+ if i == 0:
1819
+ if arg.arg != "self":
1820
+ raise ParseError(f"First parameter of protocol method '{item.name}' must be 'self'", item)
1821
+ continue
1822
+ if arg.annotation is None:
1823
+ raise ParseError(f"Protocol method parameter '{arg.arg}' must have type annotation", item)
1824
+ param_type = self._parse_type_ref(arg.annotation)
1825
+ params.append((arg.arg, param_type))
1826
+
1827
+ # return_type=None means no annotation; sema's
1828
+ # `resolve_refs` substitutes VOID.
1829
+ return_type: 'TpyType | TypeRefNode | None' = None
1830
+ if item.returns:
1831
+ return_type = self._parse_type_ref(item.returns)
1832
+
1833
+ # Capture default values for protocol method params so
1834
+ # that callers through a protocol-typed receiver can drop
1835
+ # trailing defaults (e.g. `fp.seek(0)` for `Seekable`).
1836
+ # _parse_param_defaults skips self via skip_self=True.
1837
+ param_defaults = self._parse_param_defaults(
1838
+ item, params, skip_self=True,
1839
+ )
1840
+
1841
+ methods.append(MethodSignature(
1842
+ name=item.name,
1843
+ params=params,
1844
+ return_type=return_type,
1845
+ is_readonly=is_readonly,
1846
+ readonly_opt_out=readonly_opt_out,
1847
+ param_defaults=param_defaults,
1848
+ ))
1849
+ elif isinstance(item, ast.AnnAssign):
1850
+ # Field declaration: name: Type
1851
+ if not isinstance(item.target, ast.Name):
1852
+ raise ParseError("Invalid field declaration in protocol", item)
1853
+ field_name = item.target.id
1854
+ # Emit as TypeRefNode; sema resolves under the protocol's
1855
+ # type-param scope.
1856
+ field_type = self._parse_type_ref(item.annotation)
1857
+ fields.append((field_name, field_type))
1858
+ elif isinstance(item, ast.Pass):
1859
+ pass
1860
+ elif isinstance(item, ast.Expr):
1861
+ # Allow docstrings (string literals) and ... (Ellipsis)
1862
+ if isinstance(item.value, ast.Constant):
1863
+ pass # Docstring
1864
+ elif isinstance(item.value, ast.Ellipsis):
1865
+ pass # Ellipsis at class level
1866
+ else:
1867
+ raise ParseError(f"Unexpected expression in protocol '{node.name}'", item)
1868
+ else:
1869
+ raise ParseError(f"Unsupported construct in protocol '{node.name}': {type(item).__name__}", item)
1870
+
1871
+ # Restore the scope
1872
+ self._type_param_scope = old_scope
1873
+ return TpyProtocol(name=node.name, methods=methods, fields=fields, type_params=type_params, parent_protocols=parent_protocols, is_dynamic=is_dynamic, cpp_concept=cpp_concept, loc=self._loc(node))
1874
+
1875
+ def _parse_enum(
1876
+ self, node: ast.ClassDef,
1877
+ is_int_enum: bool = False,
1878
+ underlying_type_name: str | None = None,
1879
+ ) -> TpyEnum:
1880
+ """Parse an enum class definition."""
1881
+ is_native = False
1882
+ native_name: str | None = None
1883
+ for dec in node.decorator_list:
1884
+ qname, arg = self._require_decorator(dec, f"enum '{node.name}'")
1885
+ if qname != qnames.NATIVE:
1886
+ raise ParseError(
1887
+ f"Decorators are not supported on enum '{node.name}' "
1888
+ f"(only @native is allowed)", dec)
1889
+ pos, kw = self._validate_decorator_args(qname, arg, dec)
1890
+ if kw:
1891
+ raise ParseError(
1892
+ f"@native keyword arguments are not allowed on enum "
1893
+ f"'{node.name}'", dec)
1894
+ is_native = True
1895
+ native_name = pos # may be None for bare @native; sema normalizes
1896
+
1897
+ members: list[tuple[str, int, SourceLocation | None]] = []
1898
+ cpp_member_names: dict[str, str] = {}
1899
+ has_auto = False
1900
+ # Tracks which "implicit value" form the user actually wrote so the
1901
+ # mixed-with-explicit diagnostic doesn't claim `auto()` when the user
1902
+ # only ever wrote `native_member()` (or vice versa).
1903
+ auto_form: str | None = None
1904
+ has_explicit = False
1905
+ auto_value = 1 # auto() starts at 1, matching CPython
1906
+
1907
+ for stmt in node.body:
1908
+ # Skip pass and docstrings
1909
+ if isinstance(stmt, ast.Pass):
1910
+ continue
1911
+ if (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)
1912
+ and isinstance(stmt.value.value, str)):
1913
+ continue
1914
+
1915
+ if not isinstance(stmt, ast.Assign):
1916
+ raise ParseError(
1917
+ f"Enum body must contain only member assignments (name = value), "
1918
+ f"got {type(stmt).__name__}",
1919
+ stmt,
1920
+ )
1921
+ if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name):
1922
+ raise ParseError("Enum member must be a simple name = value assignment", stmt)
1923
+
1924
+ member_name = stmt.targets[0].id
1925
+ value_node = stmt.value
1926
+
1927
+ # Check for auto() or native_member("...") call. For @native enums,
1928
+ # auto() is the idiomatic spelling -- the C++ side is the source of
1929
+ # truth for member values, and TPy-side ints are placeholders.
1930
+ # native_member("cpp_name") aliases a TPy-side name to a C++-side
1931
+ # enumerator name (for Python keywords like `None` or
1932
+ # naming-convention mismatches).
1933
+ if isinstance(value_node, ast.Call):
1934
+ resolved = self._resolve_parser_keyword(value_node.func)
1935
+ if resolved == ("enum", "auto"):
1936
+ if has_explicit:
1937
+ raise ParseError(
1938
+ "Mixed auto() and explicit values are not yet supported; "
1939
+ "use all auto() or all explicit values",
1940
+ stmt,
1941
+ )
1942
+ has_auto = True
1943
+ auto_form = "auto()"
1944
+ members.append((member_name, auto_value, self._loc(stmt)))
1945
+ auto_value += 1
1946
+ continue
1947
+ if resolved == ("tpy.extern", "native_member"):
1948
+ if not is_native:
1949
+ raise ParseError(
1950
+ "native_member() is only allowed on @native enum members",
1951
+ stmt,
1952
+ )
1953
+ if (len(value_node.args) != 1 or value_node.keywords
1954
+ or not isinstance(value_node.args[0], ast.Constant)
1955
+ or not isinstance(value_node.args[0].value, str)):
1956
+ raise ParseError(
1957
+ "native_member() takes exactly 1 positional string argument",
1958
+ stmt,
1959
+ )
1960
+ if has_explicit:
1961
+ raise ParseError(
1962
+ "Mixed native_member() and explicit values are not "
1963
+ "supported; use all native_member()/auto() or all "
1964
+ "explicit values",
1965
+ stmt,
1966
+ )
1967
+ has_auto = True
1968
+ if auto_form is None:
1969
+ auto_form = "native_member()"
1970
+ cpp_member_names[member_name] = value_node.args[0].value
1971
+ members.append((member_name, auto_value, self._loc(stmt)))
1972
+ auto_value += 1
1973
+ continue
1974
+ raise ParseError(
1975
+ "Enum member value must be an integer literal, auto(), "
1976
+ "or native_member(\"...\") (@native only)", stmt)
1977
+
1978
+ # Integer literal (positive)
1979
+ if isinstance(value_node, ast.Constant) and isinstance(value_node.value, int) and not isinstance(value_node.value, bool):
1980
+ if has_auto:
1981
+ raise ParseError(
1982
+ f"Mixed {auto_form} and explicit values are not yet supported; "
1983
+ f"use all {auto_form} or all explicit values",
1984
+ stmt,
1985
+ )
1986
+ has_explicit = True
1987
+ members.append((member_name, value_node.value, self._loc(stmt)))
1988
+ # Negative integer: -N
1989
+ elif (isinstance(value_node, ast.UnaryOp) and isinstance(value_node.op, ast.USub)
1990
+ and isinstance(value_node.operand, ast.Constant)
1991
+ and isinstance(value_node.operand.value, int)):
1992
+ if has_auto:
1993
+ raise ParseError(
1994
+ f"Mixed {auto_form} and explicit values are not yet supported; "
1995
+ f"use all {auto_form} or all explicit values",
1996
+ stmt,
1997
+ )
1998
+ has_explicit = True
1999
+ members.append((member_name, -value_node.operand.value, self._loc(stmt)))
2000
+ else:
2001
+ raise ParseError(
2002
+ "Enum member value must be an integer literal or auto()", stmt)
2003
+
2004
+ if not members:
2005
+ raise ParseError(f"Enum '{node.name}' must have at least one member", node)
2006
+
2007
+ return TpyEnum(
2008
+ name=node.name, members=members,
2009
+ is_int_enum=is_int_enum, underlying_type_name=underlying_type_name,
2010
+ is_native=is_native, native_name=native_name,
2011
+ cpp_member_names=cpp_member_names,
2012
+ has_explicit_values=has_explicit,
2013
+ loc=self._loc(node),
2014
+ )
2015
+
2016
+ _RECORD_LINKAGE_MAP: dict[str, RecordLinkage] = {
2017
+ qnames.NATIVE: RecordLinkage.NATIVE,
2018
+ }
2019
+
2020
+ _METHOD_LINKAGE_MAP: dict[str, FunctionLinkage] = {
2021
+ qnames.NATIVE: FunctionLinkage.NATIVE,
2022
+ }
2023
+
2024
+ def _parse_method(self, node: ast.FunctionDef, class_name: str, type_param_scope: dict[str, TypeParamKind] | None = None, property_names: set[str] | None = None) -> TpyFunction:
2025
+ """Parse a method definition."""
2026
+ # Check decorators (@staticmethod, @readonly, @native("cpp_name"), @override)
2027
+ is_staticmethod = False
2028
+ is_readonly = False
2029
+ readonly_opt_out = False
2030
+ is_pure = False
2031
+ is_inline = False
2032
+ is_override = False
2033
+ is_overload_stub = False
2034
+ auto_readonly = False
2035
+ auto_readonly_dec = None
2036
+ error_return: str | None = None
2037
+ method_linkage = FunctionLinkage.DEFAULT
2038
+ native_name: str | None = None
2039
+ native_function: bool = False
2040
+ native_preserves_refs: bool = False
2041
+ cpp_template: str | None = None
2042
+ is_property_getter = False
2043
+ is_property_setter = False
2044
+ property_setter_name: str | None = None
2045
+ native_cpp_return_type: str | None = None
2046
+ for dec in node.decorator_list:
2047
+ # Detect @prop_name.setter / @prop_name.deleter before general resolution
2048
+ func_node_check = dec.func if isinstance(dec, ast.Call) else dec
2049
+ if (isinstance(func_node_check, ast.Attribute)
2050
+ and isinstance(func_node_check.value, ast.Name)
2051
+ and property_names
2052
+ and func_node_check.value.id in property_names):
2053
+ if func_node_check.attr == "setter":
2054
+ is_property_setter = True
2055
+ property_setter_name = func_node_check.value.id
2056
+ continue
2057
+ if func_node_check.attr == "deleter":
2058
+ raise ParseError(f"@property deleter is not supported", dec)
2059
+ qname, arg = self._require_decorator(dec, f"method '{node.name}'")
2060
+ pos, kw = self._validate_decorator_args(qname, arg, dec)
2061
+ if qname == qnames.STATICMETHOD:
2062
+ is_staticmethod = True
2063
+ elif qname == qnames.PROPERTY:
2064
+ is_property_getter = True
2065
+ elif qname == qnames.PURE:
2066
+ is_pure = True
2067
+ elif qname == qnames.INLINE:
2068
+ is_inline = True
2069
+ elif qname == qnames.OVERRIDE:
2070
+ is_override = True
2071
+ elif qname == qnames.OVERLOAD:
2072
+ is_overload_stub = True
2073
+ elif qname == qnames.READONLY:
2074
+ is_readonly, readonly_opt_out = self._parse_readonly_arg(pos, dec)
2075
+ elif qname == qnames.AUTO_READONLY:
2076
+ auto_readonly = True
2077
+ auto_readonly_dec = dec
2078
+ elif qname == qnames.ERROR_RETURN:
2079
+ error_return = pos.name
2080
+ elif qname == qnames.CPP_TEMPLATE:
2081
+ cpp_template = pos
2082
+ elif qname == qnames.NATIVE_PRESERVES_REFS:
2083
+ native_preserves_refs = True
2084
+ elif qname in self._METHOD_LINKAGE_MAP:
2085
+ method_linkage = self._METHOD_LINKAGE_MAP[qname]
2086
+ if isinstance(pos, tuple):
2087
+ raise ParseError(
2088
+ f"@{bare_name(qname)}() decorator kwargs not parsed "
2089
+ f"(schema unavailable -- ensure _bootstrap._extern is imported "
2090
+ f"before modules that use decorator kwargs)", dec)
2091
+ # binding="C" overrides method linkage
2092
+ binding = kw.get("binding", "")
2093
+ if binding == "C" and method_linkage == FunctionLinkage.NATIVE:
2094
+ method_linkage = FunctionLinkage.NATIVE_C
2095
+ elif binding and binding != "" and binding != "C":
2096
+ raise ParseError(
2097
+ f"@{bare_name(qname)}(binding=...) only supports binding=\"C\"", dec)
2098
+ native_name = pos
2099
+ native_function = kw.get("function", False)
2100
+ cpp_rt = kw.get("cpp_return_type")
2101
+ if isinstance(cpp_rt, _NameArg):
2102
+ native_cpp_return_type = cpp_rt.name
2103
+ elif cpp_rt is not None:
2104
+ raise ParseError(
2105
+ f"@{bare_name(qname)}(cpp_return_type=...) requires a type name", dec)
2106
+ else:
2107
+ dec_name = self._decorator_local_name(dec) or "?"
2108
+ raise ParseError(f"Unknown decorator '{dec_name}' on method '{node.name}'", dec)
2109
+ if is_override and is_staticmethod:
2110
+ raise ParseError(f"@override cannot be combined with @staticmethod on method '{node.name}'", node)
2111
+ if is_property_getter:
2112
+ if is_staticmethod:
2113
+ raise ParseError(f"@property cannot be combined with @staticmethod on method '{node.name}'", node)
2114
+ # Getter: only self param, must have return type
2115
+ params_without_self = [a for a in node.args.args if a.arg != "self"]
2116
+ if params_without_self:
2117
+ raise ParseError(f"@property getter '{node.name}' must take only 'self' parameter", node)
2118
+ if node.returns is None:
2119
+ raise ParseError(f"@property getter '{node.name}' must have a return type annotation", node)
2120
+ if is_property_setter:
2121
+ if is_staticmethod:
2122
+ raise ParseError(f"@property setter cannot be combined with @staticmethod on method '{node.name}'", node)
2123
+ # Setter: self + one value param
2124
+ params_without_self = [a for a in node.args.args if a.arg != "self"]
2125
+ if len(params_without_self) != 1:
2126
+ raise ParseError(f"@property setter '{node.name}' must take exactly one value parameter (plus self)", node)
2127
+ if auto_readonly:
2128
+ if is_readonly:
2129
+ raise ParseError(f"@auto_readonly cannot be combined with @readonly on method '{node.name}'", auto_readonly_dec)
2130
+ if is_staticmethod:
2131
+ raise ParseError(f"@auto_readonly cannot be combined with @staticmethod on method '{node.name}'", auto_readonly_dec)
2132
+ if node.name in ("__init__", "__del__"):
2133
+ raise ParseError(f"@auto_readonly is not valid on '{node.name}'", auto_readonly_dec)
2134
+
2135
+ # Extract method-level type parameters (e.g. def foo[T](self, x: T) -> T:)
2136
+ method_type_params: list[str] = []
2137
+ method_type_param_bounds: dict[str, 'TpyType | TypeRefNode'] = {}
2138
+ if hasattr(node, 'type_params') and node.type_params:
2139
+ for tp in node.type_params:
2140
+ if isinstance(tp, ast.TypeVar):
2141
+ method_type_params.append(tp.name)
2142
+ if tp.bound is not None:
2143
+ # Protocol bound -- resolution + validation deferred to sema.
2144
+ method_type_param_bounds[tp.name] = self._parse_type_ref(tp.bound)
2145
+ else:
2146
+ raise ParseError(f"Only simple type parameters supported, got {type(tp).__name__}", node)
2147
+
2148
+ # Early @auto_readonly + method type params guard. Sema catches
2149
+ # the self-annotation / per-param paths, but running this at parse
2150
+ # time keeps the diagnostic pinned to the decorator's line rather
2151
+ # than getting shadowed by later parse-time checks (stub bodies,
2152
+ # body validation, etc.).
2153
+ if auto_readonly_dec is not None and method_type_params:
2154
+ raise ParseError(
2155
+ f"auto_readonly on methods with method-level type parameters is not yet supported ('{node.name}')",
2156
+ auto_readonly_dec,
2157
+ )
2158
+
2159
+ # Merge class-level and method-level type param scopes
2160
+ if method_type_params:
2161
+ merged_scope = dict(type_param_scope) if type_param_scope else {}
2162
+ for tp_name in method_type_params:
2163
+ merged_scope[tp_name] = TypeParamKind.TYPE
2164
+ type_param_scope = merged_scope
2165
+
2166
+ params = []
2167
+ has_self = not is_staticmethod
2168
+ self_annotation = None # Resolved self type; consumed by sema.method_expansion.
2169
+ n_non_self = len(node.args.args) - (1 if has_self else 0)
2170
+ is_exit_method = node.name == "__exit__" and has_self and n_non_self == 3
2171
+ args_iter = iter(enumerate(node.args.args))
2172
+ for i, arg in args_iter:
2173
+ if i == 0 and has_self:
2174
+ # Non-static methods must have 'self' as first parameter
2175
+ if arg.arg != "self":
2176
+ raise ParseError(f"First parameter of method '{node.name}' must be 'self'", node)
2177
+ # Emit the self annotation as a TypeRefNode; sema
2178
+ # resolves it in `resolve_refs` and
2179
+ # `method_expansion.expand_methods` validates the
2180
+ # shape + derives flags.
2181
+ if arg.annotation is not None:
2182
+ self_annotation = self._parse_type_ref(arg.annotation, type_param_scope)
2183
+ continue
2184
+ if arg.annotation is None:
2185
+ if is_exit_method:
2186
+ # v1.5: __exit__(self, exc_type, exc_val, exc_tb). Positional
2187
+ # convention; unannotated params get synthesized types.
2188
+ # exc_type / exc_tb are always None in v1.5 (no traceback or
2189
+ # type-object machinery); exc_val carries the exception on
2190
+ # the exceptional path, None on normal exit.
2191
+ exit_idx = i - 1 if has_self else i
2192
+ loc = self._loc(arg)
2193
+ if exit_idx == 1:
2194
+ synth = TpyUnionRef(
2195
+ members=(TpyTypeRef("BaseException", (), loc),
2196
+ TpyTypeRef("None", (), loc)),
2197
+ loc=loc,
2198
+ )
2199
+ else:
2200
+ synth = TpyTypeRef("None", (), loc)
2201
+ params.append((arg.arg, synth))
2202
+ continue
2203
+ raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
2204
+ param_type = self._parse_type_ref(arg.annotation, type_param_scope)
2205
+ params.append((arg.arg, param_type))
2206
+
2207
+ # __exit__ must have exactly 3 params (exc_type, exc_val, exc_tb) to
2208
+ # match CPython's context manager protocol.
2209
+ if node.name == "__exit__" and has_self and n_non_self != 3:
2210
+ raise ParseError(
2211
+ f"__exit__ must have 3 parameters: "
2212
+ f"__exit__(self, exc_type, exc_val, exc_tb)",
2213
+ node,
2214
+ )
2215
+
2216
+ # Parse *args parameter
2217
+ vararg_name = None
2218
+ vararg_type = None
2219
+ if node.args.vararg is not None:
2220
+ va = node.args.vararg
2221
+ if is_overload_stub:
2222
+ raise ParseError("*args is not supported on @overload stubs", node)
2223
+ if va.annotation is None:
2224
+ raise ParseError(
2225
+ f"*{va.arg} must have a type annotation (element type)", node)
2226
+ vararg_name = va.arg
2227
+ vararg_type = self._parse_type_ref(va.annotation, type_param_scope)
2228
+
2229
+ # Parse keyword-only parameters (after * or *args)
2230
+ keyword_only_start = None
2231
+ if node.args.kwonlyargs:
2232
+ keyword_only_start = len(params)
2233
+ for arg in node.args.kwonlyargs:
2234
+ if arg.annotation is None:
2235
+ raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
2236
+ param_type = self._parse_type_ref(arg.annotation, type_param_scope)
2237
+ params.append((arg.arg, param_type))
2238
+
2239
+ # **kwargs: Unpack[TypedDict]
2240
+ kwarg_name = None
2241
+ kwarg_type = None
2242
+ if node.args.kwarg is not None:
2243
+ kwarg_node = node.args.kwarg
2244
+ if kwarg_node.annotation is None:
2245
+ raise ParseError("**kwargs must have Unpack[TypedDict] annotation", node)
2246
+ kwarg_type = self._parse_unpack_annotation(kwarg_node.annotation, type_param_scope, node)
2247
+ kwarg_name = kwarg_node.arg
2248
+
2249
+ # Parse default parameter values (skip_self for non-static methods)
2250
+ defaults = self._parse_param_defaults(node, params, skip_self=has_self,
2251
+ type_param_scope=type_param_scope,
2252
+ kw_defaults=node.args.kw_defaults,
2253
+ n_kwonly=len(node.args.kwonlyargs))
2254
+
2255
+ # Return type: None means no annotation; sema substitutes VOID.
2256
+ # __init__ is kept at None here (no annotation by construction);
2257
+ # sema treats it the same way.
2258
+ return_type: 'TpyType | TypeRefNode | None' = None
2259
+ if node.name != "__init__" and node.returns:
2260
+ return_type = self._parse_type_ref(node.returns, type_param_scope)
2261
+
2262
+ if cpp_template is not None:
2263
+ if not self._is_stub_body(node.body):
2264
+ raise ParseError(
2265
+ f"@cpp_template method '{node.name}' must have `...` body", node)
2266
+ is_stub_body = self._is_stub_body(node.body)
2267
+ is_overload_stub_body = is_stub_body or self._is_pass_body(node.body)
2268
+ is_stub = (is_stub_body and not is_overload_stub) or cpp_template is not None
2269
+ if is_overload_stub:
2270
+ # @overload methods may be bodyless (`...` / `pass`, paired with a
2271
+ # trailing impl) or carry their own body (self-contained overload
2272
+ # variant -- sema validates that a group is all-bodied or all-bodyless).
2273
+ body = [] if is_overload_stub_body else self._parse_body(node.body)
2274
+ elif is_stub:
2275
+ body = []
2276
+ else:
2277
+ body = self._parse_body(node.body)
2278
+
2279
+ # Detect generator methods (yield in body)
2280
+ is_generator = _body_contains_yield(body)
2281
+ if is_generator:
2282
+ if node.name in ("__init__", "__del__"):
2283
+ raise ParseError(f"'{node.name}' cannot be a generator method", node)
2284
+ if is_staticmethod:
2285
+ raise ParseError(f"@staticmethod method '{node.name}' cannot be a generator", node)
2286
+ _check_no_return_value_in_generator(body, node.name)
2287
+
2288
+ # __next__ methods implicitly get @error_return(StopIteration)
2289
+ if node.name == "__next__" and error_return is None:
2290
+ error_return = "StopIteration"
2291
+
2292
+ is_async = isinstance(node, ast.AsyncFunctionDef)
2293
+ if is_async:
2294
+ # Mirror the free async def exclusion rules from _parse_def.
2295
+ if is_generator:
2296
+ raise ParseError(
2297
+ f"async generators (async def + yield) are not yet supported "
2298
+ f"on async method '{node.name}' of '{class_name}'", node)
2299
+ if error_return is not None:
2300
+ raise ParseError(
2301
+ f"async def + @error_return is not yet supported on "
2302
+ f"async method '{node.name}' of '{class_name}'", node)
2303
+ if method_linkage != FunctionLinkage.DEFAULT:
2304
+ display = self._LINKAGE_DISPLAY_NAMES.get(
2305
+ method_linkage, method_linkage.value)
2306
+ raise ParseError(
2307
+ f"async def + @{display} is not yet supported on "
2308
+ f"async method '{node.name}' of '{class_name}'", node)
2309
+ if is_property_getter or is_property_setter:
2310
+ raise ParseError(
2311
+ f"async @property is not yet supported on "
2312
+ f"'{node.name}' of '{class_name}'", node)
2313
+ if is_staticmethod:
2314
+ raise ParseError(
2315
+ f"async @staticmethod is not yet supported on "
2316
+ f"'{node.name}' of '{class_name}'", node)
2317
+
2318
+ method = TpyFunction(
2319
+ name=node.name,
2320
+ params=params,
2321
+ return_type=return_type,
2322
+ body=body,
2323
+ is_method=True,
2324
+ is_async=is_async,
2325
+ is_staticmethod=is_staticmethod,
2326
+ is_property_getter=is_property_getter,
2327
+ is_property_setter=is_property_setter,
2328
+ property_name=property_setter_name,
2329
+ is_inline=is_inline,
2330
+ is_readonly=is_readonly,
2331
+ readonly_opt_out=readonly_opt_out,
2332
+ is_pure=is_pure,
2333
+ has_auto_readonly_decorator=auto_readonly_dec is not None,
2334
+ is_override=is_override,
2335
+ is_overload_stub=is_overload_stub,
2336
+ is_stub=is_overload_stub_body if is_overload_stub else is_stub,
2337
+ linkage=method_linkage,
2338
+ native_name=native_name,
2339
+ native_function=native_function,
2340
+ native_preserves_refs=native_preserves_refs,
2341
+ native_cpp_return_type=native_cpp_return_type,
2342
+ cpp_template=cpp_template,
2343
+ type_params=method_type_params,
2344
+ type_param_bounds=method_type_param_bounds,
2345
+ defaults=defaults,
2346
+ keyword_only_start=keyword_only_start,
2347
+ vararg_name=vararg_name,
2348
+ vararg_type=vararg_type,
2349
+ kwarg_name=kwarg_name,
2350
+ kwarg_type=kwarg_type,
2351
+ error_return=error_return,
2352
+ is_generator=is_generator,
2353
+ self_annotation=self_annotation,
2354
+ loc=self._loc(node)
2355
+ )
2356
+ return method
2357
+
2358
+ _FUNCTION_LINKAGE_MAP: dict[str, FunctionLinkage] = {
2359
+ qnames.NATIVE: FunctionLinkage.NATIVE,
2360
+ qnames.EXPORT: FunctionLinkage.EXPORT_C,
2361
+ }
2362
+
2363
+ # User-visible decorator names for error messages
2364
+ _LINKAGE_DISPLAY_NAMES: dict[FunctionLinkage, str] = {
2365
+ FunctionLinkage.NATIVE: "native",
2366
+ FunctionLinkage.NATIVE_C: 'native(binding="C")',
2367
+ FunctionLinkage.EXPORT_C: "export",
2368
+ }
2369
+
2370
+ def _parse_function(self, node: 'ast.FunctionDef | ast.AsyncFunctionDef') -> TpyFunction:
2371
+ """Parse a function definition (sync or async).
2372
+
2373
+ async def f() lowers to a state-machine struct conforming to
2374
+ Awaitable[T] in PR 3 (codegen). v1 sema rejects async + @error_return,
2375
+ async + @noalloc, async + yield (async generators), and user
2376
+ __await__ methods -- each as 'not yet supported'."""
2377
+ is_noalloc = False
2378
+ is_inline = False
2379
+ is_readonly = False
2380
+ readonly_opt_out = False
2381
+ is_pure = False
2382
+ is_overload_stub = False
2383
+ value_ptr_coercion = False
2384
+ error_return: str | None = None
2385
+ builtin_decorator_key: str | None = None
2386
+ builtin_function_key: str | None = None
2387
+ type_param_defaults: dict[str, str] = {}
2388
+ linkage = FunctionLinkage.DEFAULT
2389
+ native_name: str | None = None
2390
+ cpp_template: str | None = None
2391
+ native_cpp_return_type: str | None = None
2392
+ for dec in node.decorator_list:
2393
+ qname, arg = self._require_decorator(dec, f"function '{node.name}'")
2394
+ if qname == qnames.TYPE_PARAM_DEFAULT:
2395
+ type_param_defaults = self._parse_type_param_default(dec)
2396
+ continue
2397
+ pos, kw = self._validate_decorator_args(qname, arg, dec)
2398
+ if qname == qnames.NOALLOC:
2399
+ is_noalloc = True
2400
+ elif qname == qnames.INLINE:
2401
+ is_inline = True
2402
+ elif qname == qnames.PURE:
2403
+ is_pure = True
2404
+ elif qname == qnames.READONLY:
2405
+ is_readonly, readonly_opt_out = self._parse_readonly_arg(pos, dec)
2406
+ elif qname == qnames.AUTO_READONLY:
2407
+ raise ParseError("@auto_readonly is only valid on methods, not free functions", dec)
2408
+ elif qname == qnames.OVERLOAD:
2409
+ is_overload_stub = True
2410
+ elif qname == qnames.ERROR_RETURN:
2411
+ error_return = pos.name
2412
+ elif qname == qnames.VALUE_PTR_COERCION:
2413
+ value_ptr_coercion = True
2414
+ elif qname == qnames.CPP_TEMPLATE:
2415
+ cpp_template = pos
2416
+ elif qname == qnames.BUILTIN_DECORATOR:
2417
+ builtin_decorator_key = pos
2418
+ elif qname == qnames.BUILTIN_FUNCTION:
2419
+ builtin_function_key = pos
2420
+ elif qname in self._FUNCTION_LINKAGE_MAP:
2421
+ new_linkage = self._FUNCTION_LINKAGE_MAP[qname]
2422
+ if linkage != FunctionLinkage.DEFAULT:
2423
+ old_name = self._LINKAGE_DISPLAY_NAMES.get(linkage, linkage.value)
2424
+ new_name = self._LINKAGE_DISPLAY_NAMES.get(new_linkage, new_linkage.value)
2425
+ raise ParseError(
2426
+ f"Function '{node.name}' cannot have both @{old_name} and @{new_name}", node)
2427
+ if kw.get("function"):
2428
+ dec_name = self._decorator_local_name(dec)
2429
+ raise ParseError(f"@{dec_name}(function=...) is only valid on methods, not free functions", dec)
2430
+ # binding="C" overrides linkage to C variant
2431
+ binding = kw.get("binding", "")
2432
+ if binding == "C":
2433
+ if new_linkage == FunctionLinkage.NATIVE:
2434
+ new_linkage = FunctionLinkage.NATIVE_C
2435
+ elif binding and binding != "":
2436
+ raise ParseError(
2437
+ f"@{bare_name(qname)}(binding=...) only supports binding=\"C\"", dec)
2438
+ # @export requires binding="C"
2439
+ if qname == qnames.EXPORT and binding != "C":
2440
+ raise ParseError(
2441
+ f"@export requires binding=\"C\"", dec)
2442
+ linkage = new_linkage
2443
+ if isinstance(pos, tuple):
2444
+ raise ParseError(
2445
+ f"@{bare_name(qname)}() decorator kwargs not parsed "
2446
+ f"(schema unavailable -- ensure _bootstrap._extern is imported "
2447
+ f"before modules that use decorator kwargs)", dec)
2448
+ native_name = pos
2449
+ cpp_rt = kw.get("cpp_return_type")
2450
+ if isinstance(cpp_rt, _NameArg):
2451
+ native_cpp_return_type = cpp_rt.name
2452
+ elif cpp_rt is not None:
2453
+ raise ParseError(
2454
+ f"@{bare_name(qname)}(cpp_return_type=...) requires a type name", dec)
2455
+ else:
2456
+ dec_name = self._decorator_local_name(dec) or "?"
2457
+ raise ParseError(f"Unknown decorator '{dec_name}' on function '{node.name}'", dec)
2458
+
2459
+ # Extract type parameters from Python 3.12+ syntax: def foo[T, U]():
2460
+ # Bounds: def foo[T: Comparable](): (protocol bound)
2461
+ # INT params: def foo[T, N: int](): (integer type parameter, e.g. for Array[T, N])
2462
+ type_params = []
2463
+ type_param_bounds: dict[str, 'TpyType | TypeRefNode'] = {}
2464
+ type_param_kinds: list[TypeParamKind] = []
2465
+ if hasattr(node, 'type_params') and node.type_params:
2466
+ for tp in node.type_params:
2467
+ if isinstance(tp, ast.TypeVar):
2468
+ type_params.append(tp.name)
2469
+ if tp.bound is not None:
2470
+ if isinstance(tp.bound, ast.Name) and tp.bound.id == 'int':
2471
+ type_param_kinds.append(TypeParamKind.INT)
2472
+ else:
2473
+ type_param_kinds.append(TypeParamKind.TYPE)
2474
+ # Protocol bound -- resolution + validation deferred to sema.
2475
+ type_param_bounds[tp.name] = self._parse_type_ref(tp.bound)
2476
+ else:
2477
+ type_param_kinds.append(TypeParamKind.TYPE)
2478
+ else:
2479
+ raise ParseError(f"Only simple type parameters supported, got {type(tp).__name__}", node)
2480
+
2481
+ # Set scope for parsing parameter and return types
2482
+ type_param_scope = (
2483
+ {tp: kind for tp, kind in zip(type_params, type_param_kinds)}
2484
+ if type_params else None
2485
+ )
2486
+ old_scope = self._type_param_scope
2487
+ self._type_param_scope = type_param_scope
2488
+
2489
+ params = []
2490
+ if builtin_function_key is not None:
2491
+ # @builtin_function stubs: params are illustrative only,
2492
+ # type annotations not required (sema handles everything)
2493
+ for arg in node.args.args:
2494
+ # @builtin_function params are illustrative stubs; sema
2495
+ # handles the real types specially. Missing annotations
2496
+ # emit a `TpyTypeRef("None")` placeholder that the
2497
+ # resolver maps to VOID.
2498
+ param_type: 'TpyType | TypeRefNode' = (
2499
+ self._parse_type_ref(arg.annotation, type_param_scope)
2500
+ if arg.annotation
2501
+ else TpyTypeRef(name="None", loc=self._loc(arg))
2502
+ )
2503
+ params.append((arg.arg, param_type))
2504
+ else:
2505
+ for arg in node.args.args:
2506
+ if arg.annotation is None:
2507
+ raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
2508
+ param_type = self._parse_type_ref(arg.annotation, type_param_scope)
2509
+ params.append((arg.arg, param_type))
2510
+
2511
+ # Parse *args parameter
2512
+ vararg_name = None
2513
+ vararg_type = None
2514
+ if node.args.vararg is not None:
2515
+ va = node.args.vararg
2516
+ if builtin_function_key is None:
2517
+ if is_overload_stub:
2518
+ raise ParseError("*args is not supported on @overload stubs", node)
2519
+ if va.annotation is None:
2520
+ raise ParseError(
2521
+ f"*{va.arg} must have a type annotation (element type)", node)
2522
+ vararg_name = va.arg
2523
+ vararg_type = self._parse_type_ref(va.annotation, type_param_scope)
2524
+
2525
+ # Parse keyword-only parameters (after * or *args)
2526
+ keyword_only_start = None
2527
+ if node.args.kwonlyargs:
2528
+ keyword_only_start = len(params)
2529
+ for arg in node.args.kwonlyargs:
2530
+ if arg.annotation is None:
2531
+ raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
2532
+ param_type = self._parse_type_ref(arg.annotation, type_param_scope)
2533
+ params.append((arg.arg, param_type))
2534
+ elif vararg_name is None and node.args.vararg is not None:
2535
+ # bare * separator with no kwonlyargs -- unusual but valid Python
2536
+ pass
2537
+ # If there's a bare * (no vararg name but kwonlyargs exist), keyword_only_start is set above.
2538
+ # If there's *args, kwonlyargs after it are also keyword-only (set above).
2539
+
2540
+ # **kwargs: Unpack[TypedDict]
2541
+ kwarg_name = None
2542
+ kwarg_type = None
2543
+ if node.args.kwarg is not None and builtin_function_key is None:
2544
+ kwarg_node = node.args.kwarg
2545
+ if kwarg_node.annotation is None:
2546
+ raise ParseError("**kwargs must have Unpack[TypedDict] annotation", node)
2547
+ kwarg_type = self._parse_unpack_annotation(kwarg_node.annotation, type_param_scope, node)
2548
+ kwarg_name = kwarg_node.arg
2549
+
2550
+ # Parse default parameter values
2551
+ defaults = self._parse_param_defaults(node, params, skip_self=False,
2552
+ type_param_scope=type_param_scope,
2553
+ kw_defaults=node.args.kw_defaults,
2554
+ n_kwonly=len(node.args.kwonlyargs))
2555
+
2556
+ # None means no annotation; sema substitutes VOID.
2557
+ return_type: 'TpyType | TypeRefNode | None' = None
2558
+ if node.returns:
2559
+ return_type = self._parse_type_ref(node.returns, type_param_scope)
2560
+
2561
+ # Validate body vs linkage
2562
+ is_stub_body = self._is_stub_body(node.body)
2563
+ is_overload_stub_body = is_stub_body or self._is_pass_body(node.body)
2564
+ is_stub = False
2565
+
2566
+ if builtin_decorator_key is not None:
2567
+ if not self._is_stub_body(node.body):
2568
+ raise ParseError(
2569
+ f"@builtin_decorator function '{node.name}' must have `...` body", node)
2570
+ is_stub = True
2571
+ body = []
2572
+ elif builtin_function_key is not None:
2573
+ is_stub = True
2574
+ body = []
2575
+ elif cpp_template is not None:
2576
+ if not self._is_stub_body(node.body):
2577
+ raise ParseError(
2578
+ f"@cpp_template function '{node.name}' must have `...` body", node)
2579
+ is_stub = True
2580
+ body = []
2581
+ elif is_overload_stub:
2582
+ # @overload functions may be bodyless (`...` / `pass`, paired with a
2583
+ # trailing impl) or carry their own body (self-contained overload
2584
+ # variant -- sema validates that a group is all-bodied or all-bodyless).
2585
+ body = [] if is_overload_stub_body else self._parse_body(node.body)
2586
+ elif linkage in (FunctionLinkage.NATIVE, FunctionLinkage.NATIVE_C):
2587
+ if not (self._is_stub_body(node.body)):
2588
+ display = self._LINKAGE_DISPLAY_NAMES.get(linkage, linkage.value)
2589
+ raise ParseError(
2590
+ f"@{display} function '{node.name}' must have `...` body (it declares an external symbol)",
2591
+ node)
2592
+ is_stub = True
2593
+ body = []
2594
+ elif linkage == FunctionLinkage.EXPORT_C:
2595
+ if is_stub_body:
2596
+ raise ParseError(
2597
+ f"@export function '{node.name}' must have a body (it exports a TPy function)",
2598
+ node)
2599
+ body = self._parse_body(node.body)
2600
+ else:
2601
+ body = self._parse_body(node.body)
2602
+
2603
+ # Restore the scope
2604
+ self._type_param_scope = old_scope
2605
+
2606
+ is_generator = _body_contains_yield(body)
2607
+ if is_generator:
2608
+ # Validate: no 'return value' inside generator
2609
+ _check_no_return_value_in_generator(body, node.name)
2610
+
2611
+ is_async = isinstance(node, ast.AsyncFunctionDef)
2612
+ if is_async:
2613
+ # v1 exclusion rules: each is "not yet supported", not "forbidden
2614
+ # forever". The async-generator case (yield in async def) and
2615
+ # @error_return / @noalloc combinations are deferred to v3+ per
2616
+ # docs/ASYNC_DESIGN.md ("Mutually exclusive with @error_return /
2617
+ # @noalloc / yield in v1").
2618
+ if is_generator:
2619
+ raise ParseError(
2620
+ f"async generators (async def + yield) are not yet supported "
2621
+ f"on async function '{node.name}'", node)
2622
+ if error_return is not None:
2623
+ raise ParseError(
2624
+ f"async def + @error_return is not yet supported on '{node.name}'",
2625
+ node)
2626
+ if is_noalloc:
2627
+ raise ParseError(
2628
+ f"async def + @noalloc is not yet supported on '{node.name}'",
2629
+ node)
2630
+ if linkage != FunctionLinkage.DEFAULT:
2631
+ display = self._LINKAGE_DISPLAY_NAMES.get(linkage, linkage.value)
2632
+ raise ParseError(
2633
+ f"async def + @{display} is not yet supported on '{node.name}'",
2634
+ node)
2635
+
2636
+ return TpyFunction(
2637
+ name=node.name,
2638
+ params=params,
2639
+ return_type=return_type,
2640
+ body=body,
2641
+ is_noalloc=is_noalloc,
2642
+ is_inline=is_inline,
2643
+ is_readonly=is_readonly,
2644
+ readonly_opt_out=readonly_opt_out,
2645
+ is_pure=is_pure,
2646
+ is_overload_stub=is_overload_stub,
2647
+ linkage=linkage,
2648
+ native_name=native_name,
2649
+ native_cpp_return_type=native_cpp_return_type,
2650
+ cpp_template=cpp_template,
2651
+ is_stub=is_overload_stub_body if is_overload_stub else is_stub,
2652
+ value_ptr_coercion=value_ptr_coercion,
2653
+ type_params=type_params,
2654
+ type_param_kinds=type_param_kinds,
2655
+ type_param_bounds=type_param_bounds,
2656
+ type_param_defaults=type_param_defaults,
2657
+ defaults=defaults,
2658
+ keyword_only_start=keyword_only_start,
2659
+ vararg_name=vararg_name,
2660
+ vararg_type=vararg_type,
2661
+ kwarg_name=kwarg_name,
2662
+ kwarg_type=kwarg_type,
2663
+ error_return=error_return,
2664
+ builtin_decorator_key=builtin_decorator_key,
2665
+ builtin_function_key=builtin_function_key,
2666
+ is_generator=is_generator,
2667
+ is_async=is_async,
2668
+ loc=self._loc(node)
2669
+ )
2670
+
2671
+ def _is_stub_body(self, body: list[ast.stmt]) -> bool:
2672
+ """Check if a function body is a stub (only `...`).
2673
+
2674
+ Only Ellipsis marks a declaration-only stub. `pass` is a valid
2675
+ no-op body that should still generate a definition.
2676
+ """
2677
+ if len(body) == 1:
2678
+ stmt = body[0]
2679
+ if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and stmt.value.value is ...:
2680
+ return True
2681
+ return False
2682
+
2683
+ @staticmethod
2684
+ def _is_pass_body(body: list[ast.stmt]) -> bool:
2685
+ """Check if a function body is only `pass`."""
2686
+ return len(body) == 1 and isinstance(body[0], ast.Pass)
2687
+
2688
+ def _parse_type_annotation(self, node: ast.expr, type_param_scope: dict[str, TypeParamKind] | None = None) -> TpyType:
2689
+ """Parse a type annotation to a TpyType.
2690
+
2691
+ Thin wrapper around the walker + resolver composition; kept for
2692
+ the parse-time sites that need a resolved type immediately
2693
+ (type-parameter bounds, FragmentParser).
2694
+ """
2695
+ if type_param_scope is None:
2696
+ type_param_scope = self._type_param_scope
2697
+ ref = self._parse_type_ref(node, type_param_scope)
2698
+ return self._resolve_type_ref_impl(ref, type_param_scope)
2699
+
2700
+ # ------------------------------------------------------------------
2701
+ # Type-reference walker
2702
+ #
2703
+ # `_parse_type_ref` mirrors `_parse_type_annotation`'s structural walk
2704
+ # but returns a TypeRefNode instead of a TpyType. It resolves names
2705
+ # only enough to disambiguate structural wrappers (Ptr/Own/Optional/
2706
+ # Callable/Fn/Literal/tuple/...) from user generics; leaf names
2707
+ # (primitives, user records, enums, protocols, type parameters) stay
2708
+ # raw in `TpyTypeRef.name` and are resolved by a later resolver pass.
2709
+ #
2710
+ # Validation errors that require resolved types (e.g. "Unknown type",
2711
+ # "Qualified name with missing module import", Own-in-union, readonly
2712
+ # normalization) are deliberately not raised here -- they belong in
2713
+ # the resolver. Only errors provable from syntax alone (malformed
2714
+ # Callable/Fn/Literal shape) are raised at this layer.
2715
+ # ------------------------------------------------------------------
2716
+
2717
+ def _parse_type_ref(
2718
+ self, node: ast.expr,
2719
+ type_param_scope: dict[str, TypeParamKind] | None = None,
2720
+ ) -> ResolverInputNode:
2721
+ """Walk an ast.expr for a type annotation and produce a ResolverInputNode
2722
+ (one of the four walker outputs: TpyTypeRef, TpyUnionRef,
2723
+ TpyCallableRef, TpyLiteralRef). TpyInferFromDefaultRef is emitted
2724
+ only at a single field-declaration site, never by this walker."""
2725
+ if type_param_scope is None:
2726
+ type_param_scope = self._type_param_scope
2727
+ loc = self._loc(node)
2728
+
2729
+ if isinstance(node, ast.Name):
2730
+ name = node.id
2731
+ # Eager nested-scope substitution so refs emitted inside a class
2732
+ # body survive to sema-time resolution -- _nested_type_scope is
2733
+ # parse-time-only state, empty by the time sema runs. E.g.
2734
+ # referencing `Kind` inside `class Message: class Kind: ...`
2735
+ # emits TpyTypeRef("Message.Kind", ...) directly.
2736
+ if name in self._nested_type_scope:
2737
+ return TpyTypeRef(self._nested_type_scope[name], (), loc)
2738
+ return TpyTypeRef(name, (), loc)
2739
+
2740
+ if isinstance(node, ast.Subscript):
2741
+ return self._parse_subscript_type_ref(node, type_param_scope, loc)
2742
+
2743
+ if isinstance(node, ast.Attribute):
2744
+ parts: list[str] = []
2745
+ cur: ast.expr = node
2746
+ while isinstance(cur, ast.Attribute):
2747
+ parts.append(cur.attr)
2748
+ cur = cur.value
2749
+ if isinstance(cur, ast.Name):
2750
+ parts.append(cur.id)
2751
+ parts.reverse()
2752
+ return TpyTypeRef(".".join(parts), (), loc)
2753
+ raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
2754
+
2755
+ if isinstance(node, ast.Constant) and node.value is None:
2756
+ return TpyTypeRef("None", (), loc)
2757
+
2758
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
2759
+ # PEP 484 forward-ref strings (`-> "ClassName"`) and every
2760
+ # annotation under `from __future__ import annotations` arrive
2761
+ # as string Constants. Inner nodes from the re-parse inherit
2762
+ # the outer Constant's location so diagnostics point at the
2763
+ # annotation site instead of line 1 col 0 of the standalone
2764
+ # parse. (ast.fix_missing_locations only fills missing values;
2765
+ # the re-parsed nodes already have lineno=1, so we overwrite.)
2766
+ src = node.value
2767
+ try:
2768
+ inner = ast.parse(src, mode='eval').body
2769
+ except SyntaxError as exc:
2770
+ raise ParseError(
2771
+ f"Cannot parse string type annotation {src!r}: {exc.msg}",
2772
+ node,
2773
+ )
2774
+ for child in ast.walk(inner):
2775
+ if hasattr(child, 'lineno'):
2776
+ child.lineno = node.lineno
2777
+ child.col_offset = node.col_offset
2778
+ child.end_lineno = node.end_lineno
2779
+ child.end_col_offset = node.end_col_offset
2780
+ return self._parse_type_ref(inner, type_param_scope)
2781
+
2782
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
2783
+ arms = _collect_bitor_arms(node)
2784
+ members = tuple(self._parse_type_ref(arm, type_param_scope) for arm in arms)
2785
+ return TpyUnionRef(members, loc)
2786
+
2787
+ raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
2788
+
2789
+ def _parse_subscript_type_ref(
2790
+ self, node: ast.Subscript,
2791
+ type_param_scope: dict[str, TypeParamKind] | None,
2792
+ loc: SourceLocation | None,
2793
+ ) -> ResolverInputNode:
2794
+ """Handle ast.Subscript: disambiguate structural wrappers from generics."""
2795
+ if isinstance(node.value, ast.Name):
2796
+ resolved = self._resolve_type_name(node.value.id)
2797
+ raw_name: str | None = node.value.id
2798
+ elif isinstance(node.value, ast.Attribute):
2799
+ resolved = self._resolve_qualified_type_name(node.value)
2800
+ # Build full dotted name from the Attribute chain so 3+ level
2801
+ # (a.b.c.D[T]) nested-class references reach the resolver's
2802
+ # _resolve_dotted_class_name_str path. 2-level stays identical to
2803
+ # the original "value.attr" form.
2804
+ parts: list[str] = []
2805
+ cur: ast.expr = node.value
2806
+ while isinstance(cur, ast.Attribute):
2807
+ parts.append(cur.attr)
2808
+ cur = cur.value
2809
+ if isinstance(cur, ast.Name):
2810
+ parts.append(cur.id)
2811
+ parts.reverse()
2812
+ raw_name = ".".join(parts)
2813
+ else:
2814
+ raw_name = None
2815
+ else:
2816
+ raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
2817
+
2818
+ # Canonical structural-wrapper names use a `:` separator so they
2819
+ # cannot collide with any Python identifier-based name (including
2820
+ # raw dotted user source like "typing.Optional" written without
2821
+ # `import typing`). The walker emits the canonical form only when
2822
+ # the name actually resolved to the expected module+name pair;
2823
+ # unresolved names fall through to the generic path with the raw
2824
+ # source form, and the resolver's unresolved-name errors fire.
2825
+ if resolved:
2826
+ module, original = resolved
2827
+ if module == "tpy":
2828
+ if original in ("Ptr", "Own", "readonly", "auto_readonly", "auto_own"):
2829
+ inner_ref = self._parse_type_ref(node.slice, type_param_scope)
2830
+ return TpyTypeRef(f"tpy:{original}", (inner_ref,), loc)
2831
+ if original == "Fn":
2832
+ return self._parse_callable_type_ref(node, "Fn", type_param_scope, loc)
2833
+ elif module == "builtins":
2834
+ if original == "tuple":
2835
+ slices = _extract_subscript_slices(node)
2836
+ elem_refs = tuple(
2837
+ self._parse_type_ref(s, type_param_scope) for s in slices
2838
+ )
2839
+ return TpyTypeRef("builtins:tuple", elem_refs, loc)
2840
+ elif module == "typing":
2841
+ if original == "Optional":
2842
+ inner_ref = self._parse_type_ref(node.slice, type_param_scope)
2843
+ return TpyTypeRef("typing:Optional", (inner_ref,), loc)
2844
+ if original == "Final":
2845
+ inner_ref = self._parse_type_ref(node.slice, type_param_scope)
2846
+ return TpyTypeRef("typing:Final", (inner_ref,), loc)
2847
+ if original == "ClassVar":
2848
+ inner_ref = self._parse_type_ref(node.slice, type_param_scope)
2849
+ return TpyTypeRef("typing:ClassVar", (inner_ref,), loc)
2850
+ if original == "Callable":
2851
+ return self._parse_callable_type_ref(node, "Callable", type_param_scope, loc)
2852
+ if original == "Literal":
2853
+ return self._parse_literal_type_ref(node, loc)
2854
+
2855
+ # Not a structural wrapper: generic nominal reference. Resolver handles
2856
+ # the nominal lookup. Keep the raw source name; if the container was
2857
+ # import-resolved we still emit the source form (not the resolved
2858
+ # canonical one) to keep this pass syntactic.
2859
+ name = raw_name if raw_name is not None else (resolved[1] if resolved else None)
2860
+ if name is None:
2861
+ raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
2862
+
2863
+ slices = _extract_subscript_slices(node)
2864
+ arg_refs: list[ResolverInputNode | int] = []
2865
+ for s in slices:
2866
+ if (isinstance(s, ast.Constant) and isinstance(s.value, int)
2867
+ and not isinstance(s.value, bool)):
2868
+ arg_refs.append(s.value)
2869
+ else:
2870
+ arg_refs.append(self._parse_type_ref(s, type_param_scope))
2871
+ return TpyTypeRef(name, tuple(arg_refs), loc)
2872
+
2873
+ def _parse_callable_type_ref(
2874
+ self, node: ast.Subscript, kind: str,
2875
+ type_param_scope: dict[str, TypeParamKind] | None,
2876
+ loc: SourceLocation | None,
2877
+ ) -> TpyCallableRef:
2878
+ slices = _extract_subscript_slices(node)
2879
+ if len(slices) != 2:
2880
+ raise ParseError(
2881
+ f"{kind} requires exactly 2 arguments: {kind}[[ParamTypes...], ReturnType]",
2882
+ node,
2883
+ )
2884
+ param_list_node, return_node = slices
2885
+ if not isinstance(param_list_node, ast.List):
2886
+ raise ParseError(
2887
+ f"{kind} parameter types must be a list: {kind}[[Int32, str], bool]",
2888
+ node,
2889
+ )
2890
+ params = tuple(
2891
+ self._parse_type_ref(p, type_param_scope) for p in param_list_node.elts
2892
+ )
2893
+ return_type = self._parse_type_ref(return_node, type_param_scope)
2894
+ return TpyCallableRef(kind=kind, params=params, return_type=return_type, loc=loc)
2895
+
2896
+ def _parse_literal_type_ref(
2897
+ self, node: ast.Subscript, loc: SourceLocation | None,
2898
+ ) -> TpyLiteralRef:
2899
+ slices = _extract_subscript_slices(node)
2900
+ if not slices:
2901
+ raise ParseError("Literal requires at least one argument", node)
2902
+ values: list[LiteralValue] = []
2903
+ tag: LiteralTag | None = None
2904
+ for s in slices:
2905
+ if isinstance(s, ast.Constant) and isinstance(s.value, str):
2906
+ if tag is not None and tag is not LiteralTag.STR:
2907
+ raise ParseError("Literal cannot mix value types", node)
2908
+ tag = LiteralTag.STR
2909
+ values.append(LiteralValue(LiteralTag.STR, s.value))
2910
+ elif isinstance(s, ast.Constant) and isinstance(s.value, bool):
2911
+ if tag is not None and tag is not LiteralTag.BOOL:
2912
+ raise ParseError("Literal cannot mix value types", node)
2913
+ tag = LiteralTag.BOOL
2914
+ values.append(LiteralValue(LiteralTag.BOOL, s.value))
2915
+ elif isinstance(s, ast.Constant) and isinstance(s.value, int):
2916
+ if tag is not None and tag is not LiteralTag.INT:
2917
+ raise ParseError("Literal cannot mix value types", node)
2918
+ tag = LiteralTag.INT
2919
+ values.append(LiteralValue(LiteralTag.INT, s.value))
2920
+ elif (isinstance(s, ast.UnaryOp) and isinstance(s.op, ast.USub)
2921
+ and isinstance(s.operand, ast.Constant)
2922
+ and isinstance(s.operand.value, int)
2923
+ and not isinstance(s.operand.value, bool)):
2924
+ if tag is not None and tag is not LiteralTag.INT:
2925
+ raise ParseError("Literal cannot mix value types", node)
2926
+ tag = LiteralTag.INT
2927
+ values.append(LiteralValue(LiteralTag.INT, -s.operand.value))
2928
+ else:
2929
+ raise ParseError(
2930
+ "Literal supports string, int, and bool arguments", node)
2931
+ return TpyLiteralRef(values=tuple(values), loc=loc)
2932
+
2933
+ def _resolve_type_ref_impl(
2934
+ self, ref: ResolverInputNode,
2935
+ type_param_scope: dict[str, TypeParamKind] | None = None,
2936
+ *, is_type_arg: bool = False,
2937
+ ) -> TpyType:
2938
+ """Thin delegate to `TypeResolver.resolve`. Retained for the
2939
+ walker-vs-resolver equivalence tests and for macro-fragment
2940
+ self-annotation resolution, both of which need a TpyType in
2941
+ hand synchronously.
2942
+
2943
+ `is_type_arg` see `TypeResolver.resolve`; pass True at value-
2944
+ bearing slots (params, vararg, self_annotation, fields, ...)
2945
+ so bare `None` lowers to NoneType. The function-return slot
2946
+ keeps the default `False` so `-> None` stays VoidType.
2947
+ """
2948
+ return self._resolver.resolve(ref, type_param_scope, is_type_arg=is_type_arg)
2949
+
2950
+ def _finalize_function_refs(
2951
+ self, func: 'TpyFunction',
2952
+ outer_scope: dict[str, TypeParamKind] | None = None,
2953
+ ) -> None:
2954
+ """Resolve TypeRefNodes in a TpyFunction's params / return_type /
2955
+ vararg_type / kwarg_type in place, using the current parser state.
2956
+
2957
+ `outer_scope` is the enclosing type-param scope at the call site
2958
+ (e.g. a nested def's enclosing function scope). The function's own
2959
+ `type_params` are merged on top before resolution, so a method's
2960
+ or nested def's own generic params resolve to TypeParamRef
2961
+ correctly. Pass this explicitly rather than relying on
2962
+ `self._type_param_scope` being in the right state -- callers
2963
+ typically invoke this after `_parse_function` has already
2964
+ restored the enclosing scope on the parser instance.
2965
+
2966
+ Used by non-top-level callers of `_parse_function` (nested defs,
2967
+ macro fragment parsing, @builtin_decorator stubs) that need
2968
+ TpyType immediately, without waiting for the post-parse
2969
+ `resolve_refs` pass. Top-level functions leave refs in place for
2970
+ that pass to resolve.
2971
+ """
2972
+ scope: dict[str, TypeParamKind] = dict(outer_scope or {})
2973
+ if func.type_params:
2974
+ kinds = func.type_param_kinds or []
2975
+ for i, name in enumerate(func.type_params):
2976
+ scope[name] = kinds[i] if i < len(kinds) else TypeParamKind.TYPE
2977
+ resolve_scope = scope or None
2978
+
2979
+ ref_types = (TpyTypeRef, TpyUnionRef, TpyCallableRef, TpyLiteralRef)
2980
+ new_params: list = []
2981
+ for name, t in func.params:
2982
+ if isinstance(t, ref_types):
2983
+ t = self._resolve_type_ref_impl(t, resolve_scope, is_type_arg=True)
2984
+ new_params.append((name, t))
2985
+ func.params = new_params
2986
+ if func.return_type is None:
2987
+ # "No annotation" sentinel -- route through the resolver as
2988
+ # `TpyTypeRef("None")` so the same VOID singleton is
2989
+ # substituted without a direct typesys import. The deferred
2990
+ # (sema-side) branch lives in
2991
+ # `resolve_refs._resolve_return_slot`; both paths converge
2992
+ # on VOID.
2993
+ func.return_type = self._resolve_type_ref_impl(
2994
+ TpyTypeRef(name="None"), resolve_scope)
2995
+ elif isinstance(func.return_type, ref_types):
2996
+ func.return_type = self._resolve_type_ref_impl(
2997
+ func.return_type, resolve_scope)
2998
+ if isinstance(func.vararg_type, ref_types):
2999
+ func.vararg_type = self._resolve_type_ref_impl(
3000
+ func.vararg_type, resolve_scope, is_type_arg=True)
3001
+ if isinstance(func.kwarg_type, ref_types):
3002
+ func.kwarg_type = self._resolve_type_ref_impl(
3003
+ func.kwarg_type, resolve_scope, is_type_arg=True)
3004
+
3005
+ def _parse_type_args_from_subscript(self, node: ast.Subscript) -> 'tuple[TpyType | TypeRefNode | None, ...]':
3006
+ """Extract type arguments from a subscript for generic function calls like first[Int32](x).
3007
+
3008
+ Raises ParseError if any element is structurally not a valid
3009
+ type (e.g. integer literal at a type-args site). Name-resolution
3010
+ errors do not fire at parse time; elements emit as TypeRefNode
3011
+ and sema resolves in the pre-pass body walker.
3012
+ """
3013
+ slices = _extract_subscript_slices(node)
3014
+
3015
+ # Parse each type argument - raise error if any is structurally invalid.
3016
+ type_args: 'list[TpyType | TypeRefNode | None]' = []
3017
+ for s in slices:
3018
+ # _ wildcard: infer this type argument
3019
+ if isinstance(s, ast.Name) and s.id == '_':
3020
+ type_args.append(None)
3021
+ continue
3022
+ # Integer constants are not valid type arguments
3023
+ if isinstance(s, ast.Constant) and isinstance(s.value, int):
3024
+ raise ParseError(f"Integer '{s.value}' is not a valid type argument", s)
3025
+ type_args.append(self._parse_type_ref(s))
3026
+ return tuple(type_args)
3027
+
3028
+ def _parse_comprehension_generator(self, gen: ast.comprehension) -> TpyComprehensionGenerator:
3029
+ """Parse a single comprehension generator clause."""
3030
+ iterable = self._parse_expr(gen.iter)
3031
+ conditions = [self._parse_expr(c) for c in gen.ifs]
3032
+ if isinstance(gen.target, ast.Name):
3033
+ return TpyComprehensionGenerator(
3034
+ var=gen.target.id, iterable=iterable,
3035
+ conditions=conditions, unpack_vars=None)
3036
+ elif isinstance(gen.target, ast.Tuple):
3037
+ for elt in gen.target.elts:
3038
+ if not isinstance(elt, ast.Name):
3039
+ raise ParseError(
3040
+ "Comprehension unpacking targets must be simple variables", gen.target)
3041
+ unpack_vars: list[str | None] = [
3042
+ None if elt.id == "_" else elt.id # type: ignore[union-attr]
3043
+ for elt in gen.target.elts
3044
+ ]
3045
+ return TpyComprehensionGenerator(
3046
+ var="__comp_tup", iterable=iterable,
3047
+ conditions=conditions, unpack_vars=unpack_vars)
3048
+ else:
3049
+ raise ParseError("Unsupported comprehension target", gen.target)
3050
+
3051
+ def _try_parse_type_args(self, node: ast.Subscript) -> 'tuple[tuple[TpyType | TypeRefNode | None, ...], str | None]':
3052
+ """Try to parse type args from a subscript, capturing parse errors.
3053
+
3054
+ Returns (type_args, parse_error). On success parse_error is
3055
+ None. On failure type_args is empty and parse_error holds the
3056
+ message. Elements may be TypeRefNode pre-sema.
3057
+ """
3058
+ try:
3059
+ return self._parse_type_args_from_subscript(node), None
3060
+ except ParseError as e:
3061
+ return (), e.message
3062
+
3063
+ def _parse_body(self, nodes: list[ast.stmt]) -> list[TpyStmt]:
3064
+ """Parse a list of statements, flattening any multi-statement expansions."""
3065
+ result: list[TpyStmt] = []
3066
+ for node in nodes:
3067
+ stmt = self._parse_stmt(node)
3068
+ if isinstance(stmt, list):
3069
+ result.extend(stmt)
3070
+ else:
3071
+ result.append(stmt)
3072
+ return result
3073
+
3074
+ def _parse_stmt(self, node: ast.stmt) -> 'TpyStmt | list[TpyStmt]':
3075
+ """Parse a statement."""
3076
+ loc = self._loc(node)
3077
+
3078
+ if isinstance(node, ast.AnnAssign):
3079
+ # Annotated assignment: x: T = expr
3080
+ if not isinstance(node.target, ast.Name):
3081
+ raise ParseError("Invalid assignment target", node)
3082
+ # Emit a TypeRefNode; sema's `_analyze_var_decl` resolves
3083
+ # it via `TypeOperations.resolve_type_ref` and writes the
3084
+ # resolved type back into stmt.type, so downstream code
3085
+ # sees TpyType.
3086
+ var_type_ref = self._parse_type_ref(node.annotation)
3087
+ init_expr = self._parse_expr(node.value) if node.value else None
3088
+ return TpyVarDecl(node.target.id, var_type_ref, init_expr, loc=loc)
3089
+
3090
+ elif isinstance(node, ast.Assign):
3091
+ # Simple assignment: x = expr or x.field = expr
3092
+ if len(node.targets) > 1:
3093
+ return self._parse_multi_assign(node, loc)
3094
+ if isinstance(node.targets[0], ast.Tuple):
3095
+ elts = node.targets[0].elts
3096
+ if not elts:
3097
+ raise ParseError("Empty tuple unpacking", node)
3098
+ targets: list[str | None] = []
3099
+ for elt in elts:
3100
+ if not isinstance(elt, ast.Name):
3101
+ raise ParseError(
3102
+ "Tuple unpacking targets must be simple variable names", node)
3103
+ targets.append(None if elt.id == "_" else elt.id)
3104
+ value = self._parse_expr(node.value)
3105
+ return TpyTupleUnpack(targets=targets, value=value, loc=loc)
3106
+ target = self._parse_expr(node.targets[0])
3107
+ value = self._parse_expr(node.value)
3108
+ # Check if this is a variable declaration (unannotated)
3109
+ if isinstance(target, TpyName):
3110
+ return TpyVarDecl(target.name, None, value, loc=loc)
3111
+ return TpyAssign(target, value, loc=loc)
3112
+
3113
+ elif isinstance(node, ast.AugAssign):
3114
+ # Augmented assignment: x += expr
3115
+ target = self._parse_expr(node.target)
3116
+ value = self._parse_expr(node.value)
3117
+ op = self._binop_to_str(node.op)
3118
+ return TpyAugAssign(target, op, value, loc=loc)
3119
+
3120
+ elif isinstance(node, ast.Expr):
3121
+ if isinstance(node.value, ast.Yield):
3122
+ if node.value.value is None:
3123
+ raise ParseError("'yield' must have a value in TurboPython", node)
3124
+ value = self._parse_expr(node.value.value)
3125
+ return TpyYield(value, loc=loc)
3126
+ return TpyExprStmt(self._parse_expr(node.value), loc=loc)
3127
+
3128
+ elif isinstance(node, ast.Return):
3129
+ value = self._parse_expr(node.value) if node.value else None
3130
+ return TpyReturn(value, loc=loc)
3131
+
3132
+ elif isinstance(node, ast.Assert):
3133
+ cond = self._parse_expr(node.test)
3134
+ msg = self._parse_expr(node.msg) if node.msg else None
3135
+ return TpyAssert(cond, msg, loc=loc)
3136
+
3137
+ elif isinstance(node, ast.If):
3138
+ cond = self._parse_expr(node.test)
3139
+ then_body = self._parse_body(node.body)
3140
+ else_body = self._parse_body(node.orelse)
3141
+ return TpyIf(cond, then_body, else_body, loc=loc)
3142
+
3143
+ elif isinstance(node, ast.While):
3144
+ cond = self._parse_expr(node.test)
3145
+ body = self._parse_body(node.body)
3146
+ orelse = self._parse_body(node.orelse)
3147
+ return TpyWhile(cond, body, orelse=orelse, loc=loc)
3148
+
3149
+ elif isinstance(node, (ast.For, ast.AsyncFor)):
3150
+ is_async = isinstance(node, ast.AsyncFor)
3151
+ if is_async and node.orelse:
3152
+ raise ParseError(
3153
+ "`else:` clause on `async for` is not supported yet "
3154
+ "(the `__anext__`-driven async-for advance doesn't model "
3155
+ "break-vs-normal-exit; sync `for`/`while`-`else` with a "
3156
+ "suspension is supported)",
3157
+ node,
3158
+ )
3159
+ orelse = self._parse_body(node.orelse)
3160
+ if isinstance(node.target, ast.Tuple):
3161
+ for elt in node.target.elts:
3162
+ if not isinstance(elt, ast.Name):
3163
+ raise ParseError(
3164
+ "For loop unpacking targets must be simple variables", node
3165
+ )
3166
+ targets: list[str | None] = [
3167
+ None if elt.id == "_" else elt.id # type: ignore[union-attr]
3168
+ for elt in node.target.elts
3169
+ ]
3170
+ synth_var = f"__for_tup_{self._for_unpack_counter}"
3171
+ self._for_unpack_counter += 1
3172
+ iterable = self._parse_expr(node.iter)
3173
+ body = self._parse_body(node.body)
3174
+ unpack = TpyTupleUnpack(
3175
+ targets=targets,
3176
+ value=TpyName(synth_var, loc=loc),
3177
+ loc=loc,
3178
+ )
3179
+ return TpyForEach(synth_var, iterable, [unpack] + body, orelse=orelse, loc=loc, is_tuple_unpack=True, is_async=is_async)
3180
+ if not isinstance(node.target, ast.Name):
3181
+ raise ParseError("For loop target must be a simple variable", node)
3182
+ var = node.target.id
3183
+ body = self._parse_body(node.body)
3184
+ iterable = self._parse_expr(node.iter)
3185
+ return TpyForEach(var, iterable, body, orelse=orelse, loc=loc, is_async=is_async)
3186
+
3187
+ elif isinstance(node, ast.Pass):
3188
+ return TpyPassStmt(loc=loc)
3189
+
3190
+ elif isinstance(node, ast.Break):
3191
+ return TpyBreak(loc=loc)
3192
+
3193
+ elif isinstance(node, ast.Continue):
3194
+ return TpyContinue(loc=loc)
3195
+
3196
+ elif isinstance(node, ast.Global):
3197
+ return TpyGlobal(node.names, loc=loc)
3198
+
3199
+ elif isinstance(node, ast.Raise):
3200
+ return self._parse_raise(node, loc)
3201
+
3202
+ elif isinstance(node, ast.Delete):
3203
+ return self._parse_delete(node, loc)
3204
+
3205
+ elif isinstance(node, ast.Match):
3206
+ return self._parse_match(node, loc)
3207
+
3208
+ elif isinstance(node, ast.Try):
3209
+ return self._parse_try(node, loc)
3210
+
3211
+ elif isinstance(node, ast.With):
3212
+ return self._parse_with(node, loc, is_async=False)
3213
+
3214
+ elif isinstance(node, ast.AsyncWith):
3215
+ return self._parse_with(node, loc, is_async=True)
3216
+
3217
+ elif isinstance(node, ast.Nonlocal):
3218
+ return TpyNonlocal(node.names, loc=loc)
3219
+
3220
+ elif isinstance(node, ast.FunctionDef):
3221
+ return self._parse_nested_def(node, loc)
3222
+
3223
+ elif isinstance(node, ast.AsyncFunctionDef):
3224
+ raise ParseError(
3225
+ f"nested async def is not yet supported (function '{node.name}'); "
3226
+ f"async def is only allowed at module level", node)
3227
+
3228
+ else:
3229
+ raise ParseError(f"Unsupported statement: {type(node).__name__}", node)
3230
+
3231
+ def _parse_raise(self, node: ast.Raise, loc: SourceLocation | None) -> TpyStmt:
3232
+ """Parse a raise statement: raise E, raise E(args), or bare raise."""
3233
+ exc = node.exc
3234
+ if exc is None:
3235
+ # Bare raise (re-raise) -- validated by sema to be inside except block
3236
+ return TpyRaise(loc=loc)
3237
+ # raise Name
3238
+ if isinstance(exc, ast.Name):
3239
+ name = exc.id
3240
+ return TpyRaise(exception_type=name, loc=loc)
3241
+ # raise Name() or raise Name(args...)
3242
+ if isinstance(exc, ast.Call) and isinstance(exc.func, ast.Name):
3243
+ name = exc.func.id
3244
+ if exc.keywords:
3245
+ raise ParseError(f"'raise {name}' does not accept keyword arguments", node)
3246
+ args = [self._parse_expr(a) for a in exc.args]
3247
+ return TpyRaise(exception_type=name, args=args, is_call_form=True, loc=loc)
3248
+ # raise <expr> -- general expression (e.g. raise obj.make_err(), raise errors[i])
3249
+ return TpyRaise(raise_expr=self._parse_expr(exc), loc=loc)
3250
+
3251
+ def _parse_try(self, node: ast.Try, loc: SourceLocation | None) -> TpyStmt:
3252
+ """Parse a try/except/else/finally statement."""
3253
+ if not node.handlers and not node.finalbody:
3254
+ raise ParseError("'try' requires at least one 'except' or 'finally' clause", node)
3255
+ handlers: list[TpyExceptHandler] = []
3256
+ for h in node.handlers:
3257
+ h_loc = self._loc(h) if hasattr(h, 'lineno') else loc
3258
+ if h.type is None:
3259
+ # Bare except: -- must be last handler (Python enforces this)
3260
+ exception_type = None
3261
+ else:
3262
+ exception_type = _attr_chain_to_dotted(h.type)
3263
+ if exception_type is None:
3264
+ raise ParseError(
3265
+ "'except' requires a simple or dotted name "
3266
+ "(e.g. 'except MyError' or 'except pkg.MyError')", h)
3267
+ handlers.append(TpyExceptHandler(
3268
+ exception_type=exception_type, binding=h.name,
3269
+ body=self._parse_body(h.body), loc=h_loc))
3270
+ try_body = self._parse_body(node.body)
3271
+ else_body = self._parse_body(node.orelse)
3272
+ finally_body = self._parse_body(node.finalbody)
3273
+ return TpyTry(
3274
+ try_body=try_body,
3275
+ handlers=handlers,
3276
+ else_body=else_body,
3277
+ finally_body=finally_body,
3278
+ loc=loc,
3279
+ )
3280
+
3281
+ def _parse_with(self, node: ast.With | ast.AsyncWith,
3282
+ loc: SourceLocation | None,
3283
+ is_async: bool) -> TpyWith:
3284
+ """Parse a sync or async with statement."""
3285
+ kw = "async with" if is_async else "with"
3286
+ items: list[TpyWithItem] = []
3287
+ for item in node.items:
3288
+ context_expr = self._parse_expr(item.context_expr)
3289
+ target: str | None = None
3290
+ if item.optional_vars is not None:
3291
+ if not isinstance(item.optional_vars, ast.Name):
3292
+ raise ParseError(
3293
+ f"'{kw} ... as' target must be a simple variable", node
3294
+ )
3295
+ target = item.optional_vars.id
3296
+ item_loc = self._loc(item.context_expr)
3297
+ items.append(TpyWithItem(context_expr, target, loc=item_loc))
3298
+ body = self._parse_body(node.body)
3299
+ return TpyWith(items, body, is_async=is_async, loc=loc)
3300
+
3301
+ def _parse_nested_def(self, node: ast.FunctionDef, loc: SourceLocation | None) -> TpyNestedDef:
3302
+ """Parse a nested function definition inside a function body."""
3303
+ if node.decorator_list:
3304
+ raise ParseError(
3305
+ f"Decorators are not supported on nested functions", node)
3306
+ if hasattr(node, 'type_params') and node.type_params:
3307
+ raise ParseError(
3308
+ f"Type parameters are not supported on nested functions", node)
3309
+ func = self._parse_function(node)
3310
+ # Nested defs live inside a function body, so sema's top-level
3311
+ # resolve_refs pre-pass doesn't see them. Resolve
3312
+ # refs immediately, passing the enclosing function's type-param
3313
+ # scope so outer generic params still resolve inside the nested
3314
+ # body. _parse_function restored self._type_param_scope to the
3315
+ # enclosing value before returning.
3316
+ self._finalize_function_refs(func, outer_scope=self._type_param_scope)
3317
+ return TpyNestedDef(func=func, loc=loc)
3318
+
3319
+ def _parse_multi_assign(self, node: ast.Assign,
3320
+ loc: SourceLocation | None) -> list[TpyStmt]:
3321
+ """Desugar multi-target assignment: a = b = c = expr.
3322
+
3323
+ Uses a name target as anchor so the value is evaluated exactly once.
3324
+ All other targets reference the anchor. If no name target exists,
3325
+ a synthetic temp is introduced.
3326
+ """
3327
+ for t in node.targets:
3328
+ if isinstance(t, ast.Tuple):
3329
+ raise ParseError(
3330
+ "Tuple unpacking not supported in multiple assignment", node)
3331
+ value_expr = self._parse_expr(node.value)
3332
+ # Find rightmost Name target to use as anchor
3333
+ anchor: ast.Name | None = None
3334
+ for t in reversed(node.targets):
3335
+ if isinstance(t, ast.Name):
3336
+ anchor = t
3337
+ break
3338
+ stmts: list[TpyStmt] = []
3339
+ if anchor is not None:
3340
+ anchor_name = anchor.id
3341
+ stmts.append(TpyVarDecl(anchor_name, None, value_expr, loc=loc))
3342
+ else:
3343
+ # No name target -- introduce synthetic temp
3344
+ anchor_name = f"__ma_{self._multi_assign_counter}"
3345
+ self._multi_assign_counter += 1
3346
+ stmts.append(TpyVarDecl(anchor_name, None, value_expr, loc=loc))
3347
+ # Assign anchor to remaining targets (left-to-right, no source comment)
3348
+ for t in node.targets:
3349
+ if t is anchor:
3350
+ continue
3351
+ target = self._parse_expr(t)
3352
+ ref = TpyName(anchor_name, loc=loc)
3353
+ if isinstance(target, TpyName):
3354
+ stmts.append(TpyVarDecl(target.name, None, ref, loc=None))
3355
+ else:
3356
+ stmts.append(TpyAssign(target, ref, loc=None))
3357
+ return stmts
3358
+
3359
+ def _parse_delete(self, node: ast.Delete, loc: SourceLocation | None) -> TpyStmt:
3360
+ """Parse a del statement. Supports subscript, variable, and attribute targets."""
3361
+ subscripts: list[TpySubscript] = []
3362
+ names: list[str] = []
3363
+ attrs: list[TpyFieldAccess] = []
3364
+ for target in node.targets:
3365
+ if isinstance(target, ast.Subscript):
3366
+ obj = self._parse_expr(target.value)
3367
+ index = self._parse_expr(target.slice)
3368
+ subscripts.append(TpySubscript(obj, index, loc=loc))
3369
+ elif isinstance(target, ast.Name):
3370
+ names.append(target.id)
3371
+ elif isinstance(target, ast.Attribute):
3372
+ obj = self._parse_expr(target.value)
3373
+ attrs.append(TpyFieldAccess(obj=obj, field=target.attr, loc=loc))
3374
+ else:
3375
+ raise ParseError(f"Unsupported del target: {type(target).__name__}", node)
3376
+ # Mixing the three kinds in one statement is rare; reject for clarity
3377
+ # (matches the existing subscript+name rejection).
3378
+ kinds_present = sum(1 for k in (subscripts, names, attrs) if k)
3379
+ if kinds_present > 1:
3380
+ raise ParseError(
3381
+ "Cannot mix variable, subscript, and attribute targets in a single 'del' statement",
3382
+ node,
3383
+ )
3384
+ if names:
3385
+ return TpyDelVar(names, loc=loc)
3386
+ if attrs:
3387
+ return TpyDelAttr(attrs, loc=loc)
3388
+ return TpyDelItem(subscripts, loc=loc)
3389
+
3390
+ def _parse_match(self, node: ast.Match, loc: SourceLocation | None) -> TpyMatch:
3391
+ """Parse a match/case statement."""
3392
+ subject = self._parse_expr(node.subject)
3393
+ cases: list[TpyMatchCase] = []
3394
+ for case in node.cases:
3395
+ pattern = self._parse_pattern(case.pattern)
3396
+ guard = self._parse_expr(case.guard) if case.guard else None
3397
+ body = self._parse_body(case.body)
3398
+ # match_case nodes lack lineno; use the pattern's location instead
3399
+ cases.append(TpyMatchCase(pattern, guard, body, loc=self._loc(case.pattern)))
3400
+ return TpyMatch(subject, cases, loc=loc)
3401
+
3402
+ def _parse_pattern(self, node: ast.pattern) -> TpyPattern:
3403
+ """Parse a match/case pattern."""
3404
+ loc = self._loc(node)
3405
+
3406
+ if isinstance(node, ast.MatchAs):
3407
+ if node.pattern is None and node.name is None:
3408
+ return TpyWildcardPattern(loc=loc)
3409
+ if node.pattern is None and node.name is not None:
3410
+ return TpyCapturePattern(node.name, loc=loc)
3411
+ if node.pattern is not None and node.name is not None:
3412
+ inner = self._parse_pattern(node.pattern)
3413
+ return TpyAsPattern(inner, node.name, loc=loc)
3414
+
3415
+ elif isinstance(node, ast.MatchClass):
3416
+ cls = self._parse_expr(node.cls)
3417
+ positional = [self._parse_pattern(p) for p in node.patterns]
3418
+ keywords = [
3419
+ (attr, self._parse_pattern(pat))
3420
+ for attr, pat in zip(node.kwd_attrs, node.kwd_patterns)
3421
+ ]
3422
+ return TpyClassPattern(cls, positional, keywords, loc=loc)
3423
+
3424
+ elif isinstance(node, ast.MatchValue):
3425
+ if isinstance(node.value, ast.Constant):
3426
+ return TpyLiteralPattern(node.value.value, loc=loc)
3427
+ if isinstance(node.value, ast.Attribute):
3428
+ expr = self._parse_expr(node.value)
3429
+ return TpyValuePattern(expr, loc=loc)
3430
+ raise ParseError("Unsupported match value pattern", node)
3431
+
3432
+ elif isinstance(node, ast.MatchSingleton):
3433
+ return TpyLiteralPattern(node.value, loc=loc)
3434
+
3435
+ elif isinstance(node, ast.MatchOr):
3436
+ patterns = [self._parse_pattern(p) for p in node.patterns]
3437
+ return TpyOrPattern(patterns, loc=loc)
3438
+
3439
+ raise ParseError(f"Unsupported pattern: {type(node).__name__}", node)
3440
+
3441
+ def _parse_expr(self, node: ast.expr) -> TpyExpr:
3442
+ """Parse an expression."""
3443
+ loc = self._loc(node)
3444
+
3445
+ if isinstance(node, ast.Constant):
3446
+ if isinstance(node.value, bool):
3447
+ return TpyBoolLiteral(node.value, loc=loc)
3448
+ elif isinstance(node.value, int):
3449
+ return TpyIntLiteral(node.value, loc=loc)
3450
+ elif isinstance(node.value, float):
3451
+ return TpyFloatLiteral(node.value, loc=loc)
3452
+ elif isinstance(node.value, str):
3453
+ return TpyStrLiteral(node.value, loc=loc)
3454
+ elif isinstance(node.value, bytes):
3455
+ return TpyBytesLiteral(node.value, loc=loc)
3456
+ elif node.value is None:
3457
+ return TpyNoneLiteral(loc=loc)
3458
+ else:
3459
+ raise ParseError(f"Unsupported literal type: {type(node.value).__name__}", node)
3460
+
3461
+ elif isinstance(node, ast.Name):
3462
+ if node.id in self.FORBIDDEN_CONSTRUCTS:
3463
+ raise ParseError(f"'{node.id}' is not allowed in TurboPython", node)
3464
+ return TpyName(node.id, loc=loc)
3465
+
3466
+ elif isinstance(node, ast.BinOp):
3467
+ # Handle list repetition: [x, y, ...] * N -> TpyListRepeat
3468
+ if isinstance(node.op, ast.Mult) and isinstance(node.left, ast.List):
3469
+ elements = [self._parse_expr(e) for e in node.left.elts]
3470
+ # Empty list: [] * N -> [] (collapse to empty array literal)
3471
+ if not elements:
3472
+ return TpyArrayLiteral([], loc=loc)
3473
+ count = self._parse_expr(node.right)
3474
+ return TpyListRepeat(elements, count, loc=loc)
3475
+ left = self._parse_expr(node.left)
3476
+ right = self._parse_expr(node.right)
3477
+ op = self._binop_to_str(node.op)
3478
+ return TpyBinOp(left, op, right, loc=loc)
3479
+
3480
+ elif isinstance(node, ast.Compare):
3481
+ left = self._parse_expr(node.left)
3482
+ if len(node.ops) == 1:
3483
+ right = self._parse_expr(node.comparators[0])
3484
+ op = self._cmpop_to_str(node.ops[0])
3485
+ return TpyBinOp(left, op, right, loc=loc)
3486
+ # Chained comparison: a < b < c
3487
+ ops = []
3488
+ for ast_op in node.ops:
3489
+ op = self._cmpop_to_str(ast_op)
3490
+ if op in ("is", "is not", "in", "not in"):
3491
+ raise ParseError(
3492
+ f"'{op}' cannot be used in chained comparisons", node)
3493
+ ops.append(op)
3494
+ comparators = [self._parse_expr(c) for c in node.comparators]
3495
+ return TpyChainedCompare(left, ops, comparators, loc=loc)
3496
+
3497
+ elif isinstance(node, ast.UnaryOp):
3498
+ operand = self._parse_expr(node.operand)
3499
+ op = self._unaryop_to_str(node.op)
3500
+ return TpyUnaryOp(op, operand, loc=loc)
3501
+
3502
+ elif isinstance(node, ast.BoolOp):
3503
+ # Handle 'and' / 'or' - chain as binary ops
3504
+ op = "&&" if isinstance(node.op, ast.And) else "||"
3505
+ result = self._parse_expr(node.values[0])
3506
+ for val in node.values[1:]:
3507
+ result = TpyBinOp(result, op, self._parse_expr(val), loc=loc)
3508
+ return result
3509
+
3510
+ elif isinstance(node, ast.Call):
3511
+ args = []
3512
+ for a in node.args:
3513
+ if isinstance(a, ast.Starred):
3514
+ args.append(TpyStarUnpack(
3515
+ expr=self._parse_expr(a.value), loc=self._loc(a)))
3516
+ else:
3517
+ args.append(self._parse_expr(a))
3518
+ kwargs = {}
3519
+ double_star_unpack = None
3520
+
3521
+ if node.keywords:
3522
+ for kw in node.keywords:
3523
+ if kw.arg is None:
3524
+ # **expr unpacking
3525
+ if double_star_unpack is not None:
3526
+ raise ParseError("Only one **expr unpacking allowed per call", node)
3527
+ if kwargs:
3528
+ raise ParseError("**expr unpacking cannot be mixed with keyword arguments", node)
3529
+ double_star_unpack = self._parse_expr(kw.value)
3530
+ else:
3531
+ if double_star_unpack is not None:
3532
+ raise ParseError("**expr unpacking cannot be mixed with keyword arguments", node)
3533
+ if kw.arg in kwargs:
3534
+ raise ParseError(f"Keyword argument '{kw.arg}' repeated", node)
3535
+ kwargs[kw.arg] = self._parse_expr(kw.value)
3536
+
3537
+ if isinstance(node.func, ast.Name):
3538
+ call = TpyCall(TpyName(node.func.id, loc=loc), args, kwargs=kwargs,
3539
+ double_star_unpack=double_star_unpack, loc=loc)
3540
+ # Set resolved_import so downstream passes (e.g. the
3541
+ # builder-trace expander) can identify imported callables.
3542
+ self._resolve_call_import(call, node)
3543
+ return call
3544
+ elif isinstance(node.func, ast.Attribute):
3545
+ # ClassName[TypeArgs].method(args) -- static call with explicit class type args
3546
+ if (isinstance(node.func.value, ast.Subscript)
3547
+ and isinstance(node.func.value.value, ast.Name)
3548
+ and self._could_be_type(node.func.value.value.id)):
3549
+ name = node.func.value.value.id
3550
+ type_args, type_args_parse_error = self._try_parse_type_args(node.func.value)
3551
+ if type_args or type_args_parse_error:
3552
+ return TpyMethodCall(
3553
+ TpyName(name, loc=loc), node.func.attr, args,
3554
+ kwargs=kwargs, double_star_unpack=double_star_unpack,
3555
+ type_args=type_args, type_args_parse_error=type_args_parse_error,
3556
+ loc=loc,
3557
+ )
3558
+ # No type args and no error: fall through (e.g., variable[index].method())
3559
+ obj = self._parse_expr(node.func.value)
3560
+ mcall = TpyMethodCall(obj, node.func.attr, args, kwargs=kwargs,
3561
+ double_star_unpack=double_star_unpack, loc=loc)
3562
+ # Set resolved_import for qualified module calls
3563
+ # (``mod.func()``) so downstream passes -- e.g. the
3564
+ # builder-trace expander -- can identify imported
3565
+ # callables without re-walking the import table.
3566
+ self._resolve_call_import(mcall, node)
3567
+ return mcall
3568
+ elif isinstance(node.func, ast.Subscript):
3569
+ # Could be generic type instantiation (Stack[Int32]()) or generic function call (First[Int32](x))
3570
+ # Parse both call_type and type_args - sema decides which applies based on whether
3571
+ # the name is a record or a function
3572
+ if isinstance(node.func.value, ast.Name):
3573
+ name = node.func.value.id
3574
+ # Try to extract type_args for potential generic function call
3575
+ # (for type instantiations like Array[Int32, 8], non-type args are valid
3576
+ # so parse error is stored and sema decides whether to report it)
3577
+ type_args, type_args_parse_error = self._try_parse_type_args(node.func)
3578
+ # Try to parse as a type annotation ref (for type
3579
+ # instantiation like ArrayList[Int32]()). Sema
3580
+ # decides whether to use call_type or type_args
3581
+ # based on whether the name resolves to a type or a
3582
+ # function. Emits TypeRefNode; sema resolves in the
3583
+ # pre-pass body walker. try/except catches
3584
+ # structural parse errors (Callable/Fn/Literal
3585
+ # shape); name-resolution errors defer to sema,
3586
+ # which catches them per-call and falls back to
3587
+ # type_args / subscript_callee just like the
3588
+ # pre-flip behaviour.
3589
+ call_type = None
3590
+ if self._could_be_type(name):
3591
+ try:
3592
+ call_type = self._parse_type_ref(node.func)
3593
+ except ParseError:
3594
+ pass
3595
+ # Try to parse the subscript as an expression so sema can
3596
+ # fall back to expression callee when the name turns out to
3597
+ # be a variable, not a function/type (e.g., fns[0](args),
3598
+ # Handlers[MyType](args)). May fail for known generic types
3599
+ # (list[T], Array[T,N], etc. can't be used as values).
3600
+ # Skip slices -- they can't produce a callable value.
3601
+ subscript_callee = None
3602
+ if not isinstance(node.func.slice, ast.Slice):
3603
+ try:
3604
+ subscript_callee = self._parse_expr(node.func)
3605
+ except ParseError:
3606
+ pass
3607
+ return TpyCall(TpyName(name, loc=loc), args, call_type=call_type, type_args=type_args,
3608
+ type_args_parse_error=type_args_parse_error,
3609
+ subscript_callee=subscript_callee, kwargs=kwargs,
3610
+ double_star_unpack=double_star_unpack, loc=loc)
3611
+ elif isinstance(node.func.value, ast.Attribute):
3612
+ # module.func[T](args) -- method call with explicit type args
3613
+ obj = self._parse_expr(node.func.value.value)
3614
+ method = node.func.value.attr
3615
+ type_args, type_args_parse_error = self._try_parse_type_args(node.func)
3616
+ return TpyMethodCall(obj, method, args, kwargs=kwargs,
3617
+ double_star_unpack=double_star_unpack,
3618
+ type_args=type_args,
3619
+ type_args_parse_error=type_args_parse_error, loc=loc)
3620
+ # Expression callee with subscript: expr[i](args)
3621
+ expr_func = self._parse_expr(node.func)
3622
+ return TpyCall(expr_func, args, kwargs=kwargs,
3623
+ double_star_unpack=double_star_unpack, loc=loc)
3624
+ else:
3625
+ # Expression callee: f()(x), (lambda: fn)()(), etc.
3626
+ expr_func = self._parse_expr(node.func)
3627
+ return TpyCall(expr_func, args, kwargs=kwargs,
3628
+ double_star_unpack=double_star_unpack, loc=loc)
3629
+
3630
+ elif isinstance(node, ast.Attribute):
3631
+ obj = self._parse_expr(node.value)
3632
+ return TpyFieldAccess(obj, node.attr, loc=loc)
3633
+
3634
+ elif isinstance(node, ast.List):
3635
+ elements = [self._parse_expr(elt) for elt in node.elts]
3636
+ return TpyArrayLiteral(elements=elements, loc=loc)
3637
+
3638
+ elif isinstance(node, ast.ListComp):
3639
+ if len(node.generators) != 1:
3640
+ raise ParseError("Nested comprehensions not yet supported", node)
3641
+ gen = node.generators[0]
3642
+ if gen.is_async:
3643
+ raise ParseError("Async comprehensions not yet supported", node)
3644
+ generator = self._parse_comprehension_generator(gen)
3645
+ element_expr = self._parse_expr(node.elt)
3646
+ return TpyListComprehension(element_expr, generator, loc=loc)
3647
+
3648
+ elif isinstance(node, ast.DictComp):
3649
+ if len(node.generators) != 1:
3650
+ raise ParseError("Nested comprehensions not yet supported", node)
3651
+ gen = node.generators[0]
3652
+ if gen.is_async:
3653
+ raise ParseError("Async comprehensions not yet supported", node)
3654
+ generator = self._parse_comprehension_generator(gen)
3655
+ key_expr = self._parse_expr(node.key)
3656
+ value_expr = self._parse_expr(node.value)
3657
+ return TpyDictComprehension(key_expr, value_expr, generator, loc=loc)
3658
+
3659
+ elif isinstance(node, ast.Dict):
3660
+ if any(k is None for k in node.keys):
3661
+ raise ParseError("Dict unpacking (**) is not supported", node)
3662
+ keys = [self._parse_expr(k) for k in node.keys]
3663
+ values = [self._parse_expr(v) for v in node.values]
3664
+ return TpyDictLiteral(keys=keys, values=values, loc=loc)
3665
+
3666
+ elif isinstance(node, ast.SetComp):
3667
+ if len(node.generators) != 1:
3668
+ raise ParseError("Nested comprehensions not yet supported", node)
3669
+ gen = node.generators[0]
3670
+ if gen.is_async:
3671
+ raise ParseError("Async comprehensions not yet supported", node)
3672
+ generator = self._parse_comprehension_generator(gen)
3673
+ element_expr = self._parse_expr(node.elt)
3674
+ return TpySetComprehension(element_expr, generator, loc=loc)
3675
+
3676
+ elif isinstance(node, ast.GeneratorExp):
3677
+ if len(node.generators) != 1:
3678
+ raise ParseError("Nested comprehensions not yet supported", node)
3679
+ gen = node.generators[0]
3680
+ if gen.is_async:
3681
+ raise ParseError("Async comprehensions not yet supported", node)
3682
+ generator = self._parse_comprehension_generator(gen)
3683
+ element_expr = self._parse_expr(node.elt)
3684
+ return TpyGeneratorExpression(element_expr, generator, loc=loc)
3685
+
3686
+ elif isinstance(node, ast.Set):
3687
+ elements = [self._parse_expr(e) for e in node.elts]
3688
+ return TpySetLiteral(elements=elements, loc=loc)
3689
+
3690
+ elif isinstance(node, ast.Subscript):
3691
+ # Subscript can be indexing (values[i]) or type annotation (Array[T, N])
3692
+ # If the value is a name that's a known generic type, it's a type annotation context
3693
+ # Otherwise, it's indexing
3694
+ if isinstance(node.value, ast.Name):
3695
+ name = node.value.id
3696
+ # Type constructors that only exist in type annotations
3697
+ if name in ("Own", "Fn"):
3698
+ raise ParseError(f"Generic type '{name}' cannot be used as a value", node)
3699
+ # Module-defined generic types
3700
+ if find_factory_by_simple_name(name) is not None:
3701
+ raise ParseError(f"Generic type '{name}' cannot be used as a value", node)
3702
+ # Generic types from imported builtin submodules (tpy.mem, etc.)
3703
+ if import_src := self._imports.get_import_source(name):
3704
+ if find_factory_in_module(import_src[1], import_src[0]) is not None:
3705
+ raise ParseError(f"Generic type '{name}' cannot be used as a value", node)
3706
+ obj = self._parse_expr(node.value)
3707
+ if isinstance(node.slice, ast.Slice):
3708
+ sl = node.slice
3709
+ lower = self._parse_expr(sl.lower) if sl.lower is not None else None
3710
+ upper = self._parse_expr(sl.upper) if sl.upper is not None else None
3711
+ step = self._parse_expr(sl.step) if sl.step is not None else None
3712
+ index = TpySlice(lower=lower, upper=upper, step=step, loc=loc)
3713
+ else:
3714
+ index = self._parse_expr(node.slice)
3715
+ return TpySubscript(obj=obj, index=index, loc=loc)
3716
+
3717
+ elif isinstance(node, ast.Tuple):
3718
+ if not node.elts:
3719
+ raise ParseError("Empty tuple literal is not supported", node)
3720
+ elements = [self._parse_expr(elt) for elt in node.elts]
3721
+ return TpyTupleLiteral(elements=elements, loc=loc)
3722
+
3723
+ elif isinstance(node, ast.JoinedStr):
3724
+ return self._parse_fstring(node, loc)
3725
+
3726
+ elif isinstance(node, ast.IfExp):
3727
+ condition = self._parse_expr(node.test)
3728
+ then_expr = self._parse_expr(node.body)
3729
+ else_expr = self._parse_expr(node.orelse)
3730
+ return TpyIfExpr(condition=condition, then_expr=then_expr,
3731
+ else_expr=else_expr, loc=loc)
3732
+
3733
+ elif isinstance(node, ast.NamedExpr):
3734
+ target = node.target.id
3735
+ value = self._parse_expr(node.value)
3736
+ return TpyNamedExpr(target=target, value=value, loc=loc)
3737
+
3738
+ elif isinstance(node, ast.Lambda):
3739
+ if node.args.vararg or node.args.kwarg:
3740
+ raise ParseError("*args and **kwargs are not supported in lambda", node)
3741
+ if node.args.kwonlyargs or node.args.posonlyargs:
3742
+ raise ParseError(
3743
+ "Keyword-only and positional-only parameters are not supported in lambda", node)
3744
+ if any(d is not None for d in node.args.defaults) or node.args.kw_defaults:
3745
+ raise ParseError("Default arguments are not supported in lambda", node)
3746
+ for arg in node.args.args:
3747
+ if arg.annotation is not None:
3748
+ raise ParseError(
3749
+ "Type annotations on lambda parameters are not supported; "
3750
+ "types are inferred from context", node)
3751
+ param_names = [arg.arg for arg in node.args.args]
3752
+ body = self._parse_expr(node.body)
3753
+ return TpyLambda(param_names=param_names, body=body, loc=loc)
3754
+
3755
+ elif isinstance(node, ast.Yield):
3756
+ raise ParseError(
3757
+ "'yield' is only allowed as a statement, not in expression context", node)
3758
+
3759
+ elif isinstance(node, ast.YieldFrom):
3760
+ raise ParseError(
3761
+ "'yield from' is not yet supported in TurboPython", node)
3762
+
3763
+ elif isinstance(node, ast.Await):
3764
+ # Sema rejects await outside an async def body. v1 codegen for
3765
+ # await lowers to a poll-and-park sequence on the operand's
3766
+ # Awaitable[T] conformance (PR 3).
3767
+ value = self._parse_expr(node.value)
3768
+ return TpyAwait(value=value, loc=loc)
3769
+
3770
+ else:
3771
+ raise ParseError(f"Unsupported expression: {type(node).__name__}", node)
3772
+
3773
+ def _parse_fstring(self, node: ast.JoinedStr, loc: SourceLocation) -> TpyFString:
3774
+ """Parse an f-string (ast.JoinedStr) into a TpyFString node."""
3775
+ parts: list[str | TpyFStringValue] = []
3776
+ for val in node.values:
3777
+ if isinstance(val, ast.Constant) and isinstance(val.value, str):
3778
+ parts.append(val.value)
3779
+ elif isinstance(val, ast.FormattedValue):
3780
+ conv = val.conversion
3781
+ if conv == FSTRING_CONV_ASCII:
3782
+ raise ParseError("f-string !a conversion is not supported", node)
3783
+ expr = self._parse_expr(val.value)
3784
+ fmt_spec: str | None = None
3785
+ if val.format_spec is not None:
3786
+ # format_spec is a JoinedStr; only constant specs are supported
3787
+ spec_parts = []
3788
+ for sp in val.format_spec.values:
3789
+ if isinstance(sp, ast.Constant) and isinstance(sp.value, str):
3790
+ spec_parts.append(sp.value)
3791
+ else:
3792
+ raise ParseError(
3793
+ "Expressions inside f-string format specs are not supported", node
3794
+ )
3795
+ fmt_spec = "".join(spec_parts)
3796
+ err = _validate_fstring_format_spec(fmt_spec)
3797
+ if err is not None:
3798
+ raise ParseError(f"Unsupported f-string format spec: {err}", node)
3799
+ parts.append(TpyFStringValue(expr=expr, conversion=conv, format_spec=fmt_spec))
3800
+ else:
3801
+ raise ParseError(f"Unsupported f-string part: {type(val).__name__}", node)
3802
+ return TpyFString(parts=parts, loc=loc)
3803
+
3804
+ def _binop_to_str(self, op: ast.operator) -> str:
3805
+ """Convert binary operator to string."""
3806
+ result = _BINOP_TO_STR.get(type(op))
3807
+ if result is None:
3808
+ raise ParseError(f"Unsupported binary operator: {type(op).__name__}")
3809
+ return result
3810
+
3811
+ def _cmpop_to_str(self, op: ast.cmpop) -> str:
3812
+ """Convert comparison operator to string."""
3813
+ result = _CMPOP_TO_STR.get(type(op))
3814
+ if result is None:
3815
+ raise ParseError(f"Unsupported comparison operator: {type(op).__name__}")
3816
+ return result
3817
+
3818
+ def _unaryop_to_str(self, op: ast.unaryop) -> str:
3819
+ """Convert unary operator to string."""
3820
+ result = _UNARYOP_TO_STR.get(type(op))
3821
+ if result is None:
3822
+ raise ParseError(f"Unsupported unary operator: {type(op).__name__}")
3823
+ return result
3824
+
3825
+ def _validate_const_default(self, expr: TpyExpr, node: ast.expr) -> None:
3826
+ """Validate that a default value expression is a compile-time constant."""
3827
+ if isinstance(expr, (TpyIntLiteral, TpyFloatLiteral, TpyBoolLiteral,
3828
+ TpyStrLiteral, TpyBytesLiteral, TpyNoneLiteral,
3829
+ TpyTypeParamConstruct)):
3830
+ return
3831
+ # Bare name reference: must resolve to a module-level Final[T] constant.
3832
+ # Sema validates the binding (parser doesn't see globals or imports yet).
3833
+ if isinstance(expr, TpyName):
3834
+ return
3835
+ if isinstance(expr, TpyUnaryOp) and expr.op == "-":
3836
+ if isinstance(expr.operand, (TpyIntLiteral, TpyFloatLiteral)):
3837
+ return
3838
+ # Int32(5) etc. -- a fixed-int constructor wrapping a literal
3839
+ if isinstance(expr, TpyCall) and expr.func_name in _FIXED_INT_NAMES:
3840
+ if not expr.args:
3841
+ return # Int32() -> 0
3842
+ if len(expr.args) == 1:
3843
+ self._validate_const_default(expr.args[0], node)
3844
+ return
3845
+ raise ParseError(
3846
+ f"Default parameter value must be a constant expression "
3847
+ f"(literal, None, fixed-int constructor like Int32(5), "
3848
+ f"or a Final[T] module constant)", node)
3849
+
3850
+ def _parse_param_defaults(self, node: ast.FunctionDef, params: list,
3851
+ skip_self: bool = False,
3852
+ type_param_scope: dict | None = None,
3853
+ kw_defaults: list | None = None,
3854
+ n_kwonly: int = 0,
3855
+ ) -> list['TpyExpr | None']:
3856
+ """Parse default values from a function definition.
3857
+
3858
+ Returns a list aligned with params: None for params without defaults.
3859
+ Python's ast.arguments.defaults is right-aligned with args, so we
3860
+ left-pad with None. kw_defaults is 1:1 aligned with kwonlyargs.
3861
+ """
3862
+ n_positional = len(params) - n_kwonly
3863
+ ast_defaults = node.args.defaults
3864
+
3865
+ # In methods, self is skipped from params but still counted in node.args.args
3866
+ num_ast_args = len(node.args.args)
3867
+ # defaults are right-aligned with the full args list
3868
+ num_no_default = num_ast_args - len(ast_defaults) if ast_defaults else num_ast_args
3869
+
3870
+ defaults: list[TpyExpr | None] = []
3871
+ param_offset = 1 if skip_self else 0 # skip self in index mapping
3872
+ for i in range(n_positional):
3873
+ ast_idx = i + param_offset # index into node.args.args
3874
+ default_idx = ast_idx - num_no_default
3875
+ if ast_defaults and default_idx >= 0 and default_idx < len(ast_defaults):
3876
+ expr = self._parse_expr(ast_defaults[default_idx])
3877
+ # Detect T() where T is a type parameter
3878
+ if (isinstance(expr, TpyCall) and not expr.args and not expr.kwargs
3879
+ and type_param_scope and expr.func_name in type_param_scope):
3880
+ expr = TpyTypeParamConstruct(expr.func_name, loc=expr.loc)
3881
+ self._validate_const_default(expr, ast_defaults[default_idx])
3882
+ defaults.append(expr)
3883
+ else:
3884
+ defaults.append(None)
3885
+
3886
+ # Append keyword-only defaults (1:1 aligned with kwonlyargs)
3887
+ if kw_defaults and n_kwonly > 0:
3888
+ for kw_default in kw_defaults:
3889
+ if kw_default is not None:
3890
+ expr = self._parse_expr(kw_default)
3891
+ if (isinstance(expr, TpyCall) and not expr.args and not expr.kwargs
3892
+ and type_param_scope and expr.func_name in type_param_scope):
3893
+ expr = TpyTypeParamConstruct(expr.func_name, loc=expr.loc)
3894
+ self._validate_const_default(expr, kw_default)
3895
+ defaults.append(expr)
3896
+ else:
3897
+ defaults.append(None)
3898
+ elif n_kwonly > 0:
3899
+ defaults.extend([None] * n_kwonly)
3900
+
3901
+ return defaults
3902
+
3903
+ def _get_default_value(self, node: ast.expr) -> str:
3904
+ """Convert a field default value AST node to a C++ literal string (for FieldInfo.default_value)."""
3905
+ if isinstance(node, ast.Constant):
3906
+ val = node.value
3907
+ if val is None:
3908
+ return "std::nullopt"
3909
+ if isinstance(val, bool):
3910
+ return "true" if val else "false"
3911
+ elif isinstance(val, str):
3912
+ escaped = val.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
3913
+ return f'"{escaped}"'
3914
+ return str(val)
3915
+ elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
3916
+ inner = self._get_default_value(node.operand)
3917
+ return f"-{inner}"
3918
+ elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
3919
+ # Int32(x) just becomes x in C++
3920
+ if node.func.id in _FIXED_INT_NAMES:
3921
+ if not node.args:
3922
+ return "0"
3923
+ return self._get_default_value(node.args[0])
3924
+ args = ", ".join(str(self._get_default_value(a)) for a in node.args)
3925
+ return f"{node.func.id}({args})"
3926
+ return "0"
3927
+
3928
+ # ---------------------------------------------------------------------------
3929
+ # FragmentParser -- lightweight parser for macro source fragments
3930
+ # ---------------------------------------------------------------------------
3931
+
3932
+ class FragmentParser(Parser):
3933
+ """Parser for macro-generated source fragments (quote / add_method_from_source).
3934
+
3935
+ Differs from Parser in two ways:
3936
+ - Resolves tpy/typing exports without explicit imports
3937
+ - Returns NominalType for unresolved type names instead of raising
3938
+ """
3939
+
3940
+ def _resolve_type_name(self, local_name: str) -> tuple[str, str] | None:
3941
+ result = super()._resolve_type_name(local_name)
3942
+ if result:
3943
+ return result
3944
+ if local_name in get_tpy_exports():
3945
+ return ("tpy", local_name)
3946
+ if local_name in get_typing_exports():
3947
+ return ("typing", local_name)
3948
+ return None
3949
+
3950
+ def _raise_unresolved_import_error(
3951
+ self, raw_name: str, node: ast.expr | None = None,
3952
+ *, loc: SourceLocation | None = None,
3953
+ ) -> None:
3954
+ # Lenient: unresolved imports are not errors in fragments. Override
3955
+ # signature matches the base shape so TypeResolver's loc-based call
3956
+ # path (`parser._raise_unresolved_import_error(name, loc=ref.loc)`)
3957
+ # is compatible when a fragment's resolve hits an unresolvable name.
3958
+ pass
3959
+
3960
+ def _parse_type_annotation(
3961
+ self, node: ast.expr, type_param_scope: dict | None = None,
3962
+ ) -> TpyType:
3963
+ # Walker-then-lenient-resolver. The lenient fallback
3964
+ # (constructing a NominalType placeholder for unresolved names)
3965
+ # lives in `TypeResolver.resolve_lenient`, keeping NominalType
3966
+ # construction out of the parser.
3967
+ if type_param_scope is None:
3968
+ type_param_scope = self._type_param_scope
3969
+ ref = self._parse_type_ref(node, type_param_scope)
3970
+ return self._resolver.resolve_lenient(ref, type_param_scope)
3971
+
3972
+ @classmethod
3973
+ def parse_fragment(
3974
+ cls,
3975
+ source: str,
3976
+ kind: Literal["function", "statements", "expression"] = "function",
3977
+ ) -> 'TpyFunction | list[TpyStmt] | TpyExpr':
3978
+ """Parse a TPy source fragment without full module context.
3979
+
3980
+ Used by macro quote/add_method_from_source APIs. Unresolved type names
3981
+ become NominalType(name) -- sema resolves them later.
3982
+ """
3983
+ source = textwrap.dedent(source).strip()
3984
+ parser = cls()
3985
+ parser.source_lines = source.splitlines()
3986
+ tree = ast.parse(source)
3987
+
3988
+ if kind == "function":
3989
+ funcs = [n for n in tree.body if isinstance(n, ast.FunctionDef)]
3990
+ if len(funcs) != 1:
3991
+ raise ParseError(
3992
+ f"quote_fun: expected exactly 1 function definition, "
3993
+ f"got {len(funcs)}", tree)
3994
+ func_node = funcs[0]
3995
+ # Detect method: first param named 'self'.
3996
+ args = func_node.args.args
3997
+ has_self = bool(args and args[0].arg == "self")
3998
+ if has_self:
3999
+ # Route through `_parse_method` so method-specific state
4000
+ # (self_annotation, has_auto_readonly_decorator, @property
4001
+ # flags) gets populated -- otherwise sema's expand_methods
4002
+ # sees a bare TpyFunction and skips expansion.
4003
+ # `class_name` is only used in error messages; `<macro>`
4004
+ # is a synthetic placeholder.
4005
+ # `property_names=None` means @x.setter decorators fall
4006
+ # through to the general decorator handler and surface as
4007
+ # an "Unknown decorator" error, matching the prior free-
4008
+ # function routing.
4009
+ func = parser._parse_method(
4010
+ func_node, class_name="<macro>",
4011
+ type_param_scope=None, property_names=None)
4012
+ else:
4013
+ func = parser._parse_function(func_node)
4014
+ # Macro fragment output is consumed before sema's pre-pass runs,
4015
+ # so resolve refs immediately -- matches _parse_nested_def.
4016
+ # Fragments have no enclosing type-param scope; the function's
4017
+ # own type_params are merged in by _finalize_function_refs.
4018
+ parser._finalize_function_refs(func, outer_scope=None)
4019
+ if has_self and isinstance(
4020
+ func.self_annotation, (TpyTypeRef, TpyUnionRef,
4021
+ TpyCallableRef, TpyLiteralRef)):
4022
+ # _finalize_function_refs doesn't touch self_annotation
4023
+ # (non-fragment call sites feed through
4024
+ # `resolve_refs` per-record). Resolve it here
4025
+ # under the method's own type-param scope so expand_methods
4026
+ # sees TpyType.
4027
+ scope: dict[str, TypeParamKind] = {}
4028
+ if func.type_params:
4029
+ kinds = func.type_param_kinds or []
4030
+ for i, name in enumerate(func.type_params):
4031
+ scope[name] = kinds[i] if i < len(kinds) else TypeParamKind.TYPE
4032
+ func.self_annotation = parser._resolve_type_ref_impl(
4033
+ func.self_annotation, scope or None, is_type_arg=True)
4034
+ return func
4035
+ elif kind == "statements":
4036
+ return parser._parse_body(tree.body)
4037
+ elif kind == "expression":
4038
+ if len(tree.body) != 1 or not isinstance(tree.body[0], ast.Expr):
4039
+ raise ParseError(
4040
+ "quote_expr: expected a single expression", tree)
4041
+ return parser._parse_expr(tree.body[0].value)
4042
+ else:
4043
+ raise ValueError(f"Unknown fragment kind: {kind!r}")