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,2107 @@
1
+ """
2
+ TurboPython Type Compatibility
3
+
4
+ Type compatibility checking, coercions, and lvalue analysis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from dataclasses import replace as dc_replace
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ from ..typesys import (
12
+ TpyType, IntLiteralType, FloatLiteralType, ListRepeatType,
13
+ PendingListType, PendingDictType, PendingSetType, PendingStrType, PendingBytesType, UnknownElementType,
14
+ LiteralType, LiteralValue, LiteralTag, FLOAT, make_list, make_dict, make_set,
15
+ OwnType, ReadonlyType, VoidType, PtrType, is_readonly_ptr, TupleType,
16
+ NominalType, AliasRef, RecursiveAliasInstanceType, TypeParamRef, NoneType, AnyType, OptionalType, UnionType,
17
+ is_protocol_type, is_dyn_protocol, unwrap_own, unwrap_readonly, unwrap_optional_own,
18
+ is_any_str_type, get_covariant_params, PendingGenericInstanceType,
19
+ CallableType, is_fn_type, RefType, unwrap_ref_type,
20
+ is_callable_type, is_integer_type, is_any_float_type, is_readonly_span,
21
+ is_polymorphic_class_type)
22
+ from ..parse import (
23
+ TpyExpr, TpyName, TpyFieldAccess, TpySubscript, TpyArrayLiteral,
24
+ TpyDictLiteral, TpySetLiteral, TpyListRepeat, TpyCall, TpyMethodCall, TpyUnaryOp,
25
+ TpyBinOp, TpyCoerce, TpyNoneLiteral, TpyIntLiteral, TpyStrLiteral, TpyBytesLiteral,
26
+ TpyFunction, TpyIfExpr, TpyTupleLiteral, SourceLocation
27
+ )
28
+ from .literal_utils import literal_value_from_expr
29
+ from ..coercions import resolve_coercion, Coercion, CoercionContext, DEREF_COERCION, UPCAST_TO_PTR, UPCAST_TO_CONST_PTR, SPAN_METHOD_TO_SPAN_ARG, SPAN_METHOD_TO_SPAN, INTO_ANY, FROM_ANY
30
+ from ..modules import get_span_return_type
31
+ from .context import addr_taken_roots
32
+ from .numeric_lattice import fixed_int_range_contains, numeric_info
33
+ from ..diagnostics import SemanticError, NOCOPY_REMEDIATION_HINT
34
+ from ..type_def_registry import (
35
+ is_set, is_dict, is_array, is_span, is_span_iter, is_list,
36
+ is_str_view_type, is_bytes_view_type, is_borrowing_view_type, int_traits_of,
37
+ is_big_int_type, is_str_category, is_bytes_category, is_str_type, is_string_type,
38
+ protocol_info_of,
39
+ )
40
+ from .overloads import type_matches_numeric
41
+
42
+
43
+ def _literal_value_from_source(
44
+ source_expr: TpyExpr | None, expected: LiteralType,
45
+ ) -> LiteralValue | None:
46
+ """LiteralValue from a literal AST source, gated on tag-vs-base agreement."""
47
+ lv = literal_value_from_expr(source_expr)
48
+ if lv is None:
49
+ return None
50
+ if lv.tag is LiteralTag.STR and expected.is_str_base():
51
+ return lv
52
+ if lv.tag is LiteralTag.BOOL and expected.is_bool_base():
53
+ return lv
54
+ if lv.tag is LiteralTag.INT and expected.is_int_base():
55
+ return lv
56
+ return None
57
+
58
+
59
+ def _dangling_view_message(return_type: TpyType) -> str | None:
60
+ """Error message for returning a view that borrows from a local, or None
61
+ if return_type is not a borrowing-view type.
62
+ """
63
+ if is_str_view_type(return_type):
64
+ return ("Cannot return StrView referencing a local or temporary; "
65
+ "use str or String to return an owned copy")
66
+ if is_bytes_view_type(return_type):
67
+ return ("Cannot return BytesView referencing a local or temporary; "
68
+ "use bytes or bytearray to return an owned copy")
69
+ if is_span(return_type):
70
+ return ("Cannot return Span referencing a local or temporary; "
71
+ "use list or Array to return an owned copy")
72
+ if is_span_iter(return_type):
73
+ return ("Cannot return SpanIter referencing a local or temporary; "
74
+ "the underlying Span would dangle after the function returns")
75
+ return None
76
+
77
+
78
+ def _container_elem_matches(actual_elem: TpyType, expected_elem: TpyType) -> bool:
79
+ """Strict element type check for container assignment.
80
+
81
+ Allows Own[T] stripping and numeric literal coercions only.
82
+ Subclass coercion is excluded: C++ containers are non-converting templates
83
+ (invariant T) -- set[Child] cannot be used where set[Base] is expected.
84
+ """
85
+ check = expected_elem.wrapped if isinstance(expected_elem, OwnType) else expected_elem
86
+ return actual_elem == check or type_matches_numeric(actual_elem, check)
87
+
88
+
89
+ def _contains_ref_type(t: TpyType) -> bool:
90
+ """True if t or any nested type is or may be a reference type.
91
+
92
+ Recurses into inner_types() so that structural types like TupleType
93
+ (whose is_value_type() is hardcoded True) are checked element by element.
94
+ TypeParamRef with no ValueType bound is treated as potentially a reference type.
95
+ """
96
+ if not t.is_value_type():
97
+ return True
98
+ # is_value_type() True -- recurse into inner types to catch e.g. tuple[str, Node]
99
+ return any(_contains_ref_type(inner) for inner in t.inner_types())
100
+
101
+
102
+ def _is_definitely_ref_type(t: TpyType) -> bool:
103
+ """True if t definitely contains a reference type (no unresolved TypeParamRefs).
104
+
105
+ Returns False when the ref-ness depends on a TypeParamRef, in which case
106
+ the caller should use a "may copy" warning instead of "copies".
107
+ """
108
+ if isinstance(t, TypeParamRef):
109
+ return False # unknown until instantiated
110
+ if not t.is_value_type():
111
+ return True
112
+ return any(_is_definitely_ref_type(inner) for inner in t.inner_types())
113
+
114
+
115
+ def _contains_semantic_ref(t: TpyType) -> bool:
116
+ """True if t contains a RefType wrapper (explicit borrowed reference).
117
+
118
+ Unlike _contains_ref_type which checks is_value_type() (structural),
119
+ this checks for RefType specifically -- the semantic marker that sema
120
+ inserts for borrowed references. Used to detect iterator-to-container
121
+ copies where the source is an rvalue but yields borrowed elements.
122
+ """
123
+ if isinstance(t, RefType):
124
+ return True
125
+ return any(_contains_semantic_ref(inner) for inner in t.inner_types())
126
+
127
+
128
+ def _inner_compat_with_ro_widening(source_inner: TpyType, dest_inner: TpyType) -> bool:
129
+ """True when `source_inner` is compatible with `dest_inner` at a Ptr <-> Optional
130
+ pointer-repr boundary, allowing mutable->readonly inner widening only.
131
+
132
+ Used for both `Ptr[T] -> T | None` (source_inner = ptr pointee, dest_inner =
133
+ optional inner) and `T | None -> Ptr[T]` (source_inner = optional inner,
134
+ dest_inner = ptr pointee). The two directions are complementary: in each,
135
+ `source_inner` is on the source side and `dest_inner` is on the destination
136
+ side, regardless of which side is the Ptr.
137
+
138
+ - Same inner: always allowed.
139
+ - Mutable source -> readonly destination: allowed (adding const is safe).
140
+ - Readonly source -> mutable destination: rejected (dropping const is unsafe).
141
+ """
142
+ if source_inner == dest_inner:
143
+ return True
144
+ if isinstance(dest_inner, ReadonlyType) and not isinstance(source_inner, ReadonlyType):
145
+ return dest_inner.wrapped == source_inner
146
+ return False
147
+
148
+
149
+ def _ptr_readonly_compatible(actual: PtrType, expected: PtrType) -> bool:
150
+ """True if `actual`'s readonly-ness may flow into `expected` -- a
151
+ mutable target from a readonly source would launder away the const."""
152
+ return expected.is_readonly or not actual.is_readonly
153
+
154
+
155
+ def _is_natural_union_member(actual: TpyType, a_info, member: TpyType) -> bool:
156
+ """True if `member` is the natural target for `actual` in a union match,
157
+ i.e. selecting it does not require a category-crossing widening.
158
+
159
+ `a_info` must be `numeric_info(actual)` (passed in to avoid recomputing).
160
+ """
161
+ if member == actual:
162
+ return True
163
+ m_info = numeric_info(member)
164
+ if a_info is None or m_info is None:
165
+ return False
166
+ if a_info.family == m_info.family:
167
+ return True
168
+ # Literal families naturally land on their concrete counterpart.
169
+ if a_info.family == "int_literal" and m_info.family == "int":
170
+ return True
171
+ if a_info.family == "float_literal" and m_info.family == "float":
172
+ return True
173
+ return False
174
+
175
+
176
+ class CompatError:
177
+ """Type compatibility check failure (returned by _check_compat, not raised)."""
178
+ __slots__ = ('message', 'loc')
179
+
180
+ def __init__(self, message: str, loc: SourceLocation | None = None):
181
+ self.message = message
182
+ self.loc = loc
183
+
184
+
185
+ # Result type for _check_compat: Coercion | None (compatible) or CompatError (incompatible)
186
+ CompatResult = Coercion | CompatError | None
187
+
188
+
189
+ if TYPE_CHECKING:
190
+ from .context import SemanticContext
191
+ from .type_ops import TypeOperations
192
+ from .protocols import ProtocolChecker
193
+ from .methods import MethodAnalyzer
194
+ from .local_deduction import LocalTypeDeduction
195
+
196
+
197
+ class TypeCompatibility:
198
+ """Type compatibility checking, coercions, and lvalue analysis."""
199
+
200
+ def __init__(self, ctx: SemanticContext):
201
+ self.ctx = ctx
202
+ # Set after construction (compat is created before these exist)
203
+ self.type_ops: TypeOperations
204
+ self.protocols: ProtocolChecker
205
+ self.methods: MethodAnalyzer
206
+ self.deduction: 'LocalTypeDeduction'
207
+
208
+ def _mark_addr_taken(self, expr: TpyExpr) -> None:
209
+ """Mark all param roots of expr as mutated because their address is taken.
210
+
211
+ Resolves alias / element / field / ptr borrow chains via
212
+ `effective_storage` so element-borrowing aliases (`v = items[i]`)
213
+ trace back to their underlying param. Loop variables also propagate
214
+ to their source iterables -- both are storage that the address-take
215
+ could mutate via the resulting pointer.
216
+ """
217
+ for name in addr_taken_roots(expr):
218
+ root = self.ctx.func.borrow_tracker.effective_storage(name)
219
+ self.ctx.mark_param_mutated(root)
220
+ self.ctx.mark_loop_var_mutated(root)
221
+
222
+ def is_type_compatible(self, actual: TpyType, expected: TpyType) -> bool:
223
+ """Non-raising check: is actual assignable to expected?"""
224
+ return not isinstance(self._check_compat(actual, expected, ""), CompatError)
225
+
226
+ def _check_polymorphic_slicing(
227
+ self,
228
+ actual: TpyType,
229
+ expected: TpyType,
230
+ source_expr: TpyExpr | None,
231
+ context: str,
232
+ loc: SourceLocation | None,
233
+ ) -> None:
234
+ """Reject the Phase-20 slicing shape: a borrow of a polymorphic class
235
+ being stored into an owned slot of the same (or strict-subclass-of)
236
+ type. Without this rejection the dynamic type is silently dropped
237
+ when the value is copied into the slot. Recommended fix points at
238
+ `Box[Throwable]` (or `Box[Polymorphic]` for non-exception bases).
239
+
240
+ Allowed shapes (no slicing risk):
241
+ - fresh-rvalue constructor call of the exact destination type
242
+ - `None` assigned to an Optional slot
243
+ - any non-polymorphic destination
244
+ """
245
+ if source_expr is None:
246
+ return
247
+ # Own[T] source carries move semantics -- the rvalue's dynamic type
248
+ # transfers fully to the destination. Skip the borrow-shape check.
249
+ if isinstance(actual, OwnType):
250
+ return
251
+ # Unwrap to find the owned slot's inner concrete type, if any.
252
+ target_inner = expected
253
+ if isinstance(target_inner, OwnType):
254
+ target_inner = target_inner.wrapped
255
+ if isinstance(target_inner, OptionalType):
256
+ target_inner = target_inner.inner
257
+ if not isinstance(target_inner, NominalType):
258
+ return
259
+ if not is_polymorphic_class_type(target_inner, self.ctx.registry):
260
+ return
261
+ # Fresh-rvalue construction (TpyCall with a class-name func). The
262
+ # rvalue's dynamic type matches its static type at the construction
263
+ # site, so codegen's Phase-18/19 subclass-into-Optional machinery
264
+ # can materialize the temp at the concrete class with no slicing.
265
+ # Covers both exact-type construction and concrete-subclass
266
+ # construction of a polymorphic root.
267
+ if isinstance(source_expr, TpyCall) and isinstance(source_expr.func, TpyName):
268
+ if self.ctx.registry.get_record(source_expr.func.name) is not None:
269
+ return
270
+ # None into Optional -- trivially fine.
271
+ if isinstance(source_expr, TpyNoneLiteral):
272
+ return
273
+ # Source is a borrow-shape: name, attribute, method call, function
274
+ # call result. The dynamic type could differ from the static type
275
+ # (caught exception, field of a base-class-typed slot, ...).
276
+ is_borrow_shape = isinstance(
277
+ source_expr,
278
+ (TpyName, TpyFieldAccess, TpyMethodCall, TpyCall),
279
+ )
280
+ if not is_borrow_shape:
281
+ return
282
+ # The actual type must itself be a polymorphic class (or Optional[Poly])
283
+ # for slicing to be possible. Strip qualifiers + Optional + Ref.
284
+ src_inner = actual
285
+ for _ in range(4):
286
+ if isinstance(src_inner, RefType):
287
+ src_inner = src_inner.wrapped
288
+ elif isinstance(src_inner, OwnType):
289
+ src_inner = src_inner.wrapped
290
+ elif isinstance(src_inner, ReadonlyType):
291
+ src_inner = src_inner.wrapped
292
+ elif isinstance(src_inner, OptionalType):
293
+ src_inner = src_inner.inner
294
+ else:
295
+ break
296
+ if not isinstance(src_inner, NominalType):
297
+ return
298
+ if not is_polymorphic_class_type(src_inner, self.ctx.registry):
299
+ return
300
+ raise SemanticError(
301
+ f"cannot store '{src_inner.name}' borrow as owned "
302
+ f"'{target_inner.name}' in {context} -- the dynamic type may "
303
+ f"be a subclass and would be lost (slicing). Use "
304
+ f"`Box[Throwable]` (or `Box[{target_inner.name}]` for "
305
+ f"non-exception roots) for owned polymorphic storage: "
306
+ f"`slot = Box(e.clone())`.",
307
+ loc,
308
+ )
309
+
310
+ def check_type_compatible(
311
+ self, actual: TpyType, expected: TpyType, context: str,
312
+ loc: SourceLocation | None = None,
313
+ source_expr: TpyExpr | None = None,
314
+ is_return: bool = False,
315
+ coercion_ctx: CoercionContext | None = None,
316
+ target_is_storage_form: bool = False,
317
+ ) -> Optional[Coercion]:
318
+ """Check if actual type is compatible with expected type.
319
+
320
+ Raises SemanticError on incompatible types.
321
+ Returns a Coercion if a conversion should be applied at codegen time,
322
+ or None if compatible with no coercion needed.
323
+ """
324
+ # Phase 20 Stage 5a: reject the slicing shape -- storing a borrow
325
+ # of a polymorphic class into an owned slot of the same type would
326
+ # silently drop the dynamic type. Fires before _check_compat so the
327
+ # user sees a targeted "use Box[Throwable]" diagnostic rather than
328
+ # a generic type mismatch.
329
+ self._check_polymorphic_slicing(actual, expected, source_expr, context, loc)
330
+ # check_type_compatible raises on failure, so all callers are
331
+ # commit points -- safe to apply the retro-widen side effect here
332
+ # without leaking it into overload probes (which use _check_compat
333
+ # / is_type_compatible directly).
334
+ if isinstance(source_expr, TpyName):
335
+ new_actual = self.deduction.try_retro_widen_literal_arg(
336
+ source_expr.name, actual, expected,
337
+ getattr(source_expr, "loc", None) or loc,
338
+ )
339
+ if new_actual is not None:
340
+ actual = new_actual
341
+ self.ctx.set_expr_type(source_expr, new_actual)
342
+ else:
343
+ self._maybe_raise_literal_local_range(source_expr, actual, expected, context)
344
+ result = self._check_compat(actual, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
345
+ if isinstance(result, CompatError):
346
+ # Surface where a previously retro-widened local's type got
347
+ # pinned, so the user sees why a non-default type appears in
348
+ # the message even though they wrote `a = 0`.
349
+ prior_loc = (self.ctx.func.retro_widened_locs.get(source_expr.name)
350
+ if isinstance(source_expr, TpyName) else None)
351
+ if prior_loc is not None:
352
+ hint = (f" ('{source_expr.name}' was promoted to '{actual}' by "
353
+ f"earlier use at line {prior_loc.line})")
354
+ raise SemanticError(result.message + hint, result.loc)
355
+ raise SemanticError(result.message, result.loc)
356
+ return result
357
+
358
+ def _resolve_recursive_refs(self, typ: TpyType) -> TpyType:
359
+ """Resolve AliasRef self-references to recursive union aliases.
360
+
361
+ Handles both bare AliasRef("Tree") and AliasRef nested inside
362
+ containers (e.g. list[AliasRef("Tree")] -> list[UnionType(...)]).
363
+ Treats recursive union types as opaque (does not recurse into their
364
+ members) to prevent infinite expansion of self-referencing placeholders.
365
+ """
366
+ if isinstance(typ, AliasRef):
367
+ alias = self.ctx.registry.get_type_alias(typ.name)
368
+ if alias is not None:
369
+ return alias
370
+ # Generic recursive alias instances are opaque, like the non-generic
371
+ # UnionType wrapper below -- their alternatives live behind the
372
+ # wrapper and must not be expanded here.
373
+ if isinstance(typ, RecursiveAliasInstanceType):
374
+ return typ
375
+ if not self.ctx.recursive_union_names:
376
+ return typ
377
+ # Don't recurse into recursive union aliases -- their AliasRef
378
+ # placeholders are structural and must not be expanded.
379
+ if isinstance(typ, UnionType) and typ.needs_wrapper():
380
+ return typ
381
+ inner = typ.inner_types()
382
+ if not inner:
383
+ return typ
384
+ new_inner = tuple(self._resolve_recursive_refs(t) for t in inner)
385
+ if all(new is old for new, old in zip(new_inner, inner)):
386
+ return typ
387
+ return typ.with_inner_types(new_inner)
388
+
389
+ def _check_compat(
390
+ self, actual: TpyType, expected: TpyType, context: str,
391
+ loc: SourceLocation | None = None,
392
+ source_expr: TpyExpr | None = None,
393
+ is_return: bool = False,
394
+ coercion_ctx: CoercionContext | None = None,
395
+ target_is_storage_form: bool = False,
396
+ ) -> CompatResult:
397
+ """Core type compatibility check.
398
+
399
+ Returns Coercion or None on success, CompatError on failure.
400
+
401
+ target_is_storage_form: True when the destination is a field or
402
+ container element (value-storage form). Used to suppress address-
403
+ take mutation marking that only applies to borrow-form destinations
404
+ (params/locals/returns); storage-form assignments copy the value.
405
+ """
406
+ # Resolve NominalType self-references from recursive union members.
407
+ # e.g. NominalType("Tree") -> UnionType, and also inside containers:
408
+ # list[NominalType("Tree")] -> list[UnionType(...)].
409
+ # Uses seen-set guard to prevent infinite recursion.
410
+ actual = self._resolve_recursive_refs(actual)
411
+ expected = self._resolve_recursive_refs(expected)
412
+
413
+ # OwnType from name lookup (implicit owned local) should not
414
+ # shortcircuit the Own[T] coercion path -- that path emits copy
415
+ # warnings when storing at non-last-use.
416
+ if actual == expected:
417
+ if not (isinstance(actual, OwnType) and isinstance(source_expr, TpyName)):
418
+ return None
419
+ # CallableType (Fn and Callable): inner param/return types may carry
420
+ # Own/Ref qualifiers from FI that don't affect callable contract compatibility.
421
+ if (is_callable_type(actual)
422
+ and is_callable_type(expected)
423
+ and len(actual.param_types) == len(expected.param_types)):
424
+ stripped_actual = type(actual)(
425
+ tuple(unwrap_ref_type(unwrap_own(p)) for p in actual.param_types),
426
+ unwrap_ref_type(unwrap_own(actual.return_type)))
427
+ stripped_expected = type(expected)(
428
+ tuple(unwrap_ref_type(unwrap_own(p)) for p in expected.param_types),
429
+ unwrap_ref_type(unwrap_own(expected.return_type)))
430
+ if stripped_actual == stripped_expected:
431
+ return None
432
+ # None literal (NoneType) is compatible with None annotation (VoidType)
433
+ if isinstance(actual, NoneType) and isinstance(expected, VoidType):
434
+ return None
435
+
436
+ # Pending generic instance: try to resolve from the expected type
437
+ if isinstance(actual, PendingGenericInstanceType):
438
+ resolved = self.methods.try_resolve_pending_from_expected_type(
439
+ actual, expected, loc)
440
+ if resolved is not None:
441
+ if source_expr is not None:
442
+ self.ctx.set_expr_type(source_expr, resolved)
443
+ return self._check_compat(
444
+ resolved, expected, context, loc, source_expr,
445
+ is_return, coercion_ctx, target_is_storage_form)
446
+ return CompatError(
447
+ f"Type mismatch in {context}: '{actual.record_name}' has unresolved type "
448
+ f"arguments; call a constraining method first or add explicit type arguments",
449
+ loc,
450
+ )
451
+
452
+ # Ref[T] in expected: strip Ref and check inner type.
453
+ # Ref is a codegen annotation (reference semantics); for type
454
+ # compatibility, T is compatible with Ref[T].
455
+ if isinstance(expected, RefType):
456
+ return self._check_compat(
457
+ unwrap_ref_type(actual), expected.wrapped, context, loc,
458
+ source_expr, is_return, coercion_ctx, target_is_storage_form)
459
+ # Strip Ref from actual too (Ref[T] is compatible with T)
460
+ if isinstance(actual, RefType):
461
+ return self._check_compat(
462
+ actual.wrapped, expected, context, loc,
463
+ source_expr, is_return, coercion_ctx, target_is_storage_form)
464
+
465
+ # readonly[T] -> readonly[T]: unwrap and check inner types
466
+ # T -> readonly[T]: always OK (adding const is safe)
467
+ if isinstance(expected, ReadonlyType):
468
+ actual_inner = unwrap_readonly(actual)
469
+ return self._check_compat(
470
+ actual_inner, expected.wrapped, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form
471
+ )
472
+
473
+ # readonly[T] -> T: error for non-value types (stripping const is unsafe)
474
+ # Exceptions: value types (copies), readonly protocols (Sized, Sequence),
475
+ # return values (C++ const method handles safety via const propagation)
476
+ # Note: mutable Span is NOT excepted even for returns -- you cannot
477
+ # construct a mutable span from a const container.
478
+ if isinstance(actual, ReadonlyType) and not isinstance(expected, ReadonlyType):
479
+ is_mutable_span = is_span(expected) and not is_readonly_span(expected)
480
+ if (not expected.is_value_type() or is_mutable_span) and (not is_return or is_mutable_span):
481
+ allow = False
482
+ if is_protocol_type(expected) and self.protocols:
483
+ proto_info = protocol_info_of(expected)
484
+ if proto_info and self.protocols.is_all_readonly(proto_info):
485
+ allow = True
486
+ if not allow:
487
+ return CompatError(
488
+ f"Cannot return readonly[{actual.wrapped}] as mutable {expected}; "
489
+ f"use Span[readonly[T]] or annotate return type with auto_readonly[T]"
490
+ if is_return else
491
+ f"Cannot pass readonly[{actual.wrapped}] as mutable {expected} in {context}",
492
+ loc,
493
+ )
494
+ return self._check_compat(
495
+ actual.wrapped, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form
496
+ )
497
+
498
+ # None -> Optional[T] / Ptr[T] / Ptr[readonly[T]]: always compatible
499
+ if isinstance(actual, NoneType) and isinstance(expected, (OptionalType, PtrType)):
500
+ return None
501
+
502
+ # T -> Any: any copyable type stores into Any. Move-only sources
503
+ # (Own[T] / @nocopy records / containers thereof) are rejected --
504
+ # v1 supports copyable contents only. The INTO_ANY coercion wraps
505
+ # the source as `tpy::Any{std::any{value}, &any_ops_for<T>}` and
506
+ # upgrades view types (StrView, BytesView) to their owning form.
507
+ if isinstance(expected, AnyType) and not isinstance(actual, AnyType):
508
+ actual_for_check = unwrap_ref_type(actual)
509
+ if self.ctx.is_type_nocopy(actual_for_check):
510
+ return CompatError(
511
+ f"cannot store move-only type '{actual}' in Any "
512
+ f"(v1 supports copyable contents only)",
513
+ loc,
514
+ )
515
+ # Storing a reference type (record/list/dict/set) into Any
516
+ # silently copies (Principle #1: Any owns its contents).
517
+ # That contradicts TPy's reference-type baseline -- elsewhere a
518
+ # copy of a record/container requires explicit copy(). Suppressed
519
+ # for rvalue sources (no other handle), copy()-wrapped sources,
520
+ # and last-use auto-move (matches the dict.update convention).
521
+ inner = unwrap_own(unwrap_readonly(actual_for_check))
522
+ if (source_expr is not None
523
+ and self.is_lvalue(source_expr)
524
+ and not self.is_copy_call(source_expr)
525
+ and not self._is_auto_moved(source_expr)
526
+ and self._warns_copy_into_any(inner)):
527
+ self.ctx.warning(
528
+ f"copies {inner} into Any; use copy() to make this explicit",
529
+ source_expr,
530
+ )
531
+ return INTO_ANY
532
+
533
+ # Any -> T: runtime-checked auto-coerce. Target T must be a concrete
534
+ # type -- Union / Optional / generic-type-param / protocol targets
535
+ # are too ambiguous (which member to extract? what concrete T to
536
+ # any_cast against?). Users narrow first via isinstance or call
537
+ # typing.cast(T, x) explicitly. Codegen emits any_cast_or_panic<T_cpp>;
538
+ # typeid mismatches panic at runtime with the documented message.
539
+ # Protocol targets fall through to the structural-conformance path
540
+ # (Any satisfies Hashable / Equatable / Stringable / Representable
541
+ # at the type-system level; runtime ops may panic if the contained
542
+ # T lacks the capability). Own[Any] / readonly[Any] targets also
543
+ # fall through (Any -> Any is a no-op, not an extraction).
544
+ if isinstance(actual, AnyType):
545
+ expected_inner = expected
546
+ if isinstance(expected_inner, OwnType):
547
+ expected_inner = expected_inner.wrapped
548
+ expected_inner = unwrap_readonly(expected_inner)
549
+ if isinstance(expected_inner, AnyType):
550
+ return None
551
+ if isinstance(expected_inner, (UnionType, OptionalType)):
552
+ return CompatError(
553
+ f"cannot auto-coerce Any to {expected} -- "
554
+ f"narrow first via isinstance(x, T) or typing.cast(T, x)",
555
+ loc,
556
+ )
557
+ if isinstance(expected_inner, TypeParamRef):
558
+ return CompatError(
559
+ f"cannot auto-coerce Any to generic type parameter "
560
+ f"'{expected_inner.name}' -- typing.cast(ConcreteT, x) first",
561
+ loc,
562
+ )
563
+ if not is_protocol_type(expected_inner):
564
+ return FROM_ANY
565
+
566
+ # Union[A, B] -> Union[A, B, C]: each actual member must match some expected member
567
+ if isinstance(actual, UnionType) and isinstance(expected, UnionType):
568
+ for member in actual.members:
569
+ result = self._check_compat(member, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
570
+ if isinstance(result, CompatError):
571
+ return result
572
+ return None
573
+
574
+ # T -> Union[T, ...] (and T -> a generic recursive alias wrapper, whose
575
+ # alternatives stand in for the union members): actual must match at
576
+ # least one member. Iterate twice so a category-crossing widening (e.g.
577
+ # int -> float) never wins when an in-category member exists. Without
578
+ # this, an int going into `int | float` would silently coerce to float
579
+ # when float happens to be earlier in the canonical member order.
580
+ union_members: 'tuple[TpyType, ...] | None' = None
581
+ if isinstance(expected, UnionType):
582
+ union_members = expected.members
583
+ elif isinstance(expected, RecursiveAliasInstanceType):
584
+ union_members = expected.alternatives()
585
+ if union_members is not None:
586
+ actual_unwrapped = unwrap_own(actual)
587
+ a_info = numeric_info(actual_unwrapped)
588
+ for member in union_members:
589
+ if not _is_natural_union_member(actual_unwrapped, a_info, member):
590
+ continue
591
+ result = self._check_compat(actual, member, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
592
+ if not isinstance(result, CompatError):
593
+ return result
594
+ for member in union_members:
595
+ if _is_natural_union_member(actual_unwrapped, a_info, member):
596
+ continue
597
+ result = self._check_compat(actual, member, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
598
+ if not isinstance(result, CompatError):
599
+ return result
600
+ return CompatError(f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
601
+
602
+ # T -> Optional[T]: implicit wrapping
603
+ if isinstance(expected, OptionalType):
604
+ if isinstance(actual, OwnType):
605
+ actual_inner = actual.wrapped
606
+ else:
607
+ actual_inner = actual
608
+ # Own[Optional[T]] -> Optional[T]: exact match after unwrap
609
+ if actual_inner == expected:
610
+ return None
611
+ # Optional[Own[T]] -> Optional[T]: unwrap Own inside Optional
612
+ if isinstance(actual_inner, OptionalType) and isinstance(actual_inner.inner, OwnType):
613
+ actual_inner = OptionalType(actual_inner.inner.wrapped)
614
+ if actual_inner == expected:
615
+ return None
616
+ # For pointer-repr Optional[T], codegen takes &(source) when source is a
617
+ # non-value record (not already Optional/Ptr). Mark source params as needing
618
+ # T& so &(param) stays valid (not const T&). Skip when the destination
619
+ # is in storage form (field / container element) -- the Optional there
620
+ # lowers to std::optional<T> (value-storage) and the assignment is a
621
+ # copy; no address-take happens, so the source's const-ness is fine.
622
+ if (expected.uses_pointer_repr()
623
+ and source_expr is not None
624
+ and not actual_inner.is_value_type()
625
+ and not isinstance(actual_inner, (OptionalType, PtrType, NoneType))
626
+ and not target_is_storage_form):
627
+ self._mark_addr_taken(source_expr)
628
+ # Ptr[T] -> T | None (pointer-repr): byte-identical T* at C++ level.
629
+ # Per-context idiom: Ptr[T] for storage, T | None for nullable returns.
630
+ if (isinstance(actual_inner, PtrType)
631
+ and expected.uses_pointer_repr()
632
+ and _inner_compat_with_ro_widening(actual_inner.pointee, expected.inner)):
633
+ return None
634
+ # Optional[A] -> Optional[B] via a whole-Optional coercion rule
635
+ # (e.g. str <-> StrView at arg position, where both lower to
636
+ # `std::optional<std::string_view>`). Try before stripping to the
637
+ # inner, since the inner check can't see that the wrappers share
638
+ # a C++ representation.
639
+ if isinstance(actual_inner, OptionalType):
640
+ ctx_for_coerce = coercion_ctx or context
641
+ if isinstance(ctx_for_coerce, CoercionContext):
642
+ whole = resolve_coercion(actual_inner, expected, ctx_for_coerce)
643
+ if whole is not None:
644
+ return whole
645
+ result = self._check_compat(actual_inner, expected.inner, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
646
+ # Rewrap inner-mismatch errors with the declared Optional types so
647
+ # the diagnostic reads `expected str | None, got StrView | None`
648
+ # rather than the truncated `expected str, got StrView | None`.
649
+ if isinstance(result, CompatError) and isinstance(actual, OptionalType):
650
+ return CompatError(
651
+ f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
652
+ return result
653
+
654
+ # Optional[T] -> Optional[T] already handled by == check above
655
+ # Optional[T] -> T: error (cannot implicitly unwrap)
656
+
657
+ # T | None (pointer-repr) -> Ptr[T]: byte-identical T* at C++ level.
658
+ # Per-context idiom: T | None for nullable returns, Ptr[T] for storage.
659
+ # Storage-form Optional sources (field access, container element) need
660
+ # an `optional_to_ptr` lift at codegen, mirroring the existing
661
+ # storage-form-to-borrow-form lift for OptionalType destinations.
662
+ if isinstance(expected, PtrType):
663
+ actual_for_opt = unwrap_own(actual)
664
+ if (isinstance(actual_for_opt, OptionalType)
665
+ and actual_for_opt.uses_pointer_repr()
666
+ and _inner_compat_with_ro_widening(actual_for_opt.inner, expected.pointee)):
667
+ return None
668
+
669
+ # NominalType with Own[T] in type args signals copy semantics -- applies to both
670
+ # protocols (Iterable[Own[T]]) and concrete containers (dict[K, Own[V]]).
671
+ # Strip Own for conformance/equality check; warn when elements are implicitly copied.
672
+ if (isinstance(expected, NominalType)
673
+ and expected.inner_types()
674
+ and any(isinstance(a, OwnType) for a in expected.inner_types())):
675
+ stripped_inner = tuple(a.wrapped if isinstance(a, OwnType) else a for a in expected.inner_types())
676
+ stripped_expected = expected.with_inner_types(stripped_inner)
677
+ # Own[T] actual means caller acknowledged ownership transfer -- no warning.
678
+ actual_inner = actual.wrapped if isinstance(actual, OwnType) else actual
679
+ if is_protocol_type(expected):
680
+ structurally_ok = bool(
681
+ self.protocols and self.protocols.type_conforms_to_protocol(actual_inner, stripped_expected)
682
+ )
683
+ else:
684
+ # Concrete type: exact match after Own stripping
685
+ structurally_ok = (actual_inner == stripped_expected)
686
+ if structurally_ok:
687
+ # Own[T] from explicit return/param means caller acknowledged
688
+ # ownership -- skip warning. Own[T] from name lookup (owned
689
+ # local) is implicit and still needs the warning.
690
+ explicit_own = (isinstance(actual, OwnType)
691
+ and not isinstance(source_expr, TpyName))
692
+ if (source_expr is not None
693
+ and not explicit_own
694
+ and not self.is_copy_call(source_expr)):
695
+ is_lvalue_src = self.is_lvalue(source_expr)
696
+ # Suppress at last use: no observable semantic divergence from
697
+ # CPython when the source is dead after this point (Framing A).
698
+ is_auto_moved = self._is_auto_moved(source_expr)
699
+ if not is_auto_moved:
700
+ for inner_t in expected.inner_types():
701
+ if not isinstance(inner_t, OwnType):
702
+ continue
703
+ elem_type = inner_t.wrapped
704
+ # Rvalue iterators that yield Ref elements (e.g.
705
+ # map(identity, pts)) copy on materialization.
706
+ # RefType in the Own-wrapped element is the proof.
707
+ has_ref_elements = _contains_semantic_ref(elem_type)
708
+ if is_lvalue_src or has_ref_elements:
709
+ if not _contains_ref_type(elem_type):
710
+ continue
711
+ display_type = unwrap_ref_type(elem_type)
712
+ # Lvalue source (container): suggest copy_iter or copy.
713
+ # Rvalue with Ref elements (iterator): only copy_iter.
714
+ if is_lvalue_src:
715
+ hint = "use copy_iter() to make this explicit (or copy() to copy the entire container)"
716
+ else:
717
+ hint = "use copy_iter() to make this explicit"
718
+ if _is_definitely_ref_type(elem_type):
719
+ self.ctx.warning(
720
+ f"copies {display_type} elements; {hint}",
721
+ source_expr,
722
+ )
723
+ else:
724
+ self.ctx.warning(
725
+ f"may copy {display_type} elements if not a value type; {hint}",
726
+ source_expr,
727
+ )
728
+ return None
729
+ if is_protocol_type(expected):
730
+ return CompatError(
731
+ f"Type {actual_inner} does not conform to protocol {stripped_expected} in {context}",
732
+ source_expr.loc if source_expr is not None else loc
733
+ )
734
+ # Concrete type mismatch: fall through to generic error handling
735
+
736
+ # Protocol matching (structural subtyping)
737
+ if is_protocol_type(expected):
738
+ # Own[T] conforms to any protocol that T conforms to
739
+ check_actual = actual.wrapped if isinstance(actual, OwnType) else actual
740
+ if self.protocols and self.protocols.type_conforms_to_protocol(check_actual, expected):
741
+ return None # No coercion needed, structural match
742
+ return CompatError(
743
+ f"Type {check_actual} does not conform to protocol {expected} in {context}",
744
+ loc
745
+ )
746
+
747
+ # Callable object -> Fn/Callable: record with __call__ matching the signature
748
+ if isinstance(actual, NominalType) and is_callable_type(expected):
749
+ record = self.ctx.registry.get_record_for_type(actual)
750
+ if record:
751
+ overloads = self.ctx.registry.get_method_overloads_with_parents(record, "__call__")
752
+ if overloads:
753
+ assert len(overloads) == 1, f"multiple __call__ overloads not supported"
754
+ fi = overloads[0]
755
+ if (len(fi.params) == len(expected.param_types)
756
+ and all(p.type == e for (p, e) in zip(fi.params, expected.param_types))
757
+ and (fi.return_type == expected.return_type
758
+ or isinstance(expected.return_type, VoidType))):
759
+ return None
760
+ return CompatError(
761
+ f"'__call__' signature ({', '.join(str(p.type) for p in fi.params)}) -> {fi.return_type} "
762
+ f"does not match {expected} in {context}",
763
+ loc,
764
+ )
765
+
766
+ # Callable -> Fn: std::function satisfies template requires clauses in C++
767
+ if isinstance(actual, CallableType) and is_fn_type(expected):
768
+ if (len(actual.param_types) == len(expected.param_types)
769
+ and all(self._check_compat(a, e, "param", loc) is None
770
+ for a, e in zip(actual.param_types, expected.param_types))
771
+ and (self._check_compat(actual.return_type, expected.return_type, "return", loc) is None
772
+ or isinstance(expected.return_type, VoidType))):
773
+ return None
774
+
775
+ # Inheritance: Child -> Parent (implicit value upcast, C++ handles slicing/ref binding)
776
+ if (isinstance(actual, NominalType) and actual.is_user_record
777
+ and isinstance(expected, NominalType) and expected.is_user_record):
778
+ if self.ctx.registry.is_subclass_of(actual, expected):
779
+ if coercion_ctx in (CoercionContext.ASSIGN,
780
+ CoercionContext.INIT,
781
+ CoercionContext.RETURN):
782
+ msg = (f"upcast narrows '{actual}' to '{expected}' -- "
783
+ f"only '{expected}' fields and methods will be accessible; "
784
+ f"keep the concrete type or make '{expected}' a @dynamic protocol")
785
+ if source_expr is not None:
786
+ self.ctx.warning(msg, source_expr)
787
+ else:
788
+ self.ctx.warning_from_loc(msg, loc)
789
+ return None
790
+
791
+ # Inheritance: Ptr[Child] -> Ptr[Parent] / Ptr[readonly[Parent]]
792
+ # Ptr[readonly[Child]] -> Ptr[readonly[Parent]]
793
+ # Covers both class-to-class inheritance and `@dynamic` protocol
794
+ # implementation; C++ upcasts the pointer implicitly at the call
795
+ # site via virtual inheritance.
796
+ if isinstance(actual, PtrType) and isinstance(actual.inner_pointee, NominalType):
797
+ if isinstance(expected, PtrType) and isinstance(expected.inner_pointee, NominalType):
798
+ if _ptr_readonly_compatible(actual, expected):
799
+ if self._is_covariant_target(actual.inner_pointee, expected.inner_pointee):
800
+ return None
801
+
802
+ # Ptr[U] -> Ptr[B] where U is a type param whose subtype bound reaches
803
+ # B (`def f[U: B]`). Borrow-form pointer upcast only; the C++ upcast is
804
+ # only valid when the concrete U is a real subtype of B, which the call
805
+ # site enforces (see methods/calls bound validation).
806
+ if (isinstance(actual, PtrType) and isinstance(expected, PtrType)
807
+ and isinstance(actual.inner_pointee, TypeParamRef)
808
+ and _ptr_readonly_compatible(actual, expected)
809
+ and self._is_representational_subtype(
810
+ actual.inner_pointee, expected.inner_pointee)):
811
+ return None
812
+
813
+ # Covariant generic coercion: Box[Child] -> Box[Parent]
814
+ # when Box extends Covariant[T] and Child conforms to @dynamic Parent.
815
+ # C++ converting move ctor handles the actual conversion.
816
+ if self.is_covariant_generic_upcast(actual, expected):
817
+ return None
818
+
819
+ # Union-member coercion through generic containers:
820
+ # Container[Lit] -> Container[Expr] when Expr is a recursive union alias
821
+ # and Lit is a valid member. Works for any generic container.
822
+ # The C++ Expr wrapper struct's template constructor handles implicit
823
+ # conversion from Lit to Expr, so Box<Expr>(Lit{...}) compiles.
824
+ if (isinstance(actual, NominalType) and isinstance(expected, NominalType)
825
+ and actual.name == expected.name
826
+ and actual.type_args and expected.type_args
827
+ and len(actual.type_args) == len(expected.type_args)
828
+ and actual.type_args != expected.type_args):
829
+ if self._check_union_member_type_args(
830
+ actual, expected, context, loc, source_expr, is_return, coercion_ctx
831
+ ):
832
+ return None
833
+
834
+ # Allow T -> Own[T] coercion (ownership transfer)
835
+ if isinstance(expected, OwnType):
836
+ # Warn when lvalue is implicitly copied into owned storage
837
+ # (returns are handled separately as errors in statements.py)
838
+ # Skip warning when auto-move applies (last use of an owned variable).
839
+ # Only locals and Own[T] params are movable -- regular params are borrowed.
840
+ is_auto_moved = self._is_auto_moved(source_expr)
841
+ self.check_own_consumption(source_expr)
842
+ # Mark loop variables as consumed for auto-consuming heuristic
843
+ if isinstance(source_expr, TpyName):
844
+ self.ctx.mark_loop_var_consumed(source_expr.name)
845
+ if (not is_return and source_expr is not None
846
+ and not expected.wrapped.is_value_type()
847
+ and not self._is_value_type_param(expected.wrapped)
848
+ and self.is_lvalue(source_expr)
849
+ and not self.is_copy_call(source_expr)
850
+ and not is_auto_moved):
851
+ value_type = self.ctx.get_expr_type(source_expr)
852
+ if self.ctx.is_type_non_copyable(expected.wrapped):
853
+ verb = "may copy" if isinstance(expected.wrapped, TypeParamRef) else "cannot copy"
854
+ if value_type == expected.wrapped:
855
+ msg = (f"{verb} non-copyable type '{expected.wrapped}' "
856
+ f"into owned storage{NOCOPY_REMEDIATION_HINT}")
857
+ else:
858
+ msg = (f"{verb} {value_type} into owned storage of type "
859
+ f"'{expected.wrapped}'; target is non-copyable{NOCOPY_REMEDIATION_HINT}")
860
+ raise self.ctx.error(msg, source_expr)
861
+ if isinstance(expected.wrapped, TypeParamRef):
862
+ self.ctx.warning(
863
+ f"may copy {value_type} into owned storage if not a value type; use copy() to make this explicit",
864
+ source_expr
865
+ )
866
+ else:
867
+ self.ctx.warning(
868
+ f"copies {value_type} into owned storage; use copy() to make this explicit",
869
+ source_expr
870
+ )
871
+ # For loop variables at last use, the consuming decision is
872
+ # made post-body. Record the diagnostic index so it can be
873
+ # suppressed if the loop activates consuming iteration
874
+ # (elements at last use will be moved, not copied).
875
+ if (isinstance(source_expr, TpyName)
876
+ and source_expr.name in self.ctx.func.loop_vars
877
+ and id(source_expr) in self.ctx.all_last_uses):
878
+ diag_idx = len(self.ctx.diagnostics) - 1
879
+ self.ctx.func.deferred_loop_copy_warnings.setdefault(
880
+ source_expr.name, []).append(diag_idx)
881
+ # Subclass coercion excluded: Child -> Own[Base] stores Child by value as
882
+ # Base, silently slicing the object. Same invariance as container elements.
883
+ # Only applies when record names differ (different types, not parametric covariance).
884
+ if (isinstance(actual, NominalType) and actual.is_user_record
885
+ and isinstance(expected.wrapped, NominalType) and expected.wrapped.is_user_record
886
+ and actual.name != expected.wrapped.name):
887
+ return CompatError(
888
+ f"Type mismatch in {context}: expected {expected.wrapped}, got {actual}", loc)
889
+ return self._check_compat(actual, expected.wrapped, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
890
+
891
+ # Allow Own[T] -> T coercion (receiving an owned value)
892
+ if isinstance(actual, OwnType):
893
+ return self._check_compat(actual.wrapped, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
894
+
895
+ # Tuple-to-tuple: same length, element-wise compatible
896
+ if isinstance(actual, TupleType) and isinstance(expected, TupleType):
897
+ if len(actual.element_types) != len(expected.element_types):
898
+ return CompatError(
899
+ f"Type mismatch in {context}: expected {expected}, got {actual} "
900
+ f"(different tuple lengths)", loc
901
+ )
902
+ for i, (a, e) in enumerate(zip(actual.element_types, expected.element_types)):
903
+ result = self._check_compat(
904
+ a, e, f"{context} (tuple element {i})", loc,
905
+ source_expr=source_expr, is_return=is_return,
906
+ coercion_ctx=coercion_ctx, target_is_storage_form=target_is_storage_form
907
+ )
908
+ if isinstance(result, CompatError):
909
+ return result
910
+ return None
911
+
912
+ # IntLiteral can coerce to BigInt or stay unresolved
913
+ if isinstance(actual, IntLiteralType):
914
+ if is_big_int_type(expected) or isinstance(expected, IntLiteralType):
915
+ return None
916
+
917
+ # FloatLiteral can coerce to float/Float32 or stay unresolved
918
+ if isinstance(actual, FloatLiteralType):
919
+ if is_any_float_type(expected):
920
+ return None
921
+
922
+ # Allow Array element type coercion if sizes match
923
+ if is_array(actual) and is_array(expected):
924
+ if actual.type_args[1] == expected.type_args[1]:
925
+ if isinstance(actual.type_args[0], IntLiteralType) and is_integer_type(expected.type_args[0]):
926
+ return None
927
+
928
+ # Allow list element type coercion
929
+ if is_list(actual) and is_list(expected):
930
+ if isinstance(actual.type_args[0], IntLiteralType) and is_integer_type(expected.type_args[0]):
931
+ return None
932
+
933
+ # Allow list -> Array only for literal expressions
934
+ # (global array literals and list repeats become list[T] but can be assigned to Array variables)
935
+ # List *variables* cannot be coerced to Array - codegen can't handle std::vector -> std::array
936
+ if is_list(actual) and is_array(expected):
937
+ a_elem = actual.type_args[0]
938
+ e_elem, e_size = expected.type_args[0], expected.type_args[1]
939
+ if isinstance(source_expr, (TpyArrayLiteral, TpyListRepeat)):
940
+ # Validate size for list repeats with known count
941
+ if isinstance(source_expr, TpyListRepeat) and isinstance(source_expr.count, TpyIntLiteral):
942
+ repeat_size = len(source_expr.elements) * source_expr.count.value
943
+ if repeat_size != e_size:
944
+ return CompatError(
945
+ f"List repeat produces {repeat_size} elements but Array[..., {e_size}] expects {e_size}",
946
+ loc
947
+ )
948
+ if a_elem == e_elem:
949
+ return None
950
+ if isinstance(a_elem, IntLiteralType) and is_integer_type(e_elem):
951
+ return None
952
+
953
+ # Allow PendingListType compatibility during first phase (before resolution)
954
+ if isinstance(actual, PendingListType):
955
+ # Compatible with list[T] if element types are compatible
956
+ if is_list(expected):
957
+ e_elem = expected.type_args[0]
958
+ if actual.element_type == e_elem:
959
+ return None
960
+ if isinstance(actual.element_type, IntLiteralType) and is_integer_type(e_elem):
961
+ return None
962
+ # Element type widening (e.g. Int32 -> Int32|None, Int32 -> Int64).
963
+ # Subclass coercion excluded: storing Child in list[Base] silently
964
+ # slices objects (same invariance as dict/set).
965
+ both_records = (
966
+ isinstance(actual.element_type, NominalType) and actual.element_type.is_user_record
967
+ and isinstance(e_elem, NominalType) and e_elem.is_user_record
968
+ )
969
+ if both_records:
970
+ # Explicit error: avoid leaking PendingList internal repr in the generic message.
971
+ return CompatError(
972
+ f"Type mismatch in {context}: expected {e_elem}, got {actual.element_type}", loc)
973
+ else:
974
+ # Element type widening (e.g. Int32 -> Int32|None, Int32 -> Int64).
975
+ # Container element slot is storage form -- no address-take
976
+ # mark should fire even if the inner type is pointer-repr Optional.
977
+ result = self._check_compat(
978
+ actual.element_type, e_elem,
979
+ context, loc, source_expr, is_return, coercion_ctx,
980
+ target_is_storage_form=True,
981
+ )
982
+ if not isinstance(result, CompatError):
983
+ return None # element coercion is a probe, not propagated
984
+ # Compatible with Array[T, N] if element types and sizes match
985
+ if is_array(expected):
986
+ if self.type_ops and self.type_ops.pending_list_matches_array(actual, expected):
987
+ return None
988
+ # Specific error for list repeat size mismatch
989
+ e_size = expected.type_args[1]
990
+ if (isinstance(source_expr, TpyListRepeat)
991
+ and actual.size >= 0 and actual.size != e_size):
992
+ return CompatError(
993
+ f"List repeat produces {actual.size} elements but "
994
+ f"Array[..., {e_size}] expects {e_size}",
995
+ loc,
996
+ )
997
+ # Inline repeat cannot be passed directly to Span -- assign to a variable first
998
+ if is_span(expected) and isinstance(source_expr, TpyListRepeat):
999
+ return CompatError(
1000
+ f"Cannot pass list repeat directly to {expected}: "
1001
+ f"assign to a variable first",
1002
+ loc,
1003
+ )
1004
+
1005
+ # ListRepeatType materializes to list[T] or Array[T, N]
1006
+ if isinstance(actual, ListRepeatType):
1007
+ if is_list(expected):
1008
+ e_elem = expected.type_args[0]
1009
+ if actual.element_type == e_elem:
1010
+ return None
1011
+ if isinstance(actual.element_type, IntLiteralType) and is_integer_type(e_elem):
1012
+ return None
1013
+ if is_array(expected):
1014
+ if actual.element_type == expected.type_args[0]:
1015
+ return None
1016
+ if isinstance(actual.element_type, IntLiteralType) and is_integer_type(expected.type_args[0]):
1017
+ return None
1018
+
1019
+ # Allow PendingDictType compatibility during first phase (before resolution)
1020
+ if isinstance(actual, PendingDictType) and is_dict(expected):
1021
+ e_k, e_v = expected.type_args[0], expected.type_args[1]
1022
+ key_ok = isinstance(actual.key_type, UnknownElementType) or _container_elem_matches(actual.key_type, e_k)
1023
+ val_ok = isinstance(actual.value_type, UnknownElementType) or _container_elem_matches(actual.value_type, e_v)
1024
+ if key_ok and val_ok:
1025
+ return None
1026
+
1027
+ # Allow PendingSetType compatibility during first phase (before resolution)
1028
+ if isinstance(actual, PendingSetType) and is_set(expected):
1029
+ if isinstance(actual.element_type, UnknownElementType) or _container_elem_matches(actual.element_type, expected.type_args[0]):
1030
+ return None
1031
+
1032
+ # DictType compatibility: key and value types must match exactly.
1033
+ # Own[V] stripping is handled by _container_elem_matches.
1034
+ # Subclass coercion is intentionally excluded: dict values are stored by value,
1035
+ # and tpy::dict_update/tpy::dict_ctor are non-converting templates (invariant V).
1036
+ if is_dict(actual) and is_dict(expected):
1037
+ a_k, a_v = actual.type_args[0], actual.type_args[1]
1038
+ e_k, e_v = expected.type_args[0], expected.type_args[1]
1039
+ key_ok = _container_elem_matches(a_k, e_k)
1040
+ val_ok = _container_elem_matches(a_v, e_v)
1041
+ if key_ok and val_ok:
1042
+ return None
1043
+ # Raise a specific error pointing at the mismatching element type.
1044
+ # Strip Own[V] from the message to avoid leaking implementation details.
1045
+ check_val = e_v.wrapped if isinstance(e_v, OwnType) else e_v
1046
+ if not key_ok:
1047
+ return CompatError(
1048
+ f"Type mismatch in {context}: expected {e_k}, got {a_k}", loc)
1049
+ return CompatError(
1050
+ f"Type mismatch in {context}: expected {check_val}, got {a_v}", loc)
1051
+
1052
+ # SetType compatibility: element types must match exactly.
1053
+ # Subclass coercion is intentionally excluded: tpy::ordered_set<T> is a
1054
+ # non-converting template (invariant T).
1055
+ if is_set(actual) and is_set(expected):
1056
+ a_elem, e_elem = actual.type_args[0], expected.type_args[0]
1057
+ if _container_elem_matches(a_elem, e_elem):
1058
+ return None
1059
+ check_elem = e_elem.wrapped if isinstance(e_elem, OwnType) else e_elem
1060
+ return CompatError(
1061
+ f"Type mismatch in {context}: expected {check_elem}, got {a_elem}", loc)
1062
+
1063
+ # Allow PendingStrType compatibility during first phase (before resolution)
1064
+ if isinstance(actual, PendingStrType):
1065
+ if is_any_str_type(expected):
1066
+ return None
1067
+ if isinstance(expected, LiteralType) and expected.is_str_base():
1068
+ return None
1069
+ if isinstance(expected, PendingStrType):
1070
+ if is_str_category(actual):
1071
+ return None
1072
+ # LiteralType is compatible with its base type family
1073
+ if isinstance(actual, LiteralType) and isinstance(expected, LiteralType):
1074
+ if all(v in expected.values for v in actual.values):
1075
+ return None
1076
+ # Reject explicitly: skipping the permissive str/int branches below
1077
+ # is what enforces value-set membership.
1078
+ return CompatError(
1079
+ f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
1080
+ if isinstance(expected, LiteralType):
1081
+ src_lit = _literal_value_from_source(source_expr, expected)
1082
+ if src_lit is not None:
1083
+ if src_lit in expected.values:
1084
+ return None
1085
+ return CompatError(
1086
+ f"Type mismatch in {context}: expected {expected}, "
1087
+ f"got {src_lit}", loc)
1088
+ if isinstance(actual, IntLiteralType) and actual.value is not None and expected.is_int_base():
1089
+ if expected.contains(LiteralTag.INT, actual.value):
1090
+ return None
1091
+ return CompatError(
1092
+ f"Type mismatch in {context}: expected {expected}, "
1093
+ f"got {actual.value}", loc)
1094
+ # Non-literal source flowing into a Literal[...] LHS is rejected:
1095
+ # sema cannot prove the runtime value is in the declared set, and
1096
+ # the LHS's narrower type drives downstream Literal-specialized
1097
+ # dispatch (which would run the wrong arm on an out-of-set value).
1098
+ if isinstance(actual, LiteralType):
1099
+ if actual.is_str_base() and is_str_category(expected):
1100
+ return None
1101
+ if actual.base_type == expected:
1102
+ return None
1103
+ if actual.is_int_base() and is_integer_type(expected):
1104
+ return None
1105
+ # Allow PendingBytesType compatibility during first phase (before resolution)
1106
+ if isinstance(actual, PendingBytesType):
1107
+ if is_bytes_category(expected) or isinstance(expected, PendingBytesType):
1108
+ return None
1109
+ if isinstance(expected, PendingBytesType):
1110
+ if is_bytes_category(actual):
1111
+ return None
1112
+
1113
+ ctx = coercion_ctx or context
1114
+ coercion = resolve_coercion(actual, expected, ctx)
1115
+ if coercion is None:
1116
+ # Inheritance: Child -> Ptr[Parent] / Ptr[readonly[Parent]] (address-of with upcast).
1117
+ # `_is_covariant_target` covers both class inheritance (Child -> Ptr[ParentRecord])
1118
+ # and @dynamic-protocol implementation (Child -> Ptr[@dynamic Protocol]).
1119
+ #
1120
+ # Also accepts `actual` being a @dynamic protocol value (lowered as
1121
+ # `Base&` already): `&handle` produces `Base*` natively, matching
1122
+ # `Ptr[same protocol]`. Identity is the common case (`P -> Ptr[P]`
1123
+ # inside a function that took the protocol value as a param);
1124
+ # protocol-to-parent-protocol upcasts also work via the existing
1125
+ # covariance check.
1126
+ actual_is_addr_taker = (
1127
+ isinstance(actual, NominalType)
1128
+ and (actual.is_user_record or is_dyn_protocol(actual))
1129
+ )
1130
+ if actual_is_addr_taker:
1131
+ # Identity shortcut (actual == inner_pointee) only fires for
1132
+ # @dynamic protocol values: their `Base&`-style lowering needs
1133
+ # the value->Ptr path without going through resolve_coercion
1134
+ # (no `protocol_to_ptr` rule exists). User-record identity is
1135
+ # handled by the named `record_to_ptr` rule in resolve_coercion;
1136
+ # gating the shortcut keeps the coercion AST name stable so
1137
+ # downstream code can rely on it.
1138
+ actual_is_dyn = is_dyn_protocol(actual)
1139
+ if isinstance(expected, PtrType) and not expected.is_readonly and isinstance(expected.inner_pointee, NominalType):
1140
+ if ((actual_is_dyn and actual == expected.inner_pointee)
1141
+ or self._is_covariant_target(actual, expected.inner_pointee)):
1142
+ coercion = UPCAST_TO_PTR
1143
+ elif is_readonly_ptr(expected) and isinstance(expected.inner_pointee, NominalType):
1144
+ if ((actual_is_dyn and actual == expected.inner_pointee)
1145
+ or self._is_covariant_target(actual, expected.inner_pointee)):
1146
+ coercion = UPCAST_TO_CONST_PTR
1147
+ if coercion is None:
1148
+ # __span__() method coercion: type with __span__() -> Span[T] coerces to Span/Span[readonly[T]]
1149
+ if isinstance(actual, NominalType) and is_span(expected):
1150
+ span_ret = get_span_return_type(actual, registry=self.ctx.registry)
1151
+ if span_ret is not None and unwrap_readonly(span_ret.type_args[0]) == unwrap_readonly(expected.type_args[0]):
1152
+ # Span[readonly[T]] cannot coerce to mutable Span
1153
+ if not is_readonly_span(span_ret) or is_readonly_span(expected):
1154
+ if ctx == CoercionContext.ARG:
1155
+ coercion = SPAN_METHOD_TO_SPAN_ARG
1156
+ else:
1157
+ coercion = SPAN_METHOD_TO_SPAN
1158
+ if coercion is None:
1159
+ # Spannable[T] protocol -> Span[readonly[T]] coercion via __span__()
1160
+ if (is_protocol_type(actual) and actual.qualified_name() == "tpy.Spannable"
1161
+ and actual.type_args and is_span(expected)
1162
+ and is_readonly_span(expected) and actual.type_args[0] == unwrap_readonly(expected.type_args[0])):
1163
+ if ctx == CoercionContext.ARG:
1164
+ coercion = SPAN_METHOD_TO_SPAN_ARG
1165
+ else:
1166
+ coercion = SPAN_METHOD_TO_SPAN
1167
+ if coercion is None:
1168
+ # Generic deref coercion: any type with __deref__() -> T coerces to T
1169
+ if self.type_ops:
1170
+ deref_target = self.type_ops.get_deref_coercion_target(actual)
1171
+ if deref_target is None and is_readonly_ptr(actual):
1172
+ # Ptr[readonly[T]] -> T via deref is safe for returns and
1173
+ # assignments (the value is copied); reject only in ARG
1174
+ # context where the callee may need a mutable reference.
1175
+ if ctx != CoercionContext.ARG:
1176
+ deref_target = self.type_ops.get_deref_target_type(actual)
1177
+ if deref_target is not None and unwrap_readonly(deref_target) == expected:
1178
+ coercion = DEREF_COERCION
1179
+ if coercion is None:
1180
+ return CompatError(f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
1181
+
1182
+ if isinstance(actual, PendingListType) and is_span(expected):
1183
+ info = self.ctx.list_literals.get(actual.literal_id)
1184
+ if info:
1185
+ info.coerced_element_type = expected.type_args[0]
1186
+ info.passed_to_span_param = True
1187
+
1188
+ if coercion.check_range and not coercion.check_range(actual, expected):
1189
+ tr = int_traits_of(expected)
1190
+ return CompatError(
1191
+ f"Integer literal {actual.value} is outside {expected} range "
1192
+ f"[{tr.min_value}, {tr.max_value}] in {context}",
1193
+ loc
1194
+ )
1195
+
1196
+ if coercion.requires_mutable_lvalue:
1197
+ if source_expr is None or not self.is_mutable_lvalue(source_expr):
1198
+ return CompatError(
1199
+ f"Cannot take mutable pointer to read-only or temporary value in {context}; "
1200
+ f"use a read-only pointer for read-only access, or assign to a variable first",
1201
+ loc
1202
+ )
1203
+ # Address-taking coercion (record -> Ptr[Record]) requires T& binding.
1204
+ # Track so that codegen can't safely emit const T& for this param.
1205
+ if isinstance(source_expr, TpyName):
1206
+ root = self.ctx.func.borrow_tracker.effective_storage(source_expr.name)
1207
+ self.ctx.mark_param_mutated(root)
1208
+ elif coercion.requires_lvalue:
1209
+ if source_expr is None or not self.is_lvalue(source_expr):
1210
+ return CompatError(
1211
+ f"Cannot take address of a temporary or expression in {context}; "
1212
+ f"assign to a variable first",
1213
+ loc
1214
+ )
1215
+ # Mutable Span from a lvalue container (e.g. Array -> Span[T])
1216
+ # requires non-const source; codegen calls as_mut_span().
1217
+ # Mark source param as T&. Unlike the pointer-repr Optional
1218
+ # storage-form gate above, this mark applies even for
1219
+ # storage-form Span destinations -- the assignment still
1220
+ # emits as_mut_span(source), which can't bind to a const
1221
+ # source.
1222
+ if (is_span(expected) and not is_readonly_span(expected)
1223
+ and source_expr is not None):
1224
+ self._mark_addr_taken(source_expr)
1225
+
1226
+ if coercion.forbid_return_local and is_return:
1227
+ if source_expr is not None and self.is_dangling_return(source_expr):
1228
+ return CompatError(
1229
+ f"Cannot return local or temporary value; "
1230
+ f"the returned pointer/reference would dangle",
1231
+ loc
1232
+ )
1233
+
1234
+ return coercion
1235
+
1236
+ def coerce_expr(
1237
+ self, expr: TpyExpr, actual: TpyType, expected: TpyType, context: str,
1238
+ coercion_ctx: CoercionContext, is_return: bool = False,
1239
+ target_is_storage_form: bool = False,
1240
+ ) -> TpyExpr:
1241
+ """Wrap expr in a coercion node if a conversion is needed.
1242
+
1243
+ target_is_storage_form: see _check_compat docstring. Set by callers
1244
+ that know the destination is a field / container element so the
1245
+ pointer-repr-Optional address-take mark is suppressed.
1246
+ """
1247
+ coercion = self.check_type_compatible(
1248
+ actual, expected, context,
1249
+ getattr(expr, "loc", None),
1250
+ source_expr=expr,
1251
+ is_return=is_return,
1252
+ coercion_ctx=coercion_ctx,
1253
+ target_is_storage_form=target_is_storage_form,
1254
+ )
1255
+ if coercion is None:
1256
+ return expr
1257
+ # Value-to-mutable-Ptr coercions (`record_to_ptr`, `upcast_to_ptr`)
1258
+ # emit `&expr` and require the source storage to stay mutable -- same
1259
+ # propagation the explicit `take_ptr(x)` path gets via the
1260
+ # `value_ptr_coercion` handler in calls.py and the vararg pack handler.
1261
+ if coercion.name in ("record_to_ptr", "upcast_to_ptr"):
1262
+ self._mark_addr_taken(expr)
1263
+ runtime_bigint = False
1264
+ if coercion.name == "int_literal_to_fixed_int":
1265
+ runtime_bigint = self.is_runtime_bigint_expr(expr)
1266
+ # INTO_ANY needs a concrete actual_type for codegen (the storage
1267
+ # typeid). PendingListType / PendingDictType / PendingSetType
1268
+ # would normally resolve later via coerced_element_type, but that
1269
+ # signal is set by typed targets -- Any provides no element
1270
+ # constraint. Default-resolve here so the cell's typeid is stable.
1271
+ if coercion.name == "into_any":
1272
+ actual = self._resolve_pending_for_any_storage(actual)
1273
+ coerced = TpyCoerce(
1274
+ expr=expr,
1275
+ actual_type=actual,
1276
+ expected_type=expected,
1277
+ coercion=coercion,
1278
+ context_kind=coercion_ctx,
1279
+ context_msg=context,
1280
+ runtime_bigint=runtime_bigint,
1281
+ loc=expr.loc
1282
+ )
1283
+ self.ctx.set_expr_type(coerced, expected)
1284
+ return coerced
1285
+
1286
+ def _maybe_raise_literal_local_range(
1287
+ self, expr: TpyName, actual: TpyType, expected: TpyType, context: str,
1288
+ ) -> None:
1289
+ """Raise a range error when a literal-seeded local would have
1290
+ retro-widened to `expected` except a recorded literal value falls
1291
+ outside the target's range. Surfaces the literal value (e.g. -1,
1292
+ or 300 against UInt8) instead of the bare "got Int32" mismatch.
1293
+ """
1294
+ cand = self.deduction.literal_retro_candidate(expr.name, actual, expected)
1295
+ if cand is None:
1296
+ return
1297
+ target, values = cand
1298
+ bad = [v for v in values if not fixed_int_range_contains(target, v)]
1299
+ if not bad:
1300
+ return
1301
+ tr = int_traits_of(target)
1302
+ raise self.ctx.error(
1303
+ f"Integer literal {bad[0]} assigned to '{expr.name}' is outside "
1304
+ f"{target} range [{tr.min_value}, {tr.max_value}] in {context}",
1305
+ expr,
1306
+ )
1307
+
1308
+ def _resolve_pending_for_any_storage(self, actual: TpyType) -> TpyType:
1309
+ """Convert Pending{List,Dict,Set}Type to its concrete container
1310
+ form using the literal-info's inferred element types, defaulting
1311
+ IntLiteralType / FloatLiteralType to the configured defaults.
1312
+ """
1313
+ if isinstance(actual, PendingListType):
1314
+ elem = self._default_resolve_element(actual.element_type)
1315
+ info = self.ctx.list_literals.get(actual.literal_id)
1316
+ if info is not None and info.coerced_element_type is None:
1317
+ info.coerced_element_type = elem
1318
+ return make_list(elem)
1319
+ if isinstance(actual, PendingDictType):
1320
+ k = self._default_resolve_element(actual.key_type)
1321
+ v = self._default_resolve_element(actual.value_type)
1322
+ return make_dict(k, v)
1323
+ if isinstance(actual, PendingSetType):
1324
+ elem = self._default_resolve_element(actual.element_type)
1325
+ return make_set(elem)
1326
+ return actual
1327
+
1328
+ def _default_resolve_element(self, t: TpyType) -> TpyType:
1329
+ if isinstance(t, IntLiteralType):
1330
+ return self.ctx.default_int_type
1331
+ if isinstance(t, FloatLiteralType):
1332
+ return FLOAT
1333
+ return t
1334
+
1335
+ def is_runtime_bigint_expr(self, expr: TpyExpr) -> bool:
1336
+ """Check if an IntLiteralType expression could be BigInt at runtime."""
1337
+ if isinstance(expr, TpyCoerce):
1338
+ return self.is_runtime_bigint_expr(expr.expr)
1339
+ if isinstance(expr, TpyName):
1340
+ return True
1341
+ if isinstance(expr, TpyIntLiteral):
1342
+ return False
1343
+ if isinstance(expr, TpyBinOp):
1344
+ return self.is_runtime_bigint_expr(expr.left) or self.is_runtime_bigint_expr(expr.right)
1345
+ if isinstance(expr, TpyUnaryOp):
1346
+ return self.is_runtime_bigint_expr(expr.operand)
1347
+ if isinstance(expr, (TpyCall, TpyMethodCall)):
1348
+ return True
1349
+ return True
1350
+
1351
+ def is_lvalue(self, expr: TpyExpr) -> bool:
1352
+ """Check if an expression is an lvalue (can have its address taken)."""
1353
+ if isinstance(expr, TpyCoerce):
1354
+ return self.is_lvalue(expr.expr)
1355
+ # Named variables are lvalues
1356
+ if isinstance(expr, TpyName):
1357
+ return True
1358
+ # Field access on an lvalue is also an lvalue (e.g., obj.field)
1359
+ if isinstance(expr, TpyFieldAccess):
1360
+ return self.is_lvalue(expr.obj)
1361
+ # Subscript on an lvalue is also an lvalue (e.g., arr[i])
1362
+ if isinstance(expr, TpySubscript):
1363
+ return self.is_lvalue(expr.obj)
1364
+ # Everything else (calls, literals, operators) are rvalues
1365
+ return False
1366
+
1367
+ def is_const_ref_source(self, expr: TpyExpr) -> bool:
1368
+ """Check if expression provides a const reference (can't be captured as T&).
1369
+
1370
+ Returns True for sources that are inherently const in C++:
1371
+ - Field access / subscript through a readonly-typed object
1372
+ """
1373
+ if isinstance(expr, TpyCoerce):
1374
+ return self.is_const_ref_source(expr.expr)
1375
+ if isinstance(expr, TpySubscript):
1376
+ obj_type = self.ctx.get_expr_type(expr.obj)
1377
+ if obj_type is not None:
1378
+ unwrapped = unwrap_readonly(obj_type)
1379
+ if is_span(unwrapped) and is_readonly_span(unwrapped):
1380
+ return True
1381
+ return self.is_const_ref_source(expr.obj)
1382
+ if isinstance(expr, TpyFieldAccess):
1383
+ obj_type = self.ctx.get_expr_type(expr.obj)
1384
+ if obj_type is not None and isinstance(obj_type, ReadonlyType):
1385
+ return True
1386
+ return self.is_const_ref_source(expr.obj)
1387
+ if isinstance(expr, TpyName):
1388
+ expr_type = self.ctx.get_expr_type(expr)
1389
+ if expr_type is not None and isinstance(expr_type, ReadonlyType):
1390
+ return True
1391
+ return False
1392
+
1393
+ def _is_auto_moved(self, source_expr: 'TpyExpr | None') -> bool:
1394
+ """Last-use of an owned local: auto-move makes the copy invisible."""
1395
+ return (isinstance(source_expr, TpyName)
1396
+ and id(source_expr) in self.ctx.all_last_uses
1397
+ and self._is_owned_var(source_expr.name))
1398
+
1399
+ def _warns_copy_into_any(self, inner: TpyType) -> bool:
1400
+ """Reference types whose into-Any storage silently copies.
1401
+
1402
+ Caller must pre-unwrap Ref/Readonly/Own. Returns True for non-value
1403
+ records, list, dict, set; False for primitives, value-type records,
1404
+ str (collapses to std::string), bytes/BytesView, Ptr[T].
1405
+ """
1406
+ if isinstance(inner, NominalType):
1407
+ return not inner.is_value_type() and not inner.is_protocol
1408
+ return is_list(inner) or is_dict(inner) or is_set(inner)
1409
+
1410
+ def is_copy_call(self, expr: TpyExpr) -> bool:
1411
+ """Check if expression is a copy() or copy_iter() call from the tpy module."""
1412
+ if isinstance(expr, TpyCoerce):
1413
+ return self.is_copy_call(expr.expr)
1414
+ if not isinstance(expr, TpyCall):
1415
+ return False
1416
+ # Check if this function name maps to tpy.copy or tpy.copy_iter
1417
+ # (handles aliases like "from tpy import copy as c")
1418
+ if not isinstance(expr.func, TpyName):
1419
+ return False
1420
+ if expr.func_name in self.ctx.imported_names:
1421
+ module_name, func_name = self.ctx.imported_names[expr.func_name]
1422
+ return module_name == "tpy" and func_name in ("copy", "copy_iter")
1423
+ return False
1424
+
1425
+ def check_own_consumption(self, expr: TpyExpr) -> None:
1426
+ """Mark Own[T] param as consumed if expr transfers ownership.
1427
+
1428
+ Handles two patterns:
1429
+ - Auto-move: bare name at last use of a movable variable
1430
+ - copy(): explicit copy transfers ownership of the param's value
1431
+ """
1432
+ if isinstance(expr, TpyName):
1433
+ if (id(expr) in self.ctx.all_last_uses
1434
+ and self._is_owned_var(expr.name)):
1435
+ self.ctx.mark_own_param_consumed(expr.name)
1436
+ return
1437
+ if self.is_copy_call(expr) and isinstance(expr, TpyCall) and expr.args:
1438
+ inner = expr.args[0]
1439
+ if isinstance(inner, TpyName):
1440
+ self.ctx.mark_own_param_consumed(inner.name)
1441
+
1442
+ def check_own_lvalue_into_own(
1443
+ self, own_type: OwnType, expr: TpyExpr, context: str,
1444
+ *, action: str = "return",
1445
+ ) -> None:
1446
+ """Check that an lvalue feeding an Own[T] slot is movable, an explicit
1447
+ copy(), or otherwise safe to consume.
1448
+
1449
+ Used by:
1450
+ - return-statement Own[T] / per-element Own[tuple[T,...]] checks
1451
+ (action="return"); error message says "Cannot return borrowed
1452
+ value as ...".
1453
+ - call-arg Own[tuple[T,...]] per-element check (action="pass");
1454
+ error message says "Cannot pass borrowed value as ...".
1455
+
1456
+ Args:
1457
+ own_type: The Own[T] slot type.
1458
+ expr: The source expression occupying the slot.
1459
+ context: Slot description for error messages, e.g. "return type",
1460
+ "tuple element 1", "argument 'pname' element 0".
1461
+ action: "return" or "pass" -- selects the verb in error messages.
1462
+ """
1463
+ if self.is_copy_call(expr):
1464
+ self.check_own_consumption(expr)
1465
+ return
1466
+ if not self.is_lvalue(expr):
1467
+ return
1468
+ # Value types are always safe -- copied, not aliased. OwnType.is_value_type
1469
+ # returns True (Own represents a moved value), so we have to peel Own
1470
+ # first to see whether the underlying T is genuinely a value type.
1471
+ inner = expr
1472
+ while isinstance(inner, TpyCoerce):
1473
+ inner = inner.expr
1474
+ raw_type = self.ctx.get_raw_expr_type(inner)
1475
+ if raw_type is not None:
1476
+ unwrapped = unwrap_ref_type(raw_type)
1477
+ inner_after_own = unwrapped.wrapped if isinstance(unwrapped, OwnType) else unwrapped
1478
+ if inner_after_own.is_value_type():
1479
+ return
1480
+ # action="return": Own-typed lvalues at non-last-use are tolerated
1481
+ # (codegen falls back to copy, paired with the coercion-path warning).
1482
+ # action="pass": enforce last-use even for Own locals -- the
1483
+ # per-element check exists precisely because the user opted in to
1484
+ # ownership semantics by writing Own[tuple[...]] and the silent copy
1485
+ # is the bug being fixed.
1486
+ if action == "return" and isinstance(unwrapped, OwnType):
1487
+ return
1488
+ # Consuming method: self.field is owned and movable out of the struct.
1489
+ if (self.ctx.in_consuming_method
1490
+ and isinstance(expr, TpyFieldAccess)
1491
+ and isinstance(expr.obj, TpyName) and expr.obj.name == "self"):
1492
+ return
1493
+ is_auto_moved = (isinstance(expr, TpyName)
1494
+ and id(expr) in self.ctx.all_last_uses
1495
+ and self._is_owned_var(expr.name))
1496
+ if is_auto_moved:
1497
+ self.check_own_consumption(expr)
1498
+ return
1499
+ expr_type = self.ctx.get_expr_type(expr)
1500
+ is_nocopy = expr_type is not None and self.ctx.is_type_nocopy(expr_type)
1501
+ if is_nocopy:
1502
+ reason = self.ctx.nocopy_reason(expr_type)
1503
+ is_movable = (isinstance(expr, TpyName)
1504
+ and self._is_owned_var(expr.name))
1505
+ if is_movable:
1506
+ raise self.ctx.error(
1507
+ f"{reason} is used after this point "
1508
+ f"and cannot be moved into {context} Own[{own_type.wrapped}]. "
1509
+ f"Remove later uses or restructure the code.",
1510
+ expr
1511
+ )
1512
+ verb = "returned" if action == "return" else "passed"
1513
+ raise self.ctx.error(
1514
+ f"{reason} cannot be {verb} as "
1515
+ f"{context} Own[{own_type.wrapped}]. "
1516
+ f"Only the original owner can be moved at its last use.",
1517
+ expr
1518
+ )
1519
+ verb = "return" if action == "return" else "pass"
1520
+ raise self.ctx.error(
1521
+ f"Cannot {verb} borrowed value as {context} Own[{own_type.wrapped}] "
1522
+ f"without explicit copy(). The source is borrowed (parameter, "
1523
+ f"attribute, or non-last-use variable); use 'copy(...)' to make "
1524
+ f"an owned copy.",
1525
+ expr
1526
+ )
1527
+
1528
+ def _is_value_type_param(self, typ: TpyType) -> bool:
1529
+ """Check if a TypeParamRef has a ValueType bound in the current context."""
1530
+ if not isinstance(typ, TypeParamRef):
1531
+ return False
1532
+ bound = self.type_ops.get_type_param_bound(typ.name)
1533
+ return bound is not None and isinstance(bound, NominalType) and bound.qualified_name() == "tpy.ValueType"
1534
+
1535
+ def _check_union_member_type_args(
1536
+ self, actual: NominalType, expected: NominalType,
1537
+ context: str, loc: object,
1538
+ source_expr: 'TpyExpr | None',
1539
+ is_return: bool, coercion_ctx: CoercionContext,
1540
+ ) -> bool:
1541
+ """Check Container[Lit] -> Container[Expr] union-member coercion.
1542
+
1543
+ Returns True if all differing type args have expected = recursive union
1544
+ and actual is a valid member of that union.
1545
+ """
1546
+ for a_arg, e_arg in zip(actual.type_args, expected.type_args):
1547
+ if a_arg == e_arg:
1548
+ continue
1549
+ if not isinstance(a_arg, TpyType) or not isinstance(e_arg, TpyType):
1550
+ return False
1551
+ # Resolve recursive-alias placeholders (AliasRef from parser, or
1552
+ # forward NominalType refs from cross-module aliases) into their
1553
+ # underlying UnionType so the union-coercion path below applies.
1554
+ e_resolved = (self._resolve_recursive_refs(e_arg)
1555
+ if isinstance(e_arg, (NominalType, AliasRef)) else e_arg)
1556
+ if isinstance(e_resolved, UnionType) and e_resolved.needs_wrapper():
1557
+ # Container type-arg slot is storage form (the container stores
1558
+ # values, no address-take fires for pointer-repr Optional).
1559
+ result = self._check_compat(
1560
+ a_arg, e_resolved, context, loc, None, is_return, coercion_ctx,
1561
+ target_is_storage_form=True,
1562
+ )
1563
+ if isinstance(result, CompatError):
1564
+ return False
1565
+ else:
1566
+ return False
1567
+ # Rewrite expression type so codegen emits correct template args
1568
+ # (e.g. Box<Expr> not Box<Lit>)
1569
+ if source_expr is not None:
1570
+ self.ctx.set_expr_type(source_expr, expected)
1571
+ # Also rewrite call_type on constructor calls (codegen uses this
1572
+ # for template args: Box<Expr> vs Box<Lit>)
1573
+ if isinstance(source_expr, TpyCall) and source_expr.call_type is not None:
1574
+ source_expr.call_type = expected
1575
+ return True
1576
+
1577
+ def is_covariant_generic_upcast(self, actual: TpyType, expected: TpyType) -> bool:
1578
+ """True if `actual -> expected` is a covariant-generic wrapper upcast
1579
+ (e.g. `Box[Child] -> Box[Parent]`): same user-record generic, differing
1580
+ type args, every differing position covariant and a valid C++ upcast
1581
+ target. This is a representation-preserving converting move, NOT
1582
+ slicing. Shared by `check_type_compatible` and the container-literal
1583
+ element guards so the covariant-vs-slice line is drawn in one place."""
1584
+ if not (isinstance(actual, NominalType) and actual.is_user_record
1585
+ and isinstance(expected, NominalType) and expected.is_user_record
1586
+ and actual.name == expected.name
1587
+ and actual.type_args and expected.type_args
1588
+ and actual.type_args != expected.type_args):
1589
+ return False
1590
+ record_info = self.ctx.registry.get_record(actual.name)
1591
+ if record_info is None or not record_info.type_params:
1592
+ return False
1593
+ covariant = get_covariant_params(record_info)
1594
+ return bool(covariant and self._check_covariant_args(
1595
+ record_info, covariant, actual, expected))
1596
+
1597
+ def _check_covariant_args(
1598
+ self, record_info: 'RecordInfo', covariant: set[str],
1599
+ actual: NominalType, expected: NominalType
1600
+ ) -> bool:
1601
+ """Check if all type args are compatible under covariance rules."""
1602
+ for i, param_name in enumerate(record_info.type_params):
1603
+ if i >= len(actual.type_args) or i >= len(expected.type_args):
1604
+ return False
1605
+ actual_arg = actual.type_args[i]
1606
+ expected_arg = expected.type_args[i]
1607
+ if actual_arg == expected_arg:
1608
+ continue
1609
+ if param_name not in covariant:
1610
+ return False # invariant position must match exactly
1611
+ if not isinstance(actual_arg, TpyType) or not isinstance(expected_arg, TpyType):
1612
+ return False
1613
+ # Target must be a @dynamic protocol or class parent for C++ pointer upcast
1614
+ if not self._is_covariant_target(actual_arg, expected_arg):
1615
+ return False
1616
+ return True
1617
+
1618
+ def _is_representational_subtype(self, child: TpyType, parent: TpyType) -> bool:
1619
+ """True if a `Ptr[child] -> Ptr[parent]` C++ pointer upcast is valid,
1620
+ where `child` is a bounded type parameter.
1621
+
1622
+ Matches `child`'s IMMEDIATE bound against the actual `parent` at each
1623
+ hop -- never flattens a bound to bound(parent), which would wrongly
1624
+ admit a sibling subtype (`U: T`, `T: Pet` does NOT make `U` any
1625
+ `Pet`). A `U: V`, `V: U` cycle is guarded by a seen-set.
1626
+
1627
+ A protocol `parent` is declined: a structural conformer satisfies the
1628
+ bound without inheriting, so the plain C++ pointer upcast would be
1629
+ invalid. Those need the adapter path (out of scope here). Only a class
1630
+ or a (further) type-param bound proves the upcast. The concrete
1631
+ instantiation's soundness is enforced at the call site.
1632
+ """
1633
+ if is_protocol_type(parent):
1634
+ return False
1635
+ # Record the originating type-param as representational so codegen at
1636
+ # call sites knows to materialize structural conformers via Adapter.
1637
+ origin_param = child.name if isinstance(child, TypeParamRef) else None
1638
+ seen: set[str] = set()
1639
+ cur = child
1640
+ while isinstance(cur, TypeParamRef) and cur.name not in seen:
1641
+ seen.add(cur.name)
1642
+ bound = self.type_ops.get_type_param_bound(cur.name)
1643
+ if bound is None:
1644
+ return False
1645
+ if bound == parent:
1646
+ if origin_param is not None:
1647
+ self.ctx.func.current_representational_params.add(origin_param)
1648
+ return True
1649
+ if isinstance(bound, NominalType) and bound.is_user_record:
1650
+ if self._is_covariant_target(bound, parent):
1651
+ if origin_param is not None:
1652
+ self.ctx.func.current_representational_params.add(origin_param)
1653
+ return True
1654
+ return False
1655
+ if isinstance(bound, TypeParamRef):
1656
+ cur = bound
1657
+ continue
1658
+ return False
1659
+ return False
1660
+
1661
+ def _is_covariant_target(self, child: TpyType, parent: TpyType) -> bool:
1662
+ """Check if child -> parent is valid for covariant conversion.
1663
+
1664
+ Requires C++ struct inheritance: @dynamic protocol implementation
1665
+ or class inheritance.
1666
+ """
1667
+ if not isinstance(child, NominalType) or not isinstance(parent, NominalType):
1668
+ return False
1669
+ # @dynamic protocol: child implements parent
1670
+ if is_protocol_type(parent):
1671
+ proto_info = protocol_info_of(parent)
1672
+ if proto_info and proto_info.is_dynamic:
1673
+ if self.protocols and self.protocols.type_conforms_to_protocol(child, parent):
1674
+ return True
1675
+ return False
1676
+ # Class inheritance
1677
+ if child.is_user_record and parent.is_user_record:
1678
+ return self.ctx.registry.is_subclass_of(child, parent)
1679
+ return False
1680
+
1681
+ def _is_local_shadow(self, name: str) -> bool:
1682
+ """Check if a name is bound in a local scope, shadowing a global."""
1683
+ scope = self.ctx.func.current_scope
1684
+ while scope and scope is not self.ctx.global_scope:
1685
+ if name in scope.bindings:
1686
+ return True
1687
+ scope = scope.parent
1688
+ return False
1689
+
1690
+ def _is_owned_var(self, name: str) -> bool:
1691
+ """Check if a variable has owned storage (eligible for auto-move).
1692
+
1693
+ Uses OwnType in scope as the primary authority. Falls back to
1694
+ rvalue_vars for variables excluded from OwnType wrapping
1695
+ (reassigned vars, move-through vars).
1696
+ """
1697
+ func = self.ctx.func.current_function
1698
+ if isinstance(func, TpyFunction):
1699
+ # Own[T] params
1700
+ for pname, ptype in func.params:
1701
+ if pname == name:
1702
+ return unwrap_optional_own(unwrap_readonly(ptype)) is not None
1703
+ # Locals: check scope type.
1704
+ # Exclude for-loop vars: they get Own[T] from consuming iteration
1705
+ # element types but are not "rvalue-initialized" in the same sense
1706
+ # as constructor calls. Their movability is handled by the
1707
+ # consuming iteration system (consumed_loop_vars).
1708
+ scope_type = self.ctx.func.current_scope.lookup(name) if self.ctx.func.current_scope else None
1709
+ if scope_type and isinstance(scope_type, OwnType) and name not in self.ctx.func.loop_vars:
1710
+ return True
1711
+ # Fallback: rvalue_vars covers move-through vars and
1712
+ # reassigned-but-all-rvalue vars not wrapped with OwnType.
1713
+ # Hoisted vars are not movable (T* pointer-locals).
1714
+ if name in self.ctx.func.rvalue_vars and name not in self.ctx.func.hoisted_vars:
1715
+ if name in self.ctx.func.current_reassigned_vars:
1716
+ return name not in self.ctx.func.current_lvalue_reassigned
1717
+ return True
1718
+ return False
1719
+ # Top-level: non-value-type vars become pointer-globals, can't be moved
1720
+ var_type = self.ctx.func.current_scope.lookup(name) if self.ctx.func.current_scope else None
1721
+ if var_type:
1722
+ inner = var_type.wrapped if isinstance(var_type, OwnType) else var_type
1723
+ if not inner.is_value_type():
1724
+ return False
1725
+ return True
1726
+
1727
+ def _view_constructor_arg(self, expr: TpyExpr) -> TpyExpr | None:
1728
+ """If expr is a borrowing-view constructor call, return the borrowed-from arg.
1729
+
1730
+ StrView / BytesView / Span[T] / SpanIter[T] all borrow from their
1731
+ first argument, so provenance/dangling predicates should recurse
1732
+ into that argument rather than treating the constructor call as
1733
+ an opaque value.
1734
+ """
1735
+ if not isinstance(expr, TpyCall) or not expr.args:
1736
+ return None
1737
+ if expr.call_type is not None and is_borrowing_view_type(expr.call_type):
1738
+ return expr.args[0]
1739
+ return None
1740
+
1741
+ def _name_is_param_or_global(self, name: str) -> bool:
1742
+ """True if the name refers to caller-owned storage that outlives the call.
1743
+
1744
+ Used by is_safe_to_return_expr and is_dangling_return to keep the
1745
+ self/param/non-shadowed-global checks in one place. is_param_derived_expr
1746
+ intentionally diverges by omitting the self check (self is the receiver,
1747
+ not a "parameter" for borrow-contract purposes).
1748
+ """
1749
+ if name == "self":
1750
+ return True
1751
+ func = self.ctx.func.current_function
1752
+ if isinstance(func, TpyFunction):
1753
+ for pname, _ptype in func.params:
1754
+ if pname == name:
1755
+ return True
1756
+ if name in self.ctx.global_scope.bindings:
1757
+ if not self._is_local_shadow(name):
1758
+ return True
1759
+ return False
1760
+
1761
+ def is_param_derived_expr(self, expr: TpyExpr) -> bool:
1762
+ """Check if an expression's root storage derives from parameters or globals."""
1763
+ if isinstance(expr, TpyCoerce):
1764
+ return self.is_param_derived_expr(expr.expr)
1765
+ # String / bytes literals live in rodata; None literal is a nullptr.
1766
+ # All three have non-local, permanent storage -- safe to borrow from.
1767
+ if isinstance(expr, (TpyStrLiteral, TpyBytesLiteral, TpyNoneLiteral)):
1768
+ return True
1769
+ if isinstance(expr, TpyName):
1770
+ # Parameters are param-derived
1771
+ func = self.ctx.func.current_function
1772
+ if isinstance(func, TpyFunction):
1773
+ for pname, _ptype in func.params:
1774
+ if pname == expr.name:
1775
+ return True
1776
+ # Globals are param-derived (live forever), but only if
1777
+ # the name isn't shadowed by a local binding
1778
+ if expr.name in self.ctx.global_scope.bindings:
1779
+ if not self._is_local_shadow(expr.name):
1780
+ return True
1781
+ # Variables tracked as param-derived
1782
+ if expr.name in self.ctx.func.param_provenance_vars:
1783
+ return True
1784
+ return False
1785
+ if isinstance(expr, TpyFieldAccess):
1786
+ return self.is_param_derived_expr(expr.obj)
1787
+ if isinstance(expr, TpySubscript):
1788
+ return self.is_param_derived_expr(expr.obj)
1789
+ if isinstance(expr, TpyIfExpr):
1790
+ return (self.is_param_derived_expr(expr.then_expr)
1791
+ and self.is_param_derived_expr(expr.else_expr))
1792
+ # Pointer constructors derive provenance from their argument (address-taking)
1793
+ if isinstance(expr, TpyCall) and expr.call_type is not None and expr.call_type.is_pointer() and expr.args:
1794
+ return self.is_param_derived_expr(expr.args[0])
1795
+ view_arg = self._view_constructor_arg(expr)
1796
+ if view_arg is not None:
1797
+ return self.is_param_derived_expr(view_arg)
1798
+ # Function/method calls with return_borrows_from: result is param-derived if
1799
+ # the borrowed-from argument(s) are themselves param-derived.
1800
+ # None = unanalyzed (skip); frozenset() = returns new value (loop body never
1801
+ # runs, falls through to return False below -- correct).
1802
+ if isinstance(expr, (TpyCall, TpyMethodCall)):
1803
+ fi = expr.resolved_function_info
1804
+ if fi is not None and fi.return_borrows_from is not None:
1805
+ args = expr.args
1806
+ obj = expr.obj if isinstance(expr, TpyMethodCall) else None
1807
+ for idx in fi.return_borrows_from:
1808
+ if idx == -1 and obj is not None:
1809
+ if self.is_param_derived_expr(obj):
1810
+ return True
1811
+ elif 0 <= idx < len(args):
1812
+ if self.is_param_derived_expr(args[idx]):
1813
+ return True
1814
+ # Constructors, function calls, literals -- local storage
1815
+ return False
1816
+
1817
+ def is_safe_to_return_expr(self, expr: TpyExpr) -> bool:
1818
+ """Whether binding a local to this RHS makes the local safe to return.
1819
+
1820
+ Recurses through composite forms so a per-call-site OR doesn't miss
1821
+ ``sv = p if cond else pick(q)`` (neither arm uniformly param-derived
1822
+ nor uniformly a trusted call). Must be a superset of
1823
+ is_param_derived_expr to maintain the subset invariant on which
1824
+ flow-merge intersection relies.
1825
+ """
1826
+ if isinstance(expr, TpyCoerce):
1827
+ return self.is_safe_to_return_expr(expr.expr)
1828
+ # String / bytes / None literals live in rodata / are nullptr --
1829
+ # permanent storage, safe to return. Mirrors is_param_derived_expr.
1830
+ if isinstance(expr, (TpyStrLiteral, TpyBytesLiteral, TpyNoneLiteral)):
1831
+ return True
1832
+ if isinstance(expr, TpyName):
1833
+ return (self._name_is_param_or_global(expr.name)
1834
+ or expr.name in self.ctx.func.safe_to_return_vars)
1835
+ if isinstance(expr, TpyFieldAccess):
1836
+ return self.is_safe_to_return_expr(expr.obj)
1837
+ if isinstance(expr, TpySubscript):
1838
+ return self.is_safe_to_return_expr(expr.obj)
1839
+ if isinstance(expr, TpyIfExpr):
1840
+ return (self.is_safe_to_return_expr(expr.then_expr)
1841
+ and self.is_safe_to_return_expr(expr.else_expr))
1842
+ # Pointer constructors derive safety from their argument (address-taking).
1843
+ if isinstance(expr, TpyCall) and expr.call_type is not None and expr.call_type.is_pointer() and expr.args:
1844
+ return self.is_safe_to_return_expr(expr.args[0])
1845
+ view_arg = self._view_constructor_arg(expr)
1846
+ if view_arg is not None:
1847
+ return self.is_safe_to_return_expr(view_arg)
1848
+ # Calls with return_borrows_from are not handled explicitly here:
1849
+ # is_dangling_return already recurses into the borrowed args, so a
1850
+ # call whose return borrows from a param-derived (or otherwise safe)
1851
+ # arg is non-dangling and lands in this branch as safe. If
1852
+ # is_dangling_return is ever tightened for return_borrows_from, mirror
1853
+ # the explicit arm from is_param_derived_expr to preserve the
1854
+ # superset invariant.
1855
+ if isinstance(expr, (TpyCall, TpyMethodCall)):
1856
+ return not self.is_dangling_return(expr)
1857
+ return False
1858
+
1859
+ def is_mutable_lvalue(self, expr: TpyExpr) -> bool:
1860
+ """Check if an expression is a mutable lvalue (can get a mutable Ptr).
1861
+
1862
+ This is like is_lvalue but also rejects read-only sources like Span elements.
1863
+ """
1864
+ if isinstance(expr, TpyCoerce):
1865
+ return self.is_mutable_lvalue(expr.expr)
1866
+ # Named variables: mutable unless the binding's type is `readonly[T]`
1867
+ # (parameter declared as `readonly[T]`, or `self` inside a @readonly
1868
+ # method). Without this check, a readonly binding could launder
1869
+ # away its const via `&x` / `take_ptr(x)` / record-to-Ptr upcasts.
1870
+ if isinstance(expr, TpyName):
1871
+ expr_type = self.ctx.get_expr_type(expr)
1872
+ if isinstance(expr_type, ReadonlyType):
1873
+ return False
1874
+ return True
1875
+ # Field access on a mutable lvalue is also mutable
1876
+ if isinstance(expr, TpyFieldAccess):
1877
+ return self.is_mutable_lvalue(expr.obj)
1878
+ # Subscript: check if the base is a read-only type (Span[readonly[T]], str)
1879
+ if isinstance(expr, TpySubscript):
1880
+ obj_type = self.ctx.get_expr_type(expr.obj)
1881
+ if is_span(obj_type) and is_readonly_span(obj_type):
1882
+ return False # Span[readonly[T]] elements are read-only
1883
+ if is_any_str_type(obj_type):
1884
+ return False
1885
+ return self.is_mutable_lvalue(expr.obj)
1886
+ return False
1887
+
1888
+ def is_dangling_return(self, expr: TpyExpr) -> bool:
1889
+ """Check if returning this expression would create a dangling reference."""
1890
+ if isinstance(expr, TpyCoerce):
1891
+ return self.is_dangling_return(expr.expr)
1892
+ # Array/dict literal - creates temporary
1893
+ if isinstance(expr, (TpyArrayLiteral, TpyDictLiteral, TpySetLiteral)):
1894
+ return True
1895
+
1896
+ # List repeat - creates temporary
1897
+ if isinstance(expr, TpyListRepeat):
1898
+ return True
1899
+
1900
+ # Constructor call - creates temporary
1901
+ if isinstance(expr, TpyCall):
1902
+ # Pointer constructors: dangling depends on the argument, not the pointer itself
1903
+ if isinstance(expr.call_type, PtrType):
1904
+ if not expr.args:
1905
+ return False # Ptr[T]() -> nullptr, always safe
1906
+ return self.is_dangling_return(expr.args[0])
1907
+
1908
+ view_arg = self._view_constructor_arg(expr)
1909
+ if view_arg is not None:
1910
+ return self.is_dangling_return(view_arg)
1911
+
1912
+ # @value_ptr_coercion functions (e.g. take_ptr): result borrows
1913
+ # from the arg value, so dangling depends on the arg.
1914
+ fi = expr.resolved_function_info
1915
+ if fi is not None and fi.value_ptr_coercion and expr.args:
1916
+ return self.is_dangling_return(expr.args[0])
1917
+
1918
+ # Generic type constructor creates a temporary
1919
+ if expr.call_type is not None:
1920
+ return True
1921
+
1922
+ # Expression callees return temporaries (not dangling)
1923
+ if not isinstance(expr.func, TpyName):
1924
+ return True
1925
+
1926
+ # Record constructor
1927
+ if expr.func_name in self.ctx.registry.records:
1928
+ return True
1929
+
1930
+ # Function returning Own[T] creates a temporary (by-value return).
1931
+ # Only check user-defined functions (builtins don't appear in
1932
+ # registry.functions).
1933
+ if self.ctx.registry.get_function(expr.func_name) is not None:
1934
+ fi = expr.resolved_function_info
1935
+ if fi and isinstance(fi.return_type, OwnType):
1936
+ return True
1937
+
1938
+ # Function whose return borrows from args (e.g. generators storing
1939
+ # non-value params as T& references): dangles if any borrowed arg dangles
1940
+ if fi is not None and fi.return_borrows_from:
1941
+ for idx in fi.return_borrows_from:
1942
+ if 0 <= idx < len(expr.args):
1943
+ if self.is_dangling_return(expr.args[idx]):
1944
+ return True
1945
+
1946
+ # A function returning owned str/String creates a temporary
1947
+ # std::string that dangles if returned as StrView.
1948
+ if fi is not None and (is_str_type(fi.return_type) or is_string_type(fi.return_type)):
1949
+ return True
1950
+
1951
+ # A function returning a recursive-union wrapper struct is an
1952
+ # rvalue by value (the wrapper is value-shape). Taking its
1953
+ # address (the Optional pointer-repr return path) would yield
1954
+ # `&(call())` -- ill-formed C++. Force the diagnostic so the
1955
+ # user wraps the return type in Own[Optional[Wrapper]] (or
1956
+ # binds to a local first).
1957
+ if fi is not None:
1958
+ ret_unwrapped = unwrap_ref_type(unwrap_readonly(fi.return_type))
1959
+ if ret_unwrapped.needs_wrapper():
1960
+ return True
1961
+
1962
+ # Regular function call - assume it returns something safe
1963
+ # (the callee is responsible for not returning dangling refs)
1964
+ return False
1965
+
1966
+ # Locals dangle unless their root or current binding is safe.
1967
+ if isinstance(expr, TpyName):
1968
+ if self._name_is_param_or_global(expr.name):
1969
+ return False
1970
+ if expr.name in self.ctx.func.safe_to_return_vars:
1971
+ return False
1972
+ return True
1973
+
1974
+ # Field access - safe only if the object itself is safe
1975
+ if isinstance(expr, TpyFieldAccess):
1976
+ return self.is_dangling_return(expr.obj)
1977
+
1978
+ # Subscript - safe only if the container itself is safe
1979
+ if isinstance(expr, TpySubscript):
1980
+ return self.is_dangling_return(expr.obj)
1981
+
1982
+ # Method call returning owned str/String creates a temporary
1983
+ # std::string that dangles if returned as StrView.
1984
+ if isinstance(expr, TpyMethodCall):
1985
+ fi = expr.resolved_function_info
1986
+ if fi is not None and (is_str_type(fi.return_type) or is_string_type(fi.return_type)):
1987
+ return True
1988
+ # A method returning a recursive-union wrapper struct yields a
1989
+ # value-shape rvalue; taking its address (the Optional pointer-repr
1990
+ # return path) would emit `&(call())`. Reuses the free-function
1991
+ # branch's wrapper-shape check so `return obj.method()` through
1992
+ # Optional[Wrapper] is rejected (use Own[Optional[Wrapper]]). The
1993
+ # free-function branch's return_borrows_from handling is not yet
1994
+ # mirrored here -- see BUGS.md.
1995
+ if fi is not None and unwrap_ref_type(unwrap_readonly(fi.return_type)).needs_wrapper():
1996
+ return True
1997
+ return False
1998
+
1999
+ # Ternary - dangles if either branch dangles
2000
+ if isinstance(expr, TpyIfExpr):
2001
+ return (self.is_dangling_return(expr.then_expr)
2002
+ or self.is_dangling_return(expr.else_expr))
2003
+
2004
+ # Unary/Binary ops - might create temporaries, be conservative
2005
+ if isinstance(expr, (TpyUnaryOp, TpyBinOp)):
2006
+ return True
2007
+
2008
+ # Default: assume safe
2009
+ return False
2010
+
2011
+ def check_view_return_dangle(self, expr: TpyExpr, return_type: TpyType,
2012
+ loc: SourceLocation | None) -> None:
2013
+ """Variant of check_dangling_reference for value-return contexts
2014
+ (lambda bodies, yield values). The full check_dangling_reference
2015
+ rejects local/temporary returns when the return type is a non-value
2016
+ object -- a false positive for value-return contexts where the C++
2017
+ callable/generator machinery moves/copies by value. Only the
2018
+ view-dangling and pointer-dangling sub-rules apply here.
2019
+ """
2020
+ if isinstance(return_type, PtrType) or is_borrowing_view_type(return_type):
2021
+ self.check_dangling_reference(expr, return_type, loc)
2022
+
2023
+ def check_dangling_reference(self, expr: TpyExpr, return_type: TpyType, loc: SourceLocation | None) -> None:
2024
+ """Check if returning expr as a reference would be a dangling reference.
2025
+
2026
+ Object types are returned by reference. Returning a local variable or
2027
+ newly constructed object would create a dangling reference.
2028
+ """
2029
+ # Only check object types (value types are returned by value)
2030
+ # OwnType returns by value (ownership transfer), so no dangling risk
2031
+ # Pointer types need dangling checks (the pointer value may point to a local)
2032
+ if isinstance(return_type, PtrType):
2033
+ if self.is_dangling_return(expr):
2034
+ raise self.ctx.error(
2035
+ "Cannot return pointer to local or temporary value; "
2036
+ "the returned pointer would dangle",
2037
+ expr
2038
+ )
2039
+ return
2040
+ # Value types that hold an interior pointer -- returning one that
2041
+ # borrows from a local would dangle after the function returns.
2042
+ view_msg = _dangling_view_message(return_type)
2043
+ if view_msg is not None:
2044
+ inner = expr.expr if isinstance(expr, TpyCoerce) else expr
2045
+ view_arg = self._view_constructor_arg(inner)
2046
+ if view_arg is not None:
2047
+ if self.is_dangling_return(view_arg):
2048
+ raise self.ctx.error(view_msg, inner)
2049
+ elif self.is_dangling_return(expr):
2050
+ raise self.ctx.error(view_msg, expr)
2051
+ return
2052
+ if isinstance(return_type, TupleType):
2053
+ if isinstance(expr, TpyTupleLiteral):
2054
+ for i, et in enumerate(return_type.element_types):
2055
+ if (not et.is_value_type() and not isinstance(et, (OwnType, TypeParamRef))
2056
+ and not et.needs_wrapper()
2057
+ and i < len(expr.elements)):
2058
+ if self.is_dangling_return(expr.elements[i]):
2059
+ raise self.ctx.error(
2060
+ f"Cannot return local or temporary as tuple element {i}. "
2061
+ f"Type '{et}' is returned by reference. "
2062
+ f"Use Own[{et}] to return by value.",
2063
+ expr.elements[i]
2064
+ )
2065
+ return
2066
+ if (return_type.is_value_type()
2067
+ or isinstance(return_type, (VoidType, OwnType))
2068
+ or unwrap_readonly(return_type).needs_wrapper()):
2069
+ # Recursive-union wrappers (non-generic `UnionType` form + the
2070
+ # generic `RecursiveAliasInstanceType`) are returned by value at
2071
+ # the C++ level even though `is_value_type()` is False (the
2072
+ # alternatives may be reference types) -- codegen emits the
2073
+ # wrapper struct as a value-returned type, so no dangling concern.
2074
+ # The `unwrap_readonly` mirrors codegen's `to_cpp_return_const`
2075
+ # delegation through ReadonlyType.
2076
+ return
2077
+
2078
+ # Optional[T] for non-value T returns T* -- returning a local would dangle.
2079
+ # But `return None` is always safe (returns nullptr).
2080
+ if isinstance(return_type, OptionalType):
2081
+ if isinstance(expr, TpyNoneLiteral):
2082
+ return
2083
+ if self.is_dangling_return(expr):
2084
+ raise self.ctx.error(
2085
+ f"Cannot return local or temporary as '{return_type}'. "
2086
+ f"The returned pointer would dangle. "
2087
+ f"Return a reference to parameter data, or use Own[{return_type}] "
2088
+ f"to return by value.",
2089
+ expr
2090
+ )
2091
+ return
2092
+
2093
+ # Check if the expression is safe to return as a reference
2094
+ if self.is_dangling_return(expr):
2095
+ if is_protocol_type(return_type):
2096
+ raise self.ctx.error(
2097
+ f"Cannot return local or temporary as '{return_type}'. "
2098
+ f"Dynamic protocol return requires a value that outlives the caller "
2099
+ f"(parameter or global).",
2100
+ expr
2101
+ )
2102
+ raise self.ctx.error(
2103
+ f"Cannot return local or temporary as reference. "
2104
+ f"Object type '{return_type}' is returned by reference. "
2105
+ f"Use Own[{return_type}] to return by value.",
2106
+ expr
2107
+ )