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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. tpy_lang-0.3.0.dev0.dist-info/METADATA +151 -0
  2. tpy_lang-0.3.0.dev0.dist-info/RECORD +333 -0
  3. tpy_lang-0.3.0.dev0.dist-info/WHEEL +4 -0
  4. tpy_lang-0.3.0.dev0.dist-info/entry_points.txt +3 -0
  5. tpyc/__init__.py +104 -0
  6. tpyc/__main__.py +6 -0
  7. tpyc/_buildinfo.py +1 -0
  8. tpyc/_data/docs/LANGUAGE_FEATURES.md +6278 -0
  9. tpyc/_data/docs/STDLIB_ROADMAP.md +1258 -0
  10. tpyc/_data/docs/TPY_FOR_AGENTS.md +556 -0
  11. tpyc/_data/lib/tpy/_bindings/__init__.py +6 -0
  12. tpyc/_data/lib/tpy/_bindings/pcre2.py +173 -0
  13. tpyc/_data/lib/tpy/_bindings/posix_socket.py +161 -0
  14. tpyc/_data/lib/tpy/_functools_macros.py +80 -0
  15. tpyc/_data/lib/tpy/_macro_helpers.py +161 -0
  16. tpyc/_data/lib/tpy/argparse.py +2062 -0
  17. tpyc/_data/lib/tpy/asyncio/__init__.py +744 -0
  18. tpyc/_data/lib/tpy/asyncio/_executor.py +515 -0
  19. tpyc/_data/lib/tpy/base64.py +410 -0
  20. tpyc/_data/lib/tpy/bisect.py +39 -0
  21. tpyc/_data/lib/tpy/builtins.py +38 -0
  22. tpyc/_data/lib/tpy/dataclasses.py +354 -0
  23. tpyc/_data/lib/tpy/enum.py +23 -0
  24. tpyc/_data/lib/tpy/functools.py +33 -0
  25. tpyc/_data/lib/tpy/hashlib.py +206 -0
  26. tpyc/_data/lib/tpy/heapq.py +118 -0
  27. tpyc/_data/lib/tpy/io.py +395 -0
  28. tpyc/_data/lib/tpy/json.py +221 -0
  29. tpyc/_data/lib/tpy/math.py +406 -0
  30. tpyc/_data/lib/tpy/random.py +597 -0
  31. tpyc/_data/lib/tpy/re.py +467 -0
  32. tpyc/_data/lib/tpy/socket.py +379 -0
  33. tpyc/_data/lib/tpy/struct.py +178 -0
  34. tpyc/_data/lib/tpy/sys.py +40 -0
  35. tpyc/_data/lib/tpy/time.py +39 -0
  36. tpyc/_data/lib/tpy/tpy/__init__.py +78 -0
  37. tpyc/_data/lib/tpy/tpy/_bootstrap/__init__.py +10 -0
  38. tpyc/_data/lib/tpy/tpy/_bootstrap/_decorators.py +37 -0
  39. tpyc/_data/lib/tpy/tpy/_bootstrap/_extern.py +64 -0
  40. tpyc/_data/lib/tpy/tpy/_builtins/__init__.py +11 -0
  41. tpyc/_data/lib/tpy/tpy/_builtins/_bytes.py +378 -0
  42. tpyc/_data/lib/tpy/tpy/_builtins/_dict.py +151 -0
  43. tpyc/_data/lib/tpy/tpy/_builtins/_exceptions.py +125 -0
  44. tpyc/_data/lib/tpy/tpy/_builtins/_funcs.py +681 -0
  45. tpyc/_data/lib/tpy/tpy/_builtins/_io.py +97 -0
  46. tpyc/_data/lib/tpy/tpy/_builtins/_list.py +127 -0
  47. tpyc/_data/lib/tpy/tpy/_builtins/_range.py +52 -0
  48. tpyc/_data/lib/tpy/tpy/_builtins/_set.py +139 -0
  49. tpyc/_data/lib/tpy/tpy/_builtins/_super.py +11 -0
  50. tpyc/_data/lib/tpy/tpy/_builtins/_types.py +661 -0
  51. tpyc/_data/lib/tpy/tpy/_core/__init__.py +23 -0
  52. tpyc/_data/lib/tpy/tpy/_core/_bytes_view.py +129 -0
  53. tpyc/_data/lib/tpy/tpy/_core/_containers.py +137 -0
  54. tpyc/_data/lib/tpy/tpy/_core/_functions.py +40 -0
  55. tpyc/_data/lib/tpy/tpy/_core/_types.py +2061 -0
  56. tpyc/_data/lib/tpy/tpy/_typing/__init__.py +77 -0
  57. tpyc/_data/lib/tpy/tpy/_version.py +29 -0
  58. tpyc/_data/lib/tpy/tpy/bits.py +28 -0
  59. tpyc/_data/lib/tpy/tpy/coro/__init__.py +127 -0
  60. tpyc/_data/lib/tpy/tpy/extern.py +8 -0
  61. tpyc/_data/lib/tpy/tpy/mem.py +49 -0
  62. tpyc/_data/lib/tpy/tpy/unsafe.py +195 -0
  63. tpyc/_data/lib/tpy/tpy/version.py +21 -0
  64. tpyc/_data/lib/tpy/typing.py +13 -0
  65. tpyc/_data/runtime/cpp/include/tpy/any.hpp +461 -0
  66. tpyc/_data/runtime/cpp/include/tpy/as_ostream.hpp +117 -0
  67. tpyc/_data/runtime/cpp/include/tpy/async.hpp +76 -0
  68. tpyc/_data/runtime/cpp/include/tpy/bigint.hpp +1343 -0
  69. tpyc/_data/runtime/cpp/include/tpy/builtins.hpp +400 -0
  70. tpyc/_data/runtime/cpp/include/tpy/bytes_ops.hpp +469 -0
  71. tpyc/_data/runtime/cpp/include/tpy/container_ops.hpp +487 -0
  72. tpyc/_data/runtime/cpp/include/tpy/copy_iter.hpp +82 -0
  73. tpyc/_data/runtime/cpp/include/tpy/core.hpp +558 -0
  74. tpyc/_data/runtime/cpp/include/tpy/dict_ops.hpp +289 -0
  75. tpyc/_data/runtime/cpp/include/tpy/dunder.hpp +750 -0
  76. tpyc/_data/runtime/cpp/include/tpy/dynamic.hpp +44 -0
  77. tpyc/_data/runtime/cpp/include/tpy/enum.hpp +40 -0
  78. tpyc/_data/runtime/cpp/include/tpy/file.hpp +245 -0
  79. tpyc/_data/runtime/cpp/include/tpy/fixed_int.hpp +317 -0
  80. tpyc/_data/runtime/cpp/include/tpy/format.hpp +954 -0
  81. tpyc/_data/runtime/cpp/include/tpy/frame_slot.hpp +120 -0
  82. tpyc/_data/runtime/cpp/include/tpy/generator.hpp +47 -0
  83. tpyc/_data/runtime/cpp/include/tpy/iterable_ops.hpp +122 -0
  84. tpyc/_data/runtime/cpp/include/tpy/itertools.hpp +749 -0
  85. tpyc/_data/runtime/cpp/include/tpy/next_iter.hpp +82 -0
  86. tpyc/_data/runtime/cpp/include/tpy/ordered_map.hpp +518 -0
  87. tpyc/_data/runtime/cpp/include/tpy/ordered_set.hpp +337 -0
  88. tpyc/_data/runtime/cpp/include/tpy/own_iter.hpp +54 -0
  89. tpyc/_data/runtime/cpp/include/tpy/pascal_graph_sdl.hpp +192 -0
  90. tpyc/_data/runtime/cpp/include/tpy/printing.hpp +302 -0
  91. tpyc/_data/runtime/cpp/include/tpy/protocols.hpp +61 -0
  92. tpyc/_data/runtime/cpp/include/tpy/range.hpp +115 -0
  93. tpyc/_data/runtime/cpp/include/tpy/ranges.hpp +212 -0
  94. tpyc/_data/runtime/cpp/include/tpy/set_ops.hpp +265 -0
  95. tpyc/_data/runtime/cpp/include/tpy/slice.hpp +47 -0
  96. tpyc/_data/runtime/cpp/include/tpy/span_iter.hpp +42 -0
  97. tpyc/_data/runtime/cpp/include/tpy/stdlib/math.hpp +41 -0
  98. tpyc/_data/runtime/cpp/include/tpy/stdlib/pcre2_h.hpp +96 -0
  99. tpyc/_data/runtime/cpp/include/tpy/stdlib/random.hpp +25 -0
  100. tpyc/_data/runtime/cpp/include/tpy/stdlib/socket_h.hpp +145 -0
  101. tpyc/_data/runtime/cpp/include/tpy/stdlib/time.hpp +62 -0
  102. tpyc/_data/runtime/cpp/include/tpy/system.hpp +121 -0
  103. tpyc/_data/runtime/cpp/include/tpy/throwable.hpp +55 -0
  104. tpyc/_data/runtime/cpp/include/tpy/tpy.hpp +156 -0
  105. tpyc/_data/runtime/cpp/include/tpy/type_name.hpp +77 -0
  106. tpyc/_data/runtime/cpp/include/tpy/type_traits.hpp +240 -0
  107. tpyc/_data/runtime/cpp/include/tpy/uninit_array_storage.hpp +250 -0
  108. tpyc/_data/runtime/cpp/include/tpy/uninit_heap_storage.hpp +277 -0
  109. tpyc/_data/runtime/cpp/include/tpy/varargs.hpp +174 -0
  110. tpyc/_data/runtime/cpp/include/tpy/variant_ref.hpp +118 -0
  111. tpyc/_data/runtime/cpp/src/stdlib/socket_impl.cpp +104 -0
  112. tpyc/_data/runtime/cpp/third_party/README.md +58 -0
  113. tpyc/_data/runtime/cpp/third_party/pcre2/AUTHORS +36 -0
  114. tpyc/_data/runtime/cpp/third_party/pcre2/CMakeLists.txt +1233 -0
  115. tpyc/_data/runtime/cpp/third_party/pcre2/COPYING +5 -0
  116. tpyc/_data/runtime/cpp/third_party/pcre2/ChangeLog +3097 -0
  117. tpyc/_data/runtime/cpp/third_party/pcre2/HACKING +853 -0
  118. tpyc/_data/runtime/cpp/third_party/pcre2/INSTALL +368 -0
  119. tpyc/_data/runtime/cpp/third_party/pcre2/LICENCE +94 -0
  120. tpyc/_data/runtime/cpp/third_party/pcre2/NEWS +492 -0
  121. tpyc/_data/runtime/cpp/third_party/pcre2/NON-AUTOTOOLS-BUILD +430 -0
  122. tpyc/_data/runtime/cpp/third_party/pcre2/README +956 -0
  123. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/COPYING-CMAKE-SCRIPTS +22 -0
  124. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindEditline.cmake +16 -0
  125. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindPackageHandleStandardArgs.cmake +58 -0
  126. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindReadline.cmake +29 -0
  127. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config-version.cmake.in +15 -0
  128. tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config.cmake.in +148 -0
  129. tpyc/_data/runtime/cpp/third_party/pcre2/config-cmake.h.in +56 -0
  130. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-16.pc.in +13 -0
  131. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-32.pc.in +13 -0
  132. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-8.pc.in +13 -0
  133. tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-posix.pc.in +13 -0
  134. tpyc/_data/runtime/cpp/third_party/pcre2/pcre2-config.in +121 -0
  135. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h +483 -0
  136. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.generic +483 -0
  137. tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.in +460 -0
  138. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h +1010 -0
  139. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.generic +1010 -0
  140. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.in +1010 -0
  141. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_auto_possess.c +1371 -0
  142. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c +196 -0
  143. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c.dist +196 -0
  144. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chkdint.c +96 -0
  145. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_compile.c +11001 -0
  146. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_config.c +252 -0
  147. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_context.c +510 -0
  148. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_convert.c +1189 -0
  149. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dfa_match.c +4119 -0
  150. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dftables.c +297 -0
  151. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_error.c +345 -0
  152. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_extuni.c +162 -0
  153. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_find_bracket.c +219 -0
  154. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_fuzzsupport.c +792 -0
  155. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_internal.h +2084 -0
  156. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_intmodedep.h +940 -0
  157. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_compile.c +14972 -0
  158. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_match.c +200 -0
  159. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_misc.c +234 -0
  160. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_neon_inc.h +354 -0
  161. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_simd_inc.h +2355 -0
  162. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_test.c +2528 -0
  163. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_maketables.c +165 -0
  164. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match.c +7777 -0
  165. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match_data.c +185 -0
  166. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_newline.c +243 -0
  167. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ord2utf.c +120 -0
  168. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_pattern_info.c +432 -0
  169. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_printint.c +886 -0
  170. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_script_run.c +344 -0
  171. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_serialize.c +286 -0
  172. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_string_utils.c +237 -0
  173. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_study.c +1915 -0
  174. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substitute.c +1009 -0
  175. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substring.c +550 -0
  176. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_tables.c +234 -0
  177. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucd.c +5460 -0
  178. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucp.h +396 -0
  179. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucptables.c +1533 -0
  180. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_valid_utf.c +398 -0
  181. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_xclass.c +308 -0
  182. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2demo.c +497 -0
  183. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2grep.c +4606 -0
  184. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.c +425 -0
  185. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.h +187 -0
  186. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix_test.c +209 -0
  187. tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2test.c +9708 -0
  188. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorApple.c +137 -0
  189. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorCore.c +327 -0
  190. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorFreeBSD.c +89 -0
  191. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorPosix.c +62 -0
  192. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorWindows.c +40 -0
  193. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorNetBSD.c +72 -0
  194. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorPosix.c +172 -0
  195. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorPosix.c +141 -0
  196. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorWindows.c +102 -0
  197. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfig.h +142 -0
  198. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigCPU.h +188 -0
  199. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigInternal.h +907 -0
  200. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.c +3561 -0
  201. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.h +2466 -0
  202. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_32.c +4636 -0
  203. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_64.c +3491 -0
  204. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_T2_32.c +4302 -0
  205. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeLOONGARCH_64.c +3765 -0
  206. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_32.c +472 -0
  207. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_64.c +387 -0
  208. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_common.c +4259 -0
  209. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_32.c +485 -0
  210. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_64.c +719 -0
  211. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_common.c +3161 -0
  212. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_32.c +142 -0
  213. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_64.c +222 -0
  214. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_common.c +3121 -0
  215. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeS390X.c +4526 -0
  216. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_32.c +1685 -0
  217. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_64.c +1398 -0
  218. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_common.c +5001 -0
  219. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitSerialize.c +516 -0
  220. tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitUtils.c +344 -0
  221. tpyc/_data/runtime/cpp/third_party/pcre2.sources.txt +54 -0
  222. tpyc/_data/runtime/cpp/third_party/pcre2.vendor.json +7 -0
  223. tpyc/build/__init__.py +7 -0
  224. tpyc/build/pcre2.py +122 -0
  225. tpyc/build/third_party.py +413 -0
  226. tpyc/cli.py +822 -0
  227. tpyc/codegen_cpp/__init__.py +18 -0
  228. tpyc/codegen_cpp/builtins.py +484 -0
  229. tpyc/codegen_cpp/context.py +2064 -0
  230. tpyc/codegen_cpp/expressions.py +5940 -0
  231. tpyc/codegen_cpp/functions.py +1913 -0
  232. tpyc/codegen_cpp/gen_async.py +3258 -0
  233. tpyc/codegen_cpp/gen_generators.py +657 -0
  234. tpyc/codegen_cpp/generator.py +2258 -0
  235. tpyc/codegen_cpp/match.py +1997 -0
  236. tpyc/codegen_cpp/param_const.py +172 -0
  237. tpyc/codegen_cpp/protocols.py +907 -0
  238. tpyc/codegen_cpp/records.py +1654 -0
  239. tpyc/codegen_cpp/resumable_cfg.py +1651 -0
  240. tpyc/codegen_cpp/statements.py +4963 -0
  241. tpyc/codegen_cpp/string_dispatch.py +76 -0
  242. tpyc/codegen_cpp/test_context.py +46 -0
  243. tpyc/codegen_cpp/test_param_const.py +113 -0
  244. tpyc/codegen_cpp/test_resumable_cfg.py +182 -0
  245. tpyc/codegen_cpp/type_resolution.py +53 -0
  246. tpyc/codegen_cpp/types.py +436 -0
  247. tpyc/codegen_cpp/variant_access.py +135 -0
  248. tpyc/coercions.py +749 -0
  249. tpyc/compilation_context.py +57 -0
  250. tpyc/compiler.py +3945 -0
  251. tpyc/cycle_detection.py +358 -0
  252. tpyc/diagnostics.py +135 -0
  253. tpyc/dump_types.py +353 -0
  254. tpyc/frontend_diagnostics.py +47 -0
  255. tpyc/frontend_ir/__init__.py +140 -0
  256. tpyc/frontend_ir/lower.py +1098 -0
  257. tpyc/frontend_ir/nodes.py +718 -0
  258. tpyc/frontend_ir/resolver_adapter.py +151 -0
  259. tpyc/frontend_plugin.py +209 -0
  260. tpyc/install_docs.py +81 -0
  261. tpyc/liveness.py +756 -0
  262. tpyc/macro_api.py +1724 -0
  263. tpyc/macro_loader.py +497 -0
  264. tpyc/module_names.py +64 -0
  265. tpyc/modules/__init__.py +31 -0
  266. tpyc/modules/defs.py +89 -0
  267. tpyc/modules/registry.py +36 -0
  268. tpyc/modules/resolver.py +192 -0
  269. tpyc/modules/type_resolution.py +629 -0
  270. tpyc/namespace.py +172 -0
  271. tpyc/parse/__init__.py +84 -0
  272. tpyc/parse/imports.py +490 -0
  273. tpyc/parse/nodes.py +1732 -0
  274. tpyc/parse/parser.py +4043 -0
  275. tpyc/parse/resolve_refs.py +466 -0
  276. tpyc/parse/type_resolver.py +1060 -0
  277. tpyc/prescan.py +254 -0
  278. tpyc/qnames.py +149 -0
  279. tpyc/repl.py +529 -0
  280. tpyc/repl_backends.py +848 -0
  281. tpyc/sema/__init__.py +21 -0
  282. tpyc/sema/analyzer.py +3625 -0
  283. tpyc/sema/bound_check.py +72 -0
  284. tpyc/sema/builder_trace.py +684 -0
  285. tpyc/sema/calls.py +5406 -0
  286. tpyc/sema/compatibility.py +2107 -0
  287. tpyc/sema/context.py +1243 -0
  288. tpyc/sema/expressions.py +3737 -0
  289. tpyc/sema/flow_facts.py +199 -0
  290. tpyc/sema/init_tracker.py +150 -0
  291. tpyc/sema/list_literals.py +69 -0
  292. tpyc/sema/literal_utils.py +27 -0
  293. tpyc/sema/local_deduction.py +1088 -0
  294. tpyc/sema/macros.py +179 -0
  295. tpyc/sema/match.py +1177 -0
  296. tpyc/sema/method_expansion.py +347 -0
  297. tpyc/sema/methods.py +2197 -0
  298. tpyc/sema/mutation_propagation.py +268 -0
  299. tpyc/sema/narrowing.py +857 -0
  300. tpyc/sema/numeric_lattice.py +160 -0
  301. tpyc/sema/operators.py +402 -0
  302. tpyc/sema/overloads.py +841 -0
  303. tpyc/sema/protocols.py +1209 -0
  304. tpyc/sema/reach_analysis.py +202 -0
  305. tpyc/sema/registration.py +3156 -0
  306. tpyc/sema/scope_tracker.py +193 -0
  307. tpyc/sema/statements.py +4426 -0
  308. tpyc/sema/type_ops.py +1879 -0
  309. tpyc/sema/value_range.py +181 -0
  310. tpyc/symbol_binding.py +259 -0
  311. tpyc/test_c3_mro.py +208 -0
  312. tpyc/test_cli_argv.py +52 -0
  313. tpyc/test_compiler.py +559 -0
  314. tpyc/test_contains_type_param.py +101 -0
  315. tpyc/test_cycle_detection.py +221 -0
  316. tpyc/test_dump_types.py +225 -0
  317. tpyc/test_install_docs.py +65 -0
  318. tpyc/test_local_cpp_form.py +135 -0
  319. tpyc/test_macro_loader.py +76 -0
  320. tpyc/test_method_expansion.py +254 -0
  321. tpyc/test_nominal_identity.py +182 -0
  322. tpyc/test_overloads.py +410 -0
  323. tpyc/test_parse.py +303 -0
  324. tpyc/test_parse_type_ref.py +506 -0
  325. tpyc/test_parse_version_info.py +58 -0
  326. tpyc/test_reach_analysis.py +72 -0
  327. tpyc/test_ref_type.py +216 -0
  328. tpyc/test_send_sync_substitution.py +276 -0
  329. tpyc/test_tuple_mutation_propagation.py +206 -0
  330. tpyc/test_type_def_registry.py +1729 -0
  331. tpyc/test_union_types.py +195 -0
  332. tpyc/type_def_registry.py +975 -0
  333. tpyc/typesys.py +5104 -0
