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/methods.py
ADDED
|
@@ -0,0 +1,2197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Method Analysis
|
|
3
|
+
|
|
4
|
+
Method call and super() analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import copy
|
|
9
|
+
from typing import TYPE_CHECKING, Callable
|
|
10
|
+
|
|
11
|
+
from ..typesys import (
|
|
12
|
+
TpyType, NominalType, OwnType, OptionalType, PendingListType, PendingDictType, PendingSetType,
|
|
13
|
+
SuperType, TypeParamRef, FunctionInfo, ParamInfo, VOID, is_protocol_type,
|
|
14
|
+
PtrType, ReadonlyType, unwrap_readonly, UnknownElementType,
|
|
15
|
+
PendingGenericInstanceType, IntLiteralType, CallableType, unwrap_ref_type, unwrap_qualifiers, is_any_int_type,
|
|
16
|
+
is_callable_type,
|
|
17
|
+
RecordInfo,
|
|
18
|
+
contains_type_param,
|
|
19
|
+
)
|
|
20
|
+
from ..parse import (
|
|
21
|
+
TpyCall, TpyMethodCall, TpyName, TpyFieldAccess, TpyFunction, TpyExprStmt, TpyStrLiteral, TpyStmt,
|
|
22
|
+
TpyLambda, TpyStarUnpack,
|
|
23
|
+
is_docstring,
|
|
24
|
+
TpyFString, TpyExpr,
|
|
25
|
+
is_super_del_call,
|
|
26
|
+
)
|
|
27
|
+
from ..namespace import BindingKind
|
|
28
|
+
from ..coercions import CoercionContext
|
|
29
|
+
from ..prescan import _expr_to_narrowing_key
|
|
30
|
+
from .narrowing import deref_view_narrowed
|
|
31
|
+
from ..diagnostics import OPTIONAL_NONE_ACCESS_WARNING, SemanticError
|
|
32
|
+
from ..type_def_registry import is_list, is_fstr_type
|
|
33
|
+
from .overloads import resolve_overload, OverloadAmbiguityError
|
|
34
|
+
from .bound_check import raise_if_class_param_bound_violated
|
|
35
|
+
from .calls import (
|
|
36
|
+
arity_error_msg, resolve_kwargs, validate_generic_defaults,
|
|
37
|
+
validate_type_param_bounds,
|
|
38
|
+
_enrich_literal_types,
|
|
39
|
+
resolve_inferred_type_arg,
|
|
40
|
+
)
|
|
41
|
+
from .type_ops import seeded_arg_hint
|
|
42
|
+
from .statements import _root_name_of_expr, _is_self_call_deferred, _local_traces_to_self
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from .context import SemanticContext
|
|
46
|
+
from .type_ops import TypeOperations
|
|
47
|
+
from .protocols import ProtocolChecker
|
|
48
|
+
from .compatibility import TypeCompatibility
|
|
49
|
+
from .local_deduction import LocalTypeDeduction
|
|
50
|
+
from .expressions import ExpressionAnalyzer
|
|
51
|
+
from .calls import CallAnalyzer
|
|
52
|
+
from ..typesys import PendingGenericInstanceInfo
|
|
53
|
+
|
|
54
|
+
from .context import _storage_key
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _unresolved_params_in_type(typ: TpyType, inferred: dict[str, TpyType], param_names: set[str]) -> list[str]:
|
|
58
|
+
"""Return list of type param names that appear in typ but are not yet in inferred."""
|
|
59
|
+
result: list[str] = []
|
|
60
|
+
_collect_unresolved(typ, inferred, param_names, result)
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _collect_unresolved(
|
|
65
|
+
typ: TpyType, inferred: dict[str, TpyType], param_names: set[str], out: list[str],
|
|
66
|
+
) -> None:
|
|
67
|
+
if isinstance(typ, TypeParamRef):
|
|
68
|
+
if typ.name in param_names and typ.name not in inferred and typ.name not in out:
|
|
69
|
+
out.append(typ.name)
|
|
70
|
+
return
|
|
71
|
+
if isinstance(typ, NominalType) and typ.type_args:
|
|
72
|
+
for a in typ.type_args:
|
|
73
|
+
if isinstance(a, TpyType):
|
|
74
|
+
_collect_unresolved(a, inferred, param_names, out)
|
|
75
|
+
return
|
|
76
|
+
for attr in ('element_type', 'pointee', 'inner', 'wrapped'):
|
|
77
|
+
inner = getattr(typ, attr, None)
|
|
78
|
+
if inner is not None and isinstance(inner, TpyType):
|
|
79
|
+
_collect_unresolved(inner, inferred, param_names, out)
|
|
80
|
+
if hasattr(typ, 'element_types'):
|
|
81
|
+
for e in typ.element_types:
|
|
82
|
+
_collect_unresolved(e, inferred, param_names, out)
|
|
83
|
+
if hasattr(typ, 'key_type') and hasattr(typ, 'value_type'):
|
|
84
|
+
_collect_unresolved(typ.key_type, inferred, param_names, out)
|
|
85
|
+
_collect_unresolved(typ.value_type, inferred, param_names, out)
|
|
86
|
+
if hasattr(typ, 'members'):
|
|
87
|
+
for m in typ.members:
|
|
88
|
+
_collect_unresolved(m, inferred, param_names, out)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_dotted_record_chain(expr: TpyFieldAccess, ctx: 'SemanticContext') -> str | None:
|
|
92
|
+
"""Resolve a chain of field accesses to a nested record dotted name.
|
|
93
|
+
|
|
94
|
+
Returns the dotted name (e.g., "Outer.Mid") if the chain resolves to a
|
|
95
|
+
registered nested record, or None otherwise.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(expr.obj, TpyName):
|
|
98
|
+
if ctx.func.current_ns:
|
|
99
|
+
binding = ctx.func.current_ns.lookup(expr.obj.name)
|
|
100
|
+
if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
|
|
101
|
+
dotted = f"{expr.obj.name}.{expr.field}"
|
|
102
|
+
if ctx.registry.get_record(dotted) is not None:
|
|
103
|
+
return dotted
|
|
104
|
+
elif isinstance(expr.obj, TpyFieldAccess):
|
|
105
|
+
parent = _resolve_dotted_record_chain(expr.obj, ctx)
|
|
106
|
+
if parent is not None:
|
|
107
|
+
dotted = f"{parent}.{expr.field}"
|
|
108
|
+
if ctx.registry.get_record(dotted) is not None:
|
|
109
|
+
return dotted
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class MethodAnalyzer:
|
|
114
|
+
"""Method call and super() analysis."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
ctx: SemanticContext,
|
|
119
|
+
type_ops: TypeOperations,
|
|
120
|
+
protocols: ProtocolChecker,
|
|
121
|
+
compat: TypeCompatibility,
|
|
122
|
+
deduction: LocalTypeDeduction,
|
|
123
|
+
):
|
|
124
|
+
self.ctx = ctx
|
|
125
|
+
self.type_ops = type_ops
|
|
126
|
+
self.protocols = protocols
|
|
127
|
+
self.compat = compat
|
|
128
|
+
self.deduction = deduction
|
|
129
|
+
# Set after construction to break circular dep (expr <-> calls <-> methods)
|
|
130
|
+
self.expr: ExpressionAnalyzer
|
|
131
|
+
self.calls: CallAnalyzer
|
|
132
|
+
|
|
133
|
+
def _is_readonly_method(self, obj_type: TpyType, method_name: str) -> bool:
|
|
134
|
+
"""Check if a method is readonly on the given type (via RecordInfo)."""
|
|
135
|
+
record = self.ctx.registry.get_record_for_type(obj_type)
|
|
136
|
+
if record is None:
|
|
137
|
+
return False
|
|
138
|
+
overloads = record.get_method_overloads(method_name)
|
|
139
|
+
return bool(overloads) and all(m.is_readonly for m in overloads)
|
|
140
|
+
|
|
141
|
+
def _is_invalidating_method(self, obj_type: TpyType, method_name: str) -> bool:
|
|
142
|
+
"""Check if a method invalidates iterators/references on a container.
|
|
143
|
+
|
|
144
|
+
For builtin types (list, dict, set, etc.): a method invalidates if it
|
|
145
|
+
is non-readonly AND not marked with @native_preserves_refs.
|
|
146
|
+
For user-defined types: fall back to treating any non-readonly method
|
|
147
|
+
as potentially invalidating, since we can't know if it reallocates.
|
|
148
|
+
"""
|
|
149
|
+
# Builtin types: precise check using native_preserves_refs
|
|
150
|
+
qname = obj_type.qualified_name()
|
|
151
|
+
if qname:
|
|
152
|
+
builtin_record = self.ctx.registry.get_builtin_record(qname)
|
|
153
|
+
if builtin_record is not None:
|
|
154
|
+
overloads = builtin_record.get_method_overloads(method_name)
|
|
155
|
+
if not overloads:
|
|
156
|
+
return False
|
|
157
|
+
return any(not m.is_readonly and not m.native_preserves_refs for m in overloads)
|
|
158
|
+
# User-defined types: use inferred direct_self_mutated from Phase 1
|
|
159
|
+
# mutation analysis. More precise than is_readonly -- a method that
|
|
160
|
+
# doesn't mutate self won't invalidate references even without
|
|
161
|
+
# @readonly annotation. None means not yet analyzed (forward ref);
|
|
162
|
+
# treat conservatively as potentially mutating.
|
|
163
|
+
record = self.ctx.registry.get_record_for_type(obj_type)
|
|
164
|
+
if record is None:
|
|
165
|
+
return False
|
|
166
|
+
overloads = record.get_method_overloads(method_name)
|
|
167
|
+
if not overloads:
|
|
168
|
+
return False
|
|
169
|
+
# Use inferred structural mutation: only methods that directly
|
|
170
|
+
# structurally mutate self (append/insert/del on self's fields) can
|
|
171
|
+
# invalidate references. Getters and field-only mutations are safe.
|
|
172
|
+
# None means not yet analyzed (forward ref) -> conservative.
|
|
173
|
+
# Note: indirect structural mutation through same-class method calls
|
|
174
|
+
# is not detected here (requires Phase 2 propagation, which runs
|
|
175
|
+
# after body analysis). This is a known limitation.
|
|
176
|
+
for m in overloads:
|
|
177
|
+
if m.is_readonly:
|
|
178
|
+
continue
|
|
179
|
+
smp = m.direct_structural_mutated_params
|
|
180
|
+
if smp is None or -1 in smp:
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def _infer_pending_container_element(
|
|
185
|
+
self,
|
|
186
|
+
expr: TpyMethodCall,
|
|
187
|
+
obj_type: TpyType,
|
|
188
|
+
builtin_qname: str,
|
|
189
|
+
literal_id: int,
|
|
190
|
+
infer_fn: Callable,
|
|
191
|
+
make_pending: Callable,
|
|
192
|
+
literals_dict: dict,
|
|
193
|
+
) -> TpyType | None:
|
|
194
|
+
"""Infer element type for a pending container from method arg types.
|
|
195
|
+
|
|
196
|
+
Looks up the method's FunctionInfo, finds params that involve the
|
|
197
|
+
container's type parameters, pre-analyzes the corresponding args,
|
|
198
|
+
and infers element types. Returns the updated obj_type if changed.
|
|
199
|
+
"""
|
|
200
|
+
record = self.ctx.registry.get_builtin_record(builtin_qname)
|
|
201
|
+
if not record or not record.type_params:
|
|
202
|
+
return None
|
|
203
|
+
type_param_names = set(record.type_params)
|
|
204
|
+
overloads = record.get_method_overloads(expr.method)
|
|
205
|
+
if not overloads:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
# Find an overload with matching arity that has type-param-bearing params
|
|
209
|
+
for overload in overloads:
|
|
210
|
+
if len(overload.params) != len(expr.args):
|
|
211
|
+
continue
|
|
212
|
+
# Identify which args correspond to type-parameter-bearing params
|
|
213
|
+
inferring_indices: list[int] = []
|
|
214
|
+
for i, param in enumerate(overload.params):
|
|
215
|
+
if contains_type_param(param.type, type_param_names):
|
|
216
|
+
inferring_indices.append(i)
|
|
217
|
+
if not inferring_indices:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Pre-analyze all args (needed for _check_and_coerce_args reuse).
|
|
221
|
+
# analyze_call_arg (not analyze_expr) so a `*xs` arg yields the
|
|
222
|
+
# unpacked element type instead of hitting the structural
|
|
223
|
+
# analyzer's "Unknown expression type" catch-all; the non-variadic
|
|
224
|
+
# reject gate in _check_args_or_pack_varargs still rejects it.
|
|
225
|
+
pre_analyzed = [self.expr.analyze_call_arg(arg) for arg in expr.args]
|
|
226
|
+
|
|
227
|
+
# Infer element type from params that directly carry a type param
|
|
228
|
+
# (T or Own[T]). Params with nested type params like Iterable[Own[T]]
|
|
229
|
+
# are detected by contains_type_param but not handled here --
|
|
230
|
+
# inference from those would need protocol-level element type extraction.
|
|
231
|
+
for i in inferring_indices:
|
|
232
|
+
arg_type = pre_analyzed[i]
|
|
233
|
+
param_type = unwrap_ref_type(overload.params[i].type)
|
|
234
|
+
if isinstance(param_type, OwnType) and isinstance(param_type.wrapped, TypeParamRef):
|
|
235
|
+
infer_fn(expr.obj, arg_type)
|
|
236
|
+
elif isinstance(param_type, TypeParamRef):
|
|
237
|
+
infer_fn(expr.obj, arg_type)
|
|
238
|
+
|
|
239
|
+
# Update obj_type if element type changed
|
|
240
|
+
info = literals_dict.get(literal_id)
|
|
241
|
+
if info and not isinstance(info.element_type, UnknownElementType):
|
|
242
|
+
if info.element_type != obj_type.get_element_type():
|
|
243
|
+
obj_type = make_pending(info.element_type, literal_id)
|
|
244
|
+
self.ctx.set_expr_type(expr.obj, obj_type)
|
|
245
|
+
if isinstance(expr.obj, TpyName):
|
|
246
|
+
if self.ctx.func.current_scope:
|
|
247
|
+
self.ctx.func.current_scope.define(expr.obj.name, obj_type)
|
|
248
|
+
if self.ctx.func.current_ns:
|
|
249
|
+
self.ctx.func.current_ns.bind_variable(expr.obj.name, obj_type)
|
|
250
|
+
self.ctx.func.pre_analyzed_method_args[id(expr)] = pre_analyzed
|
|
251
|
+
return obj_type
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def _probe_analyze_method_args(
|
|
255
|
+
self,
|
|
256
|
+
expr: TpyMethodCall,
|
|
257
|
+
resolved_overloads: list[FunctionInfo],
|
|
258
|
+
) -> list[TpyType]:
|
|
259
|
+
"""Pre-analyze overload-candidate method args with an LHS-derived hint.
|
|
260
|
+
|
|
261
|
+
Method-call sibling of ``CallAnalyzer._probe_analyze_args``: each
|
|
262
|
+
substituted candidate's per-position hint comes from
|
|
263
|
+
``TypeOperations.candidate_arg_hints``; the first non-None hint at
|
|
264
|
+
each position drives the arg's first analysis.
|
|
265
|
+
|
|
266
|
+
Without this, the inner generic-call / record-ctor arg would cache
|
|
267
|
+
a hint-naive type during probe pre-analysis, and the post-selection
|
|
268
|
+
``_check_and_coerce_args`` would re-use that cached type verbatim
|
|
269
|
+
(the cache short-circuit at the top of
|
|
270
|
+
``_analyze_generic_function_call`` / ``_analyze_record_constructor``
|
|
271
|
+
prevents the re-analysis from refreshing it).
|
|
272
|
+
"""
|
|
273
|
+
lhs_hint = self.ctx.expr_type_hint
|
|
274
|
+
if lhs_hint is None:
|
|
275
|
+
return [self.expr.analyze_call_arg(arg) for arg in expr.args]
|
|
276
|
+
|
|
277
|
+
n = len(expr.args)
|
|
278
|
+
candidate_hints = [
|
|
279
|
+
self.type_ops.candidate_arg_hints(f, n, lhs_hint) for f in resolved_overloads
|
|
280
|
+
]
|
|
281
|
+
# Require a single LHS-matching candidate (see
|
|
282
|
+
# CallAnalyzer._probe_analyze_args for the cross-candidate-mixing
|
|
283
|
+
# rationale). ``candidate_arg_hints`` returns None for non-matching
|
|
284
|
+
# candidates; the count tracks LHS-matchers, not hint-contributors.
|
|
285
|
+
matching = [c for c in candidate_hints if c is not None]
|
|
286
|
+
chosen: list[TpyType | None] = (
|
|
287
|
+
list(matching[0]) if len(matching) == 1 else [None] * n
|
|
288
|
+
)
|
|
289
|
+
arg_types: list[TpyType] = []
|
|
290
|
+
for i, arg in enumerate(expr.args):
|
|
291
|
+
hint = chosen[i]
|
|
292
|
+
# See CallAnalyzer._probe_analyze_args for the (TpyName | TpyLambda)
|
|
293
|
+
# + Callable filter rationale: avoid mutating AST state on the arg
|
|
294
|
+
# node before overload selection.
|
|
295
|
+
if hint is not None and is_callable_type(hint) and isinstance(arg, (TpyName, TpyLambda)):
|
|
296
|
+
hint = None
|
|
297
|
+
arg_types.append(self.expr.analyze_call_arg(arg, hint))
|
|
298
|
+
return arg_types
|
|
299
|
+
|
|
300
|
+
def _check_args_or_pack_varargs(
|
|
301
|
+
self, expr: TpyMethodCall,
|
|
302
|
+
resolved: FunctionInfo,
|
|
303
|
+
arg_types: list[TpyType] | None = None,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Dispatch arg checking based on whether the method has *args.
|
|
306
|
+
|
|
307
|
+
Variadic methods route through `_analyze_and_pack_varargs` (mirrors
|
|
308
|
+
the free-function path). The pre-analyzed cache is dropped because
|
|
309
|
+
it holds types analyzed against Span[T], not the element type T
|
|
310
|
+
the packer needs. ``arg_types`` is ignored on the variadic path --
|
|
311
|
+
the packer re-analyzes every arg from scratch.
|
|
312
|
+
"""
|
|
313
|
+
if resolved.has_variadic:
|
|
314
|
+
self.ctx.func.pre_analyzed_method_args.pop(id(expr), None)
|
|
315
|
+
self.calls._analyze_and_pack_varargs(expr, resolved)
|
|
316
|
+
else:
|
|
317
|
+
# `*unpack` is only valid at a variadic param position; a
|
|
318
|
+
# non-variadic method must reject it (mirrors the free-function
|
|
319
|
+
# guard in CallAnalyzer._typecheck_call_args). Without this the
|
|
320
|
+
# pre-analyzed element type would be silently coerced as if the
|
|
321
|
+
# unpack were a single positional arg.
|
|
322
|
+
for arg in expr.args:
|
|
323
|
+
if isinstance(arg, TpyStarUnpack):
|
|
324
|
+
raise self.ctx.error(
|
|
325
|
+
f"Cannot use *unpacking: '{resolved.name}' "
|
|
326
|
+
f"does not accept *args", arg)
|
|
327
|
+
self._check_and_coerce_args(expr, resolved.params, arg_types,
|
|
328
|
+
target_is_readonly=resolved.is_readonly)
|
|
329
|
+
|
|
330
|
+
def _check_and_coerce_args(
|
|
331
|
+
self, expr: TpyMethodCall,
|
|
332
|
+
params: list[tuple[str, TpyType]],
|
|
333
|
+
arg_types: list[TpyType] | None = None,
|
|
334
|
+
target_is_readonly: bool = False,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Analyze, ownership-check, and coerce method arguments in place.
|
|
337
|
+
|
|
338
|
+
If arg_types is None, each arg is analyzed with a type hint from the
|
|
339
|
+
corresponding param (using pre-analyzed types from empty list inference
|
|
340
|
+
when available). Otherwise pre-analyzed arg_types are used.
|
|
341
|
+
"""
|
|
342
|
+
pre = self.ctx.func.pre_analyzed_method_args.pop(id(expr), None)
|
|
343
|
+
for i, (arg, (pname, ptype)) in enumerate(zip(expr.args, params)):
|
|
344
|
+
if arg_types is not None:
|
|
345
|
+
at = arg_types[i]
|
|
346
|
+
elif pre is not None and i < len(pre):
|
|
347
|
+
at = pre[i]
|
|
348
|
+
else:
|
|
349
|
+
at = self.expr.analyze_expr_with_hint(arg, ptype)
|
|
350
|
+
self.calls._maybe_coerce_empty_list_to_protocol(at, ptype)
|
|
351
|
+
at = self.calls._restore_readonly_arg(arg, at, target_is_readonly)
|
|
352
|
+
self.calls.check_own_param(arg, at, pname, ptype)
|
|
353
|
+
expr.args[i] = self.compat.coerce_expr(arg, at, ptype, f"argument '{pname}'",
|
|
354
|
+
coercion_ctx=CoercionContext.ARG)
|
|
355
|
+
|
|
356
|
+
def _resolve_and_check_args(
|
|
357
|
+
self, expr: TpyMethodCall,
|
|
358
|
+
overloads: list[FunctionInfo],
|
|
359
|
+
type_subst: dict[str, TpyType | int],
|
|
360
|
+
is_readonly_receiver: bool = False,
|
|
361
|
+
is_consuming_receiver: bool = False,
|
|
362
|
+
) -> TpyType:
|
|
363
|
+
"""Resolve overloads, substitute type params, check arg count, and coerce args.
|
|
364
|
+
|
|
365
|
+
Sets expr.resolved_function_info. Returns the return type.
|
|
366
|
+
"""
|
|
367
|
+
# Pack **kwargs into TypedDict construction for methods with **kwargs param
|
|
368
|
+
if len(overloads) == 1 and overloads[0].kwarg_name:
|
|
369
|
+
func = overloads[0]
|
|
370
|
+
resolved_func = (self.type_ops.substitute_method_type_params(func, type_subst)
|
|
371
|
+
if type_subst else func)
|
|
372
|
+
self.calls._pack_kwargs_into_typed_dict_method(expr, resolved_func)
|
|
373
|
+
elif expr.double_star_unpack is not None:
|
|
374
|
+
raise self.ctx.error(
|
|
375
|
+
f"'{expr.method}' does not accept **kwargs", expr)
|
|
376
|
+
|
|
377
|
+
# Resolve kwargs before arity check. For the single-overload path we
|
|
378
|
+
# expand kwargs here; the multi-overload path defers resolution to
|
|
379
|
+
# after overload resolution so kwargs can participate in tier ranking.
|
|
380
|
+
has_kwonly = any(p.keyword_only for p in (overloads[0].params if len(overloads) == 1 else []))
|
|
381
|
+
if len(overloads) == 1 and (expr.kwargs or has_kwonly):
|
|
382
|
+
target = overloads[0]
|
|
383
|
+
resolved_target = (self.type_ops.substitute_method_type_params(target, type_subst)
|
|
384
|
+
if type_subst else target)
|
|
385
|
+
expr.args = resolve_kwargs(
|
|
386
|
+
expr.args, expr.kwargs, resolved_target.params, expr.method,
|
|
387
|
+
lambda msg: self.ctx.error(msg, expr),
|
|
388
|
+
call_loc=expr.loc,
|
|
389
|
+
)
|
|
390
|
+
expr.kwargs = {}
|
|
391
|
+
|
|
392
|
+
if len(overloads) == 1:
|
|
393
|
+
unresolved = overloads[0]
|
|
394
|
+
resolved = (self.type_ops.substitute_method_type_params(unresolved, type_subst)
|
|
395
|
+
if type_subst else unresolved)
|
|
396
|
+
if len(expr.args) < resolved.min_args or len(expr.args) > resolved.max_args:
|
|
397
|
+
raise self.ctx.error(
|
|
398
|
+
arity_error_msg(expr.method, resolved.min_args, resolved.max_args, len(expr.args)),
|
|
399
|
+
expr)
|
|
400
|
+
expr.resolved_function_info = resolved
|
|
401
|
+
self._check_args_or_pack_varargs(expr, resolved)
|
|
402
|
+
if type_subst:
|
|
403
|
+
validate_generic_defaults(
|
|
404
|
+
expr.args, unresolved, type_subst, self.type_ops,
|
|
405
|
+
lambda msg: self.ctx.error(msg, expr))
|
|
406
|
+
else:
|
|
407
|
+
resolved_overloads = [
|
|
408
|
+
self.type_ops.substitute_method_type_params(m, type_subst) if type_subst else m
|
|
409
|
+
for m in overloads
|
|
410
|
+
]
|
|
411
|
+
arg_types = self._probe_analyze_method_args(expr, resolved_overloads)
|
|
412
|
+
kwarg_types: dict[str, TpyType] | None = None
|
|
413
|
+
if expr.kwargs:
|
|
414
|
+
kwarg_types = {k: self.expr.analyze_expr(v) for k, v in expr.kwargs.items()}
|
|
415
|
+
enriched_types = _enrich_literal_types(arg_types, expr.args, resolved_overloads)
|
|
416
|
+
try:
|
|
417
|
+
resolved = resolve_overload(
|
|
418
|
+
resolved_overloads, enriched_types,
|
|
419
|
+
protocol_checker=self.protocols.type_conforms_to_protocol,
|
|
420
|
+
protocol_classifier=self.protocols.classify_protocol_conformance,
|
|
421
|
+
deref_checker=self.type_ops.get_deref_coercion_target,
|
|
422
|
+
default_int_type=self.ctx.default_int_type,
|
|
423
|
+
subclass_checker=self.ctx.registry.is_subclass_of,
|
|
424
|
+
is_readonly_receiver=is_readonly_receiver,
|
|
425
|
+
is_consuming_receiver=is_consuming_receiver,
|
|
426
|
+
type_ops=self.type_ops,
|
|
427
|
+
kwarg_types=kwarg_types,
|
|
428
|
+
)
|
|
429
|
+
except OverloadAmbiguityError as e:
|
|
430
|
+
sigs = "; ".join(
|
|
431
|
+
f"{c.name}({', '.join(str(p.type) for p in c.params)})"
|
|
432
|
+
for c in e.candidates
|
|
433
|
+
)
|
|
434
|
+
raise self.ctx.error(
|
|
435
|
+
f"Ambiguous overload for '{expr.method}': "
|
|
436
|
+
f"multiple candidates match equally: {sigs}", expr)
|
|
437
|
+
if resolved is None:
|
|
438
|
+
if kwarg_types:
|
|
439
|
+
accepted_names = {p.name for o in overloads for p in o.params}
|
|
440
|
+
for kw_name in kwarg_types:
|
|
441
|
+
if kw_name not in accepted_names:
|
|
442
|
+
raise self.ctx.error(
|
|
443
|
+
f"'{expr.method}' got unexpected keyword argument '{kw_name}'",
|
|
444
|
+
expr)
|
|
445
|
+
arg_strs = ", ".join(str(t) for t in arg_types)
|
|
446
|
+
raise self.ctx.error(
|
|
447
|
+
f"No matching overload for '{expr.method}' with argument types ({arg_strs})", expr)
|
|
448
|
+
expr.resolved_function_info = resolved
|
|
449
|
+
coerce_arg_types: list[TpyType] | None = arg_types
|
|
450
|
+
if expr.kwargs:
|
|
451
|
+
# Winner picked; expand kwargs into positional slots against its signature.
|
|
452
|
+
expr.args = resolve_kwargs(
|
|
453
|
+
expr.args, expr.kwargs, resolved.params, expr.method,
|
|
454
|
+
lambda msg: self.ctx.error(msg, expr),
|
|
455
|
+
call_loc=expr.loc,
|
|
456
|
+
)
|
|
457
|
+
expr.kwargs = {}
|
|
458
|
+
# Pre-analyzed types no longer align with expanded expr.args;
|
|
459
|
+
# discard any stale empty-list inference cache so
|
|
460
|
+
# _check_and_coerce_args re-analyzes every arg with a param hint.
|
|
461
|
+
self.ctx.func.pre_analyzed_method_args.pop(id(expr), None)
|
|
462
|
+
coerce_arg_types = None
|
|
463
|
+
self._check_args_or_pack_varargs(expr, resolved, coerce_arg_types)
|
|
464
|
+
# Overloaded methods with generic defaults: find unresolved counterpart
|
|
465
|
+
if type_subst:
|
|
466
|
+
idx = resolved_overloads.index(resolved)
|
|
467
|
+
validate_generic_defaults(
|
|
468
|
+
expr.args, overloads[idx], type_subst, self.type_ops,
|
|
469
|
+
lambda msg: self.ctx.error(msg, expr))
|
|
470
|
+
|
|
471
|
+
self.calls._check_borrow_arg_conflicts(expr)
|
|
472
|
+
self.calls._check_loop_var_arg_mutation(expr)
|
|
473
|
+
self.calls._record_mutation_call_edges(expr)
|
|
474
|
+
self.calls._check_error_return_handled(expr, resolved)
|
|
475
|
+
return resolved.return_type
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _analyze_super_call_static(ctx: SemanticContext, expr: TpyCall) -> TpyType:
|
|
479
|
+
"""Analyze a super() call (static method for use from CallAnalyzer).
|
|
480
|
+
|
|
481
|
+
super() can only be called:
|
|
482
|
+
- Inside a method (not at module level)
|
|
483
|
+
- In a class that has a parent class
|
|
484
|
+
- Without arguments (Python 3 style)
|
|
485
|
+
|
|
486
|
+
Returns a SuperType that wraps the parent class type.
|
|
487
|
+
"""
|
|
488
|
+
# Validate context: must be in a method
|
|
489
|
+
if ctx.func.current_function is None or not isinstance(ctx.func.current_function, TpyFunction):
|
|
490
|
+
raise ctx.error("super() can only be used inside a method", expr)
|
|
491
|
+
|
|
492
|
+
if not ctx.func.current_function.is_method:
|
|
493
|
+
raise ctx.error("super() can only be used inside a method", expr)
|
|
494
|
+
|
|
495
|
+
if ctx.func.current_function.is_staticmethod:
|
|
496
|
+
raise ctx.error("super() cannot be used in a static method", expr)
|
|
497
|
+
|
|
498
|
+
# Validate context: must have a current record
|
|
499
|
+
if ctx.record_ctx.record is None:
|
|
500
|
+
raise ctx.error("super() can only be used inside a class method", expr)
|
|
501
|
+
|
|
502
|
+
# Validate: class must have at least one parent
|
|
503
|
+
record_info = ctx.registry.get_record(ctx.record_ctx.record.name)
|
|
504
|
+
if record_info is None or not record_info.parents:
|
|
505
|
+
raise ctx.error(
|
|
506
|
+
f"super() requires a parent class, but '{ctx.record_ctx.record.name}' has no parent",
|
|
507
|
+
expr
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Validate: no arguments (Python 3 style only)
|
|
511
|
+
if expr.args:
|
|
512
|
+
raise ctx.error("super() takes no arguments (Python 3 style)", expr)
|
|
513
|
+
|
|
514
|
+
# For multi-base classes, parent_type is a placeholder -- _analyze_super_method_call
|
|
515
|
+
# re-resolves it per method name. Single-base path uses it directly.
|
|
516
|
+
return SuperType(record_info.parents[0], ctx.record_ctx.record.name)
|
|
517
|
+
|
|
518
|
+
def _resolve_super_parent_type(self, expr: TpyMethodCall, super_type: SuperType) -> TpyType:
|
|
519
|
+
"""Pick which ancestor super().<method>() dispatches to.
|
|
520
|
+
|
|
521
|
+
Single-base uses super_type.parent_type directly. Multi-base (D22 v2.3)
|
|
522
|
+
walks the child's C3 MRO and returns the first ancestor whose own method
|
|
523
|
+
table defines `method` (matching Python's __dict__ walk -- inherited
|
|
524
|
+
methods don't count). super().__del__() is rejected; C++ invokes each
|
|
525
|
+
base's destructor automatically. Single-hop only; see LANGUAGE_FEATURES
|
|
526
|
+
"Limitation: single-hop super()" for why TPy can't replicate Python's
|
|
527
|
+
full cooperative chain under static dispatch.
|
|
528
|
+
"""
|
|
529
|
+
child_rec = self.ctx.registry.get_record(super_type.child_record_name)
|
|
530
|
+
if child_rec is None or len(child_rec.parents) < 2:
|
|
531
|
+
return super_type.parent_type
|
|
532
|
+
|
|
533
|
+
if expr.method == "__del__":
|
|
534
|
+
raise self.ctx.error(
|
|
535
|
+
f"super().__del__() is not allowed in multi-base class "
|
|
536
|
+
f"'{super_type.child_record_name}'. C++ invokes each base's destructor "
|
|
537
|
+
f"automatically; remove this call.",
|
|
538
|
+
expr
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
for anc_type in child_rec.mro_ancestors:
|
|
542
|
+
anc_info = self.ctx.registry.get_record_for_type(anc_type)
|
|
543
|
+
if anc_info is None:
|
|
544
|
+
continue
|
|
545
|
+
if anc_info.get_method_overloads(expr.method):
|
|
546
|
+
return anc_type
|
|
547
|
+
|
|
548
|
+
raise self.ctx.error(
|
|
549
|
+
f"No base of '{super_type.child_record_name}' defines method "
|
|
550
|
+
f"'{expr.method}'",
|
|
551
|
+
expr
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def _try_resolve_method(self, expr: TpyMethodCall, obj_type: TpyType,
|
|
555
|
+
is_readonly_receiver: bool = False,
|
|
556
|
+
is_consuming_receiver: bool = False) -> TpyType | None:
|
|
557
|
+
"""Try to resolve method on obj_type. Returns return type or None."""
|
|
558
|
+
result = self._analyze_instance_method(expr, obj_type, is_readonly_receiver, is_consuming_receiver)
|
|
559
|
+
if result is not None:
|
|
560
|
+
return result
|
|
561
|
+
result = self._analyze_protocol_or_bound_method(expr, obj_type)
|
|
562
|
+
if result is not None:
|
|
563
|
+
return result
|
|
564
|
+
# Callable-typed field invocation: obj.field(args) where field is Callable
|
|
565
|
+
result = self._try_callable_field_call(expr, obj_type)
|
|
566
|
+
if result is not None:
|
|
567
|
+
return result
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
def _try_callable_field_call(self, expr: TpyMethodCall, obj_type: TpyType) -> TpyType | None:
|
|
571
|
+
"""Check if expr.method is a Callable-typed field and analyze the call."""
|
|
572
|
+
if not isinstance(obj_type, NominalType):
|
|
573
|
+
return None
|
|
574
|
+
rec = self.ctx.registry.get_record(obj_type.name)
|
|
575
|
+
if rec is None:
|
|
576
|
+
return None
|
|
577
|
+
callable_type = None
|
|
578
|
+
is_optional_field = False
|
|
579
|
+
for fld in rec.fields:
|
|
580
|
+
if fld.name == expr.method:
|
|
581
|
+
fld_type = fld.type
|
|
582
|
+
if isinstance(fld_type, CallableType):
|
|
583
|
+
callable_type = fld_type
|
|
584
|
+
elif isinstance(fld_type, OptionalType) and isinstance(fld_type.inner, CallableType):
|
|
585
|
+
callable_type = fld_type.inner
|
|
586
|
+
is_optional_field = True
|
|
587
|
+
break
|
|
588
|
+
if callable_type is None:
|
|
589
|
+
return None
|
|
590
|
+
# Warn if calling Optional[Callable] field without narrowing
|
|
591
|
+
if is_optional_field:
|
|
592
|
+
narrowing_key = _expr_to_narrowing_key(expr.obj)
|
|
593
|
+
if narrowing_key is not None:
|
|
594
|
+
narrowing_key = f"{narrowing_key}.{expr.method}"
|
|
595
|
+
if narrowing_key is None or narrowing_key not in self.ctx.func.narrowed_types:
|
|
596
|
+
self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
|
|
597
|
+
expr.needs_optional_runtime_check = True
|
|
598
|
+
if expr.kwargs:
|
|
599
|
+
raise self.ctx.error(
|
|
600
|
+
"Keyword arguments are not supported for Callable-typed fields "
|
|
601
|
+
"(Callable types have no parameter names)", expr)
|
|
602
|
+
if len(expr.args) != len(callable_type.param_types):
|
|
603
|
+
raise self.ctx.error(
|
|
604
|
+
f"Callable field '{expr.method}' expects {len(callable_type.param_types)} argument(s), "
|
|
605
|
+
f"got {len(expr.args)}",
|
|
606
|
+
expr
|
|
607
|
+
)
|
|
608
|
+
for i, (arg, expected_type) in enumerate(zip(expr.args, callable_type.param_types)):
|
|
609
|
+
arg_type = self.expr.analyze_expr_with_hint(arg, expected_type)
|
|
610
|
+
if arg_type != expected_type:
|
|
611
|
+
try:
|
|
612
|
+
self.compat.check_type_compatible(
|
|
613
|
+
arg_type, expected_type,
|
|
614
|
+
f"argument {i + 1}", loc=expr.loc, source_expr=arg)
|
|
615
|
+
except SemanticError:
|
|
616
|
+
raise self.ctx.error(
|
|
617
|
+
f"Argument {i + 1}: expected '{expected_type}', got '{arg_type}'",
|
|
618
|
+
expr
|
|
619
|
+
)
|
|
620
|
+
expr.resolved_function_info = FunctionInfo(
|
|
621
|
+
name=expr.method,
|
|
622
|
+
params=[ParamInfo(f"__a{i}", t) for i, t in enumerate(callable_type.param_types)],
|
|
623
|
+
return_type=callable_type.return_type,
|
|
624
|
+
is_readonly=True,
|
|
625
|
+
)
|
|
626
|
+
expr.is_callable_field = True
|
|
627
|
+
return callable_type.return_type
|
|
628
|
+
|
|
629
|
+
def analyze_method_call(self, expr: TpyMethodCall) -> TpyType:
|
|
630
|
+
"""Analyze a method call."""
|
|
631
|
+
if isinstance(expr.obj, TpyName):
|
|
632
|
+
# ClassName.staticmethod() pattern
|
|
633
|
+
result = self._analyze_static_method_call(expr)
|
|
634
|
+
if result is not None:
|
|
635
|
+
return result
|
|
636
|
+
|
|
637
|
+
# Nested type constructor: Container.Inner(...) or Container.Kind(value)
|
|
638
|
+
if self.ctx.func.current_ns:
|
|
639
|
+
binding = self.ctx.func.current_ns.lookup(expr.obj.name)
|
|
640
|
+
if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
|
|
641
|
+
nested_result = self._analyze_nested_type_call(expr)
|
|
642
|
+
if nested_result is not None:
|
|
643
|
+
return nested_result
|
|
644
|
+
|
|
645
|
+
# Reject method calls on enum types (enums have no class methods)
|
|
646
|
+
if self.ctx.func.current_ns:
|
|
647
|
+
binding = self.ctx.func.current_ns.lookup(expr.obj.name)
|
|
648
|
+
if binding and binding.kind == BindingKind.ENUM:
|
|
649
|
+
raise self.ctx.error(
|
|
650
|
+
f"Enum type '{expr.obj.name}' has no method '{expr.method}'",
|
|
651
|
+
expr,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# module.function() pattern (import X -> X.func())
|
|
655
|
+
result = self._analyze_module_method_call(expr)
|
|
656
|
+
if result is not None:
|
|
657
|
+
return result
|
|
658
|
+
|
|
659
|
+
# Nested type constructor via chained access: Outer.Mid.Deep(...)
|
|
660
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
661
|
+
parent_dotted = _resolve_dotted_record_chain(expr.obj, self.ctx)
|
|
662
|
+
if parent_dotted is not None:
|
|
663
|
+
dotted = f"{parent_dotted}.{expr.method}"
|
|
664
|
+
nested_record = self.ctx.registry.get_record(dotted)
|
|
665
|
+
if nested_record is not None:
|
|
666
|
+
fake_call = TpyCall(
|
|
667
|
+
func=TpyName(name=dotted, loc=expr.loc),
|
|
668
|
+
args=expr.args,
|
|
669
|
+
kwargs=expr.kwargs,
|
|
670
|
+
type_args=expr.type_args,
|
|
671
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
672
|
+
loc=expr.loc,
|
|
673
|
+
)
|
|
674
|
+
expr.is_nested_constructor = True
|
|
675
|
+
expr.nested_type_name = dotted
|
|
676
|
+
result = self.calls._analyze_record_constructor(fake_call, nested_record)
|
|
677
|
+
self.ctx.set_expr_type(expr, result)
|
|
678
|
+
return result
|
|
679
|
+
|
|
680
|
+
# Module-qualified static method call: m.Foo.method() or pkg.sub.Foo.method().
|
|
681
|
+
# Receiver shape is TpyFieldAccess(<module-chain>, ClassName); none of the
|
|
682
|
+
# other TpyFieldAccess branches recognise it because the inner is a module
|
|
683
|
+
# binding (not a record chain) and the leaf is a class (not a function).
|
|
684
|
+
# `user_module_call` is only set for the static-method dispatch -- the
|
|
685
|
+
# unbound-self path (BaseN.method(self, ...)) uses `this->Parent::method`
|
|
686
|
+
# codegen that doesn't go through the module-qualified namespace.
|
|
687
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
688
|
+
resolved = self._try_resolve_module_qualified_class(expr.obj)
|
|
689
|
+
if resolved is not None:
|
|
690
|
+
module_name, _class_short, record_info = resolved
|
|
691
|
+
result = self._dispatch_static_method_call(expr, record_info)
|
|
692
|
+
if result is not None:
|
|
693
|
+
if expr.is_static_call:
|
|
694
|
+
expr.user_module_call = module_name
|
|
695
|
+
return result
|
|
696
|
+
|
|
697
|
+
# Dotted module access: X.Y.func(), X.Y.Z.func(), etc.
|
|
698
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
699
|
+
dotted_name = self._try_resolve_dotted_module(expr.obj)
|
|
700
|
+
if dotted_name:
|
|
701
|
+
flat_obj = TpyName(name=dotted_name, loc=expr.obj.loc)
|
|
702
|
+
flat_expr = TpyMethodCall(
|
|
703
|
+
obj=flat_obj, method=expr.method, args=expr.args,
|
|
704
|
+
kwargs=expr.kwargs,
|
|
705
|
+
type_args=expr.type_args,
|
|
706
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
707
|
+
loc=expr.loc,
|
|
708
|
+
)
|
|
709
|
+
result = self._analyze_module_method_call(flat_expr, module_name=dotted_name)
|
|
710
|
+
if result is not None:
|
|
711
|
+
expr.args = flat_expr.args
|
|
712
|
+
expr.kwargs = flat_expr.kwargs
|
|
713
|
+
expr.builtin_module_call = flat_expr.builtin_module_call
|
|
714
|
+
expr.user_module_call = flat_expr.user_module_call
|
|
715
|
+
expr.resolved_function_info = flat_expr.resolved_function_info
|
|
716
|
+
expr.inferred_type_args = flat_expr.inferred_type_args
|
|
717
|
+
expr.representational_subst_params = flat_expr.representational_subst_params
|
|
718
|
+
return result
|
|
719
|
+
|
|
720
|
+
obj_type = self.expr.analyze_expr(expr.obj)
|
|
721
|
+
|
|
722
|
+
# super().method() calls: the receiver resolves to SuperType via the
|
|
723
|
+
# builtins.super qname dispatch in calls.py (supersedes the old bare-string
|
|
724
|
+
# intercept so user `def super()` shadows normally).
|
|
725
|
+
if isinstance(obj_type, SuperType):
|
|
726
|
+
return self._analyze_super_method_call(expr, obj_type)
|
|
727
|
+
|
|
728
|
+
# Pending generic instance: accumulate constraints from method calls
|
|
729
|
+
if isinstance(obj_type, PendingGenericInstanceType):
|
|
730
|
+
return self._analyze_pending_generic_method_call(expr, obj_type)
|
|
731
|
+
|
|
732
|
+
# Unwrap RefType, ReadonlyType, remembering the flag for enforcement.
|
|
733
|
+
# Also check declared scope type: isinstance narrowing may strip ReadonlyType
|
|
734
|
+
# from the expr type while the scope binding preserves it.
|
|
735
|
+
obj_type = unwrap_ref_type(obj_type)
|
|
736
|
+
is_readonly_receiver = isinstance(obj_type, ReadonlyType)
|
|
737
|
+
if not is_readonly_receiver and isinstance(expr.obj, TpyName):
|
|
738
|
+
is_readonly_receiver = self.ctx.is_readonly_name(expr.obj.name)
|
|
739
|
+
if isinstance(obj_type, ReadonlyType):
|
|
740
|
+
obj_type = obj_type.wrapped
|
|
741
|
+
|
|
742
|
+
# General method calls always prefer borrowing overloads.
|
|
743
|
+
# Consuming __iter__ overloads are selected at call-site by
|
|
744
|
+
# _gen_consuming_iter in codegen_cpp/expressions.py.
|
|
745
|
+
is_consuming_receiver = False
|
|
746
|
+
|
|
747
|
+
if isinstance(obj_type, OwnType):
|
|
748
|
+
obj_type = obj_type.wrapped
|
|
749
|
+
elif isinstance(obj_type, OptionalType):
|
|
750
|
+
if obj_type.inner.is_value_type():
|
|
751
|
+
raise self.ctx.error(f"Cannot call method '{expr.method}' on type {obj_type}", expr)
|
|
752
|
+
self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
|
|
753
|
+
expr.needs_optional_runtime_check = True
|
|
754
|
+
obj_type = obj_type.inner
|
|
755
|
+
|
|
756
|
+
# List mutation tracking (before deref chain -- applies to direct list types only)
|
|
757
|
+
if isinstance(obj_type, PendingListType) or is_list(obj_type):
|
|
758
|
+
if not self._is_readonly_method(obj_type, expr.method):
|
|
759
|
+
self.deduction.mark_list_mutated(expr.obj)
|
|
760
|
+
|
|
761
|
+
# Pending container element type inference from method args.
|
|
762
|
+
# For any method on a pending container, check if params involve the
|
|
763
|
+
# container's type parameters. If so, pre-analyze the args and infer
|
|
764
|
+
# the element type (generalizes append/insert/add/etc.).
|
|
765
|
+
if isinstance(obj_type, PendingListType) and expr.args:
|
|
766
|
+
obj_type = self._infer_pending_container_element(
|
|
767
|
+
expr, obj_type, "builtins.list", obj_type.literal_id,
|
|
768
|
+
self.deduction.infer_empty_list_element_type,
|
|
769
|
+
lambda elem, lid: PendingListType(elem, obj_type.size, lid),
|
|
770
|
+
self.ctx.list_literals,
|
|
771
|
+
) or obj_type
|
|
772
|
+
elif isinstance(obj_type, PendingSetType) and expr.args:
|
|
773
|
+
obj_type = self._infer_pending_container_element(
|
|
774
|
+
expr, obj_type, "builtins.set", obj_type.literal_id,
|
|
775
|
+
self.deduction.infer_set_element_type,
|
|
776
|
+
lambda elem, lid: PendingSetType(elem, lid),
|
|
777
|
+
self.ctx.set_literals,
|
|
778
|
+
) or obj_type
|
|
779
|
+
|
|
780
|
+
# Borrow conflict: structural mutation on a container with element-level borrows.
|
|
781
|
+
# Resolves aliases so that alias.append() warns when items has element borrows.
|
|
782
|
+
# Also handles field-path receivers (self.items.append()) via _storage_key.
|
|
783
|
+
bt = self.ctx.func.borrow_tracker
|
|
784
|
+
if isinstance(expr.obj, TpyName):
|
|
785
|
+
storage = bt.effective_storage(expr.obj.name)
|
|
786
|
+
else:
|
|
787
|
+
storage = _storage_key(expr.obj)
|
|
788
|
+
if storage is not None:
|
|
789
|
+
if bt.has_element_borrow(storage):
|
|
790
|
+
is_mutation = self._is_invalidating_method(obj_type, expr.method)
|
|
791
|
+
if is_mutation:
|
|
792
|
+
if bt.has_iter_borrow(storage):
|
|
793
|
+
msg = (f"Mutation of '{storage}' while iterating over it"
|
|
794
|
+
f" ('{expr.method}' invalidates the iterator)")
|
|
795
|
+
else:
|
|
796
|
+
msg = (f"Mutation of '{storage}' while borrowed"
|
|
797
|
+
f" ('{expr.method}' may invalidate references)")
|
|
798
|
+
self.ctx.warning(msg, expr)
|
|
799
|
+
|
|
800
|
+
# Deref chain -- resolves through Ptr (mutable and readonly) and any Deref[T] type
|
|
801
|
+
original_type = obj_type
|
|
802
|
+
current_type = obj_type
|
|
803
|
+
deref_depth = 0
|
|
804
|
+
while deref_depth <= 8:
|
|
805
|
+
result = self._try_resolve_method(expr, current_type, is_readonly_receiver, is_consuming_receiver)
|
|
806
|
+
if result is not None:
|
|
807
|
+
expr.deref_depth = deref_depth
|
|
808
|
+
if deref_depth > 0 and isinstance(original_type, PtrType):
|
|
809
|
+
obj_key = _expr_to_narrowing_key(expr.obj)
|
|
810
|
+
if obj_key is not None:
|
|
811
|
+
if obj_key in self.ctx.func.non_null_ptr_vars:
|
|
812
|
+
expr.ptr_non_null = True
|
|
813
|
+
if expr.loc:
|
|
814
|
+
self.ctx.ptr_deref_facts[
|
|
815
|
+
(expr.loc.line, obj_key)
|
|
816
|
+
] = expr.ptr_non_null
|
|
817
|
+
# Post-access narrowing: queued for flush at statement
|
|
818
|
+
# boundary (see pending_non_null_ptr_vars docstring).
|
|
819
|
+
self.ctx.func.pending_non_null_ptr_vars.add(obj_key)
|
|
820
|
+
# Enforce readonly: cannot call non-readonly method on readonly receiver
|
|
821
|
+
if is_readonly_receiver:
|
|
822
|
+
info = expr.resolved_function_info
|
|
823
|
+
if info is not None and not info.is_readonly:
|
|
824
|
+
raise self.ctx.error(
|
|
825
|
+
f"Cannot call non-readonly method '{expr.method}' on readonly reference",
|
|
826
|
+
expr)
|
|
827
|
+
# Enforce consuming methods: receiver must be a local variable
|
|
828
|
+
info = expr.resolved_function_info
|
|
829
|
+
if info is not None and info.is_consuming:
|
|
830
|
+
self._validate_consuming_call(expr)
|
|
831
|
+
# Track non-readonly method calls on for-each loop variables
|
|
832
|
+
# and string view sources (receiver mutation invalidates views)
|
|
833
|
+
info = expr.resolved_function_info
|
|
834
|
+
if info is not None and not info.is_readonly:
|
|
835
|
+
obj_root = _root_name_of_expr(expr.obj)
|
|
836
|
+
if obj_root is not None:
|
|
837
|
+
self.ctx.mark_loop_var_mutated(obj_root)
|
|
838
|
+
is_direct_self_call = (
|
|
839
|
+
isinstance(expr.obj, TpyName) and expr.obj.name == "self"
|
|
840
|
+
)
|
|
841
|
+
if is_direct_self_call:
|
|
842
|
+
# self.method() -- entirely deferred to call edges
|
|
843
|
+
pass
|
|
844
|
+
else:
|
|
845
|
+
# For self.field.method() and loop_var.method() (where the
|
|
846
|
+
# loop var iterates over a self field), defer self-mutation
|
|
847
|
+
# to Phase 2 via call edges (receiver_is_self=True, recorded
|
|
848
|
+
# by _record_mutation_call_edges). Phase 2 only sets
|
|
849
|
+
# self_mutated when the callee actually mutates its self,
|
|
850
|
+
# enabling readonly inference for methods like __json_encode__
|
|
851
|
+
# that call non-mutating methods on fields.
|
|
852
|
+
self_deferred = _is_self_call_deferred(
|
|
853
|
+
expr.obj, obj_root, self.ctx.func.loop_var_iterable,
|
|
854
|
+
self.ctx.func.borrow_tracker)
|
|
855
|
+
if not self_deferred:
|
|
856
|
+
self.ctx.mark_param_mutated(obj_root)
|
|
857
|
+
# Structural mutation tracked directly (Phase 2 doesn't
|
|
858
|
+
# propagate structural self-mutation through call edges).
|
|
859
|
+
if self._is_invalidating_method(obj_type, expr.method):
|
|
860
|
+
self.ctx.mark_param_structurally_mutated(obj_root)
|
|
861
|
+
storage = self.ctx.func.borrow_tracker.effective_storage(obj_root)
|
|
862
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
863
|
+
else:
|
|
864
|
+
# Check for chained method calls rooted at self:
|
|
865
|
+
# self.get_span().sort() -- sort() is non-readonly and
|
|
866
|
+
# the span is a mutable view of self's storage.
|
|
867
|
+
# Treat as self-mutation conservatively. A chain rooted
|
|
868
|
+
# at a local that borrow-traces to self.<field> (e.g.
|
|
869
|
+
# `frame = self.frame; frame.get().mutating_method()`)
|
|
870
|
+
# is also a self-mutation since the receiver aliases
|
|
871
|
+
# self-owned storage.
|
|
872
|
+
chain = expr.obj
|
|
873
|
+
while isinstance(chain, TpyMethodCall):
|
|
874
|
+
chain = chain.obj
|
|
875
|
+
chain_root = _root_name_of_expr(chain)
|
|
876
|
+
if chain_root is not None and _local_traces_to_self(
|
|
877
|
+
self.ctx.func.borrow_tracker, chain_root):
|
|
878
|
+
self.ctx.mark_param_mutated("self")
|
|
879
|
+
return result
|
|
880
|
+
|
|
881
|
+
deref_target = self.expr.get_deref_target_type(
|
|
882
|
+
current_type, is_readonly=is_readonly_receiver)
|
|
883
|
+
if deref_target is None:
|
|
884
|
+
break
|
|
885
|
+
# __deref__() may return readonly[T]; unwrap and propagate
|
|
886
|
+
# readonly so method calls enforce const semantics.
|
|
887
|
+
if isinstance(deref_target, ReadonlyType):
|
|
888
|
+
is_readonly_receiver = True
|
|
889
|
+
deref_target = deref_target.wrapped
|
|
890
|
+
# Deref-view narrowing: inside `if isinstance(rc, Dog):` the peeled
|
|
891
|
+
# payload is narrowed to the subclass so `bark` (a Dog method, not
|
|
892
|
+
# on the Pet protocol) resolves. Tag the node for the codegen cast.
|
|
893
|
+
nsub = deref_view_narrowed(self.ctx, expr.obj, deref_target)
|
|
894
|
+
if nsub is not None:
|
|
895
|
+
expr.deref_narrowed_to = nsub
|
|
896
|
+
current_type = nsub
|
|
897
|
+
else:
|
|
898
|
+
current_type = deref_target
|
|
899
|
+
deref_depth += 1
|
|
900
|
+
|
|
901
|
+
raise self.ctx.error(f"Cannot call method '{expr.method}' on type {original_type}", expr)
|
|
902
|
+
|
|
903
|
+
def _validate_consuming_call(self, expr: TpyMethodCall) -> None:
|
|
904
|
+
"""Validate a consuming method call (self: Own[Self]).
|
|
905
|
+
|
|
906
|
+
The receiver must be a local variable (not a field or other expression).
|
|
907
|
+
Temporaries are also allowed (e.g. Box(value).take()).
|
|
908
|
+
After the call, the variable is marked as consumed.
|
|
909
|
+
"""
|
|
910
|
+
if expr.deref_depth > 0:
|
|
911
|
+
raise self.ctx.error(
|
|
912
|
+
f"Cannot call consuming method '{expr.method}' through a Deref chain; "
|
|
913
|
+
f"consuming methods must be called directly on the owning type",
|
|
914
|
+
expr,
|
|
915
|
+
)
|
|
916
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
917
|
+
raise self.ctx.error(
|
|
918
|
+
f"Cannot call consuming method '{expr.method}' on a field; "
|
|
919
|
+
f"only local variables and temporaries are allowed",
|
|
920
|
+
expr,
|
|
921
|
+
)
|
|
922
|
+
if isinstance(expr.obj, TpyName):
|
|
923
|
+
name = expr.obj.name
|
|
924
|
+
if name == "self":
|
|
925
|
+
raise self.ctx.error(
|
|
926
|
+
f"Cannot call consuming method '{expr.method}' on 'self'; "
|
|
927
|
+
f"only local variables and temporaries are allowed",
|
|
928
|
+
expr,
|
|
929
|
+
)
|
|
930
|
+
# Reject consuming through pointers -- pointer doesn't own the pointee
|
|
931
|
+
obj_type = self.ctx.func.current_scope.lookup(name)
|
|
932
|
+
if obj_type is not None and (isinstance(obj_type, PtrType)
|
|
933
|
+
or isinstance(unwrap_readonly(obj_type), PtrType)):
|
|
934
|
+
raise self.ctx.error(
|
|
935
|
+
f"Cannot call consuming method '{expr.method}' on pointer '{name}'; "
|
|
936
|
+
f"pointers do not own the pointee",
|
|
937
|
+
expr,
|
|
938
|
+
)
|
|
939
|
+
# Reject consuming outer variables inside a loop -- the variable
|
|
940
|
+
# won't be re-bound on the next iteration, causing use-after-move.
|
|
941
|
+
if self.ctx.func.loop_depth > 0:
|
|
942
|
+
var_depth = self.ctx.func.var_scope_depth.get(name, 0)
|
|
943
|
+
loop_scope_depth = self.ctx.func.current_scope.depth
|
|
944
|
+
if var_depth < loop_scope_depth:
|
|
945
|
+
raise self.ctx.error(
|
|
946
|
+
f"Cannot consume '{name}' inside a loop; "
|
|
947
|
+
f"the variable is not re-bound each iteration",
|
|
948
|
+
expr,
|
|
949
|
+
)
|
|
950
|
+
# Use-after-consume is already caught by _analyze_name before we get here.
|
|
951
|
+
self.ctx.func.consumed_vars.add(name)
|
|
952
|
+
|
|
953
|
+
def _analyze_nested_type_call(self, expr: TpyMethodCall) -> TpyType | None:
|
|
954
|
+
"""Check for Outer.Inner(...) nested type constructor call. Returns type or None."""
|
|
955
|
+
assert isinstance(expr.obj, TpyName)
|
|
956
|
+
dotted = f"{expr.obj.name}.{expr.method}"
|
|
957
|
+
# Nested record constructor
|
|
958
|
+
nested_record = self.ctx.registry.get_record(dotted)
|
|
959
|
+
if nested_record is not None:
|
|
960
|
+
# Rewrite as a direct constructor call
|
|
961
|
+
fake_call = TpyCall(
|
|
962
|
+
func=TpyName(name=dotted, loc=expr.loc),
|
|
963
|
+
args=expr.args,
|
|
964
|
+
kwargs=expr.kwargs,
|
|
965
|
+
type_args=expr.type_args,
|
|
966
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
967
|
+
loc=expr.loc,
|
|
968
|
+
)
|
|
969
|
+
# Mark the original expr so codegen knows it's a constructor
|
|
970
|
+
expr.is_nested_constructor = True
|
|
971
|
+
expr.nested_type_name = dotted
|
|
972
|
+
result = self.calls._analyze_record_constructor(fake_call, nested_record)
|
|
973
|
+
# Copy type info back
|
|
974
|
+
self.ctx.set_expr_type(expr, result)
|
|
975
|
+
return result
|
|
976
|
+
# Nested enum constructor (from_value)
|
|
977
|
+
nested_enum = self.ctx.registry.get_enum(dotted)
|
|
978
|
+
if nested_enum is not None:
|
|
979
|
+
# Container.Kind(1) -> Container::Kind from_value
|
|
980
|
+
if len(expr.args) != 1 or expr.kwargs:
|
|
981
|
+
raise self.ctx.error(
|
|
982
|
+
f"Nested enum '{dotted}' constructor takes exactly 1 positional argument",
|
|
983
|
+
expr)
|
|
984
|
+
arg_type = self.expr.analyze_expr(expr.args[0])
|
|
985
|
+
if not is_any_int_type(arg_type):
|
|
986
|
+
raise self.ctx.error(
|
|
987
|
+
f"Cannot construct '{dotted}' from '{arg_type}', "
|
|
988
|
+
f"expected an integer type",
|
|
989
|
+
expr)
|
|
990
|
+
expr.is_nested_enum_constructor = True
|
|
991
|
+
expr.nested_type_name = dotted
|
|
992
|
+
return nested_enum
|
|
993
|
+
return None
|
|
994
|
+
|
|
995
|
+
def _analyze_static_method_call(self, expr: TpyMethodCall) -> TpyType | None:
|
|
996
|
+
"""Check for ClassName.staticmethod() or BaseN.method(self, ...) pattern.
|
|
997
|
+
|
|
998
|
+
Returns the call's type, or None if ClassName is not a record binding
|
|
999
|
+
(fall-through to other method-call dispatch).
|
|
1000
|
+
|
|
1001
|
+
Routes (checked in order):
|
|
1002
|
+
1. ClassName.staticmethod(args) -- static-method call.
|
|
1003
|
+
2. BaseN.method(self, args) where BaseN is an ancestor of the current
|
|
1004
|
+
record: rebinds to instance-method machinery, dispatches statically
|
|
1005
|
+
to BaseN's definition. First arg must literally be 'self'.
|
|
1006
|
+
"""
|
|
1007
|
+
assert isinstance(expr.obj, TpyName)
|
|
1008
|
+
if self.ctx.func.current_ns is None:
|
|
1009
|
+
return None
|
|
1010
|
+
|
|
1011
|
+
record_info = None
|
|
1012
|
+
binding = self.ctx.func.current_ns.lookup(expr.obj.name)
|
|
1013
|
+
if binding and binding.kind == BindingKind.RECORD:
|
|
1014
|
+
record_info = self.ctx.registry.get_record(expr.obj.name)
|
|
1015
|
+
elif binding and binding.kind == BindingKind.IMPORTED_NAME:
|
|
1016
|
+
import_info = self.ctx.imported_names.get(expr.obj.name)
|
|
1017
|
+
if import_info:
|
|
1018
|
+
record_info = self.ctx.registry.find_record_by_qname(
|
|
1019
|
+
f"{import_info[0]}.{import_info[1]}")
|
|
1020
|
+
|
|
1021
|
+
if record_info is None:
|
|
1022
|
+
return None
|
|
1023
|
+
return self._dispatch_static_method_call(expr, record_info)
|
|
1024
|
+
|
|
1025
|
+
def _dispatch_static_method_call(
|
|
1026
|
+
self, expr: TpyMethodCall, record_info: RecordInfo,
|
|
1027
|
+
) -> TpyType | None:
|
|
1028
|
+
"""Dispatch a staticmethod / unbound-self call on a pre-resolved record.
|
|
1029
|
+
|
|
1030
|
+
Walks MRO, distinguishes static from unbound-self, handles generic
|
|
1031
|
+
class / method type args. Shared by bare-name (`Foo.method()`) and
|
|
1032
|
+
module-qualified (`m.Foo.method()`) receivers.
|
|
1033
|
+
"""
|
|
1034
|
+
overloads = self.ctx.registry.get_method_overloads_with_parents(
|
|
1035
|
+
record_info, expr.method)
|
|
1036
|
+
if not overloads:
|
|
1037
|
+
return None
|
|
1038
|
+
if not overloads[0].is_staticmethod:
|
|
1039
|
+
return self._analyze_unbound_self_method_call(expr, record_info, overloads)
|
|
1040
|
+
|
|
1041
|
+
if record_info.is_generic():
|
|
1042
|
+
return self._analyze_generic_static_method_call(expr, record_info, overloads)
|
|
1043
|
+
|
|
1044
|
+
method = overloads[0]
|
|
1045
|
+
if method.type_params:
|
|
1046
|
+
# Non-generic class with method-level type params: type_args are for the method
|
|
1047
|
+
return self._analyze_generic_static_method_call(expr, record_info, overloads)
|
|
1048
|
+
|
|
1049
|
+
if expr.type_args or expr.type_args_parse_error:
|
|
1050
|
+
raise self.ctx.error(
|
|
1051
|
+
f"'{record_info.name}' is not generic and does not accept type arguments",
|
|
1052
|
+
expr,
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
return_type = self._resolve_and_check_args(expr, overloads, {})
|
|
1056
|
+
expr.is_static_call = True
|
|
1057
|
+
return return_type
|
|
1058
|
+
|
|
1059
|
+
def _try_resolve_module_qualified_class(
|
|
1060
|
+
self, obj: TpyFieldAccess,
|
|
1061
|
+
) -> tuple[str, str, RecordInfo] | None:
|
|
1062
|
+
"""Resolve `obj` as `<module-chain>.ClassName` to (module_name, class_short, record_info).
|
|
1063
|
+
|
|
1064
|
+
Handles both single-segment module references (`TpyName(m).Class`) and
|
|
1065
|
+
dotted-segment ones (`TpyFieldAccess(pkg.sub).Class`). Returns None when
|
|
1066
|
+
the inner receiver isn't a module binding or the named class isn't
|
|
1067
|
+
registered in that module.
|
|
1068
|
+
"""
|
|
1069
|
+
class_short = obj.field
|
|
1070
|
+
if isinstance(obj.obj, TpyName):
|
|
1071
|
+
module_name = self._resolve_module_name(obj.obj.name)
|
|
1072
|
+
elif isinstance(obj.obj, TpyFieldAccess):
|
|
1073
|
+
module_name = self._try_resolve_dotted_module(obj.obj)
|
|
1074
|
+
else:
|
|
1075
|
+
return None
|
|
1076
|
+
if module_name is None:
|
|
1077
|
+
return None
|
|
1078
|
+
record_info = self.ctx.registry.find_record_by_qname(
|
|
1079
|
+
f"{module_name}.{class_short}")
|
|
1080
|
+
if record_info is None:
|
|
1081
|
+
return None
|
|
1082
|
+
return (module_name, class_short, record_info)
|
|
1083
|
+
|
|
1084
|
+
def _analyze_unbound_self_method_call(
|
|
1085
|
+
self, expr: TpyMethodCall, record_info, overloads: list[FunctionInfo],
|
|
1086
|
+
) -> TpyType:
|
|
1087
|
+
"""Handle `BaseN.method(self, ...)` calls on an ancestor class.
|
|
1088
|
+
|
|
1089
|
+
`record_info` is the class named on the left of the dot; `overloads`
|
|
1090
|
+
are the MRO-walked non-static overloads of `expr.method` (caller has
|
|
1091
|
+
already dispatched on staticmethod).
|
|
1092
|
+
|
|
1093
|
+
Rules:
|
|
1094
|
+
- The enclosing context must be an instance method (self in scope).
|
|
1095
|
+
- ClassName must be a strict ancestor of the current record.
|
|
1096
|
+
- First arg must syntactically be the name `self` -- arbitrary unbound
|
|
1097
|
+
calls (e.g. `Named.describe(other_instance)`) are rejected.
|
|
1098
|
+
"""
|
|
1099
|
+
parent_name = record_info.name
|
|
1100
|
+
method_name = expr.method
|
|
1101
|
+
|
|
1102
|
+
# Must be inside an instance method
|
|
1103
|
+
current_rec_parse = self.ctx.record_ctx.record
|
|
1104
|
+
current_fn = self.ctx.func.current_function
|
|
1105
|
+
if (current_rec_parse is None
|
|
1106
|
+
or current_fn is None
|
|
1107
|
+
or not isinstance(current_fn, TpyFunction)
|
|
1108
|
+
or not current_fn.is_method
|
|
1109
|
+
or current_fn.is_staticmethod):
|
|
1110
|
+
raise self.ctx.error(
|
|
1111
|
+
f"Method '{parent_name}.{method_name}' requires an instance "
|
|
1112
|
+
f"(not a static method)",
|
|
1113
|
+
expr,
|
|
1114
|
+
)
|
|
1115
|
+
current_rec = self.ctx.registry.get_record(current_rec_parse.name)
|
|
1116
|
+
assert current_rec is not None, (
|
|
1117
|
+
f"registry missing RecordInfo for '{current_rec_parse.name}' "
|
|
1118
|
+
f"while analyzing its methods"
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# __del__ compiles to a C++ destructor (not a callable member); __init__
|
|
1122
|
+
# compiles to a constructor and is only reachable as a base initializer
|
|
1123
|
+
# from within the child's __init__ body.
|
|
1124
|
+
if method_name == "__del__":
|
|
1125
|
+
raise self.ctx.error(
|
|
1126
|
+
f"'{parent_name}.__del__(self)' is not callable via the unbound-self "
|
|
1127
|
+
f"form. C++ invokes each base destructor automatically.",
|
|
1128
|
+
expr,
|
|
1129
|
+
)
|
|
1130
|
+
if method_name == "__init__" and current_fn.name != "__init__":
|
|
1131
|
+
raise self.ctx.error(
|
|
1132
|
+
f"'{parent_name}.__init__(self, ...)' can only be called inside "
|
|
1133
|
+
f"'__init__'",
|
|
1134
|
+
expr,
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
# Ancestor check (strict: excludes current record itself)
|
|
1138
|
+
if not self.ctx.registry.is_subclass_of_record(current_rec, record_info):
|
|
1139
|
+
raise self.ctx.error(
|
|
1140
|
+
f"'{parent_name}' is not an ancestor of '{current_rec.name}'; "
|
|
1141
|
+
f"cannot call '{parent_name}.{method_name}(self, ...)' here",
|
|
1142
|
+
expr,
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
# First arg must be the literal name 'self'.
|
|
1146
|
+
if not expr.args or not (isinstance(expr.args[0], TpyName)
|
|
1147
|
+
and expr.args[0].name == "self"):
|
|
1148
|
+
raise self.ctx.error(
|
|
1149
|
+
f"'{parent_name}.{method_name}(...)' must pass 'self' as the first "
|
|
1150
|
+
f"argument (e.g. '{parent_name}.{method_name}(self, ...)')",
|
|
1151
|
+
expr,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
# Caller passed MRO-walked overloads and dispatched on staticmethod, so
|
|
1155
|
+
# `overloads` is the authoritative list of non-static candidates here.
|
|
1156
|
+
assert overloads and not overloads[0].is_staticmethod
|
|
1157
|
+
|
|
1158
|
+
parent_type, type_subst = self.protocols.resolve_ancestor_instantiation(
|
|
1159
|
+
current_rec, record_info)
|
|
1160
|
+
|
|
1161
|
+
# Rebind to instance-method machinery via a temp TpyMethodCall with
|
|
1162
|
+
# obj=self and self dropped from args. _resolve_and_check_args mutates
|
|
1163
|
+
# the temp's args (coercions), so copy them back after. self itself
|
|
1164
|
+
# is then dropped from expr.args so codegen sees the same shape as
|
|
1165
|
+
# super().method(...) -- base-qualified call with method args only.
|
|
1166
|
+
self_arg = expr.args[0]
|
|
1167
|
+
# Analyze self so downstream (mutation tracking, type queries) has a type.
|
|
1168
|
+
self.expr.analyze_expr(self_arg)
|
|
1169
|
+
fake_expr = TpyMethodCall(
|
|
1170
|
+
obj=self_arg,
|
|
1171
|
+
method=method_name,
|
|
1172
|
+
args=list(expr.args[1:]),
|
|
1173
|
+
kwargs=expr.kwargs,
|
|
1174
|
+
type_args=expr.type_args,
|
|
1175
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
1176
|
+
loc=expr.loc,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
method_info = overloads[0]
|
|
1180
|
+
if method_info.is_generic():
|
|
1181
|
+
if len(overloads) > 1:
|
|
1182
|
+
raise self.ctx.error(
|
|
1183
|
+
f"Overloaded generic methods are not supported for '{method_name}'",
|
|
1184
|
+
expr)
|
|
1185
|
+
return_type = self._analyze_generic_method_call(
|
|
1186
|
+
fake_expr, method_info, record_info, type_subst)
|
|
1187
|
+
else:
|
|
1188
|
+
return_type = self._resolve_and_check_args(
|
|
1189
|
+
fake_expr, overloads, type_subst)
|
|
1190
|
+
|
|
1191
|
+
# Readonly self: a @readonly context calling a non-readonly ancestor
|
|
1192
|
+
# method would mutate self through a const receiver.
|
|
1193
|
+
is_readonly_context = (
|
|
1194
|
+
isinstance(self.ctx.func.current_function, TpyFunction)
|
|
1195
|
+
and self.ctx.func.current_function.is_readonly
|
|
1196
|
+
)
|
|
1197
|
+
resolved_info = fake_expr.resolved_function_info
|
|
1198
|
+
if is_readonly_context and resolved_info is not None and not resolved_info.is_readonly:
|
|
1199
|
+
raise self.ctx.error(
|
|
1200
|
+
f"Cannot call non-readonly method '{method_name}' on readonly reference",
|
|
1201
|
+
expr)
|
|
1202
|
+
|
|
1203
|
+
# Drop self from the outer expr so codegen emits Parent::method(args)
|
|
1204
|
+
# with args matching resolved_function_info.params shape.
|
|
1205
|
+
expr.args = fake_expr.args
|
|
1206
|
+
expr.kwargs = fake_expr.kwargs
|
|
1207
|
+
expr.resolved_function_info = resolved_info
|
|
1208
|
+
expr.inferred_type_args = fake_expr.inferred_type_args
|
|
1209
|
+
expr.representational_subst_params = fake_expr.representational_subst_params
|
|
1210
|
+
expr.unbound_self_parent_type = parent_type
|
|
1211
|
+
return return_type
|
|
1212
|
+
|
|
1213
|
+
def _analyze_generic_static_method_call(
|
|
1214
|
+
self, expr: TpyMethodCall, record_info, overloads: list[FunctionInfo],
|
|
1215
|
+
) -> TpyType:
|
|
1216
|
+
"""Resolve a static method call on a generic record or a static method
|
|
1217
|
+
with its own type parameters.
|
|
1218
|
+
|
|
1219
|
+
Creates a virtual FunctionInfo with class + method type params merged,
|
|
1220
|
+
then delegates to _analyze_user_function_call (same path as free generic calls).
|
|
1221
|
+
"""
|
|
1222
|
+
# NOTE: picks first overload. If overloaded static methods on generic
|
|
1223
|
+
# classes are added, this needs overload resolution per-candidate with
|
|
1224
|
+
# inference (similar to _analyze_builtin_function_overloads).
|
|
1225
|
+
method = overloads[0]
|
|
1226
|
+
class_type_params = set(record_info.type_params) if record_info.type_params else set()
|
|
1227
|
+
new_method_params = [tp for tp in (method.type_params or []) if tp not in class_type_params]
|
|
1228
|
+
|
|
1229
|
+
# Merge class type params + method's own type params into a single virtual FunctionInfo.
|
|
1230
|
+
# The free function path handles inference, explicit args, and bound validation.
|
|
1231
|
+
all_type_params = list(record_info.type_params) + new_method_params
|
|
1232
|
+
all_bounds = dict(record_info.type_param_bounds)
|
|
1233
|
+
all_bounds.update({k: v for k, v in method.type_param_bounds.items()
|
|
1234
|
+
if k in set(new_method_params)})
|
|
1235
|
+
|
|
1236
|
+
# Validate type arg count: must match either class params (method params inferred)
|
|
1237
|
+
# or all params (class + method)
|
|
1238
|
+
if expr.type_args:
|
|
1239
|
+
n_class = len(record_info.type_params)
|
|
1240
|
+
n_total = len(all_type_params)
|
|
1241
|
+
n_given = len(expr.type_args)
|
|
1242
|
+
if n_given != n_class and n_given != n_total:
|
|
1243
|
+
if new_method_params:
|
|
1244
|
+
raise self.ctx.error(
|
|
1245
|
+
f"'{record_info.name}.{method.name}' expects {n_class} class type arguments "
|
|
1246
|
+
f"or {n_total} total (class + method), got {n_given}",
|
|
1247
|
+
expr)
|
|
1248
|
+
raise self.ctx.error(
|
|
1249
|
+
f"'{record_info.name}' expects {n_class} type arguments, got {n_given}",
|
|
1250
|
+
expr)
|
|
1251
|
+
|
|
1252
|
+
virtual_func = FunctionInfo(
|
|
1253
|
+
name=method.name, params=method.params, return_type=method.return_type,
|
|
1254
|
+
is_staticmethod=method.is_staticmethod,
|
|
1255
|
+
type_params=all_type_params,
|
|
1256
|
+
type_param_bounds=all_bounds,
|
|
1257
|
+
cpp_template=method.cpp_template,
|
|
1258
|
+
native_name=method.native_name,
|
|
1259
|
+
canonical_fi=method.root,
|
|
1260
|
+
)
|
|
1261
|
+
temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args,
|
|
1262
|
+
kwargs=expr.kwargs,
|
|
1263
|
+
type_args=expr.type_args,
|
|
1264
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
1265
|
+
loc=expr.loc)
|
|
1266
|
+
result = self.calls._analyze_user_function_call(temp_call, [virtual_func])
|
|
1267
|
+
expr.args = temp_call.args
|
|
1268
|
+
expr.kwargs = temp_call.kwargs
|
|
1269
|
+
expr.resolved_function_info = temp_call.resolved_function_info
|
|
1270
|
+
expr.inferred_type_args = temp_call.inferred_type_args
|
|
1271
|
+
expr.representational_subst_params = temp_call.representational_subst_params
|
|
1272
|
+
expr.is_static_call = True
|
|
1273
|
+
return result
|
|
1274
|
+
|
|
1275
|
+
def _analyze_module_method_call(
|
|
1276
|
+
self, expr: TpyMethodCall, module_name: str | None = None,
|
|
1277
|
+
) -> TpyType | None:
|
|
1278
|
+
"""Check for module.function() pattern. Returns type or None if not a module call.
|
|
1279
|
+
|
|
1280
|
+
If module_name is provided, skips namespace resolution (used for dotted
|
|
1281
|
+
module access like tpy.unsafe.func() where the module is already known).
|
|
1282
|
+
"""
|
|
1283
|
+
assert isinstance(expr.obj, TpyName)
|
|
1284
|
+
|
|
1285
|
+
if module_name is None:
|
|
1286
|
+
module_name = self._resolve_module_name(expr.obj.name)
|
|
1287
|
+
if module_name is None:
|
|
1288
|
+
return None
|
|
1289
|
+
|
|
1290
|
+
module_info = self.ctx.registry.get_module(module_name)
|
|
1291
|
+
if module_info and module_info.functions and expr.method in module_info.functions:
|
|
1292
|
+
overloads = module_info.functions[expr.method]
|
|
1293
|
+
# Route through builtin path if the function is from a builtin module
|
|
1294
|
+
# or has a cpp_template (inline expansion, no C++ function body).
|
|
1295
|
+
is_builtin_func = (module_info.is_builtin
|
|
1296
|
+
or overloads[0].is_builtin_function
|
|
1297
|
+
or overloads[0].cpp_template is not None)
|
|
1298
|
+
if is_builtin_func:
|
|
1299
|
+
expr.builtin_module_call = module_name
|
|
1300
|
+
temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args,
|
|
1301
|
+
kwargs=expr.kwargs,
|
|
1302
|
+
type_args=expr.type_args,
|
|
1303
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
1304
|
+
loc=expr.loc)
|
|
1305
|
+
from .. import qnames
|
|
1306
|
+
_qname = f"{module_name}.{expr.method}"
|
|
1307
|
+
if _qname in (qnames.ASYNCIO_RUN, qnames.ASYNCIO_CREATE_TASK):
|
|
1308
|
+
self.calls._require_async_def_call_arg(temp_call, _qname)
|
|
1309
|
+
if overloads[0].special_handling:
|
|
1310
|
+
result = self.calls._analyze_special_builtin(temp_call, overloads)
|
|
1311
|
+
else:
|
|
1312
|
+
result = self.calls._analyze_builtin_function_overloads(temp_call, overloads)
|
|
1313
|
+
expr.args = temp_call.args
|
|
1314
|
+
expr.kwargs = temp_call.kwargs
|
|
1315
|
+
expr.resolved_function_info = temp_call.resolved_function_info
|
|
1316
|
+
expr.inferred_type_args = temp_call.inferred_type_args
|
|
1317
|
+
expr.representational_subst_params = temp_call.representational_subst_params
|
|
1318
|
+
return result
|
|
1319
|
+
else:
|
|
1320
|
+
expr.user_module_call = module_name
|
|
1321
|
+
temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args,
|
|
1322
|
+
kwargs=expr.kwargs,
|
|
1323
|
+
type_args=expr.type_args,
|
|
1324
|
+
type_args_parse_error=expr.type_args_parse_error,
|
|
1325
|
+
loc=expr.loc)
|
|
1326
|
+
result = self.calls._analyze_user_function_call(temp_call, overloads)
|
|
1327
|
+
expr.args = temp_call.args
|
|
1328
|
+
expr.kwargs = temp_call.kwargs
|
|
1329
|
+
expr.resolved_function_info = temp_call.resolved_function_info
|
|
1330
|
+
expr.inferred_type_args = temp_call.inferred_type_args
|
|
1331
|
+
expr.representational_subst_params = temp_call.representational_subst_params
|
|
1332
|
+
return result
|
|
1333
|
+
|
|
1334
|
+
qname = f"{module_name}.{expr.method}"
|
|
1335
|
+
if record_info := self.ctx.registry.get_builtin_record(qname):
|
|
1336
|
+
if record_info.get_method_overloads("__init__") and not record_info.type_params:
|
|
1337
|
+
expr.builtin_module_call = module_name
|
|
1338
|
+
temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args, kwargs=expr.kwargs, loc=expr.loc)
|
|
1339
|
+
result = self.calls._analyze_record_constructor(temp_call, record_info)
|
|
1340
|
+
expr.args = temp_call.args
|
|
1341
|
+
expr.kwargs = temp_call.kwargs
|
|
1342
|
+
expr.resolved_function_info = temp_call.resolved_function_info
|
|
1343
|
+
return result
|
|
1344
|
+
|
|
1345
|
+
# User record constructor invoked via qualified access (e.g.
|
|
1346
|
+
# `module.RecordName(...)` after `from pkg import module`).
|
|
1347
|
+
# Generic records left out -- this path doesn't forward type_args,
|
|
1348
|
+
# so let them fall through to the existing generic handling below.
|
|
1349
|
+
if (module_info and module_info.records
|
|
1350
|
+
and expr.method in module_info.records
|
|
1351
|
+
and not module_info.records[expr.method].type_params):
|
|
1352
|
+
record_info = module_info.records[expr.method]
|
|
1353
|
+
expr.user_module_call = module_name
|
|
1354
|
+
temp_call = TpyCall(func=TpyName(expr.method, loc=expr.loc), args=expr.args, kwargs=expr.kwargs, loc=expr.loc)
|
|
1355
|
+
result = self.calls._analyze_record_constructor(temp_call, record_info)
|
|
1356
|
+
expr.args = temp_call.args
|
|
1357
|
+
expr.kwargs = temp_call.kwargs
|
|
1358
|
+
expr.resolved_function_info = temp_call.resolved_function_info
|
|
1359
|
+
return result
|
|
1360
|
+
|
|
1361
|
+
# Check for call-site macro (e.g. dataclasses.asdict(...)).
|
|
1362
|
+
# Walks the re-export chain so macros re-exported through plain
|
|
1363
|
+
# modules (Phase 6) resolve to the macro registry's canonical
|
|
1364
|
+
# (ultimate_module, name) key.
|
|
1365
|
+
if self.ctx.macro_registry:
|
|
1366
|
+
macro_fn = self.ctx.macro_registry.get_call_macro(module_name, expr.method)
|
|
1367
|
+
ult_mod, ult_name = module_name, expr.method
|
|
1368
|
+
if macro_fn is None:
|
|
1369
|
+
chain = self.calls._resolve_call_macro_chain(module_name, expr.method)
|
|
1370
|
+
if chain is not None:
|
|
1371
|
+
ult_mod, ult_name = chain
|
|
1372
|
+
macro_fn = self.ctx.macro_registry.get_call_macro(ult_mod, ult_name)
|
|
1373
|
+
if macro_fn is not None:
|
|
1374
|
+
return self.calls._expand_call_macro_from_method(
|
|
1375
|
+
expr, macro_fn, ult_mod, ult_name)
|
|
1376
|
+
|
|
1377
|
+
raise self.ctx.error(f"Module '{module_name}' has no function '{expr.method}'", expr)
|
|
1378
|
+
|
|
1379
|
+
def _try_resolve_dotted_module(self, obj: TpyFieldAccess) -> str | None:
|
|
1380
|
+
"""Try to resolve nested field access as a dotted module name.
|
|
1381
|
+
|
|
1382
|
+
Walks the TpyFieldAccess chain to collect segments (e.g.,
|
|
1383
|
+
a.b.c -> ["a", "b", "c"]), then checks the registry.
|
|
1384
|
+
Returns None if the base name is shadowed (bound as anything other
|
|
1385
|
+
than MODULE).
|
|
1386
|
+
"""
|
|
1387
|
+
segments: list[str] = []
|
|
1388
|
+
current = obj
|
|
1389
|
+
while isinstance(current, TpyFieldAccess):
|
|
1390
|
+
segments.append(current.field)
|
|
1391
|
+
current = current.obj
|
|
1392
|
+
if not isinstance(current, TpyName):
|
|
1393
|
+
return None
|
|
1394
|
+
|
|
1395
|
+
# Only resolve as module if base name is unbound or bound as MODULE
|
|
1396
|
+
if self.ctx.func.current_ns:
|
|
1397
|
+
binding = self.ctx.func.current_ns.lookup(current.name)
|
|
1398
|
+
if binding and binding.kind != BindingKind.MODULE:
|
|
1399
|
+
return None
|
|
1400
|
+
|
|
1401
|
+
segments.append(current.name)
|
|
1402
|
+
segments.reverse()
|
|
1403
|
+
dotted_name = ".".join(segments)
|
|
1404
|
+
if self.ctx.registry.get_module(dotted_name):
|
|
1405
|
+
return dotted_name
|
|
1406
|
+
return None
|
|
1407
|
+
|
|
1408
|
+
def _resolve_module_name(self, name: str) -> str | None:
|
|
1409
|
+
"""Resolve a name to a module name if it refers to a module. Returns None otherwise."""
|
|
1410
|
+
if self.ctx.func.current_ns is None:
|
|
1411
|
+
return None
|
|
1412
|
+
binding = self.ctx.func.current_ns.lookup(name)
|
|
1413
|
+
if binding and binding.kind == BindingKind.MODULE:
|
|
1414
|
+
return binding.import_source[0] if binding.import_source else name
|
|
1415
|
+
return None
|
|
1416
|
+
|
|
1417
|
+
# ------------------------------------------------------------------
|
|
1418
|
+
# Pending generic instance method calls (Phase 7a)
|
|
1419
|
+
# ------------------------------------------------------------------
|
|
1420
|
+
|
|
1421
|
+
def _analyze_pending_generic_method_call(
|
|
1422
|
+
self, expr: TpyMethodCall, obj_type: PendingGenericInstanceType,
|
|
1423
|
+
) -> TpyType:
|
|
1424
|
+
"""Handle method call on a variable with unresolved generic type params.
|
|
1425
|
+
|
|
1426
|
+
Accumulates type parameter constraints from method arguments.
|
|
1427
|
+
Eagerly resolves the generic instance once all type params are known.
|
|
1428
|
+
"""
|
|
1429
|
+
info = self.ctx.func.pending_generic_instances.get(obj_type.instance_id)
|
|
1430
|
+
if info is None:
|
|
1431
|
+
raise self.ctx.error(
|
|
1432
|
+
f"Internal error: pending generic instance {obj_type.instance_id} not found", expr)
|
|
1433
|
+
|
|
1434
|
+
record = info.record_info
|
|
1435
|
+
overloads = record.get_method_overloads(expr.method)
|
|
1436
|
+
if not overloads:
|
|
1437
|
+
raise self.ctx.error(
|
|
1438
|
+
f"'{record.name}' has no method '{expr.method}'", expr)
|
|
1439
|
+
|
|
1440
|
+
# For MVP: use first overload (user records have single overloads per name)
|
|
1441
|
+
method = overloads[0]
|
|
1442
|
+
|
|
1443
|
+
# Resolve kwargs (also enforces keyword-only constraints when no kwargs)
|
|
1444
|
+
if expr.kwargs or method.has_keyword_only:
|
|
1445
|
+
expr.args = resolve_kwargs(
|
|
1446
|
+
expr.args, expr.kwargs, method.params, expr.method,
|
|
1447
|
+
lambda msg: self.ctx.error(msg, expr),
|
|
1448
|
+
call_loc=expr.loc,
|
|
1449
|
+
)
|
|
1450
|
+
expr.kwargs = {}
|
|
1451
|
+
|
|
1452
|
+
# Check arity
|
|
1453
|
+
if len(expr.args) < method.min_args or len(expr.args) > method.max_args:
|
|
1454
|
+
raise self.ctx.error(
|
|
1455
|
+
arity_error_msg(expr.method, method.min_args, method.max_args, len(expr.args)),
|
|
1456
|
+
expr)
|
|
1457
|
+
|
|
1458
|
+
# Analyze arguments and accumulate constraints. analyze_call_arg so a
|
|
1459
|
+
# `*xs` arg yields the unpacked element type rather than crashing the
|
|
1460
|
+
# structural analyzer; non-variadic reject gate handles validity.
|
|
1461
|
+
arg_types = [self.expr.analyze_call_arg(arg) for arg in expr.args]
|
|
1462
|
+
type_param_names = set(info.type_params)
|
|
1463
|
+
for (pname, ptype), arg_type in zip(method.params, arg_types):
|
|
1464
|
+
if not contains_type_param(ptype, type_param_names):
|
|
1465
|
+
continue
|
|
1466
|
+
# Resolve IntLiteralType before binding
|
|
1467
|
+
resolved_arg = arg_type
|
|
1468
|
+
if isinstance(resolved_arg, IntLiteralType):
|
|
1469
|
+
resolved_arg = self.ctx.default_int_for_literal(resolved_arg)
|
|
1470
|
+
if not self.type_ops.match_type_with_inference(ptype, resolved_arg, info.inferred):
|
|
1471
|
+
# Check if this is a conflict with an existing binding
|
|
1472
|
+
for tp in info.type_params:
|
|
1473
|
+
if tp in info.inferred:
|
|
1474
|
+
existing = info.inferred[tp]
|
|
1475
|
+
# Try matching just this param to see if it conflicts
|
|
1476
|
+
test: dict[str, TpyType] = {}
|
|
1477
|
+
self.type_ops.match_type_with_inference(ptype, resolved_arg, test)
|
|
1478
|
+
if tp in test and test[tp] != existing:
|
|
1479
|
+
raise self.ctx.error(
|
|
1480
|
+
f"Conflicting type inference for '{tp}' in '{record.name}': "
|
|
1481
|
+
f"previously inferred as '{existing}', "
|
|
1482
|
+
f"but '{expr.method}' argument '{pname}' implies '{test[tp]}'",
|
|
1483
|
+
expr,
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
# Resolve IntLiteralType in any newly inferred params
|
|
1487
|
+
for k, v in list(info.inferred.items()):
|
|
1488
|
+
if isinstance(v, IntLiteralType):
|
|
1489
|
+
info.inferred[k] = self.ctx.default_int_for_literal(v)
|
|
1490
|
+
|
|
1491
|
+
# Check if all type params are now resolved
|
|
1492
|
+
all_resolved = all(tp in info.inferred for tp in info.type_params)
|
|
1493
|
+
|
|
1494
|
+
if all_resolved:
|
|
1495
|
+
resolved_type = self._eagerly_resolve_pending_generic(info)
|
|
1496
|
+
# Re-dispatch: analyze the method call on the now-concrete type
|
|
1497
|
+
self.ctx.set_expr_type(expr.obj, resolved_type)
|
|
1498
|
+
if isinstance(expr.obj, TpyName) and self.ctx.func.current_scope:
|
|
1499
|
+
self.ctx.func.current_scope.define(expr.obj.name, resolved_type)
|
|
1500
|
+
result = self._try_resolve_method(expr, resolved_type)
|
|
1501
|
+
if result is None:
|
|
1502
|
+
raise self.ctx.error(
|
|
1503
|
+
f"'{resolved_type}' has no method '{expr.method}'", expr)
|
|
1504
|
+
return result
|
|
1505
|
+
|
|
1506
|
+
# Not fully resolved yet -- check return type
|
|
1507
|
+
return_type = method.return_type
|
|
1508
|
+
if contains_type_param(return_type, type_param_names):
|
|
1509
|
+
# Check if we can substitute what we have so far
|
|
1510
|
+
unresolved_in_return = _unresolved_params_in_type(return_type, info.inferred, type_param_names)
|
|
1511
|
+
if unresolved_in_return:
|
|
1512
|
+
raise self.ctx.error(
|
|
1513
|
+
f"Cannot determine return type of '{expr.method}' on '{record.name}': "
|
|
1514
|
+
f"type parameter{'s' if len(unresolved_in_return) > 1 else ''} "
|
|
1515
|
+
f"{', '.join(unresolved_in_return)} not yet resolved; "
|
|
1516
|
+
f"call a constraining method first or add explicit type arguments",
|
|
1517
|
+
expr,
|
|
1518
|
+
)
|
|
1519
|
+
# All params in return type are resolved, substitute
|
|
1520
|
+
return_type = self.type_ops.substitute_type_params(return_type, info.inferred)
|
|
1521
|
+
|
|
1522
|
+
# Set minimal function info for void methods
|
|
1523
|
+
expr.resolved_function_info = FunctionInfo(
|
|
1524
|
+
name=expr.method,
|
|
1525
|
+
params=method.params,
|
|
1526
|
+
return_type=return_type,
|
|
1527
|
+
# Resumable-factory flags drive call-site capture decisions
|
|
1528
|
+
# (e.g. temporary-receiver lift); a pending-generic generator/
|
|
1529
|
+
# async method must not lose them on this minimal path.
|
|
1530
|
+
is_async=method.is_async,
|
|
1531
|
+
is_generator=method.is_generator,
|
|
1532
|
+
canonical_fi=method.root,
|
|
1533
|
+
)
|
|
1534
|
+
return return_type
|
|
1535
|
+
|
|
1536
|
+
def try_resolve_pending_from_expected_type(
|
|
1537
|
+
self, pending: PendingGenericInstanceType, expected: TpyType,
|
|
1538
|
+
loc: 'SourceLocation | None' = None,
|
|
1539
|
+
) -> NominalType | None:
|
|
1540
|
+
"""Try to resolve a pending generic instance from an expected type.
|
|
1541
|
+
|
|
1542
|
+
Used when a pending-type variable is passed to a typed parameter or
|
|
1543
|
+
returned where the function return type is known. Returns the resolved
|
|
1544
|
+
concrete type, or None if the expected type doesn't match.
|
|
1545
|
+
"""
|
|
1546
|
+
info = self.ctx.func.pending_generic_instances.get(pending.instance_id)
|
|
1547
|
+
if info is None:
|
|
1548
|
+
return None
|
|
1549
|
+
|
|
1550
|
+
# Unwrap Own/Optional/Readonly/Ref to find the inner NominalType
|
|
1551
|
+
target = expected
|
|
1552
|
+
if isinstance(target, OwnType):
|
|
1553
|
+
target = target.wrapped
|
|
1554
|
+
if isinstance(target, OptionalType):
|
|
1555
|
+
target = target.inner
|
|
1556
|
+
target = unwrap_readonly(unwrap_ref_type(target))
|
|
1557
|
+
|
|
1558
|
+
if not isinstance(target, NominalType) or target.name != info.record_name:
|
|
1559
|
+
return None
|
|
1560
|
+
if not target.type_args or len(target.type_args) != len(info.type_params):
|
|
1561
|
+
return None
|
|
1562
|
+
|
|
1563
|
+
# Build pattern with TypeParamRefs for unresolved params
|
|
1564
|
+
pattern_args = []
|
|
1565
|
+
for tp in info.type_params:
|
|
1566
|
+
if tp in info.inferred:
|
|
1567
|
+
pattern_args.append(info.inferred[tp])
|
|
1568
|
+
else:
|
|
1569
|
+
pattern_args.append(TypeParamRef(tp))
|
|
1570
|
+
pattern = NominalType(
|
|
1571
|
+
info.record_name, tuple(pattern_args),
|
|
1572
|
+
_module_qname=info.record_info.qualified_name(),
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
# Match to extract constraints
|
|
1576
|
+
if not self.type_ops.match_type_with_inference(pattern, target, info.inferred):
|
|
1577
|
+
# Check if a previously-inferred param conflicts with the expected type
|
|
1578
|
+
for tp, expected_arg in zip(info.type_params, target.type_args):
|
|
1579
|
+
if tp in info.inferred and isinstance(expected_arg, TpyType):
|
|
1580
|
+
if info.inferred[tp] != expected_arg:
|
|
1581
|
+
raise SemanticError(
|
|
1582
|
+
f"Conflicting type for '{tp}' in '{info.record_name}': "
|
|
1583
|
+
f"previously inferred as '{info.inferred[tp]}', "
|
|
1584
|
+
f"but expected type requires '{expected_arg}'",
|
|
1585
|
+
loc,
|
|
1586
|
+
)
|
|
1587
|
+
return None
|
|
1588
|
+
|
|
1589
|
+
# Resolve IntLiteralType in any newly inferred params
|
|
1590
|
+
for k, v in list(info.inferred.items()):
|
|
1591
|
+
if isinstance(v, IntLiteralType):
|
|
1592
|
+
info.inferred[k] = self.ctx.default_int_for_literal(v)
|
|
1593
|
+
|
|
1594
|
+
# Check if all type params are now resolved
|
|
1595
|
+
if not all(tp in info.inferred for tp in info.type_params):
|
|
1596
|
+
return None
|
|
1597
|
+
|
|
1598
|
+
return self._eagerly_resolve_pending_generic(info)
|
|
1599
|
+
|
|
1600
|
+
def _eagerly_resolve_pending_generic(self, info: 'PendingGenericInstanceInfo') -> NominalType:
|
|
1601
|
+
"""Resolve a pending generic instance to a concrete NominalType."""
|
|
1602
|
+
type_args = tuple(info.inferred[tp] for tp in info.type_params)
|
|
1603
|
+
resolved_type = NominalType(
|
|
1604
|
+
info.record_name, type_args,
|
|
1605
|
+
_module_qname=info.record_info.qualified_name(),
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1608
|
+
# Validate type param bounds
|
|
1609
|
+
for param_name, type_arg in zip(info.type_params, type_args):
|
|
1610
|
+
if param_name in info.record_info.type_param_bounds:
|
|
1611
|
+
bound = info.record_info.type_param_bounds[param_name]
|
|
1612
|
+
if not self.protocols.satisfies_bound(type_arg, bound):
|
|
1613
|
+
raise self.ctx.error(
|
|
1614
|
+
f"Inferred type '{type_arg}' does not satisfy bound '{bound}' "
|
|
1615
|
+
f"for type parameter '{param_name}' of '{info.record_name}'",
|
|
1616
|
+
info.expr,
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
# Update constructor expression
|
|
1620
|
+
info.expr.call_type = resolved_type
|
|
1621
|
+
self.ctx.set_expr_type(info.expr, resolved_type)
|
|
1622
|
+
|
|
1623
|
+
# Update scope and var_types (only when bound to a local variable,
|
|
1624
|
+
# not for inline expressions like Container().set(...) which would
|
|
1625
|
+
# corrupt the class binding in scope)
|
|
1626
|
+
if info.decl_line is not None:
|
|
1627
|
+
if self.ctx.func.current_scope:
|
|
1628
|
+
self.ctx.func.current_scope.define(info.variable_name, resolved_type)
|
|
1629
|
+
if self.ctx.func.current_ns:
|
|
1630
|
+
self.ctx.func.current_ns.bind_variable(info.variable_name, resolved_type)
|
|
1631
|
+
var_decl = self.ctx.func.var_decl_by_name.get(info.variable_name)
|
|
1632
|
+
if var_decl:
|
|
1633
|
+
self.ctx.var_types[id(var_decl)] = resolved_type
|
|
1634
|
+
self.ctx.declared_var_types[(info.decl_line, info.variable_name)] = resolved_type
|
|
1635
|
+
|
|
1636
|
+
# Set constructor info now that we have concrete types
|
|
1637
|
+
type_subst = info.inferred
|
|
1638
|
+
self.calls._set_record_constructor_info(info.expr, info.record_info, resolved_type, type_subst)
|
|
1639
|
+
|
|
1640
|
+
# Clean up tracking
|
|
1641
|
+
del self.ctx.func.pending_generic_instances[info.instance_id]
|
|
1642
|
+
self.ctx.func.variable_to_generic_instance.pop(info.variable_name, None)
|
|
1643
|
+
|
|
1644
|
+
return resolved_type
|
|
1645
|
+
|
|
1646
|
+
def _analyze_typed_dict_get(self, expr: TpyMethodCall, record_info, obj_type: TpyType) -> TpyType:
|
|
1647
|
+
"""Analyze td.get("key") or td.get("key", default) on a TypedDict."""
|
|
1648
|
+
if len(expr.args) < 1 or len(expr.args) > 2:
|
|
1649
|
+
raise self.ctx.error(
|
|
1650
|
+
f"TypedDict.get() takes 1 or 2 arguments, got {len(expr.args)}", expr)
|
|
1651
|
+
if expr.kwargs:
|
|
1652
|
+
raise self.ctx.error("TypedDict.get() does not accept keyword arguments", expr)
|
|
1653
|
+
key_expr = expr.args[0]
|
|
1654
|
+
if not isinstance(key_expr, TpyStrLiteral):
|
|
1655
|
+
raise self.ctx.error(
|
|
1656
|
+
f"TypedDict '{obj_type.name}' keys must be string literals", key_expr)
|
|
1657
|
+
key = key_expr.value
|
|
1658
|
+
type_subst = self.type_ops.build_type_substitution(obj_type)
|
|
1659
|
+
for fld in record_info.fields:
|
|
1660
|
+
if fld.name == key:
|
|
1661
|
+
field_type = fld.type
|
|
1662
|
+
if type_subst:
|
|
1663
|
+
field_type = self.type_ops.substitute_type_params(field_type, type_subst)
|
|
1664
|
+
# total=False: field stored as Optional[T], unwrap to get inner type
|
|
1665
|
+
# total=True: field is always present, even if annotated Optional[T]
|
|
1666
|
+
is_absent_optional = record_info.is_total_false
|
|
1667
|
+
if is_absent_optional:
|
|
1668
|
+
assert isinstance(field_type, OptionalType)
|
|
1669
|
+
inner_type = field_type.inner
|
|
1670
|
+
else:
|
|
1671
|
+
inner_type = field_type
|
|
1672
|
+
expr.typed_dict_get_field = key
|
|
1673
|
+
expr.typed_dict_get_optional = is_absent_optional
|
|
1674
|
+
# Analyze key expression so its type is recorded
|
|
1675
|
+
self.expr.analyze_expr(key_expr)
|
|
1676
|
+
has_default = len(expr.args) == 2
|
|
1677
|
+
if has_default:
|
|
1678
|
+
default_type = self.expr.analyze_expr_with_hint(expr.args[1], inner_type)
|
|
1679
|
+
self.compat.check_type_compatible(
|
|
1680
|
+
default_type, inner_type,
|
|
1681
|
+
f"default value for TypedDict.get()", loc=expr.args[1].loc,
|
|
1682
|
+
source_expr=expr.args[1])
|
|
1683
|
+
return inner_type
|
|
1684
|
+
else:
|
|
1685
|
+
# Don't double-wrap: total=True Optional[T] field is already Optional
|
|
1686
|
+
if isinstance(inner_type, OptionalType):
|
|
1687
|
+
return inner_type
|
|
1688
|
+
return OptionalType(inner_type)
|
|
1689
|
+
raise self.ctx.error(
|
|
1690
|
+
f"TypedDict '{obj_type.name}' has no key '{key}'", key_expr)
|
|
1691
|
+
|
|
1692
|
+
def _analyze_instance_method(self, expr: TpyMethodCall, obj_type: TpyType,
|
|
1693
|
+
is_readonly_receiver: bool = False,
|
|
1694
|
+
is_consuming_receiver: bool = False) -> TpyType | None:
|
|
1695
|
+
"""Analyze instance method call on any type (builtin or user record)."""
|
|
1696
|
+
record_info = self.ctx.registry.get_record_for_type(obj_type)
|
|
1697
|
+
if not record_info:
|
|
1698
|
+
return None
|
|
1699
|
+
# TypedDict: td.get("key") / td.get("key", default)
|
|
1700
|
+
if record_info.is_typed_dict and expr.method == "get":
|
|
1701
|
+
return self._analyze_typed_dict_get(expr, record_info, obj_type)
|
|
1702
|
+
overloads, inherited_subst = self.protocols.lookup_record_method_overloads(
|
|
1703
|
+
record_info, expr.method)
|
|
1704
|
+
if not overloads:
|
|
1705
|
+
return None
|
|
1706
|
+
instance_subst = self.type_ops.build_type_substitution(obj_type)
|
|
1707
|
+
if inherited_subst and instance_subst:
|
|
1708
|
+
type_subst = {
|
|
1709
|
+
k: self.type_ops.substitute_type_params(v, instance_subst)
|
|
1710
|
+
for k, v in inherited_subst.items()
|
|
1711
|
+
}
|
|
1712
|
+
elif inherited_subst:
|
|
1713
|
+
type_subst = inherited_subst
|
|
1714
|
+
else:
|
|
1715
|
+
type_subst = instance_subst
|
|
1716
|
+
|
|
1717
|
+
method_info = overloads[0]
|
|
1718
|
+
|
|
1719
|
+
# @inline: clone body and substitute at call site.
|
|
1720
|
+
# For FStr params, validate that f-string literals are passed.
|
|
1721
|
+
if method_info.inline_body is not None:
|
|
1722
|
+
for i, (pi, arg) in enumerate(zip(method_info.params, expr.args)):
|
|
1723
|
+
if is_fstr_type(pi.type) and not isinstance(arg, TpyFString):
|
|
1724
|
+
if isinstance(arg, TpyStrLiteral):
|
|
1725
|
+
expr.args[i] = TpyFString(parts=[arg.value], loc=arg.loc)
|
|
1726
|
+
else:
|
|
1727
|
+
raise self.ctx.error(
|
|
1728
|
+
f"Parameter '{pi.name}' has type FStr -- only f-string "
|
|
1729
|
+
f"or string literals are accepted", arg)
|
|
1730
|
+
return self._inline_method_call(expr, method_info)
|
|
1731
|
+
|
|
1732
|
+
# Check if the method has its own type parameters (generic method).
|
|
1733
|
+
# Generic methods don't support multiple overloads; user-defined methods
|
|
1734
|
+
# always register a single overload per name (registration.py).
|
|
1735
|
+
if method_info.is_generic():
|
|
1736
|
+
if len(overloads) > 1:
|
|
1737
|
+
raise self.ctx.error(
|
|
1738
|
+
f"Overloaded generic methods are not supported for '{expr.method}'", expr)
|
|
1739
|
+
return self._analyze_generic_method_call(
|
|
1740
|
+
expr, method_info, record_info, type_subst)
|
|
1741
|
+
|
|
1742
|
+
return self._resolve_and_check_args(
|
|
1743
|
+
expr, overloads, type_subst, is_readonly_receiver=is_readonly_receiver,
|
|
1744
|
+
is_consuming_receiver=is_consuming_receiver)
|
|
1745
|
+
|
|
1746
|
+
def _inline_method_call(
|
|
1747
|
+
self, expr: TpyMethodCall, method_info: FunctionInfo,
|
|
1748
|
+
) -> TpyType:
|
|
1749
|
+
"""Inline a method with FStr parameter at the call site.
|
|
1750
|
+
|
|
1751
|
+
Clones the method's body expression, substitutes ``self`` with the
|
|
1752
|
+
receiver and the FStr parameter with the actual f-string argument,
|
|
1753
|
+
then analyzes the substituted expression. This lets the f-string
|
|
1754
|
+
literal reach the call macro for decomposition.
|
|
1755
|
+
"""
|
|
1756
|
+
body = copy.deepcopy(method_info.inline_body)
|
|
1757
|
+
|
|
1758
|
+
# Build the substitution map: param name -> call-site argument.
|
|
1759
|
+
# method_info.params does NOT include 'self' (it's implicit for methods).
|
|
1760
|
+
param_map: dict[str, TpyExpr] = {}
|
|
1761
|
+
for pi, arg in zip(method_info.params, expr.args):
|
|
1762
|
+
param_map[pi.name] = arg
|
|
1763
|
+
|
|
1764
|
+
# Substitute names in the cloned body
|
|
1765
|
+
MethodAnalyzer._substitute_inline_body(body, expr.obj, param_map)
|
|
1766
|
+
|
|
1767
|
+
# Store the inlined expression on the method call node for codegen
|
|
1768
|
+
expr.fstr_expansion = body
|
|
1769
|
+
|
|
1770
|
+
# Analyze the substituted expression (side effect: type-checks the macro expansion)
|
|
1771
|
+
self.expr.analyze_expr(body)
|
|
1772
|
+
return VOID
|
|
1773
|
+
|
|
1774
|
+
@staticmethod
|
|
1775
|
+
def _substitute_inline_body(
|
|
1776
|
+
node: TpyExpr, receiver: TpyExpr | None,
|
|
1777
|
+
param_map: dict[str, TpyExpr],
|
|
1778
|
+
) -> None:
|
|
1779
|
+
"""In-place substitute names in a cloned @inline body expression.
|
|
1780
|
+
|
|
1781
|
+
Args:
|
|
1782
|
+
node: The cloned body expression to substitute in.
|
|
1783
|
+
receiver: The ``self`` replacement (method calls), or None (free functions).
|
|
1784
|
+
param_map: Parameter name -> call-site argument expression.
|
|
1785
|
+
|
|
1786
|
+
Replaces TpyName references matching param_map keys or "self" (when
|
|
1787
|
+
receiver is provided) in positional args, function refs, and method
|
|
1788
|
+
receivers.
|
|
1789
|
+
|
|
1790
|
+
Current limitation: only handles TpyCall, TpyMethodCall, TpyFieldAccess,
|
|
1791
|
+
and TpyName in positional args. Does not recurse into kwargs, TpyBinOp,
|
|
1792
|
+
TpyIfExpr, TpySubscript, or other nested expression types. Sufficient
|
|
1793
|
+
for @inline bodies constrained to a single call. Future: support
|
|
1794
|
+
multi-statement bodies via expression blocks.
|
|
1795
|
+
"""
|
|
1796
|
+
sub = MethodAnalyzer._substitute_inline_body
|
|
1797
|
+
sub_args = MethodAnalyzer._substitute_inline_args
|
|
1798
|
+
if isinstance(node, TpyCall):
|
|
1799
|
+
if isinstance(node.func, TpyName) and node.func.name in param_map:
|
|
1800
|
+
node.func = param_map[node.func.name]
|
|
1801
|
+
sub_args(node.args, receiver, param_map)
|
|
1802
|
+
elif isinstance(node, TpyMethodCall):
|
|
1803
|
+
if receiver and isinstance(node.obj, TpyName) and node.obj.name == "self":
|
|
1804
|
+
node.obj = receiver
|
|
1805
|
+
sub_args(node.args, receiver, param_map)
|
|
1806
|
+
|
|
1807
|
+
@staticmethod
|
|
1808
|
+
def _substitute_inline_args(
|
|
1809
|
+
args: list[TpyExpr], receiver: TpyExpr | None,
|
|
1810
|
+
param_map: dict[str, TpyExpr],
|
|
1811
|
+
) -> None:
|
|
1812
|
+
"""Substitute names in a list of positional arguments."""
|
|
1813
|
+
sub = MethodAnalyzer._substitute_inline_body
|
|
1814
|
+
for i, arg in enumerate(args):
|
|
1815
|
+
if isinstance(arg, TpyName) and arg.name in param_map:
|
|
1816
|
+
args[i] = param_map[arg.name]
|
|
1817
|
+
elif receiver and isinstance(arg, TpyName) and arg.name == "self":
|
|
1818
|
+
args[i] = receiver
|
|
1819
|
+
elif isinstance(arg, TpyFieldAccess):
|
|
1820
|
+
if receiver and isinstance(arg.obj, TpyName) and arg.obj.name == "self":
|
|
1821
|
+
arg.obj = receiver
|
|
1822
|
+
else:
|
|
1823
|
+
sub(arg, receiver, param_map)
|
|
1824
|
+
elif isinstance(arg, (TpyCall, TpyMethodCall)):
|
|
1825
|
+
sub(arg, receiver, param_map)
|
|
1826
|
+
|
|
1827
|
+
def _analyze_generic_method_call(
|
|
1828
|
+
self, expr: TpyMethodCall, method_info: FunctionInfo,
|
|
1829
|
+
record_info, class_subst: dict[str, TpyType | int],
|
|
1830
|
+
) -> TpyType:
|
|
1831
|
+
"""Analyze a call to a generic method (method with its own type parameters).
|
|
1832
|
+
|
|
1833
|
+
Handles two kinds of method type params:
|
|
1834
|
+
- New params: type params not in the class (e.g. U on def transform[U])
|
|
1835
|
+
- Constrained class params: class type params with an additional method-level bound
|
|
1836
|
+
"""
|
|
1837
|
+
class_type_params = set(record_info.type_params) if record_info.type_params else set()
|
|
1838
|
+
new_params = [tp for tp in method_info.type_params if tp not in class_type_params]
|
|
1839
|
+
constrained_class_params = [tp for tp in method_info.type_params if tp in class_type_params]
|
|
1840
|
+
|
|
1841
|
+
# Validate per-method bounds on class type params: check that the concrete
|
|
1842
|
+
# class type satisfies the method's bound. These are class-level type params,
|
|
1843
|
+
# so they must be in class_subst for any fully-instantiated generic class.
|
|
1844
|
+
for tp in constrained_class_params:
|
|
1845
|
+
if tp not in class_subst:
|
|
1846
|
+
raise self.ctx.error(
|
|
1847
|
+
f"Method '{method_info.name}' has bound on class type parameter '{tp}', "
|
|
1848
|
+
f"but the class is not instantiated with a concrete type for '{tp}'",
|
|
1849
|
+
expr)
|
|
1850
|
+
raise_if_class_param_bound_violated(
|
|
1851
|
+
method_info, record_info.type_params, class_subst,
|
|
1852
|
+
self.protocols.type_conforms_to_protocol,
|
|
1853
|
+
self.ctx.error, expr,
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
if not new_params:
|
|
1857
|
+
# All method type params are constrained class params -- no inference needed.
|
|
1858
|
+
# Single-element list: generic methods can't have multiple overloads (guarded above).
|
|
1859
|
+
return self._resolve_and_check_args(expr, [method_info], class_subst)
|
|
1860
|
+
|
|
1861
|
+
# Build a partial FunctionInfo with only new params for inference
|
|
1862
|
+
partial_func = FunctionInfo(
|
|
1863
|
+
name=method_info.name,
|
|
1864
|
+
params=method_info.params,
|
|
1865
|
+
return_type=method_info.return_type,
|
|
1866
|
+
type_params=new_params,
|
|
1867
|
+
type_param_bounds={k: v for k, v in method_info.type_param_bounds.items()
|
|
1868
|
+
if k in new_params},
|
|
1869
|
+
canonical_fi=method_info.root,
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
# Pre-substitute class params in the method signature so inference
|
|
1873
|
+
# only needs to resolve new params
|
|
1874
|
+
if class_subst:
|
|
1875
|
+
partial_func = self.type_ops.substitute_method_type_params(partial_func, class_subst)
|
|
1876
|
+
|
|
1877
|
+
# Infer new params from arguments or explicit type args. The LHS-hint
|
|
1878
|
+
# seed is only useful on paths that actually re-analyze args with a
|
|
1879
|
+
# contextual hint, so it (and the closure that consumes it) lives
|
|
1880
|
+
# inside the inference branches that need it.
|
|
1881
|
+
has_wildcards = expr.type_args and None in expr.type_args
|
|
1882
|
+
if expr.type_args:
|
|
1883
|
+
if len(expr.type_args) != len(new_params):
|
|
1884
|
+
raise self.ctx.error(
|
|
1885
|
+
f"Method '{method_info.name}' expects {len(new_params)} type argument(s), "
|
|
1886
|
+
f"got {len(expr.type_args)}",
|
|
1887
|
+
expr)
|
|
1888
|
+
if not has_wildcards:
|
|
1889
|
+
# Full explicit -- no seed needed.
|
|
1890
|
+
method_subst = dict(zip(new_params, expr.type_args))
|
|
1891
|
+
else:
|
|
1892
|
+
# Partial explicit -- seed + explicit positional args drive
|
|
1893
|
+
# the per-arg hint; wildcards leave the seed binding in place.
|
|
1894
|
+
method_subst = self._infer_method_subst_with_seed(
|
|
1895
|
+
expr, partial_func, method_info, new_params,
|
|
1896
|
+
explicit_type_args=expr.type_args,
|
|
1897
|
+
)
|
|
1898
|
+
else:
|
|
1899
|
+
method_subst = self._infer_method_subst_with_seed(
|
|
1900
|
+
expr, partial_func, method_info, new_params,
|
|
1901
|
+
explicit_type_args=None,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
# Validate bounds for new params (inference checks bounds internally,
|
|
1905
|
+
# but explicit type args bypass inference)
|
|
1906
|
+
new_param_bounds = {k: v for k, v in method_info.type_param_bounds.items()
|
|
1907
|
+
if k in set(new_params)}
|
|
1908
|
+
if new_param_bounds:
|
|
1909
|
+
validate_type_param_bounds(
|
|
1910
|
+
method_subst, new_param_bounds, method_info.name,
|
|
1911
|
+
self.protocols.satisfies_bound,
|
|
1912
|
+
lambda msg: self.ctx.error(msg, expr),
|
|
1913
|
+
)
|
|
1914
|
+
|
|
1915
|
+
# Store inferred type args (new params only) for codegen
|
|
1916
|
+
expr.inferred_type_args = tuple(
|
|
1917
|
+
resolve_inferred_type_arg(method_subst[p], self.ctx.default_int_type)
|
|
1918
|
+
for p in new_params
|
|
1919
|
+
)
|
|
1920
|
+
expr.representational_subst_params = (
|
|
1921
|
+
self.type_ops.compute_representational_subst_params(
|
|
1922
|
+
method_info, expr.inferred_type_args))
|
|
1923
|
+
|
|
1924
|
+
# Merge class subst + method subst for full substitution
|
|
1925
|
+
full_subst = dict(class_subst) if class_subst else {}
|
|
1926
|
+
full_subst.update(method_subst)
|
|
1927
|
+
|
|
1928
|
+
return self._resolve_and_check_args(expr, [method_info], full_subst)
|
|
1929
|
+
|
|
1930
|
+
def _infer_method_subst_with_seed(
|
|
1931
|
+
self,
|
|
1932
|
+
expr: TpyMethodCall,
|
|
1933
|
+
partial_func: FunctionInfo,
|
|
1934
|
+
method_info: FunctionInfo,
|
|
1935
|
+
new_params: list[str],
|
|
1936
|
+
explicit_type_args: tuple['TpyType | None', ...] | None,
|
|
1937
|
+
) -> dict[str, TpyType]:
|
|
1938
|
+
"""Analyze args with an LHS-hint seed and run method-param inference.
|
|
1939
|
+
|
|
1940
|
+
Shared by the pure-inference and partial-explicit (wildcard) branches
|
|
1941
|
+
of ``_analyze_generic_method_call``. The seed is computed against the
|
|
1942
|
+
already class-substituted ``partial_func.return_type``; explicit
|
|
1943
|
+
positional type args (when present) overlay the seed at their
|
|
1944
|
+
positions.
|
|
1945
|
+
"""
|
|
1946
|
+
seed_subst = self.type_ops.seed_subst_from_return_hint(
|
|
1947
|
+
partial_func, self.ctx.expr_type_hint
|
|
1948
|
+
)
|
|
1949
|
+
merged_seed = dict(seed_subst)
|
|
1950
|
+
if explicit_type_args is not None:
|
|
1951
|
+
for tp, ta in zip(new_params, explicit_type_args):
|
|
1952
|
+
if ta is not None:
|
|
1953
|
+
merged_seed[tp] = ta
|
|
1954
|
+
|
|
1955
|
+
# Default-arg capture freezes ``partial_func`` and ``merged_seed`` at
|
|
1956
|
+
# def-time so this closure isn't sensitive to later rebinding.
|
|
1957
|
+
def _analyze_args(
|
|
1958
|
+
_pf: FunctionInfo = partial_func,
|
|
1959
|
+
_seed: dict[str, TpyType] = merged_seed,
|
|
1960
|
+
) -> list[TpyType]:
|
|
1961
|
+
out: list[TpyType] = []
|
|
1962
|
+
for i, arg in enumerate(expr.args):
|
|
1963
|
+
hint = seeded_arg_hint(_pf.params, i, _seed)
|
|
1964
|
+
out.append(self.expr.analyze_call_arg(arg, hint))
|
|
1965
|
+
return out
|
|
1966
|
+
|
|
1967
|
+
arg_types = _analyze_args()
|
|
1968
|
+
method_subst = self.type_ops.infer_type_params_for_function(
|
|
1969
|
+
partial_func, arg_types, self.protocols.satisfies_bound,
|
|
1970
|
+
expected_return_type=self.ctx.expr_type_hint,
|
|
1971
|
+
explicit_type_args=explicit_type_args,
|
|
1972
|
+
)
|
|
1973
|
+
if method_subst is None:
|
|
1974
|
+
raise self.ctx.error(
|
|
1975
|
+
f"Cannot infer type arguments for method '{method_info.name}'. "
|
|
1976
|
+
f"Specify explicitly: .{method_info.name}[{', '.join(new_params)}](...)",
|
|
1977
|
+
expr)
|
|
1978
|
+
return method_subst
|
|
1979
|
+
|
|
1980
|
+
def _is_protocol_method_readonly(self, protocol_name: str, method_name: str) -> bool:
|
|
1981
|
+
"""Check if a protocol method is readonly (per-method or protocol-level).
|
|
1982
|
+
|
|
1983
|
+
Searches inherited methods too, so a @readonly method from a parent
|
|
1984
|
+
protocol is correctly recognized.
|
|
1985
|
+
"""
|
|
1986
|
+
proto_info = self.ctx.registry.scan_by_short_name(protocol_name)
|
|
1987
|
+
if proto_info is None:
|
|
1988
|
+
return False
|
|
1989
|
+
if proto_info.is_readonly:
|
|
1990
|
+
return True
|
|
1991
|
+
for msig in self.protocols.collect_protocol_methods(protocol_name):
|
|
1992
|
+
if msig.name == method_name:
|
|
1993
|
+
return msig.is_readonly
|
|
1994
|
+
return False
|
|
1995
|
+
|
|
1996
|
+
def _build_protocol_method_info(self, protocol_name: str, method_name: str,
|
|
1997
|
+
raw_params: list[tuple[str, TpyType]],
|
|
1998
|
+
return_type: TpyType,
|
|
1999
|
+
cpp_template: str | None = None,
|
|
2000
|
+
param_defaults: list | None = None) -> FunctionInfo:
|
|
2001
|
+
"""Build a FunctionInfo for a protocol method signature."""
|
|
2002
|
+
# param_defaults is parallel to raw_params (None for required, TpyExpr for optional);
|
|
2003
|
+
# empty list means no defaults declared at protocol method level.
|
|
2004
|
+
defaults = param_defaults if param_defaults else [None] * len(raw_params)
|
|
2005
|
+
params = [
|
|
2006
|
+
ParamInfo(n, t, default_expr=default)
|
|
2007
|
+
for (n, t), default in zip(raw_params, defaults)
|
|
2008
|
+
]
|
|
2009
|
+
# __next__ on Iterator protocol has implicit @error_return(StopIteration)
|
|
2010
|
+
error_return_type = None
|
|
2011
|
+
if method_name == "__next__":
|
|
2012
|
+
error_return_type = "builtins.StopIteration"
|
|
2013
|
+
return FunctionInfo(
|
|
2014
|
+
name=method_name, params=params, return_type=return_type,
|
|
2015
|
+
is_method=True,
|
|
2016
|
+
is_readonly=self._is_protocol_method_readonly(protocol_name, method_name),
|
|
2017
|
+
cpp_template=cpp_template,
|
|
2018
|
+
error_return_type=error_return_type,
|
|
2019
|
+
)
|
|
2020
|
+
|
|
2021
|
+
def _analyze_protocol_or_bound_method(self, expr: TpyMethodCall, obj_type: TpyType) -> TpyType | None:
|
|
2022
|
+
"""Analyze method calls on protocol-typed values or bounded type parameters."""
|
|
2023
|
+
if is_protocol_type(obj_type):
|
|
2024
|
+
method_sig = self.protocols.get_protocol_method_signature(obj_type, expr.method)
|
|
2025
|
+
if method_sig is None:
|
|
2026
|
+
raise self.ctx.error(f"Protocol '{obj_type.name}' has no method '{expr.method}'", expr)
|
|
2027
|
+
raw_params, return_type, cpp_template, param_defaults = method_sig
|
|
2028
|
+
fi = self._build_protocol_method_info(
|
|
2029
|
+
obj_type.name, expr.method, raw_params, return_type, cpp_template, param_defaults)
|
|
2030
|
+
return self._resolve_and_check_args(expr, [fi], {})
|
|
2031
|
+
|
|
2032
|
+
if isinstance(obj_type, TypeParamRef):
|
|
2033
|
+
bound = self.type_ops.get_type_param_bound(obj_type.name)
|
|
2034
|
+
if bound is not None and is_protocol_type(bound):
|
|
2035
|
+
method_sig = self.protocols.get_protocol_method_signature(bound, expr.method, self_type=obj_type)
|
|
2036
|
+
if method_sig is None:
|
|
2037
|
+
raise self.ctx.error(f"Protocol '{bound.name}' has no method '{expr.method}'", expr)
|
|
2038
|
+
raw_params, return_type, cpp_template, param_defaults = method_sig
|
|
2039
|
+
fi = self._build_protocol_method_info(
|
|
2040
|
+
bound.name, expr.method, raw_params, return_type, cpp_template, param_defaults)
|
|
2041
|
+
return self._resolve_and_check_args(expr, [fi], {})
|
|
2042
|
+
|
|
2043
|
+
return None
|
|
2044
|
+
|
|
2045
|
+
def _analyze_super_method_call(self, expr: TpyMethodCall, super_type: SuperType) -> TpyType:
|
|
2046
|
+
"""Analyze a super().method() call.
|
|
2047
|
+
|
|
2048
|
+
The method is looked up in the parent class and type arguments are
|
|
2049
|
+
substituted for generic parent classes. `super_type` is produced by
|
|
2050
|
+
analyze_expr on the receiver (see calls.py qname dispatch on
|
|
2051
|
+
`builtins.super`).
|
|
2052
|
+
|
|
2053
|
+
Multi-base (D22 v2.3): __del__ is rejected outright; other methods are
|
|
2054
|
+
resolved by walking the child's C3 MRO and picking the first ancestor
|
|
2055
|
+
whose own method table defines the name. See _resolve_super_parent_type.
|
|
2056
|
+
"""
|
|
2057
|
+
parent_type = self._resolve_super_parent_type(expr, super_type)
|
|
2058
|
+
parent_info = self.ctx.registry.get_record_for_type(parent_type)
|
|
2059
|
+
if parent_info is None:
|
|
2060
|
+
raise self.ctx.error(f"Parent class '{parent_type}' not found", expr)
|
|
2061
|
+
|
|
2062
|
+
# Special handling for super().__init__()
|
|
2063
|
+
if expr.method == "__init__":
|
|
2064
|
+
# super().__init__() can only be called inside __init__
|
|
2065
|
+
if self.ctx.func.current_function is None or self.ctx.func.current_function.name != "__init__":
|
|
2066
|
+
raise self.ctx.error(
|
|
2067
|
+
"super().__init__() can only be called inside __init__",
|
|
2068
|
+
expr
|
|
2069
|
+
)
|
|
2070
|
+
# Check for duplicate super().__init__() calls
|
|
2071
|
+
if self.ctx.func.super_init_call is not None:
|
|
2072
|
+
raise self.ctx.error(
|
|
2073
|
+
"super().__init__() can only be called once",
|
|
2074
|
+
expr
|
|
2075
|
+
)
|
|
2076
|
+
# Track this call for later validation (must be first statement)
|
|
2077
|
+
self.ctx.func.super_init_call = expr
|
|
2078
|
+
|
|
2079
|
+
init_overloads = parent_info.get_method_overloads("__init__")
|
|
2080
|
+
if not init_overloads:
|
|
2081
|
+
# Parent has no __init__, allow with no arguments
|
|
2082
|
+
if expr.args:
|
|
2083
|
+
raise self.ctx.error(
|
|
2084
|
+
f"Parent class '{parent_type}' has no __init__, "
|
|
2085
|
+
"super().__init__() must be called with no arguments",
|
|
2086
|
+
expr
|
|
2087
|
+
)
|
|
2088
|
+
# Store parent type for codegen (will generate default base init)
|
|
2089
|
+
expr.super_parent_type = parent_type
|
|
2090
|
+
return VOID
|
|
2091
|
+
|
|
2092
|
+
# Special handling for super().__del__()
|
|
2093
|
+
if expr.method == "__del__":
|
|
2094
|
+
# super().__del__() can only be called inside __del__
|
|
2095
|
+
if self.ctx.func.current_function is None or self.ctx.func.current_function.name != "__del__":
|
|
2096
|
+
raise self.ctx.error(
|
|
2097
|
+
"super().__del__() can only be called inside __del__",
|
|
2098
|
+
expr
|
|
2099
|
+
)
|
|
2100
|
+
# Check for duplicate super().__del__() calls
|
|
2101
|
+
if self.ctx.func.super_del_call is not None:
|
|
2102
|
+
raise self.ctx.error(
|
|
2103
|
+
"super().__del__() can only be called once",
|
|
2104
|
+
expr
|
|
2105
|
+
)
|
|
2106
|
+
# Track this call for later validation (must be last statement)
|
|
2107
|
+
self.ctx.func.super_del_call = expr
|
|
2108
|
+
# No arguments allowed
|
|
2109
|
+
if expr.args:
|
|
2110
|
+
raise self.ctx.error(
|
|
2111
|
+
"super().__del__() takes no arguments",
|
|
2112
|
+
expr
|
|
2113
|
+
)
|
|
2114
|
+
expr.super_parent_type = parent_type
|
|
2115
|
+
return VOID
|
|
2116
|
+
|
|
2117
|
+
# Check readonly constraint: super() in @readonly method inherits readonly
|
|
2118
|
+
is_readonly_context = (
|
|
2119
|
+
isinstance(self.ctx.func.current_function, TpyFunction)
|
|
2120
|
+
and self.ctx.func.current_function.is_readonly
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
# Look up the method in the parent class.
|
|
2124
|
+
# For __init__, we already have init_overloads; for other methods, walk
|
|
2125
|
+
# the parent's MRO so inherited methods (not defined on parent_info itself)
|
|
2126
|
+
# still resolve -- fixes super().foo() when foo lives on parent's ancestor.
|
|
2127
|
+
if expr.method == "__init__":
|
|
2128
|
+
overloads = init_overloads
|
|
2129
|
+
else:
|
|
2130
|
+
overloads = self.ctx.registry.get_method_overloads_with_parents(
|
|
2131
|
+
parent_info, expr.method
|
|
2132
|
+
)
|
|
2133
|
+
if not overloads:
|
|
2134
|
+
raise self.ctx.error(
|
|
2135
|
+
f"Parent class '{parent_type}' has no method '{expr.method}'",
|
|
2136
|
+
expr
|
|
2137
|
+
)
|
|
2138
|
+
|
|
2139
|
+
# Build type substitution for generic parent (e.g., Container[Int32] -> {"T": Int32})
|
|
2140
|
+
type_subst = self.protocols.get_parent_type_subst(parent_type, parent_info)
|
|
2141
|
+
|
|
2142
|
+
# Must be set before arg resolution: mutation-call-edge recording
|
|
2143
|
+
# treats super() receivers as self for self-mutation propagation.
|
|
2144
|
+
expr.super_parent_type = parent_type
|
|
2145
|
+
|
|
2146
|
+
# Check if the method has its own type parameters (generic method)
|
|
2147
|
+
method_info = overloads[0]
|
|
2148
|
+
if method_info.is_generic():
|
|
2149
|
+
if len(overloads) > 1:
|
|
2150
|
+
raise self.ctx.error(
|
|
2151
|
+
f"Overloaded generic methods are not supported for '{expr.method}'", expr)
|
|
2152
|
+
return_type = self._analyze_generic_method_call(
|
|
2153
|
+
expr, method_info, parent_info, type_subst)
|
|
2154
|
+
else:
|
|
2155
|
+
return_type = self._resolve_and_check_args(expr, overloads, type_subst)
|
|
2156
|
+
if is_readonly_context and not expr.resolved_function_info.is_readonly:
|
|
2157
|
+
raise self.ctx.error(
|
|
2158
|
+
f"Cannot call non-readonly method '{expr.method}' on readonly reference",
|
|
2159
|
+
expr)
|
|
2160
|
+
return return_type
|
|
2161
|
+
|
|
2162
|
+
@staticmethod
|
|
2163
|
+
def stmt_contains_super_init(stmt: TpyStmt, super_init: TpyMethodCall) -> bool:
|
|
2164
|
+
"""Check if a statement contains the given super().__init__() call.
|
|
2165
|
+
|
|
2166
|
+
Used to validate that super().__init__() is the first statement.
|
|
2167
|
+
"""
|
|
2168
|
+
# Direct expression statement containing the super().__init__() call
|
|
2169
|
+
if isinstance(stmt, TpyExprStmt):
|
|
2170
|
+
return stmt.expr is super_init
|
|
2171
|
+
return False
|
|
2172
|
+
|
|
2173
|
+
@staticmethod
|
|
2174
|
+
def find_first_non_docstring_stmt(stmts: list[TpyStmt]) -> TpyStmt | None:
|
|
2175
|
+
"""Find the first non-docstring statement in a list.
|
|
2176
|
+
|
|
2177
|
+
Returns None if all statements are docstrings or list is empty.
|
|
2178
|
+
"""
|
|
2179
|
+
for stmt in stmts:
|
|
2180
|
+
if is_docstring(stmt):
|
|
2181
|
+
continue
|
|
2182
|
+
return stmt
|
|
2183
|
+
return None
|
|
2184
|
+
|
|
2185
|
+
@staticmethod
|
|
2186
|
+
def find_last_non_docstring_stmt(stmts: list[TpyStmt]) -> TpyStmt | None:
|
|
2187
|
+
"""Find the last statement, skipping only a leading docstring.
|
|
2188
|
+
|
|
2189
|
+
Only the first statement can be a docstring; trailing string literals
|
|
2190
|
+
are regular statements, not docstrings.
|
|
2191
|
+
"""
|
|
2192
|
+
if not stmts:
|
|
2193
|
+
return None
|
|
2194
|
+
if len(stmts) == 1 and is_docstring(stmts[0]):
|
|
2195
|
+
return None
|
|
2196
|
+
return stmts[-1]
|
|
2197
|
+
|