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.
- tpy_lang-0.3.0.dev0.dist-info/METADATA +151 -0
- tpy_lang-0.3.0.dev0.dist-info/RECORD +333 -0
- tpy_lang-0.3.0.dev0.dist-info/WHEEL +4 -0
- tpy_lang-0.3.0.dev0.dist-info/entry_points.txt +3 -0
- tpyc/__init__.py +104 -0
- tpyc/__main__.py +6 -0
- tpyc/_buildinfo.py +1 -0
- tpyc/_data/docs/LANGUAGE_FEATURES.md +6278 -0
- tpyc/_data/docs/STDLIB_ROADMAP.md +1258 -0
- tpyc/_data/docs/TPY_FOR_AGENTS.md +556 -0
- tpyc/_data/lib/tpy/_bindings/__init__.py +6 -0
- tpyc/_data/lib/tpy/_bindings/pcre2.py +173 -0
- tpyc/_data/lib/tpy/_bindings/posix_socket.py +161 -0
- tpyc/_data/lib/tpy/_functools_macros.py +80 -0
- tpyc/_data/lib/tpy/_macro_helpers.py +161 -0
- tpyc/_data/lib/tpy/argparse.py +2062 -0
- tpyc/_data/lib/tpy/asyncio/__init__.py +744 -0
- tpyc/_data/lib/tpy/asyncio/_executor.py +515 -0
- tpyc/_data/lib/tpy/base64.py +410 -0
- tpyc/_data/lib/tpy/bisect.py +39 -0
- tpyc/_data/lib/tpy/builtins.py +38 -0
- tpyc/_data/lib/tpy/dataclasses.py +354 -0
- tpyc/_data/lib/tpy/enum.py +23 -0
- tpyc/_data/lib/tpy/functools.py +33 -0
- tpyc/_data/lib/tpy/hashlib.py +206 -0
- tpyc/_data/lib/tpy/heapq.py +118 -0
- tpyc/_data/lib/tpy/io.py +395 -0
- tpyc/_data/lib/tpy/json.py +221 -0
- tpyc/_data/lib/tpy/math.py +406 -0
- tpyc/_data/lib/tpy/random.py +597 -0
- tpyc/_data/lib/tpy/re.py +467 -0
- tpyc/_data/lib/tpy/socket.py +379 -0
- tpyc/_data/lib/tpy/struct.py +178 -0
- tpyc/_data/lib/tpy/sys.py +40 -0
- tpyc/_data/lib/tpy/time.py +39 -0
- tpyc/_data/lib/tpy/tpy/__init__.py +78 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/__init__.py +10 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/_decorators.py +37 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/_extern.py +64 -0
- tpyc/_data/lib/tpy/tpy/_builtins/__init__.py +11 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_bytes.py +378 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_dict.py +151 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_exceptions.py +125 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_funcs.py +681 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_io.py +97 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_list.py +127 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_range.py +52 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_set.py +139 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_super.py +11 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_types.py +661 -0
- tpyc/_data/lib/tpy/tpy/_core/__init__.py +23 -0
- tpyc/_data/lib/tpy/tpy/_core/_bytes_view.py +129 -0
- tpyc/_data/lib/tpy/tpy/_core/_containers.py +137 -0
- tpyc/_data/lib/tpy/tpy/_core/_functions.py +40 -0
- tpyc/_data/lib/tpy/tpy/_core/_types.py +2061 -0
- tpyc/_data/lib/tpy/tpy/_typing/__init__.py +77 -0
- tpyc/_data/lib/tpy/tpy/_version.py +29 -0
- tpyc/_data/lib/tpy/tpy/bits.py +28 -0
- tpyc/_data/lib/tpy/tpy/coro/__init__.py +127 -0
- tpyc/_data/lib/tpy/tpy/extern.py +8 -0
- tpyc/_data/lib/tpy/tpy/mem.py +49 -0
- tpyc/_data/lib/tpy/tpy/unsafe.py +195 -0
- tpyc/_data/lib/tpy/tpy/version.py +21 -0
- tpyc/_data/lib/tpy/typing.py +13 -0
- tpyc/_data/runtime/cpp/include/tpy/any.hpp +461 -0
- tpyc/_data/runtime/cpp/include/tpy/as_ostream.hpp +117 -0
- tpyc/_data/runtime/cpp/include/tpy/async.hpp +76 -0
- tpyc/_data/runtime/cpp/include/tpy/bigint.hpp +1343 -0
- tpyc/_data/runtime/cpp/include/tpy/builtins.hpp +400 -0
- tpyc/_data/runtime/cpp/include/tpy/bytes_ops.hpp +469 -0
- tpyc/_data/runtime/cpp/include/tpy/container_ops.hpp +487 -0
- tpyc/_data/runtime/cpp/include/tpy/copy_iter.hpp +82 -0
- tpyc/_data/runtime/cpp/include/tpy/core.hpp +558 -0
- tpyc/_data/runtime/cpp/include/tpy/dict_ops.hpp +289 -0
- tpyc/_data/runtime/cpp/include/tpy/dunder.hpp +750 -0
- tpyc/_data/runtime/cpp/include/tpy/dynamic.hpp +44 -0
- tpyc/_data/runtime/cpp/include/tpy/enum.hpp +40 -0
- tpyc/_data/runtime/cpp/include/tpy/file.hpp +245 -0
- tpyc/_data/runtime/cpp/include/tpy/fixed_int.hpp +317 -0
- tpyc/_data/runtime/cpp/include/tpy/format.hpp +954 -0
- tpyc/_data/runtime/cpp/include/tpy/frame_slot.hpp +120 -0
- tpyc/_data/runtime/cpp/include/tpy/generator.hpp +47 -0
- tpyc/_data/runtime/cpp/include/tpy/iterable_ops.hpp +122 -0
- tpyc/_data/runtime/cpp/include/tpy/itertools.hpp +749 -0
- tpyc/_data/runtime/cpp/include/tpy/next_iter.hpp +82 -0
- tpyc/_data/runtime/cpp/include/tpy/ordered_map.hpp +518 -0
- tpyc/_data/runtime/cpp/include/tpy/ordered_set.hpp +337 -0
- tpyc/_data/runtime/cpp/include/tpy/own_iter.hpp +54 -0
- tpyc/_data/runtime/cpp/include/tpy/pascal_graph_sdl.hpp +192 -0
- tpyc/_data/runtime/cpp/include/tpy/printing.hpp +302 -0
- tpyc/_data/runtime/cpp/include/tpy/protocols.hpp +61 -0
- tpyc/_data/runtime/cpp/include/tpy/range.hpp +115 -0
- tpyc/_data/runtime/cpp/include/tpy/ranges.hpp +212 -0
- tpyc/_data/runtime/cpp/include/tpy/set_ops.hpp +265 -0
- tpyc/_data/runtime/cpp/include/tpy/slice.hpp +47 -0
- tpyc/_data/runtime/cpp/include/tpy/span_iter.hpp +42 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/math.hpp +41 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/pcre2_h.hpp +96 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/random.hpp +25 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/socket_h.hpp +145 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/time.hpp +62 -0
- tpyc/_data/runtime/cpp/include/tpy/system.hpp +121 -0
- tpyc/_data/runtime/cpp/include/tpy/throwable.hpp +55 -0
- tpyc/_data/runtime/cpp/include/tpy/tpy.hpp +156 -0
- tpyc/_data/runtime/cpp/include/tpy/type_name.hpp +77 -0
- tpyc/_data/runtime/cpp/include/tpy/type_traits.hpp +240 -0
- tpyc/_data/runtime/cpp/include/tpy/uninit_array_storage.hpp +250 -0
- tpyc/_data/runtime/cpp/include/tpy/uninit_heap_storage.hpp +277 -0
- tpyc/_data/runtime/cpp/include/tpy/varargs.hpp +174 -0
- tpyc/_data/runtime/cpp/include/tpy/variant_ref.hpp +118 -0
- tpyc/_data/runtime/cpp/src/stdlib/socket_impl.cpp +104 -0
- tpyc/_data/runtime/cpp/third_party/README.md +58 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/AUTHORS +36 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/CMakeLists.txt +1233 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/COPYING +5 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/ChangeLog +3097 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/HACKING +853 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/INSTALL +368 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/LICENCE +94 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/NEWS +492 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/NON-AUTOTOOLS-BUILD +430 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/README +956 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/COPYING-CMAKE-SCRIPTS +22 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindEditline.cmake +16 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindPackageHandleStandardArgs.cmake +58 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindReadline.cmake +29 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config-version.cmake.in +15 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config.cmake.in +148 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/config-cmake.h.in +56 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-16.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-32.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-8.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-posix.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/pcre2-config.in +121 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h +483 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.generic +483 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.in +460 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.generic +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.in +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_auto_possess.c +1371 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c +196 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c.dist +196 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chkdint.c +96 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_compile.c +11001 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_config.c +252 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_context.c +510 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_convert.c +1189 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dfa_match.c +4119 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dftables.c +297 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_error.c +345 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_extuni.c +162 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_find_bracket.c +219 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_fuzzsupport.c +792 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_internal.h +2084 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_intmodedep.h +940 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_compile.c +14972 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_match.c +200 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_misc.c +234 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_neon_inc.h +354 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_simd_inc.h +2355 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_test.c +2528 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_maketables.c +165 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match.c +7777 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match_data.c +185 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_newline.c +243 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ord2utf.c +120 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_pattern_info.c +432 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_printint.c +886 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_script_run.c +344 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_serialize.c +286 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_string_utils.c +237 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_study.c +1915 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substitute.c +1009 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substring.c +550 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_tables.c +234 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucd.c +5460 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucp.h +396 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucptables.c +1533 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_valid_utf.c +398 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_xclass.c +308 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2demo.c +497 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2grep.c +4606 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.c +425 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.h +187 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix_test.c +209 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2test.c +9708 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorApple.c +137 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorCore.c +327 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorFreeBSD.c +89 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorPosix.c +62 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorWindows.c +40 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorNetBSD.c +72 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorPosix.c +172 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorPosix.c +141 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorWindows.c +102 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfig.h +142 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigCPU.h +188 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigInternal.h +907 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.c +3561 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.h +2466 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_32.c +4636 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_64.c +3491 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_T2_32.c +4302 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeLOONGARCH_64.c +3765 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_32.c +472 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_64.c +387 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_common.c +4259 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_32.c +485 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_64.c +719 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_common.c +3161 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_32.c +142 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_64.c +222 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_common.c +3121 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeS390X.c +4526 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_32.c +1685 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_64.c +1398 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_common.c +5001 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitSerialize.c +516 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitUtils.c +344 -0
- tpyc/_data/runtime/cpp/third_party/pcre2.sources.txt +54 -0
- tpyc/_data/runtime/cpp/third_party/pcre2.vendor.json +7 -0
- tpyc/build/__init__.py +7 -0
- tpyc/build/pcre2.py +122 -0
- tpyc/build/third_party.py +413 -0
- tpyc/cli.py +822 -0
- tpyc/codegen_cpp/__init__.py +18 -0
- tpyc/codegen_cpp/builtins.py +484 -0
- tpyc/codegen_cpp/context.py +2064 -0
- tpyc/codegen_cpp/expressions.py +5940 -0
- tpyc/codegen_cpp/functions.py +1913 -0
- tpyc/codegen_cpp/gen_async.py +3258 -0
- tpyc/codegen_cpp/gen_generators.py +657 -0
- tpyc/codegen_cpp/generator.py +2258 -0
- tpyc/codegen_cpp/match.py +1997 -0
- tpyc/codegen_cpp/param_const.py +172 -0
- tpyc/codegen_cpp/protocols.py +907 -0
- tpyc/codegen_cpp/records.py +1654 -0
- tpyc/codegen_cpp/resumable_cfg.py +1651 -0
- tpyc/codegen_cpp/statements.py +4963 -0
- tpyc/codegen_cpp/string_dispatch.py +76 -0
- tpyc/codegen_cpp/test_context.py +46 -0
- tpyc/codegen_cpp/test_param_const.py +113 -0
- tpyc/codegen_cpp/test_resumable_cfg.py +182 -0
- tpyc/codegen_cpp/type_resolution.py +53 -0
- tpyc/codegen_cpp/types.py +436 -0
- tpyc/codegen_cpp/variant_access.py +135 -0
- tpyc/coercions.py +749 -0
- tpyc/compilation_context.py +57 -0
- tpyc/compiler.py +3945 -0
- tpyc/cycle_detection.py +358 -0
- tpyc/diagnostics.py +135 -0
- tpyc/dump_types.py +353 -0
- tpyc/frontend_diagnostics.py +47 -0
- tpyc/frontend_ir/__init__.py +140 -0
- tpyc/frontend_ir/lower.py +1098 -0
- tpyc/frontend_ir/nodes.py +718 -0
- tpyc/frontend_ir/resolver_adapter.py +151 -0
- tpyc/frontend_plugin.py +209 -0
- tpyc/install_docs.py +81 -0
- tpyc/liveness.py +756 -0
- tpyc/macro_api.py +1724 -0
- tpyc/macro_loader.py +497 -0
- tpyc/module_names.py +64 -0
- tpyc/modules/__init__.py +31 -0
- tpyc/modules/defs.py +89 -0
- tpyc/modules/registry.py +36 -0
- tpyc/modules/resolver.py +192 -0
- tpyc/modules/type_resolution.py +629 -0
- tpyc/namespace.py +172 -0
- tpyc/parse/__init__.py +84 -0
- tpyc/parse/imports.py +490 -0
- tpyc/parse/nodes.py +1732 -0
- tpyc/parse/parser.py +4043 -0
- tpyc/parse/resolve_refs.py +466 -0
- tpyc/parse/type_resolver.py +1060 -0
- tpyc/prescan.py +254 -0
- tpyc/qnames.py +149 -0
- tpyc/repl.py +529 -0
- tpyc/repl_backends.py +848 -0
- tpyc/sema/__init__.py +21 -0
- tpyc/sema/analyzer.py +3625 -0
- tpyc/sema/bound_check.py +72 -0
- tpyc/sema/builder_trace.py +684 -0
- tpyc/sema/calls.py +5406 -0
- tpyc/sema/compatibility.py +2107 -0
- tpyc/sema/context.py +1243 -0
- tpyc/sema/expressions.py +3737 -0
- tpyc/sema/flow_facts.py +199 -0
- tpyc/sema/init_tracker.py +150 -0
- tpyc/sema/list_literals.py +69 -0
- tpyc/sema/literal_utils.py +27 -0
- tpyc/sema/local_deduction.py +1088 -0
- tpyc/sema/macros.py +179 -0
- tpyc/sema/match.py +1177 -0
- tpyc/sema/method_expansion.py +347 -0
- tpyc/sema/methods.py +2197 -0
- tpyc/sema/mutation_propagation.py +268 -0
- tpyc/sema/narrowing.py +857 -0
- tpyc/sema/numeric_lattice.py +160 -0
- tpyc/sema/operators.py +402 -0
- tpyc/sema/overloads.py +841 -0
- tpyc/sema/protocols.py +1209 -0
- tpyc/sema/reach_analysis.py +202 -0
- tpyc/sema/registration.py +3156 -0
- tpyc/sema/scope_tracker.py +193 -0
- tpyc/sema/statements.py +4426 -0
- tpyc/sema/type_ops.py +1879 -0
- tpyc/sema/value_range.py +181 -0
- tpyc/symbol_binding.py +259 -0
- tpyc/test_c3_mro.py +208 -0
- tpyc/test_cli_argv.py +52 -0
- tpyc/test_compiler.py +559 -0
- tpyc/test_contains_type_param.py +101 -0
- tpyc/test_cycle_detection.py +221 -0
- tpyc/test_dump_types.py +225 -0
- tpyc/test_install_docs.py +65 -0
- tpyc/test_local_cpp_form.py +135 -0
- tpyc/test_macro_loader.py +76 -0
- tpyc/test_method_expansion.py +254 -0
- tpyc/test_nominal_identity.py +182 -0
- tpyc/test_overloads.py +410 -0
- tpyc/test_parse.py +303 -0
- tpyc/test_parse_type_ref.py +506 -0
- tpyc/test_parse_version_info.py +58 -0
- tpyc/test_reach_analysis.py +72 -0
- tpyc/test_ref_type.py +216 -0
- tpyc/test_send_sync_substitution.py +276 -0
- tpyc/test_tuple_mutation_propagation.py +206 -0
- tpyc/test_type_def_registry.py +1729 -0
- tpyc/test_union_types.py +195 -0
- tpyc/type_def_registry.py +975 -0
- tpyc/typesys.py +5104 -0
tpyc/parse/parser.py
ADDED
|
@@ -0,0 +1,4043 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Parser.
|
|
3
|
+
|
|
4
|
+
Uses CPython's ast module to parse TurboPython source code.
|
|
5
|
+
Validates that only allowed constructs are used.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import ast
|
|
10
|
+
import copy
|
|
11
|
+
import dataclasses
|
|
12
|
+
import re
|
|
13
|
+
import textwrap
|
|
14
|
+
from typing import Any, Literal, NoReturn, TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from ..typesys import (
|
|
17
|
+
FieldInfo, NominalType, OptionalType, RecordInfo, TypeRegistry,
|
|
18
|
+
FunctionInfo, MethodSignature, ProtocolInfo, TypeParamKind, LiteralValue, LiteralTag,
|
|
19
|
+
bare_name,
|
|
20
|
+
)
|
|
21
|
+
from ..module_names import public_module_name
|
|
22
|
+
from ..type_def_registry import (
|
|
23
|
+
is_bool_type, is_str_type, type_def_of,
|
|
24
|
+
find_factory_by_simple_name, find_factory_in_module,
|
|
25
|
+
)
|
|
26
|
+
from .type_resolver import TypeResolver
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from ..typesys import TpyType
|
|
30
|
+
from .nodes import (
|
|
31
|
+
ParseError, SourceLocation, ParseWarning, RecordLinkage, FunctionLinkage,
|
|
32
|
+
TpyTypeRef, TpyUnionRef, TpyCallableRef, TpyLiteralRef, TpyInferFromDefaultRef,
|
|
33
|
+
ResolverInputNode, TypeRefNode,
|
|
34
|
+
TpyExpr, TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBytesLiteral,
|
|
35
|
+
TpyFStringValue, TpyFString, FSTRING_CONV_ASCII,
|
|
36
|
+
TpyBoolLiteral,
|
|
37
|
+
TpyNoneLiteral, TpyName, TpyBinOp, TpyChainedCompare, TpyUnaryOp, TpyTypeParamConstruct,
|
|
38
|
+
TpyStarUnpack, TpyCall, TpyMethodCall,
|
|
39
|
+
TpyFieldAccess, TpyArrayLiteral, TpyTupleLiteral, TpyDictLiteral, TpySetLiteral, TpyListRepeat,
|
|
40
|
+
TpyComprehensionGenerator, TpyListComprehension, TpyDictComprehension, TpySetComprehension, TpyGeneratorExpression,
|
|
41
|
+
TpySlice, TpySubscript, TpyCoerce,
|
|
42
|
+
TpyIfExpr, TpyNamedExpr, TpyAwait, TpyLambda,
|
|
43
|
+
TpyStmt, TpyVarDecl, TpyTupleUnpack, TpyAssign, TpyAugAssign, TpyDelItem, TpyDelVar, TpyDelAttr, TpyExprStmt, TpyReturn, TpyYield,
|
|
44
|
+
TpyAssert, TpyIf, TpyWhile, TpyForEach, TpyBreak, TpyContinue,
|
|
45
|
+
TpyPassStmt, TpyGlobal, TpyNonlocal, TpyRaise, TpyExceptHandler, TpyTry, TpyWithItem, TpyWith,
|
|
46
|
+
TpyNestedDef,
|
|
47
|
+
TpyPattern, TpyWildcardPattern, TpyCapturePattern, TpyClassPattern,
|
|
48
|
+
TpyLiteralPattern, TpyValuePattern, TpyOrPattern, TpyAsPattern,
|
|
49
|
+
TpyMatchCase, TpyMatch,
|
|
50
|
+
RelativeImportKey, TpyImport, TpyFunction, TpyRecord, TpyProtocol, TpyEnum, TpyModule,
|
|
51
|
+
ModuleDirectives,
|
|
52
|
+
)
|
|
53
|
+
from .imports import (
|
|
54
|
+
ImportProcessor, _PRIVATE_MODULE_PUBLIC_NAMES, _IMPLICIT_MODULES,
|
|
55
|
+
get_builtins_exports, get_typing_exports, get_tpy_exports, read_module_all,
|
|
56
|
+
NonLiteralAllError,
|
|
57
|
+
)
|
|
58
|
+
from .. import qnames
|
|
59
|
+
|
|
60
|
+
# Modules whose names the parser resolves structurally (base class detection,
|
|
61
|
+
# enum auto(), Unpack). A local def/class/assignment that shadows one of these
|
|
62
|
+
# names is warned about so the user knows the parser keyword is hidden.
|
|
63
|
+
# Only typing and enum -- tpy/builtins names are resolved via normal sema and
|
|
64
|
+
# shadowing them is routine (e.g. user-defined `copy` replacing tpy.copy).
|
|
65
|
+
_PARSER_KEYWORD_MODULES = frozenset({"typing", "enum"})
|
|
66
|
+
|
|
67
|
+
# Decorator names that mark stdlib stubs (builtin types, decorators, functions).
|
|
68
|
+
# Definitions with these decorators are excluded from _local_defs because they
|
|
69
|
+
# intentionally re-define imported names (e.g. @builtin_type class Protocol).
|
|
70
|
+
# Checked via raw AST name (_decorator_raw_name) since import resolution hasn't
|
|
71
|
+
# run yet during pre-scan.
|
|
72
|
+
_BUILTIN_DEC_NAMES = frozenset({"builtin_type", "builtin_decorator", "builtin_function"})
|
|
73
|
+
|
|
74
|
+
# Fixed-int constructor names recognized syntactically by the parser for
|
|
75
|
+
# default-value validation (_validate_const_default) and the C++ literal
|
|
76
|
+
# simplification in _get_default_value. The actual TpyType lookup happens
|
|
77
|
+
# in the TypeResolver via `_FIXED_INT_MAP`; here we only need name
|
|
78
|
+
# membership. Kept in sync with typesys.ALL_FIXED_INTS.
|
|
79
|
+
_FIXED_INT_NAMES: frozenset[str] = frozenset({
|
|
80
|
+
"Int8", "Int16", "Int32", "Int64",
|
|
81
|
+
"UInt8", "UInt16", "UInt32", "UInt64",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
# Operator-to-string mappings for AST binary, comparison, and unary operators
|
|
85
|
+
_BINOP_TO_STR: dict[type, str] = {
|
|
86
|
+
ast.Add: "+", ast.Sub: "-", ast.Mult: "*",
|
|
87
|
+
ast.Div: "div", ast.Mod: "%", ast.FloorDiv: "//",
|
|
88
|
+
ast.BitAnd: "&", ast.BitOr: "|", ast.BitXor: "^",
|
|
89
|
+
ast.LShift: "<<", ast.RShift: ">>",
|
|
90
|
+
ast.Pow: "**",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_CMPOP_TO_STR: dict[type, str] = {
|
|
94
|
+
ast.Eq: "==", ast.NotEq: "!=",
|
|
95
|
+
ast.Lt: "<", ast.LtE: "<=",
|
|
96
|
+
ast.Gt: ">", ast.GtE: ">=",
|
|
97
|
+
ast.In: "in", ast.NotIn: "not in",
|
|
98
|
+
ast.Is: "is", ast.IsNot: "is not",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_UNARYOP_TO_STR: dict[type, str] = {
|
|
102
|
+
ast.UAdd: "+", ast.USub: "-", ast.Not: "!", ast.Invert: "~",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_fstring_format_spec(spec: str) -> str | None:
|
|
107
|
+
"""Validate an f-string format spec against C++ std::format support.
|
|
108
|
+
|
|
109
|
+
Returns an error message for Python-only features, or None if valid.
|
|
110
|
+
Python features not supported by std::format: '=' alignment, 'z' option,
|
|
111
|
+
',' and '_' grouping, 'n' and '%' type codes.
|
|
112
|
+
"""
|
|
113
|
+
if not spec:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
pos = 0
|
|
117
|
+
n = len(spec)
|
|
118
|
+
|
|
119
|
+
# [[fill]align] -- fill can be ANY character if followed by an align char
|
|
120
|
+
_ALIGN = '<>^='
|
|
121
|
+
if n >= 2 and spec[1] in _ALIGN:
|
|
122
|
+
if spec[1] == '=':
|
|
123
|
+
return "'=' alignment is not supported"
|
|
124
|
+
pos = 2
|
|
125
|
+
elif spec[0] in _ALIGN:
|
|
126
|
+
if spec[0] == '=':
|
|
127
|
+
return "'=' alignment is not supported"
|
|
128
|
+
pos = 1
|
|
129
|
+
|
|
130
|
+
# [sign]
|
|
131
|
+
if pos < n and spec[pos] in '+- ':
|
|
132
|
+
pos += 1
|
|
133
|
+
|
|
134
|
+
# [z]
|
|
135
|
+
if pos < n and spec[pos] == 'z':
|
|
136
|
+
return "'z' option is not supported"
|
|
137
|
+
|
|
138
|
+
# [#]
|
|
139
|
+
if pos < n and spec[pos] == '#':
|
|
140
|
+
pos += 1
|
|
141
|
+
|
|
142
|
+
# [0]
|
|
143
|
+
if pos < n and spec[pos] == '0':
|
|
144
|
+
pos += 1
|
|
145
|
+
|
|
146
|
+
# [width]
|
|
147
|
+
while pos < n and spec[pos].isdigit():
|
|
148
|
+
pos += 1
|
|
149
|
+
|
|
150
|
+
# [grouping_option]
|
|
151
|
+
if pos < n and spec[pos] in ',_':
|
|
152
|
+
return f"'{spec[pos]}' grouping is not supported"
|
|
153
|
+
|
|
154
|
+
# [.precision]
|
|
155
|
+
if pos < n and spec[pos] == '.':
|
|
156
|
+
pos += 1
|
|
157
|
+
while pos < n and spec[pos].isdigit():
|
|
158
|
+
pos += 1
|
|
159
|
+
|
|
160
|
+
# [type]
|
|
161
|
+
if pos < n:
|
|
162
|
+
t = spec[pos]
|
|
163
|
+
if t == 'n':
|
|
164
|
+
return "'n' (locale-aware) type is not supported"
|
|
165
|
+
if t == '%':
|
|
166
|
+
return "'%' (percentage) type is not supported"
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _extract_subscript_slices(node: ast.Subscript) -> list[ast.expr]:
|
|
172
|
+
"""Extract individual type argument nodes from a subscript slice.
|
|
173
|
+
|
|
174
|
+
Handles both single-arg (X[T]) and multi-arg (X[T, U]) forms.
|
|
175
|
+
"""
|
|
176
|
+
if isinstance(node.slice, ast.Tuple):
|
|
177
|
+
return node.slice.elts
|
|
178
|
+
return [node.slice]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _attr_chain_to_dotted(node: ast.expr) -> str | None:
|
|
182
|
+
"""Flatten ast.Name / ast.Attribute chains into a dotted string.
|
|
183
|
+
|
|
184
|
+
Returns None for any other expression shape (call result, subscript, etc.).
|
|
185
|
+
"""
|
|
186
|
+
if isinstance(node, ast.Name):
|
|
187
|
+
return node.id
|
|
188
|
+
if isinstance(node, ast.Attribute):
|
|
189
|
+
parts: list[str] = []
|
|
190
|
+
cur: ast.expr = node
|
|
191
|
+
while isinstance(cur, ast.Attribute):
|
|
192
|
+
parts.append(cur.attr)
|
|
193
|
+
cur = cur.value
|
|
194
|
+
if not isinstance(cur, ast.Name):
|
|
195
|
+
return None
|
|
196
|
+
parts.append(cur.id)
|
|
197
|
+
parts.reverse()
|
|
198
|
+
return ".".join(parts)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Requires whitespace after `tpy:` to avoid matching C++ namespace comments (# tpy::Foo)
|
|
203
|
+
_DIRECTIVE_LINE_RE = re.compile(r'^#\s*tpy:\s+(\w.+)$')
|
|
204
|
+
|
|
205
|
+
# Schema: (positional arg types, allowed keyword arg types)
|
|
206
|
+
# Keys are the known directive names; unknown names produce a warning.
|
|
207
|
+
_DIRECTIVE_SPECS: dict[str, tuple[list[type], dict[str, type]]] = {
|
|
208
|
+
"native_module": ([], {}),
|
|
209
|
+
"macro_module": ([], {}),
|
|
210
|
+
"include": ([str], {"platform": str}),
|
|
211
|
+
# link: raw `-lfoo` by default; `managed=True` routes through the
|
|
212
|
+
# third-party registry (tpyc/build/third_party.py) for bundled/system/
|
|
213
|
+
# auto resolution via the `--<lib>=<mode>` CLI flag.
|
|
214
|
+
"link": ([str], {"platform": str, "managed": bool}),
|
|
215
|
+
"cpp_namespace": ([str], {}),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_CPP_NAMESPACE_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _parse_directive_call(content: str) -> tuple[str, list, dict] | None:
|
|
222
|
+
"""Parse directive content as a bare name or Python-style call.
|
|
223
|
+
|
|
224
|
+
Returns (name, positional_args, keyword_args), or None on parse error.
|
|
225
|
+
"""
|
|
226
|
+
content = content.strip()
|
|
227
|
+
if re.match(r'^\w+$', content):
|
|
228
|
+
return (content, [], {})
|
|
229
|
+
try:
|
|
230
|
+
tree = ast.parse(content, mode='eval')
|
|
231
|
+
expr = tree.body
|
|
232
|
+
if isinstance(expr, ast.Call) and isinstance(expr.func, ast.Name):
|
|
233
|
+
name = expr.func.id
|
|
234
|
+
pos_args = [ast.literal_eval(a) for a in expr.args]
|
|
235
|
+
kw_args = {kw.arg: ast.literal_eval(kw.value)
|
|
236
|
+
for kw in expr.keywords if kw.arg is not None}
|
|
237
|
+
return (name, pos_args, kw_args)
|
|
238
|
+
except (SyntaxError, ValueError):
|
|
239
|
+
pass
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _check_directive_args(
|
|
244
|
+
name: str, args: list, kwargs: dict,
|
|
245
|
+
spec: tuple[list[type], dict[str, type]],
|
|
246
|
+
loc: SourceLocation, warnings: list[ParseWarning],
|
|
247
|
+
) -> bool:
|
|
248
|
+
"""Validate args/kwargs against a directive spec. Returns True if valid."""
|
|
249
|
+
pos_types, kw_types = spec
|
|
250
|
+
if len(args) != len(pos_types):
|
|
251
|
+
warnings.append(ParseWarning(
|
|
252
|
+
f"'{name}' expects {len(pos_types)} positional argument(s), got {len(args)}", loc))
|
|
253
|
+
return False
|
|
254
|
+
for i, (val, typ) in enumerate(zip(args, pos_types)):
|
|
255
|
+
if not isinstance(val, typ):
|
|
256
|
+
warnings.append(ParseWarning(
|
|
257
|
+
f"'{name}' argument {i + 1} must be a {typ.__name__}", loc))
|
|
258
|
+
return False
|
|
259
|
+
unknown = {k for k in kwargs if k not in kw_types}
|
|
260
|
+
if unknown:
|
|
261
|
+
warnings.append(ParseWarning(
|
|
262
|
+
f"'{name}' unknown keyword arguments: {sorted(unknown)}", loc))
|
|
263
|
+
return False
|
|
264
|
+
for k, val in kwargs.items():
|
|
265
|
+
if not isinstance(val, kw_types[k]):
|
|
266
|
+
warnings.append(ParseWarning(
|
|
267
|
+
f"'{name}' keyword '{k}' must be a {kw_types[k].__name__}", loc))
|
|
268
|
+
return False
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _scan_directives(source_lines: list[str]) -> tuple[ModuleDirectives, list[ParseWarning]]:
|
|
273
|
+
"""Scan all standalone # tpy: comment lines and return parsed directives."""
|
|
274
|
+
includes: list[tuple[str, str | None]] = []
|
|
275
|
+
link_libs: list[tuple[str, str | None]] = []
|
|
276
|
+
third_party_deps: list[tuple[str, str | None]] = []
|
|
277
|
+
native_module = False
|
|
278
|
+
cpp_namespace: str | None = None
|
|
279
|
+
warnings: list[ParseWarning] = []
|
|
280
|
+
|
|
281
|
+
preamble_ended = False
|
|
282
|
+
for lineno, line in enumerate(source_lines, start=1):
|
|
283
|
+
stripped = line.strip()
|
|
284
|
+
if stripped and not stripped.startswith('#'):
|
|
285
|
+
preamble_ended = True
|
|
286
|
+
if not stripped.startswith('#'):
|
|
287
|
+
continue
|
|
288
|
+
m = _DIRECTIVE_LINE_RE.match(stripped)
|
|
289
|
+
if not m:
|
|
290
|
+
continue
|
|
291
|
+
content = m.group(1).strip()
|
|
292
|
+
loc = SourceLocation(line=lineno)
|
|
293
|
+
if preamble_ended:
|
|
294
|
+
warnings.append(ParseWarning(
|
|
295
|
+
"# tpy: directives must appear before any code", loc))
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
parsed = _parse_directive_call(content)
|
|
299
|
+
if parsed is None:
|
|
300
|
+
warnings.append(ParseWarning(f"invalid # tpy: directive syntax: {content!r}", loc))
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
name, args, kwargs = parsed
|
|
304
|
+
spec = _DIRECTIVE_SPECS.get(name)
|
|
305
|
+
if spec is None:
|
|
306
|
+
warnings.append(ParseWarning(f"unknown # tpy: directive: {name!r}", loc))
|
|
307
|
+
continue
|
|
308
|
+
if not _check_directive_args(name, args, kwargs, spec, loc, warnings):
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
if name == "native_module":
|
|
312
|
+
native_module = True
|
|
313
|
+
elif name == "include":
|
|
314
|
+
includes.append((args[0], kwargs.get("platform")))
|
|
315
|
+
elif name == "link":
|
|
316
|
+
# managed=True -> registry-resolved third-party dep.
|
|
317
|
+
# managed=False (or omitted) -> raw -lfoo linker flag.
|
|
318
|
+
if kwargs.get("managed", False):
|
|
319
|
+
third_party_deps.append((args[0], kwargs.get("platform")))
|
|
320
|
+
else:
|
|
321
|
+
link_libs.append((args[0], kwargs.get("platform")))
|
|
322
|
+
elif name == "cpp_namespace":
|
|
323
|
+
ns_value = args[0]
|
|
324
|
+
if not _CPP_NAMESPACE_RE.match(ns_value):
|
|
325
|
+
warnings.append(ParseWarning(
|
|
326
|
+
f"invalid namespace: {ns_value!r} (must be valid C++ namespace like 'foo::bar')", loc))
|
|
327
|
+
continue
|
|
328
|
+
if cpp_namespace is not None:
|
|
329
|
+
warnings.append(ParseWarning(
|
|
330
|
+
f"duplicate 'cpp_namespace' directive (previous: {cpp_namespace!r})", loc))
|
|
331
|
+
cpp_namespace = ns_value
|
|
332
|
+
|
|
333
|
+
return ModuleDirectives(
|
|
334
|
+
includes=includes, link_libs=link_libs,
|
|
335
|
+
third_party_deps=third_party_deps,
|
|
336
|
+
native_module=native_module,
|
|
337
|
+
cpp_namespace=cpp_namespace,
|
|
338
|
+
), warnings
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _collect_bitor_arms(node: ast.BinOp) -> list[ast.expr]:
|
|
342
|
+
"""Flatten a left-recursive chain of A | B | C into [A, B, C]."""
|
|
343
|
+
arms: list[ast.expr] = []
|
|
344
|
+
if isinstance(node.left, ast.BinOp) and isinstance(node.left.op, ast.BitOr):
|
|
345
|
+
arms.extend(_collect_bitor_arms(node.left))
|
|
346
|
+
else:
|
|
347
|
+
arms.append(node.left)
|
|
348
|
+
arms.append(node.right)
|
|
349
|
+
return arms
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class _NameArg:
|
|
353
|
+
"""Decorator argument that is a name reference (e.g. StopIteration in @error_return(StopIteration))."""
|
|
354
|
+
__slots__ = ("name",)
|
|
355
|
+
|
|
356
|
+
def __init__(self, name: str) -> None:
|
|
357
|
+
self.name = name
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@dataclasses.dataclass(frozen=True)
|
|
361
|
+
class _DecoratorArgSchema:
|
|
362
|
+
"""Schema for a decorator's positional and keyword arguments.
|
|
363
|
+
|
|
364
|
+
pos_type: expected type for the positional arg (str, bool, _NameArg), or None = bare only
|
|
365
|
+
pos_required: whether the positional arg must be provided
|
|
366
|
+
pos_description: human-readable type description for error messages (e.g. "bool")
|
|
367
|
+
kwargs: allowed keyword arg names -> expected types (None = no kwargs)
|
|
368
|
+
"""
|
|
369
|
+
pos_type: type | None = None
|
|
370
|
+
pos_required: bool = False
|
|
371
|
+
pos_description: str | None = None
|
|
372
|
+
kwargs: dict[str, type] | None = None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Argument schemas for all known decorators. Decorators not listed here
|
|
376
|
+
# (macro decorators, etc.) are validated by their own paths.
|
|
377
|
+
_DECORATOR_ARG_SCHEMAS: dict[str, _DecoratorArgSchema] = {
|
|
378
|
+
# Only decorators without @builtin_decorator stubs need explicit schemas.
|
|
379
|
+
# All other schemas are derived from stub signatures in .py files
|
|
380
|
+
# (see Parser._schema_from_stub and Parser._decorator_schemas).
|
|
381
|
+
qnames.STATICMETHOD: _DecoratorArgSchema(), # Python builtin, no stub
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _stmt_child_bodies(stmt: TpyStmt) -> list[list[TpyStmt]]:
|
|
386
|
+
"""Return the child statement bodies of a compound statement (no nested defs)."""
|
|
387
|
+
if isinstance(stmt, TpyIf):
|
|
388
|
+
return [stmt.then_body, stmt.else_body]
|
|
389
|
+
elif isinstance(stmt, TpyWhile):
|
|
390
|
+
bodies = [stmt.body]
|
|
391
|
+
if stmt.orelse:
|
|
392
|
+
bodies.append(stmt.orelse)
|
|
393
|
+
return bodies
|
|
394
|
+
elif isinstance(stmt, TpyForEach):
|
|
395
|
+
bodies = [stmt.body]
|
|
396
|
+
if stmt.orelse:
|
|
397
|
+
bodies.append(stmt.orelse)
|
|
398
|
+
return bodies
|
|
399
|
+
elif isinstance(stmt, TpyWith):
|
|
400
|
+
return [stmt.body]
|
|
401
|
+
elif isinstance(stmt, TpyTry):
|
|
402
|
+
bodies = [stmt.try_body, stmt.else_body, stmt.finally_body]
|
|
403
|
+
for h in stmt.handlers:
|
|
404
|
+
bodies.append(h.body)
|
|
405
|
+
return bodies
|
|
406
|
+
elif isinstance(stmt, TpyMatch):
|
|
407
|
+
return [case.body for case in stmt.cases]
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _body_contains_yield(stmts: list[TpyStmt]) -> bool:
|
|
412
|
+
"""Check if a function body contains any yield statements (non-recursive into nested defs)."""
|
|
413
|
+
for stmt in stmts:
|
|
414
|
+
if isinstance(stmt, TpyYield):
|
|
415
|
+
return True
|
|
416
|
+
for child_body in _stmt_child_bodies(stmt):
|
|
417
|
+
if _body_contains_yield(child_body):
|
|
418
|
+
return True
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _check_no_return_value_in_generator(
|
|
423
|
+
stmts: list[TpyStmt], func_name: str,
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Reject 'return value' inside a generator function body."""
|
|
426
|
+
for stmt in stmts:
|
|
427
|
+
if isinstance(stmt, TpyReturn) and stmt.value is not None:
|
|
428
|
+
# Create a minimal object with lineno for ParseError
|
|
429
|
+
err = ParseError(
|
|
430
|
+
f"Generator function '{func_name}' cannot use 'return' with a value")
|
|
431
|
+
if stmt.loc:
|
|
432
|
+
err.lineno = stmt.loc.line
|
|
433
|
+
raise err
|
|
434
|
+
for child_body in _stmt_child_bodies(stmt):
|
|
435
|
+
_check_no_return_value_in_generator(child_body, func_name)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class Parser:
|
|
439
|
+
"""Parser for TurboPython source code."""
|
|
440
|
+
|
|
441
|
+
FORBIDDEN_CONSTRUCTS = {
|
|
442
|
+
"with", "async", "await",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
def __init__(self, decorator_schemas: dict[str, '_DecoratorArgSchema'] | None = None):
|
|
446
|
+
self.registry = TypeRegistry()
|
|
447
|
+
self.source_lines: list[str] = []
|
|
448
|
+
self._type_param_scope: dict[str, TypeParamKind] | None = None
|
|
449
|
+
self._warnings: list[ParseWarning] = []
|
|
450
|
+
self._imports = ImportProcessor(self._warn)
|
|
451
|
+
self._module_aliases: dict[str, str] = {}
|
|
452
|
+
self._bare_module_imports: set[str] = set()
|
|
453
|
+
self._reverse_module_aliases: dict[str, str] = {}
|
|
454
|
+
self._for_unpack_counter: int = 0
|
|
455
|
+
self._multi_assign_counter: int = 0
|
|
456
|
+
# Schemas derived from @builtin_decorator stubs (populated by compiler
|
|
457
|
+
# from previously-parsed modules, or from same-file definitions)
|
|
458
|
+
self._decorator_schemas: dict[str, _DecoratorArgSchema] = dict(decorator_schemas) if decorator_schemas else {}
|
|
459
|
+
# Top-level class names pre-scanned from the module body, used to
|
|
460
|
+
# resolve forward references in type annotations.
|
|
461
|
+
self._module_class_names: frozenset[str] = frozenset()
|
|
462
|
+
# Top-level type alias names pre-scanned, for forward references
|
|
463
|
+
# (e.g. Box[Expr] in a record field before `type Expr = ...` is parsed)
|
|
464
|
+
self._module_type_alias_names: frozenset[str] = frozenset()
|
|
465
|
+
# Union aliases detected as recursive (self- or mutually-referencing)
|
|
466
|
+
self._recursive_union_names: set[str] = set()
|
|
467
|
+
# All module-level definitions (def, class, assignment) pre-scanned
|
|
468
|
+
# to detect when local names shadow imports for parser keyword resolution.
|
|
469
|
+
self._local_defs: frozenset[str] = frozenset()
|
|
470
|
+
# Maps short nested type names to dotted names while inside a class body.
|
|
471
|
+
# E.g., while parsing class Message: class Kind(Enum): ..., maps "Kind" -> "Message.Kind"
|
|
472
|
+
self._nested_type_scope: dict[str, str] = {}
|
|
473
|
+
# Type-ref resolver: holds a back-reference to this parser so it
|
|
474
|
+
# reads live state (registry, imports, local_defs, ...) each call.
|
|
475
|
+
# Attached to TpyModule.resolver at end of parse(); sema delegates
|
|
476
|
+
# here.
|
|
477
|
+
self._resolver = TypeResolver(self)
|
|
478
|
+
# Module # tpy: directives, populated by parse() before
|
|
479
|
+
# _parse_module so class/protocol/enum registration can see
|
|
480
|
+
# cpp_namespace at registration time.
|
|
481
|
+
self._directives = ModuleDirectives()
|
|
482
|
+
# True when the module is compiled as an entry point. Overrides
|
|
483
|
+
# the module name used by `_public_module()` to `"__main__"`.
|
|
484
|
+
# Set by parse() from its `is_entry_point` argument.
|
|
485
|
+
self._is_entry_point: bool = False
|
|
486
|
+
|
|
487
|
+
def _loc(self, node: ast.AST) -> SourceLocation | None:
|
|
488
|
+
"""Create a SourceLocation from an AST node."""
|
|
489
|
+
if hasattr(node, 'lineno'):
|
|
490
|
+
col = getattr(node, 'col_offset', 0)
|
|
491
|
+
return SourceLocation(line=node.lineno, column=col)
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
def _warn(self, message: str, node: ast.AST | None = None) -> None:
|
|
495
|
+
"""Record a parser warning."""
|
|
496
|
+
loc = self._loc(node) if node else None
|
|
497
|
+
self._warnings.append(ParseWarning(message, loc))
|
|
498
|
+
|
|
499
|
+
def _warn_at_loc(self, message: str, loc: SourceLocation | None) -> None:
|
|
500
|
+
"""Record a parser warning at an already-resolved source location.
|
|
501
|
+
|
|
502
|
+
Used by callers (e.g. `type_resolver.py`) that hold a `SourceLocation`
|
|
503
|
+
directly rather than an `ast.AST` node.
|
|
504
|
+
"""
|
|
505
|
+
self._warnings.append(ParseWarning(message, loc))
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
def _qualify(resolved: tuple[str, str]) -> str:
|
|
509
|
+
"""Join a (module, name) resolution to a qualified name string."""
|
|
510
|
+
return f"{resolved[0]}.{resolved[1]}"
|
|
511
|
+
|
|
512
|
+
def _public_module(self) -> str:
|
|
513
|
+
"""Return the public module name for records/protocols/enums
|
|
514
|
+
registered from this module, collapsing private submodules via
|
|
515
|
+
`public_module_name`. Used to populate `RecordInfo.module`,
|
|
516
|
+
`ProtocolInfo.module`, and enum placeholder qnames.
|
|
517
|
+
|
|
518
|
+
Entry-point modules use `"__main__"` regardless of their on-disk
|
|
519
|
+
file name, matching sema's convention (`analyzer.analyze` sets
|
|
520
|
+
`ctx.module_name = "__main__"` for the entry point) and Python's
|
|
521
|
+
runtime `__name__` semantics. Parser and sema must agree on the
|
|
522
|
+
qname so the resolver mints authoritative `_module_qname` values
|
|
523
|
+
on the first pass.
|
|
524
|
+
|
|
525
|
+
Fallback: when the parser is invoked without a `module_name`
|
|
526
|
+
(e.g. the `test_parse_type_ref.py` unit harness parses snippets
|
|
527
|
+
without a module context), return `"__main__"` so qname
|
|
528
|
+
construction stays well-formed. Compiler-driven parses always
|
|
529
|
+
set `module_name` + `is_entry_point`, so this branch is only
|
|
530
|
+
reached from fragment-level tests."""
|
|
531
|
+
if self._is_entry_point:
|
|
532
|
+
return "__main__"
|
|
533
|
+
return public_module_name(
|
|
534
|
+
self._imports._module_name,
|
|
535
|
+
self._directives.cpp_namespace,
|
|
536
|
+
) or "__main__"
|
|
537
|
+
|
|
538
|
+
def _resolve_type_name(self, local_name: str) -> tuple[str, str] | None:
|
|
539
|
+
"""Resolve annotation name -> (module, original_name) or None.
|
|
540
|
+
|
|
541
|
+
Checks explicit imports, then Python builtins, then local @builtin_type
|
|
542
|
+
definitions. Returns None when the name is shadowed by a module-level
|
|
543
|
+
def/class/assignment (excluding @builtin_type/decorator/function stubs).
|
|
544
|
+
"""
|
|
545
|
+
if local_name in self._local_defs:
|
|
546
|
+
return None
|
|
547
|
+
source = self._imports.get_import_source(local_name)
|
|
548
|
+
if source:
|
|
549
|
+
return source
|
|
550
|
+
|
|
551
|
+
if local_name in get_builtins_exports():
|
|
552
|
+
return ("builtins", local_name)
|
|
553
|
+
|
|
554
|
+
# Check for @builtin_type / @builtin_decorator defined locally in this file
|
|
555
|
+
builtin_key = self.registry.get_builtin_type_key(local_name) or self.registry.get_builtin_decorator_key(local_name)
|
|
556
|
+
if builtin_key:
|
|
557
|
+
parts = builtin_key.rsplit(".", 1)
|
|
558
|
+
if len(parts) == 2:
|
|
559
|
+
return (parts[0], parts[1])
|
|
560
|
+
|
|
561
|
+
# Auto-resolve builtin_decorator within tpy.extern's own module
|
|
562
|
+
if local_name == "builtin_decorator" and self._imports._module_name:
|
|
563
|
+
pub = _PRIVATE_MODULE_PUBLIC_NAMES.get(self._imports._module_name) or public_module_name(self._imports._module_name)
|
|
564
|
+
if pub == "tpy.extern":
|
|
565
|
+
return ("tpy.extern", local_name)
|
|
566
|
+
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
def _resolve_parser_keyword(self, node: ast.expr) -> tuple[str, str] | None:
|
|
570
|
+
"""Resolve a Name or Attribute node through import resolution.
|
|
571
|
+
|
|
572
|
+
Convenience wrapper that dispatches to _resolve_type_name (for bare
|
|
573
|
+
names) or _resolve_qualified_type_name (for module.attr). Both
|
|
574
|
+
underlying methods already respect _local_defs shadowing.
|
|
575
|
+
"""
|
|
576
|
+
if isinstance(node, ast.Name):
|
|
577
|
+
return self._resolve_type_name(node.id)
|
|
578
|
+
elif isinstance(node, ast.Attribute):
|
|
579
|
+
return self._resolve_qualified_type_name(node)
|
|
580
|
+
return None
|
|
581
|
+
|
|
582
|
+
def _resolve_qualified_type_name(self, node: ast.Attribute) -> tuple[str, str] | None:
|
|
583
|
+
"""Resolve module.Name -> (module, name) or None.
|
|
584
|
+
|
|
585
|
+
Only resolves if the module was bare-imported (import X or import X as Y).
|
|
586
|
+
'from X import ...' does NOT put the module name in scope.
|
|
587
|
+
Returns None if the module prefix is shadowed by a local definition.
|
|
588
|
+
"""
|
|
589
|
+
if not isinstance(node.value, ast.Name):
|
|
590
|
+
return None
|
|
591
|
+
local_module = node.value.id
|
|
592
|
+
if local_module in self._local_defs:
|
|
593
|
+
return None
|
|
594
|
+
canonical = self._reverse_module_aliases.get(local_module, local_module)
|
|
595
|
+
# Verify the module was bare-imported (imports[canonical] is None means
|
|
596
|
+
# whole-module import; the key being absent means no import at all).
|
|
597
|
+
# For parser-keyword modules, None marks whole-module import.
|
|
598
|
+
# For user modules, they're in bare_module_imports (checked via imports dict).
|
|
599
|
+
imports = self._imports.imports
|
|
600
|
+
if imports is None or canonical not in imports:
|
|
601
|
+
return None
|
|
602
|
+
entry = imports[canonical]
|
|
603
|
+
# None means whole-module import (parser-keyword modules).
|
|
604
|
+
# For user modules, bare import sets an empty set AND adds to bare_module_imports.
|
|
605
|
+
if entry is not None and not (isinstance(entry, set) and canonical in self._bare_module_imports):
|
|
606
|
+
return None
|
|
607
|
+
return (canonical, node.attr)
|
|
608
|
+
|
|
609
|
+
def _resolve_dotted_class_name(self, node: ast.Attribute) -> str | None:
|
|
610
|
+
"""Resolve Outer.Inner (or Outer.Mid.Inner) to a dotted name string.
|
|
611
|
+
|
|
612
|
+
Returns the dotted name if the root is a known class name, else None.
|
|
613
|
+
"""
|
|
614
|
+
parts: list[str] = []
|
|
615
|
+
cur: ast.expr = node
|
|
616
|
+
while isinstance(cur, ast.Attribute):
|
|
617
|
+
parts.append(cur.attr)
|
|
618
|
+
cur = cur.value
|
|
619
|
+
if isinstance(cur, ast.Name) and cur.id in self._module_class_names:
|
|
620
|
+
parts.append(cur.id)
|
|
621
|
+
parts.reverse()
|
|
622
|
+
return ".".join(parts)
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
def _raise_unresolved_import_error(
|
|
626
|
+
self, raw_name: str, node: ast.expr | None = None,
|
|
627
|
+
*, loc: SourceLocation | None = None,
|
|
628
|
+
) -> None:
|
|
629
|
+
"""Raise a helpful error for unresolved type names with import hints."""
|
|
630
|
+
if raw_name in get_typing_exports():
|
|
631
|
+
raise ParseError(
|
|
632
|
+
f"'{raw_name}' requires: from typing import {raw_name}", node, loc=loc,
|
|
633
|
+
)
|
|
634
|
+
# In stdlib _core modules, unresolved names may be forward references
|
|
635
|
+
# to types defined later in the same file. Let them through.
|
|
636
|
+
mod = self._imports._module_name
|
|
637
|
+
if mod and "._" in mod and raw_name in self._module_class_names:
|
|
638
|
+
if public_module_name(mod) in _IMPLICIT_MODULES:
|
|
639
|
+
return
|
|
640
|
+
if raw_name in get_tpy_exports():
|
|
641
|
+
raise ParseError(
|
|
642
|
+
f"'{raw_name}' requires: from tpy import {raw_name}", node, loc=loc,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def _is_ignorable_for_import_order(self, node: ast.stmt) -> bool:
|
|
646
|
+
"""Check if a statement should be ignored for import ordering.
|
|
647
|
+
|
|
648
|
+
Module docstrings and pass statements don't count as "code"
|
|
649
|
+
for the purpose of detecting late imports.
|
|
650
|
+
"""
|
|
651
|
+
if isinstance(node, ast.Pass):
|
|
652
|
+
return True
|
|
653
|
+
# Module docstring (expression statement with a string literal)
|
|
654
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
655
|
+
return True
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
def parse(self, source: str, module_name: str | None = None,
|
|
659
|
+
is_package_init: bool = False,
|
|
660
|
+
is_entry_point: bool = False) -> TpyModule:
|
|
661
|
+
"""Parse TurboPython source code into a TpyModule.
|
|
662
|
+
|
|
663
|
+
`is_entry_point` mirrors the compiler's convention of treating
|
|
664
|
+
the entry-point module as `__main__` for qname purposes
|
|
665
|
+
(matching sema's `ctx.module_name = "__main__"` rename and
|
|
666
|
+
Python's runtime `__name__` convention). It only affects the
|
|
667
|
+
value returned by `_public_module()` and hence the qnames
|
|
668
|
+
attached to record / protocol / enum-placeholder registrations;
|
|
669
|
+
import resolution still uses the original `module_name`.
|
|
670
|
+
"""
|
|
671
|
+
self.source_lines = source.splitlines()
|
|
672
|
+
self._warnings = []
|
|
673
|
+
self._is_entry_point = is_entry_point
|
|
674
|
+
self._imports = ImportProcessor(self._warn, module_name=module_name,
|
|
675
|
+
is_package_init=is_package_init)
|
|
676
|
+
self._module_aliases = {}
|
|
677
|
+
self._bare_module_imports = set()
|
|
678
|
+
self._reverse_module_aliases = {}
|
|
679
|
+
tree = ast.parse(source)
|
|
680
|
+
# Scan directives first so cpp_namespace is available while
|
|
681
|
+
# _parse_module registers records/protocols/enums.
|
|
682
|
+
directives, directive_warnings = _scan_directives(self.source_lines)
|
|
683
|
+
self._directives = directives
|
|
684
|
+
try:
|
|
685
|
+
module_all_result = read_module_all(tree)
|
|
686
|
+
except NonLiteralAllError as exc:
|
|
687
|
+
raise ParseError(
|
|
688
|
+
"__all__ is not a compile-time literal (must be a "
|
|
689
|
+
"list / tuple / set of string literals)",
|
|
690
|
+
exc.node)
|
|
691
|
+
module = self._parse_module(tree)
|
|
692
|
+
if module_all_result is not None:
|
|
693
|
+
module.module_all, module.module_all_loc = module_all_result
|
|
694
|
+
module.directives = directives
|
|
695
|
+
module.parse_warnings.extend(directive_warnings)
|
|
696
|
+
# Attach the ref resolver so sema can resolve TypeRefNodes emitted
|
|
697
|
+
# at annotation sites. The TypeResolver instance holds a back-ref
|
|
698
|
+
# to this parser and reads parser state each call, so sema sees
|
|
699
|
+
# live containers (registry, imports, local_defs, ...).
|
|
700
|
+
module.resolver = self._resolver
|
|
701
|
+
return module
|
|
702
|
+
|
|
703
|
+
# Names that _parse_type_annotation resolves directly (not through registry)
|
|
704
|
+
_BUILTIN_TYPE_NAMES = frozenset({
|
|
705
|
+
"int", "float", "bool", "str", "None", "tuple",
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
def _is_type_name(self, name: str) -> bool:
|
|
709
|
+
"""Check if a name is recognizable as a type by _parse_type_annotation."""
|
|
710
|
+
resolved = self._resolve_type_name(name)
|
|
711
|
+
if resolved:
|
|
712
|
+
original = resolved[1]
|
|
713
|
+
if original in self._BUILTIN_TYPE_NAMES or original in get_tpy_exports():
|
|
714
|
+
return True
|
|
715
|
+
if original == "Self":
|
|
716
|
+
return True
|
|
717
|
+
# Also check registry directly for user-defined types
|
|
718
|
+
return self.registry.is_known_type(name)
|
|
719
|
+
|
|
720
|
+
def _could_be_type(self, name: str) -> bool:
|
|
721
|
+
"""Check if a name could plausibly refer to a type.
|
|
722
|
+
|
|
723
|
+
Checks the local class pre-scan, the parser registry, and import
|
|
724
|
+
resolution. Used at call-parsing sites where the parser needs to
|
|
725
|
+
decide whether Name[args](...) could be a type instantiation.
|
|
726
|
+
"""
|
|
727
|
+
return (name in self._module_class_names
|
|
728
|
+
or name in self._module_type_alias_names
|
|
729
|
+
or self.registry.is_known_type(name)
|
|
730
|
+
or self._resolve_type_name(name) is not None)
|
|
731
|
+
|
|
732
|
+
def _is_type_alias_assign(self, node: ast.Assign) -> bool:
|
|
733
|
+
"""Check if an assignment is an old-style type alias (e.g., Shape = Circle | Rect).
|
|
734
|
+
|
|
735
|
+
Triggers when ALL arms are Names/None AND at least one arm is a
|
|
736
|
+
confirmed type (registered or builtin). Pure forward-ref aliases
|
|
737
|
+
(all arms are class names defined in this module) also match.
|
|
738
|
+
"""
|
|
739
|
+
if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name):
|
|
740
|
+
return False
|
|
741
|
+
if not (isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.BitOr)):
|
|
742
|
+
return False
|
|
743
|
+
arms = _collect_bitor_arms(node.value)
|
|
744
|
+
has_confirmed_type = False
|
|
745
|
+
all_module_classes = True
|
|
746
|
+
for arm in arms:
|
|
747
|
+
if isinstance(arm, ast.Constant) and arm.value is None:
|
|
748
|
+
has_confirmed_type = True
|
|
749
|
+
continue
|
|
750
|
+
if not isinstance(arm, ast.Name):
|
|
751
|
+
return False
|
|
752
|
+
if self._is_type_name(arm.id):
|
|
753
|
+
has_confirmed_type = True
|
|
754
|
+
elif arm.id not in self._module_class_names and arm.id not in self._module_type_alias_names:
|
|
755
|
+
all_module_classes = False
|
|
756
|
+
return has_confirmed_type or all_module_classes
|
|
757
|
+
|
|
758
|
+
def _parse_alias_type_params(
|
|
759
|
+
self, node: ast.TypeAlias,
|
|
760
|
+
) -> 'tuple[list[str], list[TypeParamKind]]':
|
|
761
|
+
"""Parse PEP 695 type parameters on a `type X[...] = ...` alias.
|
|
762
|
+
|
|
763
|
+
v1 of generic recursive type aliases (see
|
|
764
|
+
`docs/GENERIC_RECURSIVE_ALIASES_DESIGN.md`) accepts only bare
|
|
765
|
+
`ast.TypeVar` entries. Bounds (`type Tree[T: Hashable] = ...`),
|
|
766
|
+
defaults (PEP 696), `TypeVarTuple`, and `ParamSpec` are rejected
|
|
767
|
+
outright -- silent stripping would diverge from CPython
|
|
768
|
+
semantics.
|
|
769
|
+
"""
|
|
770
|
+
if not (hasattr(node, 'type_params') and node.type_params):
|
|
771
|
+
return [], []
|
|
772
|
+
type_params: list[str] = []
|
|
773
|
+
type_param_kinds: list[TypeParamKind] = []
|
|
774
|
+
for tp in node.type_params:
|
|
775
|
+
if not isinstance(tp, ast.TypeVar):
|
|
776
|
+
raise ParseError(
|
|
777
|
+
f"Type alias '{node.name.id}': only simple type "
|
|
778
|
+
f"parameters are supported in v1; got "
|
|
779
|
+
f"{type(tp).__name__}",
|
|
780
|
+
node,
|
|
781
|
+
)
|
|
782
|
+
if tp.bound is not None:
|
|
783
|
+
raise ParseError(
|
|
784
|
+
f"Type alias '{node.name.id}': bounds on type "
|
|
785
|
+
f"parameters (`{tp.name}: ...`) are not yet "
|
|
786
|
+
f"supported. Remove the bound or open an issue.",
|
|
787
|
+
node,
|
|
788
|
+
)
|
|
789
|
+
if getattr(tp, 'default_value', None) is not None:
|
|
790
|
+
raise ParseError(
|
|
791
|
+
f"Type alias '{node.name.id}': default values on "
|
|
792
|
+
f"type parameters (`{tp.name} = ...`) are not yet "
|
|
793
|
+
f"supported.",
|
|
794
|
+
node,
|
|
795
|
+
)
|
|
796
|
+
if tp.name in type_params:
|
|
797
|
+
raise ParseError(
|
|
798
|
+
f"Type alias '{node.name.id}': duplicate type "
|
|
799
|
+
f"parameter name '{tp.name}'.",
|
|
800
|
+
node,
|
|
801
|
+
)
|
|
802
|
+
type_params.append(tp.name)
|
|
803
|
+
type_param_kinds.append(TypeParamKind.TYPE)
|
|
804
|
+
return type_params, type_param_kinds
|
|
805
|
+
|
|
806
|
+
def _register_type_alias(
|
|
807
|
+
self, name: str, type_node: ast.expr,
|
|
808
|
+
type_aliases: 'dict[str, tuple[TpyType | TypeRefNode, SourceLocation | None, list[str], list[TypeParamKind]]]',
|
|
809
|
+
type_params: 'list[str] | None' = None,
|
|
810
|
+
type_param_kinds: 'list[TypeParamKind] | None' = None,
|
|
811
|
+
) -> None:
|
|
812
|
+
"""Parse a type alias RHS as a TypeRefNode.
|
|
813
|
+
|
|
814
|
+
`type_params` / `type_param_kinds` describe an alias's generic
|
|
815
|
+
parameters (`type Tree[T] = ...`); both empty for non-generic
|
|
816
|
+
aliases. When present, the parser temporarily binds them into
|
|
817
|
+
`_type_param_scope` so `T` references in the body resolve as
|
|
818
|
+
`TypeParamRef` instead of failing with "Unknown type: T". Sema
|
|
819
|
+
rejects further use until generic-alias substitution lands; see
|
|
820
|
+
`docs/GENERIC_RECURSIVE_ALIASES_DESIGN.md`.
|
|
821
|
+
|
|
822
|
+
The post-parse `resolve_refs` pass resolves the ref with a
|
|
823
|
+
`pending_alias` kwarg so same-body self-references produce a
|
|
824
|
+
`NominalType(name)` placeholder. Sema then detects recursive
|
|
825
|
+
unions and registers the resolved alias in parser.registry so
|
|
826
|
+
later alias bodies can reference it.
|
|
827
|
+
"""
|
|
828
|
+
type_params = list(type_params or [])
|
|
829
|
+
type_param_kinds = list(type_param_kinds or [])
|
|
830
|
+
loc = SourceLocation(line=type_node.lineno) if hasattr(type_node, 'lineno') else None
|
|
831
|
+
if type_params:
|
|
832
|
+
# `_parse_type_ref` defaults its scope arg to `self._type_param_scope`,
|
|
833
|
+
# so setting the attribute is enough -- no explicit pass needed.
|
|
834
|
+
old_scope = self._type_param_scope
|
|
835
|
+
self._type_param_scope = dict(zip(type_params, type_param_kinds))
|
|
836
|
+
try:
|
|
837
|
+
ref = self._parse_type_ref(type_node)
|
|
838
|
+
finally:
|
|
839
|
+
self._type_param_scope = old_scope
|
|
840
|
+
else:
|
|
841
|
+
ref = self._parse_type_ref(type_node)
|
|
842
|
+
type_aliases[name] = (ref, loc, type_params, type_param_kinds)
|
|
843
|
+
|
|
844
|
+
def _parse_module(self, tree: ast.Module) -> TpyModule:
|
|
845
|
+
"""Parse a module."""
|
|
846
|
+
self._module_class_names = frozenset(
|
|
847
|
+
node.name for node in tree.body if isinstance(node, ast.ClassDef)
|
|
848
|
+
)
|
|
849
|
+
self._module_type_alias_names = frozenset(
|
|
850
|
+
node.name.id for node in tree.body if isinstance(node, ast.TypeAlias)
|
|
851
|
+
)
|
|
852
|
+
# Pre-scan all module-level definitions (def, class, assignment) so that
|
|
853
|
+
# _resolve_type_name returns None for shadowed imports. This prevents
|
|
854
|
+
# parser keywords (Enum, Protocol, auto, decorators, type names) from
|
|
855
|
+
# being misresolved when shadowed by a local name.
|
|
856
|
+
local_defs: set[str] = set()
|
|
857
|
+
for node in tree.body:
|
|
858
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
859
|
+
if not any(self._decorator_raw_name(d) in _BUILTIN_DEC_NAMES
|
|
860
|
+
for d in node.decorator_list):
|
|
861
|
+
local_defs.add(node.name)
|
|
862
|
+
elif isinstance(node, ast.Assign):
|
|
863
|
+
for target in node.targets:
|
|
864
|
+
if isinstance(target, ast.Name):
|
|
865
|
+
local_defs.add(target.id)
|
|
866
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
867
|
+
local_defs.add(node.target.id)
|
|
868
|
+
self._local_defs = frozenset(local_defs)
|
|
869
|
+
records = []
|
|
870
|
+
functions = []
|
|
871
|
+
protocols = []
|
|
872
|
+
enums = []
|
|
873
|
+
top_level_stmts = []
|
|
874
|
+
type_aliases: 'dict[str, tuple[TpyType | TypeRefNode, SourceLocation | None, list[str], list[TypeParamKind]]]' = {}
|
|
875
|
+
imports: dict[str, set[tuple[str, str]] | None | str] = {}
|
|
876
|
+
user_module_imports: dict[str, int] = {}
|
|
877
|
+
module_aliases: dict[str, str] = {}
|
|
878
|
+
bare_module_imports: set[str] = set()
|
|
879
|
+
self._module_aliases = module_aliases
|
|
880
|
+
self._bare_module_imports = bare_module_imports
|
|
881
|
+
# Set imports reference early so _resolve_qualified_type_name can check it
|
|
882
|
+
self._imports.imports = imports
|
|
883
|
+
seen_non_import = False
|
|
884
|
+
|
|
885
|
+
for node in tree.body:
|
|
886
|
+
is_import = isinstance(node, (ast.Import, ast.ImportFrom))
|
|
887
|
+
|
|
888
|
+
# Check for late imports (imports after non-import code)
|
|
889
|
+
if is_import and seen_non_import:
|
|
890
|
+
self._warn("Import statement should be at the top of the file", node)
|
|
891
|
+
|
|
892
|
+
if isinstance(node, ast.ImportFrom):
|
|
893
|
+
self._imports.process_import_from(node, imports, user_module_imports, top_level_stmts, module_aliases)
|
|
894
|
+
# Rebuild reverse alias mapping after each import
|
|
895
|
+
self._reverse_module_aliases = {v: k for k, v in module_aliases.items()}
|
|
896
|
+
elif isinstance(node, ast.Import):
|
|
897
|
+
self._imports.process_import(node, imports, user_module_imports, top_level_stmts, module_aliases, bare_module_imports)
|
|
898
|
+
# Rebuild reverse alias mapping after each import
|
|
899
|
+
self._reverse_module_aliases = {v: k for k, v in module_aliases.items()}
|
|
900
|
+
elif isinstance(node, ast.ClassDef):
|
|
901
|
+
seen_non_import = True
|
|
902
|
+
# Warn if class shadows an imported parser keyword name
|
|
903
|
+
# (skip for @builtin_type classes -- shadow is intentional)
|
|
904
|
+
source = self._imports.get_import_source(node.name)
|
|
905
|
+
has_builtin_type = any(
|
|
906
|
+
self._decorator_local_name(d) in ("builtin_type", qnames.BUILTIN_TYPE)
|
|
907
|
+
for d in node.decorator_list)
|
|
908
|
+
if source and source[0] in _PARSER_KEYWORD_MODULES and not has_builtin_type:
|
|
909
|
+
self._warn(f"class '{node.name}' shadows import from '{source[0]}'", node)
|
|
910
|
+
result = self._parse_class(node)
|
|
911
|
+
if isinstance(result, TpyProtocol):
|
|
912
|
+
protocols.append(result)
|
|
913
|
+
# Register the protocol type
|
|
914
|
+
self.registry.register_protocol(ProtocolInfo(
|
|
915
|
+
name=result.name,
|
|
916
|
+
methods=result.methods,
|
|
917
|
+
type_params=result.type_params,
|
|
918
|
+
is_dynamic=result.is_dynamic,
|
|
919
|
+
module=self._public_module(),
|
|
920
|
+
))
|
|
921
|
+
elif isinstance(result, TpyEnum):
|
|
922
|
+
enums.append(result)
|
|
923
|
+
# Register an enum placeholder so it can be used in type
|
|
924
|
+
# annotations within the same file. Sema re-registers with
|
|
925
|
+
# the fully-populated NominalType + TypeDef.enum payload.
|
|
926
|
+
self.registry.register_enum_placeholder(
|
|
927
|
+
result.name, module=self._public_module())
|
|
928
|
+
else:
|
|
929
|
+
records.append(result)
|
|
930
|
+
# Register the record type
|
|
931
|
+
self.registry.register_record(RecordInfo(
|
|
932
|
+
name=result.name,
|
|
933
|
+
fields=result.fields,
|
|
934
|
+
has_init=result.init_method is not None,
|
|
935
|
+
builtin_type_key=result.builtin_type_key,
|
|
936
|
+
is_indirecting=result.is_indirecting,
|
|
937
|
+
module=self._public_module(),
|
|
938
|
+
))
|
|
939
|
+
# Prefix nested type names with parent chain and register
|
|
940
|
+
self._prefix_nested_names(result, result.name)
|
|
941
|
+
self._register_nested_types(result)
|
|
942
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
943
|
+
seen_non_import = True
|
|
944
|
+
# Warn if function shadows an imported parser keyword name
|
|
945
|
+
# (skip for @builtin_function/builtin_decorator -- shadow is intentional)
|
|
946
|
+
source = self._imports.get_import_source(node.name)
|
|
947
|
+
if source and source[0] in _PARSER_KEYWORD_MODULES:
|
|
948
|
+
has_builtin_dec = any(
|
|
949
|
+
self._decorator_local_name(d) in (
|
|
950
|
+
"builtin_function", "builtin_decorator",
|
|
951
|
+
qnames.BUILTIN_FUNCTION, qnames.BUILTIN_DECORATOR)
|
|
952
|
+
for d in node.decorator_list)
|
|
953
|
+
if not has_builtin_dec:
|
|
954
|
+
self._warn(f"def '{node.name}' shadows import from '{source[0]}'", node)
|
|
955
|
+
func = self._parse_function(node)
|
|
956
|
+
functions.append(func)
|
|
957
|
+
if func.builtin_decorator_key:
|
|
958
|
+
# @builtin_decorator stub signatures drive parse-time
|
|
959
|
+
# schema derivation for decorator argument validation
|
|
960
|
+
# (_schema_from_stub inspects param types). Resolve
|
|
961
|
+
# TypeRefNodes now so _schema_from_stub sees TpyType;
|
|
962
|
+
# sema's pre-pass will no-op on already-resolved fields.
|
|
963
|
+
# Module-level scope -- no enclosing type params.
|
|
964
|
+
self._finalize_function_refs(func, outer_scope=None)
|
|
965
|
+
# Register for decorator resolution (like @builtin_type for records)
|
|
966
|
+
# return_type may be None (no annotation) -- FunctionInfo
|
|
967
|
+
# here is used solely for decorator-key lookup; the value
|
|
968
|
+
# is not read for call resolution.
|
|
969
|
+
self.registry.register_function(FunctionInfo(
|
|
970
|
+
name=func.name, params=[], return_type=func.return_type,
|
|
971
|
+
builtin_decorator_key=func.builtin_decorator_key,
|
|
972
|
+
))
|
|
973
|
+
# Derive arg schema from stub signature
|
|
974
|
+
schema = self._schema_from_stub(func)
|
|
975
|
+
if schema is not None:
|
|
976
|
+
self._decorator_schemas[func.builtin_decorator_key] = schema
|
|
977
|
+
elif isinstance(node, ast.TypeAlias):
|
|
978
|
+
seen_non_import = True
|
|
979
|
+
tp_names, tp_kinds = self._parse_alias_type_params(node)
|
|
980
|
+
self._register_type_alias(
|
|
981
|
+
node.name.id, node.value, type_aliases,
|
|
982
|
+
type_params=tp_names, type_param_kinds=tp_kinds,
|
|
983
|
+
)
|
|
984
|
+
elif isinstance(node, ast.Assign) and self._is_type_alias_assign(node):
|
|
985
|
+
seen_non_import = True
|
|
986
|
+
self._register_type_alias(node.targets[0].id, node.value, type_aliases)
|
|
987
|
+
else:
|
|
988
|
+
# Skip docstrings and pass statements for late import detection
|
|
989
|
+
if not self._is_ignorable_for_import_order(node):
|
|
990
|
+
seen_non_import = True
|
|
991
|
+
# All other statements go through _parse_stmt (same as function bodies)
|
|
992
|
+
stmt = self._parse_stmt(node)
|
|
993
|
+
if isinstance(stmt, list):
|
|
994
|
+
top_level_stmts.extend(stmt)
|
|
995
|
+
else:
|
|
996
|
+
top_level_stmts.append(stmt)
|
|
997
|
+
|
|
998
|
+
return TpyModule(records=records, functions=functions, protocols=protocols, enums=enums, top_level_stmts=top_level_stmts, source_lines=self.source_lines, imports=imports, tpy_star_import=self._imports.tpy_star_import, star_imports=self._imports.star_imports, user_module_imports=user_module_imports, module_aliases=module_aliases, bare_module_imports=bare_module_imports, type_aliases=type_aliases, parse_warnings=self._warnings, recursive_union_names=self._recursive_union_names)
|
|
999
|
+
|
|
1000
|
+
def _is_protocol_base(self, base: ast.expr) -> bool:
|
|
1001
|
+
"""Check if a base class expression refers to typing.Protocol."""
|
|
1002
|
+
return self._resolve_parser_keyword(base) == ("typing", "Protocol")
|
|
1003
|
+
|
|
1004
|
+
def _is_enum_base(self, base: ast.expr) -> bool:
|
|
1005
|
+
"""Check if a base class expression refers to enum.Enum."""
|
|
1006
|
+
return self._resolve_parser_keyword(base) == ("enum", "Enum")
|
|
1007
|
+
|
|
1008
|
+
def _is_int_enum_base(self, base: ast.expr) -> bool:
|
|
1009
|
+
"""Check if a base class expression refers to enum.IntEnum."""
|
|
1010
|
+
return self._resolve_parser_keyword(base) == ("enum", "IntEnum")
|
|
1011
|
+
|
|
1012
|
+
def _is_typed_dict_base(self, base: ast.expr) -> bool:
|
|
1013
|
+
"""Check if a base class expression refers to typing.TypedDict."""
|
|
1014
|
+
return self._resolve_parser_keyword(base) == ("typing", "TypedDict")
|
|
1015
|
+
|
|
1016
|
+
def _parse_unpack_annotation(self, annotation: ast.expr, type_param_scope, error_node) -> 'TpyType | TypeRefNode':
|
|
1017
|
+
"""Parse Unpack[TypedDict] annotation from **kwargs.
|
|
1018
|
+
|
|
1019
|
+
Returns the inner TypedDict reference as a TypeRefNode; sema
|
|
1020
|
+
resolves it via `resolve_refs` and validates it
|
|
1021
|
+
is a TypedDict in registration.
|
|
1022
|
+
"""
|
|
1023
|
+
if not isinstance(annotation, ast.Subscript):
|
|
1024
|
+
raise ParseError("**kwargs must have Unpack[TypedDict] annotation", error_node)
|
|
1025
|
+
resolved = self._resolve_parser_keyword(annotation.value)
|
|
1026
|
+
if resolved != ("typing", "Unpack"):
|
|
1027
|
+
raise ParseError("**kwargs must have Unpack[TypedDict] annotation", error_node)
|
|
1028
|
+
return self._parse_type_ref(annotation.slice, type_param_scope)
|
|
1029
|
+
|
|
1030
|
+
# Valid integer mixin types for IntEnum: class P(int, Enum) or class P(Int8, Enum)
|
|
1031
|
+
_INT_MIXIN_TYPES: dict[str, str] = {
|
|
1032
|
+
"int": "int",
|
|
1033
|
+
"Int8": "Int8", "Int16": "Int16", "Int32": "Int32", "Int64": "Int64",
|
|
1034
|
+
"UInt8": "UInt8", "UInt16": "UInt16", "UInt32": "UInt32", "UInt64": "UInt64",
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
def _resolve_int_mixin(self, base: ast.expr) -> str | None:
|
|
1038
|
+
"""Resolve a base class to an integer mixin type name, or None."""
|
|
1039
|
+
if isinstance(base, ast.Name):
|
|
1040
|
+
return self._INT_MIXIN_TYPES.get(base.id)
|
|
1041
|
+
return None
|
|
1042
|
+
|
|
1043
|
+
# Sentinels for _resolve_decorator arg_value
|
|
1044
|
+
_EMPTY_CALL = object() # @name() -- call with zero args
|
|
1045
|
+
_BAD_ARGS = object() # @name(x, y) or non-constant -- caller must error
|
|
1046
|
+
|
|
1047
|
+
def _resolve_decorator(self, dec: ast.expr) -> tuple[str, str, object] | None:
|
|
1048
|
+
"""Resolve a decorator to (module, original_name, arg_value).
|
|
1049
|
+
|
|
1050
|
+
Handles bare (@name), qualified (@mod.name), call-with-args (@name(arg)),
|
|
1051
|
+
and qualified-call (@mod.name(arg)) forms.
|
|
1052
|
+
|
|
1053
|
+
arg_value meanings:
|
|
1054
|
+
None -- bare form (@name)
|
|
1055
|
+
_EMPTY_CALL -- call with zero args (@name())
|
|
1056
|
+
_BAD_ARGS -- invalid args (multiple or non-constant)
|
|
1057
|
+
<value> -- single constant arg value (str, bool, int, etc.)
|
|
1058
|
+
|
|
1059
|
+
Returns None if the decorator name cannot be resolved through imports.
|
|
1060
|
+
"""
|
|
1061
|
+
# Extract the function node and args from Call decorators
|
|
1062
|
+
func_node = dec
|
|
1063
|
+
arg_value = None
|
|
1064
|
+
if isinstance(dec, ast.Call):
|
|
1065
|
+
func_node = dec.func
|
|
1066
|
+
if dec.keywords:
|
|
1067
|
+
# @native("name", function=True) -- positional + keyword args
|
|
1068
|
+
if len(dec.args) == 1 and isinstance(dec.args[0], ast.Constant):
|
|
1069
|
+
kw_dict: dict[str, object] = {}
|
|
1070
|
+
for kw in dec.keywords:
|
|
1071
|
+
if isinstance(kw.value, ast.Constant):
|
|
1072
|
+
kw_dict[kw.arg] = kw.value.value
|
|
1073
|
+
elif isinstance(kw.value, ast.Name):
|
|
1074
|
+
kw_dict[kw.arg] = _NameArg(kw.value.id)
|
|
1075
|
+
arg_value = (dec.args[0].value, kw_dict)
|
|
1076
|
+
elif not dec.args:
|
|
1077
|
+
# @type_param_default(T=int) -- kwargs only
|
|
1078
|
+
kw_dict = {}
|
|
1079
|
+
for kw in dec.keywords:
|
|
1080
|
+
if isinstance(kw.value, ast.Constant):
|
|
1081
|
+
kw_dict[kw.arg] = kw.value.value
|
|
1082
|
+
elif isinstance(kw.value, ast.Name):
|
|
1083
|
+
kw_dict[kw.arg] = _NameArg(kw.value.id)
|
|
1084
|
+
arg_value = (None, kw_dict) if kw_dict else self._BAD_ARGS
|
|
1085
|
+
else:
|
|
1086
|
+
arg_value = self._BAD_ARGS
|
|
1087
|
+
elif not dec.args:
|
|
1088
|
+
arg_value = self._EMPTY_CALL
|
|
1089
|
+
elif len(dec.args) == 1 and isinstance(dec.args[0], ast.Constant):
|
|
1090
|
+
arg_value = dec.args[0].value
|
|
1091
|
+
elif len(dec.args) == 1 and isinstance(dec.args[0], ast.Name):
|
|
1092
|
+
# Name arg like @error_return(StopIteration) -- store as _NameArg
|
|
1093
|
+
arg_value = _NameArg(dec.args[0].id)
|
|
1094
|
+
else:
|
|
1095
|
+
arg_value = self._BAD_ARGS
|
|
1096
|
+
|
|
1097
|
+
# Bare name: @name or @name(arg)
|
|
1098
|
+
if isinstance(func_node, ast.Name):
|
|
1099
|
+
name = func_node.id
|
|
1100
|
+
# @staticmethod and @property are Python builtins, not resolved through imports
|
|
1101
|
+
if name == "staticmethod":
|
|
1102
|
+
return ("builtins", "staticmethod", arg_value)
|
|
1103
|
+
if name == "property":
|
|
1104
|
+
return ("builtins", "property", arg_value)
|
|
1105
|
+
resolved = self._resolve_type_name(name)
|
|
1106
|
+
if resolved:
|
|
1107
|
+
return (resolved[0], resolved[1], arg_value)
|
|
1108
|
+
return None
|
|
1109
|
+
|
|
1110
|
+
# Qualified name: @mod.name or @mod.name(arg)
|
|
1111
|
+
if isinstance(func_node, ast.Attribute) and isinstance(func_node.value, ast.Name):
|
|
1112
|
+
resolved = self._resolve_qualified_type_name(func_node)
|
|
1113
|
+
if resolved:
|
|
1114
|
+
return (resolved[0], resolved[1], arg_value)
|
|
1115
|
+
return None
|
|
1116
|
+
|
|
1117
|
+
return None
|
|
1118
|
+
|
|
1119
|
+
@staticmethod
|
|
1120
|
+
def _decorator_local_name(dec: ast.expr) -> str | None:
|
|
1121
|
+
"""Extract the local name used in the source for a decorator (for error messages)."""
|
|
1122
|
+
func_node = dec.func if isinstance(dec, ast.Call) else dec
|
|
1123
|
+
if isinstance(func_node, ast.Name):
|
|
1124
|
+
return func_node.id
|
|
1125
|
+
if isinstance(func_node, ast.Attribute):
|
|
1126
|
+
return f"{func_node.value.id}.{func_node.attr}" if isinstance(func_node.value, ast.Name) else None
|
|
1127
|
+
return None
|
|
1128
|
+
|
|
1129
|
+
@staticmethod
|
|
1130
|
+
def _decorator_raw_name(dec: ast.expr) -> str | None:
|
|
1131
|
+
"""Extract the bare function name from a decorator AST node.
|
|
1132
|
+
|
|
1133
|
+
Returns just the identifier (e.g. 'builtin_type' from both
|
|
1134
|
+
@builtin_type(...) and @mod.builtin_type(...)). Used by pre-scan
|
|
1135
|
+
to detect stdlib builtin decorators before import resolution.
|
|
1136
|
+
"""
|
|
1137
|
+
func_node = dec.func if isinstance(dec, ast.Call) else dec
|
|
1138
|
+
if isinstance(func_node, ast.Name):
|
|
1139
|
+
return func_node.id
|
|
1140
|
+
if isinstance(func_node, ast.Attribute):
|
|
1141
|
+
return func_node.attr
|
|
1142
|
+
return None
|
|
1143
|
+
|
|
1144
|
+
def _require_decorator(self, dec: ast.expr, context: str) -> tuple[str, object]:
|
|
1145
|
+
"""Resolve a decorator or raise a helpful error. Returns (qname, arg)."""
|
|
1146
|
+
resolved = self._resolve_decorator(dec)
|
|
1147
|
+
if resolved is None:
|
|
1148
|
+
local_name = self._decorator_local_name(dec) or "?"
|
|
1149
|
+
raise ParseError(f"Unknown decorator '{local_name}' on {context}", dec)
|
|
1150
|
+
return self._qualify(resolved), resolved[2]
|
|
1151
|
+
|
|
1152
|
+
def _parse_type_param_default(self, dec: ast.expr) -> dict[str, str]:
|
|
1153
|
+
"""Parse @type_param_default(T=DefaultInt) -> {"T": "tpy.extern.DefaultInt"}."""
|
|
1154
|
+
if not isinstance(dec, ast.Call) or not dec.keywords:
|
|
1155
|
+
raise ParseError("@type_param_default() requires keyword arguments, e.g. @type_param_default(T=DefaultInt)", dec)
|
|
1156
|
+
result: dict[str, str] = {}
|
|
1157
|
+
for kw in dec.keywords:
|
|
1158
|
+
if not isinstance(kw.value, ast.Name):
|
|
1159
|
+
raise ParseError(f"@type_param_default({kw.arg}=...) value must be a type name", dec)
|
|
1160
|
+
name = kw.value.id
|
|
1161
|
+
resolved = self._resolve_type_name(name)
|
|
1162
|
+
if resolved is None:
|
|
1163
|
+
raise ParseError(
|
|
1164
|
+
f"@type_param_default({kw.arg}={name}): unknown type '{name}'", dec)
|
|
1165
|
+
result[kw.arg] = self._qualify(resolved)
|
|
1166
|
+
return result
|
|
1167
|
+
|
|
1168
|
+
def _parse_readonly_arg(self, arg: object, dec: ast.expr) -> tuple[bool, bool]:
|
|
1169
|
+
"""Parse @readonly validated arg -> (is_readonly, readonly_opt_out).
|
|
1170
|
+
|
|
1171
|
+
arg should be the validated positional value from _validate_decorator_args
|
|
1172
|
+
(None for bare/@readonly(), bool for @readonly(True/False)).
|
|
1173
|
+
"""
|
|
1174
|
+
if arg is None:
|
|
1175
|
+
return (True, False)
|
|
1176
|
+
if isinstance(arg, bool):
|
|
1177
|
+
return (arg, not arg)
|
|
1178
|
+
raise ParseError("@readonly() requires a bool argument", dec)
|
|
1179
|
+
|
|
1180
|
+
def _schema_from_stub(self, func: TpyFunction) -> _DecoratorArgSchema | None:
|
|
1181
|
+
"""Derive a _DecoratorArgSchema from a @builtin_decorator stub's signature.
|
|
1182
|
+
|
|
1183
|
+
Single param -> positional arg. Additional params with defaults -> kwargs.
|
|
1184
|
+
Type mapping: bool->bool, str->str, type->_NameArg (type name reference).
|
|
1185
|
+
"""
|
|
1186
|
+
if not func.params:
|
|
1187
|
+
return _DecoratorArgSchema() # bare only
|
|
1188
|
+
|
|
1189
|
+
def _map_type(ptype: TpyType) -> tuple[type, str] | None:
|
|
1190
|
+
# Optional[T] kwargs default to None and accept either a value of T
|
|
1191
|
+
# or no value -- unwrap to T for the schema.
|
|
1192
|
+
if isinstance(ptype, OptionalType):
|
|
1193
|
+
ptype = ptype.inner
|
|
1194
|
+
if is_bool_type(ptype):
|
|
1195
|
+
return (bool, "bool")
|
|
1196
|
+
if is_str_type(ptype):
|
|
1197
|
+
return (str, "str")
|
|
1198
|
+
# `type` annotation -> _NameArg (type-name reference). The TYPE
|
|
1199
|
+
# qname has no registered TypeDef, so check by qualified name.
|
|
1200
|
+
if (isinstance(ptype, NominalType)
|
|
1201
|
+
and ptype.qualified_name() == qnames.TYPE):
|
|
1202
|
+
return (_NameArg, "type name")
|
|
1203
|
+
return None
|
|
1204
|
+
|
|
1205
|
+
# First param -> positional
|
|
1206
|
+
_, ptype = func.params[0]
|
|
1207
|
+
match = _map_type(ptype)
|
|
1208
|
+
if match is None:
|
|
1209
|
+
return None
|
|
1210
|
+
pos_type, pos_desc = match
|
|
1211
|
+
has_default = func.defaults and func.defaults[0] is not None
|
|
1212
|
+
|
|
1213
|
+
# Additional params -> kwargs
|
|
1214
|
+
kwargs: dict[str, type] | None = None
|
|
1215
|
+
for i in range(1, len(func.params)):
|
|
1216
|
+
pname, ptype = func.params[i]
|
|
1217
|
+
match = _map_type(ptype)
|
|
1218
|
+
if match is None:
|
|
1219
|
+
return None
|
|
1220
|
+
if kwargs is None:
|
|
1221
|
+
kwargs = {}
|
|
1222
|
+
kwargs[pname] = match[0]
|
|
1223
|
+
|
|
1224
|
+
return _DecoratorArgSchema(pos_type=pos_type, pos_required=not has_default,
|
|
1225
|
+
pos_description=pos_desc, kwargs=kwargs)
|
|
1226
|
+
|
|
1227
|
+
def _validate_decorator_args(
|
|
1228
|
+
self, qname: str, arg: object, dec: ast.expr,
|
|
1229
|
+
) -> tuple[object, dict[str, object]]:
|
|
1230
|
+
"""Validate decorator args against schema. Returns (positional, kwargs).
|
|
1231
|
+
|
|
1232
|
+
positional is the validated positional arg value, or None if not provided
|
|
1233
|
+
(both bare @name and empty @name() normalize to None for optional args).
|
|
1234
|
+
kwargs is a dict of validated keyword arg values (empty if none).
|
|
1235
|
+
"""
|
|
1236
|
+
# Explicit schemas (for non-stub decorators like auto_readonly,
|
|
1237
|
+
# staticmethod) take precedence; then schemas derived from stubs.
|
|
1238
|
+
schema = _DECORATOR_ARG_SCHEMAS.get(qname) or self._decorator_schemas.get(qname)
|
|
1239
|
+
if schema is None:
|
|
1240
|
+
return (arg, {})
|
|
1241
|
+
dec_name = self._decorator_local_name(dec) or bare_name(qname)
|
|
1242
|
+
|
|
1243
|
+
# Handle tuple form: (positional, {kwargs}) from @native("name", function=True)
|
|
1244
|
+
pos_arg = arg
|
|
1245
|
+
raw_kwargs: dict[str, object] = {}
|
|
1246
|
+
if isinstance(arg, tuple) and len(arg) == 2 and isinstance(arg[1], dict):
|
|
1247
|
+
pos_arg, raw_kwargs = arg
|
|
1248
|
+
|
|
1249
|
+
# Validate positional arg
|
|
1250
|
+
_fallback_desc = {str: "string", bool: "bool", _NameArg: "type name"}
|
|
1251
|
+
desc = schema.pos_description or _fallback_desc.get(schema.pos_type, "valid")
|
|
1252
|
+
if schema.pos_type is None:
|
|
1253
|
+
if pos_arg is not None:
|
|
1254
|
+
raise ParseError(f"@{dec_name} does not take arguments", dec)
|
|
1255
|
+
elif schema.pos_required:
|
|
1256
|
+
if not isinstance(pos_arg, schema.pos_type):
|
|
1257
|
+
raise ParseError(f"@{dec_name}() requires a {desc} argument", dec)
|
|
1258
|
+
else:
|
|
1259
|
+
if pos_arg is not None and pos_arg is not self._EMPTY_CALL and not isinstance(pos_arg, schema.pos_type):
|
|
1260
|
+
raise ParseError(f"@{dec_name}() requires a {desc} argument", dec)
|
|
1261
|
+
if pos_arg is self._EMPTY_CALL:
|
|
1262
|
+
pos_arg = None
|
|
1263
|
+
|
|
1264
|
+
# Validate keyword args
|
|
1265
|
+
if raw_kwargs:
|
|
1266
|
+
if schema.kwargs is None:
|
|
1267
|
+
raise ParseError(f"@{dec_name} does not accept keyword arguments", dec)
|
|
1268
|
+
for key, val in raw_kwargs.items():
|
|
1269
|
+
if key not in schema.kwargs:
|
|
1270
|
+
raise ParseError(f"@{dec_name}() got unexpected keyword argument '{key}'", dec)
|
|
1271
|
+
expected_type = schema.kwargs[key]
|
|
1272
|
+
if not isinstance(val, expected_type):
|
|
1273
|
+
raise ParseError(f"@{dec_name}({key}=...) expects {expected_type.__name__}", dec)
|
|
1274
|
+
|
|
1275
|
+
validated_kwargs = raw_kwargs if schema.kwargs else {}
|
|
1276
|
+
return (pos_arg, validated_kwargs)
|
|
1277
|
+
|
|
1278
|
+
def _extract_decorator_kwargs(self, dec: ast.expr, arg: object, class_name: str) -> dict[str, Any]:
|
|
1279
|
+
"""Extract keyword arguments from a macro decorator call.
|
|
1280
|
+
|
|
1281
|
+
Handles bare ``@name``, empty ``@name()``, and ``@name(k=v, ...)``.
|
|
1282
|
+
Positional arguments are rejected. Kwarg values must be literals.
|
|
1283
|
+
"""
|
|
1284
|
+
if arg is None or arg is self._EMPTY_CALL:
|
|
1285
|
+
return {}
|
|
1286
|
+
# kwargs-only tuple from _resolve_decorator: (None, {k: v, ...})
|
|
1287
|
+
if isinstance(arg, tuple) and len(arg) == 2 and arg[0] is None and isinstance(arg[1], dict):
|
|
1288
|
+
return arg[1]
|
|
1289
|
+
if arg is not self._BAD_ARGS:
|
|
1290
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
1291
|
+
raise ParseError(f"@{dec_name} does not take positional arguments", dec)
|
|
1292
|
+
if not isinstance(dec, ast.Call):
|
|
1293
|
+
return {}
|
|
1294
|
+
if dec.args:
|
|
1295
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
1296
|
+
raise ParseError(f"@{dec_name} does not take positional arguments", dec)
|
|
1297
|
+
result: dict[str, Any] = {}
|
|
1298
|
+
for kw in dec.keywords:
|
|
1299
|
+
if kw.arg is None:
|
|
1300
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
1301
|
+
raise ParseError(f"@{dec_name} does not support **kwargs", dec)
|
|
1302
|
+
try:
|
|
1303
|
+
result[kw.arg] = ast.literal_eval(kw.value)
|
|
1304
|
+
except (ValueError, TypeError):
|
|
1305
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
1306
|
+
raise ParseError(
|
|
1307
|
+
f"@{dec_name}: keyword '{kw.arg}' must be a literal value", dec)
|
|
1308
|
+
return result
|
|
1309
|
+
|
|
1310
|
+
def _resolve_call_import(self, call: TpyExpr, node: ast.expr) -> None:
|
|
1311
|
+
"""Resolve import origin for a call expression and set resolved_import.
|
|
1312
|
+
|
|
1313
|
+
Handles both ``field(...)`` (TpyCall) and ``dataclasses.field(...)``
|
|
1314
|
+
(TpyMethodCall) forms.
|
|
1315
|
+
"""
|
|
1316
|
+
if not isinstance(node, ast.Call):
|
|
1317
|
+
return
|
|
1318
|
+
func = node.func
|
|
1319
|
+
source = None
|
|
1320
|
+
if isinstance(func, ast.Name):
|
|
1321
|
+
source = self._resolve_type_name(func.id)
|
|
1322
|
+
elif isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
1323
|
+
source = self._resolve_qualified_type_name(func)
|
|
1324
|
+
if source is not None:
|
|
1325
|
+
call.resolved_import = source
|
|
1326
|
+
|
|
1327
|
+
def _parse_class(self, node: ast.ClassDef) -> TpyRecord | TpyProtocol | TpyEnum:
|
|
1328
|
+
"""Parse a class definition as a record, protocol, or enum."""
|
|
1329
|
+
if node.bases:
|
|
1330
|
+
# Check for IntEnum first: class P(IntEnum) or class P(int, Enum)
|
|
1331
|
+
has_int_enum = any(self._is_int_enum_base(base) for base in node.bases)
|
|
1332
|
+
if has_int_enum:
|
|
1333
|
+
if len(node.bases) != 1:
|
|
1334
|
+
raise ParseError(
|
|
1335
|
+
"IntEnum must be the only base class", node)
|
|
1336
|
+
# IntEnum without mixin defaults to Int32 (not BigInt)
|
|
1337
|
+
return self._parse_enum(node, is_int_enum=True, underlying_type_name="Int32")
|
|
1338
|
+
|
|
1339
|
+
# Check for mixin pattern: class P(int, Enum) or class P(Int8, Enum)
|
|
1340
|
+
has_enum = any(self._is_enum_base(base) for base in node.bases)
|
|
1341
|
+
if has_enum:
|
|
1342
|
+
if len(node.bases) == 2:
|
|
1343
|
+
# Two bases: one must be Enum, the other an int mixin
|
|
1344
|
+
mixin_type = None
|
|
1345
|
+
for base in node.bases:
|
|
1346
|
+
if not self._is_enum_base(base):
|
|
1347
|
+
mixin_type = self._resolve_int_mixin(base)
|
|
1348
|
+
if mixin_type is None:
|
|
1349
|
+
base_name = base.id if isinstance(base, ast.Name) else ast.unparse(base)
|
|
1350
|
+
raise ParseError(
|
|
1351
|
+
f"Invalid enum mixin type '{base_name}'; "
|
|
1352
|
+
f"expected int, Int8..Int64, or UInt8..UInt64",
|
|
1353
|
+
node)
|
|
1354
|
+
if mixin_type is not None:
|
|
1355
|
+
return self._parse_enum(
|
|
1356
|
+
node, is_int_enum=True, underlying_type_name=mixin_type)
|
|
1357
|
+
elif len(node.bases) > 2:
|
|
1358
|
+
raise ParseError(
|
|
1359
|
+
"Enum class must have at most 2 base classes (mixin + Enum)", node)
|
|
1360
|
+
return self._parse_enum(node)
|
|
1361
|
+
# Check if this is a Protocol definition (has Protocol as one of its bases)
|
|
1362
|
+
has_protocol = any(self._is_protocol_base(base) for base in node.bases)
|
|
1363
|
+
if has_protocol:
|
|
1364
|
+
return self._parse_protocol(node)
|
|
1365
|
+
|
|
1366
|
+
# Check if this is a TypedDict definition
|
|
1367
|
+
is_typed_dict = bool(node.bases) and any(self._is_typed_dict_base(base) for base in node.bases)
|
|
1368
|
+
is_total_false = False
|
|
1369
|
+
if is_typed_dict:
|
|
1370
|
+
non_td_bases = [b for b in node.bases if not self._is_typed_dict_base(b)]
|
|
1371
|
+
if non_td_bases:
|
|
1372
|
+
raise ParseError("TypedDict cannot have additional base classes", node)
|
|
1373
|
+
if node.decorator_list:
|
|
1374
|
+
raise ParseError("Decorators are not supported on TypedDict", node)
|
|
1375
|
+
if hasattr(node, 'type_params') and node.type_params:
|
|
1376
|
+
raise ParseError("Type parameters are not supported on TypedDict", node)
|
|
1377
|
+
# Parse total=False keyword
|
|
1378
|
+
for kw in node.keywords:
|
|
1379
|
+
if kw.arg == "total":
|
|
1380
|
+
if isinstance(kw.value, ast.Constant) and kw.value.value is False:
|
|
1381
|
+
is_total_false = True
|
|
1382
|
+
elif isinstance(kw.value, ast.Constant) and kw.value.value is True:
|
|
1383
|
+
pass # total=True is the default
|
|
1384
|
+
else:
|
|
1385
|
+
raise ParseError("total must be True or False", node)
|
|
1386
|
+
else:
|
|
1387
|
+
self._warnings.append(ParseWarning(
|
|
1388
|
+
f"Unknown class keyword argument '{kw.arg}' on TypedDict",
|
|
1389
|
+
self._loc(node)))
|
|
1390
|
+
|
|
1391
|
+
# Warn on class keyword arguments for non-TypedDict classes
|
|
1392
|
+
if not is_typed_dict and node.keywords:
|
|
1393
|
+
for kw in node.keywords:
|
|
1394
|
+
self._warnings.append(ParseWarning(
|
|
1395
|
+
f"Class keyword argument '{kw.arg}' is not supported",
|
|
1396
|
+
self._loc(node)))
|
|
1397
|
+
|
|
1398
|
+
# Parse record decorators (@native, @nocopy, macro decorators)
|
|
1399
|
+
linkage = RecordLinkage.DEFAULT
|
|
1400
|
+
native_name: str | None = None
|
|
1401
|
+
is_nocopy = False
|
|
1402
|
+
builtin_type_key: str | None = None
|
|
1403
|
+
is_indirecting = False
|
|
1404
|
+
pending_macros: list[tuple[str, dict[str, Any]]] = []
|
|
1405
|
+
for dec in node.decorator_list:
|
|
1406
|
+
qname, arg = self._require_decorator(dec, f"class '{node.name}'")
|
|
1407
|
+
if qname in self._RECORD_LINKAGE_MAP:
|
|
1408
|
+
pos, kw = self._validate_decorator_args(qname, arg, dec)
|
|
1409
|
+
new_linkage = self._RECORD_LINKAGE_MAP[qname]
|
|
1410
|
+
# binding="C" overrides linkage to C variant
|
|
1411
|
+
binding = kw.get("binding", "")
|
|
1412
|
+
if binding == "C":
|
|
1413
|
+
if new_linkage == RecordLinkage.NATIVE:
|
|
1414
|
+
new_linkage = RecordLinkage.NATIVE_C
|
|
1415
|
+
elif binding and binding != "":
|
|
1416
|
+
raise ParseError(
|
|
1417
|
+
f"@{bare_name(qname)}(binding=...) only supports binding=\"C\"", dec)
|
|
1418
|
+
if linkage != RecordLinkage.DEFAULT:
|
|
1419
|
+
raise ParseError(
|
|
1420
|
+
f"Class '{node.name}' cannot have both @{linkage.value} and @{new_linkage.value}", node)
|
|
1421
|
+
linkage = new_linkage
|
|
1422
|
+
native_name = pos
|
|
1423
|
+
if kw.get("indirecting"):
|
|
1424
|
+
is_indirecting = True
|
|
1425
|
+
elif qname == qnames.NOCOPY:
|
|
1426
|
+
self._validate_decorator_args(qname, arg, dec)
|
|
1427
|
+
is_nocopy = True
|
|
1428
|
+
elif qname == qnames.BUILTIN_TYPE:
|
|
1429
|
+
pos, _ = self._validate_decorator_args(qname, arg, dec)
|
|
1430
|
+
builtin_type_key = pos
|
|
1431
|
+
else:
|
|
1432
|
+
# Treat as a macro decorator -- extract kwargs and store for later
|
|
1433
|
+
macro_kwargs = self._extract_decorator_kwargs(dec, arg, node.name)
|
|
1434
|
+
pending_macros.append((qname, macro_kwargs))
|
|
1435
|
+
|
|
1436
|
+
# Extract type parameters FIRST so they're in scope when parsing bases
|
|
1437
|
+
# Python 3.12+ syntax: class Foo[T, U]:
|
|
1438
|
+
# Also extract bounds: class Foo[T: Comparable]: or class Foo[N: int]:
|
|
1439
|
+
type_params = []
|
|
1440
|
+
type_param_kinds: list[TypeParamKind] = []
|
|
1441
|
+
type_param_bounds: dict[str, 'TpyType | TypeRefNode'] = {}
|
|
1442
|
+
if hasattr(node, 'type_params') and node.type_params:
|
|
1443
|
+
for tp in node.type_params:
|
|
1444
|
+
if isinstance(tp, ast.TypeVar):
|
|
1445
|
+
type_params.append(tp.name)
|
|
1446
|
+
if tp.bound is not None:
|
|
1447
|
+
# Check for N: int syntax (integer type parameter)
|
|
1448
|
+
if isinstance(tp.bound, ast.Name) and tp.bound.id == 'int':
|
|
1449
|
+
type_param_kinds.append(TypeParamKind.INT)
|
|
1450
|
+
else:
|
|
1451
|
+
# Protocol bound -- resolution and protocol-shape
|
|
1452
|
+
# validation deferred to sema.
|
|
1453
|
+
type_param_kinds.append(TypeParamKind.TYPE)
|
|
1454
|
+
type_param_bounds[tp.name] = self._parse_type_ref(tp.bound)
|
|
1455
|
+
else:
|
|
1456
|
+
type_param_kinds.append(TypeParamKind.TYPE)
|
|
1457
|
+
else:
|
|
1458
|
+
raise ParseError(f"Only simple type parameters supported, got {type(tp).__name__}", node)
|
|
1459
|
+
|
|
1460
|
+
# Create a dict of type param names to kinds for scope during parsing
|
|
1461
|
+
type_param_scope = dict(zip(type_params, type_param_kinds)) if type_params else None
|
|
1462
|
+
# Store scope for use during method body parsing (expression parsing uses this)
|
|
1463
|
+
old_scope = self._type_param_scope
|
|
1464
|
+
self._type_param_scope = type_param_scope
|
|
1465
|
+
|
|
1466
|
+
# Parse base classes/protocols for inheritance. Classification
|
|
1467
|
+
# into parent class vs protocol happens above via
|
|
1468
|
+
# `_is_protocol_base` (keyword-matching, robust to shadowing
|
|
1469
|
+
# detection). TypedDict marker base is filtered out.
|
|
1470
|
+
#
|
|
1471
|
+
# Bases emit as TypeRefNode; sema re-resolves them in
|
|
1472
|
+
# `resolve_refs` under the record's type-param
|
|
1473
|
+
# scope. Class-body checks that could mask base-resolution
|
|
1474
|
+
# errors (stub-body validation, @native decorator validation,
|
|
1475
|
+
# field-type inference failure) all run in sema, so no
|
|
1476
|
+
# parse-time base resolution side-effect is needed.
|
|
1477
|
+
bases: 'list[TpyType | TypeRefNode]' = []
|
|
1478
|
+
for base in node.bases:
|
|
1479
|
+
if is_typed_dict and self._is_typed_dict_base(base):
|
|
1480
|
+
continue
|
|
1481
|
+
ref = self._parse_type_ref(base, type_param_scope)
|
|
1482
|
+
bases.append(ref)
|
|
1483
|
+
|
|
1484
|
+
fields = []
|
|
1485
|
+
methods = []
|
|
1486
|
+
nested_records: list[TpyRecord] = []
|
|
1487
|
+
nested_enums: list[TpyEnum] = []
|
|
1488
|
+
property_names: set[str] = set()
|
|
1489
|
+
# Save and extend nested type scope so short names resolve inside the class body
|
|
1490
|
+
old_nested_scope = self._nested_type_scope
|
|
1491
|
+
self._nested_type_scope = dict(old_nested_scope)
|
|
1492
|
+
|
|
1493
|
+
for item in node.body:
|
|
1494
|
+
if isinstance(item, ast.AnnAssign):
|
|
1495
|
+
# Field declaration: name: Type = default
|
|
1496
|
+
if not isinstance(item.target, ast.Name):
|
|
1497
|
+
raise ParseError("Invalid field declaration", item)
|
|
1498
|
+
field_name = item.target.id
|
|
1499
|
+
# Emit a TypeRefNode. The post-parse `resolve_refs` pass
|
|
1500
|
+
# pre-pass resolves it before any reader consumes fld.type.
|
|
1501
|
+
field_type = self._parse_type_ref(item.annotation, type_param_scope)
|
|
1502
|
+
default_val = None
|
|
1503
|
+
default_expr = None
|
|
1504
|
+
if item.value is not None:
|
|
1505
|
+
default_expr = self._parse_expr(item.value)
|
|
1506
|
+
# Resolve import origin for call expressions (e.g. field() or dataclasses.field())
|
|
1507
|
+
if isinstance(default_expr, (TpyCall, TpyMethodCall)):
|
|
1508
|
+
self._resolve_call_import(default_expr, item.value)
|
|
1509
|
+
default_val = self._get_default_value(item.value)
|
|
1510
|
+
fields.append(FieldInfo(field_name, field_type, default_val, default_expr=default_expr, loc=self._loc(item)))
|
|
1511
|
+
if is_typed_dict and default_expr is not None:
|
|
1512
|
+
self._warnings.append(ParseWarning(
|
|
1513
|
+
f"TypedDict field '{field_name}' has a default value which is "
|
|
1514
|
+
f"ignored by CPython at runtime; consider using total=False "
|
|
1515
|
+
f"for optional fields",
|
|
1516
|
+
self._loc(item),
|
|
1517
|
+
))
|
|
1518
|
+
elif isinstance(item, ast.Assign):
|
|
1519
|
+
# Field with inferred type: name = Int32(0)
|
|
1520
|
+
if len(item.targets) != 1 or not isinstance(item.targets[0], ast.Name):
|
|
1521
|
+
raise ParseError("Invalid field declaration", item)
|
|
1522
|
+
field_name = item.targets[0].id
|
|
1523
|
+
# Emit a marker; sema's field-resolution pass runs the
|
|
1524
|
+
# inferrer on FieldInfo.default_expr, raising only on
|
|
1525
|
+
# genuine failure. Keeping inference out of the parser
|
|
1526
|
+
# avoids coupling parser to the full typesys surface.
|
|
1527
|
+
field_type: 'TpyType | TypeRefNode | TpyInferFromDefaultRef' = TpyInferFromDefaultRef(loc=self._loc(item))
|
|
1528
|
+
default_val = self._get_default_value(item.value)
|
|
1529
|
+
default_expr = self._parse_expr(item.value)
|
|
1530
|
+
fields.append(FieldInfo(field_name, field_type, default_val,
|
|
1531
|
+
default_expr=default_expr,
|
|
1532
|
+
loc=self._loc(item)))
|
|
1533
|
+
elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
1534
|
+
if is_typed_dict:
|
|
1535
|
+
raise ParseError(f"Methods are not allowed on TypedDict '{node.name}'", item)
|
|
1536
|
+
parsed = self._parse_method(item, node.name, type_param_scope, property_names)
|
|
1537
|
+
if parsed.is_property_getter:
|
|
1538
|
+
property_names.add(parsed.name)
|
|
1539
|
+
# Dual overloads (const + mutable) for correct reference
|
|
1540
|
+
# semantics; sema.method_expansion performs the actual
|
|
1541
|
+
# clone. Registration prunes the mutable clone for
|
|
1542
|
+
# value-type returns.
|
|
1543
|
+
parsed.auto_readonly = True
|
|
1544
|
+
methods.append(parsed)
|
|
1545
|
+
elif isinstance(item, ast.Pass):
|
|
1546
|
+
pass
|
|
1547
|
+
elif isinstance(item, ast.Expr) and isinstance(item.value, ast.Constant) and item.value.value is ...:
|
|
1548
|
+
pass # Ellipsis for opaque native types
|
|
1549
|
+
elif isinstance(item, ast.Expr) and isinstance(item.value, ast.Constant) and isinstance(item.value.value, str):
|
|
1550
|
+
pass # Docstring
|
|
1551
|
+
elif isinstance(item, ast.ClassDef):
|
|
1552
|
+
if is_typed_dict:
|
|
1553
|
+
raise ParseError(f"Nested classes are not allowed in TypedDict '{node.name}'", item)
|
|
1554
|
+
# Parse first so we can give an enum-specific diagnostic when the
|
|
1555
|
+
# nested entity is a @native enum (pointing the user at the
|
|
1556
|
+
# top-level qualified-name binding pattern). The broad-linkage
|
|
1557
|
+
# rejection below catches any other nested class.
|
|
1558
|
+
nested = self._parse_class(item)
|
|
1559
|
+
if isinstance(nested, TpyProtocol):
|
|
1560
|
+
raise ParseError("Protocols cannot be nested inside classes", item)
|
|
1561
|
+
if isinstance(nested, TpyEnum) and nested.is_native:
|
|
1562
|
+
raise ParseError(
|
|
1563
|
+
f"@native enum '{nested.name}' cannot be nested inside "
|
|
1564
|
+
f"a class; declare it at module top level with the "
|
|
1565
|
+
f"fully-qualified C++ name, e.g. "
|
|
1566
|
+
f"`@native(\"ns::Container::{nested.name}\") "
|
|
1567
|
+
f"class {nested.name}(Enum): ...`. TPy structure does "
|
|
1568
|
+
f"not need to mirror C++ structure -- the @native "
|
|
1569
|
+
f"qname encodes the C++ nesting.",
|
|
1570
|
+
item)
|
|
1571
|
+
if linkage != RecordLinkage.DEFAULT:
|
|
1572
|
+
raise ParseError(f"Nested classes are not allowed in @{linkage.value} classes", item)
|
|
1573
|
+
if isinstance(nested, TpyEnum):
|
|
1574
|
+
if type_params:
|
|
1575
|
+
raise ParseError(
|
|
1576
|
+
f"Nested enums are not supported inside generic classes "
|
|
1577
|
+
f"('{node.name}' has type parameters)", item)
|
|
1578
|
+
# Register immediately with dotted name so forward references
|
|
1579
|
+
# within the same class body work (e.g., kind: Container.Kind)
|
|
1580
|
+
dotted_name = f"{node.name}.{nested.name}"
|
|
1581
|
+
self.registry.register_enum_placeholder(
|
|
1582
|
+
dotted_name, module=self._public_module())
|
|
1583
|
+
self._nested_type_scope[nested.name] = dotted_name
|
|
1584
|
+
nested_enums.append(nested)
|
|
1585
|
+
else:
|
|
1586
|
+
if type_params:
|
|
1587
|
+
raise ParseError(
|
|
1588
|
+
f"Nested classes are not supported inside generic classes "
|
|
1589
|
+
f"('{node.name}' has type parameters)", item)
|
|
1590
|
+
# Register immediately with dotted name for forward references
|
|
1591
|
+
dotted_name = f"{node.name}.{nested.name}"
|
|
1592
|
+
self.registry.register_record(RecordInfo(
|
|
1593
|
+
name=dotted_name,
|
|
1594
|
+
fields=nested.fields,
|
|
1595
|
+
has_init=nested.init_method is not None,
|
|
1596
|
+
module=self._public_module(),
|
|
1597
|
+
))
|
|
1598
|
+
self._nested_type_scope[nested.name] = dotted_name
|
|
1599
|
+
nested_records.append(nested)
|
|
1600
|
+
else:
|
|
1601
|
+
raise ParseError(f"Unsupported construct in class '{node.name}'", item)
|
|
1602
|
+
|
|
1603
|
+
# Auto-declare fields from self.f = param assignments in __init__.
|
|
1604
|
+
# Phase 1: only for non-native classes without bases (inheritance
|
|
1605
|
+
# needs parent field info to avoid shadowing, not available at parse time).
|
|
1606
|
+
if not bases and linkage == RecordLinkage.DEFAULT:
|
|
1607
|
+
init_method = None
|
|
1608
|
+
for m in methods:
|
|
1609
|
+
if m.name == "__init__":
|
|
1610
|
+
init_method = m
|
|
1611
|
+
break
|
|
1612
|
+
if init_method is not None:
|
|
1613
|
+
new_fields = self._auto_declare_fields_from_init(init_method, fields, property_names)
|
|
1614
|
+
fields.extend(new_fields)
|
|
1615
|
+
# Reorder fields to match __init__ assignment order so that
|
|
1616
|
+
# C++ struct layout matches the init list (avoids -Wreorder).
|
|
1617
|
+
fields = self._reorder_fields_by_init(init_method, fields)
|
|
1618
|
+
|
|
1619
|
+
# Method-linkage validation (stubs allowed/required per class
|
|
1620
|
+
# linkage, @native decorator restrictions) runs at sema time in
|
|
1621
|
+
# `_validate_record_method_linkage` after base resolution, so
|
|
1622
|
+
# base-resolution errors (e.g. "'Protocol' requires: from typing
|
|
1623
|
+
# import Protocol" on a class whose Protocol base is shadowed)
|
|
1624
|
+
# fire first instead of being masked here.
|
|
1625
|
+
|
|
1626
|
+
# Restore scopes
|
|
1627
|
+
self._type_param_scope = old_scope
|
|
1628
|
+
self._nested_type_scope = old_nested_scope
|
|
1629
|
+
return TpyRecord(name=node.name, fields=fields, methods=methods, type_params=type_params, type_param_kinds=type_param_kinds, type_param_bounds=type_param_bounds, bases=bases, linkage=linkage, native_name=native_name, is_nocopy=is_nocopy, builtin_type_key=builtin_type_key, is_indirecting=is_indirecting, pending_macros=pending_macros, nested_records=nested_records, nested_enums=nested_enums, is_typed_dict=is_typed_dict, is_total_false=is_total_false, loc=self._loc(node))
|
|
1630
|
+
|
|
1631
|
+
def _auto_declare_fields_from_init(
|
|
1632
|
+
self,
|
|
1633
|
+
init_method: TpyFunction,
|
|
1634
|
+
existing_fields: list[FieldInfo],
|
|
1635
|
+
property_names: set[str] | None = None,
|
|
1636
|
+
) -> list[FieldInfo]:
|
|
1637
|
+
"""Auto-declare fields from top-level `self.f = param` in __init__.
|
|
1638
|
+
|
|
1639
|
+
For CPython compatibility: fields can be created by assignment in
|
|
1640
|
+
__init__ without requiring class-level annotations. Only handles
|
|
1641
|
+
the case where the RHS is a parameter name (type taken from param).
|
|
1642
|
+
"""
|
|
1643
|
+
param_types = {name: typ for name, typ in init_method.params}
|
|
1644
|
+
existing_names = {fld.name for fld in existing_fields}
|
|
1645
|
+
|
|
1646
|
+
new_fields: list[FieldInfo] = []
|
|
1647
|
+
for stmt in init_method.body:
|
|
1648
|
+
if not isinstance(stmt, TpyAssign):
|
|
1649
|
+
continue
|
|
1650
|
+
target = stmt.target
|
|
1651
|
+
if not isinstance(target, TpyFieldAccess):
|
|
1652
|
+
continue
|
|
1653
|
+
if not isinstance(target.obj, TpyName) or target.obj.name != "self":
|
|
1654
|
+
continue
|
|
1655
|
+
field_name = target.field
|
|
1656
|
+
if field_name in existing_names:
|
|
1657
|
+
continue
|
|
1658
|
+
if property_names and field_name in property_names:
|
|
1659
|
+
continue
|
|
1660
|
+
value = stmt.value
|
|
1661
|
+
if not isinstance(value, TpyName):
|
|
1662
|
+
continue
|
|
1663
|
+
if value.name not in param_types:
|
|
1664
|
+
continue
|
|
1665
|
+
new_fields.append(FieldInfo(field_name, param_types[value.name], loc=stmt.loc))
|
|
1666
|
+
existing_names.add(field_name)
|
|
1667
|
+
|
|
1668
|
+
return new_fields
|
|
1669
|
+
|
|
1670
|
+
@staticmethod
|
|
1671
|
+
def _reorder_fields_by_init(
|
|
1672
|
+
init_method: TpyFunction,
|
|
1673
|
+
fields: list[FieldInfo],
|
|
1674
|
+
) -> list[FieldInfo]:
|
|
1675
|
+
"""Reorder fields to match __init__ body assignment order.
|
|
1676
|
+
|
|
1677
|
+
C++ initializes members in struct declaration order regardless of
|
|
1678
|
+
init-list order. Matching the two avoids -Wreorder-ctor warnings.
|
|
1679
|
+
Fields not assigned in __init__ are appended at the end.
|
|
1680
|
+
"""
|
|
1681
|
+
# Collect field assignment order from __init__ top-level statements
|
|
1682
|
+
init_order: list[str] = []
|
|
1683
|
+
for stmt in init_method.body:
|
|
1684
|
+
if not isinstance(stmt, TpyAssign):
|
|
1685
|
+
continue
|
|
1686
|
+
target = stmt.target
|
|
1687
|
+
if not isinstance(target, TpyFieldAccess):
|
|
1688
|
+
continue
|
|
1689
|
+
if not isinstance(target.obj, TpyName) or target.obj.name != "self":
|
|
1690
|
+
continue
|
|
1691
|
+
if target.field not in init_order:
|
|
1692
|
+
init_order.append(target.field)
|
|
1693
|
+
|
|
1694
|
+
field_map = {f.name: f for f in fields}
|
|
1695
|
+
seen: set[str] = set()
|
|
1696
|
+
ordered: list[FieldInfo] = []
|
|
1697
|
+
for name in init_order:
|
|
1698
|
+
if name in field_map and name not in seen:
|
|
1699
|
+
ordered.append(field_map[name])
|
|
1700
|
+
seen.add(name)
|
|
1701
|
+
for f in fields:
|
|
1702
|
+
if f.name not in seen:
|
|
1703
|
+
ordered.append(f)
|
|
1704
|
+
return ordered
|
|
1705
|
+
|
|
1706
|
+
@staticmethod
|
|
1707
|
+
def _prefix_nested_names(record: TpyRecord, parent_name: str) -> None:
|
|
1708
|
+
"""Prefix immediate nested types with parent_name, then recurse."""
|
|
1709
|
+
for nr in record.nested_records:
|
|
1710
|
+
nr.name = f"{parent_name}.{nr.name}"
|
|
1711
|
+
Parser._prefix_nested_names(nr, nr.name)
|
|
1712
|
+
for ne in record.nested_enums:
|
|
1713
|
+
ne.name = f"{parent_name}.{ne.name}"
|
|
1714
|
+
|
|
1715
|
+
def _register_nested_types(self, record: TpyRecord) -> None:
|
|
1716
|
+
"""Register all nested records and enums in the parser registry."""
|
|
1717
|
+
for nr in record.nested_records:
|
|
1718
|
+
self.registry.register_record(RecordInfo(
|
|
1719
|
+
name=nr.name,
|
|
1720
|
+
fields=nr.fields,
|
|
1721
|
+
has_init=nr.init_method is not None,
|
|
1722
|
+
module=self._public_module(),
|
|
1723
|
+
))
|
|
1724
|
+
self._register_nested_types(nr)
|
|
1725
|
+
for ne in record.nested_enums:
|
|
1726
|
+
self.registry.register_enum_placeholder(
|
|
1727
|
+
ne.name, module=self._public_module())
|
|
1728
|
+
|
|
1729
|
+
def _parse_protocol(self, node: ast.ClassDef) -> TpyProtocol:
|
|
1730
|
+
"""Parse a protocol definition."""
|
|
1731
|
+
is_dynamic = False
|
|
1732
|
+
cpp_concept: str | None = None
|
|
1733
|
+
for dec in node.decorator_list:
|
|
1734
|
+
qname, arg = self._require_decorator(dec, f"protocol '{node.name}'")
|
|
1735
|
+
pos, kw = self._validate_decorator_args(qname, arg, dec)
|
|
1736
|
+
if qname == qnames.DYNAMIC:
|
|
1737
|
+
is_dynamic = True
|
|
1738
|
+
continue
|
|
1739
|
+
if qname == qnames.NATIVE:
|
|
1740
|
+
if not isinstance(pos, str):
|
|
1741
|
+
raise ParseError("@native on protocol requires a C++ concept name string argument", dec)
|
|
1742
|
+
# Store the raw form; sema's `register_protocol` applies
|
|
1743
|
+
# `ensure_qualified()` when copying into ProtocolInfo.
|
|
1744
|
+
cpp_concept = pos
|
|
1745
|
+
continue
|
|
1746
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
1747
|
+
raise ParseError(
|
|
1748
|
+
f"Unsupported decorator '@{dec_name}' on protocol '{node.name}'. "
|
|
1749
|
+
f"Only @dynamic (from tpy) and @native (from tpy.extern) are allowed on protocols", dec)
|
|
1750
|
+
# @dynamic + @native together is meaningful for runtime-defined
|
|
1751
|
+
# abstract bases: the C++ class (named by @native) provides the
|
|
1752
|
+
# vtable, and TPy treats the protocol as @dynamic for the
|
|
1753
|
+
# polymorphism predicate (is_polymorphic_class_type) and
|
|
1754
|
+
# inheritance-based conformance. Codegen suppresses concept /
|
|
1755
|
+
# abstract-base / Adapter emission in this case and uses the
|
|
1756
|
+
# @native name wherever the protocol's C++ type is needed.
|
|
1757
|
+
# Used by `Throwable` in lib/tpy/tpy/_core/_types.py to bridge
|
|
1758
|
+
# to runtime/cpp/include/tpy/throwable.hpp::tpy::Throwable.
|
|
1759
|
+
|
|
1760
|
+
# Extract type parameters from Python 3.12+ syntax: class Foo[T](Protocol):
|
|
1761
|
+
# Note: Protocols don't support INT type params (only TYPE).
|
|
1762
|
+
# Set scope before parsing bases so generic parents like `Iterable[T]`
|
|
1763
|
+
# can reference the protocol's own type params.
|
|
1764
|
+
type_params = []
|
|
1765
|
+
if hasattr(node, 'type_params') and node.type_params:
|
|
1766
|
+
for tp in node.type_params:
|
|
1767
|
+
if isinstance(tp, ast.TypeVar):
|
|
1768
|
+
type_params.append(tp.name)
|
|
1769
|
+
else:
|
|
1770
|
+
raise ParseError(f"Only simple type parameters supported in protocols, got {type(tp).__name__}", node)
|
|
1771
|
+
|
|
1772
|
+
# Set type param scope for parsing parent protocols and method signatures
|
|
1773
|
+
# (all TYPE kind for protocols).
|
|
1774
|
+
old_scope = self._type_param_scope
|
|
1775
|
+
self._type_param_scope = {tp: TypeParamKind.TYPE for tp in type_params} if type_params else None
|
|
1776
|
+
|
|
1777
|
+
# Extract parent protocols (excluding Protocol itself). Both bare
|
|
1778
|
+
# (`Sized`) and generic (`Iterable[T]`) parents are accepted. Sema
|
|
1779
|
+
# resolves the TypeRefNode entries to NominalType under the protocol's
|
|
1780
|
+
# type-param scope (see resolve_refs.py).
|
|
1781
|
+
parent_protocols: 'list[NominalType | TypeRefNode]' = []
|
|
1782
|
+
for base in node.bases:
|
|
1783
|
+
if self._is_protocol_base(base):
|
|
1784
|
+
continue
|
|
1785
|
+
ref = self._parse_type_ref(base, self._type_param_scope)
|
|
1786
|
+
if not isinstance(ref, TpyTypeRef):
|
|
1787
|
+
raise ParseError(
|
|
1788
|
+
f"Protocol parents must be simple type references "
|
|
1789
|
+
f"(`Name` or `Name[...]`), got {type(ref).__name__}",
|
|
1790
|
+
base
|
|
1791
|
+
)
|
|
1792
|
+
parent_protocols.append(ref)
|
|
1793
|
+
|
|
1794
|
+
methods = []
|
|
1795
|
+
fields = []
|
|
1796
|
+
|
|
1797
|
+
for item in node.body:
|
|
1798
|
+
if isinstance(item, ast.AsyncFunctionDef):
|
|
1799
|
+
raise ParseError(
|
|
1800
|
+
f"async methods are not allowed in protocols "
|
|
1801
|
+
f"(method '{item.name}' on protocol '{node.name}')", item)
|
|
1802
|
+
if isinstance(item, ast.FunctionDef):
|
|
1803
|
+
# Parse @readonly decorator
|
|
1804
|
+
is_readonly = False
|
|
1805
|
+
readonly_opt_out = False
|
|
1806
|
+
for dec in item.decorator_list:
|
|
1807
|
+
qname, arg = self._require_decorator(dec, f"protocol method '{item.name}'")
|
|
1808
|
+
pos, kw = self._validate_decorator_args(qname, arg, dec)
|
|
1809
|
+
if qname == qnames.READONLY:
|
|
1810
|
+
is_readonly, readonly_opt_out = self._parse_readonly_arg(pos, dec)
|
|
1811
|
+
else:
|
|
1812
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
1813
|
+
raise ParseError(f"Unknown decorator '{dec_name}' on protocol method '{item.name}'", dec)
|
|
1814
|
+
|
|
1815
|
+
# Parse method signature (body should be ... or pass)
|
|
1816
|
+
params = []
|
|
1817
|
+
for i, arg in enumerate(item.args.args):
|
|
1818
|
+
if i == 0:
|
|
1819
|
+
if arg.arg != "self":
|
|
1820
|
+
raise ParseError(f"First parameter of protocol method '{item.name}' must be 'self'", item)
|
|
1821
|
+
continue
|
|
1822
|
+
if arg.annotation is None:
|
|
1823
|
+
raise ParseError(f"Protocol method parameter '{arg.arg}' must have type annotation", item)
|
|
1824
|
+
param_type = self._parse_type_ref(arg.annotation)
|
|
1825
|
+
params.append((arg.arg, param_type))
|
|
1826
|
+
|
|
1827
|
+
# return_type=None means no annotation; sema's
|
|
1828
|
+
# `resolve_refs` substitutes VOID.
|
|
1829
|
+
return_type: 'TpyType | TypeRefNode | None' = None
|
|
1830
|
+
if item.returns:
|
|
1831
|
+
return_type = self._parse_type_ref(item.returns)
|
|
1832
|
+
|
|
1833
|
+
# Capture default values for protocol method params so
|
|
1834
|
+
# that callers through a protocol-typed receiver can drop
|
|
1835
|
+
# trailing defaults (e.g. `fp.seek(0)` for `Seekable`).
|
|
1836
|
+
# _parse_param_defaults skips self via skip_self=True.
|
|
1837
|
+
param_defaults = self._parse_param_defaults(
|
|
1838
|
+
item, params, skip_self=True,
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1841
|
+
methods.append(MethodSignature(
|
|
1842
|
+
name=item.name,
|
|
1843
|
+
params=params,
|
|
1844
|
+
return_type=return_type,
|
|
1845
|
+
is_readonly=is_readonly,
|
|
1846
|
+
readonly_opt_out=readonly_opt_out,
|
|
1847
|
+
param_defaults=param_defaults,
|
|
1848
|
+
))
|
|
1849
|
+
elif isinstance(item, ast.AnnAssign):
|
|
1850
|
+
# Field declaration: name: Type
|
|
1851
|
+
if not isinstance(item.target, ast.Name):
|
|
1852
|
+
raise ParseError("Invalid field declaration in protocol", item)
|
|
1853
|
+
field_name = item.target.id
|
|
1854
|
+
# Emit as TypeRefNode; sema resolves under the protocol's
|
|
1855
|
+
# type-param scope.
|
|
1856
|
+
field_type = self._parse_type_ref(item.annotation)
|
|
1857
|
+
fields.append((field_name, field_type))
|
|
1858
|
+
elif isinstance(item, ast.Pass):
|
|
1859
|
+
pass
|
|
1860
|
+
elif isinstance(item, ast.Expr):
|
|
1861
|
+
# Allow docstrings (string literals) and ... (Ellipsis)
|
|
1862
|
+
if isinstance(item.value, ast.Constant):
|
|
1863
|
+
pass # Docstring
|
|
1864
|
+
elif isinstance(item.value, ast.Ellipsis):
|
|
1865
|
+
pass # Ellipsis at class level
|
|
1866
|
+
else:
|
|
1867
|
+
raise ParseError(f"Unexpected expression in protocol '{node.name}'", item)
|
|
1868
|
+
else:
|
|
1869
|
+
raise ParseError(f"Unsupported construct in protocol '{node.name}': {type(item).__name__}", item)
|
|
1870
|
+
|
|
1871
|
+
# Restore the scope
|
|
1872
|
+
self._type_param_scope = old_scope
|
|
1873
|
+
return TpyProtocol(name=node.name, methods=methods, fields=fields, type_params=type_params, parent_protocols=parent_protocols, is_dynamic=is_dynamic, cpp_concept=cpp_concept, loc=self._loc(node))
|
|
1874
|
+
|
|
1875
|
+
def _parse_enum(
|
|
1876
|
+
self, node: ast.ClassDef,
|
|
1877
|
+
is_int_enum: bool = False,
|
|
1878
|
+
underlying_type_name: str | None = None,
|
|
1879
|
+
) -> TpyEnum:
|
|
1880
|
+
"""Parse an enum class definition."""
|
|
1881
|
+
is_native = False
|
|
1882
|
+
native_name: str | None = None
|
|
1883
|
+
for dec in node.decorator_list:
|
|
1884
|
+
qname, arg = self._require_decorator(dec, f"enum '{node.name}'")
|
|
1885
|
+
if qname != qnames.NATIVE:
|
|
1886
|
+
raise ParseError(
|
|
1887
|
+
f"Decorators are not supported on enum '{node.name}' "
|
|
1888
|
+
f"(only @native is allowed)", dec)
|
|
1889
|
+
pos, kw = self._validate_decorator_args(qname, arg, dec)
|
|
1890
|
+
if kw:
|
|
1891
|
+
raise ParseError(
|
|
1892
|
+
f"@native keyword arguments are not allowed on enum "
|
|
1893
|
+
f"'{node.name}'", dec)
|
|
1894
|
+
is_native = True
|
|
1895
|
+
native_name = pos # may be None for bare @native; sema normalizes
|
|
1896
|
+
|
|
1897
|
+
members: list[tuple[str, int, SourceLocation | None]] = []
|
|
1898
|
+
cpp_member_names: dict[str, str] = {}
|
|
1899
|
+
has_auto = False
|
|
1900
|
+
# Tracks which "implicit value" form the user actually wrote so the
|
|
1901
|
+
# mixed-with-explicit diagnostic doesn't claim `auto()` when the user
|
|
1902
|
+
# only ever wrote `native_member()` (or vice versa).
|
|
1903
|
+
auto_form: str | None = None
|
|
1904
|
+
has_explicit = False
|
|
1905
|
+
auto_value = 1 # auto() starts at 1, matching CPython
|
|
1906
|
+
|
|
1907
|
+
for stmt in node.body:
|
|
1908
|
+
# Skip pass and docstrings
|
|
1909
|
+
if isinstance(stmt, ast.Pass):
|
|
1910
|
+
continue
|
|
1911
|
+
if (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)
|
|
1912
|
+
and isinstance(stmt.value.value, str)):
|
|
1913
|
+
continue
|
|
1914
|
+
|
|
1915
|
+
if not isinstance(stmt, ast.Assign):
|
|
1916
|
+
raise ParseError(
|
|
1917
|
+
f"Enum body must contain only member assignments (name = value), "
|
|
1918
|
+
f"got {type(stmt).__name__}",
|
|
1919
|
+
stmt,
|
|
1920
|
+
)
|
|
1921
|
+
if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name):
|
|
1922
|
+
raise ParseError("Enum member must be a simple name = value assignment", stmt)
|
|
1923
|
+
|
|
1924
|
+
member_name = stmt.targets[0].id
|
|
1925
|
+
value_node = stmt.value
|
|
1926
|
+
|
|
1927
|
+
# Check for auto() or native_member("...") call. For @native enums,
|
|
1928
|
+
# auto() is the idiomatic spelling -- the C++ side is the source of
|
|
1929
|
+
# truth for member values, and TPy-side ints are placeholders.
|
|
1930
|
+
# native_member("cpp_name") aliases a TPy-side name to a C++-side
|
|
1931
|
+
# enumerator name (for Python keywords like `None` or
|
|
1932
|
+
# naming-convention mismatches).
|
|
1933
|
+
if isinstance(value_node, ast.Call):
|
|
1934
|
+
resolved = self._resolve_parser_keyword(value_node.func)
|
|
1935
|
+
if resolved == ("enum", "auto"):
|
|
1936
|
+
if has_explicit:
|
|
1937
|
+
raise ParseError(
|
|
1938
|
+
"Mixed auto() and explicit values are not yet supported; "
|
|
1939
|
+
"use all auto() or all explicit values",
|
|
1940
|
+
stmt,
|
|
1941
|
+
)
|
|
1942
|
+
has_auto = True
|
|
1943
|
+
auto_form = "auto()"
|
|
1944
|
+
members.append((member_name, auto_value, self._loc(stmt)))
|
|
1945
|
+
auto_value += 1
|
|
1946
|
+
continue
|
|
1947
|
+
if resolved == ("tpy.extern", "native_member"):
|
|
1948
|
+
if not is_native:
|
|
1949
|
+
raise ParseError(
|
|
1950
|
+
"native_member() is only allowed on @native enum members",
|
|
1951
|
+
stmt,
|
|
1952
|
+
)
|
|
1953
|
+
if (len(value_node.args) != 1 or value_node.keywords
|
|
1954
|
+
or not isinstance(value_node.args[0], ast.Constant)
|
|
1955
|
+
or not isinstance(value_node.args[0].value, str)):
|
|
1956
|
+
raise ParseError(
|
|
1957
|
+
"native_member() takes exactly 1 positional string argument",
|
|
1958
|
+
stmt,
|
|
1959
|
+
)
|
|
1960
|
+
if has_explicit:
|
|
1961
|
+
raise ParseError(
|
|
1962
|
+
"Mixed native_member() and explicit values are not "
|
|
1963
|
+
"supported; use all native_member()/auto() or all "
|
|
1964
|
+
"explicit values",
|
|
1965
|
+
stmt,
|
|
1966
|
+
)
|
|
1967
|
+
has_auto = True
|
|
1968
|
+
if auto_form is None:
|
|
1969
|
+
auto_form = "native_member()"
|
|
1970
|
+
cpp_member_names[member_name] = value_node.args[0].value
|
|
1971
|
+
members.append((member_name, auto_value, self._loc(stmt)))
|
|
1972
|
+
auto_value += 1
|
|
1973
|
+
continue
|
|
1974
|
+
raise ParseError(
|
|
1975
|
+
"Enum member value must be an integer literal, auto(), "
|
|
1976
|
+
"or native_member(\"...\") (@native only)", stmt)
|
|
1977
|
+
|
|
1978
|
+
# Integer literal (positive)
|
|
1979
|
+
if isinstance(value_node, ast.Constant) and isinstance(value_node.value, int) and not isinstance(value_node.value, bool):
|
|
1980
|
+
if has_auto:
|
|
1981
|
+
raise ParseError(
|
|
1982
|
+
f"Mixed {auto_form} and explicit values are not yet supported; "
|
|
1983
|
+
f"use all {auto_form} or all explicit values",
|
|
1984
|
+
stmt,
|
|
1985
|
+
)
|
|
1986
|
+
has_explicit = True
|
|
1987
|
+
members.append((member_name, value_node.value, self._loc(stmt)))
|
|
1988
|
+
# Negative integer: -N
|
|
1989
|
+
elif (isinstance(value_node, ast.UnaryOp) and isinstance(value_node.op, ast.USub)
|
|
1990
|
+
and isinstance(value_node.operand, ast.Constant)
|
|
1991
|
+
and isinstance(value_node.operand.value, int)):
|
|
1992
|
+
if has_auto:
|
|
1993
|
+
raise ParseError(
|
|
1994
|
+
f"Mixed {auto_form} and explicit values are not yet supported; "
|
|
1995
|
+
f"use all {auto_form} or all explicit values",
|
|
1996
|
+
stmt,
|
|
1997
|
+
)
|
|
1998
|
+
has_explicit = True
|
|
1999
|
+
members.append((member_name, -value_node.operand.value, self._loc(stmt)))
|
|
2000
|
+
else:
|
|
2001
|
+
raise ParseError(
|
|
2002
|
+
"Enum member value must be an integer literal or auto()", stmt)
|
|
2003
|
+
|
|
2004
|
+
if not members:
|
|
2005
|
+
raise ParseError(f"Enum '{node.name}' must have at least one member", node)
|
|
2006
|
+
|
|
2007
|
+
return TpyEnum(
|
|
2008
|
+
name=node.name, members=members,
|
|
2009
|
+
is_int_enum=is_int_enum, underlying_type_name=underlying_type_name,
|
|
2010
|
+
is_native=is_native, native_name=native_name,
|
|
2011
|
+
cpp_member_names=cpp_member_names,
|
|
2012
|
+
has_explicit_values=has_explicit,
|
|
2013
|
+
loc=self._loc(node),
|
|
2014
|
+
)
|
|
2015
|
+
|
|
2016
|
+
_RECORD_LINKAGE_MAP: dict[str, RecordLinkage] = {
|
|
2017
|
+
qnames.NATIVE: RecordLinkage.NATIVE,
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
_METHOD_LINKAGE_MAP: dict[str, FunctionLinkage] = {
|
|
2021
|
+
qnames.NATIVE: FunctionLinkage.NATIVE,
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
def _parse_method(self, node: ast.FunctionDef, class_name: str, type_param_scope: dict[str, TypeParamKind] | None = None, property_names: set[str] | None = None) -> TpyFunction:
|
|
2025
|
+
"""Parse a method definition."""
|
|
2026
|
+
# Check decorators (@staticmethod, @readonly, @native("cpp_name"), @override)
|
|
2027
|
+
is_staticmethod = False
|
|
2028
|
+
is_readonly = False
|
|
2029
|
+
readonly_opt_out = False
|
|
2030
|
+
is_pure = False
|
|
2031
|
+
is_inline = False
|
|
2032
|
+
is_override = False
|
|
2033
|
+
is_overload_stub = False
|
|
2034
|
+
auto_readonly = False
|
|
2035
|
+
auto_readonly_dec = None
|
|
2036
|
+
error_return: str | None = None
|
|
2037
|
+
method_linkage = FunctionLinkage.DEFAULT
|
|
2038
|
+
native_name: str | None = None
|
|
2039
|
+
native_function: bool = False
|
|
2040
|
+
native_preserves_refs: bool = False
|
|
2041
|
+
cpp_template: str | None = None
|
|
2042
|
+
is_property_getter = False
|
|
2043
|
+
is_property_setter = False
|
|
2044
|
+
property_setter_name: str | None = None
|
|
2045
|
+
native_cpp_return_type: str | None = None
|
|
2046
|
+
for dec in node.decorator_list:
|
|
2047
|
+
# Detect @prop_name.setter / @prop_name.deleter before general resolution
|
|
2048
|
+
func_node_check = dec.func if isinstance(dec, ast.Call) else dec
|
|
2049
|
+
if (isinstance(func_node_check, ast.Attribute)
|
|
2050
|
+
and isinstance(func_node_check.value, ast.Name)
|
|
2051
|
+
and property_names
|
|
2052
|
+
and func_node_check.value.id in property_names):
|
|
2053
|
+
if func_node_check.attr == "setter":
|
|
2054
|
+
is_property_setter = True
|
|
2055
|
+
property_setter_name = func_node_check.value.id
|
|
2056
|
+
continue
|
|
2057
|
+
if func_node_check.attr == "deleter":
|
|
2058
|
+
raise ParseError(f"@property deleter is not supported", dec)
|
|
2059
|
+
qname, arg = self._require_decorator(dec, f"method '{node.name}'")
|
|
2060
|
+
pos, kw = self._validate_decorator_args(qname, arg, dec)
|
|
2061
|
+
if qname == qnames.STATICMETHOD:
|
|
2062
|
+
is_staticmethod = True
|
|
2063
|
+
elif qname == qnames.PROPERTY:
|
|
2064
|
+
is_property_getter = True
|
|
2065
|
+
elif qname == qnames.PURE:
|
|
2066
|
+
is_pure = True
|
|
2067
|
+
elif qname == qnames.INLINE:
|
|
2068
|
+
is_inline = True
|
|
2069
|
+
elif qname == qnames.OVERRIDE:
|
|
2070
|
+
is_override = True
|
|
2071
|
+
elif qname == qnames.OVERLOAD:
|
|
2072
|
+
is_overload_stub = True
|
|
2073
|
+
elif qname == qnames.READONLY:
|
|
2074
|
+
is_readonly, readonly_opt_out = self._parse_readonly_arg(pos, dec)
|
|
2075
|
+
elif qname == qnames.AUTO_READONLY:
|
|
2076
|
+
auto_readonly = True
|
|
2077
|
+
auto_readonly_dec = dec
|
|
2078
|
+
elif qname == qnames.ERROR_RETURN:
|
|
2079
|
+
error_return = pos.name
|
|
2080
|
+
elif qname == qnames.CPP_TEMPLATE:
|
|
2081
|
+
cpp_template = pos
|
|
2082
|
+
elif qname == qnames.NATIVE_PRESERVES_REFS:
|
|
2083
|
+
native_preserves_refs = True
|
|
2084
|
+
elif qname in self._METHOD_LINKAGE_MAP:
|
|
2085
|
+
method_linkage = self._METHOD_LINKAGE_MAP[qname]
|
|
2086
|
+
if isinstance(pos, tuple):
|
|
2087
|
+
raise ParseError(
|
|
2088
|
+
f"@{bare_name(qname)}() decorator kwargs not parsed "
|
|
2089
|
+
f"(schema unavailable -- ensure _bootstrap._extern is imported "
|
|
2090
|
+
f"before modules that use decorator kwargs)", dec)
|
|
2091
|
+
# binding="C" overrides method linkage
|
|
2092
|
+
binding = kw.get("binding", "")
|
|
2093
|
+
if binding == "C" and method_linkage == FunctionLinkage.NATIVE:
|
|
2094
|
+
method_linkage = FunctionLinkage.NATIVE_C
|
|
2095
|
+
elif binding and binding != "" and binding != "C":
|
|
2096
|
+
raise ParseError(
|
|
2097
|
+
f"@{bare_name(qname)}(binding=...) only supports binding=\"C\"", dec)
|
|
2098
|
+
native_name = pos
|
|
2099
|
+
native_function = kw.get("function", False)
|
|
2100
|
+
cpp_rt = kw.get("cpp_return_type")
|
|
2101
|
+
if isinstance(cpp_rt, _NameArg):
|
|
2102
|
+
native_cpp_return_type = cpp_rt.name
|
|
2103
|
+
elif cpp_rt is not None:
|
|
2104
|
+
raise ParseError(
|
|
2105
|
+
f"@{bare_name(qname)}(cpp_return_type=...) requires a type name", dec)
|
|
2106
|
+
else:
|
|
2107
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
2108
|
+
raise ParseError(f"Unknown decorator '{dec_name}' on method '{node.name}'", dec)
|
|
2109
|
+
if is_override and is_staticmethod:
|
|
2110
|
+
raise ParseError(f"@override cannot be combined with @staticmethod on method '{node.name}'", node)
|
|
2111
|
+
if is_property_getter:
|
|
2112
|
+
if is_staticmethod:
|
|
2113
|
+
raise ParseError(f"@property cannot be combined with @staticmethod on method '{node.name}'", node)
|
|
2114
|
+
# Getter: only self param, must have return type
|
|
2115
|
+
params_without_self = [a for a in node.args.args if a.arg != "self"]
|
|
2116
|
+
if params_without_self:
|
|
2117
|
+
raise ParseError(f"@property getter '{node.name}' must take only 'self' parameter", node)
|
|
2118
|
+
if node.returns is None:
|
|
2119
|
+
raise ParseError(f"@property getter '{node.name}' must have a return type annotation", node)
|
|
2120
|
+
if is_property_setter:
|
|
2121
|
+
if is_staticmethod:
|
|
2122
|
+
raise ParseError(f"@property setter cannot be combined with @staticmethod on method '{node.name}'", node)
|
|
2123
|
+
# Setter: self + one value param
|
|
2124
|
+
params_without_self = [a for a in node.args.args if a.arg != "self"]
|
|
2125
|
+
if len(params_without_self) != 1:
|
|
2126
|
+
raise ParseError(f"@property setter '{node.name}' must take exactly one value parameter (plus self)", node)
|
|
2127
|
+
if auto_readonly:
|
|
2128
|
+
if is_readonly:
|
|
2129
|
+
raise ParseError(f"@auto_readonly cannot be combined with @readonly on method '{node.name}'", auto_readonly_dec)
|
|
2130
|
+
if is_staticmethod:
|
|
2131
|
+
raise ParseError(f"@auto_readonly cannot be combined with @staticmethod on method '{node.name}'", auto_readonly_dec)
|
|
2132
|
+
if node.name in ("__init__", "__del__"):
|
|
2133
|
+
raise ParseError(f"@auto_readonly is not valid on '{node.name}'", auto_readonly_dec)
|
|
2134
|
+
|
|
2135
|
+
# Extract method-level type parameters (e.g. def foo[T](self, x: T) -> T:)
|
|
2136
|
+
method_type_params: list[str] = []
|
|
2137
|
+
method_type_param_bounds: dict[str, 'TpyType | TypeRefNode'] = {}
|
|
2138
|
+
if hasattr(node, 'type_params') and node.type_params:
|
|
2139
|
+
for tp in node.type_params:
|
|
2140
|
+
if isinstance(tp, ast.TypeVar):
|
|
2141
|
+
method_type_params.append(tp.name)
|
|
2142
|
+
if tp.bound is not None:
|
|
2143
|
+
# Protocol bound -- resolution + validation deferred to sema.
|
|
2144
|
+
method_type_param_bounds[tp.name] = self._parse_type_ref(tp.bound)
|
|
2145
|
+
else:
|
|
2146
|
+
raise ParseError(f"Only simple type parameters supported, got {type(tp).__name__}", node)
|
|
2147
|
+
|
|
2148
|
+
# Early @auto_readonly + method type params guard. Sema catches
|
|
2149
|
+
# the self-annotation / per-param paths, but running this at parse
|
|
2150
|
+
# time keeps the diagnostic pinned to the decorator's line rather
|
|
2151
|
+
# than getting shadowed by later parse-time checks (stub bodies,
|
|
2152
|
+
# body validation, etc.).
|
|
2153
|
+
if auto_readonly_dec is not None and method_type_params:
|
|
2154
|
+
raise ParseError(
|
|
2155
|
+
f"auto_readonly on methods with method-level type parameters is not yet supported ('{node.name}')",
|
|
2156
|
+
auto_readonly_dec,
|
|
2157
|
+
)
|
|
2158
|
+
|
|
2159
|
+
# Merge class-level and method-level type param scopes
|
|
2160
|
+
if method_type_params:
|
|
2161
|
+
merged_scope = dict(type_param_scope) if type_param_scope else {}
|
|
2162
|
+
for tp_name in method_type_params:
|
|
2163
|
+
merged_scope[tp_name] = TypeParamKind.TYPE
|
|
2164
|
+
type_param_scope = merged_scope
|
|
2165
|
+
|
|
2166
|
+
params = []
|
|
2167
|
+
has_self = not is_staticmethod
|
|
2168
|
+
self_annotation = None # Resolved self type; consumed by sema.method_expansion.
|
|
2169
|
+
n_non_self = len(node.args.args) - (1 if has_self else 0)
|
|
2170
|
+
is_exit_method = node.name == "__exit__" and has_self and n_non_self == 3
|
|
2171
|
+
args_iter = iter(enumerate(node.args.args))
|
|
2172
|
+
for i, arg in args_iter:
|
|
2173
|
+
if i == 0 and has_self:
|
|
2174
|
+
# Non-static methods must have 'self' as first parameter
|
|
2175
|
+
if arg.arg != "self":
|
|
2176
|
+
raise ParseError(f"First parameter of method '{node.name}' must be 'self'", node)
|
|
2177
|
+
# Emit the self annotation as a TypeRefNode; sema
|
|
2178
|
+
# resolves it in `resolve_refs` and
|
|
2179
|
+
# `method_expansion.expand_methods` validates the
|
|
2180
|
+
# shape + derives flags.
|
|
2181
|
+
if arg.annotation is not None:
|
|
2182
|
+
self_annotation = self._parse_type_ref(arg.annotation, type_param_scope)
|
|
2183
|
+
continue
|
|
2184
|
+
if arg.annotation is None:
|
|
2185
|
+
if is_exit_method:
|
|
2186
|
+
# v1.5: __exit__(self, exc_type, exc_val, exc_tb). Positional
|
|
2187
|
+
# convention; unannotated params get synthesized types.
|
|
2188
|
+
# exc_type / exc_tb are always None in v1.5 (no traceback or
|
|
2189
|
+
# type-object machinery); exc_val carries the exception on
|
|
2190
|
+
# the exceptional path, None on normal exit.
|
|
2191
|
+
exit_idx = i - 1 if has_self else i
|
|
2192
|
+
loc = self._loc(arg)
|
|
2193
|
+
if exit_idx == 1:
|
|
2194
|
+
synth = TpyUnionRef(
|
|
2195
|
+
members=(TpyTypeRef("BaseException", (), loc),
|
|
2196
|
+
TpyTypeRef("None", (), loc)),
|
|
2197
|
+
loc=loc,
|
|
2198
|
+
)
|
|
2199
|
+
else:
|
|
2200
|
+
synth = TpyTypeRef("None", (), loc)
|
|
2201
|
+
params.append((arg.arg, synth))
|
|
2202
|
+
continue
|
|
2203
|
+
raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
|
|
2204
|
+
param_type = self._parse_type_ref(arg.annotation, type_param_scope)
|
|
2205
|
+
params.append((arg.arg, param_type))
|
|
2206
|
+
|
|
2207
|
+
# __exit__ must have exactly 3 params (exc_type, exc_val, exc_tb) to
|
|
2208
|
+
# match CPython's context manager protocol.
|
|
2209
|
+
if node.name == "__exit__" and has_self and n_non_self != 3:
|
|
2210
|
+
raise ParseError(
|
|
2211
|
+
f"__exit__ must have 3 parameters: "
|
|
2212
|
+
f"__exit__(self, exc_type, exc_val, exc_tb)",
|
|
2213
|
+
node,
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
# Parse *args parameter
|
|
2217
|
+
vararg_name = None
|
|
2218
|
+
vararg_type = None
|
|
2219
|
+
if node.args.vararg is not None:
|
|
2220
|
+
va = node.args.vararg
|
|
2221
|
+
if is_overload_stub:
|
|
2222
|
+
raise ParseError("*args is not supported on @overload stubs", node)
|
|
2223
|
+
if va.annotation is None:
|
|
2224
|
+
raise ParseError(
|
|
2225
|
+
f"*{va.arg} must have a type annotation (element type)", node)
|
|
2226
|
+
vararg_name = va.arg
|
|
2227
|
+
vararg_type = self._parse_type_ref(va.annotation, type_param_scope)
|
|
2228
|
+
|
|
2229
|
+
# Parse keyword-only parameters (after * or *args)
|
|
2230
|
+
keyword_only_start = None
|
|
2231
|
+
if node.args.kwonlyargs:
|
|
2232
|
+
keyword_only_start = len(params)
|
|
2233
|
+
for arg in node.args.kwonlyargs:
|
|
2234
|
+
if arg.annotation is None:
|
|
2235
|
+
raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
|
|
2236
|
+
param_type = self._parse_type_ref(arg.annotation, type_param_scope)
|
|
2237
|
+
params.append((arg.arg, param_type))
|
|
2238
|
+
|
|
2239
|
+
# **kwargs: Unpack[TypedDict]
|
|
2240
|
+
kwarg_name = None
|
|
2241
|
+
kwarg_type = None
|
|
2242
|
+
if node.args.kwarg is not None:
|
|
2243
|
+
kwarg_node = node.args.kwarg
|
|
2244
|
+
if kwarg_node.annotation is None:
|
|
2245
|
+
raise ParseError("**kwargs must have Unpack[TypedDict] annotation", node)
|
|
2246
|
+
kwarg_type = self._parse_unpack_annotation(kwarg_node.annotation, type_param_scope, node)
|
|
2247
|
+
kwarg_name = kwarg_node.arg
|
|
2248
|
+
|
|
2249
|
+
# Parse default parameter values (skip_self for non-static methods)
|
|
2250
|
+
defaults = self._parse_param_defaults(node, params, skip_self=has_self,
|
|
2251
|
+
type_param_scope=type_param_scope,
|
|
2252
|
+
kw_defaults=node.args.kw_defaults,
|
|
2253
|
+
n_kwonly=len(node.args.kwonlyargs))
|
|
2254
|
+
|
|
2255
|
+
# Return type: None means no annotation; sema substitutes VOID.
|
|
2256
|
+
# __init__ is kept at None here (no annotation by construction);
|
|
2257
|
+
# sema treats it the same way.
|
|
2258
|
+
return_type: 'TpyType | TypeRefNode | None' = None
|
|
2259
|
+
if node.name != "__init__" and node.returns:
|
|
2260
|
+
return_type = self._parse_type_ref(node.returns, type_param_scope)
|
|
2261
|
+
|
|
2262
|
+
if cpp_template is not None:
|
|
2263
|
+
if not self._is_stub_body(node.body):
|
|
2264
|
+
raise ParseError(
|
|
2265
|
+
f"@cpp_template method '{node.name}' must have `...` body", node)
|
|
2266
|
+
is_stub_body = self._is_stub_body(node.body)
|
|
2267
|
+
is_overload_stub_body = is_stub_body or self._is_pass_body(node.body)
|
|
2268
|
+
is_stub = (is_stub_body and not is_overload_stub) or cpp_template is not None
|
|
2269
|
+
if is_overload_stub:
|
|
2270
|
+
# @overload methods may be bodyless (`...` / `pass`, paired with a
|
|
2271
|
+
# trailing impl) or carry their own body (self-contained overload
|
|
2272
|
+
# variant -- sema validates that a group is all-bodied or all-bodyless).
|
|
2273
|
+
body = [] if is_overload_stub_body else self._parse_body(node.body)
|
|
2274
|
+
elif is_stub:
|
|
2275
|
+
body = []
|
|
2276
|
+
else:
|
|
2277
|
+
body = self._parse_body(node.body)
|
|
2278
|
+
|
|
2279
|
+
# Detect generator methods (yield in body)
|
|
2280
|
+
is_generator = _body_contains_yield(body)
|
|
2281
|
+
if is_generator:
|
|
2282
|
+
if node.name in ("__init__", "__del__"):
|
|
2283
|
+
raise ParseError(f"'{node.name}' cannot be a generator method", node)
|
|
2284
|
+
if is_staticmethod:
|
|
2285
|
+
raise ParseError(f"@staticmethod method '{node.name}' cannot be a generator", node)
|
|
2286
|
+
_check_no_return_value_in_generator(body, node.name)
|
|
2287
|
+
|
|
2288
|
+
# __next__ methods implicitly get @error_return(StopIteration)
|
|
2289
|
+
if node.name == "__next__" and error_return is None:
|
|
2290
|
+
error_return = "StopIteration"
|
|
2291
|
+
|
|
2292
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
2293
|
+
if is_async:
|
|
2294
|
+
# Mirror the free async def exclusion rules from _parse_def.
|
|
2295
|
+
if is_generator:
|
|
2296
|
+
raise ParseError(
|
|
2297
|
+
f"async generators (async def + yield) are not yet supported "
|
|
2298
|
+
f"on async method '{node.name}' of '{class_name}'", node)
|
|
2299
|
+
if error_return is not None:
|
|
2300
|
+
raise ParseError(
|
|
2301
|
+
f"async def + @error_return is not yet supported on "
|
|
2302
|
+
f"async method '{node.name}' of '{class_name}'", node)
|
|
2303
|
+
if method_linkage != FunctionLinkage.DEFAULT:
|
|
2304
|
+
display = self._LINKAGE_DISPLAY_NAMES.get(
|
|
2305
|
+
method_linkage, method_linkage.value)
|
|
2306
|
+
raise ParseError(
|
|
2307
|
+
f"async def + @{display} is not yet supported on "
|
|
2308
|
+
f"async method '{node.name}' of '{class_name}'", node)
|
|
2309
|
+
if is_property_getter or is_property_setter:
|
|
2310
|
+
raise ParseError(
|
|
2311
|
+
f"async @property is not yet supported on "
|
|
2312
|
+
f"'{node.name}' of '{class_name}'", node)
|
|
2313
|
+
if is_staticmethod:
|
|
2314
|
+
raise ParseError(
|
|
2315
|
+
f"async @staticmethod is not yet supported on "
|
|
2316
|
+
f"'{node.name}' of '{class_name}'", node)
|
|
2317
|
+
|
|
2318
|
+
method = TpyFunction(
|
|
2319
|
+
name=node.name,
|
|
2320
|
+
params=params,
|
|
2321
|
+
return_type=return_type,
|
|
2322
|
+
body=body,
|
|
2323
|
+
is_method=True,
|
|
2324
|
+
is_async=is_async,
|
|
2325
|
+
is_staticmethod=is_staticmethod,
|
|
2326
|
+
is_property_getter=is_property_getter,
|
|
2327
|
+
is_property_setter=is_property_setter,
|
|
2328
|
+
property_name=property_setter_name,
|
|
2329
|
+
is_inline=is_inline,
|
|
2330
|
+
is_readonly=is_readonly,
|
|
2331
|
+
readonly_opt_out=readonly_opt_out,
|
|
2332
|
+
is_pure=is_pure,
|
|
2333
|
+
has_auto_readonly_decorator=auto_readonly_dec is not None,
|
|
2334
|
+
is_override=is_override,
|
|
2335
|
+
is_overload_stub=is_overload_stub,
|
|
2336
|
+
is_stub=is_overload_stub_body if is_overload_stub else is_stub,
|
|
2337
|
+
linkage=method_linkage,
|
|
2338
|
+
native_name=native_name,
|
|
2339
|
+
native_function=native_function,
|
|
2340
|
+
native_preserves_refs=native_preserves_refs,
|
|
2341
|
+
native_cpp_return_type=native_cpp_return_type,
|
|
2342
|
+
cpp_template=cpp_template,
|
|
2343
|
+
type_params=method_type_params,
|
|
2344
|
+
type_param_bounds=method_type_param_bounds,
|
|
2345
|
+
defaults=defaults,
|
|
2346
|
+
keyword_only_start=keyword_only_start,
|
|
2347
|
+
vararg_name=vararg_name,
|
|
2348
|
+
vararg_type=vararg_type,
|
|
2349
|
+
kwarg_name=kwarg_name,
|
|
2350
|
+
kwarg_type=kwarg_type,
|
|
2351
|
+
error_return=error_return,
|
|
2352
|
+
is_generator=is_generator,
|
|
2353
|
+
self_annotation=self_annotation,
|
|
2354
|
+
loc=self._loc(node)
|
|
2355
|
+
)
|
|
2356
|
+
return method
|
|
2357
|
+
|
|
2358
|
+
_FUNCTION_LINKAGE_MAP: dict[str, FunctionLinkage] = {
|
|
2359
|
+
qnames.NATIVE: FunctionLinkage.NATIVE,
|
|
2360
|
+
qnames.EXPORT: FunctionLinkage.EXPORT_C,
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
# User-visible decorator names for error messages
|
|
2364
|
+
_LINKAGE_DISPLAY_NAMES: dict[FunctionLinkage, str] = {
|
|
2365
|
+
FunctionLinkage.NATIVE: "native",
|
|
2366
|
+
FunctionLinkage.NATIVE_C: 'native(binding="C")',
|
|
2367
|
+
FunctionLinkage.EXPORT_C: "export",
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
def _parse_function(self, node: 'ast.FunctionDef | ast.AsyncFunctionDef') -> TpyFunction:
|
|
2371
|
+
"""Parse a function definition (sync or async).
|
|
2372
|
+
|
|
2373
|
+
async def f() lowers to a state-machine struct conforming to
|
|
2374
|
+
Awaitable[T] in PR 3 (codegen). v1 sema rejects async + @error_return,
|
|
2375
|
+
async + @noalloc, async + yield (async generators), and user
|
|
2376
|
+
__await__ methods -- each as 'not yet supported'."""
|
|
2377
|
+
is_noalloc = False
|
|
2378
|
+
is_inline = False
|
|
2379
|
+
is_readonly = False
|
|
2380
|
+
readonly_opt_out = False
|
|
2381
|
+
is_pure = False
|
|
2382
|
+
is_overload_stub = False
|
|
2383
|
+
value_ptr_coercion = False
|
|
2384
|
+
error_return: str | None = None
|
|
2385
|
+
builtin_decorator_key: str | None = None
|
|
2386
|
+
builtin_function_key: str | None = None
|
|
2387
|
+
type_param_defaults: dict[str, str] = {}
|
|
2388
|
+
linkage = FunctionLinkage.DEFAULT
|
|
2389
|
+
native_name: str | None = None
|
|
2390
|
+
cpp_template: str | None = None
|
|
2391
|
+
native_cpp_return_type: str | None = None
|
|
2392
|
+
for dec in node.decorator_list:
|
|
2393
|
+
qname, arg = self._require_decorator(dec, f"function '{node.name}'")
|
|
2394
|
+
if qname == qnames.TYPE_PARAM_DEFAULT:
|
|
2395
|
+
type_param_defaults = self._parse_type_param_default(dec)
|
|
2396
|
+
continue
|
|
2397
|
+
pos, kw = self._validate_decorator_args(qname, arg, dec)
|
|
2398
|
+
if qname == qnames.NOALLOC:
|
|
2399
|
+
is_noalloc = True
|
|
2400
|
+
elif qname == qnames.INLINE:
|
|
2401
|
+
is_inline = True
|
|
2402
|
+
elif qname == qnames.PURE:
|
|
2403
|
+
is_pure = True
|
|
2404
|
+
elif qname == qnames.READONLY:
|
|
2405
|
+
is_readonly, readonly_opt_out = self._parse_readonly_arg(pos, dec)
|
|
2406
|
+
elif qname == qnames.AUTO_READONLY:
|
|
2407
|
+
raise ParseError("@auto_readonly is only valid on methods, not free functions", dec)
|
|
2408
|
+
elif qname == qnames.OVERLOAD:
|
|
2409
|
+
is_overload_stub = True
|
|
2410
|
+
elif qname == qnames.ERROR_RETURN:
|
|
2411
|
+
error_return = pos.name
|
|
2412
|
+
elif qname == qnames.VALUE_PTR_COERCION:
|
|
2413
|
+
value_ptr_coercion = True
|
|
2414
|
+
elif qname == qnames.CPP_TEMPLATE:
|
|
2415
|
+
cpp_template = pos
|
|
2416
|
+
elif qname == qnames.BUILTIN_DECORATOR:
|
|
2417
|
+
builtin_decorator_key = pos
|
|
2418
|
+
elif qname == qnames.BUILTIN_FUNCTION:
|
|
2419
|
+
builtin_function_key = pos
|
|
2420
|
+
elif qname in self._FUNCTION_LINKAGE_MAP:
|
|
2421
|
+
new_linkage = self._FUNCTION_LINKAGE_MAP[qname]
|
|
2422
|
+
if linkage != FunctionLinkage.DEFAULT:
|
|
2423
|
+
old_name = self._LINKAGE_DISPLAY_NAMES.get(linkage, linkage.value)
|
|
2424
|
+
new_name = self._LINKAGE_DISPLAY_NAMES.get(new_linkage, new_linkage.value)
|
|
2425
|
+
raise ParseError(
|
|
2426
|
+
f"Function '{node.name}' cannot have both @{old_name} and @{new_name}", node)
|
|
2427
|
+
if kw.get("function"):
|
|
2428
|
+
dec_name = self._decorator_local_name(dec)
|
|
2429
|
+
raise ParseError(f"@{dec_name}(function=...) is only valid on methods, not free functions", dec)
|
|
2430
|
+
# binding="C" overrides linkage to C variant
|
|
2431
|
+
binding = kw.get("binding", "")
|
|
2432
|
+
if binding == "C":
|
|
2433
|
+
if new_linkage == FunctionLinkage.NATIVE:
|
|
2434
|
+
new_linkage = FunctionLinkage.NATIVE_C
|
|
2435
|
+
elif binding and binding != "":
|
|
2436
|
+
raise ParseError(
|
|
2437
|
+
f"@{bare_name(qname)}(binding=...) only supports binding=\"C\"", dec)
|
|
2438
|
+
# @export requires binding="C"
|
|
2439
|
+
if qname == qnames.EXPORT and binding != "C":
|
|
2440
|
+
raise ParseError(
|
|
2441
|
+
f"@export requires binding=\"C\"", dec)
|
|
2442
|
+
linkage = new_linkage
|
|
2443
|
+
if isinstance(pos, tuple):
|
|
2444
|
+
raise ParseError(
|
|
2445
|
+
f"@{bare_name(qname)}() decorator kwargs not parsed "
|
|
2446
|
+
f"(schema unavailable -- ensure _bootstrap._extern is imported "
|
|
2447
|
+
f"before modules that use decorator kwargs)", dec)
|
|
2448
|
+
native_name = pos
|
|
2449
|
+
cpp_rt = kw.get("cpp_return_type")
|
|
2450
|
+
if isinstance(cpp_rt, _NameArg):
|
|
2451
|
+
native_cpp_return_type = cpp_rt.name
|
|
2452
|
+
elif cpp_rt is not None:
|
|
2453
|
+
raise ParseError(
|
|
2454
|
+
f"@{bare_name(qname)}(cpp_return_type=...) requires a type name", dec)
|
|
2455
|
+
else:
|
|
2456
|
+
dec_name = self._decorator_local_name(dec) or "?"
|
|
2457
|
+
raise ParseError(f"Unknown decorator '{dec_name}' on function '{node.name}'", dec)
|
|
2458
|
+
|
|
2459
|
+
# Extract type parameters from Python 3.12+ syntax: def foo[T, U]():
|
|
2460
|
+
# Bounds: def foo[T: Comparable](): (protocol bound)
|
|
2461
|
+
# INT params: def foo[T, N: int](): (integer type parameter, e.g. for Array[T, N])
|
|
2462
|
+
type_params = []
|
|
2463
|
+
type_param_bounds: dict[str, 'TpyType | TypeRefNode'] = {}
|
|
2464
|
+
type_param_kinds: list[TypeParamKind] = []
|
|
2465
|
+
if hasattr(node, 'type_params') and node.type_params:
|
|
2466
|
+
for tp in node.type_params:
|
|
2467
|
+
if isinstance(tp, ast.TypeVar):
|
|
2468
|
+
type_params.append(tp.name)
|
|
2469
|
+
if tp.bound is not None:
|
|
2470
|
+
if isinstance(tp.bound, ast.Name) and tp.bound.id == 'int':
|
|
2471
|
+
type_param_kinds.append(TypeParamKind.INT)
|
|
2472
|
+
else:
|
|
2473
|
+
type_param_kinds.append(TypeParamKind.TYPE)
|
|
2474
|
+
# Protocol bound -- resolution + validation deferred to sema.
|
|
2475
|
+
type_param_bounds[tp.name] = self._parse_type_ref(tp.bound)
|
|
2476
|
+
else:
|
|
2477
|
+
type_param_kinds.append(TypeParamKind.TYPE)
|
|
2478
|
+
else:
|
|
2479
|
+
raise ParseError(f"Only simple type parameters supported, got {type(tp).__name__}", node)
|
|
2480
|
+
|
|
2481
|
+
# Set scope for parsing parameter and return types
|
|
2482
|
+
type_param_scope = (
|
|
2483
|
+
{tp: kind for tp, kind in zip(type_params, type_param_kinds)}
|
|
2484
|
+
if type_params else None
|
|
2485
|
+
)
|
|
2486
|
+
old_scope = self._type_param_scope
|
|
2487
|
+
self._type_param_scope = type_param_scope
|
|
2488
|
+
|
|
2489
|
+
params = []
|
|
2490
|
+
if builtin_function_key is not None:
|
|
2491
|
+
# @builtin_function stubs: params are illustrative only,
|
|
2492
|
+
# type annotations not required (sema handles everything)
|
|
2493
|
+
for arg in node.args.args:
|
|
2494
|
+
# @builtin_function params are illustrative stubs; sema
|
|
2495
|
+
# handles the real types specially. Missing annotations
|
|
2496
|
+
# emit a `TpyTypeRef("None")` placeholder that the
|
|
2497
|
+
# resolver maps to VOID.
|
|
2498
|
+
param_type: 'TpyType | TypeRefNode' = (
|
|
2499
|
+
self._parse_type_ref(arg.annotation, type_param_scope)
|
|
2500
|
+
if arg.annotation
|
|
2501
|
+
else TpyTypeRef(name="None", loc=self._loc(arg))
|
|
2502
|
+
)
|
|
2503
|
+
params.append((arg.arg, param_type))
|
|
2504
|
+
else:
|
|
2505
|
+
for arg in node.args.args:
|
|
2506
|
+
if arg.annotation is None:
|
|
2507
|
+
raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
|
|
2508
|
+
param_type = self._parse_type_ref(arg.annotation, type_param_scope)
|
|
2509
|
+
params.append((arg.arg, param_type))
|
|
2510
|
+
|
|
2511
|
+
# Parse *args parameter
|
|
2512
|
+
vararg_name = None
|
|
2513
|
+
vararg_type = None
|
|
2514
|
+
if node.args.vararg is not None:
|
|
2515
|
+
va = node.args.vararg
|
|
2516
|
+
if builtin_function_key is None:
|
|
2517
|
+
if is_overload_stub:
|
|
2518
|
+
raise ParseError("*args is not supported on @overload stubs", node)
|
|
2519
|
+
if va.annotation is None:
|
|
2520
|
+
raise ParseError(
|
|
2521
|
+
f"*{va.arg} must have a type annotation (element type)", node)
|
|
2522
|
+
vararg_name = va.arg
|
|
2523
|
+
vararg_type = self._parse_type_ref(va.annotation, type_param_scope)
|
|
2524
|
+
|
|
2525
|
+
# Parse keyword-only parameters (after * or *args)
|
|
2526
|
+
keyword_only_start = None
|
|
2527
|
+
if node.args.kwonlyargs:
|
|
2528
|
+
keyword_only_start = len(params)
|
|
2529
|
+
for arg in node.args.kwonlyargs:
|
|
2530
|
+
if arg.annotation is None:
|
|
2531
|
+
raise ParseError(f"Parameter '{arg.arg}' must have type annotation", node)
|
|
2532
|
+
param_type = self._parse_type_ref(arg.annotation, type_param_scope)
|
|
2533
|
+
params.append((arg.arg, param_type))
|
|
2534
|
+
elif vararg_name is None and node.args.vararg is not None:
|
|
2535
|
+
# bare * separator with no kwonlyargs -- unusual but valid Python
|
|
2536
|
+
pass
|
|
2537
|
+
# If there's a bare * (no vararg name but kwonlyargs exist), keyword_only_start is set above.
|
|
2538
|
+
# If there's *args, kwonlyargs after it are also keyword-only (set above).
|
|
2539
|
+
|
|
2540
|
+
# **kwargs: Unpack[TypedDict]
|
|
2541
|
+
kwarg_name = None
|
|
2542
|
+
kwarg_type = None
|
|
2543
|
+
if node.args.kwarg is not None and builtin_function_key is None:
|
|
2544
|
+
kwarg_node = node.args.kwarg
|
|
2545
|
+
if kwarg_node.annotation is None:
|
|
2546
|
+
raise ParseError("**kwargs must have Unpack[TypedDict] annotation", node)
|
|
2547
|
+
kwarg_type = self._parse_unpack_annotation(kwarg_node.annotation, type_param_scope, node)
|
|
2548
|
+
kwarg_name = kwarg_node.arg
|
|
2549
|
+
|
|
2550
|
+
# Parse default parameter values
|
|
2551
|
+
defaults = self._parse_param_defaults(node, params, skip_self=False,
|
|
2552
|
+
type_param_scope=type_param_scope,
|
|
2553
|
+
kw_defaults=node.args.kw_defaults,
|
|
2554
|
+
n_kwonly=len(node.args.kwonlyargs))
|
|
2555
|
+
|
|
2556
|
+
# None means no annotation; sema substitutes VOID.
|
|
2557
|
+
return_type: 'TpyType | TypeRefNode | None' = None
|
|
2558
|
+
if node.returns:
|
|
2559
|
+
return_type = self._parse_type_ref(node.returns, type_param_scope)
|
|
2560
|
+
|
|
2561
|
+
# Validate body vs linkage
|
|
2562
|
+
is_stub_body = self._is_stub_body(node.body)
|
|
2563
|
+
is_overload_stub_body = is_stub_body or self._is_pass_body(node.body)
|
|
2564
|
+
is_stub = False
|
|
2565
|
+
|
|
2566
|
+
if builtin_decorator_key is not None:
|
|
2567
|
+
if not self._is_stub_body(node.body):
|
|
2568
|
+
raise ParseError(
|
|
2569
|
+
f"@builtin_decorator function '{node.name}' must have `...` body", node)
|
|
2570
|
+
is_stub = True
|
|
2571
|
+
body = []
|
|
2572
|
+
elif builtin_function_key is not None:
|
|
2573
|
+
is_stub = True
|
|
2574
|
+
body = []
|
|
2575
|
+
elif cpp_template is not None:
|
|
2576
|
+
if not self._is_stub_body(node.body):
|
|
2577
|
+
raise ParseError(
|
|
2578
|
+
f"@cpp_template function '{node.name}' must have `...` body", node)
|
|
2579
|
+
is_stub = True
|
|
2580
|
+
body = []
|
|
2581
|
+
elif is_overload_stub:
|
|
2582
|
+
# @overload functions may be bodyless (`...` / `pass`, paired with a
|
|
2583
|
+
# trailing impl) or carry their own body (self-contained overload
|
|
2584
|
+
# variant -- sema validates that a group is all-bodied or all-bodyless).
|
|
2585
|
+
body = [] if is_overload_stub_body else self._parse_body(node.body)
|
|
2586
|
+
elif linkage in (FunctionLinkage.NATIVE, FunctionLinkage.NATIVE_C):
|
|
2587
|
+
if not (self._is_stub_body(node.body)):
|
|
2588
|
+
display = self._LINKAGE_DISPLAY_NAMES.get(linkage, linkage.value)
|
|
2589
|
+
raise ParseError(
|
|
2590
|
+
f"@{display} function '{node.name}' must have `...` body (it declares an external symbol)",
|
|
2591
|
+
node)
|
|
2592
|
+
is_stub = True
|
|
2593
|
+
body = []
|
|
2594
|
+
elif linkage == FunctionLinkage.EXPORT_C:
|
|
2595
|
+
if is_stub_body:
|
|
2596
|
+
raise ParseError(
|
|
2597
|
+
f"@export function '{node.name}' must have a body (it exports a TPy function)",
|
|
2598
|
+
node)
|
|
2599
|
+
body = self._parse_body(node.body)
|
|
2600
|
+
else:
|
|
2601
|
+
body = self._parse_body(node.body)
|
|
2602
|
+
|
|
2603
|
+
# Restore the scope
|
|
2604
|
+
self._type_param_scope = old_scope
|
|
2605
|
+
|
|
2606
|
+
is_generator = _body_contains_yield(body)
|
|
2607
|
+
if is_generator:
|
|
2608
|
+
# Validate: no 'return value' inside generator
|
|
2609
|
+
_check_no_return_value_in_generator(body, node.name)
|
|
2610
|
+
|
|
2611
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
2612
|
+
if is_async:
|
|
2613
|
+
# v1 exclusion rules: each is "not yet supported", not "forbidden
|
|
2614
|
+
# forever". The async-generator case (yield in async def) and
|
|
2615
|
+
# @error_return / @noalloc combinations are deferred to v3+ per
|
|
2616
|
+
# docs/ASYNC_DESIGN.md ("Mutually exclusive with @error_return /
|
|
2617
|
+
# @noalloc / yield in v1").
|
|
2618
|
+
if is_generator:
|
|
2619
|
+
raise ParseError(
|
|
2620
|
+
f"async generators (async def + yield) are not yet supported "
|
|
2621
|
+
f"on async function '{node.name}'", node)
|
|
2622
|
+
if error_return is not None:
|
|
2623
|
+
raise ParseError(
|
|
2624
|
+
f"async def + @error_return is not yet supported on '{node.name}'",
|
|
2625
|
+
node)
|
|
2626
|
+
if is_noalloc:
|
|
2627
|
+
raise ParseError(
|
|
2628
|
+
f"async def + @noalloc is not yet supported on '{node.name}'",
|
|
2629
|
+
node)
|
|
2630
|
+
if linkage != FunctionLinkage.DEFAULT:
|
|
2631
|
+
display = self._LINKAGE_DISPLAY_NAMES.get(linkage, linkage.value)
|
|
2632
|
+
raise ParseError(
|
|
2633
|
+
f"async def + @{display} is not yet supported on '{node.name}'",
|
|
2634
|
+
node)
|
|
2635
|
+
|
|
2636
|
+
return TpyFunction(
|
|
2637
|
+
name=node.name,
|
|
2638
|
+
params=params,
|
|
2639
|
+
return_type=return_type,
|
|
2640
|
+
body=body,
|
|
2641
|
+
is_noalloc=is_noalloc,
|
|
2642
|
+
is_inline=is_inline,
|
|
2643
|
+
is_readonly=is_readonly,
|
|
2644
|
+
readonly_opt_out=readonly_opt_out,
|
|
2645
|
+
is_pure=is_pure,
|
|
2646
|
+
is_overload_stub=is_overload_stub,
|
|
2647
|
+
linkage=linkage,
|
|
2648
|
+
native_name=native_name,
|
|
2649
|
+
native_cpp_return_type=native_cpp_return_type,
|
|
2650
|
+
cpp_template=cpp_template,
|
|
2651
|
+
is_stub=is_overload_stub_body if is_overload_stub else is_stub,
|
|
2652
|
+
value_ptr_coercion=value_ptr_coercion,
|
|
2653
|
+
type_params=type_params,
|
|
2654
|
+
type_param_kinds=type_param_kinds,
|
|
2655
|
+
type_param_bounds=type_param_bounds,
|
|
2656
|
+
type_param_defaults=type_param_defaults,
|
|
2657
|
+
defaults=defaults,
|
|
2658
|
+
keyword_only_start=keyword_only_start,
|
|
2659
|
+
vararg_name=vararg_name,
|
|
2660
|
+
vararg_type=vararg_type,
|
|
2661
|
+
kwarg_name=kwarg_name,
|
|
2662
|
+
kwarg_type=kwarg_type,
|
|
2663
|
+
error_return=error_return,
|
|
2664
|
+
builtin_decorator_key=builtin_decorator_key,
|
|
2665
|
+
builtin_function_key=builtin_function_key,
|
|
2666
|
+
is_generator=is_generator,
|
|
2667
|
+
is_async=is_async,
|
|
2668
|
+
loc=self._loc(node)
|
|
2669
|
+
)
|
|
2670
|
+
|
|
2671
|
+
def _is_stub_body(self, body: list[ast.stmt]) -> bool:
|
|
2672
|
+
"""Check if a function body is a stub (only `...`).
|
|
2673
|
+
|
|
2674
|
+
Only Ellipsis marks a declaration-only stub. `pass` is a valid
|
|
2675
|
+
no-op body that should still generate a definition.
|
|
2676
|
+
"""
|
|
2677
|
+
if len(body) == 1:
|
|
2678
|
+
stmt = body[0]
|
|
2679
|
+
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and stmt.value.value is ...:
|
|
2680
|
+
return True
|
|
2681
|
+
return False
|
|
2682
|
+
|
|
2683
|
+
@staticmethod
|
|
2684
|
+
def _is_pass_body(body: list[ast.stmt]) -> bool:
|
|
2685
|
+
"""Check if a function body is only `pass`."""
|
|
2686
|
+
return len(body) == 1 and isinstance(body[0], ast.Pass)
|
|
2687
|
+
|
|
2688
|
+
def _parse_type_annotation(self, node: ast.expr, type_param_scope: dict[str, TypeParamKind] | None = None) -> TpyType:
|
|
2689
|
+
"""Parse a type annotation to a TpyType.
|
|
2690
|
+
|
|
2691
|
+
Thin wrapper around the walker + resolver composition; kept for
|
|
2692
|
+
the parse-time sites that need a resolved type immediately
|
|
2693
|
+
(type-parameter bounds, FragmentParser).
|
|
2694
|
+
"""
|
|
2695
|
+
if type_param_scope is None:
|
|
2696
|
+
type_param_scope = self._type_param_scope
|
|
2697
|
+
ref = self._parse_type_ref(node, type_param_scope)
|
|
2698
|
+
return self._resolve_type_ref_impl(ref, type_param_scope)
|
|
2699
|
+
|
|
2700
|
+
# ------------------------------------------------------------------
|
|
2701
|
+
# Type-reference walker
|
|
2702
|
+
#
|
|
2703
|
+
# `_parse_type_ref` mirrors `_parse_type_annotation`'s structural walk
|
|
2704
|
+
# but returns a TypeRefNode instead of a TpyType. It resolves names
|
|
2705
|
+
# only enough to disambiguate structural wrappers (Ptr/Own/Optional/
|
|
2706
|
+
# Callable/Fn/Literal/tuple/...) from user generics; leaf names
|
|
2707
|
+
# (primitives, user records, enums, protocols, type parameters) stay
|
|
2708
|
+
# raw in `TpyTypeRef.name` and are resolved by a later resolver pass.
|
|
2709
|
+
#
|
|
2710
|
+
# Validation errors that require resolved types (e.g. "Unknown type",
|
|
2711
|
+
# "Qualified name with missing module import", Own-in-union, readonly
|
|
2712
|
+
# normalization) are deliberately not raised here -- they belong in
|
|
2713
|
+
# the resolver. Only errors provable from syntax alone (malformed
|
|
2714
|
+
# Callable/Fn/Literal shape) are raised at this layer.
|
|
2715
|
+
# ------------------------------------------------------------------
|
|
2716
|
+
|
|
2717
|
+
def _parse_type_ref(
|
|
2718
|
+
self, node: ast.expr,
|
|
2719
|
+
type_param_scope: dict[str, TypeParamKind] | None = None,
|
|
2720
|
+
) -> ResolverInputNode:
|
|
2721
|
+
"""Walk an ast.expr for a type annotation and produce a ResolverInputNode
|
|
2722
|
+
(one of the four walker outputs: TpyTypeRef, TpyUnionRef,
|
|
2723
|
+
TpyCallableRef, TpyLiteralRef). TpyInferFromDefaultRef is emitted
|
|
2724
|
+
only at a single field-declaration site, never by this walker."""
|
|
2725
|
+
if type_param_scope is None:
|
|
2726
|
+
type_param_scope = self._type_param_scope
|
|
2727
|
+
loc = self._loc(node)
|
|
2728
|
+
|
|
2729
|
+
if isinstance(node, ast.Name):
|
|
2730
|
+
name = node.id
|
|
2731
|
+
# Eager nested-scope substitution so refs emitted inside a class
|
|
2732
|
+
# body survive to sema-time resolution -- _nested_type_scope is
|
|
2733
|
+
# parse-time-only state, empty by the time sema runs. E.g.
|
|
2734
|
+
# referencing `Kind` inside `class Message: class Kind: ...`
|
|
2735
|
+
# emits TpyTypeRef("Message.Kind", ...) directly.
|
|
2736
|
+
if name in self._nested_type_scope:
|
|
2737
|
+
return TpyTypeRef(self._nested_type_scope[name], (), loc)
|
|
2738
|
+
return TpyTypeRef(name, (), loc)
|
|
2739
|
+
|
|
2740
|
+
if isinstance(node, ast.Subscript):
|
|
2741
|
+
return self._parse_subscript_type_ref(node, type_param_scope, loc)
|
|
2742
|
+
|
|
2743
|
+
if isinstance(node, ast.Attribute):
|
|
2744
|
+
parts: list[str] = []
|
|
2745
|
+
cur: ast.expr = node
|
|
2746
|
+
while isinstance(cur, ast.Attribute):
|
|
2747
|
+
parts.append(cur.attr)
|
|
2748
|
+
cur = cur.value
|
|
2749
|
+
if isinstance(cur, ast.Name):
|
|
2750
|
+
parts.append(cur.id)
|
|
2751
|
+
parts.reverse()
|
|
2752
|
+
return TpyTypeRef(".".join(parts), (), loc)
|
|
2753
|
+
raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
|
|
2754
|
+
|
|
2755
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
2756
|
+
return TpyTypeRef("None", (), loc)
|
|
2757
|
+
|
|
2758
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
2759
|
+
# PEP 484 forward-ref strings (`-> "ClassName"`) and every
|
|
2760
|
+
# annotation under `from __future__ import annotations` arrive
|
|
2761
|
+
# as string Constants. Inner nodes from the re-parse inherit
|
|
2762
|
+
# the outer Constant's location so diagnostics point at the
|
|
2763
|
+
# annotation site instead of line 1 col 0 of the standalone
|
|
2764
|
+
# parse. (ast.fix_missing_locations only fills missing values;
|
|
2765
|
+
# the re-parsed nodes already have lineno=1, so we overwrite.)
|
|
2766
|
+
src = node.value
|
|
2767
|
+
try:
|
|
2768
|
+
inner = ast.parse(src, mode='eval').body
|
|
2769
|
+
except SyntaxError as exc:
|
|
2770
|
+
raise ParseError(
|
|
2771
|
+
f"Cannot parse string type annotation {src!r}: {exc.msg}",
|
|
2772
|
+
node,
|
|
2773
|
+
)
|
|
2774
|
+
for child in ast.walk(inner):
|
|
2775
|
+
if hasattr(child, 'lineno'):
|
|
2776
|
+
child.lineno = node.lineno
|
|
2777
|
+
child.col_offset = node.col_offset
|
|
2778
|
+
child.end_lineno = node.end_lineno
|
|
2779
|
+
child.end_col_offset = node.end_col_offset
|
|
2780
|
+
return self._parse_type_ref(inner, type_param_scope)
|
|
2781
|
+
|
|
2782
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
2783
|
+
arms = _collect_bitor_arms(node)
|
|
2784
|
+
members = tuple(self._parse_type_ref(arm, type_param_scope) for arm in arms)
|
|
2785
|
+
return TpyUnionRef(members, loc)
|
|
2786
|
+
|
|
2787
|
+
raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
|
|
2788
|
+
|
|
2789
|
+
def _parse_subscript_type_ref(
|
|
2790
|
+
self, node: ast.Subscript,
|
|
2791
|
+
type_param_scope: dict[str, TypeParamKind] | None,
|
|
2792
|
+
loc: SourceLocation | None,
|
|
2793
|
+
) -> ResolverInputNode:
|
|
2794
|
+
"""Handle ast.Subscript: disambiguate structural wrappers from generics."""
|
|
2795
|
+
if isinstance(node.value, ast.Name):
|
|
2796
|
+
resolved = self._resolve_type_name(node.value.id)
|
|
2797
|
+
raw_name: str | None = node.value.id
|
|
2798
|
+
elif isinstance(node.value, ast.Attribute):
|
|
2799
|
+
resolved = self._resolve_qualified_type_name(node.value)
|
|
2800
|
+
# Build full dotted name from the Attribute chain so 3+ level
|
|
2801
|
+
# (a.b.c.D[T]) nested-class references reach the resolver's
|
|
2802
|
+
# _resolve_dotted_class_name_str path. 2-level stays identical to
|
|
2803
|
+
# the original "value.attr" form.
|
|
2804
|
+
parts: list[str] = []
|
|
2805
|
+
cur: ast.expr = node.value
|
|
2806
|
+
while isinstance(cur, ast.Attribute):
|
|
2807
|
+
parts.append(cur.attr)
|
|
2808
|
+
cur = cur.value
|
|
2809
|
+
if isinstance(cur, ast.Name):
|
|
2810
|
+
parts.append(cur.id)
|
|
2811
|
+
parts.reverse()
|
|
2812
|
+
raw_name = ".".join(parts)
|
|
2813
|
+
else:
|
|
2814
|
+
raw_name = None
|
|
2815
|
+
else:
|
|
2816
|
+
raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
|
|
2817
|
+
|
|
2818
|
+
# Canonical structural-wrapper names use a `:` separator so they
|
|
2819
|
+
# cannot collide with any Python identifier-based name (including
|
|
2820
|
+
# raw dotted user source like "typing.Optional" written without
|
|
2821
|
+
# `import typing`). The walker emits the canonical form only when
|
|
2822
|
+
# the name actually resolved to the expected module+name pair;
|
|
2823
|
+
# unresolved names fall through to the generic path with the raw
|
|
2824
|
+
# source form, and the resolver's unresolved-name errors fire.
|
|
2825
|
+
if resolved:
|
|
2826
|
+
module, original = resolved
|
|
2827
|
+
if module == "tpy":
|
|
2828
|
+
if original in ("Ptr", "Own", "readonly", "auto_readonly", "auto_own"):
|
|
2829
|
+
inner_ref = self._parse_type_ref(node.slice, type_param_scope)
|
|
2830
|
+
return TpyTypeRef(f"tpy:{original}", (inner_ref,), loc)
|
|
2831
|
+
if original == "Fn":
|
|
2832
|
+
return self._parse_callable_type_ref(node, "Fn", type_param_scope, loc)
|
|
2833
|
+
elif module == "builtins":
|
|
2834
|
+
if original == "tuple":
|
|
2835
|
+
slices = _extract_subscript_slices(node)
|
|
2836
|
+
elem_refs = tuple(
|
|
2837
|
+
self._parse_type_ref(s, type_param_scope) for s in slices
|
|
2838
|
+
)
|
|
2839
|
+
return TpyTypeRef("builtins:tuple", elem_refs, loc)
|
|
2840
|
+
elif module == "typing":
|
|
2841
|
+
if original == "Optional":
|
|
2842
|
+
inner_ref = self._parse_type_ref(node.slice, type_param_scope)
|
|
2843
|
+
return TpyTypeRef("typing:Optional", (inner_ref,), loc)
|
|
2844
|
+
if original == "Final":
|
|
2845
|
+
inner_ref = self._parse_type_ref(node.slice, type_param_scope)
|
|
2846
|
+
return TpyTypeRef("typing:Final", (inner_ref,), loc)
|
|
2847
|
+
if original == "ClassVar":
|
|
2848
|
+
inner_ref = self._parse_type_ref(node.slice, type_param_scope)
|
|
2849
|
+
return TpyTypeRef("typing:ClassVar", (inner_ref,), loc)
|
|
2850
|
+
if original == "Callable":
|
|
2851
|
+
return self._parse_callable_type_ref(node, "Callable", type_param_scope, loc)
|
|
2852
|
+
if original == "Literal":
|
|
2853
|
+
return self._parse_literal_type_ref(node, loc)
|
|
2854
|
+
|
|
2855
|
+
# Not a structural wrapper: generic nominal reference. Resolver handles
|
|
2856
|
+
# the nominal lookup. Keep the raw source name; if the container was
|
|
2857
|
+
# import-resolved we still emit the source form (not the resolved
|
|
2858
|
+
# canonical one) to keep this pass syntactic.
|
|
2859
|
+
name = raw_name if raw_name is not None else (resolved[1] if resolved else None)
|
|
2860
|
+
if name is None:
|
|
2861
|
+
raise ParseError(f"Cannot parse type annotation: {ast.dump(node)}", node)
|
|
2862
|
+
|
|
2863
|
+
slices = _extract_subscript_slices(node)
|
|
2864
|
+
arg_refs: list[ResolverInputNode | int] = []
|
|
2865
|
+
for s in slices:
|
|
2866
|
+
if (isinstance(s, ast.Constant) and isinstance(s.value, int)
|
|
2867
|
+
and not isinstance(s.value, bool)):
|
|
2868
|
+
arg_refs.append(s.value)
|
|
2869
|
+
else:
|
|
2870
|
+
arg_refs.append(self._parse_type_ref(s, type_param_scope))
|
|
2871
|
+
return TpyTypeRef(name, tuple(arg_refs), loc)
|
|
2872
|
+
|
|
2873
|
+
def _parse_callable_type_ref(
|
|
2874
|
+
self, node: ast.Subscript, kind: str,
|
|
2875
|
+
type_param_scope: dict[str, TypeParamKind] | None,
|
|
2876
|
+
loc: SourceLocation | None,
|
|
2877
|
+
) -> TpyCallableRef:
|
|
2878
|
+
slices = _extract_subscript_slices(node)
|
|
2879
|
+
if len(slices) != 2:
|
|
2880
|
+
raise ParseError(
|
|
2881
|
+
f"{kind} requires exactly 2 arguments: {kind}[[ParamTypes...], ReturnType]",
|
|
2882
|
+
node,
|
|
2883
|
+
)
|
|
2884
|
+
param_list_node, return_node = slices
|
|
2885
|
+
if not isinstance(param_list_node, ast.List):
|
|
2886
|
+
raise ParseError(
|
|
2887
|
+
f"{kind} parameter types must be a list: {kind}[[Int32, str], bool]",
|
|
2888
|
+
node,
|
|
2889
|
+
)
|
|
2890
|
+
params = tuple(
|
|
2891
|
+
self._parse_type_ref(p, type_param_scope) for p in param_list_node.elts
|
|
2892
|
+
)
|
|
2893
|
+
return_type = self._parse_type_ref(return_node, type_param_scope)
|
|
2894
|
+
return TpyCallableRef(kind=kind, params=params, return_type=return_type, loc=loc)
|
|
2895
|
+
|
|
2896
|
+
def _parse_literal_type_ref(
|
|
2897
|
+
self, node: ast.Subscript, loc: SourceLocation | None,
|
|
2898
|
+
) -> TpyLiteralRef:
|
|
2899
|
+
slices = _extract_subscript_slices(node)
|
|
2900
|
+
if not slices:
|
|
2901
|
+
raise ParseError("Literal requires at least one argument", node)
|
|
2902
|
+
values: list[LiteralValue] = []
|
|
2903
|
+
tag: LiteralTag | None = None
|
|
2904
|
+
for s in slices:
|
|
2905
|
+
if isinstance(s, ast.Constant) and isinstance(s.value, str):
|
|
2906
|
+
if tag is not None and tag is not LiteralTag.STR:
|
|
2907
|
+
raise ParseError("Literal cannot mix value types", node)
|
|
2908
|
+
tag = LiteralTag.STR
|
|
2909
|
+
values.append(LiteralValue(LiteralTag.STR, s.value))
|
|
2910
|
+
elif isinstance(s, ast.Constant) and isinstance(s.value, bool):
|
|
2911
|
+
if tag is not None and tag is not LiteralTag.BOOL:
|
|
2912
|
+
raise ParseError("Literal cannot mix value types", node)
|
|
2913
|
+
tag = LiteralTag.BOOL
|
|
2914
|
+
values.append(LiteralValue(LiteralTag.BOOL, s.value))
|
|
2915
|
+
elif isinstance(s, ast.Constant) and isinstance(s.value, int):
|
|
2916
|
+
if tag is not None and tag is not LiteralTag.INT:
|
|
2917
|
+
raise ParseError("Literal cannot mix value types", node)
|
|
2918
|
+
tag = LiteralTag.INT
|
|
2919
|
+
values.append(LiteralValue(LiteralTag.INT, s.value))
|
|
2920
|
+
elif (isinstance(s, ast.UnaryOp) and isinstance(s.op, ast.USub)
|
|
2921
|
+
and isinstance(s.operand, ast.Constant)
|
|
2922
|
+
and isinstance(s.operand.value, int)
|
|
2923
|
+
and not isinstance(s.operand.value, bool)):
|
|
2924
|
+
if tag is not None and tag is not LiteralTag.INT:
|
|
2925
|
+
raise ParseError("Literal cannot mix value types", node)
|
|
2926
|
+
tag = LiteralTag.INT
|
|
2927
|
+
values.append(LiteralValue(LiteralTag.INT, -s.operand.value))
|
|
2928
|
+
else:
|
|
2929
|
+
raise ParseError(
|
|
2930
|
+
"Literal supports string, int, and bool arguments", node)
|
|
2931
|
+
return TpyLiteralRef(values=tuple(values), loc=loc)
|
|
2932
|
+
|
|
2933
|
+
def _resolve_type_ref_impl(
|
|
2934
|
+
self, ref: ResolverInputNode,
|
|
2935
|
+
type_param_scope: dict[str, TypeParamKind] | None = None,
|
|
2936
|
+
*, is_type_arg: bool = False,
|
|
2937
|
+
) -> TpyType:
|
|
2938
|
+
"""Thin delegate to `TypeResolver.resolve`. Retained for the
|
|
2939
|
+
walker-vs-resolver equivalence tests and for macro-fragment
|
|
2940
|
+
self-annotation resolution, both of which need a TpyType in
|
|
2941
|
+
hand synchronously.
|
|
2942
|
+
|
|
2943
|
+
`is_type_arg` see `TypeResolver.resolve`; pass True at value-
|
|
2944
|
+
bearing slots (params, vararg, self_annotation, fields, ...)
|
|
2945
|
+
so bare `None` lowers to NoneType. The function-return slot
|
|
2946
|
+
keeps the default `False` so `-> None` stays VoidType.
|
|
2947
|
+
"""
|
|
2948
|
+
return self._resolver.resolve(ref, type_param_scope, is_type_arg=is_type_arg)
|
|
2949
|
+
|
|
2950
|
+
def _finalize_function_refs(
|
|
2951
|
+
self, func: 'TpyFunction',
|
|
2952
|
+
outer_scope: dict[str, TypeParamKind] | None = None,
|
|
2953
|
+
) -> None:
|
|
2954
|
+
"""Resolve TypeRefNodes in a TpyFunction's params / return_type /
|
|
2955
|
+
vararg_type / kwarg_type in place, using the current parser state.
|
|
2956
|
+
|
|
2957
|
+
`outer_scope` is the enclosing type-param scope at the call site
|
|
2958
|
+
(e.g. a nested def's enclosing function scope). The function's own
|
|
2959
|
+
`type_params` are merged on top before resolution, so a method's
|
|
2960
|
+
or nested def's own generic params resolve to TypeParamRef
|
|
2961
|
+
correctly. Pass this explicitly rather than relying on
|
|
2962
|
+
`self._type_param_scope` being in the right state -- callers
|
|
2963
|
+
typically invoke this after `_parse_function` has already
|
|
2964
|
+
restored the enclosing scope on the parser instance.
|
|
2965
|
+
|
|
2966
|
+
Used by non-top-level callers of `_parse_function` (nested defs,
|
|
2967
|
+
macro fragment parsing, @builtin_decorator stubs) that need
|
|
2968
|
+
TpyType immediately, without waiting for the post-parse
|
|
2969
|
+
`resolve_refs` pass. Top-level functions leave refs in place for
|
|
2970
|
+
that pass to resolve.
|
|
2971
|
+
"""
|
|
2972
|
+
scope: dict[str, TypeParamKind] = dict(outer_scope or {})
|
|
2973
|
+
if func.type_params:
|
|
2974
|
+
kinds = func.type_param_kinds or []
|
|
2975
|
+
for i, name in enumerate(func.type_params):
|
|
2976
|
+
scope[name] = kinds[i] if i < len(kinds) else TypeParamKind.TYPE
|
|
2977
|
+
resolve_scope = scope or None
|
|
2978
|
+
|
|
2979
|
+
ref_types = (TpyTypeRef, TpyUnionRef, TpyCallableRef, TpyLiteralRef)
|
|
2980
|
+
new_params: list = []
|
|
2981
|
+
for name, t in func.params:
|
|
2982
|
+
if isinstance(t, ref_types):
|
|
2983
|
+
t = self._resolve_type_ref_impl(t, resolve_scope, is_type_arg=True)
|
|
2984
|
+
new_params.append((name, t))
|
|
2985
|
+
func.params = new_params
|
|
2986
|
+
if func.return_type is None:
|
|
2987
|
+
# "No annotation" sentinel -- route through the resolver as
|
|
2988
|
+
# `TpyTypeRef("None")` so the same VOID singleton is
|
|
2989
|
+
# substituted without a direct typesys import. The deferred
|
|
2990
|
+
# (sema-side) branch lives in
|
|
2991
|
+
# `resolve_refs._resolve_return_slot`; both paths converge
|
|
2992
|
+
# on VOID.
|
|
2993
|
+
func.return_type = self._resolve_type_ref_impl(
|
|
2994
|
+
TpyTypeRef(name="None"), resolve_scope)
|
|
2995
|
+
elif isinstance(func.return_type, ref_types):
|
|
2996
|
+
func.return_type = self._resolve_type_ref_impl(
|
|
2997
|
+
func.return_type, resolve_scope)
|
|
2998
|
+
if isinstance(func.vararg_type, ref_types):
|
|
2999
|
+
func.vararg_type = self._resolve_type_ref_impl(
|
|
3000
|
+
func.vararg_type, resolve_scope, is_type_arg=True)
|
|
3001
|
+
if isinstance(func.kwarg_type, ref_types):
|
|
3002
|
+
func.kwarg_type = self._resolve_type_ref_impl(
|
|
3003
|
+
func.kwarg_type, resolve_scope, is_type_arg=True)
|
|
3004
|
+
|
|
3005
|
+
def _parse_type_args_from_subscript(self, node: ast.Subscript) -> 'tuple[TpyType | TypeRefNode | None, ...]':
|
|
3006
|
+
"""Extract type arguments from a subscript for generic function calls like first[Int32](x).
|
|
3007
|
+
|
|
3008
|
+
Raises ParseError if any element is structurally not a valid
|
|
3009
|
+
type (e.g. integer literal at a type-args site). Name-resolution
|
|
3010
|
+
errors do not fire at parse time; elements emit as TypeRefNode
|
|
3011
|
+
and sema resolves in the pre-pass body walker.
|
|
3012
|
+
"""
|
|
3013
|
+
slices = _extract_subscript_slices(node)
|
|
3014
|
+
|
|
3015
|
+
# Parse each type argument - raise error if any is structurally invalid.
|
|
3016
|
+
type_args: 'list[TpyType | TypeRefNode | None]' = []
|
|
3017
|
+
for s in slices:
|
|
3018
|
+
# _ wildcard: infer this type argument
|
|
3019
|
+
if isinstance(s, ast.Name) and s.id == '_':
|
|
3020
|
+
type_args.append(None)
|
|
3021
|
+
continue
|
|
3022
|
+
# Integer constants are not valid type arguments
|
|
3023
|
+
if isinstance(s, ast.Constant) and isinstance(s.value, int):
|
|
3024
|
+
raise ParseError(f"Integer '{s.value}' is not a valid type argument", s)
|
|
3025
|
+
type_args.append(self._parse_type_ref(s))
|
|
3026
|
+
return tuple(type_args)
|
|
3027
|
+
|
|
3028
|
+
def _parse_comprehension_generator(self, gen: ast.comprehension) -> TpyComprehensionGenerator:
|
|
3029
|
+
"""Parse a single comprehension generator clause."""
|
|
3030
|
+
iterable = self._parse_expr(gen.iter)
|
|
3031
|
+
conditions = [self._parse_expr(c) for c in gen.ifs]
|
|
3032
|
+
if isinstance(gen.target, ast.Name):
|
|
3033
|
+
return TpyComprehensionGenerator(
|
|
3034
|
+
var=gen.target.id, iterable=iterable,
|
|
3035
|
+
conditions=conditions, unpack_vars=None)
|
|
3036
|
+
elif isinstance(gen.target, ast.Tuple):
|
|
3037
|
+
for elt in gen.target.elts:
|
|
3038
|
+
if not isinstance(elt, ast.Name):
|
|
3039
|
+
raise ParseError(
|
|
3040
|
+
"Comprehension unpacking targets must be simple variables", gen.target)
|
|
3041
|
+
unpack_vars: list[str | None] = [
|
|
3042
|
+
None if elt.id == "_" else elt.id # type: ignore[union-attr]
|
|
3043
|
+
for elt in gen.target.elts
|
|
3044
|
+
]
|
|
3045
|
+
return TpyComprehensionGenerator(
|
|
3046
|
+
var="__comp_tup", iterable=iterable,
|
|
3047
|
+
conditions=conditions, unpack_vars=unpack_vars)
|
|
3048
|
+
else:
|
|
3049
|
+
raise ParseError("Unsupported comprehension target", gen.target)
|
|
3050
|
+
|
|
3051
|
+
def _try_parse_type_args(self, node: ast.Subscript) -> 'tuple[tuple[TpyType | TypeRefNode | None, ...], str | None]':
|
|
3052
|
+
"""Try to parse type args from a subscript, capturing parse errors.
|
|
3053
|
+
|
|
3054
|
+
Returns (type_args, parse_error). On success parse_error is
|
|
3055
|
+
None. On failure type_args is empty and parse_error holds the
|
|
3056
|
+
message. Elements may be TypeRefNode pre-sema.
|
|
3057
|
+
"""
|
|
3058
|
+
try:
|
|
3059
|
+
return self._parse_type_args_from_subscript(node), None
|
|
3060
|
+
except ParseError as e:
|
|
3061
|
+
return (), e.message
|
|
3062
|
+
|
|
3063
|
+
def _parse_body(self, nodes: list[ast.stmt]) -> list[TpyStmt]:
|
|
3064
|
+
"""Parse a list of statements, flattening any multi-statement expansions."""
|
|
3065
|
+
result: list[TpyStmt] = []
|
|
3066
|
+
for node in nodes:
|
|
3067
|
+
stmt = self._parse_stmt(node)
|
|
3068
|
+
if isinstance(stmt, list):
|
|
3069
|
+
result.extend(stmt)
|
|
3070
|
+
else:
|
|
3071
|
+
result.append(stmt)
|
|
3072
|
+
return result
|
|
3073
|
+
|
|
3074
|
+
def _parse_stmt(self, node: ast.stmt) -> 'TpyStmt | list[TpyStmt]':
|
|
3075
|
+
"""Parse a statement."""
|
|
3076
|
+
loc = self._loc(node)
|
|
3077
|
+
|
|
3078
|
+
if isinstance(node, ast.AnnAssign):
|
|
3079
|
+
# Annotated assignment: x: T = expr
|
|
3080
|
+
if not isinstance(node.target, ast.Name):
|
|
3081
|
+
raise ParseError("Invalid assignment target", node)
|
|
3082
|
+
# Emit a TypeRefNode; sema's `_analyze_var_decl` resolves
|
|
3083
|
+
# it via `TypeOperations.resolve_type_ref` and writes the
|
|
3084
|
+
# resolved type back into stmt.type, so downstream code
|
|
3085
|
+
# sees TpyType.
|
|
3086
|
+
var_type_ref = self._parse_type_ref(node.annotation)
|
|
3087
|
+
init_expr = self._parse_expr(node.value) if node.value else None
|
|
3088
|
+
return TpyVarDecl(node.target.id, var_type_ref, init_expr, loc=loc)
|
|
3089
|
+
|
|
3090
|
+
elif isinstance(node, ast.Assign):
|
|
3091
|
+
# Simple assignment: x = expr or x.field = expr
|
|
3092
|
+
if len(node.targets) > 1:
|
|
3093
|
+
return self._parse_multi_assign(node, loc)
|
|
3094
|
+
if isinstance(node.targets[0], ast.Tuple):
|
|
3095
|
+
elts = node.targets[0].elts
|
|
3096
|
+
if not elts:
|
|
3097
|
+
raise ParseError("Empty tuple unpacking", node)
|
|
3098
|
+
targets: list[str | None] = []
|
|
3099
|
+
for elt in elts:
|
|
3100
|
+
if not isinstance(elt, ast.Name):
|
|
3101
|
+
raise ParseError(
|
|
3102
|
+
"Tuple unpacking targets must be simple variable names", node)
|
|
3103
|
+
targets.append(None if elt.id == "_" else elt.id)
|
|
3104
|
+
value = self._parse_expr(node.value)
|
|
3105
|
+
return TpyTupleUnpack(targets=targets, value=value, loc=loc)
|
|
3106
|
+
target = self._parse_expr(node.targets[0])
|
|
3107
|
+
value = self._parse_expr(node.value)
|
|
3108
|
+
# Check if this is a variable declaration (unannotated)
|
|
3109
|
+
if isinstance(target, TpyName):
|
|
3110
|
+
return TpyVarDecl(target.name, None, value, loc=loc)
|
|
3111
|
+
return TpyAssign(target, value, loc=loc)
|
|
3112
|
+
|
|
3113
|
+
elif isinstance(node, ast.AugAssign):
|
|
3114
|
+
# Augmented assignment: x += expr
|
|
3115
|
+
target = self._parse_expr(node.target)
|
|
3116
|
+
value = self._parse_expr(node.value)
|
|
3117
|
+
op = self._binop_to_str(node.op)
|
|
3118
|
+
return TpyAugAssign(target, op, value, loc=loc)
|
|
3119
|
+
|
|
3120
|
+
elif isinstance(node, ast.Expr):
|
|
3121
|
+
if isinstance(node.value, ast.Yield):
|
|
3122
|
+
if node.value.value is None:
|
|
3123
|
+
raise ParseError("'yield' must have a value in TurboPython", node)
|
|
3124
|
+
value = self._parse_expr(node.value.value)
|
|
3125
|
+
return TpyYield(value, loc=loc)
|
|
3126
|
+
return TpyExprStmt(self._parse_expr(node.value), loc=loc)
|
|
3127
|
+
|
|
3128
|
+
elif isinstance(node, ast.Return):
|
|
3129
|
+
value = self._parse_expr(node.value) if node.value else None
|
|
3130
|
+
return TpyReturn(value, loc=loc)
|
|
3131
|
+
|
|
3132
|
+
elif isinstance(node, ast.Assert):
|
|
3133
|
+
cond = self._parse_expr(node.test)
|
|
3134
|
+
msg = self._parse_expr(node.msg) if node.msg else None
|
|
3135
|
+
return TpyAssert(cond, msg, loc=loc)
|
|
3136
|
+
|
|
3137
|
+
elif isinstance(node, ast.If):
|
|
3138
|
+
cond = self._parse_expr(node.test)
|
|
3139
|
+
then_body = self._parse_body(node.body)
|
|
3140
|
+
else_body = self._parse_body(node.orelse)
|
|
3141
|
+
return TpyIf(cond, then_body, else_body, loc=loc)
|
|
3142
|
+
|
|
3143
|
+
elif isinstance(node, ast.While):
|
|
3144
|
+
cond = self._parse_expr(node.test)
|
|
3145
|
+
body = self._parse_body(node.body)
|
|
3146
|
+
orelse = self._parse_body(node.orelse)
|
|
3147
|
+
return TpyWhile(cond, body, orelse=orelse, loc=loc)
|
|
3148
|
+
|
|
3149
|
+
elif isinstance(node, (ast.For, ast.AsyncFor)):
|
|
3150
|
+
is_async = isinstance(node, ast.AsyncFor)
|
|
3151
|
+
if is_async and node.orelse:
|
|
3152
|
+
raise ParseError(
|
|
3153
|
+
"`else:` clause on `async for` is not supported yet "
|
|
3154
|
+
"(the `__anext__`-driven async-for advance doesn't model "
|
|
3155
|
+
"break-vs-normal-exit; sync `for`/`while`-`else` with a "
|
|
3156
|
+
"suspension is supported)",
|
|
3157
|
+
node,
|
|
3158
|
+
)
|
|
3159
|
+
orelse = self._parse_body(node.orelse)
|
|
3160
|
+
if isinstance(node.target, ast.Tuple):
|
|
3161
|
+
for elt in node.target.elts:
|
|
3162
|
+
if not isinstance(elt, ast.Name):
|
|
3163
|
+
raise ParseError(
|
|
3164
|
+
"For loop unpacking targets must be simple variables", node
|
|
3165
|
+
)
|
|
3166
|
+
targets: list[str | None] = [
|
|
3167
|
+
None if elt.id == "_" else elt.id # type: ignore[union-attr]
|
|
3168
|
+
for elt in node.target.elts
|
|
3169
|
+
]
|
|
3170
|
+
synth_var = f"__for_tup_{self._for_unpack_counter}"
|
|
3171
|
+
self._for_unpack_counter += 1
|
|
3172
|
+
iterable = self._parse_expr(node.iter)
|
|
3173
|
+
body = self._parse_body(node.body)
|
|
3174
|
+
unpack = TpyTupleUnpack(
|
|
3175
|
+
targets=targets,
|
|
3176
|
+
value=TpyName(synth_var, loc=loc),
|
|
3177
|
+
loc=loc,
|
|
3178
|
+
)
|
|
3179
|
+
return TpyForEach(synth_var, iterable, [unpack] + body, orelse=orelse, loc=loc, is_tuple_unpack=True, is_async=is_async)
|
|
3180
|
+
if not isinstance(node.target, ast.Name):
|
|
3181
|
+
raise ParseError("For loop target must be a simple variable", node)
|
|
3182
|
+
var = node.target.id
|
|
3183
|
+
body = self._parse_body(node.body)
|
|
3184
|
+
iterable = self._parse_expr(node.iter)
|
|
3185
|
+
return TpyForEach(var, iterable, body, orelse=orelse, loc=loc, is_async=is_async)
|
|
3186
|
+
|
|
3187
|
+
elif isinstance(node, ast.Pass):
|
|
3188
|
+
return TpyPassStmt(loc=loc)
|
|
3189
|
+
|
|
3190
|
+
elif isinstance(node, ast.Break):
|
|
3191
|
+
return TpyBreak(loc=loc)
|
|
3192
|
+
|
|
3193
|
+
elif isinstance(node, ast.Continue):
|
|
3194
|
+
return TpyContinue(loc=loc)
|
|
3195
|
+
|
|
3196
|
+
elif isinstance(node, ast.Global):
|
|
3197
|
+
return TpyGlobal(node.names, loc=loc)
|
|
3198
|
+
|
|
3199
|
+
elif isinstance(node, ast.Raise):
|
|
3200
|
+
return self._parse_raise(node, loc)
|
|
3201
|
+
|
|
3202
|
+
elif isinstance(node, ast.Delete):
|
|
3203
|
+
return self._parse_delete(node, loc)
|
|
3204
|
+
|
|
3205
|
+
elif isinstance(node, ast.Match):
|
|
3206
|
+
return self._parse_match(node, loc)
|
|
3207
|
+
|
|
3208
|
+
elif isinstance(node, ast.Try):
|
|
3209
|
+
return self._parse_try(node, loc)
|
|
3210
|
+
|
|
3211
|
+
elif isinstance(node, ast.With):
|
|
3212
|
+
return self._parse_with(node, loc, is_async=False)
|
|
3213
|
+
|
|
3214
|
+
elif isinstance(node, ast.AsyncWith):
|
|
3215
|
+
return self._parse_with(node, loc, is_async=True)
|
|
3216
|
+
|
|
3217
|
+
elif isinstance(node, ast.Nonlocal):
|
|
3218
|
+
return TpyNonlocal(node.names, loc=loc)
|
|
3219
|
+
|
|
3220
|
+
elif isinstance(node, ast.FunctionDef):
|
|
3221
|
+
return self._parse_nested_def(node, loc)
|
|
3222
|
+
|
|
3223
|
+
elif isinstance(node, ast.AsyncFunctionDef):
|
|
3224
|
+
raise ParseError(
|
|
3225
|
+
f"nested async def is not yet supported (function '{node.name}'); "
|
|
3226
|
+
f"async def is only allowed at module level", node)
|
|
3227
|
+
|
|
3228
|
+
else:
|
|
3229
|
+
raise ParseError(f"Unsupported statement: {type(node).__name__}", node)
|
|
3230
|
+
|
|
3231
|
+
def _parse_raise(self, node: ast.Raise, loc: SourceLocation | None) -> TpyStmt:
|
|
3232
|
+
"""Parse a raise statement: raise E, raise E(args), or bare raise."""
|
|
3233
|
+
exc = node.exc
|
|
3234
|
+
if exc is None:
|
|
3235
|
+
# Bare raise (re-raise) -- validated by sema to be inside except block
|
|
3236
|
+
return TpyRaise(loc=loc)
|
|
3237
|
+
# raise Name
|
|
3238
|
+
if isinstance(exc, ast.Name):
|
|
3239
|
+
name = exc.id
|
|
3240
|
+
return TpyRaise(exception_type=name, loc=loc)
|
|
3241
|
+
# raise Name() or raise Name(args...)
|
|
3242
|
+
if isinstance(exc, ast.Call) and isinstance(exc.func, ast.Name):
|
|
3243
|
+
name = exc.func.id
|
|
3244
|
+
if exc.keywords:
|
|
3245
|
+
raise ParseError(f"'raise {name}' does not accept keyword arguments", node)
|
|
3246
|
+
args = [self._parse_expr(a) for a in exc.args]
|
|
3247
|
+
return TpyRaise(exception_type=name, args=args, is_call_form=True, loc=loc)
|
|
3248
|
+
# raise <expr> -- general expression (e.g. raise obj.make_err(), raise errors[i])
|
|
3249
|
+
return TpyRaise(raise_expr=self._parse_expr(exc), loc=loc)
|
|
3250
|
+
|
|
3251
|
+
def _parse_try(self, node: ast.Try, loc: SourceLocation | None) -> TpyStmt:
|
|
3252
|
+
"""Parse a try/except/else/finally statement."""
|
|
3253
|
+
if not node.handlers and not node.finalbody:
|
|
3254
|
+
raise ParseError("'try' requires at least one 'except' or 'finally' clause", node)
|
|
3255
|
+
handlers: list[TpyExceptHandler] = []
|
|
3256
|
+
for h in node.handlers:
|
|
3257
|
+
h_loc = self._loc(h) if hasattr(h, 'lineno') else loc
|
|
3258
|
+
if h.type is None:
|
|
3259
|
+
# Bare except: -- must be last handler (Python enforces this)
|
|
3260
|
+
exception_type = None
|
|
3261
|
+
else:
|
|
3262
|
+
exception_type = _attr_chain_to_dotted(h.type)
|
|
3263
|
+
if exception_type is None:
|
|
3264
|
+
raise ParseError(
|
|
3265
|
+
"'except' requires a simple or dotted name "
|
|
3266
|
+
"(e.g. 'except MyError' or 'except pkg.MyError')", h)
|
|
3267
|
+
handlers.append(TpyExceptHandler(
|
|
3268
|
+
exception_type=exception_type, binding=h.name,
|
|
3269
|
+
body=self._parse_body(h.body), loc=h_loc))
|
|
3270
|
+
try_body = self._parse_body(node.body)
|
|
3271
|
+
else_body = self._parse_body(node.orelse)
|
|
3272
|
+
finally_body = self._parse_body(node.finalbody)
|
|
3273
|
+
return TpyTry(
|
|
3274
|
+
try_body=try_body,
|
|
3275
|
+
handlers=handlers,
|
|
3276
|
+
else_body=else_body,
|
|
3277
|
+
finally_body=finally_body,
|
|
3278
|
+
loc=loc,
|
|
3279
|
+
)
|
|
3280
|
+
|
|
3281
|
+
def _parse_with(self, node: ast.With | ast.AsyncWith,
|
|
3282
|
+
loc: SourceLocation | None,
|
|
3283
|
+
is_async: bool) -> TpyWith:
|
|
3284
|
+
"""Parse a sync or async with statement."""
|
|
3285
|
+
kw = "async with" if is_async else "with"
|
|
3286
|
+
items: list[TpyWithItem] = []
|
|
3287
|
+
for item in node.items:
|
|
3288
|
+
context_expr = self._parse_expr(item.context_expr)
|
|
3289
|
+
target: str | None = None
|
|
3290
|
+
if item.optional_vars is not None:
|
|
3291
|
+
if not isinstance(item.optional_vars, ast.Name):
|
|
3292
|
+
raise ParseError(
|
|
3293
|
+
f"'{kw} ... as' target must be a simple variable", node
|
|
3294
|
+
)
|
|
3295
|
+
target = item.optional_vars.id
|
|
3296
|
+
item_loc = self._loc(item.context_expr)
|
|
3297
|
+
items.append(TpyWithItem(context_expr, target, loc=item_loc))
|
|
3298
|
+
body = self._parse_body(node.body)
|
|
3299
|
+
return TpyWith(items, body, is_async=is_async, loc=loc)
|
|
3300
|
+
|
|
3301
|
+
def _parse_nested_def(self, node: ast.FunctionDef, loc: SourceLocation | None) -> TpyNestedDef:
|
|
3302
|
+
"""Parse a nested function definition inside a function body."""
|
|
3303
|
+
if node.decorator_list:
|
|
3304
|
+
raise ParseError(
|
|
3305
|
+
f"Decorators are not supported on nested functions", node)
|
|
3306
|
+
if hasattr(node, 'type_params') and node.type_params:
|
|
3307
|
+
raise ParseError(
|
|
3308
|
+
f"Type parameters are not supported on nested functions", node)
|
|
3309
|
+
func = self._parse_function(node)
|
|
3310
|
+
# Nested defs live inside a function body, so sema's top-level
|
|
3311
|
+
# resolve_refs pre-pass doesn't see them. Resolve
|
|
3312
|
+
# refs immediately, passing the enclosing function's type-param
|
|
3313
|
+
# scope so outer generic params still resolve inside the nested
|
|
3314
|
+
# body. _parse_function restored self._type_param_scope to the
|
|
3315
|
+
# enclosing value before returning.
|
|
3316
|
+
self._finalize_function_refs(func, outer_scope=self._type_param_scope)
|
|
3317
|
+
return TpyNestedDef(func=func, loc=loc)
|
|
3318
|
+
|
|
3319
|
+
def _parse_multi_assign(self, node: ast.Assign,
|
|
3320
|
+
loc: SourceLocation | None) -> list[TpyStmt]:
|
|
3321
|
+
"""Desugar multi-target assignment: a = b = c = expr.
|
|
3322
|
+
|
|
3323
|
+
Uses a name target as anchor so the value is evaluated exactly once.
|
|
3324
|
+
All other targets reference the anchor. If no name target exists,
|
|
3325
|
+
a synthetic temp is introduced.
|
|
3326
|
+
"""
|
|
3327
|
+
for t in node.targets:
|
|
3328
|
+
if isinstance(t, ast.Tuple):
|
|
3329
|
+
raise ParseError(
|
|
3330
|
+
"Tuple unpacking not supported in multiple assignment", node)
|
|
3331
|
+
value_expr = self._parse_expr(node.value)
|
|
3332
|
+
# Find rightmost Name target to use as anchor
|
|
3333
|
+
anchor: ast.Name | None = None
|
|
3334
|
+
for t in reversed(node.targets):
|
|
3335
|
+
if isinstance(t, ast.Name):
|
|
3336
|
+
anchor = t
|
|
3337
|
+
break
|
|
3338
|
+
stmts: list[TpyStmt] = []
|
|
3339
|
+
if anchor is not None:
|
|
3340
|
+
anchor_name = anchor.id
|
|
3341
|
+
stmts.append(TpyVarDecl(anchor_name, None, value_expr, loc=loc))
|
|
3342
|
+
else:
|
|
3343
|
+
# No name target -- introduce synthetic temp
|
|
3344
|
+
anchor_name = f"__ma_{self._multi_assign_counter}"
|
|
3345
|
+
self._multi_assign_counter += 1
|
|
3346
|
+
stmts.append(TpyVarDecl(anchor_name, None, value_expr, loc=loc))
|
|
3347
|
+
# Assign anchor to remaining targets (left-to-right, no source comment)
|
|
3348
|
+
for t in node.targets:
|
|
3349
|
+
if t is anchor:
|
|
3350
|
+
continue
|
|
3351
|
+
target = self._parse_expr(t)
|
|
3352
|
+
ref = TpyName(anchor_name, loc=loc)
|
|
3353
|
+
if isinstance(target, TpyName):
|
|
3354
|
+
stmts.append(TpyVarDecl(target.name, None, ref, loc=None))
|
|
3355
|
+
else:
|
|
3356
|
+
stmts.append(TpyAssign(target, ref, loc=None))
|
|
3357
|
+
return stmts
|
|
3358
|
+
|
|
3359
|
+
def _parse_delete(self, node: ast.Delete, loc: SourceLocation | None) -> TpyStmt:
|
|
3360
|
+
"""Parse a del statement. Supports subscript, variable, and attribute targets."""
|
|
3361
|
+
subscripts: list[TpySubscript] = []
|
|
3362
|
+
names: list[str] = []
|
|
3363
|
+
attrs: list[TpyFieldAccess] = []
|
|
3364
|
+
for target in node.targets:
|
|
3365
|
+
if isinstance(target, ast.Subscript):
|
|
3366
|
+
obj = self._parse_expr(target.value)
|
|
3367
|
+
index = self._parse_expr(target.slice)
|
|
3368
|
+
subscripts.append(TpySubscript(obj, index, loc=loc))
|
|
3369
|
+
elif isinstance(target, ast.Name):
|
|
3370
|
+
names.append(target.id)
|
|
3371
|
+
elif isinstance(target, ast.Attribute):
|
|
3372
|
+
obj = self._parse_expr(target.value)
|
|
3373
|
+
attrs.append(TpyFieldAccess(obj=obj, field=target.attr, loc=loc))
|
|
3374
|
+
else:
|
|
3375
|
+
raise ParseError(f"Unsupported del target: {type(target).__name__}", node)
|
|
3376
|
+
# Mixing the three kinds in one statement is rare; reject for clarity
|
|
3377
|
+
# (matches the existing subscript+name rejection).
|
|
3378
|
+
kinds_present = sum(1 for k in (subscripts, names, attrs) if k)
|
|
3379
|
+
if kinds_present > 1:
|
|
3380
|
+
raise ParseError(
|
|
3381
|
+
"Cannot mix variable, subscript, and attribute targets in a single 'del' statement",
|
|
3382
|
+
node,
|
|
3383
|
+
)
|
|
3384
|
+
if names:
|
|
3385
|
+
return TpyDelVar(names, loc=loc)
|
|
3386
|
+
if attrs:
|
|
3387
|
+
return TpyDelAttr(attrs, loc=loc)
|
|
3388
|
+
return TpyDelItem(subscripts, loc=loc)
|
|
3389
|
+
|
|
3390
|
+
def _parse_match(self, node: ast.Match, loc: SourceLocation | None) -> TpyMatch:
|
|
3391
|
+
"""Parse a match/case statement."""
|
|
3392
|
+
subject = self._parse_expr(node.subject)
|
|
3393
|
+
cases: list[TpyMatchCase] = []
|
|
3394
|
+
for case in node.cases:
|
|
3395
|
+
pattern = self._parse_pattern(case.pattern)
|
|
3396
|
+
guard = self._parse_expr(case.guard) if case.guard else None
|
|
3397
|
+
body = self._parse_body(case.body)
|
|
3398
|
+
# match_case nodes lack lineno; use the pattern's location instead
|
|
3399
|
+
cases.append(TpyMatchCase(pattern, guard, body, loc=self._loc(case.pattern)))
|
|
3400
|
+
return TpyMatch(subject, cases, loc=loc)
|
|
3401
|
+
|
|
3402
|
+
def _parse_pattern(self, node: ast.pattern) -> TpyPattern:
|
|
3403
|
+
"""Parse a match/case pattern."""
|
|
3404
|
+
loc = self._loc(node)
|
|
3405
|
+
|
|
3406
|
+
if isinstance(node, ast.MatchAs):
|
|
3407
|
+
if node.pattern is None and node.name is None:
|
|
3408
|
+
return TpyWildcardPattern(loc=loc)
|
|
3409
|
+
if node.pattern is None and node.name is not None:
|
|
3410
|
+
return TpyCapturePattern(node.name, loc=loc)
|
|
3411
|
+
if node.pattern is not None and node.name is not None:
|
|
3412
|
+
inner = self._parse_pattern(node.pattern)
|
|
3413
|
+
return TpyAsPattern(inner, node.name, loc=loc)
|
|
3414
|
+
|
|
3415
|
+
elif isinstance(node, ast.MatchClass):
|
|
3416
|
+
cls = self._parse_expr(node.cls)
|
|
3417
|
+
positional = [self._parse_pattern(p) for p in node.patterns]
|
|
3418
|
+
keywords = [
|
|
3419
|
+
(attr, self._parse_pattern(pat))
|
|
3420
|
+
for attr, pat in zip(node.kwd_attrs, node.kwd_patterns)
|
|
3421
|
+
]
|
|
3422
|
+
return TpyClassPattern(cls, positional, keywords, loc=loc)
|
|
3423
|
+
|
|
3424
|
+
elif isinstance(node, ast.MatchValue):
|
|
3425
|
+
if isinstance(node.value, ast.Constant):
|
|
3426
|
+
return TpyLiteralPattern(node.value.value, loc=loc)
|
|
3427
|
+
if isinstance(node.value, ast.Attribute):
|
|
3428
|
+
expr = self._parse_expr(node.value)
|
|
3429
|
+
return TpyValuePattern(expr, loc=loc)
|
|
3430
|
+
raise ParseError("Unsupported match value pattern", node)
|
|
3431
|
+
|
|
3432
|
+
elif isinstance(node, ast.MatchSingleton):
|
|
3433
|
+
return TpyLiteralPattern(node.value, loc=loc)
|
|
3434
|
+
|
|
3435
|
+
elif isinstance(node, ast.MatchOr):
|
|
3436
|
+
patterns = [self._parse_pattern(p) for p in node.patterns]
|
|
3437
|
+
return TpyOrPattern(patterns, loc=loc)
|
|
3438
|
+
|
|
3439
|
+
raise ParseError(f"Unsupported pattern: {type(node).__name__}", node)
|
|
3440
|
+
|
|
3441
|
+
def _parse_expr(self, node: ast.expr) -> TpyExpr:
|
|
3442
|
+
"""Parse an expression."""
|
|
3443
|
+
loc = self._loc(node)
|
|
3444
|
+
|
|
3445
|
+
if isinstance(node, ast.Constant):
|
|
3446
|
+
if isinstance(node.value, bool):
|
|
3447
|
+
return TpyBoolLiteral(node.value, loc=loc)
|
|
3448
|
+
elif isinstance(node.value, int):
|
|
3449
|
+
return TpyIntLiteral(node.value, loc=loc)
|
|
3450
|
+
elif isinstance(node.value, float):
|
|
3451
|
+
return TpyFloatLiteral(node.value, loc=loc)
|
|
3452
|
+
elif isinstance(node.value, str):
|
|
3453
|
+
return TpyStrLiteral(node.value, loc=loc)
|
|
3454
|
+
elif isinstance(node.value, bytes):
|
|
3455
|
+
return TpyBytesLiteral(node.value, loc=loc)
|
|
3456
|
+
elif node.value is None:
|
|
3457
|
+
return TpyNoneLiteral(loc=loc)
|
|
3458
|
+
else:
|
|
3459
|
+
raise ParseError(f"Unsupported literal type: {type(node.value).__name__}", node)
|
|
3460
|
+
|
|
3461
|
+
elif isinstance(node, ast.Name):
|
|
3462
|
+
if node.id in self.FORBIDDEN_CONSTRUCTS:
|
|
3463
|
+
raise ParseError(f"'{node.id}' is not allowed in TurboPython", node)
|
|
3464
|
+
return TpyName(node.id, loc=loc)
|
|
3465
|
+
|
|
3466
|
+
elif isinstance(node, ast.BinOp):
|
|
3467
|
+
# Handle list repetition: [x, y, ...] * N -> TpyListRepeat
|
|
3468
|
+
if isinstance(node.op, ast.Mult) and isinstance(node.left, ast.List):
|
|
3469
|
+
elements = [self._parse_expr(e) for e in node.left.elts]
|
|
3470
|
+
# Empty list: [] * N -> [] (collapse to empty array literal)
|
|
3471
|
+
if not elements:
|
|
3472
|
+
return TpyArrayLiteral([], loc=loc)
|
|
3473
|
+
count = self._parse_expr(node.right)
|
|
3474
|
+
return TpyListRepeat(elements, count, loc=loc)
|
|
3475
|
+
left = self._parse_expr(node.left)
|
|
3476
|
+
right = self._parse_expr(node.right)
|
|
3477
|
+
op = self._binop_to_str(node.op)
|
|
3478
|
+
return TpyBinOp(left, op, right, loc=loc)
|
|
3479
|
+
|
|
3480
|
+
elif isinstance(node, ast.Compare):
|
|
3481
|
+
left = self._parse_expr(node.left)
|
|
3482
|
+
if len(node.ops) == 1:
|
|
3483
|
+
right = self._parse_expr(node.comparators[0])
|
|
3484
|
+
op = self._cmpop_to_str(node.ops[0])
|
|
3485
|
+
return TpyBinOp(left, op, right, loc=loc)
|
|
3486
|
+
# Chained comparison: a < b < c
|
|
3487
|
+
ops = []
|
|
3488
|
+
for ast_op in node.ops:
|
|
3489
|
+
op = self._cmpop_to_str(ast_op)
|
|
3490
|
+
if op in ("is", "is not", "in", "not in"):
|
|
3491
|
+
raise ParseError(
|
|
3492
|
+
f"'{op}' cannot be used in chained comparisons", node)
|
|
3493
|
+
ops.append(op)
|
|
3494
|
+
comparators = [self._parse_expr(c) for c in node.comparators]
|
|
3495
|
+
return TpyChainedCompare(left, ops, comparators, loc=loc)
|
|
3496
|
+
|
|
3497
|
+
elif isinstance(node, ast.UnaryOp):
|
|
3498
|
+
operand = self._parse_expr(node.operand)
|
|
3499
|
+
op = self._unaryop_to_str(node.op)
|
|
3500
|
+
return TpyUnaryOp(op, operand, loc=loc)
|
|
3501
|
+
|
|
3502
|
+
elif isinstance(node, ast.BoolOp):
|
|
3503
|
+
# Handle 'and' / 'or' - chain as binary ops
|
|
3504
|
+
op = "&&" if isinstance(node.op, ast.And) else "||"
|
|
3505
|
+
result = self._parse_expr(node.values[0])
|
|
3506
|
+
for val in node.values[1:]:
|
|
3507
|
+
result = TpyBinOp(result, op, self._parse_expr(val), loc=loc)
|
|
3508
|
+
return result
|
|
3509
|
+
|
|
3510
|
+
elif isinstance(node, ast.Call):
|
|
3511
|
+
args = []
|
|
3512
|
+
for a in node.args:
|
|
3513
|
+
if isinstance(a, ast.Starred):
|
|
3514
|
+
args.append(TpyStarUnpack(
|
|
3515
|
+
expr=self._parse_expr(a.value), loc=self._loc(a)))
|
|
3516
|
+
else:
|
|
3517
|
+
args.append(self._parse_expr(a))
|
|
3518
|
+
kwargs = {}
|
|
3519
|
+
double_star_unpack = None
|
|
3520
|
+
|
|
3521
|
+
if node.keywords:
|
|
3522
|
+
for kw in node.keywords:
|
|
3523
|
+
if kw.arg is None:
|
|
3524
|
+
# **expr unpacking
|
|
3525
|
+
if double_star_unpack is not None:
|
|
3526
|
+
raise ParseError("Only one **expr unpacking allowed per call", node)
|
|
3527
|
+
if kwargs:
|
|
3528
|
+
raise ParseError("**expr unpacking cannot be mixed with keyword arguments", node)
|
|
3529
|
+
double_star_unpack = self._parse_expr(kw.value)
|
|
3530
|
+
else:
|
|
3531
|
+
if double_star_unpack is not None:
|
|
3532
|
+
raise ParseError("**expr unpacking cannot be mixed with keyword arguments", node)
|
|
3533
|
+
if kw.arg in kwargs:
|
|
3534
|
+
raise ParseError(f"Keyword argument '{kw.arg}' repeated", node)
|
|
3535
|
+
kwargs[kw.arg] = self._parse_expr(kw.value)
|
|
3536
|
+
|
|
3537
|
+
if isinstance(node.func, ast.Name):
|
|
3538
|
+
call = TpyCall(TpyName(node.func.id, loc=loc), args, kwargs=kwargs,
|
|
3539
|
+
double_star_unpack=double_star_unpack, loc=loc)
|
|
3540
|
+
# Set resolved_import so downstream passes (e.g. the
|
|
3541
|
+
# builder-trace expander) can identify imported callables.
|
|
3542
|
+
self._resolve_call_import(call, node)
|
|
3543
|
+
return call
|
|
3544
|
+
elif isinstance(node.func, ast.Attribute):
|
|
3545
|
+
# ClassName[TypeArgs].method(args) -- static call with explicit class type args
|
|
3546
|
+
if (isinstance(node.func.value, ast.Subscript)
|
|
3547
|
+
and isinstance(node.func.value.value, ast.Name)
|
|
3548
|
+
and self._could_be_type(node.func.value.value.id)):
|
|
3549
|
+
name = node.func.value.value.id
|
|
3550
|
+
type_args, type_args_parse_error = self._try_parse_type_args(node.func.value)
|
|
3551
|
+
if type_args or type_args_parse_error:
|
|
3552
|
+
return TpyMethodCall(
|
|
3553
|
+
TpyName(name, loc=loc), node.func.attr, args,
|
|
3554
|
+
kwargs=kwargs, double_star_unpack=double_star_unpack,
|
|
3555
|
+
type_args=type_args, type_args_parse_error=type_args_parse_error,
|
|
3556
|
+
loc=loc,
|
|
3557
|
+
)
|
|
3558
|
+
# No type args and no error: fall through (e.g., variable[index].method())
|
|
3559
|
+
obj = self._parse_expr(node.func.value)
|
|
3560
|
+
mcall = TpyMethodCall(obj, node.func.attr, args, kwargs=kwargs,
|
|
3561
|
+
double_star_unpack=double_star_unpack, loc=loc)
|
|
3562
|
+
# Set resolved_import for qualified module calls
|
|
3563
|
+
# (``mod.func()``) so downstream passes -- e.g. the
|
|
3564
|
+
# builder-trace expander -- can identify imported
|
|
3565
|
+
# callables without re-walking the import table.
|
|
3566
|
+
self._resolve_call_import(mcall, node)
|
|
3567
|
+
return mcall
|
|
3568
|
+
elif isinstance(node.func, ast.Subscript):
|
|
3569
|
+
# Could be generic type instantiation (Stack[Int32]()) or generic function call (First[Int32](x))
|
|
3570
|
+
# Parse both call_type and type_args - sema decides which applies based on whether
|
|
3571
|
+
# the name is a record or a function
|
|
3572
|
+
if isinstance(node.func.value, ast.Name):
|
|
3573
|
+
name = node.func.value.id
|
|
3574
|
+
# Try to extract type_args for potential generic function call
|
|
3575
|
+
# (for type instantiations like Array[Int32, 8], non-type args are valid
|
|
3576
|
+
# so parse error is stored and sema decides whether to report it)
|
|
3577
|
+
type_args, type_args_parse_error = self._try_parse_type_args(node.func)
|
|
3578
|
+
# Try to parse as a type annotation ref (for type
|
|
3579
|
+
# instantiation like ArrayList[Int32]()). Sema
|
|
3580
|
+
# decides whether to use call_type or type_args
|
|
3581
|
+
# based on whether the name resolves to a type or a
|
|
3582
|
+
# function. Emits TypeRefNode; sema resolves in the
|
|
3583
|
+
# pre-pass body walker. try/except catches
|
|
3584
|
+
# structural parse errors (Callable/Fn/Literal
|
|
3585
|
+
# shape); name-resolution errors defer to sema,
|
|
3586
|
+
# which catches them per-call and falls back to
|
|
3587
|
+
# type_args / subscript_callee just like the
|
|
3588
|
+
# pre-flip behaviour.
|
|
3589
|
+
call_type = None
|
|
3590
|
+
if self._could_be_type(name):
|
|
3591
|
+
try:
|
|
3592
|
+
call_type = self._parse_type_ref(node.func)
|
|
3593
|
+
except ParseError:
|
|
3594
|
+
pass
|
|
3595
|
+
# Try to parse the subscript as an expression so sema can
|
|
3596
|
+
# fall back to expression callee when the name turns out to
|
|
3597
|
+
# be a variable, not a function/type (e.g., fns[0](args),
|
|
3598
|
+
# Handlers[MyType](args)). May fail for known generic types
|
|
3599
|
+
# (list[T], Array[T,N], etc. can't be used as values).
|
|
3600
|
+
# Skip slices -- they can't produce a callable value.
|
|
3601
|
+
subscript_callee = None
|
|
3602
|
+
if not isinstance(node.func.slice, ast.Slice):
|
|
3603
|
+
try:
|
|
3604
|
+
subscript_callee = self._parse_expr(node.func)
|
|
3605
|
+
except ParseError:
|
|
3606
|
+
pass
|
|
3607
|
+
return TpyCall(TpyName(name, loc=loc), args, call_type=call_type, type_args=type_args,
|
|
3608
|
+
type_args_parse_error=type_args_parse_error,
|
|
3609
|
+
subscript_callee=subscript_callee, kwargs=kwargs,
|
|
3610
|
+
double_star_unpack=double_star_unpack, loc=loc)
|
|
3611
|
+
elif isinstance(node.func.value, ast.Attribute):
|
|
3612
|
+
# module.func[T](args) -- method call with explicit type args
|
|
3613
|
+
obj = self._parse_expr(node.func.value.value)
|
|
3614
|
+
method = node.func.value.attr
|
|
3615
|
+
type_args, type_args_parse_error = self._try_parse_type_args(node.func)
|
|
3616
|
+
return TpyMethodCall(obj, method, args, kwargs=kwargs,
|
|
3617
|
+
double_star_unpack=double_star_unpack,
|
|
3618
|
+
type_args=type_args,
|
|
3619
|
+
type_args_parse_error=type_args_parse_error, loc=loc)
|
|
3620
|
+
# Expression callee with subscript: expr[i](args)
|
|
3621
|
+
expr_func = self._parse_expr(node.func)
|
|
3622
|
+
return TpyCall(expr_func, args, kwargs=kwargs,
|
|
3623
|
+
double_star_unpack=double_star_unpack, loc=loc)
|
|
3624
|
+
else:
|
|
3625
|
+
# Expression callee: f()(x), (lambda: fn)()(), etc.
|
|
3626
|
+
expr_func = self._parse_expr(node.func)
|
|
3627
|
+
return TpyCall(expr_func, args, kwargs=kwargs,
|
|
3628
|
+
double_star_unpack=double_star_unpack, loc=loc)
|
|
3629
|
+
|
|
3630
|
+
elif isinstance(node, ast.Attribute):
|
|
3631
|
+
obj = self._parse_expr(node.value)
|
|
3632
|
+
return TpyFieldAccess(obj, node.attr, loc=loc)
|
|
3633
|
+
|
|
3634
|
+
elif isinstance(node, ast.List):
|
|
3635
|
+
elements = [self._parse_expr(elt) for elt in node.elts]
|
|
3636
|
+
return TpyArrayLiteral(elements=elements, loc=loc)
|
|
3637
|
+
|
|
3638
|
+
elif isinstance(node, ast.ListComp):
|
|
3639
|
+
if len(node.generators) != 1:
|
|
3640
|
+
raise ParseError("Nested comprehensions not yet supported", node)
|
|
3641
|
+
gen = node.generators[0]
|
|
3642
|
+
if gen.is_async:
|
|
3643
|
+
raise ParseError("Async comprehensions not yet supported", node)
|
|
3644
|
+
generator = self._parse_comprehension_generator(gen)
|
|
3645
|
+
element_expr = self._parse_expr(node.elt)
|
|
3646
|
+
return TpyListComprehension(element_expr, generator, loc=loc)
|
|
3647
|
+
|
|
3648
|
+
elif isinstance(node, ast.DictComp):
|
|
3649
|
+
if len(node.generators) != 1:
|
|
3650
|
+
raise ParseError("Nested comprehensions not yet supported", node)
|
|
3651
|
+
gen = node.generators[0]
|
|
3652
|
+
if gen.is_async:
|
|
3653
|
+
raise ParseError("Async comprehensions not yet supported", node)
|
|
3654
|
+
generator = self._parse_comprehension_generator(gen)
|
|
3655
|
+
key_expr = self._parse_expr(node.key)
|
|
3656
|
+
value_expr = self._parse_expr(node.value)
|
|
3657
|
+
return TpyDictComprehension(key_expr, value_expr, generator, loc=loc)
|
|
3658
|
+
|
|
3659
|
+
elif isinstance(node, ast.Dict):
|
|
3660
|
+
if any(k is None for k in node.keys):
|
|
3661
|
+
raise ParseError("Dict unpacking (**) is not supported", node)
|
|
3662
|
+
keys = [self._parse_expr(k) for k in node.keys]
|
|
3663
|
+
values = [self._parse_expr(v) for v in node.values]
|
|
3664
|
+
return TpyDictLiteral(keys=keys, values=values, loc=loc)
|
|
3665
|
+
|
|
3666
|
+
elif isinstance(node, ast.SetComp):
|
|
3667
|
+
if len(node.generators) != 1:
|
|
3668
|
+
raise ParseError("Nested comprehensions not yet supported", node)
|
|
3669
|
+
gen = node.generators[0]
|
|
3670
|
+
if gen.is_async:
|
|
3671
|
+
raise ParseError("Async comprehensions not yet supported", node)
|
|
3672
|
+
generator = self._parse_comprehension_generator(gen)
|
|
3673
|
+
element_expr = self._parse_expr(node.elt)
|
|
3674
|
+
return TpySetComprehension(element_expr, generator, loc=loc)
|
|
3675
|
+
|
|
3676
|
+
elif isinstance(node, ast.GeneratorExp):
|
|
3677
|
+
if len(node.generators) != 1:
|
|
3678
|
+
raise ParseError("Nested comprehensions not yet supported", node)
|
|
3679
|
+
gen = node.generators[0]
|
|
3680
|
+
if gen.is_async:
|
|
3681
|
+
raise ParseError("Async comprehensions not yet supported", node)
|
|
3682
|
+
generator = self._parse_comprehension_generator(gen)
|
|
3683
|
+
element_expr = self._parse_expr(node.elt)
|
|
3684
|
+
return TpyGeneratorExpression(element_expr, generator, loc=loc)
|
|
3685
|
+
|
|
3686
|
+
elif isinstance(node, ast.Set):
|
|
3687
|
+
elements = [self._parse_expr(e) for e in node.elts]
|
|
3688
|
+
return TpySetLiteral(elements=elements, loc=loc)
|
|
3689
|
+
|
|
3690
|
+
elif isinstance(node, ast.Subscript):
|
|
3691
|
+
# Subscript can be indexing (values[i]) or type annotation (Array[T, N])
|
|
3692
|
+
# If the value is a name that's a known generic type, it's a type annotation context
|
|
3693
|
+
# Otherwise, it's indexing
|
|
3694
|
+
if isinstance(node.value, ast.Name):
|
|
3695
|
+
name = node.value.id
|
|
3696
|
+
# Type constructors that only exist in type annotations
|
|
3697
|
+
if name in ("Own", "Fn"):
|
|
3698
|
+
raise ParseError(f"Generic type '{name}' cannot be used as a value", node)
|
|
3699
|
+
# Module-defined generic types
|
|
3700
|
+
if find_factory_by_simple_name(name) is not None:
|
|
3701
|
+
raise ParseError(f"Generic type '{name}' cannot be used as a value", node)
|
|
3702
|
+
# Generic types from imported builtin submodules (tpy.mem, etc.)
|
|
3703
|
+
if import_src := self._imports.get_import_source(name):
|
|
3704
|
+
if find_factory_in_module(import_src[1], import_src[0]) is not None:
|
|
3705
|
+
raise ParseError(f"Generic type '{name}' cannot be used as a value", node)
|
|
3706
|
+
obj = self._parse_expr(node.value)
|
|
3707
|
+
if isinstance(node.slice, ast.Slice):
|
|
3708
|
+
sl = node.slice
|
|
3709
|
+
lower = self._parse_expr(sl.lower) if sl.lower is not None else None
|
|
3710
|
+
upper = self._parse_expr(sl.upper) if sl.upper is not None else None
|
|
3711
|
+
step = self._parse_expr(sl.step) if sl.step is not None else None
|
|
3712
|
+
index = TpySlice(lower=lower, upper=upper, step=step, loc=loc)
|
|
3713
|
+
else:
|
|
3714
|
+
index = self._parse_expr(node.slice)
|
|
3715
|
+
return TpySubscript(obj=obj, index=index, loc=loc)
|
|
3716
|
+
|
|
3717
|
+
elif isinstance(node, ast.Tuple):
|
|
3718
|
+
if not node.elts:
|
|
3719
|
+
raise ParseError("Empty tuple literal is not supported", node)
|
|
3720
|
+
elements = [self._parse_expr(elt) for elt in node.elts]
|
|
3721
|
+
return TpyTupleLiteral(elements=elements, loc=loc)
|
|
3722
|
+
|
|
3723
|
+
elif isinstance(node, ast.JoinedStr):
|
|
3724
|
+
return self._parse_fstring(node, loc)
|
|
3725
|
+
|
|
3726
|
+
elif isinstance(node, ast.IfExp):
|
|
3727
|
+
condition = self._parse_expr(node.test)
|
|
3728
|
+
then_expr = self._parse_expr(node.body)
|
|
3729
|
+
else_expr = self._parse_expr(node.orelse)
|
|
3730
|
+
return TpyIfExpr(condition=condition, then_expr=then_expr,
|
|
3731
|
+
else_expr=else_expr, loc=loc)
|
|
3732
|
+
|
|
3733
|
+
elif isinstance(node, ast.NamedExpr):
|
|
3734
|
+
target = node.target.id
|
|
3735
|
+
value = self._parse_expr(node.value)
|
|
3736
|
+
return TpyNamedExpr(target=target, value=value, loc=loc)
|
|
3737
|
+
|
|
3738
|
+
elif isinstance(node, ast.Lambda):
|
|
3739
|
+
if node.args.vararg or node.args.kwarg:
|
|
3740
|
+
raise ParseError("*args and **kwargs are not supported in lambda", node)
|
|
3741
|
+
if node.args.kwonlyargs or node.args.posonlyargs:
|
|
3742
|
+
raise ParseError(
|
|
3743
|
+
"Keyword-only and positional-only parameters are not supported in lambda", node)
|
|
3744
|
+
if any(d is not None for d in node.args.defaults) or node.args.kw_defaults:
|
|
3745
|
+
raise ParseError("Default arguments are not supported in lambda", node)
|
|
3746
|
+
for arg in node.args.args:
|
|
3747
|
+
if arg.annotation is not None:
|
|
3748
|
+
raise ParseError(
|
|
3749
|
+
"Type annotations on lambda parameters are not supported; "
|
|
3750
|
+
"types are inferred from context", node)
|
|
3751
|
+
param_names = [arg.arg for arg in node.args.args]
|
|
3752
|
+
body = self._parse_expr(node.body)
|
|
3753
|
+
return TpyLambda(param_names=param_names, body=body, loc=loc)
|
|
3754
|
+
|
|
3755
|
+
elif isinstance(node, ast.Yield):
|
|
3756
|
+
raise ParseError(
|
|
3757
|
+
"'yield' is only allowed as a statement, not in expression context", node)
|
|
3758
|
+
|
|
3759
|
+
elif isinstance(node, ast.YieldFrom):
|
|
3760
|
+
raise ParseError(
|
|
3761
|
+
"'yield from' is not yet supported in TurboPython", node)
|
|
3762
|
+
|
|
3763
|
+
elif isinstance(node, ast.Await):
|
|
3764
|
+
# Sema rejects await outside an async def body. v1 codegen for
|
|
3765
|
+
# await lowers to a poll-and-park sequence on the operand's
|
|
3766
|
+
# Awaitable[T] conformance (PR 3).
|
|
3767
|
+
value = self._parse_expr(node.value)
|
|
3768
|
+
return TpyAwait(value=value, loc=loc)
|
|
3769
|
+
|
|
3770
|
+
else:
|
|
3771
|
+
raise ParseError(f"Unsupported expression: {type(node).__name__}", node)
|
|
3772
|
+
|
|
3773
|
+
def _parse_fstring(self, node: ast.JoinedStr, loc: SourceLocation) -> TpyFString:
|
|
3774
|
+
"""Parse an f-string (ast.JoinedStr) into a TpyFString node."""
|
|
3775
|
+
parts: list[str | TpyFStringValue] = []
|
|
3776
|
+
for val in node.values:
|
|
3777
|
+
if isinstance(val, ast.Constant) and isinstance(val.value, str):
|
|
3778
|
+
parts.append(val.value)
|
|
3779
|
+
elif isinstance(val, ast.FormattedValue):
|
|
3780
|
+
conv = val.conversion
|
|
3781
|
+
if conv == FSTRING_CONV_ASCII:
|
|
3782
|
+
raise ParseError("f-string !a conversion is not supported", node)
|
|
3783
|
+
expr = self._parse_expr(val.value)
|
|
3784
|
+
fmt_spec: str | None = None
|
|
3785
|
+
if val.format_spec is not None:
|
|
3786
|
+
# format_spec is a JoinedStr; only constant specs are supported
|
|
3787
|
+
spec_parts = []
|
|
3788
|
+
for sp in val.format_spec.values:
|
|
3789
|
+
if isinstance(sp, ast.Constant) and isinstance(sp.value, str):
|
|
3790
|
+
spec_parts.append(sp.value)
|
|
3791
|
+
else:
|
|
3792
|
+
raise ParseError(
|
|
3793
|
+
"Expressions inside f-string format specs are not supported", node
|
|
3794
|
+
)
|
|
3795
|
+
fmt_spec = "".join(spec_parts)
|
|
3796
|
+
err = _validate_fstring_format_spec(fmt_spec)
|
|
3797
|
+
if err is not None:
|
|
3798
|
+
raise ParseError(f"Unsupported f-string format spec: {err}", node)
|
|
3799
|
+
parts.append(TpyFStringValue(expr=expr, conversion=conv, format_spec=fmt_spec))
|
|
3800
|
+
else:
|
|
3801
|
+
raise ParseError(f"Unsupported f-string part: {type(val).__name__}", node)
|
|
3802
|
+
return TpyFString(parts=parts, loc=loc)
|
|
3803
|
+
|
|
3804
|
+
def _binop_to_str(self, op: ast.operator) -> str:
|
|
3805
|
+
"""Convert binary operator to string."""
|
|
3806
|
+
result = _BINOP_TO_STR.get(type(op))
|
|
3807
|
+
if result is None:
|
|
3808
|
+
raise ParseError(f"Unsupported binary operator: {type(op).__name__}")
|
|
3809
|
+
return result
|
|
3810
|
+
|
|
3811
|
+
def _cmpop_to_str(self, op: ast.cmpop) -> str:
|
|
3812
|
+
"""Convert comparison operator to string."""
|
|
3813
|
+
result = _CMPOP_TO_STR.get(type(op))
|
|
3814
|
+
if result is None:
|
|
3815
|
+
raise ParseError(f"Unsupported comparison operator: {type(op).__name__}")
|
|
3816
|
+
return result
|
|
3817
|
+
|
|
3818
|
+
def _unaryop_to_str(self, op: ast.unaryop) -> str:
|
|
3819
|
+
"""Convert unary operator to string."""
|
|
3820
|
+
result = _UNARYOP_TO_STR.get(type(op))
|
|
3821
|
+
if result is None:
|
|
3822
|
+
raise ParseError(f"Unsupported unary operator: {type(op).__name__}")
|
|
3823
|
+
return result
|
|
3824
|
+
|
|
3825
|
+
def _validate_const_default(self, expr: TpyExpr, node: ast.expr) -> None:
|
|
3826
|
+
"""Validate that a default value expression is a compile-time constant."""
|
|
3827
|
+
if isinstance(expr, (TpyIntLiteral, TpyFloatLiteral, TpyBoolLiteral,
|
|
3828
|
+
TpyStrLiteral, TpyBytesLiteral, TpyNoneLiteral,
|
|
3829
|
+
TpyTypeParamConstruct)):
|
|
3830
|
+
return
|
|
3831
|
+
# Bare name reference: must resolve to a module-level Final[T] constant.
|
|
3832
|
+
# Sema validates the binding (parser doesn't see globals or imports yet).
|
|
3833
|
+
if isinstance(expr, TpyName):
|
|
3834
|
+
return
|
|
3835
|
+
if isinstance(expr, TpyUnaryOp) and expr.op == "-":
|
|
3836
|
+
if isinstance(expr.operand, (TpyIntLiteral, TpyFloatLiteral)):
|
|
3837
|
+
return
|
|
3838
|
+
# Int32(5) etc. -- a fixed-int constructor wrapping a literal
|
|
3839
|
+
if isinstance(expr, TpyCall) and expr.func_name in _FIXED_INT_NAMES:
|
|
3840
|
+
if not expr.args:
|
|
3841
|
+
return # Int32() -> 0
|
|
3842
|
+
if len(expr.args) == 1:
|
|
3843
|
+
self._validate_const_default(expr.args[0], node)
|
|
3844
|
+
return
|
|
3845
|
+
raise ParseError(
|
|
3846
|
+
f"Default parameter value must be a constant expression "
|
|
3847
|
+
f"(literal, None, fixed-int constructor like Int32(5), "
|
|
3848
|
+
f"or a Final[T] module constant)", node)
|
|
3849
|
+
|
|
3850
|
+
def _parse_param_defaults(self, node: ast.FunctionDef, params: list,
|
|
3851
|
+
skip_self: bool = False,
|
|
3852
|
+
type_param_scope: dict | None = None,
|
|
3853
|
+
kw_defaults: list | None = None,
|
|
3854
|
+
n_kwonly: int = 0,
|
|
3855
|
+
) -> list['TpyExpr | None']:
|
|
3856
|
+
"""Parse default values from a function definition.
|
|
3857
|
+
|
|
3858
|
+
Returns a list aligned with params: None for params without defaults.
|
|
3859
|
+
Python's ast.arguments.defaults is right-aligned with args, so we
|
|
3860
|
+
left-pad with None. kw_defaults is 1:1 aligned with kwonlyargs.
|
|
3861
|
+
"""
|
|
3862
|
+
n_positional = len(params) - n_kwonly
|
|
3863
|
+
ast_defaults = node.args.defaults
|
|
3864
|
+
|
|
3865
|
+
# In methods, self is skipped from params but still counted in node.args.args
|
|
3866
|
+
num_ast_args = len(node.args.args)
|
|
3867
|
+
# defaults are right-aligned with the full args list
|
|
3868
|
+
num_no_default = num_ast_args - len(ast_defaults) if ast_defaults else num_ast_args
|
|
3869
|
+
|
|
3870
|
+
defaults: list[TpyExpr | None] = []
|
|
3871
|
+
param_offset = 1 if skip_self else 0 # skip self in index mapping
|
|
3872
|
+
for i in range(n_positional):
|
|
3873
|
+
ast_idx = i + param_offset # index into node.args.args
|
|
3874
|
+
default_idx = ast_idx - num_no_default
|
|
3875
|
+
if ast_defaults and default_idx >= 0 and default_idx < len(ast_defaults):
|
|
3876
|
+
expr = self._parse_expr(ast_defaults[default_idx])
|
|
3877
|
+
# Detect T() where T is a type parameter
|
|
3878
|
+
if (isinstance(expr, TpyCall) and not expr.args and not expr.kwargs
|
|
3879
|
+
and type_param_scope and expr.func_name in type_param_scope):
|
|
3880
|
+
expr = TpyTypeParamConstruct(expr.func_name, loc=expr.loc)
|
|
3881
|
+
self._validate_const_default(expr, ast_defaults[default_idx])
|
|
3882
|
+
defaults.append(expr)
|
|
3883
|
+
else:
|
|
3884
|
+
defaults.append(None)
|
|
3885
|
+
|
|
3886
|
+
# Append keyword-only defaults (1:1 aligned with kwonlyargs)
|
|
3887
|
+
if kw_defaults and n_kwonly > 0:
|
|
3888
|
+
for kw_default in kw_defaults:
|
|
3889
|
+
if kw_default is not None:
|
|
3890
|
+
expr = self._parse_expr(kw_default)
|
|
3891
|
+
if (isinstance(expr, TpyCall) and not expr.args and not expr.kwargs
|
|
3892
|
+
and type_param_scope and expr.func_name in type_param_scope):
|
|
3893
|
+
expr = TpyTypeParamConstruct(expr.func_name, loc=expr.loc)
|
|
3894
|
+
self._validate_const_default(expr, kw_default)
|
|
3895
|
+
defaults.append(expr)
|
|
3896
|
+
else:
|
|
3897
|
+
defaults.append(None)
|
|
3898
|
+
elif n_kwonly > 0:
|
|
3899
|
+
defaults.extend([None] * n_kwonly)
|
|
3900
|
+
|
|
3901
|
+
return defaults
|
|
3902
|
+
|
|
3903
|
+
def _get_default_value(self, node: ast.expr) -> str:
|
|
3904
|
+
"""Convert a field default value AST node to a C++ literal string (for FieldInfo.default_value)."""
|
|
3905
|
+
if isinstance(node, ast.Constant):
|
|
3906
|
+
val = node.value
|
|
3907
|
+
if val is None:
|
|
3908
|
+
return "std::nullopt"
|
|
3909
|
+
if isinstance(val, bool):
|
|
3910
|
+
return "true" if val else "false"
|
|
3911
|
+
elif isinstance(val, str):
|
|
3912
|
+
escaped = val.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
|
|
3913
|
+
return f'"{escaped}"'
|
|
3914
|
+
return str(val)
|
|
3915
|
+
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
|
3916
|
+
inner = self._get_default_value(node.operand)
|
|
3917
|
+
return f"-{inner}"
|
|
3918
|
+
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
3919
|
+
# Int32(x) just becomes x in C++
|
|
3920
|
+
if node.func.id in _FIXED_INT_NAMES:
|
|
3921
|
+
if not node.args:
|
|
3922
|
+
return "0"
|
|
3923
|
+
return self._get_default_value(node.args[0])
|
|
3924
|
+
args = ", ".join(str(self._get_default_value(a)) for a in node.args)
|
|
3925
|
+
return f"{node.func.id}({args})"
|
|
3926
|
+
return "0"
|
|
3927
|
+
|
|
3928
|
+
# ---------------------------------------------------------------------------
|
|
3929
|
+
# FragmentParser -- lightweight parser for macro source fragments
|
|
3930
|
+
# ---------------------------------------------------------------------------
|
|
3931
|
+
|
|
3932
|
+
class FragmentParser(Parser):
|
|
3933
|
+
"""Parser for macro-generated source fragments (quote / add_method_from_source).
|
|
3934
|
+
|
|
3935
|
+
Differs from Parser in two ways:
|
|
3936
|
+
- Resolves tpy/typing exports without explicit imports
|
|
3937
|
+
- Returns NominalType for unresolved type names instead of raising
|
|
3938
|
+
"""
|
|
3939
|
+
|
|
3940
|
+
def _resolve_type_name(self, local_name: str) -> tuple[str, str] | None:
|
|
3941
|
+
result = super()._resolve_type_name(local_name)
|
|
3942
|
+
if result:
|
|
3943
|
+
return result
|
|
3944
|
+
if local_name in get_tpy_exports():
|
|
3945
|
+
return ("tpy", local_name)
|
|
3946
|
+
if local_name in get_typing_exports():
|
|
3947
|
+
return ("typing", local_name)
|
|
3948
|
+
return None
|
|
3949
|
+
|
|
3950
|
+
def _raise_unresolved_import_error(
|
|
3951
|
+
self, raw_name: str, node: ast.expr | None = None,
|
|
3952
|
+
*, loc: SourceLocation | None = None,
|
|
3953
|
+
) -> None:
|
|
3954
|
+
# Lenient: unresolved imports are not errors in fragments. Override
|
|
3955
|
+
# signature matches the base shape so TypeResolver's loc-based call
|
|
3956
|
+
# path (`parser._raise_unresolved_import_error(name, loc=ref.loc)`)
|
|
3957
|
+
# is compatible when a fragment's resolve hits an unresolvable name.
|
|
3958
|
+
pass
|
|
3959
|
+
|
|
3960
|
+
def _parse_type_annotation(
|
|
3961
|
+
self, node: ast.expr, type_param_scope: dict | None = None,
|
|
3962
|
+
) -> TpyType:
|
|
3963
|
+
# Walker-then-lenient-resolver. The lenient fallback
|
|
3964
|
+
# (constructing a NominalType placeholder for unresolved names)
|
|
3965
|
+
# lives in `TypeResolver.resolve_lenient`, keeping NominalType
|
|
3966
|
+
# construction out of the parser.
|
|
3967
|
+
if type_param_scope is None:
|
|
3968
|
+
type_param_scope = self._type_param_scope
|
|
3969
|
+
ref = self._parse_type_ref(node, type_param_scope)
|
|
3970
|
+
return self._resolver.resolve_lenient(ref, type_param_scope)
|
|
3971
|
+
|
|
3972
|
+
@classmethod
|
|
3973
|
+
def parse_fragment(
|
|
3974
|
+
cls,
|
|
3975
|
+
source: str,
|
|
3976
|
+
kind: Literal["function", "statements", "expression"] = "function",
|
|
3977
|
+
) -> 'TpyFunction | list[TpyStmt] | TpyExpr':
|
|
3978
|
+
"""Parse a TPy source fragment without full module context.
|
|
3979
|
+
|
|
3980
|
+
Used by macro quote/add_method_from_source APIs. Unresolved type names
|
|
3981
|
+
become NominalType(name) -- sema resolves them later.
|
|
3982
|
+
"""
|
|
3983
|
+
source = textwrap.dedent(source).strip()
|
|
3984
|
+
parser = cls()
|
|
3985
|
+
parser.source_lines = source.splitlines()
|
|
3986
|
+
tree = ast.parse(source)
|
|
3987
|
+
|
|
3988
|
+
if kind == "function":
|
|
3989
|
+
funcs = [n for n in tree.body if isinstance(n, ast.FunctionDef)]
|
|
3990
|
+
if len(funcs) != 1:
|
|
3991
|
+
raise ParseError(
|
|
3992
|
+
f"quote_fun: expected exactly 1 function definition, "
|
|
3993
|
+
f"got {len(funcs)}", tree)
|
|
3994
|
+
func_node = funcs[0]
|
|
3995
|
+
# Detect method: first param named 'self'.
|
|
3996
|
+
args = func_node.args.args
|
|
3997
|
+
has_self = bool(args and args[0].arg == "self")
|
|
3998
|
+
if has_self:
|
|
3999
|
+
# Route through `_parse_method` so method-specific state
|
|
4000
|
+
# (self_annotation, has_auto_readonly_decorator, @property
|
|
4001
|
+
# flags) gets populated -- otherwise sema's expand_methods
|
|
4002
|
+
# sees a bare TpyFunction and skips expansion.
|
|
4003
|
+
# `class_name` is only used in error messages; `<macro>`
|
|
4004
|
+
# is a synthetic placeholder.
|
|
4005
|
+
# `property_names=None` means @x.setter decorators fall
|
|
4006
|
+
# through to the general decorator handler and surface as
|
|
4007
|
+
# an "Unknown decorator" error, matching the prior free-
|
|
4008
|
+
# function routing.
|
|
4009
|
+
func = parser._parse_method(
|
|
4010
|
+
func_node, class_name="<macro>",
|
|
4011
|
+
type_param_scope=None, property_names=None)
|
|
4012
|
+
else:
|
|
4013
|
+
func = parser._parse_function(func_node)
|
|
4014
|
+
# Macro fragment output is consumed before sema's pre-pass runs,
|
|
4015
|
+
# so resolve refs immediately -- matches _parse_nested_def.
|
|
4016
|
+
# Fragments have no enclosing type-param scope; the function's
|
|
4017
|
+
# own type_params are merged in by _finalize_function_refs.
|
|
4018
|
+
parser._finalize_function_refs(func, outer_scope=None)
|
|
4019
|
+
if has_self and isinstance(
|
|
4020
|
+
func.self_annotation, (TpyTypeRef, TpyUnionRef,
|
|
4021
|
+
TpyCallableRef, TpyLiteralRef)):
|
|
4022
|
+
# _finalize_function_refs doesn't touch self_annotation
|
|
4023
|
+
# (non-fragment call sites feed through
|
|
4024
|
+
# `resolve_refs` per-record). Resolve it here
|
|
4025
|
+
# under the method's own type-param scope so expand_methods
|
|
4026
|
+
# sees TpyType.
|
|
4027
|
+
scope: dict[str, TypeParamKind] = {}
|
|
4028
|
+
if func.type_params:
|
|
4029
|
+
kinds = func.type_param_kinds or []
|
|
4030
|
+
for i, name in enumerate(func.type_params):
|
|
4031
|
+
scope[name] = kinds[i] if i < len(kinds) else TypeParamKind.TYPE
|
|
4032
|
+
func.self_annotation = parser._resolve_type_ref_impl(
|
|
4033
|
+
func.self_annotation, scope or None, is_type_arg=True)
|
|
4034
|
+
return func
|
|
4035
|
+
elif kind == "statements":
|
|
4036
|
+
return parser._parse_body(tree.body)
|
|
4037
|
+
elif kind == "expression":
|
|
4038
|
+
if len(tree.body) != 1 or not isinstance(tree.body[0], ast.Expr):
|
|
4039
|
+
raise ParseError(
|
|
4040
|
+
"quote_expr: expected a single expression", tree)
|
|
4041
|
+
return parser._parse_expr(tree.body[0].value)
|
|
4042
|
+
else:
|
|
4043
|
+
raise ValueError(f"Unknown fragment kind: {kind!r}")
|