@@ -0,0 +1,3737 @@
1
+ """
2
+ TurboPython Expression Analysis
3
+
4
+ Core expression analysis including literals, names, operators, field access, and subscripts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from contextlib import ExitStack
9
+ from collections.abc import Callable as CallableFn
10
+ from dataclasses import replace as dc_replace
11
+ from typing import Literal, TYPE_CHECKING
12
+
13
+ from ..typesys import (
14
+ TpyType, IntLiteralType, FloatLiteralType, RecordInfo,
15
+ NominalType, PtrType, OwnType, make_array, make_dict, make_set, make_span, make_list, span_as_const, span_as_mutable, PendingListType, ListRepeatType, GenExprType, TupleType,
16
+ TypeParamRef, TypeParamKind, ListLiteralInfo, NoneType, AnyType, OptionalType, UnionType, VoidType,
17
+ ReadonlyType, unwrap_readonly, unwrap_qualifiers, is_any_str_type, PendingStrType, PendingViewType,
18
+ is_any_bytes_type, PendingBytesType,
19
+ make_union,
20
+ ResolvedBinop, FunctionInfo, ParamInfo, UnknownElementType, UNKNOWN_ELEMENT,
21
+ PendingDictType, PendingSetType, DictLiteralInfo,
22
+ resolve_int_literals, CallableType, make_fn_type, is_fn_type,
23
+ INT32, FLOAT, STR, FSTR, STRVIEW, CHAR, BOOL, BIGINT, NONE, BASIC_SLICE, SLICE, BYTES, BYTESVIEW, UINT8,
24
+ is_protocol_type, container_to_str_template, contains_type_param,
25
+ PendingGenericInstanceType, unwrap_ref_type, make_ref, RefType,
26
+ is_integer_type, is_any_int_type, is_union_or_optional_type,
27
+ is_callable_type, is_float_type, is_any_float_type, is_numeric_type,
28
+ unwrap_own, is_readonly_span,
29
+ RecursiveAliasInstanceType, recursive_union_alternatives)
30
+ from ..parse import (
31
+ TpyExpr, TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBytesLiteral,
32
+ TpyFStringValue, TpyFString, FSTRING_CONV_REPR, FSTRING_CONV_STR,
33
+ TpyBoolLiteral,
34
+ TpyNoneLiteral, TpyName, TpyBinOp, TpyChainedCompare, TpyUnaryOp, TpyTypeParamConstruct,
35
+ TpyCall, TpyMethodCall, TpyFieldAccess, TpyFunction,
36
+ is_stable_address_lvalue,
37
+ TpyArrayLiteral, TpyTupleLiteral, TpyDictLiteral, TpySetLiteral, TpyListRepeat,
38
+ TpyListComprehension, TpyDictComprehension, TpySetComprehension, TpyGeneratorExpression, TpyComprehensionGenerator,
39
+ TpySlice, TpySubscript, TpyCoerce,
40
+ TpyIfExpr, TpyNamedExpr, TpyAwait,
41
+ TpyLambda, TpyStarUnpack,
42
+ TpyStmt, TpyVarDecl, TpyTupleUnpack, TpyAssign, TpyForEach, TpyWith,
43
+ TpyNestedDef,
44
+ collect_name_refs,
45
+ )
46
+ from .. import qnames
47
+ from ..type_def_registry import (
48
+ is_set, is_dict, is_array, is_span, is_list,
49
+ is_fixed_int_type, is_big_int_type, is_bool_type, is_char_type, is_fstr_type,
50
+ is_basic_slice_type, is_slice_type,
51
+ int_traits_of,
52
+ is_enum_type, is_int_enum_type, enum_info_of,
53
+ find_factory_by_simple_name, protocol_info_of,
54
+ )
55
+ from ..namespace import BindingKind, NameBinding
56
+ from ..coercions import CoercionContext
57
+ from ..prescan import _expr_to_narrowing_key
58
+ from ..diagnostics import SemanticError, OPTIONAL_NONE_ACCESS_WARNING
59
+ from .. import qnames
60
+ from .context import is_body_like_scope
61
+ from .narrowing import NarrowingTracker, deref_view_narrowed
62
+ from .numeric_lattice import widen_numeric_types
63
+ from .list_literals import IterableHelper
64
+ from .local_deduction import collect_pending_source_types
65
+ from .operators import DUNDER_CPP_TEMPLATES, _substitute_type_params
66
+ from .bound_check import raise_if_class_param_bound_violated
67
+ from .overloads import resolve_overload
68
+
69
+ if TYPE_CHECKING:
70
+ from .context import SemanticContext
71
+ from .type_ops import TypeOperations
72
+ from .operators import OperatorResolver
73
+ from .protocols import ProtocolChecker
74
+ from .compatibility import TypeCompatibility
75
+ from .calls import CallAnalyzer
76
+ from .methods import MethodAnalyzer
77
+ from .scope_tracker import ScopeTracker
78
+
79
+ from tpyc import modules as builtin_modules
80
+
81
+
82
+ def _walk_body_stmts(
83
+ stmts: list[TpyStmt],
84
+ on_expr: CallableFn[[TpyExpr], None],
85
+ on_stmt: CallableFn[[TpyStmt], None],
86
+ ) -> None:
87
+ """Walk statements calling on_expr/on_stmt. Does NOT recurse into TpyNestedDef."""
88
+ for stmt in stmts:
89
+ on_stmt(stmt)
90
+ if isinstance(stmt, TpyNestedDef):
91
+ continue # separate scope
92
+ for expr in stmt.exprs():
93
+ on_expr(expr)
94
+ for body in stmt.sub_bodies():
95
+ _walk_body_stmts(body, on_expr, on_stmt)
96
+
97
+
98
+ def _collect_body_name_refs(stmts: list[TpyStmt]) -> set[str]:
99
+ """Collect all name references from a list of statements.
100
+
101
+ Walks all expressions in statements to find free variable references.
102
+ Does NOT recurse into nested function definitions (separate scope).
103
+ """
104
+ names: set[str] = set()
105
+
106
+ def on_expr(expr: TpyExpr) -> None:
107
+ names.update(collect_name_refs(expr))
108
+
109
+ _walk_body_stmts(stmts, on_expr, lambda s: None)
110
+ return names
111
+
112
+
113
+ def _collect_body_local_defs(stmts: list[TpyStmt]) -> set[str]:
114
+ """Collect names defined locally in a statement body (not from enclosing scope)."""
115
+ defs: set[str] = set()
116
+
117
+ def on_stmt(stmt: TpyStmt) -> None:
118
+ if isinstance(stmt, TpyVarDecl):
119
+ defs.add(stmt.name)
120
+ elif isinstance(stmt, TpyAssign):
121
+ if isinstance(stmt.target, TpyName):
122
+ defs.add(stmt.target.name)
123
+ elif isinstance(stmt, TpyTupleUnpack):
124
+ for name in stmt.targets:
125
+ if name is not None:
126
+ defs.add(name)
127
+ elif isinstance(stmt, TpyForEach):
128
+ defs.add(stmt.var)
129
+ elif isinstance(stmt, TpyWith):
130
+ for item in stmt.items:
131
+ if item.target is not None:
132
+ defs.add(item.target)
133
+ elif isinstance(stmt, TpyNestedDef):
134
+ defs.add(stmt.func.name)
135
+
136
+ _walk_body_stmts(stmts, lambda e: None, on_stmt)
137
+ return defs
138
+
139
+
140
+ def _union_like_members(ut: TpyType) -> 'tuple[TpyType, ...]':
141
+ """The variant alternatives of a recursive-union wrapper -- the non-generic
142
+ UnionType's members or a generic instance's alternatives."""
143
+ if isinstance(ut, UnionType):
144
+ return ut.members
145
+ return recursive_union_alternatives(ut) or ()
146
+
147
+
148
+ def _find_list_member(ut: TpyType) -> NominalType | None:
149
+ """Find the list[...] member of a recursive-union wrapper, if any."""
150
+ for m in _union_like_members(ut):
151
+ if is_list(m):
152
+ return m
153
+ return None
154
+
155
+
156
+ def _find_dict_member(ut: TpyType) -> NominalType | None:
157
+ """Find the dict[...] member of a recursive-union wrapper, if any."""
158
+ for m in _union_like_members(ut):
159
+ if is_dict(m):
160
+ return m
161
+ return None
162
+
163
+
164
+ class ExpressionAnalyzer:
165
+ """Core expression analysis."""
166
+
167
+ def __init__(
168
+ self,
169
+ ctx: SemanticContext,
170
+ type_ops: TypeOperations,
171
+ operators: OperatorResolver,
172
+ protocols: ProtocolChecker,
173
+ compat: TypeCompatibility,
174
+ narrowing: NarrowingTracker,
175
+ calls: CallAnalyzer,
176
+ methods: MethodAnalyzer,
177
+ ):
178
+ self.ctx = ctx
179
+ self.type_ops = type_ops
180
+ self.operators = operators
181
+ self.protocols = protocols
182
+ self.compat = compat
183
+ self.narrowing = narrowing
184
+ self.calls = calls
185
+ self.methods = methods
186
+ self.scopes: ScopeTracker | None = None
187
+
188
+ def set_scopes(self, scopes: ScopeTracker) -> None:
189
+ """Set scope tracker (available once StatementAnalyzer is created)."""
190
+ self.scopes = scopes
191
+
192
+ def _resolve_literal_type(self, t: TpyType) -> TpyType:
193
+ """Replace IntLiteralType/FloatLiteralType with concrete types for display."""
194
+ if isinstance(t, IntLiteralType):
195
+ return self.ctx.default_int_type
196
+ if isinstance(t, FloatLiteralType):
197
+ return FLOAT
198
+ if isinstance(t, TupleType):
199
+ resolved = tuple(self._resolve_literal_type(e) for e in t.element_types)
200
+ if resolved != t.element_types:
201
+ return TupleType(resolved)
202
+ if isinstance(t, PendingListType):
203
+ resolved_elem = self._resolve_literal_type(t.element_type)
204
+ if resolved_elem is not t.element_type:
205
+ return PendingListType(resolved_elem, t.size, t.literal_id)
206
+ return t
207
+
208
+ def _user_type_name(self, t: TpyType) -> str:
209
+ """User-facing type name for error messages (resolves literal types)."""
210
+ return str(self._resolve_literal_type(t))
211
+
212
+ def analyze_call_arg(self, arg: TpyExpr, hint: 'TpyType | None' = None) -> TpyType:
213
+ """Analyze one call argument, transparently handling `*xs` unpack.
214
+
215
+ Every call-argument pre-analyzer (generic inference, overload
216
+ probing, vararg packing -- in both `calls.py` and `methods.py`)
217
+ must route args through here rather than `analyze_expr` directly:
218
+ a `TpyStarUnpack` carries no expression-type semantics of its own,
219
+ so feeding it to the structural analyzer hits the catch-all
220
+ "Unknown expression type" error. For a `*container` arg this
221
+ returns the container's element type (the value an individual
222
+ positional arg would have contributed); for everything else it
223
+ is `analyze_expr[_with_hint]`.
224
+ """
225
+ if isinstance(arg, TpyStarUnpack):
226
+ return self._unpack_star_element_type(arg, hint)
227
+ if hint is not None:
228
+ return self.analyze_expr_with_hint(arg, hint)
229
+ return self.analyze_expr(arg)
230
+
231
+ def _unpack_star_element_type(
232
+ self, node: TpyStarUnpack, elem_hint: 'TpyType | None'
233
+ ) -> TpyType:
234
+ """Element type of the container unpacked by `*node.expr`.
235
+
236
+ `elem_hint`, when present, is the vararg parameter's element type;
237
+ we lift it to `list[elem_hint]` so the inner container literal can
238
+ resolve its own element type from context (mirrors the seeded-hint
239
+ path other args get).
240
+
241
+ Only directly-iterable lvalue containers (list / span / array) are
242
+ accepted -- the same set the vararg-pack codegen can lower via
243
+ `as_mut_span`. A reference-type container *parameter* (e.g.
244
+ `xs: list[T]`) carries a `Ref[...]` wrapper from by-reference passing;
245
+ that is just the borrow form codegen already emits, so unwrap it.
246
+ Owning-rvalue (`Own[list[...]]`) and other wrapped shapes are
247
+ deliberately NOT unwrapped: sema must not accept a shape codegen can't
248
+ emit (the owning-rvalue unpack gap is tracked in TODO.md).
249
+ """
250
+ inner_hint = make_list(elem_hint) if elem_hint is not None else None
251
+ if inner_hint is not None:
252
+ inner_type = self.analyze_expr_with_hint(node.expr, inner_hint)
253
+ else:
254
+ inner_type = self.analyze_expr(node.expr)
255
+ inner_type = unwrap_ref_type(inner_type)
256
+ elem: 'TpyType | None' = None
257
+ if is_array(inner_type) or is_span(inner_type) or is_list(inner_type):
258
+ elem = inner_type.get_element_type()
259
+ elif isinstance(inner_type, PendingListType):
260
+ elem = inner_type.element_type
261
+ if elem is None:
262
+ raise self.ctx.error(
263
+ f"Cannot unpack type '{self._user_type_name(inner_type)}' "
264
+ f"into *args", node)
265
+ return elem
266
+
267
+ def analyze_expr(self, expr: TpyExpr) -> TpyType:
268
+ """Analyze an expression and return its type."""
269
+ if isinstance(expr, TpyIntLiteral):
270
+ typ = IntLiteralType(expr.value)
271
+ elif isinstance(expr, TpyFloatLiteral):
272
+ typ = FloatLiteralType(expr.value)
273
+ elif isinstance(expr, TpyStrLiteral):
274
+ # String literals are always str type (including single-char)
275
+ # Char type is only used when explicitly annotated or from string indexing
276
+ typ = STR
277
+ elif isinstance(expr, TpyBytesLiteral):
278
+ typ = BYTES
279
+ elif isinstance(expr, TpyBoolLiteral):
280
+ typ = BOOL
281
+ elif isinstance(expr, TpyNoneLiteral):
282
+ typ = NONE
283
+ elif isinstance(expr, TpyName):
284
+ typ = self._analyze_name(expr)
285
+ elif isinstance(expr, TpyBinOp):
286
+ typ = self._analyze_binop(expr)
287
+ elif isinstance(expr, TpyChainedCompare):
288
+ typ = self._analyze_chained_compare(expr)
289
+ elif isinstance(expr, TpyUnaryOp):
290
+ typ = self._analyze_unaryop(expr)
291
+ elif isinstance(expr, TpyCall):
292
+ typ = self.calls.analyze_call(expr)
293
+ self.narrowing.invalidate_field_facts_for_call(expr)
294
+ elif isinstance(expr, TpyMethodCall):
295
+ typ = self.methods.analyze_method_call(expr)
296
+ self.narrowing.invalidate_field_facts_for_method_call(expr)
297
+ elif isinstance(expr, TpyFieldAccess):
298
+ typ = self._analyze_field_access(expr)
299
+ elif isinstance(expr, TpyArrayLiteral):
300
+ typ = self._analyze_array_literal(expr)
301
+ elif isinstance(expr, TpyTupleLiteral):
302
+ typ = self._analyze_tuple_literal(expr)
303
+ elif isinstance(expr, TpyDictLiteral):
304
+ typ = self._analyze_dict_literal(expr)
305
+ elif isinstance(expr, TpySetLiteral):
306
+ typ = self._analyze_set_literal(expr)
307
+ elif isinstance(expr, TpyListRepeat):
308
+ typ = self._analyze_list_repeat(expr)
309
+ elif isinstance(expr, TpyListComprehension):
310
+ typ = self._analyze_list_comprehension(expr)
311
+ elif isinstance(expr, TpyDictComprehension):
312
+ typ = self._analyze_dict_comprehension(expr)
313
+ elif isinstance(expr, TpySetComprehension):
314
+ typ = self._analyze_set_comprehension(expr)
315
+ elif isinstance(expr, TpyGeneratorExpression):
316
+ typ = self._analyze_generator_expression(expr)
317
+ elif isinstance(expr, TpySubscript):
318
+ typ = self._analyze_subscript(expr)
319
+ elif isinstance(expr, TpyFString):
320
+ typ = self._analyze_fstring(expr)
321
+ elif isinstance(expr, TpyTypeParamConstruct):
322
+ self.ctx.warning(
323
+ "T() default-construction syntax is not supported in CPython; "
324
+ "use make_default() from tpy instead", expr)
325
+ typ = TypeParamRef(expr.param_name)
326
+ elif isinstance(expr, TpyIfExpr):
327
+ typ = self._analyze_if_expr(expr)
328
+ elif isinstance(expr, TpyNamedExpr):
329
+ typ = self._analyze_named_expr(expr)
330
+ elif isinstance(expr, TpyAwait):
331
+ typ = self._analyze_await(expr)
332
+ elif isinstance(expr, TpyLambda):
333
+ typ = self._analyze_lambda(expr)
334
+ elif isinstance(expr, TpyCoerce):
335
+ # Coercions are attached post-analysis; treat as the expected type.
336
+ typ = expr.expected_type
337
+ elif isinstance(expr, TpyStarUnpack):
338
+ # `*xs` is only meaningful at a variadic-accepting call position,
339
+ # where the handler routes it through `analyze_call_arg` (element
340
+ # extraction) instead of here. Reaching the structural analyzer
341
+ # means the surrounding call target does not accept *args -- reject
342
+ # cleanly rather than falling through to the "Unknown expression
343
+ # type" catch-all (or silently mis-typing the unpack as one arg).
344
+ raise self.ctx.error(
345
+ "Cannot use *unpacking here: the call target does not accept "
346
+ "*args (only a variadic parameter can receive `*iterable`)",
347
+ expr)
348
+ else:
349
+ raise self.ctx.error(f"Unknown expression type: {type(expr).__name__}", expr)
350
+
351
+ self.ctx.set_expr_type(expr, typ)
352
+ return typ
353
+
354
+ def analyze_expr_with_hint(self, expr: TpyExpr, type_hint: TpyType | None) -> TpyType:
355
+ """Analyze an expression with an optional type hint for inference.
356
+
357
+ The type hint allows constructs like list() to infer their type parameters
358
+ from context (e.g., function parameter type).
359
+ """
360
+ if type_hint is None:
361
+ return self.analyze_expr(expr)
362
+
363
+ type_hint = unwrap_ref_type(type_hint)
364
+
365
+ # Lambda with Fn/Callable type hint: infer param types from the hint
366
+ if isinstance(expr, TpyLambda) and is_callable_type(type_hint):
367
+ typ = self._analyze_lambda_with_fn_hint(expr, type_hint)
368
+ self.ctx.set_expr_type(expr, typ)
369
+ return typ
370
+
371
+ # Named function reference with Fn/Callable hint: resolve as function value.
372
+ # Unwrap OwnType so that e.g. list.append(Own[Callable[...]]) works.
373
+ fn_hint = type_hint.wrapped if isinstance(type_hint, OwnType) else type_hint
374
+ if isinstance(expr, TpyName) and is_callable_type(fn_hint):
375
+ result = self._try_resolve_function_ref(expr, fn_hint)
376
+ if result is not None:
377
+ self.ctx.set_expr_type(expr, result)
378
+ return result
379
+
380
+ # Ternary expression: propagate hint to both branches
381
+ if isinstance(expr, TpyIfExpr):
382
+ typ = self._analyze_if_expr(expr, type_hint=type_hint)
383
+ self.ctx.set_expr_type(expr, typ)
384
+ return typ
385
+
386
+ # F-string with FStr hint: keep decomposed for macro consumption.
387
+ # Skip format-validity checks -- the call macro handles per-type dispatch.
388
+ if isinstance(expr, TpyFString) and is_fstr_type(type_hint):
389
+ self._analyze_fstring(expr, for_fstr=True)
390
+ self.ctx.set_expr_type(expr, FSTR)
391
+ return FSTR
392
+
393
+ # T() default-construction: resolves to whatever T maps to
394
+ if isinstance(expr, TpyTypeParamConstruct):
395
+ self.ctx.warning(
396
+ "T() default-construction syntax is not supported in CPython; "
397
+ "use make_default() from tpy instead", expr)
398
+ self.ctx.set_expr_type(expr, type_hint)
399
+ return type_hint
400
+
401
+ # Tuple literal with TupleType hint: pass per-element hints
402
+ if isinstance(expr, TpyTupleLiteral) and isinstance(type_hint, TupleType):
403
+ if len(expr.elements) == len(type_hint.element_types):
404
+ hints = list(type_hint.element_types)
405
+ typ = self._analyze_tuple_literal(expr, element_hints=hints)
406
+ self.ctx.set_expr_type(expr, typ)
407
+ return typ
408
+
409
+ # Check for generic type constructor (list(), Container[T](), etc.)
410
+ _generic_td = (find_factory_by_simple_name(expr.func_name)
411
+ if isinstance(expr, TpyCall) and isinstance(expr.func, TpyName) else None)
412
+ is_generic_constructor = (isinstance(expr, TpyCall) and
413
+ not expr.args and
414
+ expr.call_type is None and
415
+ _generic_td is not None and
416
+ bool(_generic_td.param_kinds))
417
+
418
+ # Check for empty list literal []
419
+ is_empty_literal = isinstance(expr, TpyArrayLiteral) and not expr.elements
420
+
421
+ if is_generic_constructor or is_empty_literal:
422
+ # Unwrap ReadonlyType/OwnType so hints like Own[list[T]] work
423
+ inner_hint = unwrap_readonly(type_hint)
424
+ if isinstance(inner_hint, OwnType):
425
+ inner_hint = inner_hint.wrapped
426
+ # Check if type_hint matches the constructor's generic type
427
+ hint_matches = False
428
+ if is_generic_constructor:
429
+ td = find_factory_by_simple_name(expr.func_name) # type: ignore
430
+ hint_matches = (td is not None and
431
+ inner_hint.qualified_name() == td.qname)
432
+ else:
433
+ # Empty literal [] can match list[T] hint
434
+ hint_matches = is_list(inner_hint)
435
+
436
+ if hint_matches:
437
+ if is_list(inner_hint):
438
+ # list[T]: Use PendingListType for potential Array optimization
439
+ elem_type = inner_hint.get_element_type()
440
+ # Set call_type so codegen generates explicit type (e.g., std::vector<int>())
441
+ if is_generic_constructor:
442
+ expr.call_type = inner_hint # type: ignore
443
+ if self.ctx.func.current_function is None:
444
+ typ = make_list(elem_type)
445
+ else:
446
+ literal_id = self.ctx.literal_counter
447
+ self.ctx.literal_counter += 1
448
+ info = ListLiteralInfo(
449
+ literal_id=literal_id,
450
+ expr=expr,
451
+ element_type=elem_type,
452
+ size=0,
453
+ is_global=self.ctx.is_top_level,
454
+ has_explicit_annotation=True,
455
+ explicit_type=inner_hint
456
+ )
457
+ self.ctx.list_literals[literal_id] = info
458
+ self.ctx.func.pending_resolutions.append(literal_id)
459
+ typ = PendingListType(elem_type, 0, literal_id)
460
+ self.ctx.set_expr_type(expr, typ)
461
+ return typ
462
+ else:
463
+ # Other generic types (Array, etc.): use hint directly
464
+ # Set call_type so codegen knows the concrete template type
465
+ if is_generic_constructor:
466
+ expr.call_type = inner_hint # type: ignore
467
+ self.ctx.set_expr_type(expr, inner_hint)
468
+ return inner_hint
469
+
470
+ # Non-empty array literal with list type hint
471
+ # (e.g. return [x, y] with -> Own[list[T]], or x: list[Int32|None] = [1, None])
472
+ if isinstance(expr, TpyArrayLiteral) and expr.elements:
473
+ inner_hint = unwrap_readonly(type_hint)
474
+ if isinstance(inner_hint, OwnType):
475
+ inner_hint = inner_hint.wrapped
476
+ if is_array(inner_hint) or is_list(inner_hint):
477
+ result = self._analyze_array_literal(expr, inner_hint.get_element_type())
478
+ if isinstance(result, PendingListType):
479
+ info = self.ctx.list_literals.get(result.literal_id)
480
+ if info:
481
+ info.has_explicit_annotation = True
482
+ info.explicit_type = inner_hint
483
+ if info.coerced_element_type is None:
484
+ info.coerced_element_type = inner_hint.get_element_type()
485
+ self.ctx.set_expr_type(expr, result)
486
+ return result
487
+ # Recursive union type with a list member: propagate the union
488
+ # as element hint so nested list literals infer as list[Tree]
489
+ # (e.g. x: Tree = [1, [3, 4]] where type Tree = int | list[Tree])
490
+ if inner_hint.needs_wrapper():
491
+ list_member = _find_list_member(inner_hint)
492
+ if list_member is not None:
493
+ result = self._analyze_array_literal(expr, inner_hint)
494
+ if isinstance(result, PendingListType):
495
+ info = self.ctx.list_literals.get(result.literal_id)
496
+ if info:
497
+ info.has_explicit_annotation = True
498
+ info.explicit_type = list_member
499
+ if info.coerced_element_type is None:
500
+ info.coerced_element_type = inner_hint
501
+ self.ctx.set_expr_type(expr, result)
502
+ return result
503
+
504
+ # List comprehension with list type hint: propagate element type
505
+ if isinstance(expr, TpyListComprehension):
506
+ inner_hint = unwrap_readonly(type_hint)
507
+ if isinstance(inner_hint, OwnType):
508
+ inner_hint = inner_hint.wrapped
509
+ if is_list(inner_hint):
510
+ typ = self._analyze_list_comprehension(expr, expected_elem=inner_hint.get_element_type())
511
+ if isinstance(typ, PendingListType):
512
+ info = self.ctx.list_literals.get(typ.literal_id)
513
+ if info:
514
+ info.has_explicit_annotation = True
515
+ info.explicit_type = inner_hint
516
+ self.ctx.set_expr_type(expr, typ)
517
+ return typ
518
+ if is_array(inner_hint):
519
+ typ = self._analyze_list_comprehension(expr, expected_elem=inner_hint.get_element_type())
520
+ if isinstance(typ, PendingListType):
521
+ info = self.ctx.list_literals.get(typ.literal_id)
522
+ if info:
523
+ info.has_explicit_annotation = True
524
+ info.explicit_type = inner_hint
525
+ self.ctx.set_expr_type(expr, typ)
526
+ return typ
527
+
528
+ # Dict comprehension with dict type hint: propagate key/value types
529
+ if isinstance(expr, TpyDictComprehension):
530
+ inner_hint = unwrap_readonly(type_hint)
531
+ if isinstance(inner_hint, OwnType):
532
+ inner_hint = inner_hint.wrapped
533
+ if is_dict(inner_hint):
534
+ typ = self._analyze_dict_comprehension(
535
+ expr, expected_key=inner_hint.type_args[0],
536
+ expected_value=inner_hint.type_args[1])
537
+ self.ctx.set_expr_type(expr, typ)
538
+ return typ
539
+
540
+ # Dict literal with dict type hint
541
+ if isinstance(expr, TpyDictLiteral):
542
+ inner_hint = unwrap_readonly(type_hint)
543
+ if isinstance(inner_hint, OwnType):
544
+ inner_hint = inner_hint.wrapped
545
+ if is_dict(inner_hint):
546
+ result = self._analyze_dict_literal(expr, inner_hint.type_args[0], inner_hint.type_args[1])
547
+ self.ctx.set_expr_type(expr, result)
548
+ return result
549
+ if inner_hint.needs_wrapper():
550
+ dict_member = _find_dict_member(inner_hint)
551
+ if dict_member is not None:
552
+ result = self._analyze_dict_literal(expr, dict_member.type_args[0], inner_hint)
553
+ self.ctx.set_expr_type(expr, result)
554
+ return result
555
+
556
+ # Set comprehension with set type hint: propagate element type
557
+ if isinstance(expr, TpySetComprehension):
558
+ inner_hint = unwrap_readonly(type_hint)
559
+ if isinstance(inner_hint, OwnType):
560
+ inner_hint = inner_hint.wrapped
561
+ if is_set(inner_hint):
562
+ typ = self._analyze_set_comprehension(
563
+ expr, expected_elem=inner_hint.type_args[0])
564
+ self.ctx.set_expr_type(expr, typ)
565
+ return typ
566
+
567
+ # Non-empty set literal with set type hint
568
+ if isinstance(expr, TpySetLiteral) and expr.elements:
569
+ inner_hint = unwrap_readonly(type_hint)
570
+ if isinstance(inner_hint, OwnType):
571
+ inner_hint = inner_hint.wrapped
572
+ if is_set(inner_hint):
573
+ result = self._analyze_set_literal(expr, inner_hint.type_args[0])
574
+ self.ctx.set_expr_type(expr, result)
575
+ return result
576
+
577
+ # Fall back to regular analysis, propagating hint through context
578
+ # for functions that need it (e.g. unsafe_cast)
579
+ old_hint = self.ctx.expr_type_hint
580
+ self.ctx.expr_type_hint = type_hint
581
+ try:
582
+ return self.analyze_expr(expr)
583
+ finally:
584
+ self.ctx.expr_type_hint = old_hint
585
+
586
+ def _analyze_name(self, expr: TpyName) -> TpyType:
587
+ """Analyze a name reference."""
588
+ # Use-after-consume: variable was consumed by a consuming method call
589
+ if expr.name in self.ctx.func.consumed_vars:
590
+ raise self.ctx.error(
591
+ f"Cannot use '{expr.name}' after it was consumed by a consuming method call",
592
+ expr,
593
+ )
594
+
595
+ # Check for INT type parameter references in generic class context
596
+ # INT type params can be used as values in expressions (e.g., Int32(N))
597
+ if self.ctx.record_ctx.type_params and self.ctx.record_ctx.type_param_kinds:
598
+ try:
599
+ idx = self.ctx.record_ctx.type_params.index(expr.name)
600
+ if self.ctx.record_ctx.type_param_kinds[idx] == TypeParamKind.INT:
601
+ # INT type param - return TypeParamRef with INT kind
602
+ # This represents a compile-time constant, treated as Int32-compatible
603
+ return TypeParamRef(expr.name, kind=TypeParamKind.INT)
604
+ except ValueError:
605
+ pass # Not a type parameter
606
+
607
+ # Use namespace for unified lookup (includes builtins)
608
+ if self.ctx.func.current_ns:
609
+ binding = self.ctx.func.current_ns.lookup(expr.name)
610
+ if binding:
611
+ if binding.kind == BindingKind.VARIABLE:
612
+ self._check_definitely_assigned(expr)
613
+ result = self.narrowing.narrow_name_type(expr.name, binding.type)
614
+ return self._apply_own_wrapper(expr, result)
615
+ if binding.kind == BindingKind.BUILTIN:
616
+ return binding.type
617
+ # For other bindings (FUNCTION, RECORD, MODULE, IMPORTED_NAME),
618
+ # the name exists but isn't usable as a variable
619
+ raise self.ctx.error(f"'{expr.name}' is not a variable", expr)
620
+
621
+ # Migration bridge: scope may contain names not yet in namespace
622
+ # (e.g., during incremental namespace adoption). Remove once all
623
+ # name registration flows go through Namespace.
624
+ typ = self.ctx.func.current_scope.lookup(expr.name)
625
+ if typ is None:
626
+ # Check built-in names (like __name__)
627
+ if expr.name in self.ctx.builtin_names:
628
+ return self.ctx.builtin_names[expr.name]
629
+ # Lazy promotion of loop-scoped variables referenced after the loop
630
+ if self._promote_pending_loop_var(expr.name):
631
+ typ = self.ctx.func.current_scope.lookup(expr.name)
632
+ else:
633
+ raise self.ctx.error(f"Undefined variable: '{expr.name}'", expr)
634
+ self._check_definitely_assigned(expr)
635
+ result = self.narrowing.narrow_name_type(expr.name, typ)
636
+ return self._apply_own_wrapper(expr, result)
637
+
638
+ def _apply_own_wrapper(self, expr: TpyName, result: TpyType) -> TpyType:
639
+ """Derive OwnType wrapping at use time for local variable names.
640
+
641
+ Params keep their FunctionInfo type (Ref/Own is a C++ contract).
642
+ Owned locals (rvalue-init, not ref-returning) get OwnType at
643
+ non-last-use, bare T at last-use (auto-move).
644
+ """
645
+ name = expr.name
646
+ # Params: FunctionInfo type already carries Ref/Own.
647
+ # Reassigned params fall through to local derivation.
648
+ if name in self.ctx.func.current_param_names:
649
+ if name not in self.ctx.func.current_reassigned_vars:
650
+ # Strip Own at last-use for auto-move (same as before)
651
+ if isinstance(result, OwnType) and id(expr) in self.ctx.all_last_uses:
652
+ return result.wrapped
653
+ return result
654
+ # Locals: derive OwnType from owned_locals set.
655
+ # Scope stores bare T; wrap when owned and not at last-use.
656
+ if (not result.is_value_type()
657
+ and not isinstance(result, (OwnType, RefType, NoneType,
658
+ PendingListType, PendingDictType, PendingSetType,
659
+ PendingViewType, PendingGenericInstanceType))
660
+ and name in self.ctx.func.owned_locals
661
+ and name not in self.ctx.func.hoisted_vars
662
+ and name not in self.ctx.func.loop_vars):
663
+ if id(expr) not in self.ctx.all_last_uses:
664
+ return OwnType(result)
665
+ return result
666
+
667
+ def _check_definitely_assigned(self, expr: TpyName) -> None:
668
+ """Check that a local variable is definitely assigned before use."""
669
+ if (not self.ctx.func.init_terminated
670
+ and expr.name in self.ctx.func.var_scope_depth
671
+ and self.ctx.func.var_scope_depth[expr.name] >= 1
672
+ and expr.name not in self.ctx.func.definitely_assigned):
673
+ raise self.ctx.error(
674
+ f"variable '{expr.name}' may not be assigned at this point", expr)
675
+
676
+ def _promote_pending_loop_var(self, name: str) -> bool:
677
+ """Promote a pending for-loop-scoped variable if present.
678
+
679
+ Returns True if the variable was promoted (added to scope and
680
+ definitely_assigned, registered for codegen pre-declaration).
681
+ """
682
+ pending = self.ctx.func.pending_loop_vars.pop(name, None)
683
+ if pending is None:
684
+ return False
685
+ var_type, loop_stmt, orig_stmt = pending
686
+ self.ctx.func.current_scope.define(name, var_type)
687
+ self.ctx.func.definitely_assigned.add(name)
688
+ # Register for codegen pre-declaration
689
+ decls = self.ctx.if_branch_decls.setdefault(id(loop_stmt), {})
690
+ decls[name] = var_type
691
+ # Mark the original for-loop's var for hoisted codegen (hidden counter)
692
+ if isinstance(orig_stmt, TpyForEach) and name == orig_stmt.var:
693
+ orig_stmt.hoist_loop_var = True
694
+ return True
695
+
696
+ def _normalize_pending_container(self, t: TpyType) -> TpyType:
697
+ """Normalize a pending container type to a concrete type with resolved IntLiteralType elements.
698
+
699
+ Used in or/and/ternary type comparison: two PendingListType literals with the same
700
+ element type but different IDs (or different IntLiteralType values) are compatible.
701
+ """
702
+ if isinstance(t, PendingListType):
703
+ return resolve_int_literals(make_list(t.element_type), self.ctx.default_int_for_literal)
704
+ if isinstance(t, PendingDictType):
705
+ k = self.ctx.default_int_for_literal(t.key_type) if isinstance(t.key_type, IntLiteralType) else t.key_type
706
+ k = FLOAT if isinstance(k, FloatLiteralType) else k
707
+ v = self.ctx.default_int_for_literal(t.value_type) if isinstance(t.value_type, IntLiteralType) else t.value_type
708
+ v = FLOAT if isinstance(v, FloatLiteralType) else v
709
+ return make_dict(k, v)
710
+ if isinstance(t, PendingSetType):
711
+ elem = self.ctx.default_int_for_literal(t.element_type) if isinstance(t.element_type, IntLiteralType) else t.element_type
712
+ elem = FLOAT if isinstance(elem, FloatLiteralType) else elem
713
+ return make_set(elem)
714
+ return t
715
+
716
+ def _logical_op_result_type(self, left: TpyType, right: TpyType) -> TpyType:
717
+ """Determine result type for and/or operators.
718
+
719
+ Python semantics: `x and y` returns an operand, not bool.
720
+ Same non-bool type -> return that type (enables value-context usage).
721
+ Different types or bool operands -> return bool.
722
+ """
723
+ # Bool operands: C++ &&/|| already correct
724
+ if is_bool_type(left) or is_bool_type(right):
725
+ return BOOL
726
+ # Resolve int literals to match concrete int type on the other side
727
+ if isinstance(left, IntLiteralType):
728
+ if is_integer_type(right):
729
+ left = right
730
+ elif isinstance(right, IntLiteralType):
731
+ return self.ctx.default_int_type
732
+ else:
733
+ return BOOL
734
+ elif isinstance(right, IntLiteralType):
735
+ if is_integer_type(left):
736
+ right = left
737
+ else:
738
+ return BOOL
739
+ # Normalize PendingViewType to its owned type for comparison; preserve
740
+ # the pending type when both sides are pending so view deduction can
741
+ # chain the result variable back to the operands' resolution.
742
+ if isinstance(left, PendingViewType) and isinstance(right, PendingViewType) and left.family is right.family:
743
+ return left
744
+ if isinstance(left, PendingViewType):
745
+ left = left.family.owned_type
746
+ if isinstance(right, PendingViewType):
747
+ right = right.family.owned_type
748
+ # Normalize pending container types to concrete types for equality comparison.
749
+ # Two PendingListType literals with the same element type (but different IDs
750
+ # or different IntLiteralType values like 1 vs 3) are compatible.
751
+ # Caller marks source literals via collect_pending_source_types.
752
+ left = self._normalize_pending_container(left)
753
+ right = self._normalize_pending_container(right)
754
+ if left == right:
755
+ return left
756
+ return BOOL
757
+
758
+ def _analyze_binop(self, expr: TpyBinOp) -> TpyType:
759
+ """Analyze a binary operation."""
760
+ left_type = self.analyze_expr(expr.left)
761
+ if expr.op in ("&&", "||"):
762
+ type_true, type_false = self.narrowing.condition_type_facts(expr.left)
763
+ saved_types = dict(self.ctx.func.narrowed_types)
764
+ # Save definitely_assigned: RHS may not execute due to short-circuit
765
+ saved_assigned = frozenset(self.ctx.func.definitely_assigned)
766
+ if expr.op == "&&":
767
+ self.ctx.func.narrowed_types.update(type_true)
768
+ else:
769
+ self.ctx.func.narrowed_types.update(type_false)
770
+ try:
771
+ right_type = self.analyze_expr(expr.right)
772
+ finally:
773
+ self.ctx.func.narrowed_types = saved_types
774
+ # Track walrus vars introduced in RHS (short-circuit conditional)
775
+ rhs_walrus = self.ctx.func.definitely_assigned - saved_assigned
776
+ if rhs_walrus:
777
+ if expr.op == "&&":
778
+ self.ctx.sc_and_walrus |= rhs_walrus
779
+ else:
780
+ self.ctx.sc_or_walrus |= rhs_walrus
781
+ # Rollback: RHS walrus vars are not definitely assigned
782
+ self.ctx.func.definitely_assigned = set(saved_assigned)
783
+ else:
784
+ right_type = self.analyze_expr(expr.right)
785
+
786
+ # Preserve declared Optional/Union type for identity checks when flow
787
+ # narrowing resolved an expression to its inner type.
788
+ if expr.op in ("is", "is not"):
789
+ declared_left = self.narrowing.declared_type_for_expr(expr.left)
790
+ if is_union_or_optional_type(declared_left):
791
+ left_type = declared_left
792
+ declared_right = self.narrowing.declared_type_for_expr(expr.right)
793
+ if is_union_or_optional_type(declared_right):
794
+ right_type = declared_right
795
+
796
+ # Enforce Pythonic None identity checks for Optional values.
797
+ # `x == None` / `x != None` on Optional values should use `is` / `is not`.
798
+ if expr.op in ("==", "!="):
799
+ left_is_none = isinstance(left_type, NoneType)
800
+ right_is_none = isinstance(right_type, NoneType)
801
+ if left_is_none or right_is_none:
802
+ other = right_type if left_is_none else left_type
803
+ if isinstance(other, OptionalType):
804
+ raise self.ctx.error(
805
+ "Use 'is None' / 'is not None' for Optional None checks "
806
+ "(not '==' / '!=')",
807
+ expr,
808
+ )
809
+ if isinstance(other, PtrType):
810
+ raise self.ctx.error(
811
+ "Use 'is None' / 'is not None' for pointer None checks "
812
+ "(not '==' / '!=')",
813
+ expr,
814
+ )
815
+ if isinstance(other, UnionType) and other.has_none_member():
816
+ raise self.ctx.error(
817
+ "Use 'is None' / 'is not None' for union None checks "
818
+ "(not '==' / '!=')",
819
+ expr,
820
+ )
821
+ raise self.ctx.error(
822
+ f"Cannot compare {left_type} and {right_type} with '{expr.op}'",
823
+ expr,
824
+ )
825
+
826
+ # Unwrap OwnType/RefType -- ownership/references don't affect operator resolution
827
+ left_effective = unwrap_ref_type(left_type)
828
+ left_effective = left_effective.wrapped if isinstance(left_effective, OwnType) else left_effective
829
+ right_effective = unwrap_ref_type(right_type)
830
+ right_effective = right_effective.wrapped if isinstance(right_effective, OwnType) else right_effective
831
+ # Value optionals in operator expressions use runtime null checks unless
832
+ # flow already proved non-None for the specific expression.
833
+ warned_optional_operator = False
834
+ if expr.op not in ("is", "is not", "&&", "||", "in", "not in", "==", "!="):
835
+ if isinstance(left_effective, OptionalType) and left_effective.inner.is_value_type():
836
+ left_effective = left_effective.inner
837
+ warned_optional_operator = True
838
+ if isinstance(right_effective, OptionalType) and right_effective.inner.is_value_type():
839
+ right_effective = right_effective.inner
840
+ warned_optional_operator = True
841
+ if warned_optional_operator:
842
+ self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
843
+
844
+ # For ==/!=, unwrap Optional value-types only for operator resolution
845
+ # (no warning -- C++ std::optional handles None comparison natively)
846
+ if expr.op in ("==", "!="):
847
+ if isinstance(left_effective, OptionalType) and left_effective.inner.is_value_type():
848
+ left_effective = left_effective.inner
849
+ expr.optional_safe_eq = True
850
+ if isinstance(right_effective, OptionalType) and right_effective.inner.is_value_type():
851
+ right_effective = right_effective.inner
852
+ expr.optional_safe_eq = True
853
+
854
+ # Helper to check if type is any numeric type
855
+ def is_numeric_type(t: TpyType) -> bool:
856
+ return is_any_int_type(t) or is_any_float_type(t)
857
+
858
+ # Identity operators (is / is not) -- only valid with None or enums
859
+ if expr.op in ("is", "is not"):
860
+ # Unwrap RefType, ReadonlyType, and OwnType for nullable checks.
861
+ left_check = unwrap_ref_type(unwrap_readonly(left_type))
862
+ if isinstance(left_check, OwnType):
863
+ left_check = left_check.wrapped
864
+ right_check = unwrap_ref_type(unwrap_readonly(right_type))
865
+ if isinstance(right_check, OwnType):
866
+ right_check = right_check.wrapped
867
+ # Enum identity: lower to ==/!=
868
+ if is_enum_type(left_check) and is_enum_type(right_check):
869
+ if left_check.name == right_check.name:
870
+ expr.op = "==" if expr.op == "is" else "!="
871
+ return BOOL
872
+ raise self.ctx.error(
873
+ f"Cannot compare enum types '{left_check.name}' and '{right_check.name}'",
874
+ expr,
875
+ )
876
+ # Bool literal identity: lower to ==/!=
877
+ if is_bool_type(left_check) and isinstance(expr.right, TpyBoolLiteral):
878
+ expr.op = "==" if expr.op == "is" else "!="
879
+ return BOOL
880
+ if is_bool_type(right_check) and isinstance(expr.left, TpyBoolLiteral):
881
+ expr.op = "==" if expr.op == "is" else "!="
882
+ return BOOL
883
+ nullable_types = (OptionalType, PtrType)
884
+ if isinstance(left_check, NoneType) and isinstance(right_check, nullable_types):
885
+ return BOOL
886
+ if isinstance(right_check, NoneType) and isinstance(left_check, nullable_types):
887
+ return BOOL
888
+ if isinstance(left_check, NoneType) and isinstance(right_check, NoneType):
889
+ return BOOL
890
+ # Nullable unions: v is None / v is not None
891
+ if isinstance(right_check, NoneType) and isinstance(left_check, UnionType) and left_check.has_none_member():
892
+ return BOOL
893
+ if isinstance(left_check, NoneType) and isinstance(right_check, UnionType) and right_check.has_none_member():
894
+ return BOOL
895
+ # Any vs None: typeid-based check (D15). Other RHS forms of
896
+ # `is` on Any are still rejected -- see the Future Extensions
897
+ # row in docs/ANY_TYPE_DESIGN.md.
898
+ if isinstance(right_check, NoneType) and isinstance(left_check, AnyType):
899
+ return BOOL
900
+ if isinstance(left_check, NoneType) and isinstance(right_check, AnyType):
901
+ return BOOL
902
+ raise self.ctx.error(
903
+ f"'is' / 'is not' can only compare Optional/Ptr/union types with None, "
904
+ f"got {left_type} and {right_type}",
905
+ expr,
906
+ )
907
+
908
+ # Enum operators: base Enum supports == and != only;
909
+ # IntEnum also supports ordering, comparison with integers, and arithmetic
910
+ if is_enum_type(left_effective) or is_enum_type(right_effective):
911
+ left_is_int_enum = is_int_enum_type(left_effective)
912
+ right_is_int_enum = is_int_enum_type(right_effective)
913
+ is_comparison = expr.op in ("==", "!=", "<", ">", "<=", ">=")
914
+
915
+ if is_comparison:
916
+ # IntEnum vs integer: coerce enum to underlying type
917
+ if left_is_int_enum and is_any_int_type(right_effective):
918
+ expr.int_enum_coercion = left_effective
919
+ return BOOL
920
+ if right_is_int_enum and is_any_int_type(left_effective):
921
+ expr.int_enum_coercion = right_effective
922
+ return BOOL
923
+ if is_enum_type(left_effective) and is_enum_type(right_effective):
924
+ if left_effective.name != right_effective.name:
925
+ raise self.ctx.error(
926
+ f"Cannot compare enum types '{left_effective.name}' and '{right_effective.name}'",
927
+ expr,
928
+ )
929
+ # IntEnum supports ordering; base Enum does not
930
+ if expr.op in ("<", ">", "<=", ">=") and not left_is_int_enum:
931
+ raise self.ctx.error(
932
+ f"Ordering operators not supported for enum type '{left_effective.name}'",
933
+ expr,
934
+ )
935
+ # IntEnum ordering needs cast to underlying type
936
+ if left_is_int_enum and expr.op in ("<", ">", "<=", ">="):
937
+ expr.int_enum_coercion = left_effective
938
+ return BOOL
939
+ # One side is enum, other is not (and not int for IntEnum)
940
+ enum_name = left_effective.name if is_enum_type(left_effective) else right_effective.name
941
+ raise self.ctx.error(
942
+ f"Cannot compare '{enum_name}' with '{right_effective if is_enum_type(left_effective) else left_effective}'",
943
+ expr,
944
+ )
945
+
946
+ # Non-comparison ops: IntEnum arithmetic is handled below;
947
+ # base Enum in arithmetic is an error
948
+ if not left_is_int_enum and not right_is_int_enum:
949
+ enum_name = left_effective.name if is_enum_type(left_effective) else right_effective.name
950
+ raise self.ctx.error(
951
+ f"Operator '{expr.op}' not supported for enum type '{enum_name}'",
952
+ expr,
953
+ )
954
+
955
+ # Tuple comparison: ==, !=, <, <=, >, >= with element-wise validation
956
+ if isinstance(left_effective, TupleType) or isinstance(right_effective, TupleType):
957
+ # Both shapes (membership in a tuple literal `x in (a, b, c)` and
958
+ # tuple-as-key `key in dict`) are handled by the in/not-in section
959
+ # below; fall through here.
960
+ if expr.op in ("in", "not in"):
961
+ pass
962
+ elif expr.op in ("==", "!=", "<", "<=", ">", ">="):
963
+ if not (isinstance(left_effective, TupleType) and isinstance(right_effective, TupleType)):
964
+ raise self.ctx.error(
965
+ f"Cannot compare {left_effective} with {right_effective}",
966
+ expr,
967
+ )
968
+ if len(left_effective.element_types) != len(right_effective.element_types):
969
+ raise self.ctx.error(
970
+ f"Cannot compare tuples of different lengths: "
971
+ f"{left_effective} vs {right_effective}",
972
+ expr,
973
+ )
974
+ for i, (lt, rt) in enumerate(zip(
975
+ left_effective.element_types, right_effective.element_types
976
+ )):
977
+ if lt != rt:
978
+ try:
979
+ self.compat.check_type_compatible(lt, rt, "tuple comparison", source_expr=expr)
980
+ except SemanticError:
981
+ try:
982
+ self.compat.check_type_compatible(rt, lt, "tuple comparison", source_expr=expr)
983
+ except SemanticError:
984
+ raise self.ctx.error(
985
+ f"Cannot compare tuple element {i}: "
986
+ f"{lt} vs {rt}",
987
+ expr,
988
+ )
989
+ # For ordering ops, validate that element types support the operator
990
+ if expr.op in ("<", "<=", ">", ">="):
991
+ self._validate_comparison(expr, lt, rt)
992
+ return BOOL
993
+ else:
994
+ raise self.ctx.error(
995
+ f"Operator '{expr.op}' is not supported for tuple types",
996
+ expr,
997
+ )
998
+
999
+ # Comparison operators return Bool
1000
+ if expr.op in ("==", "!=", "<", ">", "<=", ">="):
1001
+ # Mixed-sign fixed-int comparison is well-defined under C++'s usual
1002
+ # arithmetic conversions (the signed operand is reinterpreted as
1003
+ # unsigned), but the result rarely matches user intent on negative
1004
+ # values. Codegen routes these through std::cmp_* so the answer is
1005
+ # mathematically correct; warn here so the user can choose to
1006
+ # cast explicitly if they care about readability. Skipped when
1007
+ # either side is still literal-seeded -- retro-widening may yet
1008
+ # resolve the operand to a compatible type.
1009
+ if (not self._is_literal_seed_operand(expr.left)
1010
+ and not self._is_literal_seed_operand(expr.right)
1011
+ and is_fixed_int_type(left_effective)
1012
+ and is_fixed_int_type(right_effective)):
1013
+ lt = int_traits_of(left_effective)
1014
+ rt = int_traits_of(right_effective)
1015
+ if lt is not None and rt is not None and lt.signed != rt.signed:
1016
+ self.ctx.warning(
1017
+ f"comparison between signed and unsigned integer types "
1018
+ f"('{left_effective}' and '{right_effective}'); "
1019
+ f"cast one operand to make the type intent explicit",
1020
+ expr,
1021
+ )
1022
+ # Validate that user record types support the comparison
1023
+ self._validate_comparison(expr, left_effective, right_effective)
1024
+ # Resolve comparison method (__eq__, __lt__, etc.) for codegen.
1025
+ if result := self.operators.resolve_binop(left_effective, expr.op, right_effective, loc_node=expr):
1026
+ expr.resolved_binop = result
1027
+ elif expr.op == "!=":
1028
+ # No __ne__: fall back to negated __eq__ when the method
1029
+ # can't use raw C++ != (e.g. native freestanding function).
1030
+ if result := self.operators.resolve_binop(left_effective, "==", right_effective, loc_node=expr):
1031
+ if result.method.native_function:
1032
+ expr.resolved_binop = result
1033
+ return BOOL
1034
+
1035
+ # Membership operators (in, not in) return Bool
1036
+ if expr.op in ("in", "not in"):
1037
+ right_type = unwrap_ref_type(right_type)
1038
+ if isinstance(right_type, OwnType):
1039
+ right_type = right_type.wrapped
1040
+ # TypedDict: "key" in td -> compile-time field presence check
1041
+ right_record = self.ctx.registry.get_record_for_type(right_type)
1042
+ if right_record and right_record.is_typed_dict:
1043
+ if not isinstance(expr.left, TpyStrLiteral):
1044
+ raise self.ctx.error(
1045
+ f"TypedDict '{right_type.name}' membership test requires a string literal key",
1046
+ expr.left)
1047
+ key = expr.left.value
1048
+ for fld in right_record.fields:
1049
+ if fld.name == key:
1050
+ expr.typed_dict_in_field = key
1051
+ expr.typed_dict_in_always_true = not right_record.is_total_false
1052
+ return BOOL
1053
+ raise self.ctx.error(
1054
+ f"TypedDict '{right_type.name}' has no key '{key}'", expr.left)
1055
+ if right_record:
1056
+ contains_overloads = right_record.get_method_overloads("__contains__")
1057
+ if contains_overloads:
1058
+ # Drop int-kind type args (e.g. N in Array[T, N]) --
1059
+ # _substitute_type_params only acts on TypeParamRef -> TpyType.
1060
+ type_subst = {
1061
+ k: v for k, v in self.type_ops.build_type_substitution(right_type).items()
1062
+ if isinstance(v, TpyType)
1063
+ }
1064
+ # Substitute type params for generic containers
1065
+ subst_overloads = contains_overloads
1066
+ if type_subst:
1067
+ subst_overloads = [
1068
+ dc_replace(m, params=[
1069
+ ParamInfo(p.name, _substitute_type_params(p.type, type_subst))
1070
+ for p in m.params
1071
+ ]) for m in contains_overloads
1072
+ ]
1073
+ # Resolve IntLiteralType: use param type if the literal fits,
1074
+ # otherwise fall back to default int type
1075
+ check_left = left_type
1076
+ if isinstance(check_left, IntLiteralType):
1077
+ for m in subst_overloads:
1078
+ pt = m.params[0].type
1079
+ pt_tr = int_traits_of(pt)
1080
+ if (pt_tr is not None
1081
+ and pt_tr.min_value <= check_left.value <= pt_tr.max_value):
1082
+ check_left = pt
1083
+ break
1084
+ else:
1085
+ check_left = self.ctx.default_int_for_literal(check_left)
1086
+ matched = resolve_overload(subst_overloads, [check_left])
1087
+ if matched is not None:
1088
+ # Map back to the original (un-substituted) method for codegen
1089
+ idx = subst_overloads.index(matched)
1090
+ original_method = contains_overloads[idx]
1091
+ raise_if_class_param_bound_violated(
1092
+ original_method, right_record.type_params, type_subst,
1093
+ self.protocols.type_conforms_to_protocol,
1094
+ self.ctx.error, expr,
1095
+ )
1096
+ expr.resolved_contains = original_method
1097
+ return BOOL
1098
+ # No __contains__ overload matched. Defer to
1099
+ # check_type_compatible: it raises for genuine mismatches
1100
+ # (preserving int-literal range diagnostics) and accepts
1101
+ # compatible coercions, in which case control falls
1102
+ # through to the outer iterable-membership path.
1103
+ int_overload = None
1104
+ if isinstance(left_type, IntLiteralType):
1105
+ for o in subst_overloads:
1106
+ if int_traits_of(o.params[0].type) is not None:
1107
+ int_overload = o
1108
+ break
1109
+ if int_overload is not None or len(subst_overloads) == 1:
1110
+ param_type = (int_overload or subst_overloads[0]).params[0].type
1111
+ self.compat.check_type_compatible(
1112
+ left_type, param_type,
1113
+ f"membership test (expected {param_type})",
1114
+ loc=expr.loc,
1115
+ )
1116
+ else:
1117
+ expected_str = " or ".join(
1118
+ str(o.params[0].type) for o in subst_overloads)
1119
+ raise self.ctx.error(
1120
+ f"Type mismatch in membership test: "
1121
+ f"expected {expected_str}, got {left_type}",
1122
+ expr,
1123
+ )
1124
+ # Tuple literal membership: x in (1, 2, 3) -> x == 1 || x == 2 || x == 3
1125
+ if isinstance(right_type, TupleType) and isinstance(expr.right, TpyTupleLiteral):
1126
+ for et in right_type.element_types:
1127
+ if not self.compat.is_type_compatible(left_type, et) \
1128
+ and not self.compat.is_type_compatible(et, left_type):
1129
+ raise self.ctx.error(
1130
+ f"Tuple element type '{et}' is not compatible "
1131
+ f"with membership test type '{left_type}'",
1132
+ expr)
1133
+ return BOOL
1134
+ # Right side must be iterable (intrinsically or via NativeIterable protocol)
1135
+ helper = IterableHelper(self.ctx)
1136
+ if helper.is_type_iterable(right_type):
1137
+ # For string containers, LHS must be str or Char
1138
+ if is_any_str_type(right_type):
1139
+ if not (is_any_str_type(left_type) or is_char_type(left_type)):
1140
+ raise SemanticError(
1141
+ f"Cannot check '{left_type}' membership in str (expected str or Char)",
1142
+ expr.loc
1143
+ )
1144
+ else:
1145
+ # Non-string collections use std::find which requires ==
1146
+ elem_type = right_type.get_element_type()
1147
+ if elem_type is not None:
1148
+ equatable = NominalType("Equatable", is_protocol=True)
1149
+ if not self.protocols.type_conforms_to_protocol(elem_type, equatable):
1150
+ raise self.ctx.error(
1151
+ f"'in' requires element type '{elem_type}' to "
1152
+ f"conform to 'Equatable' (no '__eq__' method)",
1153
+ expr)
1154
+ return BOOL
1155
+ raise self.ctx.error(f"Cannot use '{expr.op}' with non-iterable type {right_type}", expr)
1156
+
1157
+ # Logical operators: Python semantics returns an operand, not bool.
1158
+ # Same non-bool type -> return that type; otherwise -> bool.
1159
+ if expr.op in ("&&", "||"):
1160
+ result = self._logical_op_result_type(left_type, right_type)
1161
+ if is_list(result):
1162
+ # Both ternary branches must share a C++ type; force sources to
1163
+ # ListType so they don't independently become incompatible Arrays.
1164
+ for t in collect_pending_source_types(self.ctx, expr):
1165
+ if isinstance(t, PendingListType):
1166
+ info = self.ctx.list_literals.get(t.literal_id)
1167
+ if info is not None:
1168
+ info.needs_list_type = True
1169
+ return result
1170
+
1171
+ # IntEnum arithmetic: coerce to underlying type, delegate to standard binop
1172
+ if expr.op in ("+", "-", "*", "//", "%"):
1173
+ int_enum_side = None
1174
+ other_side = None
1175
+ if is_int_enum_type(left_effective):
1176
+ int_enum_side = left_effective
1177
+ other_side = right_effective
1178
+ elif is_int_enum_type(right_effective):
1179
+ int_enum_side = right_effective
1180
+ other_side = left_effective
1181
+ if int_enum_side is not None:
1182
+ if is_int_enum_type(other_side):
1183
+ if other_side.name != int_enum_side.name:
1184
+ raise self.ctx.error(
1185
+ f"Cannot mix arithmetic between '{int_enum_side.name}' "
1186
+ f"and '{other_side.name}'",
1187
+ expr,
1188
+ )
1189
+ if (is_int_enum_type(other_side) or isinstance(other_side, IntLiteralType)
1190
+ or is_fixed_int_type(other_side) or is_big_int_type(other_side)):
1191
+ # Coerce IntEnum operands to underlying type so standard
1192
+ # FixedInt binop resolution (with checked arithmetic) handles it
1193
+ expr.int_enum_coercion = int_enum_side
1194
+ if is_int_enum_type(left_effective):
1195
+ left_effective = enum_info_of(left_effective).underlying_type
1196
+ if is_int_enum_type(right_effective):
1197
+ right_effective = enum_info_of(right_effective).underlying_type
1198
+ # Fall through to standard binop resolution below
1199
+
1200
+ # IntLiteral + IntLiteral -> IntLiteral (stays unresolved until context determines type)
1201
+ if isinstance(left_effective, IntLiteralType) and isinstance(right_effective, IntLiteralType):
1202
+ # Keep Python-style true division semantics for all-literal integer
1203
+ # expressions regardless of default-int setting.
1204
+ if expr.op == "div":
1205
+ if result := self.operators.resolve_binop(BIGINT, expr.op, BIGINT, loc_node=expr):
1206
+ expr.resolved_binop = result
1207
+ return result.method.return_type
1208
+ return FLOAT
1209
+ literal_result = self._try_eval_int_literal_binop(expr.op, left_effective.value, right_effective.value)
1210
+ resolved_int = (
1211
+ self.ctx.default_int_for_literal(IntLiteralType(literal_result))
1212
+ if literal_result is not None
1213
+ else self.ctx.default_int_type
1214
+ )
1215
+ # Still resolve for codegen (bitwise ops need cpp template).
1216
+ if result := self.operators.resolve_binop(resolved_int, expr.op, resolved_int, loc_node=expr):
1217
+ expr.resolved_binop = result
1218
+ return IntLiteralType(literal_result)
1219
+
1220
+ # Protocol-typed operands - look up the dunder method in the protocol
1221
+ # For Self in protocols, Self binds to the protocol itself when used as a value type
1222
+ if is_protocol_type(left_effective):
1223
+ method_name = builtin_modules.BINOP_TO_METHOD.get(expr.op)
1224
+ if method_name:
1225
+ return_type = self.protocols.lookup_protocol_method_return(
1226
+ left_effective,
1227
+ method_name,
1228
+ [right_effective],
1229
+ )
1230
+ if return_type is not None:
1231
+ return return_type
1232
+
1233
+ # Arithmetic/bitwise operators - use registry
1234
+ if result := self.operators.resolve_binop(left_effective, expr.op, right_effective, loc_node=expr):
1235
+ expr.resolved_binop = result
1236
+ # Check if divisor is provably non-zero for div/mod elision
1237
+ if expr.op in ("//", "%"):
1238
+ self._check_divisor_non_zero(expr)
1239
+ # List concat produces a list -- mark pending literals as mutated
1240
+ if is_list(result.method.return_type):
1241
+ self._mark_list_concat_operands_mutated(expr, left_effective, right_effective)
1242
+ return result.method.return_type
1243
+
1244
+ # Record types (user-defined or module) with dunder methods
1245
+ if isinstance(left_effective, NominalType) and left_effective.is_record:
1246
+ method_name = builtin_modules.BINOP_TO_METHOD.get(expr.op)
1247
+ if method_name:
1248
+ record = self.ctx.registry.get_record_for_type(left_effective)
1249
+ if record and (method := record.get_method(method_name)):
1250
+ # Check parameter count and type
1251
+ if len(method.params) == 1:
1252
+ _, param_type = method.params[0]
1253
+ # Substitute type params for generic types (e.g. list[T].__add__(list[T]))
1254
+ type_subst = self.type_ops.build_type_substitution(left_effective)
1255
+ if type_subst:
1256
+ param_type = self.type_ops.substitute_types(param_type, type_subst)
1257
+ if param_type == right_effective:
1258
+ raise_if_class_param_bound_violated(
1259
+ method, record.type_params, type_subst,
1260
+ self.protocols.type_conforms_to_protocol,
1261
+ self.ctx.error, expr,
1262
+ )
1263
+ ret_type = method.return_type
1264
+ if type_subst:
1265
+ ret_type = self.type_ops.substitute_types(ret_type, type_subst)
1266
+ # Build ResolvedBinop so codegen uses the method's
1267
+ # cpp_template instead of raw C++ operator syntax.
1268
+ cpp = method.cpp_template
1269
+ if not cpp and not method.native_function:
1270
+ cpp = DUNDER_CPP_TEMPLATES.get(method_name)
1271
+ resolved_method = FunctionInfo(
1272
+ name=method_name,
1273
+ params=list(method.params),
1274
+ return_type=ret_type,
1275
+ cpp_template=cpp,
1276
+ native_name=method.native_name,
1277
+ native_function=method.native_function,
1278
+ is_method=True,
1279
+ owning_type_qname=method.owning_type_qname,
1280
+ )
1281
+ expr.resolved_binop = ResolvedBinop(
1282
+ method=resolved_method,
1283
+ left_wrapper="{expr}",
1284
+ right_wrapper="{expr}",
1285
+ receiver_type=left_effective,
1286
+ )
1287
+ return ret_type
1288
+
1289
+ raise SemanticError(
1290
+ f"Invalid operand types for '{expr.op}': {left_type} and {right_type}",
1291
+ expr.loc,
1292
+ )
1293
+
1294
+ def _check_subscript_bounds_safe(self, expr: TpySubscript) -> None:
1295
+ """Set bounds_safe when the index is provably in [0, len(obj))."""
1296
+ index = expr.index
1297
+ obj = expr.obj
1298
+ if not isinstance(index, TpyName) or not isinstance(obj, TpyName):
1299
+ return
1300
+ index_range = self.ctx.func.value_ranges.get(index.name)
1301
+ is_safe = (
1302
+ index_range is not None
1303
+ and index_range.is_non_negative()
1304
+ and index_range.is_bounded_by_len(obj.name)
1305
+ )
1306
+ expr.bounds_safe = is_safe
1307
+ if expr.loc:
1308
+ self.ctx.subscript_bounds_facts[(expr.loc.line, obj.name)] = is_safe
1309
+
1310
+ def _check_divisor_non_zero(self, expr: TpyBinOp) -> None:
1311
+ """Set divisor_non_zero when the divisor is provably != 0."""
1312
+ right = expr.right
1313
+ if isinstance(right, TpyIntLiteral):
1314
+ is_safe = right.value != 0
1315
+ expr.divisor_non_zero = is_safe
1316
+ return
1317
+ if not isinstance(right, TpyName):
1318
+ return
1319
+ divisor_range = self.ctx.func.value_ranges.get(right.name)
1320
+ is_safe = divisor_range is not None and divisor_range.non_zero
1321
+ expr.divisor_non_zero = is_safe
1322
+ if expr.loc:
1323
+ self.ctx.div_zero_facts[(expr.loc.line, right.name)] = is_safe
1324
+
1325
+ def _mark_list_concat_operands_mutated(
1326
+ self, expr: TpyBinOp, left_type: TpyType, right_type: TpyType,
1327
+ ) -> None:
1328
+ """Mark PendingListType operands as mutated so they resolve to list, not Array."""
1329
+ for sub_expr, sub_type in ((expr.left, left_type), (expr.right, right_type)):
1330
+ if isinstance(sub_type, PendingListType):
1331
+ info = self.ctx.list_literals.get(sub_type.literal_id)
1332
+ if info:
1333
+ info.is_mutated = True
1334
+ elif isinstance(sub_expr, TpyName):
1335
+ var_name = sub_expr.name
1336
+ if var_name in self.ctx.func.variable_to_literal:
1337
+ lit_id = self.ctx.func.variable_to_literal[var_name]
1338
+ info = self.ctx.list_literals.get(lit_id)
1339
+ if info:
1340
+ info.is_mutated = True
1341
+
1342
+ def _try_eval_int_literal_binop(self, op: str, left: int | None, right: int | None) -> int | None:
1343
+ """Best-effort constant evaluation for int literal binops."""
1344
+ if left is None or right is None:
1345
+ return None
1346
+ try:
1347
+ if op == "+":
1348
+ return left + right
1349
+ if op == "-":
1350
+ return left - right
1351
+ if op == "*":
1352
+ return left * right
1353
+ if op == "//":
1354
+ if right == 0:
1355
+ return None
1356
+ return left // right
1357
+ if op == "%":
1358
+ if right == 0:
1359
+ return None
1360
+ return left % right
1361
+ if op == "**":
1362
+ if right < 0 or right > 10000:
1363
+ return None
1364
+ return left ** right
1365
+ if op == "<<":
1366
+ if right < 0 or right > 10000:
1367
+ return None
1368
+ return left << right
1369
+ if op == ">>":
1370
+ if right < 0:
1371
+ return None
1372
+ return left >> right
1373
+ if op == "&":
1374
+ return left & right
1375
+ if op == "|":
1376
+ return left | right
1377
+ if op == "^":
1378
+ return left ^ right
1379
+ except (OverflowError, ValueError):
1380
+ return None
1381
+ return None
1382
+
1383
+ def _analyze_chained_compare(self, expr: TpyChainedCompare) -> TpyType:
1384
+ """Analyze a chained comparison (a < b < c, etc.)."""
1385
+ pairs: list[TpyBinOp] = []
1386
+ prev = expr.left
1387
+ for op, comp in zip(expr.ops, expr.comparators):
1388
+ pair = TpyBinOp(prev, op, comp, loc=expr.loc)
1389
+ self.analyze_expr(pair)
1390
+ pairs.append(pair)
1391
+ prev = comp
1392
+ expr.pairs = pairs
1393
+ return BOOL
1394
+
1395
+ def _analyze_unaryop(self, expr: TpyUnaryOp) -> TpyType:
1396
+ """Analyze a unary operation."""
1397
+ operand_type = self.analyze_expr(expr.operand)
1398
+ effective_type = unwrap_ref_type(operand_type)
1399
+ if isinstance(effective_type, OwnType):
1400
+ effective_type = effective_type.wrapped
1401
+
1402
+ # Value optionals in unary arithmetic/bitwise ops use runtime checks
1403
+ # unless flow already narrowed them to non-Optional.
1404
+ if expr.op in ("-", "~") and isinstance(operand_type, OptionalType) and operand_type.inner.is_value_type():
1405
+ effective_type = operand_type.inner
1406
+ self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
1407
+
1408
+ # Logical not: validate operand type (Bool, numeric, Optional, or types with __bool__/__len__)
1409
+ if expr.op == "!":
1410
+ if (is_bool_type(effective_type)
1411
+ or is_any_int_type(effective_type)
1412
+ or is_any_float_type(effective_type)
1413
+ or isinstance(effective_type, OptionalType)
1414
+ or isinstance(effective_type, AnyType)
1415
+ or is_enum_type(effective_type)):
1416
+ return BOOL
1417
+ record = self.ctx.registry.get_record_for_type(effective_type)
1418
+ if record and (record.get_method_overloads("__bool__")
1419
+ or record.get_method_overloads("__len__")):
1420
+ return BOOL
1421
+ raise self.ctx.error(f"Invalid operand type for 'not': {effective_type} (expected bool, numeric, or type with __bool__/__len__)", expr)
1422
+
1423
+ # Float types support unary negation and plus
1424
+ if is_any_float_type(effective_type):
1425
+ if expr.op in ("-", "+"):
1426
+ if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
1427
+ expr.resolved_unaryop = result
1428
+ return effective_type
1429
+
1430
+ # IntEnum: unary negation returns the underlying integer type
1431
+ if is_int_enum_type(effective_type) and expr.op == "-":
1432
+ return enum_info_of(effective_type).underlying_type
1433
+
1434
+ # IntLiteralType special cases - preserve literal nature when possible
1435
+ if isinstance(effective_type, IntLiteralType):
1436
+ if expr.op == "-":
1437
+ # Still resolve for codegen (needs cpp template)
1438
+ if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
1439
+ expr.resolved_unaryop = result
1440
+ neg = -effective_type.value if effective_type.value is not None else None
1441
+ return IntLiteralType(neg)
1442
+ if expr.op == "+":
1443
+ if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
1444
+ expr.resolved_unaryop = result
1445
+ return effective_type
1446
+ if expr.op == "~":
1447
+ # Bitwise not on literal - treat as Int32
1448
+ # Still resolve for codegen
1449
+ if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
1450
+ expr.resolved_unaryop = result
1451
+ return INT32
1452
+
1453
+ # Use registry for unary operators
1454
+ if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
1455
+ expr.resolved_unaryop = result
1456
+ return result.method.return_type
1457
+
1458
+ raise self.ctx.error(f"Invalid operand type for unary '{expr.op}': {operand_type}", expr)
1459
+
1460
+ def _is_user_record_type(self, typ: TpyType) -> bool:
1461
+ """Check if a type is a user-defined record (not a builtin container)."""
1462
+ return isinstance(typ, NominalType) and typ.is_record and typ.is_user_record
1463
+
1464
+ def _is_literal_seed_operand(self, operand: TpyExpr) -> bool:
1465
+ """True when ``operand`` is still pending literal-driven type resolution.
1466
+
1467
+ Two cases: an analyzer-level ``IntLiteralType`` (the operand is itself a
1468
+ literal), or a ``TpyName`` whose local is in ``literal_default_vars``
1469
+ (literal-seeded local awaiting retro-widen). Used to suppress the
1470
+ mixed-sign-comparison warning before sema has finalised the type --
1471
+ the operand may yet resolve to a same-sign type.
1472
+ """
1473
+ if isinstance(self.ctx.get_expr_type(operand), IntLiteralType):
1474
+ return True
1475
+ return (isinstance(operand, TpyName)
1476
+ and operand.name in self.ctx.func.literal_default_vars)
1477
+
1478
+ def _validate_comparison(self, expr: TpyBinOp, left_type: TpyType, right_type: TpyType) -> None:
1479
+ """Error when comparing user record types that lack the relevant dunder."""
1480
+ # Only check when at least one side is a user record
1481
+ if not self._is_user_record_type(left_type) and not self._is_user_record_type(right_type):
1482
+ return
1483
+ # Determine which dunder to check
1484
+ COMPARE_OP_TO_DUNDER = {
1485
+ "==": "__eq__", "!=": "__ne__",
1486
+ "<": "__lt__", "<=": "__le__",
1487
+ ">": "__gt__", ">=": "__ge__",
1488
+ }
1489
+ dunder = COMPARE_OP_TO_DUNDER.get(expr.op)
1490
+ if not dunder:
1491
+ return
1492
+ # Check the left side (operator dispatch goes left to right)
1493
+ check_type = left_type if self._is_user_record_type(left_type) else right_type
1494
+ record = self.ctx.registry.get_record_for_type(check_type)
1495
+ if record:
1496
+ # Use lookup that walks the inheritance chain
1497
+ overloads, _ = self.protocols.lookup_record_method_overloads(record, dunder)
1498
+ has_dunder = bool(overloads)
1499
+ # != is valid if __eq__ is defined (C++ generates != from ==)
1500
+ if not has_dunder and dunder == "__ne__":
1501
+ overloads, _ = self.protocols.lookup_record_method_overloads(record, "__eq__")
1502
+ has_dunder = bool(overloads)
1503
+ if not has_dunder:
1504
+ if dunder == "__ne__":
1505
+ msg = (f"Comparison '!=' on '{check_type}': "
1506
+ f"no '__ne__' or '__eq__' method defined")
1507
+ else:
1508
+ msg = (f"Comparison '{expr.op}' on '{check_type}': "
1509
+ f"no '{dunder}' method defined")
1510
+ self.ctx.emit_error(msg, expr)
1511
+
1512
+ def get_deref_target_type(self, typ: TpyType, is_readonly: bool = False) -> TpyType | None:
1513
+ """If typ has __deref__(), return resolved return type. Else None."""
1514
+ return self.type_ops.get_deref_target_type(typ, is_readonly=is_readonly)
1515
+
1516
+ def _try_find_field(self, typ: TpyType, expr: TpyFieldAccess) -> TpyType | None:
1517
+ """Try to find a field on typ. Returns field type or None."""
1518
+ if is_basic_slice_type(typ):
1519
+ if expr.field in ("start", "stop"):
1520
+ return OptionalType(INT32)
1521
+ return None
1522
+ if is_slice_type(typ):
1523
+ if expr.field in ("start", "stop", "step"):
1524
+ return OptionalType(INT32)
1525
+ return None
1526
+
1527
+ if isinstance(typ, NominalType) and typ.is_record:
1528
+ record = self.ctx.registry.get_record_for_type(typ)
1529
+ if not record:
1530
+ return None
1531
+ # Multi-base same-name ambiguity: when the child doesn't declare
1532
+ # the field itself and more than one direct-parent branch reaches
1533
+ # it, unqualified access could silently pick the first hit --
1534
+ # reject instead so the user disambiguates via `BaseN.field`.
1535
+ child_owns_field = any(f.name == expr.field for f in record.fields)
1536
+ if not child_owns_field and len(record.parents) > 1:
1537
+ branches = self.protocols.find_field_parent_branches(record, expr.field)
1538
+ if len(branches) > 1:
1539
+ names = ", ".join(branches)
1540
+ first, second = branches[0], branches[1]
1541
+ raise self.ctx.error(
1542
+ f"Ambiguous field '{expr.field}' inherited from {{{names}}} "
1543
+ f"in '{record.name}'; use '{first}.{expr.field}' "
1544
+ f"or '{second}.{expr.field}'",
1545
+ expr,
1546
+ )
1547
+ type_subst = self.type_ops.build_type_substitution(typ)
1548
+ field_info = self.protocols.lookup_record_field(record, expr.field)
1549
+ if field_info:
1550
+ field_type = field_info.type
1551
+ if type_subst:
1552
+ field_type = self.type_ops.substitute_type_params(field_type, type_subst)
1553
+ return field_type
1554
+ # Check properties (getter access)
1555
+ prop = self.protocols.lookup_record_property(record, expr.field)
1556
+ if prop is not None:
1557
+ expr.is_property_access = True
1558
+ # Construct a TpyMethodCall so codegen can delegate to normal method path
1559
+ getter_call = TpyMethodCall(obj=expr.obj, method=expr.field, args=[])
1560
+ getter_call.resolved_function_info = prop.getter
1561
+ expr.property_getter_call = getter_call
1562
+ prop_type = prop.type
1563
+ if type_subst:
1564
+ prop_type = self.type_ops.substitute_type_params(prop_type, type_subst)
1565
+ return prop_type
1566
+ # Class constant fallback for instance-side reads (`obj.X`,
1567
+ # `self.X`); codegen emits the declaring class's qualified name
1568
+ # regardless of which class the user accessed through.
1569
+ return self._lookup_class_constant_owner(record, expr)
1570
+
1571
+ if isinstance(typ, TypeParamRef):
1572
+ bound = self.type_ops.get_type_param_bound(typ.name)
1573
+ if bound is not None and is_protocol_type(bound):
1574
+ protocol_info = protocol_info_of(bound)
1575
+ if protocol_info:
1576
+ for field_name, field_type in protocol_info.fields or []:
1577
+ if field_name == expr.field:
1578
+ type_subst: dict[str, TpyType] = {"Self": typ}
1579
+ if protocol_info.type_params and bound.type_args:
1580
+ type_subst.update(dict(zip(protocol_info.type_params, bound.type_args)))
1581
+ resolved = self.type_ops.substitute_types(field_type, type_subst)
1582
+ return resolved
1583
+ raise self.ctx.error(f"Protocol '{bound.name}' has no field '{expr.field}'", expr)
1584
+
1585
+ return None
1586
+
1587
+ def _resolve_nested_type_access(self, parent_name: str, field: str,
1588
+ expr: TpyFieldAccess) -> TpyType | None:
1589
+ """Check if parent_name.field is a nested enum or record. Returns type or None."""
1590
+ dotted = f"{parent_name}.{field}"
1591
+ nested_enum = self.ctx.registry.get_enum(dotted)
1592
+ if nested_enum is not None:
1593
+ return nested_enum
1594
+ nested_record = self.ctx.registry.get_record(dotted)
1595
+ if nested_record is not None:
1596
+ return NominalType(dotted)
1597
+ return None
1598
+
1599
+ def _resolve_nested_chain(self, expr: TpyFieldAccess) -> tuple[str, TpyType] | None:
1600
+ """Resolve a chain of field accesses to a nested type (e.g., Outer.Mid.Inner).
1601
+
1602
+ Returns (dotted_name, resolved_type) or None if not a nested type chain.
1603
+ """
1604
+ if isinstance(expr.obj, TpyName):
1605
+ if self.ctx.func.current_ns:
1606
+ binding = self.ctx.func.current_ns.lookup(expr.obj.name)
1607
+ if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
1608
+ dotted = f"{expr.obj.name}.{expr.field}"
1609
+ nested_enum = self.ctx.registry.get_enum(dotted)
1610
+ if nested_enum is not None:
1611
+ return (dotted, nested_enum)
1612
+ nested_record = self.ctx.registry.get_record(dotted)
1613
+ if nested_record is not None:
1614
+ return (dotted, NominalType(dotted))
1615
+ elif isinstance(expr.obj, TpyFieldAccess):
1616
+ parent = self._resolve_nested_chain(expr.obj)
1617
+ if parent is not None:
1618
+ dotted_parent, parent_type = parent
1619
+ if isinstance(parent_type, NominalType):
1620
+ dotted = f"{dotted_parent}.{expr.field}"
1621
+ nested_enum = self.ctx.registry.get_enum(dotted)
1622
+ if nested_enum is not None:
1623
+ return (dotted, nested_enum)
1624
+ nested_record = self.ctx.registry.get_record(dotted)
1625
+ if nested_record is not None:
1626
+ return (dotted, NominalType(dotted))
1627
+ return None
1628
+
1629
+ def _lookup_class_constant_owner(
1630
+ self, record: RecordInfo, expr: TpyFieldAccess,
1631
+ ) -> TpyType | None:
1632
+ """Resolve `expr.field` against `record.class_constants`, walking
1633
+ `mro_ancestors` to the declaring ancestor when not declared directly.
1634
+ Sets `expr.class_constant_owner` to the *declaring* record so codegen
1635
+ emits `<declaring_qname>::<member>` regardless of the access path.
1636
+ """
1637
+ owner = self.ctx.registry.find_class_constant_owner(record, expr.field)
1638
+ if owner is None:
1639
+ return None
1640
+ # Multi-base same-name ambiguity: mirror the instance-field check in
1641
+ # `_try_find_field` so C3 linearization doesn't silently pick one
1642
+ # branch when more than one parent contributes the constant.
1643
+ if owner is not record and len(record.parents) > 1:
1644
+ branches = self.protocols.find_class_constant_parent_branches(record, expr.field)
1645
+ if len(branches) > 1:
1646
+ names = ", ".join(branches)
1647
+ first, second = branches[0], branches[1]
1648
+ raise self.ctx.error(
1649
+ f"Ambiguous class constant '{expr.field}' inherited from {{{names}}} "
1650
+ f"in '{record.name}'; use '{first}.{expr.field}' "
1651
+ f"or '{second}.{expr.field}'",
1652
+ expr,
1653
+ )
1654
+ # Phase 9: a class constant on a generic class is per-instantiation
1655
+ # in C++ (`C<T>::X`), so codegen needs the concrete (or template-scope)
1656
+ # type args at the access site. We get them from the receiver's type
1657
+ # only when the receiver is a direct instance of the generic owner.
1658
+ # Inheritance with fixed type-args (`class Child(C[Int32]): pass;
1659
+ # obj: Child; obj.X`) loses the args at the receiver-record level
1660
+ # and is deferred -- reject for now with a clear hint.
1661
+ if owner.type_params and owner is not record:
1662
+ raise self.ctx.error(
1663
+ f"class constant '{expr.field}' on generic class "
1664
+ f"'{owner.name}' cannot be accessed through subclass "
1665
+ f"'{record.name}'; access through an instance of "
1666
+ f"'{owner.name}[...]' instead",
1667
+ expr,
1668
+ )
1669
+ expr.class_constant_owner = owner
1670
+ return owner.class_constants[expr.field].type
1671
+
1672
+ def _try_class_constant_access(
1673
+ self, expr: TpyFieldAccess, binding: NameBinding,
1674
+ ) -> TpyType | None:
1675
+ """Resolve `<RecordName>.<field>` against the record's class_constants
1676
+ (with MRO walk via `_lookup_class_constant_owner`).
1677
+ """
1678
+ assert isinstance(expr.obj, TpyName)
1679
+ record_info: RecordInfo | None = None
1680
+ if binding.kind == BindingKind.RECORD:
1681
+ record_info = self.ctx.registry.get_record(expr.obj.name)
1682
+ elif binding.kind == BindingKind.IMPORTED_NAME:
1683
+ import_info = self.ctx.imported_names.get(expr.obj.name)
1684
+ if import_info:
1685
+ record_info = self.ctx.registry.find_record_by_qname(
1686
+ f"{import_info[0]}.{import_info[1]}")
1687
+ if record_info is None:
1688
+ return None
1689
+ return self._class_constant_access_on_record(expr, record_info)
1690
+
1691
+ def _class_constant_access_on_record(
1692
+ self, expr: TpyFieldAccess, record_info: RecordInfo,
1693
+ ) -> TpyType | None:
1694
+ """Class-constant resolution given a pre-resolved record. Shared by the
1695
+ bare-name path (`Foo.CONST`) and the module-qualified path
1696
+ (`m.Foo.CONST`)."""
1697
+ # Phase 9: bare-class access on a generic class can't render the
1698
+ # parameterized qname (no type args at the access site), so reject
1699
+ # and point the user at instance access. `Class[Int32].X` syntax
1700
+ # for class-level access on a parameterized generic is not yet
1701
+ # supported either.
1702
+ if record_info.type_params and expr.field in record_info.class_constants:
1703
+ raise self.ctx.error(
1704
+ f"cannot access class constant '{expr.field}' on generic "
1705
+ f"class '{record_info.name}' through the bare class name; "
1706
+ f"access through an instance instead "
1707
+ f"(e.g. `{record_info.name}[T_args]().{expr.field}`)",
1708
+ expr,
1709
+ )
1710
+ return self._lookup_class_constant_owner(record_info, expr)
1711
+
1712
+ def _try_unbound_self_field_access(
1713
+ self, expr: TpyFieldAccess, binding: NameBinding,
1714
+ ) -> TpyType | None:
1715
+ """Handle `BaseN.field` accessing an ancestor subobject's field.
1716
+
1717
+ Returns the resolved field type, or None when the access is not an
1718
+ unbound-self form (not inside an instance method, or BaseN doesn't
1719
+ resolve to a known record) so the caller can fall through to the
1720
+ regular field-access path.
1721
+ """
1722
+ assert isinstance(expr.obj, TpyName)
1723
+ current_rec_parse = self.ctx.record_ctx.record
1724
+ current_fn = self.ctx.func.current_function
1725
+ if (current_rec_parse is None
1726
+ or current_fn is None
1727
+ or not isinstance(current_fn, TpyFunction)
1728
+ or not current_fn.is_method
1729
+ or current_fn.is_staticmethod):
1730
+ return None
1731
+ current_rec = self.ctx.registry.get_record(current_rec_parse.name)
1732
+ if current_rec is None:
1733
+ return None
1734
+
1735
+ record_info = None
1736
+ if binding.kind == BindingKind.RECORD:
1737
+ record_info = self.ctx.registry.get_record(expr.obj.name)
1738
+ elif binding.kind == BindingKind.IMPORTED_NAME:
1739
+ import_info = self.ctx.imported_names.get(expr.obj.name)
1740
+ if import_info:
1741
+ record_info = self.ctx.registry.find_record_by_qname(
1742
+ f"{import_info[0]}.{import_info[1]}")
1743
+ if record_info is None:
1744
+ return None
1745
+
1746
+ # Walk BaseN's own MRO so `B.foo` resolves a field B inherits from
1747
+ # its own parent, matching Python's `B.foo` semantics.
1748
+ field_info = self.protocols.lookup_record_field(record_info, expr.field)
1749
+ if field_info is None:
1750
+ return None
1751
+
1752
+ # BaseN has the field but isn't an ancestor: the user clearly meant
1753
+ # unbound-self, so emit a targeted error rather than letting the
1754
+ # caller fall through to a generic "can't treat class as value".
1755
+ if not self.ctx.registry.is_subclass_of_record(current_rec, record_info):
1756
+ raise self.ctx.error(
1757
+ f"'{record_info.name}' is not an ancestor of '{current_rec.name}'; "
1758
+ f"cannot access '{record_info.name}.{expr.field}' here",
1759
+ expr,
1760
+ )
1761
+
1762
+ parent_type, type_subst = self.protocols.resolve_ancestor_instantiation(
1763
+ current_rec, record_info)
1764
+ field_type = field_info.type
1765
+ if type_subst:
1766
+ field_type = self.type_ops.substitute_type_params(field_type, type_subst)
1767
+
1768
+ # Readonly self propagates into non-value reads so writes through
1769
+ # the result are rejected and references come back const.
1770
+ if current_fn.is_readonly and not field_type.is_value_type():
1771
+ if isinstance(field_type, PtrType) and not field_type.is_readonly:
1772
+ field_type = field_type.as_const()
1773
+ elif is_span(field_type) and not is_readonly_span(field_type):
1774
+ field_type = span_as_const(field_type)
1775
+ elif not isinstance(field_type, ReadonlyType):
1776
+ field_type = ReadonlyType(field_type)
1777
+
1778
+ expr.unbound_self_parent_type = parent_type
1779
+ return make_ref(field_type)
1780
+
1781
+ def _analyze_field_access(self, expr: TpyFieldAccess) -> TpyType:
1782
+ """Analyze a field access."""
1783
+ # Check for module variable access (e.g., sys.argv)
1784
+ if isinstance(expr.obj, TpyName):
1785
+ if self.ctx.func.current_ns:
1786
+ binding = self.ctx.func.current_ns.lookup(expr.obj.name)
1787
+ if binding and binding.kind == BindingKind.MODULE:
1788
+ # Get actual module name (may differ from local name for aliased imports)
1789
+ module_name = binding.import_source[0] if binding.import_source else expr.obj.name
1790
+ module_info = self.ctx.registry.get_module(module_name)
1791
+ if module_info and expr.field in module_info.variables:
1792
+ return module_info.variables[expr.field].type
1793
+ # If not a variable, let it fall through to error at the end
1794
+ # (method calls are handled in _analyze_method_call)
1795
+
1796
+ # Enum type-level member access: Color.Red -> EnumType
1797
+ if binding and binding.kind == BindingKind.ENUM:
1798
+ enum_type = binding.enum_type
1799
+ if expr.field in enum_info_of(enum_type).members:
1800
+ return enum_type
1801
+ raise self.ctx.error(
1802
+ f"Enum '{enum_type.name}' has no member '{expr.field}'", expr)
1803
+
1804
+ # Nested type access on a record: Container.Kind, Container.Inner
1805
+ # Also check IMPORTED_NAME that resolves to a record (cross-module)
1806
+ if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
1807
+ nested = self._resolve_nested_type_access(expr.obj.name, expr.field, expr)
1808
+ if nested is not None:
1809
+ return nested
1810
+ unbound = self._try_unbound_self_field_access(expr, binding)
1811
+ if unbound is not None:
1812
+ return unbound
1813
+ class_const = self._try_class_constant_access(expr, binding)
1814
+ if class_const is not None:
1815
+ return class_const
1816
+
1817
+ # Module-qualified class member access: m.Foo.CONST, pkg.sub.Foo.CONST.
1818
+ # Mirrors the method-call dispatcher's _try_resolve_module_qualified_class.
1819
+ # Codegen reads class_constant_owner (set inside the helper) to emit the
1820
+ # full C++ qname; the syntactic receiver shape doesn't matter from there.
1821
+ if isinstance(expr.obj, TpyFieldAccess):
1822
+ resolved = self.methods._try_resolve_module_qualified_class(expr.obj)
1823
+ if resolved is not None:
1824
+ _module_name, _class_short, record_info = resolved
1825
+ class_const = self._class_constant_access_on_record(expr, record_info)
1826
+ if class_const is not None:
1827
+ return class_const
1828
+
1829
+ # `import pkg.sub` + `pkg.sub.X`: walk the chain to recover a dotted
1830
+ # module name and treat the leaf as a variable on that module.
1831
+ # Mirrors the method-call form that already works via
1832
+ # MethodAnalyzer._try_resolve_dotted_module.
1833
+ if isinstance(expr.obj, TpyFieldAccess):
1834
+ dotted_name = self.methods._try_resolve_dotted_module(expr.obj)
1835
+ if dotted_name is not None:
1836
+ module_info = self.ctx.registry.get_module(dotted_name)
1837
+ if module_info is not None and expr.field in module_info.variables:
1838
+ expr.module_var_access = (dotted_name, expr.field)
1839
+ return module_info.variables[expr.field].type
1840
+
1841
+ # Handle chained nested type access: Outer.Mid.Inner.field
1842
+ if isinstance(expr.obj, TpyFieldAccess):
1843
+ chain = self._resolve_nested_chain(expr.obj)
1844
+ if chain is not None:
1845
+ # chain is a (dotted_name, type) for the intermediate nested type
1846
+ dotted_name, chain_type = chain
1847
+ if is_enum_type(chain_type):
1848
+ if expr.field in enum_info_of(chain_type).members:
1849
+ return chain_type
1850
+ if expr.field in ("name", "value"):
1851
+ pass # fall through to normal field access
1852
+ else:
1853
+ raise self.ctx.error(
1854
+ f"Enum '{chain_type.name}' has no member '{expr.field}'", expr)
1855
+ elif isinstance(chain_type, NominalType):
1856
+ # Try further nesting
1857
+ nested = self._resolve_nested_type_access(dotted_name, expr.field, expr)
1858
+ if nested is not None:
1859
+ return nested
1860
+
1861
+ obj_type = self.analyze_expr(expr.obj)
1862
+
1863
+ # Unwrap transparent wrappers
1864
+ is_readonly_obj = isinstance(obj_type, ReadonlyType)
1865
+ actual_type = unwrap_ref_type(obj_type)
1866
+ if isinstance(actual_type, ReadonlyType):
1867
+ actual_type = actual_type.wrapped
1868
+ if isinstance(actual_type, OwnType):
1869
+ actual_type = actual_type.wrapped
1870
+ if isinstance(actual_type, OptionalType):
1871
+ if actual_type.inner.is_value_type():
1872
+ raise self.ctx.error(f"Cannot access field '{expr.field}' on type {obj_type}", expr)
1873
+ self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
1874
+ expr.needs_optional_runtime_check = True
1875
+ actual_type = actual_type.inner
1876
+
1877
+ # Pending generic instance: field access is not allowed until resolved
1878
+ if isinstance(actual_type, PendingGenericInstanceType):
1879
+ raise self.ctx.error(
1880
+ f"Cannot access field '{expr.field}' on '{actual_type.record_name}' "
1881
+ f"until its type arguments are resolved; call a constraining method first "
1882
+ f"or add explicit type arguments to the constructor",
1883
+ expr,
1884
+ )
1885
+
1886
+ # Enum instance property access: c.name -> str, c.value -> underlying type
1887
+ if is_enum_type(actual_type):
1888
+ if expr.field == "name":
1889
+ return STR
1890
+ elif expr.field == "value":
1891
+ return enum_info_of(actual_type).underlying_type
1892
+ raise self.ctx.error(
1893
+ f"Enum value of type '{actual_type.name}' has no attribute '{expr.field}'. "
1894
+ f"Use '{actual_type.name}.{expr.field}' to access enum members", expr)
1895
+
1896
+ # Deref chain loop -- resolves through Ptr (mutable and readonly) and any Deref[T] type
1897
+ current_type = actual_type
1898
+ deref_depth = 0
1899
+ while deref_depth <= 8:
1900
+ result = self._try_find_field(current_type, expr)
1901
+ if result is not None:
1902
+ # Class constants emit `<owner_qname>::<member>` ignoring
1903
+ # `obj`, so deref state, ptr-non-null narrowing,
1904
+ # ownership-from-self, and field-path narrowing don't apply.
1905
+ if expr.class_constant_owner is not None:
1906
+ return make_ref(result)
1907
+ expr.deref_depth = deref_depth
1908
+ if deref_depth > 0 and isinstance(actual_type, PtrType):
1909
+ obj_key = _expr_to_narrowing_key(expr.obj)
1910
+ if obj_key is not None:
1911
+ if obj_key in self.ctx.func.non_null_ptr_vars:
1912
+ expr.ptr_non_null = True
1913
+ if expr.loc:
1914
+ self.ctx.ptr_deref_facts[
1915
+ (expr.loc.line, obj_key)
1916
+ ] = expr.ptr_non_null
1917
+ # Post-access narrowing: a successful deref here means
1918
+ # the pointer is non-null for *subsequent statements*;
1919
+ # queued for flush at statement boundary rather than
1920
+ # applied immediately to avoid unsafe elision between
1921
+ # unspecified-order siblings in the same expression.
1922
+ self.ctx.func.pending_non_null_ptr_vars.add(obj_key)
1923
+ # Propagate readonly: accessing a non-value field through a
1924
+ # readonly reference yields a readonly result.
1925
+ # Ptr[T] fields become Ptr[readonly[T]], Span[T] -> Span[readonly[T]].
1926
+ if is_readonly_obj:
1927
+ if isinstance(result, PtrType) and not result.is_readonly:
1928
+ result = result.as_const()
1929
+ elif is_span(result) and not is_readonly_span(result):
1930
+ result = span_as_const(result)
1931
+ elif not result.is_value_type():
1932
+ result = ReadonlyType(unwrap_readonly(result))
1933
+ # Ownership propagation: in a consuming method (self: Own[Self]),
1934
+ # self.field yields Own[FieldType] since the struct is being consumed.
1935
+ if (self.ctx.in_consuming_method
1936
+ and not is_readonly_obj
1937
+ and isinstance(expr.obj, TpyName) and expr.obj.name == "self"
1938
+ and not result.is_value_type()):
1939
+ result = OwnType(result)
1940
+ # Apply field path narrowing (e.g. after `if obj.field is not None:`)
1941
+ field_key = _expr_to_narrowing_key(expr)
1942
+ if field_key is not None:
1943
+ narrowed = self.ctx.func.narrowed_types.get(field_key)
1944
+ if narrowed is not None:
1945
+ result = narrowed
1946
+ return make_ref(result)
1947
+
1948
+ deref_target = self.get_deref_target_type(
1949
+ current_type, is_readonly=is_readonly_obj)
1950
+ if deref_target is None:
1951
+ break
1952
+ # __deref__() may return readonly[T]; unwrap and propagate
1953
+ # readonly so field access enforces const semantics.
1954
+ if isinstance(deref_target, ReadonlyType):
1955
+ is_readonly_obj = True
1956
+ deref_target = deref_target.wrapped
1957
+ # Deref-view narrowing: a field declared only on the narrowed
1958
+ # subclass resolves through the cast (see the method-call path).
1959
+ nsub = deref_view_narrowed(self.ctx, expr.obj, deref_target)
1960
+ if nsub is not None:
1961
+ expr.deref_narrowed_to = nsub
1962
+ current_type = nsub
1963
+ else:
1964
+ current_type = deref_target
1965
+ deref_depth += 1
1966
+
1967
+ # D16 dynamic-attribute fallback: if the receiver is a record with
1968
+ # __getattr__ reachable via MRO, route the access through the dunder
1969
+ # as a synthesized method call (parallel to property routing). Fires
1970
+ # only after the static-resolution + deref loop has failed.
1971
+ if isinstance(actual_type, NominalType) and actual_type.is_record:
1972
+ dyn_result = self._try_dyn_getattr(actual_type, expr)
1973
+ if dyn_result is not None:
1974
+ return make_ref(dyn_result)
1975
+ raise self.ctx.error(f"Record '{actual_type.name}' has no field '{expr.field}'", expr)
1976
+ raise self.ctx.error(f"Cannot access field '{expr.field}' on type {obj_type}", expr)
1977
+
1978
+ def _try_dyn_getattr(self, typ: NominalType, expr: TpyFieldAccess) -> TpyType | None:
1979
+ """D16 Phase 1: try routing `obj.field` through __getattr__.
1980
+
1981
+ Returns the dunder's substituted return type (and stashes the
1982
+ synthesized TpyMethodCall on `expr.dyn_getattr_call`) if the
1983
+ receiver's class has `__getattr__` via MRO; None otherwise.
1984
+ """
1985
+ record = self.ctx.registry.get_record_for_type(typ)
1986
+ if record is None:
1987
+ return None
1988
+ overloads, _ = self.protocols.lookup_record_method_overloads(
1989
+ record, "__getattr__")
1990
+ if not overloads:
1991
+ return None
1992
+ # Delegate to method-call analysis so @readonly enforcement, mutation
1993
+ # propagation, and call-edge recording all fire uniformly. Returns
1994
+ # the (substituted) dunder return type. Bypassing this path was the
1995
+ # original D16 v1 review gap.
1996
+ getter_call = TpyMethodCall(
1997
+ obj=expr.obj,
1998
+ method="__getattr__",
1999
+ args=[TpyStrLiteral(value=expr.field)],
2000
+ loc=expr.loc,
2001
+ )
2002
+ ret_type = self.analyze_expr(getter_call)
2003
+ expr.dyn_getattr_call = getter_call
2004
+ return ret_type
2005
+
2006
+ def _analyze_array_literal(
2007
+ self, expr: TpyArrayLiteral, expected_elem: TpyType | None = None
2008
+ ) -> TpyType:
2009
+ """Analyze an array literal [expr, expr, ...]
2010
+
2011
+ In function-local contexts, returns a PendingListType that will be
2012
+ resolved to Array or list based on usage (mutation, parameter passing).
2013
+ In global/module context, returns ListType directly.
2014
+
2015
+ Args:
2016
+ expected_elem: When provided (from a type annotation or return type hint),
2017
+ each element is checked against this type instead of against the first
2018
+ element. Enables mixed-type literals like [Int32(1), None] when the
2019
+ annotation is list[Int32 | None].
2020
+ """
2021
+ if not expr.elements:
2022
+ if not is_body_like_scope(self.ctx.func.current_function):
2023
+ raise self.ctx.error("Empty array literal requires explicit type annotation", expr)
2024
+ # Empty list with no annotation -- create PendingListType with unknown
2025
+ # element type. The element type will be inferred from subsequent usage
2026
+ # (e.g. .append(v), xs[i] = v, param context, return context).
2027
+
2028
+ # Reuse an existing PendingListType for this expr if one was
2029
+ # already created. analyze_expr can be called multiple times for
2030
+ # the same arg expr (pre-overload arg-type collection, then post-
2031
+ # overload _typecheck_call_args). Without this cache, each call
2032
+ # mints a new literal_id and a new ListLiteralInfo added to
2033
+ # pending_resolutions -- coercion writes to one, but the resolver
2034
+ # still fails on the stale one.
2035
+ cached = self.ctx.get_expr_type(expr)
2036
+ if isinstance(cached, PendingListType) and cached.size == 0:
2037
+ return cached
2038
+ literal_id = self.ctx.literal_counter
2039
+ self.ctx.literal_counter += 1
2040
+ info = ListLiteralInfo(
2041
+ literal_id=literal_id,
2042
+ expr=expr,
2043
+ element_type=UNKNOWN_ELEMENT,
2044
+ size=0,
2045
+ is_mutated=True, # empty list is always list, never Array
2046
+ )
2047
+ self.ctx.list_literals[literal_id] = info
2048
+ self.ctx.func.pending_resolutions.append(literal_id)
2049
+ typ = PendingListType(UNKNOWN_ELEMENT, 0, literal_id)
2050
+ self.ctx.set_expr_type(expr, typ)
2051
+ return typ
2052
+
2053
+ # Analyze all elements, propagating expected type as hint when available
2054
+ if expected_elem is not None:
2055
+ elem_types = [self.analyze_expr_with_hint(e, expected_elem) for e in expr.elements]
2056
+ else:
2057
+ elem_types = [self.analyze_expr(e) for e in expr.elements]
2058
+
2059
+ # Strip OwnType wrappers -- _apply_own_wrapper marks non-last-use
2060
+ # refs as Own[T], but element type compatibility must compare the
2061
+ # underlying types (codegen handles the move/copy distinction).
2062
+ elem_types = [unwrap_own(t) for t in elem_types]
2063
+
2064
+ if expected_elem is not None:
2065
+ # Contextual mode: check each element against expected element type
2066
+ first_type = expected_elem
2067
+ for i, elem_type in enumerate(elem_types, 1):
2068
+ if elem_type == expected_elem:
2069
+ continue
2070
+ # Subclass coercion excluded: storing Child in list[Base] silently
2071
+ # slices objects (same invariance as dict/set). A covariant-generic
2072
+ # wrapper upcast (Box[Dog] -> Box[Pet]) is exempt -- it's a
2073
+ # representation-preserving converting move, not slicing -- and
2074
+ # falls through to check_type_compatible below (which the append
2075
+ # path already uses).
2076
+ if (isinstance(elem_type, NominalType) and elem_type.is_user_record
2077
+ and isinstance(expected_elem, NominalType) and expected_elem.is_user_record
2078
+ and not self.compat.is_covariant_generic_upcast(elem_type, expected_elem)):
2079
+ raise self.ctx.error(
2080
+ f"List literal element {i} has type {elem_type}, "
2081
+ f"incompatible with annotated element type {expected_elem}", expr
2082
+ )
2083
+ try:
2084
+ self.compat.check_type_compatible(
2085
+ elem_type, expected_elem,
2086
+ f"array literal element {i}",
2087
+ expr.loc,
2088
+ source_expr=expr.elements[i - 1],
2089
+ )
2090
+ except SemanticError:
2091
+ raise self.ctx.error(
2092
+ f"List literal element {i} has type {elem_type}, "
2093
+ f"incompatible with annotated element type {expected_elem}", expr
2094
+ )
2095
+ else:
2096
+ # Inferred mode: check all elements against first element's type
2097
+ first_type = elem_types[0]
2098
+ # Keep IntLiteralType so array can coerce to either Int32 or BigInt based on context
2099
+
2100
+ for i, elem_type in enumerate(elem_types[1:], 2):
2101
+ # Literal-aware unification (int/float literals, tuples of literals)
2102
+ unified = self._unify_literal_types(first_type, elem_type)
2103
+ if unified is not None:
2104
+ first_type = unified
2105
+ continue
2106
+ # Nested lists with IntLiteralType elements are compatible
2107
+ if (is_list(first_type) and is_list(elem_type) and
2108
+ isinstance(first_type.element_type, IntLiteralType) and
2109
+ isinstance(elem_type.element_type, IntLiteralType)):
2110
+ continue
2111
+ # PendingListTypes with compatible element types are compatible
2112
+ if (isinstance(first_type, PendingListType) and isinstance(elem_type, PendingListType) and
2113
+ first_type.size == elem_type.size):
2114
+ # IntLiteralType elements are compatible regardless of value
2115
+ if (isinstance(first_type.element_type, IntLiteralType) and
2116
+ isinstance(elem_type.element_type, IntLiteralType)):
2117
+ continue
2118
+ if first_type.element_type == elem_type.element_type:
2119
+ continue
2120
+ if elem_type != first_type:
2121
+ ft = self._user_type_name(first_type)
2122
+ et = self._user_type_name(elem_type)
2123
+ raise self.ctx.error(
2124
+ f"List literal has mixed types: element {i} is {et}, "
2125
+ f"but earlier elements are {ft}. "
2126
+ f"Use a type annotation like list[{ft} | {et}]", expr
2127
+ )
2128
+
2129
+ size = len(expr.elements)
2130
+
2131
+ # Global context (no current function) -> ListType (std::vector)
2132
+ # Keep IntLiteralType to allow coercion to Int32 when annotation is present
2133
+ if self.ctx.func.current_function is None:
2134
+ return make_list(first_type)
2135
+
2136
+ # Function-local context -> create PendingListType for deferred resolution
2137
+ literal_id = self.ctx.literal_counter
2138
+ self.ctx.literal_counter += 1
2139
+
2140
+ info = ListLiteralInfo(
2141
+ literal_id=literal_id,
2142
+ expr=expr,
2143
+ element_type=first_type,
2144
+ size=size,
2145
+ is_global=self.ctx.is_top_level
2146
+ )
2147
+ self.ctx.list_literals[literal_id] = info
2148
+ self.ctx.func.pending_resolutions.append(literal_id)
2149
+
2150
+ return PendingListType(first_type, size, literal_id)
2151
+
2152
+
2153
+ # -- await -----------------------------------------------------------
2154
+
2155
+ def _analyze_await(self, expr: TpyAwait) -> TpyType:
2156
+ """Analyze `await x`.
2157
+
2158
+ v1 (Commit 3 of PR 3) supports statically-resolvable awaits only:
2159
+ the operand must be a direct call to a known async def. Type-erased
2160
+ awaits (Awaitable[T] params, Task[T], unions) lower via
2161
+ AsyncFrameBase<T> in PR 4.
2162
+
2163
+ The await expression's type is the awaited async def's declared
2164
+ return type. The compiler tags the TpyAwait node with
2165
+ `awaited_async_func` so codegen can resolve the sub-coroutine
2166
+ struct and emit the in-frame `std::optional<__SubCoro>` field.
2167
+ """
2168
+ cur = self.ctx.func.current_function
2169
+ if not (isinstance(cur, TpyFunction) and cur.is_async):
2170
+ raise self.ctx.error(
2171
+ "'await' is only allowed inside an `async def` function body; "
2172
+ "use asyncio.run(coro) at the top level to drive a coroutine",
2173
+ expr)
2174
+ # Two supported v1 shapes:
2175
+ # 1. Inline: operand is a direct call to a known async def.
2176
+ # 2. Erased: operand has type Task[T] (heap-allocated frame).
2177
+ operand = expr.value
2178
+ async_fi = self._resolve_call_to_async_def(operand)
2179
+ if async_fi is not None:
2180
+ # Recursively analyze the operand call (validates arg types and,
2181
+ # for generic async defs, infers and substitutes type args into
2182
+ # the operand's `resolved_function_info`).
2183
+ self.analyze_expr(operand)
2184
+ expr.awaited_async_func_name = async_fi.name
2185
+ # Prefer the operand's substituted FunctionInfo's return type;
2186
+ # for generic async defs, `async_fi.return_type` from the
2187
+ # registry still carries unsubstituted TypeParamRefs.
2188
+ resolved_fi = getattr(operand, "resolved_function_info", None)
2189
+ source_fi = resolved_fi if resolved_fi is not None else async_fi
2190
+ return self._unwrap_awaitable_return(source_fi.return_type)
2191
+
2192
+ # Type-erased path: analyze operand. Supported v1 erased forms:
2193
+ # - tpy.Task[T] (heap-erased coroutine frame)
2194
+ # - asyncio.Future[T] (manual-completion awaitable)
2195
+ # - any record type with `poll(self, w: Waker) -> Poll[T]`
2196
+ # (structural Awaitable -- supports user-written awaitables
2197
+ # alongside hand-written awaiter types).
2198
+ operand_type = self.analyze_expr(operand)
2199
+
2200
+ # Method-call shape: `await obj.method()`. The free-function probe
2201
+ # above only matches bare-name calls; method calls land here. After
2202
+ # analyze_expr, the method call carries `resolved_function_info`;
2203
+ # treat an async one like the inline shape.
2204
+ if isinstance(operand, TpyMethodCall):
2205
+ mfi = operand.resolved_function_info
2206
+ if mfi is not None and mfi.is_async:
2207
+ # Receiver must be a stable lvalue: the coro captures it as
2208
+ # `<Class>&` across polls, so a temporary would dangle at
2209
+ # the emplace expression's semicolon. Mirrors
2210
+ # `AsyncCoroCodegen._is_stable_lvalue` in codegen.
2211
+ if not is_stable_address_lvalue(operand.obj):
2212
+ raise self.ctx.error(
2213
+ "receiver of an awaited async method must be "
2214
+ "a stable lvalue (a local, parameter, or "
2215
+ "field chain rooted at one) -- the coro "
2216
+ "captures it by reference across "
2217
+ "suspensions, so a temporary would dangle. "
2218
+ "Bind the receiver to a local first: "
2219
+ "`r = <expr>; await r.method(...)`",
2220
+ operand)
2221
+ expr.awaited_async_func_name = mfi.name
2222
+ owner_type = self._method_call_receiver_type(operand)
2223
+ if owner_type is not None:
2224
+ expr.awaited_method_owner_type = owner_type
2225
+ if mfi.return_type is not None:
2226
+ return self._unwrap_awaitable_return(mfi.return_type)
2227
+ # An Own[Task[T]] rvalue (e.g. `await asyncio.create_task(...)`)
2228
+ # is a valid await operand -- strip the Own[] before structural
2229
+ # matching so the inner Task[T] / Awaitable conformance check fires.
2230
+ unwrapped = unwrap_own(unwrap_ref_type(operand_type))
2231
+ if isinstance(unwrapped, NominalType):
2232
+ inner = self._extract_awaitable_inner(unwrapped)
2233
+ if inner is not None:
2234
+ if isinstance(inner, TpyType):
2235
+ expr.awaited_task_inner = inner
2236
+ return inner
2237
+ raise self.ctx.error(
2238
+ "await operand must be a direct call to an async def, a "
2239
+ "Task[T] / Future[T], or a value of a type with a "
2240
+ "`__poll__(self, waker: Waker) -> Own[Poll[T]]` method",
2241
+ expr)
2242
+
2243
+ def _unwrap_awaitable_return(self, ret_type: 'TpyType') -> 'TpyType':
2244
+ """Strip `Awaitable[T]` / `Cancellable[T]` wrapping from an async
2245
+ def's return type. Cancellable is the post-registration shape of
2246
+ every async-def call result; Awaitable is the shape user types
2247
+ with just `__poll__` declare. Both unwrap to `T` for `await`."""
2248
+ ret = unwrap_ref_type(ret_type)
2249
+ if (isinstance(ret, NominalType)
2250
+ and ret.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
2251
+ and len(ret.type_args) == 1):
2252
+ return ret.type_args[0]
2253
+ return ret
2254
+
2255
+ def _extract_awaitable_inner(self, typ) -> 'TpyType | None':
2256
+ """Return the awaited type T if `typ` conforms to Awaitable[T],
2257
+ else None.
2258
+
2259
+ Structural: any record with `__poll__(self, waker: Waker) -> Own[Poll[T]]`.
2260
+ Covers tpy.Task[T] (qname `tpy.Task` -> the @builtin_type stub in
2261
+ asyncio._executor), user-defined Future[T] / Event-like types, and
2262
+ any other record that satisfies the Awaitable protocol. The Own
2263
+ wrapper is required because Poll[T] is @nocopy (v1.2 step 5).
2264
+ """
2265
+ from ..typesys import NominalType, TpyType as _TpyType
2266
+ # Structural: look up the record and check for a __poll__ method
2267
+ # whose signature matches Awaitable[T].
2268
+ record_info = self.ctx.registry.get_record_for_type(typ)
2269
+ if record_info is None:
2270
+ return None
2271
+ poll_overloads = record_info.get_method_overloads("__poll__")
2272
+ if not poll_overloads:
2273
+ return None
2274
+ # Pick the first overload whose return type is Poll[T] for some T.
2275
+ # Substitute the record's class-level type params with typ.type_args
2276
+ # so `Future[Int32]` returns Int32, not the type-var T.
2277
+ from ..typesys import unwrap_ref_type
2278
+ type_subst: dict[str, _TpyType] = {}
2279
+ if record_info.type_params and len(typ.type_args) == len(record_info.type_params):
2280
+ for tp, arg in zip(record_info.type_params, typ.type_args):
2281
+ if isinstance(arg, _TpyType):
2282
+ type_subst[tp] = arg
2283
+ for fi in poll_overloads:
2284
+ ret = fi.return_type
2285
+ if ret is None:
2286
+ continue
2287
+ # Poll[T] is @nocopy, so a bare `-> Poll[T]` is a reference
2288
+ # return -- skip non-Own returns so the user gets the "no
2289
+ # __poll__ method" diagnostic (which prints the correct
2290
+ # `Own[Poll[T]]` signature) instead of a C++ build failure.
2291
+ ret_outer = unwrap_ref_type(ret)
2292
+ if not isinstance(ret_outer, OwnType):
2293
+ continue
2294
+ ret = unwrap_own(ret_outer)
2295
+ if (isinstance(ret, NominalType)
2296
+ and ret._module_qname == qnames.POLL
2297
+ and len(ret.type_args) == 1):
2298
+ # Recursively substitute T -> typ.type_args[i] -- handles
2299
+ # both bare TypeParamRef and nested shapes like list[T],
2300
+ # tuple[T, U], etc.
2301
+ return _substitute_type_params(ret.type_args[0], type_subst)
2302
+ return None
2303
+
2304
+ def _method_call_receiver_type(self, call) -> 'NominalType | None':
2305
+ """For a TpyMethodCall whose receiver resolves to a known record,
2306
+ return the receiver's NominalType (carrying class-level
2307
+ type_args). Used to name and qualify async-method coro structs
2308
+ as `__coro_<Record>_<method>` plus a `<owner_type_args>` suffix
2309
+ for receivers with non-empty class-level type args.
2310
+ """
2311
+ recv_type = self.ctx.get_expr_type(call.obj)
2312
+ if recv_type is None:
2313
+ return None
2314
+ inner = unwrap_own(unwrap_ref_type(recv_type))
2315
+ if isinstance(inner, NominalType):
2316
+ return inner
2317
+ return None
2318
+
2319
+ def _resolve_call_to_async_def(self, operand) -> 'FunctionInfo | None':
2320
+ """If operand is a direct TpyCall whose target is a known async def,
2321
+ return its FunctionInfo. Otherwise return None.
2322
+
2323
+ Free-function call shape only -- method calls (`await obj.m()`)
2324
+ are detected after `analyze_expr` populates the
2325
+ `TpyMethodCall.resolved_function_info` field; that branch lives
2326
+ in `analyze_await` above.
2327
+ """
2328
+ from ..parse.nodes import TpyCall
2329
+ if not isinstance(operand, TpyCall):
2330
+ return None
2331
+ func_name = operand.maybe_func_name
2332
+ if not func_name:
2333
+ return None
2334
+ overloads = self.ctx.registry.get_function(func_name)
2335
+ if not overloads:
2336
+ return None
2337
+ # Async defs do not participate in @overload, so a single match is OK.
2338
+ for fi in overloads:
2339
+ if fi.is_async:
2340
+ return fi
2341
+ return None
2342
+
2343
+ # -- Ternary expression analysis ------------------------------------------
2344
+
2345
+ def _analyze_named_expr(self, expr: TpyNamedExpr) -> TpyType:
2346
+ """Analyze walrus operator: (x := expr)."""
2347
+ value_type = self.analyze_expr(expr.value)
2348
+ name = expr.target
2349
+
2350
+ # Resolve pending/literal types for the variable binding
2351
+ resolved = value_type
2352
+ if isinstance(resolved, IntLiteralType):
2353
+ resolved = self.ctx.default_int_type
2354
+ elif isinstance(resolved, FloatLiteralType):
2355
+ resolved = FLOAT
2356
+ elif isinstance(resolved, PendingViewType):
2357
+ resolved = resolved.family.owned_type
2358
+
2359
+ # PEP 572: walrus in comprehension leaks to enclosing function scope
2360
+ target_scope = self.ctx.func.current_scope
2361
+ levels = self.ctx.in_comprehension
2362
+ while levels > 0 and target_scope.parent is not None:
2363
+ target_scope = target_scope.parent
2364
+ levels -= 1
2365
+
2366
+ existing = target_scope.lookup(name)
2367
+ if existing is not None:
2368
+ # Reassignment via walrus -- keep existing type
2369
+ pass
2370
+ else:
2371
+ # New binding
2372
+ target_scope.define(name, resolved)
2373
+ if self.ctx.func.current_ns:
2374
+ self.ctx.func.current_ns.bind_variable(name, resolved)
2375
+
2376
+ self.ctx.func.definitely_assigned.add(name)
2377
+ self.ctx.func.rvalue_vars.add(name)
2378
+ if name not in self.ctx.func.var_scope_depth:
2379
+ self.ctx.func.var_scope_depth[name] = target_scope.depth
2380
+
2381
+ return value_type
2382
+
2383
+ def _analyze_if_expr(
2384
+ self, expr: TpyIfExpr, type_hint: TpyType | None = None,
2385
+ ) -> TpyType:
2386
+ """Analyze a ternary conditional: then_expr if condition else else_expr."""
2387
+ self.analyze_expr(expr.condition)
2388
+ self.narrowing.warn_truthy_value_optionals(expr.condition)
2389
+
2390
+ then_facts, else_facts = self.narrowing.condition_type_facts(
2391
+ expr.condition)
2392
+
2393
+ # Save narrowed_types (ternary doesn't create vars, so we only
2394
+ # need to save/restore narrowing, not the full InitTracker state).
2395
+ saved_narrowed = dict(self.ctx.func.narrowed_types)
2396
+
2397
+ self.ctx.func.narrowed_types.update(then_facts)
2398
+ if type_hint is not None:
2399
+ then_type = self.analyze_expr_with_hint(expr.then_expr, type_hint)
2400
+ else:
2401
+ then_type = self.analyze_expr(expr.then_expr)
2402
+
2403
+ self.ctx.func.narrowed_types = dict(saved_narrowed)
2404
+ self.ctx.func.narrowed_types.update(else_facts)
2405
+ if type_hint is not None:
2406
+ else_type = self.analyze_expr_with_hint(expr.else_expr, type_hint)
2407
+ else:
2408
+ else_type = self.analyze_expr(expr.else_expr)
2409
+
2410
+ self.ctx.func.narrowed_types = saved_narrowed
2411
+
2412
+ # Strip Ref/Own from branch types -- these are provenance qualifiers,
2413
+ # not part of the result type. The ternary produces a value.
2414
+ then_type = unwrap_ref_type(then_type)
2415
+ if isinstance(then_type, OwnType):
2416
+ then_type = then_type.wrapped
2417
+ else_type = unwrap_ref_type(else_type)
2418
+ if isinstance(else_type, OwnType):
2419
+ else_type = else_type.wrapped
2420
+
2421
+ common = self._ternary_common_type(expr, then_type, else_type,
2422
+ widen_numeric_types)
2423
+
2424
+ if is_list(common):
2425
+ # Both ternary branches must share a C++ type; force sources to
2426
+ # ListType so they don't independently become incompatible Arrays.
2427
+ for t in collect_pending_source_types(self.ctx, expr):
2428
+ if isinstance(t, PendingListType):
2429
+ info = self.ctx.list_literals.get(t.literal_id)
2430
+ if info is not None:
2431
+ info.needs_list_type = True
2432
+
2433
+ # Coerce branches to the common type so C++ ternary has
2434
+ # matching branch types (e.g. None -> std::optional<T>).
2435
+ if then_type != common:
2436
+ expr.then_expr = self.compat.coerce_expr(
2437
+ expr.then_expr, then_type, common,
2438
+ "ternary branch", coercion_ctx=CoercionContext.INIT)
2439
+ if else_type != common:
2440
+ expr.else_expr = self.compat.coerce_expr(
2441
+ expr.else_expr, else_type, common,
2442
+ "ternary branch", coercion_ctx=CoercionContext.INIT)
2443
+
2444
+ return common
2445
+
2446
+ def _ternary_common_type(
2447
+ self, expr: TpyIfExpr,
2448
+ then_type: TpyType, else_type: TpyType,
2449
+ widen_numeric_types: object,
2450
+ ) -> TpyType:
2451
+ """Compute the common result type of a ternary expression's branches."""
2452
+ if then_type == else_type:
2453
+ return then_type
2454
+
2455
+ t, e = then_type, else_type
2456
+
2457
+ # IntLiteral resolution
2458
+ if isinstance(t, IntLiteralType) and isinstance(e, IntLiteralType):
2459
+ return self.ctx.default_int_for_literal(t, expr.then_expr)
2460
+ if isinstance(t, IntLiteralType):
2461
+ if is_integer_type(e) or is_float_type(e):
2462
+ return e
2463
+ t = self.ctx.default_int_for_literal(t, expr.then_expr)
2464
+ if isinstance(e, IntLiteralType):
2465
+ if is_integer_type(t) or is_float_type(t):
2466
+ return t
2467
+ e = self.ctx.default_int_for_literal(e, expr.else_expr)
2468
+
2469
+ # FloatLiteral resolution: adapts to the concrete float type in context
2470
+ if isinstance(t, FloatLiteralType) and isinstance(e, FloatLiteralType):
2471
+ return FLOAT
2472
+ if isinstance(t, FloatLiteralType):
2473
+ if is_float_type(e):
2474
+ return e
2475
+ t = FLOAT
2476
+ if isinstance(e, FloatLiteralType):
2477
+ if is_float_type(t):
2478
+ return t
2479
+ e = FLOAT
2480
+
2481
+ # Normalize PendingViewType to its owned type for comparison; preserve
2482
+ # the pending type when both sides are pending so view deduction can
2483
+ # chain the result variable back to the operands' resolution.
2484
+ if isinstance(t, PendingViewType) and isinstance(e, PendingViewType) and t.family is e.family:
2485
+ return t
2486
+ if isinstance(t, PendingViewType):
2487
+ t = t.family.owned_type
2488
+ if isinstance(e, PendingViewType):
2489
+ e = e.family.owned_type
2490
+ # Normalize pending container types to concrete types for equality comparison,
2491
+ # resolving IntLiteralType elements so [1,2] and [3,4] both normalize to list[int].
2492
+ t = self._normalize_pending_container(t)
2493
+ e = self._normalize_pending_container(e)
2494
+
2495
+ if t == e:
2496
+ return t
2497
+
2498
+ # Numeric widening (Int32 + Int64 -> Int64, etc.)
2499
+ widened = widen_numeric_types(t, e)
2500
+ if widened is not None:
2501
+ return widened
2502
+
2503
+ # T + None / None + T -> Optional[T]
2504
+ if isinstance(e, NoneType):
2505
+ return make_union(t, NoneType())
2506
+ if isinstance(t, NoneType):
2507
+ return make_union(e, NoneType())
2508
+
2509
+ raise self.ctx.error(
2510
+ f"Incompatible types in ternary expression: "
2511
+ f"'{t}' and '{e}'",
2512
+ expr,
2513
+ )
2514
+
2515
+ def _resolve_literals_with_hint(self, t: TpyType, hint: TpyType | None) -> TpyType:
2516
+ """Resolve IntLiteralType / FloatLiteralType inside t using hint as a
2517
+ structural guide. Recurses into TupleType. Falls back to default_int /
2518
+ FLOAT when hint doesn't match the literal's family.
2519
+
2520
+ Mirrors the asymmetric behavior of the bare-literal path: integer
2521
+ literals adopt the hint when present (compat-checked downstream),
2522
+ float literals only adopt the hint when it's a float type.
2523
+ """
2524
+ if isinstance(t, IntLiteralType):
2525
+ return hint if hint is not None else self.ctx.default_int_for_literal(t)
2526
+ if isinstance(t, FloatLiteralType):
2527
+ return hint if is_float_type(hint) else FLOAT
2528
+ if isinstance(t, TupleType):
2529
+ if isinstance(hint, TupleType) and len(t.element_types) == len(hint.element_types):
2530
+ elems = tuple(
2531
+ self._resolve_literals_with_hint(et, ht)
2532
+ for et, ht in zip(t.element_types, hint.element_types)
2533
+ )
2534
+ else:
2535
+ elems = tuple(
2536
+ self._resolve_literals_with_hint(et, None)
2537
+ for et in t.element_types
2538
+ )
2539
+ return TupleType(elems)
2540
+ return t
2541
+
2542
+ def _unify_literal_types(self, a: TpyType, b: TpyType) -> TpyType | None:
2543
+ """Unify two dict/set literal element types, treating IntLiteralType /
2544
+ FloatLiteralType as compatible with their concrete equivalents and
2545
+ recursing into TupleType. Returns the unified type or None if they
2546
+ cannot be unified.
2547
+ """
2548
+ if a == b:
2549
+ return a
2550
+ if isinstance(a, IntLiteralType) and isinstance(b, IntLiteralType):
2551
+ return a
2552
+ if isinstance(a, IntLiteralType) and is_integer_type(b):
2553
+ return b
2554
+ if isinstance(b, IntLiteralType) and is_integer_type(a):
2555
+ return a
2556
+ if isinstance(a, FloatLiteralType) and isinstance(b, FloatLiteralType):
2557
+ return a
2558
+ if isinstance(a, FloatLiteralType) and is_float_type(b):
2559
+ return b
2560
+ if isinstance(b, FloatLiteralType) and is_float_type(a):
2561
+ return a
2562
+ if isinstance(a, TupleType) and isinstance(b, TupleType):
2563
+ if len(a.element_types) != len(b.element_types):
2564
+ return None
2565
+ unified: list[TpyType] = []
2566
+ for ea, eb in zip(a.element_types, b.element_types):
2567
+ u = self._unify_literal_types(ea, eb)
2568
+ if u is None:
2569
+ return None
2570
+ unified.append(u)
2571
+ return TupleType(tuple(unified))
2572
+ return None
2573
+
2574
+ def _analyze_dict_literal(
2575
+ self, expr: TpyDictLiteral,
2576
+ expected_key: TpyType | None = None,
2577
+ expected_value: TpyType | None = None,
2578
+ ) -> TpyType:
2579
+ """Analyze a dict literal {key: value, ...}"""
2580
+ if not expr.keys:
2581
+ # Hint from LHS / param / return type pins K, V directly -- no
2582
+ # usage-based inference needed.
2583
+ if expected_key is not None and expected_value is not None:
2584
+ return make_dict(expected_key, expected_value)
2585
+ if not is_body_like_scope(self.ctx.func.current_function):
2586
+ raise self.ctx.error(
2587
+ "Empty dict literal requires explicit type annotation", expr)
2588
+ literal_id = self.ctx.literal_counter
2589
+ self.ctx.literal_counter += 1
2590
+ info = DictLiteralInfo(
2591
+ literal_id=literal_id,
2592
+ expr=expr,
2593
+ key_type=UNKNOWN_ELEMENT,
2594
+ value_type=UNKNOWN_ELEMENT,
2595
+ )
2596
+ self.ctx.dict_literals[literal_id] = info
2597
+ self.ctx.func.pending_dict_resolutions.append(literal_id)
2598
+ return PendingDictType(UNKNOWN_ELEMENT, UNKNOWN_ELEMENT, literal_id)
2599
+
2600
+ if expected_key:
2601
+ key_types = [self._analyze_and_strip(k, expected_key) for k in expr.keys]
2602
+ else:
2603
+ key_types = [self._analyze_and_strip(k) for k in expr.keys]
2604
+ if expected_value:
2605
+ value_types = [self._analyze_and_strip(v, expected_value) for v in expr.values]
2606
+ else:
2607
+ value_types = [self._analyze_and_strip(v) for v in expr.values]
2608
+
2609
+ # Unify key types
2610
+ if is_union_or_optional_type(expected_key) or isinstance(expected_key, AnyType):
2611
+ key_type = expected_key
2612
+ for i, kt in enumerate(key_types, 1):
2613
+ if kt == expected_key:
2614
+ continue
2615
+ try:
2616
+ self.compat.check_type_compatible(
2617
+ kt, expected_key, f"dict literal key {i}", expr.loc,
2618
+ source_expr=expr.keys[i - 1])
2619
+ except SemanticError:
2620
+ raise self.ctx.error(
2621
+ f"Dict literal key {i} has type {kt}, "
2622
+ f"incompatible with annotated key type {expected_key}", expr)
2623
+ else:
2624
+ key_type = key_types[0]
2625
+ for i, kt in enumerate(key_types[1:], 2):
2626
+ unified = self._unify_literal_types(key_type, kt)
2627
+ if unified is None:
2628
+ raise self.ctx.error(
2629
+ f"Dict has mixed key types: key {i} is {self._user_type_name(kt)}, "
2630
+ f"but earlier keys are {self._user_type_name(key_type)}", expr,
2631
+ )
2632
+ key_type = unified
2633
+
2634
+ # Unify value types
2635
+ if (is_union_or_optional_type(expected_value)
2636
+ or isinstance(expected_value, AnyType)
2637
+ or (expected_value is not None and expected_value.needs_wrapper())
2638
+ or (expected_value is not None and any(
2639
+ self.compat.is_covariant_generic_upcast(vt, expected_value)
2640
+ for vt in value_types))):
2641
+ # Annotation provides a union/optional/Any/recursive-alias wrapper --
2642
+ # each value validates against the slot independently rather than
2643
+ # against its peers (heterogeneous values are the whole point of
2644
+ # these slot types; a recursive dict alias's values are leaves or
2645
+ # nested wrappers). Same when the annotated value is a covariant-
2646
+ # generic wrapper the values upcast to (Box[Dog] -> Box[Pet]):
2647
+ # check each against the annotation so the dict builds at the
2648
+ # annotated instantiation, not the peer-unified subclass.
2649
+ value_type = expected_value
2650
+ for i, vt in enumerate(value_types, 1):
2651
+ if vt == expected_value:
2652
+ continue
2653
+ try:
2654
+ self.compat.check_type_compatible(
2655
+ vt, expected_value, f"dict literal value {i}", expr.loc,
2656
+ source_expr=expr.values[i - 1])
2657
+ except SemanticError:
2658
+ raise self.ctx.error(
2659
+ f"Dict literal value {i} has type {vt}, "
2660
+ f"incompatible with annotated value type {expected_value}", expr)
2661
+ else:
2662
+ value_type = value_types[0]
2663
+ for i, vt in enumerate(value_types[1:], 2):
2664
+ unified = self._unify_literal_types(value_type, vt)
2665
+ if unified is None:
2666
+ raise self.ctx.error(
2667
+ f"Dict has mixed value types: value {i} is {self._user_type_name(vt)}, "
2668
+ f"but earlier values are {self._user_type_name(value_type)}", expr,
2669
+ )
2670
+ value_type = unified
2671
+
2672
+ # Resolve any literal types (bare or nested in tuples) using the
2673
+ # annotation as a structural hint.
2674
+ key_type = self._resolve_literals_with_hint(key_type, expected_key)
2675
+ value_type = self._resolve_literals_with_hint(value_type, expected_value)
2676
+ # Container elements must be owned -- views can't be stored in a dict.
2677
+ if isinstance(key_type, PendingViewType):
2678
+ key_type = key_type.family.owned_type
2679
+ if isinstance(value_type, PendingViewType):
2680
+ value_type = value_type.family.owned_type
2681
+
2682
+ self.type_ops.validate_hashable_container_elem(key_type, "dict key", expr.loc)
2683
+ return make_dict(key_type, value_type)
2684
+
2685
+ def _analyze_set_literal(
2686
+ self, expr: TpySetLiteral,
2687
+ expected_elem: TpyType | None = None,
2688
+ ) -> TpyType:
2689
+ """Analyze a set literal {value, ...}"""
2690
+ if not expr.elements:
2691
+ raise self.ctx.error(
2692
+ "Empty set literal requires type annotation "
2693
+ "(e.g. s: set[int] = set())", expr,
2694
+ )
2695
+
2696
+ if expected_elem:
2697
+ elem_types = [self._analyze_and_strip(e, expected_elem) for e in expr.elements]
2698
+ else:
2699
+ elem_types = [self._analyze_and_strip(e) for e in expr.elements]
2700
+
2701
+ # Unify element types
2702
+ if (is_union_or_optional_type(expected_elem) or isinstance(expected_elem, AnyType)
2703
+ or (expected_elem is not None and any(
2704
+ self.compat.is_covariant_generic_upcast(et, expected_elem)
2705
+ for et in elem_types))):
2706
+ # Annotation provides a union/optional/Any -- each element validates
2707
+ # against the slot independently rather than against its peers. Same
2708
+ # for a covariant-generic element the values upcast to (parallel to
2709
+ # the dict-value path; moot for @nocopy Box/Rc, which are rejected
2710
+ # as set elements, but kept symmetric for a hashable covariant
2711
+ # value-type element).
2712
+ elem_type = expected_elem
2713
+ for i, et in enumerate(elem_types, 1):
2714
+ if et == expected_elem:
2715
+ continue
2716
+ try:
2717
+ self.compat.check_type_compatible(
2718
+ et, expected_elem, f"set literal element {i}", expr.loc,
2719
+ source_expr=expr.elements[i - 1])
2720
+ except SemanticError:
2721
+ raise self.ctx.error(
2722
+ f"Set literal element {i} has type {et}, "
2723
+ f"incompatible with annotated element type {expected_elem}", expr,
2724
+ )
2725
+ else:
2726
+ elem_type = elem_types[0]
2727
+ for i, et in enumerate(elem_types[1:], 2):
2728
+ unified = self._unify_literal_types(elem_type, et)
2729
+ if unified is None:
2730
+ raise self.ctx.error(
2731
+ f"Set has mixed element types: element {i} is {self._user_type_name(et)}, "
2732
+ f"but earlier elements are {self._user_type_name(elem_type)}", expr,
2733
+ )
2734
+ elem_type = unified
2735
+
2736
+ elem_type = self._resolve_literals_with_hint(elem_type, expected_elem)
2737
+ # Container elements must be owned -- views can't be stored in a set.
2738
+ if isinstance(elem_type, PendingViewType):
2739
+ elem_type = elem_type.family.owned_type
2740
+
2741
+ self.type_ops.validate_hashable_container_elem(elem_type, "set element", expr.loc)
2742
+ return make_set(elem_type)
2743
+
2744
+ def _analyze_list_repeat(self, expr: TpyListRepeat) -> TpyType:
2745
+ """Analyze a list repetition: [elements...] * count"""
2746
+ count_type = self.analyze_expr(expr.count)
2747
+
2748
+ if not is_any_int_type(count_type):
2749
+ raise self.ctx.error(f"List repetition count must be an integer type, got {count_type}", expr)
2750
+
2751
+ # Note: Empty list repetition [] * N is collapsed to [] in the parser
2752
+
2753
+ # Analyze all elements
2754
+ elem_types = [self.analyze_expr(e) for e in expr.elements]
2755
+ first_type = elem_types[0]
2756
+
2757
+ # Check all elements are compatible (similar to array literal)
2758
+ for i, elem_type in enumerate(elem_types[1:], 2):
2759
+ if isinstance(first_type, IntLiteralType) and isinstance(elem_type, IntLiteralType):
2760
+ continue
2761
+ if isinstance(elem_type, IntLiteralType) and is_integer_type(first_type):
2762
+ continue
2763
+ if isinstance(first_type, IntLiteralType) and is_integer_type(elem_type):
2764
+ first_type = elem_type
2765
+ continue
2766
+ if isinstance(first_type, FloatLiteralType) and isinstance(elem_type, FloatLiteralType):
2767
+ continue
2768
+ if isinstance(elem_type, FloatLiteralType) and is_float_type(first_type):
2769
+ continue
2770
+ if isinstance(first_type, FloatLiteralType) and is_float_type(elem_type):
2771
+ first_type = elem_type
2772
+ continue
2773
+ if first_type != elem_type:
2774
+ raise self.ctx.error(f"List repetition element {i} has type {elem_type}, expected {first_type}", expr)
2775
+
2776
+ # Global context -> ListType (no deferred resolution)
2777
+ if self.ctx.func.current_function is None:
2778
+ return make_list(first_type)
2779
+
2780
+ # Function-local context -> PendingListType for deferred resolution
2781
+ # Compute size if count is compile-time constant
2782
+ if isinstance(expr.count, TpyIntLiteral):
2783
+ size = len(expr.elements) * expr.count.value
2784
+ else:
2785
+ size = -1 # Variable count -- cannot resolve to Array
2786
+
2787
+ literal_id = self.ctx.literal_counter
2788
+ self.ctx.literal_counter += 1
2789
+
2790
+ info = ListLiteralInfo(
2791
+ literal_id=literal_id,
2792
+ expr=expr,
2793
+ element_type=first_type,
2794
+ size=size,
2795
+ is_global=self.ctx.is_top_level,
2796
+ )
2797
+ self.ctx.list_literals[literal_id] = info
2798
+ self.ctx.func.pending_resolutions.append(literal_id)
2799
+
2800
+ return PendingListType(first_type, size, literal_id)
2801
+
2802
+ def _analyze_list_comprehension(
2803
+ self, expr: TpyListComprehension, expected_elem: TpyType | None = None
2804
+ ) -> TpyType:
2805
+ return self._analyze_elem_comprehension(expr, expected_elem, kind="list")
2806
+
2807
+ def _analyze_set_comprehension(
2808
+ self, expr: TpySetComprehension, expected_elem: TpyType | None = None
2809
+ ) -> TpyType:
2810
+ return self._analyze_elem_comprehension(expr, expected_elem, kind="set")
2811
+
2812
+ def _analyze_generator_expression(self, expr: TpyGeneratorExpression) -> TpyType:
2813
+ gen = expr.generator
2814
+ elem_type = self._resolve_comp_iterable(gen, expr)
2815
+
2816
+ if self.scopes is None:
2817
+ raise RuntimeError("generator expression requires ScopeTracker")
2818
+ result_elem_type = self._enter_comp_scope(gen, expr, elem_type, None)
2819
+
2820
+ if isinstance(result_elem_type, IntLiteralType):
2821
+ result_elem_type = self.ctx.default_int_type
2822
+ elif isinstance(result_elem_type, FloatLiteralType):
2823
+ result_elem_type = FLOAT
2824
+ else:
2825
+ result_elem_type = resolve_int_literals(result_elem_type, self.ctx.default_int_for_literal)
2826
+
2827
+ expr.result_elem_type = result_elem_type
2828
+ return GenExprType(result_elem_type)
2829
+
2830
+ def _analyze_elem_comprehension(
2831
+ self, expr: TpyListComprehension | TpySetComprehension,
2832
+ expected_elem: TpyType | None,
2833
+ kind: Literal["list", "set"],
2834
+ ) -> TpyType:
2835
+ """Shared analysis for list and set comprehensions."""
2836
+ gen = expr.generator
2837
+ elem_type = self._resolve_comp_iterable(gen, expr)
2838
+
2839
+ if self.scopes is None:
2840
+ raise RuntimeError(f"{kind} comprehension requires ScopeTracker")
2841
+ result_elem_type = self._enter_comp_scope(gen, expr, elem_type, expected_elem)
2842
+
2843
+ if isinstance(result_elem_type, IntLiteralType):
2844
+ result_elem_type = expected_elem if expected_elem is not None else self.ctx.default_int_type
2845
+ if isinstance(result_elem_type, FloatLiteralType):
2846
+ result_elem_type = expected_elem if is_float_type(expected_elem) else FLOAT
2847
+
2848
+ if expected_elem is not None and result_elem_type != expected_elem:
2849
+ # Subclass coercion excluded: storing Child in list/set[Base] silently
2850
+ # slices objects (same invariance as container literals). Covariant-
2851
+ # generic wrapper upcasts (Box[Dog] -> Box[Pet]) are exempt -- a
2852
+ # representation-preserving converting move, not slicing -- and flow
2853
+ # through coerce_expr below.
2854
+ if (isinstance(result_elem_type, NominalType) and result_elem_type.is_user_record
2855
+ and isinstance(expected_elem, NominalType) and expected_elem.is_user_record
2856
+ and not self.compat.is_covariant_generic_upcast(result_elem_type, expected_elem)):
2857
+ raise self.ctx.error(
2858
+ f"{kind.capitalize()} comprehension element has type {result_elem_type}, "
2859
+ f"incompatible with annotated element type {expected_elem}", expr
2860
+ )
2861
+ expr.element_expr = self.compat.coerce_expr(
2862
+ expr.element_expr, result_elem_type, expected_elem,
2863
+ f"{kind} comprehension element", coercion_ctx=CoercionContext.INIT,
2864
+ target_is_storage_form=True)
2865
+ result_elem_type = expected_elem
2866
+
2867
+ if kind == "set":
2868
+ self.type_ops.validate_hashable_container_elem(result_elem_type, "set element", expr.loc)
2869
+
2870
+ expr.result_elem_type = result_elem_type
2871
+
2872
+ if kind == "list":
2873
+ array_size = self._try_comp_array_size(expr)
2874
+ if array_size is not None and self.ctx.func.current_function is not None:
2875
+ literal_id = self.ctx.literal_counter
2876
+ self.ctx.literal_counter += 1
2877
+ info = ListLiteralInfo(
2878
+ literal_id=literal_id,
2879
+ expr=expr,
2880
+ element_type=result_elem_type,
2881
+ size=array_size,
2882
+ is_global=self.ctx.is_top_level,
2883
+ )
2884
+ self.ctx.list_literals[literal_id] = info
2885
+ self.ctx.func.pending_resolutions.append(literal_id)
2886
+ return PendingListType(result_elem_type, array_size, literal_id)
2887
+
2888
+ return make_set(result_elem_type) if kind == "set" else make_list(result_elem_type)
2889
+
2890
+ def _try_comp_array_size(self, expr: TpyListComprehension) -> int | None:
2891
+ """Return the compile-time known size if this comprehension can be an Array."""
2892
+ gen = expr.generator
2893
+ if gen.conditions:
2894
+ return None
2895
+
2896
+ # range(N) or range(start, stop) with literal args
2897
+ if isinstance(gen.iterable, TpyCall) and gen.iterable.func_name == "range":
2898
+ return self._range_literal_size(gen.iterable)
2899
+
2900
+ # Array[T, N] source -- size is known from the type
2901
+ iterable_type = unwrap_readonly(self.ctx.get_expr_type(gen.iterable))
2902
+ if is_array(iterable_type):
2903
+ return iterable_type.type_args[1]
2904
+
2905
+ return None
2906
+
2907
+ @staticmethod
2908
+ def _try_int_literal(expr: TpyExpr) -> int | None:
2909
+ """Extract an integer literal value, unwrapping TpyCoerce and unary minus."""
2910
+ if isinstance(expr, TpyCoerce):
2911
+ expr = expr.expr
2912
+ if isinstance(expr, TpyIntLiteral):
2913
+ return expr.value
2914
+ if isinstance(expr, TpyUnaryOp) and expr.op == '-' and isinstance(expr.operand, TpyIntLiteral):
2915
+ return -expr.operand.value
2916
+ return None
2917
+
2918
+ def _range_literal_size(self, call: TpyCall) -> int | None:
2919
+ """Extract compile-time size from range() with literal args."""
2920
+ args = call.args
2921
+ if len(args) == 1:
2922
+ n = self._try_int_literal(args[0])
2923
+ if n is not None:
2924
+ return max(n, 0)
2925
+ elif len(args) == 2:
2926
+ s = self._try_int_literal(args[0])
2927
+ e = self._try_int_literal(args[1])
2928
+ if s is not None and e is not None and e >= s:
2929
+ return e - s
2930
+ elif len(args) == 3:
2931
+ s = self._try_int_literal(args[0])
2932
+ e = self._try_int_literal(args[1])
2933
+ d = self._try_int_literal(args[2])
2934
+ if s is not None and e is not None and d is not None and d != 0:
2935
+ if d > 0 and e > s:
2936
+ return (e - s + d - 1) // d
2937
+ elif d < 0 and s > e:
2938
+ return (s - e - d - 1) // (-d)
2939
+ else:
2940
+ return 0
2941
+ return None
2942
+
2943
+ def _analyze_dict_comprehension(
2944
+ self, expr: TpyDictComprehension,
2945
+ expected_key: TpyType | None = None,
2946
+ expected_value: TpyType | None = None,
2947
+ ) -> TpyType:
2948
+ """Analyze a dict comprehension: {key: value for var in iterable if cond}"""
2949
+ gen = expr.generator
2950
+ elem_type = self._resolve_comp_iterable(gen, expr)
2951
+
2952
+ if self.scopes is None:
2953
+ raise RuntimeError("dict comprehension requires ScopeTracker")
2954
+ key_type, value_type = self._enter_comp_scope(
2955
+ gen, expr, elem_type, (expected_key, expected_value))
2956
+
2957
+ if isinstance(key_type, IntLiteralType):
2958
+ key_type = expected_key if expected_key is not None else self.ctx.default_int_type
2959
+ if isinstance(value_type, IntLiteralType):
2960
+ value_type = expected_value if expected_value is not None else self.ctx.default_int_type
2961
+ if isinstance(key_type, FloatLiteralType):
2962
+ key_type = expected_key if is_float_type(expected_key) else FLOAT
2963
+ if isinstance(value_type, FloatLiteralType):
2964
+ value_type = expected_value if is_float_type(expected_value) else FLOAT
2965
+
2966
+ if expected_key is not None and key_type != expected_key:
2967
+ expr.key_expr = self.compat.coerce_expr(
2968
+ expr.key_expr, key_type, expected_key,
2969
+ "dict comprehension key", coercion_ctx=CoercionContext.INIT,
2970
+ target_is_storage_form=True)
2971
+ key_type = expected_key
2972
+ if expected_value is not None and value_type != expected_value:
2973
+ expr.value_expr = self.compat.coerce_expr(
2974
+ expr.value_expr, value_type, expected_value,
2975
+ "dict comprehension value", coercion_ctx=CoercionContext.INIT,
2976
+ target_is_storage_form=True)
2977
+ value_type = expected_value
2978
+
2979
+ self.type_ops.validate_hashable_container_elem(key_type, "dict key", expr.loc)
2980
+ expr.result_key_type = key_type
2981
+ expr.result_value_type = value_type
2982
+ return make_dict(key_type, value_type)
2983
+
2984
+ def _resolve_comp_iterable(
2985
+ self, gen: TpyComprehensionGenerator, expr: TpyExpr
2986
+ ) -> TpyType:
2987
+ """Analyze the iterable and extract its element type (shared by all comprehensions)."""
2988
+ iterable_type = self.analyze_expr(gen.iterable)
2989
+ inner = unwrap_readonly(iterable_type)
2990
+ if isinstance(inner, TypeParamRef):
2991
+ bound = self.type_ops.get_type_param_bound(inner.name)
2992
+ if bound is not None and is_protocol_type(bound):
2993
+ inner = bound
2994
+ return IterableHelper(self.ctx).get_iterable_element_type(inner, loc=expr.loc)
2995
+
2996
+ def _enter_comp_scope(
2997
+ self,
2998
+ gen: TpyComprehensionGenerator,
2999
+ expr: TpyListComprehension | TpySetComprehension | TpyDictComprehension | TpyGeneratorExpression,
3000
+ elem_type: TpyType,
3001
+ hint: TpyType | tuple[TpyType | None, TpyType | None] | None,
3002
+ ) -> TpyType | tuple[TpyType, TpyType]:
3003
+ # unwrap_qualifiers (not unwrap_readonly) so a wrapped value-type
3004
+ # element still counts as value-type, matching the for-loop predicate
3005
+ # in sema/statements.py.
3006
+ unwrapped = unwrap_qualifiers(elem_type)
3007
+ worth_const_ref = (not unwrapped.is_value_type()
3008
+ or unwrapped.is_expensive_copy())
3009
+ if gen.unpack_vars is not None:
3010
+ names = [u for u in gen.unpack_vars if u is not None]
3011
+ else:
3012
+ names = [gen.var]
3013
+ # Clear any prior marks in case an outer scope already used these
3014
+ # names; the post-body check at the bottom must see only marks from
3015
+ # this comp's body.
3016
+ for n in names:
3017
+ self.ctx.func.mutated_loop_vars.discard(n)
3018
+ self.ctx.func.consumed_loop_vars.discard(n)
3019
+
3020
+ with self.scopes.comprehension_scope() as inner_scope:
3021
+ if gen.unpack_vars is not None:
3022
+ if not isinstance(elem_type, TupleType):
3023
+ raise self.ctx.error(
3024
+ f"Cannot unpack non-tuple type {elem_type}", expr)
3025
+ if len(gen.unpack_vars) != len(elem_type.element_types):
3026
+ raise self.ctx.error(
3027
+ f"Cannot unpack tuple of {len(elem_type.element_types)} "
3028
+ f"elements into {len(gen.unpack_vars)} targets", expr)
3029
+ with ExitStack() as stack:
3030
+ for uvar, utype in zip(gen.unpack_vars, elem_type.element_types):
3031
+ if uvar is not None:
3032
+ stack.enter_context(
3033
+ self.scopes.loop_var(inner_scope, uvar, utype,
3034
+ inner_scope.depth, is_foreach=True))
3035
+ result = self._analyze_comp_body(gen, expr, hint)
3036
+ else:
3037
+ with self.scopes.loop_var(inner_scope, gen.var, elem_type,
3038
+ inner_scope.depth, is_foreach=True):
3039
+ result = self._analyze_comp_body(gen, expr, hint)
3040
+
3041
+ if worth_const_ref and not any(n in self.ctx.func.mutated_loop_vars
3042
+ for n in names):
3043
+ gen.const_loop_var = True
3044
+ return result
3045
+
3046
+ def _analyze_comp_body(
3047
+ self,
3048
+ gen: TpyComprehensionGenerator,
3049
+ expr: TpyListComprehension | TpySetComprehension | TpyDictComprehension | TpyGeneratorExpression,
3050
+ hint: TpyType | tuple[TpyType | None, TpyType | None] | None,
3051
+ ) -> TpyType | tuple[TpyType, TpyType]:
3052
+ for cond in gen.conditions:
3053
+ self.analyze_expr(cond)
3054
+ if isinstance(expr, TpyDictComprehension):
3055
+ key_hint, value_hint = hint
3056
+ key_type = (self.analyze_expr_with_hint(expr.key_expr, key_hint)
3057
+ if key_hint else self.analyze_expr(expr.key_expr))
3058
+ value_type = (self.analyze_expr_with_hint(expr.value_expr, value_hint)
3059
+ if value_hint else self.analyze_expr(expr.value_expr))
3060
+ return key_type, value_type
3061
+ if hint is not None:
3062
+ return self.analyze_expr_with_hint(expr.element_expr, hint)
3063
+ return self.analyze_expr(expr.element_expr)
3064
+
3065
+ def _analyze_and_strip(self, expr: TpyExpr, hint: TpyType | None = None) -> TpyType:
3066
+ """Analyze an expression and strip sema-internal wrappers (Own/Ref).
3067
+
3068
+ Used for container literal element types where the declared type
3069
+ should be the user-facing type, not the provenance-tagged expression type.
3070
+ """
3071
+ if hint is not None:
3072
+ self.analyze_expr_with_hint(expr, hint)
3073
+ else:
3074
+ self.analyze_expr(expr)
3075
+ result = self.ctx.get_expr_type(expr)
3076
+ assert result is not None
3077
+ return result
3078
+
3079
+ def _analyze_tuple_literal(
3080
+ self, expr: TpyTupleLiteral, element_hints: list[TpyType | None] | None = None
3081
+ ) -> TupleType:
3082
+ """Analyze a tuple literal (expr, expr, ...)."""
3083
+ elem_types = []
3084
+ for i, elem in enumerate(expr.elements):
3085
+ hint = element_hints[i] if element_hints and i < len(element_hints) else None
3086
+ if hint is not None:
3087
+ analyzed = self.analyze_expr_with_hint(elem, hint)
3088
+ # Preserve Own[] from hint when the analyzed type matches
3089
+ if isinstance(hint, OwnType) and not isinstance(analyzed, OwnType):
3090
+ analyzed = OwnType(analyzed)
3091
+ elem_types.append(analyzed)
3092
+ else:
3093
+ self.analyze_expr(elem)
3094
+ # Use get_expr_type to strip expression-level OwnType/Ref:
3095
+ # the tuple's declared element type should be bare T,
3096
+ # not the provenance-tagged expression type.
3097
+ elem_types.append(self.ctx.get_expr_type(elem))
3098
+ return TupleType(tuple(elem_types))
3099
+
3100
+ def _analyze_tuple_subscript(self, expr: TpySubscript, tuple_type: TupleType) -> TpyType:
3101
+ """Analyze tuple subscript: t[0], t[-1] with compile-time constant index."""
3102
+ index = expr.index
3103
+ n = len(tuple_type.element_types)
3104
+ # Register index type for codegen
3105
+ self.analyze_expr(index)
3106
+ # Extract compile-time index
3107
+ if isinstance(index, TpyIntLiteral):
3108
+ idx = index.value
3109
+ elif (isinstance(index, TpyUnaryOp) and index.op == "-"
3110
+ and isinstance(index.operand, TpyIntLiteral)):
3111
+ idx = -index.operand.value
3112
+ else:
3113
+ raise self.ctx.error(
3114
+ "Tuple index must be a compile-time integer literal", expr
3115
+ )
3116
+ # Resolve negative index
3117
+ original_idx = idx
3118
+ if idx < 0:
3119
+ idx += n
3120
+ # Range check
3121
+ if idx < 0 or idx >= n:
3122
+ raise self.ctx.error(
3123
+ f"Tuple index {original_idx} out of range for "
3124
+ f"tuple[{', '.join(str(t) for t in tuple_type.element_types)}] "
3125
+ f"(length {n})",
3126
+ expr,
3127
+ )
3128
+ return tuple_type.element_types[idx]
3129
+
3130
+ def _analyze_subscript(self, expr: TpySubscript) -> TpyType:
3131
+ """Analyze subscript indexing: obj[index] or slicing: obj[start:stop]"""
3132
+ # Enum name lookup: Color["Red"] -> Color (panics on invalid)
3133
+ if isinstance(expr.obj, TpyName) and self.ctx.func.current_ns:
3134
+ binding = self.ctx.func.current_ns.lookup(expr.obj.name)
3135
+ if binding and binding.kind == BindingKind.ENUM:
3136
+ index_type = self.analyze_expr(expr.index)
3137
+ if not is_any_str_type(index_type):
3138
+ raise self.ctx.error(
3139
+ f"Enum subscript index must be a string, got '{index_type}'",
3140
+ expr,
3141
+ )
3142
+ expr.enum_from_name = binding.enum_type
3143
+ return binding.enum_type
3144
+
3145
+ obj_type = self.analyze_expr(expr.obj)
3146
+
3147
+ # Unwrap transparent wrappers -- Ref/Own don't affect subscript behavior
3148
+ inner_obj_type = unwrap_ref_type(obj_type)
3149
+ if isinstance(inner_obj_type, OwnType):
3150
+ inner_obj_type = inner_obj_type.wrapped
3151
+
3152
+ # Tuple indexing: t[0], t[-1] -- compile-time constant index only
3153
+ actual_for_tuple = unwrap_readonly(inner_obj_type)
3154
+ if isinstance(actual_for_tuple, TupleType):
3155
+ return self._analyze_tuple_subscript(expr, actual_for_tuple)
3156
+
3157
+ # Slice: obj[start:stop]
3158
+ if isinstance(expr.index, TpySlice):
3159
+ return self._analyze_slice(expr, inner_obj_type)
3160
+
3161
+ # TypedDict subscript: d["key"] -> field type (compile-time string literal only)
3162
+ actual_obj = unwrap_readonly(inner_obj_type)
3163
+ if isinstance(actual_obj, NominalType) and actual_obj.is_record:
3164
+ record_info = self.ctx.registry.get_record_for_type(actual_obj)
3165
+ if record_info and record_info.is_typed_dict:
3166
+ if not isinstance(expr.index, TpyStrLiteral):
3167
+ raise self.ctx.error(
3168
+ f"TypedDict '{actual_obj.name}' keys must be string literals", expr.index)
3169
+ key = expr.index.value
3170
+ type_subst = self.type_ops.build_type_substitution(actual_obj)
3171
+ for fld in record_info.fields:
3172
+ if fld.name == key:
3173
+ field_type = fld.type
3174
+ if type_subst:
3175
+ field_type = self.type_ops.substitute_type_params(field_type, type_subst)
3176
+ expr.typed_dict_field = key
3177
+ # total=False: field is Optional[T], unwrap to T with runtime check
3178
+ if isinstance(field_type, OptionalType):
3179
+ expr.typed_dict_optional = True
3180
+ field_type = field_type.inner
3181
+ # Analyze the index expression so its type is recorded
3182
+ self.analyze_expr(expr.index)
3183
+ return make_ref(field_type)
3184
+ raise self.ctx.error(
3185
+ f"TypedDict '{actual_obj.name}' has no key '{key}'", expr.index)
3186
+
3187
+ index_type = self.analyze_expr(expr.index)
3188
+
3189
+ # Dict subscript: d[key] -> V (key can be non-integer)
3190
+ if isinstance(actual_obj, PendingDictType):
3191
+ if not isinstance(actual_obj.key_type, UnknownElementType):
3192
+ self.compat.check_type_compatible(
3193
+ index_type, actual_obj.key_type,
3194
+ f"dict key (expected {actual_obj.key_type})",
3195
+ loc=expr.loc,
3196
+ source_expr=expr.index,
3197
+ )
3198
+ return make_ref(actual_obj.value_type)
3199
+ if is_dict(actual_obj):
3200
+ k_type = actual_obj.type_args[0]
3201
+ v_type = actual_obj.type_args[1]
3202
+ self.compat.check_type_compatible(
3203
+ index_type, k_type,
3204
+ f"dict key (expected {k_type})",
3205
+ loc=expr.loc,
3206
+ source_expr=expr.index,
3207
+ )
3208
+ return make_ref(v_type)
3209
+
3210
+ # Slice-typed variable as index: route through __getitem__ overload
3211
+ # resolution (same path as literal a:b syntax but with variable index).
3212
+ if is_basic_slice_type(index_type) or is_slice_type(index_type):
3213
+ stepped = is_slice_type(index_type)
3214
+ if stepped:
3215
+ expr.is_stepped_slice = True
3216
+ is_readonly = isinstance(inner_obj_type, ReadonlyType)
3217
+ actual_type = unwrap_readonly(inner_obj_type)
3218
+ result = self._find_slice_getitem(actual_type, stepped=stepped, is_readonly=is_readonly)
3219
+ if result is not None:
3220
+ ret, fi = result
3221
+ expr.slice_function_info = fi
3222
+ return ret
3223
+ raise self.ctx.error(f"Slicing is not supported for {inner_obj_type}", expr)
3224
+
3225
+ if not is_any_int_type(index_type):
3226
+ raise self.ctx.error(f"Subscript index must be an integer type, got {index_type}", expr)
3227
+
3228
+ # Check if index is provably in-bounds for bounds check elision
3229
+ self._check_subscript_bounds_safe(expr)
3230
+
3231
+ # Unwrap ReadonlyType, remember the flag
3232
+ is_readonly_obj = isinstance(inner_obj_type, ReadonlyType)
3233
+ actual_type = unwrap_readonly(inner_obj_type)
3234
+
3235
+ # Optional[T] index access uses runtime null checks for unproven access.
3236
+ if isinstance(actual_type, OptionalType):
3237
+ if actual_type.inner.is_value_type():
3238
+ raise self.ctx.error(f"Cannot index type {obj_type}", expr)
3239
+ self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
3240
+ expr.needs_optional_runtime_check = True
3241
+ actual_type = actual_type.inner
3242
+
3243
+ # Use get_element_type() trait for containers and strings
3244
+ elem_type = actual_type.get_element_type()
3245
+ if elem_type is not None:
3246
+ # Subscript on a repeat-sourced pending list needs indexing support
3247
+ # (repeat_range doesn't have operator[], but Array does).
3248
+ if isinstance(actual_type, PendingListType):
3249
+ info = self.ctx.list_literals.get(actual_type.literal_id)
3250
+ if info and isinstance(info.expr, TpyListRepeat):
3251
+ info.needs_indexing = True
3252
+ if is_readonly_obj and not elem_type.is_value_type():
3253
+ elem_type = ReadonlyType(unwrap_readonly(elem_type))
3254
+ return make_ref(elem_type)
3255
+
3256
+ # Protocol types - lookup __getitem__ return type
3257
+ if is_protocol_type(actual_type):
3258
+ ret = self.narrowing._get_protocol_getitem_type(actual_type)
3259
+ if ret is None:
3260
+ raise self.ctx.error(f"Protocol {actual_type.name} does not support indexing", expr)
3261
+ if is_readonly_obj and not ret.is_value_type():
3262
+ ret = ReadonlyType(unwrap_readonly(ret))
3263
+ return make_ref(ret)
3264
+
3265
+ # Records with __getitem__ method
3266
+ if isinstance(actual_type, NominalType) and actual_type.is_record:
3267
+ ret = self.narrowing._get_record_getitem_type(actual_type)
3268
+ if ret is None:
3269
+ raise self.ctx.error(f"Cannot index type {actual_type}: no __getitem__ method", expr)
3270
+ if is_readonly_obj and not ret.is_value_type():
3271
+ ret = ReadonlyType(unwrap_readonly(ret))
3272
+ return make_ref(ret)
3273
+
3274
+ raise self.ctx.error(f"Cannot index type {obj_type}", expr)
3275
+
3276
+ def _analyze_slice(self, expr: TpySubscript, obj_type: TpyType) -> TpyType:
3277
+ """Analyze slice expression: obj[start:stop] or obj[start:stop:step].
3278
+
3279
+ Resolves the __getitem__(basic_slice) or __getitem__(slice) overload
3280
+ on the type (built-in or user-defined) and stores the FunctionInfo
3281
+ on the expression for codegen.
3282
+ """
3283
+ sl = expr.index
3284
+ assert isinstance(sl, TpySlice)
3285
+ stepped = sl.step is not None
3286
+ for bound, label in ((sl.lower, "start"), (sl.upper, "stop"), (sl.step, "step")):
3287
+ if bound is not None:
3288
+ bound_type = self.analyze_expr(bound)
3289
+ if not is_any_int_type(bound_type):
3290
+ raise self.ctx.error(
3291
+ f"Slice {label} must be an integer type, got {bound_type}", bound
3292
+ )
3293
+ if stepped:
3294
+ expr.is_stepped_slice = True
3295
+
3296
+ is_readonly = isinstance(obj_type, ReadonlyType)
3297
+ actual_type = unwrap_readonly(obj_type)
3298
+
3299
+ result = self._find_slice_getitem(actual_type, stepped=stepped, is_readonly=is_readonly)
3300
+ if result is not None:
3301
+ ret, fi = result
3302
+ expr.slice_function_info = fi
3303
+ return ret
3304
+
3305
+ raise self.ctx.error(f"Slicing is not supported for {obj_type}", expr)
3306
+
3307
+ def _find_slice_getitem(self, actual_type: TpyType, *, stepped: bool = False,
3308
+ is_readonly: bool = False) -> tuple[TpyType, 'FunctionInfo'] | None:
3309
+ """Find __getitem__(basic_slice) or __getitem__(slice) overload on any type.
3310
+
3311
+ Works for both built-in types (via qualified_name -> registry) and user records.
3312
+ When stepped=False, looks for basic_slice param first, falls back to slice.
3313
+ When stepped=True, looks for slice param first, falls back to basic_slice.
3314
+ Prefers the const overload when is_readonly=True.
3315
+ Returns (return_type, FunctionInfo) or None.
3316
+ """
3317
+ record = self.ctx.registry.get_record_for_type(actual_type)
3318
+ if record is None:
3319
+ return None
3320
+ getitem_overloads = record.methods.get("__getitem__", [])
3321
+ primary = is_slice_type if stepped else is_basic_slice_type
3322
+ fallback = is_basic_slice_type if stepped else is_slice_type
3323
+ slice_overloads = [
3324
+ fi for fi in getitem_overloads
3325
+ if len(fi.params) == 1 and primary(fi.params[0].type)
3326
+ ]
3327
+ if not slice_overloads:
3328
+ slice_overloads = [
3329
+ fi for fi in getitem_overloads
3330
+ if len(fi.params) == 1 and fallback(fi.params[0].type)
3331
+ ]
3332
+ if not slice_overloads:
3333
+ return None
3334
+ preferred = [fi for fi in slice_overloads if fi.is_readonly == is_readonly]
3335
+ func_info = preferred[0] if preferred else slice_overloads[0]
3336
+ ret = func_info.return_type
3337
+ type_subst = self.type_ops.build_type_substitution(actual_type)
3338
+ if type_subst:
3339
+ ret = self.type_ops.substitute_type_params(ret, type_subst)
3340
+ # Propagate readonly to Span return types (source is readonly or
3341
+ # Span[readonly[T]] -> sliced result should also be readonly)
3342
+ if is_span(ret) and not is_readonly_span(ret):
3343
+ src_readonly = is_span(actual_type) and is_readonly_span(actual_type)
3344
+ if is_readonly or src_readonly:
3345
+ ret = make_span(ret.type_args[0], is_readonly=True)
3346
+ return ret, func_info
3347
+
3348
+ def _find_slice_setitem(self, actual_type: TpyType, *, stepped: bool = False
3349
+ ) -> tuple[TpyType, 'FunctionInfo'] | None:
3350
+ """Find __setitem__(basic_slice, value) or __setitem__(slice, value) overload.
3351
+
3352
+ Similar to _find_slice_getitem but for assignment.
3353
+ No fallback from slice to basic_slice (or vice versa) -- unlike
3354
+ getitem where a basic_slice can promote to slice for reading, assignment
3355
+ semantics differ (stepped requires exact-length match).
3356
+ Returns (value_param_type, FunctionInfo) or None.
3357
+ """
3358
+ record = self.ctx.registry.get_record_for_type(actual_type)
3359
+ if record is None:
3360
+ return None
3361
+ setitem_overloads = record.methods.get("__setitem__", [])
3362
+ target = is_slice_type if stepped else is_basic_slice_type
3363
+ slice_overloads = [
3364
+ fi for fi in setitem_overloads
3365
+ if len(fi.params) == 2 and target(fi.params[0].type)
3366
+ ]
3367
+ if not slice_overloads:
3368
+ return None
3369
+ func_info = slice_overloads[0]
3370
+ value_type = func_info.params[1].type
3371
+ type_subst = self.type_ops.build_type_substitution(actual_type)
3372
+ if type_subst:
3373
+ value_type = self.type_ops.substitute_type_params(value_type, type_subst)
3374
+ return value_type, func_info
3375
+
3376
+ _STRINGABLE = NominalType("Stringable", is_protocol=True)
3377
+ _REPRESENTABLE = NominalType("Representable", is_protocol=True)
3378
+
3379
+ @staticmethod
3380
+ def _is_formattable(t: TpyType) -> bool:
3381
+ """True for types that f-string can format directly (no __str__/__repr__ needed)."""
3382
+ return (is_numeric_type(t) or is_char_type(t) or is_any_str_type(t)
3383
+ or isinstance(t, (IntLiteralType, FloatLiteralType))
3384
+ or is_enum_type(t))
3385
+
3386
+ def _is_fstring_renderable(self, t: TpyType, repr_only: bool = False) -> bool:
3387
+ """True if t can appear in an f-string slot (and is __repr__-able when
3388
+ repr_only). Recurses into UnionType members (codegen dispatches via
3389
+ std::visit to the runtime variant __str__/__repr__ overloads)."""
3390
+ if isinstance(t, UnionType):
3391
+ return all(self._is_fstring_renderable(m, repr_only) for m in t.members)
3392
+ if repr_only:
3393
+ if self.protocols.type_conforms_to_protocol(t, self._REPRESENTABLE):
3394
+ return True
3395
+ # Built-in primitives have runtime __repr__ overloads but no
3396
+ # method-level Representable conformance; treat formattable
3397
+ # types as repr-able too.
3398
+ return self._is_formattable(t)
3399
+ if self._is_formattable(t):
3400
+ return True
3401
+ if self.protocols.type_conforms_to_protocol(t, self._STRINGABLE):
3402
+ return True
3403
+ return self.protocols.type_conforms_to_protocol(t, self._REPRESENTABLE)
3404
+
3405
+ def _analyze_fstring(self, expr: TpyFString, *, for_fstr: bool = False) -> TpyType:
3406
+ """Analyze f-string parts and return STR (owned string).
3407
+
3408
+ Args:
3409
+ for_fstr: If True, only analyze expression types without validating
3410
+ formattability. Used for FStr parameters where the call macro
3411
+ handles per-type dispatch (types don't need __str__/__repr__).
3412
+ """
3413
+ for part in expr.parts:
3414
+ if isinstance(part, TpyFStringValue):
3415
+ part_type = self.analyze_expr(part.expr)
3416
+ if for_fstr:
3417
+ continue
3418
+ resolved = unwrap_readonly(part_type)
3419
+ if isinstance(resolved, OwnType):
3420
+ resolved = resolved.wrapped
3421
+ conv = part.conversion
3422
+
3423
+ if container_to_str_template(resolved) is not None:
3424
+ pass # containers have runtime to_str
3425
+ elif isinstance(resolved, AnyType):
3426
+ pass # Any dispatches via the per-type str/repr ops slot
3427
+ elif conv == FSTRING_CONV_REPR:
3428
+ if not self._is_fstring_renderable(resolved, repr_only=True):
3429
+ raise self.ctx.error(
3430
+ f"Type {part_type} cannot use !r conversion (no __repr__ method)",
3431
+ part.expr,
3432
+ )
3433
+ elif conv == FSTRING_CONV_STR:
3434
+ if not self._is_fstring_renderable(resolved):
3435
+ raise self.ctx.error(
3436
+ f"Type {part_type} cannot use !s conversion"
3437
+ " (no __str__ or __repr__ method)",
3438
+ part.expr,
3439
+ )
3440
+ elif not self._is_fstring_renderable(resolved):
3441
+ raise self.ctx.error(
3442
+ f"Type {part_type} cannot be used in f-string"
3443
+ " (no __str__ or __repr__ method)",
3444
+ part.expr,
3445
+ )
3446
+ if part.format_spec is not None and (is_big_int_type(resolved) or isinstance(resolved, IntLiteralType)):
3447
+ raise self.ctx.error(
3448
+ "Format specs on int are not yet supported (use a fixed-width type like Int32)",
3449
+ part.expr,
3450
+ )
3451
+ return STR
3452
+
3453
+ # --- Lambda expressions ---
3454
+
3455
+ def _analyze_lambda(self, expr: TpyLambda) -> TpyType:
3456
+ """Analyze a lambda without a type hint -- error (types cannot be inferred)."""
3457
+ raise self.ctx.error(
3458
+ "Lambda parameter types cannot be inferred without context. "
3459
+ "Pass the lambda to a function that accepts Fn[...] or Callable[...] type",
3460
+ expr
3461
+ )
3462
+
3463
+ def _analyze_lambda_with_fn_hint(self, expr: TpyLambda, fn_type: CallableType) -> CallableType:
3464
+ """Analyze a lambda with a Fn/Callable type hint providing parameter types."""
3465
+ type_name = "Fn" if is_fn_type(fn_type) else "Callable"
3466
+ if len(expr.param_names) != len(fn_type.param_types):
3467
+ raise self.ctx.error(
3468
+ f"Lambda has {len(expr.param_names)} parameter(s) but "
3469
+ f"{type_name} type expects {len(fn_type.param_types)}",
3470
+ expr
3471
+ )
3472
+
3473
+ expr.inferred_param_types = list(fn_type.param_types)
3474
+
3475
+ # Save outer scope locals for capture filtering
3476
+ outer_locals = set(self.ctx.func.definitely_assigned)
3477
+
3478
+ with self.scopes.lambda_scope() as scope:
3479
+ for pname, ptype in zip(expr.param_names, fn_type.param_types):
3480
+ scope.define(pname, ptype)
3481
+ if self.ctx.func.current_ns:
3482
+ self.ctx.func.current_ns.bind_variable(pname, ptype)
3483
+ self.ctx.func.definitely_assigned.add(pname)
3484
+
3485
+ body_type = self.analyze_expr(expr.body)
3486
+
3487
+ # Detect captures: names in body that are local variables from the outer scope
3488
+ # (not lambda params, not global functions, not builtins)
3489
+ param_set = set(expr.param_names)
3490
+ free_names = collect_name_refs(expr.body)
3491
+ captured = sorted((free_names - param_set) & outer_locals)
3492
+ expr.captured_names = captured
3493
+ # Callable context: captures must be by value (std::function can escape).
3494
+ # Fn (template) stays inline; captures by reference are safe.
3495
+ if isinstance(fn_type, CallableType) and not fn_type.is_template:
3496
+ expr.captures_by_value = True
3497
+
3498
+ # Check return type compatibility (allow implicit coercions like int literal -> Int32)
3499
+ if isinstance(fn_type.return_type, TypeParamRef):
3500
+ # Hint has unresolved type param (e.g. from generic builtin map[T,U]):
3501
+ # use the body's inferred type and return a concrete CallableType.
3502
+ # Resolve IntLiteralType so overload resolution sees a concrete int type.
3503
+ if isinstance(body_type, IntLiteralType):
3504
+ body_type = self.ctx.default_int_for_literal(body_type)
3505
+ expr.inferred_return_type = body_type
3506
+ self.compat.check_view_return_dangle(expr.body, body_type, expr.loc)
3507
+ concrete_params = tuple(fn_type.param_types)
3508
+ if fn_type.is_template:
3509
+ return make_fn_type(concrete_params, body_type)
3510
+ return CallableType(concrete_params, body_type)
3511
+ if body_type != fn_type.return_type:
3512
+ try:
3513
+ self.compat.check_type_compatible(
3514
+ body_type, fn_type.return_type,
3515
+ "lambda return", loc=expr.loc)
3516
+ except SemanticError:
3517
+ raise self.ctx.error(
3518
+ f"Lambda body type '{body_type}' is not compatible with "
3519
+ f"expected return type '{fn_type.return_type}'",
3520
+ expr
3521
+ )
3522
+ self.compat.check_view_return_dangle(expr.body, fn_type.return_type, expr.loc)
3523
+
3524
+ expr.inferred_return_type = fn_type.return_type
3525
+ return fn_type
3526
+
3527
+ # --- Function references ---
3528
+
3529
+ def _try_resolve_function_ref(
3530
+ self, expr: TpyName, hint: CallableType,
3531
+ ) -> CallableType | None:
3532
+ """Try to resolve a name as a function reference matching an Fn/Callable hint.
3533
+
3534
+ Returns a concrete Fn/Callable type if a matching function is found,
3535
+ None to fall through to normal name analysis.
3536
+ """
3537
+ # Look up in namespace -- variables shadow functions
3538
+ if self.ctx.func.current_ns:
3539
+ binding = self.ctx.func.current_ns.lookup(expr.name)
3540
+ if binding:
3541
+ if binding.kind == BindingKind.VARIABLE:
3542
+ return None # local variable shadows any function
3543
+ if binding.kind == BindingKind.FUNCTION and binding.func_infos:
3544
+ matched_data = self._match_function_to_hint_data(
3545
+ binding.func_infos, hint, expr.name, expr)
3546
+ if matched_data is not None:
3547
+ matched, type_args = matched_data
3548
+ if type_args is not None:
3549
+ expr.function_ref_type_args = type_args
3550
+ expr.is_function_ref = True
3551
+ expr.function_ref_info = matched
3552
+ # Escape tracking: passing nested def to Callable (type-erased)
3553
+ # marks it as escaping. Fn (template) stays inline -- no escape.
3554
+ if (isinstance(hint, CallableType) and not hint.is_template
3555
+ and expr.name in self.ctx.func.nested_def_names):
3556
+ self.ctx.func.nested_def_escapes.add(expr.name)
3557
+ return self._concrete_fn_type(matched, expr, hint)
3558
+
3559
+ # Check registry (covers imported functions not yet in namespace)
3560
+ func_infos = self.ctx.registry.get_function(expr.name)
3561
+ if func_infos:
3562
+ matched_data = self._match_function_to_hint_data(
3563
+ func_infos, hint, expr.name, expr)
3564
+ if matched_data is not None:
3565
+ matched, type_args = matched_data
3566
+ if type_args is not None:
3567
+ expr.function_ref_type_args = type_args
3568
+ expr.is_function_ref = True
3569
+ expr.function_ref_info = matched
3570
+ return self._concrete_fn_type(matched, expr, hint)
3571
+
3572
+ return None
3573
+
3574
+ def _concrete_fn_type(
3575
+ self, fi: FunctionInfo, expr: TpyName, hint: CallableType,
3576
+ ) -> CallableType:
3577
+ """Build a concrete Fn/Callable type from a matched function's signature.
3578
+
3579
+ When the hint has TypeParamRef (e.g. from a generic builtin like map[T,U]),
3580
+ returns the concrete type from the function's actual signature so that
3581
+ overload resolution can infer the outer type params.
3582
+ """
3583
+ if not contains_type_param(hint):
3584
+ return hint
3585
+ return self.build_concrete_callable(fi, expr.function_ref_type_args, hint)
3586
+
3587
+ def build_concrete_callable(
3588
+ self, fi: FunctionInfo,
3589
+ type_args: tuple[TpyType, ...] | None,
3590
+ hint: CallableType,
3591
+ ) -> CallableType:
3592
+ """Concrete Fn/Callable from `fi`'s actual signature, optionally
3593
+ substituted with `type_args`.
3594
+
3595
+ Strips Ref from param types and Own from return type -- the Fn
3596
+ type represents the logical callable contract. Ref on return
3597
+ type IS preserved so type inference can track reference
3598
+ semantics through combinators (e.g. map(identity, pts) infers
3599
+ U=Ref[Point] -> val_or_ref<Point>). Shape mirrors `hint` -- Fn
3600
+ if template, Callable otherwise.
3601
+ """
3602
+ param_types = tuple(unwrap_ref_type(ptype) for _, ptype in fi.params)
3603
+ return_type = unwrap_own(fi.return_type)
3604
+ if fi.is_generic() and type_args:
3605
+ subst = dict(zip(fi.type_params, type_args))
3606
+ param_types = tuple(
3607
+ self.type_ops.substitute_type_params(p, subst) for p in param_types
3608
+ )
3609
+ return_type = self.type_ops.substitute_type_params(return_type, subst)
3610
+ if hint.is_template:
3611
+ return make_fn_type(param_types, return_type)
3612
+ return CallableType(param_types, return_type)
3613
+
3614
+ def _match_function_to_hint_data(
3615
+ self, func_infos: list[FunctionInfo], hint: CallableType,
3616
+ name: str, err_node: TpyName,
3617
+ ) -> tuple[FunctionInfo, tuple[TpyType, ...] | None] | None:
3618
+ """Find a function overload matching the Fn/Callable hint signature.
3619
+
3620
+ Pure data lookup: returns ``(matched_fi, inferred_type_args)`` on a
3621
+ unique match (``type_args`` is ``None`` for non-generic matches),
3622
+ ``None`` when no overload matches (callers may treat the name as a
3623
+ variable instead). Raises ``SemanticError`` on ambiguity or
3624
+ generic-rejection -- ``err_node`` provides source location and is
3625
+ not otherwise mutated, so this matcher is safe to use during
3626
+ speculative overload probing.
3627
+
3628
+ Callers that want the AST mutated (``is_function_ref``,
3629
+ ``function_ref_info``, ``function_ref_type_args``) commit those
3630
+ themselves after a winning candidate is chosen.
3631
+ """
3632
+ hint_params = hint.param_types
3633
+ hint_return = hint.return_type
3634
+ # Each candidate is (FunctionInfo, inferred_type_args_or_None)
3635
+ candidates: list[tuple[FunctionInfo, tuple[TpyType, ...] | None]] = []
3636
+ # Track first generic rejection for diagnostics
3637
+ generic_rejection: str | None = None
3638
+ for fi in func_infos:
3639
+ if len(fi.params) != len(hint_params):
3640
+ continue
3641
+ if fi.is_generic():
3642
+ type_args, rejection = self._infer_generic_ref_type_args(fi, hint)
3643
+ if type_args is not None:
3644
+ candidates.append((fi, type_args))
3645
+ elif rejection is not None and generic_rejection is None:
3646
+ generic_rejection = rejection
3647
+ continue
3648
+ match = True
3649
+ for (_, ptype), htype in zip(fi.params, hint_params):
3650
+ if isinstance(htype, TypeParamRef):
3651
+ continue # unresolved type param in hint -- wildcard match
3652
+ if ptype != htype:
3653
+ try:
3654
+ self.compat.check_type_compatible(htype, ptype, "param")
3655
+ except SemanticError:
3656
+ match = False
3657
+ break
3658
+ if not match:
3659
+ continue
3660
+ if fi.return_type != hint_return:
3661
+ if isinstance(hint_return, (VoidType, TypeParamRef)):
3662
+ # VoidType: Python semantics (callers discard the return value)
3663
+ # TypeParamRef: unresolved type param in hint -- wildcard match
3664
+ pass
3665
+ else:
3666
+ try:
3667
+ self.compat.check_type_compatible(fi.return_type, hint_return, "return")
3668
+ except SemanticError:
3669
+ continue
3670
+ candidates.append((fi, None))
3671
+ if len(candidates) == 1:
3672
+ return candidates[0]
3673
+ if len(candidates) > 1:
3674
+ raise self.ctx.error(
3675
+ f"Ambiguous function reference: multiple overloads of '{name}' "
3676
+ f"match {hint}", err_node)
3677
+ # No match -- emit generic rejection diagnostic if we have one
3678
+ if generic_rejection is not None:
3679
+ raise self.ctx.error(generic_rejection, err_node)
3680
+ # Return None to fall through (might be a variable, not a function)
3681
+ return None
3682
+
3683
+ def _infer_generic_ref_type_args(
3684
+ self, fi: FunctionInfo, hint: CallableType,
3685
+ ) -> tuple[tuple[TpyType, ...] | None, str | None]:
3686
+ """Try to infer type parameters for a generic function from an Fn/Callable hint.
3687
+
3688
+ Returns (inferred_type_args, None) on success,
3689
+ (None, rejection_message) on bound or inference failure,
3690
+ (None, None) on type mismatch (not a candidate at all).
3691
+ """
3692
+ inferred: dict[str, TpyType] = {}
3693
+ # Match each function param type against the hint param type
3694
+ for (_, ptype), htype in zip(fi.params, hint.param_types):
3695
+ if not self.type_ops.match_type_with_inference(ptype, htype, inferred):
3696
+ return None, None
3697
+ # Match return type (unless hint is void -- any return is acceptable)
3698
+ if not isinstance(hint.return_type, VoidType):
3699
+ if not self.type_ops.match_type_with_inference(fi.return_type, hint.return_type, inferred):
3700
+ return None, None
3701
+ # Check all type params were inferred
3702
+ unresolved = [tp for tp in fi.type_params if tp not in inferred]
3703
+ if unresolved:
3704
+ return None, (
3705
+ f"Cannot use '{fi.name}' as function reference: "
3706
+ f"cannot infer type parameter(s) {', '.join(unresolved)} "
3707
+ f"from {hint}"
3708
+ )
3709
+ # Validate type parameter bounds
3710
+ for param_name, type_arg in inferred.items():
3711
+ if param_name in fi.type_param_bounds:
3712
+ bound = fi.type_param_bounds[param_name]
3713
+ if not self.protocols.type_conforms_to_protocol(type_arg, bound):
3714
+ return None, (
3715
+ f"Cannot use '{fi.name}' as function reference: "
3716
+ f"inferred type argument {type_arg} for {param_name} "
3717
+ f"does not satisfy bound '{bound.name}'"
3718
+ )
3719
+ # Check for C++ param type mismatch (e.g. str: string_view vs const string&).
3720
+ # Generic functions use param_val_or_ref_t<T> which resolves based on the
3721
+ # storage type, but some types have a different param convention (str uses
3722
+ # string_view). This causes C++ compilation errors when the function is
3723
+ # passed through Fn/Callable.
3724
+ for param_name, type_arg in inferred.items():
3725
+ cpp_storage = type_arg.to_cpp()
3726
+ cpp_param = type_arg.to_cpp_param_type()
3727
+ # param_val_or_ref_t<T> resolves to const T& (value) or T& (object).
3728
+ # Accept T, T&, or const T& -- all compatible with the template.
3729
+ # Reject types with a different convention (e.g. str: string_view).
3730
+ if cpp_param not in (cpp_storage, f"{cpp_storage}&", f"const {cpp_storage}&"):
3731
+ return None, (
3732
+ f"Cannot use '{fi.name}' as function reference with "
3733
+ f"{param_name}={type_arg}: generic functions use a different "
3734
+ f"C++ parameter convention than {type_arg} "
3735
+ f"(use a lambda instead)"
3736
+ )
3737
+ return tuple(inferred[tp] for tp in fi.type_params), None