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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. tpy_lang-0.3.0.dev0.dist-info/METADATA +151 -0
  2. tpy_lang-0.3.0.dev0.dist-info/RECORD +333 -0
  3. tpy_lang-0.3.0.dev0.dist-info/WHEEL +4 -0
  4. tpy_lang-0.3.0.dev0.dist-info/entry_points.txt +3 -0
  5. tpyc/__init__.py +104 -0
  6. tpyc/__main__.py +6 -0
  7. tpyc/_buildinfo.py +1 -0
  8. tpyc/_data/docs/LANGUAGE_FEATURES.md +6278 -0
  9. tpyc/_data/docs/STDLIB_ROADMAP.md +1258 -0
  10. tpyc/_data/docs/TPY_FOR_AGENTS.md +556 -0
  11. tpyc/_data/lib/tpy/_bindings/__init__.py +6 -0
  12. tpyc/_data/lib/tpy/_bindings/pcre2.py +173 -0
  13. tpyc/_data/lib/tpy/_bindings/posix_socket.py +161 -0
  14. tpyc/_data/lib/tpy/_functools_macros.py +80 -0
  15. tpyc/_data/lib/tpy/_macro_helpers.py +161 -0
  16. tpyc/_data/lib/tpy/argparse.py +2062 -0
  17. tpyc/_data/lib/tpy/asyncio/__init__.py +744 -0
  18. tpyc/_data/lib/tpy/asyncio/_executor.py +515 -0
  19. tpyc/_data/lib/tpy/base64.py +410 -0
  20. tpyc/_data/lib/tpy/bisect.py +39 -0
  21. tpyc/_data/lib/tpy/builtins.py +38 -0
  22. tpyc/_data/lib/tpy/dataclasses.py +354 -0
  23. tpyc/_data/lib/tpy/enum.py +23 -0
  24. tpyc/_data/lib/tpy/functools.py +33 -0
  25. tpyc/_data/lib/tpy/hashlib.py +206 -0
  26. tpyc/_data/lib/tpy/heapq.py +118 -0
  27. tpyc/_data/lib/tpy/io.py +395 -0
  28. tpyc/_data/lib/tpy/json.py +221 -0
  29. tpyc/_data/lib/tpy/math.py +406 -0
  30. tpyc/_data/lib/tpy/random.py +597 -0
  31. tpyc/_data/lib/tpy/re.py +467 -0
  32. tpyc/_data/lib/tpy/socket.py +379 -0
  33. tpyc/_data/lib/tpy/struct.py +178 -0
  34. tpyc/_data/lib/tpy/sys.py +40 -0
  35. tpyc/_data/lib/tpy/time.py +39 -0
  36. tpyc/_data/lib/tpy/tpy/__init__.py +78 -0
  37. tpyc/_data/lib/tpy/tpy/_bootstrap/__init__.py +10 -0
  38. tpyc/_data/lib/tpy/tpy/_bootstrap/_decorators.py +37 -0
  39. tpyc/_data/lib/tpy/tpy/_bootstrap/_extern.py +64 -0
  40. tpyc/_data/lib/tpy/tpy/_builtins/__init__.py +11 -0
  41. tpyc/_data/lib/tpy/tpy/_builtins/_bytes.py +378 -0
  42. tpyc/_data/lib/tpy/tpy/_builtins/_dict.py +151 -0
  43. tpyc/_data/lib/tpy/tpy/_builtins/_exceptions.py +125 -0
  44. tpyc/_data/lib/tpy/tpy/_builtins/_funcs.py +681 -0
  45. tpyc/_data/lib/tpy/tpy/_builtins/_io.py +97 -0
  46. tpyc/_data/lib/tpy/tpy/_builtins/_list.py +127 -0
  47. tpyc/_data/lib/tpy/tpy/_builtins/_range.py +52 -0
  48. tpyc/_data/lib/tpy/tpy/_builtins/_set.py +139 -0
  49. tpyc/_data/lib/tpy/tpy/_builtins/_super.py +11 -0
  50. tpyc/_data/lib/tpy/tpy/_builtins/_types.py +661 -0
  51. tpyc/_data/lib/tpy/tpy/_core/__init__.py +23 -0
  52. tpyc/_data/lib/tpy/tpy/_core/_bytes_view.py +129 -0
  53. tpyc/_data/lib/tpy/tpy/_core/_containers.py +137 -0
  54. tpyc/_data/lib/tpy/tpy/_core/_functions.py +40 -0
  55. tpyc/_data/lib/tpy/tpy/_core/_types.py +2061 -0
  56. tpyc/_data/lib/tpy/tpy/_typing/__init__.py +77 -0
  57. tpyc/_data/lib/tpy/tpy/_version.py +29 -0
  58. tpyc/_data/lib/tpy/tpy/bits.py +28 -0
  59. tpyc/_data/lib/tpy/tpy/coro/__init__.py +127 -0
  60. tpyc/_data/lib/tpy/tpy/extern.py +8 -0
  61. tpyc/_data/lib/tpy/tpy/mem.py +49 -0
  62. tpyc/_data/lib/tpy/tpy/unsafe.py +195 -0
  63. tpyc/_data/lib/tpy/tpy/version.py +21 -0
  64. tpyc/_data/lib/tpy/typing.py +13 -0
  65. tpyc/_data/runtime/cpp/include/tpy/any.hpp +461 -0
  66. tpyc/_data/runtime/cpp/include/tpy/as_ostream.hpp +117 -0
  67. tpyc/_data/runtime/cpp/include/tpy/async.hpp +76 -0
  68. tpyc/_data/runtime/cpp/include/tpy/bigint.hpp +1343 -0
  69. tpyc/_data/runtime/cpp/include/tpy/builtins.hpp +400 -0
  70. tpyc/_data/runtime/cpp/include/tpy/bytes_ops.hpp +469 -0
  71. tpyc/_data/runtime/cpp/include/tpy/container_ops.hpp +487 -0
  72. tpyc/_data/runtime/cpp/include/tpy/copy_iter.hpp +82 -0
  73. tpyc/_data/runtime/cpp/include/tpy/core.hpp +558 -0
  74. tpyc/_data/runtime/cpp/include/tpy/dict_ops.hpp +289 -0
  75. tpyc/_data/runtime/cpp/include/tpy/dunder.hpp +750 -0
  76. tpyc/_data/runtime/cpp/include/tpy/dynamic.hpp +44 -0
  77. tpyc/_data/runtime/cpp/include/tpy/enum.hpp +40 -0
  78. tpyc/_data/runtime/cpp/include/tpy/file.hpp +245 -0
  79. tpyc/_data/runtime/cpp/include/tpy/fixed_int.hpp +317 -0
  80. tpyc/_data/runtime/cpp/include/tpy/format.hpp +954 -0
  81. tpyc/_data/runtime/cpp/include/tpy/frame_slot.hpp +120 -0
  82. tpyc/_data/runtime/cpp/include/tpy/generator.hpp +47 -0
  83. tpyc/_data/runtime/cpp/include/tpy/iterable_ops.hpp +122 -0
  84. tpyc/_data/runtime/cpp/include/tpy/itertools.hpp +749 -0
  85. tpyc/_data/runtime/cpp/include/tpy/next_iter.hpp +82 -0
  86. tpyc/_data/runtime/cpp/include/tpy/ordered_map.hpp +518 -0
  87. tpyc/_data/runtime/cpp/include/tpy/ordered_set.hpp +337 -0
  88. tpyc/_data/runtime/cpp/include/tpy/own_iter.hpp +54 -0
  89. tpyc/_data/runtime/cpp/include/tpy/pascal_graph_sdl.hpp +192 -0
  90. tpyc/_data/runtime/cpp/include/tpy/printing.hpp +302 -0
  91. tpyc/_data/runtime/cpp/include/tpy/protocols.hpp +61 -0
  92. tpyc/_data/runtime/cpp/include/tpy/range.hpp +115 -0
  93. tpyc/_data/runtime/cpp/include/tpy/ranges.hpp +212 -0
  94. tpyc/_data/runtime/cpp/include/tpy/set_ops.hpp +265 -0
  95. tpyc/_data/runtime/cpp/include/tpy/slice.hpp +47 -0
  96. tpyc/_data/runtime/cpp/include/tpy/span_iter.hpp +42 -0
  97. tpyc/_data/runtime/cpp/include/tpy/stdlib/math.hpp +41 -0
  98. tpyc/_data/runtime/cpp/include/tpy/stdlib/pcre2_h.hpp +96 -0
  99. tpyc/_data/runtime/cpp/include/tpy/stdlib/random.hpp +25 -0
  100. tpyc/_data/runtime/cpp/include/tpy/stdlib/socket_h.hpp +145 -0
  101. tpyc/_data/runtime/cpp/include/tpy/stdlib/time.hpp +62 -0
  102. tpyc/_data/runtime/cpp/include/tpy/system.hpp +121 -0
  103. tpyc/_data/runtime/cpp/include/tpy/throwable.hpp +55 -0
  104. tpyc/_data/runtime/cpp/include/tpy/tpy.hpp +156 -0
  105. tpyc/_data/runtime/cpp/include/tpy/type_name.hpp +77 -0
  106. tpyc/_data/runtime/cpp/include/tpy/type_traits.hpp +240 -0
  107. tpyc/_data/runtime/cpp/include/tpy/uninit_array_storage.hpp +250 -0
  108. tpyc/_data/runtime/cpp/include/tpy/uninit_heap_storage.hpp +277 -0
  109. tpyc/_data/runtime/cpp/include/tpy/varargs.hpp +174 -0
  110. tpyc/_data/runtime/cpp/include/tpy/variant_ref.hpp +118 -0
  111. tpyc/_data/runtime/cpp/src/stdlib/socket_impl.cpp +104 -0
  112. tpyc/_data/runtime/cpp/third_party/README.md +58 -0
  113. tpyc/_data/runtime/cpp/third_party/pcre2/AUTHORS +36 -0
  114. tpyc/_data/runtime/cpp/third_party/pcre2/CMakeLists.txt +1233 -0
  115. tpyc/_data/runtime/cpp/third_party/pcre2/COPYING +5 -0
  116. tpyc/_data/runtime/cpp/third_party/pcre2/ChangeLog +3097 -0
  117. tpyc/_data/runtime/cpp/third_party/pcre2/HACKING +853 -0
  118. tpyc/_data/runtime/cpp/third_party/pcre2/INSTALL +368 -0
  119. tpyc/_data/runtime/cpp/third_party/pcre2/LICENCE +94 -0
  120. tpyc/_data/runtime/cpp/third_party/pcre2/NEWS +492 -0
  121. tpyc/_data/runtime/cpp/third_party/pcre2/NON-AUTOTOOLS-BUILD +430 -0
  122. tpyc/_data/runtime/cpp/third_party/pcre2/README +956 -0
  123. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/COPYING-CMAKE-SCRIPTS +22 -0
  124. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindEditline.cmake +16 -0
  125. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindPackageHandleStandardArgs.cmake +58 -0
  126. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindReadline.cmake +29 -0
  127. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config-version.cmake.in +15 -0
  128. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config.cmake.in +148 -0
  129. tpyc/_data/runtime/cpp/third_party/pcre2/config-cmake.h.in +56 -0
  130. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-16.pc.in +13 -0
  131. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-32.pc.in +13 -0
  132. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-8.pc.in +13 -0
  133. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-posix.pc.in +13 -0
  134. tpyc/_data/runtime/cpp/third_party/pcre2/pcre2-config.in +121 -0
  135. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h +483 -0
  136. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.generic +483 -0
  137. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.in +460 -0
  138. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h +1010 -0
  139. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.generic +1010 -0
  140. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.in +1010 -0
  141. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_auto_possess.c +1371 -0
  142. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c +196 -0
  143. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c.dist +196 -0
  144. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chkdint.c +96 -0
  145. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_compile.c +11001 -0
  146. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_config.c +252 -0
  147. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_context.c +510 -0
  148. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_convert.c +1189 -0
  149. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dfa_match.c +4119 -0
  150. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dftables.c +297 -0
  151. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_error.c +345 -0
  152. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_extuni.c +162 -0
  153. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_find_bracket.c +219 -0
  154. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_fuzzsupport.c +792 -0
  155. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_internal.h +2084 -0
  156. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_intmodedep.h +940 -0
  157. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_compile.c +14972 -0
  158. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_match.c +200 -0
  159. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_misc.c +234 -0
  160. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_neon_inc.h +354 -0
  161. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_simd_inc.h +2355 -0
  162. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_test.c +2528 -0
  163. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_maketables.c +165 -0
  164. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match.c +7777 -0
  165. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match_data.c +185 -0
  166. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_newline.c +243 -0
  167. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ord2utf.c +120 -0
  168. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_pattern_info.c +432 -0
  169. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_printint.c +886 -0
  170. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_script_run.c +344 -0
  171. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_serialize.c +286 -0
  172. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_string_utils.c +237 -0
  173. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_study.c +1915 -0
  174. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substitute.c +1009 -0
  175. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substring.c +550 -0
  176. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_tables.c +234 -0
  177. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucd.c +5460 -0
  178. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucp.h +396 -0
  179. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucptables.c +1533 -0
  180. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_valid_utf.c +398 -0
  181. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_xclass.c +308 -0
  182. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2demo.c +497 -0
  183. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2grep.c +4606 -0
  184. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.c +425 -0
  185. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.h +187 -0
  186. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix_test.c +209 -0
  187. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2test.c +9708 -0
  188. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorApple.c +137 -0
  189. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorCore.c +327 -0
  190. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorFreeBSD.c +89 -0
  191. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorPosix.c +62 -0
  192. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorWindows.c +40 -0
  193. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorNetBSD.c +72 -0
  194. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorPosix.c +172 -0
  195. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorPosix.c +141 -0
  196. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorWindows.c +102 -0
  197. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfig.h +142 -0
  198. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigCPU.h +188 -0
  199. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigInternal.h +907 -0
  200. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.c +3561 -0
  201. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.h +2466 -0
  202. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_32.c +4636 -0
  203. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_64.c +3491 -0
  204. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_T2_32.c +4302 -0
  205. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeLOONGARCH_64.c +3765 -0
  206. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_32.c +472 -0
  207. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_64.c +387 -0
  208. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_common.c +4259 -0
  209. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_32.c +485 -0
  210. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_64.c +719 -0
  211. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_common.c +3161 -0
  212. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_32.c +142 -0
  213. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_64.c +222 -0
  214. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_common.c +3121 -0
  215. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeS390X.c +4526 -0
  216. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_32.c +1685 -0
  217. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_64.c +1398 -0
  218. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_common.c +5001 -0
  219. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitSerialize.c +516 -0
  220. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitUtils.c +344 -0
  221. tpyc/_data/runtime/cpp/third_party/pcre2.sources.txt +54 -0
  222. tpyc/_data/runtime/cpp/third_party/pcre2.vendor.json +7 -0
  223. tpyc/build/__init__.py +7 -0
  224. tpyc/build/pcre2.py +122 -0
  225. tpyc/build/third_party.py +413 -0
  226. tpyc/cli.py +822 -0
  227. tpyc/codegen_cpp/__init__.py +18 -0
  228. tpyc/codegen_cpp/builtins.py +484 -0
  229. tpyc/codegen_cpp/context.py +2064 -0
  230. tpyc/codegen_cpp/expressions.py +5940 -0
  231. tpyc/codegen_cpp/functions.py +1913 -0
  232. tpyc/codegen_cpp/gen_async.py +3258 -0
  233. tpyc/codegen_cpp/gen_generators.py +657 -0
  234. tpyc/codegen_cpp/generator.py +2258 -0
  235. tpyc/codegen_cpp/match.py +1997 -0
  236. tpyc/codegen_cpp/param_const.py +172 -0
  237. tpyc/codegen_cpp/protocols.py +907 -0
  238. tpyc/codegen_cpp/records.py +1654 -0
  239. tpyc/codegen_cpp/resumable_cfg.py +1651 -0
  240. tpyc/codegen_cpp/statements.py +4963 -0
  241. tpyc/codegen_cpp/string_dispatch.py +76 -0
  242. tpyc/codegen_cpp/test_context.py +46 -0
  243. tpyc/codegen_cpp/test_param_const.py +113 -0
  244. tpyc/codegen_cpp/test_resumable_cfg.py +182 -0
  245. tpyc/codegen_cpp/type_resolution.py +53 -0
  246. tpyc/codegen_cpp/types.py +436 -0
  247. tpyc/codegen_cpp/variant_access.py +135 -0
  248. tpyc/coercions.py +749 -0
  249. tpyc/compilation_context.py +57 -0
  250. tpyc/compiler.py +3945 -0
  251. tpyc/cycle_detection.py +358 -0
  252. tpyc/diagnostics.py +135 -0
  253. tpyc/dump_types.py +353 -0
  254. tpyc/frontend_diagnostics.py +47 -0
  255. tpyc/frontend_ir/__init__.py +140 -0
  256. tpyc/frontend_ir/lower.py +1098 -0
  257. tpyc/frontend_ir/nodes.py +718 -0
  258. tpyc/frontend_ir/resolver_adapter.py +151 -0
  259. tpyc/frontend_plugin.py +209 -0
  260. tpyc/install_docs.py +81 -0
  261. tpyc/liveness.py +756 -0
  262. tpyc/macro_api.py +1724 -0
  263. tpyc/macro_loader.py +497 -0
  264. tpyc/module_names.py +64 -0
  265. tpyc/modules/__init__.py +31 -0
  266. tpyc/modules/defs.py +89 -0
  267. tpyc/modules/registry.py +36 -0
  268. tpyc/modules/resolver.py +192 -0
  269. tpyc/modules/type_resolution.py +629 -0
  270. tpyc/namespace.py +172 -0
  271. tpyc/parse/__init__.py +84 -0
  272. tpyc/parse/imports.py +490 -0
  273. tpyc/parse/nodes.py +1732 -0
  274. tpyc/parse/parser.py +4043 -0
  275. tpyc/parse/resolve_refs.py +466 -0
  276. tpyc/parse/type_resolver.py +1060 -0
  277. tpyc/prescan.py +254 -0
  278. tpyc/qnames.py +149 -0
  279. tpyc/repl.py +529 -0
  280. tpyc/repl_backends.py +848 -0
  281. tpyc/sema/__init__.py +21 -0
  282. tpyc/sema/analyzer.py +3625 -0
  283. tpyc/sema/bound_check.py +72 -0
  284. tpyc/sema/builder_trace.py +684 -0
  285. tpyc/sema/calls.py +5406 -0
  286. tpyc/sema/compatibility.py +2107 -0
  287. tpyc/sema/context.py +1243 -0
  288. tpyc/sema/expressions.py +3737 -0
  289. tpyc/sema/flow_facts.py +199 -0
  290. tpyc/sema/init_tracker.py +150 -0
  291. tpyc/sema/list_literals.py +69 -0
  292. tpyc/sema/literal_utils.py +27 -0
  293. tpyc/sema/local_deduction.py +1088 -0
  294. tpyc/sema/macros.py +179 -0
  295. tpyc/sema/match.py +1177 -0
  296. tpyc/sema/method_expansion.py +347 -0
  297. tpyc/sema/methods.py +2197 -0
  298. tpyc/sema/mutation_propagation.py +268 -0
  299. tpyc/sema/narrowing.py +857 -0
  300. tpyc/sema/numeric_lattice.py +160 -0
  301. tpyc/sema/operators.py +402 -0
  302. tpyc/sema/overloads.py +841 -0
  303. tpyc/sema/protocols.py +1209 -0
  304. tpyc/sema/reach_analysis.py +202 -0
  305. tpyc/sema/registration.py +3156 -0
  306. tpyc/sema/scope_tracker.py +193 -0
  307. tpyc/sema/statements.py +4426 -0
  308. tpyc/sema/type_ops.py +1879 -0
  309. tpyc/sema/value_range.py +181 -0
  310. tpyc/symbol_binding.py +259 -0
  311. tpyc/test_c3_mro.py +208 -0
  312. tpyc/test_cli_argv.py +52 -0
  313. tpyc/test_compiler.py +559 -0
  314. tpyc/test_contains_type_param.py +101 -0
  315. tpyc/test_cycle_detection.py +221 -0
  316. tpyc/test_dump_types.py +225 -0
  317. tpyc/test_install_docs.py +65 -0
  318. tpyc/test_local_cpp_form.py +135 -0
  319. tpyc/test_macro_loader.py +76 -0
  320. tpyc/test_method_expansion.py +254 -0
  321. tpyc/test_nominal_identity.py +182 -0
  322. tpyc/test_overloads.py +410 -0
  323. tpyc/test_parse.py +303 -0
  324. tpyc/test_parse_type_ref.py +506 -0
  325. tpyc/test_parse_version_info.py +58 -0
  326. tpyc/test_reach_analysis.py +72 -0
  327. tpyc/test_ref_type.py +216 -0
  328. tpyc/test_send_sync_substitution.py +276 -0
  329. tpyc/test_tuple_mutation_propagation.py +206 -0
  330. tpyc/test_type_def_registry.py +1729 -0
  331. tpyc/test_union_types.py +195 -0
  332. tpyc/type_def_registry.py +975 -0
  333. tpyc/typesys.py +5104 -0
