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/sema/analyzer.py
ADDED
|
@@ -0,0 +1,3625 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Semantic Analyzer
|
|
3
|
+
|
|
4
|
+
Main orchestrator that wires all components together.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from dataclasses import replace as dc_replace
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from ..typesys import (
|
|
12
|
+
TpyType, TypeRegistry, NominalType, AliasRef, UnionType, FinalType, STR, LiteralType, VoidType, VOID,
|
|
13
|
+
NoneType, INT32, ReadonlyType, unwrap_readonly, unwrap_optional_own, OwnType, OptionalType, RecordInfo, FieldInfo,
|
|
14
|
+
RecursiveUnionInfo, RecursiveAliasInstanceType,
|
|
15
|
+
FunctionInfo, ParamInfo, MethodSignature, is_any_str_type, BIGINT, FLOAT,
|
|
16
|
+
make_ref, unwrap_ref_type, RefType, TypeParamKind, TypeParamRef, TupleType, PtrType,
|
|
17
|
+
TypeAliasInfo,
|
|
18
|
+
is_integer_type, is_void_like_type,
|
|
19
|
+
span_as_const, span_is_readonly,
|
|
20
|
+
_contains_self_reference,
|
|
21
|
+
contains_type_param,
|
|
22
|
+
del_suppresses_default_ctor,
|
|
23
|
+
)
|
|
24
|
+
from ..type_def_registry import is_span
|
|
25
|
+
from ..compilation_context import get_current_compiler
|
|
26
|
+
from ..namespace import Namespace, NameBinding, BindingKind
|
|
27
|
+
from ..parse import TpyModule, TpyRecord, TpyFunction, TpyExpr, TpyStmt, TpyVarDecl, is_docstring, is_super_del_call, is_base_init_call, ParseError
|
|
28
|
+
from ..parse.nodes import RecordLinkage
|
|
29
|
+
from .registration import build_record_self_type, _vararg_span_type
|
|
30
|
+
from ..parse.nodes import (
|
|
31
|
+
TpyStrLiteral, TpyAssign, TpyIf, TpyWhile, TpyForEach, TpyFieldAccess, TpyName, TpyCall,
|
|
32
|
+
TpyMethodCall, TpyExprStmt, TpyRaise, TpyTry, TpyMatch, TpyNestedDef,
|
|
33
|
+
expr_contains_self_method_call,
|
|
34
|
+
)
|
|
35
|
+
from .expressions import _collect_body_name_refs
|
|
36
|
+
|
|
37
|
+
from ..diagnostics import Scope, Diagnostic, SemanticError
|
|
38
|
+
from .context import SemanticContext, RecordContext, MODULE_INIT_CONTEXT
|
|
39
|
+
from .type_ops import TypeOperations
|
|
40
|
+
from .operators import OperatorResolver
|
|
41
|
+
from .compatibility import TypeCompatibility
|
|
42
|
+
from .list_literals import IterableHelper
|
|
43
|
+
from .local_deduction import LocalTypeDeduction
|
|
44
|
+
from .protocols import ProtocolChecker
|
|
45
|
+
from .registration import TypeRegistrar
|
|
46
|
+
from .narrowing import NarrowingTracker
|
|
47
|
+
from .expressions import ExpressionAnalyzer
|
|
48
|
+
from .calls import CallAnalyzer
|
|
49
|
+
from .methods import MethodAnalyzer
|
|
50
|
+
from .statements import StatementAnalyzer
|
|
51
|
+
|
|
52
|
+
from ..prescan import ScanResult, scan_reassigned_vars
|
|
53
|
+
from ..liveness import analyze_last_uses
|
|
54
|
+
from .mutation_propagation import propagate_mutation_facts, infer_method_const
|
|
55
|
+
from tpyc import modules as builtin_modules
|
|
56
|
+
from ..cycle_detection import detect_type_cycles
|
|
57
|
+
from ..parse import SourceLocation, is_parser_keyword
|
|
58
|
+
from ..type_def_registry import (
|
|
59
|
+
is_str_type, is_str_view_type, enum_info_of,
|
|
60
|
+
is_array, is_enum_type, is_list, is_dict, is_set, is_span,
|
|
61
|
+
)
|
|
62
|
+
from ..parse.resolve_refs import (
|
|
63
|
+
_walk_body, _merged_method_scope, _record_scope,
|
|
64
|
+
promote_bare_nominals,
|
|
65
|
+
)
|
|
66
|
+
from .macros import _promote_method_signature
|
|
67
|
+
from .builder_trace import BuilderTraceExpander
|
|
68
|
+
from ..symbol_binding import (
|
|
69
|
+
SymbolKind, install_binding, lookup_imported, protocol_kind_for,
|
|
70
|
+
is_macro_kind, is_kind, walk_attribute_chain, resolve_definer,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _body_has_raise(stmts: list[TpyStmt], exception_type: str) -> bool:
|
|
77
|
+
"""Return True if any path in *stmts* contains ``raise <exception_type>``.
|
|
78
|
+
|
|
79
|
+
Called after body analysis, so TpyRaise.exception_type is already
|
|
80
|
+
qualified (e.g. 'builtins.StopIteration').
|
|
81
|
+
"""
|
|
82
|
+
for stmt in stmts:
|
|
83
|
+
if isinstance(stmt, TpyRaise) and stmt.exception_type == exception_type:
|
|
84
|
+
return True
|
|
85
|
+
if isinstance(stmt, TpyIf):
|
|
86
|
+
if _body_has_raise(stmt.then_body, exception_type) or _body_has_raise(stmt.else_body, exception_type):
|
|
87
|
+
return True
|
|
88
|
+
elif isinstance(stmt, (TpyWhile, TpyForEach)):
|
|
89
|
+
if _body_has_raise(stmt.body, exception_type) or _body_has_raise(stmt.orelse, exception_type):
|
|
90
|
+
return True
|
|
91
|
+
elif isinstance(stmt, TpyTry):
|
|
92
|
+
if (_body_has_raise(stmt.try_body, exception_type)
|
|
93
|
+
or _body_has_raise(stmt.else_body, exception_type)
|
|
94
|
+
or _body_has_raise(stmt.finally_body, exception_type)):
|
|
95
|
+
return True
|
|
96
|
+
for handler in stmt.handlers:
|
|
97
|
+
if _body_has_raise(handler.body, exception_type):
|
|
98
|
+
return True
|
|
99
|
+
elif isinstance(stmt, TpyMatch):
|
|
100
|
+
for case in stmt.cases:
|
|
101
|
+
if _body_has_raise(case.body, exception_type):
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SemanticAnalyzer:
|
|
107
|
+
"""Semantic analyzer for TurboPython."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, default_int_type: TpyType = INT32,
|
|
110
|
+
shared_modules: 'dict[str, object] | None' = None):
|
|
111
|
+
# Create shared context. `shared_modules`, when provided, gives
|
|
112
|
+
# this analyzer's TypeRegistry the workspace-wide ModuleInfo dict
|
|
113
|
+
# owned by the Compiler -- every analyzer in one compilation
|
|
114
|
+
# reads/writes the same dict, so cross-module qname lookups
|
|
115
|
+
# (`get_record_for_type`, recursive-union detection, builder-trace
|
|
116
|
+
# type rendering) see every peer module's contribution. Per-module
|
|
117
|
+
# short-name bindings (records / functions / protocols /
|
|
118
|
+
# type_aliases / enums) stay strictly per-analyzer; only the
|
|
119
|
+
# `modules` surface is shared. None preserves the legacy
|
|
120
|
+
# per-analyzer behavior (used by REPL, tests, ad-hoc callers).
|
|
121
|
+
self.ctx = SemanticContext(
|
|
122
|
+
registry=TypeRegistry(shared_modules=shared_modules),
|
|
123
|
+
global_scope=Scope(),
|
|
124
|
+
default_int_type=default_int_type,
|
|
125
|
+
builtins_ns=Namespace(),
|
|
126
|
+
macro_ns=None, # Set below
|
|
127
|
+
global_ns=None, # Set below
|
|
128
|
+
)
|
|
129
|
+
# Chain: global_ns -> macro_ns -> builtins_ns
|
|
130
|
+
self.ctx.macro_ns = Namespace(parent=self.ctx.builtins_ns)
|
|
131
|
+
self.ctx.global_ns = Namespace(parent=self.ctx.macro_ns)
|
|
132
|
+
|
|
133
|
+
# Layer 1: No dependencies on other analyzers
|
|
134
|
+
self.type_ops = TypeOperations(self.ctx)
|
|
135
|
+
self.compat = TypeCompatibility(self.ctx)
|
|
136
|
+
self.iterable = IterableHelper(self.ctx)
|
|
137
|
+
self.deduction = LocalTypeDeduction(self.ctx, self.compat)
|
|
138
|
+
|
|
139
|
+
# Layer 2: Depends on type_ops
|
|
140
|
+
self.protocols = ProtocolChecker(self.ctx, self.type_ops)
|
|
141
|
+
self.registrar = TypeRegistrar(self.ctx, self.type_ops, self.protocols)
|
|
142
|
+
self.operators = OperatorResolver(self.ctx, self.type_ops, self.protocols)
|
|
143
|
+
|
|
144
|
+
# Wire up compatibility's deferred dependencies
|
|
145
|
+
self.compat.type_ops = self.type_ops
|
|
146
|
+
self.compat.protocols = self.protocols
|
|
147
|
+
self.compat.deduction = self.deduction
|
|
148
|
+
# validate_hashable_container_elem needs Hashable conformance
|
|
149
|
+
self.type_ops.protocols = self.protocols
|
|
150
|
+
|
|
151
|
+
# Narrowing tracker (depends on type_ops, protocols)
|
|
152
|
+
self.narrowing = NarrowingTracker(self.ctx, self.type_ops, self.protocols)
|
|
153
|
+
|
|
154
|
+
# Layer 3: Analyzers ordered by dependencies
|
|
155
|
+
# Cycle: expr <-> calls <-> methods. Break by creating calls/methods
|
|
156
|
+
# first, then expr (which takes them), then setting back-references.
|
|
157
|
+
self.calls = CallAnalyzer(
|
|
158
|
+
self.ctx, self.type_ops, self.protocols, self.compat, self.deduction
|
|
159
|
+
)
|
|
160
|
+
self.methods = MethodAnalyzer(
|
|
161
|
+
self.ctx, self.type_ops, self.protocols, self.compat, self.deduction
|
|
162
|
+
)
|
|
163
|
+
self.expr = ExpressionAnalyzer(
|
|
164
|
+
self.ctx, self.type_ops, self.operators, self.protocols, self.compat,
|
|
165
|
+
self.narrowing, self.calls, self.methods,
|
|
166
|
+
)
|
|
167
|
+
# Complete the back-references
|
|
168
|
+
self.calls.expr = self.expr
|
|
169
|
+
self.calls.methods = self.methods
|
|
170
|
+
self.methods.expr = self.expr
|
|
171
|
+
self.methods.calls = self.calls
|
|
172
|
+
self.compat.methods = self.methods
|
|
173
|
+
|
|
174
|
+
# stmts and match: one-way deps, passed through constructors
|
|
175
|
+
self.stmts = StatementAnalyzer(
|
|
176
|
+
self.ctx, self.type_ops, self.compat, self.deduction, self.iterable,
|
|
177
|
+
self.protocols, self.narrowing, self.expr,
|
|
178
|
+
)
|
|
179
|
+
self.expr.set_scopes(self.stmts.scopes)
|
|
180
|
+
|
|
181
|
+
# Per-function/method pre-scan results (shared with codegen)
|
|
182
|
+
self.function_scan_results: dict[int, ScanResult] = {}
|
|
183
|
+
self.top_level_scan_result: ScanResult | None = None
|
|
184
|
+
|
|
185
|
+
# Per-function/method hoisted vars (scope escape phase 2)
|
|
186
|
+
self.function_hoisted_vars: dict[int, set[str]] = {}
|
|
187
|
+
self.top_level_hoisted_vars: set[str] = set()
|
|
188
|
+
|
|
189
|
+
# Per-function/method move-through vars (lvalue alias promoted to rvalue)
|
|
190
|
+
self.function_move_through_vars: dict[int, set[str]] = {}
|
|
191
|
+
self.top_level_move_through_vars: set[str] = set()
|
|
192
|
+
|
|
193
|
+
# Per-function movable locals (owned, not hoisted/loop/lvalue-reassigned)
|
|
194
|
+
self.function_movable_locals: dict[int, set[str]] = {}
|
|
195
|
+
|
|
196
|
+
# Per-function `global x` declarations (for codegen)
|
|
197
|
+
self.function_global_decls: dict[int, set[str]] = {}
|
|
198
|
+
|
|
199
|
+
# Branch-declared vars that need pre-declaration before if-statements
|
|
200
|
+
self.if_branch_decls: dict[int, dict[str, TpyType]] = {}
|
|
201
|
+
|
|
202
|
+
# @overload dispatch groups: implementation func id -> list of stub TpyFunctions
|
|
203
|
+
# Used by codegen to emit per-overload specialized C++ functions.
|
|
204
|
+
self.overload_groups: dict[int, list[TpyFunction]] = {}
|
|
205
|
+
|
|
206
|
+
# Convenience aliases for public API
|
|
207
|
+
self.registry = self.ctx.registry
|
|
208
|
+
self.global_scope = self.ctx.global_scope
|
|
209
|
+
self.diagnostics = self.ctx.diagnostics
|
|
210
|
+
|
|
211
|
+
# Populate builtins_ns with Python builtins (always available without import)
|
|
212
|
+
# These are like CPython's builtins module - int, str, list, len, print, etc.
|
|
213
|
+
self._register_python_builtins()
|
|
214
|
+
|
|
215
|
+
# Public API compatibility properties
|
|
216
|
+
@property
|
|
217
|
+
def current_scope(self) -> Optional[Scope]:
|
|
218
|
+
return self.ctx.func.current_scope
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def current_function(self) -> Optional[TpyFunction]:
|
|
222
|
+
return self.ctx.func.current_function
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def expr_types(self) -> dict[int, TpyType]:
|
|
226
|
+
return self.ctx.expr_types
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def var_types(self) -> dict[int, TpyType]:
|
|
230
|
+
return self.ctx.var_types
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def imports(self) -> dict:
|
|
234
|
+
return self.ctx.imports
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def imported_names(self) -> dict:
|
|
238
|
+
return self.ctx.imported_names
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def list_literals(self) -> dict:
|
|
242
|
+
return self.ctx.list_literals
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def global_ns(self):
|
|
246
|
+
return self.ctx.global_ns
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def builtins_ns(self):
|
|
250
|
+
return self.ctx.builtins_ns
|
|
251
|
+
|
|
252
|
+
def _error(self, message: str, node: TpyExpr | TpyStmt | None = None) -> SemanticError:
|
|
253
|
+
"""Create a SemanticError with location from a node."""
|
|
254
|
+
return self.ctx.error(message, node)
|
|
255
|
+
|
|
256
|
+
def _warning(self, message: str, node: TpyExpr | TpyStmt | TpyRecord | None = None) -> None:
|
|
257
|
+
"""Record a warning diagnostic."""
|
|
258
|
+
self.ctx.warning(message, node)
|
|
259
|
+
|
|
260
|
+
def _register_python_builtins(self) -> None:
|
|
261
|
+
"""Register Python builtins in builtins_ns (always available without import).
|
|
262
|
+
|
|
263
|
+
This mirrors CPython's builtins module. Names like int, str, len, print
|
|
264
|
+
are available in every scope without explicit import.
|
|
265
|
+
"""
|
|
266
|
+
# Python builtin types (as constructors)
|
|
267
|
+
# These are registered as IMPORTED_NAME from "builtins" module
|
|
268
|
+
# Generic types (list) are also registered - they're handled by generic type
|
|
269
|
+
# inference code but need namespace entry to distinguish from tpy types
|
|
270
|
+
python_builtin_types = ["int", "str", "bool", "float", "bytes", "bytearray",
|
|
271
|
+
"list", "dict", "set",
|
|
272
|
+
"slice", "super",
|
|
273
|
+
"BaseException", "Exception", "StopIteration",
|
|
274
|
+
"TextIO"]
|
|
275
|
+
for name in python_builtin_types:
|
|
276
|
+
self.ctx.builtins_ns.bind_imported_name(name, "builtins", name)
|
|
277
|
+
|
|
278
|
+
# Python builtin functions -- available without import (like CPython).
|
|
279
|
+
# Registered in both builtins_ns (for sema lookup) and imported_names
|
|
280
|
+
# (for codegen to resolve to the lib/tpy/builtins.py module functions).
|
|
281
|
+
python_builtin_functions = ["len", "repr", "hash", "chr", "ord", "abs",
|
|
282
|
+
"min", "max", "pow", "round", "divmod",
|
|
283
|
+
"next", "print", "input", "range", "enumerate",
|
|
284
|
+
"zip", "isinstance", "iter",
|
|
285
|
+
"all", "any", "sum", "sorted",
|
|
286
|
+
"bin", "hex", "oct", "reversed",
|
|
287
|
+
"map", "filter",
|
|
288
|
+
"open", "getattr", "setattr", "delattr", "hasattr"]
|
|
289
|
+
for name in python_builtin_functions:
|
|
290
|
+
self.ctx.builtins_ns.bind_imported_name(name, "builtins", name)
|
|
291
|
+
self.ctx.imported_names[name] = ("builtins", name)
|
|
292
|
+
|
|
293
|
+
# Sub-phase indices used by the phase-counter assertion. The five
|
|
294
|
+
# public methods below must be called in this exact order; calling
|
|
295
|
+
# them out of order raises AssertionError. Compiler still drives
|
|
296
|
+
# all five through `analyze()` today; future phases (Phase 3+) will
|
|
297
|
+
# call individual sub-phases workspace-wide.
|
|
298
|
+
_PHASE_NONE = 0
|
|
299
|
+
_PHASE_BIND_IMPORTS = 1
|
|
300
|
+
_PHASE_REGISTER_RECORDS_AND_PROTOCOLS = 2
|
|
301
|
+
_PHASE_REGISTER_SIGNATURES = 3
|
|
302
|
+
_PHASE_ANALYZE_BODIES = 4
|
|
303
|
+
_PHASE_PHASE2_FIXPOINT = 5
|
|
304
|
+
|
|
305
|
+
def _advance_phase(self, expected_prev: int, completed: int) -> None:
|
|
306
|
+
"""Phase-counter guard. `expected_prev` is the phase that must
|
|
307
|
+
have just completed; `completed` is the phase the caller is
|
|
308
|
+
finishing now. Raises AssertionError on out-of-order calls.
|
|
309
|
+
"""
|
|
310
|
+
current = getattr(self, "_completed_phase", self._PHASE_NONE)
|
|
311
|
+
assert current == expected_prev, (
|
|
312
|
+
f"sub-phase ordering violated: expected previous phase "
|
|
313
|
+
f"{expected_prev}, got {current} (trying to complete "
|
|
314
|
+
f"{completed})"
|
|
315
|
+
)
|
|
316
|
+
self._completed_phase = completed
|
|
317
|
+
|
|
318
|
+
def analyze(self, module: TpyModule, module_name: str = "__main__",
|
|
319
|
+
cpp_module_name: str | None = None) -> None:
|
|
320
|
+
"""Analyze a module for semantic correctness.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
module: The parsed module AST.
|
|
324
|
+
module_name: Name of this module ("__main__" for entry point).
|
|
325
|
+
cpp_module_name: File-derived structural name used by codegen
|
|
326
|
+
for namespace identity. Defaults to `module_name`; callers
|
|
327
|
+
analyzing an entry-point module should pass the file name
|
|
328
|
+
so `union_wrapper_index.origin` matches what codegen
|
|
329
|
+
compares against. See `bind_imports` for details.
|
|
330
|
+
|
|
331
|
+
Note: User module dependencies should be registered in registry.modules
|
|
332
|
+
before calling this method (via register_module).
|
|
333
|
+
|
|
334
|
+
This drives the five public sub-phases in order. Workspace-wide
|
|
335
|
+
callers (Phase 3+) will eventually call the sub-phases directly
|
|
336
|
+
across all modules; for now `analyze()` is a thin wrapper that
|
|
337
|
+
preserves the legacy per-module-with-publishing pipeline.
|
|
338
|
+
"""
|
|
339
|
+
self.bind_imports(module, module_name, cpp_module_name=cpp_module_name)
|
|
340
|
+
self.register_records_and_protocols(module)
|
|
341
|
+
self.register_signatures(module)
|
|
342
|
+
self.analyze_bodies(module)
|
|
343
|
+
self.run_phase2_fixpoint(module)
|
|
344
|
+
|
|
345
|
+
def bind_imports(self, module: TpyModule, module_name: str = "__main__",
|
|
346
|
+
cpp_module_name: str | None = None) -> None:
|
|
347
|
+
"""Sub-phase 1: set module context, bind imports + bare modules.
|
|
348
|
+
|
|
349
|
+
Establishes `ctx.module_name`, `ctx.parser_resolver`, the imports
|
|
350
|
+
dict, star-import registrations, tpy type-alias bindings, and
|
|
351
|
+
bare-module bindings. Macro registries and resolved type refs
|
|
352
|
+
are expected to already be in place (compiler-driven).
|
|
353
|
+
|
|
354
|
+
`module_name` is the runtime/`__name__` flavor ("__main__" for
|
|
355
|
+
the entry point). `cpp_module_name` (when given) is the
|
|
356
|
+
file-derived structural name that codegen uses for namespace
|
|
357
|
+
identity -- needed by `_register_union_wrappers` so the
|
|
358
|
+
wrapper-index origin matches what codegen compares against.
|
|
359
|
+
Defaults to `module_name` for non-entry callers that already
|
|
360
|
+
pass the structural name.
|
|
361
|
+
"""
|
|
362
|
+
# Set module context
|
|
363
|
+
self.ctx.module_name = module_name
|
|
364
|
+
self.ctx.cpp_module_name = cpp_module_name or module_name
|
|
365
|
+
self.ctx.module_cpp_namespace = getattr(module.directives, 'cpp_namespace', None) if hasattr(module, 'directives') else None
|
|
366
|
+
# Builder-trace expansion (phase 7) needs to splice synthesized
|
|
367
|
+
# records/functions into the module while bodies are being analyzed.
|
|
368
|
+
self._module = module
|
|
369
|
+
# Module resolver -- consumed by `_infer_field_type_from_default`
|
|
370
|
+
# (same-module record lookup) and the macro post-resolve step in
|
|
371
|
+
# `register_record` (body TypeRefNodes on macro-added methods).
|
|
372
|
+
# See `SemanticContext.parser_resolver` docstring.
|
|
373
|
+
self.ctx.parser_resolver = module.resolver
|
|
374
|
+
|
|
375
|
+
# Convert parse warnings to diagnostics
|
|
376
|
+
for warning in module.parse_warnings:
|
|
377
|
+
self.ctx.warning_from_loc(warning.message, warning.loc)
|
|
378
|
+
|
|
379
|
+
# Process imports
|
|
380
|
+
self.ctx.imports = module.imports
|
|
381
|
+
self.ctx.bare_module_imports = module.bare_module_imports
|
|
382
|
+
if module.tpy_star_import:
|
|
383
|
+
self.registrar.register_tpy_star_import()
|
|
384
|
+
self._register_all_tpy_type_aliases()
|
|
385
|
+
for import_module_name, names in self.ctx.imports.items():
|
|
386
|
+
if isinstance(names, set):
|
|
387
|
+
if module.tpy_star_import and import_module_name == "tpy":
|
|
388
|
+
continue # already fully registered above
|
|
389
|
+
# "from X import Y" or "from X import Y as Z"
|
|
390
|
+
# Stored as (original_name, local_name) tuples to support aliases
|
|
391
|
+
is_star = import_module_name in module.star_imports
|
|
392
|
+
for original_name, local_name in names:
|
|
393
|
+
is_user_module = import_module_name in module.user_module_imports
|
|
394
|
+
# Generic IMPORTED_NAME goes in first so the
|
|
395
|
+
# kind-specific binding installed by
|
|
396
|
+
# `_register_user_module_import` (bind_variable /
|
|
397
|
+
# bind_enum / ...) lands on top for callers that
|
|
398
|
+
# consult the namespace by kind.
|
|
399
|
+
self.ctx.imported_names[local_name] = (import_module_name, original_name)
|
|
400
|
+
self.ctx.global_ns.bind_imported_name(local_name, import_module_name, original_name)
|
|
401
|
+
|
|
402
|
+
if is_star and is_user_module:
|
|
403
|
+
if not self._register_user_module_import(
|
|
404
|
+
import_module_name, original_name, local_name,
|
|
405
|
+
from_star_import=True):
|
|
406
|
+
# Re-resolve a name absent from this source's
|
|
407
|
+
# exports against any module that does export
|
|
408
|
+
# it (typical case: `Int32` star-imported via
|
|
409
|
+
# `from utils import *` where utils itself
|
|
410
|
+
# re-imports it from tpy). `_bind_star_reexport`
|
|
411
|
+
# rewrites `imported_names` and the namespace
|
|
412
|
+
# binding to point at the actual definer.
|
|
413
|
+
self._bind_star_reexport(original_name, local_name)
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
if is_user_module:
|
|
417
|
+
self._register_user_module_import(import_module_name, original_name, local_name)
|
|
418
|
+
# Register tpy type aliases from .py stubs (no-op for non-alias names)
|
|
419
|
+
elif import_module_name == "tpy":
|
|
420
|
+
self._register_tpy_type_alias(original_name, local_name)
|
|
421
|
+
# Re-exported module-level variables from tpy/__init__.py
|
|
422
|
+
# need explicit promotion to VARIABLE bindings (the implicit
|
|
423
|
+
# path otherwise leaves them as IMPORTED_NAME, which use
|
|
424
|
+
# sites reject). Records/functions/factories already resolve
|
|
425
|
+
# through other machinery -- leave them alone here.
|
|
426
|
+
self._register_implicit_module_variable(
|
|
427
|
+
import_module_name, original_name, local_name)
|
|
428
|
+
elif names is None:
|
|
429
|
+
# "import X" for special modules (tpy, typing, etc.)
|
|
430
|
+
alias = module.module_aliases.get(import_module_name)
|
|
431
|
+
self.ctx.global_ns.bind_module(import_module_name, alias)
|
|
432
|
+
# Register tpy type aliases for qualified annotation resolution
|
|
433
|
+
# (e.g. import tpy; x: tpy.Float64)
|
|
434
|
+
if import_module_name == "tpy":
|
|
435
|
+
self._register_all_tpy_type_aliases()
|
|
436
|
+
|
|
437
|
+
# Bind modules for bare `import X` statements (user modules)
|
|
438
|
+
for mod_name in module.bare_module_imports:
|
|
439
|
+
alias = module.module_aliases.get(mod_name)
|
|
440
|
+
self.ctx.global_ns.bind_module(mod_name, alias)
|
|
441
|
+
|
|
442
|
+
# TypeRefNodes were already resolved before `analyze()` was
|
|
443
|
+
# called (by `Compiler._resolve_module_refs` between
|
|
444
|
+
# canonicalization and sema). Everything downstream can read
|
|
445
|
+
# TpyType uniformly.
|
|
446
|
+
self._advance_phase(self._PHASE_NONE, self._PHASE_BIND_IMPORTS)
|
|
447
|
+
|
|
448
|
+
def register_records_and_protocols(self, module: TpyModule) -> None:
|
|
449
|
+
"""Sub-phase 2: enums, records (incl. macro application), then
|
|
450
|
+
protocols, inheritance + value-type validation, recursive-union
|
|
451
|
+
detection, and type-alias registration. Everything declaration-
|
|
452
|
+
level except function signatures.
|
|
453
|
+
"""
|
|
454
|
+
# Method-linkage validation (stubs allowed/required per record
|
|
455
|
+
# linkage, @native decorator restrictions) runs here so any
|
|
456
|
+
# base-resolution errors from the resolve phase fire first.
|
|
457
|
+
self._validate_record_method_linkage(module)
|
|
458
|
+
|
|
459
|
+
# Resolve imported type aliases in AST type annotations.
|
|
460
|
+
# The parser creates NominalType("Shape") for imported aliases since it
|
|
461
|
+
# doesn't know about cross-module aliases at parse time. Substitute them
|
|
462
|
+
# with the resolved types before registration/analysis.
|
|
463
|
+
if self.ctx.registry.imported_type_alias_info:
|
|
464
|
+
self._resolve_imported_aliases(module)
|
|
465
|
+
|
|
466
|
+
# First pass: register all enums then records. Method expansion
|
|
467
|
+
# (self-flag derivation, wrapping, cloning, validation) runs
|
|
468
|
+
# inside `register_record` after `_apply_class_macros` so macro-
|
|
469
|
+
# added methods flow through the same pipeline as regular ones.
|
|
470
|
+
# Enums are registered first so record field-type resolution
|
|
471
|
+
# (inside `register_record`) can substitute parser-level
|
|
472
|
+
# `NominalType("Color")` placeholders with the registered enum
|
|
473
|
+
# NominalType that carries `_module_qname`. Records never depend
|
|
474
|
+
# on each other at registration time, and enums are self-
|
|
475
|
+
# contained, so this ordering is safe even for nested enums --
|
|
476
|
+
# nested enum names are already dotted ("Message.Kind") when
|
|
477
|
+
# `register_enum` sees them, so the parent record need not
|
|
478
|
+
# exist in the sema registry yet.
|
|
479
|
+
for enum in module.all_enums():
|
|
480
|
+
self.registrar.register_enum(enum)
|
|
481
|
+
for record in module.all_records():
|
|
482
|
+
self.registrar.register_record(record)
|
|
483
|
+
|
|
484
|
+
# Populate macro_ns with exports from macro dep modules.
|
|
485
|
+
# Macros have run during register_record, so we know which macro
|
|
486
|
+
# modules were used and can import their dependencies.
|
|
487
|
+
self._populate_macro_deps(module)
|
|
488
|
+
|
|
489
|
+
# Promote bare NominalTypes that macros emitted via
|
|
490
|
+
# `types.named(...)` to qname-bearing form now that macro_deps
|
|
491
|
+
# are in ctx.registry. Macros like `@model` reference types
|
|
492
|
+
# (JsonReader, JsonWriter) that main.py doesn't import
|
|
493
|
+
# explicitly; they land in the registry only after
|
|
494
|
+
# `_populate_macro_deps`, so promotion has to run here rather
|
|
495
|
+
# than inside `register_record`.
|
|
496
|
+
self._promote_macro_generated_types(module)
|
|
497
|
+
|
|
498
|
+
# Register protocols (two phases to allow forward references)
|
|
499
|
+
for protocol in module.protocols:
|
|
500
|
+
self.registrar.register_protocol(protocol)
|
|
501
|
+
# Populate ProtocolInfo.transitive_supertypes (single source of
|
|
502
|
+
# truth for subtype queries) before any record-side closure or
|
|
503
|
+
# subtype-using check reads it.
|
|
504
|
+
for protocol in module.protocols:
|
|
505
|
+
self.registrar.finalize_protocol_closure(protocol)
|
|
506
|
+
for protocol in module.protocols:
|
|
507
|
+
self.registrar.validate_protocol_parents(protocol)
|
|
508
|
+
|
|
509
|
+
# Validate inheritance relationships (after all records and protocols
|
|
510
|
+
# are registered). Sets `implemented_protocols` / MRO on each record.
|
|
511
|
+
for record in module.all_records():
|
|
512
|
+
self.registrar.validate_record_inheritance(record)
|
|
513
|
+
|
|
514
|
+
# Re-validate record field types now that protocols are registered AND
|
|
515
|
+
# inheritance is finalized. Optional[@dynamic] / container-element /
|
|
516
|
+
# union-protocol checks rely on protocol_info_of, which only resolves
|
|
517
|
+
# once protocols are in the registry; the Own[Optional[Polymorphic]]
|
|
518
|
+
# check needs implemented_protocols/MRO populated. Runs after
|
|
519
|
+
# inheritance so both prerequisites hold.
|
|
520
|
+
for record in module.all_records():
|
|
521
|
+
self.registrar.validate_record_field_protocols(record)
|
|
522
|
+
|
|
523
|
+
# Validate @error_return(E) on methods (ReturnException markers are now set)
|
|
524
|
+
for record in module.all_records():
|
|
525
|
+
self.registrar.validate_method_error_returns(record)
|
|
526
|
+
|
|
527
|
+
# Validate ValueType fields in a second pass (all ValueType flags are set now)
|
|
528
|
+
for record in module.all_records():
|
|
529
|
+
self.registrar.validate_value_type_fields(record)
|
|
530
|
+
|
|
531
|
+
# Validate default_factory fields conform to Default protocol
|
|
532
|
+
self._validate_factory_defaults(module)
|
|
533
|
+
|
|
534
|
+
# Propagate @nocopy from fields to containing records.
|
|
535
|
+
# Done after inheritance validation so parent types are resolved.
|
|
536
|
+
# Definition order handles transitive propagation naturally.
|
|
537
|
+
self._propagate_nocopy(module)
|
|
538
|
+
|
|
539
|
+
# Validate that every recursive path in a recursive union alias goes
|
|
540
|
+
# through an indirecting container. Runs here -- after record/protocol
|
|
541
|
+
# registration -- so the field-walk in `validate_recursive_union_paths`
|
|
542
|
+
# can resolve same-module RecordInfo / TypeDef entries for user
|
|
543
|
+
# indirecting records (otherwise we'd false-positive on `class
|
|
544
|
+
# MyBox[T]: _ptr: Ptr[T]; type Bad = Lit | MyBox[Bad]`).
|
|
545
|
+
self._validate_recursive_union_paths(module)
|
|
546
|
+
|
|
547
|
+
# Detect mutual recursion cycles and tag recursive union aliases
|
|
548
|
+
self._detect_recursive_unions(module)
|
|
549
|
+
self.ctx.recursive_union_names = module.recursive_union_names
|
|
550
|
+
self._register_union_wrappers(module)
|
|
551
|
+
|
|
552
|
+
# The parser eagerly expands same-module union aliases, so
|
|
553
|
+
# RecursiveAlias | None becomes UnionType(NoneType, member1, member2, ...)
|
|
554
|
+
# instead of OptionalType(NominalType("RecursiveAlias")).
|
|
555
|
+
# Now that recursive aliases are identified, fix up those annotations.
|
|
556
|
+
if module.recursive_union_names:
|
|
557
|
+
self._fix_recursive_optional_annotations(module)
|
|
558
|
+
|
|
559
|
+
# Transfer type aliases from parser to sema registry, validating members
|
|
560
|
+
compiler = get_current_compiler()
|
|
561
|
+
display_names = compiler.union_display_names if compiler is not None else None
|
|
562
|
+
for name, entry in module.type_aliases.items():
|
|
563
|
+
typ, loc, type_params, type_param_kinds = entry
|
|
564
|
+
is_recursive = name in module.recursive_union_names
|
|
565
|
+
self._validate_type_alias_members(name, typ, loc)
|
|
566
|
+
info = TypeAliasInfo(
|
|
567
|
+
body=typ,
|
|
568
|
+
type_params=list(type_params),
|
|
569
|
+
type_param_kinds=list(type_param_kinds),
|
|
570
|
+
loc=loc,
|
|
571
|
+
is_recursive=is_recursive,
|
|
572
|
+
)
|
|
573
|
+
self.ctx.registry.register_type_alias(name, typ, info=info)
|
|
574
|
+
install_binding(
|
|
575
|
+
self.ctx.module_attributes, name,
|
|
576
|
+
SymbolKind.TYPE_ALIAS, typ,
|
|
577
|
+
)
|
|
578
|
+
# Register the defining-module's canonical short name for
|
|
579
|
+
# `UnionType.__str__` so diagnostics print "Shape" instead
|
|
580
|
+
# of "Circle | Rect". First write wins via `setdefault` --
|
|
581
|
+
# the defining module's name takes precedence over any
|
|
582
|
+
# import aliasing in downstream modules. Skip generic aliases:
|
|
583
|
+
# their display name is only meaningful parameterized
|
|
584
|
+
# (`Either[Int32]`), and keying on the unsubstituted members
|
|
585
|
+
# would mislabel an unrelated concrete union of the same shape.
|
|
586
|
+
if (display_names is not None and not type_params
|
|
587
|
+
and isinstance(typ, UnionType)):
|
|
588
|
+
display_names.setdefault(typ.members, name)
|
|
589
|
+
# Convert the parser's AliasRef placeholders for generic recursive
|
|
590
|
+
# aliases (self-refs in bodies + use sites in annotations) into
|
|
591
|
+
# semantic RecursiveAliasInstanceType nodes, now that the alias
|
|
592
|
+
# registry carries each alias's TypeAliasInfo.
|
|
593
|
+
self._finalize_generic_recursive_aliases(module)
|
|
594
|
+
self._advance_phase(
|
|
595
|
+
self._PHASE_BIND_IMPORTS,
|
|
596
|
+
self._PHASE_REGISTER_RECORDS_AND_PROTOCOLS,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def register_signatures(self, module: TpyModule) -> None:
|
|
600
|
+
"""Sub-phase 3: register function signatures with @overload
|
|
601
|
+
grouping, normalize FunctionInfo refs (`make_ref` over params /
|
|
602
|
+
returns now that records/protocols/value-type flags are final),
|
|
603
|
+
inject synthetic `__name__` Final[str] for non-private modules.
|
|
604
|
+
"""
|
|
605
|
+
# Second pass: register all functions (with @overload grouping)
|
|
606
|
+
self._register_functions_with_overloads(module.functions)
|
|
607
|
+
|
|
608
|
+
# Normalize FunctionInfo types: wrap non-value params/returns with Ref[T].
|
|
609
|
+
# Must run after ALL types are registered (records, protocols, value types,
|
|
610
|
+
# AND free functions) so make_ref correctly identifies which types need wrapping.
|
|
611
|
+
self._normalize_function_info_refs()
|
|
612
|
+
|
|
613
|
+
# Inject synthetic __name__: Final[str] before registration so it flows
|
|
614
|
+
# through the same path as user-defined Finals (single source of truth).
|
|
615
|
+
# Skip for private submodules (containing "._") that may share their
|
|
616
|
+
# parent's C++ namespace -- they'd cause duplicate __name__ definitions.
|
|
617
|
+
# This applies to both stdlib (tpy._core._types, tpy._builtins._list) and user private
|
|
618
|
+
# submodules (myapp._internal), which is fine since __name__ is rarely
|
|
619
|
+
# needed in submodules compiled to C++.
|
|
620
|
+
module_name = self.ctx.module_name
|
|
621
|
+
is_private_submodule = "._" in module_name
|
|
622
|
+
if not is_private_submodule:
|
|
623
|
+
name_decl = TpyVarDecl(
|
|
624
|
+
name="__name__",
|
|
625
|
+
type=FinalType(STR),
|
|
626
|
+
init=TpyStrLiteral(value=module_name),
|
|
627
|
+
)
|
|
628
|
+
if module.top_level_stmts is None:
|
|
629
|
+
module.top_level_stmts = []
|
|
630
|
+
module.top_level_stmts.insert(0, name_decl)
|
|
631
|
+
|
|
632
|
+
# Register top-level variable declarations (globals) here, in
|
|
633
|
+
# sub-phase 3, rather than waiting for body sema. The
|
|
634
|
+
# registration only inspects each TpyVarDecl's name + declared
|
|
635
|
+
# type (no init-expression analysis); doing it now lets peer
|
|
636
|
+
# modules' `from X import VAR` resolve at decl-time, which the
|
|
637
|
+
# workspace-wide two-pass sema needs (Phase 5 split).
|
|
638
|
+
self.registrar.register_globals(module.top_level_stmts)
|
|
639
|
+
# Top-level statement analysis also moves into the declaration
|
|
640
|
+
# sub-phases. It populates `global_scope` with names assigned via
|
|
641
|
+
# top-level expressions, including tuple-unpacks
|
|
642
|
+
# (`lo, hi = get_bounds()`) whose types only emerge from
|
|
643
|
+
# analyzing the RHS. Without this, peer modules' `from X import lo`
|
|
644
|
+
# would fail because `lo` would not yet be in
|
|
645
|
+
# `X.exports.variables` when the peer's declarations pass runs.
|
|
646
|
+
# Cross-module function calls in top-level statements resolve
|
|
647
|
+
# against decl-finalized peer ModuleInfos (deps run first in
|
|
648
|
+
# topo order in the declarations pass).
|
|
649
|
+
if module.top_level_stmts:
|
|
650
|
+
self._analyze_top_level(module.top_level_stmts)
|
|
651
|
+
self._advance_phase(
|
|
652
|
+
self._PHASE_REGISTER_RECORDS_AND_PROTOCOLS,
|
|
653
|
+
self._PHASE_REGISTER_SIGNATURES,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
def analyze_bodies(self, module: TpyModule) -> None:
|
|
657
|
+
"""Sub-phase 4: analyze class-constant initializers, builder-trace
|
|
658
|
+
expansion, record method bodies, free function bodies. Top-level
|
|
659
|
+
statement analysis moved into `register_signatures` so peer
|
|
660
|
+
modules' decl pass sees globals defined via tuple-unpack
|
|
661
|
+
assignments.
|
|
662
|
+
"""
|
|
663
|
+
# Fifth pass: analyze class-constant initializers. Runs after
|
|
664
|
+
# `_analyze_top_level` so module-level Final globals are in scope, and
|
|
665
|
+
# before `_analyze_record_methods` so methods see typed constants.
|
|
666
|
+
for record in module.all_records():
|
|
667
|
+
self._analyze_class_constants(record)
|
|
668
|
+
|
|
669
|
+
# Pass 5.5: see `_expand_builder_traces`.
|
|
670
|
+
self._expand_builder_traces(module)
|
|
671
|
+
|
|
672
|
+
# Sixth pass: analyze record methods (including nested records)
|
|
673
|
+
for record in module.all_records():
|
|
674
|
+
self._analyze_record_methods(record)
|
|
675
|
+
|
|
676
|
+
# Seventh pass: analyze function bodies (skip bodyless @overload stubs and @inline)
|
|
677
|
+
for func in module.functions:
|
|
678
|
+
if func.is_inline and not func.is_stub:
|
|
679
|
+
func.skip_codegen = True
|
|
680
|
+
continue
|
|
681
|
+
# Bodied @overload stubs (mode b) carry their own body that needs
|
|
682
|
+
# analysis just like a regular function; bodyless @overload stubs
|
|
683
|
+
# are handled via their trailing implementation.
|
|
684
|
+
if func.is_overload_stub and func.is_stub:
|
|
685
|
+
continue
|
|
686
|
+
self._analyze_function(func)
|
|
687
|
+
self._advance_phase(
|
|
688
|
+
self._PHASE_REGISTER_SIGNATURES,
|
|
689
|
+
self._PHASE_ANALYZE_BODIES,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
def run_phase2_fixpoint(self, module: TpyModule) -> None:
|
|
693
|
+
"""Sub-phase 5: call-graph mutation-fact propagation + readonly
|
|
694
|
+
inference. Borrow-check resolution is deferred to a post-pass
|
|
695
|
+
(`finalize_borrow_checks`) that the Compiler runs after every
|
|
696
|
+
module's propagation has completed so cross-module mutation
|
|
697
|
+
facts are settled before borrow warnings are emitted.
|
|
698
|
+
"""
|
|
699
|
+
self._propagate_mutation_facts()
|
|
700
|
+
self._sync_inferred_const(module)
|
|
701
|
+
self._sync_inferred_vararg_readonly(module)
|
|
702
|
+
self._advance_phase(
|
|
703
|
+
self._PHASE_ANALYZE_BODIES,
|
|
704
|
+
self._PHASE_PHASE2_FIXPOINT,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
def finalize_borrow_checks(self) -> None:
|
|
708
|
+
"""Emit / suppress deferred borrow warnings using fully-propagated
|
|
709
|
+
cross-module facts. Compiler runs this once per analyzer in a
|
|
710
|
+
third workspace-wide pass after every module has completed
|
|
711
|
+
`run_phase2_fixpoint`, guaranteeing that
|
|
712
|
+
`resolve_pending_borrow_checks` reads finalized
|
|
713
|
+
`mutated_params` / `structural_mutated_params` regardless of
|
|
714
|
+
body-sema iteration order. This is what makes the suite pass
|
|
715
|
+
byte-identical when body sema runs in reverse-topo order
|
|
716
|
+
(Phase 6 acceptance criterion).
|
|
717
|
+
"""
|
|
718
|
+
self.calls.resolve_pending_borrow_checks()
|
|
719
|
+
|
|
720
|
+
def _normalize_function_info_refs(self) -> None:
|
|
721
|
+
"""Apply make_ref to all registered FunctionInfo param/return types.
|
|
722
|
+
|
|
723
|
+
Wraps non-value types with RefType so codegen and type inference see
|
|
724
|
+
explicit reference semantics. Called after all types (including value
|
|
725
|
+
type markers) are registered, so make_ref correctly identifies which
|
|
726
|
+
types need wrapping.
|
|
727
|
+
|
|
728
|
+
Stamps `originating_module` on every FunctionInfo registered
|
|
729
|
+
in this module's analyzer. Imported peer FIs already carry
|
|
730
|
+
their defining module's stamp, so we skip when set. Opaque
|
|
731
|
+
FIs (builtin / @builtin_decorator stubs) keep
|
|
732
|
+
`originating_module=None` so mutation propagation treats
|
|
733
|
+
them as unowned and skips them.
|
|
734
|
+
"""
|
|
735
|
+
current_module = self.ctx.module_name
|
|
736
|
+
|
|
737
|
+
def _ref_params(params: list) -> list:
|
|
738
|
+
result = []
|
|
739
|
+
for p in params:
|
|
740
|
+
if isinstance(p, ParamInfo):
|
|
741
|
+
result.append(dc_replace(p, type=make_ref(p.type)))
|
|
742
|
+
else:
|
|
743
|
+
# Legacy tuple (name, type) form
|
|
744
|
+
result.append((p[0], make_ref(p[1])))
|
|
745
|
+
return result
|
|
746
|
+
|
|
747
|
+
for overloads in self.ctx.registry.functions.values():
|
|
748
|
+
for fi in overloads:
|
|
749
|
+
if fi.originating_module is None and not fi.is_builtin_function and not fi.builtin_decorator_key:
|
|
750
|
+
fi.originating_module = current_module
|
|
751
|
+
if isinstance(fi.return_type, RefType):
|
|
752
|
+
continue
|
|
753
|
+
fi.return_type = make_ref(fi.return_type)
|
|
754
|
+
fi.params = _ref_params(fi.params)
|
|
755
|
+
for rec in self.ctx.registry.records.values():
|
|
756
|
+
for method_list in rec.methods.values():
|
|
757
|
+
for fi in method_list:
|
|
758
|
+
if fi.originating_module is None and not fi.is_builtin_function and not fi.builtin_decorator_key:
|
|
759
|
+
# Method's defining module = the record's defining module,
|
|
760
|
+
# not the current module. Records that flow in via cross-
|
|
761
|
+
# module registration carry RecordInfo.module set during
|
|
762
|
+
# their owning module's sema; trust that when present.
|
|
763
|
+
fi.originating_module = rec.module or current_module
|
|
764
|
+
if isinstance(fi.return_type, RefType):
|
|
765
|
+
continue
|
|
766
|
+
fi.return_type = make_ref(fi.return_type)
|
|
767
|
+
fi.params = _ref_params(fi.params)
|
|
768
|
+
|
|
769
|
+
def _propagate_mutation_facts(self) -> None:
|
|
770
|
+
"""Run call-graph mutation-fact propagation, taking advantage of
|
|
771
|
+
the workspace-shared `registry.modules` dict to pull peer
|
|
772
|
+
modules' FunctionInfos in addition to this module's own.
|
|
773
|
+
|
|
774
|
+
Each per-analyzer call walks the workspace once. After the
|
|
775
|
+
first call clears `call_edges` on its propagated FIs, the
|
|
776
|
+
gate (`call_edges is not None`) excludes them from later
|
|
777
|
+
analyzers' collections, so subsequent calls only process
|
|
778
|
+
whatever module just finished body sema -- effectively
|
|
779
|
+
incremental rather than re-doing all modules' work.
|
|
780
|
+
|
|
781
|
+
The `originating_module` distinction from Phase 6 stays
|
|
782
|
+
(gates body-sema-time fact production); cross-module
|
|
783
|
+
mutating calls propagate facts uniformly through the
|
|
784
|
+
workspace call graph.
|
|
785
|
+
"""
|
|
786
|
+
all_fis: list = []
|
|
787
|
+
seen: set[int] = set()
|
|
788
|
+
|
|
789
|
+
def _collect_from(reg_or_mod) -> None:
|
|
790
|
+
for overloads in reg_or_mod.functions.values():
|
|
791
|
+
for fi in overloads:
|
|
792
|
+
if id(fi) in seen:
|
|
793
|
+
continue
|
|
794
|
+
if fi.direct_mutated_params is not None and fi.call_edges is not None:
|
|
795
|
+
seen.add(id(fi))
|
|
796
|
+
all_fis.append(fi)
|
|
797
|
+
for rec in reg_or_mod.records.values():
|
|
798
|
+
for method_overloads in rec.methods.values():
|
|
799
|
+
for fi in method_overloads:
|
|
800
|
+
if id(fi) in seen:
|
|
801
|
+
continue
|
|
802
|
+
if fi.direct_mutated_params is not None and fi.call_edges is not None:
|
|
803
|
+
seen.add(id(fi))
|
|
804
|
+
all_fis.append(fi)
|
|
805
|
+
|
|
806
|
+
_collect_from(self.ctx.registry)
|
|
807
|
+
for mod_info in self.ctx.registry.modules.values():
|
|
808
|
+
if not mod_info.is_builtin:
|
|
809
|
+
_collect_from(mod_info)
|
|
810
|
+
propagate_mutation_facts(all_fis)
|
|
811
|
+
infer_method_const(all_fis)
|
|
812
|
+
|
|
813
|
+
def _sync_inferred_const(self, module: TpyModule) -> None:
|
|
814
|
+
"""Copy inferred is_readonly=True from FunctionInfo back to TpyFunction nodes.
|
|
815
|
+
|
|
816
|
+
infer_method_const() sets FunctionInfo.is_readonly on the registry objects,
|
|
817
|
+
but codegen reads method.is_readonly from the TpyFunction AST nodes.
|
|
818
|
+
This pass syncs the two representations.
|
|
819
|
+
"""
|
|
820
|
+
for record in module.all_records():
|
|
821
|
+
record_info = self.ctx.registry.get_record(record.name)
|
|
822
|
+
if record_info is None:
|
|
823
|
+
continue
|
|
824
|
+
for method in record.methods:
|
|
825
|
+
if method.is_readonly:
|
|
826
|
+
continue # already readonly, no need to sync
|
|
827
|
+
# Phase 1 facts for @overload methods land on overloads[0]'s FI
|
|
828
|
+
# (see comment at get_method() call below). Syncing stub nodes
|
|
829
|
+
# would re-read via get_method() and hit the same FI repeatedly --
|
|
830
|
+
# skip them and let the non-stub TpyFunction node do the sync.
|
|
831
|
+
if method.is_overload_stub:
|
|
832
|
+
continue
|
|
833
|
+
# @auto_readonly mutable clones are paired with a const clone --
|
|
834
|
+
# keep them mutable so the pair generates both overloads correctly.
|
|
835
|
+
if method.is_auto_readonly_mutable_clone:
|
|
836
|
+
continue
|
|
837
|
+
# @readonly(False) is an explicit opt-out -- respect it.
|
|
838
|
+
if method.readonly_opt_out:
|
|
839
|
+
continue
|
|
840
|
+
fi = record_info.get_method(method.name)
|
|
841
|
+
if fi is None or not fi.is_readonly:
|
|
842
|
+
continue
|
|
843
|
+
# For @dynamic protocol overrides, the const-ness of the concrete
|
|
844
|
+
# method must match the virtual base declaration. If the protocol
|
|
845
|
+
# declares the method as non-const, don't infer const here --
|
|
846
|
+
# it would produce a different C++ signature and break the override.
|
|
847
|
+
if self._dynamic_proto_requires_nonconst(record_info, method.name):
|
|
848
|
+
continue
|
|
849
|
+
method.is_readonly = True
|
|
850
|
+
|
|
851
|
+
def _dynamic_proto_requires_nonconst(self, record_info: RecordInfo, method_name: str) -> bool:
|
|
852
|
+
"""Return True if method_name must remain non-const due to a @dynamic protocol override.
|
|
853
|
+
|
|
854
|
+
A @dynamic protocol generates C++ pure virtual methods that concrete implementations
|
|
855
|
+
must override with matching (non-const) signatures. The method may be declared in a
|
|
856
|
+
non-dynamic ancestor, but still ends up in the dynamic vtable.
|
|
857
|
+
|
|
858
|
+
Walks the MRO so a class inheriting a @dynamic protocol transitively (via a
|
|
859
|
+
concrete-class parent) still has its overrides pinned to the protocol's
|
|
860
|
+
const-ness -- otherwise auto-readonly inference would emit a const signature
|
|
861
|
+
that mismatches the virtual slot.
|
|
862
|
+
"""
|
|
863
|
+
visited: set[str] = set()
|
|
864
|
+
for proto_type, _ in self.ctx.registry.iter_dynamic_protocols(record_info):
|
|
865
|
+
# Search this dynamic protocol and ALL its ancestors for method_name,
|
|
866
|
+
# regardless of whether ancestors are themselves dynamic.
|
|
867
|
+
if self._proto_hierarchy_has_nonconst(proto_type.name, method_name, visited):
|
|
868
|
+
return True
|
|
869
|
+
return False
|
|
870
|
+
|
|
871
|
+
def _proto_hierarchy_has_nonconst(self, proto_name: str, method_name: str, visited: set[str]) -> bool:
|
|
872
|
+
"""Search method_name in this protocol and all ancestors (ignoring dynamic flag)."""
|
|
873
|
+
if proto_name in visited:
|
|
874
|
+
return False
|
|
875
|
+
visited.add(proto_name)
|
|
876
|
+
proto_info = self.ctx.registry.scan_by_short_name(proto_name)
|
|
877
|
+
if proto_info is None:
|
|
878
|
+
return False
|
|
879
|
+
for sig in proto_info.methods:
|
|
880
|
+
if sig.name == method_name and not sig.is_readonly:
|
|
881
|
+
return True
|
|
882
|
+
for parent in proto_info.parent_protocols:
|
|
883
|
+
if self._proto_hierarchy_has_nonconst(parent.name, method_name, visited):
|
|
884
|
+
return True
|
|
885
|
+
return False
|
|
886
|
+
|
|
887
|
+
def _sync_inferred_vararg_readonly(self, module: TpyModule) -> None:
|
|
888
|
+
"""Flip vararg slots `Span[T]` -> `Span[readonly[T]]` for bodies that
|
|
889
|
+
don't mutate the vararg pack. Parallels `_sync_inferred_const` for
|
|
890
|
+
methods and the per-index `mutated_params` gate that `decide_param_const`
|
|
891
|
+
consumes for non-vararg ref params -- both extend the same body-mutation
|
|
892
|
+
inference to the vararg case so non-mutating callees collapse to
|
|
893
|
+
`varargs<const T>` and accept const arg sources without caller-side
|
|
894
|
+
marking workarounds. Skip rules mirror `_sync_inferred_const`: explicit
|
|
895
|
+
readonly slot, @native, overload stubs, and @dynamic-protocol overrides
|
|
896
|
+
whose virtual slot is non-const.
|
|
897
|
+
"""
|
|
898
|
+
for func in module.functions:
|
|
899
|
+
if func.is_overload_stub or func.native_function:
|
|
900
|
+
continue
|
|
901
|
+
if func.vararg_name is None:
|
|
902
|
+
continue
|
|
903
|
+
# For @overload groups, mutation facts land on the implementation's
|
|
904
|
+
# FI (last in overload list); see analyzer.py:1142-1143 where
|
|
905
|
+
# Phase 1 writes them. Plain defs have a single entry.
|
|
906
|
+
overloads = self.ctx.registry.get_function(func.name)
|
|
907
|
+
if not overloads:
|
|
908
|
+
continue
|
|
909
|
+
self._maybe_flip_vararg_readonly(func, overloads[-1], dyn_pin_nonconst=False)
|
|
910
|
+
|
|
911
|
+
for record in module.all_records():
|
|
912
|
+
rec_info = self.ctx.registry.get_record(record.name)
|
|
913
|
+
if rec_info is None:
|
|
914
|
+
continue
|
|
915
|
+
for method in record.methods:
|
|
916
|
+
if method.is_overload_stub or method.native_function:
|
|
917
|
+
continue
|
|
918
|
+
if method.vararg_name is None:
|
|
919
|
+
continue
|
|
920
|
+
# @auto_readonly methods are cloned into mutable + const pairs;
|
|
921
|
+
# the mutable clone exists precisely to offer the non-const
|
|
922
|
+
# form, so its vararg slot must stay mutable even when the
|
|
923
|
+
# body doesn't mutate the pack. The const clone gets the slot
|
|
924
|
+
# via its own ReadonlyType wrapping at clone time.
|
|
925
|
+
if method.is_auto_readonly_mutable_clone:
|
|
926
|
+
continue
|
|
927
|
+
method_fi = rec_info.get_method(method.name)
|
|
928
|
+
if method_fi is None:
|
|
929
|
+
continue
|
|
930
|
+
pin_nonconst = self._dynamic_proto_pins_vararg_nonconst(
|
|
931
|
+
rec_info, method.name)
|
|
932
|
+
self._maybe_flip_vararg_readonly(method, method_fi,
|
|
933
|
+
dyn_pin_nonconst=pin_nonconst)
|
|
934
|
+
|
|
935
|
+
def _maybe_flip_vararg_readonly(
|
|
936
|
+
self, func: TpyFunction, fi: 'FunctionInfo', *, dyn_pin_nonconst: bool,
|
|
937
|
+
) -> None:
|
|
938
|
+
"""Apply the slot flip when body analysis shows the vararg isn't
|
|
939
|
+
mutated. Mutates both `fi.params` and `func.params` so codegen
|
|
940
|
+
(which reads from the AST) and downstream sema (which reads from FI)
|
|
941
|
+
agree on the resolved slot type.
|
|
942
|
+
"""
|
|
943
|
+
va_idx = next((i for i, p in enumerate(fi.params) if p.is_variadic), -1)
|
|
944
|
+
if va_idx < 0:
|
|
945
|
+
return
|
|
946
|
+
va_param = fi.params[va_idx]
|
|
947
|
+
bare_va = unwrap_ref_type(va_param.type)
|
|
948
|
+
if not is_span(bare_va) or span_is_readonly(bare_va):
|
|
949
|
+
return
|
|
950
|
+
# mutated_params=None means no body was analyzed (native, stub,
|
|
951
|
+
# builtin). The signature is the source of truth in those cases --
|
|
952
|
+
# don't infer-flip what the user (or C++ binding) declared.
|
|
953
|
+
if fi.mutated_params is None or va_idx in fi.mutated_params:
|
|
954
|
+
return
|
|
955
|
+
if dyn_pin_nonconst:
|
|
956
|
+
return
|
|
957
|
+
new_span = span_as_const(bare_va)
|
|
958
|
+
new_param_type = make_ref(new_span) if isinstance(va_param.type, RefType) else new_span
|
|
959
|
+
fi.params[va_idx] = ParamInfo(
|
|
960
|
+
name=va_param.name,
|
|
961
|
+
type=new_param_type,
|
|
962
|
+
requires_mutable_lvalue=va_param.requires_mutable_lvalue,
|
|
963
|
+
default_expr=va_param.default_expr,
|
|
964
|
+
keyword_only=va_param.keyword_only,
|
|
965
|
+
is_variadic=True,
|
|
966
|
+
)
|
|
967
|
+
# AST mirror: codegen reads from `func.params` directly.
|
|
968
|
+
for i, (pname, _) in enumerate(func.params):
|
|
969
|
+
if pname == func.vararg_name:
|
|
970
|
+
func.params[i] = (pname, new_param_type)
|
|
971
|
+
break
|
|
972
|
+
|
|
973
|
+
def _dynamic_proto_pins_vararg_nonconst(
|
|
974
|
+
self, record_info: 'RecordInfo', method_name: str,
|
|
975
|
+
) -> bool:
|
|
976
|
+
"""Parallel to `_dynamic_proto_requires_nonconst`, but for the vararg
|
|
977
|
+
slot: an override of a @dynamic protocol method must keep the vararg
|
|
978
|
+
mutable when the protocol declares it mutable, since the C++ vtable
|
|
979
|
+
slot is monomorphized to `varargs<T>` vs `varargs<const T>`.
|
|
980
|
+
|
|
981
|
+
Reachable but unexercised today: `MethodSignature` (typesys.py:4127)
|
|
982
|
+
does not carry `is_variadic`, so protocol methods can't declare a
|
|
983
|
+
`*args` slot; `_proto_hierarchy_has_vararg_nonconst` therefore never
|
|
984
|
+
finds a mutable-vararg sig. Kept as a forward-compat guard so the
|
|
985
|
+
invariant is in place the day protocols gain vararg method support.
|
|
986
|
+
"""
|
|
987
|
+
visited: set[str] = set()
|
|
988
|
+
for proto_type, _ in self.ctx.registry.iter_dynamic_protocols(record_info):
|
|
989
|
+
if self._proto_hierarchy_has_vararg_nonconst(
|
|
990
|
+
proto_type.name, method_name, visited):
|
|
991
|
+
return True
|
|
992
|
+
return False
|
|
993
|
+
|
|
994
|
+
def _proto_hierarchy_has_vararg_nonconst(
|
|
995
|
+
self, proto_name: str, method_name: str, visited: set[str],
|
|
996
|
+
) -> bool:
|
|
997
|
+
if proto_name in visited:
|
|
998
|
+
return False
|
|
999
|
+
visited.add(proto_name)
|
|
1000
|
+
proto_info = self.ctx.registry.scan_by_short_name(proto_name)
|
|
1001
|
+
if proto_info is None:
|
|
1002
|
+
return False
|
|
1003
|
+
for sig in proto_info.methods:
|
|
1004
|
+
if sig.name != method_name:
|
|
1005
|
+
continue
|
|
1006
|
+
for _, ptype in sig.params:
|
|
1007
|
+
bare = unwrap_ref_type(ptype) if isinstance(ptype, TpyType) else None
|
|
1008
|
+
if bare is not None and is_span(bare) and not span_is_readonly(bare):
|
|
1009
|
+
return True
|
|
1010
|
+
for parent in proto_info.parent_protocols:
|
|
1011
|
+
if self._proto_hierarchy_has_vararg_nonconst(
|
|
1012
|
+
parent.name, method_name, visited):
|
|
1013
|
+
return True
|
|
1014
|
+
return False
|
|
1015
|
+
|
|
1016
|
+
def _is_type_nocopy(self, typ: TpyType) -> bool:
|
|
1017
|
+
"""Delegate to canonical is_type_nocopy on context."""
|
|
1018
|
+
return self.ctx.is_type_nocopy(typ)
|
|
1019
|
+
|
|
1020
|
+
def _validate_factory_defaults(self, module: TpyModule) -> None:
|
|
1021
|
+
"""Validate that field(default_factory=X) fields have Default-constructible types."""
|
|
1022
|
+
default_proto = NominalType("Default", (), is_protocol=True)
|
|
1023
|
+
for record in module.all_records():
|
|
1024
|
+
for fld in record.fields:
|
|
1025
|
+
if not fld.is_factory_default:
|
|
1026
|
+
continue
|
|
1027
|
+
if not self.protocols.type_conforms_to_protocol(fld.type, default_proto):
|
|
1028
|
+
raise SemanticError(
|
|
1029
|
+
f"Field '{fld.name}' in '{record.name}' uses "
|
|
1030
|
+
f"default_factory but type '{fld.type}' is not default-constructible",
|
|
1031
|
+
fld.loc or record.loc,
|
|
1032
|
+
)
|
|
1033
|
+
# Validate factory name matches field type
|
|
1034
|
+
expr = fld.default_expr
|
|
1035
|
+
if isinstance(expr, TpyCall) and isinstance(fld.type, NominalType):
|
|
1036
|
+
if expr.func_name != fld.type.name:
|
|
1037
|
+
raise SemanticError(
|
|
1038
|
+
f"default_factory '{expr.func_name}' does not match "
|
|
1039
|
+
f"field type '{fld.type}'",
|
|
1040
|
+
fld.loc or record.loc,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
def _propagate_nocopy(self, module: TpyModule) -> None:
|
|
1044
|
+
"""Propagate nocopy from fields/parents to containing records.
|
|
1045
|
+
|
|
1046
|
+
Processes records in definition order. If any field's type is nocopy,
|
|
1047
|
+
the containing record becomes nocopy too. Also checks parent type.
|
|
1048
|
+
Skips records that define __copy__ (opt-out escape hatch).
|
|
1049
|
+
"""
|
|
1050
|
+
for record in module.all_records():
|
|
1051
|
+
info = self.ctx.registry.get_record(record.name)
|
|
1052
|
+
if info is None or info.is_nocopy:
|
|
1053
|
+
continue
|
|
1054
|
+
# __copy__ opts out of propagation
|
|
1055
|
+
if info.has_copy:
|
|
1056
|
+
continue
|
|
1057
|
+
# Check parents (any nocopy parent propagates to the child)
|
|
1058
|
+
if any(self._is_type_nocopy(p) for p in info.parents):
|
|
1059
|
+
info.is_nocopy = True
|
|
1060
|
+
continue
|
|
1061
|
+
# Check fields
|
|
1062
|
+
for f in info.fields:
|
|
1063
|
+
if self._is_type_nocopy(f.type):
|
|
1064
|
+
info.is_nocopy = True
|
|
1065
|
+
break
|
|
1066
|
+
|
|
1067
|
+
def _normalize_param_type(self, ptype: TpyType, is_readonly_ctx: bool) -> TpyType:
|
|
1068
|
+
"""Normalize a parameter type for readonly context.
|
|
1069
|
+
|
|
1070
|
+
Strips ReadonlyType from value types (copies are always safe).
|
|
1071
|
+
Wraps non-value types with ReadonlyType in @readonly contexts.
|
|
1072
|
+
"""
|
|
1073
|
+
if isinstance(ptype, ReadonlyType) and ptype.wrapped.is_value_type():
|
|
1074
|
+
return ptype.wrapped
|
|
1075
|
+
if is_readonly_ctx and not isinstance(ptype, ReadonlyType):
|
|
1076
|
+
if not ptype.is_value_type():
|
|
1077
|
+
return ReadonlyType(ptype)
|
|
1078
|
+
return ptype
|
|
1079
|
+
|
|
1080
|
+
def _warn_unconsumed_own_params(self, func: TpyFunction) -> None:
|
|
1081
|
+
"""Warn when Own[T] params are never consumed (stored, forwarded, or returned)."""
|
|
1082
|
+
if func.is_stub:
|
|
1083
|
+
return
|
|
1084
|
+
for pname, ptype in func.params:
|
|
1085
|
+
own = unwrap_optional_own(unwrap_readonly(ptype))
|
|
1086
|
+
if own is None:
|
|
1087
|
+
continue
|
|
1088
|
+
if pname in self.ctx.func.current_consumed_own_params:
|
|
1089
|
+
continue
|
|
1090
|
+
# Optional[Own[T]]: None branch has nothing to consume, making
|
|
1091
|
+
# flow-sensitive intersection unreliable
|
|
1092
|
+
if isinstance(unwrap_readonly(ptype), OptionalType):
|
|
1093
|
+
continue
|
|
1094
|
+
# Value types: copy == move, no semantic difference
|
|
1095
|
+
if own.wrapped.is_value_type():
|
|
1096
|
+
continue
|
|
1097
|
+
# Generic T bounded to ValueType: same as value type
|
|
1098
|
+
if isinstance(own.wrapped, TypeParamRef):
|
|
1099
|
+
bound = self.type_ops.get_type_param_bound(own.wrapped.name)
|
|
1100
|
+
if (bound is not None and isinstance(bound, NominalType)
|
|
1101
|
+
and bound.qualified_name() == "tpy.ValueType"):
|
|
1102
|
+
continue
|
|
1103
|
+
# @nocopy types: Own is the only way to pass them
|
|
1104
|
+
if self.ctx.is_type_nocopy(own.wrapped):
|
|
1105
|
+
continue
|
|
1106
|
+
self.ctx.warning(
|
|
1107
|
+
f"Own[{own.wrapped}] param '{pname}' is never consumed "
|
|
1108
|
+
f"(not stored in a field, forwarded to another Own[T], or returned)",
|
|
1109
|
+
func,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
def _analyze_function(self, func: TpyFunction) -> None:
|
|
1113
|
+
"""Analyze a function body."""
|
|
1114
|
+
# Stub functions (extern imports with ... body) have no body to analyze
|
|
1115
|
+
if func.is_stub:
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1118
|
+
self.ctx.reset_function_tracking()
|
|
1119
|
+
|
|
1120
|
+
self.ctx.func.current_function = func
|
|
1121
|
+
# Async def bodies are analyzed normally. The await-expression
|
|
1122
|
+
# analyzer (sema/expressions.py:_analyze_await) handles the supported
|
|
1123
|
+
# v1 forms (direct call to async def, Task[T], Future[T], structural
|
|
1124
|
+
# awaitable) and rejects unsupported shapes with a clear diagnostic.
|
|
1125
|
+
# Resolve return type (sets is_protocol for cross-module imports)
|
|
1126
|
+
func.return_type = make_ref(self.type_ops.resolve_type(func.return_type))
|
|
1127
|
+
scope = Scope(parent=self.ctx.global_scope)
|
|
1128
|
+
self.ctx.func.current_scope = scope
|
|
1129
|
+
|
|
1130
|
+
# Resolve and normalize params (@readonly wraps all non-value params).
|
|
1131
|
+
# Write back to AST so codegen sees Ref/ReadonlyType (codegen reads func.params directly, not FI).
|
|
1132
|
+
local_ns = Namespace(parent=self.ctx.global_ns)
|
|
1133
|
+
self.ctx.func.current_ns = local_ns
|
|
1134
|
+
resolved_params: list[tuple[str, TpyType]] = []
|
|
1135
|
+
for i, (pname, ptype) in enumerate(func.params):
|
|
1136
|
+
resolved_ptype = self._normalize_param_type(
|
|
1137
|
+
self.type_ops.resolve_type(ptype), func.is_readonly)
|
|
1138
|
+
func.params[i] = (pname, make_ref(resolved_ptype))
|
|
1139
|
+
resolved_params.append((pname, resolved_ptype))
|
|
1140
|
+
|
|
1141
|
+
# Add *args parameter as Span[readonly[T]] to func.params and resolved_params.
|
|
1142
|
+
# Insert before keyword-only params to match FunctionInfo param order.
|
|
1143
|
+
if func.vararg_name is not None and func.vararg_type is not None:
|
|
1144
|
+
va_type = _vararg_span_type(self.type_ops.resolve_type(func.vararg_type))
|
|
1145
|
+
kw_start = func.keyword_only_start
|
|
1146
|
+
if kw_start is not None and kw_start < len(func.params):
|
|
1147
|
+
func.params.insert(kw_start, (func.vararg_name, make_ref(va_type)))
|
|
1148
|
+
resolved_params.insert(kw_start, (func.vararg_name, va_type))
|
|
1149
|
+
if func.defaults:
|
|
1150
|
+
func.defaults.insert(kw_start, None)
|
|
1151
|
+
# Adjust keyword_only_start since we inserted before it
|
|
1152
|
+
func.keyword_only_start = kw_start + 1
|
|
1153
|
+
else:
|
|
1154
|
+
func.params.append((func.vararg_name, make_ref(va_type)))
|
|
1155
|
+
resolved_params.append((func.vararg_name, va_type))
|
|
1156
|
+
if func.defaults:
|
|
1157
|
+
func.defaults.append(None)
|
|
1158
|
+
# Clear vararg_type to prevent re-processing; keep vararg_name for codegen
|
|
1159
|
+
func.vararg_type = None
|
|
1160
|
+
|
|
1161
|
+
# Add **kwargs parameter as TypedDict type to func.params and resolved_params.
|
|
1162
|
+
if func.kwarg_name is not None and func.kwarg_type is not None:
|
|
1163
|
+
kw_type = self.type_ops.resolve_type(func.kwarg_type)
|
|
1164
|
+
func.params.append((func.kwarg_name, make_ref(kw_type)))
|
|
1165
|
+
resolved_params.append((func.kwarg_name, kw_type))
|
|
1166
|
+
if func.defaults:
|
|
1167
|
+
func.defaults.append(None)
|
|
1168
|
+
# Clear kwarg_type to prevent re-processing; keep kwarg_name for codegen
|
|
1169
|
+
func.kwarg_type = None
|
|
1170
|
+
|
|
1171
|
+
# Track consuming method for ownership propagation through fields
|
|
1172
|
+
prev_consuming = self.ctx.in_consuming_method
|
|
1173
|
+
self.ctx.in_consuming_method = func.is_consuming
|
|
1174
|
+
|
|
1175
|
+
self._validate_named_defaults(func)
|
|
1176
|
+
|
|
1177
|
+
# Shared core: bind params, prescan, analyze body
|
|
1178
|
+
scan = self.stmts._prescan_and_analyze_body(func, resolved_params, scope, local_ns)
|
|
1179
|
+
self.deduction.resolve_all()
|
|
1180
|
+
|
|
1181
|
+
# Collect generator/async local variables for struct field generation.
|
|
1182
|
+
# Same hoisting policy: every function-level local becomes a struct
|
|
1183
|
+
# field. Async coros use the same `func.generator_locals` slot; the
|
|
1184
|
+
# field-rewrite path in expressions/statements is shared via the
|
|
1185
|
+
# in_generator_body flag (the `in_async_coro_body` flag steers only
|
|
1186
|
+
# the return-statement rewrite).
|
|
1187
|
+
if func.is_generator or func.is_async:
|
|
1188
|
+
param_names = {pname for pname, _ in func.params}
|
|
1189
|
+
locals_dict: dict[str, 'TpyType'] = {}
|
|
1190
|
+
for name, binding in local_ns.all_bindings().items():
|
|
1191
|
+
if name not in param_names and binding.type is not None:
|
|
1192
|
+
locals_dict[name] = binding.type
|
|
1193
|
+
for name, (vtype, _, _) in self.ctx.func.pending_loop_vars.items():
|
|
1194
|
+
if name not in param_names and vtype is not None:
|
|
1195
|
+
locals_dict[name] = vtype
|
|
1196
|
+
func.generator_locals = list(locals_dict.items())
|
|
1197
|
+
|
|
1198
|
+
# Finalize nested def escape analysis
|
|
1199
|
+
self._finalize_nested_def_escapes()
|
|
1200
|
+
|
|
1201
|
+
# Store Phase 1 local mutation facts (resolved by Phase 2 propagation).
|
|
1202
|
+
# Phase 6 (mutual-imports) ownership gate: only mutate body-sema
|
|
1203
|
+
# fields on FunctionInfos that originate in this module. The
|
|
1204
|
+
# `direct_mutated_params is None` check covers the legacy case
|
|
1205
|
+
# (FI not yet body-analyzed); the strict `originating_module ==
|
|
1206
|
+
# self.ctx.module_name` check enforces the doc's ownership rule
|
|
1207
|
+
# -- declaration-time `_normalize_function_info_refs` stamps
|
|
1208
|
+
# `originating_module` on every non-opaque FI, so by the time a
|
|
1209
|
+
# body sema reaches this gate, any reachable FI should carry
|
|
1210
|
+
# its defining module's name. A None at this point indicates a
|
|
1211
|
+
# missing-origin-stamping bug, not a reason to write through.
|
|
1212
|
+
func_overloads = self.ctx.registry.get_function(func.name)
|
|
1213
|
+
func_info = func_overloads[-1] if func_overloads else None
|
|
1214
|
+
if (func_info is not None
|
|
1215
|
+
and func_info.direct_mutated_params is None
|
|
1216
|
+
and func_info.originating_module == self.ctx.module_name):
|
|
1217
|
+
param_list = [pname for pname, _ in func.params]
|
|
1218
|
+
direct = frozenset(
|
|
1219
|
+
i for i, pname in enumerate(param_list)
|
|
1220
|
+
if pname in self.ctx.func.current_mutated_param_names
|
|
1221
|
+
)
|
|
1222
|
+
func_info.direct_mutated_params = direct
|
|
1223
|
+
func_info.call_edges = list(self.ctx.func.current_call_edges)
|
|
1224
|
+
func_info.representational_type_params = frozenset(self.ctx.func.current_representational_params)
|
|
1225
|
+
# Set mutated_params to direct facts as initial estimate;
|
|
1226
|
+
# Phase 2 propagation will replace with the complete transitive set.
|
|
1227
|
+
func_info.mutated_params = direct
|
|
1228
|
+
# Structural mutation facts (append/insert/clear/del/etc.)
|
|
1229
|
+
direct_struct = frozenset(
|
|
1230
|
+
i for i, pname in enumerate(param_list)
|
|
1231
|
+
if pname in self.ctx.func.current_struct_mutated_param_names
|
|
1232
|
+
)
|
|
1233
|
+
func_info.direct_structural_mutated_params = direct_struct
|
|
1234
|
+
func_info.structural_mutated_params = direct_struct
|
|
1235
|
+
# 8b: Return borrow facts -- which params does the return value borrow from?
|
|
1236
|
+
func_info.return_borrows_from = frozenset(
|
|
1237
|
+
i for i, pname in enumerate(param_list)
|
|
1238
|
+
if pname in self.ctx.func.current_returned_param_names
|
|
1239
|
+
)
|
|
1240
|
+
func_info.addr_escapes_params = frozenset(
|
|
1241
|
+
i for i, pname in enumerate(param_list)
|
|
1242
|
+
if pname in self.ctx.func.current_addr_escape_param_names
|
|
1243
|
+
)
|
|
1244
|
+
# Generator functions: the returned struct stores non-value params
|
|
1245
|
+
# as T& references (or &ref lambda captures), and str params as
|
|
1246
|
+
# string_view, so the result borrows from those params
|
|
1247
|
+
if func.is_generator:
|
|
1248
|
+
gen_borrows = frozenset(
|
|
1249
|
+
i for i, (_, ptype) in enumerate(func.params)
|
|
1250
|
+
if not ptype.is_value_type() or is_str_type(ptype) or is_str_view_type(ptype)
|
|
1251
|
+
)
|
|
1252
|
+
if gen_borrows:
|
|
1253
|
+
func_info.return_borrows_from = func_info.return_borrows_from | gen_borrows
|
|
1254
|
+
|
|
1255
|
+
self._warn_unconsumed_own_params(func)
|
|
1256
|
+
self._store_analysis_results(func, scan)
|
|
1257
|
+
|
|
1258
|
+
self.ctx.in_consuming_method = prev_consuming
|
|
1259
|
+
self.ctx.func.current_function = None
|
|
1260
|
+
self.ctx.func.current_scope = None
|
|
1261
|
+
self.ctx.func.current_ns = None
|
|
1262
|
+
|
|
1263
|
+
def _store_analysis_results(self, func: TpyFunction, scan: ScanResult) -> None:
|
|
1264
|
+
"""Store prescan/liveness results for codegen consumption."""
|
|
1265
|
+
self.function_scan_results[id(func)] = scan
|
|
1266
|
+
if self.ctx.func.hoisted_vars:
|
|
1267
|
+
self.function_hoisted_vars[id(func)] = self.ctx.func.hoisted_vars.copy()
|
|
1268
|
+
if self.ctx.func.move_through_vars:
|
|
1269
|
+
self.function_move_through_vars[id(func)] = self.ctx.func.move_through_vars.copy()
|
|
1270
|
+
# Compute movable locals from ever_owned_locals (survives FlowFacts restores)
|
|
1271
|
+
movable = set()
|
|
1272
|
+
for name in self.ctx.func.ever_owned_locals:
|
|
1273
|
+
if name in self.ctx.func.hoisted_vars:
|
|
1274
|
+
continue
|
|
1275
|
+
if name in self.ctx.func.current_reassigned_vars and name in self.ctx.func.current_lvalue_reassigned:
|
|
1276
|
+
continue
|
|
1277
|
+
movable.add(name)
|
|
1278
|
+
if movable:
|
|
1279
|
+
self.function_movable_locals[id(func)] = movable
|
|
1280
|
+
if self.ctx.func.global_declarations:
|
|
1281
|
+
self.function_global_decls[id(func)] = self.ctx.func.global_declarations.copy()
|
|
1282
|
+
self.if_branch_decls.update(self.ctx.if_branch_decls)
|
|
1283
|
+
|
|
1284
|
+
def _validate_named_defaults(self, func: TpyFunction) -> None:
|
|
1285
|
+
"""Validate `def f(x: T = NAME)` defaults: NAME must be a Final[T] global.
|
|
1286
|
+
|
|
1287
|
+
The parser accepts any TpyName in default position; binding shape is
|
|
1288
|
+
deferred here so module-level Finals (declared after functions in the
|
|
1289
|
+
registration order) and imported Finals are visible.
|
|
1290
|
+
"""
|
|
1291
|
+
for default in func.defaults:
|
|
1292
|
+
if not isinstance(default, TpyName):
|
|
1293
|
+
continue
|
|
1294
|
+
name = default.name
|
|
1295
|
+
if name in self.ctx.final_globals:
|
|
1296
|
+
continue
|
|
1297
|
+
imp = lookup_imported(
|
|
1298
|
+
self.ctx.module_attributes, name, SymbolKind.VARIABLE)
|
|
1299
|
+
if imp is not None:
|
|
1300
|
+
source_module, original_name = imp
|
|
1301
|
+
source_info = self.ctx.registry.get_module(source_module)
|
|
1302
|
+
var_info = source_info.variables.get(original_name) if source_info else None
|
|
1303
|
+
if var_info is not None and var_info.is_final:
|
|
1304
|
+
continue
|
|
1305
|
+
raise self.ctx.error(
|
|
1306
|
+
f"Default parameter value '{name}' must be a module-level "
|
|
1307
|
+
f"Final[T] constant", default)
|
|
1308
|
+
|
|
1309
|
+
def _finalize_nested_def_escapes(self) -> None:
|
|
1310
|
+
"""Finalize escape analysis for nested defs after the enclosing function is analyzed."""
|
|
1311
|
+
# Outer function's parameter names and types
|
|
1312
|
+
outer_params: dict[str, TpyType] = {}
|
|
1313
|
+
if self.ctx.func.current_function:
|
|
1314
|
+
for pname, ptype in self.ctx.func.current_function.params:
|
|
1315
|
+
outer_params[pname] = ptype
|
|
1316
|
+
outer_param_names = set(outer_params.keys())
|
|
1317
|
+
# Get the enclosing function body for "used after" analysis
|
|
1318
|
+
body = self.ctx.func.current_function.body if self.ctx.func.current_function else []
|
|
1319
|
+
for name in self.ctx.func.nested_def_escapes:
|
|
1320
|
+
node = self.ctx.func.nested_def_nodes.get(name)
|
|
1321
|
+
if node is None:
|
|
1322
|
+
continue
|
|
1323
|
+
node.escapes = True
|
|
1324
|
+
# Reject nonlocal in escaping closures
|
|
1325
|
+
if node.nonlocal_names:
|
|
1326
|
+
nl_list = ", ".join(f"'{n}'" for n in sorted(node.nonlocal_names))
|
|
1327
|
+
self.ctx.emit_error(
|
|
1328
|
+
f"nonlocal {nl_list} in escaping closure '{name}' is not supported"
|
|
1329
|
+
f" (the closure is returned or stored; use a class instead)",
|
|
1330
|
+
node)
|
|
1331
|
+
# Collect names referenced after the nested def for move analysis
|
|
1332
|
+
names_used_after = self._names_used_after(body, node)
|
|
1333
|
+
# Per-capture analysis: determine ref vs value vs move capture mode
|
|
1334
|
+
for cap_name in node.captured_names:
|
|
1335
|
+
cap_type = self.ctx.func.current_scope.lookup(cap_name) if self.ctx.func.current_scope else None
|
|
1336
|
+
if cap_type is None:
|
|
1337
|
+
continue
|
|
1338
|
+
raw_type = unwrap_readonly(cap_type)
|
|
1339
|
+
is_own_param = isinstance(raw_type, OwnType)
|
|
1340
|
+
if isinstance(raw_type, OwnType):
|
|
1341
|
+
raw_type = raw_type.wrapped
|
|
1342
|
+
check_type = raw_type.inner if isinstance(raw_type, OptionalType) else raw_type
|
|
1343
|
+
if cap_name in outer_param_names:
|
|
1344
|
+
# Reject str/StrView parameter captures (string_view dangles)
|
|
1345
|
+
if is_str_type(check_type) or is_str_view_type(check_type):
|
|
1346
|
+
self.ctx.emit_error(
|
|
1347
|
+
f"Escaping closure '{name}' captures str parameter"
|
|
1348
|
+
f" '{cap_name}' which would dangle (string_view into"
|
|
1349
|
+
f" caller's storage). Use String for owned capture",
|
|
1350
|
+
node)
|
|
1351
|
+
elif not raw_type.is_value_type():
|
|
1352
|
+
if is_own_param:
|
|
1353
|
+
# Own[T] param is destroyed on return -- must move
|
|
1354
|
+
node.move_captures.add(cap_name)
|
|
1355
|
+
self.ctx.mark_own_param_consumed(cap_name)
|
|
1356
|
+
else:
|
|
1357
|
+
# Regular const-ref param: caller's object outlives closure
|
|
1358
|
+
node.ref_captures.add(cap_name)
|
|
1359
|
+
else:
|
|
1360
|
+
# Local variable: must copy or move (local dies on return).
|
|
1361
|
+
# Only emit std::move for types where move is cheaper than
|
|
1362
|
+
# copy (string, BigInt, etc.) -- for trivial types like
|
|
1363
|
+
# Int32 it's just noise.
|
|
1364
|
+
wants_move = (not raw_type.is_value_type()
|
|
1365
|
+
or raw_type.is_expensive_copy())
|
|
1366
|
+
if wants_move and cap_name not in names_used_after:
|
|
1367
|
+
# Last use -- move into the closure, no warning
|
|
1368
|
+
node.move_captures.add(cap_name)
|
|
1369
|
+
elif wants_move:
|
|
1370
|
+
# Used after the closure -- must copy, warn
|
|
1371
|
+
self.ctx.warning(
|
|
1372
|
+
f"Escaping closure '{name}' copies local"
|
|
1373
|
+
f" '{cap_name}' (used after closure definition,"
|
|
1374
|
+
f" preventing move). Reorder code so the closure"
|
|
1375
|
+
f" is the last use, or use copy() to make the"
|
|
1376
|
+
f" copy explicit",
|
|
1377
|
+
node)
|
|
1378
|
+
|
|
1379
|
+
@staticmethod
|
|
1380
|
+
def _names_used_after(body: list[TpyStmt], nested_node: TpyNestedDef) -> set[str]:
|
|
1381
|
+
"""Collect names referenced in top-level statements after the nested def."""
|
|
1382
|
+
found = False
|
|
1383
|
+
after_stmts: list[TpyStmt] = []
|
|
1384
|
+
target_name = nested_node.func.name
|
|
1385
|
+
for stmt in body:
|
|
1386
|
+
if found:
|
|
1387
|
+
after_stmts.append(stmt)
|
|
1388
|
+
elif isinstance(stmt, TpyNestedDef) and stmt.func.name == target_name:
|
|
1389
|
+
found = True
|
|
1390
|
+
if not found:
|
|
1391
|
+
# Nested def not at top level of body -- conservatively assume all used
|
|
1392
|
+
return set(nested_node.captured_names or [])
|
|
1393
|
+
return _collect_body_name_refs(after_stmts)
|
|
1394
|
+
|
|
1395
|
+
def _collect_method_overload_groups(self, record: TpyRecord) -> None:
|
|
1396
|
+
"""Identify and validate @overload groups among a record's methods.
|
|
1397
|
+
|
|
1398
|
+
For each group, stores the mapping from implementation -> stubs
|
|
1399
|
+
in self.overload_groups. Also validates exhaustiveness.
|
|
1400
|
+
"""
|
|
1401
|
+
pending_stubs: dict[str, list[TpyFunction]] = {}
|
|
1402
|
+
for method in record.methods:
|
|
1403
|
+
if method.is_overload_stub:
|
|
1404
|
+
pending_stubs.setdefault(method.name, []).append(method)
|
|
1405
|
+
continue
|
|
1406
|
+
stubs = pending_stubs.pop(method.name, None)
|
|
1407
|
+
if stubs:
|
|
1408
|
+
self._reject_bodied_overloads_with_impl(
|
|
1409
|
+
stubs, method, f"{record.name}.{method.name}")
|
|
1410
|
+
self._validate_method_overload_group(method, stubs, record.name)
|
|
1411
|
+
self.overload_groups[id(method)] = stubs
|
|
1412
|
+
for name, stubs in pending_stubs.items():
|
|
1413
|
+
# All stubs must be self-contained: each is @native, @cpp_template,
|
|
1414
|
+
# or carries its own body (mode b). Mixed native + bodied groups OK.
|
|
1415
|
+
if all(s.has_implementation for s in stubs):
|
|
1416
|
+
continue
|
|
1417
|
+
raise SemanticError(
|
|
1418
|
+
f"@overload stubs for '{record.name}.{name}' have no implementation method",
|
|
1419
|
+
stubs[0].loc or record.loc,
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
def _validate_method_overload_group(
|
|
1423
|
+
self, impl: TpyFunction, stubs: list[TpyFunction], record_name: str,
|
|
1424
|
+
) -> None:
|
|
1425
|
+
"""Validate exhaustiveness of method overload stubs."""
|
|
1426
|
+
self._validate_overload_signatures(
|
|
1427
|
+
impl, stubs, f"{record_name}.{impl.name}")
|
|
1428
|
+
|
|
1429
|
+
def _validate_overload_signatures(
|
|
1430
|
+
self, impl: TpyFunction, stubs: list[TpyFunction], display_name: str,
|
|
1431
|
+
) -> None:
|
|
1432
|
+
"""Shared @overload stub validation (functions + methods).
|
|
1433
|
+
|
|
1434
|
+
Rules:
|
|
1435
|
+
- Stub arity must be <= impl arity. When shorter, every missing trailing
|
|
1436
|
+
impl param must have a default.
|
|
1437
|
+
- Stub params must prefix-match impl params by name; stub types must be
|
|
1438
|
+
compatible with impl types at each shared position (exact match or
|
|
1439
|
+
LiteralType over the impl base for non-unions; subset of members for
|
|
1440
|
+
unions).
|
|
1441
|
+
- When any stub has fewer params than the impl, the impl may not use
|
|
1442
|
+
keyword-only params, *args, or **kwargs.
|
|
1443
|
+
- Union-typed impl params: stubs that include the param must only
|
|
1444
|
+
reference members of the union; when every stub includes the param,
|
|
1445
|
+
all members must be covered.
|
|
1446
|
+
"""
|
|
1447
|
+
impl_defaults = impl.defaults if impl.defaults else []
|
|
1448
|
+
has_short_arity = any(len(s.params) < len(impl.params) for s in stubs)
|
|
1449
|
+
if has_short_arity:
|
|
1450
|
+
if impl.keyword_only_start is not None:
|
|
1451
|
+
raise SemanticError(
|
|
1452
|
+
f"@overload implementation '{display_name}' cannot have "
|
|
1453
|
+
f"keyword-only parameters when stubs have different arities",
|
|
1454
|
+
impl.loc,
|
|
1455
|
+
)
|
|
1456
|
+
if impl.vararg_name is not None:
|
|
1457
|
+
raise SemanticError(
|
|
1458
|
+
f"@overload implementation '{display_name}' cannot have "
|
|
1459
|
+
f"*args when stubs have different arities",
|
|
1460
|
+
impl.loc,
|
|
1461
|
+
)
|
|
1462
|
+
if impl.kwarg_name is not None:
|
|
1463
|
+
raise SemanticError(
|
|
1464
|
+
f"@overload implementation '{display_name}' cannot have "
|
|
1465
|
+
f"**kwargs when stubs have different arities",
|
|
1466
|
+
impl.loc,
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
for stub in stubs:
|
|
1470
|
+
if len(stub.params) > len(impl.params):
|
|
1471
|
+
raise SemanticError(
|
|
1472
|
+
f"@overload stub for '{display_name}' has "
|
|
1473
|
+
f"{len(stub.params)} parameter(s), more than the "
|
|
1474
|
+
f"implementation's {len(impl.params)}",
|
|
1475
|
+
stub.loc,
|
|
1476
|
+
)
|
|
1477
|
+
if len(stub.params) < len(impl.params):
|
|
1478
|
+
for i in range(len(stub.params), len(impl.params)):
|
|
1479
|
+
impl_pname = impl.params[i][0]
|
|
1480
|
+
has_default = (i < len(impl_defaults)
|
|
1481
|
+
and impl_defaults[i] is not None)
|
|
1482
|
+
if not has_default:
|
|
1483
|
+
raise SemanticError(
|
|
1484
|
+
f"@overload stub for '{display_name}' omits "
|
|
1485
|
+
f"parameter '{impl_pname}', but the implementation "
|
|
1486
|
+
f"has no default value for it",
|
|
1487
|
+
stub.loc,
|
|
1488
|
+
)
|
|
1489
|
+
for (impl_pname, _), (stub_pname, _) in zip(impl.params, stub.params):
|
|
1490
|
+
if impl_pname != stub_pname:
|
|
1491
|
+
raise SemanticError(
|
|
1492
|
+
f"@overload stub parameter '{stub_pname}' does not match "
|
|
1493
|
+
f"implementation parameter '{impl_pname}'",
|
|
1494
|
+
stub.loc,
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
for param_idx, (pname, ptype) in enumerate(impl.params):
|
|
1498
|
+
resolved = self.type_ops.resolve_type(ptype)
|
|
1499
|
+
param_stubs = [s for s in stubs if param_idx < len(s.params)]
|
|
1500
|
+
if not param_stubs:
|
|
1501
|
+
# Every stub skips this param; impl default covers all call sites.
|
|
1502
|
+
continue
|
|
1503
|
+
if isinstance(resolved, OptionalType):
|
|
1504
|
+
inner_covered = False
|
|
1505
|
+
none_covered = False
|
|
1506
|
+
for stub in param_stubs:
|
|
1507
|
+
stub_ptype = self.type_ops.resolve_type(stub.params[param_idx][1])
|
|
1508
|
+
if stub_ptype == resolved:
|
|
1509
|
+
inner_covered = True
|
|
1510
|
+
none_covered = True
|
|
1511
|
+
continue
|
|
1512
|
+
if stub_ptype == resolved.inner:
|
|
1513
|
+
inner_covered = True
|
|
1514
|
+
continue
|
|
1515
|
+
if is_void_like_type(stub_ptype):
|
|
1516
|
+
# `None` as a type annotation parses to VoidType; a
|
|
1517
|
+
# `None` literal's type is NoneType -- both mean "None".
|
|
1518
|
+
none_covered = True
|
|
1519
|
+
continue
|
|
1520
|
+
if isinstance(stub_ptype, LiteralType):
|
|
1521
|
+
if stub_ptype.base_type == resolved.inner:
|
|
1522
|
+
inner_covered = True
|
|
1523
|
+
continue
|
|
1524
|
+
if stub_ptype.is_int_base() and is_integer_type(resolved.inner):
|
|
1525
|
+
inner_covered = True
|
|
1526
|
+
continue
|
|
1527
|
+
raise SemanticError(
|
|
1528
|
+
f"@overload stub type '{stub_ptype}' for parameter '{pname}' "
|
|
1529
|
+
f"is not compatible with implementation type '{resolved}'",
|
|
1530
|
+
stub.loc,
|
|
1531
|
+
)
|
|
1532
|
+
# Exhaustiveness only when every stub includes this param; when
|
|
1533
|
+
# some stubs skip it the impl default covers the missing path.
|
|
1534
|
+
if len(param_stubs) == len(stubs):
|
|
1535
|
+
missing_parts = []
|
|
1536
|
+
if not inner_covered:
|
|
1537
|
+
missing_parts.append(str(resolved.inner))
|
|
1538
|
+
if not none_covered:
|
|
1539
|
+
missing_parts.append("None")
|
|
1540
|
+
if missing_parts:
|
|
1541
|
+
raise SemanticError(
|
|
1542
|
+
f"@overload stubs for '{display_name}' don't cover "
|
|
1543
|
+
f"all variants of parameter '{pname}': missing "
|
|
1544
|
+
f"{', '.join(missing_parts)}",
|
|
1545
|
+
impl.loc,
|
|
1546
|
+
)
|
|
1547
|
+
continue
|
|
1548
|
+
if not isinstance(resolved, UnionType):
|
|
1549
|
+
for stub in param_stubs:
|
|
1550
|
+
stub_ptype = self.type_ops.resolve_type(stub.params[param_idx][1])
|
|
1551
|
+
if stub_ptype != resolved:
|
|
1552
|
+
# LiteralType is compatible with its base type family
|
|
1553
|
+
if isinstance(stub_ptype, LiteralType):
|
|
1554
|
+
if stub_ptype.base_type == resolved:
|
|
1555
|
+
continue
|
|
1556
|
+
if stub_ptype.is_int_base() and is_integer_type(resolved):
|
|
1557
|
+
continue
|
|
1558
|
+
raise SemanticError(
|
|
1559
|
+
f"@overload stub type '{stub_ptype}' for parameter '{pname}' "
|
|
1560
|
+
f"does not match implementation type '{resolved}'",
|
|
1561
|
+
stub.loc,
|
|
1562
|
+
)
|
|
1563
|
+
continue
|
|
1564
|
+
covered: set[TpyType] = set()
|
|
1565
|
+
for stub in param_stubs:
|
|
1566
|
+
stub_ptype = self.type_ops.resolve_type(stub.params[param_idx][1])
|
|
1567
|
+
if isinstance(stub_ptype, UnionType):
|
|
1568
|
+
stub_members = set(stub_ptype.members)
|
|
1569
|
+
else:
|
|
1570
|
+
stub_members = {stub_ptype}
|
|
1571
|
+
extra = [m for m in stub_members if m not in resolved.members]
|
|
1572
|
+
if extra:
|
|
1573
|
+
extra_names = ", ".join(str(m) for m in extra)
|
|
1574
|
+
raise SemanticError(
|
|
1575
|
+
f"@overload stub type for parameter '{pname}' includes "
|
|
1576
|
+
f"{extra_names} which is not in the implementation's "
|
|
1577
|
+
f"union type '{resolved}'",
|
|
1578
|
+
stub.loc,
|
|
1579
|
+
)
|
|
1580
|
+
covered.update(stub_members)
|
|
1581
|
+
# Only require full union coverage when every stub includes the param.
|
|
1582
|
+
# If some stubs skip it, the impl default handles those call sites.
|
|
1583
|
+
if len(param_stubs) == len(stubs):
|
|
1584
|
+
missing = [m for m in resolved.members if m not in covered]
|
|
1585
|
+
if missing:
|
|
1586
|
+
missing_names = ", ".join(str(m) for m in missing)
|
|
1587
|
+
raise SemanticError(
|
|
1588
|
+
f"@overload stubs for '{display_name}' don't cover all "
|
|
1589
|
+
f"variants of parameter '{pname}': missing {missing_names}",
|
|
1590
|
+
impl.loc,
|
|
1591
|
+
)
|
|
1592
|
+
|
|
1593
|
+
def _register_functions_with_overloads(self, functions: list[TpyFunction]) -> None:
|
|
1594
|
+
"""Register functions, grouping @overload stubs with their implementations.
|
|
1595
|
+
|
|
1596
|
+
@overload stubs precede their implementation function (same name).
|
|
1597
|
+
Stubs are registered as callable overloads; the implementation is
|
|
1598
|
+
registered for body analysis only (not directly callable).
|
|
1599
|
+
"""
|
|
1600
|
+
pending_stubs: dict[str, list[TpyFunction]] = {}
|
|
1601
|
+
|
|
1602
|
+
for func in functions:
|
|
1603
|
+
if func.builtin_decorator_key:
|
|
1604
|
+
# Register minimal FunctionInfo so the key flows through exports.
|
|
1605
|
+
# return_type comes from func (already VOID-substituted by
|
|
1606
|
+
# parse.resolve_refs); this FunctionInfo is used for
|
|
1607
|
+
# decorator-key lookup, not call resolution.
|
|
1608
|
+
self.ctx.registry.register_function(FunctionInfo(
|
|
1609
|
+
name=func.name, params=[], return_type=func.return_type,
|
|
1610
|
+
builtin_decorator_key=func.builtin_decorator_key,
|
|
1611
|
+
))
|
|
1612
|
+
# Refresh the attribute-table binding to point at the
|
|
1613
|
+
# registry's list -- pre-pop installed a placeholder
|
|
1614
|
+
# FunctionInfo without builtin_decorator_key, which
|
|
1615
|
+
# exports.functions does NOT use here (the registry's
|
|
1616
|
+
# special one wins).
|
|
1617
|
+
install_binding(
|
|
1618
|
+
self.ctx.module_attributes, func.name,
|
|
1619
|
+
SymbolKind.FUNCTION,
|
|
1620
|
+
self.ctx.registry.get_function(func.name),
|
|
1621
|
+
)
|
|
1622
|
+
continue
|
|
1623
|
+
if func.is_overload_stub:
|
|
1624
|
+
pending_stubs.setdefault(func.name, []).append(func)
|
|
1625
|
+
continue
|
|
1626
|
+
|
|
1627
|
+
# Non-stub function: check if there are pending stubs for this name
|
|
1628
|
+
stubs = pending_stubs.pop(func.name, None)
|
|
1629
|
+
if stubs:
|
|
1630
|
+
self._reject_bodied_overloads_with_impl(stubs, func, func.name)
|
|
1631
|
+
self._register_overload_group(func, stubs)
|
|
1632
|
+
else:
|
|
1633
|
+
self.registrar.register_function(func)
|
|
1634
|
+
|
|
1635
|
+
# Stubs left without an implementation: each stub must be self-contained
|
|
1636
|
+
# (native, cpp_template, or carries its own body).
|
|
1637
|
+
for name, stubs in pending_stubs.items():
|
|
1638
|
+
if all(s.has_implementation for s in stubs):
|
|
1639
|
+
self.registrar.register_overload_group(stubs)
|
|
1640
|
+
else:
|
|
1641
|
+
raise SemanticError(
|
|
1642
|
+
f"@overload stubs for '{name}' have no implementation function",
|
|
1643
|
+
stubs[0].loc,
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
@staticmethod
|
|
1647
|
+
def _reject_bodied_overloads_with_impl(
|
|
1648
|
+
stubs: list[TpyFunction], impl: TpyFunction, display_name: str,
|
|
1649
|
+
) -> None:
|
|
1650
|
+
"""Disallow mixing bodied @overload with a trailing implementation.
|
|
1651
|
+
|
|
1652
|
+
A bodied @overload variant is itself the implementation of that
|
|
1653
|
+
signature. Pairing it with another trailing impl would make dispatch
|
|
1654
|
+
ambiguous; require the author to pick one mode per group.
|
|
1655
|
+
"""
|
|
1656
|
+
for stub in stubs:
|
|
1657
|
+
if stub.is_overload_stub and not stub.is_stub:
|
|
1658
|
+
raise SemanticError(
|
|
1659
|
+
f"@overload '{display_name}' has a body and cannot be "
|
|
1660
|
+
f"paired with a trailing implementation; either remove "
|
|
1661
|
+
f"the body or remove the trailing implementation",
|
|
1662
|
+
stub.loc or impl.loc,
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
def _register_overload_group(
|
|
1666
|
+
self, impl: TpyFunction, stubs: list[TpyFunction],
|
|
1667
|
+
) -> None:
|
|
1668
|
+
"""Validate and register an @overload group.
|
|
1669
|
+
|
|
1670
|
+
- Validates stub parameter types cover all union variants in the implementation
|
|
1671
|
+
- Registers each stub as a callable FunctionInfo overload
|
|
1672
|
+
- Stores the group mapping for codegen
|
|
1673
|
+
"""
|
|
1674
|
+
self._validate_overload_signatures(impl, stubs, impl.name)
|
|
1675
|
+
|
|
1676
|
+
# Register each stub as a callable overload via a single binding
|
|
1677
|
+
self.registrar.register_overload_group(stubs)
|
|
1678
|
+
|
|
1679
|
+
# The implementation is NOT registered in the namespace/registry --
|
|
1680
|
+
# callers resolve against stubs only. The body is still analyzed
|
|
1681
|
+
# via _analyze_function (which works on the TpyFunction directly).
|
|
1682
|
+
|
|
1683
|
+
# Store the group mapping for codegen
|
|
1684
|
+
self.overload_groups[id(impl)] = stubs
|
|
1685
|
+
|
|
1686
|
+
def _validate_recursive_union_paths(self, module: TpyModule) -> None:
|
|
1687
|
+
"""For every alias tagged as recursive in `resolve_refs`, verify that
|
|
1688
|
+
every self-reference goes through an indirecting container. Runs
|
|
1689
|
+
post-registration so the field walk in `validate_recursive_union_paths`
|
|
1690
|
+
can see same-module RecordInfo / TypeDef entries.
|
|
1691
|
+
"""
|
|
1692
|
+
if not module.recursive_union_names:
|
|
1693
|
+
return
|
|
1694
|
+
from ..cycle_detection import validate_recursive_union_paths
|
|
1695
|
+
for alias_name in module.recursive_union_names:
|
|
1696
|
+
entry = module.type_aliases.get(alias_name)
|
|
1697
|
+
if entry is None:
|
|
1698
|
+
continue
|
|
1699
|
+
alias_type, alias_loc = entry[0], entry[1]
|
|
1700
|
+
if not isinstance(alias_type, UnionType):
|
|
1701
|
+
continue
|
|
1702
|
+
err = validate_recursive_union_paths(
|
|
1703
|
+
alias_name, alias_type.members, type_params=entry[2])
|
|
1704
|
+
if err is not None:
|
|
1705
|
+
raise SemanticError(err, loc=alias_loc)
|
|
1706
|
+
|
|
1707
|
+
def _detect_recursive_unions(self, module: TpyModule) -> None:
|
|
1708
|
+
"""Detect type cycles and tag union aliases as recursive.
|
|
1709
|
+
|
|
1710
|
+
Runs after all records are registered but before type alias registration.
|
|
1711
|
+
Tags aliases in module.recursive_union_names (codegen emits wrapper structs).
|
|
1712
|
+
"""
|
|
1713
|
+
# Build inputs: record fields and non-recursive union aliases
|
|
1714
|
+
record_fields: dict[str, list[tuple[str, TpyType]]] = {}
|
|
1715
|
+
for record in module.all_records():
|
|
1716
|
+
record_fields[record.name] = [(f.name, f.type) for f in record.fields]
|
|
1717
|
+
|
|
1718
|
+
union_aliases: dict[str, tuple[TpyType, ...]] = {}
|
|
1719
|
+
alias_locs: dict[str, object] = {}
|
|
1720
|
+
for name, entry in module.type_aliases.items():
|
|
1721
|
+
typ, loc = entry[0], entry[1]
|
|
1722
|
+
if isinstance(typ, UnionType) and name not in module.recursive_union_names:
|
|
1723
|
+
union_aliases[name] = typ.members
|
|
1724
|
+
alias_locs[name] = loc
|
|
1725
|
+
|
|
1726
|
+
if not union_aliases:
|
|
1727
|
+
return
|
|
1728
|
+
|
|
1729
|
+
cycles = detect_type_cycles(record_fields, union_aliases)
|
|
1730
|
+
|
|
1731
|
+
for cycle in cycles:
|
|
1732
|
+
# Validate indirection: cycle must have at least one indirected edge
|
|
1733
|
+
if cycle.is_fully_unindirected():
|
|
1734
|
+
edge = cycle.first_unindirected_edge()
|
|
1735
|
+
assert edge is not None
|
|
1736
|
+
if edge.field_name:
|
|
1737
|
+
msg = (
|
|
1738
|
+
f"Types form an infinite-size cycle: "
|
|
1739
|
+
f"field '{edge.field_name}' in '{edge.source}' references "
|
|
1740
|
+
f"'{edge.target}' without indirection -- "
|
|
1741
|
+
f"use Box[{edge.target}] or another indirecting container"
|
|
1742
|
+
)
|
|
1743
|
+
else:
|
|
1744
|
+
msg = (
|
|
1745
|
+
f"Types form an infinite-size cycle through "
|
|
1746
|
+
f"'{edge.source}' and '{edge.target}' -- "
|
|
1747
|
+
f"use Box or another indirecting container to break the cycle"
|
|
1748
|
+
)
|
|
1749
|
+
# At least one node must be an alias (cycles are reachable
|
|
1750
|
+
# from alias nodes only), so loc should never be None here.
|
|
1751
|
+
loc = alias_locs.get(edge.source) or alias_locs.get(edge.target)
|
|
1752
|
+
raise SemanticError(msg, loc)
|
|
1753
|
+
|
|
1754
|
+
# Tag union aliases in this cycle as recursive. A *generic* alias
|
|
1755
|
+
# reaching this point is recursive transitively (through a record
|
|
1756
|
+
# or another alias) rather than via a direct self-ref -- direct
|
|
1757
|
+
# generic self-refs are tagged at parse-resolution and excluded
|
|
1758
|
+
# from the cycle graph above. v1 supports only direct identity
|
|
1759
|
+
# recursion, so reject the transitive case with a clear diagnostic.
|
|
1760
|
+
for alias_name in cycle.alias_names:
|
|
1761
|
+
entry = module.type_aliases.get(alias_name)
|
|
1762
|
+
if entry is not None and entry[2]:
|
|
1763
|
+
others = [n for n in cycle.path if n != alias_name]
|
|
1764
|
+
through = f" (through {', '.join(others)})" if others else ""
|
|
1765
|
+
raise SemanticError(
|
|
1766
|
+
f"alias '{alias_name}' participates in a type "
|
|
1767
|
+
f"cycle{through}; mutual recursion across generic "
|
|
1768
|
+
f"aliases is not supported in v1",
|
|
1769
|
+
alias_locs.get(alias_name),
|
|
1770
|
+
)
|
|
1771
|
+
module.recursive_union_names.add(alias_name)
|
|
1772
|
+
|
|
1773
|
+
def _register_union_wrappers(self, module: TpyModule) -> None:
|
|
1774
|
+
"""Register this module's recursive union aliases in the compiler-wide
|
|
1775
|
+
`union_wrapper_index`. The index is keyed by the canonical member tuple
|
|
1776
|
+
(and, when None is a direct member, also by the non-None subset) so a
|
|
1777
|
+
narrowed-by-None type still resolves to the same wrapper.
|
|
1778
|
+
|
|
1779
|
+
Sema is the authoritative source: each module's pass writes its own
|
|
1780
|
+
entries with `setdefault` (idempotent), accumulating across the
|
|
1781
|
+
compilation. Downstream lookups go through `UnionType.wrapper_info()`
|
|
1782
|
+
/ `UnionType.needs_wrapper()`.
|
|
1783
|
+
"""
|
|
1784
|
+
if not module.recursive_union_names:
|
|
1785
|
+
return
|
|
1786
|
+
compiler = get_current_compiler()
|
|
1787
|
+
if compiler is None:
|
|
1788
|
+
return
|
|
1789
|
+
index = compiler.union_wrapper_index
|
|
1790
|
+
# Use the codegen-flavor (file-derived) name -- the entry point's
|
|
1791
|
+
# ctx.module_name is "__main__" for runtime semantics, but
|
|
1792
|
+
# codegen filters `info.origin == self.module_name` against the
|
|
1793
|
+
# file name. They must agree.
|
|
1794
|
+
module_name = self.ctx.cpp_module_name
|
|
1795
|
+
for alias_name in module.recursive_union_names:
|
|
1796
|
+
entry = module.type_aliases.get(alias_name)
|
|
1797
|
+
if entry is None:
|
|
1798
|
+
continue
|
|
1799
|
+
typ = entry[0]
|
|
1800
|
+
if not isinstance(typ, UnionType):
|
|
1801
|
+
continue
|
|
1802
|
+
# Generic recursive aliases are keyed by their on-type alias_info
|
|
1803
|
+
# (RecursiveAliasInstanceType), not the member-tuple index -- their
|
|
1804
|
+
# members carry unbound TypeParamRefs that must not leak into the
|
|
1805
|
+
# concrete-union index.
|
|
1806
|
+
if entry[2]:
|
|
1807
|
+
continue
|
|
1808
|
+
info = RecursiveUnionInfo(
|
|
1809
|
+
name=alias_name, full_members=typ.members, origin=module_name,
|
|
1810
|
+
)
|
|
1811
|
+
index.setdefault(typ.members, info)
|
|
1812
|
+
non_none = tuple(m for m in typ.members if not is_void_like_type(m))
|
|
1813
|
+
if len(non_none) < len(typ.members):
|
|
1814
|
+
index.setdefault(non_none, info)
|
|
1815
|
+
|
|
1816
|
+
def _validate_record_method_linkage(self, module: TpyModule) -> None:
|
|
1817
|
+
"""Check per-record linkage rules against each method:
|
|
1818
|
+
@native classes require stub bodies; regular classes disallow
|
|
1819
|
+
stub bodies (except @overload stubs) and @native("...") decorators.
|
|
1820
|
+
|
|
1821
|
+
Runs after `parse.resolve_refs` so base-resolution
|
|
1822
|
+
errors fire first (previously these checks ran at parse time
|
|
1823
|
+
and could mask those errors).
|
|
1824
|
+
"""
|
|
1825
|
+
for record in module.all_records():
|
|
1826
|
+
for method in record.methods:
|
|
1827
|
+
# Use record.loc so the diagnostic points at the class header.
|
|
1828
|
+
if record.linkage != RecordLinkage.DEFAULT:
|
|
1829
|
+
if not method.is_stub:
|
|
1830
|
+
raise SemanticError(
|
|
1831
|
+
"Methods on @native classes must have '...' "
|
|
1832
|
+
"body (stub declaration)",
|
|
1833
|
+
loc=record.loc,
|
|
1834
|
+
)
|
|
1835
|
+
else:
|
|
1836
|
+
if method.is_stub and not method.is_overload_stub:
|
|
1837
|
+
raise SemanticError(
|
|
1838
|
+
f"Method '{method.name}' cannot have '...' body "
|
|
1839
|
+
f"on a regular class (only allowed on @native classes)",
|
|
1840
|
+
loc=record.loc,
|
|
1841
|
+
)
|
|
1842
|
+
if method.native_name is not None:
|
|
1843
|
+
raise SemanticError(
|
|
1844
|
+
f"@native(\"...\") decorator on method '{method.name}' "
|
|
1845
|
+
f"is only allowed on @native classes",
|
|
1846
|
+
loc=record.loc,
|
|
1847
|
+
)
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
def _fix_recursive_optional_annotations(self, module: TpyModule) -> None:
|
|
1851
|
+
"""Fix annotations where a recursive union alias + None was flattened.
|
|
1852
|
+
|
|
1853
|
+
The parser eagerly expands same-module aliases, so `Expr | None`
|
|
1854
|
+
(where `Expr = Lit | BinOp`) becomes UnionType(NoneType, Lit, BinOp).
|
|
1855
|
+
This pass converts that back to OptionalType(AliasRef("Expr")) by
|
|
1856
|
+
matching the non-None members against the alias definitions.
|
|
1857
|
+
|
|
1858
|
+
When the alias's own definition already includes None (e.g.
|
|
1859
|
+
`JsonValue = None | bool | ... | list[JsonValue]`), a bare reference
|
|
1860
|
+
expands the same way -- but the input is the alias itself, not
|
|
1861
|
+
`Alias | None`. The type stays as the full UnionType so downstream
|
|
1862
|
+
passes (is-None narrowing, match dispatch with `case None:`) keep
|
|
1863
|
+
the NoneType member visible.
|
|
1864
|
+
|
|
1865
|
+
Only covers module-level declarations (function signatures, record
|
|
1866
|
+
fields, top-level vars); local annotations are resolved during body
|
|
1867
|
+
analysis.
|
|
1868
|
+
"""
|
|
1869
|
+
# Build reverse map: frozenset(alias non-None members) -> (alias name, alias_has_none)
|
|
1870
|
+
alias_by_members: dict[frozenset, tuple[str, bool]] = {}
|
|
1871
|
+
for name in module.recursive_union_names:
|
|
1872
|
+
entry = module.type_aliases.get(name)
|
|
1873
|
+
if entry is not None:
|
|
1874
|
+
# Generic recursive aliases have parameterized use sites
|
|
1875
|
+
# (RecursiveAliasInstanceType), never a bare expanded union, so
|
|
1876
|
+
# the member-set reverse map does not apply -- and their members
|
|
1877
|
+
# carry TypeParamRefs that would mis-key it.
|
|
1878
|
+
if entry[2]:
|
|
1879
|
+
continue
|
|
1880
|
+
typ = entry[0] # entry[0] is the body type
|
|
1881
|
+
if isinstance(typ, UnionType):
|
|
1882
|
+
alias_has_none = any(
|
|
1883
|
+
is_void_like_type(m) for m in typ.members
|
|
1884
|
+
)
|
|
1885
|
+
non_none = frozenset(
|
|
1886
|
+
m for m in typ.members
|
|
1887
|
+
if not is_void_like_type(m)
|
|
1888
|
+
)
|
|
1889
|
+
alias_by_members[non_none] = (name, alias_has_none)
|
|
1890
|
+
|
|
1891
|
+
if not alias_by_members:
|
|
1892
|
+
return
|
|
1893
|
+
|
|
1894
|
+
def _fix(typ: TpyType) -> TpyType:
|
|
1895
|
+
if not isinstance(typ, UnionType):
|
|
1896
|
+
return typ.map_inner_types(lambda t: _fix(t))
|
|
1897
|
+
non_none = [m for m in typ.members if not is_void_like_type(m)]
|
|
1898
|
+
if len(non_none) == len(typ.members):
|
|
1899
|
+
return typ.map_inner_types(lambda t: _fix(t))
|
|
1900
|
+
entry = alias_by_members.get(frozenset(non_none))
|
|
1901
|
+
if entry is None:
|
|
1902
|
+
return typ.map_inner_types(lambda t: _fix(t))
|
|
1903
|
+
alias_name, alias_has_none = entry
|
|
1904
|
+
if alias_has_none:
|
|
1905
|
+
return typ
|
|
1906
|
+
return OptionalType(AliasRef(alias_name, module=self.ctx.module_name))
|
|
1907
|
+
|
|
1908
|
+
def _fix_func(func: TpyFunction) -> None:
|
|
1909
|
+
for i, (pname, typ) in enumerate(func.params):
|
|
1910
|
+
fixed = _fix(typ)
|
|
1911
|
+
if fixed is not typ:
|
|
1912
|
+
func.params[i] = (pname, fixed)
|
|
1913
|
+
if func.return_type is not None:
|
|
1914
|
+
fixed = _fix(func.return_type)
|
|
1915
|
+
if fixed is not func.return_type:
|
|
1916
|
+
func.return_type = fixed
|
|
1917
|
+
|
|
1918
|
+
for func in module.functions:
|
|
1919
|
+
_fix_func(func)
|
|
1920
|
+
for record in module.all_records():
|
|
1921
|
+
for f in record.fields:
|
|
1922
|
+
fixed = _fix(f.type)
|
|
1923
|
+
if fixed is not f.type:
|
|
1924
|
+
f.type = fixed
|
|
1925
|
+
for method in record.methods:
|
|
1926
|
+
_fix_func(method)
|
|
1927
|
+
if module.top_level_stmts:
|
|
1928
|
+
for stmt in module.top_level_stmts:
|
|
1929
|
+
if isinstance(stmt, TpyVarDecl) and stmt.type is not None:
|
|
1930
|
+
fixed = _fix(stmt.type)
|
|
1931
|
+
if fixed is not stmt.type:
|
|
1932
|
+
stmt.type = fixed
|
|
1933
|
+
|
|
1934
|
+
def _validate_type_alias_members(
|
|
1935
|
+
self, alias_name: str, typ: TpyType, loc: 'SourceLocation | None'
|
|
1936
|
+
) -> None:
|
|
1937
|
+
"""Validate that all NominalType members in a type alias are registered."""
|
|
1938
|
+
# Recursive union aliases have self-referencing NominalType placeholders
|
|
1939
|
+
# inside their members -- safety was already validated.
|
|
1940
|
+
if alias_name in self.ctx.recursive_union_names:
|
|
1941
|
+
return
|
|
1942
|
+
members: list[TpyType] = []
|
|
1943
|
+
if isinstance(typ, UnionType):
|
|
1944
|
+
members = list(typ.members)
|
|
1945
|
+
elif isinstance(typ, NominalType):
|
|
1946
|
+
members = [typ]
|
|
1947
|
+
for m in members:
|
|
1948
|
+
# Bare NominalType reference in an alias body -- the registry
|
|
1949
|
+
# lookup is the actual check. Exclude resolved types (which have
|
|
1950
|
+
# a _module_qname) and protocols.
|
|
1951
|
+
if (isinstance(m, NominalType) and not m.is_protocol
|
|
1952
|
+
and not m._module_qname):
|
|
1953
|
+
if self.ctx.registry.get_record(m.name) is None:
|
|
1954
|
+
raise SemanticError(
|
|
1955
|
+
f"Type alias '{alias_name}' references unknown type '{m.name}'",
|
|
1956
|
+
loc,
|
|
1957
|
+
)
|
|
1958
|
+
|
|
1959
|
+
@staticmethod
|
|
1960
|
+
def _resolve_alias(typ: TpyType, aliases: dict[str, TpyType],
|
|
1961
|
+
_seen: frozenset[str] = frozenset(),
|
|
1962
|
+
_skip: frozenset[str] = frozenset()) -> TpyType:
|
|
1963
|
+
"""Recursively substitute alias NominalTypes with their resolved types.
|
|
1964
|
+
|
|
1965
|
+
Uses _seen to prevent infinite recursion on self-referencing aliases.
|
|
1966
|
+
_skip contains recursive union alias names that must not be expanded
|
|
1967
|
+
(their NominalType placeholders are structural).
|
|
1968
|
+
"""
|
|
1969
|
+
# Alias placeholders are bare parser NominalTypes (no _module_qname,
|
|
1970
|
+
# no TypeDef entry). Exclude protocols and anything already resolved.
|
|
1971
|
+
if (isinstance(typ, NominalType) and not typ.is_protocol
|
|
1972
|
+
and not typ._module_qname):
|
|
1973
|
+
if typ.name in _seen or typ.name in _skip:
|
|
1974
|
+
return typ
|
|
1975
|
+
resolved = aliases.get(typ.name)
|
|
1976
|
+
if resolved is not None:
|
|
1977
|
+
new_seen = _seen | {typ.name}
|
|
1978
|
+
return resolved.map_inner_types(
|
|
1979
|
+
lambda t: SemanticAnalyzer._resolve_alias(t, aliases, new_seen, _skip)
|
|
1980
|
+
)
|
|
1981
|
+
return typ.map_inner_types(
|
|
1982
|
+
lambda t: SemanticAnalyzer._resolve_alias(t, aliases, _seen, _skip)
|
|
1983
|
+
)
|
|
1984
|
+
|
|
1985
|
+
def _resolve_imported_aliases(self, module: TpyModule) -> None:
|
|
1986
|
+
"""Substitute imported alias NominalTypes in module AST type annotations."""
|
|
1987
|
+
# Exclude generic aliases from the substitution pool: their bodies
|
|
1988
|
+
# contain unbound TypeParamRefs that would leak into annotations if
|
|
1989
|
+
# _resolve_alias rewrote a bare NominalType(name) placeholder.
|
|
1990
|
+
# Generic alias use sites go through the parse-resolution path
|
|
1991
|
+
# (`_resolve_generic_alias_use`) which substitutes correctly with
|
|
1992
|
+
# the supplied type args.
|
|
1993
|
+
aliases = {
|
|
1994
|
+
n: info.body
|
|
1995
|
+
for n, info in self.ctx.registry.type_aliases.items()
|
|
1996
|
+
if not info.type_params
|
|
1997
|
+
}
|
|
1998
|
+
skip = frozenset(module.recursive_union_names)
|
|
1999
|
+
for func in module.functions:
|
|
2000
|
+
self._resolve_func_aliases(func, aliases, skip)
|
|
2001
|
+
for record in module.all_records():
|
|
2002
|
+
for f in record.fields:
|
|
2003
|
+
f.type = self._resolve_alias(f.type, aliases, _skip=skip)
|
|
2004
|
+
for method in record.methods:
|
|
2005
|
+
self._resolve_func_aliases(method, aliases, skip)
|
|
2006
|
+
for stmt in module.top_level_stmts:
|
|
2007
|
+
if isinstance(stmt, TpyVarDecl) and stmt.type is not None:
|
|
2008
|
+
stmt.type = self._resolve_alias(stmt.type, aliases, _skip=skip)
|
|
2009
|
+
|
|
2010
|
+
@staticmethod
|
|
2011
|
+
def _resolve_func_aliases(func: TpyFunction, aliases: dict[str, TpyType],
|
|
2012
|
+
skip: frozenset[str] = frozenset()) -> None:
|
|
2013
|
+
"""Resolve alias types in a function's signature."""
|
|
2014
|
+
if func.return_type is not None:
|
|
2015
|
+
func.return_type = SemanticAnalyzer._resolve_alias(func.return_type, aliases, _skip=skip)
|
|
2016
|
+
for i, (name, typ) in enumerate(func.params):
|
|
2017
|
+
resolved = SemanticAnalyzer._resolve_alias(typ, aliases, _skip=skip)
|
|
2018
|
+
if resolved is not typ:
|
|
2019
|
+
func.params[i] = (name, resolved)
|
|
2020
|
+
|
|
2021
|
+
def _alias_lookup_for_finalize(
|
|
2022
|
+
self, name: str,
|
|
2023
|
+
) -> 'tuple[TypeAliasInfo, str, str] | None':
|
|
2024
|
+
"""Resolve a referenced alias name to (info, qname, short_name),
|
|
2025
|
+
covering both local and imported aliases, or None if not an alias.
|
|
2026
|
+
|
|
2027
|
+
The qname is the defining module + the alias's original name --
|
|
2028
|
+
collision-proof across modules that define same-short-named aliases,
|
|
2029
|
+
and consistent whether the alias is referenced from its own module or
|
|
2030
|
+
an importer (both resolve to the defining module's name).
|
|
2031
|
+
|
|
2032
|
+
Imported aliases are checked first: `from m import Tree` also registers
|
|
2033
|
+
a local entry, so a local-first lookup would mis-stamp the importer's
|
|
2034
|
+
module name onto the qname (diverging from the defining module's body,
|
|
2035
|
+
which the importer inherits via the shared alias_info)."""
|
|
2036
|
+
imp = self.ctx.registry.imported_type_alias_info.get(name)
|
|
2037
|
+
if imp is not None:
|
|
2038
|
+
decl_mod, orig = imp
|
|
2039
|
+
mod_info = self.ctx.registry.modules.get(decl_mod)
|
|
2040
|
+
iinfo = mod_info.type_aliases.get(orig) if mod_info is not None else None
|
|
2041
|
+
if iinfo is None:
|
|
2042
|
+
iinfo = self.ctx.registry.get_type_alias_info(name)
|
|
2043
|
+
if iinfo is not None:
|
|
2044
|
+
return iinfo, f"{decl_mod}.{orig}", orig
|
|
2045
|
+
info = self.ctx.registry.get_type_alias_info(name)
|
|
2046
|
+
if info is not None:
|
|
2047
|
+
return info, f"{self.ctx.module_name}.{name}", name
|
|
2048
|
+
return None
|
|
2049
|
+
|
|
2050
|
+
def _finalize_alias_refs(self, typ: TpyType) -> TpyType:
|
|
2051
|
+
"""Rewrite a generic-recursive `AliasRef` placeholder into the
|
|
2052
|
+
semantic `RecursiveAliasInstanceType`, recursing through inner types.
|
|
2053
|
+
|
|
2054
|
+
Non-generic recursive aliases keep their bare `AliasRef` self-ref
|
|
2055
|
+
(their use sites stay UnionType + union_wrapper_index)."""
|
|
2056
|
+
if isinstance(typ, AliasRef):
|
|
2057
|
+
looked = self._alias_lookup_for_finalize(typ.name)
|
|
2058
|
+
if (looked is not None and looked[0].type_params
|
|
2059
|
+
and looked[0].is_recursive):
|
|
2060
|
+
info, qname, _short = looked
|
|
2061
|
+
new_args = tuple(
|
|
2062
|
+
self._finalize_alias_refs(a) if isinstance(a, TpyType) else a
|
|
2063
|
+
for a in typ.args
|
|
2064
|
+
)
|
|
2065
|
+
# Render via the *local* reference name (`typ.name`) -- codegen
|
|
2066
|
+
# keys native_cpp_names by the importing name (e.g. `from m
|
|
2067
|
+
# import Tree as MyTree`), exactly as cross-module records do,
|
|
2068
|
+
# so two same-short-named aliases imported into one module do
|
|
2069
|
+
# not collide on rendering. Identity stays the qname.
|
|
2070
|
+
return RecursiveAliasInstanceType(qname, new_args, typ.name, info)
|
|
2071
|
+
return typ.map_inner_types(self._finalize_alias_refs)
|
|
2072
|
+
|
|
2073
|
+
def _finalize_generic_recursive_aliases(self, module: TpyModule) -> None:
|
|
2074
|
+
"""Convert the parser's generic-recursive `AliasRef` placeholders into
|
|
2075
|
+
`RecursiveAliasInstanceType` across alias bodies and all annotation
|
|
2076
|
+
slots, using the sema-registry alias_info (parse-resolve runs against
|
|
2077
|
+
a separate registry and cannot mint the semantic type).
|
|
2078
|
+
|
|
2079
|
+
Single conversion point so use-site instances and recursive children
|
|
2080
|
+
share one alias_info source. Runs both for locally-defined generic
|
|
2081
|
+
recursive aliases (whose bodies get finalized) and for modules that
|
|
2082
|
+
merely *use* an imported one (use sites still need conversion). See
|
|
2083
|
+
docs/GENERIC_RECURSIVE_ALIASES_DESIGN.md."""
|
|
2084
|
+
generic_rec = {
|
|
2085
|
+
n for n in module.recursive_union_names
|
|
2086
|
+
if (gi := self.ctx.registry.get_type_alias_info(n)) is not None
|
|
2087
|
+
and gi.type_params
|
|
2088
|
+
}
|
|
2089
|
+
imports_generic_rec = any(
|
|
2090
|
+
(lk := self._alias_lookup_for_finalize(n)) is not None
|
|
2091
|
+
and lk[0].type_params and lk[0].is_recursive
|
|
2092
|
+
for n in self.ctx.registry.imported_type_alias_info
|
|
2093
|
+
)
|
|
2094
|
+
if not generic_rec and not imports_generic_rec:
|
|
2095
|
+
return
|
|
2096
|
+
# Alias bodies: the registered TypeAliasInfo (read by alternatives
|
|
2097
|
+
# expansion) and the cross-phase tuple (read by codegen + export).
|
|
2098
|
+
for n in generic_rec:
|
|
2099
|
+
info = self.ctx.registry.get_type_alias_info(n)
|
|
2100
|
+
info.body = self._finalize_alias_refs(info.body)
|
|
2101
|
+
entry = module.type_aliases.get(n)
|
|
2102
|
+
if entry is not None:
|
|
2103
|
+
module.type_aliases[n] = (
|
|
2104
|
+
(self._finalize_alias_refs(entry[0]),) + tuple(entry[1:])
|
|
2105
|
+
)
|
|
2106
|
+
# Use sites in annotations.
|
|
2107
|
+
for func in module.functions:
|
|
2108
|
+
self._finalize_func_aliases(func)
|
|
2109
|
+
for record in module.all_records():
|
|
2110
|
+
for f in record.fields:
|
|
2111
|
+
f.type = self._finalize_alias_refs(f.type)
|
|
2112
|
+
for method in record.methods:
|
|
2113
|
+
self._finalize_func_aliases(method)
|
|
2114
|
+
# RecordInfo carries registration-time snapshots of the ctor and
|
|
2115
|
+
# method signatures, built before this pass, so their alias
|
|
2116
|
+
# placeholders are stale and must be finalized here too. (Free
|
|
2117
|
+
# functions register after this pass, so their FunctionInfo is
|
|
2118
|
+
# already finalized; protocol signatures are finalized below.)
|
|
2119
|
+
info = self.ctx.registry.get_record(record.name)
|
|
2120
|
+
if info is not None:
|
|
2121
|
+
if info.init_params:
|
|
2122
|
+
info.init_params = [
|
|
2123
|
+
(n, self._finalize_alias_refs(t), d)
|
|
2124
|
+
for (n, t, d) in info.init_params
|
|
2125
|
+
]
|
|
2126
|
+
for overloads in info.methods.values():
|
|
2127
|
+
for fi in overloads:
|
|
2128
|
+
if fi.return_type is not None:
|
|
2129
|
+
fi.return_type = self._finalize_alias_refs(fi.return_type)
|
|
2130
|
+
for pi in fi.params:
|
|
2131
|
+
pi.type = self._finalize_alias_refs(pi.type)
|
|
2132
|
+
# Protocol method signatures are another registration-time snapshot:
|
|
2133
|
+
# the registry ProtocolInfo (shared with the TypeDef payload) is read
|
|
2134
|
+
# by conformance + protocol-call dispatch; the AST copy is separate.
|
|
2135
|
+
for protocol in module.protocols:
|
|
2136
|
+
self._finalize_method_signatures(protocol.methods)
|
|
2137
|
+
pinfo = self.ctx.registry.scan_by_short_name(protocol.name)
|
|
2138
|
+
if pinfo is not None:
|
|
2139
|
+
self._finalize_method_signatures(pinfo.methods)
|
|
2140
|
+
if module.top_level_stmts:
|
|
2141
|
+
self._finalize_stmts_aliases(module.top_level_stmts)
|
|
2142
|
+
|
|
2143
|
+
def _finalize_method_signatures(self, methods: list[MethodSignature]) -> None:
|
|
2144
|
+
for msig in methods:
|
|
2145
|
+
for i, (name, typ) in enumerate(msig.params):
|
|
2146
|
+
new = self._finalize_alias_refs(typ)
|
|
2147
|
+
if new is not typ:
|
|
2148
|
+
msig.params[i] = (name, new)
|
|
2149
|
+
if msig.return_type is not None:
|
|
2150
|
+
msig.return_type = self._finalize_alias_refs(msig.return_type)
|
|
2151
|
+
|
|
2152
|
+
def _finalize_func_aliases(self, func: TpyFunction) -> None:
|
|
2153
|
+
if func.return_type is not None:
|
|
2154
|
+
func.return_type = self._finalize_alias_refs(func.return_type)
|
|
2155
|
+
for i, (name, typ) in enumerate(func.params):
|
|
2156
|
+
new = self._finalize_alias_refs(typ)
|
|
2157
|
+
if new is not typ:
|
|
2158
|
+
func.params[i] = (name, new)
|
|
2159
|
+
if func.vararg_type is not None:
|
|
2160
|
+
func.vararg_type = self._finalize_alias_refs(func.vararg_type)
|
|
2161
|
+
if func.kwarg_type is not None:
|
|
2162
|
+
func.kwarg_type = self._finalize_alias_refs(func.kwarg_type)
|
|
2163
|
+
if func.body:
|
|
2164
|
+
self._finalize_stmts_aliases(func.body)
|
|
2165
|
+
|
|
2166
|
+
def _finalize_stmts_aliases(self, stmts: 'list') -> None:
|
|
2167
|
+
"""Convert generic-recursive AliasRef placeholders in local variable
|
|
2168
|
+
annotations. Local annotations are resolved at parse-resolution (via
|
|
2169
|
+
resolve_refs `_walk_body`), so they carry the same placeholders as
|
|
2170
|
+
signatures and must be finalized too."""
|
|
2171
|
+
for stmt in stmts:
|
|
2172
|
+
if isinstance(stmt, TpyVarDecl) and stmt.type is not None:
|
|
2173
|
+
stmt.type = self._finalize_alias_refs(stmt.type)
|
|
2174
|
+
if isinstance(stmt, TpyNestedDef):
|
|
2175
|
+
self._finalize_func_aliases(stmt.func)
|
|
2176
|
+
continue
|
|
2177
|
+
for body in stmt.sub_bodies():
|
|
2178
|
+
self._finalize_stmts_aliases(body)
|
|
2179
|
+
|
|
2180
|
+
def _analyze_class_constants(self, record: TpyRecord) -> None:
|
|
2181
|
+
"""Analyze each class constant's initializer and validate it is a
|
|
2182
|
+
compile-time constant. Earlier constants in declaration order are
|
|
2183
|
+
bound into the class-body namespace so later constants can reference
|
|
2184
|
+
them (e.g. `B: Final[int] = A + 1`).
|
|
2185
|
+
"""
|
|
2186
|
+
record_info = self.ctx.registry.get_record(record.name)
|
|
2187
|
+
if record_info is None or not record_info.class_constants:
|
|
2188
|
+
return
|
|
2189
|
+
|
|
2190
|
+
# Class-body context: synthetic module-init scope so the const-validity
|
|
2191
|
+
# check (`_find_nonconstant_leaf`) accepts Final names via
|
|
2192
|
+
# `analyzed_finals`, and so name resolution inherits module-level
|
|
2193
|
+
# bindings. Class-constant names get added to scope/ns as analysis
|
|
2194
|
+
# progresses (so later constants can forward-ref earlier ones).
|
|
2195
|
+
self.ctx.reset_function_tracking()
|
|
2196
|
+
self.ctx.func.current_function = MODULE_INIT_CONTEXT
|
|
2197
|
+
self.ctx.func.current_scope = Scope(parent=self.ctx.global_scope)
|
|
2198
|
+
self.ctx.func.current_ns = Namespace(parent=self.ctx.global_ns)
|
|
2199
|
+
self.ctx.is_top_level = True
|
|
2200
|
+
|
|
2201
|
+
# Class-constant names get added to `analyzed_finals` so the
|
|
2202
|
+
# const-validity check accepts forward refs. Track adds and roll
|
|
2203
|
+
# back at the end -- avoids cloning the full module Finals set.
|
|
2204
|
+
added_finals: list[str] = []
|
|
2205
|
+
|
|
2206
|
+
try:
|
|
2207
|
+
for cc_name, cc_fld in record_info.class_constants.items():
|
|
2208
|
+
if cc_fld.default_expr is None:
|
|
2209
|
+
# @native extern binding: no initializer to analyze.
|
|
2210
|
+
continue
|
|
2211
|
+
self.expr.analyze_expr_with_hint(cc_fld.default_expr, cc_fld.type)
|
|
2212
|
+
self.stmts.validate_compile_time_constant(
|
|
2213
|
+
cc_fld.default_expr, cc_fld.type,
|
|
2214
|
+
f"class constant '{record.name}.{cc_name}'",
|
|
2215
|
+
cc_fld.loc,
|
|
2216
|
+
)
|
|
2217
|
+
# Bind so later class constants in the same body can reference it.
|
|
2218
|
+
self.ctx.func.current_scope.define(cc_name, cc_fld.type)
|
|
2219
|
+
self.ctx.func.current_ns.bind_variable(cc_name, cc_fld.type)
|
|
2220
|
+
self.ctx.analyzed_finals.add(cc_name)
|
|
2221
|
+
added_finals.append(cc_name)
|
|
2222
|
+
finally:
|
|
2223
|
+
self.ctx.analyzed_finals.difference_update(added_finals)
|
|
2224
|
+
self.ctx.func.current_function = None
|
|
2225
|
+
self.ctx.func.current_scope = None
|
|
2226
|
+
self.ctx.func.current_ns = None
|
|
2227
|
+
self.ctx.is_top_level = False
|
|
2228
|
+
|
|
2229
|
+
def _analyze_record_methods(self, record: TpyRecord) -> None:
|
|
2230
|
+
"""Analyze all methods of a record."""
|
|
2231
|
+
# Set up record context for super() support and generic type params
|
|
2232
|
+
self.ctx.record_ctx.record = record
|
|
2233
|
+
self.ctx.record_ctx.type_params = record.type_params if record.type_params else None
|
|
2234
|
+
self.ctx.record_ctx.type_param_kinds = record.type_param_kinds if record.type_param_kinds else None
|
|
2235
|
+
self.ctx.record_ctx.type_param_bounds = record.type_param_bounds if record.type_param_bounds else None
|
|
2236
|
+
|
|
2237
|
+
# Collect method overload groups for this record
|
|
2238
|
+
self._collect_method_overload_groups(record)
|
|
2239
|
+
|
|
2240
|
+
for method in record.methods:
|
|
2241
|
+
# Skip bodyless @overload stubs -- their trailing impl is analyzed
|
|
2242
|
+
# instead. Bodied @overload stubs (mode b) need body analysis just
|
|
2243
|
+
# like regular methods.
|
|
2244
|
+
if method.is_overload_stub and method.is_stub:
|
|
2245
|
+
continue
|
|
2246
|
+
|
|
2247
|
+
self.ctx.reset_function_tracking()
|
|
2248
|
+
self.ctx.func.current_function = method
|
|
2249
|
+
# Resolve return type (sets is_protocol for cross-module imports)
|
|
2250
|
+
method.return_type = make_ref(self.type_ops.resolve_type(method.return_type))
|
|
2251
|
+
scope = Scope(parent=self.ctx.global_scope)
|
|
2252
|
+
self.ctx.func.current_scope = scope
|
|
2253
|
+
|
|
2254
|
+
local_ns = Namespace(parent=self.ctx.global_ns)
|
|
2255
|
+
self.ctx.func.current_ns = local_ns
|
|
2256
|
+
if not method.is_staticmethod:
|
|
2257
|
+
info = self.ctx.registry.get_record(record.name)
|
|
2258
|
+
self_named = build_record_self_type(
|
|
2259
|
+
record,
|
|
2260
|
+
qname=info.qualified_name() if info is not None else None,
|
|
2261
|
+
)
|
|
2262
|
+
self_type = self._normalize_param_type(self_named, method.is_readonly)
|
|
2263
|
+
scope.define("self", self_type)
|
|
2264
|
+
self.ctx.func.var_scope_depth["self"] = scope.depth
|
|
2265
|
+
self.ctx.func.definitely_assigned.add("self")
|
|
2266
|
+
local_ns.bind_variable("self", self_type)
|
|
2267
|
+
|
|
2268
|
+
# Resolve and normalize params (@readonly wraps all non-value params).
|
|
2269
|
+
# Write back to AST so codegen sees Ref/ReadonlyType (codegen reads func.params directly, not FI).
|
|
2270
|
+
# For auto_readonly_params_resolved methods, the parser clone already applied
|
|
2271
|
+
# ReadonlyType to the params that need it -- skip blanket wrapping.
|
|
2272
|
+
readonly_ctx = method.is_readonly and not method.auto_readonly_params_resolved
|
|
2273
|
+
resolved_params: list[tuple[str, TpyType]] = []
|
|
2274
|
+
for i, (pname, ptype) in enumerate(method.params):
|
|
2275
|
+
resolved_ptype = self._normalize_param_type(
|
|
2276
|
+
self.type_ops.resolve_type(ptype), readonly_ctx)
|
|
2277
|
+
method.params[i] = (pname, make_ref(resolved_ptype))
|
|
2278
|
+
resolved_params.append((pname, resolved_ptype))
|
|
2279
|
+
|
|
2280
|
+
# Add *args parameter as Span[readonly[T]] to method.params and resolved_params
|
|
2281
|
+
if method.vararg_name is not None and method.vararg_type is not None:
|
|
2282
|
+
va_type = _vararg_span_type(self.type_ops.resolve_type(method.vararg_type))
|
|
2283
|
+
kw_start = method.keyword_only_start
|
|
2284
|
+
if kw_start is not None and kw_start < len(method.params):
|
|
2285
|
+
method.params.insert(kw_start, (method.vararg_name, make_ref(va_type)))
|
|
2286
|
+
resolved_params.insert(kw_start, (method.vararg_name, va_type))
|
|
2287
|
+
if method.defaults:
|
|
2288
|
+
method.defaults.insert(kw_start, None)
|
|
2289
|
+
method.keyword_only_start = kw_start + 1
|
|
2290
|
+
else:
|
|
2291
|
+
method.params.append((method.vararg_name, make_ref(va_type)))
|
|
2292
|
+
resolved_params.append((method.vararg_name, va_type))
|
|
2293
|
+
if method.defaults:
|
|
2294
|
+
method.defaults.append(None)
|
|
2295
|
+
method.vararg_type = None
|
|
2296
|
+
|
|
2297
|
+
# Add **kwargs parameter as TypedDict type
|
|
2298
|
+
if method.kwarg_name is not None and method.kwarg_type is not None:
|
|
2299
|
+
kw_type = self.type_ops.resolve_type(method.kwarg_type)
|
|
2300
|
+
method.params.append((method.kwarg_name, make_ref(kw_type)))
|
|
2301
|
+
resolved_params.append((method.kwarg_name, kw_type))
|
|
2302
|
+
if method.defaults:
|
|
2303
|
+
method.defaults.append(None)
|
|
2304
|
+
method.kwarg_type = None
|
|
2305
|
+
|
|
2306
|
+
# __next__ must have an explicit non-void return type annotation
|
|
2307
|
+
if method.name == "__next__" and isinstance(method.return_type, VoidType):
|
|
2308
|
+
raise self._error(
|
|
2309
|
+
"__next__ method must have a return type annotation",
|
|
2310
|
+
method
|
|
2311
|
+
)
|
|
2312
|
+
|
|
2313
|
+
# @inline methods: skip body analysis. The body is a template that
|
|
2314
|
+
# gets cloned and substituted at each call site (see methods.py
|
|
2315
|
+
# _inline_method_call).
|
|
2316
|
+
if method.is_inline and not method.is_stub:
|
|
2317
|
+
method.skip_codegen = True
|
|
2318
|
+
continue
|
|
2319
|
+
|
|
2320
|
+
# Track consuming method for ownership propagation through fields
|
|
2321
|
+
prev_consuming = self.ctx.in_consuming_method
|
|
2322
|
+
self.ctx.in_consuming_method = method.is_consuming
|
|
2323
|
+
|
|
2324
|
+
self._validate_named_defaults(method)
|
|
2325
|
+
|
|
2326
|
+
# Shared core: bind params, prescan, analyze body
|
|
2327
|
+
scan = self.stmts._prescan_and_analyze_body(method, resolved_params, scope, local_ns)
|
|
2328
|
+
|
|
2329
|
+
# Collect generator / async-coro local variables for struct
|
|
2330
|
+
# field generation (locals that may live across yield / await
|
|
2331
|
+
# suspensions are hoisted to the resumable-frame struct).
|
|
2332
|
+
if method.is_generator or method.is_async:
|
|
2333
|
+
param_names = {pname for pname, _ in method.params}
|
|
2334
|
+
locals_dict: dict[str, 'TpyType'] = {}
|
|
2335
|
+
for name, binding in local_ns.all_bindings().items():
|
|
2336
|
+
if name not in param_names and name != "self" and binding.type is not None:
|
|
2337
|
+
locals_dict[name] = binding.type
|
|
2338
|
+
for name, (vtype, _, _) in self.ctx.func.pending_loop_vars.items():
|
|
2339
|
+
if name not in param_names and name != "self" and vtype is not None:
|
|
2340
|
+
locals_dict[name] = vtype
|
|
2341
|
+
method.generator_locals = list(locals_dict.items())
|
|
2342
|
+
|
|
2343
|
+
# Finalize nested def escape analysis (same as _analyze_function)
|
|
2344
|
+
self._finalize_nested_def_escapes()
|
|
2345
|
+
|
|
2346
|
+
self.ctx.in_consuming_method = prev_consuming
|
|
2347
|
+
|
|
2348
|
+
# Warn if __next__ has no raise StopIteration (likely infinite loop)
|
|
2349
|
+
# Skip for native methods (cpp_template set) -- StopIteration handled in C++
|
|
2350
|
+
if (method.name == "__next__" and not method.cpp_template
|
|
2351
|
+
and not _body_has_raise(method.body, "builtins.StopIteration")):
|
|
2352
|
+
self.ctx.warning(
|
|
2353
|
+
"__next__() has no 'raise StopIteration' -- "
|
|
2354
|
+
"iterator will loop forever if caller exhausts it",
|
|
2355
|
+
method
|
|
2356
|
+
)
|
|
2357
|
+
|
|
2358
|
+
# Check for field assignments inside control flow in __init__
|
|
2359
|
+
if method.name == "__init__":
|
|
2360
|
+
self._check_init_field_assignments(method, record)
|
|
2361
|
+
|
|
2362
|
+
# Validate super().__init__() position in __init__ methods
|
|
2363
|
+
if method.name == "__init__" and self.ctx.func.super_init_call is not None:
|
|
2364
|
+
# super().__init__() must be the first non-docstring statement
|
|
2365
|
+
first_real_stmt = MethodAnalyzer.find_first_non_docstring_stmt(method.body)
|
|
2366
|
+
if first_real_stmt is not None:
|
|
2367
|
+
# Check if first real statement contains the super().__init__() call
|
|
2368
|
+
if not MethodAnalyzer.stmt_contains_super_init(first_real_stmt, self.ctx.func.super_init_call):
|
|
2369
|
+
raise self._error(
|
|
2370
|
+
"super().__init__() must be the first statement in __init__",
|
|
2371
|
+
self.ctx.func.super_init_call
|
|
2372
|
+
)
|
|
2373
|
+
|
|
2374
|
+
# Require an explicit base-init call (super().__init__(...) or
|
|
2375
|
+
# Base.__init__(self, ...)) when the single user-record base has
|
|
2376
|
+
# no synthesizable default constructor. Otherwise the C++ MIL
|
|
2377
|
+
# would try to implicit-default-construct the base subobject and
|
|
2378
|
+
# the build fails with a cryptic "no matching function for call
|
|
2379
|
+
# to Base::Base()" error from inside the subclass ctor.
|
|
2380
|
+
if method.name == "__init__":
|
|
2381
|
+
self._require_super_init_for_non_default_base(method, record)
|
|
2382
|
+
|
|
2383
|
+
# Validate __del__ methods
|
|
2384
|
+
if method.name == "__del__":
|
|
2385
|
+
record_info = self.ctx.registry.get_record(record.name)
|
|
2386
|
+
if self.ctx.func.super_del_call is not None:
|
|
2387
|
+
# super().__del__() must be the last non-docstring statement
|
|
2388
|
+
last_real_stmt = MethodAnalyzer.find_last_non_docstring_stmt(method.body)
|
|
2389
|
+
if last_real_stmt is not None and not is_super_del_call(last_real_stmt):
|
|
2390
|
+
raise self._error(
|
|
2391
|
+
"super().__del__() must be the last statement in __del__",
|
|
2392
|
+
self.ctx.func.super_del_call
|
|
2393
|
+
)
|
|
2394
|
+
elif record_info and record_info.parents:
|
|
2395
|
+
# No super().__del__() but parent(s) may have __del__. Any parent
|
|
2396
|
+
# with __del__ runs automatically after this destructor.
|
|
2397
|
+
parent_has_del = False
|
|
2398
|
+
for p in record_info.parents:
|
|
2399
|
+
if not hasattr(p, 'name'):
|
|
2400
|
+
continue
|
|
2401
|
+
parent_info = self.ctx.registry.get_record(p.name)
|
|
2402
|
+
if parent_info and parent_info.get_method("__del__") is not None:
|
|
2403
|
+
parent_has_del = True
|
|
2404
|
+
break
|
|
2405
|
+
if parent_has_del:
|
|
2406
|
+
self.ctx.warning(
|
|
2407
|
+
"Parent class has __del__() which will be called automatically by C++ "
|
|
2408
|
+
"after this destructor runs. Unlike Python, you do not need "
|
|
2409
|
+
"super().__del__() -- but it also means the parent destructor "
|
|
2410
|
+
"always runs even without an explicit call.",
|
|
2411
|
+
method
|
|
2412
|
+
)
|
|
2413
|
+
|
|
2414
|
+
self.deduction.resolve_all()
|
|
2415
|
+
|
|
2416
|
+
# Store Phase 1 local mutation facts on method FunctionInfo.
|
|
2417
|
+
# For @overload methods, get_method() returns overloads[0] (the first
|
|
2418
|
+
# stub). The implementation's FI is not separately registered, so all
|
|
2419
|
+
# Phase 1 facts are stored on stub[0] and Phase 2 / const inference
|
|
2420
|
+
# work through it. This is consistent with _sync_inferred_const, which
|
|
2421
|
+
# also reads back via get_method() and skips is_overload_stub nodes.
|
|
2422
|
+
record_info = self.ctx.registry.get_record(record.name)
|
|
2423
|
+
if record_info is not None and not method.is_stub:
|
|
2424
|
+
method_fi = record_info.get_method(method.name)
|
|
2425
|
+
# Property methods are not in the methods dict (popped during
|
|
2426
|
+
# registration) -- look them up through the properties registry
|
|
2427
|
+
# so their return_borrows_from facts are recorded.
|
|
2428
|
+
if method_fi is None and method.is_property_getter:
|
|
2429
|
+
prop = record_info.properties.get(method.name)
|
|
2430
|
+
if prop is not None:
|
|
2431
|
+
method_fi = prop.getter
|
|
2432
|
+
elif method_fi is None and method.is_property_setter:
|
|
2433
|
+
prop = record_info.properties.get(method.property_name)
|
|
2434
|
+
if prop is not None:
|
|
2435
|
+
method_fi = prop.setter
|
|
2436
|
+
if (method_fi is not None
|
|
2437
|
+
and method_fi.direct_mutated_params is None
|
|
2438
|
+
and method_fi.originating_module == self.ctx.module_name):
|
|
2439
|
+
param_list = [pname for pname, _ in method.params]
|
|
2440
|
+
direct = frozenset(
|
|
2441
|
+
i for i, pname in enumerate(param_list)
|
|
2442
|
+
if pname in self.ctx.func.current_mutated_param_names
|
|
2443
|
+
)
|
|
2444
|
+
method_fi.direct_mutated_params = direct
|
|
2445
|
+
method_fi.direct_self_mutated = self.ctx.func.current_self_mutated
|
|
2446
|
+
method_fi.call_edges = list(self.ctx.func.current_call_edges)
|
|
2447
|
+
method_fi.mutated_params = direct
|
|
2448
|
+
method_fi.representational_type_params = frozenset(self.ctx.func.current_representational_params)
|
|
2449
|
+
# Structural mutation facts (append/insert/clear/del/etc.)
|
|
2450
|
+
direct_struct = frozenset(
|
|
2451
|
+
i for i, pname in enumerate(param_list)
|
|
2452
|
+
if pname in self.ctx.func.current_struct_mutated_param_names
|
|
2453
|
+
)
|
|
2454
|
+
if self.ctx.func.current_self_struct_mutated:
|
|
2455
|
+
direct_struct = direct_struct | frozenset({-1})
|
|
2456
|
+
method_fi.direct_structural_mutated_params = direct_struct
|
|
2457
|
+
method_fi.structural_mutated_params = direct_struct
|
|
2458
|
+
# 8b: Return borrow facts
|
|
2459
|
+
returned = frozenset(
|
|
2460
|
+
i for i, pname in enumerate(param_list)
|
|
2461
|
+
if pname in self.ctx.func.current_returned_param_names
|
|
2462
|
+
)
|
|
2463
|
+
if "self" in self.ctx.func.current_returned_param_names:
|
|
2464
|
+
returned = returned | frozenset([-1])
|
|
2465
|
+
method_fi.return_borrows_from = returned
|
|
2466
|
+
method_fi.addr_escapes_params = frozenset(
|
|
2467
|
+
i for i, pname in enumerate(param_list)
|
|
2468
|
+
if pname in self.ctx.func.current_addr_escape_param_names
|
|
2469
|
+
)
|
|
2470
|
+
|
|
2471
|
+
self._warn_unconsumed_own_params(method)
|
|
2472
|
+
self._store_analysis_results(method, scan)
|
|
2473
|
+
|
|
2474
|
+
self.ctx.func.current_scope = None
|
|
2475
|
+
self.ctx.func.current_function = None
|
|
2476
|
+
self.ctx.func.current_ns = None
|
|
2477
|
+
|
|
2478
|
+
# Multi-base init-call coverage check. Runs after all method bodies
|
|
2479
|
+
# are analyzed so expr.unbound_self_parent_type is set on any
|
|
2480
|
+
# BaseN.__init__(self, ...) calls in the child __init__.
|
|
2481
|
+
record_info_for_init = self.ctx.registry.get_record(record.name)
|
|
2482
|
+
if record_info_for_init is not None:
|
|
2483
|
+
self.registrar.validate_multi_base_init_calls(record, record_info_for_init)
|
|
2484
|
+
|
|
2485
|
+
self.ctx.record_ctx = RecordContext()
|
|
2486
|
+
|
|
2487
|
+
def _check_init_field_assignments(self, method: TpyFunction, record: TpyRecord) -> None:
|
|
2488
|
+
"""Enforce the two-section __init__ model.
|
|
2489
|
+
|
|
2490
|
+
The init section is the leading prefix of super().__init__() +
|
|
2491
|
+
self.field = expr (own fields only, each at most once). Everything
|
|
2492
|
+
after the first statement that breaks this pattern is the body section.
|
|
2493
|
+
|
|
2494
|
+
At the split point:
|
|
2495
|
+
- Own fields not initialized + no default ctor -> error.
|
|
2496
|
+
- Own fields not initialized + has default ctor -> warning (CPython gap).
|
|
2497
|
+
- self.method() call with uninitialized fields in RHS -> warning.
|
|
2498
|
+
|
|
2499
|
+
In the body section (depth > 0 branches):
|
|
2500
|
+
- @nocopy field assigned -> error (copy assignment deleted).
|
|
2501
|
+
- __del__ field assigned -> error (destructor on default-constructed value).
|
|
2502
|
+
- Other fields: silently allowed (e.g. accumulation in a loop body).
|
|
2503
|
+
"""
|
|
2504
|
+
record_info = self.ctx.registry.get_record(record.name)
|
|
2505
|
+
if record_info is None:
|
|
2506
|
+
return
|
|
2507
|
+
# Native types manage their own construction in C++
|
|
2508
|
+
if record_info.is_native:
|
|
2509
|
+
return
|
|
2510
|
+
# Own fields for split-point and missing-field checks
|
|
2511
|
+
own_fields: dict[str, FieldInfo] = {f.name: f for f in record_info.fields}
|
|
2512
|
+
# All fields (incl. inherited) for branch-body safety checks
|
|
2513
|
+
all_fields = self._collect_all_fields(record_info)
|
|
2514
|
+
if not own_fields and not all_fields:
|
|
2515
|
+
return
|
|
2516
|
+
|
|
2517
|
+
# --- Find split point ---
|
|
2518
|
+
init_section_fields: set[str] = set()
|
|
2519
|
+
split_idx = len(method.body)
|
|
2520
|
+
|
|
2521
|
+
for i, stmt in enumerate(method.body):
|
|
2522
|
+
if is_base_init_call(stmt):
|
|
2523
|
+
continue
|
|
2524
|
+
if is_docstring(stmt):
|
|
2525
|
+
continue
|
|
2526
|
+
if (isinstance(stmt, TpyAssign)
|
|
2527
|
+
and isinstance(stmt.target, TpyFieldAccess)
|
|
2528
|
+
and isinstance(stmt.target.obj, TpyName)
|
|
2529
|
+
and stmt.target.obj.name == "self"
|
|
2530
|
+
and stmt.target.field in all_fields
|
|
2531
|
+
and stmt.target.field not in init_section_fields):
|
|
2532
|
+
field_name = stmt.target.field
|
|
2533
|
+
# Warn if RHS calls self.method() while fields are still uninitialized
|
|
2534
|
+
uninit = set(own_fields.keys()) - init_section_fields
|
|
2535
|
+
if uninit and expr_contains_self_method_call(stmt.value):
|
|
2536
|
+
self._warning(
|
|
2537
|
+
f"instance method called in __init__ before all fields are initialized; "
|
|
2538
|
+
f"the method may access uninitialized fields",
|
|
2539
|
+
stmt
|
|
2540
|
+
)
|
|
2541
|
+
init_section_fields.add(field_name)
|
|
2542
|
+
continue
|
|
2543
|
+
# Not a valid init-section statement: this is the split point
|
|
2544
|
+
split_idx = i
|
|
2545
|
+
break
|
|
2546
|
+
|
|
2547
|
+
# Collect all fields assigned unconditionally (depth 0) in the full body.
|
|
2548
|
+
# Fields in this set are OK even if not in init section: they will be
|
|
2549
|
+
# assigned in the constructor body before any branch or method call uses them.
|
|
2550
|
+
depth0_assigned: set[str] = {
|
|
2551
|
+
stmt.target.field
|
|
2552
|
+
for stmt in method.body
|
|
2553
|
+
if (isinstance(stmt, TpyAssign)
|
|
2554
|
+
and isinstance(stmt.target, TpyFieldAccess)
|
|
2555
|
+
and isinstance(stmt.target.obj, TpyName)
|
|
2556
|
+
and stmt.target.obj.name == "self")
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
# --- Check fields at split point ---
|
|
2560
|
+
first_error: SemanticError | None = None
|
|
2561
|
+
|
|
2562
|
+
if own_fields:
|
|
2563
|
+
split_node = method.body[split_idx] if split_idx < len(method.body) else None
|
|
2564
|
+
for field_name, field_info in own_fields.items():
|
|
2565
|
+
if field_name in init_section_fields:
|
|
2566
|
+
continue
|
|
2567
|
+
if field_name in depth0_assigned:
|
|
2568
|
+
# Field is assigned unconditionally in the body (just not in the
|
|
2569
|
+
# init section), so it will be initialized before first use.
|
|
2570
|
+
continue
|
|
2571
|
+
if field_info.default_value is not None or field_info.default_expr is not None:
|
|
2572
|
+
continue
|
|
2573
|
+
field_type = field_info.type
|
|
2574
|
+
# In a generic record, fields whose type involves a type parameter
|
|
2575
|
+
# cannot be checked here -- C++ handles the constraint at instantiation.
|
|
2576
|
+
if record.type_params and contains_type_param(field_type):
|
|
2577
|
+
continue
|
|
2578
|
+
if self.protocols._is_default_constructible(field_type):
|
|
2579
|
+
self._warning(
|
|
2580
|
+
f"field '{field_name}' is not initialized before the constructor body; "
|
|
2581
|
+
f"it will be default-constructed in C++ "
|
|
2582
|
+
f"(in CPython, the attribute would not exist)",
|
|
2583
|
+
split_node
|
|
2584
|
+
)
|
|
2585
|
+
else:
|
|
2586
|
+
err = self._error(
|
|
2587
|
+
f"field '{field_name}' of type '{str(field_type)}' has no default "
|
|
2588
|
+
f"constructor and is not initialized before the constructor body",
|
|
2589
|
+
split_node
|
|
2590
|
+
)
|
|
2591
|
+
if first_error is None:
|
|
2592
|
+
first_error = err
|
|
2593
|
+
|
|
2594
|
+
# --- Check branch assignments in body (all fields incl. inherited) ---
|
|
2595
|
+
def walk_body(stmts: list[TpyStmt], depth: int) -> None:
|
|
2596
|
+
nonlocal first_error
|
|
2597
|
+
for stmt in stmts:
|
|
2598
|
+
if isinstance(stmt, TpyAssign):
|
|
2599
|
+
target = stmt.target
|
|
2600
|
+
if (depth > 0
|
|
2601
|
+
and isinstance(target, TpyFieldAccess)
|
|
2602
|
+
and isinstance(target.obj, TpyName)
|
|
2603
|
+
and target.obj.name == "self"
|
|
2604
|
+
and target.field in all_fields):
|
|
2605
|
+
field_name = target.field
|
|
2606
|
+
field_type = all_fields[field_name].type
|
|
2607
|
+
if self._type_has_del_or_nocopy(field_type):
|
|
2608
|
+
type_name = str(field_type)
|
|
2609
|
+
err = self._error(
|
|
2610
|
+
f"field '{field_name}' of type '{type_name}' assigned inside "
|
|
2611
|
+
f"control flow in __init__; '{type_name}' is move-only or has "
|
|
2612
|
+
f"'__del__', so it must be initialized unconditionally before "
|
|
2613
|
+
f"any branch",
|
|
2614
|
+
stmt
|
|
2615
|
+
)
|
|
2616
|
+
if first_error is None:
|
|
2617
|
+
first_error = err
|
|
2618
|
+
elif isinstance(stmt, TpyIf):
|
|
2619
|
+
walk_body(stmt.then_body, depth + 1)
|
|
2620
|
+
walk_body(stmt.else_body, depth + 1)
|
|
2621
|
+
elif isinstance(stmt, (TpyWhile, TpyForEach)):
|
|
2622
|
+
walk_body(stmt.body, depth + 1)
|
|
2623
|
+
walk_body(stmt.orelse, depth + 1)
|
|
2624
|
+
|
|
2625
|
+
walk_body(method.body, 0)
|
|
2626
|
+
|
|
2627
|
+
if first_error is not None:
|
|
2628
|
+
raise first_error
|
|
2629
|
+
|
|
2630
|
+
def _collect_all_fields(self, record_info: RecordInfo) -> dict[str, FieldInfo]:
|
|
2631
|
+
"""Collect all fields from a record and its ancestors, walking MRO base-first.
|
|
2632
|
+
|
|
2633
|
+
Child fields override ancestor fields with the same name (last-write-wins
|
|
2634
|
+
in dict semantics, consistent with Python attribute shadowing).
|
|
2635
|
+
"""
|
|
2636
|
+
result: dict[str, FieldInfo] = {}
|
|
2637
|
+
for anc_rec in self.ctx.registry.iter_ancestor_records(record_info, reverse=True):
|
|
2638
|
+
for f in anc_rec.fields:
|
|
2639
|
+
result[f.name] = f
|
|
2640
|
+
for f in record_info.fields:
|
|
2641
|
+
result[f.name] = f
|
|
2642
|
+
return result
|
|
2643
|
+
|
|
2644
|
+
def _require_super_init_for_non_default_base(
|
|
2645
|
+
self, method: TpyFunction, record: TpyRecord,
|
|
2646
|
+
) -> None:
|
|
2647
|
+
"""Reject `class Child(Base): def __init__(self, ...): ...` without an
|
|
2648
|
+
explicit base-init call when Base's C++ default constructor would be
|
|
2649
|
+
implicitly deleted -- C++ would otherwise fail to synthesize the
|
|
2650
|
+
base subobject default-construction in the child's MIL.
|
|
2651
|
+
`super().__init__(...)` and `Base.__init__(self, ...)` are both
|
|
2652
|
+
accepted (mirrors `is_base_init_call`). Multi-base inheritance is
|
|
2653
|
+
already covered by `validate_multi_base_init_calls`.
|
|
2654
|
+
|
|
2655
|
+
The predicate matches the codegen-level "would `Base() = default;`
|
|
2656
|
+
succeed?" not the user-level "is Base default-constructible from
|
|
2657
|
+
TPy?". A base with a required-arg `__init__` but all-default-ctor
|
|
2658
|
+
fields synthesizes `Base()` fine at C++ level (leaves fields
|
|
2659
|
+
default-initialized), so it does NOT trigger the rule.
|
|
2660
|
+
"""
|
|
2661
|
+
record_info = self.ctx.registry.get_record(record.name)
|
|
2662
|
+
if record_info is None or record_info.is_native:
|
|
2663
|
+
return
|
|
2664
|
+
if len(record_info.parents) != 1:
|
|
2665
|
+
return
|
|
2666
|
+
parent = record_info.parents[0]
|
|
2667
|
+
if not isinstance(parent, NominalType) or parent.is_protocol:
|
|
2668
|
+
return
|
|
2669
|
+
parent_rec = self.ctx.registry.get_record_for_type(parent)
|
|
2670
|
+
if parent_rec is None or parent_rec.is_native or parent_rec.builtin_type_key:
|
|
2671
|
+
return
|
|
2672
|
+
if not self._record_default_ctor_is_deleted(parent_rec):
|
|
2673
|
+
return
|
|
2674
|
+
# Accept either super().__init__(...) or Base.__init__(self, ...).
|
|
2675
|
+
if self.ctx.func.super_init_call is not None:
|
|
2676
|
+
return
|
|
2677
|
+
for stmt in method.body:
|
|
2678
|
+
if is_base_init_call(stmt):
|
|
2679
|
+
return
|
|
2680
|
+
reason = self._explain_default_ctor_deletion(parent_rec)
|
|
2681
|
+
if method.is_macro_generated:
|
|
2682
|
+
origin = (f"'@{method.macro_origin}'" if method.macro_origin
|
|
2683
|
+
else "a macro")
|
|
2684
|
+
raise self._error(
|
|
2685
|
+
f"the '__init__' synthesized by {origin} for '{record.name}' "
|
|
2686
|
+
f"does not initialize parent class '{parent_rec.name}', but "
|
|
2687
|
+
f"'{parent_rec.name}' cannot be constructed without arguments "
|
|
2688
|
+
f"({reason}). Either give '{parent_rec.name}' a zero-argument "
|
|
2689
|
+
f"form, or write an explicit '__init__' on '{record.name}' "
|
|
2690
|
+
f"that calls 'super().__init__(...)'.",
|
|
2691
|
+
method,
|
|
2692
|
+
)
|
|
2693
|
+
raise self._error(
|
|
2694
|
+
f"'{record.name}.__init__' must call 'super().__init__(...)' "
|
|
2695
|
+
f"(or '{parent_rec.name}.__init__(self, ...)') as its first "
|
|
2696
|
+
f"statement: '{parent_rec.name}' cannot be constructed without "
|
|
2697
|
+
f"arguments ({reason}), so the inherited fields are left "
|
|
2698
|
+
f"uninitialized.",
|
|
2699
|
+
method,
|
|
2700
|
+
)
|
|
2701
|
+
|
|
2702
|
+
def _explain_default_ctor_deletion(
|
|
2703
|
+
self, rec: 'RecordInfo', _visited: set[str] | None = None,
|
|
2704
|
+
) -> str:
|
|
2705
|
+
"""Produce a one-clause user-facing explanation of why `rec` cannot
|
|
2706
|
+
be constructed without arguments. Walks the same chain as
|
|
2707
|
+
`_record_default_ctor_is_deleted` and reports the FIRST source it
|
|
2708
|
+
finds, so the message attributes the actual cause (a specific
|
|
2709
|
+
field on `rec` itself, or on an ancestor) rather than blaming
|
|
2710
|
+
`rec` generically. `_visited` mirrors the cycle guard in
|
|
2711
|
+
`_record_default_ctor_is_deleted`.
|
|
2712
|
+
"""
|
|
2713
|
+
if _visited is None:
|
|
2714
|
+
_visited = set()
|
|
2715
|
+
if rec.name in _visited:
|
|
2716
|
+
return f"'{rec.name}' (cycle in ancestor/field chain)"
|
|
2717
|
+
_visited = _visited | {rec.name}
|
|
2718
|
+
if del_suppresses_default_ctor(rec):
|
|
2719
|
+
return (f"'{rec.name}' has '__del__' and required '__init__' "
|
|
2720
|
+
f"parameters")
|
|
2721
|
+
for fld in rec.fields:
|
|
2722
|
+
if self._field_type_blocks_default_ctor(fld.type):
|
|
2723
|
+
inner = self._explain_field_type_blocks_default_ctor(
|
|
2724
|
+
fld.type, _visited)
|
|
2725
|
+
return (f"field '{fld.name}' has type '{str(fld.type)}' "
|
|
2726
|
+
f"({inner})")
|
|
2727
|
+
for p in rec.parents:
|
|
2728
|
+
if not isinstance(p, NominalType) or p.is_protocol:
|
|
2729
|
+
continue
|
|
2730
|
+
p_rec = self.ctx.registry.get_record_for_type(p)
|
|
2731
|
+
if p_rec is None or p_rec.is_native or p_rec.builtin_type_key:
|
|
2732
|
+
continue
|
|
2733
|
+
if self._record_default_ctor_is_deleted(p_rec):
|
|
2734
|
+
inner = self._explain_default_ctor_deletion(p_rec, _visited)
|
|
2735
|
+
return (f"ancestor '{p_rec.name}' (inherited by "
|
|
2736
|
+
f"'{rec.name}') has the same restriction: {inner}")
|
|
2737
|
+
return "a field or ancestor has no zero-argument constructor"
|
|
2738
|
+
|
|
2739
|
+
def _record_default_ctor_is_deleted(
|
|
2740
|
+
self, rec: 'RecordInfo', _visited: set[str] | None = None,
|
|
2741
|
+
) -> bool:
|
|
2742
|
+
"""True if the C++ `Base() = default;` for this user record would
|
|
2743
|
+
be implicitly deleted -- either codegen explicitly suppresses it
|
|
2744
|
+
(via `del_suppresses_default_ctor`) or some field / ancestor's
|
|
2745
|
+
type lacks a C++ default ctor.
|
|
2746
|
+
|
|
2747
|
+
Mirrors codegen's `_all_fields_default_constructible` but inverted
|
|
2748
|
+
(returns True when default-ctor is unavailable). `_visited` guards
|
|
2749
|
+
cyclic shapes (parent reachable via own generic instantiation,
|
|
2750
|
+
mutual-import edge cases).
|
|
2751
|
+
"""
|
|
2752
|
+
if _visited is None:
|
|
2753
|
+
_visited = set()
|
|
2754
|
+
if rec.name in _visited:
|
|
2755
|
+
return False
|
|
2756
|
+
_visited = _visited | {rec.name}
|
|
2757
|
+
if del_suppresses_default_ctor(rec):
|
|
2758
|
+
return True
|
|
2759
|
+
for fld in rec.fields:
|
|
2760
|
+
if self._field_type_blocks_default_ctor(fld.type, _visited):
|
|
2761
|
+
return True
|
|
2762
|
+
# Route parents through the field-type predicate so generic
|
|
2763
|
+
# parents (e.g. `class C(Box[Int32])`) dispatch through the
|
|
2764
|
+
# `type_args` branch and consult the base template's
|
|
2765
|
+
# `del_suppresses_default_ctor`.
|
|
2766
|
+
for p in rec.parents:
|
|
2767
|
+
if self._field_type_blocks_default_ctor(p, _visited):
|
|
2768
|
+
return True
|
|
2769
|
+
return False
|
|
2770
|
+
|
|
2771
|
+
def _field_type_blocks_default_ctor(
|
|
2772
|
+
self, typ: TpyType, _visited: set[str] | None = None,
|
|
2773
|
+
) -> bool:
|
|
2774
|
+
"""True if a field of this type forces the enclosing record's C++
|
|
2775
|
+
default ctor to be deleted. Mirrors codegen's
|
|
2776
|
+
`_fld_type_cpp_default_constructible` in `codegen_cpp/records.py`
|
|
2777
|
+
(this is the inverse).
|
|
2778
|
+
"""
|
|
2779
|
+
typ = unwrap_readonly(typ)
|
|
2780
|
+
# Storage-form Own[T] field stores T inline; the wrapped type's
|
|
2781
|
+
# default-ctor is what matters.
|
|
2782
|
+
if isinstance(typ, OwnType):
|
|
2783
|
+
return self._field_type_blocks_default_ctor(typ.wrapped, _visited)
|
|
2784
|
+
# Shapes with usable C++ default ctors regardless of T.
|
|
2785
|
+
if isinstance(typ, (OptionalType, PtrType)):
|
|
2786
|
+
return False
|
|
2787
|
+
if is_enum_type(typ):
|
|
2788
|
+
return False
|
|
2789
|
+
if is_list(typ) or is_dict(typ) or is_set(typ) or is_span(typ):
|
|
2790
|
+
return False
|
|
2791
|
+
# Tuple / Array: default-ctorable iff every element is.
|
|
2792
|
+
if isinstance(typ, TupleType):
|
|
2793
|
+
return any(self._field_type_blocks_default_ctor(et, _visited)
|
|
2794
|
+
for et in typ.element_types)
|
|
2795
|
+
if is_array(typ):
|
|
2796
|
+
elem = typ.get_element_type()
|
|
2797
|
+
return elem is not None and self._field_type_blocks_default_ctor(elem, _visited)
|
|
2798
|
+
# Union field lowers to std::variant<...>; codegen's
|
|
2799
|
+
# `_fld_type_cpp_default_constructible` does not special-case
|
|
2800
|
+
# UnionType and falls through to `_is_default_constructible`
|
|
2801
|
+
# which also returns False for it -- so codegen never emits
|
|
2802
|
+
# `Outer() = default;` for a class with a union field. Mirror
|
|
2803
|
+
# that: every union field blocks the enclosing default ctor.
|
|
2804
|
+
# (Optional[T] is OptionalType, not UnionType, and stays
|
|
2805
|
+
# default-ctorable via the OptionalType branch above.)
|
|
2806
|
+
if isinstance(typ, UnionType):
|
|
2807
|
+
return True
|
|
2808
|
+
if isinstance(typ, NominalType) and not typ.is_protocol:
|
|
2809
|
+
# Generic instantiation: codegen emits `= default;` for the
|
|
2810
|
+
# template unless the base template itself suppresses default
|
|
2811
|
+
# construction (Box/Rc/Weak, __del__-shapes). Mirror that.
|
|
2812
|
+
if typ.type_args:
|
|
2813
|
+
base_rec = self.ctx.registry.get_record(typ.name)
|
|
2814
|
+
if base_rec is None:
|
|
2815
|
+
return False
|
|
2816
|
+
return del_suppresses_default_ctor(base_rec)
|
|
2817
|
+
rec = self.ctx.registry.get_record_for_type(typ)
|
|
2818
|
+
if rec is None or rec.builtin_type_key or rec.is_native:
|
|
2819
|
+
return False
|
|
2820
|
+
return self._record_default_ctor_is_deleted(rec, _visited)
|
|
2821
|
+
return False
|
|
2822
|
+
|
|
2823
|
+
def _explain_field_type_blocks_default_ctor(
|
|
2824
|
+
self, typ: TpyType, _visited: set[str] | None = None,
|
|
2825
|
+
) -> str:
|
|
2826
|
+
"""Mirror of `_field_type_blocks_default_ctor`: name the cause for
|
|
2827
|
+
the user instead of just returning bool. Caller has already
|
|
2828
|
+
verified that the predicate is True for this type. `_visited`
|
|
2829
|
+
threads through to `_explain_default_ctor_deletion` to guard
|
|
2830
|
+
against cyclic ancestor/field chains.
|
|
2831
|
+
"""
|
|
2832
|
+
typ = unwrap_readonly(typ)
|
|
2833
|
+
if isinstance(typ, OwnType):
|
|
2834
|
+
return self._explain_field_type_blocks_default_ctor(
|
|
2835
|
+
typ.wrapped, _visited)
|
|
2836
|
+
if isinstance(typ, TupleType):
|
|
2837
|
+
for i, et in enumerate(typ.element_types):
|
|
2838
|
+
if self._field_type_blocks_default_ctor(et):
|
|
2839
|
+
inner = self._explain_field_type_blocks_default_ctor(
|
|
2840
|
+
et, _visited)
|
|
2841
|
+
return f"tuple element {i} ({inner})"
|
|
2842
|
+
return "a tuple element cannot be constructed without arguments"
|
|
2843
|
+
if is_array(typ):
|
|
2844
|
+
elem = typ.get_element_type()
|
|
2845
|
+
if elem is not None:
|
|
2846
|
+
inner = self._explain_field_type_blocks_default_ctor(
|
|
2847
|
+
elem, _visited)
|
|
2848
|
+
return f"array element type ({inner})"
|
|
2849
|
+
return "the array element type cannot be constructed without arguments"
|
|
2850
|
+
if isinstance(typ, UnionType):
|
|
2851
|
+
return "union fields have no default in C++"
|
|
2852
|
+
if isinstance(typ, NominalType) and not typ.is_protocol:
|
|
2853
|
+
if typ.type_args:
|
|
2854
|
+
base_rec = self.ctx.registry.get_record(typ.name)
|
|
2855
|
+
if base_rec is not None and del_suppresses_default_ctor(base_rec):
|
|
2856
|
+
return (f"'{typ.name}' has '__del__' and required "
|
|
2857
|
+
f"'__init__' parameters")
|
|
2858
|
+
rec = self.ctx.registry.get_record_for_type(typ)
|
|
2859
|
+
if rec is not None and not rec.builtin_type_key and not rec.is_native:
|
|
2860
|
+
return self._explain_default_ctor_deletion(rec, _visited)
|
|
2861
|
+
return "the type has no zero-argument constructor"
|
|
2862
|
+
|
|
2863
|
+
def _type_has_del_or_nocopy(self, field_type: TpyType) -> bool:
|
|
2864
|
+
"""Check if a type (or any ancestor in its MRO) has __del__ or is @nocopy."""
|
|
2865
|
+
inner = field_type
|
|
2866
|
+
if isinstance(inner, OwnType):
|
|
2867
|
+
inner = inner.wrapped
|
|
2868
|
+
inner = unwrap_readonly(inner)
|
|
2869
|
+
rec = self.ctx.registry.get_record_for_type(inner)
|
|
2870
|
+
if rec is None:
|
|
2871
|
+
return False
|
|
2872
|
+
if rec.is_nocopy or rec.has_del:
|
|
2873
|
+
return True
|
|
2874
|
+
for anc_rec in self.ctx.registry.iter_ancestor_records(rec):
|
|
2875
|
+
if anc_rec.has_del or anc_rec.is_nocopy:
|
|
2876
|
+
return True
|
|
2877
|
+
return False
|
|
2878
|
+
|
|
2879
|
+
def _analyze_top_level(self, stmts: list[TpyStmt]) -> None:
|
|
2880
|
+
"""Analyze top-level statements (for generated main()).
|
|
2881
|
+
|
|
2882
|
+
Treats top-level code like a function body so list literals and other
|
|
2883
|
+
constructs go through the same analysis path.
|
|
2884
|
+
"""
|
|
2885
|
+
self.ctx.reset_function_tracking()
|
|
2886
|
+
self.ctx.func.current_function = MODULE_INIT_CONTEXT
|
|
2887
|
+
self.ctx.func.current_scope = Scope(parent=self.ctx.global_scope)
|
|
2888
|
+
self.ctx.is_top_level = True
|
|
2889
|
+
|
|
2890
|
+
# Set up namespace - use global_ns for top-level (globals are visible)
|
|
2891
|
+
# New local variables will be added to global_ns as they're declared
|
|
2892
|
+
self.ctx.func.current_ns = self.ctx.global_ns
|
|
2893
|
+
|
|
2894
|
+
# Top-level builder-trace expansion runs here (not in pass 5.5)
|
|
2895
|
+
# because pass 4 is itself the body-analysis pass for module
|
|
2896
|
+
# code -- the rewrite has to land before the analyze loop below.
|
|
2897
|
+
self._expand_builder_trace_top_level(stmts)
|
|
2898
|
+
|
|
2899
|
+
# Pre-scan for codegen
|
|
2900
|
+
self.top_level_scan_result = scan_reassigned_vars(stmts)
|
|
2901
|
+
# Last-use analysis for auto-move (shared with codegen)
|
|
2902
|
+
self.ctx.all_last_uses |= analyze_last_uses(
|
|
2903
|
+
stmts, self.top_level_scan_result.alias_sources)
|
|
2904
|
+
self.ctx.func.current_reassigned_vars = self.top_level_scan_result.reassigned.copy()
|
|
2905
|
+
self.ctx.func.current_lvalue_reassigned = self.top_level_scan_result.lvalue_reassigned.copy()
|
|
2906
|
+
self.ctx.func.current_aug_assigned_vars = self.top_level_scan_result.aug_assigned.copy()
|
|
2907
|
+
|
|
2908
|
+
for stmt in stmts:
|
|
2909
|
+
self.stmts.analyze_stmt(stmt)
|
|
2910
|
+
self.deduction.resolve_all()
|
|
2911
|
+
|
|
2912
|
+
if self.ctx.func.hoisted_vars:
|
|
2913
|
+
self.top_level_hoisted_vars = self.ctx.func.hoisted_vars.copy()
|
|
2914
|
+
if self.ctx.func.move_through_vars:
|
|
2915
|
+
self.top_level_move_through_vars = self.ctx.func.move_through_vars.copy()
|
|
2916
|
+
self.if_branch_decls.update(self.ctx.if_branch_decls)
|
|
2917
|
+
|
|
2918
|
+
self.ctx.func.current_function = None
|
|
2919
|
+
self.ctx.func.current_scope = None
|
|
2920
|
+
self.ctx.func.current_ns = None
|
|
2921
|
+
self.ctx.is_top_level = False
|
|
2922
|
+
|
|
2923
|
+
def get_expr_type(self, expr: TpyExpr) -> Optional[TpyType]:
|
|
2924
|
+
"""Get the cached type of an expression."""
|
|
2925
|
+
return self.ctx.get_expr_type(expr)
|
|
2926
|
+
|
|
2927
|
+
def _register_implicit_module_variable(self, module_name: str,
|
|
2928
|
+
original_name: str,
|
|
2929
|
+
local_name: str) -> bool:
|
|
2930
|
+
"""Promote an IMPORTED_NAME to VARIABLE when the implicit module
|
|
2931
|
+
re-exports a module-level constant.
|
|
2932
|
+
|
|
2933
|
+
The parser's special handling for ``tpy`` / ``typing`` / ``builtins``
|
|
2934
|
+
skips ``user_module_imports``, so the regular
|
|
2935
|
+
``_register_user_module_import`` path doesn't run on imports from these
|
|
2936
|
+
modules. Records / functions / type aliases already have dedicated
|
|
2937
|
+
resolution paths in the implicit-module branch; module-level variables
|
|
2938
|
+
do not. This helper covers the variable-only case so re-exported
|
|
2939
|
+
constants (e.g. ``__version__`` from a ``# tpy: native_module``
|
|
2940
|
+
``tpy/__init__.py``) become first-class VARIABLE bindings instead of
|
|
2941
|
+
being rejected at use site as "not a variable".
|
|
2942
|
+
"""
|
|
2943
|
+
module_info = self.ctx.registry.get_module(module_name)
|
|
2944
|
+
if module_info is None or module_info.is_builtin:
|
|
2945
|
+
return False
|
|
2946
|
+
if not module_info.variables or original_name not in module_info.variables:
|
|
2947
|
+
return False
|
|
2948
|
+
var_info = module_info.variables[original_name]
|
|
2949
|
+
self.ctx.global_scope.define(local_name, var_info.type)
|
|
2950
|
+
self.ctx.global_ns.bind_variable(local_name, var_info.type)
|
|
2951
|
+
# Install into the attribute table so use-site checks that read
|
|
2952
|
+
# `module_attributes` (e.g. `is_indirect_name`, `pointer_globals`)
|
|
2953
|
+
# find non-value imports and emit the right deref / pointer
|
|
2954
|
+
# qualification. Without this, a non-value-typed implicit-stdlib
|
|
2955
|
+
# variable would lose its `(*x)` indirection at use sites.
|
|
2956
|
+
install_binding(
|
|
2957
|
+
self.ctx.module_attributes, local_name,
|
|
2958
|
+
SymbolKind.VARIABLE, var_info.type,
|
|
2959
|
+
defining_module=module_name, canonical_name=original_name,
|
|
2960
|
+
)
|
|
2961
|
+
return True
|
|
2962
|
+
|
|
2963
|
+
def _register_tpy_type_alias(self, original_name: str, local_name: str) -> None:
|
|
2964
|
+
"""Register a single tpy type alias from the compiled tpy module_info."""
|
|
2965
|
+
# Compile-time-only types (e.g. FStr) take priority
|
|
2966
|
+
self.registrar._register_compile_time_type_alias(local_name, "tpy", original_name)
|
|
2967
|
+
|
|
2968
|
+
# AST-level type aliases (e.g. Float64 = float)
|
|
2969
|
+
tpy_info = self.ctx.registry.get_module("tpy")
|
|
2970
|
+
if not tpy_info or not tpy_info.type_aliases:
|
|
2971
|
+
return
|
|
2972
|
+
alias_info = tpy_info.type_aliases.get(original_name)
|
|
2973
|
+
if alias_info is not None:
|
|
2974
|
+
self.ctx.registry.register_type_alias(local_name, alias_info.body,
|
|
2975
|
+
imported_from=("tpy", original_name),
|
|
2976
|
+
info=alias_info)
|
|
2977
|
+
if local_name != original_name:
|
|
2978
|
+
self.ctx.registry.register_type_alias(original_name, alias_info.body,
|
|
2979
|
+
info=alias_info)
|
|
2980
|
+
|
|
2981
|
+
def _register_all_tpy_type_aliases(self) -> None:
|
|
2982
|
+
"""Register all tpy type aliases (for bare 'import tpy' and star imports)."""
|
|
2983
|
+
tpy_info = self.ctx.registry.get_module("tpy")
|
|
2984
|
+
if not tpy_info or not tpy_info.type_aliases:
|
|
2985
|
+
return
|
|
2986
|
+
for name, alias_info in tpy_info.type_aliases.items():
|
|
2987
|
+
self.ctx.registry.register_type_alias(name, alias_info.body,
|
|
2988
|
+
imported_from=("tpy", name),
|
|
2989
|
+
info=alias_info)
|
|
2990
|
+
|
|
2991
|
+
def _bind_star_reexport(self, original_name: str, local_name: str) -> None:
|
|
2992
|
+
"""Try to bind a star-imported re-export from a known module.
|
|
2993
|
+
|
|
2994
|
+
When 'from utils import *' brings in a name like Int32 that utils
|
|
2995
|
+
imported from tpy, we need to find the original source and bind it
|
|
2996
|
+
correctly so the name is usable.
|
|
2997
|
+
"""
|
|
2998
|
+
for mod_name, mod_info in self.ctx.registry.modules.items():
|
|
2999
|
+
if mod_info.is_builtin:
|
|
3000
|
+
continue
|
|
3001
|
+
if mod_info.has_export(original_name):
|
|
3002
|
+
self.ctx.imported_names[local_name] = (mod_name, original_name)
|
|
3003
|
+
self.ctx.global_ns.bind_imported_name(local_name, mod_name, original_name)
|
|
3004
|
+
self._register_user_module_import(mod_name, original_name, local_name)
|
|
3005
|
+
return
|
|
3006
|
+
# Fallback: tpy type aliases (e.g. Int32, Float64)
|
|
3007
|
+
self._register_tpy_type_alias(original_name, local_name)
|
|
3008
|
+
|
|
3009
|
+
def _register_nested_with_alias(self, items, register_fn,
|
|
3010
|
+
original_name: str, local_name: str) -> None:
|
|
3011
|
+
"""Register nested children of `original_name` from `items` (a name -> info
|
|
3012
|
+
dict) under both the canonical key (`Outer.Inner`) and -- when the import
|
|
3013
|
+
is aliased -- the alias-prefixed key (`L.Inner`). Mirrors the pre-register
|
|
3014
|
+
pattern used for the flat record above.
|
|
3015
|
+
"""
|
|
3016
|
+
if not items:
|
|
3017
|
+
return
|
|
3018
|
+
prefix = original_name + "."
|
|
3019
|
+
aliased = local_name != original_name
|
|
3020
|
+
for canonical, info in items.items():
|
|
3021
|
+
if not canonical.startswith(prefix):
|
|
3022
|
+
continue
|
|
3023
|
+
register_fn(info, canonical)
|
|
3024
|
+
if aliased:
|
|
3025
|
+
register_fn(info, local_name + canonical[len(original_name):])
|
|
3026
|
+
|
|
3027
|
+
def _register_from_attribute_binding(
|
|
3028
|
+
self, src_binding, local_name: str, module_name: str,
|
|
3029
|
+
original_name: str,
|
|
3030
|
+
) -> bool:
|
|
3031
|
+
"""Register an imported name when the source's per-kind dicts
|
|
3032
|
+
haven't materialized yet but its attribute-table binding has
|
|
3033
|
+
(cycle re-export). Dispatches on `src_binding.kind` to install
|
|
3034
|
+
the same registry/global_ns/attribute-table state the per-kind
|
|
3035
|
+
branches in `_register_user_module_import` would, so downstream
|
|
3036
|
+
consumers see the import. Returns True on success.
|
|
3037
|
+
"""
|
|
3038
|
+
ult_mod = src_binding.defining_module or module_name
|
|
3039
|
+
ult_name = src_binding.canonical_name
|
|
3040
|
+
kind = src_binding.kind
|
|
3041
|
+
info = src_binding.info
|
|
3042
|
+
if kind == SymbolKind.FUNCTION:
|
|
3043
|
+
func_infos = info if isinstance(info, list) else [info]
|
|
3044
|
+
self.ctx.registry.register_function_group(local_name, func_infos)
|
|
3045
|
+
if len(func_infos) > 1:
|
|
3046
|
+
self.ctx.global_ns.bind(NameBinding(
|
|
3047
|
+
kind=BindingKind.FUNCTION,
|
|
3048
|
+
name=local_name,
|
|
3049
|
+
func_infos=func_infos,
|
|
3050
|
+
))
|
|
3051
|
+
install_binding(
|
|
3052
|
+
self.ctx.module_attributes, local_name,
|
|
3053
|
+
SymbolKind.FUNCTION, func_infos,
|
|
3054
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3055
|
+
)
|
|
3056
|
+
return True
|
|
3057
|
+
if kind == SymbolKind.RECORD:
|
|
3058
|
+
self.ctx.registry.register_record(info, local_name)
|
|
3059
|
+
if local_name != original_name:
|
|
3060
|
+
self.ctx.registry.register_record(info, original_name)
|
|
3061
|
+
install_binding(
|
|
3062
|
+
self.ctx.module_attributes, local_name,
|
|
3063
|
+
SymbolKind.RECORD, info,
|
|
3064
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3065
|
+
)
|
|
3066
|
+
return True
|
|
3067
|
+
if kind in (SymbolKind.PROTOCOL_STATIC, SymbolKind.PROTOCOL_DYNAMIC):
|
|
3068
|
+
self.ctx.registry.register_protocol(info, local_name)
|
|
3069
|
+
self.ctx.global_ns.bind_imported_name(local_name, module_name, original_name)
|
|
3070
|
+
install_binding(
|
|
3071
|
+
self.ctx.module_attributes, local_name,
|
|
3072
|
+
kind, info,
|
|
3073
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3074
|
+
)
|
|
3075
|
+
return True
|
|
3076
|
+
if kind == SymbolKind.ENUM:
|
|
3077
|
+
self.ctx.registry.register_enum(info, local_name)
|
|
3078
|
+
if local_name != original_name:
|
|
3079
|
+
self.ctx.registry.register_enum(info, original_name)
|
|
3080
|
+
self.ctx.global_ns.bind_enum(info, name=local_name)
|
|
3081
|
+
install_binding(
|
|
3082
|
+
self.ctx.module_attributes, local_name,
|
|
3083
|
+
SymbolKind.ENUM, info,
|
|
3084
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3085
|
+
)
|
|
3086
|
+
return True
|
|
3087
|
+
if kind in (SymbolKind.OPAQUE, SymbolKind.PARSER_KEYWORD):
|
|
3088
|
+
# Parser keywords / builtin types have no analyzer registry
|
|
3089
|
+
# registration (the parser resolves them via TypeDef /
|
|
3090
|
+
# decorator schemas) and no `global_ns` counterpart (use
|
|
3091
|
+
# sites for these names go through the parser's
|
|
3092
|
+
# `_name_index` route, not the namespace lookup). We only
|
|
3093
|
+
# propagate the per-module attribute binding so consumers'
|
|
3094
|
+
# `from M import X` (explicit or star) and the cycle
|
|
3095
|
+
# re-export pre-pop both find the chain entry. The lack of
|
|
3096
|
+
# a `global_ns.bind_imported_name` here is intentional --
|
|
3097
|
+
# adding it would shadow the parser-keyword fallback path
|
|
3098
|
+
# in `_analyze_name` and break the bootstrap that lets
|
|
3099
|
+
# `@builtin_decorator` resolve before sema runs.
|
|
3100
|
+
install_binding(
|
|
3101
|
+
self.ctx.module_attributes, local_name,
|
|
3102
|
+
kind, info,
|
|
3103
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3104
|
+
)
|
|
3105
|
+
return True
|
|
3106
|
+
return False
|
|
3107
|
+
|
|
3108
|
+
def _register_user_module_import(self, module_name: str, original_name: str, local_name: str,
|
|
3109
|
+
from_star_import: bool = False) -> bool:
|
|
3110
|
+
"""Register an imported item from a user module.
|
|
3111
|
+
|
|
3112
|
+
Looks up the item in the unified registry (user modules are registered
|
|
3113
|
+
as ModuleInfo before analysis) and registers it in the appropriate
|
|
3114
|
+
namespace (function, record, or protocol).
|
|
3115
|
+
|
|
3116
|
+
Returns True if the name was found and registered, False if not found
|
|
3117
|
+
(only possible when from_star_import is True).
|
|
3118
|
+
|
|
3119
|
+
When from_star_import is True, names not found in the module's exports
|
|
3120
|
+
are silently skipped (they may be re-imported names visible in the
|
|
3121
|
+
source but not in the compiled module's ModuleInfo).
|
|
3122
|
+
"""
|
|
3123
|
+
module_info = self.ctx.registry.get_module(module_name)
|
|
3124
|
+
if module_info is None:
|
|
3125
|
+
# Module not in registry - may be a macro module (loaded
|
|
3126
|
+
# via macro_registry but never registered as a compiled
|
|
3127
|
+
# module). Install a macro binding when applicable so
|
|
3128
|
+
# downstream re-export chases find the macro.
|
|
3129
|
+
macro_chain = self._resolve_macro_chain(module_name, original_name)
|
|
3130
|
+
if macro_chain is not None:
|
|
3131
|
+
ult_mod, ult_name, kind = macro_chain
|
|
3132
|
+
install_binding(
|
|
3133
|
+
self.ctx.module_attributes, local_name,
|
|
3134
|
+
kind, None,
|
|
3135
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3136
|
+
)
|
|
3137
|
+
return True
|
|
3138
|
+
if module_info.is_builtin:
|
|
3139
|
+
# Builtin module (no user file shadowing it), skip to let builtin handling work
|
|
3140
|
+
return True
|
|
3141
|
+
|
|
3142
|
+
# Check for function (list of overloads in ModuleInfo)
|
|
3143
|
+
if module_info.functions and original_name in module_info.functions:
|
|
3144
|
+
func_infos = module_info.functions[original_name]
|
|
3145
|
+
# Skip callable registration for special_handling builtins
|
|
3146
|
+
# (e.g. native_global in tpy.extern). Statement interception in
|
|
3147
|
+
# statements.py looks up the IMPORTED_NAME binding's import_source;
|
|
3148
|
+
# registering a function group here would clobber that binding.
|
|
3149
|
+
# Still record the import so re-export works.
|
|
3150
|
+
if func_infos and func_infos[0].special_handling:
|
|
3151
|
+
install_binding(
|
|
3152
|
+
self.ctx.module_attributes, local_name,
|
|
3153
|
+
SymbolKind.FUNCTION, func_infos,
|
|
3154
|
+
defining_module=module_name, canonical_name=original_name,
|
|
3155
|
+
)
|
|
3156
|
+
return True
|
|
3157
|
+
self.ctx.registry.register_function_group(local_name, func_infos)
|
|
3158
|
+
if len(func_infos) > 1:
|
|
3159
|
+
self.ctx.global_ns.bind(NameBinding(
|
|
3160
|
+
kind=BindingKind.FUNCTION,
|
|
3161
|
+
name=local_name,
|
|
3162
|
+
func_infos=func_infos,
|
|
3163
|
+
))
|
|
3164
|
+
# Attribution matches `_extract_declaration_exports`'s
|
|
3165
|
+
# function re-export logic: defining_module is the ultimate
|
|
3166
|
+
# definer (originating_module); canonical_name is the
|
|
3167
|
+
# function's name in that module.
|
|
3168
|
+
ult_mod = func_infos[0].originating_module if func_infos else None
|
|
3169
|
+
ult_name = func_infos[0].name if func_infos else original_name
|
|
3170
|
+
install_binding(
|
|
3171
|
+
self.ctx.module_attributes, local_name,
|
|
3172
|
+
SymbolKind.FUNCTION, func_infos,
|
|
3173
|
+
defining_module=ult_mod or module_name,
|
|
3174
|
+
canonical_name=ult_name,
|
|
3175
|
+
)
|
|
3176
|
+
return True
|
|
3177
|
+
|
|
3178
|
+
# Check for record
|
|
3179
|
+
if module_info.records and original_name in module_info.records:
|
|
3180
|
+
record_info = module_info.records[original_name]
|
|
3181
|
+
# Register with local name for lookup (and original name for type resolution)
|
|
3182
|
+
self.ctx.registry.register_record(record_info, local_name)
|
|
3183
|
+
if local_name != original_name:
|
|
3184
|
+
self.ctx.registry.register_record(record_info, original_name)
|
|
3185
|
+
# Also register nested types so Outer.Inner resolves in the importing module.
|
|
3186
|
+
self._register_nested_with_alias(
|
|
3187
|
+
module_info.records, self.ctx.registry.register_record,
|
|
3188
|
+
original_name, local_name)
|
|
3189
|
+
self._register_nested_with_alias(
|
|
3190
|
+
module_info.enums, self.ctx.registry.register_enum,
|
|
3191
|
+
original_name, local_name)
|
|
3192
|
+
# Install with the chain-flattened attribution: defining_module
|
|
3193
|
+
# is the *ultimate* definer (record_info.defining_module),
|
|
3194
|
+
# canonical_name is the record's name in that module.
|
|
3195
|
+
install_binding(
|
|
3196
|
+
self.ctx.module_attributes, local_name,
|
|
3197
|
+
SymbolKind.RECORD, record_info,
|
|
3198
|
+
defining_module=record_info.defining_module,
|
|
3199
|
+
canonical_name=record_info.name,
|
|
3200
|
+
)
|
|
3201
|
+
return True
|
|
3202
|
+
|
|
3203
|
+
# Check for protocol
|
|
3204
|
+
if module_info.protocols and original_name in module_info.protocols:
|
|
3205
|
+
protocol_info = module_info.protocols[original_name]
|
|
3206
|
+
# Register with local name for lookup (supports aliases)
|
|
3207
|
+
self.ctx.registry.register_protocol(protocol_info, local_name)
|
|
3208
|
+
# Also bind in namespace so it can be resolved as a type
|
|
3209
|
+
self.ctx.global_ns.bind_imported_name(local_name, module_name, original_name)
|
|
3210
|
+
# Install with chain-flattened attribution: defining_module
|
|
3211
|
+
# is the *ultimate* definer (protocol_info.module), canonical
|
|
3212
|
+
# name is the protocol's name in that module. Mirrors the
|
|
3213
|
+
# records / enums / functions paths above; the prior
|
|
3214
|
+
# `defining_module=module_name` (immediate import source)
|
|
3215
|
+
# caused chain re-exports through `pkg/__init__.py` to
|
|
3216
|
+
# qualify protocols via the intermediate package, which
|
|
3217
|
+
# broke `RefAdapter` instantiation when the package's own
|
|
3218
|
+
# `using` was suppressed for sibling-cycle reasons.
|
|
3219
|
+
ult_mod = protocol_info.module if protocol_info.module else module_name
|
|
3220
|
+
install_binding(
|
|
3221
|
+
self.ctx.module_attributes, local_name,
|
|
3222
|
+
protocol_kind_for(protocol_info.is_dynamic), protocol_info,
|
|
3223
|
+
defining_module=ult_mod, canonical_name=protocol_info.name,
|
|
3224
|
+
)
|
|
3225
|
+
return True
|
|
3226
|
+
|
|
3227
|
+
# Check for type alias
|
|
3228
|
+
if module_info.type_aliases and original_name in module_info.type_aliases:
|
|
3229
|
+
alias_info = module_info.type_aliases[original_name]
|
|
3230
|
+
typ = alias_info.body
|
|
3231
|
+
self.ctx.registry.register_type_alias(local_name, typ,
|
|
3232
|
+
imported_from=(module_name, original_name),
|
|
3233
|
+
info=alias_info)
|
|
3234
|
+
# Implicitly import member record types so codegen can qualify them
|
|
3235
|
+
if isinstance(typ, UnionType) and module_info.records:
|
|
3236
|
+
for member in typ.members:
|
|
3237
|
+
if isinstance(member, NominalType) and member.name in module_info.records:
|
|
3238
|
+
if self.ctx.registry.get_record(member.name) is None:
|
|
3239
|
+
rec = module_info.records[member.name]
|
|
3240
|
+
self.ctx.registry.register_record(rec, member.name)
|
|
3241
|
+
install_binding(
|
|
3242
|
+
self.ctx.module_attributes, local_name,
|
|
3243
|
+
SymbolKind.TYPE_ALIAS, typ,
|
|
3244
|
+
defining_module=module_name, canonical_name=original_name,
|
|
3245
|
+
)
|
|
3246
|
+
return True
|
|
3247
|
+
|
|
3248
|
+
# Check for enum
|
|
3249
|
+
if module_info.enums and original_name in module_info.enums:
|
|
3250
|
+
enum_type = module_info.enums[original_name]
|
|
3251
|
+
self.ctx.registry.register_enum(enum_type, local_name)
|
|
3252
|
+
# Also register under original name: the parser resolves aliases
|
|
3253
|
+
# back to original names for type annotations (NominalType("Color")
|
|
3254
|
+
# even when the alias is "C")
|
|
3255
|
+
if local_name != original_name:
|
|
3256
|
+
self.ctx.registry.register_enum(enum_type, original_name)
|
|
3257
|
+
self.ctx.global_ns.bind_enum(enum_type, name=local_name)
|
|
3258
|
+
# Install with chain-flattened attribution: defining_module
|
|
3259
|
+
# is the ultimate declaring module (EnumInfo.module_name),
|
|
3260
|
+
# canonical_name is the enum's name in that module.
|
|
3261
|
+
einfo = enum_info_of(enum_type)
|
|
3262
|
+
ult_mod = einfo.module_name if (einfo and einfo.module_name) else module_name
|
|
3263
|
+
install_binding(
|
|
3264
|
+
self.ctx.module_attributes, local_name,
|
|
3265
|
+
SymbolKind.ENUM, enum_type,
|
|
3266
|
+
defining_module=ult_mod, canonical_name=enum_type.name,
|
|
3267
|
+
)
|
|
3268
|
+
return True
|
|
3269
|
+
|
|
3270
|
+
# Check for variable
|
|
3271
|
+
if module_info.variables and original_name in module_info.variables:
|
|
3272
|
+
var_info = module_info.variables[original_name]
|
|
3273
|
+
self.ctx.global_scope.define(local_name, var_info.type)
|
|
3274
|
+
self.ctx.global_ns.bind_variable(local_name, var_info.type)
|
|
3275
|
+
# Install with the chain-flattened ultimate defining module
|
|
3276
|
+
# so the consumer's `.hpp` renders the definer's qname
|
|
3277
|
+
# directly (matters when the immediate source is a
|
|
3278
|
+
# native_module facade with no .hpp).
|
|
3279
|
+
ult_mod, ult_name = resolve_definer(
|
|
3280
|
+
self.ctx.registry, module_name, original_name,
|
|
3281
|
+
SymbolKind.VARIABLE)
|
|
3282
|
+
install_binding(
|
|
3283
|
+
self.ctx.module_attributes, local_name,
|
|
3284
|
+
SymbolKind.VARIABLE, var_info.type,
|
|
3285
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3286
|
+
)
|
|
3287
|
+
return True
|
|
3288
|
+
|
|
3289
|
+
# @builtin_type/@builtin_decorator stubs and parser keywords are handled
|
|
3290
|
+
# at parse time, not exported by .py files -- silently skip them here.
|
|
3291
|
+
if is_parser_keyword(module_name, original_name):
|
|
3292
|
+
return True
|
|
3293
|
+
if (self.ctx.registry.get_builtin_type_key(original_name) or
|
|
3294
|
+
self.ctx.registry.get_builtin_decorator_key(original_name)):
|
|
3295
|
+
return True
|
|
3296
|
+
|
|
3297
|
+
# `from pkg import submod` where submod is itself a registered user
|
|
3298
|
+
# module: bind it as a MODULE so consumer code can do
|
|
3299
|
+
# `submod.X(...)` / `submod.Type` qualified access (mirrors CPython
|
|
3300
|
+
# which treats the imported name as the submodule namespace object).
|
|
3301
|
+
submodule_qname = f"{module_name}.{original_name}"
|
|
3302
|
+
if self.ctx.registry.get_module(submodule_qname) is not None:
|
|
3303
|
+
self.ctx.global_ns.bind_module(submodule_qname, alias=local_name)
|
|
3304
|
+
return True
|
|
3305
|
+
|
|
3306
|
+
# Macro re-export (Phase 6): the source module may itself be a
|
|
3307
|
+
# macro module that registers `original_name`, or it may be a
|
|
3308
|
+
# plain module that re-exports a macro. Walk the chain to find
|
|
3309
|
+
# the ultimate macro source and install a binding so consumer
|
|
3310
|
+
# sites pick up the macro under the right registry key.
|
|
3311
|
+
macro_chain = self._resolve_macro_chain(module_name, original_name)
|
|
3312
|
+
if macro_chain is not None:
|
|
3313
|
+
ult_mod, ult_name, kind = macro_chain
|
|
3314
|
+
install_binding(
|
|
3315
|
+
self.ctx.module_attributes, local_name,
|
|
3316
|
+
kind, None,
|
|
3317
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3318
|
+
)
|
|
3319
|
+
return True
|
|
3320
|
+
|
|
3321
|
+
# Cycle re-export fallback: the per-kind dicts on the source
|
|
3322
|
+
# ModuleInfo may not yet hold `original_name` because the source
|
|
3323
|
+
# is a cycle peer whose bind_imports hasn't run. Consult the
|
|
3324
|
+
# source's pre-populated attribute table, and if it has a
|
|
3325
|
+
# re-export binding, dispatch on kind to register the imported
|
|
3326
|
+
# name in the analyzer's registry just as the per-kind branches
|
|
3327
|
+
# above would.
|
|
3328
|
+
if module_info is not None and module_info.module_attributes is not None:
|
|
3329
|
+
src_cell = module_info.module_attributes.get(original_name)
|
|
3330
|
+
if src_cell is not None:
|
|
3331
|
+
if self._register_from_attribute_binding(
|
|
3332
|
+
src_cell.binding, local_name, module_name, original_name):
|
|
3333
|
+
return True
|
|
3334
|
+
|
|
3335
|
+
# Star imports include all public names from the source (matching
|
|
3336
|
+
# CPython), but not all of them have entries in ModuleInfo (e.g.
|
|
3337
|
+
# re-imported names like Int32 from tpy). Skip those so the caller
|
|
3338
|
+
# doesn't bind them as coming from this module.
|
|
3339
|
+
if from_star_import:
|
|
3340
|
+
return False
|
|
3341
|
+
|
|
3342
|
+
raise self._error(f"'{original_name}' not found in module '{module_name}'")
|
|
3343
|
+
|
|
3344
|
+
def _resolve_macro_chain(
|
|
3345
|
+
self, module_name: str, name: str,
|
|
3346
|
+
) -> 'tuple[str, str, SymbolKind] | None':
|
|
3347
|
+
"""Find the ultimate macro source for `(module_name, name)` by
|
|
3348
|
+
first probing the macro registry directly (handles macro
|
|
3349
|
+
modules that aren't registered as compiled modules) then
|
|
3350
|
+
walking the binding chain.
|
|
3351
|
+
|
|
3352
|
+
Returns (defining_module, canonical_name, SymbolKind), or None
|
|
3353
|
+
if no chain reaches a macro. Used by `_register_user_module_import`
|
|
3354
|
+
when the per-kind dict branches miss -- macros aren't tracked
|
|
3355
|
+
in the per-kind dicts on `ModuleInfo`.
|
|
3356
|
+
"""
|
|
3357
|
+
registry = self.ctx.macro_registry
|
|
3358
|
+
if registry is None:
|
|
3359
|
+
return None
|
|
3360
|
+
for kind, getter in (
|
|
3361
|
+
(SymbolKind.CLASS_MACRO, registry.get_macro),
|
|
3362
|
+
(SymbolKind.CALL_MACRO, registry.get_call_macro),
|
|
3363
|
+
(SymbolKind.BUILDER_MACRO, registry.get_builder_macro),
|
|
3364
|
+
):
|
|
3365
|
+
if getter(module_name, name) is not None:
|
|
3366
|
+
return (module_name, name, kind)
|
|
3367
|
+
result = walk_attribute_chain(
|
|
3368
|
+
self.ctx.registry, module_name, name, is_macro_kind)
|
|
3369
|
+
if result is None:
|
|
3370
|
+
return None
|
|
3371
|
+
ult_mod, ult_name, bd = result
|
|
3372
|
+
return (ult_mod, ult_name, bd.kind)
|
|
3373
|
+
|
|
3374
|
+
def _expand_builder_traces(self, module: TpyModule) -> None:
|
|
3375
|
+
"""Pass 5.5: walk every record-method body and free-function body
|
|
3376
|
+
in the module, running the builder-trace expander on each.
|
|
3377
|
+
Splices synthesized records / functions into ``module`` before
|
|
3378
|
+
passes 6/7 iterate, so emitted method bodies get sema-analyzed
|
|
3379
|
+
like user-written ones.
|
|
3380
|
+
|
|
3381
|
+
Top-level statements are expanded later, inside
|
|
3382
|
+
``_analyze_top_level`` (pass 4 is itself the body-analysis pass
|
|
3383
|
+
for module-level code, so expansion has to run there).
|
|
3384
|
+
|
|
3385
|
+
No-op when no @builder_macro classes are registered.
|
|
3386
|
+
"""
|
|
3387
|
+
if (self.ctx.macro_registry is None
|
|
3388
|
+
or not self.ctx.macro_registry.has_builder_macros()):
|
|
3389
|
+
return
|
|
3390
|
+
# Both loops iterate snapshots: `all_records()` already returns a
|
|
3391
|
+
# fresh list, and `list(module.functions)` is explicit because that
|
|
3392
|
+
# one is live. Records and functions synthesized during expansion
|
|
3393
|
+
# need no further expansion -- they are built from quoted fragments
|
|
3394
|
+
# / AST primitives, not user source containing more builder traces.
|
|
3395
|
+
for record in module.all_records():
|
|
3396
|
+
for method in record.methods:
|
|
3397
|
+
self._expand_builder_trace_body(
|
|
3398
|
+
method, f"{record.name}.{method.name}")
|
|
3399
|
+
for func in list(module.functions):
|
|
3400
|
+
self._expand_builder_trace_body(func, func.name)
|
|
3401
|
+
|
|
3402
|
+
def _expand_builder_trace_body(self, func: TpyFunction, function_qname: str) -> None:
|
|
3403
|
+
"""Run BuilderTraceExpander on a function/method body in place.
|
|
3404
|
+
|
|
3405
|
+
No-op when the macro registry has no @builder_macro classes or
|
|
3406
|
+
the body is empty.
|
|
3407
|
+
"""
|
|
3408
|
+
if not func.body:
|
|
3409
|
+
return
|
|
3410
|
+
if (self.ctx.macro_registry is None
|
|
3411
|
+
or not self.ctx.macro_registry.has_builder_macros()):
|
|
3412
|
+
return
|
|
3413
|
+
expander = BuilderTraceExpander(
|
|
3414
|
+
ctx=self.ctx, registrar=self.registrar,
|
|
3415
|
+
module=self._module, function_being_traced=function_qname,
|
|
3416
|
+
)
|
|
3417
|
+
new_body = expander.expand(func.body)
|
|
3418
|
+
if new_body is not None:
|
|
3419
|
+
func.body = new_body
|
|
3420
|
+
|
|
3421
|
+
def _expand_builder_trace_top_level(self, stmts: list[TpyStmt]) -> None:
|
|
3422
|
+
"""Run BuilderTraceExpander on module top-level statements in place.
|
|
3423
|
+
|
|
3424
|
+
Mutates ``stmts`` directly (the caller passed in module.top_level_stmts).
|
|
3425
|
+
"""
|
|
3426
|
+
if not stmts:
|
|
3427
|
+
return
|
|
3428
|
+
if (self.ctx.macro_registry is None
|
|
3429
|
+
or not self.ctx.macro_registry.has_builder_macros()):
|
|
3430
|
+
return
|
|
3431
|
+
expander = BuilderTraceExpander(
|
|
3432
|
+
ctx=self.ctx, registrar=self.registrar,
|
|
3433
|
+
module=self._module, function_being_traced="<module>",
|
|
3434
|
+
)
|
|
3435
|
+
new_stmts = expander.expand(stmts)
|
|
3436
|
+
if new_stmts is not None:
|
|
3437
|
+
stmts[:] = new_stmts
|
|
3438
|
+
|
|
3439
|
+
def _promote_macro_generated_types(self, module: TpyModule) -> None:
|
|
3440
|
+
"""Promote bare `NominalType` placeholders on macro-generated
|
|
3441
|
+
method signatures / bodies / registered `FunctionInfo` to
|
|
3442
|
+
qname-bearing form using the post-macro-deps `ctx.registry`.
|
|
3443
|
+
|
|
3444
|
+
Macros may reference types (JsonReader, JsonWriter, ...) that
|
|
3445
|
+
main.py doesn't import explicitly -- those land in the registry
|
|
3446
|
+
only after `_populate_macro_deps`, so promotion must run here
|
|
3447
|
+
rather than inside `register_record`.
|
|
3448
|
+
"""
|
|
3449
|
+
resolver = self.ctx.parser_resolver
|
|
3450
|
+
if resolver is None:
|
|
3451
|
+
return
|
|
3452
|
+
registry = self.ctx.registry
|
|
3453
|
+
for record in module.all_records():
|
|
3454
|
+
# AST-level method (TpyFunction) signatures + bodies.
|
|
3455
|
+
for method in record.methods:
|
|
3456
|
+
_promote_method_signature(method, registry)
|
|
3457
|
+
scope = _merged_method_scope(_record_scope(record), method)
|
|
3458
|
+
_walk_body(method.body, scope, resolver, promote_registry=registry)
|
|
3459
|
+
# Registered FunctionInfo (captured before promotion) --
|
|
3460
|
+
# rebuild ParamInfo / return_type in place so overload
|
|
3461
|
+
# resolution sees the promoted param types.
|
|
3462
|
+
info = registry.get_record(record.name)
|
|
3463
|
+
if info is None:
|
|
3464
|
+
continue
|
|
3465
|
+
for overloads in info.methods.values():
|
|
3466
|
+
for i, finfo in enumerate(overloads):
|
|
3467
|
+
new_params = [
|
|
3468
|
+
dc_replace(p, type=promote_bare_nominals(p.type, registry))
|
|
3469
|
+
for p in finfo.params
|
|
3470
|
+
]
|
|
3471
|
+
new_return = (
|
|
3472
|
+
promote_bare_nominals(finfo.return_type, registry)
|
|
3473
|
+
if finfo.return_type is not None else finfo.return_type
|
|
3474
|
+
)
|
|
3475
|
+
overloads[i] = dc_replace(
|
|
3476
|
+
finfo,
|
|
3477
|
+
params=new_params,
|
|
3478
|
+
return_type=new_return,
|
|
3479
|
+
)
|
|
3480
|
+
# Top-level functions: walk bodies so nested-def signatures
|
|
3481
|
+
# (resolved at parse time, before F.5 canonicalization) get
|
|
3482
|
+
# their bare NominalTypes promoted.
|
|
3483
|
+
for func in module.functions:
|
|
3484
|
+
_walk_body(func.body, None, resolver, promote_registry=registry)
|
|
3485
|
+
|
|
3486
|
+
def _resolve_builder_macro_chain(
|
|
3487
|
+
self, module_name: str, name: str,
|
|
3488
|
+
) -> str | None:
|
|
3489
|
+
"""Return the defining module of the builder macro reachable via
|
|
3490
|
+
the binding chain from `(module_name, name)`, or None if the
|
|
3491
|
+
chain doesn't end at a BUILDER_MACRO binding. Used by
|
|
3492
|
+
`_populate_macro_deps` to fold a re-exported builder macro's
|
|
3493
|
+
MACRO_DEPS into the consumer's macro_ns.
|
|
3494
|
+
"""
|
|
3495
|
+
result = walk_attribute_chain(
|
|
3496
|
+
self.ctx.registry, module_name, name,
|
|
3497
|
+
is_kind(SymbolKind.BUILDER_MACRO))
|
|
3498
|
+
return result[0] if result is not None else None
|
|
3499
|
+
|
|
3500
|
+
def _populate_macro_deps(self, module: TpyModule) -> None:
|
|
3501
|
+
"""Populate macro_ns with exports from MACRO_DEPS of used macro modules.
|
|
3502
|
+
|
|
3503
|
+
Collects dependency modules from all macro modules referenced by records
|
|
3504
|
+
in this module, then binds their public exports (records, functions,
|
|
3505
|
+
enums) into macro_ns -- a shadow namespace below global_ns so user
|
|
3506
|
+
bindings always take priority.
|
|
3507
|
+
"""
|
|
3508
|
+
macro_reg = self.ctx.macro_registry
|
|
3509
|
+
if macro_reg is None:
|
|
3510
|
+
return
|
|
3511
|
+
|
|
3512
|
+
# Collect all macro dep modules from records that have macros.
|
|
3513
|
+
# Maps dep_module -> name_filter (None = all exports, list = specific names).
|
|
3514
|
+
dep_modules: dict[str, list[str] | None] = {}
|
|
3515
|
+
|
|
3516
|
+
def _merge(mod_name: str) -> None:
|
|
3517
|
+
for dep, names in macro_reg.get_deps(mod_name).items():
|
|
3518
|
+
if dep not in dep_modules:
|
|
3519
|
+
dep_modules[dep] = names
|
|
3520
|
+
elif dep_modules[dep] is None or names is None:
|
|
3521
|
+
dep_modules[dep] = None # None wins (all exports)
|
|
3522
|
+
else:
|
|
3523
|
+
dep_modules[dep] = list(set(dep_modules[dep]) | set(names))
|
|
3524
|
+
|
|
3525
|
+
# Class macros: triggered by records carrying pending_macros.
|
|
3526
|
+
for record in module.all_records():
|
|
3527
|
+
if not record.pending_macros:
|
|
3528
|
+
continue
|
|
3529
|
+
for qname, _kwargs in record.pending_macros:
|
|
3530
|
+
mod_name = qname.rsplit(".", 1)[0] if "." in qname else ""
|
|
3531
|
+
_merge(mod_name)
|
|
3532
|
+
|
|
3533
|
+
# Builder-trace macros: expanded later (during body analysis), so
|
|
3534
|
+
# there's no equivalent of pending_macros to inspect here. Use the
|
|
3535
|
+
# import set as a proxy -- if this module imports a module that
|
|
3536
|
+
# has any @builder_macro classes registered, eagerly bring its
|
|
3537
|
+
# deps into macro_ns so synthesized code can reference them.
|
|
3538
|
+
# Walks re-export chains via ModuleInfo.module_attributes so
|
|
3539
|
+
# `from utils import ArgumentParser` (where utils re-exports
|
|
3540
|
+
# from argparse) pulls argparse's deps into main's macro_ns.
|
|
3541
|
+
builder_macro_modules = macro_reg.builder_macro_modules()
|
|
3542
|
+
for imported, names in module.imports.items():
|
|
3543
|
+
if imported in builder_macro_modules:
|
|
3544
|
+
_merge(imported)
|
|
3545
|
+
continue
|
|
3546
|
+
if not isinstance(names, set):
|
|
3547
|
+
continue
|
|
3548
|
+
for original_name, _local in names:
|
|
3549
|
+
ult = self._resolve_builder_macro_chain(imported, original_name)
|
|
3550
|
+
if ult is not None and ult in builder_macro_modules:
|
|
3551
|
+
_merge(ult)
|
|
3552
|
+
|
|
3553
|
+
if not dep_modules:
|
|
3554
|
+
return
|
|
3555
|
+
|
|
3556
|
+
self.ctx.macro_dep_modules = set(dep_modules.keys())
|
|
3557
|
+
|
|
3558
|
+
# Bind exports from each dep module into macro_ns
|
|
3559
|
+
for dep_mod_name, name_filter in dep_modules.items():
|
|
3560
|
+
module_info = self.ctx.registry.get_module(dep_mod_name)
|
|
3561
|
+
if module_info is None:
|
|
3562
|
+
continue
|
|
3563
|
+
|
|
3564
|
+
# Bind the module name itself so synthesized code can use
|
|
3565
|
+
# the qualified form (e.g. ``sys.argv`` / ``tpy.copy``).
|
|
3566
|
+
# Module attribute access then resolves through ModuleInfo
|
|
3567
|
+
# without polluting the user's namespace with every export.
|
|
3568
|
+
if name_filter is None and dep_mod_name not in self.ctx.macro_ns:
|
|
3569
|
+
self.ctx.macro_ns.bind_module(dep_mod_name)
|
|
3570
|
+
|
|
3571
|
+
# Macro-dep symbols flow into the consumer's per-module
|
|
3572
|
+
# attribute table as imports so the re-export emit picks
|
|
3573
|
+
# them up (the macro-expanded code references these names,
|
|
3574
|
+
# so the consumer's `.hpp` needs to alias them in its
|
|
3575
|
+
# namespace just like any other `from M import X`).
|
|
3576
|
+
if module_info.records:
|
|
3577
|
+
for name, record_info in module_info.records.items():
|
|
3578
|
+
if name_filter is not None and name not in name_filter:
|
|
3579
|
+
continue
|
|
3580
|
+
if self.ctx.registry.get_record(name) is None:
|
|
3581
|
+
self.ctx.registry.register_record(record_info, name)
|
|
3582
|
+
self.ctx.macro_ns.bind_imported_name(name, dep_mod_name, name)
|
|
3583
|
+
self.ctx.imported_names.setdefault(name, (dep_mod_name, name))
|
|
3584
|
+
install_binding(
|
|
3585
|
+
self.ctx.module_attributes, name,
|
|
3586
|
+
SymbolKind.RECORD, record_info,
|
|
3587
|
+
defining_module=(record_info.defining_module or dep_mod_name),
|
|
3588
|
+
canonical_name=record_info.name,
|
|
3589
|
+
)
|
|
3590
|
+
|
|
3591
|
+
if module_info.functions:
|
|
3592
|
+
for name, func_infos in module_info.functions.items():
|
|
3593
|
+
if name_filter is not None and name not in name_filter:
|
|
3594
|
+
continue
|
|
3595
|
+
is_special = func_infos and func_infos[0].special_handling
|
|
3596
|
+
if not is_special:
|
|
3597
|
+
if self.ctx.registry.get_function(name) is None:
|
|
3598
|
+
self.ctx.registry.register_function_group(name, func_infos)
|
|
3599
|
+
self.ctx.macro_ns.bind_imported_name(name, dep_mod_name, name)
|
|
3600
|
+
self.ctx.imported_names.setdefault(name, (dep_mod_name, name))
|
|
3601
|
+
ult_mod = (func_infos[0].originating_module
|
|
3602
|
+
if func_infos else None) or dep_mod_name
|
|
3603
|
+
ult_name = func_infos[0].name if func_infos else name
|
|
3604
|
+
install_binding(
|
|
3605
|
+
self.ctx.module_attributes, name,
|
|
3606
|
+
SymbolKind.FUNCTION, func_infos,
|
|
3607
|
+
defining_module=ult_mod, canonical_name=ult_name,
|
|
3608
|
+
)
|
|
3609
|
+
|
|
3610
|
+
if module_info.enums:
|
|
3611
|
+
for name, enum_type in module_info.enums.items():
|
|
3612
|
+
if name_filter is not None and name not in name_filter:
|
|
3613
|
+
continue
|
|
3614
|
+
if self.ctx.registry.get_enum(name) is None:
|
|
3615
|
+
self.ctx.registry.register_enum(enum_type, name)
|
|
3616
|
+
self.ctx.macro_ns.bind_enum(enum_type, name=name)
|
|
3617
|
+
self.ctx.imported_names.setdefault(name, (dep_mod_name, name))
|
|
3618
|
+
einfo = enum_info_of(enum_type)
|
|
3619
|
+
ult_mod = (einfo.module_name if einfo and einfo.module_name
|
|
3620
|
+
else dep_mod_name)
|
|
3621
|
+
install_binding(
|
|
3622
|
+
self.ctx.module_attributes, name,
|
|
3623
|
+
SymbolKind.ENUM, enum_type,
|
|
3624
|
+
defining_module=ult_mod, canonical_name=enum_type.name,
|
|
3625
|
+
)
|