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,4426 @@
1
+ """
2
+ TurboPython Statement Analysis
3
+
4
+ Statement analysis including variable declarations, assignments, and control flow.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from typing import TYPE_CHECKING
9
+
10
+ from ..typesys import (
11
+ TpyType, IntLiteralType, FloatLiteralType, OwnType, ReadonlyType,
12
+ FinalType,
13
+ PendingListType, PendingDictType, make_list, PendingSetType, PendingStrType, PendingBytesType, PendingViewType, NominalType, TypeParamRef,
14
+ ListLiteralInfo, DictLiteralInfo, SetLiteralInfo, ViewVarInfo, PtrType, is_readonly_ptr, NoneType, OptionalType, AnyType, UnionType, UnknownElementType,
15
+ unwrap_readonly, unwrap_own, unwrap_qualifiers, is_any_str_type, is_any_bytes_type, TupleType, own_tuple_target,
16
+ LiteralType,
17
+ ViewTypeFamily, view_family_for_type,
18
+ PendingGenericInstanceType, contains_fn_type,
19
+ INT32, VOID, BIGINT, FLOAT, STRVIEW, BYTES, BYTESVIEW, BOOL, is_protocol_type, is_protocol_union, final_type_str_to_strview,
20
+ is_final_allowed_inner, FINAL_INNER_TYPE_ERROR,
21
+ qualify_exception_name, is_return_exception, is_exception_type, error_return_matches,
22
+ FunctionInfo, ParamInfo, RecordInfo,
23
+ make_ref, unwrap_ref_type, RefType, param_has_mutable_borrow_surface,
24
+ is_integer_type, is_any_int_type, is_numeric_type, is_readonly_span,
25
+ is_float_type, is_any_float_type, is_polymorphic_subclass_fact,
26
+ resolve_int_literals,
27
+ bare_name)
28
+ from ..parse import (
29
+ TpyExpr,
30
+ TpyStmt, TpyVarDecl, TpyTupleUnpack, TpyAssign, TpyAugAssign, TpyDelItem, TpyDelVar, TpyDelAttr, TpyExprStmt, TpyReturn, TpyYield,
31
+ TpyIf, TpyWhile, TpyForEach, TpyBreak, TpyContinue, TpyAssert,
32
+ TpyRaise, TpyExceptHandler, TpyTry, TpyWith,
33
+ TpyGlobal, TpyNonlocal, TpyNestedDef,
34
+ TpyCall, TpyMethodCall, TpyArrayLiteral, TpyListComprehension, TpyCoerce,
35
+ TpySubscript, TpySlice, TpyStrLiteral, TpyName, TpyTupleLiteral,
36
+ TpyIntLiteral, TpyFloatLiteral, TpyBoolLiteral, TpyUnaryOp,
37
+ TpyNoneLiteral,
38
+ TpyFieldAccess, TpyFunction, TupleElemCapture,
39
+ TpyMatch, TpyBinOp, TpyIfExpr,
40
+ )
41
+ from ..coercions import CoercionContext
42
+ from ..namespace import BindingKind
43
+ from ..symbol_binding import SymbolKind, lookup_imported
44
+ from ..prescan import ScanResult, scan_reassigned_vars, parse_deref_view_key
45
+ from ..liveness import analyze_last_uses
46
+ from ..parse.nodes import VarLinkage
47
+ from .context import addr_taken_roots, expr_yields_non_null_ptr
48
+ from ..diagnostics import SemanticError, NOCOPY_REMEDIATION_HINT
49
+ from .match import MatchAnalyzer
50
+ from .narrowing import NarrowingTracker
51
+ from .scope_tracker import ScopeTracker
52
+ from .init_tracker import InitTracker
53
+ from .value_range import ValueRange
54
+ if TYPE_CHECKING:
55
+ from .context import SemanticContext, BorrowTracker
56
+ from .type_ops import TypeOperations
57
+ from .compatibility import TypeCompatibility
58
+ from .local_deduction import LocalTypeDeduction
59
+ from .list_literals import IterableHelper
60
+ from .expressions import ExpressionAnalyzer
61
+ from .protocols import ProtocolChecker
62
+
63
+ from .context import BorrowKind, MODULE_INIT_CONTEXT, PENDING_CONTAINER_TYPES, _storage_key, _storage_root, _borrow_storage_root
64
+ from .expressions import _collect_body_name_refs, _collect_body_local_defs
65
+ from .local_deduction import collect_pending_source_types
66
+ from tpyc import modules as builtin_modules
67
+ from tpyc import qnames
68
+ from ..type_def_registry import (
69
+ is_dict, is_array, is_span, is_list,
70
+ is_char_type, is_str_type, is_string_type, is_str_view_type,
71
+ is_bytes_type, is_bytearray_type, is_bytes_view_type,
72
+ is_borrowing_view_type,
73
+ is_fixed_int_type, is_big_int_type,
74
+ find_factory_by_simple_name, protocol_info_of,
75
+ )
76
+
77
+
78
+ def _needs_provenance_tracking(t: TpyType) -> bool:
79
+ """Whether a local of this type participates in param-provenance / trusted-call tracking.
80
+
81
+ Non-value types are reference types and always track provenance.
82
+ PtrType is a value type but carries an address. Borrowing views
83
+ (StrView / BytesView / Span / SpanIter) are value types with an
84
+ interior pointer into their source storage. PendingViewType covers
85
+ unresolved str/bytes locals -- if they settle into view semantics
86
+ the tracking is required; if they settle into owned (String/bytes)
87
+ the extra tracking is harmless.
88
+ """
89
+ return (not t.is_value_type()
90
+ or isinstance(t, PtrType)
91
+ or is_borrowing_view_type(t)
92
+ or isinstance(t, PendingViewType))
93
+
94
+
95
+ def _is_dangling_temporary_arg(expr: TpyExpr) -> bool:
96
+ """Check if an expression is a temporary whose storage won't survive.
97
+
98
+ Function calls and binary ops produce temporaries destroyed at
99
+ end-of-statement. Literals (string, int, array, etc.) are either
100
+ static or materialized by codegen into named locals. Names and field
101
+ accesses have addressable storage.
102
+ """
103
+ if isinstance(expr, TpyCoerce):
104
+ return _is_dangling_temporary_arg(expr.expr)
105
+ if isinstance(expr, (TpyCall, TpyMethodCall, TpyBinOp)):
106
+ return True
107
+ if isinstance(expr, TpyIfExpr):
108
+ return (_is_dangling_temporary_arg(expr.then_expr)
109
+ or _is_dangling_temporary_arg(expr.else_expr))
110
+ return False
111
+
112
+
113
+ def _register_call_result_borrow(ctx: SemanticContext, borrower: str, expr: TpyExpr) -> None:
114
+ """Register borrow from function call return value (8b).
115
+
116
+ When a function has return_borrows_from facts, the result variable
117
+ borrows from the indicated argument(s). None means unanalyzed -- skip.
118
+ """
119
+ if isinstance(expr, TpyCall):
120
+ fi = expr.resolved_function_info
121
+ args = expr.args
122
+ obj = None
123
+ elif isinstance(expr, TpyMethodCall):
124
+ fi = expr.resolved_function_info
125
+ args = expr.args
126
+ obj = expr.obj
127
+ elif isinstance(expr, TpyFieldAccess) and expr.property_getter_call is not None:
128
+ # Property getter is a method call; use its return_borrows_from facts
129
+ fi = expr.property_getter_call.resolved_function_info
130
+ args = expr.property_getter_call.args
131
+ obj = expr.property_getter_call.obj
132
+ else:
133
+ return
134
+ if fi is None or fi.return_borrows_from is None:
135
+ return
136
+ bt = ctx.func.borrow_tracker
137
+ for idx in fi.return_borrows_from:
138
+ if idx == -1 and obj is not None:
139
+ root = _borrow_storage_root(obj)
140
+ if root is not None:
141
+ bt.add_borrow(root, borrower, BorrowKind.ELEMENT)
142
+ elif _is_dangling_temporary_arg(obj):
143
+ ctx.warning(
144
+ f"Result borrows from temporary receiver object; "
145
+ f"the temporary is destroyed at end-of-statement",
146
+ expr,
147
+ )
148
+ elif idx >= 0 and idx < len(args):
149
+ root = _borrow_storage_root(args[idx])
150
+ if root is not None:
151
+ bt.add_borrow(root, borrower, BorrowKind.ELEMENT)
152
+ elif _is_dangling_temporary_arg(args[idx]):
153
+ ctx.warning(
154
+ f"Result borrows from temporary argument '{fi.params[idx].name}'; "
155
+ f"the temporary is destroyed at end-of-statement",
156
+ expr,
157
+ )
158
+
159
+
160
+
161
+ def _format_aug_target(target: TpyExpr) -> str:
162
+ """Format an aug-assign target as a short label for error messages.
163
+
164
+ e.g. items[0] -> 'items[...]', obj.x -> 'obj.x', otherwise 'target'.
165
+ """
166
+ if isinstance(target, TpyFieldAccess):
167
+ obj = target.obj.name if isinstance(target.obj, TpyName) else "..."
168
+ return f"'{obj}.{target.field}'"
169
+ if isinstance(target, TpySubscript):
170
+ obj = target.obj.name if isinstance(target.obj, TpyName) else "..."
171
+ return f"'{obj}[...]'"
172
+ return "target"
173
+
174
+
175
+ def _root_name_of_expr(expr: TpyExpr) -> str | None:
176
+ """Extract the root TpyName from a chain of field/subscript accesses.
177
+
178
+ e.g. p.inner.v -> "p", c.items[0] -> "c", x -> "x".
179
+ Returns None for non-name roots (calls, literals, etc.).
180
+
181
+ Unbound-self field access (BaseN.field, set by sema) reports "self"
182
+ since the implicit receiver is `this` -- the syntactic root name is
183
+ the ancestor class, but the mutation travels through self.
184
+ """
185
+ while isinstance(expr, (TpyFieldAccess, TpySubscript)):
186
+ if (isinstance(expr, TpyFieldAccess)
187
+ and expr.unbound_self_parent_type is not None):
188
+ return "self"
189
+ expr = expr.obj
190
+ return expr.name if isinstance(expr, TpyName) else None
191
+
192
+
193
+ def _local_traces_to_self(borrow_tracker: 'BorrowTracker', name: str) -> bool:
194
+ """True when a local borrow-traces to self or a self.<field> storage key.
195
+
196
+ Used to recognize a method call receiver whose root is a local alias of
197
+ self-owned storage (e.g. ``frame = self.frame; frame.get().cancel()``)
198
+ so the call's self-mutation flows back through the enclosing method.
199
+ """
200
+ ultimate = borrow_tracker.effective_storage_through_borrows(name)
201
+ return ultimate == "self" or ultimate.startswith("self.")
202
+
203
+
204
+ def _is_self_call_deferred(
205
+ expr_obj: TpyExpr, obj_root: str | None,
206
+ loop_var_iterable: dict[str, str],
207
+ borrow_tracker: 'BorrowTracker',
208
+ ) -> bool:
209
+ """Check if a method call receiver traces to self through field accesses or loop vars.
210
+
211
+ When True, self-mutation is deferred to Phase 2 via call edges
212
+ (receiver_is_self=True) instead of being marked directly in Phase 1.
213
+ This enables readonly inference for methods that call non-mutating
214
+ methods on fields or loop elements.
215
+ """
216
+ if obj_root == "self":
217
+ # Verify the chain is purely field accesses (no subscripts like
218
+ # self.items[0].method()). _root_name_of_expr strips both FieldAccess
219
+ # and Subscript, so obj_root=="self" doesn't rule out subscripts.
220
+ # Subscript-rooted calls are not deferred because the call edge in
221
+ # calls.py also only walks TpyFieldAccess.
222
+ chain = expr_obj
223
+ while isinstance(chain, TpyFieldAccess):
224
+ chain = chain.obj
225
+ return isinstance(chain, TpyName) and chain.name == "self"
226
+ if obj_root is not None:
227
+ # loop_var.method() where loop_var iterates over self.field
228
+ iterable = loop_var_iterable.get(obj_root)
229
+ if iterable is not None and _storage_root(iterable) == "self":
230
+ return True
231
+ # local.method() where `local = self.<field>` registered a FIELD borrow.
232
+ # Without this, the call is treated as a regular non-self call and the
233
+ # callee's self-mutation never propagates back to the enclosing method,
234
+ # so non-const methods reached through a local alias of a self field
235
+ # leave self_mutated unset and auto-readonly wrongly marks the method
236
+ # const. Deferring lets Phase 2 propagate precisely.
237
+ if _local_traces_to_self(borrow_tracker, obj_root):
238
+ return True
239
+ return False
240
+
241
+
242
+ # view_family_for_type is in typesys (alongside ViewTypeFamily / VIEW_TYPE_FAMILIES).
243
+ # Re-imported at the top of this module.
244
+
245
+
246
+ def _handle_pinned_view_rebind(ctx: SemanticContext, name: str, stmt: TpyStmt) -> None:
247
+ """Handle the two pinned-view tracker effects of rebinding ``name``.
248
+
249
+ 1. If ``name`` is a source with pinned-view borrowers, those views dangle
250
+ (str/bytes is value-type storage with no slot/pointer indirection) --
251
+ warn for each and drop the entry. ``sorted`` for deterministic order.
252
+ 2. If ``name`` is itself a pinned view, remove it from its source's set
253
+ so the source no longer warns about it on subsequent rebinds.
254
+ """
255
+ aliases = ctx.func.pinned_view_aliases
256
+ if not aliases:
257
+ return
258
+ pinned_views = aliases.pop(name, None)
259
+ if pinned_views:
260
+ for view_name in sorted(pinned_views):
261
+ ctx.warning(
262
+ f"Mutation of '{name}' while borrowed"
263
+ f" (reassignment invalidates view '{view_name}')",
264
+ stmt
265
+ )
266
+ for source in list(aliases):
267
+ views = aliases[source]
268
+ if name in views:
269
+ views.discard(name)
270
+ if not views:
271
+ del aliases[source]
272
+
273
+
274
+ class StatementAnalyzer:
275
+ """Statement analysis."""
276
+
277
+ def __init__(
278
+ self,
279
+ ctx: SemanticContext,
280
+ type_ops: TypeOperations,
281
+ compat: TypeCompatibility,
282
+ deduction: LocalTypeDeduction,
283
+ iterable: IterableHelper,
284
+ protocols: ProtocolChecker,
285
+ narrowing: NarrowingTracker,
286
+ expr: ExpressionAnalyzer,
287
+ ):
288
+ self.ctx = ctx
289
+ self.type_ops = type_ops
290
+ self.compat = compat
291
+ self.deduction = deduction
292
+ self.iterable = iterable
293
+ self.protocols = protocols
294
+ self.narrowing = narrowing
295
+ self.scopes = ScopeTracker(ctx, compat)
296
+ self.init = InitTracker(ctx)
297
+ self.expr = expr
298
+ self.match = MatchAnalyzer(ctx, self, expr)
299
+
300
+ def _resolve_obj_storage(self, obj: TpyExpr) -> str | None:
301
+ """Resolve the borrow-tracker storage key for a mutation target's object.
302
+
303
+ For simple names, resolves aliases via effective_storage().
304
+ For single-level field access (self.items), returns the dotted key directly
305
+ (no alias resolution -- field paths are not aliased in the tracker).
306
+ """
307
+ if isinstance(obj, TpyName):
308
+ return self.ctx.func.borrow_tracker.effective_storage(obj.name)
309
+ return _storage_key(obj)
310
+
311
+ def _warn_all_caps_without_final(self, name: str, type_hint: str, node: TpyStmt) -> None:
312
+ """Warn on ALL_CAPS module-level variables without Final annotation."""
313
+ if (not name.startswith("_")
314
+ and name.replace("_", "").isalpha()
315
+ and name == name.upper()
316
+ and len(name) >= 2):
317
+ self.ctx.warning(
318
+ f"ALL_CAPS variable '{name}' without Final annotation; "
319
+ f"use Final[{type_hint}] if this is a constant",
320
+ node
321
+ )
322
+
323
+ def _check_no_bare_async_call(self, stmt: TpyStmt) -> None:
324
+ """Sema rule: a call to an async def whose result is dropped or
325
+ bound to a non-consuming local is rejected. The caller must
326
+ consume the coroutine via `await`, `asyncio.run`, `create_task`,
327
+ or pass it as an arg to a function that does.
328
+
329
+ Rejected (caught at TpyStmt analysis time):
330
+ c = f() (TpyVarDecl / TpyAssign whose RHS is a bare async call)
331
+ f() (TpyExprStmt whose expr is a bare async call)
332
+ return f() (TpyReturn whose value is a bare async call)
333
+ Allowed:
334
+ await f() -- TpyAwait, not TpyCall
335
+ x = await f() -- RHS is TpyAwait
336
+ asyncio.run(f()) -- f() is an arg of another call
337
+ asyncio.create_task(f())
338
+ """
339
+ target_expr: TpyExpr | None = None
340
+ if isinstance(stmt, TpyVarDecl):
341
+ target_expr = stmt.init
342
+ elif isinstance(stmt, TpyAssign):
343
+ target_expr = stmt.value
344
+ elif isinstance(stmt, TpyExprStmt):
345
+ target_expr = stmt.expr
346
+ elif isinstance(stmt, TpyReturn):
347
+ target_expr = stmt.value
348
+ if not isinstance(target_expr, TpyCall):
349
+ return
350
+ func_name = target_expr.maybe_func_name
351
+ if not func_name:
352
+ return
353
+ overloads = self.ctx.registry.get_function(func_name)
354
+ if not any(getattr(fi, "is_async", False) for fi in (overloads or ())):
355
+ return
356
+ raise self.ctx.error(
357
+ f"Coroutine value from async def '{func_name}' must be consumed: "
358
+ f"use `await {func_name}(...)` inside an async def, or "
359
+ f"`asyncio.run({func_name}(...))` at top level. "
360
+ f"Coroutines are single-use, must-use values.",
361
+ stmt)
362
+
363
+ def _warn_unnecessary_return_copy(self, value: TpyExpr) -> None:
364
+ """Warn when return copy(x) is used but x is at last use (auto-move suffices)."""
365
+ if not (isinstance(value, TpyCall) and len(value.args) == 1
366
+ and value.resolved_function_info
367
+ and value.resolved_function_info.qualified_name == "tpy.copy"):
368
+ return
369
+ inner = value.args[0]
370
+ if (isinstance(inner, TpyName)
371
+ and id(inner) in self.ctx.all_last_uses
372
+ and self.compat._is_owned_var(inner.name)):
373
+ self.ctx.warning(
374
+ f"unnecessary copy() -- '{inner.name}' is at its last use and would be moved automatically",
375
+ value,
376
+ )
377
+
378
+ def _warn_str_field_return_copy(self, value: TpyExpr, expected: TpyType) -> None:
379
+ """Warn when a method returns self.field where field is str.
380
+
381
+ This copies the string; suggest StrView (zero-copy view) or String
382
+ (explicit owned) so the user makes an intentional choice.
383
+ Suppressed for dunder methods (__str__, __repr__).
384
+ """
385
+ if not is_str_type(expected):
386
+ return
387
+ func = self.ctx.func.current_function
388
+ if func is None or not func.is_method:
389
+ return
390
+ # Dunder methods (__str__, __repr__) have a fixed str contract
391
+ if func.name.startswith("__") and func.name.endswith("__"):
392
+ return
393
+ inner = value
394
+ while isinstance(inner, TpyCoerce):
395
+ inner = inner.expr
396
+ if not (isinstance(inner, TpyFieldAccess)
397
+ and isinstance(inner.obj, TpyName)
398
+ and inner.obj.name == "self"):
399
+ return
400
+ field_name = inner.field
401
+ self.ctx.warning(
402
+ f"returns a copy of str field 'self.{field_name}'; "
403
+ f"use -> StrView for zero-copy access or -> String to silence this warning",
404
+ value,
405
+ )
406
+
407
+ def _check_own_lvalue_return(self, own_type: OwnType, expr: TpyExpr, context: str) -> None:
408
+ """Check that an lvalue returned as Own[T] has explicit copy() or is auto-moved."""
409
+ self.compat.check_own_lvalue_into_own(own_type, expr, context, action="return")
410
+
411
+ def _is_in_constructor(self) -> bool:
412
+ """Check if currently analyzing an __init__ method body."""
413
+ func = self.ctx.func.current_function
414
+ return (func is not None
415
+ and getattr(func, 'name', None) == "__init__"
416
+ and getattr(func, 'is_method', False))
417
+
418
+ def _annotate_tuple_elem_capture(
419
+ self, literal: TpyTupleLiteral, tuple_type: TupleType,
420
+ *, is_return: bool = False, is_field: bool = False
421
+ ) -> None:
422
+ """Annotate each element of a tuple literal with its capture mode.
423
+
424
+ Args:
425
+ literal: The tuple literal AST node to annotate.
426
+ tuple_type: The resolved TupleType for the literal.
427
+ is_return: True if this literal is in a return statement.
428
+ is_field: True if this literal is assigned to a class field.
429
+ """
430
+ V = TupleElemCapture.VALUE
431
+ R = TupleElemCapture.REF
432
+ CR = TupleElemCapture.CONST_REF
433
+
434
+ is_readonly = (self.ctx.func.current_function is not None
435
+ and getattr(self.ctx.func.current_function, 'is_readonly', False))
436
+
437
+ literal.elem_capture = []
438
+ for i, et in enumerate(tuple_type.element_types):
439
+ if i >= len(literal.elements):
440
+ literal.elem_capture.append(V)
441
+ continue
442
+ elem = literal.elements[i]
443
+
444
+ # Value types, Own[T], and TypeParamRef are always VALUE
445
+ if et.is_value_type() or isinstance(et, (OwnType, TypeParamRef)):
446
+ literal.elem_capture.append(V)
447
+ continue
448
+
449
+ # Field context: all reference-type elements are owned (VALUE)
450
+ # Warn if not an explicit copy() -- same as scalar field assignment
451
+ if is_field:
452
+ elem_val_type = self.ctx.get_raw_expr_type(elem)
453
+ # Last-use of an owned local moves into the slot (matches
454
+ # scalar `self.f = local` auto-move) -- no copy, no warning.
455
+ is_auto_moved = (isinstance(elem, TpyName)
456
+ and id(elem) in self.ctx.all_last_uses
457
+ and self.compat._is_owned_var(elem.name))
458
+ should_warn = False
459
+ if elem_val_type is not None and isinstance(elem_val_type, (RefType, OwnType)):
460
+ if not self.compat.is_copy_call(elem) and not is_auto_moved:
461
+ should_warn = isinstance(elem_val_type, RefType) or isinstance(elem, TpyName)
462
+ elif self._is_non_owned_var_copy(elem, et):
463
+ should_warn = True
464
+ else:
465
+ elem_stripped = self.ctx.get_expr_type(elem)
466
+ if not self.compat.is_copy_call(elem):
467
+ if (isinstance(elem_stripped, OptionalType) and not elem_stripped.inner.is_value_type()):
468
+ should_warn = True
469
+ elif (isinstance(elem_stripped, UnionType) and elem_stripped.uses_pointer_repr()
470
+ and not elem_stripped.needs_wrapper()):
471
+ should_warn = True
472
+ if should_warn:
473
+ if self.ctx.is_type_non_copyable(et):
474
+ raise self.ctx.error(
475
+ f"cannot copy non-copyable type '{et}' into field "
476
+ f"(tuple element {i}){NOCOPY_REMEDIATION_HINT}",
477
+ elem
478
+ )
479
+ self.ctx.warning(
480
+ f"copies {et} into field (tuple element {i}); "
481
+ f"use copy() to make this explicit",
482
+ elem
483
+ )
484
+ literal.elem_capture.append(V)
485
+ continue
486
+
487
+ # Return context
488
+ if is_return:
489
+ if self.compat.is_dangling_return(elem):
490
+ # Will error separately in check_dangling_reference
491
+ literal.elem_capture.append(V)
492
+ elif self.compat.is_const_ref_source(elem):
493
+ if is_readonly:
494
+ literal.elem_capture.append(CR)
495
+ else:
496
+ raise self.ctx.error(
497
+ f"Cannot return readonly source as tuple element {i}. "
498
+ f"Type '{et}' would be returned by mutable reference, "
499
+ f"but the source is readonly. "
500
+ f"Use Own[{et}] with copy() to return by value.",
501
+ elem
502
+ )
503
+ elif is_readonly:
504
+ literal.elem_capture.append(CR)
505
+ else:
506
+ literal.elem_capture.append(R)
507
+ # Returning a non-value element by reference takes its address.
508
+ # Mark source params as needing T& (not const T&).
509
+ for tup_root in addr_taken_roots(elem):
510
+ self.ctx.mark_param_mutated(tup_root)
511
+ continue
512
+
513
+ # Local context: is_const_ref_source handles ReadonlyType
514
+ # (including constructor params which are typed as ReadonlyType)
515
+ # Pointer-form Optional: force REF for all sources (including
516
+ # non-lvalue None) so the tuple has a uniform slot shape. A
517
+ # mixed `(t, None)` would otherwise yield std::tuple<T*,
518
+ # std::optional<T>> and not match a uniformly pointer-form
519
+ # downstream type.
520
+ if isinstance(et, OptionalType) and et.uses_pointer_repr():
521
+ if (not isinstance(elem, TpyNoneLiteral)
522
+ and not self.compat.is_lvalue(elem)
523
+ and self.ctx.is_type_non_copyable(et.inner)):
524
+ raise self.ctx.error(
525
+ f"cannot bind tuple element {i} of non-copyable type "
526
+ f"'{et.inner}' from rvalue. Bind to a local first, or "
527
+ f"place the literal directly at its consumer"
528
+ f"{NOCOPY_REMEDIATION_HINT}",
529
+ elem,
530
+ )
531
+ if is_readonly or self.compat.is_const_ref_source(elem):
532
+ literal.elem_capture.append(CR)
533
+ else:
534
+ literal.elem_capture.append(R)
535
+ continue
536
+ if not self.compat.is_lvalue(elem):
537
+ literal.elem_capture.append(V)
538
+ else:
539
+ # A ref-tuple of a non-copyable element (`tuple<T&, ...>`) has
540
+ # no path to a value tuple later -- the C++ conversion fails
541
+ # with cryptic <tuple> template errors. Reject with a clean
542
+ # diagnostic; users either annotate the local for ownership
543
+ # transfer or place the literal directly at its consumer.
544
+ if self.ctx.is_type_non_copyable(et):
545
+ raise self.ctx.error(
546
+ f"cannot bind tuple element {i} of non-copyable type "
547
+ f"'{et}' by reference. Annotate the local as "
548
+ f"'tuple[Own[{et}], ...]' to consume the source, or "
549
+ f"place the literal directly at its consumer (return, "
550
+ f"call, outer literal) without an intermediate "
551
+ f"variable{NOCOPY_REMEDIATION_HINT}",
552
+ elem,
553
+ )
554
+ if self.compat.is_const_ref_source(elem):
555
+ literal.elem_capture.append(CR)
556
+ else:
557
+ literal.elem_capture.append(R)
558
+
559
+ def _save_ns_var_types(self) -> dict[str, TpyType]:
560
+ """Save namespace variable types for later restoration."""
561
+ result: dict[str, TpyType] = {}
562
+ if self.ctx.func.current_ns:
563
+ for name, binding in self.ctx.func.current_ns.all_bindings().items():
564
+ if binding.kind == BindingKind.VARIABLE:
565
+ result[name] = binding.type
566
+ return result
567
+
568
+ def _restore_ns_var_types(self, saved: dict[str, TpyType]) -> None:
569
+ """Restore namespace variable types from a saved snapshot."""
570
+ if self.ctx.func.current_ns:
571
+ for name, typ in saved.items():
572
+ self.ctx.func.current_ns.update_variable_type(name, typ)
573
+
574
+ def _sync_ns_var_type(self, name: str, typ: TpyType) -> None:
575
+ """Sync a single variable's namespace type to match scope."""
576
+ if self.ctx.func.current_ns:
577
+ self.ctx.func.current_ns.update_variable_type(name, typ)
578
+
579
+ def _sync_promoted_var_types(self, names: set[str] | None = None) -> None:
580
+ """Sync scope and namespace with var_types after a control-flow restore.
581
+
582
+ After restoring scope/namespace to a pre-block state (if-branch,
583
+ while, for-each), variables whose canonical declaration type was
584
+ widened inside the block (e.g., Int32 promoted to BigInt, or None
585
+ promoted to Optional[T]) need to be re-synced so that subsequent
586
+ analysis sees the correct type.
587
+
588
+ Args:
589
+ names: Variable names to check. If None, checks all tracked
590
+ variable declarations in the current function.
591
+ """
592
+ items = (
593
+ self.ctx.func.var_decl_by_name.items() if names is None
594
+ else ((n, self.ctx.func.var_decl_by_name[n]) for n in names if n in self.ctx.func.var_decl_by_name)
595
+ )
596
+ for name, var_decl in items:
597
+ canonical = self.ctx.var_types.get(id(var_decl))
598
+ if canonical is None:
599
+ continue
600
+ current = self.ctx.func.current_scope.lookup(name)
601
+ # Preserve ReadonlyType from branch merge: var_types stores
602
+ # unwrapped types, so re-wrap with ReadonlyType if the merge
603
+ # determined this variable should be readonly.
604
+ if isinstance(current, ReadonlyType):
605
+ if unwrap_readonly(current) == canonical:
606
+ continue
607
+ target = ReadonlyType(canonical)
608
+ else:
609
+ target = canonical
610
+ if target != current:
611
+ self.ctx.func.current_scope.define(name, target)
612
+ self._sync_ns_var_type(name, target)
613
+
614
+ def _is_class_name_receiver(self, expr: TpyExpr) -> bool:
615
+ """True when `expr` is a class-name reference (e.g. `MyClass`), not
616
+ an instance reference (`self`, `obj`, `f()`, ...). Mirrors the
617
+ binding-kind discrimination in `_try_class_constant_access`.
618
+ """
619
+ if not isinstance(expr, TpyName):
620
+ return False
621
+ ns = self.ctx.func.current_ns
622
+ if ns is None:
623
+ return False
624
+ binding = ns.lookup(expr.name)
625
+ if binding is None:
626
+ return False
627
+ if binding.kind == BindingKind.RECORD:
628
+ return True
629
+ if binding.kind == BindingKind.IMPORTED_NAME:
630
+ import_info = self.ctx.imported_names.get(expr.name)
631
+ if import_info is None:
632
+ return False
633
+ return bool(self.ctx.registry.find_record_by_qname(
634
+ f"{import_info[0]}.{import_info[1]}"))
635
+ return False
636
+
637
+ def _check_class_constant_write(self, target: TpyExpr, stmt: TpyStmt) -> None:
638
+ """Reject `=` / `+=` on Final class constants; warn on instance-side
639
+ writes to mutable ClassVar (CPython would create an instance attribute
640
+ instead of writing through to class storage -- mypy/pyright already
641
+ flag this; matching them keeps the tooling story consistent).
642
+ """
643
+ if not isinstance(target, TpyFieldAccess):
644
+ return
645
+ owner = target.class_constant_owner
646
+ if owner is None:
647
+ return
648
+ if owner.is_final_class_constant(target.field):
649
+ raise self.ctx.error(
650
+ f"Cannot reassign Final class constant "
651
+ f"'{owner.name}.{target.field}'",
652
+ stmt,
653
+ )
654
+ if not self._is_class_name_receiver(target.obj):
655
+ self.ctx.warning(
656
+ f"Assigning to ClassVar '{owner.name}.{target.field}' via "
657
+ f"instance writes to class storage in TPy (CPython creates "
658
+ f"an instance attribute); use "
659
+ f"'{owner.name}.{target.field} = ...' for portability",
660
+ stmt,
661
+ )
662
+
663
+ def _enforce_readonly_assignment_target(self, target: TpyExpr) -> None:
664
+ """Reject assignments through readonly references, frozen fields, and readonly field declarations."""
665
+ # ClassVar writes always go to class-scoped `static inline` storage,
666
+ # never to instance fields, so receiver const-ness/freezing is
667
+ # irrelevant -- the write doesn't touch the instance.
668
+ if (isinstance(target, TpyFieldAccess)
669
+ and target.class_constant_owner is not None):
670
+ return
671
+ # BaseN.field = v goes through `this`, but the syntactic receiver
672
+ # (BaseN) has no value type, so the obj_type branch below can't
673
+ # catch it -- gate on the current method's @readonly flag directly.
674
+ if (isinstance(target, TpyFieldAccess)
675
+ and target.unbound_self_parent_type is not None):
676
+ cur = self.ctx.func.current_function
677
+ if isinstance(cur, TpyFunction) and cur.is_readonly:
678
+ raise self.ctx.error("Cannot mutate readonly reference", target)
679
+ if isinstance(target, (TpyFieldAccess, TpySubscript)):
680
+ obj_type = self.ctx.get_expr_type(target.obj)
681
+ if obj_type is not None:
682
+ check_type = obj_type
683
+ if isinstance(check_type, OptionalType):
684
+ check_type = check_type.inner
685
+ if isinstance(check_type, ReadonlyType):
686
+ raise self.ctx.error("Cannot mutate readonly reference", target)
687
+ # isinstance narrowing strips ReadonlyType from the expr type;
688
+ # the scope binding preserves it, so check there.
689
+ if isinstance(target.obj, TpyName) and self.ctx.is_readonly_name(target.obj.name):
690
+ raise self.ctx.error("Cannot mutate readonly reference", target)
691
+ # Frozen dataclass / readonly field: reject assignment except self.field in __init__
692
+ if isinstance(target, TpyFieldAccess):
693
+ actual = unwrap_readonly(check_type)
694
+ if isinstance(actual, NominalType):
695
+ info = self.ctx.registry.get_record(actual.name)
696
+ if info is not None:
697
+ cur = self.ctx.func.current_function
698
+ rec = self.ctx.record_ctx.record
699
+ in_own_init = (
700
+ isinstance(cur, TpyFunction) and cur.name == "__init__"
701
+ and isinstance(target.obj, TpyName) and target.obj.name == "self"
702
+ and rec is not None and rec.name == actual.name
703
+ )
704
+ if info.is_frozen and not in_own_init:
705
+ raise self.ctx.error(
706
+ f"Cannot assign to field '{target.field}' of frozen dataclass '{actual.name}'",
707
+ target,
708
+ )
709
+ # Walk the class hierarchy to find readonly fields
710
+ # (own and inherited). __init__ of the declaring
711
+ # class or any subclass is exempt.
712
+ in_any_init = (
713
+ isinstance(cur, TpyFunction) and cur.name == "__init__"
714
+ and isinstance(target.obj, TpyName) and target.obj.name == "self"
715
+ and rec is not None
716
+ )
717
+ if not in_any_init:
718
+ # Walk self + MRO ancestors for a readonly field declaration.
719
+ records_to_check = [info, *self.ctx.registry.iter_ancestor_records(info)]
720
+ for check in records_to_check:
721
+ for fld in check.fields:
722
+ if fld.name == target.field and isinstance(fld.type, ReadonlyType):
723
+ raise self.ctx.error(
724
+ f"Cannot assign to readonly field '{target.field}'",
725
+ target,
726
+ )
727
+
728
+ def _find_consuming_iter(self, iterable_type: TpyType) -> FunctionInfo | None:
729
+ """Find the consuming __iter__ overload for a type, if any.
730
+
731
+ Only matches concrete types (list, user records with auto_own __iter__).
732
+ Skips pending/unresolved types to avoid mismatched codegen.
733
+ """
734
+ # Only match concrete types, not pending/unresolved
735
+ if isinstance(iterable_type, (PendingListType, PendingGenericInstanceType)):
736
+ return None
737
+ # Value-type elements: moving is identical to copying, so consuming
738
+ # the container's internal structure is pure overhead.
739
+ elem = builtin_modules.get_iterable_element_type(iterable_type, registry=self.ctx.registry)
740
+ if elem is not None and elem.is_value_type():
741
+ return None
742
+ record_info = self.ctx.registry.get_record_for_type(iterable_type)
743
+ if record_info is None:
744
+ return None
745
+ overloads = record_info.get_method_overloads("__iter__")
746
+ for fi in overloads:
747
+ if fi.is_consuming:
748
+ # Build type substitution for generic types
749
+ type_subst = self.type_ops.build_type_substitution(iterable_type)
750
+ if type_subst:
751
+ fi = self.type_ops.substitute_method_type_params(fi, type_subst)
752
+ return fi
753
+ return None
754
+
755
+ def _resolve_enum_iterable(self, stmt: TpyForEach) -> NominalType | None:
756
+ """Check if for-each iterates over an enum type (e.g. `for c in Color`).
757
+
758
+ Returns the enum NominalType if so, None otherwise.
759
+ """
760
+ iterable = stmt.iterable
761
+ if not isinstance(iterable, TpyName):
762
+ return None
763
+ binding = self.ctx.func.current_ns.lookup(iterable.name) if self.ctx.func.current_ns else None
764
+ if binding is None:
765
+ return None
766
+ if binding.kind == BindingKind.ENUM and binding.enum_type is not None:
767
+ return binding.enum_type
768
+ if binding.kind == BindingKind.IMPORTED_NAME and binding.import_source:
769
+ src_mod, original_name = binding.import_source
770
+ enum_type = self.ctx.registry.get_enum(original_name)
771
+ if enum_type is not None:
772
+ return enum_type
773
+ return None
774
+
775
+ def analyze_stmt(self, stmt: TpyStmt) -> None:
776
+ """Analyze a statement."""
777
+ try:
778
+ self._analyze_stmt_dispatch(stmt)
779
+ finally:
780
+ # Flush post-access ptr narrowing queued during expression analysis.
781
+ # See FunctionTrackingState.pending_non_null_ptr_vars for rationale
782
+ # (deferred to statement boundary so within-statement sibling
783
+ # accesses keep their individual null checks).
784
+ pending = self.ctx.func.pending_non_null_ptr_vars
785
+ if pending:
786
+ self.ctx.func.non_null_ptr_vars |= pending
787
+ pending.clear()
788
+
789
+ def _analyze_stmt_dispatch(self, stmt: TpyStmt) -> None:
790
+ # Coroutine[T] single-use: a bare call to an async def whose
791
+ # result is dropped/stored without consumption is a sema error.
792
+ # See docs/ASYNC_DESIGN.md "Coroutine value model". Allowed:
793
+ # await f() (TpyAwait wraps the call)
794
+ # x = await f()
795
+ # asyncio.run(f()), asyncio.create_task(f()), Task wrap (the
796
+ # coro is the arg of another call, which consumes it)
797
+ # Rejected:
798
+ # c = f() (drops the coro into a non-consuming local)
799
+ # f() (statement-level call, no consumption)
800
+ # return f() (returns the coro instead of awaiting)
801
+ self._check_no_bare_async_call(stmt)
802
+
803
+ if isinstance(stmt, TpyVarDecl):
804
+ self._analyze_var_decl(stmt)
805
+ elif isinstance(stmt, TpyTupleUnpack):
806
+ self._analyze_tuple_unpack(stmt)
807
+ elif isinstance(stmt, TpyAssign):
808
+ self._analyze_assign(stmt)
809
+ elif isinstance(stmt, TpyAugAssign):
810
+ self._analyze_aug_assign(stmt)
811
+ elif isinstance(stmt, TpyDelItem):
812
+ self._analyze_del_item(stmt)
813
+ elif isinstance(stmt, TpyDelVar):
814
+ self._analyze_del_var(stmt)
815
+ elif isinstance(stmt, TpyDelAttr):
816
+ self._analyze_del_attr(stmt)
817
+ elif isinstance(stmt, TpyExprStmt):
818
+ self.expr.analyze_expr(stmt.expr)
819
+ elif isinstance(stmt, TpyReturn):
820
+ if stmt.value:
821
+ expected = unwrap_ref_type(self.ctx.func.current_function.return_type) if self.ctx.func.current_function else VOID
822
+ ret_type = self.expr.analyze_expr_with_hint(stmt.value, expected)
823
+ stmt.value_type = ret_type
824
+ stmt.value = self.compat.coerce_expr(stmt.value, ret_type, expected, "return value",
825
+ coercion_ctx=CoercionContext.RETURN, is_return=True)
826
+ # Track return-type context for pending list deduction
827
+ self.deduction.mark_list_return_context(stmt.value, expected)
828
+ # Warn when a method copies a str field on return
829
+ self._warn_str_field_return_copy(stmt.value, expected)
830
+ # Check for lvalue returned as Own[T] without explicit copy()
831
+ if isinstance(expected, OwnType):
832
+ if self.compat.is_copy_call(stmt.value):
833
+ self._warn_unnecessary_return_copy(stmt.value)
834
+ else:
835
+ self._check_own_lvalue_return(expected, stmt.value, "return type")
836
+ # Check Own[T] elements in tuple literals.
837
+ if isinstance(stmt.value, TpyTupleLiteral):
838
+ tuple_target = own_tuple_target(expected)
839
+ if tuple_target is not None:
840
+ for i, et in enumerate(tuple_target.element_types):
841
+ if isinstance(et, OwnType) and i < len(stmt.value.elements):
842
+ self._check_own_lvalue_return(et, stmt.value.elements[i],
843
+ f"tuple element {i}")
844
+ # Annotate per-element capture mode (ref/value/const_ref)
845
+ self._annotate_tuple_elem_capture(
846
+ stmt.value, tuple_target, is_return=True)
847
+ # Check for dangling reference (returning local/temporary as reference)
848
+ self.compat.check_dangling_reference(stmt.value, expected, stmt.loc)
849
+ # Returning a non-value type by reference takes the source's address.
850
+ # Mark both loop vars and params so they keep T& (not const T&/const T*).
851
+ if isinstance(stmt.value, TpyName):
852
+ self.ctx.mark_loop_var_mutated(stmt.value.name)
853
+ # Escape tracking: returning a nested def marks it as escaping
854
+ if stmt.value.name in self.ctx.func.nested_def_names:
855
+ self.ctx.func.nested_def_escapes.add(stmt.value.name)
856
+ # Returning a borrowing view does not give the caller write
857
+ # access to the source, so we record borrow provenance but
858
+ # must not raise self_mutated -- that would suppress auto-const
859
+ # on the enclosing method and break const callers.
860
+ if expected is not None:
861
+ returns_borrowing_view = is_borrowing_view_type(expected)
862
+ if not expected.is_value_type() or returns_borrowing_view:
863
+ for ret_root in addr_taken_roots(stmt.value):
864
+ if not returns_borrowing_view:
865
+ self.ctx.mark_param_mutated(ret_root)
866
+ self.ctx.mark_param_returned(ret_root) # 8b: track which param storage the return borrows
867
+ # 8b rule 3: transitive return -- if returning the result of a call
868
+ # whose return_borrows_from is known, propagate the borrow contract.
869
+ # e.g. `return inner(items)` where inner borrows param 0 -> mark items.
870
+ ret_inner = stmt.value.expr if isinstance(stmt.value, TpyCoerce) else stmt.value
871
+ if isinstance(ret_inner, (TpyCall, TpyMethodCall)):
872
+ fi_ret = ret_inner.resolved_function_info
873
+ if fi_ret is not None and fi_ret.return_borrows_from:
874
+ ret_args = ret_inner.args
875
+ ret_obj = getattr(ret_inner, 'obj', None)
876
+ for idx in fi_ret.return_borrows_from:
877
+ if idx == -1 and ret_obj is not None:
878
+ src = _borrow_storage_root(ret_obj)
879
+ elif idx >= 0 and idx < len(ret_args):
880
+ src = _borrow_storage_root(ret_args[idx])
881
+ else:
882
+ src = None
883
+ if src is not None:
884
+ # Read-only sources (readonly callees, view returns)
885
+ # don't propagate mutation to their borrowed-from arg.
886
+ if not returns_borrowing_view and not fi_ret.is_readonly:
887
+ self.ctx.mark_param_mutated(src)
888
+ self.ctx.mark_param_returned(src)
889
+ self.init.mark_terminated()
890
+ elif isinstance(stmt, TpyYield):
891
+ self._analyze_yield(stmt)
892
+ elif isinstance(stmt, TpyIf):
893
+ assigned_before_cond = frozenset(self.ctx.func.definitely_assigned)
894
+ saved_sc_and = self.ctx.sc_and_walrus.copy()
895
+ saved_sc_or = self.ctx.sc_or_walrus.copy()
896
+ self.ctx.sc_and_walrus = set()
897
+ self.ctx.sc_or_walrus = set()
898
+ self.expr.analyze_expr(stmt.condition)
899
+ always_walrus = self.ctx.func.definitely_assigned - assigned_before_cond
900
+ sc_and = self.ctx.sc_and_walrus # safe in then-body
901
+ sc_or = self.ctx.sc_or_walrus # safe in else-body
902
+ self.ctx.sc_and_walrus = saved_sc_and
903
+ self.ctx.sc_or_walrus = saved_sc_or
904
+ self.narrowing.warn_truthy_value_optionals(stmt.condition)
905
+ then_type_facts, else_type_facts = self.narrowing.condition_type_facts(stmt.condition)
906
+ ptr_nn_then, ptr_nn_else = self.narrowing.condition_ptr_null_facts(stmt.condition)
907
+ range_true, range_false = self.narrowing.condition_range_facts(stmt.condition)
908
+ stmt.then_type_facts = self._filter_union_codegen_facts(then_type_facts)
909
+ stmt.else_type_facts = self._filter_union_codegen_facts(else_type_facts)
910
+ scope_before = set(self.ctx.func.current_scope.bindings.keys())
911
+ assigned_before = frozenset(self.ctx.func.definitely_assigned)
912
+ before = self.init.save()
913
+ consumed_before = self.ctx.func.current_consumed_own_params.copy()
914
+ # Save binding types for ReadonlyType merge after branches
915
+ bindings_before = dict(self.ctx.func.current_scope.bindings)
916
+ ns_types_before = self._save_ns_var_types()
917
+ # Then-body: condition was true -> && operands all evaluated,
918
+ # but || RHS may have been skipped (LHS alone was truthy)
919
+ self.ctx.func.definitely_assigned |= always_walrus | sc_and
920
+ self.ctx.func.narrowed_types.update(then_type_facts)
921
+ self.ctx.func.non_null_ptr_vars |= ptr_nn_then
922
+ self._apply_range_facts(range_true)
923
+ for s in stmt.then_body:
924
+ self.analyze_stmt(s)
925
+ then_state = self.init.save()
926
+ consumed_after_then = self.ctx.func.current_consumed_own_params.copy()
927
+ then_terminated = self.ctx.func.init_terminated
928
+ bindings_after_then = dict(self.ctx.func.current_scope.bindings)
929
+ # Restore bindings for else branch
930
+ self.ctx.func.current_scope.bindings.update(bindings_before)
931
+ self._restore_ns_var_types(ns_types_before)
932
+ self.init.restore(before)
933
+ # Else-body: condition was false -> || operands all evaluated,
934
+ # but && RHS may have been skipped (LHS alone was falsy)
935
+ self.ctx.func.definitely_assigned |= always_walrus | sc_or
936
+ self.ctx.func.current_consumed_own_params = consumed_before.copy()
937
+ self.ctx.func.narrowed_types.update(else_type_facts)
938
+ self.ctx.func.non_null_ptr_vars |= ptr_nn_else
939
+ self._apply_range_facts(range_false)
940
+ for s in stmt.else_body:
941
+ self.analyze_stmt(s)
942
+ else_state = self.init.save()
943
+ consumed_after_else = self.ctx.func.current_consumed_own_params.copy()
944
+ else_terminated = self.ctx.func.init_terminated
945
+ bindings_after_else = dict(self.ctx.func.current_scope.bindings)
946
+ self.init.merge_branches(then_state, else_state)
947
+ # Merge consumed Own[T] params: must be consumed on ALL non-terminated paths
948
+ if then_terminated and else_terminated:
949
+ self.ctx.func.current_consumed_own_params = consumed_after_then | consumed_after_else
950
+ elif then_terminated:
951
+ self.ctx.func.current_consumed_own_params = consumed_after_else
952
+ elif else_terminated:
953
+ self.ctx.func.current_consumed_own_params = consumed_after_then
954
+ else:
955
+ self.ctx.func.current_consumed_own_params = consumed_after_then & consumed_after_else
956
+ # Merge ReadonlyType: if readonly on EITHER branch, keep readonly
957
+ for name in set(bindings_after_then) | set(bindings_after_else):
958
+ then_type = bindings_after_then.get(name)
959
+ else_type = bindings_after_else.get(name)
960
+ if then_type is not None and else_type is not None:
961
+ then_is_ro = isinstance(then_type, ReadonlyType)
962
+ else_is_ro = isinstance(else_type, ReadonlyType)
963
+ if then_is_ro and not else_is_ro:
964
+ merged = ReadonlyType(unwrap_readonly(else_type))
965
+ self.ctx.func.current_scope.define(name, merged)
966
+ self._sync_ns_var_type(name, merged)
967
+ elif else_is_ro and not then_is_ro:
968
+ merged = ReadonlyType(unwrap_readonly(then_type))
969
+ self.ctx.func.current_scope.define(name, merged)
970
+ self._sync_ns_var_type(name, merged)
971
+ # Sync scope/namespace with var_types for variables whose
972
+ # declaration type was promoted inside a branch.
973
+ self._sync_promoted_var_types(
974
+ set(bindings_after_then) | set(bindings_after_else)
975
+ )
976
+ # Detect variables first declared inside branches that need
977
+ # pre-declaration. Skip when both branches terminate (no code
978
+ # after the if needs the variable).
979
+ if not self.ctx.func.init_terminated:
980
+ branch_new = set(self.ctx.func.current_scope.bindings.keys()) - scope_before
981
+ newly_assigned = self.ctx.func.definitely_assigned - assigned_before
982
+ predecl = (branch_new & newly_assigned) - self.ctx.func.global_declarations
983
+ else:
984
+ predecl = set()
985
+ if predecl:
986
+ self.ctx.if_branch_decls[id(stmt)] = {
987
+ name: self.ctx.func.current_scope.lookup(name)
988
+ for name in sorted(predecl)
989
+ }
990
+ elif isinstance(stmt, TpyWhile):
991
+ assigned_before_cond = frozenset(self.ctx.func.definitely_assigned)
992
+ saved_sc_and = self.ctx.sc_and_walrus.copy()
993
+ saved_sc_or = self.ctx.sc_or_walrus.copy()
994
+ self.ctx.sc_and_walrus = set()
995
+ self.ctx.sc_or_walrus = set()
996
+ self.expr.analyze_expr(stmt.condition)
997
+ always_walrus_w = self.ctx.func.definitely_assigned - assigned_before_cond
998
+ all_walrus_w = always_walrus_w | self.ctx.sc_and_walrus | self.ctx.sc_or_walrus
999
+ self.ctx.sc_and_walrus = saved_sc_and
1000
+ self.ctx.sc_or_walrus = saved_sc_or
1001
+ self.narrowing.warn_truthy_value_optionals(stmt.condition)
1002
+ then_type_facts, _ = self.narrowing.condition_type_facts(stmt.condition)
1003
+ ptr_nn, _ = self.narrowing.condition_ptr_null_facts(stmt.condition)
1004
+ range_true, _ = self.narrowing.condition_range_facts(stmt.condition)
1005
+ stmt.then_type_facts = self._filter_union_codegen_facts(then_type_facts)
1006
+ before = self.init.save()
1007
+ consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
1008
+ # Save namespace types -- loop_scope() restores scope bindings
1009
+ # automatically, but namespace mutations inside the loop persist.
1010
+ ns_types_before_while = self._save_ns_var_types()
1011
+ with self.scopes.loop_scope():
1012
+ self.init.apply_loop_entry_facts(
1013
+ before,
1014
+ condition_type_facts=then_type_facts,
1015
+ )
1016
+ # Applied separately from apply_loop_entry_facts because
1017
+ # that method only handles type narrowing, not ptr non-null.
1018
+ self.ctx.func.non_null_ptr_vars |= ptr_nn
1019
+ self._apply_range_facts(range_true)
1020
+ for s in stmt.body:
1021
+ self.analyze_stmt(s)
1022
+ self.init.apply_loop_exit_facts(before)
1023
+ # Re-add all walrus vars (condition always evaluates fully)
1024
+ self.ctx.func.definitely_assigned |= all_walrus_w
1025
+ # Loop might not execute — consumption inside is not definite
1026
+ self.ctx.func.current_consumed_own_params = consumed_before_loop
1027
+ # Restore namespace to pre-loop state (scope was already restored
1028
+ # by loop_scope context manager)
1029
+ self._restore_ns_var_types(ns_types_before_while)
1030
+ self._sync_promoted_var_types()
1031
+ for s in stmt.orelse:
1032
+ self.analyze_stmt(s)
1033
+ elif isinstance(stmt, TpyForEach):
1034
+ if stmt.is_async:
1035
+ self._analyze_async_for(stmt)
1036
+ return
1037
+ # Check for enum iteration: `for c in Color`
1038
+ enum_type = self._resolve_enum_iterable(stmt)
1039
+ if enum_type is not None:
1040
+ stmt.enum_iterable = enum_type
1041
+ elem_type = enum_type
1042
+ self._record_for_loop_var_type(stmt, elem_type)
1043
+ before = self.init.save()
1044
+ consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
1045
+ ns_types_before_foreach = self._save_ns_var_types()
1046
+ with self.scopes.loop_scope() as inner_scope:
1047
+ self.init.apply_loop_entry_facts(before)
1048
+ with self.scopes.loop_var(inner_scope, stmt.var, elem_type, inner_scope.depth, is_foreach=True):
1049
+ for s in stmt.body:
1050
+ self.analyze_stmt(s)
1051
+ self.init.apply_loop_exit_facts(before)
1052
+ self.ctx.func.current_consumed_own_params = consumed_before_loop
1053
+ self._restore_ns_var_types(ns_types_before_foreach)
1054
+ self._sync_promoted_var_types()
1055
+ self._propagate_for_loop_scope(stmt, inner_scope, elem_type)
1056
+ for s in stmt.orelse:
1057
+ self.analyze_stmt(s)
1058
+ else:
1059
+ iterable_type = self.expr.analyze_expr(stmt.iterable)
1060
+ is_readonly_iterable = isinstance(iterable_type, ReadonlyType)
1061
+ inner_iterable_type = unwrap_readonly(unwrap_own(unwrap_ref_type(iterable_type)))
1062
+ # Resolve TypeParamRef to its bound for element type extraction
1063
+ resolved_for_iter = inner_iterable_type
1064
+ if isinstance(inner_iterable_type, TypeParamRef):
1065
+ bound = self.type_ops.get_type_param_bound(inner_iterable_type.name)
1066
+ if bound is not None and is_protocol_type(bound):
1067
+ resolved_for_iter = bound
1068
+ elem_type = self.iterable.get_iterable_element_type(resolved_for_iter, loc=stmt.loc)
1069
+ # Elements from a readonly iterable inherit readonly status
1070
+ if is_readonly_iterable and not elem_type.is_value_type():
1071
+ elem_type = ReadonlyType(unwrap_readonly(elem_type))
1072
+ elem_type = self._infer_new_local_type(
1073
+ stmt.var, elem_type, None, None,
1074
+ line=(stmt.loc.line if stmt.loc else None),
1075
+ )
1076
+ stmt.elem_type = make_ref(elem_type)
1077
+
1078
+ # Auto-consuming decision is deferred until after body analysis
1079
+ # (see below) so we know whether the loop var is mutated.
1080
+
1081
+ is_direct_next_iter = builtin_modules.get_error_return_next_element_type(inner_iterable_type, registry=self.ctx.registry) is not None
1082
+ is_iter_based = builtin_modules.get_iter_element_type(inner_iterable_type, registry=self.ctx.registry) is not None
1083
+ is_protocol_iter = is_protocol_type(resolved_for_iter) and resolved_for_iter.qualified_name() in ("typing.Iterator", "typing.Iterable")
1084
+ before = self.init.save()
1085
+ consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
1086
+ ns_types_before_foreach = self._save_ns_var_types()
1087
+ with self.scopes.loop_scope() as inner_scope:
1088
+ self.init.apply_loop_entry_facts(before)
1089
+ # Track range facts for loop variable from range() calls
1090
+ self._track_for_range_facts(stmt)
1091
+ bt = self.ctx.func.borrow_tracker
1092
+ if isinstance(stmt.iterable, TpyName):
1093
+ bt.add_borrow(stmt.iterable.name, "__for_iter", BorrowKind.ITER)
1094
+ self.ctx.func.loop_var_iterable[stmt.var] = stmt.iterable.name
1095
+ # Protocol-typed and TypeParamRef params used as for-loop iterables
1096
+ # require mutable access: .__next__() mutates iterator state.
1097
+ # Mark them mutated so the generated param gets T& not const T&.
1098
+ inner = unwrap_ref_type(unwrap_readonly(iterable_type))
1099
+ if (isinstance(inner, (TypeParamRef,)) or is_protocol_type(inner)):
1100
+ self.ctx.mark_param_mutated(stmt.iterable.name)
1101
+ elif isinstance(stmt.iterable, TpyFieldAccess):
1102
+ # Property getter: register ITER borrow via return_borrows_from
1103
+ # on the receiver object (more precise than the field-path key).
1104
+ gc = stmt.iterable.property_getter_call
1105
+ if gc is not None:
1106
+ fi_gc = gc.resolved_function_info
1107
+ if fi_gc is not None and fi_gc.return_borrows_from:
1108
+ for idx in fi_gc.return_borrows_from:
1109
+ if idx == -1 and gc.obj is not None:
1110
+ src = _borrow_storage_root(gc.obj)
1111
+ if src is not None:
1112
+ bt.add_borrow(src, "__for_iter", BorrowKind.ITER)
1113
+ self.ctx.func.loop_var_iterable[stmt.var] = src
1114
+ elif idx >= 0 and idx < len(gc.args):
1115
+ src = _borrow_storage_root(gc.args[idx])
1116
+ if src is not None:
1117
+ bt.add_borrow(src, "__for_iter", BorrowKind.ITER)
1118
+ self.ctx.func.loop_var_iterable[stmt.var] = src
1119
+ else:
1120
+ key = _storage_key(stmt.iterable)
1121
+ if key is not None:
1122
+ bt.add_borrow(key, "__for_iter", BorrowKind.ITER)
1123
+ self.ctx.func.loop_var_iterable[stmt.var] = key
1124
+ elif isinstance(stmt.iterable, (TpyCall, TpyMethodCall)):
1125
+ # 8b: iterable is a call whose return borrows from source arg(s).
1126
+ # Register ITER borrow directly on those source containers so that
1127
+ # structural mutations during the loop generate conflict warnings.
1128
+ fi_iter = stmt.iterable.resolved_function_info
1129
+ if fi_iter is not None and fi_iter.return_borrows_from:
1130
+ call_args = stmt.iterable.args
1131
+ call_obj = getattr(stmt.iterable, 'obj', None)
1132
+ for idx in fi_iter.return_borrows_from:
1133
+ arg = None
1134
+ if idx == -1 and call_obj is not None:
1135
+ src = _borrow_storage_root(call_obj)
1136
+ arg = call_obj
1137
+ elif idx >= 0 and idx < len(call_args):
1138
+ src = _borrow_storage_root(call_args[idx])
1139
+ arg = call_args[idx]
1140
+ else:
1141
+ src = None
1142
+ if src is not None:
1143
+ bt.add_borrow(src, "__for_iter", BorrowKind.ITER)
1144
+ self.ctx.func.loop_var_iterable[stmt.var] = src
1145
+ elif arg is not None and _is_dangling_temporary_arg(arg):
1146
+ # Call results returning non-value types are
1147
+ # materialized into named variables by codegen
1148
+ # (for by-reference passing), so they survive
1149
+ # the for-loop. Only warn for value-type temporaries
1150
+ # (e.g. str -> string_view conversion) where the
1151
+ # underlying storage is truly destroyed.
1152
+ is_materialized = False
1153
+ if isinstance(arg, (TpyCall, TpyMethodCall)):
1154
+ arg_fi = arg.resolved_function_info
1155
+ if arg_fi is not None and not arg_fi.return_type.is_value_type():
1156
+ is_materialized = True
1157
+ if not is_materialized:
1158
+ if idx == -1:
1159
+ detail = "temporary receiver object"
1160
+ else:
1161
+ detail = f"temporary argument '{fi_iter.params[idx].name}'"
1162
+ self.ctx.warning(
1163
+ f"Iterator borrows from {detail}; "
1164
+ f"the temporary is destroyed before iteration begins",
1165
+ stmt.iterable,
1166
+ )
1167
+ if is_direct_next_iter or is_protocol_iter:
1168
+ iter_depth = inner_scope.depth
1169
+ elif is_iter_based:
1170
+ # __iter__() either references the container's storage
1171
+ # (NativeIterable types -- builtins like list/dict/str
1172
+ # and span-backed types like SpanIter) or creates a
1173
+ # fresh owned iterator (user-defined iterators).
1174
+ # For the former, the loop-var lifetime is the
1175
+ # container's; for the latter, it's loop-body scope.
1176
+ #
1177
+ # Known gap (no failing test): user records whose
1178
+ # `__iter__()` returns `SpanIter` (e.g. ArrayList,
1179
+ # Stack with `__iter__(self) -> SpanIter[T]`) are NOT
1180
+ # `is_native_iterable`, but their iterator does
1181
+ # reference the container's storage. Provenance
1182
+ # tracking only activates for non-value loop vars
1183
+ # from param-derived iterables, so the gap is
1184
+ # currently invisible. If it surfaces, recover by
1185
+ # also checking whether `__iter__()` returns a
1186
+ # `SpanIter` here.
1187
+ references_container = builtin_modules.is_native_iterable(
1188
+ inner_iterable_type, registry=self.ctx.registry
1189
+ )
1190
+ if references_container:
1191
+ if self.compat.is_lvalue(stmt.iterable):
1192
+ iter_depth = self.scopes.get_expr_scope_depth(stmt.iterable)
1193
+ else:
1194
+ iter_depth = inner_scope.depth
1195
+ else:
1196
+ iter_depth = inner_scope.depth
1197
+ elif self.compat.is_lvalue(stmt.iterable):
1198
+ # For-each var references container's storage -- use container's depth.
1199
+ iter_depth = self.scopes.get_expr_scope_depth(stmt.iterable)
1200
+ else:
1201
+ # For rvalue iterables (calls), C++ extends the temporary's lifetime
1202
+ # to the for statement, but it dies when the loop ends. Use body depth
1203
+ # so that escaping to any outer-scoped variable is caught.
1204
+ iter_depth = inner_scope.depth
1205
+ # Track provenance for loop vars whose type participates in
1206
+ # provenance tracking, when iterating over a param-derived iterable.
1207
+ track_loop_prov = (
1208
+ _needs_provenance_tracking(unwrap_readonly(elem_type))
1209
+ and self.compat.is_param_derived_expr(stmt.iterable)
1210
+ )
1211
+ if track_loop_prov:
1212
+ self.init.add_loop_var_provenance(stmt.var)
1213
+ self.ctx.func.mutated_loop_vars.discard(stmt.var)
1214
+ self.ctx.func.consumed_loop_vars.discard(stmt.var)
1215
+ self.ctx.func.deferred_loop_copy_warnings.pop(stmt.var, None)
1216
+ self._record_for_loop_var_type(stmt, elem_type)
1217
+ with self.scopes.loop_var(inner_scope, stmt.var, elem_type, iter_depth, is_foreach=True):
1218
+ for s in stmt.body:
1219
+ self.analyze_stmt(s)
1220
+ if track_loop_prov:
1221
+ self.init.remove_loop_var_provenance(stmt.var)
1222
+ # Set const-ref binding when the loop var was never mutated.
1223
+ # mutated_loop_vars was cleared for stmt.var before entering the
1224
+ # loop body, so it only reflects mutations from this loop.
1225
+ # Only for non-value types or expensive-to-copy value types
1226
+ # (BigInt, String, tuples with expensive elements). Cheap
1227
+ # primitives (int32_t, bool, double, etc.) are better copied
1228
+ # into a register than referenced through a pointer.
1229
+ # For synthetic tuple-unpack loop vars, skip const binding when
1230
+ # the unpack has elements that need mutable references --
1231
+ # const tuple prevents T& bindings via std::get.
1232
+ unwrapped = unwrap_qualifiers(elem_type)
1233
+ worth_const_ref = (not unwrapped.is_value_type()
1234
+ or unwrapped.is_expensive_copy())
1235
+ needs_mut_unpack = False
1236
+ if stmt.is_tuple_unpack and stmt.body:
1237
+ first = stmt.body[0]
1238
+ if isinstance(first, TpyTupleUnpack) and any(first.is_ref):
1239
+ needs_mut_unpack = True
1240
+ if (worth_const_ref
1241
+ and stmt.var not in self.ctx.func.mutated_loop_vars
1242
+ and stmt.var not in self.ctx.func.consumed_loop_vars
1243
+ and not needs_mut_unpack):
1244
+ stmt.const_loop_var = True
1245
+ # Auto-consuming iteration: use consuming __iter__ when the
1246
+ # container is at last use and elements are either mutated
1247
+ # or consumed (copied into owned storage via Own[T] params,
1248
+ # append, etc.). The container is dead after the loop, so
1249
+ # moving it into OwnIter is free (pointer swap). Consumed
1250
+ # elements become movable at last use, avoiding copies.
1251
+ loop_var_needs_ownership = (
1252
+ stmt.var in self.ctx.func.mutated_loop_vars
1253
+ or stmt.var in self.ctx.func.consumed_loop_vars
1254
+ )
1255
+ if (loop_var_needs_ownership
1256
+ and not stmt.hoist_loop_var
1257
+ and not unwrapped.is_value_type()
1258
+ and isinstance(stmt.iterable, TpyName)
1259
+ and id(stmt.iterable) in self.ctx.all_last_uses
1260
+ and self.compat._is_owned_var(stmt.iterable.name)):
1261
+ consuming_fi = self._find_consuming_iter(inner_iterable_type)
1262
+ if consuming_fi is not None:
1263
+ stmt.consuming_iter_fi = consuming_fi
1264
+ # Suppress copy warnings for the loop variable --
1265
+ # elements will be moved, not copied.
1266
+ deferred = self.ctx.func.deferred_loop_copy_warnings.pop(stmt.var, None)
1267
+ if deferred:
1268
+ for idx in sorted(deferred, reverse=True):
1269
+ del self.ctx.diagnostics[idx]
1270
+ # Also suppress copy warnings when elem_type is Own[T]
1271
+ # (e.g. Iterable[Own[T]] params) -- elements will be moved.
1272
+ if (stmt.consuming_iter_fi is None
1273
+ and isinstance(elem_type, OwnType)):
1274
+ deferred = self.ctx.func.deferred_loop_copy_warnings.pop(stmt.var, None)
1275
+ if deferred:
1276
+ for idx in sorted(deferred, reverse=True):
1277
+ del self.ctx.diagnostics[idx]
1278
+ self.init.apply_loop_exit_facts(before)
1279
+ # Loop might not execute — consumption inside is not definite
1280
+ self.ctx.func.current_consumed_own_params = consumed_before_loop
1281
+ self._restore_ns_var_types(ns_types_before_foreach)
1282
+ self._sync_promoted_var_types()
1283
+ self._propagate_for_loop_scope(stmt, inner_scope, elem_type)
1284
+ for s in stmt.orelse:
1285
+ self.analyze_stmt(s)
1286
+ elif isinstance(stmt, TpyBreak):
1287
+ if self.ctx.func.loop_depth == 0:
1288
+ raise self.ctx.error("'break' outside loop", stmt)
1289
+ self.init.mark_terminated()
1290
+ elif isinstance(stmt, TpyContinue):
1291
+ if self.ctx.func.loop_depth == 0:
1292
+ raise self.ctx.error("'continue' outside loop", stmt)
1293
+ self.init.mark_terminated()
1294
+ elif isinstance(stmt, TpyAssert):
1295
+ self.expr.analyze_expr(stmt.condition)
1296
+ self.narrowing.warn_truthy_value_optionals(stmt.condition)
1297
+ if stmt.message is not None:
1298
+ msg_type = self.expr.analyze_expr(stmt.message)
1299
+ if not is_any_str_type(msg_type):
1300
+ raise self.ctx.error("assert message must be a string", stmt)
1301
+ then_type_facts, _ = self.narrowing.condition_type_facts(stmt.condition)
1302
+ ptr_nn, _ = self.narrowing.condition_ptr_null_facts(stmt.condition)
1303
+ range_true, _ = self.narrowing.condition_range_facts(stmt.condition)
1304
+ stmt.then_type_facts = self._filter_union_codegen_facts(then_type_facts)
1305
+ self.ctx.func.narrowed_types.update(then_type_facts)
1306
+ self.ctx.func.non_null_ptr_vars |= ptr_nn
1307
+ self._apply_range_facts(range_true)
1308
+ elif isinstance(stmt, TpyGlobal):
1309
+ self._analyze_global_stmt(stmt)
1310
+ elif isinstance(stmt, TpyRaise):
1311
+ self._analyze_raise(stmt)
1312
+ elif isinstance(stmt, TpyTry):
1313
+ self._analyze_try(stmt)
1314
+ elif isinstance(stmt, TpyMatch):
1315
+ self.match.analyze_match(stmt)
1316
+ elif isinstance(stmt, TpyWith):
1317
+ self._analyze_with(stmt)
1318
+ elif isinstance(stmt, TpyNonlocal):
1319
+ self._analyze_nonlocal(stmt)
1320
+ elif isinstance(stmt, TpyNestedDef):
1321
+ self._analyze_nested_def(stmt)
1322
+
1323
+ def _apply_range_facts(self, facts: dict[str, 'ValueRange']) -> None:
1324
+ """Apply integer range facts, intersecting with any existing ranges."""
1325
+ for name, new_range in facts.items():
1326
+ existing = self.ctx.func.value_ranges.get(name)
1327
+ if existing is not None:
1328
+ self.ctx.func.value_ranges[name] = ValueRange.intersect(existing, new_range)
1329
+ else:
1330
+ self.ctx.func.value_ranges[name] = new_range
1331
+
1332
+ def _track_for_range_facts(self, stmt: TpyForEach) -> None:
1333
+ """Set range facts for loop variable when iterating over range().
1334
+
1335
+ Detects: range(len(arr)), range(N).
1336
+ """
1337
+ iterable = stmt.iterable
1338
+ if not isinstance(iterable, TpyCall) or iterable.func_name != "range":
1339
+ return
1340
+
1341
+ args = iterable.args
1342
+ if len(args) == 1:
1343
+ arg = args[0]
1344
+ # range(len(arr)) -- symbolic bound
1345
+ if (isinstance(arg, TpyCall) and arg.func_name == "len"
1346
+ and len(arg.args) == 1 and isinstance(arg.args[0], TpyName)):
1347
+ self.ctx.func.value_ranges[stmt.var] = ValueRange.for_range_index(
1348
+ stop_len_of=arg.args[0].name,
1349
+ )
1350
+ return
1351
+ # range(N) -- literal bound
1352
+ if isinstance(arg, TpyIntLiteral):
1353
+ self.ctx.func.value_ranges[stmt.var] = ValueRange.for_range_index(
1354
+ stop_literal=arg.value,
1355
+ )
1356
+ return
1357
+ # range(n) -- unknown bound, but still non-negative
1358
+ self.ctx.func.value_ranges[stmt.var] = ValueRange.for_range_index()
1359
+
1360
+ def _filter_union_codegen_facts(
1361
+ self, facts: dict[str, TpyType],
1362
+ ) -> dict[str, TpyType]:
1363
+ """Keep narrowing facts that drive codegen extraction.
1364
+
1365
+ Optional narrowing is handled implicitly by std::optional in C++,
1366
+ so only UnionType variables need explicit std::get<T> extraction.
1367
+ LiteralType facts are passed through for dead branch elimination.
1368
+ Protocol facts (e.g. Iterable[T] -> NativeIterable[T] via isinstance)
1369
+ feed protocol_narrowings so downstream dispatch sees the refined type.
1370
+ Polymorphic-class facts (e.g. isinstance(opt_exc, OSError) where
1371
+ opt_exc: Optional[BaseException]) drive cast-and-cache codegen so
1372
+ subclass-typed reads in the true branch route through a dynamic_cast'd
1373
+ local rather than the base pointer.
1374
+ """
1375
+ def _peel(t: TpyType) -> TpyType:
1376
+ # Strip Own / readonly so Own[A|B] params surface as UnionType
1377
+ # for the union-fact check; mirrors _isinstance_unwrap in
1378
+ # calls.py.
1379
+ t = unwrap_readonly(t)
1380
+ if isinstance(t, OwnType):
1381
+ t = unwrap_readonly(t.wrapped)
1382
+ return t
1383
+
1384
+ def _polymorphic_subclass_fact(name: str, ty: TpyType) -> bool:
1385
+ # Strict subclass only -- `is not None` narrowing keeps the source's
1386
+ # inner class and is gated out by the shared predicate. `_peel` strips
1387
+ # Own/readonly so `Own[A | B]` params surface their inner shape.
1388
+ return is_polymorphic_subclass_fact(
1389
+ _peel(self.narrowing.declared_type_for_name(name)),
1390
+ ty, self.ctx.registry)
1391
+
1392
+ return {
1393
+ name: ty for name, ty in facts.items()
1394
+ if (isinstance(_peel(self.narrowing.declared_type_for_name(name)), (UnionType, AnyType))
1395
+ or isinstance(ty, LiteralType)
1396
+ or is_protocol_type(ty)
1397
+ or _polymorphic_subclass_fact(name, ty)
1398
+ # Deref-view facts (key carries the deref suffix) drive the
1399
+ # if-init cast-and-cache for owning wrappers; the wrapper var
1400
+ # itself is never retyped, so they pass through verbatim.
1401
+ or parse_deref_view_key(name) is not None)
1402
+ }
1403
+
1404
+ def _analyze_raise(self, stmt: TpyRaise) -> None:
1405
+ """Analyze a raise statement (return-tier, throw-tier, or bare re-raise)."""
1406
+ # Bare raise (re-raise)
1407
+ if stmt.exception_type is None and stmt.raise_expr is None:
1408
+ if self.ctx.in_except_tier is None:
1409
+ raise self.ctx.error(
1410
+ "bare 'raise' is only valid inside an 'except' block", stmt)
1411
+ if self.ctx.in_except_tier == "return" and not self.ctx.in_except_has_binding:
1412
+ raise self.ctx.error(
1413
+ "bare 'raise' in return-tier except requires 'as' binding "
1414
+ "(e.g. 'except E as e') to capture the error value",
1415
+ stmt)
1416
+ self.init.mark_terminated()
1417
+ return
1418
+
1419
+ # Expression raise (general expression, e.g. raise <expr>)
1420
+ if stmt.raise_expr is not None:
1421
+ self._analyze_raise_expr(stmt)
1422
+ return
1423
+
1424
+ func = self.ctx.func.current_function
1425
+ if not isinstance(func, TpyFunction):
1426
+ raise self.ctx.error(
1427
+ f"'raise {stmt.exception_type}' can only be used inside a function", stmt)
1428
+
1429
+ bare_exc = bare_name(stmt.exception_type)
1430
+ record = self.ctx.registry.find_record(bare_exc)
1431
+ if not record:
1432
+ # Not a type -- check if it's a variable of exception type
1433
+ self._analyze_raise_name_as_expr(stmt)
1434
+ return
1435
+
1436
+ qualified_exc = qualify_exception_name(
1437
+ stmt.exception_type, self.ctx.registry, self.ctx.module_name)
1438
+ is_cf = is_return_exception(qualified_exc)
1439
+
1440
+ if is_cf:
1441
+ # Return-tier: must be inside @error_return(E) function with matching E
1442
+ if func.error_return is None:
1443
+ raise self.ctx.error(
1444
+ f"'raise {stmt.exception_type}' requires "
1445
+ f"@error_return({stmt.exception_type}) on the enclosing function",
1446
+ stmt)
1447
+ if not error_return_matches(func.error_return, qualified_exc):
1448
+ raise self.ctx.error(
1449
+ f"'raise {stmt.exception_type}' does not match "
1450
+ f"@error_return({stmt.exception_type})", stmt)
1451
+ else:
1452
+ # Throw-tier: must inherit from Exception
1453
+ if not is_exception_type(stmt.exception_type, self.ctx.registry):
1454
+ raise self.ctx.error(
1455
+ f"'{stmt.exception_type}' is not an exception type; "
1456
+ f"it must inherit from Exception",
1457
+ stmt)
1458
+
1459
+ # Type-check constructor arguments against __init__ params
1460
+ if stmt.args:
1461
+ if record.has_init:
1462
+ min_args = sum(1 for _, _, d in record.init_params if d is None)
1463
+ max_args = len(record.init_params)
1464
+ if len(stmt.args) < min_args or len(stmt.args) > max_args:
1465
+ expected = (f"{max_args}" if min_args == max_args
1466
+ else f"{min_args} to {max_args}")
1467
+ raise self.ctx.error(
1468
+ f"'raise {stmt.exception_type}()' expects "
1469
+ f"{expected} arguments, got {len(stmt.args)}",
1470
+ stmt)
1471
+ for i, (arg, (pname, ptype, _)) in enumerate(
1472
+ zip(stmt.args, record.init_params)):
1473
+ arg_type = self.expr.analyze_expr_with_hint(arg, ptype)
1474
+ stmt.args[i] = self.compat.coerce_expr(
1475
+ arg, arg_type, ptype, f"argument '{pname}'",
1476
+ coercion_ctx=CoercionContext.ARG)
1477
+ elif record.fields:
1478
+ raise self.ctx.error(
1479
+ f"'{stmt.exception_type}' has data fields but no __init__; "
1480
+ f"add __init__ to use 'raise {stmt.exception_type}(...)'",
1481
+ stmt)
1482
+ else:
1483
+ raise self.ctx.error(
1484
+ f"'raise {stmt.exception_type}()' does not accept arguments",
1485
+ stmt)
1486
+ # Propagate qualified name to AST
1487
+ stmt.exception_type = qualified_exc
1488
+ self.init.mark_terminated()
1489
+
1490
+ def _analyze_raise_expr(self, stmt: TpyRaise) -> None:
1491
+ """Analyze 'raise <expr>' where expr is a general expression.
1492
+
1493
+ Phase 20 Stage 3: accepts a `Box[Throwable]` (or any other type
1494
+ implementing the `Deref` protocol whose peeled type is Throwable)
1495
+ in addition to plain Exception subclasses. Codegen lowers via
1496
+ `<expr>.__raise__()` which auto-derefs through Box's vtable.
1497
+ """
1498
+ func = self.ctx.func.current_function
1499
+ if not isinstance(func, TpyFunction):
1500
+ raise self.ctx.error(
1501
+ "'raise' can only be used inside a function", stmt)
1502
+ expr_type = self.expr.analyze_expr(stmt.raise_expr)
1503
+ # Peel __deref__ until we hit a non-Deref type. Box[Throwable]
1504
+ # peels once to Throwable; an Optional[Box[T]] field already
1505
+ # narrows the Optional away before this point. Stores the depth on
1506
+ # the AST so codegen can insert the matching `.__deref__()` chain.
1507
+ peeled_source = unwrap_qualifiers(expr_type)
1508
+ depth = 0
1509
+ while True:
1510
+ next_peeled = self.expr.get_deref_target_type(peeled_source)
1511
+ if next_peeled is None:
1512
+ break
1513
+ depth += 1
1514
+ peeled_source = unwrap_qualifiers(next_peeled)
1515
+ if depth > 8:
1516
+ raise self.ctx.error(
1517
+ f"raise expression __deref__ chain exceeds 8 levels",
1518
+ stmt)
1519
+ if depth > 0:
1520
+ stmt.deref_depth = depth
1521
+ t = peeled_source
1522
+ if isinstance(t, NominalType) and t.is_protocol and t.qualified_name() == "tpy.Throwable":
1523
+ self.init.mark_terminated()
1524
+ return
1525
+ if isinstance(t, NominalType) and not t.is_protocol:
1526
+ if is_exception_type(t.name, self.ctx.registry):
1527
+ self.init.mark_terminated()
1528
+ return
1529
+ raise self.ctx.error(
1530
+ f"cannot raise expression of type '{expr_type}'; "
1531
+ f"its __deref__ target '{t}' is not Throwable",
1532
+ stmt)
1533
+ type_name = self._raise_expr_type_name(expr_type, stmt)
1534
+ if is_return_exception(
1535
+ qualify_exception_name(type_name, self.ctx.registry, self.ctx.module_name)):
1536
+ raise self.ctx.error(
1537
+ f"'raise <expr>' cannot be used with ReturnException type "
1538
+ f"'{type_name}'; use direct 'raise {type_name}' inside "
1539
+ f"an @error_return function instead",
1540
+ stmt)
1541
+ if not is_exception_type(type_name, self.ctx.registry):
1542
+ raise self.ctx.error(
1543
+ f"cannot raise expression of type '{type_name}'; "
1544
+ f"it must inherit from Exception",
1545
+ stmt)
1546
+ self.init.mark_terminated()
1547
+
1548
+ def _analyze_raise_name_as_expr(self, stmt: TpyRaise) -> None:
1549
+ """Handle 'raise Name' or 'raise Name(args)' where Name is not a type.
1550
+
1551
+ Converts to expression raise if Name is a variable/call of exception type.
1552
+ """
1553
+ name = stmt.exception_type
1554
+ if stmt.args or stmt.is_call_form:
1555
+ # raise func() or raise func(args) -- convert to call expression
1556
+ call_expr = TpyCall(func=TpyName(name, loc=stmt.loc), args=stmt.args, loc=stmt.loc)
1557
+ stmt.raise_expr = call_expr
1558
+ stmt.exception_type = None
1559
+ stmt.args = []
1560
+ self._analyze_raise_expr(stmt)
1561
+ return
1562
+ # raise name -- convert to variable reference
1563
+ name_expr = TpyName(name=name, loc=stmt.loc)
1564
+ stmt.raise_expr = name_expr
1565
+ stmt.exception_type = None
1566
+ self._analyze_raise_expr(stmt)
1567
+
1568
+ def _raise_expr_type_name(self, expr_type: TpyType, stmt: TpyRaise) -> str:
1569
+ """Extract the type name from a raise expression's type for validation."""
1570
+ t = unwrap_qualifiers(expr_type)
1571
+ if isinstance(t, NominalType) and not t.is_protocol:
1572
+ return t.name
1573
+ raise self.ctx.error(
1574
+ f"cannot raise expression of type '{expr_type}'; "
1575
+ f"expected an exception type",
1576
+ stmt)
1577
+
1578
+ def _classify_try_tier(self, stmt: TpyTry) -> str:
1579
+ """Classify a try statement as 'return', 'throw', or 'finally_only'."""
1580
+ if not stmt.handlers:
1581
+ return "finally_only"
1582
+
1583
+ has_cf = False
1584
+ has_throw = False
1585
+ has_bare = False
1586
+ for h in stmt.handlers:
1587
+ if h.exception_type is None:
1588
+ has_bare = True
1589
+ continue
1590
+ proto = self.ctx.registry.scan_by_short_name(h.exception_type)
1591
+ is_cf_catch_all = (
1592
+ proto is not None
1593
+ and f"{proto.module}.{proto.name}" == qnames.RETURN_EXCEPTION
1594
+ )
1595
+ if is_cf_catch_all or is_return_exception(
1596
+ qualify_exception_name(
1597
+ h.exception_type, self.ctx.registry, self.ctx.module_name)):
1598
+ has_cf = True
1599
+ else:
1600
+ has_throw = True
1601
+
1602
+ if has_cf and has_throw:
1603
+ raise self.ctx.error(
1604
+ "cannot mix ReturnException and non-ReturnException exception types "
1605
+ "in the same try/except block", stmt)
1606
+ if has_cf and has_bare:
1607
+ raise self.ctx.error(
1608
+ "bare 'except:' cannot be mixed with ReturnException handlers", stmt)
1609
+
1610
+ return "return" if has_cf else "throw"
1611
+
1612
+ def _analyze_try(self, stmt: TpyTry) -> None:
1613
+ """Analyze a try/except/else/finally statement.
1614
+
1615
+ Classifies the try block into tiers:
1616
+ - 'return': ReturnException handlers -> goto-based dispatch (existing)
1617
+ - 'throw': non-ReturnException handlers -> C++ try/catch
1618
+ - 'finally_only': no handlers, just finally cleanup
1619
+ """
1620
+ tier = self._classify_try_tier(stmt)
1621
+ stmt.tier = tier
1622
+
1623
+ if tier == "finally_only":
1624
+ self._analyze_try_finally_only(stmt)
1625
+ elif tier == "return":
1626
+ self._analyze_try_return(stmt)
1627
+ else:
1628
+ self._analyze_try_throw(stmt)
1629
+
1630
+ def _analyze_try_finally_only(self, stmt: TpyTry) -> None:
1631
+ """Analyze try/finally with no except handlers."""
1632
+ scope_before = set(self.ctx.func.current_scope.bindings.keys())
1633
+ for s in stmt.try_body:
1634
+ self.analyze_stmt(s)
1635
+ try_bindings = dict(self.ctx.func.current_scope.bindings)
1636
+ prev_in_finally = self.ctx.in_finally
1637
+ self.ctx.in_finally = True
1638
+ for s in stmt.finally_body:
1639
+ self.analyze_stmt(s)
1640
+ self.ctx.in_finally = prev_in_finally
1641
+ # Hoist try-body variables so they're accessible in the finally body.
1642
+ # Mark as hoisted so non-value types use pointer indirection.
1643
+ branch_new = set(try_bindings.keys()) - scope_before
1644
+ predecl = branch_new - self.ctx.func.global_declarations
1645
+ if predecl:
1646
+ self.ctx.if_branch_decls[id(stmt)] = {
1647
+ name: try_bindings[name]
1648
+ for name in sorted(predecl)
1649
+ if name in try_bindings
1650
+ }
1651
+ self.ctx.func.hoisted_vars |= predecl
1652
+
1653
+ def _analyze_try_return(self, stmt: TpyTry) -> None:
1654
+ """Analyze return-tier try/except (ReturnException, goto-based)."""
1655
+ # Return tier supports single handler or ReturnException catch-all
1656
+ if len(stmt.handlers) != 1:
1657
+ raise self.ctx.error(
1658
+ "return-tier (ReturnException) try/except supports only a single handler", stmt)
1659
+ handler = stmt.handlers[0]
1660
+
1661
+ scope_before = set(self.ctx.func.current_scope.bindings.keys())
1662
+ before = self.init.save()
1663
+ consumed_before = self.ctx.func.current_consumed_own_params.copy()
1664
+ bindings_before = dict(self.ctx.func.current_scope.bindings)
1665
+ ns_types_before = self._save_ns_var_types()
1666
+
1667
+ # Detect except ReturnException catch-all
1668
+ proto = self.ctx.registry.scan_by_short_name(handler.exception_type)
1669
+ is_return_exception_catch_all = (
1670
+ proto is not None
1671
+ and f"{proto.module}.{proto.name}" == qnames.RETURN_EXCEPTION
1672
+ )
1673
+
1674
+ if is_return_exception_catch_all and handler.binding:
1675
+ raise self.ctx.error(
1676
+ "'except ReturnException as' binding is not supported", stmt)
1677
+
1678
+ if not is_return_exception_catch_all:
1679
+ bare_exc = bare_name(handler.exception_type)
1680
+ if not self.ctx.registry.find_record(bare_exc):
1681
+ raise self.ctx.error(
1682
+ f"Unknown error type '{handler.exception_type}'", stmt)
1683
+
1684
+ # Set try context so call analysis can allow error_return calls
1685
+ prev_try_error = self.ctx.try_except_error_type
1686
+ if is_return_exception_catch_all:
1687
+ self.ctx.try_except_error_type = "*"
1688
+ else:
1689
+ self.ctx.try_except_error_type = qualify_exception_name(
1690
+ handler.exception_type, self.ctx.registry, self.ctx.module_name)
1691
+
1692
+ for s in stmt.try_body:
1693
+ self.analyze_stmt(s)
1694
+
1695
+ self.ctx.try_except_error_type = prev_try_error
1696
+
1697
+ for s in stmt.else_body:
1698
+ self.analyze_stmt(s)
1699
+ then_state = self.init.save()
1700
+ consumed_after_then = self.ctx.func.current_consumed_own_params.copy()
1701
+ then_terminated = self.ctx.func.init_terminated
1702
+ try_bindings = dict(self.ctx.func.current_scope.bindings)
1703
+
1704
+ # Restore to pre-try state for except branch
1705
+ self.ctx.func.current_scope.bindings = dict(bindings_before)
1706
+ self._restore_ns_var_types(ns_types_before)
1707
+ self.init.restore(before)
1708
+ self.ctx.func.current_consumed_own_params = consumed_before.copy()
1709
+
1710
+ # Register except binding -- use find_record_by_qname because
1711
+ # handler.exception_type may be module-qualified (e.g. from macros).
1712
+ if handler.binding:
1713
+ exc_record = self.ctx.registry.find_record_by_qname(handler.exception_type)
1714
+ if exc_record:
1715
+ exc_type = NominalType(exc_record.name, _module_qname=exc_record.qualified_name())
1716
+ self.ctx.func.current_scope.bindings[handler.binding] = exc_type
1717
+ self.init.mark_assigned(handler.binding)
1718
+
1719
+ # Set in_except_tier for bare raise validation
1720
+ prev_except_tier = self.ctx.in_except_tier
1721
+ prev_has_binding = self.ctx.in_except_has_binding
1722
+ self.ctx.in_except_tier = "return"
1723
+ self.ctx.in_except_has_binding = handler.binding is not None
1724
+ for s in handler.body:
1725
+ self.analyze_stmt(s)
1726
+ self.ctx.in_except_tier = prev_except_tier
1727
+ self.ctx.in_except_has_binding = prev_has_binding
1728
+
1729
+ if handler.binding and handler.binding in self.ctx.func.current_scope.bindings:
1730
+ del self.ctx.func.current_scope.bindings[handler.binding]
1731
+ else_state = self.init.save()
1732
+ consumed_after_else = self.ctx.func.current_consumed_own_params.copy()
1733
+ else_terminated = self.ctx.func.init_terminated
1734
+
1735
+ self.init.merge_branches(then_state, else_state)
1736
+ self._merge_consumed_own(
1737
+ then_terminated, else_terminated,
1738
+ consumed_after_then, consumed_after_else)
1739
+
1740
+ # Analyze finally body (runs on all paths, doesn't affect branch merging)
1741
+ prev_in_finally = self.ctx.in_finally
1742
+ self.ctx.in_finally = True
1743
+ for s in stmt.finally_body:
1744
+ self.analyze_stmt(s)
1745
+ self.ctx.in_finally = prev_in_finally
1746
+
1747
+ # Hoist all declarations for goto-based dispatch
1748
+ all_bindings = dict(try_bindings)
1749
+ all_bindings.update(self.ctx.func.current_scope.bindings)
1750
+ branch_new = set(all_bindings.keys()) - scope_before
1751
+ if handler.binding:
1752
+ branch_new.discard(handler.binding)
1753
+ predecl = branch_new - self.ctx.func.global_declarations
1754
+ if predecl:
1755
+ self.ctx.if_branch_decls[id(stmt)] = {
1756
+ name: all_bindings[name]
1757
+ for name in sorted(predecl)
1758
+ if name in all_bindings
1759
+ }
1760
+
1761
+ def _analyze_try_throw(self, stmt: TpyTry) -> None:
1762
+ """Analyze throw-tier try/except (C++ try/catch)."""
1763
+ scope_before = set(self.ctx.func.current_scope.bindings.keys())
1764
+ before = self.init.save()
1765
+ consumed_before = self.ctx.func.current_consumed_own_params.copy()
1766
+ bindings_before = dict(self.ctx.func.current_scope.bindings)
1767
+ ns_types_before = self._save_ns_var_types()
1768
+
1769
+ # Validate all handlers and save resolved records for binding.
1770
+ # Qualify before lookup so dotted forms (`re.error`) bypass any local
1771
+ # class with the same bare name.
1772
+ handler_records: list[RecordInfo | None] = []
1773
+ for h in stmt.handlers:
1774
+ if h.exception_type is not None:
1775
+ qualified = qualify_exception_name(
1776
+ h.exception_type, self.ctx.registry, self.ctx.module_name)
1777
+ record = self.ctx.registry.find_record_by_qname(qualified)
1778
+ if not record:
1779
+ raise self.ctx.error(
1780
+ f"Unknown exception type '{h.exception_type}'", stmt)
1781
+ if not is_exception_type(qualified, self.ctx.registry):
1782
+ raise self.ctx.error(
1783
+ f"'{h.exception_type}' is not an exception type; "
1784
+ f"it must inherit from Exception", stmt)
1785
+ handler_records.append(record)
1786
+ h.exception_type = qualified
1787
+ else:
1788
+ handler_records.append(None)
1789
+
1790
+ # Analyze try body
1791
+ for s in stmt.try_body:
1792
+ self.analyze_stmt(s)
1793
+ # Capture try-body bindings BEFORE else (else vars are scoped to the
1794
+ # if(__ok) block in C++ and must not leak into post-try scope)
1795
+ try_bindings = dict(self.ctx.func.current_scope.bindings)
1796
+
1797
+ # Analyze else body
1798
+ for s in stmt.else_body:
1799
+ self.analyze_stmt(s)
1800
+ then_state = self.init.save()
1801
+ consumed_after_then = self.ctx.func.current_consumed_own_params.copy()
1802
+ then_terminated = self.ctx.func.init_terminated
1803
+
1804
+ # Analyze each except handler as a separate branch from pre-try state
1805
+ handler_states: list[tuple] = []
1806
+ for i, h in enumerate(stmt.handlers):
1807
+ self.ctx.func.current_scope.bindings = dict(bindings_before)
1808
+ self._restore_ns_var_types(ns_types_before)
1809
+ self.init.restore(before)
1810
+ self.ctx.func.current_consumed_own_params = consumed_before.copy()
1811
+
1812
+ record = handler_records[i]
1813
+ if h.binding and record is not None:
1814
+ exc_type = NominalType(record.name, _module_qname=record.qualified_name())
1815
+ self.ctx.func.current_scope.bindings[h.binding] = exc_type
1816
+ self.init.mark_assigned(h.binding)
1817
+
1818
+ prev_except_tier = self.ctx.in_except_tier
1819
+ self.ctx.in_except_tier = "throw"
1820
+ for s in h.body:
1821
+ self.analyze_stmt(s)
1822
+ self.ctx.in_except_tier = prev_except_tier
1823
+
1824
+ if h.binding and h.binding in self.ctx.func.current_scope.bindings:
1825
+ del self.ctx.func.current_scope.bindings[h.binding]
1826
+
1827
+ handler_states.append((
1828
+ self.init.save(),
1829
+ self.ctx.func.current_consumed_own_params.copy(),
1830
+ self.ctx.func.init_terminated,
1831
+ ))
1832
+
1833
+ # Merge all branches: success path + all handler paths
1834
+ all_states = [then_state] + [s for s, _, _ in handler_states]
1835
+ all_terminated = [then_terminated] + [t for _, _, t in handler_states]
1836
+ all_consumed = [consumed_after_then] + [c for _, c, _ in handler_states]
1837
+
1838
+ # Multi-branch merge: start from first, merge pairwise
1839
+ merged = all_states[0]
1840
+ for s in all_states[1:]:
1841
+ self.init.merge_branches(merged, s)
1842
+ merged = self.init.save()
1843
+
1844
+ # Merge consumed Own[T] params
1845
+ if all(all_terminated):
1846
+ result_consumed: set[str] = set()
1847
+ for c in all_consumed:
1848
+ result_consumed |= c
1849
+ elif any(all_terminated):
1850
+ result_consumed = set()
1851
+ for t, c in zip(all_terminated, all_consumed):
1852
+ if not t:
1853
+ result_consumed = result_consumed & c if result_consumed else c.copy()
1854
+ else:
1855
+ result_consumed = all_consumed[0].copy()
1856
+ for c in all_consumed[1:]:
1857
+ result_consumed &= c
1858
+ self.ctx.func.current_consumed_own_params = result_consumed
1859
+
1860
+ # Analyze finally body
1861
+ prev_in_finally = self.ctx.in_finally
1862
+ self.ctx.in_finally = True
1863
+ for s in stmt.finally_body:
1864
+ self.analyze_stmt(s)
1865
+ self.ctx.in_finally = prev_in_finally
1866
+
1867
+ # Throw-tier uses C++ try/catch with proper scoping -- no goto hoisting
1868
+ # needed in general. BUT try-body variables must be hoisted when:
1869
+ # - finally body exists: finally code runs after the try/catch block
1870
+ # - else body exists: else code is emitted after the try/catch block
1871
+ # - code continues after try/except and try-body vars are in scope
1872
+ # (all handlers terminate, so post-try code uses try-body vars)
1873
+ all_handlers_terminate = all(t for _, _, t in handler_states)
1874
+ needs_hoist = (stmt.finally_body or stmt.else_body
1875
+ or (all_handlers_terminate and not self.ctx.func.init_terminated))
1876
+ self.ctx.func.current_scope.bindings = dict(try_bindings)
1877
+ if needs_hoist:
1878
+ branch_new = set(try_bindings.keys()) - scope_before
1879
+ for h in stmt.handlers:
1880
+ if h.binding:
1881
+ branch_new.discard(h.binding)
1882
+ predecl = branch_new - self.ctx.func.global_declarations
1883
+ if predecl:
1884
+ self.ctx.if_branch_decls[id(stmt)] = {
1885
+ name: try_bindings[name]
1886
+ for name in sorted(predecl)
1887
+ if name in try_bindings
1888
+ }
1889
+ self.ctx.func.hoisted_vars |= predecl
1890
+
1891
+ def _merge_consumed_own(self, then_terminated: bool, else_terminated: bool,
1892
+ consumed_then: set[str], consumed_else: set[str]) -> None:
1893
+ """Merge consumed Own[T] params from two branches."""
1894
+ if then_terminated and else_terminated:
1895
+ self.ctx.func.current_consumed_own_params = consumed_then | consumed_else
1896
+ elif then_terminated:
1897
+ self.ctx.func.current_consumed_own_params = consumed_else
1898
+ elif else_terminated:
1899
+ self.ctx.func.current_consumed_own_params = consumed_then
1900
+ else:
1901
+ self.ctx.func.current_consumed_own_params = consumed_then & consumed_else
1902
+
1903
+ def _analyze_async_for(self, stmt: TpyForEach) -> None:
1904
+ """Analyze `async for x in ait: <body>` (v1.5 M6).
1905
+
1906
+ Resolves `<ait>.__aiter__()` (sync method, returns the async
1907
+ iterator) and the iterator's `__anext__()` (async method, returns
1908
+ `Awaitable[T]`); unwraps T as the loop var's element type.
1909
+ Only allowed inside `async def`.
1910
+ """
1911
+ cur = self.ctx.func.current_function
1912
+ if not (isinstance(cur, TpyFunction) and cur.is_async):
1913
+ raise self.ctx.error(
1914
+ "`async for` is only allowed inside an `async def` "
1915
+ "function body", stmt)
1916
+
1917
+ iterable_type = self.expr.analyze_expr(stmt.iterable)
1918
+ iterable_inner = unwrap_own(unwrap_ref_type(iterable_type))
1919
+
1920
+ record_info = self.ctx.registry.get_record_for_type(iterable_inner)
1921
+ if record_info is None:
1922
+ raise self.ctx.error(
1923
+ f"Type '{iterable_inner}' cannot be used as an async "
1924
+ f"iterable (not a record type)", stmt.iterable)
1925
+
1926
+ aiter_overloads = record_info.methods.get("__aiter__")
1927
+ if not aiter_overloads:
1928
+ raise self.ctx.error(
1929
+ f"Type '{iterable_inner}' cannot be used as an async "
1930
+ f"iterable (missing __aiter__ method)", stmt.iterable)
1931
+ aiter_info = aiter_overloads[0]
1932
+ if aiter_info.is_async:
1933
+ raise self.ctx.error(
1934
+ f"`__aiter__` on '{iterable_inner}' must be a sync "
1935
+ f"`def` (returns the async iterator); only `__anext__` "
1936
+ f"is `async def`", stmt.iterable)
1937
+
1938
+ aiter_type = unwrap_own(unwrap_ref_type(aiter_info.return_type))
1939
+ if not isinstance(aiter_type, NominalType):
1940
+ raise self.ctx.error(
1941
+ f"`__aiter__` on '{iterable_inner}' returns "
1942
+ f"'{aiter_type}', which is not a record type and cannot "
1943
+ f"provide `__anext__`", stmt.iterable)
1944
+ aiter_record = self.ctx.registry.get_record_for_type(aiter_type)
1945
+ if aiter_record is None:
1946
+ raise self.ctx.error(
1947
+ f"`__aiter__` on '{iterable_inner}' returns "
1948
+ f"'{aiter_type}', which is not a record type and cannot "
1949
+ f"provide `__anext__`", stmt.iterable)
1950
+ # Codegen reads this to find the __anext__ sub-coro struct name
1951
+ # without re-walking the registry.
1952
+ stmt.async_aiter_type = aiter_type
1953
+
1954
+ anext_overloads = aiter_record.methods.get("__anext__")
1955
+ if not anext_overloads:
1956
+ raise self.ctx.error(
1957
+ f"Async iterator '{aiter_type}' is missing `__anext__` "
1958
+ f"method", stmt.iterable)
1959
+ anext_info = anext_overloads[0]
1960
+ if not anext_info.is_async:
1961
+ raise self.ctx.error(
1962
+ f"`__anext__` on '{aiter_type}' must be `async def` "
1963
+ f"for use in `async for`", stmt.iterable)
1964
+
1965
+ anext_ret = unwrap_ref_type(anext_info.return_type)
1966
+ if (isinstance(anext_ret, NominalType)
1967
+ and anext_ret.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
1968
+ and len(anext_ret.type_args) == 1):
1969
+ elem_type = anext_ret.type_args[0]
1970
+ else:
1971
+ raise self.ctx.error(
1972
+ f"`__anext__` on '{aiter_type}' must return "
1973
+ f"`Awaitable[T]` (got '{anext_ret}')", stmt.iterable)
1974
+
1975
+ elem_type = self._infer_new_local_type(
1976
+ stmt.var, elem_type, None, None,
1977
+ line=(stmt.loc.line if stmt.loc else None),
1978
+ )
1979
+ stmt.elem_type = make_ref(elem_type)
1980
+ self._record_for_loop_var_type(stmt, elem_type)
1981
+
1982
+ before = self.init.save()
1983
+ consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
1984
+ ns_types_before = self._save_ns_var_types()
1985
+ with self.scopes.loop_scope() as inner_scope:
1986
+ self.init.apply_loop_entry_facts(before)
1987
+ self.ctx.func.mutated_loop_vars.discard(stmt.var)
1988
+ self.ctx.func.consumed_loop_vars.discard(stmt.var)
1989
+ # Register an ITER borrow on a named iterable so structural
1990
+ # mutations of it inside the loop body generate conflict
1991
+ # warnings (e.g. `async for x in items: items.append(...)`).
1992
+ # Limited to TpyName today: __aiter__ returns the aiter by
1993
+ # value into a frame slot, so call/field-access iterables
1994
+ # don't share storage with the loop var (unlike sync for,
1995
+ # where the iterator references the iterable's storage).
1996
+ if isinstance(stmt.iterable, TpyName):
1997
+ bt = self.ctx.func.borrow_tracker
1998
+ bt.add_borrow(stmt.iterable.name, "__for_iter", BorrowKind.ITER)
1999
+ self.ctx.func.loop_var_iterable[stmt.var] = stmt.iterable.name
2000
+ # Protocol-typed or generic-typed iterables: `__aiter__`
2001
+ # is called on the param and may mutate self, so the
2002
+ # generated signature must be `T&` not `const T&`.
2003
+ # Concrete-typed iterables: the analyzer infers
2004
+ # mutability from the call itself, so no extra hint
2005
+ # needed here.
2006
+ inner = unwrap_ref_type(unwrap_readonly(iterable_type))
2007
+ if isinstance(inner, TypeParamRef) or is_protocol_type(inner):
2008
+ self.ctx.mark_param_mutated(stmt.iterable.name)
2009
+ with self.scopes.loop_var(inner_scope, stmt.var, elem_type,
2010
+ inner_scope.depth, is_foreach=True):
2011
+ for s in stmt.body:
2012
+ self.analyze_stmt(s)
2013
+ self.init.apply_loop_exit_facts(before)
2014
+ # Loop body might not execute; consumption inside isn't definite.
2015
+ self.ctx.func.current_consumed_own_params = consumed_before_loop
2016
+ self._restore_ns_var_types(ns_types_before)
2017
+ self._sync_promoted_var_types()
2018
+ self._propagate_for_loop_scope(stmt, inner_scope, elem_type)
2019
+ # `async for` parser already rejects orelse; nothing to analyze.
2020
+
2021
+ def _analyze_with(self, stmt: TpyWith) -> None:
2022
+ """Analyze a with statement (context managers).
2023
+
2024
+ Checks that each context expression has __enter__ and __exit__ methods.
2025
+ Types the as-binding from __enter__ return type. The as-variable is
2026
+ visible after the with block (matching CPython semantics).
2027
+
2028
+ `async with` (stmt.is_async): looks for __aenter__ / __aexit__,
2029
+ validates both are async methods, and unwraps the
2030
+ `Awaitable[T]` wrapper that async-method registration applies to
2031
+ their return types. Only allowed inside `async def`.
2032
+ """
2033
+ if stmt.is_async:
2034
+ cur = self.ctx.func.current_function
2035
+ if not (isinstance(cur, TpyFunction) and cur.is_async):
2036
+ raise self.ctx.error(
2037
+ "`async with` is only allowed inside an `async def` "
2038
+ "function body", stmt)
2039
+
2040
+ enter_method = "__aenter__" if stmt.is_async else "__enter__"
2041
+ exit_method = "__aexit__" if stmt.is_async else "__exit__"
2042
+ kind_label = "async context manager" if stmt.is_async else "context manager"
2043
+
2044
+ for item in stmt.items:
2045
+ ctx_type = unwrap_own(unwrap_ref_type(self.expr.analyze_expr(item.context_expr)))
2046
+
2047
+ # Look up __[a]enter__ / __[a]exit__ on the context manager type.
2048
+ record_info = self.ctx.registry.get_record_for_type(ctx_type)
2049
+ err_node = item.context_expr
2050
+ if record_info is None:
2051
+ raise self.ctx.error(
2052
+ f"Type '{ctx_type}' cannot be used as a {kind_label}"
2053
+ f" (not a record type)", err_node)
2054
+
2055
+ enter_overloads = record_info.methods.get(enter_method)
2056
+ if not enter_overloads:
2057
+ raise self.ctx.error(
2058
+ f"Type '{ctx_type}' cannot be used as a {kind_label}"
2059
+ f" (missing {enter_method} method)", err_node)
2060
+
2061
+ exit_overloads = record_info.methods.get(exit_method)
2062
+ if not exit_overloads:
2063
+ raise self.ctx.error(
2064
+ f"Type '{ctx_type}' cannot be used as a {kind_label}"
2065
+ f" (missing {exit_method} method)", err_node)
2066
+
2067
+ # Validate async-ness: in `async with`, both methods must be
2068
+ # `async def`. In sync `with`, neither should be.
2069
+ enter_info = enter_overloads[0]
2070
+ exit_info = exit_overloads[0]
2071
+ if stmt.is_async:
2072
+ if not enter_info.is_async:
2073
+ raise self.ctx.error(
2074
+ f"`{enter_method}` on '{ctx_type}' must be `async def` "
2075
+ f"for use in `async with`", err_node)
2076
+ if not exit_info.is_async:
2077
+ raise self.ctx.error(
2078
+ f"`{exit_method}` on '{ctx_type}' must be `async def` "
2079
+ f"for use in `async with`", err_node)
2080
+ # v1.5 M5 limitation: cleanup-only `__aexit__` (all-None
2081
+ # args). Inspecting `exc_val: Optional[BaseException]`
2082
+ # requires polymorphic exception storage across the
2083
+ # suspension (E9 / Phase 20).
2084
+ if len(exit_info.params) >= 2 and isinstance(
2085
+ exit_info.params[1].type, OptionalType):
2086
+ raise self.ctx.error(
2087
+ f"`{exit_method}` with `exc_val: Optional["
2088
+ f"BaseException]` is not yet supported in "
2089
+ f"`async with` -- v1.5 M5 only handles "
2090
+ f"cleanup-only managers. Annotate exc_val as "
2091
+ f"`None` to discard the exception. Full "
2092
+ f"polymorphic-exception inspection is tracked "
2093
+ f"as E9 / Phase 20.", err_node)
2094
+
2095
+ # v1.5 M1: tag whether __exit__ / __aexit__ may suppress
2096
+ # exceptions, and whether exc_val is typed Optional[BaseException]
2097
+ # (vs None). Registration rejects multi-overload `__exit__`;
2098
+ # async methods don't participate in @overload, so each has
2099
+ # exactly one entry.
2100
+ # For async methods, the FunctionInfo return type is wrapped
2101
+ # in `Cancellable[T]` for async-def results (or `Awaitable[T]`
2102
+ # for user types with bare `__poll__`); unwrap one level to
2103
+ # see the user's declared T.
2104
+ exit_ret = exit_info.return_type
2105
+ if (stmt.is_async and isinstance(exit_ret, NominalType)
2106
+ and exit_ret.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
2107
+ and len(exit_ret.type_args) == 1):
2108
+ exit_ret = exit_ret.type_args[0]
2109
+ item.exit_can_suppress = exit_ret == BOOL
2110
+ item.exit_takes_exc_val = (
2111
+ len(exit_info.params) >= 2
2112
+ and isinstance(exit_info.params[1].type, OptionalType)
2113
+ )
2114
+
2115
+ # Get return type of __[a]enter__() -- use the first overload.
2116
+ # For async, unwrap Awaitable[T] / Cancellable[T] so the
2117
+ # as-binding sees T.
2118
+ enter_type = unwrap_ref_type(enter_info.return_type)
2119
+ if (stmt.is_async and isinstance(enter_type, NominalType)
2120
+ and enter_type.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
2121
+ and len(enter_type.type_args) == 1):
2122
+ enter_type = enter_type.type_args[0]
2123
+ item.enter_type = enter_type
2124
+
2125
+ # Register the as-variable if present
2126
+ if item.target is not None:
2127
+ resolved = self._infer_new_local_type(
2128
+ item.target, enter_type, None, None,
2129
+ line=(stmt.loc.line if stmt.loc else None),
2130
+ )
2131
+ item.enter_type = resolved
2132
+ self.ctx.func.current_scope.define(item.target, resolved)
2133
+ self.init.mark_assigned(item.target)
2134
+
2135
+ # Analyze the body -- track new variable declarations so codegen
2136
+ # can pre-declare them outside the guard {} scope (C++ scoping).
2137
+ scope_before = set(self.ctx.func.current_scope.bindings.keys())
2138
+
2139
+ for s in stmt.body:
2140
+ self.analyze_stmt(s)
2141
+
2142
+ # All variables first declared inside the body need pre-declaration
2143
+ # since codegen wraps the body in try {} for the with's cleanup pattern.
2144
+ branch_new = set(self.ctx.func.current_scope.bindings.keys()) - scope_before
2145
+ predecl = branch_new - self.ctx.func.global_declarations
2146
+ if predecl:
2147
+ self.ctx.if_branch_decls[id(stmt)] = {
2148
+ name: self.ctx.func.current_scope.lookup(name)
2149
+ for name in sorted(predecl)
2150
+ if name in self.ctx.func.current_scope.bindings
2151
+ }
2152
+
2153
+ # --- Nested def / nonlocal ---
2154
+
2155
+ def _analyze_nonlocal(self, stmt: TpyNonlocal) -> None:
2156
+ """Analyze a nonlocal declaration."""
2157
+ if not self.ctx.func.in_nested_def:
2158
+ raise self.ctx.error(
2159
+ "'nonlocal' is only valid inside a nested function", stmt)
2160
+ for name in stmt.names:
2161
+ if name not in self.ctx.func.outer_scope_locals:
2162
+ raise self.ctx.error(
2163
+ f"No binding for nonlocal '{name}' found in enclosing scope",
2164
+ stmt)
2165
+ self.ctx.func.current_nonlocal_names.add(name)
2166
+
2167
+ def _prescan_and_analyze_body(
2168
+ self,
2169
+ func: TpyFunction,
2170
+ params: list[tuple[str, TpyType]],
2171
+ scope: 'Scope',
2172
+ ns: 'Namespace | None',
2173
+ ) -> ScanResult:
2174
+ """Shared function body analysis: bind params, prescan, analyze statements.
2175
+
2176
+ Used by both _analyze_function (analyzer.py) and _analyze_nested_def.
2177
+ Callers handle scope creation, type resolution, and post-processing.
2178
+ """
2179
+ # Bind params to scope with Ref for non-value types.
2180
+ # analyze_expr strips Ref from return values, so downstream sema
2181
+ # sees bare types. Ref on scope types enables provenance-aware
2182
+ # code (assignment warnings, local scope propagation).
2183
+ param_names: set[str] = set()
2184
+ for pname, ptype in params:
2185
+ param_names.add(pname)
2186
+ scope_type = make_ref(ptype)
2187
+ scope.define(pname, scope_type)
2188
+ self.ctx.func.var_scope_depth[pname] = scope.depth
2189
+ self.ctx.func.definitely_assigned.add(pname)
2190
+ if ns:
2191
+ ns.bind_variable(pname, scope_type)
2192
+
2193
+ # Param tracking for mutation analysis
2194
+ self.ctx.func.current_param_names = param_names
2195
+ self.ctx.func.current_param_name_to_idx = {p: i for i, (p, _) in enumerate(params)}
2196
+ # For non-static methods, include "self" in the param index map with
2197
+ # sentinel -1 so that _record_mutation_call_edges can track self
2198
+ # flowing through free function calls (e.g., helper(self)).
2199
+ # Phase 2 (_resolve_single) converts caller_idx == -1 to self_mutated.
2200
+ if func.is_method and not func.is_staticmethod:
2201
+ self.ctx.func.current_param_name_to_idx["self"] = -1
2202
+
2203
+ # Prescan for reassigned variables + last-use liveness
2204
+ scan = scan_reassigned_vars(func.body, pre_declared=param_names)
2205
+ self.ctx.all_last_uses |= analyze_last_uses(func.body, scan.alias_sources)
2206
+ self.ctx.func.current_reassigned_vars = scan.reassigned.copy()
2207
+ self.ctx.func.current_lvalue_reassigned = scan.lvalue_reassigned.copy()
2208
+ self.ctx.func.current_aug_assigned_vars = scan.aug_assigned.copy()
2209
+
2210
+ # Analyze body
2211
+ for stmt in func.body:
2212
+ self.analyze_stmt(stmt)
2213
+
2214
+ return scan
2215
+
2216
+ def _analyze_nested_def(self, stmt: TpyNestedDef) -> None:
2217
+ """Analyze a nested function definition."""
2218
+ func = stmt.func
2219
+
2220
+ if self.ctx.func.in_nested_def:
2221
+ raise self.ctx.error(
2222
+ "Nested functions cannot contain further nested functions",
2223
+ stmt)
2224
+
2225
+ # Collect outer locals available for capture
2226
+ outer_locals = self.ctx.func.definitely_assigned.copy()
2227
+
2228
+ # Pre-scan nonlocal declarations (at any nesting depth) to bind
2229
+ # them in the inner scope before body analysis begins.
2230
+ nonlocal_names: set[str] = set()
2231
+ self._collect_nonlocal_names(func.body, nonlocal_names)
2232
+
2233
+ # Resolve param and return types
2234
+ resolved_params_bare = [(p, self.type_ops.resolve_type(t)) for p, t in func.params]
2235
+ func.params = [(p, make_ref(t)) for p, t in resolved_params_bare]
2236
+ params = resolved_params_bare
2237
+ param_names = {p for p, _ in params}
2238
+ return_type = self.type_ops.resolve_type(func.return_type)
2239
+ func.return_type = make_ref(return_type)
2240
+
2241
+ # Analyze body in isolated scope
2242
+ with self.scopes.nested_def_scope(func) as inner_scope:
2243
+ self.ctx.func.outer_scope_locals = outer_locals
2244
+
2245
+ # Add nonlocal names to inner scope with types from outer
2246
+ for name in nonlocal_names:
2247
+ outer_type = inner_scope.parent.lookup(name) if inner_scope.parent else None
2248
+ if outer_type is not None:
2249
+ inner_scope.define(name, outer_type)
2250
+ self.ctx.func.definitely_assigned.add(name)
2251
+ if self.ctx.func.current_ns:
2252
+ self.ctx.func.current_ns.bind_variable(name, outer_type)
2253
+
2254
+ self._prescan_and_analyze_body(func, params, inner_scope, self.ctx.func.current_ns)
2255
+
2256
+ # Use the authoritative nonlocal set from body analysis
2257
+ # (covers nonlocal declarations at any nesting depth)
2258
+ nonlocal_names = self.ctx.func.current_nonlocal_names.copy()
2259
+ stmt.nonlocal_names = nonlocal_names
2260
+
2261
+ # Compute captures: free variables that come from outer scope
2262
+ free_names = _collect_body_name_refs(func.body)
2263
+ local_defs = _collect_body_local_defs(func.body)
2264
+ captured = sorted(
2265
+ (free_names - param_names - local_defs - nonlocal_names) & outer_locals
2266
+ )
2267
+ # Nonlocal names are also captures (mutable references)
2268
+ for name in sorted(nonlocal_names):
2269
+ if name not in captured:
2270
+ captured.append(name)
2271
+ stmt.captured_names = captured
2272
+
2273
+ # Create FunctionInfo and register as local function
2274
+ param_infos = [ParamInfo(name=pname, type=ptype) for pname, ptype in params]
2275
+ fi = FunctionInfo(
2276
+ name=func.name,
2277
+ params=param_infos,
2278
+ return_type=return_type,
2279
+ )
2280
+
2281
+ # Bind as FUNCTION in the local namespace
2282
+ if self.ctx.func.current_ns:
2283
+ self.ctx.func.current_ns.bind_function(fi)
2284
+ self.ctx.func.definitely_assigned.add(func.name)
2285
+
2286
+ # Track for escape analysis
2287
+ self.ctx.func.nested_def_names.add(func.name)
2288
+ self.ctx.func.nested_def_nodes[func.name] = stmt
2289
+
2290
+ def _collect_nonlocal_names(self, stmts: list, names: set[str]) -> None:
2291
+ """Recursively collect nonlocal declarations from all nesting depths."""
2292
+ for s in stmts:
2293
+ if isinstance(s, TpyNonlocal):
2294
+ names.update(s.names)
2295
+ elif not isinstance(s, TpyNestedDef):
2296
+ for body in s.sub_bodies():
2297
+ self._collect_nonlocal_names(body, names)
2298
+
2299
+ def _resolve_literal_type(self, t: TpyType) -> TpyType:
2300
+ """Resolve IntLiteralType/FloatLiteralType to concrete types."""
2301
+ if isinstance(t, IntLiteralType):
2302
+ return self.ctx.default_int_for_literal(t)
2303
+ if isinstance(t, FloatLiteralType):
2304
+ return FLOAT
2305
+ return t
2306
+
2307
+ def _record_for_loop_var_type(self, stmt: TpyForEach, elem_type: TpyType) -> None:
2308
+ """Record the resolved loop var type for `# tpyc: type()` validation
2309
+ and IDE display. The actual `loop_var` binding keeps `elem_type` as-is
2310
+ so the body's overload resolution retains IntLiteralType-to-BigInt
2311
+ flexibility (heapq pattern: `for v in [literals]: heappush(h: list[int], v)`).
2312
+ Tuple-unpack for-loops have a synthetic `stmt.var` -- the user-facing
2313
+ names are recorded by the body's TpyTupleUnpack handler instead.
2314
+
2315
+ Limitation: if the body retro-widens the loop var to BigInt (heapq
2316
+ pattern), this recording isn't updated -- local_deduction's retro-widen
2317
+ path keys on var_decl_by_name, which only contains TpyVarDecl-backed
2318
+ names. `# tpyc: type()` on such a loop var would show the literal-
2319
+ defaulted Int32, not the widened BigInt.
2320
+ """
2321
+ if not stmt.loc or stmt.is_tuple_unpack:
2322
+ return
2323
+ self.ctx.declared_var_types[(stmt.loc.line, stmt.var)] = (
2324
+ resolve_int_literals(elem_type, self.ctx.default_int_for_literal))
2325
+
2326
+ def _propagate_loop_body_vars(self, stmt: TpyStmt,
2327
+ inner_scope: 'Scope',
2328
+ skip_var: str | None = None) -> None:
2329
+ """Store body-declared variables from a for-loop scope as pending.
2330
+
2331
+ Variables are lazily promoted to the parent scope when first
2332
+ referenced after the loop.
2333
+
2334
+ skip_var: loop variable name to exclude (already handled by caller).
2335
+ """
2336
+ for name, var_type in inner_scope.bindings.items():
2337
+ if name == skip_var:
2338
+ continue
2339
+ if name in self.ctx.func.global_declarations:
2340
+ continue
2341
+ resolved = self._resolve_literal_type(var_type)
2342
+ self.ctx.func.pending_loop_vars[name] = (resolved, stmt, None)
2343
+
2344
+ # Re-parent any pending vars from nested loops to this (outer) loop,
2345
+ # so they get pre-declared before this loop if referenced after it.
2346
+ for name, (vtype, loop_stmt, orig_stmt) in list(self.ctx.func.pending_loop_vars.items()):
2347
+ if loop_stmt is not stmt and name not in inner_scope.bindings:
2348
+ self.ctx.func.pending_loop_vars[name] = (vtype, stmt, orig_stmt)
2349
+
2350
+ def _propagate_for_loop_scope(self, stmt: TpyForEach,
2351
+ inner_scope: 'Scope',
2352
+ elem_type: TpyType) -> None:
2353
+ """Store for-loop variable and body-declared variables as pending."""
2354
+ # Loop variable (skip synthetic tuple-unpack vars in non-generators;
2355
+ # generators need the synthetic var as a struct field)
2356
+ var_name = stmt.var
2357
+ is_generator = (isinstance(self.ctx.func.current_function, TpyFunction)
2358
+ and self.ctx.func.current_function.is_generator)
2359
+ if ((not stmt.is_tuple_unpack or is_generator)
2360
+ and var_name not in self.ctx.func.global_declarations):
2361
+ resolved = self._resolve_literal_type(elem_type)
2362
+ self.ctx.func.pending_loop_vars[var_name] = (resolved, stmt, stmt)
2363
+
2364
+ self._propagate_loop_body_vars(stmt, inner_scope, skip_var=var_name)
2365
+
2366
+ def _analyze_global_stmt(self, stmt: TpyGlobal) -> None:
2367
+ """Analyze a `global x, y` statement."""
2368
+ # Must be inside a function, not at module level
2369
+ if self.ctx.is_top_level or isinstance(self.ctx.func.current_function, type(MODULE_INIT_CONTEXT)):
2370
+ raise self.ctx.error("'global' declaration is only allowed inside a function", stmt)
2371
+ for name in stmt.names:
2372
+ # Cannot use 'global' with Final variables
2373
+ if name in self.ctx.final_globals:
2374
+ raise self.ctx.error(
2375
+ f"Cannot use 'global' with Final variable '{name}'", stmt)
2376
+ # Name must exist in global scope
2377
+ global_type = self.ctx.global_scope.lookup(name)
2378
+ if global_type is None:
2379
+ raise self.ctx.error(f"name '{name}' is not defined at module level", stmt)
2380
+ # Must not shadow a function parameter
2381
+ func = self.ctx.func.current_function
2382
+ if isinstance(func, TpyFunction):
2383
+ for pname, _ in func.params:
2384
+ if pname == name:
2385
+ raise self.ctx.error(
2386
+ f"name '{name}' is a parameter and cannot be declared global", stmt)
2387
+ self.ctx.func.global_declarations.add(name)
2388
+
2389
+ def validate_compile_time_constant(
2390
+ self, init: TpyExpr, target_type: 'TpyType | None', label: str, loc: object,
2391
+ ) -> None:
2392
+ """Validate that `init` is a compile-time constant; raise a uniform
2393
+ diagnostic otherwise. Shared between module-level `Final` decls and
2394
+ class constants -- callers pass `label` (e.g. `"Final variable 'X'"`
2395
+ or `"class constant 'C.X'"`) to customize the noun phrase.
2396
+ """
2397
+ bad = self._find_nonconstant_leaf(init, target_type)
2398
+ if bad is None:
2399
+ return
2400
+ prefix = f"{label} requires a compile-time constant initializer"
2401
+ if isinstance(bad, TpyName):
2402
+ imp = lookup_imported(
2403
+ self.ctx.module_attributes, bad.name, SymbolKind.VARIABLE)
2404
+ if imp is not None:
2405
+ src_mod, _ = imp
2406
+ raise self.ctx.error(
2407
+ f"{prefix}; cross-module Final references are not yet supported "
2408
+ f"('{bad.name}' is imported from '{src_mod}')",
2409
+ loc,
2410
+ )
2411
+ if bad is not init and isinstance(bad, TpyName):
2412
+ raise self.ctx.error(
2413
+ f"{prefix}; '{bad.name}' is not a Final constant",
2414
+ loc,
2415
+ )
2416
+ raise self.ctx.error(prefix, loc)
2417
+
2418
+ def _find_nonconstant_leaf(self, expr: TpyExpr, target_type: 'TpyType | None' = None) -> 'TpyExpr | None':
2419
+ """Find the first non-constant sub-expression in a Final initializer.
2420
+
2421
+ Returns None if the expression is a valid compile-time constant,
2422
+ or the offending sub-expression otherwise.
2423
+
2424
+ Accepts: literals, unary ops on constant operands, references to other
2425
+ Final globals, @call_macro expansions that reduce to a constant,
2426
+ primitive type constructor calls with constant args, binary ops on
2427
+ constant operands (when target_type is numeric/bool), and tuple
2428
+ literals with all-constant elements.
2429
+ """
2430
+ # Unwrap compile-time macro expansions
2431
+ macro_exp = getattr(expr, "macro_expansion", None)
2432
+ if macro_exp is not None:
2433
+ return self._find_nonconstant_leaf(macro_exp, target_type)
2434
+ if isinstance(expr, (TpyIntLiteral, TpyFloatLiteral, TpyBoolLiteral, TpyStrLiteral)):
2435
+ return None
2436
+ if isinstance(expr, TpyUnaryOp):
2437
+ return self._find_nonconstant_leaf(expr.operand, target_type)
2438
+ if isinstance(expr, TpyName) and expr.name in self.ctx.analyzed_finals:
2439
+ return None
2440
+ if isinstance(expr, TpyBinOp) and is_numeric_type(target_type):
2441
+ return (self._find_nonconstant_leaf(expr.left, target_type)
2442
+ or self._find_nonconstant_leaf(expr.right, target_type))
2443
+ # Primitive type constructor: Float32(0.5), Int64(SOME_FINAL), etc.
2444
+ # Identified by: resolved to an __init__ method, result is a primitive.
2445
+ if isinstance(expr, TpyCall) and len(expr.args) == 1:
2446
+ fi = expr.resolved_function_info
2447
+ if fi is not None and fi.name == '__init__':
2448
+ result_type = expr.call_type or self.ctx.get_expr_type(expr)
2449
+ if is_numeric_type(result_type) or is_char_type(result_type):
2450
+ return self._find_nonconstant_leaf(expr.args[0], result_type)
2451
+ if isinstance(expr, TpyTupleLiteral):
2452
+ if isinstance(target_type, TupleType) and len(target_type.element_types) == len(expr.elements):
2453
+ for e, t in zip(expr.elements, target_type.element_types):
2454
+ bad = self._find_nonconstant_leaf(e, t)
2455
+ if bad is not None:
2456
+ return bad
2457
+ return None
2458
+ for e in expr.elements:
2459
+ bad = self._find_nonconstant_leaf(e)
2460
+ if bad is not None:
2461
+ return bad
2462
+ return None
2463
+ return expr
2464
+
2465
+ def _check_nonvalue_rebinding(self, name: str, node: TpyStmt) -> None:
2466
+ """Error if reassigning a non-value-type param, loop variable, or global."""
2467
+ existing_type = self.ctx.func.current_scope.lookup(name)
2468
+ if existing_type is None or unwrap_readonly(existing_type).is_value_type():
2469
+ return
2470
+ # Check function parameters
2471
+ func = self.ctx.func.current_function
2472
+ if isinstance(func, TpyFunction):
2473
+ for pname, ptype in func.params:
2474
+ ptype_bare = unwrap_ref_type(ptype)
2475
+ if pname == name and not unwrap_readonly(ptype_bare).is_value_type():
2476
+ raise self.ctx.error(
2477
+ f"Cannot reassign parameter '{name}' of type '{unwrap_readonly(ptype_bare)}'; "
2478
+ f"assign to a new local variable instead",
2479
+ node
2480
+ )
2481
+ # Check for-each loop variables
2482
+ if name in self.ctx.func.loop_vars:
2483
+ raise self.ctx.error(
2484
+ f"Cannot reassign loop variable '{name}' of type '{existing_type}'; "
2485
+ f"assign to a new local variable instead",
2486
+ node
2487
+ )
2488
+ # Check global-declared non-value-type variables
2489
+ if name in self.ctx.func.global_declarations:
2490
+ raise self.ctx.error(
2491
+ f"Cannot reassign global variable '{name}' of non-value type '{existing_type}'",
2492
+ node
2493
+ )
2494
+
2495
+ def _infer_new_local_view_type(
2496
+ self, name: str, var_type: TpyType,
2497
+ init_expr: TpyExpr | None, init_type: TpyType | None,
2498
+ line: int | None, family: ViewTypeFamily,
2499
+ ) -> TpyType:
2500
+ """Create a pending view type for a new local of the given family."""
2501
+ var_id = self.ctx.next_view_var_id(family)
2502
+ vars_reg = self.ctx.view_vars(family)
2503
+ var_map = self.ctx.view_var_map(family)
2504
+
2505
+ if isinstance(var_type, PendingViewType):
2506
+ # Alias from another pending view type -- collect ALL leaf sources.
2507
+ # For `x = a or b` both a and b are sources; if either resolves to
2508
+ # owned, x must too (otherwise x would be a view into a
2509
+ # potentially-reallocated buffer).
2510
+ if init_expr is not None:
2511
+ source_ids = [t.var_id for t in collect_pending_source_types(self.ctx, init_expr)
2512
+ if isinstance(t, family.pending_type_class)]
2513
+ else:
2514
+ source_ids = [var_type.var_id]
2515
+ info = ViewVarInfo(var_id=var_id, variable_name=name,
2516
+ decl_line=line, source_var_ids=source_ids)
2517
+ else:
2518
+ # Fresh from owned type (str or bytes)
2519
+ if init_expr is not None:
2520
+ is_owned = not self.deduction.is_view_compatible_source(init_expr, init_type)
2521
+ else:
2522
+ is_owned = False
2523
+ # Track source storage for subscript/field views so that
2524
+ # mutations on the source fall back to the owned type.
2525
+ source_storage: str | None = None
2526
+ if not is_owned and init_expr is not None:
2527
+ unwrapped_init = init_expr.expr if isinstance(init_expr, TpyCoerce) else init_expr
2528
+ if isinstance(unwrapped_init, (TpySubscript, TpyFieldAccess)):
2529
+ root = _borrow_storage_root(unwrapped_init)
2530
+ if root is not None:
2531
+ source_storage = self.ctx.func.borrow_tracker.effective_storage(root)
2532
+ info = ViewVarInfo(var_id=var_id, variable_name=name,
2533
+ decl_line=line, initialized_from_owned=is_owned,
2534
+ source_storage=source_storage)
2535
+ if source_storage is not None:
2536
+ self.ctx.view_source_borrows_map(family).setdefault(source_storage, set()).add(var_id)
2537
+
2538
+ vars_reg[var_id] = info
2539
+ var_map[name] = var_id
2540
+ self.ctx.view_pending_resolutions(family).append(var_id)
2541
+ return family.pending_type_class(var_id)
2542
+
2543
+ def _infer_new_local_type(
2544
+ self, name: str, var_type: TpyType,
2545
+ init_expr: TpyExpr | None, init_type: TpyType | None,
2546
+ line: int | None,
2547
+ ) -> TpyType:
2548
+ """Apply deferred type inference for a new local variable.
2549
+
2550
+ Handles view-type families (str/bytes -> PendingViewType) and
2551
+ PendingListType alias logic. Skipped at module top-level.
2552
+
2553
+ When init_expr is None (for-loop var, tuple unpack), the source is
2554
+ considered view-compatible (initialized_from_owned=False) because
2555
+ the container outlives the loop/unpack scope.
2556
+ """
2557
+ if self.ctx.is_top_level:
2558
+ return var_type
2559
+ # Float literals resolve to float64 as new local variables
2560
+ if isinstance(var_type, FloatLiteralType):
2561
+ return FLOAT
2562
+
2563
+ # View-type families (str/bytes): register a pending view-vars entry for
2564
+ # deferred view-vs-owned storage resolution. For LiteralType[str/bytes] the
2565
+ # entry is registered for codegen but `stmt.type` stays LiteralType so
2566
+ # OOS rejection / dispatch / narrowing keep seeing the annotation.
2567
+ family = view_family_for_type(var_type)
2568
+ if family is not None:
2569
+ pending = self._infer_new_local_view_type(name, var_type, init_expr, init_type, line, family)
2570
+ return var_type if isinstance(var_type, LiteralType) else pending
2571
+ elif (isinstance(var_type, PendingListType)
2572
+ and init_expr is not None and isinstance(init_expr, TpyName)):
2573
+ return self.deduction.register_list_alias(
2574
+ name, var_type,
2575
+ decl_line=line,
2576
+ )
2577
+
2578
+ return var_type
2579
+
2580
+ def _analyze_yield(self, stmt: TpyYield) -> None:
2581
+ """Analyze a yield statement in a generator function."""
2582
+ func = self.ctx.func.current_function
2583
+ if func is None or not func.is_generator:
2584
+ raise self.ctx.error("'yield' can only be used inside a generator function", stmt)
2585
+ elem_type = func.generator_yield_type
2586
+ assert elem_type is not None
2587
+ yield_type = self.expr.analyze_expr_with_hint(stmt.value, elem_type)
2588
+ stmt.value = self.compat.coerce_expr(
2589
+ stmt.value, yield_type, elem_type, "yield value",
2590
+ coercion_ctx=CoercionContext.RETURN)
2591
+ self.compat.check_view_return_dangle(stmt.value, elem_type, stmt.loc)
2592
+
2593
+ def _analyze_var_decl(self, stmt: TpyVarDecl) -> None:
2594
+ """Analyze a variable declaration."""
2595
+ # In nested defs, assigning to an outer variable requires nonlocal
2596
+ if (self.ctx.func.in_nested_def
2597
+ and stmt.name in self.ctx.func.outer_scope_locals
2598
+ and stmt.name not in self.ctx.func.current_nonlocal_names):
2599
+ raise self.ctx.error(
2600
+ f"Cannot assign to '{stmt.name}' in nested function"
2601
+ f" without 'nonlocal' declaration",
2602
+ stmt)
2603
+ # Type alias expansion: replace NominalType("Shape") with the alias
2604
+ # target (one level; recurses into the target's inner types; _seen
2605
+ # guards self-referential aliases). Must run before `resolve_type`
2606
+ # because alias targets can contain further type references that
2607
+ # resolve_type wants to process uniformly. Enum substitution is
2608
+ # handled by `resolve_type` itself -- it replaces bare
2609
+ # NominalType("Color") placeholders with the registered enum
2610
+ # NominalType (which carries `_module_qname`), whether the enum
2611
+ # appears directly or is the target of a resolved alias.
2612
+ if stmt.type and self.ctx.registry.type_aliases:
2613
+ registry = self.ctx.registry
2614
+ recursive_names = self.ctx.recursive_union_names
2615
+
2616
+ def _expand_aliases(t: TpyType, _seen: frozenset[str] = frozenset()) -> TpyType:
2617
+ # Alias references are bare parser NominalType placeholders
2618
+ # (no _module_qname, no TypeDef entry) -- the qname guard
2619
+ # enforces "unresolved only," matching `_resolve_alias` in
2620
+ # analyzer.py. `get_type_alias` is the specific check.
2621
+ if (isinstance(t, NominalType)
2622
+ and not t.is_protocol
2623
+ and not t._module_qname
2624
+ and t.name not in _seen
2625
+ and t.name not in recursive_names):
2626
+ alias = registry.get_type_alias(t.name)
2627
+ if alias is not None:
2628
+ new_seen = _seen | {t.name}
2629
+ return alias.map_inner_types(
2630
+ lambda inner: _expand_aliases(inner, new_seen)
2631
+ )
2632
+ return t.map_inner_types(lambda inner: _expand_aliases(inner, _seen))
2633
+
2634
+ stmt.type = _expand_aliases(stmt.type)
2635
+
2636
+ # resolve_type sets is_protocol flag, upgrades NominalType -> TypeParamRef
2637
+ # in generic scopes, substitutes enum placeholders, and handles
2638
+ # compile-time-only aliases (FStr etc.).
2639
+ if stmt.type:
2640
+ stmt.type = self.type_ops.resolve_type(stmt.type)
2641
+
2642
+ # Validate the type annotation if present
2643
+ # Allow TypeParamRef inside generic functions or generic record methods
2644
+ if stmt.type:
2645
+ # Own[T] / readonly[T] are only valid for parameters and return
2646
+ # types, not variables. Run these context-specific rejections
2647
+ # BEFORE validate_type so a richer-context diagnostic wins over
2648
+ # the generic shape rejection (e.g. `Own[Optional[Polymorphic]]`
2649
+ # at a local should surface "Own[T] not allowed as variable",
2650
+ # not "use Optional[T] or Box[T]" -- the user can't use either
2651
+ # in a local context either).
2652
+ if isinstance(stmt.type, OwnType):
2653
+ raise self.ctx.error(
2654
+ f"Own[{stmt.type.wrapped}] cannot be used as a variable type. "
2655
+ f"Use '{stmt.type.wrapped}' instead (Own[T] is for parameters and return types only)",
2656
+ stmt
2657
+ )
2658
+ if isinstance(stmt.type, ReadonlyType):
2659
+ raise self.ctx.error(
2660
+ f"readonly[{stmt.type.wrapped}] cannot be used as a variable type. "
2661
+ f"Readonly on locals is deduced from initialization",
2662
+ stmt
2663
+ )
2664
+ try:
2665
+ in_generic = bool(
2666
+ (isinstance(self.ctx.func.current_function, TpyFunction) and self.ctx.func.current_function.type_params)
2667
+ or self.ctx.record_ctx.type_params
2668
+ )
2669
+ # An init-only function-local `p: Optional[Pet] = Dog()` lowers
2670
+ # to a `const Pet*` pointing at the materialized rvalue, same
2671
+ # shape as the parameter position. Allow the pointer-repr
2672
+ # @dynamic Optional here; the rvalue-rebind reject (shared slot
2673
+ # can't retype) and definite-assignment keep it sound. Globals
2674
+ # (module-init sentinel) and no-init declarations stay rejected
2675
+ # -- a static slot has no value representation for an abstract
2676
+ # base.
2677
+ allow_ptr_repr_dyn = (
2678
+ stmt.init is not None
2679
+ and isinstance(self.ctx.func.current_function, TpyFunction)
2680
+ )
2681
+ self.type_ops.validate_type(
2682
+ stmt.type, allow_type_param_ref=in_generic, loc=stmt.loc,
2683
+ allow_forward_ref=False,
2684
+ allow_pointer_repr_dynamic=allow_ptr_repr_dyn)
2685
+ except SemanticError as e:
2686
+ raise self.ctx.error(str(e), stmt)
2687
+
2688
+ # Fn[...] is only valid in parameter position
2689
+ if stmt.type and contains_fn_type(stmt.type):
2690
+ raise self.ctx.error(
2691
+ "Fn type is only valid in parameter position. "
2692
+ "Use Callable for fields, returns, and locals",
2693
+ stmt
2694
+ )
2695
+
2696
+ # Final[T] validation
2697
+ # Detect FinalType from annotation (covers function-level where register_globals didn't run)
2698
+ if stmt.type and isinstance(stmt.type, FinalType):
2699
+ stmt.type = final_type_str_to_strview(stmt.type.wrapped)
2700
+ stmt.is_final = True
2701
+ if stmt.is_final:
2702
+ if not self.ctx.is_top_level:
2703
+ raise self.ctx.error(
2704
+ f"Final can only be used at module level",
2705
+ stmt
2706
+ )
2707
+ if stmt.name in self.ctx.analyzed_finals:
2708
+ raise self.ctx.error(
2709
+ f"Cannot re-declare Final variable '{stmt.name}'",
2710
+ stmt
2711
+ )
2712
+ if not stmt.init:
2713
+ raise self.ctx.error(
2714
+ f"Final variable '{stmt.name}' must have an initializer",
2715
+ stmt
2716
+ )
2717
+ inner = stmt.type
2718
+ if not is_final_allowed_inner(inner):
2719
+ raise self.ctx.error(
2720
+ f"Final[{inner}] is not supported; {FINAL_INNER_TYPE_ERROR}",
2721
+ stmt
2722
+ )
2723
+
2724
+ if self.ctx.is_top_level and not stmt.is_final:
2725
+ type_hint = str(stmt.type) if stmt.type else "<type>"
2726
+ self._warn_all_caps_without_final(stmt.name, type_hint, stmt)
2727
+
2728
+ # Protocol types can only be used for function parameters, not variables
2729
+ # Exception: @dynamic protocols can be used as variable types
2730
+ if stmt.type and is_protocol_type(stmt.type):
2731
+ protocol_info = protocol_info_of(stmt.type)
2732
+ if not protocol_info or not protocol_info.is_dynamic:
2733
+ raise self.ctx.error(
2734
+ f"Protocol type '{stmt.type.name}' cannot be used as a variable type. "
2735
+ f"Only @dynamic protocols can be used as variable types",
2736
+ stmt
2737
+ )
2738
+
2739
+ # Detect native global import: x: T = native_global("name")
2740
+ if isinstance(stmt.init, TpyCall) and isinstance(stmt.init.func, TpyName) and self.ctx.func.current_ns:
2741
+ binding = self.ctx.func.current_ns.lookup(stmt.init.func_name)
2742
+ if (binding and binding.kind == BindingKind.IMPORTED_NAME
2743
+ and binding.import_source
2744
+ and binding.import_source[0] == "tpy.extern"
2745
+ and binding.import_source[1] == "native_global"):
2746
+ func_name = binding.import_source[1]
2747
+ if not self.ctx.is_top_level:
2748
+ raise self.ctx.error(
2749
+ f"{func_name}() can only be used at module level",
2750
+ stmt
2751
+ )
2752
+ if stmt.type is None:
2753
+ raise self.ctx.error(
2754
+ f"{func_name}() requires a type annotation",
2755
+ stmt
2756
+ )
2757
+ native_name = None
2758
+ if len(stmt.init.args) == 1:
2759
+ if not isinstance(stmt.init.args[0], TpyStrLiteral):
2760
+ raise self.ctx.error(
2761
+ f"{func_name}() argument must be a string literal",
2762
+ stmt
2763
+ )
2764
+ native_name = stmt.init.args[0].value
2765
+ elif len(stmt.init.args) > 1:
2766
+ raise self.ctx.error(
2767
+ f"{func_name}() takes 0 or 1 arguments",
2768
+ stmt
2769
+ )
2770
+ # Determine linkage from kwargs
2771
+ kw_binding = stmt.init.kwargs.get("binding")
2772
+ kw_array = stmt.init.kwargs.get("array")
2773
+ is_c_binding = (isinstance(kw_binding, TpyStrLiteral)
2774
+ and kw_binding.value == "C")
2775
+ is_array = (isinstance(kw_array, TpyBoolLiteral)
2776
+ and kw_array.value is True)
2777
+ if is_c_binding and is_array:
2778
+ stmt.linkage = VarLinkage.NATIVE_C_ARRAY
2779
+ elif is_c_binding:
2780
+ stmt.linkage = VarLinkage.NATIVE_C
2781
+ else:
2782
+ stmt.linkage = VarLinkage.NATIVE
2783
+ stmt.native_name = native_name
2784
+ stmt.init = None
2785
+ return
2786
+
2787
+ # Handle `global x` declarations: treat as reassignment of the global variable
2788
+ is_global_declared = stmt.name in self.ctx.func.global_declarations
2789
+ if is_global_declared:
2790
+ existing_type = self.ctx.global_scope.lookup(stmt.name)
2791
+ if stmt.type is not None:
2792
+ raise self.ctx.error(
2793
+ f"Cannot add type annotation to global variable '{stmt.name}' from inside a function",
2794
+ stmt
2795
+ )
2796
+ else:
2797
+ # Check if this is a reassignment (variable already exists in scope)
2798
+ existing_type = self.ctx.func.current_scope.lookup(stmt.name)
2799
+ # Top-level typed globals are pre-registered before statement analysis.
2800
+ # For an earlier unannotated write to the same name, treat this as a
2801
+ # fresh local write and let a later annotation retro-validate history.
2802
+ is_preregistered_global_write = (
2803
+ self.ctx.is_top_level
2804
+ and stmt.type is None
2805
+ and stmt.init is not None
2806
+ and stmt.name not in self.ctx.func.current_scope.bindings
2807
+ and stmt.name in self.ctx.global_scope.bindings
2808
+ and stmt.name not in self.ctx.func.authoritative_types
2809
+ )
2810
+ if is_preregistered_global_write:
2811
+ existing_type = None
2812
+
2813
+ # Block reassignment of Final globals at module level
2814
+ # (inside functions, local shadowing is allowed)
2815
+ if self.ctx.is_top_level and not stmt.is_final and stmt.name in self.ctx.final_globals:
2816
+ raise self.ctx.error(
2817
+ f"Cannot reassign Final variable '{stmt.name}'",
2818
+ stmt
2819
+ )
2820
+
2821
+ # Disallow reassignment of non-value-type params and loop vars
2822
+ if existing_type is not None:
2823
+ self._check_nonvalue_rebinding(stmt.name, stmt)
2824
+
2825
+ init_type: TpyType | None = None
2826
+ if stmt.init:
2827
+ # Handle empty list literal or generic type constructor with explicit type annotation
2828
+ # Note: [] * N is collapsed to [] in the parser
2829
+ is_empty_literal = isinstance(stmt.init, TpyArrayLiteral) and not stmt.init.elements
2830
+ _generic_td = (find_factory_by_simple_name(stmt.init.func_name)
2831
+ if isinstance(stmt.init, TpyCall) and isinstance(stmt.init.func, TpyName) else None)
2832
+ is_generic_constructor = (isinstance(stmt.init, TpyCall) and
2833
+ not stmt.init.args and
2834
+ stmt.init.call_type is None and
2835
+ _generic_td is not None and
2836
+ bool(_generic_td.param_kinds))
2837
+
2838
+ if (is_empty_literal or is_generic_constructor) and stmt.type:
2839
+ # Check if annotation matches the constructor's generic type
2840
+ annotation_matches = False
2841
+ if is_generic_constructor:
2842
+ td = find_factory_by_simple_name(stmt.init.func_name)
2843
+ annotation_matches = (td is not None and
2844
+ stmt.type.qualified_name() == td.qname)
2845
+ else:
2846
+ # Empty literal [] can match list[T] annotation
2847
+ annotation_matches = is_list(stmt.type)
2848
+
2849
+ if annotation_matches:
2850
+ if is_list(stmt.type):
2851
+ # list[T]: Use PendingListType for potential Array optimization
2852
+ elem_type = stmt.type.type_args[0]
2853
+ # Set call_type so codegen generates explicit type (e.g., std::vector<int>())
2854
+ if is_generic_constructor:
2855
+ stmt.init.call_type = stmt.type # type: ignore
2856
+ if self.ctx.func.current_function is None:
2857
+ # Global context: return ListType directly
2858
+ init_type = make_list(elem_type)
2859
+ else:
2860
+ # Function-local context: create PendingListType
2861
+ literal_id = self.ctx.literal_counter
2862
+ self.ctx.literal_counter += 1
2863
+ info = ListLiteralInfo(
2864
+ literal_id=literal_id,
2865
+ expr=stmt.init,
2866
+ element_type=elem_type,
2867
+ size=0,
2868
+ is_global=self.ctx.is_top_level,
2869
+ has_explicit_annotation=True,
2870
+ explicit_type=stmt.type
2871
+ )
2872
+ self.ctx.list_literals[literal_id] = info
2873
+ self.ctx.func.pending_resolutions.append(literal_id)
2874
+ init_type = PendingListType(elem_type, 0, literal_id)
2875
+ else:
2876
+ # Other generic types (Array, etc.): use annotation directly
2877
+ init_type = stmt.type
2878
+ # Set call_type so codegen knows the concrete template type
2879
+ if is_generic_constructor:
2880
+ stmt.init.call_type = stmt.type # type: ignore
2881
+ # Cache expr_type since we bypassed _analyze_expr
2882
+ self.ctx.set_expr_type(stmt.init, init_type)
2883
+ else:
2884
+ func_name = stmt.init.func_name if is_generic_constructor else "[]"
2885
+ raise self.ctx.error(
2886
+ f"{func_name} requires matching type annotation, got {stmt.type}", stmt
2887
+ )
2888
+ else:
2889
+ # Use annotation as hint, or existing type for reassignments
2890
+ type_hint = stmt.type if stmt.type else existing_type
2891
+ init_type = self.expr.analyze_expr_with_hint(stmt.init, type_hint)
2892
+
2893
+ # Deferred Final constant check: runs after init analysis so that
2894
+ # @call_macro expansions are available via macro_expansion attr.
2895
+ if stmt.is_final:
2896
+ self.validate_compile_time_constant(
2897
+ stmt.init, stmt.type, f"Final variable '{stmt.name}'", stmt,
2898
+ )
2899
+ self.ctx.analyzed_finals.add(stmt.name)
2900
+
2901
+ # Track container literal to variable mapping for mutation/type inference.
2902
+ # Only bind when the init is an actual literal or empty constructor,
2903
+ # not a name reference (aliases are handled by register_list_alias).
2904
+ if isinstance(init_type, PENDING_CONTAINER_TYPES) and not isinstance(stmt.init, TpyName):
2905
+ self.ctx.track_container_variable(
2906
+ stmt.name, init_type, stmt.loc.line if stmt.loc else None,
2907
+ )
2908
+ # List-specific: if explicit annotation is provided, record it
2909
+ if isinstance(init_type, PendingListType):
2910
+ if stmt.type and isinstance(stmt.init, (TpyArrayLiteral, TpyListComprehension)):
2911
+ info = self.ctx.list_literals[init_type.literal_id]
2912
+ info.has_explicit_annotation = True
2913
+ info.explicit_type = stmt.type
2914
+
2915
+ # Track pending generic instance to variable mapping
2916
+ if isinstance(init_type, PendingGenericInstanceType):
2917
+ self.ctx.func.variable_to_generic_instance[stmt.name] = init_type.instance_id
2918
+ info = self.ctx.func.pending_generic_instances.get(init_type.instance_id)
2919
+ if info is not None:
2920
+ info.variable_name = stmt.name
2921
+ info.decl_line = stmt.loc.line if stmt.loc else None
2922
+
2923
+ if stmt.type:
2924
+ if existing_type is not None:
2925
+ self.deduction.check_conflicting_annotation(
2926
+ stmt.name,
2927
+ stmt.type,
2928
+ stmt,
2929
+ new_line=(stmt.loc.line if stmt.loc else None),
2930
+ )
2931
+ ann_line = stmt.loc.line if stmt.loc else None
2932
+ self.deduction.retro_validate_against_annotation(stmt.name, stmt.type, annotation_line=ann_line)
2933
+ # Special case: single-char string literal can be assigned to Char
2934
+ if (is_char_type(stmt.type) and is_any_str_type(init_type) and
2935
+ isinstance(stmt.init, TpyStrLiteral) and len(stmt.init.value) == 1):
2936
+ pass # Allow str literal -> Char
2937
+ else:
2938
+ # Unwrap ReadonlyType for coercion -- readonly is tracked
2939
+ # via type deduction, not the compatibility check.
2940
+ inner_init = unwrap_readonly(init_type)
2941
+ stmt.init = self.compat.coerce_expr(stmt.init, inner_init, stmt.type,
2942
+ f"variable '{stmt.name}'",
2943
+ coercion_ctx=CoercionContext.INIT)
2944
+ var_type = stmt.type
2945
+ # Inherit ReadonlyType from init expression
2946
+ if isinstance(init_type, ReadonlyType) and not var_type.is_value_type():
2947
+ var_type = ReadonlyType(var_type)
2948
+ self.deduction.set_authoritative_annotation(
2949
+ stmt.name,
2950
+ stmt.type,
2951
+ line=(stmt.loc.line if stmt.loc else None),
2952
+ )
2953
+ elif existing_type:
2954
+ # If RHS analysis triggered a retro-widen on this name
2955
+ # (literal-seeded local promoted to a fixed-int target by
2956
+ # any typed-slot use), the existing_type captured before
2957
+ # the RHS is stale -- refresh from the scope so the
2958
+ # reassignment's compat check sees the now-final declared
2959
+ # type.
2960
+ if (stmt.name in self.ctx.func.retro_widened_locs
2961
+ and self.ctx.func.current_scope is not None):
2962
+ fresh = self.ctx.func.current_scope.lookup(stmt.name)
2963
+ if fresh is not None and fresh != existing_type:
2964
+ existing_type = fresh
2965
+ # Unwrap ReadonlyType for reassignment type resolution and
2966
+ # coercion -- this is a binding, not passing by reference.
2967
+ inner_existing = unwrap_readonly(existing_type)
2968
+ inner_init = unwrap_readonly(init_type)
2969
+ # PendingGenericInstanceType: reject reassignment while pending
2970
+ if isinstance(inner_existing, PendingGenericInstanceType):
2971
+ raise self.ctx.error(
2972
+ f"Cannot reassign '{stmt.name}' while its generic type is still "
2973
+ f"being inferred; add explicit type arguments to the constructor",
2974
+ stmt,
2975
+ )
2976
+ # PendingListType reassignment: different sizes force list
2977
+ if isinstance(inner_existing, PendingListType):
2978
+ if isinstance(inner_init, PendingListType):
2979
+ if inner_existing.size != inner_init.size:
2980
+ self.deduction.mark_list_different_size(inner_existing.literal_id)
2981
+ self.deduction.mark_list_different_size(inner_init.literal_id)
2982
+ else:
2983
+ self.deduction.link_list_literals(inner_existing.literal_id, inner_init.literal_id)
2984
+ var_type = existing_type
2985
+ # View-family reassignment (PendingViewType or LiteralType[str/bytes]).
2986
+ # Pre-filter with cheap isinstance tuple so non-view reassignments
2987
+ # (records / lists / dicts) skip the qualified_name() lookup inside
2988
+ # view_family_for_type.
2989
+ elif (isinstance(inner_existing, (PendingViewType, LiteralType))
2990
+ and (vf := view_family_for_type(inner_existing)) is not None):
2991
+ if vf.is_any_member(inner_init):
2992
+ if not self.deduction.is_view_compatible_source(stmt.init, inner_init):
2993
+ self.deduction.mark_view_reassigned_from_owned(stmt.name, vf)
2994
+ else:
2995
+ self.deduction.track_view_reassign_source(stmt.name, inner_init, vf)
2996
+ var_type = existing_type
2997
+ if isinstance(inner_existing, LiteralType):
2998
+ # LiteralType additionally runs coerce_expr so out-of-set
2999
+ # literal RHS is rejected. (PendingViewType silently widens --
3000
+ # tracked in BUGS.md.)
3001
+ stmt.init = self.compat.coerce_expr(stmt.init, inner_init, var_type,
3002
+ f"reassignment to '{stmt.name}'",
3003
+ coercion_ctx=CoercionContext.ASSIGN)
3004
+ else:
3005
+ var_type = self.deduction.resolve_reassignment_target_type(
3006
+ stmt.name, inner_existing, inner_init, init_expr=stmt.init
3007
+ )
3008
+ # Reassignment: check if we need to upgrade IntLiteralType
3009
+ if isinstance(inner_existing, IntLiteralType) and is_integer_type(var_type):
3010
+ # Upgrade from IntLiteralType to concrete type
3011
+ # Update var_types so codegen knows the resolved type
3012
+ orig_decl = self.ctx.func.var_decl_by_name.get(stmt.name)
3013
+ if orig_decl:
3014
+ self.ctx.var_types[id(orig_decl)] = var_type
3015
+ else:
3016
+ # Normal reassignment: use existing type, check compatibility
3017
+ stmt.init = self.compat.coerce_expr(stmt.init, inner_init, var_type,
3018
+ f"reassignment to '{stmt.name}'",
3019
+ coercion_ctx=CoercionContext.ASSIGN)
3020
+ # Readonly status flows from the value expression
3021
+ if isinstance(init_type, ReadonlyType) and not var_type.is_value_type():
3022
+ if isinstance(var_type, OptionalType):
3023
+ var_type = OptionalType(ReadonlyType(var_type.inner))
3024
+ else:
3025
+ var_type = ReadonlyType(var_type)
3026
+ if var_type != existing_type:
3027
+ # Keep original declaration's resolved type in sync for codegen.
3028
+ resolved = unwrap_readonly(var_type)
3029
+ orig_decl = self.ctx.func.var_decl_by_name.get(stmt.name)
3030
+ if orig_decl:
3031
+ self.ctx.var_types[id(orig_decl)] = resolved
3032
+ # Retroactively update declared_var_types for earlier lines
3033
+ # so # tpyc: type() reflects the final variable type.
3034
+ for key in self.ctx.declared_var_types:
3035
+ if key[1] == stmt.name:
3036
+ self.ctx.declared_var_types[key] = resolved
3037
+ else:
3038
+ # New variable: resolve IntLiteralType/FloatLiteralType.
3039
+ if isinstance(init_type, IntLiteralType):
3040
+ var_type = self.ctx.default_int_for_literal(init_type, warn_node=stmt.init)
3041
+ self.ctx.func.literal_default_vars.add(stmt.name)
3042
+ elif isinstance(init_type, FloatLiteralType):
3043
+ var_type = FLOAT # float literals always default to float64
3044
+ # Preserve OwnType on variables -- Own[T] indicates the variable
3045
+ # owns its storage and can be moved at last use.
3046
+ # Exceptions: union types need the raw type for isinstance/
3047
+ # narrowing codegen; optional pointer types use T* storage.
3048
+ elif isinstance(init_type, OwnType):
3049
+ inner = init_type.wrapped
3050
+ if (isinstance(inner, UnionType)
3051
+ or (isinstance(inner, OptionalType) and inner.uses_pointer_repr())):
3052
+ var_type = inner
3053
+ else:
3054
+ var_type = init_type
3055
+ # None literal without annotation -- can't infer the Optional type
3056
+ elif isinstance(init_type, NoneType):
3057
+ var_type = init_type
3058
+ self.ctx.func.unresolved_none_vars.add(stmt.name)
3059
+ else:
3060
+ var_type = init_type
3061
+ # Resolve IntLiteralType nested inside TupleType / Array / list
3062
+ # so `t = (1, 2)` records `tuple[Int32, Int32]` rather than
3063
+ # `tuple[IntLiteral(1), IntLiteral(2)]`. Codegen already lowers
3064
+ # the storage to concrete types via downstream passes; this
3065
+ # aligns sema's view so type queries return the same answer.
3066
+ var_type = resolve_int_literals(var_type, self.ctx.default_int_for_literal)
3067
+ # Strip OwnType from init_type: ownership of the source variable
3068
+ # doesn't transfer to the target. The target determines its own
3069
+ # ownership via the OwnType wrapping logic below (line ~2570).
3070
+ if isinstance(var_type, OwnType):
3071
+ var_type = var_type.wrapped
3072
+ # Preserve Ref on non-reassigned function locals from reference
3073
+ # sources (call returns, field access, subscript, params).
3074
+ # Strip for: reassigned locals (T* codegen), top-level globals.
3075
+ if (stmt.name in self.ctx.func.current_reassigned_vars
3076
+ or self.ctx.is_top_level):
3077
+ var_type = unwrap_ref_type(var_type)
3078
+ # Track inferred writes for potential future retro-validation.
3079
+ self.deduction.record_write(stmt.name, stmt.init, init_type)
3080
+ # Annotate tuple literal element capture modes (local context)
3081
+ if isinstance(stmt.init, TpyTupleLiteral) and isinstance(var_type, TupleType):
3082
+ self._annotate_tuple_elem_capture(stmt.init, var_type)
3083
+ elif stmt.type:
3084
+ if isinstance(stmt.type, OptionalType):
3085
+ # Optional without initializer is allowed (defaults to None/nullptr)
3086
+ var_type = stmt.type
3087
+ elif not stmt.type.is_value_type():
3088
+ raise self.ctx.error(
3089
+ f"Variable '{stmt.name}' of type '{stmt.type}' must have an initializer",
3090
+ stmt
3091
+ )
3092
+ else:
3093
+ var_type = stmt.type
3094
+ self.deduction.set_authoritative_annotation(
3095
+ stmt.name,
3096
+ stmt.type,
3097
+ line=(stmt.loc.line if stmt.loc else None),
3098
+ )
3099
+ else:
3100
+ raise self.ctx.error(f"Variable '{stmt.name}' has no type annotation and no initializer", stmt)
3101
+
3102
+ # Deferred type inference for new locals (PendingViewType, list alias, etc.)
3103
+ if not is_global_declared and existing_type is None:
3104
+ var_type = self._infer_new_local_type(
3105
+ stmt.name, var_type, stmt.init, init_type,
3106
+ line=(stmt.loc.line if stmt.loc else None),
3107
+ )
3108
+ if isinstance(var_type, PendingViewType):
3109
+ stmt.type = var_type
3110
+
3111
+ if is_global_declared:
3112
+ # Update global scope type; bind in current scope for local reads
3113
+ self.ctx.global_scope.define(stmt.name, var_type)
3114
+ self.ctx.func.current_scope.define(stmt.name, var_type)
3115
+ else:
3116
+ self.ctx.func.current_scope.define(stmt.name, var_type)
3117
+ # Reassignment revives a consumed variable
3118
+ self.ctx.func.consumed_vars.discard(stmt.name)
3119
+ # Reassigning a loop variable prevents const-ref binding
3120
+ if existing_type is not None:
3121
+ self.ctx.mark_loop_var_mutated(stmt.name)
3122
+ # Assigning a loop var to a non-value-type local takes &(var) in codegen
3123
+ if (stmt.init is not None and isinstance(stmt.init, TpyName)
3124
+ and var_type is not None and not var_type.is_value_type()):
3125
+ self.ctx.mark_loop_var_mutated(stmt.init.name)
3126
+ # Borrow tracking: reassignment breaks aliases in both directions.
3127
+ # `retarget_storage_borrows` runs before `remove_borrower` so that
3128
+ # borrowers of `name` get re-pointed to `name`'s former upstream
3129
+ # (with kind promoted to the most-restrictive in the chain) rather
3130
+ # than silently dropped. Required for chains through reassigned vars
3131
+ # (e.g. `view = s; s = items[1]`) to keep tracking the source.
3132
+ self.ctx.mark_all_view_borrowers_mutated(stmt.name)
3133
+ _handle_pinned_view_rebind(self.ctx, stmt.name, stmt)
3134
+ bt = self.ctx.func.borrow_tracker
3135
+ bt.retarget_storage_borrows(stmt.name)
3136
+ bt.remove_borrower(stmt.name)
3137
+ # Create borrow when the target aliases another variable's storage.
3138
+ # Reassigned vars register too: codegen uses T* pointer-locals, so
3139
+ # `view = s` aliases the storage `s` currently points into; the
3140
+ # retarget logic above keeps the chain valid across reassignments.
3141
+ if (stmt.init is not None
3142
+ and var_type is not None):
3143
+ if not var_type.is_value_type():
3144
+ # Non-value lvalue: y = x, v = items[i], v = obj.field
3145
+ init_unwrapped = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
3146
+ root = _borrow_storage_root(stmt.init)
3147
+ if root is not None:
3148
+ if isinstance(init_unwrapped, TpySubscript):
3149
+ kind = BorrowKind.ELEMENT
3150
+ elif isinstance(init_unwrapped, TpyFieldAccess):
3151
+ kind = BorrowKind.FIELD
3152
+ else:
3153
+ kind = BorrowKind.ALIAS
3154
+ bt.add_borrow(root, stmt.name, kind)
3155
+ # 8a.5: defer marking the source as mutated until the borrower is
3156
+ # actually written through. Deferral applies to:
3157
+ # - ELEMENT borrows (v = items[i])
3158
+ # - ALIAS/FIELD borrows whose root traces back to an ELEMENT borrow
3159
+ # (w = v, x = w where v = items[i]) -- checked transitively.
3160
+ # PTR/ITER borrows and chains not rooted at an ELEMENT mark immediately.
3161
+ if not (kind == BorrowKind.ELEMENT
3162
+ or bt.is_deferred_borrow(root)):
3163
+ self.ctx.mark_param_mutated(root)
3164
+ else:
3165
+ # _borrow_storage_root returned None: init is not a simple name/
3166
+ # subscript/field (e.g. or/and/ternary, or a deep chain like
3167
+ # outer.inner[i]). Use addr_taken_roots to find all root params
3168
+ # and mark them T& (not const T&) since we're aliasing into them.
3169
+ for alias_root in addr_taken_roots(stmt.init):
3170
+ self.ctx.mark_param_mutated(alias_root)
3171
+ elif isinstance(var_type, PtrType):
3172
+ # take_ptr(x) / Ptr(x) borrows x's storage even though Ptr is a value type
3173
+ init_inner = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
3174
+ if isinstance(init_inner, TpyCall) and len(init_inner.args) > 0:
3175
+ is_ptr_ctor = (init_inner.call_type is not None
3176
+ and init_inner.call_type.is_pointer())
3177
+ fi = init_inner.resolved_function_info
3178
+ is_vpc = fi is not None and fi.value_ptr_coercion
3179
+ if is_ptr_ctor or is_vpc:
3180
+ root = _borrow_storage_root(init_inner.args[0])
3181
+ if root is not None:
3182
+ bt.add_borrow(root, stmt.name, BorrowKind.PTR)
3183
+ # Direct value-to-Ptr coercion (`pp: Ptr[T] = x`): same borrow
3184
+ # semantics as the take_ptr / Ptr(x) explicit forms. The mutation
3185
+ # signal is already propagated in coerce_expr; this block adds
3186
+ # the borrow registration so warn_borrow_* diagnostics fire.
3187
+ if isinstance(stmt.init, TpyCoerce) and stmt.init.coercion.name in (
3188
+ "record_to_ptr", "record_to_const_ptr",
3189
+ "upcast_to_ptr", "upcast_to_const_ptr"):
3190
+ root = _borrow_storage_root(stmt.init.expr)
3191
+ if root is not None:
3192
+ bt.add_borrow(root, stmt.name, BorrowKind.PTR)
3193
+ elif is_span(var_type):
3194
+ # Span from slicing borrows the source container
3195
+ # (StrView excluded: str is immutable, no mutations to warn about)
3196
+ init_inner = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
3197
+ if isinstance(init_inner, TpySubscript) and isinstance(init_inner.index, TpySlice):
3198
+ root = _borrow_storage_root(init_inner)
3199
+ if root is not None:
3200
+ bt.add_borrow(root, stmt.name, BorrowKind.ELEMENT)
3201
+ elif is_str_view_type(var_type) or is_bytes_view_type(var_type):
3202
+ # Pinned view annotation aliasing a name source: register so
3203
+ # source reassignment warns. Pending views handle this via
3204
+ # source_mutated fall-back; the explicit annotation can't
3205
+ # fall back so we warn at the mutation site instead.
3206
+ init_inner = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
3207
+ if isinstance(init_inner, TpyName):
3208
+ self.ctx.func.pinned_view_aliases.setdefault(
3209
+ init_inner.name, set()).add(stmt.name)
3210
+ # 8b: Register call result borrow for ALL assignments (including reassignments).
3211
+ # Unlike general alias tracking, borrow contracts use precise return_borrows_from
3212
+ # facts and don't need pointer-alias analysis -- safe to apply to reassigned vars.
3213
+ if (stmt.init is not None
3214
+ and var_type is not None
3215
+ and not var_type.is_value_type()):
3216
+ init_unwrapped = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
3217
+ _register_call_result_borrow(self.ctx, stmt.name, init_unwrapped)
3218
+ # Reassigned non-value locals generate T* local = &(source) in C++.
3219
+ # Mark source param as mutated so it stays T& (not const T&), regardless
3220
+ # of whether the borrow-tracking block above ran.
3221
+ if (stmt.init is not None
3222
+ and stmt.name in self.ctx.func.current_reassigned_vars
3223
+ and var_type is not None
3224
+ and not var_type.is_value_type()):
3225
+ for alias_root in addr_taken_roots(stmt.init):
3226
+ self.ctx.mark_param_mutated(alias_root)
3227
+ if stmt.init:
3228
+ self.init.mark_assigned(stmt.name)
3229
+ self.narrowing.update_after_write(stmt.name, var_type, init_type if stmt.init else None, stmt.init)
3230
+ # Union assignment narrowing: narrow to concrete member on initial declaration
3231
+ if existing_type is None:
3232
+ inner_var = unwrap_readonly(var_type)
3233
+ if (isinstance(inner_var, UnionType) and init_type is not None
3234
+ and init_type in inner_var.members):
3235
+ self.ctx.func.narrowed_types[stmt.name] = init_type
3236
+ facts = {stmt.name: init_type}
3237
+ stmt.then_type_facts = self._filter_union_codegen_facts(facts)
3238
+ # Record scope depth for new variables (not reassignments of outer-scope
3239
+ # vars). Uses scope lookup rather than var_scope_depth existence, so that
3240
+ # stale entries from discarded inner scopes get overwritten correctly.
3241
+ #
3242
+ # Note: when an outer-scoped variable is reassigned with an rvalue inside
3243
+ # an inner scope (e.g. `p = Point()` in a loop body where `p` was declared
3244
+ # outside), the depth stays at the outer scope. This is safe because the
3245
+ # codegen uses a rebind slot at the declaration scope for rvalue rebinds.
3246
+ if existing_type is None:
3247
+ self.ctx.func.var_scope_depth[stmt.name] = self.ctx.func.current_scope.depth
3248
+ # Update rvalue status for hoist eligibility (both new vars and reassignments)
3249
+ if stmt.init:
3250
+ if self.compat.is_lvalue(stmt.init):
3251
+ # Move-through: lvalue alias at last use of source promotes to rvalue.
3252
+ # Both target and source must be non-reassigned Tier 1 locals
3253
+ # (reassigned vars become T* pointer-locals in codegen).
3254
+ if (isinstance(stmt.init, TpyName)
3255
+ and existing_type is None
3256
+ and stmt.name not in self.ctx.func.current_reassigned_vars
3257
+ and stmt.init.name not in self.ctx.func.current_reassigned_vars
3258
+ and id(stmt.init) in self.ctx.all_last_uses
3259
+ and self.compat._is_owned_var(stmt.init.name)
3260
+ and var_type is not None
3261
+ and not var_type.is_value_type()
3262
+ and not (isinstance(var_type, OptionalType) and var_type.uses_pointer_repr())
3263
+ and not is_protocol_union(var_type)):
3264
+ self.ctx.func.rvalue_vars.add(stmt.name)
3265
+ self.ctx.func.owned_locals.add(stmt.name)
3266
+ self.ctx.func.ever_owned_locals.add(stmt.name)
3267
+ self.ctx.func.move_through_vars.add(stmt.name)
3268
+ else:
3269
+ self.ctx.func.rvalue_vars.discard(stmt.name)
3270
+ self.ctx.func.owned_locals.discard(stmt.name)
3271
+ else:
3272
+ self.ctx.func.rvalue_vars.add(stmt.name)
3273
+ # Track ownership: rvalue-init from value-creating expression
3274
+ # (constructor, Own return) vs reference-returning call.
3275
+ if not self._is_reference_returning_call(stmt.init):
3276
+ self.ctx.func.owned_locals.add(stmt.name)
3277
+ self.ctx.func.ever_owned_locals.add(stmt.name)
3278
+ else:
3279
+ self.ctx.func.owned_locals.discard(stmt.name)
3280
+ if stmt.init and _needs_provenance_tracking(var_type):
3281
+ self.init.mark_provenance(stmt.name, self.compat.is_param_derived_expr(stmt.init))
3282
+ self.init.mark_safe_to_return(
3283
+ stmt.name, self.compat.is_safe_to_return_expr(stmt.init))
3284
+ # Track non-null pointer provenance for null-check elision
3285
+ if stmt.init and isinstance(var_type, PtrType):
3286
+ is_non_null = expr_yields_non_null_ptr(stmt.init, self.ctx.func.non_null_ptr_vars)
3287
+ self.init.mark_non_null_ptr(stmt.name, is_non_null)
3288
+
3289
+ # Scope escape check for variable declarations (new and reassignment)
3290
+ if stmt.init and not var_type.is_value_type():
3291
+ self.scopes.check_escape(stmt.name, stmt.init, stmt)
3292
+ if self.ctx.func.current_ns:
3293
+ self.ctx.func.current_ns.bind_variable(stmt.name, var_type)
3294
+ # Track top-level declarations with line number for order-aware codegen
3295
+ # Use earliest declaration line (min) so uses between redeclarations work
3296
+ if self.ctx.is_top_level:
3297
+ decl_line = stmt.loc.line if stmt.loc else 0
3298
+ if stmt.name not in self.ctx.top_level_decls:
3299
+ self.ctx.top_level_decls[stmt.name] = decl_line
3300
+ else:
3301
+ self.ctx.top_level_decls[stmt.name] = min(self.ctx.top_level_decls[stmt.name], decl_line)
3302
+ # Track first var_decl for later type updates on reassignment-driven inference.
3303
+ if existing_type is None:
3304
+ self.ctx.func.var_decl_by_name[stmt.name] = stmt
3305
+ # Record declared type for test type-annotation validation.
3306
+ # Strip Own[T] and Ref[T] for display -- internal annotations, not user-facing.
3307
+ if stmt.loc:
3308
+ display_type = unwrap_ref_type(unwrap_own(var_type)) if var_type else var_type
3309
+ self.ctx.declared_var_types[(stmt.loc.line, stmt.name)] = display_type
3310
+
3311
+ def _is_reference_returning_call(self, expr: TpyExpr | None) -> bool:
3312
+ """Check if an expression is a function call that returns a reference.
3313
+
3314
+ Returns True for function/method calls that return non-Own non-value
3315
+ types (i.e., they return references to existing storage).
3316
+ Returns False for constructors, Own[T] returns, value returns, and
3317
+ non-call expressions.
3318
+ """
3319
+ if expr is None:
3320
+ return False
3321
+ if isinstance(expr, TpyCoerce):
3322
+ return self._is_reference_returning_call(expr.expr)
3323
+ if isinstance(expr, (TpyCall, TpyMethodCall)):
3324
+ # Constructor calls always create new values
3325
+ if isinstance(expr, TpyCall) and expr.call_type is not None:
3326
+ return False
3327
+ fi = expr.resolved_function_info
3328
+ if fi is not None and fi.return_type is not None:
3329
+ rt = fi.return_type
3330
+ # Own[T] returns are value-creating (ownership transfer)
3331
+ if isinstance(rt, OwnType):
3332
+ return False
3333
+ # Value types are always by-value
3334
+ if rt.is_value_type():
3335
+ return False
3336
+ # User-defined record constructors: call_type is None (not set
3337
+ # by the constructor resolution path) but fi resolves to __init__
3338
+ # or has the record's name as the function name.
3339
+ if fi.name == "__init__":
3340
+ return False
3341
+ if (isinstance(expr, TpyCall) and isinstance(expr.func, TpyName)
3342
+ and self.ctx.registry.find_record(expr.func.name) is not None):
3343
+ return False
3344
+ # Non-Own non-value return = reference
3345
+ return True
3346
+ return False
3347
+
3348
+ def _analyze_tuple_unpack(self, stmt: TpyTupleUnpack) -> None:
3349
+ """Analyze tuple unpacking: a, b = expr."""
3350
+ rhs_type = self.expr.analyze_expr(stmt.value)
3351
+ rhs_check = rhs_type.wrapped if isinstance(rhs_type, OwnType) else rhs_type
3352
+
3353
+ if not isinstance(rhs_check, TupleType):
3354
+ raise self.ctx.error(
3355
+ f"Cannot unpack non-tuple type {rhs_type}", stmt)
3356
+
3357
+ rhs_type = rhs_check
3358
+ n_targets = len(stmt.targets)
3359
+ n_elems = len(rhs_type.element_types)
3360
+ if n_targets != n_elems:
3361
+ raise self.ctx.error(
3362
+ f"Cannot unpack tuple of {n_elems} elements into "
3363
+ f"{n_targets} targets", stmt)
3364
+
3365
+ # Per-element expressions for narrowing (range facts, etc.)
3366
+ has_elem_exprs = isinstance(stmt.value, TpyTupleLiteral)
3367
+ for i, name in enumerate(stmt.targets):
3368
+ elem_type = rhs_type.element_types[i]
3369
+ elem_expr = stmt.value.elements[i] if has_elem_exprs and i < len(stmt.value.elements) else None
3370
+ owned = isinstance(elem_type, OwnType)
3371
+ stmt.is_owned.append(owned)
3372
+ if owned:
3373
+ elem_type = elem_type.wrapped
3374
+ is_ref = (not owned and not elem_type.is_value_type()
3375
+ and not isinstance(elem_type, TypeParamRef))
3376
+ stmt.is_ref.append(is_ref)
3377
+
3378
+ stmt.target_types.append(elem_type)
3379
+
3380
+ if name is None:
3381
+ stmt.is_new.append(True)
3382
+ continue
3383
+
3384
+ if self.ctx.is_top_level:
3385
+ # Block reassignment of Final globals at module level
3386
+ if name in self.ctx.final_globals:
3387
+ raise self.ctx.error(
3388
+ f"Cannot reassign Final variable '{name}'",
3389
+ stmt)
3390
+ # At module level, targets become globals with namespace-scope
3391
+ # definitions. Mark is_new=False so codegen emits assignment
3392
+ # (the declaration is handled by gen_global_decl).
3393
+ # Resolve IntLiteralType so the global's declared type aligns
3394
+ # with the int32_t storage codegen emits (sema/codegen parity).
3395
+ elem_type = resolve_int_literals(elem_type, self.ctx.default_int_for_literal)
3396
+ self.ctx.global_scope.define(name, elem_type)
3397
+ self.ctx.func.current_scope.define(name, elem_type)
3398
+ self.init.mark_assigned(name)
3399
+ self.narrowing.update_after_write(name, elem_type, elem_type, elem_expr)
3400
+ if self.ctx.func.current_ns:
3401
+ self.ctx.func.current_ns.bind_variable(name, elem_type)
3402
+ decl_line = stmt.loc.line if stmt.loc else 0
3403
+ if name not in self.ctx.top_level_decls:
3404
+ self.ctx.top_level_decls[name] = decl_line
3405
+ self._warn_all_caps_without_final(name, str(elem_type), stmt)
3406
+ if stmt.loc:
3407
+ display_type = unwrap_own(elem_type) if elem_type else elem_type
3408
+ self.ctx.declared_var_types[(stmt.loc.line, name)] = display_type
3409
+ stmt.is_new.append(False)
3410
+ continue
3411
+
3412
+ existing = self.ctx.func.current_scope.lookup(name)
3413
+ # Don't treat globals as existing unless explicitly declared
3414
+ # with 'global' -- unpack should create locals by default
3415
+ if (existing is not None
3416
+ and name not in self.ctx.func.global_declarations
3417
+ and name not in self.ctx.func.current_scope.bindings
3418
+ and name in self.ctx.global_scope.bindings):
3419
+ existing = None
3420
+ if existing is not None:
3421
+ self._check_nonvalue_rebinding(name, stmt)
3422
+ self.compat.check_type_compatible(
3423
+ elem_type, existing, "tuple unpacking", source_expr=stmt)
3424
+ self.narrowing.update_after_write(name, existing, elem_type, elem_expr)
3425
+ stmt.is_new.append(False)
3426
+ else:
3427
+ elem_type = self._infer_new_local_type(
3428
+ name, elem_type, None, None,
3429
+ line=(stmt.loc.line if stmt.loc else None),
3430
+ )
3431
+ # Commit IntLiteralType to default_int_type at the new-local
3432
+ # binding site -- otherwise codegen emits invalid C++ like
3433
+ # `1 a = std::get<0>(...)` since IntLiteralType.to_cpp()
3434
+ # returns the literal value, not a type. Applied here (not in
3435
+ # _infer_new_local_type) so for-loop var binding still leaves
3436
+ # IntLiteralType in place: `for v in [...]: f(v)` where f
3437
+ # takes BigInt needs that flexibility (heapq pattern).
3438
+ elem_type = resolve_int_literals(elem_type, self.ctx.default_int_for_literal)
3439
+ stmt.target_types[i] = elem_type
3440
+ self.ctx.func.current_scope.define(name, elem_type)
3441
+ self.init.mark_assigned(name)
3442
+ self.narrowing.update_after_write(name, elem_type, elem_type, elem_expr)
3443
+ stmt.is_new.append(True)
3444
+ if stmt.loc:
3445
+ display_type = unwrap_own(elem_type) if elem_type else elem_type
3446
+ self.ctx.declared_var_types[(stmt.loc.line, name)] = display_type
3447
+
3448
+ # Register element-borrow edges so a later mutation through an
3449
+ # unpacked target (a, b = p; a.x = ...) traces back to the source
3450
+ # tuple's storage. Mirrors the deferred-element-ref pattern used for
3451
+ # `a = items[0]`. Two source shapes:
3452
+ # - name source `a, b = p` -> edges p -> a, p -> b
3453
+ # - tuple-literal source `a, b = (x, y)` -> edges x -> a, y -> b
3454
+ # Only register for slots whose unpacked type exposes a mutable
3455
+ # borrow surface; value-only elements never need the edge.
3456
+ if not self.ctx.is_top_level:
3457
+ bt = self.ctx.func.borrow_tracker
3458
+ literal_src = stmt.value if isinstance(stmt.value, TpyTupleLiteral) else None
3459
+ name_src_root = (_root_name_of_expr(stmt.value)
3460
+ if literal_src is None else None)
3461
+ for i, name in enumerate(stmt.targets):
3462
+ if name is None or not stmt.is_ref[i]:
3463
+ continue
3464
+ # Skip value-type elements (BigInt, str, ...) bound by
3465
+ # const-ref for copy avoidance: they have no borrow surface
3466
+ # the call-edge gate would care about, and registering an
3467
+ # edge here just bloats the borrow tracker.
3468
+ if not param_has_mutable_borrow_surface(stmt.target_types[i]):
3469
+ continue
3470
+ if literal_src is not None and i < len(literal_src.elements):
3471
+ src_root = _root_name_of_expr(literal_src.elements[i])
3472
+ else:
3473
+ src_root = name_src_root
3474
+ if src_root is None or src_root == name:
3475
+ continue
3476
+ bt.add_borrow(src_root, name, BorrowKind.ELEMENT)
3477
+
3478
+ # Determine const-ref eligibility per element for expensive value types.
3479
+ # Safe because tuples are immutable -- no in-place mutation possible.
3480
+ if not self.ctx.is_top_level:
3481
+ source_is_lvalue = isinstance(stmt.value, TpyName)
3482
+ source_safe = True
3483
+ if source_is_lvalue:
3484
+ source_name = stmt.value.name
3485
+ source_safe = (source_name not in self.ctx.func.current_reassigned_vars)
3486
+ for i, name in enumerate(stmt.targets):
3487
+ if name is None:
3488
+ stmt.is_const_ref.append(False)
3489
+ continue
3490
+ target_type = stmt.target_types[i]
3491
+ eligible = (
3492
+ stmt.is_new[i]
3493
+ and not stmt.is_ref[i]
3494
+ and not stmt.is_owned[i]
3495
+ and not isinstance(target_type, PendingViewType)
3496
+ and target_type.is_value_type()
3497
+ and target_type.is_expensive_copy()
3498
+ and name not in self.ctx.func.current_reassigned_vars
3499
+ and name not in self.ctx.func.current_aug_assigned_vars
3500
+ and source_safe
3501
+ )
3502
+ stmt.is_const_ref.append(eligible)
3503
+
3504
+ def _analyze_slice_assign(self, stmt: TpyAssign) -> None:
3505
+ """Analyze a slice assignment: a[x:y] = rhs or a[x:y:z] = rhs."""
3506
+ assert isinstance(stmt.target, TpySubscript)
3507
+ sl = stmt.target.index
3508
+ assert isinstance(sl, TpySlice)
3509
+
3510
+ stepped = sl.step is not None
3511
+
3512
+ obj_type = self.expr.analyze_expr(stmt.target.obj)
3513
+ actual_type = unwrap_own(unwrap_readonly(unwrap_ref_type(obj_type)))
3514
+
3515
+ # Look up __setitem__(basic_slice/slice, value) overload via .py stubs
3516
+ result = self.expr._find_slice_setitem(actual_type, stepped=stepped)
3517
+ if result is None:
3518
+ raise self.ctx.error(
3519
+ f"Slice assignment not supported for type '{obj_type}'", stmt)
3520
+
3521
+ _value_param_type, fi = result
3522
+ stmt.target.slice_function_info = fi
3523
+ stmt.target.is_stepped_slice = stepped
3524
+
3525
+ self._enforce_readonly_assignment_target(stmt.target)
3526
+
3527
+ # Analyze slice bounds and validate they are integer types
3528
+ for bound in (sl.lower, sl.upper, sl.step):
3529
+ if bound is not None:
3530
+ bound_type = self.expr.analyze_expr(bound)
3531
+ if not is_any_int_type(bound_type):
3532
+ raise self.ctx.error(
3533
+ f"Slice bound must be an integer, got '{bound_type}'", bound)
3534
+
3535
+ # Use the container's element type as hint so array literals infer correctly.
3536
+ # Use the stub's value param type (e.g. Iterable[Own[T]]) for coercion,
3537
+ # which accepts any iterable and triggers copy warnings for lvalue sources.
3538
+ elem_type = actual_type.get_element_type()
3539
+ rhs_hint = make_list(elem_type) if elem_type is not None else _value_param_type
3540
+ self.ctx.set_expr_type(stmt.target, rhs_hint)
3541
+
3542
+ value_type = self.expr.analyze_expr_with_hint(stmt.value, rhs_hint)
3543
+ stmt.value = self.compat.coerce_expr(stmt.value, value_type, _value_param_type, "slice assignment",
3544
+ coercion_ctx=CoercionContext.ASSIGN,
3545
+ target_is_storage_form=True)
3546
+
3547
+ # Mutation tracking
3548
+ root = _root_name_of_expr(stmt.target)
3549
+ if root is not None:
3550
+ self.ctx.mark_loop_var_mutated(root)
3551
+ self.ctx.mark_param_mutated(root)
3552
+ # Slice assignment replaces a subrange -- structural mutation.
3553
+ self.ctx.mark_param_structurally_mutated(root)
3554
+ storage = self._resolve_obj_storage(stmt.target.obj)
3555
+ if storage is not None:
3556
+ if self.ctx.func.borrow_tracker.has_borrow_of_kinds(storage, (BorrowKind.ELEMENT, BorrowKind.PTR, BorrowKind.ITER)):
3557
+ self.ctx.warning(
3558
+ f"Mutation of '{storage}' while borrowed"
3559
+ " (slice assignment may invalidate references)", stmt)
3560
+ self.ctx.mark_all_view_borrowers_mutated(storage)
3561
+
3562
+ def _analyze_assign(self, stmt: TpyAssign) -> None:
3563
+ """Analyze an assignment."""
3564
+ # In nested defs, assigning to an outer variable requires nonlocal
3565
+ if (self.ctx.func.in_nested_def
3566
+ and isinstance(stmt.target, TpyName)
3567
+ and stmt.target.name in self.ctx.func.outer_scope_locals
3568
+ and stmt.target.name not in self.ctx.func.current_nonlocal_names):
3569
+ raise self.ctx.error(
3570
+ f"Cannot assign to '{stmt.target.name}' in nested function"
3571
+ f" without 'nonlocal' declaration",
3572
+ stmt)
3573
+ # Slice assignment: a[x:y] = rhs -- handled separately
3574
+ if isinstance(stmt.target, TpySubscript) and isinstance(stmt.target.index, TpySlice):
3575
+ self._analyze_slice_assign(stmt)
3576
+ return
3577
+ # D16 dyn-attr write: if the target is `obj.foo` where foo is undeclared
3578
+ # AND the class has __setattr__, the read-side analysis would either
3579
+ # resolve via __getattr__ (handled below) or raise "no field". Detect
3580
+ # the latter by analyzing the target with a recovery path: pre-check
3581
+ # for an undeclared FieldAccess target on a dyn-writable class.
3582
+ if (isinstance(stmt.target, TpyFieldAccess)
3583
+ and self._target_is_dyn_writable_only(stmt.target)):
3584
+ self._analyze_dyn_setattr_assign(stmt)
3585
+ return
3586
+ target_type = self.expr.analyze_expr(stmt.target)
3587
+ self._check_class_constant_write(stmt.target, stmt)
3588
+ # D16 dyn-attr write detection: if the read-side analysis resolved the
3589
+ # target via __getattr__ (static lookup miss + dyn-readable class),
3590
+ # reinterpret as a __setattr__ write. Synthesize a method call and
3591
+ # delegate to the normal method-call analyzer so all standard arg
3592
+ # coercion (e.g. into-Any wrapping) is applied uniformly.
3593
+ if (isinstance(stmt.target, TpyFieldAccess)
3594
+ and stmt.target.dyn_getattr_call is not None):
3595
+ obj_type = self.ctx.get_expr_type(stmt.target.obj)
3596
+ actual = unwrap_qualifiers(obj_type) if obj_type is not None else None
3597
+ record = (self.ctx.registry.get_record_for_type(actual)
3598
+ if isinstance(actual, NominalType) else None)
3599
+ sa_overloads, _sa_subst = (
3600
+ self.protocols.lookup_record_method_overloads(record, "__setattr__")
3601
+ if record is not None else ([], {}))
3602
+ if not sa_overloads:
3603
+ rec_name = record.name if record is not None else str(actual)
3604
+ raise self.ctx.error(
3605
+ f"Record '{rec_name}' has no field '{stmt.target.field}' "
3606
+ f"and does not define __setattr__",
3607
+ stmt,
3608
+ )
3609
+ # Clear the read-side resolution; this is a write. Then synthesize
3610
+ # a __setattr__ call with the original value AST and analyze it
3611
+ # via the method-call path -- coerce_expr applies arg coercion
3612
+ # (including into-Any wrapping) on the value automatically.
3613
+ stmt.target.dyn_getattr_call = None
3614
+ setter_call = TpyMethodCall(
3615
+ obj=stmt.target.obj,
3616
+ method="__setattr__",
3617
+ args=[TpyStrLiteral(value=stmt.target.field), stmt.value],
3618
+ loc=stmt.loc,
3619
+ )
3620
+ self.expr.analyze_expr(setter_call)
3621
+ stmt.target.dyn_setattr_call = setter_call
3622
+ # Mark mutation: writing through a dyn-attr is mutating self/obj.
3623
+ root = _root_name_of_expr(stmt.target.obj)
3624
+ if root is not None:
3625
+ self.ctx.mark_loop_var_mutated(root)
3626
+ self.ctx.mark_param_mutated(root)
3627
+ self._enforce_readonly_assignment_target(stmt.target)
3628
+ return
3629
+ value_type = self.expr.analyze_expr_with_hint(stmt.value, target_type)
3630
+ # Property setter: validate and tag for codegen
3631
+ if isinstance(stmt.target, TpyFieldAccess) and stmt.target.is_property_access:
3632
+ obj_type = self.ctx.get_expr_type(stmt.target.obj)
3633
+ actual = unwrap_readonly(obj_type) if obj_type else None
3634
+ record = self.ctx.registry.get_record_for_type(actual) if isinstance(actual, NominalType) else None
3635
+ prop = self.protocols.lookup_record_property(record, stmt.target.field) if record else None
3636
+ if prop and prop.setter:
3637
+ stmt.target.property_setter = True
3638
+ # Construct setter TpyMethodCall for codegen delegation
3639
+ setter_name = f"set_{stmt.target.field}"
3640
+ setter_call = TpyMethodCall(
3641
+ obj=stmt.target.obj, method=setter_name, args=[stmt.value])
3642
+ setter_call.resolved_function_info = prop.setter
3643
+ stmt.target.property_setter_call = setter_call
3644
+ elif prop and not prop.setter:
3645
+ raise self.ctx.error(
3646
+ f"Property '{stmt.target.field}' is read-only (no setter defined)",
3647
+ stmt,
3648
+ )
3649
+ self._enforce_readonly_assignment_target(stmt.target)
3650
+ # Track mutation of for-each loop variables (prevents const-ref binding)
3651
+ root = _root_name_of_expr(stmt.target)
3652
+ if root is not None:
3653
+ self.ctx.mark_loop_var_mutated(root)
3654
+ # Through-reference writes (field/subscript) mutate the param's object;
3655
+ # plain name reassignment just rebinds the local.
3656
+ if isinstance(stmt.target, (TpyFieldAccess, TpySubscript)):
3657
+ self.ctx.mark_param_mutated(root)
3658
+ copy_warning_fired = False
3659
+ if isinstance(stmt.target, (TpyFieldAccess, TpySubscript)):
3660
+ declared_target_type = self.narrowing.declared_type_for_expr(stmt.target)
3661
+ if declared_target_type is not None:
3662
+ target_type = declared_target_type
3663
+ # Unified copy detection: Ref (borrowed), Own (owned at non-last-use),
3664
+ # or compound borrowed types (Optional/Union with pointer repr) on
3665
+ # the value expression type means storing it into a field/container
3666
+ # will copy. Ref covers params, call returns, field access, subscript.
3667
+ # Own covers owned locals at non-last-use.
3668
+ # Optional[NonValue] and Union[pointer-repr] use pointer representation
3669
+ # (T*, variant<A*,B*>) which is semantically borrowed -- copying into
3670
+ # storage (std::optional<T>, variant<A,B>) is a pointer-to-value copy.
3671
+ # Skip explicit copy() calls (caller acknowledged the copy) and
3672
+ # OwnType from non-name sources (explicit Own return from function).
3673
+ is_own_from_name = isinstance(value_type, OwnType) and isinstance(stmt.value, TpyName)
3674
+ stripped_value = self.ctx.get_expr_type(stmt.value)
3675
+ is_compound_ref = (
3676
+ (isinstance(stripped_value, OptionalType) and not stripped_value.inner.is_value_type())
3677
+ or (isinstance(stripped_value, UnionType) and stripped_value.uses_pointer_repr()
3678
+ and not stripped_value.needs_wrapper())
3679
+ )
3680
+ # Ptr[T] target takes the address of the source (`_a(&a)`), no copy.
3681
+ target_is_ptr = isinstance(unwrap_qualifiers(target_type), PtrType)
3682
+ # Any target: INTO_ANY in compatibility.py emits its own copy
3683
+ # warning for reference-type sources -- skip the generic
3684
+ # field/container warning here to avoid a double-fire.
3685
+ target_is_any = isinstance(unwrap_qualifiers(target_type), AnyType)
3686
+ if ((isinstance(value_type, RefType) or is_own_from_name or is_compound_ref)
3687
+ and stmt.loc is not None
3688
+ and not target_is_ptr
3689
+ and not target_is_any
3690
+ and not self.compat.is_copy_call(stmt.value)):
3691
+ inner = unwrap_qualifiers(value_type)
3692
+ dest = "field" if isinstance(stmt.target, TpyFieldAccess) else "container"
3693
+ if self.ctx.is_type_non_copyable(target_type):
3694
+ verb = "may copy" if isinstance(inner, TypeParamRef) else "cannot copy"
3695
+ if inner == target_type:
3696
+ msg = (f"{verb} non-copyable type '{target_type}' into "
3697
+ f"{dest}{NOCOPY_REMEDIATION_HINT}")
3698
+ else:
3699
+ msg = (f"{verb} {inner} into {dest} of type '{target_type}'; "
3700
+ f"target is non-copyable{NOCOPY_REMEDIATION_HINT}")
3701
+ raise self.ctx.error(msg, stmt)
3702
+ if isinstance(inner, TypeParamRef):
3703
+ msg = f"may copy {inner} into {dest} if not a value type; use copy() to make this explicit"
3704
+ else:
3705
+ msg = f"copies {inner} into {dest}; use copy() to make this explicit"
3706
+ self.ctx.warning(msg, stmt)
3707
+ copy_warning_fired = True
3708
+ # Own[T] param stored in a field/container — mark as consumed
3709
+ if isinstance(stmt.value, TpyName) and stmt.value.name in self.ctx.func.current_param_names:
3710
+ self.ctx.mark_own_param_consumed(stmt.value.name)
3711
+ # Address-escape tracking: storing a param into a mutable Ptr[T]
3712
+ # takes its address. Suppress the perf-default `const T&` param
3713
+ # emission so the `&param -> T*` store type-checks.
3714
+ tgt_inner = unwrap_qualifiers(target_type)
3715
+ if (isinstance(stmt.target, TpyFieldAccess)
3716
+ and isinstance(tgt_inner, PtrType)
3717
+ and not tgt_inner.is_readonly):
3718
+ self.ctx.func.current_addr_escape_param_names.add(stmt.value.name)
3719
+ if isinstance(stmt.target, TpyName):
3720
+ # Track param rebinding (subsequent mutations target the new local, not the arg)
3721
+ if stmt.target.name in self.ctx.func.current_param_names:
3722
+ self.ctx.func.current_rebound_params.add(stmt.target.name)
3723
+ # Block reassignment of Final globals at module level
3724
+ if self.ctx.is_top_level and stmt.target.name in self.ctx.final_globals:
3725
+ raise self.ctx.error(
3726
+ f"Cannot reassign Final variable '{stmt.target.name}'",
3727
+ stmt
3728
+ )
3729
+ # Match var-decl flow: reject forbidden rebinding before any type mutation.
3730
+ self._check_nonvalue_rebinding(stmt.target.name, stmt)
3731
+ declared_target_type = self.ctx.func.current_scope.lookup(stmt.target.name)
3732
+ if declared_target_type is not None:
3733
+ target_type = declared_target_type
3734
+ # Unwrap Ref, Own, and ReadonlyType for reassignment type resolution --
3735
+ # this is a binding, not passing by reference.
3736
+ inner_target = unwrap_own(unwrap_ref_type(unwrap_readonly(target_type)))
3737
+ inner_value = unwrap_own(unwrap_ref_type(unwrap_readonly(value_type)))
3738
+ # PendingListType reassignment: different sizes force list
3739
+ if isinstance(inner_target, PendingListType):
3740
+ if isinstance(inner_value, PendingListType):
3741
+ if inner_target.size != inner_value.size:
3742
+ self.deduction.mark_list_different_size(inner_target.literal_id)
3743
+ self.deduction.mark_list_different_size(inner_value.literal_id)
3744
+ else:
3745
+ self.deduction.link_list_literals(inner_target.literal_id, inner_value.literal_id)
3746
+ # target_type stays PendingListType
3747
+ # PendingDictType/PendingSetType reassignment: keep pending
3748
+ elif isinstance(inner_target, (PendingDictType, PendingSetType)):
3749
+ pass # target_type stays pending
3750
+ # PendingViewType reassignment: track view-compatibility, keep pending
3751
+ elif isinstance(inner_target, PendingViewType):
3752
+ vf = inner_target.family
3753
+ if vf.is_any_member(inner_value):
3754
+ if not self.deduction.is_view_compatible_source(stmt.value, inner_value):
3755
+ self.deduction.mark_view_reassigned_from_owned(stmt.target.name, vf)
3756
+ else:
3757
+ self.deduction.track_view_reassign_source(stmt.target.name, inner_value, vf)
3758
+ # target_type stays pending
3759
+ else:
3760
+ target_type = self.deduction.resolve_reassignment_target_type(
3761
+ stmt.target.name, inner_target, inner_value, init_expr=stmt.value
3762
+ )
3763
+ # Readonly status flows from the value expression
3764
+ if isinstance(value_type, ReadonlyType) and not target_type.is_value_type():
3765
+ target_type = ReadonlyType(target_type)
3766
+ self.ctx.func.current_scope.define(stmt.target.name, target_type)
3767
+ # Reassignment revives a consumed variable
3768
+ self.ctx.func.consumed_vars.discard(stmt.target.name)
3769
+ # Borrow tracking: reassignment breaks aliases in both directions.
3770
+ # Retarget runs before remove_borrower so borrowers of the target
3771
+ # get re-pointed to the upstream source (with promoted kind),
3772
+ # keeping chains through reassigned vars valid.
3773
+ self.ctx.mark_all_view_borrowers_mutated(stmt.target.name)
3774
+ _handle_pinned_view_rebind(self.ctx, stmt.target.name, stmt)
3775
+ bt = self.ctx.func.borrow_tracker
3776
+ bt.retarget_storage_borrows(stmt.target.name)
3777
+ bt.remove_borrower(stmt.target.name)
3778
+ # Rebinding a non-value pointer-local generates local = &(source) in C++,
3779
+ # requiring source param to be T& (not const T&).
3780
+ if not inner_target.is_value_type() and self.compat.is_lvalue(stmt.value):
3781
+ for rebind_root in addr_taken_roots(stmt.value):
3782
+ self.ctx.mark_param_mutated(rebind_root)
3783
+ if self.ctx.func.current_ns:
3784
+ self.ctx.func.current_ns.update_variable_type(stmt.target.name, target_type)
3785
+ self.ctx.set_expr_type(stmt.target, target_type)
3786
+ self.deduction.record_write(stmt.target.name, stmt.value, inner_value)
3787
+ if not isinstance(inner_target, (*PENDING_CONTAINER_TYPES, PendingViewType)):
3788
+ resolved = unwrap_readonly(target_type)
3789
+ var_decl = self.ctx.func.var_decl_by_name.get(stmt.target.name)
3790
+ if var_decl:
3791
+ self.ctx.var_types[id(var_decl)] = resolved
3792
+ # Retroactively update declared_var_types for earlier lines
3793
+ # so # tpyc: type() reflects the final variable type.
3794
+ if resolved != unwrap_readonly(inner_target):
3795
+ for key in self.ctx.declared_var_types:
3796
+ if key[1] == stmt.target.name:
3797
+ self.ctx.declared_var_types[key] = resolved
3798
+
3799
+ # Disallow reassignment of non-value-type params and loop vars
3800
+ if isinstance(stmt.target, TpyName):
3801
+ # Update rvalue/ownership status for hoist eligibility and copy detection
3802
+ if self.compat.is_lvalue(stmt.value):
3803
+ self.ctx.func.rvalue_vars.discard(stmt.target.name)
3804
+ self.ctx.func.owned_locals.discard(stmt.target.name)
3805
+ else:
3806
+ self.ctx.func.rvalue_vars.add(stmt.target.name)
3807
+ if not self._is_reference_returning_call(stmt.value):
3808
+ self.ctx.func.owned_locals.add(stmt.target.name)
3809
+ self.ctx.func.ever_owned_locals.add(stmt.target.name)
3810
+ else:
3811
+ self.ctx.func.owned_locals.discard(stmt.target.name)
3812
+
3813
+ # Tuples are immutable -- reject element assignment
3814
+ if isinstance(stmt.target, TpySubscript):
3815
+ obj_type = self.ctx.get_expr_type(stmt.target.obj)
3816
+ if isinstance(obj_type, TupleType):
3817
+ raise self.ctx.error("Tuples are immutable; cannot assign to tuple elements", stmt)
3818
+ # Dict/TypedDict subscript assignment is always allowed
3819
+ actual_obj = unwrap_readonly(obj_type)
3820
+ is_typed_dict_target = (
3821
+ isinstance(actual_obj, NominalType) and actual_obj.is_record
3822
+ and stmt.target.typed_dict_field is not None
3823
+ )
3824
+ if not (is_dict(actual_obj) or isinstance(actual_obj, PendingDictType)) and not is_typed_dict_target:
3825
+ elem_type = obj_type.get_element_type()
3826
+ if elem_type is not None:
3827
+ # Span[readonly[T]] always rejects element assignment
3828
+ if is_span(obj_type) and is_readonly_span(obj_type):
3829
+ raise self.ctx.error(f"Cannot assign to elements of {obj_type} (read-only)", stmt)
3830
+ # Check if type conforms to MutableSequence[elem_type]
3831
+ mutable_seq = NominalType("MutableSequence", (elem_type,), is_protocol=True)
3832
+ if not self.protocols.type_conforms_to_protocol(obj_type, mutable_seq):
3833
+ raise self.ctx.error(f"Cannot assign to elements of {obj_type} (read-only)", stmt)
3834
+
3835
+ # Prevent assignment through read-only pointer
3836
+ if isinstance(stmt.target, TpyName):
3837
+ pass # TpyName targets are fine
3838
+ else:
3839
+ if isinstance(stmt.target, TpyFieldAccess):
3840
+ obj_type = self.ctx.get_expr_type(stmt.target.obj)
3841
+ if is_readonly_ptr(obj_type):
3842
+ raise self.ctx.error("Cannot assign through read-only pointer", stmt)
3843
+
3844
+ # Borrow conflict: field write on borrowed storage.
3845
+ # Resolves aliases so alias.field = val warns when the underlying storage
3846
+ # has field/element/ptr borrows.
3847
+ # Subscript assignment (items[i] = val, d[k] = val) is in-place and does
3848
+ # NOT invalidate element references: list element replacement doesn't
3849
+ # reallocate, and ordered_map is node-based so insertion is stable
3850
+ # (confirmed by @native_preserves_refs on dict.__setitem__).
3851
+ if isinstance(stmt.target, TpySubscript):
3852
+ storage = self._resolve_obj_storage(stmt.target.obj)
3853
+ if storage is not None:
3854
+ self.ctx.mark_all_view_borrowers_mutated(storage)
3855
+ elif isinstance(stmt.target, TpyFieldAccess):
3856
+ storage = self._resolve_obj_storage(stmt.target.obj)
3857
+ # Also check the field-path key itself (e.g. "self.items" for self.items = [...])
3858
+ # since borrows may be registered on the dotted key.
3859
+ field_storage = _storage_key(stmt.target)
3860
+ _BORROW_KINDS = (BorrowKind.FIELD, BorrowKind.ELEMENT, BorrowKind.PTR, BorrowKind.ITER)
3861
+ has_conflict = False
3862
+ bt = self.ctx.func.borrow_tracker
3863
+ if storage is not None and bt.has_borrow_of_kinds(storage, _BORROW_KINDS):
3864
+ has_conflict = True
3865
+ if not has_conflict and field_storage is not None and bt.has_borrow_of_kinds(field_storage, _BORROW_KINDS):
3866
+ storage = field_storage
3867
+ has_conflict = True
3868
+ if has_conflict:
3869
+ msg = (f"Mutation of '{storage}' while borrowed"
3870
+ " (field assignment may invalidate references)")
3871
+ self.ctx.warning(msg, stmt)
3872
+ if storage is not None:
3873
+ self.ctx.mark_all_view_borrowers_mutated(storage)
3874
+ if field_storage is not None and field_storage != storage:
3875
+ self.ctx.mark_all_view_borrowers_mutated(field_storage)
3876
+
3877
+ # PendingDictType subscript assignment: d[k] = v -- infer key/value types
3878
+ if isinstance(stmt.target, TpySubscript):
3879
+ obj_type_for_dict = self.ctx.get_expr_type(stmt.target.obj)
3880
+ if isinstance(obj_type_for_dict, PendingDictType):
3881
+ index_type = self.ctx.get_expr_type(stmt.target.index)
3882
+ self.deduction.infer_dict_key_value_types(
3883
+ stmt.target.obj, index_type, value_type)
3884
+ # Update obj_type and target_type if types were inferred
3885
+ dict_info = self.ctx.dict_literals.get(obj_type_for_dict.literal_id)
3886
+ if dict_info and not isinstance(dict_info.key_type, UnknownElementType):
3887
+ new_pending = PendingDictType(dict_info.key_type, dict_info.value_type, obj_type_for_dict.literal_id)
3888
+ if new_pending.key_type != obj_type_for_dict.key_type or new_pending.value_type != obj_type_for_dict.value_type:
3889
+ self.ctx.set_expr_type(stmt.target.obj, new_pending)
3890
+ if isinstance(stmt.target.obj, TpyName):
3891
+ if self.ctx.func.current_scope:
3892
+ self.ctx.func.current_scope.define(stmt.target.obj.name, new_pending)
3893
+ if self.ctx.func.current_ns:
3894
+ self.ctx.func.current_ns.bind_variable(stmt.target.obj.name, new_pending)
3895
+ target_type = dict_info.value_type
3896
+ self.ctx.set_expr_type(stmt.target, target_type)
3897
+
3898
+ # Field / container element targets store the value (storage form);
3899
+ # the `T -> Optional[T]` address-take that fires for borrow-form
3900
+ # destinations doesn't apply here.
3901
+ target_is_storage = isinstance(stmt.target, (TpyFieldAccess, TpySubscript))
3902
+ stmt.value = self.compat.coerce_expr(stmt.value, value_type, target_type, "assignment",
3903
+ coercion_ctx=CoercionContext.ASSIGN,
3904
+ target_is_storage_form=target_is_storage)
3905
+ # Annotate tuple literal element capture modes
3906
+ if isinstance(stmt.value, TpyTupleLiteral):
3907
+ tuple_target = own_tuple_target(target_type)
3908
+ if tuple_target is not None:
3909
+ is_field = isinstance(stmt.target, TpyFieldAccess)
3910
+ self._annotate_tuple_elem_capture(
3911
+ stmt.value, tuple_target, is_field=is_field)
3912
+ # Residual copy warning: reassigned vars without OwnType in scope
3913
+ if isinstance(stmt.target, (TpyFieldAccess, TpySubscript)):
3914
+ if stmt.loc is not None and not copy_warning_fired:
3915
+ if self._is_non_owned_var_copy(stmt.value, target_type):
3916
+ dest = "field" if isinstance(stmt.target, TpyFieldAccess) else "container"
3917
+ if self.ctx.is_type_non_copyable(target_type):
3918
+ verb = "may copy" if isinstance(target_type, TypeParamRef) else "cannot copy"
3919
+ if value_type == target_type:
3920
+ msg = (f"{verb} non-copyable type '{target_type}' into "
3921
+ f"{dest}{NOCOPY_REMEDIATION_HINT}")
3922
+ else:
3923
+ msg = (f"{verb} {value_type} into {dest} of type '{target_type}'; "
3924
+ f"target is non-copyable{NOCOPY_REMEDIATION_HINT}")
3925
+ raise self.ctx.error(msg, stmt)
3926
+ if isinstance(target_type, TypeParamRef):
3927
+ msg = f"may copy {target_type} into {dest} if not a value type; use copy() to make this explicit"
3928
+ else:
3929
+ msg = f"copies {value_type} into {dest}; use copy() to make this explicit"
3930
+ self.ctx.warning(msg, stmt)
3931
+
3932
+ # Scope escape check for assignments to named variables
3933
+ if isinstance(stmt.target, TpyName) and not target_type.is_value_type():
3934
+ self.scopes.check_escape(stmt.target.name, stmt.value, stmt)
3935
+ # Assigning a loop var to a pointer-local takes &(var) in codegen
3936
+ if isinstance(stmt.value, TpyName):
3937
+ self.ctx.mark_loop_var_mutated(stmt.value.name)
3938
+
3939
+ if isinstance(stmt.target, TpyName) and _needs_provenance_tracking(target_type):
3940
+ self.init.mark_provenance(stmt.target.name, self.compat.is_param_derived_expr(stmt.value))
3941
+ self.init.mark_safe_to_return(
3942
+ stmt.target.name, self.compat.is_safe_to_return_expr(stmt.value))
3943
+ # Track non-null pointer provenance for null-check elision
3944
+ if isinstance(stmt.target, TpyName) and isinstance(target_type, PtrType):
3945
+ is_non_null = expr_yields_non_null_ptr(stmt.value, self.ctx.func.non_null_ptr_vars)
3946
+ self.init.mark_non_null_ptr(stmt.target.name, is_non_null)
3947
+
3948
+ # Mark as definitely assigned for plain name targets
3949
+ if isinstance(stmt.target, TpyName):
3950
+ self.init.mark_assigned(stmt.target.name)
3951
+ self.narrowing.update_after_write(stmt.target.name, target_type, value_type, stmt.value)
3952
+ elif isinstance(stmt.target, TpyFieldAccess):
3953
+ self.narrowing.invalidate_for_field_write(stmt.target)
3954
+
3955
+ def _analyze_del_item(self, stmt: TpyDelItem) -> None:
3956
+ """Analyze del obj[key] statement."""
3957
+ for subscript in stmt.targets:
3958
+ # Analyze obj and index separately to avoid triggering __getitem__
3959
+ # validation (del doesn't read the element, only deletes it).
3960
+ self.expr.analyze_expr(subscript.obj)
3961
+ # Track mutation of for-each loop variables and parameters
3962
+ del_root = _root_name_of_expr(subscript.obj)
3963
+ if del_root is not None:
3964
+ self.ctx.mark_loop_var_mutated(del_root)
3965
+ self.ctx.mark_param_mutated(del_root)
3966
+ # del item removes an element from the container -- structural mutation.
3967
+ self.ctx.mark_param_structurally_mutated(del_root)
3968
+ # Borrow conflict: del on a container with element-level borrows
3969
+ storage = self._resolve_obj_storage(subscript.obj)
3970
+ if storage is not None:
3971
+ bt = self.ctx.func.borrow_tracker
3972
+ if bt.has_element_borrow(storage):
3973
+ if bt.has_iter_borrow(storage):
3974
+ msg = (f"Mutation of '{storage}' while iterating over it"
3975
+ " ('del' invalidates the iterator)")
3976
+ else:
3977
+ msg = (f"Mutation of '{storage}' while borrowed"
3978
+ " ('del' may invalidate references)")
3979
+ self.ctx.warning(msg, stmt)
3980
+ self.ctx.mark_all_view_borrowers_mutated(storage)
3981
+ self._enforce_readonly_assignment_target(subscript)
3982
+ obj_type = self.ctx.get_expr_type(subscript.obj)
3983
+ actual = unwrap_readonly(obj_type)
3984
+ # Reject known-immutable/fixed-size types before analyzing the index
3985
+ if isinstance(actual, TupleType):
3986
+ raise self.ctx.error(
3987
+ "Tuples are immutable; cannot delete tuple elements", stmt)
3988
+ if is_array(actual):
3989
+ raise self.ctx.error(
3990
+ "Arrays are fixed-size; cannot delete array elements", stmt)
3991
+ if is_span(actual):
3992
+ raise self.ctx.error(
3993
+ "Spans are read-only views; cannot delete span elements", stmt)
3994
+ # Check that the type has __delitem__
3995
+ record_info = self.ctx.registry.get_record_for_type(actual)
3996
+ if record_info:
3997
+ overloads = record_info.get_method_overloads("__delitem__")
3998
+ if not overloads:
3999
+ raise self.ctx.error(
4000
+ f"'del' is not supported for type {actual}; "
4001
+ f"define __delitem__ to enable element deletion", stmt)
4002
+ else:
4003
+ raise self.ctx.error(
4004
+ f"'del' is not supported for type {actual}", stmt)
4005
+ # Analyze the index expression only after confirming __delitem__ exists
4006
+ self.expr.analyze_expr(subscript.index)
4007
+
4008
+ def _target_is_dyn_writable_only(self, target: TpyFieldAccess) -> bool:
4009
+ """D16: True if `target` is `obj.foo` where foo is undeclared on the
4010
+ receiver's class AND the class has __setattr__ but not __getattr__.
4011
+
4012
+ In that combination the read-side analyzer would raise "no field"
4013
+ before reaching the assign-time dunder routing, so the assign path
4014
+ needs to take over BEFORE calling analyze_expr on the target.
4015
+ """
4016
+ try:
4017
+ obj_type = self.expr.analyze_expr(target.obj)
4018
+ except SemanticError:
4019
+ return False
4020
+ actual = unwrap_qualifiers(obj_type) if obj_type is not None else None
4021
+ if not (isinstance(actual, NominalType) and actual.is_record):
4022
+ return False
4023
+ record = self.ctx.registry.get_record_for_type(actual)
4024
+ if record is None:
4025
+ return False
4026
+ # Cheap reject first: if the class has __getattr__, the standard path
4027
+ # (dyn_getattr_call) handles it -- this fast-path is for the
4028
+ # __setattr__-only case. If there is also no __setattr__, irrelevant.
4029
+ if self.protocols.lookup_record_method_overloads(record, "__getattr__")[0]:
4030
+ return False
4031
+ if not self.protocols.lookup_record_method_overloads(record, "__setattr__")[0]:
4032
+ return False
4033
+ # Static lookup must miss for the dyn-setattr fallback to fire.
4034
+ if self.protocols.lookup_record_field(record, target.field) is not None:
4035
+ return False
4036
+ if self.protocols.lookup_record_property(record, target.field) is not None:
4037
+ return False
4038
+ if self.protocols.lookup_record_method_overloads(record, target.field)[0]:
4039
+ return False
4040
+ if target.field in record.class_constants:
4041
+ return False
4042
+ return True
4043
+
4044
+ def _analyze_dyn_setattr_assign(self, stmt: TpyAssign) -> None:
4045
+ """D16: write-only dyn-setattr path used when the class has __setattr__
4046
+ but no __getattr__ (so the read-side analyzer can't resolve the target).
4047
+ """
4048
+ assert isinstance(stmt.target, TpyFieldAccess)
4049
+ setter_call = TpyMethodCall(
4050
+ obj=stmt.target.obj,
4051
+ method="__setattr__",
4052
+ args=[TpyStrLiteral(value=stmt.target.field), stmt.value],
4053
+ loc=stmt.loc,
4054
+ )
4055
+ self.expr.analyze_expr(setter_call)
4056
+ stmt.target.dyn_setattr_call = setter_call
4057
+ # Mutation tracking parallel to plain field assignment.
4058
+ root = _root_name_of_expr(stmt.target.obj)
4059
+ if root is not None:
4060
+ self.ctx.mark_loop_var_mutated(root)
4061
+ self.ctx.mark_param_mutated(root)
4062
+ self._enforce_readonly_assignment_target(stmt.target)
4063
+
4064
+ def _analyze_del_attr(self, stmt: 'TpyDelAttr') -> None:
4065
+ """D16 Phase 3: del obj.foo, obj2.bar -- route through __delattr__.
4066
+
4067
+ Declared fields, properties, methods, and class constants are not
4068
+ deletable in TPy regardless of whether the class defines __delattr__
4069
+ (record layout is fixed). For undeclared names, route through the
4070
+ dunder if present.
4071
+ """
4072
+ for target in stmt.targets:
4073
+ obj_type = self.expr.analyze_expr(target.obj)
4074
+ actual = unwrap_qualifiers(obj_type) if obj_type is not None else None
4075
+ if not (isinstance(actual, NominalType) and actual.is_record):
4076
+ raise self.ctx.error(
4077
+ f"Cannot delete attribute '{target.field}' on type {obj_type}",
4078
+ stmt,
4079
+ )
4080
+ record = self.ctx.registry.get_record_for_type(actual)
4081
+ if record is None:
4082
+ raise self.ctx.error(
4083
+ f"Cannot delete attribute '{target.field}' on type {obj_type}",
4084
+ stmt,
4085
+ )
4086
+ field_name = target.field
4087
+ # Reject declared-member targets uniformly.
4088
+ if self.protocols.lookup_record_field(record, field_name) is not None:
4089
+ raise self.ctx.error(
4090
+ f"Cannot delete declared field '{field_name}' from "
4091
+ f"'{record.name}' (record layout is fixed)",
4092
+ stmt,
4093
+ )
4094
+ if self.protocols.lookup_record_property(record, field_name) is not None:
4095
+ raise self.ctx.error(
4096
+ f"Cannot delete declared property '{field_name}' from "
4097
+ f"'{record.name}'",
4098
+ stmt,
4099
+ )
4100
+ method_overloads, _ = self.protocols.lookup_record_method_overloads(record, field_name)
4101
+ if method_overloads:
4102
+ raise self.ctx.error(
4103
+ f"Cannot delete method '{field_name}' from '{record.name}'",
4104
+ stmt,
4105
+ )
4106
+ if field_name in record.class_constants:
4107
+ raise self.ctx.error(
4108
+ f"Cannot delete class constant '{field_name}' from '{record.name}'",
4109
+ stmt,
4110
+ )
4111
+ # Route through __delattr__ if defined.
4112
+ da_overloads, _da_subst = self.protocols.lookup_record_method_overloads(
4113
+ record, "__delattr__")
4114
+ if not da_overloads:
4115
+ # Only mention __delattr__ when the class has already opted
4116
+ # into dyn-attrs (any of __getattr__/__setattr__ defined);
4117
+ # for a plain record, just say the field doesn't exist.
4118
+ has_dyn_attrs = bool(
4119
+ self.protocols.lookup_record_method_overloads(record, "__getattr__")[0]
4120
+ or self.protocols.lookup_record_method_overloads(record, "__setattr__")[0]
4121
+ )
4122
+ if has_dyn_attrs:
4123
+ raise self.ctx.error(
4124
+ f"Record '{record.name}' has no field '{field_name}' and does not "
4125
+ f"define __delattr__",
4126
+ stmt,
4127
+ )
4128
+ raise self.ctx.error(
4129
+ f"Record '{record.name}' has no field '{field_name}'", stmt)
4130
+ synth = TpyMethodCall(
4131
+ obj=target.obj,
4132
+ method="__delattr__",
4133
+ args=[TpyStrLiteral(value=field_name)],
4134
+ loc=stmt.loc,
4135
+ )
4136
+ self.expr.analyze_expr(synth)
4137
+ target.dyn_delattr_call = synth
4138
+ # Mark mutation: deletion through dunder mutates the receiver.
4139
+ root = _root_name_of_expr(target.obj)
4140
+ if root is not None:
4141
+ self.ctx.mark_loop_var_mutated(root)
4142
+ self.ctx.mark_param_mutated(root)
4143
+ self._enforce_readonly_assignment_target(target)
4144
+
4145
+ def _analyze_del_var(self, stmt: TpyDelVar) -> None:
4146
+ """Analyze a variable deletion statement (del x)."""
4147
+ for name in stmt.names:
4148
+ # Global-declared and nonlocal vars are always reachable;
4149
+ # locals/params must be definitely assigned.
4150
+ is_external = (name in self.ctx.func.global_declarations
4151
+ or name in self.ctx.func.current_nonlocal_names)
4152
+ if not is_external and name not in self.ctx.func.definitely_assigned:
4153
+ raise self.ctx.error(
4154
+ f"variable '{name}' may not be assigned at this point", stmt)
4155
+ # Remove from definitely_assigned so use-after-del is caught
4156
+ self.ctx.func.definitely_assigned.discard(name)
4157
+ # Clear narrowing facts
4158
+ self.ctx.func.narrowed_types.pop(name, None)
4159
+ self.ctx.func.non_null_ptr_vars.discard(name)
4160
+
4161
+ def _apply_aug_assign_writeback(
4162
+ self,
4163
+ target: TpyExpr,
4164
+ target_type: TpyType,
4165
+ result_type: TpyType,
4166
+ op: str,
4167
+ stmt: TpyAugAssign,
4168
+ ) -> None:
4169
+ """Check and apply the write-back step of an augmented assignment.
4170
+
4171
+ After the binop is resolved with result_type, verifies result_type is
4172
+ compatible with the target and updates variable caches when widening applies.
4173
+ Uses the same type rules as regular assignment (resolve_reassignment_target_type
4174
+ + check_type_compatible), so annotated variables and non-wideneable pairs
4175
+ produce a standard type mismatch error.
4176
+ """
4177
+ if result_type == target_type:
4178
+ return
4179
+ if isinstance(target, TpyName):
4180
+ name = target.name
4181
+ effective_type = self.deduction.resolve_reassignment_target_type(
4182
+ name, target_type, result_type,
4183
+ )
4184
+ # check_type_compatible errors when effective_type refused widening
4185
+ # (e.g. annotated variable, or mixed-sign fixed-int pair).
4186
+ self.compat.check_type_compatible(
4187
+ result_type, effective_type, f"'{op}=' to '{name}'", loc=stmt.loc,
4188
+ )
4189
+ if effective_type != target_type:
4190
+ if self.ctx.func.current_scope:
4191
+ self.ctx.func.current_scope.define(name, effective_type)
4192
+ var_decl = self.ctx.func.var_decl_by_name.get(name)
4193
+ if var_decl:
4194
+ self.ctx.var_types[id(var_decl)] = effective_type
4195
+ for key in self.ctx.declared_var_types:
4196
+ if key[1] == name:
4197
+ self.ctx.declared_var_types[key] = effective_type
4198
+ else:
4199
+ # Subscript/field target: element type is fixed, cannot widen.
4200
+ self.compat.check_type_compatible(
4201
+ result_type, target_type,
4202
+ f"'{op}=' to {_format_aug_target(target)}", loc=stmt.loc,
4203
+ target_is_storage_form=True,
4204
+ )
4205
+
4206
+ def _is_non_owned_var_copy(self, expr: TpyExpr, target_type: TpyType) -> bool:
4207
+ """Check if storing a non-OwnType variable copies into storage.
4208
+
4209
+ Covers locals excluded from OwnType wrapping: reassigned vars,
4210
+ union types, Optional with pointer repr. These are not caught by
4211
+ the unified Ref/Own check.
4212
+ """
4213
+ if target_type.is_value_type():
4214
+ return False
4215
+ if self.compat._is_value_type_param(target_type):
4216
+ return False
4217
+ if self.compat.is_copy_call(expr):
4218
+ return False
4219
+ if not isinstance(expr, TpyName):
4220
+ return False
4221
+ scope_type = self.ctx.func.current_scope.lookup(expr.name) if self.ctx.func.current_scope else None
4222
+ if scope_type is not None and isinstance(scope_type, (RefType, OwnType)):
4223
+ return False # already caught by the Ref/Own check
4224
+ if scope_type is not None and scope_type.is_value_type():
4225
+ return False
4226
+ # Skip at last-use of movable var
4227
+ if id(expr) in self.ctx.all_last_uses and self.compat._is_owned_var(expr.name):
4228
+ return False
4229
+ if scope_type is None:
4230
+ return False
4231
+ return True
4232
+
4233
+ def _analyze_aug_assign(self, stmt: TpyAugAssign) -> None:
4234
+ """Analyze an augmented assignment (+=, -=, etc.)."""
4235
+ # In nested defs, aug-assign to an outer variable requires nonlocal
4236
+ if (self.ctx.func.in_nested_def
4237
+ and isinstance(stmt.target, TpyName)
4238
+ and stmt.target.name in self.ctx.func.outer_scope_locals
4239
+ and stmt.target.name not in self.ctx.func.current_nonlocal_names):
4240
+ raise self.ctx.error(
4241
+ f"Cannot modify '{stmt.target.name}' in nested function"
4242
+ f" without 'nonlocal' declaration",
4243
+ stmt)
4244
+ # Block augmented assignment of Final globals at module level
4245
+ if isinstance(stmt.target, TpyName) and self.ctx.is_top_level and stmt.target.name in self.ctx.final_globals:
4246
+ raise self.ctx.error(
4247
+ f"Cannot reassign Final variable '{stmt.target.name}'",
4248
+ stmt
4249
+ )
4250
+ target_type = unwrap_own(unwrap_ref_type(self.expr.analyze_expr(stmt.target)))
4251
+ # Augmented assignment on properties not yet supported
4252
+ if isinstance(stmt.target, TpyFieldAccess) and stmt.target.is_property_access:
4253
+ raise self.ctx.error(
4254
+ f"Augmented assignment on property '{stmt.target.field}' is not yet supported",
4255
+ stmt,
4256
+ )
4257
+ self._check_class_constant_write(stmt.target, stmt)
4258
+ # Aug-assign replaces the target's value with a freshly computed one
4259
+ # (owned str/bytes concat, reallocated list, etc.), so any prior
4260
+ # param-derived / safe-to-return provenance is now stale and must be
4261
+ # cleared -- otherwise a later `return` as a view would pass the
4262
+ # dangling check despite pointing into local storage.
4263
+ if isinstance(stmt.target, TpyName) and _needs_provenance_tracking(target_type):
4264
+ self.init.mark_provenance(stmt.target.name, False)
4265
+ self.init.mark_safe_to_return(stmt.target.name, False)
4266
+ value_type = self.expr.analyze_expr_with_hint(stmt.value, target_type)
4267
+ # Track mutation of for-each loop variables and parameters
4268
+ aug_root = _root_name_of_expr(stmt.target)
4269
+ if aug_root is not None:
4270
+ self.ctx.mark_loop_var_mutated(aug_root)
4271
+ self.ctx.mark_param_mutated(aug_root)
4272
+ # items += other_list extends the container in-place (structural mutation).
4273
+ # items[i] += x and obj.field += x are in-place element/field writes -- not structural.
4274
+ if isinstance(stmt.target, TpyName):
4275
+ self.ctx.mark_param_structurally_mutated(aug_root)
4276
+ self._enforce_readonly_assignment_target(stmt.target)
4277
+ # Borrow conflict: augmented assignment may mutate borrowed storage.
4278
+ # Subscript aug-assign (items[i] += x) modifies element in-place -- same as
4279
+ # subscript assign, no reallocation, element/ptr borrows remain valid.
4280
+ if isinstance(stmt.target, TpySubscript):
4281
+ storage = self._resolve_obj_storage(stmt.target.obj)
4282
+ if storage is not None:
4283
+ self.ctx.mark_all_view_borrowers_mutated(storage)
4284
+ elif isinstance(stmt.target, TpyFieldAccess):
4285
+ storage = self._resolve_obj_storage(stmt.target.obj)
4286
+ field_storage = _storage_key(stmt.target)
4287
+ _BORROW_KINDS = (BorrowKind.FIELD, BorrowKind.ELEMENT, BorrowKind.PTR, BorrowKind.ITER)
4288
+ has_conflict = False
4289
+ bt = self.ctx.func.borrow_tracker
4290
+ if storage is not None and bt.has_borrow_of_kinds(storage, _BORROW_KINDS):
4291
+ has_conflict = True
4292
+ if not has_conflict and field_storage is not None and bt.has_borrow_of_kinds(field_storage, _BORROW_KINDS):
4293
+ storage = field_storage
4294
+ has_conflict = True
4295
+ if has_conflict:
4296
+ self.ctx.warning(
4297
+ f"Mutation of '{storage}' while borrowed"
4298
+ " (field assignment may invalidate references)",
4299
+ stmt,
4300
+ )
4301
+ if storage is not None:
4302
+ self.ctx.mark_all_view_borrowers_mutated(storage)
4303
+ if field_storage is not None and field_storage != storage:
4304
+ self.ctx.mark_all_view_borrowers_mutated(field_storage)
4305
+ # Borrow conflict: aug-assign on a name target that has element borrows.
4306
+ # Any structural aug-assign (list +=, set |=, user-defined __iadd__ that
4307
+ # reallocates) is a mutation -- check the borrow state, not the container type.
4308
+ elif isinstance(stmt.target, TpyName):
4309
+ bt = self.ctx.func.borrow_tracker
4310
+ storage = bt.effective_storage(stmt.target.name)
4311
+ if bt.has_element_borrow(storage):
4312
+ if bt.has_iter_borrow(storage):
4313
+ self.ctx.warning(
4314
+ f"Mutation of '{storage}' while iterating over it"
4315
+ f" ('{stmt.op}=' invalidates the iterator)",
4316
+ stmt,
4317
+ )
4318
+ else:
4319
+ self.ctx.warning(
4320
+ f"Mutation of '{storage}' while borrowed"
4321
+ f" ('{stmt.op}=' may invalidate references)",
4322
+ stmt,
4323
+ )
4324
+ self.ctx.mark_all_view_borrowers_mutated(storage)
4325
+ if (
4326
+ isinstance(stmt.target, TpyName)
4327
+ and is_big_int_type(target_type)
4328
+ and is_fixed_int_type(value_type)
4329
+ and stmt.target.name in self.ctx.func.literal_default_vars
4330
+ ):
4331
+ type_name = str(value_type)
4332
+ self.ctx.warning(
4333
+ f"Augmented assignment does not narrow '{stmt.target.name}' from int to {type_name}; "
4334
+ f"variable remains int (BigInt). Annotate or initialize '{stmt.target.name}' as {type_name} "
4335
+ f"to keep {type_name} arithmetic.",
4336
+ stmt,
4337
+ )
4338
+ # Literal-typed targets reject augmented assignment outright: the result
4339
+ # of `x += y` is rarely in the declared value set, and Literal[str] locals
4340
+ # use std::string_view storage which can't hold a new owned string anyway.
4341
+ if isinstance(target_type, LiteralType):
4342
+ raise self.ctx.error(
4343
+ f"Augmented assignment is not supported for Literal[...] -- the "
4344
+ f"result is not guaranteed to be in the declared value set",
4345
+ stmt,
4346
+ )
4347
+ # Target must be numeric, owned string, or a type with registered operators.
4348
+ # StrView is excluded -- it's non-owning, so += would dangle.
4349
+ is_numeric_target = is_any_int_type(target_type) or is_float_type(target_type)
4350
+ is_str_target = (is_str_type(target_type) or is_string_type(target_type)
4351
+ or isinstance(target_type, PendingStrType))
4352
+ is_bytes_target = (is_bytes_type(target_type) or is_bytearray_type(target_type)
4353
+ or isinstance(target_type, PendingBytesType))
4354
+ # PendingViewType += promotes to owned
4355
+ if isinstance(target_type, PendingViewType) and isinstance(stmt.target, TpyName):
4356
+ self.deduction.mark_view_augassign(stmt.target.name, target_type.family)
4357
+ if not is_numeric_target and not is_str_target and not is_bytes_target:
4358
+ # StrView/BytesView += would dangle (result is a temporary assigned to a view)
4359
+ if is_str_view_type(target_type):
4360
+ raise self.ctx.error(
4361
+ f"Augmented assignment is not supported for StrView (result would dangle)",
4362
+ stmt,
4363
+ )
4364
+ if is_bytes_view_type(target_type):
4365
+ raise self.ctx.error(
4366
+ f"Augmented assignment is not supported for BytesView (result would dangle)",
4367
+ stmt,
4368
+ )
4369
+ # Try in-place method first (e.g. __iadd__, __ior__), then binary operator
4370
+ operators = self.expr.operators
4371
+ protocol_checker = self.protocols.type_conforms_to_protocol if self.protocols else None
4372
+ if result := operators.resolve_aug_inplace(
4373
+ target_type, stmt.op, value_type,
4374
+ protocol_checker=protocol_checker, loc_node=stmt,
4375
+ ):
4376
+ stmt.resolved_inplace = result
4377
+ if result.method.params:
4378
+ _, param_type = result.method.params[0]
4379
+ self.compat.check_type_compatible(
4380
+ value_type, param_type, f"'{stmt.op}=' operand", source_expr=stmt.value,
4381
+ )
4382
+ return
4383
+ # Method exists but arg type mismatches -- produce a specific type error.
4384
+ expected_param = operators.get_aug_inplace_param_type(target_type, stmt.op)
4385
+ if expected_param is not None:
4386
+ self.compat.check_type_compatible(
4387
+ value_type, expected_param, f"'{stmt.op}=' operand",
4388
+ loc=stmt.loc, source_expr=stmt.value,
4389
+ )
4390
+ if result := operators.resolve_binop(target_type, stmt.op, value_type, loc_node=stmt):
4391
+ stmt.resolved_binop = result
4392
+ return
4393
+ raise self.ctx.error(
4394
+ f"Operator '{stmt.op}=' is not supported for {target_type}",
4395
+ stmt,
4396
+ )
4397
+ check_value_type = value_type.wrapped if isinstance(value_type, OwnType) else value_type
4398
+ if is_numeric_target and not (is_any_int_type(check_value_type) or is_any_float_type(check_value_type)):
4399
+ raise self.ctx.error(
4400
+ f"Augmented assignment value must be a numeric type, got {check_value_type}",
4401
+ stmt,
4402
+ )
4403
+ if is_str_target and not is_any_str_type(check_value_type):
4404
+ raise self.ctx.error(
4405
+ f"Augmented assignment value must be a string type, got {check_value_type}",
4406
+ stmt,
4407
+ )
4408
+ # Special case: FixedInt += BigInt should use the target's ops (value gets converted)
4409
+ # This preserves checked arithmetic and avoids unnecessary promotion to BigInt
4410
+ resolve_value_type = check_value_type
4411
+ if is_fixed_int_type(target_type) and is_big_int_type(check_value_type):
4412
+ resolve_value_type = target_type
4413
+ # Invalidate range facts for the target (value has changed)
4414
+ if isinstance(stmt.target, TpyName):
4415
+ self.ctx.func.value_ranges.pop(stmt.target.name, None)
4416
+ # Resolve the binary operation for codegen
4417
+ operators = self.expr.operators
4418
+ if result := operators.resolve_binop(target_type, stmt.op, resolve_value_type, loc_node=stmt):
4419
+ stmt.resolved_binop = result
4420
+ if is_numeric_target:
4421
+ self._apply_aug_assign_writeback(stmt.target, target_type, result.method.return_type, stmt.op, stmt)
4422
+ elif is_numeric_target:
4423
+ raise self.ctx.error(
4424
+ f"Operator '{stmt.op}=' is not supported between {target_type} and {value_type}",
4425
+ stmt,
4426
+ )