tpyc/sema/methods.py ADDED
@@ -0,0 +1,2197 @@
1
+ """
2
+ TurboPython Method Analysis
3
+
4
+ Method call and super() analysis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import copy
9
+ from typing import TYPE_CHECKING, Callable
10
+
11
+ from ..typesys import (
12
+ TpyType, NominalType, OwnType, OptionalType, PendingListType, PendingDictType, PendingSetType,
13
+ SuperType, TypeParamRef, FunctionInfo, ParamInfo, VOID, is_protocol_type,
14
+ PtrType, ReadonlyType, unwrap_readonly, UnknownElementType,
15
+ PendingGenericInstanceType, IntLiteralType, CallableType, unwrap_ref_type, unwrap_qualifiers, is_any_int_type,
16
+ is_callable_type,
17
+ RecordInfo,
18
+ contains_type_param,
19
+ )
20
+ from ..parse import (
21
+ TpyCall, TpyMethodCall, TpyName, TpyFieldAccess, TpyFunction, TpyExprStmt, TpyStrLiteral, TpyStmt,
22
+ TpyLambda, TpyStarUnpack,
23
+ is_docstring,
24
+ TpyFString, TpyExpr,
25
+ is_super_del_call,
26
+ )
27
+ from ..namespace import BindingKind
28
+ from ..coercions import CoercionContext
29
+ from ..prescan import _expr_to_narrowing_key
30
+ from .narrowing import deref_view_narrowed
31
+ from ..diagnostics import OPTIONAL_NONE_ACCESS_WARNING, SemanticError
32
+ from ..type_def_registry import is_list, is_fstr_type
33
+ from .overloads import resolve_overload, OverloadAmbiguityError
34
+ from .bound_check import raise_if_class_param_bound_violated
35
+ from .calls import (
36
+ arity_error_msg, resolve_kwargs, validate_generic_defaults,
37
+ validate_type_param_bounds,
38
+ _enrich_literal_types,
39
+ resolve_inferred_type_arg,
40
+ )
41
+ from .type_ops import seeded_arg_hint
42
+ from .statements import _root_name_of_expr, _is_self_call_deferred, _local_traces_to_self
43
+
44
+ if TYPE_CHECKING:
45
+ from .context import SemanticContext
46
+ from .type_ops import TypeOperations
47
+ from .protocols import ProtocolChecker
48
+ from .compatibility import TypeCompatibility
49
+ from .local_deduction import LocalTypeDeduction
50
+ from .expressions import ExpressionAnalyzer
51
+ from .calls import CallAnalyzer
52
+ from ..typesys import PendingGenericInstanceInfo
53
+
54
+ from .context import _storage_key
55
+
56
+
57
+ def _unresolved_params_in_type(typ: TpyType, inferred: dict[str, TpyType], param_names: set[str]) -> list[str]:
58
+ """Return list of type param names that appear in typ but are not yet in inferred."""
59
+ result: list[str] = []
60
+ _collect_unresolved(typ, inferred, param_names, result)
61
+ return result
62
+
63
+
64
+ def _collect_unresolved(
65
+ typ: TpyType, inferred: dict[str, TpyType], param_names: set[str], out: list[str],
66
+ ) -> None:
67
+ if isinstance(typ, TypeParamRef):
68
+ if typ.name in param_names and typ.name not in inferred and typ.name not in out:
69
+ out.append(typ.name)
70
+ return
71
+ if isinstance(typ, NominalType) and typ.type_args:
72
+ for a in typ.type_args:
73
+ if isinstance(a, TpyType):
74
+ _collect_unresolved(a, inferred, param_names, out)
75
+ return
76
+ for attr in ('element_type', 'pointee', 'inner', 'wrapped'):
77
+ inner = getattr(typ, attr, None)
78
+ if inner is not None and isinstance(inner, TpyType):
79
+ _collect_unresolved(inner, inferred, param_names, out)
80
+ if hasattr(typ, 'element_types'):
81
+ for e in typ.element_types:
82
+ _collect_unresolved(e, inferred, param_names, out)
83
+ if hasattr(typ, 'key_type') and hasattr(typ, 'value_type'):
84
+ _collect_unresolved(typ.key_type, inferred, param_names, out)
85
+ _collect_unresolved(typ.value_type, inferred, param_names, out)
86
+ if hasattr(typ, 'members'):
87
+ for m in typ.members:
88
+ _collect_unresolved(m, inferred, param_names, out)
89
+
90
+
91
+ def _resolve_dotted_record_chain(expr: TpyFieldAccess, ctx: 'SemanticContext') -> str | None:
92
+ """Resolve a chain of field accesses to a nested record dotted name.
93
+
94
+ Returns the dotted name (e.g., "Outer.Mid") if the chain resolves to a
95
+ registered nested record, or None otherwise.
96
+ """
97
+ if isinstance(expr.obj, TpyName):
98
+ if ctx.func.current_ns:
99
+ binding = ctx.func.current_ns.lookup(expr.obj.name)
100
+ if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
101
+ dotted = f"{expr.obj.name}.{expr.field}"
102
+ if ctx.registry.get_record(dotted) is not None:
103
+ return dotted
104
+ elif isinstance(expr.obj, TpyFieldAccess):
105
+ parent = _resolve_dotted_record_chain(expr.obj, ctx)
106
+ if parent is not None:
107
+ dotted = f"{parent}.{expr.field}"
108
+ if ctx.registry.get_record(dotted) is not None:
109
+ return dotted
110
+ return None
111
+
112
+
113
+ class MethodAnalyzer:
114
+ """Method call and super() analysis."""
115
+
116
+ def __init__(
117
+ self,
118
+ ctx: SemanticContext,
119
+ type_ops: TypeOperations,
120
+ protocols: ProtocolChecker,
121
+ compat: TypeCompatibility,
122
+ deduction: LocalTypeDeduction,
123
+ ):
124
+ self.ctx = ctx
125
+ self.type_ops = type_ops
126
+ self.protocols = protocols
127
+ self.compat = compat
128
+ self.deduction = deduction
129
+ # Set after construction to break circular dep (expr <-> calls <-> methods)
130
+ self.expr: ExpressionAnalyzer
131
+ self.calls: CallAnalyzer
132
+
133
+ def _is_readonly_method(self, obj_type: TpyType, method_name: str) -> bool:
134
+ """Check if a method is readonly on the given type (via RecordInfo)."""
135
+ record = self.ctx.registry.get_record_for_type(obj_type)
136
+ if record is None:
137
+ return False
138
+ overloads = record.get_method_overloads(method_name)
139
+ return bool(overloads) and all(m.is_readonly for m in overloads)
140
+
141
+ def _is_invalidating_method(self, obj_type: TpyType, method_name: str) -> bool:
142
+ """Check if a method invalidates iterators/references on a container.
143
+
144
+ For builtin types (list, dict, set, etc.): a method invalidates if it
145
+ is non-readonly AND not marked with @native_preserves_refs.
146
+ For user-defined types: fall back to treating any non-readonly method
147
+ as potentially invalidating, since we can't know if it reallocates.
148
+ """
149
+ # Builtin types: precise check using native_preserves_refs
150
+ qname = obj_type.qualified_name()
151
+ if qname:
152
+ builtin_record = self.ctx.registry.get_builtin_record(qname)
153
+ if builtin_record is not None:
154
+ overloads = builtin_record.get_method_overloads(method_name)
155
+ if not overloads:
156
+ return False
157
+ return any(not m.is_readonly and not m.native_preserves_refs for m in overloads)
158
+ # User-defined types: use inferred direct_self_mutated from Phase 1
159
+ # mutation analysis. More precise than is_readonly -- a method that
160
+ # doesn't mutate self won't invalidate references even without
161
+ # @readonly annotation. None means not yet analyzed (forward ref);
162
+ # treat conservatively as potentially mutating.
163
+ record = self.ctx.registry.get_record_for_type(obj_type)
164
+ if record is None:
165
+ return False
166
+ overloads = record.get_method_overloads(method_name)
167
+ if not overloads:
168
+ return False
169
+ # Use inferred structural mutation: only methods that directly
170
+ # structurally mutate self (append/insert/del on self's fields) can
171
+ # invalidate references. Getters and field-only mutations are safe.
172
+ # None means not yet analyzed (forward ref) -> conservative.
173
+ # Note: indirect structural mutation through same-class method calls
174
+ # is not detected here (requires Phase 2 propagation, which runs
175
+ # after body analysis). This is a known limitation.
176
+ for m in overloads:
177
+ if m.is_readonly:
178
+ continue
179
+ smp = m.direct_structural_mutated_params
180
+ if smp is None or -1 in smp:
181
+ return True
182
+ return False
183
+
184
+ def _infer_pending_container_element(
185
+ self,
186
+ expr: TpyMethodCall,
187
+ obj_type: TpyType,
188
+ builtin_qname: str,
189
+ literal_id: int,
190
+ infer_fn: Callable,
191
+ make_pending: Callable,
192
+ literals_dict: dict,
193
+ ) -> TpyType | None:
194
+ """Infer element type for a pending container from method arg types.
195
+
196
+ Looks up the method's FunctionInfo, finds params that involve the
197
+ container's type parameters, pre-analyzes the corresponding args,
198
+ and infers element types. Returns the updated obj_type if changed.
199
+ """
200
+ record = self.ctx.registry.get_builtin_record(builtin_qname)
201
+ if not record or not record.type_params:
202
+ return None
203
+ type_param_names = set(record.type_params)
204
+ overloads = record.get_method_overloads(expr.method)
205
+ if not overloads:
206
+ return None
207
+
208
+ # Find an overload with matching arity that has type-param-bearing params
209
+ for overload in overloads:
210
+ if len(overload.params) != len(expr.args):
211
+ continue
212
+ # Identify which args correspond to type-parameter-bearing params
213
+ inferring_indices: list[int] = []
214
+ for i, param in enumerate(overload.params):
215
+ if contains_type_param(param.type, type_param_names):
216
+ inferring_indices.append(i)
217
+ if not inferring_indices:
218
+ continue
219
+
220
+ # Pre-analyze all args (needed for _check_and_coerce_args reuse).
221
+ # analyze_call_arg (not analyze_expr) so a `*xs` arg yields the
222
+ # unpacked element type instead of hitting the structural
223
+ # analyzer's "Unknown expression type" catch-all; the non-variadic
224
+ # reject gate in _check_args_or_pack_varargs still rejects it.
225
+ pre_analyzed = [self.expr.analyze_call_arg(arg) for arg in expr.args]
226
+
227
+ # Infer element type from params that directly carry a type param
228
+ # (T or Own[T]). Params with nested type params like Iterable[Own[T]]
229
+ # are detected by contains_type_param but not handled here --
230
+ # inference from those would need protocol-level element type extraction.
231
+ for i in inferring_indices:
232
+ arg_type = pre_analyzed[i]
233
+ param_type = unwrap_ref_type(overload.params[i].type)
234
+ if isinstance(param_type, OwnType) and isinstance(param_type.wrapped, TypeParamRef):
235
+ infer_fn(expr.obj, arg_type)
236
+ elif isinstance(param_type, TypeParamRef):
237
+ infer_fn(expr.obj, arg_type)
238
+
239
+ # Update obj_type if element type changed
240
+ info = literals_dict.get(literal_id)
241
+ if info and not isinstance(info.element_type, UnknownElementType):
242
+ if info.element_type != obj_type.get_element_type():
243
+ obj_type = make_pending(info.element_type, literal_id)
244
+ self.ctx.set_expr_type(expr.obj, obj_type)
245
+ if isinstance(expr.obj, TpyName):
246
+ if self.ctx.func.current_scope:
247
+ self.ctx.func.current_scope.define(expr.obj.name, obj_type)
248
+ if self.ctx.func.current_ns:
249
+ self.ctx.func.current_ns.bind_variable(expr.obj.name, obj_type)
250
+ self.ctx.func.pre_analyzed_method_args[id(expr)] = pre_analyzed
251
+ return obj_type
252
+ return None
253
+
254
+ def _probe_analyze_method_args(
255
+ self,
256
+ expr: TpyMethodCall,
257
+ resolved_overloads: list[FunctionInfo],
258
+ ) -> list[TpyType]:
259
+ """Pre-analyze overload-candidate method args with an LHS-derived hint.
260
+
261
+ Method-call sibling of ``CallAnalyzer._probe_analyze_args``: each
262
+ substituted candidate's per-position hint comes from
263
+ ``TypeOperations.candidate_arg_hints``; the first non-None hint at
264
+ each position drives the arg's first analysis.
265
+
266
+ Without this, the inner generic-call / record-ctor arg would cache
267
+ a hint-naive type during probe pre-analysis, and the post-selection
268
+ ``_check_and_coerce_args`` would re-use that cached type verbatim
269
+ (the cache short-circuit at the top of
270
+ ``_analyze_generic_function_call`` / ``_analyze_record_constructor``
271
+ prevents the re-analysis from refreshing it).
272
+ """
273
+ lhs_hint = self.ctx.expr_type_hint
274
+ if lhs_hint is None:
275
+ return [self.expr.analyze_call_arg(arg) for arg in expr.args]
276
+
277
+ n = len(expr.args)
278
+ candidate_hints = [
279
+ self.type_ops.candidate_arg_hints(f, n, lhs_hint) for f in resolved_overloads
280
+ ]
281
+ # Require a single LHS-matching candidate (see
282
+ # CallAnalyzer._probe_analyze_args for the cross-candidate-mixing
283
+ # rationale). ``candidate_arg_hints`` returns None for non-matching
284
+ # candidates; the count tracks LHS-matchers, not hint-contributors.
285
+ matching = [c for c in candidate_hints if c is not None]
286
+ chosen: list[TpyType | None] = (
287
+ list(matching[0]) if len(matching) == 1 else [None] * n
288
+ )
289
+ arg_types: list[TpyType] = []
290
+ for i, arg in enumerate(expr.args):
291
+ hint = chosen[i]
292
+ # See CallAnalyzer._probe_analyze_args for the (TpyName | TpyLambda)
293
+ # + Callable filter rationale: avoid mutating AST state on the arg
294
+ # node before overload selection.
295
+ if hint is not None and is_callable_type(hint) and isinstance(arg, (TpyName, TpyLambda)):
296
+ hint = None
297
+ arg_types.append(self.expr.analyze_call_arg(arg, hint))
298
+ return arg_types
299
+
300
+ def _check_args_or_pack_varargs(
301
+ self, expr: TpyMethodCall,
302
+ resolved: FunctionInfo,
303
+ arg_types: list[TpyType] | None = None,
304
+ ) -> None:
305
+ """Dispatch arg checking based on whether the method has *args.
306
+
307
+ Variadic methods route through `_analyze_and_pack_varargs` (mirrors
308
+ the free-function path). The pre-analyzed cache is dropped because
309
+ it holds types analyzed against Span[T], not the element type T
310
+ the packer needs. ``arg_types`` is ignored on the variadic path --
311
+ the packer re-analyzes every arg from scratch.
312
+ """
313
+ if resolved.has_variadic:
314
+ self.ctx.func.pre_analyzed_method_args.pop(id(expr), None)
315
+ self.calls._analyze_and_pack_varargs(expr, resolved)
316
+ else:
317
+ # `*unpack` is only valid at a variadic param position; a
318
+ # non-variadic method must reject it (mirrors the free-function
319
+ # guard in CallAnalyzer._typecheck_call_args). Without this the
320
+ # pre-analyzed element type would be silently coerced as if the
321
+ # unpack were a single positional arg.
322
+ for arg in expr.args:
323
+ if isinstance(arg, TpyStarUnpack):
324
+ raise self.ctx.error(
325
+ f"Cannot use *unpacking: '{resolved.name}' "
326
+ f"does not accept *args", arg)
327
+ self._check_and_coerce_args(expr, resolved.params, arg_types,
328
+ target_is_readonly=resolved.is_readonly)
329
+
330
+ def _check_and_coerce_args(
331
+ self, expr: TpyMethodCall,
332
+ params: list[tuple[str, TpyType]],
333
+ arg_types: list[TpyType] | None = None,
334
+ target_is_readonly: bool = False,
335
+ ) -> None:
336
+ """Analyze, ownership-check, and coerce method arguments in place.
337
+
338
+ If arg_types is None, each arg is analyzed with a type hint from the
339
+ corresponding param (using pre-analyzed types from empty list inference
340
+ when available). Otherwise pre-analyzed arg_types are used.
341
+ """
342
+ pre = self.ctx.func.pre_analyzed_method_args.pop(id(expr), None)
343
+ for i, (arg, (pname, ptype)) in enumerate(zip(expr.args, params)):
344
+ if arg_types is not None:
345
+ at = arg_types[i]
346
+ elif pre is not None and i < len(pre):
347
+ at = pre[i]
348
+ else:
349
+ at = self.expr.analyze_expr_with_hint(arg, ptype)
350
+ self.calls._maybe_coerce_empty_list_to_protocol(at, ptype)
351
+ at = self.calls._restore_readonly_arg(arg, at, target_is_readonly)
352
+ self.calls.check_own_param(arg, at, pname, ptype)
353
+ expr.args[i] = self.compat.coerce_expr(arg, at, ptype, f"argument '{pname}'",
354
+ coercion_ctx=CoercionContext.ARG)
355
+
356
+ def _resolve_and_check_args(
357
+ self, expr: TpyMethodCall,
358
+ overloads: list[FunctionInfo],
359
+ type_subst: dict[str, TpyType | int],
360
+ is_readonly_receiver: bool = False,
361
+ is_consuming_receiver: bool = False,
362
+ ) -> TpyType:
363
+ """Resolve overloads, substitute type params, check arg count, and coerce args.
364
+
365
+ Sets expr.resolved_function_info. Returns the return type.
366
+ """
367
+ # Pack **kwargs into TypedDict construction for methods with **kwargs param
368
+ if len(overloads) == 1 and overloads[0].kwarg_name:
369
+ func = overloads[0]
370
+ resolved_func = (self.type_ops.substitute_method_type_params(func, type_subst)
371
+ if type_subst else func)
372
+ self.calls._pack_kwargs_into_typed_dict_method(expr, resolved_func)
373
+ elif expr.double_star_unpack is not None:
374
+ raise self.ctx.error(
375
+ f"'{expr.method}' does not accept **kwargs", expr)
376
+
377
+ # Resolve kwargs before arity check. For the single-overload path we
378
+ # expand kwargs here; the multi-overload path defers resolution to
379
+ # after overload resolution so kwargs can participate in tier ranking.
380
+ has_kwonly = any(p.keyword_only for p in (overloads[0].params if len(overloads) == 1 else []))
381
+ if len(overloads) == 1 and (expr.kwargs or has_kwonly):
382
+ target = overloads[0]
383
+ resolved_target = (self.type_ops.substitute_method_type_params(target, type_subst)
384
+ if type_subst else target)
385
+ expr.args = resolve_kwargs(
386
+ expr.args, expr.kwargs, resolved_target.params, expr.method,
387
+ lambda msg: self.ctx.error(msg, expr),
388
+ call_loc=expr.loc,
389
+ )
390
+ expr.kwargs = {}
391
+
392
+ if len(overloads) == 1:
393
+ unresolved = overloads[0]
394
+ resolved = (self.type_ops.substitute_method_type_params(unresolved, type_subst)
395
+ if type_subst else unresolved)
396
+ if len(expr.args) < resolved.min_args or len(expr.args) > resolved.max_args:
397
+ raise self.ctx.error(
398
+ arity_error_msg(expr.method, resolved.min_args, resolved.max_args, len(expr.args)),
399
+ expr)
400
+ expr.resolved_function_info = resolved
401
+ self._check_args_or_pack_varargs(expr, resolved)
402
+ if type_subst:
403
+ validate_generic_defaults(
404
+ expr.args, unresolved, type_subst, self.type_ops,
405
+ lambda msg: self.ctx.error(msg, expr))
406
+ else:
407
+ resolved_overloads = [
408
+ self.type_ops.substitute_method_type_params(m, type_subst) if type_subst else m
409
+ for m in overloads
410
+ ]
411
+ arg_types = self._probe_analyze_method_args(expr, resolved_overloads)
412
+ kwarg_types: dict[str, TpyType] | None = None
413
+ if expr.kwargs:
414
+ kwarg_types = {k: self.expr.analyze_expr(v) for k, v in expr.kwargs.items()}
415
+ enriched_types = _enrich_literal_types(arg_types, expr.args, resolved_overloads)
416
+ try:
417
+ resolved = resolve_overload(
418
+ resolved_overloads, enriched_types,
419
+ protocol_checker=self.protocols.type_conforms_to_protocol,
420
+ protocol_classifier=self.protocols.classify_protocol_conformance,
421
+ deref_checker=self.type_ops.get_deref_coercion_target,
422
+ default_int_type=self.ctx.default_int_type,
423
+ subclass_checker=self.ctx.registry.is_subclass_of,
424
+ is_readonly_receiver=is_readonly_receiver,
425
+ is_consuming_receiver=is_consuming_receiver,
426
+ type_ops=self.type_ops,
427
+ kwarg_types=kwarg_types,
428
+ )
429
+ except OverloadAmbiguityError as e:
430
+ sigs = "; ".join(
431
+ f"{c.name}({', '.join(str(p.type) for p in c.params)})"
432
+ for c in e.candidates
433
+ )
434
+ raise self.ctx.error(
435
+ f"Ambiguous overload for '{expr.method}': "
436
+ f"multiple candidates match equally: {sigs}", expr)
437
+ if resolved is None:
438
+ if kwarg_types:
439
+ accepted_names = {p.name for o in overloads for p in o.params}
440
+ for kw_name in kwarg_types:
441
+ if kw_name not in accepted_names:
442
+ raise self.ctx.error(
443
+ f"'{expr.method}' got unexpected keyword argument '{kw_name}'",
444
+ expr)
445
+ arg_strs = ", ".join(str(t) for t in arg_types)
446
+ raise self.ctx.error(
447
+ f"No matching overload for '{expr.method}' with argument types ({arg_strs})", expr)
448
+ expr.resolved_function_info = resolved
449
+ coerce_arg_types: list[TpyType] | None = arg_types
450
+ if expr.kwargs:
451
+ # Winner picked; expand kwargs into positional slots against its signature.
452
+ expr.args = resolve_kwargs(
453
+ expr.args, expr.kwargs, resolved.params, expr.method,
454
+ lambda msg: self.ctx.error(msg, expr),
455
+ call_loc=expr.loc,
456
+ )
457
+ expr.kwargs = {}
458
+ # Pre-analyzed types no longer align with expanded expr.args;
459
+ # discard any stale empty-list inference cache so
460
+ # _check_and_coerce_args re-analyzes every arg with a param hint.
461
+ self.ctx.func.pre_analyzed_method_args.pop(id(expr), None)
462
+ coerce_arg_types = None
463
+ self._check_args_or_pack_varargs(expr, resolved, coerce_arg_types)
464
+ # Overloaded methods with generic defaults: find unresolved counterpart
465
+ if type_subst:
466
+ idx = resolved_overloads.index(resolved)
467
+ validate_generic_defaults(
468
+ expr.args, overloads[idx], type_subst, self.type_ops,
469
+ lambda msg: self.ctx.error(msg, expr))
470
+
471
+ self.calls._check_borrow_arg_conflicts(expr)
472
+ self.calls._check_loop_var_arg_mutation(expr)
473
+ self.calls._record_mutation_call_edges(expr)
474
+ self.calls._check_error_return_handled(expr, resolved)
475
+ return resolved.return_type
476
+
477
+ @staticmethod
478
+ def _analyze_super_call_static(ctx: SemanticContext, expr: TpyCall) -> TpyType:
479
+ """Analyze a super() call (static method for use from CallAnalyzer).
480
+
481
+ super() can only be called:
482
+ - Inside a method (not at module level)
483
+ - In a class that has a parent class
484
+ - Without arguments (Python 3 style)
485
+
486
+ Returns a SuperType that wraps the parent class type.
487
+ """
488
+ # Validate context: must be in a method
489
+ if ctx.func.current_function is None or not isinstance(ctx.func.current_function, TpyFunction):
490
+ raise ctx.error("super() can only be used inside a method", expr)
491
+
492
+ if not ctx.func.current_function.is_method:
493
+ raise ctx.error("super() can only be used inside a method", expr)
494
+
495
+ if ctx.func.current_function.is_staticmethod:
496
+ raise ctx.error("super() cannot be used in a static method", expr)
497
+
498
+ # Validate context: must have a current record
499
+ if ctx.record_ctx.record is None:
500
+ raise ctx.error("super() can only be used inside a class method", expr)
501
+
502
+ # Validate: class must have at least one parent
503
+ record_info = ctx.registry.get_record(ctx.record_ctx.record.name)
504
+ if record_info is None or not record_info.parents:
505
+ raise ctx.error(
506
+ f"super() requires a parent class, but '{ctx.record_ctx.record.name}' has no parent",
507
+ expr
508
+ )
509
+
510
+ # Validate: no arguments (Python 3 style only)
511
+ if expr.args:
512
+ raise ctx.error("super() takes no arguments (Python 3 style)", expr)
513
+
514
+ # For multi-base classes, parent_type is a placeholder -- _analyze_super_method_call
515
+ # re-resolves it per method name. Single-base path uses it directly.
516
+ return SuperType(record_info.parents[0], ctx.record_ctx.record.name)
517
+
518
+ def _resolve_super_parent_type(self, expr: TpyMethodCall, super_type: SuperType) -> TpyType:
519
+ """Pick which ancestor super().<method>() dispatches to.
520
+
521
+ Single-base uses super_type.parent_type directly. Multi-base (D22 v2.3)
522
+ walks the child's C3 MRO and returns the first ancestor whose own method
523
+ table defines `method` (matching Python's __dict__ walk -- inherited
524
+ methods don't count). super().__del__() is rejected; C++ invokes each
525
+ base's destructor automatically. Single-hop only; see LANGUAGE_FEATURES
526
+ "Limitation: single-hop super()" for why TPy can't replicate Python's
527
+ full cooperative chain under static dispatch.
528
+ """
529
+ child_rec = self.ctx.registry.get_record(super_type.child_record_name)
530
+ if child_rec is None or len(child_rec.parents) < 2:
531
+ return super_type.parent_type
532
+
533
+ if expr.method == "__del__":
534
+ raise self.ctx.error(
535
+ f"super().__del__() is not allowed in multi-base class "
536
+ f"'{super_type.child_record_name}'. C++ invokes each base's destructor "
537
+ f"automatically; remove this call.",
538
+ expr
539
+ )
540
+
541
+ for anc_type in child_rec.mro_ancestors:
542
+ anc_info = self.ctx.registry.get_record_for_type(anc_type)
543
+ if anc_info is None:
544
+ continue
545
+ if anc_info.get_method_overloads(expr.method):
546
+ return anc_type
547
+
548
+ raise self.ctx.error(
549
+ f"No base of '{super_type.child_record_name}' defines method "
550
+ f"'{expr.method}'",
551
+ expr
552
+ )
553
+
554
+ def _try_resolve_method(self, expr: TpyMethodCall, obj_type: TpyType,
555
+ is_readonly_receiver: bool = False,
556
+ is_consuming_receiver: bool = False) -> TpyType | None:
557
+ """Try to resolve method on obj_type. Returns return type or None."""
558
+ result = self._analyze_instance_method(expr, obj_type, is_readonly_receiver, is_consuming_receiver)
559
+ if result is not None:
560
+ return result
561
+ result = self._analyze_protocol_or_bound_method(expr, obj_type)
562
+ if result is not None:
563
+ return result
564
+ # Callable-typed field invocation: obj.field(args) where field is Callable
565
+ result = self._try_callable_field_call(expr, obj_type)
566
+ if result is not None:
567
+ return result
568
+ return None
569
+
570
+ def _try_callable_field_call(self, expr: TpyMethodCall, obj_type: TpyType) -> TpyType | None:
571
+ """Check if expr.method is a Callable-typed field and analyze the call."""
572
+ if not isinstance(obj_type, NominalType):
573
+ return None
574
+ rec = self.ctx.registry.get_record(obj_type.name)
575
+ if rec is None:
576
+ return None
577
+ callable_type = None
578
+ is_optional_field = False
579
+ for fld in rec.fields:
580
+ if fld.name == expr.method:
581
+ fld_type = fld.type
582
+ if isinstance(fld_type, CallableType):
583
+ callable_type = fld_type
584
+ elif isinstance(fld_type, OptionalType) and isinstance(fld_type.inner, CallableType):
585
+ callable_type = fld_type.inner
586
+ is_optional_field = True
587
+ break
588
+ if callable_type is None:
589
+ return None
590
+ # Warn if calling Optional[Callable] field without narrowing
591
+ if is_optional_field:
592
+ narrowing_key = _expr_to_narrowing_key(expr.obj)
593
+ if narrowing_key is not None:
594
+ narrowing_key = f"{narrowing_key}.{expr.method}"
595
+ if narrowing_key is None or narrowing_key not in self.ctx.func.narrowed_types:
596
+ self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
597
+ expr.needs_optional_runtime_check = True
598
+ if expr.kwargs:
599
+ raise self.ctx.error(
600
+ "Keyword arguments are not supported for Callable-typed fields "
601
+ "(Callable types have no parameter names)", expr)
602
+ if len(expr.args) != len(callable_type.param_types):
603
+ raise self.ctx.error(
604
+ f"Callable field '{expr.method}' expects {len(callable_type.param_types)} argument(s), "
605
+ f"got {len(expr.args)}",
606
+ expr
607
+ )
608
+ for i, (arg, expected_type) in enumerate(zip(expr.args, callable_type.param_types)):
609
+ arg_type = self.expr.analyze_expr_with_hint(arg, expected_type)
610
+ if arg_type != expected_type:
611
+ try:
612
+ self.compat.check_type_compatible(
613
+ arg_type, expected_type,
614
+ f"argument {i + 1}", loc=expr.loc, source_expr=arg)
615
+ except SemanticError:
616
+ raise self.ctx.error(
617
+ f"Argument {i + 1}: expected '{expected_type}', got '{arg_type}'",
618
+ expr
619
+ )
620
+ expr.resolved_function_info = FunctionInfo(
621
+ name=expr.method,
622
+ params=[ParamInfo(f"__a{i}", t) for i, t in enumerate(callable_type.param_types)],
623
+ return_type=callable_type.return_type,
624
+ is_readonly=True,
625
+ )
626
+ expr.is_callable_field = True
627
+ return callable_type.return_type
628
+
629
+ def analyze_method_call(self, expr: TpyMethodCall) -> TpyType:
630
+ """Analyze a method call."""
631
+ if isinstance(expr.obj, TpyName):
632
+ # ClassName.staticmethod() pattern
633
+ result = self._analyze_static_method_call(expr)
634
+ if result is not None:
635
+ return result
636
+
637
+ # Nested type constructor: Container.Inner(...) or Container.Kind(value)
638
+ if self.ctx.func.current_ns:
639
+ binding = self.ctx.func.current_ns.lookup(expr.obj.name)
640
+ if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
641
+ nested_result = self._analyze_nested_type_call(expr)
642
+ if nested_result is not None:
643
+ return nested_result
644
+
645
+ # Reject method calls on enum types (enums have no class methods)
646
+ if self.ctx.func.current_ns:
647
+ binding = self.ctx.func.current_ns.lookup(expr.obj.name)
648
+ if binding and binding.kind == BindingKind.ENUM:
649
+ raise self.ctx.error(
650
+ f"Enum type '{expr.obj.name}' has no method '{expr.method}'",
651
+ expr,
652
+ )
653
+
654
+ # module.function() pattern (import X -> X.func())
655
+ result = self._analyze_module_method_call(expr)
656
+ if result is not None:
657
+ return result
658
+
659
+ # Nested type constructor via chained access: Outer.Mid.Deep(...)
660
+ if isinstance(expr.obj, TpyFieldAccess):
661
+ parent_dotted = _resolve_dotted_record_chain(expr.obj, self.ctx)
662
+ if parent_dotted is not None:
663
+ dotted = f"{parent_dotted}.{expr.method}"
664
+ nested_record = self.ctx.registry.get_record(dotted)
665
+ if nested_record is not None:
666
+ fake_call = TpyCall(
667
+ func=TpyName(name=dotted, loc=expr.loc),
668
+ args=expr.args,
669
+ kwargs=expr.kwargs,
670
+ type_args=expr.type_args,
671
+ type_args_parse_error=expr.type_args_parse_error,
672
+ loc=expr.loc,
673
+ )
674
+ expr.is_nested_constructor = True
675
+ expr.nested_type_name = dotted
676
+ result = self.calls._analyze_record_constructor(fake_call, nested_record)
677
+ self.ctx.set_expr_type(expr, result)
678
+ return result
679
+
680
+ # Module-qualified static method call: m.Foo.method() or pkg.sub.Foo.method().
681
+ # Receiver shape is TpyFieldAccess(<module-chain>, ClassName); none of the
682
+ # other TpyFieldAccess branches recognise it because the inner is a module
683
+ # binding (not a record chain) and the leaf is a class (not a function).
684
+ # `user_module_call` is only set for the static-method dispatch -- the
685
+ # unbound-self path (BaseN.method(self, ...)) uses `this->Parent::method`
686
+ # codegen that doesn't go through the module-qualified namespace.
687
+ if isinstance(expr.obj, TpyFieldAccess):
688
+ resolved = self._try_resolve_module_qualified_class(expr.obj)
689
+ if resolved is not None:
690
+ module_name, _class_short, record_info = resolved
691
+ result = self._dispatch_static_method_call(expr, record_info)
692
+ if result is not None:
693
+ if expr.is_static_call:
694
+ expr.user_module_call = module_name
695
+ return result
696
+
697
+ # Dotted module access: X.Y.func(), X.Y.Z.func(), etc.
698
+ if isinstance(expr.obj, TpyFieldAccess):
699
+ dotted_name = self._try_resolve_dotted_module(expr.obj)
700
+ if dotted_name:
701
+ flat_obj = TpyName(name=dotted_name, loc=expr.obj.loc)
702
+ flat_expr = TpyMethodCall(
703
+ obj=flat_obj, method=expr.method, args=expr.args,
704
+ kwargs=expr.kwargs,
705
+ type_args=expr.type_args,
706
+ type_args_parse_error=expr.type_args_parse_error,
707
+ loc=expr.loc,
708
+ )
709
+ result = self._analyze_module_method_call(flat_expr, module_name=dotted_name)
710
+ if result is not None:
711
+ expr.args = flat_expr.args
712
+ expr.kwargs = flat_expr.kwargs
713
+ expr.builtin_module_call = flat_expr.builtin_module_call
714
+ expr.user_module_call = flat_expr.user_module_call
715
+ expr.resolved_function_info = flat_expr.resolved_function_info
716
+ expr.inferred_type_args = flat_expr.inferred_type_args
717
+ expr.representational_subst_params = flat_expr.representational_subst_params
718
+ return result
719
+
720
+ obj_type = self.expr.analyze_expr(expr.obj)
721
+
722
+ # super().method() calls: the receiver resolves to SuperType via the
723
+ # builtins.super qname dispatch in calls.py (supersedes the old bare-string
724
+ # intercept so user `def super()` shadows normally).
725
+ if isinstance(obj_type, SuperType):
726
+ return self._analyze_super_method_call(expr, obj_type)
727
+
728
+ # Pending generic instance: accumulate constraints from method calls
729
+ if isinstance(obj_type, PendingGenericInstanceType):
730
+ return self._analyze_pending_generic_method_call(expr, obj_type)
731
+
732
+ # Unwrap RefType, ReadonlyType, remembering the flag for enforcement.
733
+ # Also check declared scope type: isinstance narrowing may strip ReadonlyType
734
+ # from the expr type while the scope binding preserves it.
735
+ obj_type = unwrap_ref_type(obj_type)
736
+ is_readonly_receiver = isinstance(obj_type, ReadonlyType)
737
+ if not is_readonly_receiver and isinstance(expr.obj, TpyName):
738
+ is_readonly_receiver = self.ctx.is_readonly_name(expr.obj.name)
739
+ if isinstance(obj_type, ReadonlyType):
740
+ obj_type = obj_type.wrapped
741
+
742
+ # General method calls always prefer borrowing overloads.
743
+ # Consuming __iter__ overloads are selected at call-site by
744
+ # _gen_consuming_iter in codegen_cpp/expressions.py.
745
+ is_consuming_receiver = False
746
+
747
+ if isinstance(obj_type, OwnType):
748
+ obj_type = obj_type.wrapped
749
+ elif isinstance(obj_type, OptionalType):
750
+ if obj_type.inner.is_value_type():
751
+ raise self.ctx.error(f"Cannot call method '{expr.method}' on type {obj_type}", expr)
752
+ self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
753
+ expr.needs_optional_runtime_check = True
754
+ obj_type = obj_type.inner
755
+
756
+ # List mutation tracking (before deref chain -- applies to direct list types only)
757
+ if isinstance(obj_type, PendingListType) or is_list(obj_type):
758
+ if not self._is_readonly_method(obj_type, expr.method):
759
+ self.deduction.mark_list_mutated(expr.obj)
760
+
761
+ # Pending container element type inference from method args.
762
+ # For any method on a pending container, check if params involve the
763
+ # container's type parameters. If so, pre-analyze the args and infer
764
+ # the element type (generalizes append/insert/add/etc.).
765
+ if isinstance(obj_type, PendingListType) and expr.args:
766
+ obj_type = self._infer_pending_container_element(
767
+ expr, obj_type, "builtins.list", obj_type.literal_id,
768
+ self.deduction.infer_empty_list_element_type,
769
+ lambda elem, lid: PendingListType(elem, obj_type.size, lid),
770
+ self.ctx.list_literals,
771
+ ) or obj_type
772
+ elif isinstance(obj_type, PendingSetType) and expr.args:
773
+ obj_type = self._infer_pending_container_element(
774
+ expr, obj_type, "builtins.set", obj_type.literal_id,
775
+ self.deduction.infer_set_element_type,
776
+ lambda elem, lid: PendingSetType(elem, lid),
777
+ self.ctx.set_literals,
778
+ ) or obj_type
779
+
780
+ # Borrow conflict: structural mutation on a container with element-level borrows.
781
+ # Resolves aliases so that alias.append() warns when items has element borrows.
782
+ # Also handles field-path receivers (self.items.append()) via _storage_key.
783
+ bt = self.ctx.func.borrow_tracker
784
+ if isinstance(expr.obj, TpyName):
785
+ storage = bt.effective_storage(expr.obj.name)
786
+ else:
787
+ storage = _storage_key(expr.obj)
788
+ if storage is not None:
789
+ if bt.has_element_borrow(storage):
790
+ is_mutation = self._is_invalidating_method(obj_type, expr.method)
791
+ if is_mutation:
792
+ if bt.has_iter_borrow(storage):
793
+ msg = (f"Mutation of '{storage}' while iterating over it"
794
+ f" ('{expr.method}' invalidates the iterator)")
795
+ else:
796
+ msg = (f"Mutation of '{storage}' while borrowed"
797
+ f" ('{expr.method}' may invalidate references)")
798
+ self.ctx.warning(msg, expr)
799
+
800
+ # Deref chain -- resolves through Ptr (mutable and readonly) and any Deref[T] type
801
+ original_type = obj_type
802
+ current_type = obj_type
803
+ deref_depth = 0
804
+ while deref_depth <= 8:
805
+ result = self._try_resolve_method(expr, current_type, is_readonly_receiver, is_consuming_receiver)
806
+ if result is not None:
807
+ expr.deref_depth = deref_depth
808
+ if deref_depth > 0 and isinstance(original_type, PtrType):
809
+ obj_key = _expr_to_narrowing_key(expr.obj)
810
+ if obj_key is not None:
811
+ if obj_key in self.ctx.func.non_null_ptr_vars:
812
+ expr.ptr_non_null = True
813
+ if expr.loc:
814
+ self.ctx.ptr_deref_facts[
815
+ (expr.loc.line, obj_key)
816
+ ] = expr.ptr_non_null
817
+ # Post-access narrowing: queued for flush at statement
818
+ # boundary (see pending_non_null_ptr_vars docstring).
819
+ self.ctx.func.pending_non_null_ptr_vars.add(obj_key)
820
+ # Enforce readonly: cannot call non-readonly method on readonly receiver
821
+ if is_readonly_receiver:
822
+ info = expr.resolved_function_info
823
+ if info is not None and not info.is_readonly:
824
+ raise self.ctx.error(
825
+ f"Cannot call non-readonly method '{expr.method}' on readonly reference",
826
+ expr)
827
+ # Enforce consuming methods: receiver must be a local variable
828
+ info = expr.resolved_function_info
829
+ if info is not None and info.is_consuming:
830
+ self._validate_consuming_call(expr)
831
+ # Track non-readonly method calls on for-each loop variables
832
+ # and string view sources (receiver mutation invalidates views)
833
+ info = expr.resolved_function_info
834
+ if info is not None and not info.is_readonly:
835
+ obj_root = _root_name_of_expr(expr.obj)
836
+ if obj_root is not None:
837
+ self.ctx.mark_loop_var_mutated(obj_root)
838
+ is_direct_self_call = (
839
+ isinstance(expr.obj, TpyName) and expr.obj.name == "self"
840
+ )
841
+ if is_direct_self_call:
842
+ # self.method() -- entirely deferred to call edges
843
+ pass
844
+ else:
845
+ # For self.field.method() and loop_var.method() (where the
846
+ # loop var iterates over a self field), defer self-mutation
847
+ # to Phase 2 via call edges (receiver_is_self=True, recorded
848
+ # by _record_mutation_call_edges). Phase 2 only sets
849
+ # self_mutated when the callee actually mutates its self,
850
+ # enabling readonly inference for methods like __json_encode__
851
+ # that call non-mutating methods on fields.
852
+ self_deferred = _is_self_call_deferred(
853
+ expr.obj, obj_root, self.ctx.func.loop_var_iterable,
854
+ self.ctx.func.borrow_tracker)
855
+ if not self_deferred:
856
+ self.ctx.mark_param_mutated(obj_root)
857
+ # Structural mutation tracked directly (Phase 2 doesn't
858
+ # propagate structural self-mutation through call edges).
859
+ if self._is_invalidating_method(obj_type, expr.method):
860
+ self.ctx.mark_param_structurally_mutated(obj_root)
861
+ storage = self.ctx.func.borrow_tracker.effective_storage(obj_root)
862
+ self.ctx.mark_all_view_borrowers_mutated(storage)
863
+ else:
864
+ # Check for chained method calls rooted at self:
865
+ # self.get_span().sort() -- sort() is non-readonly and
866
+ # the span is a mutable view of self's storage.
867
+ # Treat as self-mutation conservatively. A chain rooted
868
+ # at a local that borrow-traces to self.<field> (e.g.
869
+ # `frame = self.frame; frame.get().mutating_method()`)
870
+ # is also a self-mutation since the receiver aliases
871
+ # self-owned storage.
872
+ chain = expr.obj
873
+ while isinstance(chain, TpyMethodCall):
874
+ chain = chain.obj
875
+ chain_root = _root_name_of_expr(chain)
876
+ if chain_root is not None and _local_traces_to_self(
877
+ self.ctx.func.borrow_tracker, chain_root):
878
+ self.ctx.mark_param_mutated("self")
879
+ return result
880
+
881
+ deref_target = self.expr.get_deref_target_type(
882
+ current_type, is_readonly=is_readonly_receiver)
883
+ if deref_target is None:
884
+ break
885
+ # __deref__() may return readonly[T]; unwrap and propagate
886
+ # readonly so method calls enforce const semantics.
887
+ if isinstance(deref_target, ReadonlyType):
888
+ is_readonly_receiver = True
889
+ deref_target = deref_target.wrapped
890
+ # Deref-view narrowing: inside `if isinstance(rc, Dog):` the peeled
891
+ # payload is narrowed to the subclass so `bark` (a Dog method, not
892
+ # on the Pet protocol) resolves. Tag the node for the codegen cast.
893
+ nsub = deref_view_narrowed(self.ctx, expr.obj, deref_target)
894
+ if nsub is not None:
895
+ expr.deref_narrowed_to = nsub
896
+ current_type = nsub
897
+ else:
898
+ current_type = deref_target
899
+ deref_depth += 1
900
+
901
+ raise self.ctx.error(f"Cannot call method '{expr.method}' on type {original_type}", expr)
902
+
903
+ def _validate_consuming_call(self, expr: TpyMethodCall) -> None:
904
+ """Validate a consuming method call (self: Own[Self]).
905
+
906
+ The receiver must be a local variable (not a field or other expression).
907
+ Temporaries are also allowed (e.g. Box(value).take()).
908
+ After the call, the variable is marked as consumed.
909
+ """
910
+ if expr.deref_depth > 0:
911
+ raise self.ctx.error(
912
+ f"Cannot call consuming method '{expr.method}' through a Deref chain; "
913
+ f"consuming methods must be called directly on the owning type",
914
+ expr,
915
+ )
916
+ if isinstance(expr.obj, TpyFieldAccess):
917
+ raise self.ctx.error(
918
+ f"Cannot call consuming method '{expr.method}' on a field; "
919
+ f"only local variables and temporaries are allowed",
920
+ expr,
921
+ )
922
+ if isinstance(expr.obj, TpyName):
923
+ name = expr.obj.name
924
+ if name == "self":
925
+ raise self.ctx.error(
926
+ f"Cannot call consuming method '{expr.method}' on 'self'; "
927
+ f"only local variables and temporaries are allowed",
928
+ expr,
929
+ )
930
+ # Reject consuming through pointers -- pointer doesn't own the pointee
931
+ obj_type = self.ctx.func.current_scope.lookup(name)
932
+ if obj_type is not None and (isinstance(obj_type, PtrType)
933
+ or isinstance(unwrap_readonly(obj_type), PtrType)):
934
+ raise self.ctx.error(
935
+ f"Cannot call consuming method '{expr.method}' on pointer '{name}'; "
936
+ f"pointers do not own the pointee",
937
+ expr,
938
+ )
939
+ # Reject consuming outer variables inside a loop -- the variable
940
+ # won't be re-bound on the next iteration, causing use-after-move.
941
+ if self.ctx.func.loop_depth > 0:
942
+ var_depth = self.ctx.func.var_scope_depth.get(name, 0)
943
+ loop_scope_depth = self.ctx.func.current_scope.depth
944
+ if var_depth < loop_scope_depth:
945
+ raise self.ctx.error(
946
+ f"Cannot consume '{name}' inside a loop; "
947
+ f"the variable is not re-bound each iteration",
948
+ expr,
949
+ )
950
+ # Use-after-consume is already caught by _analyze_name before we get here.
951
+ self.ctx.func.consumed_vars.add(name)
952
+
953
+ def _analyze_nested_type_call(self, expr: TpyMethodCall) -> TpyType | None:
954
+ """Check for Outer.Inner(...) nested type constructor call. Returns type or None."""
955
+ assert isinstance(expr.obj, TpyName)
956
+ dotted = f"{expr.obj.name}.{expr.method}"
957
+ # Nested record constructor
958
+ nested_record = self.ctx.registry.get_record(dotted)
959
+ if nested_record is not None:
960
+ # Rewrite as a direct constructor call
961
+ fake_call = TpyCall(
962
+ func=TpyName(name=dotted, loc=expr.loc),
963
+ args=expr.args,
964
+ kwargs=expr.kwargs,
965
+ type_args=expr.type_args,
966
+ type_args_parse_error=expr.type_args_parse_error,
967
+ loc=expr.loc,
968
+ )
969
+ # Mark the original expr so codegen knows it's a constructor
970
+ expr.is_nested_constructor = True
971
+ expr.nested_type_name = dotted
972
+ result = self.calls._analyze_record_constructor(fake_call, nested_record)
973
+ # Copy type info back
974
+ self.ctx.set_expr_type(expr, result)
975
+ return result
976
+ # Nested enum constructor (from_value)
977
+ nested_enum = self.ctx.registry.get_enum(dotted)
978
+ if nested_enum is not None:
979
+ # Container.Kind(1) -> Container::Kind from_value
980
+ if len(expr.args) != 1 or expr.kwargs:
981
+ raise self.ctx.error(
982
+ f"Nested enum '{dotted}' constructor takes exactly 1 positional argument",
983
+ expr)
984
+ arg_type = self.expr.analyze_expr(expr.args[0])
985
+ if not is_any_int_type(arg_type):
986
+ raise self.ctx.error(
987
+ f"Cannot construct '{dotted}' from '{arg_type}', "
988
+ f"expected an integer type",
989
+ expr)
990
+ expr.is_nested_enum_constructor = True
991
+ expr.nested_type_name = dotted
992
+ return nested_enum
993
+ return None
994
+
995
+ def _analyze_static_method_call(self, expr: TpyMethodCall) -> TpyType | None:
996
+ """Check for ClassName.staticmethod() or BaseN.method(self, ...) pattern.
997
+
998
+ Returns the call's type, or None if ClassName is not a record binding
999
+ (fall-through to other method-call dispatch).
1000
+
1001
+ Routes (checked in order):
1002
+ 1. ClassName.staticmethod(args) -- static-method call.
1003
+ 2. BaseN.method(self, args) where BaseN is an ancestor of the current
1004
+ record: rebinds to instance-method machinery, dispatches statically
1005
+ to BaseN's definition. First arg must literally be 'self'.
1006
+ """
1007
+ assert isinstance(expr.obj, TpyName)
1008
+ if self.ctx.func.current_ns is None:
1009
+ return None
1010
+
1011
+ record_info = None
1012
+ binding = self.ctx.func.current_ns.lookup(expr.obj.name)
1013
+ if binding and binding.kind == BindingKind.RECORD:
1014
+ record_info = self.ctx.registry.get_record(expr.obj.name)
1015
+ elif binding and binding.kind == BindingKind.IMPORTED_NAME:
1016
+ import_info = self.ctx.imported_names.get(expr.obj.name)
1017
+ if import_info:
1018
+ record_info = self.ctx.registry.find_record_by_qname(
1019
+ f"{import_info[0]}.{import_info[1]}")
1020
+
1021
+ if record_info is None:
1022
+ return None
1023
+ return self._dispatch_static_method_call(expr, record_info)
1024
+
1025
+ def _dispatch_static_method_call(
1026
+ self, expr: TpyMethodCall, record_info: RecordInfo,
1027
+ ) -> TpyType | None:
1028
+ """Dispatch a staticmethod / unbound-self call on a pre-resolved record.
1029
+
1030
+ Walks MRO, distinguishes static from unbound-self, handles generic
1031
+ class / method type args. Shared by bare-name (`Foo.method()`) and
1032
+ module-qualified (`m.Foo.method()`) receivers.
1033
+ """
1034
+ overloads = self.ctx.registry.get_method_overloads_with_parents(
1035
+ record_info, expr.method)
1036
+ if not overloads:
1037
+ return None
1038
+ if not overloads[0].is_staticmethod:
1039
+ return self._analyze_unbound_self_method_call(expr, record_info, overloads)
1040
+
1041
+ if record_info.is_generic():
1042
+ return self._analyze_generic_static_method_call(expr, record_info, overloads)
1043
+
1044
+ method = overloads[0]
1045
+ if method.type_params:
1046
+ # Non-generic class with method-level type params: type_args are for the method
1047
+ return self._analyze_generic_static_method_call(expr, record_info, overloads)
1048
+
1049
+ if expr.type_args or expr.type_args_parse_error:
1050
+ raise self.ctx.error(
1051
+ f"'{record_info.name}' is not generic and does not accept type arguments",
1052
+ expr,
1053
+ )
1054
+
1055
+ return_type = self._resolve_and_check_args(expr, overloads, {})
1056
+ expr.is_static_call = True
1057
+ return return_type
1058
+
1059
+ def _try_resolve_module_qualified_class(
1060
+ self, obj: TpyFieldAccess,
1061
+ ) -> tuple[str, str, RecordInfo] | None:
1062
+ """Resolve `obj` as `<module-chain>.ClassName` to (module_name, class_short, record_info).
1063
+
1064
+ Handles both single-segment module references (`TpyName(m).Class`) and
1065
+ dotted-segment ones (`TpyFieldAccess(pkg.sub).Class`). Returns None when
1066
+ the inner receiver isn't a module binding or the named class isn't
1067
+ registered in that module.
1068
+ """
1069
+ class_short = obj.field
1070
+ if isinstance(obj.obj, TpyName):
1071
+ module_name = self._resolve_module_name(obj.obj.name)
1072
+ elif isinstance(obj.obj, TpyFieldAccess):
1073
+ module_name = self._try_resolve_dotted_module(obj.obj)
1074
+ else:
1075
+ return None
1076
+ if module_name is None:
1077
+ return None
1078
+ record_info = self.ctx.registry.find_record_by_qname(
1079
+ f"{module_name}.{class_short}")
1080
+ if record_info is None:
1081
+ return None
1082
+ return (module_name, class_short, record_info)
1083
+
1084
+ def _analyze_unbound_self_method_call(
1085
+ self, expr: TpyMethodCall, record_info, overloads: list[FunctionInfo],
1086
+ ) -> TpyType:
1087
+ """Handle `BaseN.method(self, ...)` calls on an ancestor class.
1088
+
1089
+ `record_info` is the class named on the left of the dot; `overloads`
1090
+ are the MRO-walked non-static overloads of `expr.method` (caller has
1091
+ already dispatched on staticmethod).
1092
+
1093
+ Rules:
1094
+ - The enclosing context must be an instance method (self in scope).
1095
+ - ClassName must be a strict ancestor of the current record.
1096
+ - First arg must syntactically be the name `self` -- arbitrary unbound
1097
+ calls (e.g. `Named.describe(other_instance)`) are rejected.
1098
+ """
1099
+ parent_name = record_info.name
1100
+ method_name = expr.method
1101
+
1102
+ # Must be inside an instance method
1103
+ current_rec_parse = self.ctx.record_ctx.record
1104
+ current_fn = self.ctx.func.current_function
1105
+ if (current_rec_parse is None
1106
+ or current_fn is None
1107
+ or not isinstance(current_fn, TpyFunction)
1108
+ or not current_fn.is_method
1109
+ or current_fn.is_staticmethod):
1110
+ raise self.ctx.error(
1111
+ f"Method '{parent_name}.{method_name}' requires an instance "
1112
+ f"(not a static method)",
1113
+ expr,
1114
+ )
1115
+ current_rec = self.ctx.registry.get_record(current_rec_parse.name)
1116
+ assert current_rec is not None, (
1117
+ f"registry missing RecordInfo for '{current_rec_parse.name}' "
1118
+ f"while analyzing its methods"
1119
+ )
1120
+
1121
+ # __del__ compiles to a C++ destructor (not a callable member); __init__
1122
+ # compiles to a constructor and is only reachable as a base initializer
1123
+ # from within the child's __init__ body.
1124
+ if method_name == "__del__":
1125
+ raise self.ctx.error(
1126
+ f"'{parent_name}.__del__(self)' is not callable via the unbound-self "
1127
+ f"form. C++ invokes each base destructor automatically.",
1128
+ expr,
1129
+ )
1130
+ if method_name == "__init__" and current_fn.name != "__init__":
1131
+ raise self.ctx.error(
1132
+ f"'{parent_name}.__init__(self, ...)' can only be called inside "
1133
+ f"'__init__'",
1134
+ expr,
1135
+ )
1136
+
1137
+ # Ancestor check (strict: excludes current record itself)
1138
+ if not self.ctx.registry.is_subclass_of_record(current_rec, record_info):
1139
+ raise self.ctx.error(
1140
+ f"'{parent_name}' is not an ancestor of '{current_rec.name}'; "
1141
+ f"cannot call '{parent_name}.{method_name}(self, ...)' here",
1142
+ expr,
1143
+ )
1144
+
1145
+ # First arg must be the literal name 'self'.
1146
+ if not expr.args or not (isinstance(expr.args[0], TpyName)
1147
+ and expr.args[0].name == "self"):
1148
+ raise self.ctx.error(
1149
+ f"'{parent_name}.{method_name}(...)' must pass 'self' as the first "
1150
+ f"argument (e.g. '{parent_name}.{method_name}(self, ...)')",
1151
+ expr,
1152
+ )
1153
+
1154
+ # Caller passed MRO-walked overloads and dispatched on staticmethod, so
1155
+ # `overloads` is the authoritative list of non-static candidates here.
1156
+ assert overloads and not overloads[0].is_staticmethod
1157
+
1158
+ parent_type, type_subst = self.protocols.resolve_ancestor_instantiation(
1159
+ current_rec, record_info)
1160
+
1161
+ # Rebind to instance-method machinery via a temp TpyMethodCall with
1162
+ # obj=self and self dropped from args. _resolve_and_check_args mutates
1163
+ # the temp's args (coercions), so copy them back after. self itself
1164
+ # is then dropped from expr.args so codegen sees the same shape as
1165
+ # super().method(...) -- base-qualified call with method args only.
1166
+ self_arg = expr.args[0]
1167
+ # Analyze self so downstream (mutation tracking, type queries) has a type.
1168
+ self.expr.analyze_expr(self_arg)
1169
+ fake_expr = TpyMethodCall(
1170
+ obj=self_arg,
1171
+ method=method_name,
1172
+ args=list(expr.args[1:]),
1173
+ kwargs=expr.kwargs,
1174
+ type_args=expr.type_args,
1175
+ type_args_parse_error=expr.type_args_parse_error,
1176
+ loc=expr.loc,
1177
+ )
1178
+
1179
+ method_info = overloads[0]
1180
+ if method_info.is_generic():
1181
+ if len(overloads) > 1:
1182
+ raise self.ctx.error(
1183
+ f"Overloaded generic methods are not supported for '{method_name}'",
1184
+ expr)
1185
+ return_type = self._analyze_generic_method_call(
1186
+ fake_expr, method_info, record_info, type_subst)
1187
+ else:
1188
+ return_type = self._resolve_and_check_args(
1189
+ fake_expr, overloads, type_subst)
1190
+
1191
+ # Readonly self: a @readonly context calling a non-readonly ancestor
1192
+ # method would mutate self through a const receiver.
1193
+ is_readonly_context = (
1194
+ isinstance(self.ctx.func.current_function, TpyFunction)
1195
+ and self.ctx.func.current_function.is_readonly
1196
+ )
1197
+ resolved_info = fake_expr.resolved_function_info
1198
+ if is_readonly_context and resolved_info is not None and not resolved_info.is_readonly:
1199
+ raise self.ctx.error(
1200
+ f"Cannot call non-readonly method '{method_name}' on readonly reference",
1201
+ expr)
1202
+
1203
+ # Drop self from the outer expr so codegen emits Parent::method(args)
1204
+ # with args matching resolved_function_info.params shape.
1205
+ expr.args = fake_expr.args
1206
+ expr.kwargs = fake_expr.kwargs
1207
+ expr.resolved_function_info = resolved_info
1208
+ expr.inferred_type_args = fake_expr.inferred_type_args
1209
+ expr.representational_subst_params = fake_expr.representational_subst_params
1210
+ expr.unbound_self_parent_type = parent_type
1211
+ return return_type
1212
+
1213
+ def _analyze_generic_static_method_call(
1214
+ self, expr: TpyMethodCall, record_info, overloads: list[FunctionInfo],
1215
+ ) -> TpyType:
1216
+ """Resolve a static method call on a generic record or a static method
1217
+ with its own type parameters.
1218
+
1219
+ Creates a virtual FunctionInfo with class + method type params merged,
1220
+ then delegates to _analyze_user_function_call (same path as free generic calls).
1221
+ """
1222
+ # NOTE: picks first overload. If overloaded static methods on generic
1223
+ # classes are added, this needs overload resolution per-candidate with
1224
+ # inference (similar to _analyze_builtin_function_overloads).
1225
+ method = overloads[0]
1226
+ class_type_params = set(record_info.type_params) if record_info.type_params else set()
1227
+ new_method_params = [tp for tp in (method.type_params or []) if tp not in class_type_params]
1228
+
1229
+ # Merge class type params + method's own type params into a single virtual FunctionInfo.
1230
+ # The free function path handles inference, explicit args, and bound validation.
1231
+ all_type_params = list(record_info.type_params) + new_method_params
1232
+ all_bounds = dict(record_info.type_param_bounds)
1233
+ all_bounds.update({k: v for k, v in method.type_param_bounds.items()
1234
+ if k in set(new_method_params)})
1235
+
1236
+ # Validate type arg count: must match either class params (method params inferred)
1237
+ # or all params (class + method)
1238
+ if expr.type_args:
1239
+ n_class = len(record_info.type_params)
1240
+ n_total = len(all_type_params)
1241
+ n_given = len(expr.type_args)
1242
+ if n_given != n_class and n_given != n_total:
1243
+ if new_method_params:
1244
+ raise self.ctx.error(
1245
+ f"'{record_info.name}.{method.name}' expects {n_class} class type arguments "
1246
+ f"or {n_total} total (class + method), got {n_given}",
1247
+ expr)
1248
+ raise self.ctx.error(
1249
+ f"'{record_info.name}' expects {n_class} type arguments, got {n_given}",
1250
+ expr)
1251
+
1252
+ virtual_func = FunctionInfo(
1253
+ name=method.name, params=method.params, return_type=method.return_type,
1254
+ is_staticmethod=method.is_staticmethod,
1255
+ type_params=all_type_params,
1256
+ type_param_bounds=all_bounds,
1257
+ cpp_template=method.cpp_template,
1258
+ native_name=method.native_name,
1259
+ canonical_fi=method.root,
1260
+ )
1261
+ temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args,
1262
+ kwargs=expr.kwargs,
1263
+ type_args=expr.type_args,
1264
+ type_args_parse_error=expr.type_args_parse_error,
1265
+ loc=expr.loc)
1266
+ result = self.calls._analyze_user_function_call(temp_call, [virtual_func])
1267
+ expr.args = temp_call.args
1268
+ expr.kwargs = temp_call.kwargs
1269
+ expr.resolved_function_info = temp_call.resolved_function_info
1270
+ expr.inferred_type_args = temp_call.inferred_type_args
1271
+ expr.representational_subst_params = temp_call.representational_subst_params
1272
+ expr.is_static_call = True
1273
+ return result
1274
+
1275
+ def _analyze_module_method_call(
1276
+ self, expr: TpyMethodCall, module_name: str | None = None,
1277
+ ) -> TpyType | None:
1278
+ """Check for module.function() pattern. Returns type or None if not a module call.
1279
+
1280
+ If module_name is provided, skips namespace resolution (used for dotted
1281
+ module access like tpy.unsafe.func() where the module is already known).
1282
+ """
1283
+ assert isinstance(expr.obj, TpyName)
1284
+
1285
+ if module_name is None:
1286
+ module_name = self._resolve_module_name(expr.obj.name)
1287
+ if module_name is None:
1288
+ return None
1289
+
1290
+ module_info = self.ctx.registry.get_module(module_name)
1291
+ if module_info and module_info.functions and expr.method in module_info.functions:
1292
+ overloads = module_info.functions[expr.method]
1293
+ # Route through builtin path if the function is from a builtin module
1294
+ # or has a cpp_template (inline expansion, no C++ function body).
1295
+ is_builtin_func = (module_info.is_builtin
1296
+ or overloads[0].is_builtin_function
1297
+ or overloads[0].cpp_template is not None)
1298
+ if is_builtin_func:
1299
+ expr.builtin_module_call = module_name
1300
+ temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args,
1301
+ kwargs=expr.kwargs,
1302
+ type_args=expr.type_args,
1303
+ type_args_parse_error=expr.type_args_parse_error,
1304
+ loc=expr.loc)
1305
+ from .. import qnames
1306
+ _qname = f"{module_name}.{expr.method}"
1307
+ if _qname in (qnames.ASYNCIO_RUN, qnames.ASYNCIO_CREATE_TASK):
1308
+ self.calls._require_async_def_call_arg(temp_call, _qname)
1309
+ if overloads[0].special_handling:
1310
+ result = self.calls._analyze_special_builtin(temp_call, overloads)
1311
+ else:
1312
+ result = self.calls._analyze_builtin_function_overloads(temp_call, overloads)
1313
+ expr.args = temp_call.args
1314
+ expr.kwargs = temp_call.kwargs
1315
+ expr.resolved_function_info = temp_call.resolved_function_info
1316
+ expr.inferred_type_args = temp_call.inferred_type_args
1317
+ expr.representational_subst_params = temp_call.representational_subst_params
1318
+ return result
1319
+ else:
1320
+ expr.user_module_call = module_name
1321
+ temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args,
1322
+ kwargs=expr.kwargs,
1323
+ type_args=expr.type_args,
1324
+ type_args_parse_error=expr.type_args_parse_error,
1325
+ loc=expr.loc)
1326
+ result = self.calls._analyze_user_function_call(temp_call, overloads)
1327
+ expr.args = temp_call.args
1328
+ expr.kwargs = temp_call.kwargs
1329
+ expr.resolved_function_info = temp_call.resolved_function_info
1330
+ expr.inferred_type_args = temp_call.inferred_type_args
1331
+ expr.representational_subst_params = temp_call.representational_subst_params
1332
+ return result
1333
+
1334
+ qname = f"{module_name}.{expr.method}"
1335
+ if record_info := self.ctx.registry.get_builtin_record(qname):
1336
+ if record_info.get_method_overloads("__init__") and not record_info.type_params:
1337
+ expr.builtin_module_call = module_name
1338
+ temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args, kwargs=expr.kwargs, loc=expr.loc)
1339
+ result = self.calls._analyze_record_constructor(temp_call, record_info)
1340
+ expr.args = temp_call.args
1341
+ expr.kwargs = temp_call.kwargs
1342
+ expr.resolved_function_info = temp_call.resolved_function_info
1343
+ return result
1344
+
1345
+ # User record constructor invoked via qualified access (e.g.
1346
+ # `module.RecordName(...)` after `from pkg import module`).
1347
+ # Generic records left out -- this path doesn't forward type_args,
1348
+ # so let them fall through to the existing generic handling below.
1349
+ if (module_info and module_info.records
1350
+ and expr.method in module_info.records
1351
+ and not module_info.records[expr.method].type_params):
1352
+ record_info = module_info.records[expr.method]
1353
+ expr.user_module_call = module_name
1354
+ temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args, kwargs=expr.kwargs, loc=expr.loc)
1355
+ result = self.calls._analyze_record_constructor(temp_call, record_info)
1356
+ expr.args = temp_call.args
1357
+ expr.kwargs = temp_call.kwargs
1358
+ expr.resolved_function_info = temp_call.resolved_function_info
1359
+ return result
1360
+
1361
+ # Check for call-site macro (e.g. dataclasses.asdict(...)).
1362
+ # Walks the re-export chain so macros re-exported through plain
1363
+ # modules (Phase 6) resolve to the macro registry's canonical
1364
+ # (ultimate_module, name) key.
1365
+ if self.ctx.macro_registry:
1366
+ macro_fn = self.ctx.macro_registry.get_call_macro(module_name, expr.method)
1367
+ ult_mod, ult_name = module_name, expr.method
1368
+ if macro_fn is None:
1369
+ chain = self.calls._resolve_call_macro_chain(module_name, expr.method)
1370
+ if chain is not None:
1371
+ ult_mod, ult_name = chain
1372
+ macro_fn = self.ctx.macro_registry.get_call_macro(ult_mod, ult_name)
1373
+ if macro_fn is not None:
1374
+ return self.calls._expand_call_macro_from_method(
1375
+ expr, macro_fn, ult_mod, ult_name)
1376
+
1377
+ raise self.ctx.error(f"Module '{module_name}' has no function '{expr.method}'", expr)
1378
+
1379
+ def _try_resolve_dotted_module(self, obj: TpyFieldAccess) -> str | None:
1380
+ """Try to resolve nested field access as a dotted module name.
1381
+
1382
+ Walks the TpyFieldAccess chain to collect segments (e.g.,
1383
+ a.b.c -> ["a", "b", "c"]), then checks the registry.
1384
+ Returns None if the base name is shadowed (bound as anything other
1385
+ than MODULE).
1386
+ """
1387
+ segments: list[str] = []
1388
+ current = obj
1389
+ while isinstance(current, TpyFieldAccess):
1390
+ segments.append(current.field)
1391
+ current = current.obj
1392
+ if not isinstance(current, TpyName):
1393
+ return None
1394
+
1395
+ # Only resolve as module if base name is unbound or bound as MODULE
1396
+ if self.ctx.func.current_ns:
1397
+ binding = self.ctx.func.current_ns.lookup(current.name)
1398
+ if binding and binding.kind != BindingKind.MODULE:
1399
+ return None
1400
+
1401
+ segments.append(current.name)
1402
+ segments.reverse()
1403
+ dotted_name = ".".join(segments)
1404
+ if self.ctx.registry.get_module(dotted_name):
1405
+ return dotted_name
1406
+ return None
1407
+
1408
+ def _resolve_module_name(self, name: str) -> str | None:
1409
+ """Resolve a name to a module name if it refers to a module. Returns None otherwise."""
1410
+ if self.ctx.func.current_ns is None:
1411
+ return None
1412
+ binding = self.ctx.func.current_ns.lookup(name)
1413
+ if binding and binding.kind == BindingKind.MODULE:
1414
+ return binding.import_source[0] if binding.import_source else name
1415
+ return None
1416
+
1417
+ # ------------------------------------------------------------------
1418
+ # Pending generic instance method calls (Phase 7a)
1419
+ # ------------------------------------------------------------------
1420
+
1421
+ def _analyze_pending_generic_method_call(
1422
+ self, expr: TpyMethodCall, obj_type: PendingGenericInstanceType,
1423
+ ) -> TpyType:
1424
+ """Handle method call on a variable with unresolved generic type params.
1425
+
1426
+ Accumulates type parameter constraints from method arguments.
1427
+ Eagerly resolves the generic instance once all type params are known.
1428
+ """
1429
+ info = self.ctx.func.pending_generic_instances.get(obj_type.instance_id)
1430
+ if info is None:
1431
+ raise self.ctx.error(
1432
+ f"Internal error: pending generic instance {obj_type.instance_id} not found", expr)
1433
+
1434
+ record = info.record_info
1435
+ overloads = record.get_method_overloads(expr.method)
1436
+ if not overloads:
1437
+ raise self.ctx.error(
1438
+ f"'{record.name}' has no method '{expr.method}'", expr)
1439
+
1440
+ # For MVP: use first overload (user records have single overloads per name)
1441
+ method = overloads[0]
1442
+
1443
+ # Resolve kwargs (also enforces keyword-only constraints when no kwargs)
1444
+ if expr.kwargs or method.has_keyword_only:
1445
+ expr.args = resolve_kwargs(
1446
+ expr.args, expr.kwargs, method.params, expr.method,
1447
+ lambda msg: self.ctx.error(msg, expr),
1448
+ call_loc=expr.loc,
1449
+ )
1450
+ expr.kwargs = {}
1451
+
1452
+ # Check arity
1453
+ if len(expr.args) < method.min_args or len(expr.args) > method.max_args:
1454
+ raise self.ctx.error(
1455
+ arity_error_msg(expr.method, method.min_args, method.max_args, len(expr.args)),
1456
+ expr)
1457
+
1458
+ # Analyze arguments and accumulate constraints. analyze_call_arg so a
1459
+ # `*xs` arg yields the unpacked element type rather than crashing the
1460
+ # structural analyzer; non-variadic reject gate handles validity.
1461
+ arg_types = [self.expr.analyze_call_arg(arg) for arg in expr.args]
1462
+ type_param_names = set(info.type_params)
1463
+ for (pname, ptype), arg_type in zip(method.params, arg_types):
1464
+ if not contains_type_param(ptype, type_param_names):
1465
+ continue
1466
+ # Resolve IntLiteralType before binding
1467
+ resolved_arg = arg_type
1468
+ if isinstance(resolved_arg, IntLiteralType):
1469
+ resolved_arg = self.ctx.default_int_for_literal(resolved_arg)
1470
+ if not self.type_ops.match_type_with_inference(ptype, resolved_arg, info.inferred):
1471
+ # Check if this is a conflict with an existing binding
1472
+ for tp in info.type_params:
1473
+ if tp in info.inferred:
1474
+ existing = info.inferred[tp]
1475
+ # Try matching just this param to see if it conflicts
1476
+ test: dict[str, TpyType] = {}
1477
+ self.type_ops.match_type_with_inference(ptype, resolved_arg, test)
1478
+ if tp in test and test[tp] != existing:
1479
+ raise self.ctx.error(
1480
+ f"Conflicting type inference for '{tp}' in '{record.name}': "
1481
+ f"previously inferred as '{existing}', "
1482
+ f"but '{expr.method}' argument '{pname}' implies '{test[tp]}'",
1483
+ expr,
1484
+ )
1485
+
1486
+ # Resolve IntLiteralType in any newly inferred params
1487
+ for k, v in list(info.inferred.items()):
1488
+ if isinstance(v, IntLiteralType):
1489
+ info.inferred[k] = self.ctx.default_int_for_literal(v)
1490
+
1491
+ # Check if all type params are now resolved
1492
+ all_resolved = all(tp in info.inferred for tp in info.type_params)
1493
+
1494
+ if all_resolved:
1495
+ resolved_type = self._eagerly_resolve_pending_generic(info)
1496
+ # Re-dispatch: analyze the method call on the now-concrete type
1497
+ self.ctx.set_expr_type(expr.obj, resolved_type)
1498
+ if isinstance(expr.obj, TpyName) and self.ctx.func.current_scope:
1499
+ self.ctx.func.current_scope.define(expr.obj.name, resolved_type)
1500
+ result = self._try_resolve_method(expr, resolved_type)
1501
+ if result is None:
1502
+ raise self.ctx.error(
1503
+ f"'{resolved_type}' has no method '{expr.method}'", expr)
1504
+ return result
1505
+
1506
+ # Not fully resolved yet -- check return type
1507
+ return_type = method.return_type
1508
+ if contains_type_param(return_type, type_param_names):
1509
+ # Check if we can substitute what we have so far
1510
+ unresolved_in_return = _unresolved_params_in_type(return_type, info.inferred, type_param_names)
1511
+ if unresolved_in_return:
1512
+ raise self.ctx.error(
1513
+ f"Cannot determine return type of '{expr.method}' on '{record.name}': "
1514
+ f"type parameter{'s' if len(unresolved_in_return) > 1 else ''} "
1515
+ f"{', '.join(unresolved_in_return)} not yet resolved; "
1516
+ f"call a constraining method first or add explicit type arguments",
1517
+ expr,
1518
+ )
1519
+ # All params in return type are resolved, substitute
1520
+ return_type = self.type_ops.substitute_type_params(return_type, info.inferred)
1521
+
1522
+ # Set minimal function info for void methods
1523
+ expr.resolved_function_info = FunctionInfo(
1524
+ name=expr.method,
1525
+ params=method.params,
1526
+ return_type=return_type,
1527
+ # Resumable-factory flags drive call-site capture decisions
1528
+ # (e.g. temporary-receiver lift); a pending-generic generator/
1529
+ # async method must not lose them on this minimal path.
1530
+ is_async=method.is_async,
1531
+ is_generator=method.is_generator,
1532
+ canonical_fi=method.root,
1533
+ )
1534
+ return return_type
1535
+
1536
+ def try_resolve_pending_from_expected_type(
1537
+ self, pending: PendingGenericInstanceType, expected: TpyType,
1538
+ loc: 'SourceLocation | None' = None,
1539
+ ) -> NominalType | None:
1540
+ """Try to resolve a pending generic instance from an expected type.
1541
+
1542
+ Used when a pending-type variable is passed to a typed parameter or
1543
+ returned where the function return type is known. Returns the resolved
1544
+ concrete type, or None if the expected type doesn't match.
1545
+ """
1546
+ info = self.ctx.func.pending_generic_instances.get(pending.instance_id)
1547
+ if info is None:
1548
+ return None
1549
+
1550
+ # Unwrap Own/Optional/Readonly/Ref to find the inner NominalType
1551
+ target = expected
1552
+ if isinstance(target, OwnType):
1553
+ target = target.wrapped
1554
+ if isinstance(target, OptionalType):
1555
+ target = target.inner
1556
+ target = unwrap_readonly(unwrap_ref_type(target))
1557
+
1558
+ if not isinstance(target, NominalType) or target.name != info.record_name:
1559
+ return None
1560
+ if not target.type_args or len(target.type_args) != len(info.type_params):
1561
+ return None
1562
+
1563
+ # Build pattern with TypeParamRefs for unresolved params
1564
+ pattern_args = []
1565
+ for tp in info.type_params:
1566
+ if tp in info.inferred:
1567
+ pattern_args.append(info.inferred[tp])
1568
+ else:
1569
+ pattern_args.append(TypeParamRef(tp))
1570
+ pattern = NominalType(
1571
+ info.record_name, tuple(pattern_args),
1572
+ _module_qname=info.record_info.qualified_name(),
1573
+ )
1574
+
1575
+ # Match to extract constraints
1576
+ if not self.type_ops.match_type_with_inference(pattern, target, info.inferred):
1577
+ # Check if a previously-inferred param conflicts with the expected type
1578
+ for tp, expected_arg in zip(info.type_params, target.type_args):
1579
+ if tp in info.inferred and isinstance(expected_arg, TpyType):
1580
+ if info.inferred[tp] != expected_arg:
1581
+ raise SemanticError(
1582
+ f"Conflicting type for '{tp}' in '{info.record_name}': "
1583
+ f"previously inferred as '{info.inferred[tp]}', "
1584
+ f"but expected type requires '{expected_arg}'",
1585
+ loc,
1586
+ )
1587
+ return None
1588
+
1589
+ # Resolve IntLiteralType in any newly inferred params
1590
+ for k, v in list(info.inferred.items()):
1591
+ if isinstance(v, IntLiteralType):
1592
+ info.inferred[k] = self.ctx.default_int_for_literal(v)
1593
+
1594
+ # Check if all type params are now resolved
1595
+ if not all(tp in info.inferred for tp in info.type_params):
1596
+ return None
1597
+
1598
+ return self._eagerly_resolve_pending_generic(info)
1599
+
1600
+ def _eagerly_resolve_pending_generic(self, info: 'PendingGenericInstanceInfo') -> NominalType:
1601
+ """Resolve a pending generic instance to a concrete NominalType."""
1602
+ type_args = tuple(info.inferred[tp] for tp in info.type_params)
1603
+ resolved_type = NominalType(
1604
+ info.record_name, type_args,
1605
+ _module_qname=info.record_info.qualified_name(),
1606
+ )
1607
+
1608
+ # Validate type param bounds
1609
+ for param_name, type_arg in zip(info.type_params, type_args):
1610
+ if param_name in info.record_info.type_param_bounds:
1611
+ bound = info.record_info.type_param_bounds[param_name]
1612
+ if not self.protocols.satisfies_bound(type_arg, bound):
1613
+ raise self.ctx.error(
1614
+ f"Inferred type '{type_arg}' does not satisfy bound '{bound}' "
1615
+ f"for type parameter '{param_name}' of '{info.record_name}'",
1616
+ info.expr,
1617
+ )
1618
+
1619
+ # Update constructor expression
1620
+ info.expr.call_type = resolved_type
1621
+ self.ctx.set_expr_type(info.expr, resolved_type)
1622
+
1623
+ # Update scope and var_types (only when bound to a local variable,
1624
+ # not for inline expressions like Container().set(...) which would
1625
+ # corrupt the class binding in scope)
1626
+ if info.decl_line is not None:
1627
+ if self.ctx.func.current_scope:
1628
+ self.ctx.func.current_scope.define(info.variable_name, resolved_type)
1629
+ if self.ctx.func.current_ns:
1630
+ self.ctx.func.current_ns.bind_variable(info.variable_name, resolved_type)
1631
+ var_decl = self.ctx.func.var_decl_by_name.get(info.variable_name)
1632
+ if var_decl:
1633
+ self.ctx.var_types[id(var_decl)] = resolved_type
1634
+ self.ctx.declared_var_types[(info.decl_line, info.variable_name)] = resolved_type
1635
+
1636
+ # Set constructor info now that we have concrete types
1637
+ type_subst = info.inferred
1638
+ self.calls._set_record_constructor_info(info.expr, info.record_info, resolved_type, type_subst)
1639
+
1640
+ # Clean up tracking
1641
+ del self.ctx.func.pending_generic_instances[info.instance_id]
1642
+ self.ctx.func.variable_to_generic_instance.pop(info.variable_name, None)
1643
+
1644
+ return resolved_type
1645
+
1646
+ def _analyze_typed_dict_get(self, expr: TpyMethodCall, record_info, obj_type: TpyType) -> TpyType:
1647
+ """Analyze td.get("key") or td.get("key", default) on a TypedDict."""
1648
+ if len(expr.args) < 1 or len(expr.args) > 2:
1649
+ raise self.ctx.error(
1650
+ f"TypedDict.get() takes 1 or 2 arguments, got {len(expr.args)}", expr)
1651
+ if expr.kwargs:
1652
+ raise self.ctx.error("TypedDict.get() does not accept keyword arguments", expr)
1653
+ key_expr = expr.args[0]
1654
+ if not isinstance(key_expr, TpyStrLiteral):
1655
+ raise self.ctx.error(
1656
+ f"TypedDict '{obj_type.name}' keys must be string literals", key_expr)
1657
+ key = key_expr.value
1658
+ type_subst = self.type_ops.build_type_substitution(obj_type)
1659
+ for fld in record_info.fields:
1660
+ if fld.name == key:
1661
+ field_type = fld.type
1662
+ if type_subst:
1663
+ field_type = self.type_ops.substitute_type_params(field_type, type_subst)
1664
+ # total=False: field stored as Optional[T], unwrap to get inner type
1665
+ # total=True: field is always present, even if annotated Optional[T]
1666
+ is_absent_optional = record_info.is_total_false
1667
+ if is_absent_optional:
1668
+ assert isinstance(field_type, OptionalType)
1669
+ inner_type = field_type.inner
1670
+ else:
1671
+ inner_type = field_type
1672
+ expr.typed_dict_get_field = key
1673
+ expr.typed_dict_get_optional = is_absent_optional
1674
+ # Analyze key expression so its type is recorded
1675
+ self.expr.analyze_expr(key_expr)
1676
+ has_default = len(expr.args) == 2
1677
+ if has_default:
1678
+ default_type = self.expr.analyze_expr_with_hint(expr.args[1], inner_type)
1679
+ self.compat.check_type_compatible(
1680
+ default_type, inner_type,
1681
+ f"default value for TypedDict.get()", loc=expr.args[1].loc,
1682
+ source_expr=expr.args[1])
1683
+ return inner_type
1684
+ else:
1685
+ # Don't double-wrap: total=True Optional[T] field is already Optional
1686
+ if isinstance(inner_type, OptionalType):
1687
+ return inner_type
1688
+ return OptionalType(inner_type)
1689
+ raise self.ctx.error(
1690
+ f"TypedDict '{obj_type.name}' has no key '{key}'", key_expr)
1691
+
1692
+ def _analyze_instance_method(self, expr: TpyMethodCall, obj_type: TpyType,
1693
+ is_readonly_receiver: bool = False,
1694
+ is_consuming_receiver: bool = False) -> TpyType | None:
1695
+ """Analyze instance method call on any type (builtin or user record)."""
1696
+ record_info = self.ctx.registry.get_record_for_type(obj_type)
1697
+ if not record_info:
1698
+ return None
1699
+ # TypedDict: td.get("key") / td.get("key", default)
1700
+ if record_info.is_typed_dict and expr.method == "get":
1701
+ return self._analyze_typed_dict_get(expr, record_info, obj_type)
1702
+ overloads, inherited_subst = self.protocols.lookup_record_method_overloads(
1703
+ record_info, expr.method)
1704
+ if not overloads:
1705
+ return None
1706
+ instance_subst = self.type_ops.build_type_substitution(obj_type)
1707
+ if inherited_subst and instance_subst:
1708
+ type_subst = {
1709
+ k: self.type_ops.substitute_type_params(v, instance_subst)
1710
+ for k, v in inherited_subst.items()
1711
+ }
1712
+ elif inherited_subst:
1713
+ type_subst = inherited_subst
1714
+ else:
1715
+ type_subst = instance_subst
1716
+
1717
+ method_info = overloads[0]
1718
+
1719
+ # @inline: clone body and substitute at call site.
1720
+ # For FStr params, validate that f-string literals are passed.
1721
+ if method_info.inline_body is not None:
1722
+ for i, (pi, arg) in enumerate(zip(method_info.params, expr.args)):
1723
+ if is_fstr_type(pi.type) and not isinstance(arg, TpyFString):
1724
+ if isinstance(arg, TpyStrLiteral):
1725
+ expr.args[i] = TpyFString(parts=[arg.value], loc=arg.loc)
1726
+ else:
1727
+ raise self.ctx.error(
1728
+ f"Parameter '{pi.name}' has type FStr -- only f-string "
1729
+ f"or string literals are accepted", arg)
1730
+ return self._inline_method_call(expr, method_info)
1731
+
1732
+ # Check if the method has its own type parameters (generic method).
1733
+ # Generic methods don't support multiple overloads; user-defined methods
1734
+ # always register a single overload per name (registration.py).
1735
+ if method_info.is_generic():
1736
+ if len(overloads) > 1:
1737
+ raise self.ctx.error(
1738
+ f"Overloaded generic methods are not supported for '{expr.method}'", expr)
1739
+ return self._analyze_generic_method_call(
1740
+ expr, method_info, record_info, type_subst)
1741
+
1742
+ return self._resolve_and_check_args(
1743
+ expr, overloads, type_subst, is_readonly_receiver=is_readonly_receiver,
1744
+ is_consuming_receiver=is_consuming_receiver)
1745
+
1746
+ def _inline_method_call(
1747
+ self, expr: TpyMethodCall, method_info: FunctionInfo,
1748
+ ) -> TpyType:
1749
+ """Inline a method with FStr parameter at the call site.
1750
+
1751
+ Clones the method's body expression, substitutes ``self`` with the
1752
+ receiver and the FStr parameter with the actual f-string argument,
1753
+ then analyzes the substituted expression. This lets the f-string
1754
+ literal reach the call macro for decomposition.
1755
+ """
1756
+ body = copy.deepcopy(method_info.inline_body)
1757
+
1758
+ # Build the substitution map: param name -> call-site argument.
1759
+ # method_info.params does NOT include 'self' (it's implicit for methods).
1760
+ param_map: dict[str, TpyExpr] = {}
1761
+ for pi, arg in zip(method_info.params, expr.args):
1762
+ param_map[pi.name] = arg
1763
+
1764
+ # Substitute names in the cloned body
1765
+ MethodAnalyzer._substitute_inline_body(body, expr.obj, param_map)
1766
+
1767
+ # Store the inlined expression on the method call node for codegen
1768
+ expr.fstr_expansion = body
1769
+
1770
+ # Analyze the substituted expression (side effect: type-checks the macro expansion)
1771
+ self.expr.analyze_expr(body)
1772
+ return VOID
1773
+
1774
+ @staticmethod
1775
+ def _substitute_inline_body(
1776
+ node: TpyExpr, receiver: TpyExpr | None,
1777
+ param_map: dict[str, TpyExpr],
1778
+ ) -> None:
1779
+ """In-place substitute names in a cloned @inline body expression.
1780
+
1781
+ Args:
1782
+ node: The cloned body expression to substitute in.
1783
+ receiver: The ``self`` replacement (method calls), or None (free functions).
1784
+ param_map: Parameter name -> call-site argument expression.
1785
+
1786
+ Replaces TpyName references matching param_map keys or "self" (when
1787
+ receiver is provided) in positional args, function refs, and method
1788
+ receivers.
1789
+
1790
+ Current limitation: only handles TpyCall, TpyMethodCall, TpyFieldAccess,
1791
+ and TpyName in positional args. Does not recurse into kwargs, TpyBinOp,
1792
+ TpyIfExpr, TpySubscript, or other nested expression types. Sufficient
1793
+ for @inline bodies constrained to a single call. Future: support
1794
+ multi-statement bodies via expression blocks.
1795
+ """
1796
+ sub = MethodAnalyzer._substitute_inline_body
1797
+ sub_args = MethodAnalyzer._substitute_inline_args
1798
+ if isinstance(node, TpyCall):
1799
+ if isinstance(node.func, TpyName) and node.func.name in param_map:
1800
+ node.func = param_map[node.func.name]
1801
+ sub_args(node.args, receiver, param_map)
1802
+ elif isinstance(node, TpyMethodCall):
1803
+ if receiver and isinstance(node.obj, TpyName) and node.obj.name == "self":
1804
+ node.obj = receiver
1805
+ sub_args(node.args, receiver, param_map)
1806
+
1807
+ @staticmethod
1808
+ def _substitute_inline_args(
1809
+ args: list[TpyExpr], receiver: TpyExpr | None,
1810
+ param_map: dict[str, TpyExpr],
1811
+ ) -> None:
1812
+ """Substitute names in a list of positional arguments."""
1813
+ sub = MethodAnalyzer._substitute_inline_body
1814
+ for i, arg in enumerate(args):
1815
+ if isinstance(arg, TpyName) and arg.name in param_map:
1816
+ args[i] = param_map[arg.name]
1817
+ elif receiver and isinstance(arg, TpyName) and arg.name == "self":
1818
+ args[i] = receiver
1819
+ elif isinstance(arg, TpyFieldAccess):
1820
+ if receiver and isinstance(arg.obj, TpyName) and arg.obj.name == "self":
1821
+ arg.obj = receiver
1822
+ else:
1823
+ sub(arg, receiver, param_map)
1824
+ elif isinstance(arg, (TpyCall, TpyMethodCall)):
1825
+ sub(arg, receiver, param_map)
1826
+
1827
+ def _analyze_generic_method_call(
1828
+ self, expr: TpyMethodCall, method_info: FunctionInfo,
1829
+ record_info, class_subst: dict[str, TpyType | int],
1830
+ ) -> TpyType:
1831
+ """Analyze a call to a generic method (method with its own type parameters).
1832
+
1833
+ Handles two kinds of method type params:
1834
+ - New params: type params not in the class (e.g. U on def transform[U])
1835
+ - Constrained class params: class type params with an additional method-level bound
1836
+ """
1837
+ class_type_params = set(record_info.type_params) if record_info.type_params else set()
1838
+ new_params = [tp for tp in method_info.type_params if tp not in class_type_params]
1839
+ constrained_class_params = [tp for tp in method_info.type_params if tp in class_type_params]
1840
+
1841
+ # Validate per-method bounds on class type params: check that the concrete
1842
+ # class type satisfies the method's bound. These are class-level type params,
1843
+ # so they must be in class_subst for any fully-instantiated generic class.
1844
+ for tp in constrained_class_params:
1845
+ if tp not in class_subst:
1846
+ raise self.ctx.error(
1847
+ f"Method '{method_info.name}' has bound on class type parameter '{tp}', "
1848
+ f"but the class is not instantiated with a concrete type for '{tp}'",
1849
+ expr)
1850
+ raise_if_class_param_bound_violated(
1851
+ method_info, record_info.type_params, class_subst,
1852
+ self.protocols.type_conforms_to_protocol,
1853
+ self.ctx.error, expr,
1854
+ )
1855
+
1856
+ if not new_params:
1857
+ # All method type params are constrained class params -- no inference needed.
1858
+ # Single-element list: generic methods can't have multiple overloads (guarded above).
1859
+ return self._resolve_and_check_args(expr, [method_info], class_subst)
1860
+
1861
+ # Build a partial FunctionInfo with only new params for inference
1862
+ partial_func = FunctionInfo(
1863
+ name=method_info.name,
1864
+ params=method_info.params,
1865
+ return_type=method_info.return_type,
1866
+ type_params=new_params,
1867
+ type_param_bounds={k: v for k, v in method_info.type_param_bounds.items()
1868
+ if k in new_params},
1869
+ canonical_fi=method_info.root,
1870
+ )
1871
+
1872
+ # Pre-substitute class params in the method signature so inference
1873
+ # only needs to resolve new params
1874
+ if class_subst:
1875
+ partial_func = self.type_ops.substitute_method_type_params(partial_func, class_subst)
1876
+
1877
+ # Infer new params from arguments or explicit type args. The LHS-hint
1878
+ # seed is only useful on paths that actually re-analyze args with a
1879
+ # contextual hint, so it (and the closure that consumes it) lives
1880
+ # inside the inference branches that need it.
1881
+ has_wildcards = expr.type_args and None in expr.type_args
1882
+ if expr.type_args:
1883
+ if len(expr.type_args) != len(new_params):
1884
+ raise self.ctx.error(
1885
+ f"Method '{method_info.name}' expects {len(new_params)} type argument(s), "
1886
+ f"got {len(expr.type_args)}",
1887
+ expr)
1888
+ if not has_wildcards:
1889
+ # Full explicit -- no seed needed.
1890
+ method_subst = dict(zip(new_params, expr.type_args))
1891
+ else:
1892
+ # Partial explicit -- seed + explicit positional args drive
1893
+ # the per-arg hint; wildcards leave the seed binding in place.
1894
+ method_subst = self._infer_method_subst_with_seed(
1895
+ expr, partial_func, method_info, new_params,
1896
+ explicit_type_args=expr.type_args,
1897
+ )
1898
+ else:
1899
+ method_subst = self._infer_method_subst_with_seed(
1900
+ expr, partial_func, method_info, new_params,
1901
+ explicit_type_args=None,
1902
+ )
1903
+
1904
+ # Validate bounds for new params (inference checks bounds internally,
1905
+ # but explicit type args bypass inference)
1906
+ new_param_bounds = {k: v for k, v in method_info.type_param_bounds.items()
1907
+ if k in set(new_params)}
1908
+ if new_param_bounds:
1909
+ validate_type_param_bounds(
1910
+ method_subst, new_param_bounds, method_info.name,
1911
+ self.protocols.satisfies_bound,
1912
+ lambda msg: self.ctx.error(msg, expr),
1913
+ )
1914
+
1915
+ # Store inferred type args (new params only) for codegen
1916
+ expr.inferred_type_args = tuple(
1917
+ resolve_inferred_type_arg(method_subst[p], self.ctx.default_int_type)
1918
+ for p in new_params
1919
+ )
1920
+ expr.representational_subst_params = (
1921
+ self.type_ops.compute_representational_subst_params(
1922
+ method_info, expr.inferred_type_args))
1923
+
1924
+ # Merge class subst + method subst for full substitution
1925
+ full_subst = dict(class_subst) if class_subst else {}
1926
+ full_subst.update(method_subst)
1927
+
1928
+ return self._resolve_and_check_args(expr, [method_info], full_subst)
1929
+
1930
+ def _infer_method_subst_with_seed(
1931
+ self,
1932
+ expr: TpyMethodCall,
1933
+ partial_func: FunctionInfo,
1934
+ method_info: FunctionInfo,
1935
+ new_params: list[str],
1936
+ explicit_type_args: tuple['TpyType | None', ...] | None,
1937
+ ) -> dict[str, TpyType]:
1938
+ """Analyze args with an LHS-hint seed and run method-param inference.
1939
+
1940
+ Shared by the pure-inference and partial-explicit (wildcard) branches
1941
+ of ``_analyze_generic_method_call``. The seed is computed against the
1942
+ already class-substituted ``partial_func.return_type``; explicit
1943
+ positional type args (when present) overlay the seed at their
1944
+ positions.
1945
+ """
1946
+ seed_subst = self.type_ops.seed_subst_from_return_hint(
1947
+ partial_func, self.ctx.expr_type_hint
1948
+ )
1949
+ merged_seed = dict(seed_subst)
1950
+ if explicit_type_args is not None:
1951
+ for tp, ta in zip(new_params, explicit_type_args):
1952
+ if ta is not None:
1953
+ merged_seed[tp] = ta
1954
+
1955
+ # Default-arg capture freezes ``partial_func`` and ``merged_seed`` at
1956
+ # def-time so this closure isn't sensitive to later rebinding.
1957
+ def _analyze_args(
1958
+ _pf: FunctionInfo = partial_func,
1959
+ _seed: dict[str, TpyType] = merged_seed,
1960
+ ) -> list[TpyType]:
1961
+ out: list[TpyType] = []
1962
+ for i, arg in enumerate(expr.args):
1963
+ hint = seeded_arg_hint(_pf.params, i, _seed)
1964
+ out.append(self.expr.analyze_call_arg(arg, hint))
1965
+ return out
1966
+
1967
+ arg_types = _analyze_args()
1968
+ method_subst = self.type_ops.infer_type_params_for_function(
1969
+ partial_func, arg_types, self.protocols.satisfies_bound,
1970
+ expected_return_type=self.ctx.expr_type_hint,
1971
+ explicit_type_args=explicit_type_args,
1972
+ )
1973
+ if method_subst is None:
1974
+ raise self.ctx.error(
1975
+ f"Cannot infer type arguments for method '{method_info.name}'. "
1976
+ f"Specify explicitly: .{method_info.name}[{', '.join(new_params)}](...)",
1977
+ expr)
1978
+ return method_subst
1979
+
1980
+ def _is_protocol_method_readonly(self, protocol_name: str, method_name: str) -> bool:
1981
+ """Check if a protocol method is readonly (per-method or protocol-level).
1982
+
1983
+ Searches inherited methods too, so a @readonly method from a parent
1984
+ protocol is correctly recognized.
1985
+ """
1986
+ proto_info = self.ctx.registry.scan_by_short_name(protocol_name)
1987
+ if proto_info is None:
1988
+ return False
1989
+ if proto_info.is_readonly:
1990
+ return True
1991
+ for msig in self.protocols.collect_protocol_methods(protocol_name):
1992
+ if msig.name == method_name:
1993
+ return msig.is_readonly
1994
+ return False
1995
+
1996
+ def _build_protocol_method_info(self, protocol_name: str, method_name: str,
1997
+ raw_params: list[tuple[str, TpyType]],
1998
+ return_type: TpyType,
1999
+ cpp_template: str | None = None,
2000
+ param_defaults: list | None = None) -> FunctionInfo:
2001
+ """Build a FunctionInfo for a protocol method signature."""
2002
+ # param_defaults is parallel to raw_params (None for required, TpyExpr for optional);
2003
+ # empty list means no defaults declared at protocol method level.
2004
+ defaults = param_defaults if param_defaults else [None] * len(raw_params)
2005
+ params = [
2006
+ ParamInfo(n, t, default_expr=default)
2007
+ for (n, t), default in zip(raw_params, defaults)
2008
+ ]
2009
+ # __next__ on Iterator protocol has implicit @error_return(StopIteration)
2010
+ error_return_type = None
2011
+ if method_name == "__next__":
2012
+ error_return_type = "builtins.StopIteration"
2013
+ return FunctionInfo(
2014
+ name=method_name, params=params, return_type=return_type,
2015
+ is_method=True,
2016
+ is_readonly=self._is_protocol_method_readonly(protocol_name, method_name),
2017
+ cpp_template=cpp_template,
2018
+ error_return_type=error_return_type,
2019
+ )
2020
+
2021
+ def _analyze_protocol_or_bound_method(self, expr: TpyMethodCall, obj_type: TpyType) -> TpyType | None:
2022
+ """Analyze method calls on protocol-typed values or bounded type parameters."""
2023
+ if is_protocol_type(obj_type):
2024
+ method_sig = self.protocols.get_protocol_method_signature(obj_type, expr.method)
2025
+ if method_sig is None:
2026
+ raise self.ctx.error(f"Protocol '{obj_type.name}' has no method '{expr.method}'", expr)
2027
+ raw_params, return_type, cpp_template, param_defaults = method_sig
2028
+ fi = self._build_protocol_method_info(
2029
+ obj_type.name, expr.method, raw_params, return_type, cpp_template, param_defaults)
2030
+ return self._resolve_and_check_args(expr, [fi], {})
2031
+
2032
+ if isinstance(obj_type, TypeParamRef):
2033
+ bound = self.type_ops.get_type_param_bound(obj_type.name)
2034
+ if bound is not None and is_protocol_type(bound):
2035
+ method_sig = self.protocols.get_protocol_method_signature(bound, expr.method, self_type=obj_type)
2036
+ if method_sig is None:
2037
+ raise self.ctx.error(f"Protocol '{bound.name}' has no method '{expr.method}'", expr)
2038
+ raw_params, return_type, cpp_template, param_defaults = method_sig
2039
+ fi = self._build_protocol_method_info(
2040
+ bound.name, expr.method, raw_params, return_type, cpp_template, param_defaults)
2041
+ return self._resolve_and_check_args(expr, [fi], {})
2042
+
2043
+ return None
2044
+
2045
+ def _analyze_super_method_call(self, expr: TpyMethodCall, super_type: SuperType) -> TpyType:
2046
+ """Analyze a super().method() call.
2047
+
2048
+ The method is looked up in the parent class and type arguments are
2049
+ substituted for generic parent classes. `super_type` is produced by
2050
+ analyze_expr on the receiver (see calls.py qname dispatch on
2051
+ `builtins.super`).
2052
+
2053
+ Multi-base (D22 v2.3): __del__ is rejected outright; other methods are
2054
+ resolved by walking the child's C3 MRO and picking the first ancestor
2055
+ whose own method table defines the name. See _resolve_super_parent_type.
2056
+ """
2057
+ parent_type = self._resolve_super_parent_type(expr, super_type)
2058
+ parent_info = self.ctx.registry.get_record_for_type(parent_type)
2059
+ if parent_info is None:
2060
+ raise self.ctx.error(f"Parent class '{parent_type}' not found", expr)
2061
+
2062
+ # Special handling for super().__init__()
2063
+ if expr.method == "__init__":
2064
+ # super().__init__() can only be called inside __init__
2065
+ if self.ctx.func.current_function is None or self.ctx.func.current_function.name != "__init__":
2066
+ raise self.ctx.error(
2067
+ "super().__init__() can only be called inside __init__",
2068
+ expr
2069
+ )
2070
+ # Check for duplicate super().__init__() calls
2071
+ if self.ctx.func.super_init_call is not None:
2072
+ raise self.ctx.error(
2073
+ "super().__init__() can only be called once",
2074
+ expr
2075
+ )
2076
+ # Track this call for later validation (must be first statement)
2077
+ self.ctx.func.super_init_call = expr
2078
+
2079
+ init_overloads = parent_info.get_method_overloads("__init__")
2080
+ if not init_overloads:
2081
+ # Parent has no __init__, allow with no arguments
2082
+ if expr.args:
2083
+ raise self.ctx.error(
2084
+ f"Parent class '{parent_type}' has no __init__, "
2085
+ "super().__init__() must be called with no arguments",
2086
+ expr
2087
+ )
2088
+ # Store parent type for codegen (will generate default base init)
2089
+ expr.super_parent_type = parent_type
2090
+ return VOID
2091
+
2092
+ # Special handling for super().__del__()
2093
+ if expr.method == "__del__":
2094
+ # super().__del__() can only be called inside __del__
2095
+ if self.ctx.func.current_function is None or self.ctx.func.current_function.name != "__del__":
2096
+ raise self.ctx.error(
2097
+ "super().__del__() can only be called inside __del__",
2098
+ expr
2099
+ )
2100
+ # Check for duplicate super().__del__() calls
2101
+ if self.ctx.func.super_del_call is not None:
2102
+ raise self.ctx.error(
2103
+ "super().__del__() can only be called once",
2104
+ expr
2105
+ )
2106
+ # Track this call for later validation (must be last statement)
2107
+ self.ctx.func.super_del_call = expr
2108
+ # No arguments allowed
2109
+ if expr.args:
2110
+ raise self.ctx.error(
2111
+ "super().__del__() takes no arguments",
2112
+ expr
2113
+ )
2114
+ expr.super_parent_type = parent_type
2115
+ return VOID
2116
+
2117
+ # Check readonly constraint: super() in @readonly method inherits readonly
2118
+ is_readonly_context = (
2119
+ isinstance(self.ctx.func.current_function, TpyFunction)
2120
+ and self.ctx.func.current_function.is_readonly
2121
+ )
2122
+
2123
+ # Look up the method in the parent class.
2124
+ # For __init__, we already have init_overloads; for other methods, walk
2125
+ # the parent's MRO so inherited methods (not defined on parent_info itself)
2126
+ # still resolve -- fixes super().foo() when foo lives on parent's ancestor.
2127
+ if expr.method == "__init__":
2128
+ overloads = init_overloads
2129
+ else:
2130
+ overloads = self.ctx.registry.get_method_overloads_with_parents(
2131
+ parent_info, expr.method
2132
+ )
2133
+ if not overloads:
2134
+ raise self.ctx.error(
2135
+ f"Parent class '{parent_type}' has no method '{expr.method}'",
2136
+ expr
2137
+ )
2138
+
2139
+ # Build type substitution for generic parent (e.g., Container[Int32] -> {"T": Int32})
2140
+ type_subst = self.protocols.get_parent_type_subst(parent_type, parent_info)
2141
+
2142
+ # Must be set before arg resolution: mutation-call-edge recording
2143
+ # treats super() receivers as self for self-mutation propagation.
2144
+ expr.super_parent_type = parent_type
2145
+
2146
+ # Check if the method has its own type parameters (generic method)
2147
+ method_info = overloads[0]
2148
+ if method_info.is_generic():
2149
+ if len(overloads) > 1:
2150
+ raise self.ctx.error(
2151
+ f"Overloaded generic methods are not supported for '{expr.method}'", expr)
2152
+ return_type = self._analyze_generic_method_call(
2153
+ expr, method_info, parent_info, type_subst)
2154
+ else:
2155
+ return_type = self._resolve_and_check_args(expr, overloads, type_subst)
2156
+ if is_readonly_context and not expr.resolved_function_info.is_readonly:
2157
+ raise self.ctx.error(
2158
+ f"Cannot call non-readonly method '{expr.method}' on readonly reference",
2159
+ expr)
2160
+ return return_type
2161
+
2162
+ @staticmethod
2163
+ def stmt_contains_super_init(stmt: TpyStmt, super_init: TpyMethodCall) -> bool:
2164
+ """Check if a statement contains the given super().__init__() call.
2165
+
2166
+ Used to validate that super().__init__() is the first statement.
2167
+ """
2168
+ # Direct expression statement containing the super().__init__() call
2169
+ if isinstance(stmt, TpyExprStmt):
2170
+ return stmt.expr is super_init
2171
+ return False
2172
+
2173
+ @staticmethod
2174
+ def find_first_non_docstring_stmt(stmts: list[TpyStmt]) -> TpyStmt | None:
2175
+ """Find the first non-docstring statement in a list.
2176
+
2177
+ Returns None if all statements are docstrings or list is empty.
2178
+ """
2179
+ for stmt in stmts:
2180
+ if is_docstring(stmt):
2181
+ continue
2182
+ return stmt
2183
+ return None
2184
+
2185
+ @staticmethod
2186
+ def find_last_non_docstring_stmt(stmts: list[TpyStmt]) -> TpyStmt | None:
2187
+ """Find the last statement, skipping only a leading docstring.
2188
+
2189
+ Only the first statement can be a docstring; trailing string literals
2190
+ are regular statements, not docstrings.
2191
+ """
2192
+ if not stmts:
2193
+ return None
2194
+ if len(stmts) == 1 and is_docstring(stmts[0]):
2195
+ return None
2196
+ return stmts[-1]
2197
+