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,2062 @@
1
+ # tpy: macro_module
2
+ """argparse builder-trace macro.
3
+
4
+ Mirrors a slice of CPython's argparse.ArgumentParser surface as a
5
+ @builder_macro: the user writes ordinary builder-style code, the
6
+ compiler synthesizes a per-call-site record + parse function on
7
+ parse_args(...). Under CPython the same source resolves to the
8
+ stdlib argparse module (this file is invisible outside tpyc's lib
9
+ search path), so test programs run under both backends without
10
+ code changes.
11
+
12
+ Subparsers are layered on top via @builder_returns: the macro
13
+ classes ``_SubparsersAction`` (returned by ``add_subparsers()``)
14
+ and ``_SubparserBuilder`` (returned by ``add_parser(name)``)
15
+ collect per-sub arg specs that the top-level terminal walks to
16
+ emit per-sub records and parse fns. The top namespace flattens
17
+ per-sub fields as ``Optional[T]`` (CPython argparse Namespace
18
+ shape); a typed-union escape hatch is **not** stored -- see the
19
+ argparse Future Work table in ``docs/MACRO_DESIGN.md`` for the
20
+ phasing reason and the planned pre-pass-6 expansion that would
21
+ unlock it.
22
+
23
+ v1 surface, future-work tiers, and known CPython divergences are
24
+ tracked in ``docs/MACRO_DESIGN.md`` (the argparse use-case section)
25
+ and mirrored in ``docs/STDLIB_ROADMAP.md``. Keeping the canonical
26
+ list there avoids drift between the macro module and the design doc.
27
+ """
28
+
29
+ from tpyc.macro_api import (
30
+ builder_macro, builder_method, builder_returns, builder_terminal,
31
+ BuilderContext, MacroArg, MacroArgs, TypeInfo, MacroError,
32
+ macro_deps, types, ast, Type,
33
+ )
34
+
35
+ macro_deps("tpy", "sys")
36
+
37
+
38
+ # Default ``prog`` used in synthesized usage / help / error text when
39
+ # the user doesn't pass ``prog=`` to ``ArgumentParser``. Hardcoded for
40
+ # v1; CPython uses ``os.path.basename(sys.argv[0])`` -- runtime-derived
41
+ # prog is Tier 2 follow-up. Defined up here (before any helper that
42
+ # uses it as a default-arg) so module load order is well-defined.
43
+ _DEFAULT_PROG = "prog"
44
+ _DEFAULT_ERROR_PREFIX = f"{_DEFAULT_PROG}: error: "
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Internal: ArgSpec -- one registered argument
49
+ # ---------------------------------------------------------------------------
50
+
51
+ class _ArgSpec:
52
+ """One @builder_method add_argument(...) call, fully resolved at
53
+ macro time. Carries everything the terminal needs to synthesize
54
+ init / dispatch / record-construction code.
55
+ """
56
+ __slots__ = (
57
+ "is_flag", "flag_names", "name", "dest",
58
+ "type_info", "is_arg_type", "action", "default", "has_default",
59
+ "const", "has_const",
60
+ "choices", "required",
61
+ "nargs",
62
+ "help_text",
63
+ "metavar",
64
+ )
65
+
66
+ def __init__(
67
+ self, *, is_flag: bool, flag_names: list[str], name: str,
68
+ dest: str, type_info: TypeInfo, is_arg_type: bool,
69
+ action: str, default, has_default: bool,
70
+ const=None, has_const: bool = False,
71
+ choices: list | None = None, required: bool = False,
72
+ nargs=None, help_text: str | None = None,
73
+ metavar: str | None = None,
74
+ ) -> None:
75
+ self.is_flag = is_flag
76
+ self.flag_names = flag_names
77
+ self.name = name
78
+ self.dest = dest
79
+ self.type_info = type_info # resolved ``type=`` (defaults to str)
80
+ self.is_arg_type = is_arg_type # True iff type uses from_arg factory
81
+ self.action = action
82
+ self.default = default
83
+ self.has_default = has_default
84
+ self.const = const
85
+ self.has_const = has_const
86
+ self.choices = choices # None or list of macro-time literals
87
+ self.required = required # always True for positionals; user-controllable for flags
88
+ self.nargs = nargs # None | "?" | "*" | "+" | int
89
+ self.help_text = help_text # source text for the `--help` printer
90
+ self.metavar = metavar # display name override for usage / help
91
+
92
+ @property
93
+ def is_list_field(self) -> bool:
94
+ """True when the synthesized record field is list[T]."""
95
+ if self.action in ("append", "extend"):
96
+ return True
97
+ if self.action == "store" and isinstance(self.nargs, int):
98
+ return True
99
+ if self.action == "store" and self.nargs in ("*", "+"):
100
+ return True
101
+ return False
102
+
103
+ @property
104
+ def is_optional_field(self) -> bool:
105
+ """True when the synthesized field is wrapped in Optional[T].
106
+
107
+ Mirrors CPython argparse: an optional flag that wasn't given
108
+ on the command line and has no ``default=`` returns ``None``.
109
+ Positional arguments are always required (or carry an explicit
110
+ default for ``nargs='?'``), so positionals never produce
111
+ Optional fields.
112
+
113
+ For list-typed actions (``append`` / ``extend`` / ``store``
114
+ with multi-valued nargs), an absent flag also produces ``None``
115
+ rather than an empty list -- the synthesizer accumulates into
116
+ a local list and reconciles to ``None`` post-loop when the
117
+ flag was never seen.
118
+ """
119
+ if not self.is_flag:
120
+ return False
121
+ if self.has_default:
122
+ return False
123
+ if self.required:
124
+ return False
125
+ if self.is_list_field:
126
+ return True
127
+ if self.action == "store_const":
128
+ return True
129
+ if self.action == "store" and self.nargs == "?" and not self.has_const:
130
+ return True
131
+ if self.action == "store" and self.nargs is None:
132
+ return True
133
+ return False
134
+
135
+ @property
136
+ def is_optional_list_field(self) -> bool:
137
+ """True when the synthesized field is ``Optional[list[T]]``.
138
+
139
+ These specs use the accumulator + post-loop reconciliation
140
+ pattern: the flag handler writes into an accumulator local,
141
+ and the post-loop fixup either copies it into the dest field
142
+ (flag was seen) or leaves the dest at ``None``.
143
+ """
144
+ return self.is_list_field and self.is_optional_field
145
+
146
+ @property
147
+ def field_type(self):
148
+ # Field type follows action / nargs.
149
+ if self.action in ("store_true", "store_false"):
150
+ return types.bool
151
+ if self.action == "count":
152
+ return types.bigint
153
+ if self.action == "store_const":
154
+ scalar = _python_value_type_info(self.const).raw_type
155
+ else:
156
+ scalar = self.type_info.raw_type
157
+ if self.is_optional_list_field:
158
+ return types.optional(types.list(scalar))
159
+ if self.is_list_field:
160
+ return types.list(scalar)
161
+ if self.is_optional_field:
162
+ return types.optional(scalar)
163
+ return scalar
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Internal: resolution helpers
168
+ # ---------------------------------------------------------------------------
169
+
170
+ # Actions that consume value(s) from argv and apply ``type=`` to them.
171
+ _VALUE_TAKING_ACTIONS: frozenset[str] = frozenset({"store", "append", "extend"})
172
+
173
+ # Actions that take no value (the flag itself fully specifies the result).
174
+ _VALUE_FREE_ACTIONS: frozenset[str] = frozenset(
175
+ {"store_true", "store_false", "count", "store_const"}
176
+ )
177
+
178
+ # Actions that produce a list-typed field.
179
+ _LIST_ACTIONS: frozenset[str] = frozenset({"append", "extend"})
180
+
181
+ _ALLOWED_ACTIONS: frozenset[str] = _VALUE_TAKING_ACTIONS | _VALUE_FREE_ACTIONS
182
+
183
+
184
+ # Default ``type=`` is ``str`` (CPython parity). Cached as a TypeInfo
185
+ # so callers don't have to special-case the absent-kwarg path.
186
+ _STR_TYPE_INFO: TypeInfo = TypeInfo.from_tpy_type(types.str)
187
+
188
+
189
+ def _is_allowed_arg_type(ti: TypeInfo) -> bool:
190
+ """Whether ``type=<ti>`` is supported as a value-taking arg type.
191
+
192
+ Keyed on TypeInfo's category helpers rather than name lookup so new
193
+ acceptable types get picked up via the type system, not a string
194
+ table. ``is_float`` covers both ``float`` (Float64) and ``Float32``
195
+ -- both lower to a constructor that accepts ``str`` at runtime.
196
+ """
197
+ return ti.is_str or ti.is_bigint or ti.is_int or ti.is_float
198
+
199
+
200
+ def _is_arg_type(ctx: BuilderContext, ti: TypeInfo) -> bool:
201
+ """Whether ``ti`` is a user type with a ``@staticmethod from_arg(s: str) -> Self``.
202
+
203
+ This is the duck-typed escape hatch that lets users plug in custom
204
+ types (Path, datetime, domain records) without a built-in
205
+ enumeration: any record exposing such a factory is accepted, and
206
+ the synthesized parse fn calls ``T.from_arg(token)`` for it.
207
+ Sema checks the param/return-type details when the synthesized
208
+ call is analyzed -- the macro just gates on existence + staticness.
209
+ """
210
+ return ctx.get_static_method_return_type(ti, "from_arg") is not None
211
+
212
+
213
+ def _arg_type_name(ti: TypeInfo) -> str:
214
+ """User-facing / source-text name for an arg type.
215
+
216
+ Doubles as the constructor expression for value coercion: ``int(s)``
217
+ for BigInt, ``Int32(s)`` for fixed-width, ``float(s)`` for Float64,
218
+ ``Float32(s)`` for Float32. The ``str`` case is handled by callers
219
+ that skip wrapping (no constructor needed when the field stays a
220
+ plain string). ``is_float32`` is checked before ``is_float`` because
221
+ ``is_float`` is true for both Float64 and Float32.
222
+ """
223
+ if ti.is_str:
224
+ return "str"
225
+ if ti.is_bigint:
226
+ return "int"
227
+ if ti.is_int:
228
+ return ti.int_type_name
229
+ if ti.is_float32:
230
+ return "Float32"
231
+ if ti.is_float:
232
+ return "float"
233
+ # Should not occur once _is_allowed_arg_type gates entries; the
234
+ # raw_type's own repr is the safest fallback for diagnostics.
235
+ return str(ti.raw_type)
236
+
237
+
238
+ def _python_value_type_info(value) -> TypeInfo:
239
+ """TypeInfo for a Python literal value extracted via
240
+ eval_literal_or_final. Used to derive a record field type from a
241
+ ``const=`` literal so ``store_const`` and ``store + type=`` agree
242
+ on the field type for the same conceptual value.
243
+ """
244
+ if isinstance(value, bool):
245
+ return TypeInfo.from_tpy_type(types.bool)
246
+ if isinstance(value, int):
247
+ return TypeInfo.from_tpy_type(types.bigint)
248
+ if isinstance(value, float):
249
+ return TypeInfo.from_tpy_type(types.float64)
250
+ if isinstance(value, str):
251
+ return TypeInfo.from_tpy_type(types.str)
252
+ raise MacroError(
253
+ f"argparse: const= must be a bool/int/float/str literal, "
254
+ f"got {type(value).__name__}"
255
+ )
256
+
257
+
258
+ def _resolve_type_info(
259
+ ctx: BuilderContext, args: MacroArgs,
260
+ ) -> tuple[TypeInfo, bool]:
261
+ """Read ``type=`` and return ``(TypeInfo, is_arg_type)``.
262
+
263
+ Defaults to ``(str, False)``. The second tuple element is True when
264
+ the type is a user record providing the ``ArgType`` factory hook
265
+ (see ``_is_arg_type``); callers consult it to decide between
266
+ ``T(arg)`` and ``T.from_arg(arg)`` at synthesis time.
267
+ """
268
+ ti = ctx.kwarg_type(args, "type")
269
+ if ti is None:
270
+ return _STR_TYPE_INFO, False
271
+ if _is_allowed_arg_type(ti):
272
+ return ti, False
273
+ if _is_arg_type(ctx, ti):
274
+ # Builder-trace passes user-defined types as placeholders with
275
+ # ``_tpy_type=None``; resolve_type_info rebuilds them on the
276
+ # registered record's qname-bearing NominalType so downstream
277
+ # field types and codegen identity-match sema-resolved
278
+ # references in the synthesized parse function body.
279
+ return ctx.resolve_type_info(ti), True
280
+ ctx.error(
281
+ f"argparse: unsupported type={ti.name!r}; "
282
+ f"supported: int, float, str, "
283
+ f"Int8/16/32/64, UInt8/16/32/64, Float32, "
284
+ f"or any record with @staticmethod from_arg(s: str) -> Self"
285
+ )
286
+ return ti, False
287
+
288
+
289
+ def _dest_for_flags(flag_names: list[str], explicit_dest: str | None) -> str:
290
+ """Compute the destination field name from the flag spelling.
291
+
292
+ Mirrors CPython argparse: prefer the long flag with leading dashes
293
+ stripped and remaining dashes replaced by underscores; otherwise
294
+ use the (single) short flag.
295
+ """
296
+ if explicit_dest is not None:
297
+ return explicit_dest
298
+ long_flags = [f for f in flag_names if f.startswith("--")]
299
+ if long_flags:
300
+ return long_flags[0][2:].replace("-", "_")
301
+ short = flag_names[0]
302
+ return short[1:] if short.startswith("-") else short
303
+
304
+
305
+ def _is_flag(name: str) -> bool:
306
+ return name.startswith("-")
307
+
308
+
309
+ def _unwrap_optional(t):
310
+ """Strip one ``Optional[...]`` wrapper if present, else return ``t``
311
+ unchanged. Used to compare per-sub field types under the flat
312
+ namespace where ``T`` and ``Optional[T]`` should be treated as
313
+ equivalent (the top record always wraps once anyway).
314
+ """
315
+ ti = TypeInfo.from_tpy_type(t)
316
+ inner = ti.unwrap_optional()
317
+ return inner.raw_type if inner is not None else t
318
+
319
+
320
+ def _resolve_nargs_kwarg(ctx: BuilderContext, args: MacroArgs):
321
+ """Read ``nargs=`` and validate. Returns None | '?' | '*' | '+' | int."""
322
+ ma = ctx.kwarg_macroarg(args, "nargs")
323
+ if ma is None:
324
+ return None
325
+ v = ctx.eval_literal_or_final(ma.expr)
326
+ if isinstance(v, bool) or not isinstance(v, (str, int)):
327
+ ctx.error(
328
+ f"argparse: nargs= must be a string '?'/'*'/'+' or a "
329
+ f"positive int, got {type(v).__name__}"
330
+ )
331
+ if isinstance(v, str):
332
+ if v not in ("?", "*", "+"):
333
+ ctx.error(f"argparse: nargs={v!r} is not supported")
334
+ return v
335
+ # int
336
+ if v <= 0:
337
+ ctx.error(f"argparse: nargs= must be a positive integer, got {v}")
338
+ return v
339
+
340
+
341
+ def _check_default_elem(ctx: BuilderContext, value, ti: TypeInfo) -> None:
342
+ """Validate a single element of a list-typed ``default=`` against
343
+ the spec's resolved ``type=``. Bool literals are rejected upfront
344
+ even for int / float types, since Python's ``isinstance(True, int)``
345
+ quirk would otherwise let ``default=[True]`` slip through.
346
+ """
347
+ name = _arg_type_name(ti)
348
+ if isinstance(value, bool):
349
+ ctx.error(f"argparse: list default element must be {name}, got bool")
350
+ if ti.is_str:
351
+ ok = isinstance(value, str)
352
+ elif ti.is_bigint or ti.is_int:
353
+ ok = isinstance(value, int)
354
+ elif ti.is_float:
355
+ ok = isinstance(value, (int, float))
356
+ else:
357
+ ok = True # _is_allowed_arg_type already gated unsupported types
358
+ if not ok:
359
+ ctx.error(
360
+ f"argparse: list default element must be {name}, "
361
+ f"got {type(value).__name__}"
362
+ )
363
+
364
+
365
+ def _action_implicit_default(action: str):
366
+ """Default value applied when the user omits default= for a value-
367
+ free action. Mirrors CPython argparse where it can: store_true
368
+ defaults to False, store_false defaults to True, count and
369
+ store_const default to None there but we materialize 0 / None to
370
+ keep the typed record well-formed.
371
+ """
372
+ if action == "store_true":
373
+ return False
374
+ if action == "store_false":
375
+ return True
376
+ if action == "count":
377
+ return 0
378
+ return None
379
+
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # Internal: shared add_argument body
383
+ # ---------------------------------------------------------------------------
384
+
385
+ def _build_arg_spec(
386
+ ctx: BuilderContext, args: MacroArgs, *,
387
+ add_help_reserved: bool,
388
+ ) -> _ArgSpec:
389
+ """Validate add_argument() kwargs and construct an _ArgSpec.
390
+
391
+ Shared between ArgumentParser.add_argument and
392
+ _SubparserBuilder.add_argument. ``add_help_reserved`` controls
393
+ whether ``-h``/``--help`` are rejected as user-registered flag
394
+ names (true for the top-level parser when ``add_help=True``;
395
+ always false for sub-parsers in v1, since sub-help is not yet
396
+ auto-emitted).
397
+ """
398
+ names = ctx.positional_strs(args)
399
+ if not names:
400
+ ctx.error("argparse: add_argument() requires at least one name")
401
+
402
+ help_text = ctx.kwarg_str(args, "help")
403
+ metavar = ctx.kwarg_str(args, "metavar")
404
+ explicit_dest = ctx.kwarg_str(args, "dest")
405
+ required_kw = ctx.kwarg_bool(args, "required", False)
406
+ choices_arg = ctx.kwarg_macroarg(args, "choices")
407
+ choices: list | None = None
408
+ if choices_arg is not None:
409
+ choices = ctx.eval_sequence_of_literal_or_final(choices_arg.expr)
410
+ if not choices:
411
+ ctx.error("argparse: choices= must be non-empty")
412
+
413
+ flags = [n for n in names if _is_flag(n)]
414
+ positionals = [n for n in names if not _is_flag(n)]
415
+ if flags and positionals:
416
+ ctx.error(
417
+ "argparse: add_argument() positional and optional names "
418
+ "cannot be mixed"
419
+ )
420
+
421
+ action = ctx.kwarg_str(args, "action") or "store"
422
+ if action not in _ALLOWED_ACTIONS:
423
+ ctx.error(
424
+ f"argparse: action={action!r} is not supported "
425
+ f"(supported: store, store_true, store_false, count, "
426
+ f"append, extend, store_const)"
427
+ )
428
+
429
+ # type= is meaningful only for value-taking actions. Reject
430
+ # explicit type= on the value-free ones to mirror CPython.
431
+ if action in _VALUE_FREE_ACTIONS and ctx.kwarg_macroarg(args, "type") is not None:
432
+ ctx.error(
433
+ f"argparse: type= cannot be combined with action={action!r}"
434
+ )
435
+ type_info, is_arg_type = _resolve_type_info(ctx, args)
436
+
437
+ nargs = _resolve_nargs_kwarg(ctx, args)
438
+ # Custom ``type=<MyType>`` (ArgType) v1: ``store`` /
439
+ # ``append`` / ``extend`` plus all four nargs shapes are
440
+ # supported via the same synthesis as the built-in types
441
+ # (``T.from_arg`` is just a different value-coercion than the
442
+ # builtin constructor). ``choices=`` stays out of v1 because
443
+ # comparing the unparsed token against a literal set would
444
+ # diverge from CPython's "compare parsed values" semantics.
445
+ if is_arg_type and choices is not None:
446
+ ctx.error(
447
+ "argparse: type=<custom> does not support choices= in v1"
448
+ )
449
+ if nargs is not None and action in _VALUE_FREE_ACTIONS and action != "store_const":
450
+ ctx.error(
451
+ f"argparse: nargs= cannot be combined with action={action!r}"
452
+ )
453
+ # store_const + nargs is allowed only as nargs='?' (and means
454
+ # "if flag-bare use const, if flag-with-value use value, if
455
+ # absent use default"). For now reject the explicit pairing.
456
+ if action == "store_const" and nargs is not None:
457
+ ctx.error(
458
+ "argparse: nargs= is not supported with action='store_const'"
459
+ )
460
+ # extend without nargs is the confusing CPython case (iterates
461
+ # the converted value); require nargs explicitly.
462
+ if action == "extend" and nargs is None:
463
+ ctx.error(
464
+ "argparse: action='extend' requires nargs= (otherwise "
465
+ "the converted value gets iterated, which is rarely "
466
+ "what callers want)"
467
+ )
468
+
469
+ # const= is required for store_const and for store + nargs='?'
470
+ # (where it's the value used when the flag appears bare).
471
+ const_arg = ctx.kwarg_macroarg(args, "const")
472
+ const = None
473
+ has_const = False
474
+ if const_arg is not None:
475
+ if action == "store_const":
476
+ pass # always allowed
477
+ elif action == "store" and nargs == "?":
478
+ pass # const provides the bare-flag value
479
+ else:
480
+ ctx.error(
481
+ f"argparse: const= is only valid with action='store_const' "
482
+ f"or action='store' + nargs='?' (got action={action!r}, "
483
+ f"nargs={nargs!r})"
484
+ )
485
+ const = ctx.eval_literal_or_final(const_arg.expr)
486
+ has_const = True
487
+ elif action == "store_const":
488
+ ctx.error("argparse: action='store_const' requires const=")
489
+
490
+ # Default handling. ``default=`` is a literal at macro time;
491
+ # absent for required positionals, None-typed for absent flags
492
+ # without explicit default. List literals are accepted only
493
+ # for list-typed actions (append / extend / store + nargs=*/+/N).
494
+ default_arg = ctx.kwarg_macroarg(args, "default")
495
+ if default_arg is not None:
496
+ default = ctx.eval_literal_or_final(default_arg.expr)
497
+ has_default = True
498
+ if is_arg_type and not isinstance(default, str):
499
+ # Mirrors CPython's "string defaults run through type="
500
+ # rule: a single string literal is routed through
501
+ # ``T.from_arg`` at parse-fn entry. List defaults
502
+ # diverge from CPython (CPython keeps the elements as
503
+ # strings while TPy needs them typed as T to fit a
504
+ # ``list[T]`` field), so they're rejected; users can
505
+ # omit ``default=`` to get ``Optional[list[T]]`` or an
506
+ # empty ``list[T]``.
507
+ ctx.error(
508
+ f"argparse: type=<custom> with default= requires "
509
+ f"a string literal (got {type(default).__name__})"
510
+ )
511
+ if isinstance(default, list):
512
+ list_action_ok = (
513
+ action in _LIST_ACTIONS
514
+ or (action == "store"
515
+ and (isinstance(nargs, int) or nargs in ("*", "+")))
516
+ )
517
+ if not list_action_ok:
518
+ ctx.error(
519
+ f"argparse: list default is only valid for "
520
+ f"list-typed actions (append/extend or store + "
521
+ f"nargs=*/+/<int>); got action={action!r}, "
522
+ f"nargs={nargs!r}"
523
+ )
524
+ for v in default:
525
+ _check_default_elem(ctx, v, type_info)
526
+ else:
527
+ default = _action_implicit_default(action)
528
+ has_default = default is not None
529
+
530
+ if positionals:
531
+ if action != "store":
532
+ ctx.error(
533
+ f"argparse: action={action!r} is only valid for "
534
+ f"optional flags, not positional arguments"
535
+ )
536
+ if has_default and nargs != "?":
537
+ # nargs='?' positionals can use default= when the slot
538
+ # is missing from argv; non-'?' positionals are always
539
+ # required.
540
+ ctx.error(
541
+ "argparse: positional arguments may only specify "
542
+ "default= when nargs='?'"
543
+ )
544
+ if required_kw:
545
+ ctx.error(
546
+ "argparse: required= is meaningless for positional "
547
+ "arguments (they are always required)"
548
+ )
549
+ name = positionals[0]
550
+ dest = explicit_dest if explicit_dest is not None else name
551
+ return _ArgSpec(
552
+ is_flag=False, flag_names=[], name=name, dest=dest,
553
+ type_info=type_info, is_arg_type=is_arg_type, action=action,
554
+ default=default, has_default=has_default,
555
+ choices=choices, required=True,
556
+ nargs=nargs, help_text=help_text,
557
+ metavar=metavar,
558
+ )
559
+
560
+ # Optional flag(s). Absent flags fall back to their default
561
+ # (or None when no default is given; field type becomes
562
+ # Optional[T] or Optional[list[T]] depending on action).
563
+ # nargs='?' on a flag uses const when the flag is bare and
564
+ # value when the flag carries one.
565
+ name = flags[0]
566
+ dest = _dest_for_flags(flags, explicit_dest)
567
+ if add_help_reserved:
568
+ for f in flags:
569
+ if f in ("-h", "--help"):
570
+ ctx.error(
571
+ "argparse: -h / --help is reserved by the "
572
+ "auto-generated help printer; pass "
573
+ "add_help=False to ArgumentParser() to "
574
+ "register your own"
575
+ )
576
+ return _ArgSpec(
577
+ is_flag=True, flag_names=list(flags), name=name, dest=dest,
578
+ type_info=type_info, is_arg_type=is_arg_type, action=action,
579
+ default=default, has_default=has_default,
580
+ const=const, has_const=has_const,
581
+ choices=choices, required=required_kw,
582
+ nargs=nargs, help_text=help_text,
583
+ metavar=metavar,
584
+ )
585
+
586
+
587
+ # ---------------------------------------------------------------------------
588
+ # Public macro: nested builders for subparsers
589
+ # ---------------------------------------------------------------------------
590
+
591
+ class _SubparserBuilder:
592
+ """One sub-parser registered via ``parser.add_subparsers().add_parser(name)``.
593
+
594
+ Holds its own arg specs; the top-level terminal walks every
595
+ sub-parser registered on the enclosing _SubparsersAction to
596
+ synthesize per-sub records and parse functions. v1 does not
597
+ auto-emit ``--help`` on sub-parsers, so ``add_help_reserved`` is
598
+ always false for sub-arg validation -- users may register their
599
+ own ``-h`` / ``--help`` here.
600
+ """
601
+
602
+ def __init__(self, name: str, help_text: str | None) -> None:
603
+ self.name = name
604
+ self.help_text = help_text
605
+ self.specs: list[_ArgSpec] = []
606
+ self._dest_seen: set[str] = set()
607
+
608
+ @builder_method
609
+ def add_argument(self, ctx: BuilderContext, args: MacroArgs) -> None:
610
+ spec = _build_arg_spec(ctx, args, add_help_reserved=False)
611
+ if spec.dest in self._dest_seen:
612
+ ctx.error(f"argparse: duplicate argument destination {spec.dest!r}")
613
+ self._dest_seen.add(spec.dest)
614
+ self.specs.append(spec)
615
+
616
+ @builder_method
617
+ def add_subparsers(self, ctx: BuilderContext, args: MacroArgs) -> None:
618
+ # No nested subparsers in v1: the top-level synth assumes a
619
+ # flat (top, sub*) shape so it can emit a single union field.
620
+ # Lifting this requires recursive sub-record synthesis and a
621
+ # nested union representation -- defer.
622
+ ctx.error(
623
+ "argparse: nested add_subparsers() is not supported in v1"
624
+ )
625
+
626
+
627
+ class _SubparsersAction:
628
+ """Result of ``parser.add_subparsers()``.
629
+
630
+ Tracks the subparser dispatch parameters (`dest=`, `required=`,
631
+ optional `help=`/`title=` for help-formatting hooks) and the
632
+ list of registered sub-parsers. Reachable through the trace via
633
+ ``add_parser(name)``, which returns a fresh _SubparserBuilder.
634
+ """
635
+
636
+ def __init__(
637
+ self, *, dest: str, required: bool,
638
+ title: str | None, action_help: str | None,
639
+ loc: object,
640
+ ) -> None:
641
+ self.dest = dest
642
+ self.required = required
643
+ self.title = title
644
+ self.action_help = action_help
645
+ self.loc = loc
646
+ self.subparsers: list[_SubparserBuilder] = []
647
+ self._names_seen: set[str] = set()
648
+
649
+ @builder_returns(_SubparserBuilder)
650
+ def add_parser(
651
+ self, ctx: BuilderContext, args: MacroArgs,
652
+ ) -> _SubparserBuilder:
653
+ if not args.positional:
654
+ ctx.error("argparse: add_parser() requires a name argument")
655
+ name = ctx.positional_str(args, 0)
656
+ if name in self._names_seen:
657
+ ctx.error(f"argparse: duplicate sub-parser name {name!r}")
658
+ if not name or name.startswith("-"):
659
+ ctx.error(
660
+ f"argparse: sub-parser name must be a non-empty "
661
+ f"non-flag token, got {name!r}"
662
+ )
663
+ self._names_seen.add(name)
664
+ help_text = ctx.kwarg_str(args, "help")
665
+ sub = _SubparserBuilder(name=name, help_text=help_text)
666
+ self.subparsers.append(sub)
667
+ return sub
668
+
669
+
670
+ # ---------------------------------------------------------------------------
671
+ # Public macro: ArgumentParser
672
+ # ---------------------------------------------------------------------------
673
+
674
+ @builder_macro
675
+ class ArgumentParser:
676
+ def __init__(self, ctx: BuilderContext, args: MacroArgs) -> None:
677
+ self.description: str | None = ctx.kwarg_str(args, "description")
678
+ # Help-text customization. ``prog`` substitutes for the
679
+ # default ``"prog"`` placeholder in the usage line and
680
+ # parse-error prefix; ``usage`` overrides the auto-generated
681
+ # usage line entirely (CPython prefixes it with ``"usage: "``);
682
+ # ``epilog`` is appended after the options block in --help.
683
+ # ``add_help`` controls whether the ``-h`` / ``--help`` printer
684
+ # is auto-emitted and the flag names reserved.
685
+ self.prog: str | None = ctx.kwarg_str(args, "prog")
686
+ self.usage: str | None = ctx.kwarg_str(args, "usage")
687
+ self.epilog: str | None = ctx.kwarg_str(args, "epilog")
688
+ self.add_help: bool = ctx.kwarg_bool(args, "add_help", True)
689
+ self.specs: list[_ArgSpec] = []
690
+ self._dest_seen: set[str] = set()
691
+ # Set by add_subparsers(); a single _SubparsersAction holds
692
+ # every registered sub-parser. v1 allows at most one call.
693
+ self._subparsers: _SubparsersAction | None = None
694
+
695
+ @builder_method
696
+ def add_argument(self, ctx: BuilderContext, args: MacroArgs) -> None:
697
+ spec = _build_arg_spec(ctx, args, add_help_reserved=self.add_help)
698
+ if spec.dest in self._dest_seen:
699
+ ctx.error(f"argparse: duplicate argument destination {spec.dest!r}")
700
+ self._dest_seen.add(spec.dest)
701
+ self.specs.append(spec)
702
+
703
+ @builder_returns(_SubparsersAction)
704
+ def add_subparsers(
705
+ self, ctx: BuilderContext, args: MacroArgs,
706
+ ) -> _SubparsersAction:
707
+ if self._subparsers is not None:
708
+ ctx.error(
709
+ "argparse: add_subparsers() can only be called once "
710
+ "per parser"
711
+ )
712
+ # CPython argparse rejects sub-parsers when any positional
713
+ # argument is already registered (the regex matcher can't
714
+ # disambiguate where the subcommand starts). v1 keeps that
715
+ # restriction; lifting it requires the regex matcher.
716
+ for s in self.specs:
717
+ if not s.is_flag:
718
+ ctx.error(
719
+ "argparse: add_subparsers() is not supported when "
720
+ "the parser also has positional arguments"
721
+ )
722
+ dest = ctx.kwarg_str(args, "dest") or "cmd"
723
+ if dest in self._dest_seen:
724
+ ctx.error(
725
+ f"argparse: subparsers dest={dest!r} collides with "
726
+ f"a top-level argument of the same dest"
727
+ )
728
+ required = ctx.kwarg_bool(args, "required", False)
729
+ title = ctx.kwarg_str(args, "title")
730
+ action_help = ctx.kwarg_str(args, "help")
731
+ action = _SubparsersAction(
732
+ dest=dest, required=required,
733
+ title=title, action_help=action_help,
734
+ loc=ctx.call_loc,
735
+ )
736
+ self._subparsers = action
737
+ return action
738
+
739
+ @builder_terminal
740
+ def parse_args(self, ctx: BuilderContext, args: MacroArgs) -> TypeInfo:
741
+ # CPython's ``parse_args()`` falls back to ``sys.argv[1:]``
742
+ # when called with no arguments. ``sys`` is injected into the
743
+ # user's macro_ns via this module's ``macro_deps("tpy", "sys")``
744
+ # declaration, so the synthesized rewrite can reference it
745
+ # without the user having imported sys explicitly.
746
+ if not args.positional:
747
+ # ``list(...)`` materializes a fresh list[str] from the
748
+ # span returned by ``sys.argv[1:]`` so the synthesized
749
+ # parse fn (which takes list[str]) accepts it.
750
+ args.positional.append(MacroArg(
751
+ expr=ast.quote_expr("list(sys.argv[1:])"),
752
+ type=TypeInfo("?", _tpy_type=None),
753
+ ))
754
+
755
+ if self._subparsers is None:
756
+ return self._terminal_single(ctx, args)
757
+ return self._terminal_with_subparsers(ctx, args)
758
+
759
+ def _terminal_single(
760
+ self, ctx: BuilderContext, args: MacroArgs,
761
+ ) -> TypeInfo:
762
+ # Validate positional layout before code synthesis: ``*``,
763
+ # ``+``, and ``?`` positionals must be the LAST positional --
764
+ # earlier ones can't be deterministically consumed without
765
+ # CPython's full regex-fitting algorithm.
766
+ positional_specs = [s for s in self.specs if not s.is_flag]
767
+ for i, s in enumerate(positional_specs):
768
+ if s.nargs in ("*", "+", "?") and i != len(positional_specs) - 1:
769
+ ctx.error(
770
+ f"argparse: positional {s.name!r} with "
771
+ f"nargs={s.nargs!r} must be the last positional"
772
+ )
773
+
774
+ record_name = ctx.fresh_module_name("argparse_args")
775
+ record_fields = [(s.dest, s.field_type) for s in self.specs]
776
+ record_type = ctx.emit_record(record_name, record_fields)
777
+
778
+ prog = self.prog if self.prog is not None else _DEFAULT_PROG
779
+
780
+ # Render usage once and reuse it for both the parse-error path
781
+ # and the help-printer body so we don't walk specs twice.
782
+ usage_text = _format_usage(
783
+ self.specs, prog=prog, usage=self.usage,
784
+ include_help_opt=self.add_help,
785
+ )
786
+
787
+ # Synthesize the help-printer (when add_help=True) so the parse
788
+ # fn's `-h` / `--help` detection prelude can reference its name.
789
+ # Pre-render the entire help text at macro time -- the printer
790
+ # body is just `print(<literal>); sys.exit(0)`.
791
+ help_fn_name: str | None = None
792
+ if self.add_help:
793
+ help_fn_name = ctx.fresh_module_name("argparse_help")
794
+ help_text = _format_help_text(
795
+ self.specs, self.description,
796
+ usage_text=usage_text, epilog=self.epilog,
797
+ )
798
+ ctx.emit_function(
799
+ help_fn_name, [], types.void,
800
+ ast.quote(f"print({help_text!r})\nsys.exit(Int32(0))"),
801
+ )
802
+ body = _build_parse_body(
803
+ self.specs, record_name, help_fn_name, usage_text,
804
+ prog=prog, add_help=self.add_help,
805
+ )
806
+
807
+ # ``argv`` is ``list[str]`` rather than ``Span[str]`` so an
808
+ # empty list literal at the call site (``parse_args([])``)
809
+ # infers its element type from the parameter -- empty-literal
810
+ # inference doesn't reach through Span[T] context today.
811
+ fn_name = ctx.fresh_module_name("argparse_parse")
812
+ ctx.emit_function(
813
+ fn_name,
814
+ [("argv", types.list(types.str))],
815
+ record_type.raw_type,
816
+ body,
817
+ )
818
+ ctx.replace_call(fn_name, args)
819
+ return record_type
820
+
821
+ def _terminal_with_subparsers(
822
+ self, ctx: BuilderContext, args: MacroArgs,
823
+ ) -> TypeInfo:
824
+ """Synthesize the subparsers-aware parser.
825
+
826
+ Layout (Phase 1, no property forwarders):
827
+ per_sub_record_i: <fields for sub_i> # one record per sub-parser
828
+ per_sub_parse_i: list[str] -> per_sub_record_i # one parse fn per sub
829
+ top_record: <common arg fields>
830
+ <dest=:str | Optional[str]> # CPython-shape subcommand name
831
+ <_subcommand: Union[Sub] | Optional[Union[Sub]]>
832
+ # TPy-only typed union
833
+ top_parse: list[str] -> top_record
834
+
835
+ Top-level parse:
836
+ - --help / -h scan (top help only; sub-help is v2)
837
+ - main loop dispatches common flags
838
+ - non-flag token -> sub-parser dispatch:
839
+ tail = list(argv[i+1:]); _subcommand = sub_parse_<name>(tail); break
840
+ - post-loop required-flag and required-subparser checks.
841
+ """
842
+ sp = self._subparsers
843
+ assert sp is not None
844
+ if not sp.subparsers:
845
+ ctx.error(
846
+ "argparse: add_subparsers() requires at least one "
847
+ "add_parser() call before parse_args()"
848
+ )
849
+
850
+ # Per-sub vs common-arg collisions are checked further below
851
+ # when per-sub flat fields are added to the top record.
852
+ # ``add_subparsers`` already rejected ``sp.dest`` colliding
853
+ # with an existing common-arg dest at registration time.
854
+ top_dests = {s.dest for s in self.specs}
855
+
856
+ prog = self.prog if self.prog is not None else _DEFAULT_PROG
857
+
858
+ # Synthesize per-sub records + per-sub parse fns. Each sub's
859
+ # error path uses ``"<top-prog> <sub-name>"`` as the prog so
860
+ # error messages match CPython's "prog cmd: error: ..." shape.
861
+ sub_record_types: list = [] # parallel to sp.subparsers
862
+ sub_record_names: list[str] = [] # bare names, parallel to sp.subparsers
863
+ sub_parse_fn_names: list[str] = []
864
+ for sub in sp.subparsers:
865
+ positional_in_sub = [x for x in sub.specs if not x.is_flag]
866
+ for i, s in enumerate(positional_in_sub):
867
+ if s.nargs in ("*", "+", "?") and i != len(positional_in_sub) - 1:
868
+ ctx.error(
869
+ f"argparse: positional {s.name!r} with "
870
+ f"nargs={s.nargs!r} must be the last "
871
+ f"positional in sub-parser {sub.name!r}"
872
+ )
873
+ sub_record_name = ctx.fresh_module_name(f"argparse_{sub.name}_args")
874
+ sub_fields = [(s.dest, s.field_type) for s in sub.specs]
875
+ sub_record_type = ctx.emit_record(sub_record_name, sub_fields)
876
+ sub_record_types.append(sub_record_type)
877
+ sub_record_names.append(sub_record_name)
878
+
879
+ sub_prog = f"{prog} {sub.name}"
880
+ sub_usage_text = _format_usage(
881
+ sub.specs, prog=sub_prog, usage=None,
882
+ include_help_opt=False,
883
+ )
884
+ sub_body = _build_parse_body(
885
+ sub.specs, sub_record_name, None, sub_usage_text,
886
+ prog=sub_prog, add_help=False,
887
+ )
888
+ sub_fn_name = ctx.fresh_module_name(f"argparse_{sub.name}_parse")
889
+ ctx.emit_function(
890
+ sub_fn_name,
891
+ [("argv", types.list(types.str))],
892
+ sub_record_type.raw_type,
893
+ sub_body,
894
+ )
895
+ sub_parse_fn_names.append(sub_fn_name)
896
+
897
+ cmd_field_type = types.str if sp.required else types.optional(types.str)
898
+
899
+ # Per-sub flat fields on the top record. CPython argparse
900
+ # exposes per-sub args directly as Optional[T] attributes on
901
+ # the Namespace; we mirror that. A typed-union escape hatch
902
+ # ``_subcommand: A | B`` is intentionally NOT stored: users
903
+ # can't reference the synthesized sub-record names (they
904
+ # carry the ``__tpy_builder_`` private prefix), so a stored
905
+ # union would be unreachable. Once builder-trace expansion
906
+ # moves to a dedicated pre-pass-6 pass (see MACRO_DESIGN.md's
907
+ # "Move builder-trace expansion to a dedicated pre-pass-6
908
+ # pass"), property forwarders that match over the union
909
+ # become viable and the union storage can be added back as
910
+ # the underlying state.
911
+ flat_field_map: dict[str, list[tuple[int, _ArgSpec]]] = {}
912
+ for sub_idx, sub in enumerate(sp.subparsers):
913
+ for spec in sub.specs:
914
+ flat_field_map.setdefault(spec.dest, []).append(
915
+ (sub_idx, spec)
916
+ )
917
+ flat_fields: list[tuple[str, Type, list[tuple[int, _ArgSpec]]]] = []
918
+ # Reserve the cmd dest now -- the top record carries cmd as a
919
+ # stored field too, so a per-sub arg named the same as sp.dest
920
+ # would shadow it.
921
+ reserved_dests = {sp.dest}
922
+ for name in sorted(flat_field_map.keys()):
923
+ occurrences = flat_field_map[name]
924
+ if name in top_dests:
925
+ ctx.error(
926
+ f"argparse: per-sub argument {name!r} collides with "
927
+ f"a top-level argument of the same dest; rename one "
928
+ f"of them so the top record can carry distinct fields"
929
+ )
930
+ if name in reserved_dests:
931
+ ctx.error(
932
+ f"argparse: per-sub argument {name!r} is reserved "
933
+ f"by the subparsers machinery (matches the "
934
+ f"subcommand-name field on the top record)"
935
+ )
936
+ # Compare *unwrapped* field types: a sub with ``required=True``
937
+ # carries ``T`` and another with the same flag but no required=
938
+ # carries ``Optional[T]`` -- both unify to a single ``Optional[T]``
939
+ # field on the top record (matching CPython, which doesn't care
940
+ # which sub set the attribute). Reject only when the underlying
941
+ # base types disagree.
942
+ base_types = {_unwrap_optional(spec.field_type) for _, spec in occurrences}
943
+ if len(base_types) > 1:
944
+ ctx.error(
945
+ f"argparse: per-sub argument {name!r} has conflicting "
946
+ f"field types across sub-parsers; under Option B "
947
+ f"(flat namespace) the top record needs a single "
948
+ f"field type per name. Rename one of them, or unify "
949
+ f"the types (same ``type=`` and same default-shape)"
950
+ )
951
+ top_type = types.optional(next(iter(base_types)))
952
+ flat_fields.append((name, top_type, occurrences))
953
+
954
+ top_record_name = ctx.fresh_module_name("argparse_args")
955
+ top_fields: list = [(s.dest, s.field_type) for s in self.specs]
956
+ top_fields.append((sp.dest, cmd_field_type))
957
+ for name, top_type, _ in flat_fields:
958
+ top_fields.append((name, top_type))
959
+ top_record_type = ctx.emit_record(top_record_name, top_fields)
960
+
961
+ # Top-level usage / help. v1 lists subcommands as a single
962
+ # positional placeholder ``{a,b,c}`` to match CPython's output
963
+ # shape; per-sub help is not auto-emitted in v1.
964
+ usage_text = _format_usage(
965
+ self.specs, prog=prog, usage=self.usage,
966
+ include_help_opt=self.add_help,
967
+ subparser_action=sp,
968
+ )
969
+ help_fn_name: str | None = None
970
+ if self.add_help:
971
+ help_fn_name = ctx.fresh_module_name("argparse_help")
972
+ help_text = _format_help_text(
973
+ self.specs, self.description,
974
+ usage_text=usage_text, epilog=self.epilog,
975
+ subparser_action=sp,
976
+ )
977
+ ctx.emit_function(
978
+ help_fn_name, [], types.void,
979
+ ast.quote(f"print({help_text!r})\nsys.exit(Int32(0))"),
980
+ )
981
+
982
+ body = _build_subparser_parse_body(
983
+ self.specs, sp, sub_parse_fn_names, flat_fields,
984
+ top_record_name, help_fn_name, usage_text,
985
+ prog=prog, add_help=self.add_help,
986
+ )
987
+
988
+ fn_name = ctx.fresh_module_name("argparse_parse")
989
+ ctx.emit_function(
990
+ fn_name,
991
+ [("argv", types.list(types.str))],
992
+ top_record_type.raw_type,
993
+ body,
994
+ )
995
+ ctx.replace_call(fn_name, args)
996
+ return top_record_type
997
+
998
+
999
+ # ---------------------------------------------------------------------------
1000
+ # Internal: parse-function body synthesis
1001
+ # ---------------------------------------------------------------------------
1002
+
1003
+ def _emit_parse_prelude(
1004
+ specs: list[_ArgSpec], usage_text: str, *,
1005
+ add_help: bool, help_fn_name: str | None,
1006
+ ) -> tuple[list, list[str]]:
1007
+ """Returns ``(init_stmts, src_lines)`` for the boilerplate every
1008
+ parse-fn body opens with: usage local, optional ``-h``/``--help``
1009
+ pre-scan, per-spec init, required-flag-seen tracking. Shared by
1010
+ the single-parser and subparser-aware parse-body builders.
1011
+
1012
+ Both lists are returned mutable; callers append further AST
1013
+ decls / source lines onto them before assembling the final body
1014
+ (e.g. the subparser path adds ``acc_<dest>`` and per-sub flat
1015
+ locals to ``init_stmts`` and emits its dispatch loop into
1016
+ ``src_lines``).
1017
+
1018
+ The help fn prints help and calls ``sys.exit(0)``, so the
1019
+ pre-scan loop never returns from the call -- but its return type
1020
+ is ``None``, so sema sees normal flow and the post-call increment
1021
+ compiles fine.
1022
+ """
1023
+ init_stmts: list = []
1024
+ src_lines: list[str] = []
1025
+ src_lines.append(f"__tpy_argparse_usage = {usage_text!r}")
1026
+ if add_help:
1027
+ assert help_fn_name is not None
1028
+ src_lines.append("__tpy_argparse_h = 0")
1029
+ src_lines.append("while __tpy_argparse_h < len(argv):")
1030
+ src_lines.append(
1031
+ ' if argv[__tpy_argparse_h] == "-h" '
1032
+ 'or argv[__tpy_argparse_h] == "--help":'
1033
+ )
1034
+ src_lines.append(f" {help_fn_name}()")
1035
+ src_lines.append(" __tpy_argparse_h = __tpy_argparse_h + 1")
1036
+ for s in specs:
1037
+ init_stmts.extend(_spec_init_stmts(s))
1038
+ for s in specs:
1039
+ if s.is_flag and s.required:
1040
+ src_lines.append(f"__tpy_argparse_seen_{s.dest} = False")
1041
+ return init_stmts, src_lines
1042
+
1043
+
1044
+ def _emit_parse_reconciliation(
1045
+ specs: list[_ArgSpec], *, error_prefix: str,
1046
+ ) -> list[str]:
1047
+ """Returns ``src_lines`` for the post-loop reconciliation shared
1048
+ by both parse-body builders: required-flag missing checks, then
1049
+ Optional[list[T]] accumulator -> dest copy, then ArgType
1050
+ accumulator unwrap (assert + copy).
1051
+ """
1052
+ src_lines: list[str] = []
1053
+ for s in specs:
1054
+ if s.is_flag and s.required:
1055
+ src_lines.append(f"if not __tpy_argparse_seen_{s.dest}:")
1056
+ src_lines.extend(_error_emit_lines(
1057
+ " ", repr(f"missing required argument: {s.flag_names[0]}"),
1058
+ error_prefix=error_prefix,
1059
+ ))
1060
+ # Optional[list[T]] reconciliation: ``tpy.copy`` is here for the
1061
+ # implicit-copy warning, not correctness -- assigning a
1062
+ # ``list[T]`` local into an ``Optional[list[T]]`` slot warns even
1063
+ # when the local is at its last use. Codegen lowers ``copy`` to a
1064
+ # ``vector(acc)`` copy ctor, so this is one redundant allocation
1065
+ # per seen flag. Replace with a true move once TPy core gains
1066
+ # move-into-Optional.
1067
+ for s in specs:
1068
+ if s.is_optional_list_field:
1069
+ src_lines.append(f"if {_seen_local(s)}:")
1070
+ src_lines.append(f" {s.dest} = tpy.copy({_acc_local(s)})")
1071
+ # ArgType accumulator unwrap: missing-required checks above
1072
+ # already exited via sys.exit(2) when the accumulator stayed
1073
+ # None, so the assert is for sema's flow-narrowing
1074
+ # (Optional[T] -> T) rather than runtime safety. ``tpy.copy``
1075
+ # makes the implicit copy explicit so the record ctor (which
1076
+ # moves into owned storage) doesn't warn.
1077
+ for s in specs:
1078
+ if _needs_arg_type_accumulator(s):
1079
+ src_lines.append(f"assert {_acc_local(s)} is not None")
1080
+ src_lines.append(f"{s.dest} = tpy.copy({_acc_local(s)})")
1081
+ return src_lines
1082
+
1083
+
1084
+ def _build_parse_body(
1085
+ specs: list[_ArgSpec], record_name: str, help_fn_name: str | None,
1086
+ usage_text: str, *, prog: str, add_help: bool,
1087
+ ) -> list:
1088
+ """Generate the full body of the synthesized parse function.
1089
+
1090
+ Layout:
1091
+ <init defaults / typed empty lists>
1092
+ <seen-tracking for required flags>
1093
+ __tpy_argparse_i = 0
1094
+ __tpy_argparse_pi = 0
1095
+ while __tpy_argparse_i < len(argv):
1096
+ __tpy_argparse_tok = argv[__tpy_argparse_i]
1097
+ if __tpy_argparse_tok == "--flag1": ...handler advances i...
1098
+ elif ...: ...
1099
+ else: ...positional dispatch advances i and pi...
1100
+ <missing-positional check>
1101
+ <required-flag check>
1102
+ return Record(...)
1103
+ """
1104
+ error_prefix = f"{prog}: error: "
1105
+
1106
+ # Empty parser: no add_argument() was called. Reject any argv
1107
+ # tokens (matching CPython argparse) and return an empty record
1108
+ # without entering the main loop -- the loop body would otherwise
1109
+ # never advance __tpy_argparse_i and hang on non-empty argv.
1110
+ if not specs:
1111
+ init_stmts, src_lines = _emit_parse_prelude(
1112
+ specs, usage_text, add_help=add_help, help_fn_name=help_fn_name,
1113
+ )
1114
+ src_lines.append("if len(argv) != 0:")
1115
+ src_lines.extend(_error_emit_lines(
1116
+ " ", '"unrecognized arguments: " + argv[0]',
1117
+ error_prefix=error_prefix,
1118
+ ))
1119
+ src_lines.append(f"return {record_name}()")
1120
+ return init_stmts + ast.quote("\n".join(src_lines))
1121
+
1122
+ init_stmts, src_lines = _emit_parse_prelude(
1123
+ specs, usage_text, add_help=add_help, help_fn_name=help_fn_name,
1124
+ )
1125
+
1126
+ flag_specs = [s for s in specs if s.is_flag]
1127
+ positional_specs = [s for s in specs if not s.is_flag]
1128
+
1129
+ # --- Main loop ---
1130
+ src_lines.append("__tpy_argparse_i = 0")
1131
+ if positional_specs:
1132
+ src_lines.append("__tpy_argparse_pi = 0")
1133
+ src_lines.append("while __tpy_argparse_i < len(argv):")
1134
+ src_lines.append(" __tpy_argparse_tok = argv[__tpy_argparse_i]")
1135
+
1136
+ # Flag dispatch
1137
+ first = True
1138
+ for s in flag_specs:
1139
+ match_expr = " or ".join(
1140
+ f"__tpy_argparse_tok == {f!r}" for f in s.flag_names
1141
+ )
1142
+ kw = "if" if first else "elif"
1143
+ first = False
1144
+ src_lines.append(f" {kw} {match_expr}:")
1145
+ for line in _flag_handler_lines(s, indent=" ",
1146
+ error_prefix=error_prefix):
1147
+ src_lines.append(line)
1148
+ if s.required:
1149
+ src_lines.append(f" __tpy_argparse_seen_{s.dest} = True")
1150
+
1151
+ # Positional dispatch
1152
+ if positional_specs:
1153
+ kw = "else" if not first else "if True"
1154
+ src_lines.append(f" {kw}:")
1155
+ inner_first = True
1156
+ for pi, s in enumerate(positional_specs):
1157
+ inner_kw = "if" if inner_first else "elif"
1158
+ inner_first = False
1159
+ src_lines.append(f" {inner_kw} __tpy_argparse_pi == {pi}:")
1160
+ for line in _positional_handler_lines(
1161
+ s, indent=" ", error_prefix=error_prefix,
1162
+ ):
1163
+ src_lines.append(line)
1164
+ src_lines.append(" else:")
1165
+ src_lines.extend(_error_emit_lines(
1166
+ " ",
1167
+ '"unexpected positional argument: " + __tpy_argparse_tok',
1168
+ error_prefix=error_prefix,
1169
+ ))
1170
+ elif not first:
1171
+ src_lines.append(" else:")
1172
+ src_lines.extend(_error_emit_lines(
1173
+ " ",
1174
+ '"unknown argument: " + __tpy_argparse_tok',
1175
+ error_prefix=error_prefix,
1176
+ ))
1177
+
1178
+ # Missing-positional check: every required positional slot must
1179
+ # have been filled. Variable-nargs trailing positionals (* / ?)
1180
+ # are themselves optional; +/N/none are required.
1181
+ # nargs='+' on the trailing positional must have got at least
1182
+ # one value; the dispatch sets pi only when consumed, so the
1183
+ # count check below already covers this.
1184
+ required_positional_count = sum(
1185
+ 1 for s in positional_specs
1186
+ if s.nargs not in ("*", "?")
1187
+ )
1188
+ if required_positional_count > 0:
1189
+ src_lines.append(f"if __tpy_argparse_pi < {required_positional_count}:")
1190
+ src_lines.extend(_error_emit_lines(
1191
+ " ", '"missing required positional argument(s)"',
1192
+ error_prefix=error_prefix,
1193
+ ))
1194
+
1195
+ src_lines.extend(_emit_parse_reconciliation(specs, error_prefix=error_prefix))
1196
+
1197
+ ctor_args = ", ".join(s.dest for s in specs)
1198
+ src_lines.append(f"return {record_name}({ctor_args})")
1199
+
1200
+ return init_stmts + ast.quote("\n".join(src_lines))
1201
+
1202
+
1203
+ def _spec_init_stmts(spec: _ArgSpec) -> list:
1204
+ """Initialization statements for one spec.
1205
+
1206
+ Type-bearing decls (typed list, Optional[T]) go through ``ast.*``
1207
+ so the annotation is a TpyType the per-module resolver doesn't
1208
+ need to import. Plain scalar inits go through source text so
1209
+ default-literal rendering stays compact.
1210
+ """
1211
+ if spec.is_optional_list_field:
1212
+ # Three locals: dest field (Optional[list[T]] = None), the
1213
+ # accumulator the flag handler writes into, and a seen flag
1214
+ # so the post-loop reconciliation can distinguish "absent"
1215
+ # from "present with zero values".
1216
+ elem = spec.type_info.raw_type
1217
+ return [
1218
+ ast.var_decl(
1219
+ spec.dest, types.optional(types.list(elem)), ast.none_lit()
1220
+ ),
1221
+ ast.var_decl(
1222
+ _acc_local(spec), types.list(elem), ast.list_lit()
1223
+ ),
1224
+ *ast.quote(f"{_seen_local(spec)} = False"),
1225
+ ]
1226
+ if spec.is_list_field:
1227
+ elem = spec.type_info.raw_type
1228
+ if spec.has_default:
1229
+ # Non-empty list literal default. Element-typed list
1230
+ # initializer with each element wrapped through the
1231
+ # spec's value-conversion (handles fixed-width int
1232
+ # constructor wrapping the same way scalar defaults do).
1233
+ elements = [_render_default_elem(v, spec.type_info)
1234
+ for v in spec.default]
1235
+ return [ast.var_decl(
1236
+ spec.dest, types.list(elem), ast.list_lit(elements),
1237
+ )]
1238
+ return [ast.var_decl(spec.dest, types.list(elem), ast.list_lit())]
1239
+ if spec.is_optional_field:
1240
+ if spec.action == "store_const":
1241
+ scalar = _python_value_type_info(spec.const).raw_type
1242
+ else:
1243
+ scalar = spec.type_info.raw_type
1244
+ return [ast.var_decl(
1245
+ spec.dest, types.optional(scalar), ast.none_lit()
1246
+ )]
1247
+ if _needs_arg_type_accumulator(spec):
1248
+ # No zero-arg sentinel for the user type. Init only the
1249
+ # ``Optional[T]`` accumulator; ``spec.dest`` is rebound to the
1250
+ # unwrapped ``T`` after the post-loop missing-required check.
1251
+ scalar = spec.type_info.raw_type
1252
+ return [ast.var_decl(
1253
+ _acc_local(spec), types.optional(scalar), ast.none_lit()
1254
+ )]
1255
+ return ast.quote(f"{spec.dest} = {_default_expr_src(spec)}")
1256
+
1257
+
1258
+ def _render_default_elem(value, ti: TypeInfo):
1259
+ """AST expression for one element of a list-typed default.
1260
+
1261
+ Mirrors ``_default_expr_src``'s wrapping at the AST level:
1262
+ ``Int32(1)`` / ``Float32(0.5)`` for fixed-width primitives so the
1263
+ typed list annotation stays consistent regardless of options.json
1264
+ defaults and Float32 elements don't widen to Float64.
1265
+ """
1266
+ if ti.is_str:
1267
+ return ast.str_lit(str(value))
1268
+ if ti.is_bigint:
1269
+ return ast.int_lit(int(value))
1270
+ if ti.is_float32:
1271
+ return ast.call("Float32", [ast.float_lit(float(value))])
1272
+ if ti.is_float:
1273
+ return ast.float_lit(float(value))
1274
+ # Fixed-width int: wrap with the constructor so the literal type
1275
+ # matches the field type regardless of options.json's default_int.
1276
+ return ast.call(_arg_type_name(ti), [ast.int_lit(int(value))])
1277
+
1278
+
1279
+ def _acc_local(spec: _ArgSpec) -> str:
1280
+ return f"__tpy_argparse_acc_{spec.dest}"
1281
+
1282
+
1283
+ def _seen_local(spec: _ArgSpec) -> str:
1284
+ return f"__tpy_argparse_seen_{spec.dest}"
1285
+
1286
+
1287
+ def _list_target(spec: _ArgSpec) -> str:
1288
+ """Local the flag handler writes into: accumulator for
1289
+ Optional[list[T]] (post-loop reconciled via tpy.copy), dest field
1290
+ directly otherwise.
1291
+ """
1292
+ return _acc_local(spec) if spec.is_optional_list_field else spec.dest
1293
+
1294
+
1295
+ def _needs_arg_type_accumulator(spec: _ArgSpec) -> bool:
1296
+ """True for ArgType specs whose record field is non-Optional T but
1297
+ whose parse local has to start as ``Optional[T]`` (no zero-arg
1298
+ sentinel for the user type). Covers required positional + required
1299
+ flag without a string default. Optional flags without default
1300
+ already get an Optional[T] field via ``is_optional_field``, so
1301
+ they don't need the accumulator+narrow dance. List-typed fields
1302
+ (``nargs=*/+/<int>`` or append/extend) own a ``list[T]`` directly
1303
+ and never reach the scalar-unwrap path either.
1304
+ """
1305
+ if not spec.is_arg_type:
1306
+ return False
1307
+ if spec.has_default:
1308
+ return False
1309
+ if spec.is_optional_field:
1310
+ return False
1311
+ if spec.is_list_field:
1312
+ return False
1313
+ return True
1314
+
1315
+
1316
+ def _store_target(spec: _ArgSpec) -> str:
1317
+ """Local the ``store`` handler writes into. For ArgType specs that
1318
+ need the accumulator-then-narrow pattern, that's ``_acc_<dest>``
1319
+ (Optional[T]); a post-missing-check rebind to ``<dest>`` (T) lands
1320
+ the unwrapped value in the name the record constructor consumes.
1321
+ """
1322
+ return _acc_local(spec) if _needs_arg_type_accumulator(spec) else spec.dest
1323
+
1324
+
1325
+ def _flag_handler_lines(
1326
+ spec: _ArgSpec, *, indent: str,
1327
+ error_prefix: str = _DEFAULT_ERROR_PREFIX,
1328
+ ) -> list[str]:
1329
+ """Source lines that consume the matched flag plus its values
1330
+ (per nargs/action) and advance __tpy_argparse_i.
1331
+ """
1332
+ L: list[str] = []
1333
+ a = spec.action
1334
+ if a == "store_true":
1335
+ L.append(f"{indent}{spec.dest} = True")
1336
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1337
+ return L
1338
+ if a == "store_false":
1339
+ L.append(f"{indent}{spec.dest} = False")
1340
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1341
+ return L
1342
+ if a == "count":
1343
+ L.append(f"{indent}{spec.dest} = {spec.dest} + 1")
1344
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1345
+ return L
1346
+ if a == "store_const":
1347
+ const_repr = _literal_repr(spec.const)
1348
+ L.append(f"{indent}{spec.dest} = {const_repr}")
1349
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1350
+ return L
1351
+
1352
+ target = _list_target(spec)
1353
+
1354
+ # Value-taking actions: store / append / extend, with optional nargs.
1355
+ if spec.nargs is None:
1356
+ # Single value, advance by 2.
1357
+ value_expr = _value_expr_src(spec, "argv[__tpy_argparse_i + 1]")
1358
+ L.append(f"{indent}if __tpy_argparse_i + 1 >= len(argv):")
1359
+ L.extend(_error_emit_lines(
1360
+ f"{indent} ", '"missing value for " + __tpy_argparse_tok',
1361
+ error_prefix=error_prefix,
1362
+ ))
1363
+ if spec.choices:
1364
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1365
+ L.append(f"{indent}{tmp} = {value_expr}")
1366
+ for line in _choices_check_lines(
1367
+ spec, tmp, indent=indent, error_prefix=error_prefix,
1368
+ ):
1369
+ L.append(line)
1370
+ if a == "store":
1371
+ L.append(f"{indent}{_store_target(spec)} = {tmp}")
1372
+ else:
1373
+ L.append(f"{indent}{target}.append({tmp})")
1374
+ else:
1375
+ if a == "store":
1376
+ L.append(f"{indent}{_store_target(spec)} = {value_expr}")
1377
+ else:
1378
+ L.append(f"{indent}{target}.append({value_expr})")
1379
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 2")
1380
+ if spec.is_optional_list_field:
1381
+ L.append(f"{indent}{_seen_local(spec)} = True")
1382
+ return L
1383
+
1384
+ if spec.nargs == "?":
1385
+ # 0 or 1 value: peek at next token; if absent or starts
1386
+ # with '-' use const, otherwise consume one value.
1387
+ L.append(
1388
+ f"{indent}if __tpy_argparse_i + 1 < len(argv) and not "
1389
+ f"argv[__tpy_argparse_i + 1].startswith(\"-\"):"
1390
+ )
1391
+ value_expr = _value_expr_src(spec, "argv[__tpy_argparse_i + 1]")
1392
+ if spec.choices:
1393
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1394
+ L.append(f"{indent} {tmp} = {value_expr}")
1395
+ for line in _choices_check_lines(
1396
+ spec, tmp, indent=indent + " ", error_prefix=error_prefix,
1397
+ ):
1398
+ L.append(line)
1399
+ L.append(f"{indent} {spec.dest} = {tmp}")
1400
+ else:
1401
+ L.append(f"{indent} {spec.dest} = {value_expr}")
1402
+ L.append(f"{indent} __tpy_argparse_i = __tpy_argparse_i + 2")
1403
+ L.append(f"{indent}else:")
1404
+ if spec.has_const:
1405
+ const_repr = _literal_repr(spec.const)
1406
+ L.append(f"{indent} {spec.dest} = {const_repr}")
1407
+ # else: leave field at its initial default
1408
+ L.append(f"{indent} __tpy_argparse_i = __tpy_argparse_i + 1")
1409
+ return L
1410
+
1411
+ # nargs in {'*', '+', int}: scan forward consuming non-flag
1412
+ # tokens, appending into the field directly. The field is
1413
+ # already typed list[T] (or Optional[list[T]]) from the init
1414
+ # pass; for action=store the field is cleared at the top of
1415
+ # the match so repeated flags replace rather than accumulate.
1416
+ if a == "store":
1417
+ # Reassign to empty list -- field type was set at init.
1418
+ L.append(f"{indent}{target} = []")
1419
+ L.append(f"{indent}__tpy_argparse_j = __tpy_argparse_i + 1")
1420
+ L.append(
1421
+ f"{indent}while __tpy_argparse_j < len(argv) and not "
1422
+ f"argv[__tpy_argparse_j].startswith(\"-\"):"
1423
+ )
1424
+ inner_value = _value_expr_src(spec, "argv[__tpy_argparse_j]")
1425
+ if spec.choices:
1426
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1427
+ L.append(f"{indent} {tmp} = {inner_value}")
1428
+ for line in _choices_check_lines(
1429
+ spec, tmp, indent=indent + " ", error_prefix=error_prefix,
1430
+ ):
1431
+ L.append(line)
1432
+ L.append(f"{indent} {target}.append({tmp})")
1433
+ else:
1434
+ L.append(f"{indent} {target}.append({inner_value})")
1435
+ L.append(f"{indent} __tpy_argparse_j = __tpy_argparse_j + 1")
1436
+ consumed = "(__tpy_argparse_j - __tpy_argparse_i - 1)"
1437
+ if isinstance(spec.nargs, int):
1438
+ L.append(f"{indent}if {consumed} != {spec.nargs}:")
1439
+ L.extend(_error_emit_lines(
1440
+ f"{indent} ",
1441
+ f'__tpy_argparse_tok + " requires exactly {spec.nargs} value(s)"',
1442
+ error_prefix=error_prefix,
1443
+ ))
1444
+ elif spec.nargs == "+":
1445
+ L.append(f"{indent}if {consumed} == 0:")
1446
+ L.extend(_error_emit_lines(
1447
+ f"{indent} ",
1448
+ '__tpy_argparse_tok + " requires at least one value"',
1449
+ error_prefix=error_prefix,
1450
+ ))
1451
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_j")
1452
+ if spec.is_optional_list_field:
1453
+ L.append(f"{indent}{_seen_local(spec)} = True")
1454
+ return L
1455
+
1456
+
1457
+ def _positional_handler_lines(
1458
+ spec: _ArgSpec, *, indent: str,
1459
+ error_prefix: str = _DEFAULT_ERROR_PREFIX,
1460
+ ) -> list[str]:
1461
+ """Source lines that handle one positional slot when the loop
1462
+ falls through to the else branch. Each branch advances both
1463
+ __tpy_argparse_i and __tpy_argparse_pi.
1464
+ """
1465
+ L: list[str] = []
1466
+ if spec.nargs is None:
1467
+ value_expr = _value_expr_src(spec, "__tpy_argparse_tok")
1468
+ if spec.choices:
1469
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1470
+ L.append(f"{indent}{tmp} = {value_expr}")
1471
+ for line in _choices_check_lines(
1472
+ spec, tmp, indent=indent, error_prefix=error_prefix,
1473
+ ):
1474
+ L.append(line)
1475
+ L.append(f"{indent}{_store_target(spec)} = {tmp}")
1476
+ else:
1477
+ L.append(f"{indent}{_store_target(spec)} = {value_expr}")
1478
+ L.append(f"{indent}__tpy_argparse_pi = __tpy_argparse_pi + 1")
1479
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1480
+ return L
1481
+ if isinstance(spec.nargs, int):
1482
+ N = spec.nargs
1483
+ L.append(f"{indent}if __tpy_argparse_i + {N} > len(argv):")
1484
+ L.extend(_error_emit_lines(
1485
+ f"{indent} ",
1486
+ repr(f"positional {spec.name!r} requires {N} value(s)"),
1487
+ error_prefix=error_prefix,
1488
+ ))
1489
+ L.append(f"{indent}__tpy_argparse_k = 0")
1490
+ L.append(f"{indent}while __tpy_argparse_k < {N}:")
1491
+ idx_expr = "argv[__tpy_argparse_i + __tpy_argparse_k]"
1492
+ inner_value = _value_expr_src(spec, idx_expr)
1493
+ if spec.choices:
1494
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1495
+ L.append(f"{indent} {tmp} = {inner_value}")
1496
+ for line in _choices_check_lines(
1497
+ spec, tmp, indent=indent + " ", error_prefix=error_prefix,
1498
+ ):
1499
+ L.append(line)
1500
+ L.append(f"{indent} {spec.dest}.append({tmp})")
1501
+ else:
1502
+ L.append(f"{indent} {spec.dest}.append({inner_value})")
1503
+ L.append(f"{indent} __tpy_argparse_k = __tpy_argparse_k + 1")
1504
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + {N}")
1505
+ L.append(f"{indent}__tpy_argparse_pi = __tpy_argparse_pi + 1")
1506
+ return L
1507
+ if spec.nargs == "?":
1508
+ # consume exactly one (the current token); falling off the end
1509
+ # is handled by the missing-positional check after the loop.
1510
+ value_expr = _value_expr_src(spec, "__tpy_argparse_tok")
1511
+ L.append(f"{indent}{spec.dest} = {value_expr}")
1512
+ L.append(f"{indent}__tpy_argparse_pi = __tpy_argparse_pi + 1")
1513
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1514
+ return L
1515
+ # nargs in {'*', '+'}: greedy -- consume the current token plus
1516
+ # all consecutive non-flag tokens that follow.
1517
+ value_expr = _value_expr_src(spec, "__tpy_argparse_tok")
1518
+ if spec.choices:
1519
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1520
+ L.append(f"{indent}{tmp} = {value_expr}")
1521
+ for line in _choices_check_lines(
1522
+ spec, tmp, indent=indent, error_prefix=error_prefix,
1523
+ ):
1524
+ L.append(line)
1525
+ L.append(f"{indent}{spec.dest}.append({tmp})")
1526
+ else:
1527
+ L.append(f"{indent}{spec.dest}.append({value_expr})")
1528
+ L.append(f"{indent}__tpy_argparse_i = __tpy_argparse_i + 1")
1529
+ L.append(
1530
+ f"{indent}while __tpy_argparse_i < len(argv) and not "
1531
+ f"argv[__tpy_argparse_i].startswith(\"-\"):"
1532
+ )
1533
+ inner_value = _value_expr_src(spec, "argv[__tpy_argparse_i]")
1534
+ if spec.choices:
1535
+ tmp = f"__tpy_argparse_v_{spec.dest}"
1536
+ L.append(f"{indent} {tmp} = {inner_value}")
1537
+ for line in _choices_check_lines(
1538
+ spec, tmp, indent=indent + " ", error_prefix=error_prefix,
1539
+ ):
1540
+ L.append(line)
1541
+ L.append(f"{indent} {spec.dest}.append({tmp})")
1542
+ else:
1543
+ L.append(f"{indent} {spec.dest}.append({inner_value})")
1544
+ L.append(f"{indent} __tpy_argparse_i = __tpy_argparse_i + 1")
1545
+ L.append(f"{indent}__tpy_argparse_pi = __tpy_argparse_pi + 1")
1546
+ return L
1547
+
1548
+
1549
+ def _default_expr_src(spec: _ArgSpec) -> str:
1550
+ """Source-text rendering of the spec's default expression.
1551
+
1552
+ Only called for scalar-typed fields; list-typed fields are
1553
+ initialized via ast.var_decl in _spec_init_stmts.
1554
+ """
1555
+ # Both optional flags and ``nargs='?'`` positionals can carry a
1556
+ # user-supplied default literal; render it as-is when present.
1557
+ # For fixed-width primitives, wrap with the constructor so the
1558
+ # literal carries the field's type rather than the default int /
1559
+ # float literal type.
1560
+ ti = spec.type_info
1561
+ if spec.has_default:
1562
+ rendered = _literal_repr(spec.default)
1563
+ if spec.is_arg_type:
1564
+ # Default is a string literal (validated at add_argument);
1565
+ # route through ``T.from_arg`` so the field carries T at
1566
+ # parse-fn entry. Mirrors CPython's "string defaults run
1567
+ # through type=" behaviour.
1568
+ return f"{ti.name}.from_arg({rendered})"
1569
+ if ti.is_int or ti.is_float32:
1570
+ return f"{_arg_type_name(ti)}({rendered})"
1571
+ return rendered
1572
+ if spec.action in ("store_true", "store_false"):
1573
+ return "False" if spec.action == "store_true" else "True"
1574
+ if spec.action == "count":
1575
+ return "0"
1576
+ if ti.is_bigint:
1577
+ return "0"
1578
+ if ti.is_int or ti.is_float32:
1579
+ # Constructor form so the literal type matches the field type
1580
+ # (regardless of options.json's default_int, and so Float32
1581
+ # fields don't get a Float64 init that widens the inferred type).
1582
+ return f"{_arg_type_name(ti)}(0)"
1583
+ if ti.is_float:
1584
+ return "0.0"
1585
+ return '""'
1586
+
1587
+
1588
+ def _choices_check_lines(
1589
+ spec: _ArgSpec, value_var: str, *, indent: str,
1590
+ error_prefix: str = _DEFAULT_ERROR_PREFIX,
1591
+ ) -> list[str]:
1592
+ """Source-text lines that emit a parse error to stderr + exit(2)
1593
+ if value_var is not in the spec's choices=. Empty when no choices=.
1594
+ """
1595
+ if not spec.choices:
1596
+ return []
1597
+ options = ", ".join(_literal_repr(c) for c in spec.choices)
1598
+ out = [f"{indent}if {value_var} not in ({options},):"]
1599
+ out.extend(_error_emit_lines(
1600
+ f"{indent} ",
1601
+ f'"invalid choice for {spec.name}: " + str({value_var})',
1602
+ error_prefix=error_prefix,
1603
+ ))
1604
+ return out
1605
+
1606
+
1607
+ def _value_expr_src(spec: _ArgSpec, source_expr: str) -> str:
1608
+ """Wrap a source expression with the spec's type conversion.
1609
+
1610
+ Fixed-width int / BigInt / float constructors all accept a string
1611
+ at runtime and parse it (panicking on overflow / invalid). The
1612
+ ``str`` case skips wrapping since argv tokens are already strings.
1613
+ Custom ``ArgType`` types route through ``T.from_arg(token)``.
1614
+ """
1615
+ if spec.type_info.is_str:
1616
+ return source_expr
1617
+ if spec.is_arg_type:
1618
+ return f"{spec.type_info.name}.from_arg({source_expr})"
1619
+ return f"{_arg_type_name(spec.type_info)}({source_expr})"
1620
+
1621
+
1622
+ def _literal_repr(value) -> str:
1623
+ """Source-text rendering of a literal default. Macro-time literal
1624
+ values flow through eval_literal_or_final, so values are plain
1625
+ Python int / float / str / bool / None.
1626
+ """
1627
+ if isinstance(value, str):
1628
+ return repr(value)
1629
+ if isinstance(value, bool):
1630
+ return "True" if value else "False"
1631
+ if value is None:
1632
+ return "None"
1633
+ return repr(value)
1634
+
1635
+
1636
+ def _error_emit_lines(
1637
+ indent: str, msg_expr: str, *, error_prefix: str = _DEFAULT_ERROR_PREFIX,
1638
+ ) -> list[str]:
1639
+ """Render the parse-error sequence at ``indent``.
1640
+
1641
+ Prints ``<usage>\\n<prog>: error: <msg>`` to stderr and calls
1642
+ ``sys.exit(Int32(2))``. ``msg_expr`` is a TPy source expression
1643
+ for the error message (string-typed, may concat a runtime
1644
+ token). The ``error_prefix`` parameter is a Python string baked
1645
+ in at macro time so the generated code keeps the prefix as a
1646
+ plain string literal -- threading it through the helpers (rather
1647
+ than via a runtime local) keeps generated output stable for
1648
+ callers that don't pass ``prog=``.
1649
+ """
1650
+ return [
1651
+ f'{indent}print(__tpy_argparse_usage, '
1652
+ f'{error_prefix!r} + {msg_expr}, sep="\\n", file=sys.stderr)',
1653
+ f"{indent}sys.exit(Int32(2))",
1654
+ ]
1655
+
1656
+
1657
+ # ---------------------------------------------------------------------------
1658
+ # Help-text formatting (also reused by parse-error path)
1659
+ # ---------------------------------------------------------------------------
1660
+
1661
+ _HELP_OPT_FORM = "-h, --help"
1662
+ _HELP_OPT_DESC = "show this help message and exit"
1663
+
1664
+
1665
+ def _metavar_for(spec: _ArgSpec) -> str:
1666
+ """Metavar shown in usage / help for a value-taking arg.
1667
+
1668
+ Honors an explicit ``metavar=`` override; otherwise flags use the
1669
+ dest in uppercase (matching CPython) and positionals use their
1670
+ literal name.
1671
+ """
1672
+ if spec.metavar is not None:
1673
+ return spec.metavar
1674
+ return spec.dest.upper() if spec.is_flag else spec.name
1675
+
1676
+
1677
+ def _nargs_pattern(token: str, nargs) -> str:
1678
+ """Render the metavar pattern for a given nargs value.
1679
+
1680
+ ``token`` is the metavar to repeat; ``nargs`` is the spec's nargs
1681
+ field. Returns just the metavar pattern (no flag prefix).
1682
+ """
1683
+ if nargs is None:
1684
+ return token
1685
+ if isinstance(nargs, int):
1686
+ return " ".join([token] * nargs)
1687
+ if nargs == "?":
1688
+ return f"[{token}]"
1689
+ if nargs == "*":
1690
+ return f"[{token} ...]"
1691
+ if nargs == "+":
1692
+ return f"{token} [{token} ...]"
1693
+ return token
1694
+
1695
+
1696
+ def _flag_usage_token(spec: _ArgSpec) -> str:
1697
+ """One flag's contribution to the usage line. Shows the first
1698
+ flag form only (CPython does the same to keep usage compact).
1699
+ """
1700
+ first = spec.flag_names[0]
1701
+ metavar = _metavar_for(spec)
1702
+ if spec.action in _VALUE_FREE_ACTIONS:
1703
+ body = first
1704
+ else:
1705
+ body = f"{first} {_nargs_pattern(metavar, spec.nargs)}"
1706
+ return body if spec.required else f"[{body}]"
1707
+
1708
+
1709
+ def _positional_usage_token(spec: _ArgSpec) -> str:
1710
+ return _nargs_pattern(_metavar_for(spec), spec.nargs)
1711
+
1712
+
1713
+ def _flag_help_signature(spec: _ArgSpec) -> str:
1714
+ """All flag forms with metavar, for the options section.
1715
+
1716
+ e.g. ``-n NAME, --name NAME`` -- CPython repeats the metavar on
1717
+ every form for clarity.
1718
+ """
1719
+ metavar = _metavar_for(spec)
1720
+ if spec.action in _VALUE_FREE_ACTIONS:
1721
+ return ", ".join(spec.flag_names)
1722
+ pattern = _nargs_pattern(metavar, spec.nargs)
1723
+ return ", ".join(f"{f} {pattern}" for f in spec.flag_names)
1724
+
1725
+
1726
+ _USAGE_TEXT_WIDTH = 80
1727
+ _USAGE_PREFIX = "usage: "
1728
+
1729
+
1730
+ def _format_usage(
1731
+ specs: list[_ArgSpec], *,
1732
+ prog: str = _DEFAULT_PROG,
1733
+ usage: str | None = None,
1734
+ include_help_opt: bool = True,
1735
+ subparser_action: '_SubparsersAction | None' = None,
1736
+ ) -> str:
1737
+ """Render just the usage line. Reused by parse-error path.
1738
+
1739
+ When ``usage`` is provided, it overrides the auto-generated tail
1740
+ after ``"usage: "`` (matching CPython's ``ArgumentParser(usage=)``
1741
+ behavior). When ``include_help_opt`` is False, the ``[-h]`` token
1742
+ is omitted from the auto-generated form. When ``subparser_action``
1743
+ is provided, the ``{a,b} ...`` subcommand placeholder is appended
1744
+ after any common positionals (matches CPython's rendering of the
1745
+ subparsers action).
1746
+
1747
+ Long usage lines wrap at ``_USAGE_TEXT_WIDTH`` (80) cols, matching
1748
+ CPython argparse's behavior when stdout isn't a TTY. Continuation
1749
+ lines are indented to align past the prog name (or past
1750
+ ``"usage: "`` when prog is too long for that to fit).
1751
+ """
1752
+ if usage is not None:
1753
+ return _USAGE_PREFIX + usage
1754
+ opt_parts: list[str] = []
1755
+ if include_help_opt:
1756
+ opt_parts.append("[-h]")
1757
+ for s in specs:
1758
+ if s.is_flag:
1759
+ opt_parts.append(_flag_usage_token(s))
1760
+ pos_parts = [_positional_usage_token(s) for s in specs if not s.is_flag]
1761
+ if subparser_action is not None:
1762
+ pos_parts.append(_subcommand_metavar(subparser_action))
1763
+ pos_parts.append("...")
1764
+
1765
+ # Try the single-line form first.
1766
+ flat = " ".join([prog, *opt_parts, *pos_parts]).rstrip()
1767
+ if len(_USAGE_PREFIX) + len(flat) <= _USAGE_TEXT_WIDTH:
1768
+ return _USAGE_PREFIX + flat
1769
+
1770
+ # Wrap. Mirrors CPython argparse.HelpFormatter._format_usage:
1771
+ # short prog stays on row 1 (continuation aligns past it); long
1772
+ # prog gets its own row (continuation aligns past "usage: ").
1773
+ if len(_USAGE_PREFIX) + len(prog) <= 0.75 * _USAGE_TEXT_WIDTH:
1774
+ indent = " " * (len(_USAGE_PREFIX) + len(prog) + 1)
1775
+ opt_lines = _wrap_usage_parts(
1776
+ [prog, *opt_parts], indent, prefix=_USAGE_PREFIX,
1777
+ )
1778
+ pos_lines = _wrap_usage_parts(pos_parts, indent, prefix=None)
1779
+ body = "\n".join(opt_lines + pos_lines)
1780
+ else:
1781
+ indent = " " * len(_USAGE_PREFIX)
1782
+ wrapped = _wrap_usage_parts(
1783
+ [*opt_parts, *pos_parts], indent, prefix=None,
1784
+ )
1785
+ body = "\n".join([prog, *wrapped])
1786
+ return _USAGE_PREFIX + body
1787
+
1788
+
1789
+ def _wrap_usage_parts(
1790
+ parts: list[str], indent: str, *, prefix: str | None,
1791
+ ) -> list[str]:
1792
+ """Greedy-wrap usage tokens to ``_USAGE_TEXT_WIDTH``.
1793
+
1794
+ First line uses ``prefix`` as starter (the caller prepends the
1795
+ prefix; this function strips ``indent`` from the first line so
1796
+ the prefix lines up); continuation lines are emitted with
1797
+ ``indent`` already prepended. When ``prefix`` is None, every line
1798
+ starts with ``indent`` (no special-cased first row). Empty
1799
+ ``parts`` returns an empty list.
1800
+ """
1801
+ if not parts:
1802
+ return []
1803
+ lines: list[str] = []
1804
+ line: list[str] = []
1805
+ indent_length = len(indent)
1806
+ line_len = (len(prefix) if prefix is not None else indent_length) - 1
1807
+ for part in parts:
1808
+ if line and line_len + 1 + len(part) > _USAGE_TEXT_WIDTH:
1809
+ lines.append(indent + " ".join(line))
1810
+ line = []
1811
+ line_len = indent_length - 1
1812
+ line.append(part)
1813
+ line_len += len(part) + 1
1814
+ if line:
1815
+ lines.append(indent + " ".join(line))
1816
+ if prefix is not None:
1817
+ # First line carries the prefix instead of the indent; caller
1818
+ # prepends prefix once at the very start of the usage block.
1819
+ lines[0] = lines[0][indent_length:]
1820
+ return lines
1821
+
1822
+
1823
+ def _subcommand_metavar(sp: _SubparsersAction) -> str:
1824
+ """``{a,b,c}`` placeholder shown in usage / help. Matches CPython
1825
+ argparse's rendering of the subparsers positional.
1826
+ """
1827
+ return "{" + ",".join(s.name for s in sp.subparsers) + "}"
1828
+
1829
+
1830
+ def _subparser_cmd_acc(sp: _SubparsersAction) -> str:
1831
+ return f"__tpy_argparse_acc_{sp.dest}"
1832
+
1833
+
1834
+ def _subparser_flat_local(field_name: str) -> str:
1835
+ return f"__tpy_argparse_flat_{field_name}"
1836
+
1837
+
1838
+ def _subparser_sub_local(sub_name: str) -> str:
1839
+ return f"__tpy_argparse_sub_{sub_name}"
1840
+
1841
+
1842
+ def _format_help_text(
1843
+ specs: list[_ArgSpec], description: str | None, *,
1844
+ usage_text: str,
1845
+ epilog: str | None = None,
1846
+ subparser_action: '_SubparsersAction | None' = None,
1847
+ ) -> str:
1848
+ """Render the full --help output: usage line, description (if
1849
+ any), per-section listings of positionals and options, optional
1850
+ epilog. Only emitted when ``add_help=True``, so the auto
1851
+ ``-h, --help`` row is always present. The caller pre-renders
1852
+ ``usage_text`` so it isn't walked twice.
1853
+
1854
+ When ``subparser_action`` is provided, the positional section
1855
+ shows the ``{a,b}`` subcommand metavar plus one indented row per
1856
+ registered sub-parser instead of plain positional rows. v1
1857
+ rejects mixing positionals with subparsers, so the two cases
1858
+ don't overlap.
1859
+ """
1860
+ flags = [s for s in specs if s.is_flag]
1861
+ positionals = [s for s in specs if not s.is_flag]
1862
+
1863
+ pos_rows = [(_positional_usage_token(s), s.help_text or "")
1864
+ for s in positionals]
1865
+ flag_rows = [(_flag_help_signature(s), s.help_text or "") for s in flags]
1866
+ sub_rows: list[tuple[str, str]] = []
1867
+ sub_token = ""
1868
+ if subparser_action is not None:
1869
+ sub_token = _subcommand_metavar(subparser_action)
1870
+ sub_rows = [(s.name, s.help_text or "")
1871
+ for s in subparser_action.subparsers]
1872
+ # Match CPython's ``self._action_max_length + 2``: align all rows
1873
+ # (including the auto ``-h, --help`` entry) to the longest
1874
+ # signature plus two spaces of separation.
1875
+ sigs = [_HELP_OPT_FORM, *(sig for sig, _ in pos_rows),
1876
+ *(sig for sig, _ in flag_rows),
1877
+ *([sub_token] if sub_token else []),
1878
+ *(sig for sig, _ in sub_rows)]
1879
+ pad = max(len(s) for s in sigs) + 2
1880
+
1881
+ lines = [usage_text, ""]
1882
+ if description:
1883
+ lines.append(description)
1884
+ lines.append("")
1885
+
1886
+ if subparser_action is not None:
1887
+ lines.append("positional arguments:")
1888
+ sub_action_help = subparser_action.action_help or ""
1889
+ lines.append(f" {sub_token.ljust(pad)}{sub_action_help}".rstrip())
1890
+ for name, text in sub_rows:
1891
+ lines.append(f" {name.ljust(pad - 2)}{text}".rstrip())
1892
+ lines.append("")
1893
+ elif positionals:
1894
+ lines.append("positional arguments:")
1895
+ for sig, text in pos_rows:
1896
+ lines.append(f" {sig.ljust(pad)}{text}".rstrip())
1897
+ lines.append("")
1898
+
1899
+ lines.append("options:")
1900
+ lines.append(f" {_HELP_OPT_FORM.ljust(pad)}{_HELP_OPT_DESC}")
1901
+ for sig, text in flag_rows:
1902
+ lines.append(f" {sig.ljust(pad)}{text}".rstrip())
1903
+
1904
+ if epilog:
1905
+ if lines and lines[-1] != "":
1906
+ lines.append("")
1907
+ lines.append(epilog)
1908
+ return "\n".join(lines)
1909
+
1910
+
1911
+ def _build_subparser_parse_body(
1912
+ specs: list[_ArgSpec], sp: _SubparsersAction,
1913
+ sub_parse_fn_names: list[str],
1914
+ flat_fields: list[tuple[str, Type, list[tuple[int, _ArgSpec]]]],
1915
+ record_name: str, help_fn_name: str | None,
1916
+ usage_text: str, *, prog: str, add_help: bool,
1917
+ ) -> list:
1918
+ """Generate the body of the synthesized top-level parse function
1919
+ when subparsers are present (Option B / flat namespace).
1920
+
1921
+ Each per-sub field appears as an ``Optional[T]`` stored field on
1922
+ the top record. The parse fn dispatches the subcommand name,
1923
+ invokes the matching sub-parser, then copies the chosen sub
1924
+ record's fields into the corresponding flat locals (per-sub
1925
+ fields not on the chosen sub stay None). Layout:
1926
+
1927
+ __tpy_argparse_usage = <usage>
1928
+ <-h/--help scan>
1929
+ <init common arg defaults>
1930
+ <init required-flag-seen tracking>
1931
+ __tpy_argparse_acc_<dest>: Optional[str] = None
1932
+ <flat_field_i>: Optional[T_i] = None # one per per-sub field name
1933
+ while __tpy_argparse_i < len(argv):
1934
+ __tpy_argparse_tok = argv[__tpy_argparse_i]
1935
+ if __tpy_argparse_tok == "--top-flag-1": ...
1936
+ elif __tpy_argparse_tok == "<sub-name-1>":
1937
+ __tpy_argparse_acc_<dest> = "<sub-name-1>"
1938
+ __tpy_argparse_i = __tpy_argparse_i + 1
1939
+ <sub_var> = <sub_parse_1>(list(argv[i:]))
1940
+ <copy sub_var.field into flat_field for each field on this sub>
1941
+ break
1942
+ ...
1943
+ else: <unknown / invalid-choice error>
1944
+ <required flag missing checks>
1945
+ <Optional[list[T]] / ArgType reconciliation for common args>
1946
+ if sp.required and __tpy_argparse_acc_<dest> is None: <error>
1947
+ <build top record>
1948
+ """
1949
+ error_prefix = f"{prog}: error: "
1950
+ init_stmts, src_lines = _emit_parse_prelude(
1951
+ specs, usage_text, add_help=add_help, help_fn_name=help_fn_name,
1952
+ )
1953
+
1954
+ # Subcommand-name accumulator. cmd is always typed Optional[str]
1955
+ # in the body even when sp.required forces the field to str: the
1956
+ # post-loop required-check converts None -> error and the assert
1957
+ # narrows back to str for the field write.
1958
+ init_stmts.append(ast.var_decl(
1959
+ _subparser_cmd_acc(sp),
1960
+ types.optional(types.str),
1961
+ ast.none_lit(),
1962
+ ))
1963
+
1964
+ # Per-sub flat field locals (Optional[T] = None). Populated inside
1965
+ # the matching sub-parser dispatch branch from the chosen sub
1966
+ # record's fields.
1967
+ flat_local_for: dict[str, str] = {
1968
+ name: _subparser_flat_local(name) for name, _, _ in flat_fields
1969
+ }
1970
+ for name, top_type, _ in flat_fields:
1971
+ init_stmts.append(ast.var_decl(
1972
+ flat_local_for[name], top_type, ast.none_lit(),
1973
+ ))
1974
+
1975
+ # Main loop.
1976
+ src_lines.append("__tpy_argparse_i = 0")
1977
+ src_lines.append("while __tpy_argparse_i < len(argv):")
1978
+ src_lines.append(" __tpy_argparse_tok = argv[__tpy_argparse_i]")
1979
+
1980
+ flag_specs = [s for s in specs if s.is_flag]
1981
+ first = True
1982
+ for s in flag_specs:
1983
+ match_expr = " or ".join(
1984
+ f"__tpy_argparse_tok == {f!r}" for f in s.flag_names
1985
+ )
1986
+ kw = "if" if first else "elif"
1987
+ first = False
1988
+ src_lines.append(f" {kw} {match_expr}:")
1989
+ for line in _flag_handler_lines(s, indent=" ",
1990
+ error_prefix=error_prefix):
1991
+ src_lines.append(line)
1992
+ if s.required:
1993
+ src_lines.append(f" __tpy_argparse_seen_{s.dest} = True")
1994
+
1995
+ # Subcommand dispatch: each registered sub-parser becomes a
1996
+ # branch. After calling the sub parse fn, copy each declared
1997
+ # field on the chosen sub into its flat local on the top record.
1998
+ cmd_acc = _subparser_cmd_acc(sp)
1999
+ for sub_idx, sub in enumerate(sp.subparsers):
2000
+ kw = "if" if first else "elif"
2001
+ first = False
2002
+ src_lines.append(
2003
+ f' {kw} __tpy_argparse_tok == {sub.name!r}:'
2004
+ )
2005
+ src_lines.append(f" {cmd_acc} = {sub.name!r}")
2006
+ src_lines.append(" __tpy_argparse_i = __tpy_argparse_i + 1")
2007
+ sub_var = _subparser_sub_local(sub.name)
2008
+ src_lines.append(
2009
+ f" {sub_var} = "
2010
+ f"{sub_parse_fn_names[sub_idx]}"
2011
+ f"(list(argv[__tpy_argparse_i:]))"
2012
+ )
2013
+ sub_field_names = {s.dest for s in sub.specs}
2014
+ for name, _, _ in flat_fields:
2015
+ if name in sub_field_names:
2016
+ src_lines.append(
2017
+ f" {flat_local_for[name]} = {sub_var}.{name}"
2018
+ )
2019
+ src_lines.append(" break")
2020
+
2021
+ # Else branch: unknown token. Distinguishes "unknown flag" (token
2022
+ # starts with '-') from "invalid choice" (positional that doesn't
2023
+ # match any sub-parser name) so error messages match CPython.
2024
+ src_lines.append(" else:")
2025
+ src_lines.append(
2026
+ " if __tpy_argparse_tok.startswith(\"-\"):"
2027
+ )
2028
+ src_lines.extend(_error_emit_lines(
2029
+ " ",
2030
+ '"unknown argument: " + __tpy_argparse_tok',
2031
+ error_prefix=error_prefix,
2032
+ ))
2033
+ src_lines.append(" else:")
2034
+ src_lines.extend(_error_emit_lines(
2035
+ " ",
2036
+ '"invalid choice: " + __tpy_argparse_tok',
2037
+ error_prefix=error_prefix,
2038
+ ))
2039
+
2040
+ src_lines.extend(_emit_parse_reconciliation(specs, error_prefix=error_prefix))
2041
+
2042
+ # Subcommand resolution. ``__tpy_argparse_cmd`` is bound here as
2043
+ # the ctor input for the top record's cmd field; sp.required
2044
+ # widens / narrows the accumulator type.
2045
+ if sp.required:
2046
+ src_lines.append(f"if {cmd_acc} is None:")
2047
+ src_lines.extend(_error_emit_lines(
2048
+ " ",
2049
+ repr(f"the following argument is required: {_subcommand_metavar(sp)}"),
2050
+ error_prefix=error_prefix,
2051
+ ))
2052
+ src_lines.append(f"assert {cmd_acc} is not None")
2053
+ src_lines.append(f"__tpy_argparse_cmd = {cmd_acc}")
2054
+
2055
+ ctor_args = ", ".join(
2056
+ [s.dest for s in specs]
2057
+ + ["__tpy_argparse_cmd"]
2058
+ + [flat_local_for[name] for name, _, _ in flat_fields]
2059
+ )
2060
+ src_lines.append(f"return {record_name}({ctor_args})")
2061
+
2062
+ return init_stmts + ast.quote("\n".join(src_lines))