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/expressions.py
ADDED
|
@@ -0,0 +1,3737 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Expression Analysis
|
|
3
|
+
|
|
4
|
+
Core expression analysis including literals, names, operators, field access, and subscripts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from contextlib import ExitStack
|
|
9
|
+
from collections.abc import Callable as CallableFn
|
|
10
|
+
from dataclasses import replace as dc_replace
|
|
11
|
+
from typing import Literal, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from ..typesys import (
|
|
14
|
+
TpyType, IntLiteralType, FloatLiteralType, RecordInfo,
|
|
15
|
+
NominalType, PtrType, OwnType, make_array, make_dict, make_set, make_span, make_list, span_as_const, span_as_mutable, PendingListType, ListRepeatType, GenExprType, TupleType,
|
|
16
|
+
TypeParamRef, TypeParamKind, ListLiteralInfo, NoneType, AnyType, OptionalType, UnionType, VoidType,
|
|
17
|
+
ReadonlyType, unwrap_readonly, unwrap_qualifiers, is_any_str_type, PendingStrType, PendingViewType,
|
|
18
|
+
is_any_bytes_type, PendingBytesType,
|
|
19
|
+
make_union,
|
|
20
|
+
ResolvedBinop, FunctionInfo, ParamInfo, UnknownElementType, UNKNOWN_ELEMENT,
|
|
21
|
+
PendingDictType, PendingSetType, DictLiteralInfo,
|
|
22
|
+
resolve_int_literals, CallableType, make_fn_type, is_fn_type,
|
|
23
|
+
INT32, FLOAT, STR, FSTR, STRVIEW, CHAR, BOOL, BIGINT, NONE, BASIC_SLICE, SLICE, BYTES, BYTESVIEW, UINT8,
|
|
24
|
+
is_protocol_type, container_to_str_template, contains_type_param,
|
|
25
|
+
PendingGenericInstanceType, unwrap_ref_type, make_ref, RefType,
|
|
26
|
+
is_integer_type, is_any_int_type, is_union_or_optional_type,
|
|
27
|
+
is_callable_type, is_float_type, is_any_float_type, is_numeric_type,
|
|
28
|
+
unwrap_own, is_readonly_span,
|
|
29
|
+
RecursiveAliasInstanceType, recursive_union_alternatives)
|
|
30
|
+
from ..parse import (
|
|
31
|
+
TpyExpr, TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBytesLiteral,
|
|
32
|
+
TpyFStringValue, TpyFString, FSTRING_CONV_REPR, FSTRING_CONV_STR,
|
|
33
|
+
TpyBoolLiteral,
|
|
34
|
+
TpyNoneLiteral, TpyName, TpyBinOp, TpyChainedCompare, TpyUnaryOp, TpyTypeParamConstruct,
|
|
35
|
+
TpyCall, TpyMethodCall, TpyFieldAccess, TpyFunction,
|
|
36
|
+
is_stable_address_lvalue,
|
|
37
|
+
TpyArrayLiteral, TpyTupleLiteral, TpyDictLiteral, TpySetLiteral, TpyListRepeat,
|
|
38
|
+
TpyListComprehension, TpyDictComprehension, TpySetComprehension, TpyGeneratorExpression, TpyComprehensionGenerator,
|
|
39
|
+
TpySlice, TpySubscript, TpyCoerce,
|
|
40
|
+
TpyIfExpr, TpyNamedExpr, TpyAwait,
|
|
41
|
+
TpyLambda, TpyStarUnpack,
|
|
42
|
+
TpyStmt, TpyVarDecl, TpyTupleUnpack, TpyAssign, TpyForEach, TpyWith,
|
|
43
|
+
TpyNestedDef,
|
|
44
|
+
collect_name_refs,
|
|
45
|
+
)
|
|
46
|
+
from .. import qnames
|
|
47
|
+
from ..type_def_registry import (
|
|
48
|
+
is_set, is_dict, is_array, is_span, is_list,
|
|
49
|
+
is_fixed_int_type, is_big_int_type, is_bool_type, is_char_type, is_fstr_type,
|
|
50
|
+
is_basic_slice_type, is_slice_type,
|
|
51
|
+
int_traits_of,
|
|
52
|
+
is_enum_type, is_int_enum_type, enum_info_of,
|
|
53
|
+
find_factory_by_simple_name, protocol_info_of,
|
|
54
|
+
)
|
|
55
|
+
from ..namespace import BindingKind, NameBinding
|
|
56
|
+
from ..coercions import CoercionContext
|
|
57
|
+
from ..prescan import _expr_to_narrowing_key
|
|
58
|
+
from ..diagnostics import SemanticError, OPTIONAL_NONE_ACCESS_WARNING
|
|
59
|
+
from .. import qnames
|
|
60
|
+
from .context import is_body_like_scope
|
|
61
|
+
from .narrowing import NarrowingTracker, deref_view_narrowed
|
|
62
|
+
from .numeric_lattice import widen_numeric_types
|
|
63
|
+
from .list_literals import IterableHelper
|
|
64
|
+
from .local_deduction import collect_pending_source_types
|
|
65
|
+
from .operators import DUNDER_CPP_TEMPLATES, _substitute_type_params
|
|
66
|
+
from .bound_check import raise_if_class_param_bound_violated
|
|
67
|
+
from .overloads import resolve_overload
|
|
68
|
+
|
|
69
|
+
if TYPE_CHECKING:
|
|
70
|
+
from .context import SemanticContext
|
|
71
|
+
from .type_ops import TypeOperations
|
|
72
|
+
from .operators import OperatorResolver
|
|
73
|
+
from .protocols import ProtocolChecker
|
|
74
|
+
from .compatibility import TypeCompatibility
|
|
75
|
+
from .calls import CallAnalyzer
|
|
76
|
+
from .methods import MethodAnalyzer
|
|
77
|
+
from .scope_tracker import ScopeTracker
|
|
78
|
+
|
|
79
|
+
from tpyc import modules as builtin_modules
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _walk_body_stmts(
|
|
83
|
+
stmts: list[TpyStmt],
|
|
84
|
+
on_expr: CallableFn[[TpyExpr], None],
|
|
85
|
+
on_stmt: CallableFn[[TpyStmt], None],
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Walk statements calling on_expr/on_stmt. Does NOT recurse into TpyNestedDef."""
|
|
88
|
+
for stmt in stmts:
|
|
89
|
+
on_stmt(stmt)
|
|
90
|
+
if isinstance(stmt, TpyNestedDef):
|
|
91
|
+
continue # separate scope
|
|
92
|
+
for expr in stmt.exprs():
|
|
93
|
+
on_expr(expr)
|
|
94
|
+
for body in stmt.sub_bodies():
|
|
95
|
+
_walk_body_stmts(body, on_expr, on_stmt)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _collect_body_name_refs(stmts: list[TpyStmt]) -> set[str]:
|
|
99
|
+
"""Collect all name references from a list of statements.
|
|
100
|
+
|
|
101
|
+
Walks all expressions in statements to find free variable references.
|
|
102
|
+
Does NOT recurse into nested function definitions (separate scope).
|
|
103
|
+
"""
|
|
104
|
+
names: set[str] = set()
|
|
105
|
+
|
|
106
|
+
def on_expr(expr: TpyExpr) -> None:
|
|
107
|
+
names.update(collect_name_refs(expr))
|
|
108
|
+
|
|
109
|
+
_walk_body_stmts(stmts, on_expr, lambda s: None)
|
|
110
|
+
return names
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _collect_body_local_defs(stmts: list[TpyStmt]) -> set[str]:
|
|
114
|
+
"""Collect names defined locally in a statement body (not from enclosing scope)."""
|
|
115
|
+
defs: set[str] = set()
|
|
116
|
+
|
|
117
|
+
def on_stmt(stmt: TpyStmt) -> None:
|
|
118
|
+
if isinstance(stmt, TpyVarDecl):
|
|
119
|
+
defs.add(stmt.name)
|
|
120
|
+
elif isinstance(stmt, TpyAssign):
|
|
121
|
+
if isinstance(stmt.target, TpyName):
|
|
122
|
+
defs.add(stmt.target.name)
|
|
123
|
+
elif isinstance(stmt, TpyTupleUnpack):
|
|
124
|
+
for name in stmt.targets:
|
|
125
|
+
if name is not None:
|
|
126
|
+
defs.add(name)
|
|
127
|
+
elif isinstance(stmt, TpyForEach):
|
|
128
|
+
defs.add(stmt.var)
|
|
129
|
+
elif isinstance(stmt, TpyWith):
|
|
130
|
+
for item in stmt.items:
|
|
131
|
+
if item.target is not None:
|
|
132
|
+
defs.add(item.target)
|
|
133
|
+
elif isinstance(stmt, TpyNestedDef):
|
|
134
|
+
defs.add(stmt.func.name)
|
|
135
|
+
|
|
136
|
+
_walk_body_stmts(stmts, lambda e: None, on_stmt)
|
|
137
|
+
return defs
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _union_like_members(ut: TpyType) -> 'tuple[TpyType, ...]':
|
|
141
|
+
"""The variant alternatives of a recursive-union wrapper -- the non-generic
|
|
142
|
+
UnionType's members or a generic instance's alternatives."""
|
|
143
|
+
if isinstance(ut, UnionType):
|
|
144
|
+
return ut.members
|
|
145
|
+
return recursive_union_alternatives(ut) or ()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _find_list_member(ut: TpyType) -> NominalType | None:
|
|
149
|
+
"""Find the list[...] member of a recursive-union wrapper, if any."""
|
|
150
|
+
for m in _union_like_members(ut):
|
|
151
|
+
if is_list(m):
|
|
152
|
+
return m
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _find_dict_member(ut: TpyType) -> NominalType | None:
|
|
157
|
+
"""Find the dict[...] member of a recursive-union wrapper, if any."""
|
|
158
|
+
for m in _union_like_members(ut):
|
|
159
|
+
if is_dict(m):
|
|
160
|
+
return m
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ExpressionAnalyzer:
|
|
165
|
+
"""Core expression analysis."""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
ctx: SemanticContext,
|
|
170
|
+
type_ops: TypeOperations,
|
|
171
|
+
operators: OperatorResolver,
|
|
172
|
+
protocols: ProtocolChecker,
|
|
173
|
+
compat: TypeCompatibility,
|
|
174
|
+
narrowing: NarrowingTracker,
|
|
175
|
+
calls: CallAnalyzer,
|
|
176
|
+
methods: MethodAnalyzer,
|
|
177
|
+
):
|
|
178
|
+
self.ctx = ctx
|
|
179
|
+
self.type_ops = type_ops
|
|
180
|
+
self.operators = operators
|
|
181
|
+
self.protocols = protocols
|
|
182
|
+
self.compat = compat
|
|
183
|
+
self.narrowing = narrowing
|
|
184
|
+
self.calls = calls
|
|
185
|
+
self.methods = methods
|
|
186
|
+
self.scopes: ScopeTracker | None = None
|
|
187
|
+
|
|
188
|
+
def set_scopes(self, scopes: ScopeTracker) -> None:
|
|
189
|
+
"""Set scope tracker (available once StatementAnalyzer is created)."""
|
|
190
|
+
self.scopes = scopes
|
|
191
|
+
|
|
192
|
+
def _resolve_literal_type(self, t: TpyType) -> TpyType:
|
|
193
|
+
"""Replace IntLiteralType/FloatLiteralType with concrete types for display."""
|
|
194
|
+
if isinstance(t, IntLiteralType):
|
|
195
|
+
return self.ctx.default_int_type
|
|
196
|
+
if isinstance(t, FloatLiteralType):
|
|
197
|
+
return FLOAT
|
|
198
|
+
if isinstance(t, TupleType):
|
|
199
|
+
resolved = tuple(self._resolve_literal_type(e) for e in t.element_types)
|
|
200
|
+
if resolved != t.element_types:
|
|
201
|
+
return TupleType(resolved)
|
|
202
|
+
if isinstance(t, PendingListType):
|
|
203
|
+
resolved_elem = self._resolve_literal_type(t.element_type)
|
|
204
|
+
if resolved_elem is not t.element_type:
|
|
205
|
+
return PendingListType(resolved_elem, t.size, t.literal_id)
|
|
206
|
+
return t
|
|
207
|
+
|
|
208
|
+
def _user_type_name(self, t: TpyType) -> str:
|
|
209
|
+
"""User-facing type name for error messages (resolves literal types)."""
|
|
210
|
+
return str(self._resolve_literal_type(t))
|
|
211
|
+
|
|
212
|
+
def analyze_call_arg(self, arg: TpyExpr, hint: 'TpyType | None' = None) -> TpyType:
|
|
213
|
+
"""Analyze one call argument, transparently handling `*xs` unpack.
|
|
214
|
+
|
|
215
|
+
Every call-argument pre-analyzer (generic inference, overload
|
|
216
|
+
probing, vararg packing -- in both `calls.py` and `methods.py`)
|
|
217
|
+
must route args through here rather than `analyze_expr` directly:
|
|
218
|
+
a `TpyStarUnpack` carries no expression-type semantics of its own,
|
|
219
|
+
so feeding it to the structural analyzer hits the catch-all
|
|
220
|
+
"Unknown expression type" error. For a `*container` arg this
|
|
221
|
+
returns the container's element type (the value an individual
|
|
222
|
+
positional arg would have contributed); for everything else it
|
|
223
|
+
is `analyze_expr[_with_hint]`.
|
|
224
|
+
"""
|
|
225
|
+
if isinstance(arg, TpyStarUnpack):
|
|
226
|
+
return self._unpack_star_element_type(arg, hint)
|
|
227
|
+
if hint is not None:
|
|
228
|
+
return self.analyze_expr_with_hint(arg, hint)
|
|
229
|
+
return self.analyze_expr(arg)
|
|
230
|
+
|
|
231
|
+
def _unpack_star_element_type(
|
|
232
|
+
self, node: TpyStarUnpack, elem_hint: 'TpyType | None'
|
|
233
|
+
) -> TpyType:
|
|
234
|
+
"""Element type of the container unpacked by `*node.expr`.
|
|
235
|
+
|
|
236
|
+
`elem_hint`, when present, is the vararg parameter's element type;
|
|
237
|
+
we lift it to `list[elem_hint]` so the inner container literal can
|
|
238
|
+
resolve its own element type from context (mirrors the seeded-hint
|
|
239
|
+
path other args get).
|
|
240
|
+
|
|
241
|
+
Only directly-iterable lvalue containers (list / span / array) are
|
|
242
|
+
accepted -- the same set the vararg-pack codegen can lower via
|
|
243
|
+
`as_mut_span`. A reference-type container *parameter* (e.g.
|
|
244
|
+
`xs: list[T]`) carries a `Ref[...]` wrapper from by-reference passing;
|
|
245
|
+
that is just the borrow form codegen already emits, so unwrap it.
|
|
246
|
+
Owning-rvalue (`Own[list[...]]`) and other wrapped shapes are
|
|
247
|
+
deliberately NOT unwrapped: sema must not accept a shape codegen can't
|
|
248
|
+
emit (the owning-rvalue unpack gap is tracked in TODO.md).
|
|
249
|
+
"""
|
|
250
|
+
inner_hint = make_list(elem_hint) if elem_hint is not None else None
|
|
251
|
+
if inner_hint is not None:
|
|
252
|
+
inner_type = self.analyze_expr_with_hint(node.expr, inner_hint)
|
|
253
|
+
else:
|
|
254
|
+
inner_type = self.analyze_expr(node.expr)
|
|
255
|
+
inner_type = unwrap_ref_type(inner_type)
|
|
256
|
+
elem: 'TpyType | None' = None
|
|
257
|
+
if is_array(inner_type) or is_span(inner_type) or is_list(inner_type):
|
|
258
|
+
elem = inner_type.get_element_type()
|
|
259
|
+
elif isinstance(inner_type, PendingListType):
|
|
260
|
+
elem = inner_type.element_type
|
|
261
|
+
if elem is None:
|
|
262
|
+
raise self.ctx.error(
|
|
263
|
+
f"Cannot unpack type '{self._user_type_name(inner_type)}' "
|
|
264
|
+
f"into *args", node)
|
|
265
|
+
return elem
|
|
266
|
+
|
|
267
|
+
def analyze_expr(self, expr: TpyExpr) -> TpyType:
|
|
268
|
+
"""Analyze an expression and return its type."""
|
|
269
|
+
if isinstance(expr, TpyIntLiteral):
|
|
270
|
+
typ = IntLiteralType(expr.value)
|
|
271
|
+
elif isinstance(expr, TpyFloatLiteral):
|
|
272
|
+
typ = FloatLiteralType(expr.value)
|
|
273
|
+
elif isinstance(expr, TpyStrLiteral):
|
|
274
|
+
# String literals are always str type (including single-char)
|
|
275
|
+
# Char type is only used when explicitly annotated or from string indexing
|
|
276
|
+
typ = STR
|
|
277
|
+
elif isinstance(expr, TpyBytesLiteral):
|
|
278
|
+
typ = BYTES
|
|
279
|
+
elif isinstance(expr, TpyBoolLiteral):
|
|
280
|
+
typ = BOOL
|
|
281
|
+
elif isinstance(expr, TpyNoneLiteral):
|
|
282
|
+
typ = NONE
|
|
283
|
+
elif isinstance(expr, TpyName):
|
|
284
|
+
typ = self._analyze_name(expr)
|
|
285
|
+
elif isinstance(expr, TpyBinOp):
|
|
286
|
+
typ = self._analyze_binop(expr)
|
|
287
|
+
elif isinstance(expr, TpyChainedCompare):
|
|
288
|
+
typ = self._analyze_chained_compare(expr)
|
|
289
|
+
elif isinstance(expr, TpyUnaryOp):
|
|
290
|
+
typ = self._analyze_unaryop(expr)
|
|
291
|
+
elif isinstance(expr, TpyCall):
|
|
292
|
+
typ = self.calls.analyze_call(expr)
|
|
293
|
+
self.narrowing.invalidate_field_facts_for_call(expr)
|
|
294
|
+
elif isinstance(expr, TpyMethodCall):
|
|
295
|
+
typ = self.methods.analyze_method_call(expr)
|
|
296
|
+
self.narrowing.invalidate_field_facts_for_method_call(expr)
|
|
297
|
+
elif isinstance(expr, TpyFieldAccess):
|
|
298
|
+
typ = self._analyze_field_access(expr)
|
|
299
|
+
elif isinstance(expr, TpyArrayLiteral):
|
|
300
|
+
typ = self._analyze_array_literal(expr)
|
|
301
|
+
elif isinstance(expr, TpyTupleLiteral):
|
|
302
|
+
typ = self._analyze_tuple_literal(expr)
|
|
303
|
+
elif isinstance(expr, TpyDictLiteral):
|
|
304
|
+
typ = self._analyze_dict_literal(expr)
|
|
305
|
+
elif isinstance(expr, TpySetLiteral):
|
|
306
|
+
typ = self._analyze_set_literal(expr)
|
|
307
|
+
elif isinstance(expr, TpyListRepeat):
|
|
308
|
+
typ = self._analyze_list_repeat(expr)
|
|
309
|
+
elif isinstance(expr, TpyListComprehension):
|
|
310
|
+
typ = self._analyze_list_comprehension(expr)
|
|
311
|
+
elif isinstance(expr, TpyDictComprehension):
|
|
312
|
+
typ = self._analyze_dict_comprehension(expr)
|
|
313
|
+
elif isinstance(expr, TpySetComprehension):
|
|
314
|
+
typ = self._analyze_set_comprehension(expr)
|
|
315
|
+
elif isinstance(expr, TpyGeneratorExpression):
|
|
316
|
+
typ = self._analyze_generator_expression(expr)
|
|
317
|
+
elif isinstance(expr, TpySubscript):
|
|
318
|
+
typ = self._analyze_subscript(expr)
|
|
319
|
+
elif isinstance(expr, TpyFString):
|
|
320
|
+
typ = self._analyze_fstring(expr)
|
|
321
|
+
elif isinstance(expr, TpyTypeParamConstruct):
|
|
322
|
+
self.ctx.warning(
|
|
323
|
+
"T() default-construction syntax is not supported in CPython; "
|
|
324
|
+
"use make_default() from tpy instead", expr)
|
|
325
|
+
typ = TypeParamRef(expr.param_name)
|
|
326
|
+
elif isinstance(expr, TpyIfExpr):
|
|
327
|
+
typ = self._analyze_if_expr(expr)
|
|
328
|
+
elif isinstance(expr, TpyNamedExpr):
|
|
329
|
+
typ = self._analyze_named_expr(expr)
|
|
330
|
+
elif isinstance(expr, TpyAwait):
|
|
331
|
+
typ = self._analyze_await(expr)
|
|
332
|
+
elif isinstance(expr, TpyLambda):
|
|
333
|
+
typ = self._analyze_lambda(expr)
|
|
334
|
+
elif isinstance(expr, TpyCoerce):
|
|
335
|
+
# Coercions are attached post-analysis; treat as the expected type.
|
|
336
|
+
typ = expr.expected_type
|
|
337
|
+
elif isinstance(expr, TpyStarUnpack):
|
|
338
|
+
# `*xs` is only meaningful at a variadic-accepting call position,
|
|
339
|
+
# where the handler routes it through `analyze_call_arg` (element
|
|
340
|
+
# extraction) instead of here. Reaching the structural analyzer
|
|
341
|
+
# means the surrounding call target does not accept *args -- reject
|
|
342
|
+
# cleanly rather than falling through to the "Unknown expression
|
|
343
|
+
# type" catch-all (or silently mis-typing the unpack as one arg).
|
|
344
|
+
raise self.ctx.error(
|
|
345
|
+
"Cannot use *unpacking here: the call target does not accept "
|
|
346
|
+
"*args (only a variadic parameter can receive `*iterable`)",
|
|
347
|
+
expr)
|
|
348
|
+
else:
|
|
349
|
+
raise self.ctx.error(f"Unknown expression type: {type(expr).__name__}", expr)
|
|
350
|
+
|
|
351
|
+
self.ctx.set_expr_type(expr, typ)
|
|
352
|
+
return typ
|
|
353
|
+
|
|
354
|
+
def analyze_expr_with_hint(self, expr: TpyExpr, type_hint: TpyType | None) -> TpyType:
|
|
355
|
+
"""Analyze an expression with an optional type hint for inference.
|
|
356
|
+
|
|
357
|
+
The type hint allows constructs like list() to infer their type parameters
|
|
358
|
+
from context (e.g., function parameter type).
|
|
359
|
+
"""
|
|
360
|
+
if type_hint is None:
|
|
361
|
+
return self.analyze_expr(expr)
|
|
362
|
+
|
|
363
|
+
type_hint = unwrap_ref_type(type_hint)
|
|
364
|
+
|
|
365
|
+
# Lambda with Fn/Callable type hint: infer param types from the hint
|
|
366
|
+
if isinstance(expr, TpyLambda) and is_callable_type(type_hint):
|
|
367
|
+
typ = self._analyze_lambda_with_fn_hint(expr, type_hint)
|
|
368
|
+
self.ctx.set_expr_type(expr, typ)
|
|
369
|
+
return typ
|
|
370
|
+
|
|
371
|
+
# Named function reference with Fn/Callable hint: resolve as function value.
|
|
372
|
+
# Unwrap OwnType so that e.g. list.append(Own[Callable[...]]) works.
|
|
373
|
+
fn_hint = type_hint.wrapped if isinstance(type_hint, OwnType) else type_hint
|
|
374
|
+
if isinstance(expr, TpyName) and is_callable_type(fn_hint):
|
|
375
|
+
result = self._try_resolve_function_ref(expr, fn_hint)
|
|
376
|
+
if result is not None:
|
|
377
|
+
self.ctx.set_expr_type(expr, result)
|
|
378
|
+
return result
|
|
379
|
+
|
|
380
|
+
# Ternary expression: propagate hint to both branches
|
|
381
|
+
if isinstance(expr, TpyIfExpr):
|
|
382
|
+
typ = self._analyze_if_expr(expr, type_hint=type_hint)
|
|
383
|
+
self.ctx.set_expr_type(expr, typ)
|
|
384
|
+
return typ
|
|
385
|
+
|
|
386
|
+
# F-string with FStr hint: keep decomposed for macro consumption.
|
|
387
|
+
# Skip format-validity checks -- the call macro handles per-type dispatch.
|
|
388
|
+
if isinstance(expr, TpyFString) and is_fstr_type(type_hint):
|
|
389
|
+
self._analyze_fstring(expr, for_fstr=True)
|
|
390
|
+
self.ctx.set_expr_type(expr, FSTR)
|
|
391
|
+
return FSTR
|
|
392
|
+
|
|
393
|
+
# T() default-construction: resolves to whatever T maps to
|
|
394
|
+
if isinstance(expr, TpyTypeParamConstruct):
|
|
395
|
+
self.ctx.warning(
|
|
396
|
+
"T() default-construction syntax is not supported in CPython; "
|
|
397
|
+
"use make_default() from tpy instead", expr)
|
|
398
|
+
self.ctx.set_expr_type(expr, type_hint)
|
|
399
|
+
return type_hint
|
|
400
|
+
|
|
401
|
+
# Tuple literal with TupleType hint: pass per-element hints
|
|
402
|
+
if isinstance(expr, TpyTupleLiteral) and isinstance(type_hint, TupleType):
|
|
403
|
+
if len(expr.elements) == len(type_hint.element_types):
|
|
404
|
+
hints = list(type_hint.element_types)
|
|
405
|
+
typ = self._analyze_tuple_literal(expr, element_hints=hints)
|
|
406
|
+
self.ctx.set_expr_type(expr, typ)
|
|
407
|
+
return typ
|
|
408
|
+
|
|
409
|
+
# Check for generic type constructor (list(), Container[T](), etc.)
|
|
410
|
+
_generic_td = (find_factory_by_simple_name(expr.func_name)
|
|
411
|
+
if isinstance(expr, TpyCall) and isinstance(expr.func, TpyName) else None)
|
|
412
|
+
is_generic_constructor = (isinstance(expr, TpyCall) and
|
|
413
|
+
not expr.args and
|
|
414
|
+
expr.call_type is None and
|
|
415
|
+
_generic_td is not None and
|
|
416
|
+
bool(_generic_td.param_kinds))
|
|
417
|
+
|
|
418
|
+
# Check for empty list literal []
|
|
419
|
+
is_empty_literal = isinstance(expr, TpyArrayLiteral) and not expr.elements
|
|
420
|
+
|
|
421
|
+
if is_generic_constructor or is_empty_literal:
|
|
422
|
+
# Unwrap ReadonlyType/OwnType so hints like Own[list[T]] work
|
|
423
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
424
|
+
if isinstance(inner_hint, OwnType):
|
|
425
|
+
inner_hint = inner_hint.wrapped
|
|
426
|
+
# Check if type_hint matches the constructor's generic type
|
|
427
|
+
hint_matches = False
|
|
428
|
+
if is_generic_constructor:
|
|
429
|
+
td = find_factory_by_simple_name(expr.func_name) # type: ignore
|
|
430
|
+
hint_matches = (td is not None and
|
|
431
|
+
inner_hint.qualified_name() == td.qname)
|
|
432
|
+
else:
|
|
433
|
+
# Empty literal [] can match list[T] hint
|
|
434
|
+
hint_matches = is_list(inner_hint)
|
|
435
|
+
|
|
436
|
+
if hint_matches:
|
|
437
|
+
if is_list(inner_hint):
|
|
438
|
+
# list[T]: Use PendingListType for potential Array optimization
|
|
439
|
+
elem_type = inner_hint.get_element_type()
|
|
440
|
+
# Set call_type so codegen generates explicit type (e.g., std::vector<int>())
|
|
441
|
+
if is_generic_constructor:
|
|
442
|
+
expr.call_type = inner_hint # type: ignore
|
|
443
|
+
if self.ctx.func.current_function is None:
|
|
444
|
+
typ = make_list(elem_type)
|
|
445
|
+
else:
|
|
446
|
+
literal_id = self.ctx.literal_counter
|
|
447
|
+
self.ctx.literal_counter += 1
|
|
448
|
+
info = ListLiteralInfo(
|
|
449
|
+
literal_id=literal_id,
|
|
450
|
+
expr=expr,
|
|
451
|
+
element_type=elem_type,
|
|
452
|
+
size=0,
|
|
453
|
+
is_global=self.ctx.is_top_level,
|
|
454
|
+
has_explicit_annotation=True,
|
|
455
|
+
explicit_type=inner_hint
|
|
456
|
+
)
|
|
457
|
+
self.ctx.list_literals[literal_id] = info
|
|
458
|
+
self.ctx.func.pending_resolutions.append(literal_id)
|
|
459
|
+
typ = PendingListType(elem_type, 0, literal_id)
|
|
460
|
+
self.ctx.set_expr_type(expr, typ)
|
|
461
|
+
return typ
|
|
462
|
+
else:
|
|
463
|
+
# Other generic types (Array, etc.): use hint directly
|
|
464
|
+
# Set call_type so codegen knows the concrete template type
|
|
465
|
+
if is_generic_constructor:
|
|
466
|
+
expr.call_type = inner_hint # type: ignore
|
|
467
|
+
self.ctx.set_expr_type(expr, inner_hint)
|
|
468
|
+
return inner_hint
|
|
469
|
+
|
|
470
|
+
# Non-empty array literal with list type hint
|
|
471
|
+
# (e.g. return [x, y] with -> Own[list[T]], or x: list[Int32|None] = [1, None])
|
|
472
|
+
if isinstance(expr, TpyArrayLiteral) and expr.elements:
|
|
473
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
474
|
+
if isinstance(inner_hint, OwnType):
|
|
475
|
+
inner_hint = inner_hint.wrapped
|
|
476
|
+
if is_array(inner_hint) or is_list(inner_hint):
|
|
477
|
+
result = self._analyze_array_literal(expr, inner_hint.get_element_type())
|
|
478
|
+
if isinstance(result, PendingListType):
|
|
479
|
+
info = self.ctx.list_literals.get(result.literal_id)
|
|
480
|
+
if info:
|
|
481
|
+
info.has_explicit_annotation = True
|
|
482
|
+
info.explicit_type = inner_hint
|
|
483
|
+
if info.coerced_element_type is None:
|
|
484
|
+
info.coerced_element_type = inner_hint.get_element_type()
|
|
485
|
+
self.ctx.set_expr_type(expr, result)
|
|
486
|
+
return result
|
|
487
|
+
# Recursive union type with a list member: propagate the union
|
|
488
|
+
# as element hint so nested list literals infer as list[Tree]
|
|
489
|
+
# (e.g. x: Tree = [1, [3, 4]] where type Tree = int | list[Tree])
|
|
490
|
+
if inner_hint.needs_wrapper():
|
|
491
|
+
list_member = _find_list_member(inner_hint)
|
|
492
|
+
if list_member is not None:
|
|
493
|
+
result = self._analyze_array_literal(expr, inner_hint)
|
|
494
|
+
if isinstance(result, PendingListType):
|
|
495
|
+
info = self.ctx.list_literals.get(result.literal_id)
|
|
496
|
+
if info:
|
|
497
|
+
info.has_explicit_annotation = True
|
|
498
|
+
info.explicit_type = list_member
|
|
499
|
+
if info.coerced_element_type is None:
|
|
500
|
+
info.coerced_element_type = inner_hint
|
|
501
|
+
self.ctx.set_expr_type(expr, result)
|
|
502
|
+
return result
|
|
503
|
+
|
|
504
|
+
# List comprehension with list type hint: propagate element type
|
|
505
|
+
if isinstance(expr, TpyListComprehension):
|
|
506
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
507
|
+
if isinstance(inner_hint, OwnType):
|
|
508
|
+
inner_hint = inner_hint.wrapped
|
|
509
|
+
if is_list(inner_hint):
|
|
510
|
+
typ = self._analyze_list_comprehension(expr, expected_elem=inner_hint.get_element_type())
|
|
511
|
+
if isinstance(typ, PendingListType):
|
|
512
|
+
info = self.ctx.list_literals.get(typ.literal_id)
|
|
513
|
+
if info:
|
|
514
|
+
info.has_explicit_annotation = True
|
|
515
|
+
info.explicit_type = inner_hint
|
|
516
|
+
self.ctx.set_expr_type(expr, typ)
|
|
517
|
+
return typ
|
|
518
|
+
if is_array(inner_hint):
|
|
519
|
+
typ = self._analyze_list_comprehension(expr, expected_elem=inner_hint.get_element_type())
|
|
520
|
+
if isinstance(typ, PendingListType):
|
|
521
|
+
info = self.ctx.list_literals.get(typ.literal_id)
|
|
522
|
+
if info:
|
|
523
|
+
info.has_explicit_annotation = True
|
|
524
|
+
info.explicit_type = inner_hint
|
|
525
|
+
self.ctx.set_expr_type(expr, typ)
|
|
526
|
+
return typ
|
|
527
|
+
|
|
528
|
+
# Dict comprehension with dict type hint: propagate key/value types
|
|
529
|
+
if isinstance(expr, TpyDictComprehension):
|
|
530
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
531
|
+
if isinstance(inner_hint, OwnType):
|
|
532
|
+
inner_hint = inner_hint.wrapped
|
|
533
|
+
if is_dict(inner_hint):
|
|
534
|
+
typ = self._analyze_dict_comprehension(
|
|
535
|
+
expr, expected_key=inner_hint.type_args[0],
|
|
536
|
+
expected_value=inner_hint.type_args[1])
|
|
537
|
+
self.ctx.set_expr_type(expr, typ)
|
|
538
|
+
return typ
|
|
539
|
+
|
|
540
|
+
# Dict literal with dict type hint
|
|
541
|
+
if isinstance(expr, TpyDictLiteral):
|
|
542
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
543
|
+
if isinstance(inner_hint, OwnType):
|
|
544
|
+
inner_hint = inner_hint.wrapped
|
|
545
|
+
if is_dict(inner_hint):
|
|
546
|
+
result = self._analyze_dict_literal(expr, inner_hint.type_args[0], inner_hint.type_args[1])
|
|
547
|
+
self.ctx.set_expr_type(expr, result)
|
|
548
|
+
return result
|
|
549
|
+
if inner_hint.needs_wrapper():
|
|
550
|
+
dict_member = _find_dict_member(inner_hint)
|
|
551
|
+
if dict_member is not None:
|
|
552
|
+
result = self._analyze_dict_literal(expr, dict_member.type_args[0], inner_hint)
|
|
553
|
+
self.ctx.set_expr_type(expr, result)
|
|
554
|
+
return result
|
|
555
|
+
|
|
556
|
+
# Set comprehension with set type hint: propagate element type
|
|
557
|
+
if isinstance(expr, TpySetComprehension):
|
|
558
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
559
|
+
if isinstance(inner_hint, OwnType):
|
|
560
|
+
inner_hint = inner_hint.wrapped
|
|
561
|
+
if is_set(inner_hint):
|
|
562
|
+
typ = self._analyze_set_comprehension(
|
|
563
|
+
expr, expected_elem=inner_hint.type_args[0])
|
|
564
|
+
self.ctx.set_expr_type(expr, typ)
|
|
565
|
+
return typ
|
|
566
|
+
|
|
567
|
+
# Non-empty set literal with set type hint
|
|
568
|
+
if isinstance(expr, TpySetLiteral) and expr.elements:
|
|
569
|
+
inner_hint = unwrap_readonly(type_hint)
|
|
570
|
+
if isinstance(inner_hint, OwnType):
|
|
571
|
+
inner_hint = inner_hint.wrapped
|
|
572
|
+
if is_set(inner_hint):
|
|
573
|
+
result = self._analyze_set_literal(expr, inner_hint.type_args[0])
|
|
574
|
+
self.ctx.set_expr_type(expr, result)
|
|
575
|
+
return result
|
|
576
|
+
|
|
577
|
+
# Fall back to regular analysis, propagating hint through context
|
|
578
|
+
# for functions that need it (e.g. unsafe_cast)
|
|
579
|
+
old_hint = self.ctx.expr_type_hint
|
|
580
|
+
self.ctx.expr_type_hint = type_hint
|
|
581
|
+
try:
|
|
582
|
+
return self.analyze_expr(expr)
|
|
583
|
+
finally:
|
|
584
|
+
self.ctx.expr_type_hint = old_hint
|
|
585
|
+
|
|
586
|
+
def _analyze_name(self, expr: TpyName) -> TpyType:
|
|
587
|
+
"""Analyze a name reference."""
|
|
588
|
+
# Use-after-consume: variable was consumed by a consuming method call
|
|
589
|
+
if expr.name in self.ctx.func.consumed_vars:
|
|
590
|
+
raise self.ctx.error(
|
|
591
|
+
f"Cannot use '{expr.name}' after it was consumed by a consuming method call",
|
|
592
|
+
expr,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Check for INT type parameter references in generic class context
|
|
596
|
+
# INT type params can be used as values in expressions (e.g., Int32(N))
|
|
597
|
+
if self.ctx.record_ctx.type_params and self.ctx.record_ctx.type_param_kinds:
|
|
598
|
+
try:
|
|
599
|
+
idx = self.ctx.record_ctx.type_params.index(expr.name)
|
|
600
|
+
if self.ctx.record_ctx.type_param_kinds[idx] == TypeParamKind.INT:
|
|
601
|
+
# INT type param - return TypeParamRef with INT kind
|
|
602
|
+
# This represents a compile-time constant, treated as Int32-compatible
|
|
603
|
+
return TypeParamRef(expr.name, kind=TypeParamKind.INT)
|
|
604
|
+
except ValueError:
|
|
605
|
+
pass # Not a type parameter
|
|
606
|
+
|
|
607
|
+
# Use namespace for unified lookup (includes builtins)
|
|
608
|
+
if self.ctx.func.current_ns:
|
|
609
|
+
binding = self.ctx.func.current_ns.lookup(expr.name)
|
|
610
|
+
if binding:
|
|
611
|
+
if binding.kind == BindingKind.VARIABLE:
|
|
612
|
+
self._check_definitely_assigned(expr)
|
|
613
|
+
result = self.narrowing.narrow_name_type(expr.name, binding.type)
|
|
614
|
+
return self._apply_own_wrapper(expr, result)
|
|
615
|
+
if binding.kind == BindingKind.BUILTIN:
|
|
616
|
+
return binding.type
|
|
617
|
+
# For other bindings (FUNCTION, RECORD, MODULE, IMPORTED_NAME),
|
|
618
|
+
# the name exists but isn't usable as a variable
|
|
619
|
+
raise self.ctx.error(f"'{expr.name}' is not a variable", expr)
|
|
620
|
+
|
|
621
|
+
# Migration bridge: scope may contain names not yet in namespace
|
|
622
|
+
# (e.g., during incremental namespace adoption). Remove once all
|
|
623
|
+
# name registration flows go through Namespace.
|
|
624
|
+
typ = self.ctx.func.current_scope.lookup(expr.name)
|
|
625
|
+
if typ is None:
|
|
626
|
+
# Check built-in names (like __name__)
|
|
627
|
+
if expr.name in self.ctx.builtin_names:
|
|
628
|
+
return self.ctx.builtin_names[expr.name]
|
|
629
|
+
# Lazy promotion of loop-scoped variables referenced after the loop
|
|
630
|
+
if self._promote_pending_loop_var(expr.name):
|
|
631
|
+
typ = self.ctx.func.current_scope.lookup(expr.name)
|
|
632
|
+
else:
|
|
633
|
+
raise self.ctx.error(f"Undefined variable: '{expr.name}'", expr)
|
|
634
|
+
self._check_definitely_assigned(expr)
|
|
635
|
+
result = self.narrowing.narrow_name_type(expr.name, typ)
|
|
636
|
+
return self._apply_own_wrapper(expr, result)
|
|
637
|
+
|
|
638
|
+
def _apply_own_wrapper(self, expr: TpyName, result: TpyType) -> TpyType:
|
|
639
|
+
"""Derive OwnType wrapping at use time for local variable names.
|
|
640
|
+
|
|
641
|
+
Params keep their FunctionInfo type (Ref/Own is a C++ contract).
|
|
642
|
+
Owned locals (rvalue-init, not ref-returning) get OwnType at
|
|
643
|
+
non-last-use, bare T at last-use (auto-move).
|
|
644
|
+
"""
|
|
645
|
+
name = expr.name
|
|
646
|
+
# Params: FunctionInfo type already carries Ref/Own.
|
|
647
|
+
# Reassigned params fall through to local derivation.
|
|
648
|
+
if name in self.ctx.func.current_param_names:
|
|
649
|
+
if name not in self.ctx.func.current_reassigned_vars:
|
|
650
|
+
# Strip Own at last-use for auto-move (same as before)
|
|
651
|
+
if isinstance(result, OwnType) and id(expr) in self.ctx.all_last_uses:
|
|
652
|
+
return result.wrapped
|
|
653
|
+
return result
|
|
654
|
+
# Locals: derive OwnType from owned_locals set.
|
|
655
|
+
# Scope stores bare T; wrap when owned and not at last-use.
|
|
656
|
+
if (not result.is_value_type()
|
|
657
|
+
and not isinstance(result, (OwnType, RefType, NoneType,
|
|
658
|
+
PendingListType, PendingDictType, PendingSetType,
|
|
659
|
+
PendingViewType, PendingGenericInstanceType))
|
|
660
|
+
and name in self.ctx.func.owned_locals
|
|
661
|
+
and name not in self.ctx.func.hoisted_vars
|
|
662
|
+
and name not in self.ctx.func.loop_vars):
|
|
663
|
+
if id(expr) not in self.ctx.all_last_uses:
|
|
664
|
+
return OwnType(result)
|
|
665
|
+
return result
|
|
666
|
+
|
|
667
|
+
def _check_definitely_assigned(self, expr: TpyName) -> None:
|
|
668
|
+
"""Check that a local variable is definitely assigned before use."""
|
|
669
|
+
if (not self.ctx.func.init_terminated
|
|
670
|
+
and expr.name in self.ctx.func.var_scope_depth
|
|
671
|
+
and self.ctx.func.var_scope_depth[expr.name] >= 1
|
|
672
|
+
and expr.name not in self.ctx.func.definitely_assigned):
|
|
673
|
+
raise self.ctx.error(
|
|
674
|
+
f"variable '{expr.name}' may not be assigned at this point", expr)
|
|
675
|
+
|
|
676
|
+
def _promote_pending_loop_var(self, name: str) -> bool:
|
|
677
|
+
"""Promote a pending for-loop-scoped variable if present.
|
|
678
|
+
|
|
679
|
+
Returns True if the variable was promoted (added to scope and
|
|
680
|
+
definitely_assigned, registered for codegen pre-declaration).
|
|
681
|
+
"""
|
|
682
|
+
pending = self.ctx.func.pending_loop_vars.pop(name, None)
|
|
683
|
+
if pending is None:
|
|
684
|
+
return False
|
|
685
|
+
var_type, loop_stmt, orig_stmt = pending
|
|
686
|
+
self.ctx.func.current_scope.define(name, var_type)
|
|
687
|
+
self.ctx.func.definitely_assigned.add(name)
|
|
688
|
+
# Register for codegen pre-declaration
|
|
689
|
+
decls = self.ctx.if_branch_decls.setdefault(id(loop_stmt), {})
|
|
690
|
+
decls[name] = var_type
|
|
691
|
+
# Mark the original for-loop's var for hoisted codegen (hidden counter)
|
|
692
|
+
if isinstance(orig_stmt, TpyForEach) and name == orig_stmt.var:
|
|
693
|
+
orig_stmt.hoist_loop_var = True
|
|
694
|
+
return True
|
|
695
|
+
|
|
696
|
+
def _normalize_pending_container(self, t: TpyType) -> TpyType:
|
|
697
|
+
"""Normalize a pending container type to a concrete type with resolved IntLiteralType elements.
|
|
698
|
+
|
|
699
|
+
Used in or/and/ternary type comparison: two PendingListType literals with the same
|
|
700
|
+
element type but different IDs (or different IntLiteralType values) are compatible.
|
|
701
|
+
"""
|
|
702
|
+
if isinstance(t, PendingListType):
|
|
703
|
+
return resolve_int_literals(make_list(t.element_type), self.ctx.default_int_for_literal)
|
|
704
|
+
if isinstance(t, PendingDictType):
|
|
705
|
+
k = self.ctx.default_int_for_literal(t.key_type) if isinstance(t.key_type, IntLiteralType) else t.key_type
|
|
706
|
+
k = FLOAT if isinstance(k, FloatLiteralType) else k
|
|
707
|
+
v = self.ctx.default_int_for_literal(t.value_type) if isinstance(t.value_type, IntLiteralType) else t.value_type
|
|
708
|
+
v = FLOAT if isinstance(v, FloatLiteralType) else v
|
|
709
|
+
return make_dict(k, v)
|
|
710
|
+
if isinstance(t, PendingSetType):
|
|
711
|
+
elem = self.ctx.default_int_for_literal(t.element_type) if isinstance(t.element_type, IntLiteralType) else t.element_type
|
|
712
|
+
elem = FLOAT if isinstance(elem, FloatLiteralType) else elem
|
|
713
|
+
return make_set(elem)
|
|
714
|
+
return t
|
|
715
|
+
|
|
716
|
+
def _logical_op_result_type(self, left: TpyType, right: TpyType) -> TpyType:
|
|
717
|
+
"""Determine result type for and/or operators.
|
|
718
|
+
|
|
719
|
+
Python semantics: `x and y` returns an operand, not bool.
|
|
720
|
+
Same non-bool type -> return that type (enables value-context usage).
|
|
721
|
+
Different types or bool operands -> return bool.
|
|
722
|
+
"""
|
|
723
|
+
# Bool operands: C++ &&/|| already correct
|
|
724
|
+
if is_bool_type(left) or is_bool_type(right):
|
|
725
|
+
return BOOL
|
|
726
|
+
# Resolve int literals to match concrete int type on the other side
|
|
727
|
+
if isinstance(left, IntLiteralType):
|
|
728
|
+
if is_integer_type(right):
|
|
729
|
+
left = right
|
|
730
|
+
elif isinstance(right, IntLiteralType):
|
|
731
|
+
return self.ctx.default_int_type
|
|
732
|
+
else:
|
|
733
|
+
return BOOL
|
|
734
|
+
elif isinstance(right, IntLiteralType):
|
|
735
|
+
if is_integer_type(left):
|
|
736
|
+
right = left
|
|
737
|
+
else:
|
|
738
|
+
return BOOL
|
|
739
|
+
# Normalize PendingViewType to its owned type for comparison; preserve
|
|
740
|
+
# the pending type when both sides are pending so view deduction can
|
|
741
|
+
# chain the result variable back to the operands' resolution.
|
|
742
|
+
if isinstance(left, PendingViewType) and isinstance(right, PendingViewType) and left.family is right.family:
|
|
743
|
+
return left
|
|
744
|
+
if isinstance(left, PendingViewType):
|
|
745
|
+
left = left.family.owned_type
|
|
746
|
+
if isinstance(right, PendingViewType):
|
|
747
|
+
right = right.family.owned_type
|
|
748
|
+
# Normalize pending container types to concrete types for equality comparison.
|
|
749
|
+
# Two PendingListType literals with the same element type (but different IDs
|
|
750
|
+
# or different IntLiteralType values like 1 vs 3) are compatible.
|
|
751
|
+
# Caller marks source literals via collect_pending_source_types.
|
|
752
|
+
left = self._normalize_pending_container(left)
|
|
753
|
+
right = self._normalize_pending_container(right)
|
|
754
|
+
if left == right:
|
|
755
|
+
return left
|
|
756
|
+
return BOOL
|
|
757
|
+
|
|
758
|
+
def _analyze_binop(self, expr: TpyBinOp) -> TpyType:
|
|
759
|
+
"""Analyze a binary operation."""
|
|
760
|
+
left_type = self.analyze_expr(expr.left)
|
|
761
|
+
if expr.op in ("&&", "||"):
|
|
762
|
+
type_true, type_false = self.narrowing.condition_type_facts(expr.left)
|
|
763
|
+
saved_types = dict(self.ctx.func.narrowed_types)
|
|
764
|
+
# Save definitely_assigned: RHS may not execute due to short-circuit
|
|
765
|
+
saved_assigned = frozenset(self.ctx.func.definitely_assigned)
|
|
766
|
+
if expr.op == "&&":
|
|
767
|
+
self.ctx.func.narrowed_types.update(type_true)
|
|
768
|
+
else:
|
|
769
|
+
self.ctx.func.narrowed_types.update(type_false)
|
|
770
|
+
try:
|
|
771
|
+
right_type = self.analyze_expr(expr.right)
|
|
772
|
+
finally:
|
|
773
|
+
self.ctx.func.narrowed_types = saved_types
|
|
774
|
+
# Track walrus vars introduced in RHS (short-circuit conditional)
|
|
775
|
+
rhs_walrus = self.ctx.func.definitely_assigned - saved_assigned
|
|
776
|
+
if rhs_walrus:
|
|
777
|
+
if expr.op == "&&":
|
|
778
|
+
self.ctx.sc_and_walrus |= rhs_walrus
|
|
779
|
+
else:
|
|
780
|
+
self.ctx.sc_or_walrus |= rhs_walrus
|
|
781
|
+
# Rollback: RHS walrus vars are not definitely assigned
|
|
782
|
+
self.ctx.func.definitely_assigned = set(saved_assigned)
|
|
783
|
+
else:
|
|
784
|
+
right_type = self.analyze_expr(expr.right)
|
|
785
|
+
|
|
786
|
+
# Preserve declared Optional/Union type for identity checks when flow
|
|
787
|
+
# narrowing resolved an expression to its inner type.
|
|
788
|
+
if expr.op in ("is", "is not"):
|
|
789
|
+
declared_left = self.narrowing.declared_type_for_expr(expr.left)
|
|
790
|
+
if is_union_or_optional_type(declared_left):
|
|
791
|
+
left_type = declared_left
|
|
792
|
+
declared_right = self.narrowing.declared_type_for_expr(expr.right)
|
|
793
|
+
if is_union_or_optional_type(declared_right):
|
|
794
|
+
right_type = declared_right
|
|
795
|
+
|
|
796
|
+
# Enforce Pythonic None identity checks for Optional values.
|
|
797
|
+
# `x == None` / `x != None` on Optional values should use `is` / `is not`.
|
|
798
|
+
if expr.op in ("==", "!="):
|
|
799
|
+
left_is_none = isinstance(left_type, NoneType)
|
|
800
|
+
right_is_none = isinstance(right_type, NoneType)
|
|
801
|
+
if left_is_none or right_is_none:
|
|
802
|
+
other = right_type if left_is_none else left_type
|
|
803
|
+
if isinstance(other, OptionalType):
|
|
804
|
+
raise self.ctx.error(
|
|
805
|
+
"Use 'is None' / 'is not None' for Optional None checks "
|
|
806
|
+
"(not '==' / '!=')",
|
|
807
|
+
expr,
|
|
808
|
+
)
|
|
809
|
+
if isinstance(other, PtrType):
|
|
810
|
+
raise self.ctx.error(
|
|
811
|
+
"Use 'is None' / 'is not None' for pointer None checks "
|
|
812
|
+
"(not '==' / '!=')",
|
|
813
|
+
expr,
|
|
814
|
+
)
|
|
815
|
+
if isinstance(other, UnionType) and other.has_none_member():
|
|
816
|
+
raise self.ctx.error(
|
|
817
|
+
"Use 'is None' / 'is not None' for union None checks "
|
|
818
|
+
"(not '==' / '!=')",
|
|
819
|
+
expr,
|
|
820
|
+
)
|
|
821
|
+
raise self.ctx.error(
|
|
822
|
+
f"Cannot compare {left_type} and {right_type} with '{expr.op}'",
|
|
823
|
+
expr,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
# Unwrap OwnType/RefType -- ownership/references don't affect operator resolution
|
|
827
|
+
left_effective = unwrap_ref_type(left_type)
|
|
828
|
+
left_effective = left_effective.wrapped if isinstance(left_effective, OwnType) else left_effective
|
|
829
|
+
right_effective = unwrap_ref_type(right_type)
|
|
830
|
+
right_effective = right_effective.wrapped if isinstance(right_effective, OwnType) else right_effective
|
|
831
|
+
# Value optionals in operator expressions use runtime null checks unless
|
|
832
|
+
# flow already proved non-None for the specific expression.
|
|
833
|
+
warned_optional_operator = False
|
|
834
|
+
if expr.op not in ("is", "is not", "&&", "||", "in", "not in", "==", "!="):
|
|
835
|
+
if isinstance(left_effective, OptionalType) and left_effective.inner.is_value_type():
|
|
836
|
+
left_effective = left_effective.inner
|
|
837
|
+
warned_optional_operator = True
|
|
838
|
+
if isinstance(right_effective, OptionalType) and right_effective.inner.is_value_type():
|
|
839
|
+
right_effective = right_effective.inner
|
|
840
|
+
warned_optional_operator = True
|
|
841
|
+
if warned_optional_operator:
|
|
842
|
+
self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
|
|
843
|
+
|
|
844
|
+
# For ==/!=, unwrap Optional value-types only for operator resolution
|
|
845
|
+
# (no warning -- C++ std::optional handles None comparison natively)
|
|
846
|
+
if expr.op in ("==", "!="):
|
|
847
|
+
if isinstance(left_effective, OptionalType) and left_effective.inner.is_value_type():
|
|
848
|
+
left_effective = left_effective.inner
|
|
849
|
+
expr.optional_safe_eq = True
|
|
850
|
+
if isinstance(right_effective, OptionalType) and right_effective.inner.is_value_type():
|
|
851
|
+
right_effective = right_effective.inner
|
|
852
|
+
expr.optional_safe_eq = True
|
|
853
|
+
|
|
854
|
+
# Helper to check if type is any numeric type
|
|
855
|
+
def is_numeric_type(t: TpyType) -> bool:
|
|
856
|
+
return is_any_int_type(t) or is_any_float_type(t)
|
|
857
|
+
|
|
858
|
+
# Identity operators (is / is not) -- only valid with None or enums
|
|
859
|
+
if expr.op in ("is", "is not"):
|
|
860
|
+
# Unwrap RefType, ReadonlyType, and OwnType for nullable checks.
|
|
861
|
+
left_check = unwrap_ref_type(unwrap_readonly(left_type))
|
|
862
|
+
if isinstance(left_check, OwnType):
|
|
863
|
+
left_check = left_check.wrapped
|
|
864
|
+
right_check = unwrap_ref_type(unwrap_readonly(right_type))
|
|
865
|
+
if isinstance(right_check, OwnType):
|
|
866
|
+
right_check = right_check.wrapped
|
|
867
|
+
# Enum identity: lower to ==/!=
|
|
868
|
+
if is_enum_type(left_check) and is_enum_type(right_check):
|
|
869
|
+
if left_check.name == right_check.name:
|
|
870
|
+
expr.op = "==" if expr.op == "is" else "!="
|
|
871
|
+
return BOOL
|
|
872
|
+
raise self.ctx.error(
|
|
873
|
+
f"Cannot compare enum types '{left_check.name}' and '{right_check.name}'",
|
|
874
|
+
expr,
|
|
875
|
+
)
|
|
876
|
+
# Bool literal identity: lower to ==/!=
|
|
877
|
+
if is_bool_type(left_check) and isinstance(expr.right, TpyBoolLiteral):
|
|
878
|
+
expr.op = "==" if expr.op == "is" else "!="
|
|
879
|
+
return BOOL
|
|
880
|
+
if is_bool_type(right_check) and isinstance(expr.left, TpyBoolLiteral):
|
|
881
|
+
expr.op = "==" if expr.op == "is" else "!="
|
|
882
|
+
return BOOL
|
|
883
|
+
nullable_types = (OptionalType, PtrType)
|
|
884
|
+
if isinstance(left_check, NoneType) and isinstance(right_check, nullable_types):
|
|
885
|
+
return BOOL
|
|
886
|
+
if isinstance(right_check, NoneType) and isinstance(left_check, nullable_types):
|
|
887
|
+
return BOOL
|
|
888
|
+
if isinstance(left_check, NoneType) and isinstance(right_check, NoneType):
|
|
889
|
+
return BOOL
|
|
890
|
+
# Nullable unions: v is None / v is not None
|
|
891
|
+
if isinstance(right_check, NoneType) and isinstance(left_check, UnionType) and left_check.has_none_member():
|
|
892
|
+
return BOOL
|
|
893
|
+
if isinstance(left_check, NoneType) and isinstance(right_check, UnionType) and right_check.has_none_member():
|
|
894
|
+
return BOOL
|
|
895
|
+
# Any vs None: typeid-based check (D15). Other RHS forms of
|
|
896
|
+
# `is` on Any are still rejected -- see the Future Extensions
|
|
897
|
+
# row in docs/ANY_TYPE_DESIGN.md.
|
|
898
|
+
if isinstance(right_check, NoneType) and isinstance(left_check, AnyType):
|
|
899
|
+
return BOOL
|
|
900
|
+
if isinstance(left_check, NoneType) and isinstance(right_check, AnyType):
|
|
901
|
+
return BOOL
|
|
902
|
+
raise self.ctx.error(
|
|
903
|
+
f"'is' / 'is not' can only compare Optional/Ptr/union types with None, "
|
|
904
|
+
f"got {left_type} and {right_type}",
|
|
905
|
+
expr,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
# Enum operators: base Enum supports == and != only;
|
|
909
|
+
# IntEnum also supports ordering, comparison with integers, and arithmetic
|
|
910
|
+
if is_enum_type(left_effective) or is_enum_type(right_effective):
|
|
911
|
+
left_is_int_enum = is_int_enum_type(left_effective)
|
|
912
|
+
right_is_int_enum = is_int_enum_type(right_effective)
|
|
913
|
+
is_comparison = expr.op in ("==", "!=", "<", ">", "<=", ">=")
|
|
914
|
+
|
|
915
|
+
if is_comparison:
|
|
916
|
+
# IntEnum vs integer: coerce enum to underlying type
|
|
917
|
+
if left_is_int_enum and is_any_int_type(right_effective):
|
|
918
|
+
expr.int_enum_coercion = left_effective
|
|
919
|
+
return BOOL
|
|
920
|
+
if right_is_int_enum and is_any_int_type(left_effective):
|
|
921
|
+
expr.int_enum_coercion = right_effective
|
|
922
|
+
return BOOL
|
|
923
|
+
if is_enum_type(left_effective) and is_enum_type(right_effective):
|
|
924
|
+
if left_effective.name != right_effective.name:
|
|
925
|
+
raise self.ctx.error(
|
|
926
|
+
f"Cannot compare enum types '{left_effective.name}' and '{right_effective.name}'",
|
|
927
|
+
expr,
|
|
928
|
+
)
|
|
929
|
+
# IntEnum supports ordering; base Enum does not
|
|
930
|
+
if expr.op in ("<", ">", "<=", ">=") and not left_is_int_enum:
|
|
931
|
+
raise self.ctx.error(
|
|
932
|
+
f"Ordering operators not supported for enum type '{left_effective.name}'",
|
|
933
|
+
expr,
|
|
934
|
+
)
|
|
935
|
+
# IntEnum ordering needs cast to underlying type
|
|
936
|
+
if left_is_int_enum and expr.op in ("<", ">", "<=", ">="):
|
|
937
|
+
expr.int_enum_coercion = left_effective
|
|
938
|
+
return BOOL
|
|
939
|
+
# One side is enum, other is not (and not int for IntEnum)
|
|
940
|
+
enum_name = left_effective.name if is_enum_type(left_effective) else right_effective.name
|
|
941
|
+
raise self.ctx.error(
|
|
942
|
+
f"Cannot compare '{enum_name}' with '{right_effective if is_enum_type(left_effective) else left_effective}'",
|
|
943
|
+
expr,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
# Non-comparison ops: IntEnum arithmetic is handled below;
|
|
947
|
+
# base Enum in arithmetic is an error
|
|
948
|
+
if not left_is_int_enum and not right_is_int_enum:
|
|
949
|
+
enum_name = left_effective.name if is_enum_type(left_effective) else right_effective.name
|
|
950
|
+
raise self.ctx.error(
|
|
951
|
+
f"Operator '{expr.op}' not supported for enum type '{enum_name}'",
|
|
952
|
+
expr,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
# Tuple comparison: ==, !=, <, <=, >, >= with element-wise validation
|
|
956
|
+
if isinstance(left_effective, TupleType) or isinstance(right_effective, TupleType):
|
|
957
|
+
# Both shapes (membership in a tuple literal `x in (a, b, c)` and
|
|
958
|
+
# tuple-as-key `key in dict`) are handled by the in/not-in section
|
|
959
|
+
# below; fall through here.
|
|
960
|
+
if expr.op in ("in", "not in"):
|
|
961
|
+
pass
|
|
962
|
+
elif expr.op in ("==", "!=", "<", "<=", ">", ">="):
|
|
963
|
+
if not (isinstance(left_effective, TupleType) and isinstance(right_effective, TupleType)):
|
|
964
|
+
raise self.ctx.error(
|
|
965
|
+
f"Cannot compare {left_effective} with {right_effective}",
|
|
966
|
+
expr,
|
|
967
|
+
)
|
|
968
|
+
if len(left_effective.element_types) != len(right_effective.element_types):
|
|
969
|
+
raise self.ctx.error(
|
|
970
|
+
f"Cannot compare tuples of different lengths: "
|
|
971
|
+
f"{left_effective} vs {right_effective}",
|
|
972
|
+
expr,
|
|
973
|
+
)
|
|
974
|
+
for i, (lt, rt) in enumerate(zip(
|
|
975
|
+
left_effective.element_types, right_effective.element_types
|
|
976
|
+
)):
|
|
977
|
+
if lt != rt:
|
|
978
|
+
try:
|
|
979
|
+
self.compat.check_type_compatible(lt, rt, "tuple comparison", source_expr=expr)
|
|
980
|
+
except SemanticError:
|
|
981
|
+
try:
|
|
982
|
+
self.compat.check_type_compatible(rt, lt, "tuple comparison", source_expr=expr)
|
|
983
|
+
except SemanticError:
|
|
984
|
+
raise self.ctx.error(
|
|
985
|
+
f"Cannot compare tuple element {i}: "
|
|
986
|
+
f"{lt} vs {rt}",
|
|
987
|
+
expr,
|
|
988
|
+
)
|
|
989
|
+
# For ordering ops, validate that element types support the operator
|
|
990
|
+
if expr.op in ("<", "<=", ">", ">="):
|
|
991
|
+
self._validate_comparison(expr, lt, rt)
|
|
992
|
+
return BOOL
|
|
993
|
+
else:
|
|
994
|
+
raise self.ctx.error(
|
|
995
|
+
f"Operator '{expr.op}' is not supported for tuple types",
|
|
996
|
+
expr,
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
# Comparison operators return Bool
|
|
1000
|
+
if expr.op in ("==", "!=", "<", ">", "<=", ">="):
|
|
1001
|
+
# Mixed-sign fixed-int comparison is well-defined under C++'s usual
|
|
1002
|
+
# arithmetic conversions (the signed operand is reinterpreted as
|
|
1003
|
+
# unsigned), but the result rarely matches user intent on negative
|
|
1004
|
+
# values. Codegen routes these through std::cmp_* so the answer is
|
|
1005
|
+
# mathematically correct; warn here so the user can choose to
|
|
1006
|
+
# cast explicitly if they care about readability. Skipped when
|
|
1007
|
+
# either side is still literal-seeded -- retro-widening may yet
|
|
1008
|
+
# resolve the operand to a compatible type.
|
|
1009
|
+
if (not self._is_literal_seed_operand(expr.left)
|
|
1010
|
+
and not self._is_literal_seed_operand(expr.right)
|
|
1011
|
+
and is_fixed_int_type(left_effective)
|
|
1012
|
+
and is_fixed_int_type(right_effective)):
|
|
1013
|
+
lt = int_traits_of(left_effective)
|
|
1014
|
+
rt = int_traits_of(right_effective)
|
|
1015
|
+
if lt is not None and rt is not None and lt.signed != rt.signed:
|
|
1016
|
+
self.ctx.warning(
|
|
1017
|
+
f"comparison between signed and unsigned integer types "
|
|
1018
|
+
f"('{left_effective}' and '{right_effective}'); "
|
|
1019
|
+
f"cast one operand to make the type intent explicit",
|
|
1020
|
+
expr,
|
|
1021
|
+
)
|
|
1022
|
+
# Validate that user record types support the comparison
|
|
1023
|
+
self._validate_comparison(expr, left_effective, right_effective)
|
|
1024
|
+
# Resolve comparison method (__eq__, __lt__, etc.) for codegen.
|
|
1025
|
+
if result := self.operators.resolve_binop(left_effective, expr.op, right_effective, loc_node=expr):
|
|
1026
|
+
expr.resolved_binop = result
|
|
1027
|
+
elif expr.op == "!=":
|
|
1028
|
+
# No __ne__: fall back to negated __eq__ when the method
|
|
1029
|
+
# can't use raw C++ != (e.g. native freestanding function).
|
|
1030
|
+
if result := self.operators.resolve_binop(left_effective, "==", right_effective, loc_node=expr):
|
|
1031
|
+
if result.method.native_function:
|
|
1032
|
+
expr.resolved_binop = result
|
|
1033
|
+
return BOOL
|
|
1034
|
+
|
|
1035
|
+
# Membership operators (in, not in) return Bool
|
|
1036
|
+
if expr.op in ("in", "not in"):
|
|
1037
|
+
right_type = unwrap_ref_type(right_type)
|
|
1038
|
+
if isinstance(right_type, OwnType):
|
|
1039
|
+
right_type = right_type.wrapped
|
|
1040
|
+
# TypedDict: "key" in td -> compile-time field presence check
|
|
1041
|
+
right_record = self.ctx.registry.get_record_for_type(right_type)
|
|
1042
|
+
if right_record and right_record.is_typed_dict:
|
|
1043
|
+
if not isinstance(expr.left, TpyStrLiteral):
|
|
1044
|
+
raise self.ctx.error(
|
|
1045
|
+
f"TypedDict '{right_type.name}' membership test requires a string literal key",
|
|
1046
|
+
expr.left)
|
|
1047
|
+
key = expr.left.value
|
|
1048
|
+
for fld in right_record.fields:
|
|
1049
|
+
if fld.name == key:
|
|
1050
|
+
expr.typed_dict_in_field = key
|
|
1051
|
+
expr.typed_dict_in_always_true = not right_record.is_total_false
|
|
1052
|
+
return BOOL
|
|
1053
|
+
raise self.ctx.error(
|
|
1054
|
+
f"TypedDict '{right_type.name}' has no key '{key}'", expr.left)
|
|
1055
|
+
if right_record:
|
|
1056
|
+
contains_overloads = right_record.get_method_overloads("__contains__")
|
|
1057
|
+
if contains_overloads:
|
|
1058
|
+
# Drop int-kind type args (e.g. N in Array[T, N]) --
|
|
1059
|
+
# _substitute_type_params only acts on TypeParamRef -> TpyType.
|
|
1060
|
+
type_subst = {
|
|
1061
|
+
k: v for k, v in self.type_ops.build_type_substitution(right_type).items()
|
|
1062
|
+
if isinstance(v, TpyType)
|
|
1063
|
+
}
|
|
1064
|
+
# Substitute type params for generic containers
|
|
1065
|
+
subst_overloads = contains_overloads
|
|
1066
|
+
if type_subst:
|
|
1067
|
+
subst_overloads = [
|
|
1068
|
+
dc_replace(m, params=[
|
|
1069
|
+
ParamInfo(p.name, _substitute_type_params(p.type, type_subst))
|
|
1070
|
+
for p in m.params
|
|
1071
|
+
]) for m in contains_overloads
|
|
1072
|
+
]
|
|
1073
|
+
# Resolve IntLiteralType: use param type if the literal fits,
|
|
1074
|
+
# otherwise fall back to default int type
|
|
1075
|
+
check_left = left_type
|
|
1076
|
+
if isinstance(check_left, IntLiteralType):
|
|
1077
|
+
for m in subst_overloads:
|
|
1078
|
+
pt = m.params[0].type
|
|
1079
|
+
pt_tr = int_traits_of(pt)
|
|
1080
|
+
if (pt_tr is not None
|
|
1081
|
+
and pt_tr.min_value <= check_left.value <= pt_tr.max_value):
|
|
1082
|
+
check_left = pt
|
|
1083
|
+
break
|
|
1084
|
+
else:
|
|
1085
|
+
check_left = self.ctx.default_int_for_literal(check_left)
|
|
1086
|
+
matched = resolve_overload(subst_overloads, [check_left])
|
|
1087
|
+
if matched is not None:
|
|
1088
|
+
# Map back to the original (un-substituted) method for codegen
|
|
1089
|
+
idx = subst_overloads.index(matched)
|
|
1090
|
+
original_method = contains_overloads[idx]
|
|
1091
|
+
raise_if_class_param_bound_violated(
|
|
1092
|
+
original_method, right_record.type_params, type_subst,
|
|
1093
|
+
self.protocols.type_conforms_to_protocol,
|
|
1094
|
+
self.ctx.error, expr,
|
|
1095
|
+
)
|
|
1096
|
+
expr.resolved_contains = original_method
|
|
1097
|
+
return BOOL
|
|
1098
|
+
# No __contains__ overload matched. Defer to
|
|
1099
|
+
# check_type_compatible: it raises for genuine mismatches
|
|
1100
|
+
# (preserving int-literal range diagnostics) and accepts
|
|
1101
|
+
# compatible coercions, in which case control falls
|
|
1102
|
+
# through to the outer iterable-membership path.
|
|
1103
|
+
int_overload = None
|
|
1104
|
+
if isinstance(left_type, IntLiteralType):
|
|
1105
|
+
for o in subst_overloads:
|
|
1106
|
+
if int_traits_of(o.params[0].type) is not None:
|
|
1107
|
+
int_overload = o
|
|
1108
|
+
break
|
|
1109
|
+
if int_overload is not None or len(subst_overloads) == 1:
|
|
1110
|
+
param_type = (int_overload or subst_overloads[0]).params[0].type
|
|
1111
|
+
self.compat.check_type_compatible(
|
|
1112
|
+
left_type, param_type,
|
|
1113
|
+
f"membership test (expected {param_type})",
|
|
1114
|
+
loc=expr.loc,
|
|
1115
|
+
)
|
|
1116
|
+
else:
|
|
1117
|
+
expected_str = " or ".join(
|
|
1118
|
+
str(o.params[0].type) for o in subst_overloads)
|
|
1119
|
+
raise self.ctx.error(
|
|
1120
|
+
f"Type mismatch in membership test: "
|
|
1121
|
+
f"expected {expected_str}, got {left_type}",
|
|
1122
|
+
expr,
|
|
1123
|
+
)
|
|
1124
|
+
# Tuple literal membership: x in (1, 2, 3) -> x == 1 || x == 2 || x == 3
|
|
1125
|
+
if isinstance(right_type, TupleType) and isinstance(expr.right, TpyTupleLiteral):
|
|
1126
|
+
for et in right_type.element_types:
|
|
1127
|
+
if not self.compat.is_type_compatible(left_type, et) \
|
|
1128
|
+
and not self.compat.is_type_compatible(et, left_type):
|
|
1129
|
+
raise self.ctx.error(
|
|
1130
|
+
f"Tuple element type '{et}' is not compatible "
|
|
1131
|
+
f"with membership test type '{left_type}'",
|
|
1132
|
+
expr)
|
|
1133
|
+
return BOOL
|
|
1134
|
+
# Right side must be iterable (intrinsically or via NativeIterable protocol)
|
|
1135
|
+
helper = IterableHelper(self.ctx)
|
|
1136
|
+
if helper.is_type_iterable(right_type):
|
|
1137
|
+
# For string containers, LHS must be str or Char
|
|
1138
|
+
if is_any_str_type(right_type):
|
|
1139
|
+
if not (is_any_str_type(left_type) or is_char_type(left_type)):
|
|
1140
|
+
raise SemanticError(
|
|
1141
|
+
f"Cannot check '{left_type}' membership in str (expected str or Char)",
|
|
1142
|
+
expr.loc
|
|
1143
|
+
)
|
|
1144
|
+
else:
|
|
1145
|
+
# Non-string collections use std::find which requires ==
|
|
1146
|
+
elem_type = right_type.get_element_type()
|
|
1147
|
+
if elem_type is not None:
|
|
1148
|
+
equatable = NominalType("Equatable", is_protocol=True)
|
|
1149
|
+
if not self.protocols.type_conforms_to_protocol(elem_type, equatable):
|
|
1150
|
+
raise self.ctx.error(
|
|
1151
|
+
f"'in' requires element type '{elem_type}' to "
|
|
1152
|
+
f"conform to 'Equatable' (no '__eq__' method)",
|
|
1153
|
+
expr)
|
|
1154
|
+
return BOOL
|
|
1155
|
+
raise self.ctx.error(f"Cannot use '{expr.op}' with non-iterable type {right_type}", expr)
|
|
1156
|
+
|
|
1157
|
+
# Logical operators: Python semantics returns an operand, not bool.
|
|
1158
|
+
# Same non-bool type -> return that type; otherwise -> bool.
|
|
1159
|
+
if expr.op in ("&&", "||"):
|
|
1160
|
+
result = self._logical_op_result_type(left_type, right_type)
|
|
1161
|
+
if is_list(result):
|
|
1162
|
+
# Both ternary branches must share a C++ type; force sources to
|
|
1163
|
+
# ListType so they don't independently become incompatible Arrays.
|
|
1164
|
+
for t in collect_pending_source_types(self.ctx, expr):
|
|
1165
|
+
if isinstance(t, PendingListType):
|
|
1166
|
+
info = self.ctx.list_literals.get(t.literal_id)
|
|
1167
|
+
if info is not None:
|
|
1168
|
+
info.needs_list_type = True
|
|
1169
|
+
return result
|
|
1170
|
+
|
|
1171
|
+
# IntEnum arithmetic: coerce to underlying type, delegate to standard binop
|
|
1172
|
+
if expr.op in ("+", "-", "*", "//", "%"):
|
|
1173
|
+
int_enum_side = None
|
|
1174
|
+
other_side = None
|
|
1175
|
+
if is_int_enum_type(left_effective):
|
|
1176
|
+
int_enum_side = left_effective
|
|
1177
|
+
other_side = right_effective
|
|
1178
|
+
elif is_int_enum_type(right_effective):
|
|
1179
|
+
int_enum_side = right_effective
|
|
1180
|
+
other_side = left_effective
|
|
1181
|
+
if int_enum_side is not None:
|
|
1182
|
+
if is_int_enum_type(other_side):
|
|
1183
|
+
if other_side.name != int_enum_side.name:
|
|
1184
|
+
raise self.ctx.error(
|
|
1185
|
+
f"Cannot mix arithmetic between '{int_enum_side.name}' "
|
|
1186
|
+
f"and '{other_side.name}'",
|
|
1187
|
+
expr,
|
|
1188
|
+
)
|
|
1189
|
+
if (is_int_enum_type(other_side) or isinstance(other_side, IntLiteralType)
|
|
1190
|
+
or is_fixed_int_type(other_side) or is_big_int_type(other_side)):
|
|
1191
|
+
# Coerce IntEnum operands to underlying type so standard
|
|
1192
|
+
# FixedInt binop resolution (with checked arithmetic) handles it
|
|
1193
|
+
expr.int_enum_coercion = int_enum_side
|
|
1194
|
+
if is_int_enum_type(left_effective):
|
|
1195
|
+
left_effective = enum_info_of(left_effective).underlying_type
|
|
1196
|
+
if is_int_enum_type(right_effective):
|
|
1197
|
+
right_effective = enum_info_of(right_effective).underlying_type
|
|
1198
|
+
# Fall through to standard binop resolution below
|
|
1199
|
+
|
|
1200
|
+
# IntLiteral + IntLiteral -> IntLiteral (stays unresolved until context determines type)
|
|
1201
|
+
if isinstance(left_effective, IntLiteralType) and isinstance(right_effective, IntLiteralType):
|
|
1202
|
+
# Keep Python-style true division semantics for all-literal integer
|
|
1203
|
+
# expressions regardless of default-int setting.
|
|
1204
|
+
if expr.op == "div":
|
|
1205
|
+
if result := self.operators.resolve_binop(BIGINT, expr.op, BIGINT, loc_node=expr):
|
|
1206
|
+
expr.resolved_binop = result
|
|
1207
|
+
return result.method.return_type
|
|
1208
|
+
return FLOAT
|
|
1209
|
+
literal_result = self._try_eval_int_literal_binop(expr.op, left_effective.value, right_effective.value)
|
|
1210
|
+
resolved_int = (
|
|
1211
|
+
self.ctx.default_int_for_literal(IntLiteralType(literal_result))
|
|
1212
|
+
if literal_result is not None
|
|
1213
|
+
else self.ctx.default_int_type
|
|
1214
|
+
)
|
|
1215
|
+
# Still resolve for codegen (bitwise ops need cpp template).
|
|
1216
|
+
if result := self.operators.resolve_binop(resolved_int, expr.op, resolved_int, loc_node=expr):
|
|
1217
|
+
expr.resolved_binop = result
|
|
1218
|
+
return IntLiteralType(literal_result)
|
|
1219
|
+
|
|
1220
|
+
# Protocol-typed operands - look up the dunder method in the protocol
|
|
1221
|
+
# For Self in protocols, Self binds to the protocol itself when used as a value type
|
|
1222
|
+
if is_protocol_type(left_effective):
|
|
1223
|
+
method_name = builtin_modules.BINOP_TO_METHOD.get(expr.op)
|
|
1224
|
+
if method_name:
|
|
1225
|
+
return_type = self.protocols.lookup_protocol_method_return(
|
|
1226
|
+
left_effective,
|
|
1227
|
+
method_name,
|
|
1228
|
+
[right_effective],
|
|
1229
|
+
)
|
|
1230
|
+
if return_type is not None:
|
|
1231
|
+
return return_type
|
|
1232
|
+
|
|
1233
|
+
# Arithmetic/bitwise operators - use registry
|
|
1234
|
+
if result := self.operators.resolve_binop(left_effective, expr.op, right_effective, loc_node=expr):
|
|
1235
|
+
expr.resolved_binop = result
|
|
1236
|
+
# Check if divisor is provably non-zero for div/mod elision
|
|
1237
|
+
if expr.op in ("//", "%"):
|
|
1238
|
+
self._check_divisor_non_zero(expr)
|
|
1239
|
+
# List concat produces a list -- mark pending literals as mutated
|
|
1240
|
+
if is_list(result.method.return_type):
|
|
1241
|
+
self._mark_list_concat_operands_mutated(expr, left_effective, right_effective)
|
|
1242
|
+
return result.method.return_type
|
|
1243
|
+
|
|
1244
|
+
# Record types (user-defined or module) with dunder methods
|
|
1245
|
+
if isinstance(left_effective, NominalType) and left_effective.is_record:
|
|
1246
|
+
method_name = builtin_modules.BINOP_TO_METHOD.get(expr.op)
|
|
1247
|
+
if method_name:
|
|
1248
|
+
record = self.ctx.registry.get_record_for_type(left_effective)
|
|
1249
|
+
if record and (method := record.get_method(method_name)):
|
|
1250
|
+
# Check parameter count and type
|
|
1251
|
+
if len(method.params) == 1:
|
|
1252
|
+
_, param_type = method.params[0]
|
|
1253
|
+
# Substitute type params for generic types (e.g. list[T].__add__(list[T]))
|
|
1254
|
+
type_subst = self.type_ops.build_type_substitution(left_effective)
|
|
1255
|
+
if type_subst:
|
|
1256
|
+
param_type = self.type_ops.substitute_types(param_type, type_subst)
|
|
1257
|
+
if param_type == right_effective:
|
|
1258
|
+
raise_if_class_param_bound_violated(
|
|
1259
|
+
method, record.type_params, type_subst,
|
|
1260
|
+
self.protocols.type_conforms_to_protocol,
|
|
1261
|
+
self.ctx.error, expr,
|
|
1262
|
+
)
|
|
1263
|
+
ret_type = method.return_type
|
|
1264
|
+
if type_subst:
|
|
1265
|
+
ret_type = self.type_ops.substitute_types(ret_type, type_subst)
|
|
1266
|
+
# Build ResolvedBinop so codegen uses the method's
|
|
1267
|
+
# cpp_template instead of raw C++ operator syntax.
|
|
1268
|
+
cpp = method.cpp_template
|
|
1269
|
+
if not cpp and not method.native_function:
|
|
1270
|
+
cpp = DUNDER_CPP_TEMPLATES.get(method_name)
|
|
1271
|
+
resolved_method = FunctionInfo(
|
|
1272
|
+
name=method_name,
|
|
1273
|
+
params=list(method.params),
|
|
1274
|
+
return_type=ret_type,
|
|
1275
|
+
cpp_template=cpp,
|
|
1276
|
+
native_name=method.native_name,
|
|
1277
|
+
native_function=method.native_function,
|
|
1278
|
+
is_method=True,
|
|
1279
|
+
owning_type_qname=method.owning_type_qname,
|
|
1280
|
+
)
|
|
1281
|
+
expr.resolved_binop = ResolvedBinop(
|
|
1282
|
+
method=resolved_method,
|
|
1283
|
+
left_wrapper="{expr}",
|
|
1284
|
+
right_wrapper="{expr}",
|
|
1285
|
+
receiver_type=left_effective,
|
|
1286
|
+
)
|
|
1287
|
+
return ret_type
|
|
1288
|
+
|
|
1289
|
+
raise SemanticError(
|
|
1290
|
+
f"Invalid operand types for '{expr.op}': {left_type} and {right_type}",
|
|
1291
|
+
expr.loc,
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
def _check_subscript_bounds_safe(self, expr: TpySubscript) -> None:
|
|
1295
|
+
"""Set bounds_safe when the index is provably in [0, len(obj))."""
|
|
1296
|
+
index = expr.index
|
|
1297
|
+
obj = expr.obj
|
|
1298
|
+
if not isinstance(index, TpyName) or not isinstance(obj, TpyName):
|
|
1299
|
+
return
|
|
1300
|
+
index_range = self.ctx.func.value_ranges.get(index.name)
|
|
1301
|
+
is_safe = (
|
|
1302
|
+
index_range is not None
|
|
1303
|
+
and index_range.is_non_negative()
|
|
1304
|
+
and index_range.is_bounded_by_len(obj.name)
|
|
1305
|
+
)
|
|
1306
|
+
expr.bounds_safe = is_safe
|
|
1307
|
+
if expr.loc:
|
|
1308
|
+
self.ctx.subscript_bounds_facts[(expr.loc.line, obj.name)] = is_safe
|
|
1309
|
+
|
|
1310
|
+
def _check_divisor_non_zero(self, expr: TpyBinOp) -> None:
|
|
1311
|
+
"""Set divisor_non_zero when the divisor is provably != 0."""
|
|
1312
|
+
right = expr.right
|
|
1313
|
+
if isinstance(right, TpyIntLiteral):
|
|
1314
|
+
is_safe = right.value != 0
|
|
1315
|
+
expr.divisor_non_zero = is_safe
|
|
1316
|
+
return
|
|
1317
|
+
if not isinstance(right, TpyName):
|
|
1318
|
+
return
|
|
1319
|
+
divisor_range = self.ctx.func.value_ranges.get(right.name)
|
|
1320
|
+
is_safe = divisor_range is not None and divisor_range.non_zero
|
|
1321
|
+
expr.divisor_non_zero = is_safe
|
|
1322
|
+
if expr.loc:
|
|
1323
|
+
self.ctx.div_zero_facts[(expr.loc.line, right.name)] = is_safe
|
|
1324
|
+
|
|
1325
|
+
def _mark_list_concat_operands_mutated(
|
|
1326
|
+
self, expr: TpyBinOp, left_type: TpyType, right_type: TpyType,
|
|
1327
|
+
) -> None:
|
|
1328
|
+
"""Mark PendingListType operands as mutated so they resolve to list, not Array."""
|
|
1329
|
+
for sub_expr, sub_type in ((expr.left, left_type), (expr.right, right_type)):
|
|
1330
|
+
if isinstance(sub_type, PendingListType):
|
|
1331
|
+
info = self.ctx.list_literals.get(sub_type.literal_id)
|
|
1332
|
+
if info:
|
|
1333
|
+
info.is_mutated = True
|
|
1334
|
+
elif isinstance(sub_expr, TpyName):
|
|
1335
|
+
var_name = sub_expr.name
|
|
1336
|
+
if var_name in self.ctx.func.variable_to_literal:
|
|
1337
|
+
lit_id = self.ctx.func.variable_to_literal[var_name]
|
|
1338
|
+
info = self.ctx.list_literals.get(lit_id)
|
|
1339
|
+
if info:
|
|
1340
|
+
info.is_mutated = True
|
|
1341
|
+
|
|
1342
|
+
def _try_eval_int_literal_binop(self, op: str, left: int | None, right: int | None) -> int | None:
|
|
1343
|
+
"""Best-effort constant evaluation for int literal binops."""
|
|
1344
|
+
if left is None or right is None:
|
|
1345
|
+
return None
|
|
1346
|
+
try:
|
|
1347
|
+
if op == "+":
|
|
1348
|
+
return left + right
|
|
1349
|
+
if op == "-":
|
|
1350
|
+
return left - right
|
|
1351
|
+
if op == "*":
|
|
1352
|
+
return left * right
|
|
1353
|
+
if op == "//":
|
|
1354
|
+
if right == 0:
|
|
1355
|
+
return None
|
|
1356
|
+
return left // right
|
|
1357
|
+
if op == "%":
|
|
1358
|
+
if right == 0:
|
|
1359
|
+
return None
|
|
1360
|
+
return left % right
|
|
1361
|
+
if op == "**":
|
|
1362
|
+
if right < 0 or right > 10000:
|
|
1363
|
+
return None
|
|
1364
|
+
return left ** right
|
|
1365
|
+
if op == "<<":
|
|
1366
|
+
if right < 0 or right > 10000:
|
|
1367
|
+
return None
|
|
1368
|
+
return left << right
|
|
1369
|
+
if op == ">>":
|
|
1370
|
+
if right < 0:
|
|
1371
|
+
return None
|
|
1372
|
+
return left >> right
|
|
1373
|
+
if op == "&":
|
|
1374
|
+
return left & right
|
|
1375
|
+
if op == "|":
|
|
1376
|
+
return left | right
|
|
1377
|
+
if op == "^":
|
|
1378
|
+
return left ^ right
|
|
1379
|
+
except (OverflowError, ValueError):
|
|
1380
|
+
return None
|
|
1381
|
+
return None
|
|
1382
|
+
|
|
1383
|
+
def _analyze_chained_compare(self, expr: TpyChainedCompare) -> TpyType:
|
|
1384
|
+
"""Analyze a chained comparison (a < b < c, etc.)."""
|
|
1385
|
+
pairs: list[TpyBinOp] = []
|
|
1386
|
+
prev = expr.left
|
|
1387
|
+
for op, comp in zip(expr.ops, expr.comparators):
|
|
1388
|
+
pair = TpyBinOp(prev, op, comp, loc=expr.loc)
|
|
1389
|
+
self.analyze_expr(pair)
|
|
1390
|
+
pairs.append(pair)
|
|
1391
|
+
prev = comp
|
|
1392
|
+
expr.pairs = pairs
|
|
1393
|
+
return BOOL
|
|
1394
|
+
|
|
1395
|
+
def _analyze_unaryop(self, expr: TpyUnaryOp) -> TpyType:
|
|
1396
|
+
"""Analyze a unary operation."""
|
|
1397
|
+
operand_type = self.analyze_expr(expr.operand)
|
|
1398
|
+
effective_type = unwrap_ref_type(operand_type)
|
|
1399
|
+
if isinstance(effective_type, OwnType):
|
|
1400
|
+
effective_type = effective_type.wrapped
|
|
1401
|
+
|
|
1402
|
+
# Value optionals in unary arithmetic/bitwise ops use runtime checks
|
|
1403
|
+
# unless flow already narrowed them to non-Optional.
|
|
1404
|
+
if expr.op in ("-", "~") and isinstance(operand_type, OptionalType) and operand_type.inner.is_value_type():
|
|
1405
|
+
effective_type = operand_type.inner
|
|
1406
|
+
self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
|
|
1407
|
+
|
|
1408
|
+
# Logical not: validate operand type (Bool, numeric, Optional, or types with __bool__/__len__)
|
|
1409
|
+
if expr.op == "!":
|
|
1410
|
+
if (is_bool_type(effective_type)
|
|
1411
|
+
or is_any_int_type(effective_type)
|
|
1412
|
+
or is_any_float_type(effective_type)
|
|
1413
|
+
or isinstance(effective_type, OptionalType)
|
|
1414
|
+
or isinstance(effective_type, AnyType)
|
|
1415
|
+
or is_enum_type(effective_type)):
|
|
1416
|
+
return BOOL
|
|
1417
|
+
record = self.ctx.registry.get_record_for_type(effective_type)
|
|
1418
|
+
if record and (record.get_method_overloads("__bool__")
|
|
1419
|
+
or record.get_method_overloads("__len__")):
|
|
1420
|
+
return BOOL
|
|
1421
|
+
raise self.ctx.error(f"Invalid operand type for 'not': {effective_type} (expected bool, numeric, or type with __bool__/__len__)", expr)
|
|
1422
|
+
|
|
1423
|
+
# Float types support unary negation and plus
|
|
1424
|
+
if is_any_float_type(effective_type):
|
|
1425
|
+
if expr.op in ("-", "+"):
|
|
1426
|
+
if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
|
|
1427
|
+
expr.resolved_unaryop = result
|
|
1428
|
+
return effective_type
|
|
1429
|
+
|
|
1430
|
+
# IntEnum: unary negation returns the underlying integer type
|
|
1431
|
+
if is_int_enum_type(effective_type) and expr.op == "-":
|
|
1432
|
+
return enum_info_of(effective_type).underlying_type
|
|
1433
|
+
|
|
1434
|
+
# IntLiteralType special cases - preserve literal nature when possible
|
|
1435
|
+
if isinstance(effective_type, IntLiteralType):
|
|
1436
|
+
if expr.op == "-":
|
|
1437
|
+
# Still resolve for codegen (needs cpp template)
|
|
1438
|
+
if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
|
|
1439
|
+
expr.resolved_unaryop = result
|
|
1440
|
+
neg = -effective_type.value if effective_type.value is not None else None
|
|
1441
|
+
return IntLiteralType(neg)
|
|
1442
|
+
if expr.op == "+":
|
|
1443
|
+
if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
|
|
1444
|
+
expr.resolved_unaryop = result
|
|
1445
|
+
return effective_type
|
|
1446
|
+
if expr.op == "~":
|
|
1447
|
+
# Bitwise not on literal - treat as Int32
|
|
1448
|
+
# Still resolve for codegen
|
|
1449
|
+
if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
|
|
1450
|
+
expr.resolved_unaryop = result
|
|
1451
|
+
return INT32
|
|
1452
|
+
|
|
1453
|
+
# Use registry for unary operators
|
|
1454
|
+
if result := self.operators.resolve_unaryop(effective_type, expr.op, loc_node=expr):
|
|
1455
|
+
expr.resolved_unaryop = result
|
|
1456
|
+
return result.method.return_type
|
|
1457
|
+
|
|
1458
|
+
raise self.ctx.error(f"Invalid operand type for unary '{expr.op}': {operand_type}", expr)
|
|
1459
|
+
|
|
1460
|
+
def _is_user_record_type(self, typ: TpyType) -> bool:
|
|
1461
|
+
"""Check if a type is a user-defined record (not a builtin container)."""
|
|
1462
|
+
return isinstance(typ, NominalType) and typ.is_record and typ.is_user_record
|
|
1463
|
+
|
|
1464
|
+
def _is_literal_seed_operand(self, operand: TpyExpr) -> bool:
|
|
1465
|
+
"""True when ``operand`` is still pending literal-driven type resolution.
|
|
1466
|
+
|
|
1467
|
+
Two cases: an analyzer-level ``IntLiteralType`` (the operand is itself a
|
|
1468
|
+
literal), or a ``TpyName`` whose local is in ``literal_default_vars``
|
|
1469
|
+
(literal-seeded local awaiting retro-widen). Used to suppress the
|
|
1470
|
+
mixed-sign-comparison warning before sema has finalised the type --
|
|
1471
|
+
the operand may yet resolve to a same-sign type.
|
|
1472
|
+
"""
|
|
1473
|
+
if isinstance(self.ctx.get_expr_type(operand), IntLiteralType):
|
|
1474
|
+
return True
|
|
1475
|
+
return (isinstance(operand, TpyName)
|
|
1476
|
+
and operand.name in self.ctx.func.literal_default_vars)
|
|
1477
|
+
|
|
1478
|
+
def _validate_comparison(self, expr: TpyBinOp, left_type: TpyType, right_type: TpyType) -> None:
|
|
1479
|
+
"""Error when comparing user record types that lack the relevant dunder."""
|
|
1480
|
+
# Only check when at least one side is a user record
|
|
1481
|
+
if not self._is_user_record_type(left_type) and not self._is_user_record_type(right_type):
|
|
1482
|
+
return
|
|
1483
|
+
# Determine which dunder to check
|
|
1484
|
+
COMPARE_OP_TO_DUNDER = {
|
|
1485
|
+
"==": "__eq__", "!=": "__ne__",
|
|
1486
|
+
"<": "__lt__", "<=": "__le__",
|
|
1487
|
+
">": "__gt__", ">=": "__ge__",
|
|
1488
|
+
}
|
|
1489
|
+
dunder = COMPARE_OP_TO_DUNDER.get(expr.op)
|
|
1490
|
+
if not dunder:
|
|
1491
|
+
return
|
|
1492
|
+
# Check the left side (operator dispatch goes left to right)
|
|
1493
|
+
check_type = left_type if self._is_user_record_type(left_type) else right_type
|
|
1494
|
+
record = self.ctx.registry.get_record_for_type(check_type)
|
|
1495
|
+
if record:
|
|
1496
|
+
# Use lookup that walks the inheritance chain
|
|
1497
|
+
overloads, _ = self.protocols.lookup_record_method_overloads(record, dunder)
|
|
1498
|
+
has_dunder = bool(overloads)
|
|
1499
|
+
# != is valid if __eq__ is defined (C++ generates != from ==)
|
|
1500
|
+
if not has_dunder and dunder == "__ne__":
|
|
1501
|
+
overloads, _ = self.protocols.lookup_record_method_overloads(record, "__eq__")
|
|
1502
|
+
has_dunder = bool(overloads)
|
|
1503
|
+
if not has_dunder:
|
|
1504
|
+
if dunder == "__ne__":
|
|
1505
|
+
msg = (f"Comparison '!=' on '{check_type}': "
|
|
1506
|
+
f"no '__ne__' or '__eq__' method defined")
|
|
1507
|
+
else:
|
|
1508
|
+
msg = (f"Comparison '{expr.op}' on '{check_type}': "
|
|
1509
|
+
f"no '{dunder}' method defined")
|
|
1510
|
+
self.ctx.emit_error(msg, expr)
|
|
1511
|
+
|
|
1512
|
+
def get_deref_target_type(self, typ: TpyType, is_readonly: bool = False) -> TpyType | None:
|
|
1513
|
+
"""If typ has __deref__(), return resolved return type. Else None."""
|
|
1514
|
+
return self.type_ops.get_deref_target_type(typ, is_readonly=is_readonly)
|
|
1515
|
+
|
|
1516
|
+
def _try_find_field(self, typ: TpyType, expr: TpyFieldAccess) -> TpyType | None:
|
|
1517
|
+
"""Try to find a field on typ. Returns field type or None."""
|
|
1518
|
+
if is_basic_slice_type(typ):
|
|
1519
|
+
if expr.field in ("start", "stop"):
|
|
1520
|
+
return OptionalType(INT32)
|
|
1521
|
+
return None
|
|
1522
|
+
if is_slice_type(typ):
|
|
1523
|
+
if expr.field in ("start", "stop", "step"):
|
|
1524
|
+
return OptionalType(INT32)
|
|
1525
|
+
return None
|
|
1526
|
+
|
|
1527
|
+
if isinstance(typ, NominalType) and typ.is_record:
|
|
1528
|
+
record = self.ctx.registry.get_record_for_type(typ)
|
|
1529
|
+
if not record:
|
|
1530
|
+
return None
|
|
1531
|
+
# Multi-base same-name ambiguity: when the child doesn't declare
|
|
1532
|
+
# the field itself and more than one direct-parent branch reaches
|
|
1533
|
+
# it, unqualified access could silently pick the first hit --
|
|
1534
|
+
# reject instead so the user disambiguates via `BaseN.field`.
|
|
1535
|
+
child_owns_field = any(f.name == expr.field for f in record.fields)
|
|
1536
|
+
if not child_owns_field and len(record.parents) > 1:
|
|
1537
|
+
branches = self.protocols.find_field_parent_branches(record, expr.field)
|
|
1538
|
+
if len(branches) > 1:
|
|
1539
|
+
names = ", ".join(branches)
|
|
1540
|
+
first, second = branches[0], branches[1]
|
|
1541
|
+
raise self.ctx.error(
|
|
1542
|
+
f"Ambiguous field '{expr.field}' inherited from {{{names}}} "
|
|
1543
|
+
f"in '{record.name}'; use '{first}.{expr.field}' "
|
|
1544
|
+
f"or '{second}.{expr.field}'",
|
|
1545
|
+
expr,
|
|
1546
|
+
)
|
|
1547
|
+
type_subst = self.type_ops.build_type_substitution(typ)
|
|
1548
|
+
field_info = self.protocols.lookup_record_field(record, expr.field)
|
|
1549
|
+
if field_info:
|
|
1550
|
+
field_type = field_info.type
|
|
1551
|
+
if type_subst:
|
|
1552
|
+
field_type = self.type_ops.substitute_type_params(field_type, type_subst)
|
|
1553
|
+
return field_type
|
|
1554
|
+
# Check properties (getter access)
|
|
1555
|
+
prop = self.protocols.lookup_record_property(record, expr.field)
|
|
1556
|
+
if prop is not None:
|
|
1557
|
+
expr.is_property_access = True
|
|
1558
|
+
# Construct a TpyMethodCall so codegen can delegate to normal method path
|
|
1559
|
+
getter_call = TpyMethodCall(obj=expr.obj, method=expr.field, args=[])
|
|
1560
|
+
getter_call.resolved_function_info = prop.getter
|
|
1561
|
+
expr.property_getter_call = getter_call
|
|
1562
|
+
prop_type = prop.type
|
|
1563
|
+
if type_subst:
|
|
1564
|
+
prop_type = self.type_ops.substitute_type_params(prop_type, type_subst)
|
|
1565
|
+
return prop_type
|
|
1566
|
+
# Class constant fallback for instance-side reads (`obj.X`,
|
|
1567
|
+
# `self.X`); codegen emits the declaring class's qualified name
|
|
1568
|
+
# regardless of which class the user accessed through.
|
|
1569
|
+
return self._lookup_class_constant_owner(record, expr)
|
|
1570
|
+
|
|
1571
|
+
if isinstance(typ, TypeParamRef):
|
|
1572
|
+
bound = self.type_ops.get_type_param_bound(typ.name)
|
|
1573
|
+
if bound is not None and is_protocol_type(bound):
|
|
1574
|
+
protocol_info = protocol_info_of(bound)
|
|
1575
|
+
if protocol_info:
|
|
1576
|
+
for field_name, field_type in protocol_info.fields or []:
|
|
1577
|
+
if field_name == expr.field:
|
|
1578
|
+
type_subst: dict[str, TpyType] = {"Self": typ}
|
|
1579
|
+
if protocol_info.type_params and bound.type_args:
|
|
1580
|
+
type_subst.update(dict(zip(protocol_info.type_params, bound.type_args)))
|
|
1581
|
+
resolved = self.type_ops.substitute_types(field_type, type_subst)
|
|
1582
|
+
return resolved
|
|
1583
|
+
raise self.ctx.error(f"Protocol '{bound.name}' has no field '{expr.field}'", expr)
|
|
1584
|
+
|
|
1585
|
+
return None
|
|
1586
|
+
|
|
1587
|
+
def _resolve_nested_type_access(self, parent_name: str, field: str,
|
|
1588
|
+
expr: TpyFieldAccess) -> TpyType | None:
|
|
1589
|
+
"""Check if parent_name.field is a nested enum or record. Returns type or None."""
|
|
1590
|
+
dotted = f"{parent_name}.{field}"
|
|
1591
|
+
nested_enum = self.ctx.registry.get_enum(dotted)
|
|
1592
|
+
if nested_enum is not None:
|
|
1593
|
+
return nested_enum
|
|
1594
|
+
nested_record = self.ctx.registry.get_record(dotted)
|
|
1595
|
+
if nested_record is not None:
|
|
1596
|
+
return NominalType(dotted)
|
|
1597
|
+
return None
|
|
1598
|
+
|
|
1599
|
+
def _resolve_nested_chain(self, expr: TpyFieldAccess) -> tuple[str, TpyType] | None:
|
|
1600
|
+
"""Resolve a chain of field accesses to a nested type (e.g., Outer.Mid.Inner).
|
|
1601
|
+
|
|
1602
|
+
Returns (dotted_name, resolved_type) or None if not a nested type chain.
|
|
1603
|
+
"""
|
|
1604
|
+
if isinstance(expr.obj, TpyName):
|
|
1605
|
+
if self.ctx.func.current_ns:
|
|
1606
|
+
binding = self.ctx.func.current_ns.lookup(expr.obj.name)
|
|
1607
|
+
if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
|
|
1608
|
+
dotted = f"{expr.obj.name}.{expr.field}"
|
|
1609
|
+
nested_enum = self.ctx.registry.get_enum(dotted)
|
|
1610
|
+
if nested_enum is not None:
|
|
1611
|
+
return (dotted, nested_enum)
|
|
1612
|
+
nested_record = self.ctx.registry.get_record(dotted)
|
|
1613
|
+
if nested_record is not None:
|
|
1614
|
+
return (dotted, NominalType(dotted))
|
|
1615
|
+
elif isinstance(expr.obj, TpyFieldAccess):
|
|
1616
|
+
parent = self._resolve_nested_chain(expr.obj)
|
|
1617
|
+
if parent is not None:
|
|
1618
|
+
dotted_parent, parent_type = parent
|
|
1619
|
+
if isinstance(parent_type, NominalType):
|
|
1620
|
+
dotted = f"{dotted_parent}.{expr.field}"
|
|
1621
|
+
nested_enum = self.ctx.registry.get_enum(dotted)
|
|
1622
|
+
if nested_enum is not None:
|
|
1623
|
+
return (dotted, nested_enum)
|
|
1624
|
+
nested_record = self.ctx.registry.get_record(dotted)
|
|
1625
|
+
if nested_record is not None:
|
|
1626
|
+
return (dotted, NominalType(dotted))
|
|
1627
|
+
return None
|
|
1628
|
+
|
|
1629
|
+
def _lookup_class_constant_owner(
|
|
1630
|
+
self, record: RecordInfo, expr: TpyFieldAccess,
|
|
1631
|
+
) -> TpyType | None:
|
|
1632
|
+
"""Resolve `expr.field` against `record.class_constants`, walking
|
|
1633
|
+
`mro_ancestors` to the declaring ancestor when not declared directly.
|
|
1634
|
+
Sets `expr.class_constant_owner` to the *declaring* record so codegen
|
|
1635
|
+
emits `<declaring_qname>::<member>` regardless of the access path.
|
|
1636
|
+
"""
|
|
1637
|
+
owner = self.ctx.registry.find_class_constant_owner(record, expr.field)
|
|
1638
|
+
if owner is None:
|
|
1639
|
+
return None
|
|
1640
|
+
# Multi-base same-name ambiguity: mirror the instance-field check in
|
|
1641
|
+
# `_try_find_field` so C3 linearization doesn't silently pick one
|
|
1642
|
+
# branch when more than one parent contributes the constant.
|
|
1643
|
+
if owner is not record and len(record.parents) > 1:
|
|
1644
|
+
branches = self.protocols.find_class_constant_parent_branches(record, expr.field)
|
|
1645
|
+
if len(branches) > 1:
|
|
1646
|
+
names = ", ".join(branches)
|
|
1647
|
+
first, second = branches[0], branches[1]
|
|
1648
|
+
raise self.ctx.error(
|
|
1649
|
+
f"Ambiguous class constant '{expr.field}' inherited from {{{names}}} "
|
|
1650
|
+
f"in '{record.name}'; use '{first}.{expr.field}' "
|
|
1651
|
+
f"or '{second}.{expr.field}'",
|
|
1652
|
+
expr,
|
|
1653
|
+
)
|
|
1654
|
+
# Phase 9: a class constant on a generic class is per-instantiation
|
|
1655
|
+
# in C++ (`C<T>::X`), so codegen needs the concrete (or template-scope)
|
|
1656
|
+
# type args at the access site. We get them from the receiver's type
|
|
1657
|
+
# only when the receiver is a direct instance of the generic owner.
|
|
1658
|
+
# Inheritance with fixed type-args (`class Child(C[Int32]): pass;
|
|
1659
|
+
# obj: Child; obj.X`) loses the args at the receiver-record level
|
|
1660
|
+
# and is deferred -- reject for now with a clear hint.
|
|
1661
|
+
if owner.type_params and owner is not record:
|
|
1662
|
+
raise self.ctx.error(
|
|
1663
|
+
f"class constant '{expr.field}' on generic class "
|
|
1664
|
+
f"'{owner.name}' cannot be accessed through subclass "
|
|
1665
|
+
f"'{record.name}'; access through an instance of "
|
|
1666
|
+
f"'{owner.name}[...]' instead",
|
|
1667
|
+
expr,
|
|
1668
|
+
)
|
|
1669
|
+
expr.class_constant_owner = owner
|
|
1670
|
+
return owner.class_constants[expr.field].type
|
|
1671
|
+
|
|
1672
|
+
def _try_class_constant_access(
|
|
1673
|
+
self, expr: TpyFieldAccess, binding: NameBinding,
|
|
1674
|
+
) -> TpyType | None:
|
|
1675
|
+
"""Resolve `<RecordName>.<field>` against the record's class_constants
|
|
1676
|
+
(with MRO walk via `_lookup_class_constant_owner`).
|
|
1677
|
+
"""
|
|
1678
|
+
assert isinstance(expr.obj, TpyName)
|
|
1679
|
+
record_info: RecordInfo | None = None
|
|
1680
|
+
if binding.kind == BindingKind.RECORD:
|
|
1681
|
+
record_info = self.ctx.registry.get_record(expr.obj.name)
|
|
1682
|
+
elif binding.kind == BindingKind.IMPORTED_NAME:
|
|
1683
|
+
import_info = self.ctx.imported_names.get(expr.obj.name)
|
|
1684
|
+
if import_info:
|
|
1685
|
+
record_info = self.ctx.registry.find_record_by_qname(
|
|
1686
|
+
f"{import_info[0]}.{import_info[1]}")
|
|
1687
|
+
if record_info is None:
|
|
1688
|
+
return None
|
|
1689
|
+
return self._class_constant_access_on_record(expr, record_info)
|
|
1690
|
+
|
|
1691
|
+
def _class_constant_access_on_record(
|
|
1692
|
+
self, expr: TpyFieldAccess, record_info: RecordInfo,
|
|
1693
|
+
) -> TpyType | None:
|
|
1694
|
+
"""Class-constant resolution given a pre-resolved record. Shared by the
|
|
1695
|
+
bare-name path (`Foo.CONST`) and the module-qualified path
|
|
1696
|
+
(`m.Foo.CONST`)."""
|
|
1697
|
+
# Phase 9: bare-class access on a generic class can't render the
|
|
1698
|
+
# parameterized qname (no type args at the access site), so reject
|
|
1699
|
+
# and point the user at instance access. `Class[Int32].X` syntax
|
|
1700
|
+
# for class-level access on a parameterized generic is not yet
|
|
1701
|
+
# supported either.
|
|
1702
|
+
if record_info.type_params and expr.field in record_info.class_constants:
|
|
1703
|
+
raise self.ctx.error(
|
|
1704
|
+
f"cannot access class constant '{expr.field}' on generic "
|
|
1705
|
+
f"class '{record_info.name}' through the bare class name; "
|
|
1706
|
+
f"access through an instance instead "
|
|
1707
|
+
f"(e.g. `{record_info.name}[T_args]().{expr.field}`)",
|
|
1708
|
+
expr,
|
|
1709
|
+
)
|
|
1710
|
+
return self._lookup_class_constant_owner(record_info, expr)
|
|
1711
|
+
|
|
1712
|
+
def _try_unbound_self_field_access(
|
|
1713
|
+
self, expr: TpyFieldAccess, binding: NameBinding,
|
|
1714
|
+
) -> TpyType | None:
|
|
1715
|
+
"""Handle `BaseN.field` accessing an ancestor subobject's field.
|
|
1716
|
+
|
|
1717
|
+
Returns the resolved field type, or None when the access is not an
|
|
1718
|
+
unbound-self form (not inside an instance method, or BaseN doesn't
|
|
1719
|
+
resolve to a known record) so the caller can fall through to the
|
|
1720
|
+
regular field-access path.
|
|
1721
|
+
"""
|
|
1722
|
+
assert isinstance(expr.obj, TpyName)
|
|
1723
|
+
current_rec_parse = self.ctx.record_ctx.record
|
|
1724
|
+
current_fn = self.ctx.func.current_function
|
|
1725
|
+
if (current_rec_parse is None
|
|
1726
|
+
or current_fn is None
|
|
1727
|
+
or not isinstance(current_fn, TpyFunction)
|
|
1728
|
+
or not current_fn.is_method
|
|
1729
|
+
or current_fn.is_staticmethod):
|
|
1730
|
+
return None
|
|
1731
|
+
current_rec = self.ctx.registry.get_record(current_rec_parse.name)
|
|
1732
|
+
if current_rec is None:
|
|
1733
|
+
return None
|
|
1734
|
+
|
|
1735
|
+
record_info = None
|
|
1736
|
+
if binding.kind == BindingKind.RECORD:
|
|
1737
|
+
record_info = self.ctx.registry.get_record(expr.obj.name)
|
|
1738
|
+
elif binding.kind == BindingKind.IMPORTED_NAME:
|
|
1739
|
+
import_info = self.ctx.imported_names.get(expr.obj.name)
|
|
1740
|
+
if import_info:
|
|
1741
|
+
record_info = self.ctx.registry.find_record_by_qname(
|
|
1742
|
+
f"{import_info[0]}.{import_info[1]}")
|
|
1743
|
+
if record_info is None:
|
|
1744
|
+
return None
|
|
1745
|
+
|
|
1746
|
+
# Walk BaseN's own MRO so `B.foo` resolves a field B inherits from
|
|
1747
|
+
# its own parent, matching Python's `B.foo` semantics.
|
|
1748
|
+
field_info = self.protocols.lookup_record_field(record_info, expr.field)
|
|
1749
|
+
if field_info is None:
|
|
1750
|
+
return None
|
|
1751
|
+
|
|
1752
|
+
# BaseN has the field but isn't an ancestor: the user clearly meant
|
|
1753
|
+
# unbound-self, so emit a targeted error rather than letting the
|
|
1754
|
+
# caller fall through to a generic "can't treat class as value".
|
|
1755
|
+
if not self.ctx.registry.is_subclass_of_record(current_rec, record_info):
|
|
1756
|
+
raise self.ctx.error(
|
|
1757
|
+
f"'{record_info.name}' is not an ancestor of '{current_rec.name}'; "
|
|
1758
|
+
f"cannot access '{record_info.name}.{expr.field}' here",
|
|
1759
|
+
expr,
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
parent_type, type_subst = self.protocols.resolve_ancestor_instantiation(
|
|
1763
|
+
current_rec, record_info)
|
|
1764
|
+
field_type = field_info.type
|
|
1765
|
+
if type_subst:
|
|
1766
|
+
field_type = self.type_ops.substitute_type_params(field_type, type_subst)
|
|
1767
|
+
|
|
1768
|
+
# Readonly self propagates into non-value reads so writes through
|
|
1769
|
+
# the result are rejected and references come back const.
|
|
1770
|
+
if current_fn.is_readonly and not field_type.is_value_type():
|
|
1771
|
+
if isinstance(field_type, PtrType) and not field_type.is_readonly:
|
|
1772
|
+
field_type = field_type.as_const()
|
|
1773
|
+
elif is_span(field_type) and not is_readonly_span(field_type):
|
|
1774
|
+
field_type = span_as_const(field_type)
|
|
1775
|
+
elif not isinstance(field_type, ReadonlyType):
|
|
1776
|
+
field_type = ReadonlyType(field_type)
|
|
1777
|
+
|
|
1778
|
+
expr.unbound_self_parent_type = parent_type
|
|
1779
|
+
return make_ref(field_type)
|
|
1780
|
+
|
|
1781
|
+
def _analyze_field_access(self, expr: TpyFieldAccess) -> TpyType:
|
|
1782
|
+
"""Analyze a field access."""
|
|
1783
|
+
# Check for module variable access (e.g., sys.argv)
|
|
1784
|
+
if isinstance(expr.obj, TpyName):
|
|
1785
|
+
if self.ctx.func.current_ns:
|
|
1786
|
+
binding = self.ctx.func.current_ns.lookup(expr.obj.name)
|
|
1787
|
+
if binding and binding.kind == BindingKind.MODULE:
|
|
1788
|
+
# Get actual module name (may differ from local name for aliased imports)
|
|
1789
|
+
module_name = binding.import_source[0] if binding.import_source else expr.obj.name
|
|
1790
|
+
module_info = self.ctx.registry.get_module(module_name)
|
|
1791
|
+
if module_info and expr.field in module_info.variables:
|
|
1792
|
+
return module_info.variables[expr.field].type
|
|
1793
|
+
# If not a variable, let it fall through to error at the end
|
|
1794
|
+
# (method calls are handled in _analyze_method_call)
|
|
1795
|
+
|
|
1796
|
+
# Enum type-level member access: Color.Red -> EnumType
|
|
1797
|
+
if binding and binding.kind == BindingKind.ENUM:
|
|
1798
|
+
enum_type = binding.enum_type
|
|
1799
|
+
if expr.field in enum_info_of(enum_type).members:
|
|
1800
|
+
return enum_type
|
|
1801
|
+
raise self.ctx.error(
|
|
1802
|
+
f"Enum '{enum_type.name}' has no member '{expr.field}'", expr)
|
|
1803
|
+
|
|
1804
|
+
# Nested type access on a record: Container.Kind, Container.Inner
|
|
1805
|
+
# Also check IMPORTED_NAME that resolves to a record (cross-module)
|
|
1806
|
+
if binding and binding.kind in (BindingKind.RECORD, BindingKind.IMPORTED_NAME):
|
|
1807
|
+
nested = self._resolve_nested_type_access(expr.obj.name, expr.field, expr)
|
|
1808
|
+
if nested is not None:
|
|
1809
|
+
return nested
|
|
1810
|
+
unbound = self._try_unbound_self_field_access(expr, binding)
|
|
1811
|
+
if unbound is not None:
|
|
1812
|
+
return unbound
|
|
1813
|
+
class_const = self._try_class_constant_access(expr, binding)
|
|
1814
|
+
if class_const is not None:
|
|
1815
|
+
return class_const
|
|
1816
|
+
|
|
1817
|
+
# Module-qualified class member access: m.Foo.CONST, pkg.sub.Foo.CONST.
|
|
1818
|
+
# Mirrors the method-call dispatcher's _try_resolve_module_qualified_class.
|
|
1819
|
+
# Codegen reads class_constant_owner (set inside the helper) to emit the
|
|
1820
|
+
# full C++ qname; the syntactic receiver shape doesn't matter from there.
|
|
1821
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
1822
|
+
resolved = self.methods._try_resolve_module_qualified_class(expr.obj)
|
|
1823
|
+
if resolved is not None:
|
|
1824
|
+
_module_name, _class_short, record_info = resolved
|
|
1825
|
+
class_const = self._class_constant_access_on_record(expr, record_info)
|
|
1826
|
+
if class_const is not None:
|
|
1827
|
+
return class_const
|
|
1828
|
+
|
|
1829
|
+
# `import pkg.sub` + `pkg.sub.X`: walk the chain to recover a dotted
|
|
1830
|
+
# module name and treat the leaf as a variable on that module.
|
|
1831
|
+
# Mirrors the method-call form that already works via
|
|
1832
|
+
# MethodAnalyzer._try_resolve_dotted_module.
|
|
1833
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
1834
|
+
dotted_name = self.methods._try_resolve_dotted_module(expr.obj)
|
|
1835
|
+
if dotted_name is not None:
|
|
1836
|
+
module_info = self.ctx.registry.get_module(dotted_name)
|
|
1837
|
+
if module_info is not None and expr.field in module_info.variables:
|
|
1838
|
+
expr.module_var_access = (dotted_name, expr.field)
|
|
1839
|
+
return module_info.variables[expr.field].type
|
|
1840
|
+
|
|
1841
|
+
# Handle chained nested type access: Outer.Mid.Inner.field
|
|
1842
|
+
if isinstance(expr.obj, TpyFieldAccess):
|
|
1843
|
+
chain = self._resolve_nested_chain(expr.obj)
|
|
1844
|
+
if chain is not None:
|
|
1845
|
+
# chain is a (dotted_name, type) for the intermediate nested type
|
|
1846
|
+
dotted_name, chain_type = chain
|
|
1847
|
+
if is_enum_type(chain_type):
|
|
1848
|
+
if expr.field in enum_info_of(chain_type).members:
|
|
1849
|
+
return chain_type
|
|
1850
|
+
if expr.field in ("name", "value"):
|
|
1851
|
+
pass # fall through to normal field access
|
|
1852
|
+
else:
|
|
1853
|
+
raise self.ctx.error(
|
|
1854
|
+
f"Enum '{chain_type.name}' has no member '{expr.field}'", expr)
|
|
1855
|
+
elif isinstance(chain_type, NominalType):
|
|
1856
|
+
# Try further nesting
|
|
1857
|
+
nested = self._resolve_nested_type_access(dotted_name, expr.field, expr)
|
|
1858
|
+
if nested is not None:
|
|
1859
|
+
return nested
|
|
1860
|
+
|
|
1861
|
+
obj_type = self.analyze_expr(expr.obj)
|
|
1862
|
+
|
|
1863
|
+
# Unwrap transparent wrappers
|
|
1864
|
+
is_readonly_obj = isinstance(obj_type, ReadonlyType)
|
|
1865
|
+
actual_type = unwrap_ref_type(obj_type)
|
|
1866
|
+
if isinstance(actual_type, ReadonlyType):
|
|
1867
|
+
actual_type = actual_type.wrapped
|
|
1868
|
+
if isinstance(actual_type, OwnType):
|
|
1869
|
+
actual_type = actual_type.wrapped
|
|
1870
|
+
if isinstance(actual_type, OptionalType):
|
|
1871
|
+
if actual_type.inner.is_value_type():
|
|
1872
|
+
raise self.ctx.error(f"Cannot access field '{expr.field}' on type {obj_type}", expr)
|
|
1873
|
+
self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
|
|
1874
|
+
expr.needs_optional_runtime_check = True
|
|
1875
|
+
actual_type = actual_type.inner
|
|
1876
|
+
|
|
1877
|
+
# Pending generic instance: field access is not allowed until resolved
|
|
1878
|
+
if isinstance(actual_type, PendingGenericInstanceType):
|
|
1879
|
+
raise self.ctx.error(
|
|
1880
|
+
f"Cannot access field '{expr.field}' on '{actual_type.record_name}' "
|
|
1881
|
+
f"until its type arguments are resolved; call a constraining method first "
|
|
1882
|
+
f"or add explicit type arguments to the constructor",
|
|
1883
|
+
expr,
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
# Enum instance property access: c.name -> str, c.value -> underlying type
|
|
1887
|
+
if is_enum_type(actual_type):
|
|
1888
|
+
if expr.field == "name":
|
|
1889
|
+
return STR
|
|
1890
|
+
elif expr.field == "value":
|
|
1891
|
+
return enum_info_of(actual_type).underlying_type
|
|
1892
|
+
raise self.ctx.error(
|
|
1893
|
+
f"Enum value of type '{actual_type.name}' has no attribute '{expr.field}'. "
|
|
1894
|
+
f"Use '{actual_type.name}.{expr.field}' to access enum members", expr)
|
|
1895
|
+
|
|
1896
|
+
# Deref chain loop -- resolves through Ptr (mutable and readonly) and any Deref[T] type
|
|
1897
|
+
current_type = actual_type
|
|
1898
|
+
deref_depth = 0
|
|
1899
|
+
while deref_depth <= 8:
|
|
1900
|
+
result = self._try_find_field(current_type, expr)
|
|
1901
|
+
if result is not None:
|
|
1902
|
+
# Class constants emit `<owner_qname>::<member>` ignoring
|
|
1903
|
+
# `obj`, so deref state, ptr-non-null narrowing,
|
|
1904
|
+
# ownership-from-self, and field-path narrowing don't apply.
|
|
1905
|
+
if expr.class_constant_owner is not None:
|
|
1906
|
+
return make_ref(result)
|
|
1907
|
+
expr.deref_depth = deref_depth
|
|
1908
|
+
if deref_depth > 0 and isinstance(actual_type, PtrType):
|
|
1909
|
+
obj_key = _expr_to_narrowing_key(expr.obj)
|
|
1910
|
+
if obj_key is not None:
|
|
1911
|
+
if obj_key in self.ctx.func.non_null_ptr_vars:
|
|
1912
|
+
expr.ptr_non_null = True
|
|
1913
|
+
if expr.loc:
|
|
1914
|
+
self.ctx.ptr_deref_facts[
|
|
1915
|
+
(expr.loc.line, obj_key)
|
|
1916
|
+
] = expr.ptr_non_null
|
|
1917
|
+
# Post-access narrowing: a successful deref here means
|
|
1918
|
+
# the pointer is non-null for *subsequent statements*;
|
|
1919
|
+
# queued for flush at statement boundary rather than
|
|
1920
|
+
# applied immediately to avoid unsafe elision between
|
|
1921
|
+
# unspecified-order siblings in the same expression.
|
|
1922
|
+
self.ctx.func.pending_non_null_ptr_vars.add(obj_key)
|
|
1923
|
+
# Propagate readonly: accessing a non-value field through a
|
|
1924
|
+
# readonly reference yields a readonly result.
|
|
1925
|
+
# Ptr[T] fields become Ptr[readonly[T]], Span[T] -> Span[readonly[T]].
|
|
1926
|
+
if is_readonly_obj:
|
|
1927
|
+
if isinstance(result, PtrType) and not result.is_readonly:
|
|
1928
|
+
result = result.as_const()
|
|
1929
|
+
elif is_span(result) and not is_readonly_span(result):
|
|
1930
|
+
result = span_as_const(result)
|
|
1931
|
+
elif not result.is_value_type():
|
|
1932
|
+
result = ReadonlyType(unwrap_readonly(result))
|
|
1933
|
+
# Ownership propagation: in a consuming method (self: Own[Self]),
|
|
1934
|
+
# self.field yields Own[FieldType] since the struct is being consumed.
|
|
1935
|
+
if (self.ctx.in_consuming_method
|
|
1936
|
+
and not is_readonly_obj
|
|
1937
|
+
and isinstance(expr.obj, TpyName) and expr.obj.name == "self"
|
|
1938
|
+
and not result.is_value_type()):
|
|
1939
|
+
result = OwnType(result)
|
|
1940
|
+
# Apply field path narrowing (e.g. after `if obj.field is not None:`)
|
|
1941
|
+
field_key = _expr_to_narrowing_key(expr)
|
|
1942
|
+
if field_key is not None:
|
|
1943
|
+
narrowed = self.ctx.func.narrowed_types.get(field_key)
|
|
1944
|
+
if narrowed is not None:
|
|
1945
|
+
result = narrowed
|
|
1946
|
+
return make_ref(result)
|
|
1947
|
+
|
|
1948
|
+
deref_target = self.get_deref_target_type(
|
|
1949
|
+
current_type, is_readonly=is_readonly_obj)
|
|
1950
|
+
if deref_target is None:
|
|
1951
|
+
break
|
|
1952
|
+
# __deref__() may return readonly[T]; unwrap and propagate
|
|
1953
|
+
# readonly so field access enforces const semantics.
|
|
1954
|
+
if isinstance(deref_target, ReadonlyType):
|
|
1955
|
+
is_readonly_obj = True
|
|
1956
|
+
deref_target = deref_target.wrapped
|
|
1957
|
+
# Deref-view narrowing: a field declared only on the narrowed
|
|
1958
|
+
# subclass resolves through the cast (see the method-call path).
|
|
1959
|
+
nsub = deref_view_narrowed(self.ctx, expr.obj, deref_target)
|
|
1960
|
+
if nsub is not None:
|
|
1961
|
+
expr.deref_narrowed_to = nsub
|
|
1962
|
+
current_type = nsub
|
|
1963
|
+
else:
|
|
1964
|
+
current_type = deref_target
|
|
1965
|
+
deref_depth += 1
|
|
1966
|
+
|
|
1967
|
+
# D16 dynamic-attribute fallback: if the receiver is a record with
|
|
1968
|
+
# __getattr__ reachable via MRO, route the access through the dunder
|
|
1969
|
+
# as a synthesized method call (parallel to property routing). Fires
|
|
1970
|
+
# only after the static-resolution + deref loop has failed.
|
|
1971
|
+
if isinstance(actual_type, NominalType) and actual_type.is_record:
|
|
1972
|
+
dyn_result = self._try_dyn_getattr(actual_type, expr)
|
|
1973
|
+
if dyn_result is not None:
|
|
1974
|
+
return make_ref(dyn_result)
|
|
1975
|
+
raise self.ctx.error(f"Record '{actual_type.name}' has no field '{expr.field}'", expr)
|
|
1976
|
+
raise self.ctx.error(f"Cannot access field '{expr.field}' on type {obj_type}", expr)
|
|
1977
|
+
|
|
1978
|
+
def _try_dyn_getattr(self, typ: NominalType, expr: TpyFieldAccess) -> TpyType | None:
|
|
1979
|
+
"""D16 Phase 1: try routing `obj.field` through __getattr__.
|
|
1980
|
+
|
|
1981
|
+
Returns the dunder's substituted return type (and stashes the
|
|
1982
|
+
synthesized TpyMethodCall on `expr.dyn_getattr_call`) if the
|
|
1983
|
+
receiver's class has `__getattr__` via MRO; None otherwise.
|
|
1984
|
+
"""
|
|
1985
|
+
record = self.ctx.registry.get_record_for_type(typ)
|
|
1986
|
+
if record is None:
|
|
1987
|
+
return None
|
|
1988
|
+
overloads, _ = self.protocols.lookup_record_method_overloads(
|
|
1989
|
+
record, "__getattr__")
|
|
1990
|
+
if not overloads:
|
|
1991
|
+
return None
|
|
1992
|
+
# Delegate to method-call analysis so @readonly enforcement, mutation
|
|
1993
|
+
# propagation, and call-edge recording all fire uniformly. Returns
|
|
1994
|
+
# the (substituted) dunder return type. Bypassing this path was the
|
|
1995
|
+
# original D16 v1 review gap.
|
|
1996
|
+
getter_call = TpyMethodCall(
|
|
1997
|
+
obj=expr.obj,
|
|
1998
|
+
method="__getattr__",
|
|
1999
|
+
args=[TpyStrLiteral(value=expr.field)],
|
|
2000
|
+
loc=expr.loc,
|
|
2001
|
+
)
|
|
2002
|
+
ret_type = self.analyze_expr(getter_call)
|
|
2003
|
+
expr.dyn_getattr_call = getter_call
|
|
2004
|
+
return ret_type
|
|
2005
|
+
|
|
2006
|
+
def _analyze_array_literal(
|
|
2007
|
+
self, expr: TpyArrayLiteral, expected_elem: TpyType | None = None
|
|
2008
|
+
) -> TpyType:
|
|
2009
|
+
"""Analyze an array literal [expr, expr, ...]
|
|
2010
|
+
|
|
2011
|
+
In function-local contexts, returns a PendingListType that will be
|
|
2012
|
+
resolved to Array or list based on usage (mutation, parameter passing).
|
|
2013
|
+
In global/module context, returns ListType directly.
|
|
2014
|
+
|
|
2015
|
+
Args:
|
|
2016
|
+
expected_elem: When provided (from a type annotation or return type hint),
|
|
2017
|
+
each element is checked against this type instead of against the first
|
|
2018
|
+
element. Enables mixed-type literals like [Int32(1), None] when the
|
|
2019
|
+
annotation is list[Int32 | None].
|
|
2020
|
+
"""
|
|
2021
|
+
if not expr.elements:
|
|
2022
|
+
if not is_body_like_scope(self.ctx.func.current_function):
|
|
2023
|
+
raise self.ctx.error("Empty array literal requires explicit type annotation", expr)
|
|
2024
|
+
# Empty list with no annotation -- create PendingListType with unknown
|
|
2025
|
+
# element type. The element type will be inferred from subsequent usage
|
|
2026
|
+
# (e.g. .append(v), xs[i] = v, param context, return context).
|
|
2027
|
+
|
|
2028
|
+
# Reuse an existing PendingListType for this expr if one was
|
|
2029
|
+
# already created. analyze_expr can be called multiple times for
|
|
2030
|
+
# the same arg expr (pre-overload arg-type collection, then post-
|
|
2031
|
+
# overload _typecheck_call_args). Without this cache, each call
|
|
2032
|
+
# mints a new literal_id and a new ListLiteralInfo added to
|
|
2033
|
+
# pending_resolutions -- coercion writes to one, but the resolver
|
|
2034
|
+
# still fails on the stale one.
|
|
2035
|
+
cached = self.ctx.get_expr_type(expr)
|
|
2036
|
+
if isinstance(cached, PendingListType) and cached.size == 0:
|
|
2037
|
+
return cached
|
|
2038
|
+
literal_id = self.ctx.literal_counter
|
|
2039
|
+
self.ctx.literal_counter += 1
|
|
2040
|
+
info = ListLiteralInfo(
|
|
2041
|
+
literal_id=literal_id,
|
|
2042
|
+
expr=expr,
|
|
2043
|
+
element_type=UNKNOWN_ELEMENT,
|
|
2044
|
+
size=0,
|
|
2045
|
+
is_mutated=True, # empty list is always list, never Array
|
|
2046
|
+
)
|
|
2047
|
+
self.ctx.list_literals[literal_id] = info
|
|
2048
|
+
self.ctx.func.pending_resolutions.append(literal_id)
|
|
2049
|
+
typ = PendingListType(UNKNOWN_ELEMENT, 0, literal_id)
|
|
2050
|
+
self.ctx.set_expr_type(expr, typ)
|
|
2051
|
+
return typ
|
|
2052
|
+
|
|
2053
|
+
# Analyze all elements, propagating expected type as hint when available
|
|
2054
|
+
if expected_elem is not None:
|
|
2055
|
+
elem_types = [self.analyze_expr_with_hint(e, expected_elem) for e in expr.elements]
|
|
2056
|
+
else:
|
|
2057
|
+
elem_types = [self.analyze_expr(e) for e in expr.elements]
|
|
2058
|
+
|
|
2059
|
+
# Strip OwnType wrappers -- _apply_own_wrapper marks non-last-use
|
|
2060
|
+
# refs as Own[T], but element type compatibility must compare the
|
|
2061
|
+
# underlying types (codegen handles the move/copy distinction).
|
|
2062
|
+
elem_types = [unwrap_own(t) for t in elem_types]
|
|
2063
|
+
|
|
2064
|
+
if expected_elem is not None:
|
|
2065
|
+
# Contextual mode: check each element against expected element type
|
|
2066
|
+
first_type = expected_elem
|
|
2067
|
+
for i, elem_type in enumerate(elem_types, 1):
|
|
2068
|
+
if elem_type == expected_elem:
|
|
2069
|
+
continue
|
|
2070
|
+
# Subclass coercion excluded: storing Child in list[Base] silently
|
|
2071
|
+
# slices objects (same invariance as dict/set). A covariant-generic
|
|
2072
|
+
# wrapper upcast (Box[Dog] -> Box[Pet]) is exempt -- it's a
|
|
2073
|
+
# representation-preserving converting move, not slicing -- and
|
|
2074
|
+
# falls through to check_type_compatible below (which the append
|
|
2075
|
+
# path already uses).
|
|
2076
|
+
if (isinstance(elem_type, NominalType) and elem_type.is_user_record
|
|
2077
|
+
and isinstance(expected_elem, NominalType) and expected_elem.is_user_record
|
|
2078
|
+
and not self.compat.is_covariant_generic_upcast(elem_type, expected_elem)):
|
|
2079
|
+
raise self.ctx.error(
|
|
2080
|
+
f"List literal element {i} has type {elem_type}, "
|
|
2081
|
+
f"incompatible with annotated element type {expected_elem}", expr
|
|
2082
|
+
)
|
|
2083
|
+
try:
|
|
2084
|
+
self.compat.check_type_compatible(
|
|
2085
|
+
elem_type, expected_elem,
|
|
2086
|
+
f"array literal element {i}",
|
|
2087
|
+
expr.loc,
|
|
2088
|
+
source_expr=expr.elements[i - 1],
|
|
2089
|
+
)
|
|
2090
|
+
except SemanticError:
|
|
2091
|
+
raise self.ctx.error(
|
|
2092
|
+
f"List literal element {i} has type {elem_type}, "
|
|
2093
|
+
f"incompatible with annotated element type {expected_elem}", expr
|
|
2094
|
+
)
|
|
2095
|
+
else:
|
|
2096
|
+
# Inferred mode: check all elements against first element's type
|
|
2097
|
+
first_type = elem_types[0]
|
|
2098
|
+
# Keep IntLiteralType so array can coerce to either Int32 or BigInt based on context
|
|
2099
|
+
|
|
2100
|
+
for i, elem_type in enumerate(elem_types[1:], 2):
|
|
2101
|
+
# Literal-aware unification (int/float literals, tuples of literals)
|
|
2102
|
+
unified = self._unify_literal_types(first_type, elem_type)
|
|
2103
|
+
if unified is not None:
|
|
2104
|
+
first_type = unified
|
|
2105
|
+
continue
|
|
2106
|
+
# Nested lists with IntLiteralType elements are compatible
|
|
2107
|
+
if (is_list(first_type) and is_list(elem_type) and
|
|
2108
|
+
isinstance(first_type.element_type, IntLiteralType) and
|
|
2109
|
+
isinstance(elem_type.element_type, IntLiteralType)):
|
|
2110
|
+
continue
|
|
2111
|
+
# PendingListTypes with compatible element types are compatible
|
|
2112
|
+
if (isinstance(first_type, PendingListType) and isinstance(elem_type, PendingListType) and
|
|
2113
|
+
first_type.size == elem_type.size):
|
|
2114
|
+
# IntLiteralType elements are compatible regardless of value
|
|
2115
|
+
if (isinstance(first_type.element_type, IntLiteralType) and
|
|
2116
|
+
isinstance(elem_type.element_type, IntLiteralType)):
|
|
2117
|
+
continue
|
|
2118
|
+
if first_type.element_type == elem_type.element_type:
|
|
2119
|
+
continue
|
|
2120
|
+
if elem_type != first_type:
|
|
2121
|
+
ft = self._user_type_name(first_type)
|
|
2122
|
+
et = self._user_type_name(elem_type)
|
|
2123
|
+
raise self.ctx.error(
|
|
2124
|
+
f"List literal has mixed types: element {i} is {et}, "
|
|
2125
|
+
f"but earlier elements are {ft}. "
|
|
2126
|
+
f"Use a type annotation like list[{ft} | {et}]", expr
|
|
2127
|
+
)
|
|
2128
|
+
|
|
2129
|
+
size = len(expr.elements)
|
|
2130
|
+
|
|
2131
|
+
# Global context (no current function) -> ListType (std::vector)
|
|
2132
|
+
# Keep IntLiteralType to allow coercion to Int32 when annotation is present
|
|
2133
|
+
if self.ctx.func.current_function is None:
|
|
2134
|
+
return make_list(first_type)
|
|
2135
|
+
|
|
2136
|
+
# Function-local context -> create PendingListType for deferred resolution
|
|
2137
|
+
literal_id = self.ctx.literal_counter
|
|
2138
|
+
self.ctx.literal_counter += 1
|
|
2139
|
+
|
|
2140
|
+
info = ListLiteralInfo(
|
|
2141
|
+
literal_id=literal_id,
|
|
2142
|
+
expr=expr,
|
|
2143
|
+
element_type=first_type,
|
|
2144
|
+
size=size,
|
|
2145
|
+
is_global=self.ctx.is_top_level
|
|
2146
|
+
)
|
|
2147
|
+
self.ctx.list_literals[literal_id] = info
|
|
2148
|
+
self.ctx.func.pending_resolutions.append(literal_id)
|
|
2149
|
+
|
|
2150
|
+
return PendingListType(first_type, size, literal_id)
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
# -- await -----------------------------------------------------------
|
|
2154
|
+
|
|
2155
|
+
def _analyze_await(self, expr: TpyAwait) -> TpyType:
|
|
2156
|
+
"""Analyze `await x`.
|
|
2157
|
+
|
|
2158
|
+
v1 (Commit 3 of PR 3) supports statically-resolvable awaits only:
|
|
2159
|
+
the operand must be a direct call to a known async def. Type-erased
|
|
2160
|
+
awaits (Awaitable[T] params, Task[T], unions) lower via
|
|
2161
|
+
AsyncFrameBase<T> in PR 4.
|
|
2162
|
+
|
|
2163
|
+
The await expression's type is the awaited async def's declared
|
|
2164
|
+
return type. The compiler tags the TpyAwait node with
|
|
2165
|
+
`awaited_async_func` so codegen can resolve the sub-coroutine
|
|
2166
|
+
struct and emit the in-frame `std::optional<__SubCoro>` field.
|
|
2167
|
+
"""
|
|
2168
|
+
cur = self.ctx.func.current_function
|
|
2169
|
+
if not (isinstance(cur, TpyFunction) and cur.is_async):
|
|
2170
|
+
raise self.ctx.error(
|
|
2171
|
+
"'await' is only allowed inside an `async def` function body; "
|
|
2172
|
+
"use asyncio.run(coro) at the top level to drive a coroutine",
|
|
2173
|
+
expr)
|
|
2174
|
+
# Two supported v1 shapes:
|
|
2175
|
+
# 1. Inline: operand is a direct call to a known async def.
|
|
2176
|
+
# 2. Erased: operand has type Task[T] (heap-allocated frame).
|
|
2177
|
+
operand = expr.value
|
|
2178
|
+
async_fi = self._resolve_call_to_async_def(operand)
|
|
2179
|
+
if async_fi is not None:
|
|
2180
|
+
# Recursively analyze the operand call (validates arg types and,
|
|
2181
|
+
# for generic async defs, infers and substitutes type args into
|
|
2182
|
+
# the operand's `resolved_function_info`).
|
|
2183
|
+
self.analyze_expr(operand)
|
|
2184
|
+
expr.awaited_async_func_name = async_fi.name
|
|
2185
|
+
# Prefer the operand's substituted FunctionInfo's return type;
|
|
2186
|
+
# for generic async defs, `async_fi.return_type` from the
|
|
2187
|
+
# registry still carries unsubstituted TypeParamRefs.
|
|
2188
|
+
resolved_fi = getattr(operand, "resolved_function_info", None)
|
|
2189
|
+
source_fi = resolved_fi if resolved_fi is not None else async_fi
|
|
2190
|
+
return self._unwrap_awaitable_return(source_fi.return_type)
|
|
2191
|
+
|
|
2192
|
+
# Type-erased path: analyze operand. Supported v1 erased forms:
|
|
2193
|
+
# - tpy.Task[T] (heap-erased coroutine frame)
|
|
2194
|
+
# - asyncio.Future[T] (manual-completion awaitable)
|
|
2195
|
+
# - any record type with `poll(self, w: Waker) -> Poll[T]`
|
|
2196
|
+
# (structural Awaitable -- supports user-written awaitables
|
|
2197
|
+
# alongside hand-written awaiter types).
|
|
2198
|
+
operand_type = self.analyze_expr(operand)
|
|
2199
|
+
|
|
2200
|
+
# Method-call shape: `await obj.method()`. The free-function probe
|
|
2201
|
+
# above only matches bare-name calls; method calls land here. After
|
|
2202
|
+
# analyze_expr, the method call carries `resolved_function_info`;
|
|
2203
|
+
# treat an async one like the inline shape.
|
|
2204
|
+
if isinstance(operand, TpyMethodCall):
|
|
2205
|
+
mfi = operand.resolved_function_info
|
|
2206
|
+
if mfi is not None and mfi.is_async:
|
|
2207
|
+
# Receiver must be a stable lvalue: the coro captures it as
|
|
2208
|
+
# `<Class>&` across polls, so a temporary would dangle at
|
|
2209
|
+
# the emplace expression's semicolon. Mirrors
|
|
2210
|
+
# `AsyncCoroCodegen._is_stable_lvalue` in codegen.
|
|
2211
|
+
if not is_stable_address_lvalue(operand.obj):
|
|
2212
|
+
raise self.ctx.error(
|
|
2213
|
+
"receiver of an awaited async method must be "
|
|
2214
|
+
"a stable lvalue (a local, parameter, or "
|
|
2215
|
+
"field chain rooted at one) -- the coro "
|
|
2216
|
+
"captures it by reference across "
|
|
2217
|
+
"suspensions, so a temporary would dangle. "
|
|
2218
|
+
"Bind the receiver to a local first: "
|
|
2219
|
+
"`r = <expr>; await r.method(...)`",
|
|
2220
|
+
operand)
|
|
2221
|
+
expr.awaited_async_func_name = mfi.name
|
|
2222
|
+
owner_type = self._method_call_receiver_type(operand)
|
|
2223
|
+
if owner_type is not None:
|
|
2224
|
+
expr.awaited_method_owner_type = owner_type
|
|
2225
|
+
if mfi.return_type is not None:
|
|
2226
|
+
return self._unwrap_awaitable_return(mfi.return_type)
|
|
2227
|
+
# An Own[Task[T]] rvalue (e.g. `await asyncio.create_task(...)`)
|
|
2228
|
+
# is a valid await operand -- strip the Own[] before structural
|
|
2229
|
+
# matching so the inner Task[T] / Awaitable conformance check fires.
|
|
2230
|
+
unwrapped = unwrap_own(unwrap_ref_type(operand_type))
|
|
2231
|
+
if isinstance(unwrapped, NominalType):
|
|
2232
|
+
inner = self._extract_awaitable_inner(unwrapped)
|
|
2233
|
+
if inner is not None:
|
|
2234
|
+
if isinstance(inner, TpyType):
|
|
2235
|
+
expr.awaited_task_inner = inner
|
|
2236
|
+
return inner
|
|
2237
|
+
raise self.ctx.error(
|
|
2238
|
+
"await operand must be a direct call to an async def, a "
|
|
2239
|
+
"Task[T] / Future[T], or a value of a type with a "
|
|
2240
|
+
"`__poll__(self, waker: Waker) -> Own[Poll[T]]` method",
|
|
2241
|
+
expr)
|
|
2242
|
+
|
|
2243
|
+
def _unwrap_awaitable_return(self, ret_type: 'TpyType') -> 'TpyType':
|
|
2244
|
+
"""Strip `Awaitable[T]` / `Cancellable[T]` wrapping from an async
|
|
2245
|
+
def's return type. Cancellable is the post-registration shape of
|
|
2246
|
+
every async-def call result; Awaitable is the shape user types
|
|
2247
|
+
with just `__poll__` declare. Both unwrap to `T` for `await`."""
|
|
2248
|
+
ret = unwrap_ref_type(ret_type)
|
|
2249
|
+
if (isinstance(ret, NominalType)
|
|
2250
|
+
and ret.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
|
|
2251
|
+
and len(ret.type_args) == 1):
|
|
2252
|
+
return ret.type_args[0]
|
|
2253
|
+
return ret
|
|
2254
|
+
|
|
2255
|
+
def _extract_awaitable_inner(self, typ) -> 'TpyType | None':
|
|
2256
|
+
"""Return the awaited type T if `typ` conforms to Awaitable[T],
|
|
2257
|
+
else None.
|
|
2258
|
+
|
|
2259
|
+
Structural: any record with `__poll__(self, waker: Waker) -> Own[Poll[T]]`.
|
|
2260
|
+
Covers tpy.Task[T] (qname `tpy.Task` -> the @builtin_type stub in
|
|
2261
|
+
asyncio._executor), user-defined Future[T] / Event-like types, and
|
|
2262
|
+
any other record that satisfies the Awaitable protocol. The Own
|
|
2263
|
+
wrapper is required because Poll[T] is @nocopy (v1.2 step 5).
|
|
2264
|
+
"""
|
|
2265
|
+
from ..typesys import NominalType, TpyType as _TpyType
|
|
2266
|
+
# Structural: look up the record and check for a __poll__ method
|
|
2267
|
+
# whose signature matches Awaitable[T].
|
|
2268
|
+
record_info = self.ctx.registry.get_record_for_type(typ)
|
|
2269
|
+
if record_info is None:
|
|
2270
|
+
return None
|
|
2271
|
+
poll_overloads = record_info.get_method_overloads("__poll__")
|
|
2272
|
+
if not poll_overloads:
|
|
2273
|
+
return None
|
|
2274
|
+
# Pick the first overload whose return type is Poll[T] for some T.
|
|
2275
|
+
# Substitute the record's class-level type params with typ.type_args
|
|
2276
|
+
# so `Future[Int32]` returns Int32, not the type-var T.
|
|
2277
|
+
from ..typesys import unwrap_ref_type
|
|
2278
|
+
type_subst: dict[str, _TpyType] = {}
|
|
2279
|
+
if record_info.type_params and len(typ.type_args) == len(record_info.type_params):
|
|
2280
|
+
for tp, arg in zip(record_info.type_params, typ.type_args):
|
|
2281
|
+
if isinstance(arg, _TpyType):
|
|
2282
|
+
type_subst[tp] = arg
|
|
2283
|
+
for fi in poll_overloads:
|
|
2284
|
+
ret = fi.return_type
|
|
2285
|
+
if ret is None:
|
|
2286
|
+
continue
|
|
2287
|
+
# Poll[T] is @nocopy, so a bare `-> Poll[T]` is a reference
|
|
2288
|
+
# return -- skip non-Own returns so the user gets the "no
|
|
2289
|
+
# __poll__ method" diagnostic (which prints the correct
|
|
2290
|
+
# `Own[Poll[T]]` signature) instead of a C++ build failure.
|
|
2291
|
+
ret_outer = unwrap_ref_type(ret)
|
|
2292
|
+
if not isinstance(ret_outer, OwnType):
|
|
2293
|
+
continue
|
|
2294
|
+
ret = unwrap_own(ret_outer)
|
|
2295
|
+
if (isinstance(ret, NominalType)
|
|
2296
|
+
and ret._module_qname == qnames.POLL
|
|
2297
|
+
and len(ret.type_args) == 1):
|
|
2298
|
+
# Recursively substitute T -> typ.type_args[i] -- handles
|
|
2299
|
+
# both bare TypeParamRef and nested shapes like list[T],
|
|
2300
|
+
# tuple[T, U], etc.
|
|
2301
|
+
return _substitute_type_params(ret.type_args[0], type_subst)
|
|
2302
|
+
return None
|
|
2303
|
+
|
|
2304
|
+
def _method_call_receiver_type(self, call) -> 'NominalType | None':
|
|
2305
|
+
"""For a TpyMethodCall whose receiver resolves to a known record,
|
|
2306
|
+
return the receiver's NominalType (carrying class-level
|
|
2307
|
+
type_args). Used to name and qualify async-method coro structs
|
|
2308
|
+
as `__coro_<Record>_<method>` plus a `<owner_type_args>` suffix
|
|
2309
|
+
for receivers with non-empty class-level type args.
|
|
2310
|
+
"""
|
|
2311
|
+
recv_type = self.ctx.get_expr_type(call.obj)
|
|
2312
|
+
if recv_type is None:
|
|
2313
|
+
return None
|
|
2314
|
+
inner = unwrap_own(unwrap_ref_type(recv_type))
|
|
2315
|
+
if isinstance(inner, NominalType):
|
|
2316
|
+
return inner
|
|
2317
|
+
return None
|
|
2318
|
+
|
|
2319
|
+
def _resolve_call_to_async_def(self, operand) -> 'FunctionInfo | None':
|
|
2320
|
+
"""If operand is a direct TpyCall whose target is a known async def,
|
|
2321
|
+
return its FunctionInfo. Otherwise return None.
|
|
2322
|
+
|
|
2323
|
+
Free-function call shape only -- method calls (`await obj.m()`)
|
|
2324
|
+
are detected after `analyze_expr` populates the
|
|
2325
|
+
`TpyMethodCall.resolved_function_info` field; that branch lives
|
|
2326
|
+
in `analyze_await` above.
|
|
2327
|
+
"""
|
|
2328
|
+
from ..parse.nodes import TpyCall
|
|
2329
|
+
if not isinstance(operand, TpyCall):
|
|
2330
|
+
return None
|
|
2331
|
+
func_name = operand.maybe_func_name
|
|
2332
|
+
if not func_name:
|
|
2333
|
+
return None
|
|
2334
|
+
overloads = self.ctx.registry.get_function(func_name)
|
|
2335
|
+
if not overloads:
|
|
2336
|
+
return None
|
|
2337
|
+
# Async defs do not participate in @overload, so a single match is OK.
|
|
2338
|
+
for fi in overloads:
|
|
2339
|
+
if fi.is_async:
|
|
2340
|
+
return fi
|
|
2341
|
+
return None
|
|
2342
|
+
|
|
2343
|
+
# -- Ternary expression analysis ------------------------------------------
|
|
2344
|
+
|
|
2345
|
+
def _analyze_named_expr(self, expr: TpyNamedExpr) -> TpyType:
|
|
2346
|
+
"""Analyze walrus operator: (x := expr)."""
|
|
2347
|
+
value_type = self.analyze_expr(expr.value)
|
|
2348
|
+
name = expr.target
|
|
2349
|
+
|
|
2350
|
+
# Resolve pending/literal types for the variable binding
|
|
2351
|
+
resolved = value_type
|
|
2352
|
+
if isinstance(resolved, IntLiteralType):
|
|
2353
|
+
resolved = self.ctx.default_int_type
|
|
2354
|
+
elif isinstance(resolved, FloatLiteralType):
|
|
2355
|
+
resolved = FLOAT
|
|
2356
|
+
elif isinstance(resolved, PendingViewType):
|
|
2357
|
+
resolved = resolved.family.owned_type
|
|
2358
|
+
|
|
2359
|
+
# PEP 572: walrus in comprehension leaks to enclosing function scope
|
|
2360
|
+
target_scope = self.ctx.func.current_scope
|
|
2361
|
+
levels = self.ctx.in_comprehension
|
|
2362
|
+
while levels > 0 and target_scope.parent is not None:
|
|
2363
|
+
target_scope = target_scope.parent
|
|
2364
|
+
levels -= 1
|
|
2365
|
+
|
|
2366
|
+
existing = target_scope.lookup(name)
|
|
2367
|
+
if existing is not None:
|
|
2368
|
+
# Reassignment via walrus -- keep existing type
|
|
2369
|
+
pass
|
|
2370
|
+
else:
|
|
2371
|
+
# New binding
|
|
2372
|
+
target_scope.define(name, resolved)
|
|
2373
|
+
if self.ctx.func.current_ns:
|
|
2374
|
+
self.ctx.func.current_ns.bind_variable(name, resolved)
|
|
2375
|
+
|
|
2376
|
+
self.ctx.func.definitely_assigned.add(name)
|
|
2377
|
+
self.ctx.func.rvalue_vars.add(name)
|
|
2378
|
+
if name not in self.ctx.func.var_scope_depth:
|
|
2379
|
+
self.ctx.func.var_scope_depth[name] = target_scope.depth
|
|
2380
|
+
|
|
2381
|
+
return value_type
|
|
2382
|
+
|
|
2383
|
+
def _analyze_if_expr(
|
|
2384
|
+
self, expr: TpyIfExpr, type_hint: TpyType | None = None,
|
|
2385
|
+
) -> TpyType:
|
|
2386
|
+
"""Analyze a ternary conditional: then_expr if condition else else_expr."""
|
|
2387
|
+
self.analyze_expr(expr.condition)
|
|
2388
|
+
self.narrowing.warn_truthy_value_optionals(expr.condition)
|
|
2389
|
+
|
|
2390
|
+
then_facts, else_facts = self.narrowing.condition_type_facts(
|
|
2391
|
+
expr.condition)
|
|
2392
|
+
|
|
2393
|
+
# Save narrowed_types (ternary doesn't create vars, so we only
|
|
2394
|
+
# need to save/restore narrowing, not the full InitTracker state).
|
|
2395
|
+
saved_narrowed = dict(self.ctx.func.narrowed_types)
|
|
2396
|
+
|
|
2397
|
+
self.ctx.func.narrowed_types.update(then_facts)
|
|
2398
|
+
if type_hint is not None:
|
|
2399
|
+
then_type = self.analyze_expr_with_hint(expr.then_expr, type_hint)
|
|
2400
|
+
else:
|
|
2401
|
+
then_type = self.analyze_expr(expr.then_expr)
|
|
2402
|
+
|
|
2403
|
+
self.ctx.func.narrowed_types = dict(saved_narrowed)
|
|
2404
|
+
self.ctx.func.narrowed_types.update(else_facts)
|
|
2405
|
+
if type_hint is not None:
|
|
2406
|
+
else_type = self.analyze_expr_with_hint(expr.else_expr, type_hint)
|
|
2407
|
+
else:
|
|
2408
|
+
else_type = self.analyze_expr(expr.else_expr)
|
|
2409
|
+
|
|
2410
|
+
self.ctx.func.narrowed_types = saved_narrowed
|
|
2411
|
+
|
|
2412
|
+
# Strip Ref/Own from branch types -- these are provenance qualifiers,
|
|
2413
|
+
# not part of the result type. The ternary produces a value.
|
|
2414
|
+
then_type = unwrap_ref_type(then_type)
|
|
2415
|
+
if isinstance(then_type, OwnType):
|
|
2416
|
+
then_type = then_type.wrapped
|
|
2417
|
+
else_type = unwrap_ref_type(else_type)
|
|
2418
|
+
if isinstance(else_type, OwnType):
|
|
2419
|
+
else_type = else_type.wrapped
|
|
2420
|
+
|
|
2421
|
+
common = self._ternary_common_type(expr, then_type, else_type,
|
|
2422
|
+
widen_numeric_types)
|
|
2423
|
+
|
|
2424
|
+
if is_list(common):
|
|
2425
|
+
# Both ternary branches must share a C++ type; force sources to
|
|
2426
|
+
# ListType so they don't independently become incompatible Arrays.
|
|
2427
|
+
for t in collect_pending_source_types(self.ctx, expr):
|
|
2428
|
+
if isinstance(t, PendingListType):
|
|
2429
|
+
info = self.ctx.list_literals.get(t.literal_id)
|
|
2430
|
+
if info is not None:
|
|
2431
|
+
info.needs_list_type = True
|
|
2432
|
+
|
|
2433
|
+
# Coerce branches to the common type so C++ ternary has
|
|
2434
|
+
# matching branch types (e.g. None -> std::optional<T>).
|
|
2435
|
+
if then_type != common:
|
|
2436
|
+
expr.then_expr = self.compat.coerce_expr(
|
|
2437
|
+
expr.then_expr, then_type, common,
|
|
2438
|
+
"ternary branch", coercion_ctx=CoercionContext.INIT)
|
|
2439
|
+
if else_type != common:
|
|
2440
|
+
expr.else_expr = self.compat.coerce_expr(
|
|
2441
|
+
expr.else_expr, else_type, common,
|
|
2442
|
+
"ternary branch", coercion_ctx=CoercionContext.INIT)
|
|
2443
|
+
|
|
2444
|
+
return common
|
|
2445
|
+
|
|
2446
|
+
def _ternary_common_type(
|
|
2447
|
+
self, expr: TpyIfExpr,
|
|
2448
|
+
then_type: TpyType, else_type: TpyType,
|
|
2449
|
+
widen_numeric_types: object,
|
|
2450
|
+
) -> TpyType:
|
|
2451
|
+
"""Compute the common result type of a ternary expression's branches."""
|
|
2452
|
+
if then_type == else_type:
|
|
2453
|
+
return then_type
|
|
2454
|
+
|
|
2455
|
+
t, e = then_type, else_type
|
|
2456
|
+
|
|
2457
|
+
# IntLiteral resolution
|
|
2458
|
+
if isinstance(t, IntLiteralType) and isinstance(e, IntLiteralType):
|
|
2459
|
+
return self.ctx.default_int_for_literal(t, expr.then_expr)
|
|
2460
|
+
if isinstance(t, IntLiteralType):
|
|
2461
|
+
if is_integer_type(e) or is_float_type(e):
|
|
2462
|
+
return e
|
|
2463
|
+
t = self.ctx.default_int_for_literal(t, expr.then_expr)
|
|
2464
|
+
if isinstance(e, IntLiteralType):
|
|
2465
|
+
if is_integer_type(t) or is_float_type(t):
|
|
2466
|
+
return t
|
|
2467
|
+
e = self.ctx.default_int_for_literal(e, expr.else_expr)
|
|
2468
|
+
|
|
2469
|
+
# FloatLiteral resolution: adapts to the concrete float type in context
|
|
2470
|
+
if isinstance(t, FloatLiteralType) and isinstance(e, FloatLiteralType):
|
|
2471
|
+
return FLOAT
|
|
2472
|
+
if isinstance(t, FloatLiteralType):
|
|
2473
|
+
if is_float_type(e):
|
|
2474
|
+
return e
|
|
2475
|
+
t = FLOAT
|
|
2476
|
+
if isinstance(e, FloatLiteralType):
|
|
2477
|
+
if is_float_type(t):
|
|
2478
|
+
return t
|
|
2479
|
+
e = FLOAT
|
|
2480
|
+
|
|
2481
|
+
# Normalize PendingViewType to its owned type for comparison; preserve
|
|
2482
|
+
# the pending type when both sides are pending so view deduction can
|
|
2483
|
+
# chain the result variable back to the operands' resolution.
|
|
2484
|
+
if isinstance(t, PendingViewType) and isinstance(e, PendingViewType) and t.family is e.family:
|
|
2485
|
+
return t
|
|
2486
|
+
if isinstance(t, PendingViewType):
|
|
2487
|
+
t = t.family.owned_type
|
|
2488
|
+
if isinstance(e, PendingViewType):
|
|
2489
|
+
e = e.family.owned_type
|
|
2490
|
+
# Normalize pending container types to concrete types for equality comparison,
|
|
2491
|
+
# resolving IntLiteralType elements so [1,2] and [3,4] both normalize to list[int].
|
|
2492
|
+
t = self._normalize_pending_container(t)
|
|
2493
|
+
e = self._normalize_pending_container(e)
|
|
2494
|
+
|
|
2495
|
+
if t == e:
|
|
2496
|
+
return t
|
|
2497
|
+
|
|
2498
|
+
# Numeric widening (Int32 + Int64 -> Int64, etc.)
|
|
2499
|
+
widened = widen_numeric_types(t, e)
|
|
2500
|
+
if widened is not None:
|
|
2501
|
+
return widened
|
|
2502
|
+
|
|
2503
|
+
# T + None / None + T -> Optional[T]
|
|
2504
|
+
if isinstance(e, NoneType):
|
|
2505
|
+
return make_union(t, NoneType())
|
|
2506
|
+
if isinstance(t, NoneType):
|
|
2507
|
+
return make_union(e, NoneType())
|
|
2508
|
+
|
|
2509
|
+
raise self.ctx.error(
|
|
2510
|
+
f"Incompatible types in ternary expression: "
|
|
2511
|
+
f"'{t}' and '{e}'",
|
|
2512
|
+
expr,
|
|
2513
|
+
)
|
|
2514
|
+
|
|
2515
|
+
def _resolve_literals_with_hint(self, t: TpyType, hint: TpyType | None) -> TpyType:
|
|
2516
|
+
"""Resolve IntLiteralType / FloatLiteralType inside t using hint as a
|
|
2517
|
+
structural guide. Recurses into TupleType. Falls back to default_int /
|
|
2518
|
+
FLOAT when hint doesn't match the literal's family.
|
|
2519
|
+
|
|
2520
|
+
Mirrors the asymmetric behavior of the bare-literal path: integer
|
|
2521
|
+
literals adopt the hint when present (compat-checked downstream),
|
|
2522
|
+
float literals only adopt the hint when it's a float type.
|
|
2523
|
+
"""
|
|
2524
|
+
if isinstance(t, IntLiteralType):
|
|
2525
|
+
return hint if hint is not None else self.ctx.default_int_for_literal(t)
|
|
2526
|
+
if isinstance(t, FloatLiteralType):
|
|
2527
|
+
return hint if is_float_type(hint) else FLOAT
|
|
2528
|
+
if isinstance(t, TupleType):
|
|
2529
|
+
if isinstance(hint, TupleType) and len(t.element_types) == len(hint.element_types):
|
|
2530
|
+
elems = tuple(
|
|
2531
|
+
self._resolve_literals_with_hint(et, ht)
|
|
2532
|
+
for et, ht in zip(t.element_types, hint.element_types)
|
|
2533
|
+
)
|
|
2534
|
+
else:
|
|
2535
|
+
elems = tuple(
|
|
2536
|
+
self._resolve_literals_with_hint(et, None)
|
|
2537
|
+
for et in t.element_types
|
|
2538
|
+
)
|
|
2539
|
+
return TupleType(elems)
|
|
2540
|
+
return t
|
|
2541
|
+
|
|
2542
|
+
def _unify_literal_types(self, a: TpyType, b: TpyType) -> TpyType | None:
|
|
2543
|
+
"""Unify two dict/set literal element types, treating IntLiteralType /
|
|
2544
|
+
FloatLiteralType as compatible with their concrete equivalents and
|
|
2545
|
+
recursing into TupleType. Returns the unified type or None if they
|
|
2546
|
+
cannot be unified.
|
|
2547
|
+
"""
|
|
2548
|
+
if a == b:
|
|
2549
|
+
return a
|
|
2550
|
+
if isinstance(a, IntLiteralType) and isinstance(b, IntLiteralType):
|
|
2551
|
+
return a
|
|
2552
|
+
if isinstance(a, IntLiteralType) and is_integer_type(b):
|
|
2553
|
+
return b
|
|
2554
|
+
if isinstance(b, IntLiteralType) and is_integer_type(a):
|
|
2555
|
+
return a
|
|
2556
|
+
if isinstance(a, FloatLiteralType) and isinstance(b, FloatLiteralType):
|
|
2557
|
+
return a
|
|
2558
|
+
if isinstance(a, FloatLiteralType) and is_float_type(b):
|
|
2559
|
+
return b
|
|
2560
|
+
if isinstance(b, FloatLiteralType) and is_float_type(a):
|
|
2561
|
+
return a
|
|
2562
|
+
if isinstance(a, TupleType) and isinstance(b, TupleType):
|
|
2563
|
+
if len(a.element_types) != len(b.element_types):
|
|
2564
|
+
return None
|
|
2565
|
+
unified: list[TpyType] = []
|
|
2566
|
+
for ea, eb in zip(a.element_types, b.element_types):
|
|
2567
|
+
u = self._unify_literal_types(ea, eb)
|
|
2568
|
+
if u is None:
|
|
2569
|
+
return None
|
|
2570
|
+
unified.append(u)
|
|
2571
|
+
return TupleType(tuple(unified))
|
|
2572
|
+
return None
|
|
2573
|
+
|
|
2574
|
+
def _analyze_dict_literal(
|
|
2575
|
+
self, expr: TpyDictLiteral,
|
|
2576
|
+
expected_key: TpyType | None = None,
|
|
2577
|
+
expected_value: TpyType | None = None,
|
|
2578
|
+
) -> TpyType:
|
|
2579
|
+
"""Analyze a dict literal {key: value, ...}"""
|
|
2580
|
+
if not expr.keys:
|
|
2581
|
+
# Hint from LHS / param / return type pins K, V directly -- no
|
|
2582
|
+
# usage-based inference needed.
|
|
2583
|
+
if expected_key is not None and expected_value is not None:
|
|
2584
|
+
return make_dict(expected_key, expected_value)
|
|
2585
|
+
if not is_body_like_scope(self.ctx.func.current_function):
|
|
2586
|
+
raise self.ctx.error(
|
|
2587
|
+
"Empty dict literal requires explicit type annotation", expr)
|
|
2588
|
+
literal_id = self.ctx.literal_counter
|
|
2589
|
+
self.ctx.literal_counter += 1
|
|
2590
|
+
info = DictLiteralInfo(
|
|
2591
|
+
literal_id=literal_id,
|
|
2592
|
+
expr=expr,
|
|
2593
|
+
key_type=UNKNOWN_ELEMENT,
|
|
2594
|
+
value_type=UNKNOWN_ELEMENT,
|
|
2595
|
+
)
|
|
2596
|
+
self.ctx.dict_literals[literal_id] = info
|
|
2597
|
+
self.ctx.func.pending_dict_resolutions.append(literal_id)
|
|
2598
|
+
return PendingDictType(UNKNOWN_ELEMENT, UNKNOWN_ELEMENT, literal_id)
|
|
2599
|
+
|
|
2600
|
+
if expected_key:
|
|
2601
|
+
key_types = [self._analyze_and_strip(k, expected_key) for k in expr.keys]
|
|
2602
|
+
else:
|
|
2603
|
+
key_types = [self._analyze_and_strip(k) for k in expr.keys]
|
|
2604
|
+
if expected_value:
|
|
2605
|
+
value_types = [self._analyze_and_strip(v, expected_value) for v in expr.values]
|
|
2606
|
+
else:
|
|
2607
|
+
value_types = [self._analyze_and_strip(v) for v in expr.values]
|
|
2608
|
+
|
|
2609
|
+
# Unify key types
|
|
2610
|
+
if is_union_or_optional_type(expected_key) or isinstance(expected_key, AnyType):
|
|
2611
|
+
key_type = expected_key
|
|
2612
|
+
for i, kt in enumerate(key_types, 1):
|
|
2613
|
+
if kt == expected_key:
|
|
2614
|
+
continue
|
|
2615
|
+
try:
|
|
2616
|
+
self.compat.check_type_compatible(
|
|
2617
|
+
kt, expected_key, f"dict literal key {i}", expr.loc,
|
|
2618
|
+
source_expr=expr.keys[i - 1])
|
|
2619
|
+
except SemanticError:
|
|
2620
|
+
raise self.ctx.error(
|
|
2621
|
+
f"Dict literal key {i} has type {kt}, "
|
|
2622
|
+
f"incompatible with annotated key type {expected_key}", expr)
|
|
2623
|
+
else:
|
|
2624
|
+
key_type = key_types[0]
|
|
2625
|
+
for i, kt in enumerate(key_types[1:], 2):
|
|
2626
|
+
unified = self._unify_literal_types(key_type, kt)
|
|
2627
|
+
if unified is None:
|
|
2628
|
+
raise self.ctx.error(
|
|
2629
|
+
f"Dict has mixed key types: key {i} is {self._user_type_name(kt)}, "
|
|
2630
|
+
f"but earlier keys are {self._user_type_name(key_type)}", expr,
|
|
2631
|
+
)
|
|
2632
|
+
key_type = unified
|
|
2633
|
+
|
|
2634
|
+
# Unify value types
|
|
2635
|
+
if (is_union_or_optional_type(expected_value)
|
|
2636
|
+
or isinstance(expected_value, AnyType)
|
|
2637
|
+
or (expected_value is not None and expected_value.needs_wrapper())
|
|
2638
|
+
or (expected_value is not None and any(
|
|
2639
|
+
self.compat.is_covariant_generic_upcast(vt, expected_value)
|
|
2640
|
+
for vt in value_types))):
|
|
2641
|
+
# Annotation provides a union/optional/Any/recursive-alias wrapper --
|
|
2642
|
+
# each value validates against the slot independently rather than
|
|
2643
|
+
# against its peers (heterogeneous values are the whole point of
|
|
2644
|
+
# these slot types; a recursive dict alias's values are leaves or
|
|
2645
|
+
# nested wrappers). Same when the annotated value is a covariant-
|
|
2646
|
+
# generic wrapper the values upcast to (Box[Dog] -> Box[Pet]):
|
|
2647
|
+
# check each against the annotation so the dict builds at the
|
|
2648
|
+
# annotated instantiation, not the peer-unified subclass.
|
|
2649
|
+
value_type = expected_value
|
|
2650
|
+
for i, vt in enumerate(value_types, 1):
|
|
2651
|
+
if vt == expected_value:
|
|
2652
|
+
continue
|
|
2653
|
+
try:
|
|
2654
|
+
self.compat.check_type_compatible(
|
|
2655
|
+
vt, expected_value, f"dict literal value {i}", expr.loc,
|
|
2656
|
+
source_expr=expr.values[i - 1])
|
|
2657
|
+
except SemanticError:
|
|
2658
|
+
raise self.ctx.error(
|
|
2659
|
+
f"Dict literal value {i} has type {vt}, "
|
|
2660
|
+
f"incompatible with annotated value type {expected_value}", expr)
|
|
2661
|
+
else:
|
|
2662
|
+
value_type = value_types[0]
|
|
2663
|
+
for i, vt in enumerate(value_types[1:], 2):
|
|
2664
|
+
unified = self._unify_literal_types(value_type, vt)
|
|
2665
|
+
if unified is None:
|
|
2666
|
+
raise self.ctx.error(
|
|
2667
|
+
f"Dict has mixed value types: value {i} is {self._user_type_name(vt)}, "
|
|
2668
|
+
f"but earlier values are {self._user_type_name(value_type)}", expr,
|
|
2669
|
+
)
|
|
2670
|
+
value_type = unified
|
|
2671
|
+
|
|
2672
|
+
# Resolve any literal types (bare or nested in tuples) using the
|
|
2673
|
+
# annotation as a structural hint.
|
|
2674
|
+
key_type = self._resolve_literals_with_hint(key_type, expected_key)
|
|
2675
|
+
value_type = self._resolve_literals_with_hint(value_type, expected_value)
|
|
2676
|
+
# Container elements must be owned -- views can't be stored in a dict.
|
|
2677
|
+
if isinstance(key_type, PendingViewType):
|
|
2678
|
+
key_type = key_type.family.owned_type
|
|
2679
|
+
if isinstance(value_type, PendingViewType):
|
|
2680
|
+
value_type = value_type.family.owned_type
|
|
2681
|
+
|
|
2682
|
+
self.type_ops.validate_hashable_container_elem(key_type, "dict key", expr.loc)
|
|
2683
|
+
return make_dict(key_type, value_type)
|
|
2684
|
+
|
|
2685
|
+
def _analyze_set_literal(
|
|
2686
|
+
self, expr: TpySetLiteral,
|
|
2687
|
+
expected_elem: TpyType | None = None,
|
|
2688
|
+
) -> TpyType:
|
|
2689
|
+
"""Analyze a set literal {value, ...}"""
|
|
2690
|
+
if not expr.elements:
|
|
2691
|
+
raise self.ctx.error(
|
|
2692
|
+
"Empty set literal requires type annotation "
|
|
2693
|
+
"(e.g. s: set[int] = set())", expr,
|
|
2694
|
+
)
|
|
2695
|
+
|
|
2696
|
+
if expected_elem:
|
|
2697
|
+
elem_types = [self._analyze_and_strip(e, expected_elem) for e in expr.elements]
|
|
2698
|
+
else:
|
|
2699
|
+
elem_types = [self._analyze_and_strip(e) for e in expr.elements]
|
|
2700
|
+
|
|
2701
|
+
# Unify element types
|
|
2702
|
+
if (is_union_or_optional_type(expected_elem) or isinstance(expected_elem, AnyType)
|
|
2703
|
+
or (expected_elem is not None and any(
|
|
2704
|
+
self.compat.is_covariant_generic_upcast(et, expected_elem)
|
|
2705
|
+
for et in elem_types))):
|
|
2706
|
+
# Annotation provides a union/optional/Any -- each element validates
|
|
2707
|
+
# against the slot independently rather than against its peers. Same
|
|
2708
|
+
# for a covariant-generic element the values upcast to (parallel to
|
|
2709
|
+
# the dict-value path; moot for @nocopy Box/Rc, which are rejected
|
|
2710
|
+
# as set elements, but kept symmetric for a hashable covariant
|
|
2711
|
+
# value-type element).
|
|
2712
|
+
elem_type = expected_elem
|
|
2713
|
+
for i, et in enumerate(elem_types, 1):
|
|
2714
|
+
if et == expected_elem:
|
|
2715
|
+
continue
|
|
2716
|
+
try:
|
|
2717
|
+
self.compat.check_type_compatible(
|
|
2718
|
+
et, expected_elem, f"set literal element {i}", expr.loc,
|
|
2719
|
+
source_expr=expr.elements[i - 1])
|
|
2720
|
+
except SemanticError:
|
|
2721
|
+
raise self.ctx.error(
|
|
2722
|
+
f"Set literal element {i} has type {et}, "
|
|
2723
|
+
f"incompatible with annotated element type {expected_elem}", expr,
|
|
2724
|
+
)
|
|
2725
|
+
else:
|
|
2726
|
+
elem_type = elem_types[0]
|
|
2727
|
+
for i, et in enumerate(elem_types[1:], 2):
|
|
2728
|
+
unified = self._unify_literal_types(elem_type, et)
|
|
2729
|
+
if unified is None:
|
|
2730
|
+
raise self.ctx.error(
|
|
2731
|
+
f"Set has mixed element types: element {i} is {self._user_type_name(et)}, "
|
|
2732
|
+
f"but earlier elements are {self._user_type_name(elem_type)}", expr,
|
|
2733
|
+
)
|
|
2734
|
+
elem_type = unified
|
|
2735
|
+
|
|
2736
|
+
elem_type = self._resolve_literals_with_hint(elem_type, expected_elem)
|
|
2737
|
+
# Container elements must be owned -- views can't be stored in a set.
|
|
2738
|
+
if isinstance(elem_type, PendingViewType):
|
|
2739
|
+
elem_type = elem_type.family.owned_type
|
|
2740
|
+
|
|
2741
|
+
self.type_ops.validate_hashable_container_elem(elem_type, "set element", expr.loc)
|
|
2742
|
+
return make_set(elem_type)
|
|
2743
|
+
|
|
2744
|
+
def _analyze_list_repeat(self, expr: TpyListRepeat) -> TpyType:
|
|
2745
|
+
"""Analyze a list repetition: [elements...] * count"""
|
|
2746
|
+
count_type = self.analyze_expr(expr.count)
|
|
2747
|
+
|
|
2748
|
+
if not is_any_int_type(count_type):
|
|
2749
|
+
raise self.ctx.error(f"List repetition count must be an integer type, got {count_type}", expr)
|
|
2750
|
+
|
|
2751
|
+
# Note: Empty list repetition [] * N is collapsed to [] in the parser
|
|
2752
|
+
|
|
2753
|
+
# Analyze all elements
|
|
2754
|
+
elem_types = [self.analyze_expr(e) for e in expr.elements]
|
|
2755
|
+
first_type = elem_types[0]
|
|
2756
|
+
|
|
2757
|
+
# Check all elements are compatible (similar to array literal)
|
|
2758
|
+
for i, elem_type in enumerate(elem_types[1:], 2):
|
|
2759
|
+
if isinstance(first_type, IntLiteralType) and isinstance(elem_type, IntLiteralType):
|
|
2760
|
+
continue
|
|
2761
|
+
if isinstance(elem_type, IntLiteralType) and is_integer_type(first_type):
|
|
2762
|
+
continue
|
|
2763
|
+
if isinstance(first_type, IntLiteralType) and is_integer_type(elem_type):
|
|
2764
|
+
first_type = elem_type
|
|
2765
|
+
continue
|
|
2766
|
+
if isinstance(first_type, FloatLiteralType) and isinstance(elem_type, FloatLiteralType):
|
|
2767
|
+
continue
|
|
2768
|
+
if isinstance(elem_type, FloatLiteralType) and is_float_type(first_type):
|
|
2769
|
+
continue
|
|
2770
|
+
if isinstance(first_type, FloatLiteralType) and is_float_type(elem_type):
|
|
2771
|
+
first_type = elem_type
|
|
2772
|
+
continue
|
|
2773
|
+
if first_type != elem_type:
|
|
2774
|
+
raise self.ctx.error(f"List repetition element {i} has type {elem_type}, expected {first_type}", expr)
|
|
2775
|
+
|
|
2776
|
+
# Global context -> ListType (no deferred resolution)
|
|
2777
|
+
if self.ctx.func.current_function is None:
|
|
2778
|
+
return make_list(first_type)
|
|
2779
|
+
|
|
2780
|
+
# Function-local context -> PendingListType for deferred resolution
|
|
2781
|
+
# Compute size if count is compile-time constant
|
|
2782
|
+
if isinstance(expr.count, TpyIntLiteral):
|
|
2783
|
+
size = len(expr.elements) * expr.count.value
|
|
2784
|
+
else:
|
|
2785
|
+
size = -1 # Variable count -- cannot resolve to Array
|
|
2786
|
+
|
|
2787
|
+
literal_id = self.ctx.literal_counter
|
|
2788
|
+
self.ctx.literal_counter += 1
|
|
2789
|
+
|
|
2790
|
+
info = ListLiteralInfo(
|
|
2791
|
+
literal_id=literal_id,
|
|
2792
|
+
expr=expr,
|
|
2793
|
+
element_type=first_type,
|
|
2794
|
+
size=size,
|
|
2795
|
+
is_global=self.ctx.is_top_level,
|
|
2796
|
+
)
|
|
2797
|
+
self.ctx.list_literals[literal_id] = info
|
|
2798
|
+
self.ctx.func.pending_resolutions.append(literal_id)
|
|
2799
|
+
|
|
2800
|
+
return PendingListType(first_type, size, literal_id)
|
|
2801
|
+
|
|
2802
|
+
def _analyze_list_comprehension(
|
|
2803
|
+
self, expr: TpyListComprehension, expected_elem: TpyType | None = None
|
|
2804
|
+
) -> TpyType:
|
|
2805
|
+
return self._analyze_elem_comprehension(expr, expected_elem, kind="list")
|
|
2806
|
+
|
|
2807
|
+
def _analyze_set_comprehension(
|
|
2808
|
+
self, expr: TpySetComprehension, expected_elem: TpyType | None = None
|
|
2809
|
+
) -> TpyType:
|
|
2810
|
+
return self._analyze_elem_comprehension(expr, expected_elem, kind="set")
|
|
2811
|
+
|
|
2812
|
+
def _analyze_generator_expression(self, expr: TpyGeneratorExpression) -> TpyType:
|
|
2813
|
+
gen = expr.generator
|
|
2814
|
+
elem_type = self._resolve_comp_iterable(gen, expr)
|
|
2815
|
+
|
|
2816
|
+
if self.scopes is None:
|
|
2817
|
+
raise RuntimeError("generator expression requires ScopeTracker")
|
|
2818
|
+
result_elem_type = self._enter_comp_scope(gen, expr, elem_type, None)
|
|
2819
|
+
|
|
2820
|
+
if isinstance(result_elem_type, IntLiteralType):
|
|
2821
|
+
result_elem_type = self.ctx.default_int_type
|
|
2822
|
+
elif isinstance(result_elem_type, FloatLiteralType):
|
|
2823
|
+
result_elem_type = FLOAT
|
|
2824
|
+
else:
|
|
2825
|
+
result_elem_type = resolve_int_literals(result_elem_type, self.ctx.default_int_for_literal)
|
|
2826
|
+
|
|
2827
|
+
expr.result_elem_type = result_elem_type
|
|
2828
|
+
return GenExprType(result_elem_type)
|
|
2829
|
+
|
|
2830
|
+
def _analyze_elem_comprehension(
|
|
2831
|
+
self, expr: TpyListComprehension | TpySetComprehension,
|
|
2832
|
+
expected_elem: TpyType | None,
|
|
2833
|
+
kind: Literal["list", "set"],
|
|
2834
|
+
) -> TpyType:
|
|
2835
|
+
"""Shared analysis for list and set comprehensions."""
|
|
2836
|
+
gen = expr.generator
|
|
2837
|
+
elem_type = self._resolve_comp_iterable(gen, expr)
|
|
2838
|
+
|
|
2839
|
+
if self.scopes is None:
|
|
2840
|
+
raise RuntimeError(f"{kind} comprehension requires ScopeTracker")
|
|
2841
|
+
result_elem_type = self._enter_comp_scope(gen, expr, elem_type, expected_elem)
|
|
2842
|
+
|
|
2843
|
+
if isinstance(result_elem_type, IntLiteralType):
|
|
2844
|
+
result_elem_type = expected_elem if expected_elem is not None else self.ctx.default_int_type
|
|
2845
|
+
if isinstance(result_elem_type, FloatLiteralType):
|
|
2846
|
+
result_elem_type = expected_elem if is_float_type(expected_elem) else FLOAT
|
|
2847
|
+
|
|
2848
|
+
if expected_elem is not None and result_elem_type != expected_elem:
|
|
2849
|
+
# Subclass coercion excluded: storing Child in list/set[Base] silently
|
|
2850
|
+
# slices objects (same invariance as container literals). Covariant-
|
|
2851
|
+
# generic wrapper upcasts (Box[Dog] -> Box[Pet]) are exempt -- a
|
|
2852
|
+
# representation-preserving converting move, not slicing -- and flow
|
|
2853
|
+
# through coerce_expr below.
|
|
2854
|
+
if (isinstance(result_elem_type, NominalType) and result_elem_type.is_user_record
|
|
2855
|
+
and isinstance(expected_elem, NominalType) and expected_elem.is_user_record
|
|
2856
|
+
and not self.compat.is_covariant_generic_upcast(result_elem_type, expected_elem)):
|
|
2857
|
+
raise self.ctx.error(
|
|
2858
|
+
f"{kind.capitalize()} comprehension element has type {result_elem_type}, "
|
|
2859
|
+
f"incompatible with annotated element type {expected_elem}", expr
|
|
2860
|
+
)
|
|
2861
|
+
expr.element_expr = self.compat.coerce_expr(
|
|
2862
|
+
expr.element_expr, result_elem_type, expected_elem,
|
|
2863
|
+
f"{kind} comprehension element", coercion_ctx=CoercionContext.INIT,
|
|
2864
|
+
target_is_storage_form=True)
|
|
2865
|
+
result_elem_type = expected_elem
|
|
2866
|
+
|
|
2867
|
+
if kind == "set":
|
|
2868
|
+
self.type_ops.validate_hashable_container_elem(result_elem_type, "set element", expr.loc)
|
|
2869
|
+
|
|
2870
|
+
expr.result_elem_type = result_elem_type
|
|
2871
|
+
|
|
2872
|
+
if kind == "list":
|
|
2873
|
+
array_size = self._try_comp_array_size(expr)
|
|
2874
|
+
if array_size is not None and self.ctx.func.current_function is not None:
|
|
2875
|
+
literal_id = self.ctx.literal_counter
|
|
2876
|
+
self.ctx.literal_counter += 1
|
|
2877
|
+
info = ListLiteralInfo(
|
|
2878
|
+
literal_id=literal_id,
|
|
2879
|
+
expr=expr,
|
|
2880
|
+
element_type=result_elem_type,
|
|
2881
|
+
size=array_size,
|
|
2882
|
+
is_global=self.ctx.is_top_level,
|
|
2883
|
+
)
|
|
2884
|
+
self.ctx.list_literals[literal_id] = info
|
|
2885
|
+
self.ctx.func.pending_resolutions.append(literal_id)
|
|
2886
|
+
return PendingListType(result_elem_type, array_size, literal_id)
|
|
2887
|
+
|
|
2888
|
+
return make_set(result_elem_type) if kind == "set" else make_list(result_elem_type)
|
|
2889
|
+
|
|
2890
|
+
def _try_comp_array_size(self, expr: TpyListComprehension) -> int | None:
|
|
2891
|
+
"""Return the compile-time known size if this comprehension can be an Array."""
|
|
2892
|
+
gen = expr.generator
|
|
2893
|
+
if gen.conditions:
|
|
2894
|
+
return None
|
|
2895
|
+
|
|
2896
|
+
# range(N) or range(start, stop) with literal args
|
|
2897
|
+
if isinstance(gen.iterable, TpyCall) and gen.iterable.func_name == "range":
|
|
2898
|
+
return self._range_literal_size(gen.iterable)
|
|
2899
|
+
|
|
2900
|
+
# Array[T, N] source -- size is known from the type
|
|
2901
|
+
iterable_type = unwrap_readonly(self.ctx.get_expr_type(gen.iterable))
|
|
2902
|
+
if is_array(iterable_type):
|
|
2903
|
+
return iterable_type.type_args[1]
|
|
2904
|
+
|
|
2905
|
+
return None
|
|
2906
|
+
|
|
2907
|
+
@staticmethod
|
|
2908
|
+
def _try_int_literal(expr: TpyExpr) -> int | None:
|
|
2909
|
+
"""Extract an integer literal value, unwrapping TpyCoerce and unary minus."""
|
|
2910
|
+
if isinstance(expr, TpyCoerce):
|
|
2911
|
+
expr = expr.expr
|
|
2912
|
+
if isinstance(expr, TpyIntLiteral):
|
|
2913
|
+
return expr.value
|
|
2914
|
+
if isinstance(expr, TpyUnaryOp) and expr.op == '-' and isinstance(expr.operand, TpyIntLiteral):
|
|
2915
|
+
return -expr.operand.value
|
|
2916
|
+
return None
|
|
2917
|
+
|
|
2918
|
+
def _range_literal_size(self, call: TpyCall) -> int | None:
|
|
2919
|
+
"""Extract compile-time size from range() with literal args."""
|
|
2920
|
+
args = call.args
|
|
2921
|
+
if len(args) == 1:
|
|
2922
|
+
n = self._try_int_literal(args[0])
|
|
2923
|
+
if n is not None:
|
|
2924
|
+
return max(n, 0)
|
|
2925
|
+
elif len(args) == 2:
|
|
2926
|
+
s = self._try_int_literal(args[0])
|
|
2927
|
+
e = self._try_int_literal(args[1])
|
|
2928
|
+
if s is not None and e is not None and e >= s:
|
|
2929
|
+
return e - s
|
|
2930
|
+
elif len(args) == 3:
|
|
2931
|
+
s = self._try_int_literal(args[0])
|
|
2932
|
+
e = self._try_int_literal(args[1])
|
|
2933
|
+
d = self._try_int_literal(args[2])
|
|
2934
|
+
if s is not None and e is not None and d is not None and d != 0:
|
|
2935
|
+
if d > 0 and e > s:
|
|
2936
|
+
return (e - s + d - 1) // d
|
|
2937
|
+
elif d < 0 and s > e:
|
|
2938
|
+
return (s - e - d - 1) // (-d)
|
|
2939
|
+
else:
|
|
2940
|
+
return 0
|
|
2941
|
+
return None
|
|
2942
|
+
|
|
2943
|
+
def _analyze_dict_comprehension(
|
|
2944
|
+
self, expr: TpyDictComprehension,
|
|
2945
|
+
expected_key: TpyType | None = None,
|
|
2946
|
+
expected_value: TpyType | None = None,
|
|
2947
|
+
) -> TpyType:
|
|
2948
|
+
"""Analyze a dict comprehension: {key: value for var in iterable if cond}"""
|
|
2949
|
+
gen = expr.generator
|
|
2950
|
+
elem_type = self._resolve_comp_iterable(gen, expr)
|
|
2951
|
+
|
|
2952
|
+
if self.scopes is None:
|
|
2953
|
+
raise RuntimeError("dict comprehension requires ScopeTracker")
|
|
2954
|
+
key_type, value_type = self._enter_comp_scope(
|
|
2955
|
+
gen, expr, elem_type, (expected_key, expected_value))
|
|
2956
|
+
|
|
2957
|
+
if isinstance(key_type, IntLiteralType):
|
|
2958
|
+
key_type = expected_key if expected_key is not None else self.ctx.default_int_type
|
|
2959
|
+
if isinstance(value_type, IntLiteralType):
|
|
2960
|
+
value_type = expected_value if expected_value is not None else self.ctx.default_int_type
|
|
2961
|
+
if isinstance(key_type, FloatLiteralType):
|
|
2962
|
+
key_type = expected_key if is_float_type(expected_key) else FLOAT
|
|
2963
|
+
if isinstance(value_type, FloatLiteralType):
|
|
2964
|
+
value_type = expected_value if is_float_type(expected_value) else FLOAT
|
|
2965
|
+
|
|
2966
|
+
if expected_key is not None and key_type != expected_key:
|
|
2967
|
+
expr.key_expr = self.compat.coerce_expr(
|
|
2968
|
+
expr.key_expr, key_type, expected_key,
|
|
2969
|
+
"dict comprehension key", coercion_ctx=CoercionContext.INIT,
|
|
2970
|
+
target_is_storage_form=True)
|
|
2971
|
+
key_type = expected_key
|
|
2972
|
+
if expected_value is not None and value_type != expected_value:
|
|
2973
|
+
expr.value_expr = self.compat.coerce_expr(
|
|
2974
|
+
expr.value_expr, value_type, expected_value,
|
|
2975
|
+
"dict comprehension value", coercion_ctx=CoercionContext.INIT,
|
|
2976
|
+
target_is_storage_form=True)
|
|
2977
|
+
value_type = expected_value
|
|
2978
|
+
|
|
2979
|
+
self.type_ops.validate_hashable_container_elem(key_type, "dict key", expr.loc)
|
|
2980
|
+
expr.result_key_type = key_type
|
|
2981
|
+
expr.result_value_type = value_type
|
|
2982
|
+
return make_dict(key_type, value_type)
|
|
2983
|
+
|
|
2984
|
+
def _resolve_comp_iterable(
|
|
2985
|
+
self, gen: TpyComprehensionGenerator, expr: TpyExpr
|
|
2986
|
+
) -> TpyType:
|
|
2987
|
+
"""Analyze the iterable and extract its element type (shared by all comprehensions)."""
|
|
2988
|
+
iterable_type = self.analyze_expr(gen.iterable)
|
|
2989
|
+
inner = unwrap_readonly(iterable_type)
|
|
2990
|
+
if isinstance(inner, TypeParamRef):
|
|
2991
|
+
bound = self.type_ops.get_type_param_bound(inner.name)
|
|
2992
|
+
if bound is not None and is_protocol_type(bound):
|
|
2993
|
+
inner = bound
|
|
2994
|
+
return IterableHelper(self.ctx).get_iterable_element_type(inner, loc=expr.loc)
|
|
2995
|
+
|
|
2996
|
+
def _enter_comp_scope(
|
|
2997
|
+
self,
|
|
2998
|
+
gen: TpyComprehensionGenerator,
|
|
2999
|
+
expr: TpyListComprehension | TpySetComprehension | TpyDictComprehension | TpyGeneratorExpression,
|
|
3000
|
+
elem_type: TpyType,
|
|
3001
|
+
hint: TpyType | tuple[TpyType | None, TpyType | None] | None,
|
|
3002
|
+
) -> TpyType | tuple[TpyType, TpyType]:
|
|
3003
|
+
# unwrap_qualifiers (not unwrap_readonly) so a wrapped value-type
|
|
3004
|
+
# element still counts as value-type, matching the for-loop predicate
|
|
3005
|
+
# in sema/statements.py.
|
|
3006
|
+
unwrapped = unwrap_qualifiers(elem_type)
|
|
3007
|
+
worth_const_ref = (not unwrapped.is_value_type()
|
|
3008
|
+
or unwrapped.is_expensive_copy())
|
|
3009
|
+
if gen.unpack_vars is not None:
|
|
3010
|
+
names = [u for u in gen.unpack_vars if u is not None]
|
|
3011
|
+
else:
|
|
3012
|
+
names = [gen.var]
|
|
3013
|
+
# Clear any prior marks in case an outer scope already used these
|
|
3014
|
+
# names; the post-body check at the bottom must see only marks from
|
|
3015
|
+
# this comp's body.
|
|
3016
|
+
for n in names:
|
|
3017
|
+
self.ctx.func.mutated_loop_vars.discard(n)
|
|
3018
|
+
self.ctx.func.consumed_loop_vars.discard(n)
|
|
3019
|
+
|
|
3020
|
+
with self.scopes.comprehension_scope() as inner_scope:
|
|
3021
|
+
if gen.unpack_vars is not None:
|
|
3022
|
+
if not isinstance(elem_type, TupleType):
|
|
3023
|
+
raise self.ctx.error(
|
|
3024
|
+
f"Cannot unpack non-tuple type {elem_type}", expr)
|
|
3025
|
+
if len(gen.unpack_vars) != len(elem_type.element_types):
|
|
3026
|
+
raise self.ctx.error(
|
|
3027
|
+
f"Cannot unpack tuple of {len(elem_type.element_types)} "
|
|
3028
|
+
f"elements into {len(gen.unpack_vars)} targets", expr)
|
|
3029
|
+
with ExitStack() as stack:
|
|
3030
|
+
for uvar, utype in zip(gen.unpack_vars, elem_type.element_types):
|
|
3031
|
+
if uvar is not None:
|
|
3032
|
+
stack.enter_context(
|
|
3033
|
+
self.scopes.loop_var(inner_scope, uvar, utype,
|
|
3034
|
+
inner_scope.depth, is_foreach=True))
|
|
3035
|
+
result = self._analyze_comp_body(gen, expr, hint)
|
|
3036
|
+
else:
|
|
3037
|
+
with self.scopes.loop_var(inner_scope, gen.var, elem_type,
|
|
3038
|
+
inner_scope.depth, is_foreach=True):
|
|
3039
|
+
result = self._analyze_comp_body(gen, expr, hint)
|
|
3040
|
+
|
|
3041
|
+
if worth_const_ref and not any(n in self.ctx.func.mutated_loop_vars
|
|
3042
|
+
for n in names):
|
|
3043
|
+
gen.const_loop_var = True
|
|
3044
|
+
return result
|
|
3045
|
+
|
|
3046
|
+
def _analyze_comp_body(
|
|
3047
|
+
self,
|
|
3048
|
+
gen: TpyComprehensionGenerator,
|
|
3049
|
+
expr: TpyListComprehension | TpySetComprehension | TpyDictComprehension | TpyGeneratorExpression,
|
|
3050
|
+
hint: TpyType | tuple[TpyType | None, TpyType | None] | None,
|
|
3051
|
+
) -> TpyType | tuple[TpyType, TpyType]:
|
|
3052
|
+
for cond in gen.conditions:
|
|
3053
|
+
self.analyze_expr(cond)
|
|
3054
|
+
if isinstance(expr, TpyDictComprehension):
|
|
3055
|
+
key_hint, value_hint = hint
|
|
3056
|
+
key_type = (self.analyze_expr_with_hint(expr.key_expr, key_hint)
|
|
3057
|
+
if key_hint else self.analyze_expr(expr.key_expr))
|
|
3058
|
+
value_type = (self.analyze_expr_with_hint(expr.value_expr, value_hint)
|
|
3059
|
+
if value_hint else self.analyze_expr(expr.value_expr))
|
|
3060
|
+
return key_type, value_type
|
|
3061
|
+
if hint is not None:
|
|
3062
|
+
return self.analyze_expr_with_hint(expr.element_expr, hint)
|
|
3063
|
+
return self.analyze_expr(expr.element_expr)
|
|
3064
|
+
|
|
3065
|
+
def _analyze_and_strip(self, expr: TpyExpr, hint: TpyType | None = None) -> TpyType:
|
|
3066
|
+
"""Analyze an expression and strip sema-internal wrappers (Own/Ref).
|
|
3067
|
+
|
|
3068
|
+
Used for container literal element types where the declared type
|
|
3069
|
+
should be the user-facing type, not the provenance-tagged expression type.
|
|
3070
|
+
"""
|
|
3071
|
+
if hint is not None:
|
|
3072
|
+
self.analyze_expr_with_hint(expr, hint)
|
|
3073
|
+
else:
|
|
3074
|
+
self.analyze_expr(expr)
|
|
3075
|
+
result = self.ctx.get_expr_type(expr)
|
|
3076
|
+
assert result is not None
|
|
3077
|
+
return result
|
|
3078
|
+
|
|
3079
|
+
def _analyze_tuple_literal(
|
|
3080
|
+
self, expr: TpyTupleLiteral, element_hints: list[TpyType | None] | None = None
|
|
3081
|
+
) -> TupleType:
|
|
3082
|
+
"""Analyze a tuple literal (expr, expr, ...)."""
|
|
3083
|
+
elem_types = []
|
|
3084
|
+
for i, elem in enumerate(expr.elements):
|
|
3085
|
+
hint = element_hints[i] if element_hints and i < len(element_hints) else None
|
|
3086
|
+
if hint is not None:
|
|
3087
|
+
analyzed = self.analyze_expr_with_hint(elem, hint)
|
|
3088
|
+
# Preserve Own[] from hint when the analyzed type matches
|
|
3089
|
+
if isinstance(hint, OwnType) and not isinstance(analyzed, OwnType):
|
|
3090
|
+
analyzed = OwnType(analyzed)
|
|
3091
|
+
elem_types.append(analyzed)
|
|
3092
|
+
else:
|
|
3093
|
+
self.analyze_expr(elem)
|
|
3094
|
+
# Use get_expr_type to strip expression-level OwnType/Ref:
|
|
3095
|
+
# the tuple's declared element type should be bare T,
|
|
3096
|
+
# not the provenance-tagged expression type.
|
|
3097
|
+
elem_types.append(self.ctx.get_expr_type(elem))
|
|
3098
|
+
return TupleType(tuple(elem_types))
|
|
3099
|
+
|
|
3100
|
+
def _analyze_tuple_subscript(self, expr: TpySubscript, tuple_type: TupleType) -> TpyType:
|
|
3101
|
+
"""Analyze tuple subscript: t[0], t[-1] with compile-time constant index."""
|
|
3102
|
+
index = expr.index
|
|
3103
|
+
n = len(tuple_type.element_types)
|
|
3104
|
+
# Register index type for codegen
|
|
3105
|
+
self.analyze_expr(index)
|
|
3106
|
+
# Extract compile-time index
|
|
3107
|
+
if isinstance(index, TpyIntLiteral):
|
|
3108
|
+
idx = index.value
|
|
3109
|
+
elif (isinstance(index, TpyUnaryOp) and index.op == "-"
|
|
3110
|
+
and isinstance(index.operand, TpyIntLiteral)):
|
|
3111
|
+
idx = -index.operand.value
|
|
3112
|
+
else:
|
|
3113
|
+
raise self.ctx.error(
|
|
3114
|
+
"Tuple index must be a compile-time integer literal", expr
|
|
3115
|
+
)
|
|
3116
|
+
# Resolve negative index
|
|
3117
|
+
original_idx = idx
|
|
3118
|
+
if idx < 0:
|
|
3119
|
+
idx += n
|
|
3120
|
+
# Range check
|
|
3121
|
+
if idx < 0 or idx >= n:
|
|
3122
|
+
raise self.ctx.error(
|
|
3123
|
+
f"Tuple index {original_idx} out of range for "
|
|
3124
|
+
f"tuple[{', '.join(str(t) for t in tuple_type.element_types)}] "
|
|
3125
|
+
f"(length {n})",
|
|
3126
|
+
expr,
|
|
3127
|
+
)
|
|
3128
|
+
return tuple_type.element_types[idx]
|
|
3129
|
+
|
|
3130
|
+
def _analyze_subscript(self, expr: TpySubscript) -> TpyType:
|
|
3131
|
+
"""Analyze subscript indexing: obj[index] or slicing: obj[start:stop]"""
|
|
3132
|
+
# Enum name lookup: Color["Red"] -> Color (panics on invalid)
|
|
3133
|
+
if isinstance(expr.obj, TpyName) and self.ctx.func.current_ns:
|
|
3134
|
+
binding = self.ctx.func.current_ns.lookup(expr.obj.name)
|
|
3135
|
+
if binding and binding.kind == BindingKind.ENUM:
|
|
3136
|
+
index_type = self.analyze_expr(expr.index)
|
|
3137
|
+
if not is_any_str_type(index_type):
|
|
3138
|
+
raise self.ctx.error(
|
|
3139
|
+
f"Enum subscript index must be a string, got '{index_type}'",
|
|
3140
|
+
expr,
|
|
3141
|
+
)
|
|
3142
|
+
expr.enum_from_name = binding.enum_type
|
|
3143
|
+
return binding.enum_type
|
|
3144
|
+
|
|
3145
|
+
obj_type = self.analyze_expr(expr.obj)
|
|
3146
|
+
|
|
3147
|
+
# Unwrap transparent wrappers -- Ref/Own don't affect subscript behavior
|
|
3148
|
+
inner_obj_type = unwrap_ref_type(obj_type)
|
|
3149
|
+
if isinstance(inner_obj_type, OwnType):
|
|
3150
|
+
inner_obj_type = inner_obj_type.wrapped
|
|
3151
|
+
|
|
3152
|
+
# Tuple indexing: t[0], t[-1] -- compile-time constant index only
|
|
3153
|
+
actual_for_tuple = unwrap_readonly(inner_obj_type)
|
|
3154
|
+
if isinstance(actual_for_tuple, TupleType):
|
|
3155
|
+
return self._analyze_tuple_subscript(expr, actual_for_tuple)
|
|
3156
|
+
|
|
3157
|
+
# Slice: obj[start:stop]
|
|
3158
|
+
if isinstance(expr.index, TpySlice):
|
|
3159
|
+
return self._analyze_slice(expr, inner_obj_type)
|
|
3160
|
+
|
|
3161
|
+
# TypedDict subscript: d["key"] -> field type (compile-time string literal only)
|
|
3162
|
+
actual_obj = unwrap_readonly(inner_obj_type)
|
|
3163
|
+
if isinstance(actual_obj, NominalType) and actual_obj.is_record:
|
|
3164
|
+
record_info = self.ctx.registry.get_record_for_type(actual_obj)
|
|
3165
|
+
if record_info and record_info.is_typed_dict:
|
|
3166
|
+
if not isinstance(expr.index, TpyStrLiteral):
|
|
3167
|
+
raise self.ctx.error(
|
|
3168
|
+
f"TypedDict '{actual_obj.name}' keys must be string literals", expr.index)
|
|
3169
|
+
key = expr.index.value
|
|
3170
|
+
type_subst = self.type_ops.build_type_substitution(actual_obj)
|
|
3171
|
+
for fld in record_info.fields:
|
|
3172
|
+
if fld.name == key:
|
|
3173
|
+
field_type = fld.type
|
|
3174
|
+
if type_subst:
|
|
3175
|
+
field_type = self.type_ops.substitute_type_params(field_type, type_subst)
|
|
3176
|
+
expr.typed_dict_field = key
|
|
3177
|
+
# total=False: field is Optional[T], unwrap to T with runtime check
|
|
3178
|
+
if isinstance(field_type, OptionalType):
|
|
3179
|
+
expr.typed_dict_optional = True
|
|
3180
|
+
field_type = field_type.inner
|
|
3181
|
+
# Analyze the index expression so its type is recorded
|
|
3182
|
+
self.analyze_expr(expr.index)
|
|
3183
|
+
return make_ref(field_type)
|
|
3184
|
+
raise self.ctx.error(
|
|
3185
|
+
f"TypedDict '{actual_obj.name}' has no key '{key}'", expr.index)
|
|
3186
|
+
|
|
3187
|
+
index_type = self.analyze_expr(expr.index)
|
|
3188
|
+
|
|
3189
|
+
# Dict subscript: d[key] -> V (key can be non-integer)
|
|
3190
|
+
if isinstance(actual_obj, PendingDictType):
|
|
3191
|
+
if not isinstance(actual_obj.key_type, UnknownElementType):
|
|
3192
|
+
self.compat.check_type_compatible(
|
|
3193
|
+
index_type, actual_obj.key_type,
|
|
3194
|
+
f"dict key (expected {actual_obj.key_type})",
|
|
3195
|
+
loc=expr.loc,
|
|
3196
|
+
source_expr=expr.index,
|
|
3197
|
+
)
|
|
3198
|
+
return make_ref(actual_obj.value_type)
|
|
3199
|
+
if is_dict(actual_obj):
|
|
3200
|
+
k_type = actual_obj.type_args[0]
|
|
3201
|
+
v_type = actual_obj.type_args[1]
|
|
3202
|
+
self.compat.check_type_compatible(
|
|
3203
|
+
index_type, k_type,
|
|
3204
|
+
f"dict key (expected {k_type})",
|
|
3205
|
+
loc=expr.loc,
|
|
3206
|
+
source_expr=expr.index,
|
|
3207
|
+
)
|
|
3208
|
+
return make_ref(v_type)
|
|
3209
|
+
|
|
3210
|
+
# Slice-typed variable as index: route through __getitem__ overload
|
|
3211
|
+
# resolution (same path as literal a:b syntax but with variable index).
|
|
3212
|
+
if is_basic_slice_type(index_type) or is_slice_type(index_type):
|
|
3213
|
+
stepped = is_slice_type(index_type)
|
|
3214
|
+
if stepped:
|
|
3215
|
+
expr.is_stepped_slice = True
|
|
3216
|
+
is_readonly = isinstance(inner_obj_type, ReadonlyType)
|
|
3217
|
+
actual_type = unwrap_readonly(inner_obj_type)
|
|
3218
|
+
result = self._find_slice_getitem(actual_type, stepped=stepped, is_readonly=is_readonly)
|
|
3219
|
+
if result is not None:
|
|
3220
|
+
ret, fi = result
|
|
3221
|
+
expr.slice_function_info = fi
|
|
3222
|
+
return ret
|
|
3223
|
+
raise self.ctx.error(f"Slicing is not supported for {inner_obj_type}", expr)
|
|
3224
|
+
|
|
3225
|
+
if not is_any_int_type(index_type):
|
|
3226
|
+
raise self.ctx.error(f"Subscript index must be an integer type, got {index_type}", expr)
|
|
3227
|
+
|
|
3228
|
+
# Check if index is provably in-bounds for bounds check elision
|
|
3229
|
+
self._check_subscript_bounds_safe(expr)
|
|
3230
|
+
|
|
3231
|
+
# Unwrap ReadonlyType, remember the flag
|
|
3232
|
+
is_readonly_obj = isinstance(inner_obj_type, ReadonlyType)
|
|
3233
|
+
actual_type = unwrap_readonly(inner_obj_type)
|
|
3234
|
+
|
|
3235
|
+
# Optional[T] index access uses runtime null checks for unproven access.
|
|
3236
|
+
if isinstance(actual_type, OptionalType):
|
|
3237
|
+
if actual_type.inner.is_value_type():
|
|
3238
|
+
raise self.ctx.error(f"Cannot index type {obj_type}", expr)
|
|
3239
|
+
self.ctx.warning(OPTIONAL_NONE_ACCESS_WARNING, expr)
|
|
3240
|
+
expr.needs_optional_runtime_check = True
|
|
3241
|
+
actual_type = actual_type.inner
|
|
3242
|
+
|
|
3243
|
+
# Use get_element_type() trait for containers and strings
|
|
3244
|
+
elem_type = actual_type.get_element_type()
|
|
3245
|
+
if elem_type is not None:
|
|
3246
|
+
# Subscript on a repeat-sourced pending list needs indexing support
|
|
3247
|
+
# (repeat_range doesn't have operator[], but Array does).
|
|
3248
|
+
if isinstance(actual_type, PendingListType):
|
|
3249
|
+
info = self.ctx.list_literals.get(actual_type.literal_id)
|
|
3250
|
+
if info and isinstance(info.expr, TpyListRepeat):
|
|
3251
|
+
info.needs_indexing = True
|
|
3252
|
+
if is_readonly_obj and not elem_type.is_value_type():
|
|
3253
|
+
elem_type = ReadonlyType(unwrap_readonly(elem_type))
|
|
3254
|
+
return make_ref(elem_type)
|
|
3255
|
+
|
|
3256
|
+
# Protocol types - lookup __getitem__ return type
|
|
3257
|
+
if is_protocol_type(actual_type):
|
|
3258
|
+
ret = self.narrowing._get_protocol_getitem_type(actual_type)
|
|
3259
|
+
if ret is None:
|
|
3260
|
+
raise self.ctx.error(f"Protocol {actual_type.name} does not support indexing", expr)
|
|
3261
|
+
if is_readonly_obj and not ret.is_value_type():
|
|
3262
|
+
ret = ReadonlyType(unwrap_readonly(ret))
|
|
3263
|
+
return make_ref(ret)
|
|
3264
|
+
|
|
3265
|
+
# Records with __getitem__ method
|
|
3266
|
+
if isinstance(actual_type, NominalType) and actual_type.is_record:
|
|
3267
|
+
ret = self.narrowing._get_record_getitem_type(actual_type)
|
|
3268
|
+
if ret is None:
|
|
3269
|
+
raise self.ctx.error(f"Cannot index type {actual_type}: no __getitem__ method", expr)
|
|
3270
|
+
if is_readonly_obj and not ret.is_value_type():
|
|
3271
|
+
ret = ReadonlyType(unwrap_readonly(ret))
|
|
3272
|
+
return make_ref(ret)
|
|
3273
|
+
|
|
3274
|
+
raise self.ctx.error(f"Cannot index type {obj_type}", expr)
|
|
3275
|
+
|
|
3276
|
+
def _analyze_slice(self, expr: TpySubscript, obj_type: TpyType) -> TpyType:
|
|
3277
|
+
"""Analyze slice expression: obj[start:stop] or obj[start:stop:step].
|
|
3278
|
+
|
|
3279
|
+
Resolves the __getitem__(basic_slice) or __getitem__(slice) overload
|
|
3280
|
+
on the type (built-in or user-defined) and stores the FunctionInfo
|
|
3281
|
+
on the expression for codegen.
|
|
3282
|
+
"""
|
|
3283
|
+
sl = expr.index
|
|
3284
|
+
assert isinstance(sl, TpySlice)
|
|
3285
|
+
stepped = sl.step is not None
|
|
3286
|
+
for bound, label in ((sl.lower, "start"), (sl.upper, "stop"), (sl.step, "step")):
|
|
3287
|
+
if bound is not None:
|
|
3288
|
+
bound_type = self.analyze_expr(bound)
|
|
3289
|
+
if not is_any_int_type(bound_type):
|
|
3290
|
+
raise self.ctx.error(
|
|
3291
|
+
f"Slice {label} must be an integer type, got {bound_type}", bound
|
|
3292
|
+
)
|
|
3293
|
+
if stepped:
|
|
3294
|
+
expr.is_stepped_slice = True
|
|
3295
|
+
|
|
3296
|
+
is_readonly = isinstance(obj_type, ReadonlyType)
|
|
3297
|
+
actual_type = unwrap_readonly(obj_type)
|
|
3298
|
+
|
|
3299
|
+
result = self._find_slice_getitem(actual_type, stepped=stepped, is_readonly=is_readonly)
|
|
3300
|
+
if result is not None:
|
|
3301
|
+
ret, fi = result
|
|
3302
|
+
expr.slice_function_info = fi
|
|
3303
|
+
return ret
|
|
3304
|
+
|
|
3305
|
+
raise self.ctx.error(f"Slicing is not supported for {obj_type}", expr)
|
|
3306
|
+
|
|
3307
|
+
def _find_slice_getitem(self, actual_type: TpyType, *, stepped: bool = False,
|
|
3308
|
+
is_readonly: bool = False) -> tuple[TpyType, 'FunctionInfo'] | None:
|
|
3309
|
+
"""Find __getitem__(basic_slice) or __getitem__(slice) overload on any type.
|
|
3310
|
+
|
|
3311
|
+
Works for both built-in types (via qualified_name -> registry) and user records.
|
|
3312
|
+
When stepped=False, looks for basic_slice param first, falls back to slice.
|
|
3313
|
+
When stepped=True, looks for slice param first, falls back to basic_slice.
|
|
3314
|
+
Prefers the const overload when is_readonly=True.
|
|
3315
|
+
Returns (return_type, FunctionInfo) or None.
|
|
3316
|
+
"""
|
|
3317
|
+
record = self.ctx.registry.get_record_for_type(actual_type)
|
|
3318
|
+
if record is None:
|
|
3319
|
+
return None
|
|
3320
|
+
getitem_overloads = record.methods.get("__getitem__", [])
|
|
3321
|
+
primary = is_slice_type if stepped else is_basic_slice_type
|
|
3322
|
+
fallback = is_basic_slice_type if stepped else is_slice_type
|
|
3323
|
+
slice_overloads = [
|
|
3324
|
+
fi for fi in getitem_overloads
|
|
3325
|
+
if len(fi.params) == 1 and primary(fi.params[0].type)
|
|
3326
|
+
]
|
|
3327
|
+
if not slice_overloads:
|
|
3328
|
+
slice_overloads = [
|
|
3329
|
+
fi for fi in getitem_overloads
|
|
3330
|
+
if len(fi.params) == 1 and fallback(fi.params[0].type)
|
|
3331
|
+
]
|
|
3332
|
+
if not slice_overloads:
|
|
3333
|
+
return None
|
|
3334
|
+
preferred = [fi for fi in slice_overloads if fi.is_readonly == is_readonly]
|
|
3335
|
+
func_info = preferred[0] if preferred else slice_overloads[0]
|
|
3336
|
+
ret = func_info.return_type
|
|
3337
|
+
type_subst = self.type_ops.build_type_substitution(actual_type)
|
|
3338
|
+
if type_subst:
|
|
3339
|
+
ret = self.type_ops.substitute_type_params(ret, type_subst)
|
|
3340
|
+
# Propagate readonly to Span return types (source is readonly or
|
|
3341
|
+
# Span[readonly[T]] -> sliced result should also be readonly)
|
|
3342
|
+
if is_span(ret) and not is_readonly_span(ret):
|
|
3343
|
+
src_readonly = is_span(actual_type) and is_readonly_span(actual_type)
|
|
3344
|
+
if is_readonly or src_readonly:
|
|
3345
|
+
ret = make_span(ret.type_args[0], is_readonly=True)
|
|
3346
|
+
return ret, func_info
|
|
3347
|
+
|
|
3348
|
+
def _find_slice_setitem(self, actual_type: TpyType, *, stepped: bool = False
|
|
3349
|
+
) -> tuple[TpyType, 'FunctionInfo'] | None:
|
|
3350
|
+
"""Find __setitem__(basic_slice, value) or __setitem__(slice, value) overload.
|
|
3351
|
+
|
|
3352
|
+
Similar to _find_slice_getitem but for assignment.
|
|
3353
|
+
No fallback from slice to basic_slice (or vice versa) -- unlike
|
|
3354
|
+
getitem where a basic_slice can promote to slice for reading, assignment
|
|
3355
|
+
semantics differ (stepped requires exact-length match).
|
|
3356
|
+
Returns (value_param_type, FunctionInfo) or None.
|
|
3357
|
+
"""
|
|
3358
|
+
record = self.ctx.registry.get_record_for_type(actual_type)
|
|
3359
|
+
if record is None:
|
|
3360
|
+
return None
|
|
3361
|
+
setitem_overloads = record.methods.get("__setitem__", [])
|
|
3362
|
+
target = is_slice_type if stepped else is_basic_slice_type
|
|
3363
|
+
slice_overloads = [
|
|
3364
|
+
fi for fi in setitem_overloads
|
|
3365
|
+
if len(fi.params) == 2 and target(fi.params[0].type)
|
|
3366
|
+
]
|
|
3367
|
+
if not slice_overloads:
|
|
3368
|
+
return None
|
|
3369
|
+
func_info = slice_overloads[0]
|
|
3370
|
+
value_type = func_info.params[1].type
|
|
3371
|
+
type_subst = self.type_ops.build_type_substitution(actual_type)
|
|
3372
|
+
if type_subst:
|
|
3373
|
+
value_type = self.type_ops.substitute_type_params(value_type, type_subst)
|
|
3374
|
+
return value_type, func_info
|
|
3375
|
+
|
|
3376
|
+
_STRINGABLE = NominalType("Stringable", is_protocol=True)
|
|
3377
|
+
_REPRESENTABLE = NominalType("Representable", is_protocol=True)
|
|
3378
|
+
|
|
3379
|
+
@staticmethod
|
|
3380
|
+
def _is_formattable(t: TpyType) -> bool:
|
|
3381
|
+
"""True for types that f-string can format directly (no __str__/__repr__ needed)."""
|
|
3382
|
+
return (is_numeric_type(t) or is_char_type(t) or is_any_str_type(t)
|
|
3383
|
+
or isinstance(t, (IntLiteralType, FloatLiteralType))
|
|
3384
|
+
or is_enum_type(t))
|
|
3385
|
+
|
|
3386
|
+
def _is_fstring_renderable(self, t: TpyType, repr_only: bool = False) -> bool:
|
|
3387
|
+
"""True if t can appear in an f-string slot (and is __repr__-able when
|
|
3388
|
+
repr_only). Recurses into UnionType members (codegen dispatches via
|
|
3389
|
+
std::visit to the runtime variant __str__/__repr__ overloads)."""
|
|
3390
|
+
if isinstance(t, UnionType):
|
|
3391
|
+
return all(self._is_fstring_renderable(m, repr_only) for m in t.members)
|
|
3392
|
+
if repr_only:
|
|
3393
|
+
if self.protocols.type_conforms_to_protocol(t, self._REPRESENTABLE):
|
|
3394
|
+
return True
|
|
3395
|
+
# Built-in primitives have runtime __repr__ overloads but no
|
|
3396
|
+
# method-level Representable conformance; treat formattable
|
|
3397
|
+
# types as repr-able too.
|
|
3398
|
+
return self._is_formattable(t)
|
|
3399
|
+
if self._is_formattable(t):
|
|
3400
|
+
return True
|
|
3401
|
+
if self.protocols.type_conforms_to_protocol(t, self._STRINGABLE):
|
|
3402
|
+
return True
|
|
3403
|
+
return self.protocols.type_conforms_to_protocol(t, self._REPRESENTABLE)
|
|
3404
|
+
|
|
3405
|
+
def _analyze_fstring(self, expr: TpyFString, *, for_fstr: bool = False) -> TpyType:
|
|
3406
|
+
"""Analyze f-string parts and return STR (owned string).
|
|
3407
|
+
|
|
3408
|
+
Args:
|
|
3409
|
+
for_fstr: If True, only analyze expression types without validating
|
|
3410
|
+
formattability. Used for FStr parameters where the call macro
|
|
3411
|
+
handles per-type dispatch (types don't need __str__/__repr__).
|
|
3412
|
+
"""
|
|
3413
|
+
for part in expr.parts:
|
|
3414
|
+
if isinstance(part, TpyFStringValue):
|
|
3415
|
+
part_type = self.analyze_expr(part.expr)
|
|
3416
|
+
if for_fstr:
|
|
3417
|
+
continue
|
|
3418
|
+
resolved = unwrap_readonly(part_type)
|
|
3419
|
+
if isinstance(resolved, OwnType):
|
|
3420
|
+
resolved = resolved.wrapped
|
|
3421
|
+
conv = part.conversion
|
|
3422
|
+
|
|
3423
|
+
if container_to_str_template(resolved) is not None:
|
|
3424
|
+
pass # containers have runtime to_str
|
|
3425
|
+
elif isinstance(resolved, AnyType):
|
|
3426
|
+
pass # Any dispatches via the per-type str/repr ops slot
|
|
3427
|
+
elif conv == FSTRING_CONV_REPR:
|
|
3428
|
+
if not self._is_fstring_renderable(resolved, repr_only=True):
|
|
3429
|
+
raise self.ctx.error(
|
|
3430
|
+
f"Type {part_type} cannot use !r conversion (no __repr__ method)",
|
|
3431
|
+
part.expr,
|
|
3432
|
+
)
|
|
3433
|
+
elif conv == FSTRING_CONV_STR:
|
|
3434
|
+
if not self._is_fstring_renderable(resolved):
|
|
3435
|
+
raise self.ctx.error(
|
|
3436
|
+
f"Type {part_type} cannot use !s conversion"
|
|
3437
|
+
" (no __str__ or __repr__ method)",
|
|
3438
|
+
part.expr,
|
|
3439
|
+
)
|
|
3440
|
+
elif not self._is_fstring_renderable(resolved):
|
|
3441
|
+
raise self.ctx.error(
|
|
3442
|
+
f"Type {part_type} cannot be used in f-string"
|
|
3443
|
+
" (no __str__ or __repr__ method)",
|
|
3444
|
+
part.expr,
|
|
3445
|
+
)
|
|
3446
|
+
if part.format_spec is not None and (is_big_int_type(resolved) or isinstance(resolved, IntLiteralType)):
|
|
3447
|
+
raise self.ctx.error(
|
|
3448
|
+
"Format specs on int are not yet supported (use a fixed-width type like Int32)",
|
|
3449
|
+
part.expr,
|
|
3450
|
+
)
|
|
3451
|
+
return STR
|
|
3452
|
+
|
|
3453
|
+
# --- Lambda expressions ---
|
|
3454
|
+
|
|
3455
|
+
def _analyze_lambda(self, expr: TpyLambda) -> TpyType:
|
|
3456
|
+
"""Analyze a lambda without a type hint -- error (types cannot be inferred)."""
|
|
3457
|
+
raise self.ctx.error(
|
|
3458
|
+
"Lambda parameter types cannot be inferred without context. "
|
|
3459
|
+
"Pass the lambda to a function that accepts Fn[...] or Callable[...] type",
|
|
3460
|
+
expr
|
|
3461
|
+
)
|
|
3462
|
+
|
|
3463
|
+
def _analyze_lambda_with_fn_hint(self, expr: TpyLambda, fn_type: CallableType) -> CallableType:
|
|
3464
|
+
"""Analyze a lambda with a Fn/Callable type hint providing parameter types."""
|
|
3465
|
+
type_name = "Fn" if is_fn_type(fn_type) else "Callable"
|
|
3466
|
+
if len(expr.param_names) != len(fn_type.param_types):
|
|
3467
|
+
raise self.ctx.error(
|
|
3468
|
+
f"Lambda has {len(expr.param_names)} parameter(s) but "
|
|
3469
|
+
f"{type_name} type expects {len(fn_type.param_types)}",
|
|
3470
|
+
expr
|
|
3471
|
+
)
|
|
3472
|
+
|
|
3473
|
+
expr.inferred_param_types = list(fn_type.param_types)
|
|
3474
|
+
|
|
3475
|
+
# Save outer scope locals for capture filtering
|
|
3476
|
+
outer_locals = set(self.ctx.func.definitely_assigned)
|
|
3477
|
+
|
|
3478
|
+
with self.scopes.lambda_scope() as scope:
|
|
3479
|
+
for pname, ptype in zip(expr.param_names, fn_type.param_types):
|
|
3480
|
+
scope.define(pname, ptype)
|
|
3481
|
+
if self.ctx.func.current_ns:
|
|
3482
|
+
self.ctx.func.current_ns.bind_variable(pname, ptype)
|
|
3483
|
+
self.ctx.func.definitely_assigned.add(pname)
|
|
3484
|
+
|
|
3485
|
+
body_type = self.analyze_expr(expr.body)
|
|
3486
|
+
|
|
3487
|
+
# Detect captures: names in body that are local variables from the outer scope
|
|
3488
|
+
# (not lambda params, not global functions, not builtins)
|
|
3489
|
+
param_set = set(expr.param_names)
|
|
3490
|
+
free_names = collect_name_refs(expr.body)
|
|
3491
|
+
captured = sorted((free_names - param_set) & outer_locals)
|
|
3492
|
+
expr.captured_names = captured
|
|
3493
|
+
# Callable context: captures must be by value (std::function can escape).
|
|
3494
|
+
# Fn (template) stays inline; captures by reference are safe.
|
|
3495
|
+
if isinstance(fn_type, CallableType) and not fn_type.is_template:
|
|
3496
|
+
expr.captures_by_value = True
|
|
3497
|
+
|
|
3498
|
+
# Check return type compatibility (allow implicit coercions like int literal -> Int32)
|
|
3499
|
+
if isinstance(fn_type.return_type, TypeParamRef):
|
|
3500
|
+
# Hint has unresolved type param (e.g. from generic builtin map[T,U]):
|
|
3501
|
+
# use the body's inferred type and return a concrete CallableType.
|
|
3502
|
+
# Resolve IntLiteralType so overload resolution sees a concrete int type.
|
|
3503
|
+
if isinstance(body_type, IntLiteralType):
|
|
3504
|
+
body_type = self.ctx.default_int_for_literal(body_type)
|
|
3505
|
+
expr.inferred_return_type = body_type
|
|
3506
|
+
self.compat.check_view_return_dangle(expr.body, body_type, expr.loc)
|
|
3507
|
+
concrete_params = tuple(fn_type.param_types)
|
|
3508
|
+
if fn_type.is_template:
|
|
3509
|
+
return make_fn_type(concrete_params, body_type)
|
|
3510
|
+
return CallableType(concrete_params, body_type)
|
|
3511
|
+
if body_type != fn_type.return_type:
|
|
3512
|
+
try:
|
|
3513
|
+
self.compat.check_type_compatible(
|
|
3514
|
+
body_type, fn_type.return_type,
|
|
3515
|
+
"lambda return", loc=expr.loc)
|
|
3516
|
+
except SemanticError:
|
|
3517
|
+
raise self.ctx.error(
|
|
3518
|
+
f"Lambda body type '{body_type}' is not compatible with "
|
|
3519
|
+
f"expected return type '{fn_type.return_type}'",
|
|
3520
|
+
expr
|
|
3521
|
+
)
|
|
3522
|
+
self.compat.check_view_return_dangle(expr.body, fn_type.return_type, expr.loc)
|
|
3523
|
+
|
|
3524
|
+
expr.inferred_return_type = fn_type.return_type
|
|
3525
|
+
return fn_type
|
|
3526
|
+
|
|
3527
|
+
# --- Function references ---
|
|
3528
|
+
|
|
3529
|
+
def _try_resolve_function_ref(
|
|
3530
|
+
self, expr: TpyName, hint: CallableType,
|
|
3531
|
+
) -> CallableType | None:
|
|
3532
|
+
"""Try to resolve a name as a function reference matching an Fn/Callable hint.
|
|
3533
|
+
|
|
3534
|
+
Returns a concrete Fn/Callable type if a matching function is found,
|
|
3535
|
+
None to fall through to normal name analysis.
|
|
3536
|
+
"""
|
|
3537
|
+
# Look up in namespace -- variables shadow functions
|
|
3538
|
+
if self.ctx.func.current_ns:
|
|
3539
|
+
binding = self.ctx.func.current_ns.lookup(expr.name)
|
|
3540
|
+
if binding:
|
|
3541
|
+
if binding.kind == BindingKind.VARIABLE:
|
|
3542
|
+
return None # local variable shadows any function
|
|
3543
|
+
if binding.kind == BindingKind.FUNCTION and binding.func_infos:
|
|
3544
|
+
matched_data = self._match_function_to_hint_data(
|
|
3545
|
+
binding.func_infos, hint, expr.name, expr)
|
|
3546
|
+
if matched_data is not None:
|
|
3547
|
+
matched, type_args = matched_data
|
|
3548
|
+
if type_args is not None:
|
|
3549
|
+
expr.function_ref_type_args = type_args
|
|
3550
|
+
expr.is_function_ref = True
|
|
3551
|
+
expr.function_ref_info = matched
|
|
3552
|
+
# Escape tracking: passing nested def to Callable (type-erased)
|
|
3553
|
+
# marks it as escaping. Fn (template) stays inline -- no escape.
|
|
3554
|
+
if (isinstance(hint, CallableType) and not hint.is_template
|
|
3555
|
+
and expr.name in self.ctx.func.nested_def_names):
|
|
3556
|
+
self.ctx.func.nested_def_escapes.add(expr.name)
|
|
3557
|
+
return self._concrete_fn_type(matched, expr, hint)
|
|
3558
|
+
|
|
3559
|
+
# Check registry (covers imported functions not yet in namespace)
|
|
3560
|
+
func_infos = self.ctx.registry.get_function(expr.name)
|
|
3561
|
+
if func_infos:
|
|
3562
|
+
matched_data = self._match_function_to_hint_data(
|
|
3563
|
+
func_infos, hint, expr.name, expr)
|
|
3564
|
+
if matched_data is not None:
|
|
3565
|
+
matched, type_args = matched_data
|
|
3566
|
+
if type_args is not None:
|
|
3567
|
+
expr.function_ref_type_args = type_args
|
|
3568
|
+
expr.is_function_ref = True
|
|
3569
|
+
expr.function_ref_info = matched
|
|
3570
|
+
return self._concrete_fn_type(matched, expr, hint)
|
|
3571
|
+
|
|
3572
|
+
return None
|
|
3573
|
+
|
|
3574
|
+
def _concrete_fn_type(
|
|
3575
|
+
self, fi: FunctionInfo, expr: TpyName, hint: CallableType,
|
|
3576
|
+
) -> CallableType:
|
|
3577
|
+
"""Build a concrete Fn/Callable type from a matched function's signature.
|
|
3578
|
+
|
|
3579
|
+
When the hint has TypeParamRef (e.g. from a generic builtin like map[T,U]),
|
|
3580
|
+
returns the concrete type from the function's actual signature so that
|
|
3581
|
+
overload resolution can infer the outer type params.
|
|
3582
|
+
"""
|
|
3583
|
+
if not contains_type_param(hint):
|
|
3584
|
+
return hint
|
|
3585
|
+
return self.build_concrete_callable(fi, expr.function_ref_type_args, hint)
|
|
3586
|
+
|
|
3587
|
+
def build_concrete_callable(
|
|
3588
|
+
self, fi: FunctionInfo,
|
|
3589
|
+
type_args: tuple[TpyType, ...] | None,
|
|
3590
|
+
hint: CallableType,
|
|
3591
|
+
) -> CallableType:
|
|
3592
|
+
"""Concrete Fn/Callable from `fi`'s actual signature, optionally
|
|
3593
|
+
substituted with `type_args`.
|
|
3594
|
+
|
|
3595
|
+
Strips Ref from param types and Own from return type -- the Fn
|
|
3596
|
+
type represents the logical callable contract. Ref on return
|
|
3597
|
+
type IS preserved so type inference can track reference
|
|
3598
|
+
semantics through combinators (e.g. map(identity, pts) infers
|
|
3599
|
+
U=Ref[Point] -> val_or_ref<Point>). Shape mirrors `hint` -- Fn
|
|
3600
|
+
if template, Callable otherwise.
|
|
3601
|
+
"""
|
|
3602
|
+
param_types = tuple(unwrap_ref_type(ptype) for _, ptype in fi.params)
|
|
3603
|
+
return_type = unwrap_own(fi.return_type)
|
|
3604
|
+
if fi.is_generic() and type_args:
|
|
3605
|
+
subst = dict(zip(fi.type_params, type_args))
|
|
3606
|
+
param_types = tuple(
|
|
3607
|
+
self.type_ops.substitute_type_params(p, subst) for p in param_types
|
|
3608
|
+
)
|
|
3609
|
+
return_type = self.type_ops.substitute_type_params(return_type, subst)
|
|
3610
|
+
if hint.is_template:
|
|
3611
|
+
return make_fn_type(param_types, return_type)
|
|
3612
|
+
return CallableType(param_types, return_type)
|
|
3613
|
+
|
|
3614
|
+
def _match_function_to_hint_data(
|
|
3615
|
+
self, func_infos: list[FunctionInfo], hint: CallableType,
|
|
3616
|
+
name: str, err_node: TpyName,
|
|
3617
|
+
) -> tuple[FunctionInfo, tuple[TpyType, ...] | None] | None:
|
|
3618
|
+
"""Find a function overload matching the Fn/Callable hint signature.
|
|
3619
|
+
|
|
3620
|
+
Pure data lookup: returns ``(matched_fi, inferred_type_args)`` on a
|
|
3621
|
+
unique match (``type_args`` is ``None`` for non-generic matches),
|
|
3622
|
+
``None`` when no overload matches (callers may treat the name as a
|
|
3623
|
+
variable instead). Raises ``SemanticError`` on ambiguity or
|
|
3624
|
+
generic-rejection -- ``err_node`` provides source location and is
|
|
3625
|
+
not otherwise mutated, so this matcher is safe to use during
|
|
3626
|
+
speculative overload probing.
|
|
3627
|
+
|
|
3628
|
+
Callers that want the AST mutated (``is_function_ref``,
|
|
3629
|
+
``function_ref_info``, ``function_ref_type_args``) commit those
|
|
3630
|
+
themselves after a winning candidate is chosen.
|
|
3631
|
+
"""
|
|
3632
|
+
hint_params = hint.param_types
|
|
3633
|
+
hint_return = hint.return_type
|
|
3634
|
+
# Each candidate is (FunctionInfo, inferred_type_args_or_None)
|
|
3635
|
+
candidates: list[tuple[FunctionInfo, tuple[TpyType, ...] | None]] = []
|
|
3636
|
+
# Track first generic rejection for diagnostics
|
|
3637
|
+
generic_rejection: str | None = None
|
|
3638
|
+
for fi in func_infos:
|
|
3639
|
+
if len(fi.params) != len(hint_params):
|
|
3640
|
+
continue
|
|
3641
|
+
if fi.is_generic():
|
|
3642
|
+
type_args, rejection = self._infer_generic_ref_type_args(fi, hint)
|
|
3643
|
+
if type_args is not None:
|
|
3644
|
+
candidates.append((fi, type_args))
|
|
3645
|
+
elif rejection is not None and generic_rejection is None:
|
|
3646
|
+
generic_rejection = rejection
|
|
3647
|
+
continue
|
|
3648
|
+
match = True
|
|
3649
|
+
for (_, ptype), htype in zip(fi.params, hint_params):
|
|
3650
|
+
if isinstance(htype, TypeParamRef):
|
|
3651
|
+
continue # unresolved type param in hint -- wildcard match
|
|
3652
|
+
if ptype != htype:
|
|
3653
|
+
try:
|
|
3654
|
+
self.compat.check_type_compatible(htype, ptype, "param")
|
|
3655
|
+
except SemanticError:
|
|
3656
|
+
match = False
|
|
3657
|
+
break
|
|
3658
|
+
if not match:
|
|
3659
|
+
continue
|
|
3660
|
+
if fi.return_type != hint_return:
|
|
3661
|
+
if isinstance(hint_return, (VoidType, TypeParamRef)):
|
|
3662
|
+
# VoidType: Python semantics (callers discard the return value)
|
|
3663
|
+
# TypeParamRef: unresolved type param in hint -- wildcard match
|
|
3664
|
+
pass
|
|
3665
|
+
else:
|
|
3666
|
+
try:
|
|
3667
|
+
self.compat.check_type_compatible(fi.return_type, hint_return, "return")
|
|
3668
|
+
except SemanticError:
|
|
3669
|
+
continue
|
|
3670
|
+
candidates.append((fi, None))
|
|
3671
|
+
if len(candidates) == 1:
|
|
3672
|
+
return candidates[0]
|
|
3673
|
+
if len(candidates) > 1:
|
|
3674
|
+
raise self.ctx.error(
|
|
3675
|
+
f"Ambiguous function reference: multiple overloads of '{name}' "
|
|
3676
|
+
f"match {hint}", err_node)
|
|
3677
|
+
# No match -- emit generic rejection diagnostic if we have one
|
|
3678
|
+
if generic_rejection is not None:
|
|
3679
|
+
raise self.ctx.error(generic_rejection, err_node)
|
|
3680
|
+
# Return None to fall through (might be a variable, not a function)
|
|
3681
|
+
return None
|
|
3682
|
+
|
|
3683
|
+
def _infer_generic_ref_type_args(
|
|
3684
|
+
self, fi: FunctionInfo, hint: CallableType,
|
|
3685
|
+
) -> tuple[tuple[TpyType, ...] | None, str | None]:
|
|
3686
|
+
"""Try to infer type parameters for a generic function from an Fn/Callable hint.
|
|
3687
|
+
|
|
3688
|
+
Returns (inferred_type_args, None) on success,
|
|
3689
|
+
(None, rejection_message) on bound or inference failure,
|
|
3690
|
+
(None, None) on type mismatch (not a candidate at all).
|
|
3691
|
+
"""
|
|
3692
|
+
inferred: dict[str, TpyType] = {}
|
|
3693
|
+
# Match each function param type against the hint param type
|
|
3694
|
+
for (_, ptype), htype in zip(fi.params, hint.param_types):
|
|
3695
|
+
if not self.type_ops.match_type_with_inference(ptype, htype, inferred):
|
|
3696
|
+
return None, None
|
|
3697
|
+
# Match return type (unless hint is void -- any return is acceptable)
|
|
3698
|
+
if not isinstance(hint.return_type, VoidType):
|
|
3699
|
+
if not self.type_ops.match_type_with_inference(fi.return_type, hint.return_type, inferred):
|
|
3700
|
+
return None, None
|
|
3701
|
+
# Check all type params were inferred
|
|
3702
|
+
unresolved = [tp for tp in fi.type_params if tp not in inferred]
|
|
3703
|
+
if unresolved:
|
|
3704
|
+
return None, (
|
|
3705
|
+
f"Cannot use '{fi.name}' as function reference: "
|
|
3706
|
+
f"cannot infer type parameter(s) {', '.join(unresolved)} "
|
|
3707
|
+
f"from {hint}"
|
|
3708
|
+
)
|
|
3709
|
+
# Validate type parameter bounds
|
|
3710
|
+
for param_name, type_arg in inferred.items():
|
|
3711
|
+
if param_name in fi.type_param_bounds:
|
|
3712
|
+
bound = fi.type_param_bounds[param_name]
|
|
3713
|
+
if not self.protocols.type_conforms_to_protocol(type_arg, bound):
|
|
3714
|
+
return None, (
|
|
3715
|
+
f"Cannot use '{fi.name}' as function reference: "
|
|
3716
|
+
f"inferred type argument {type_arg} for {param_name} "
|
|
3717
|
+
f"does not satisfy bound '{bound.name}'"
|
|
3718
|
+
)
|
|
3719
|
+
# Check for C++ param type mismatch (e.g. str: string_view vs const string&).
|
|
3720
|
+
# Generic functions use param_val_or_ref_t<T> which resolves based on the
|
|
3721
|
+
# storage type, but some types have a different param convention (str uses
|
|
3722
|
+
# string_view). This causes C++ compilation errors when the function is
|
|
3723
|
+
# passed through Fn/Callable.
|
|
3724
|
+
for param_name, type_arg in inferred.items():
|
|
3725
|
+
cpp_storage = type_arg.to_cpp()
|
|
3726
|
+
cpp_param = type_arg.to_cpp_param_type()
|
|
3727
|
+
# param_val_or_ref_t<T> resolves to const T& (value) or T& (object).
|
|
3728
|
+
# Accept T, T&, or const T& -- all compatible with the template.
|
|
3729
|
+
# Reject types with a different convention (e.g. str: string_view).
|
|
3730
|
+
if cpp_param not in (cpp_storage, f"{cpp_storage}&", f"const {cpp_storage}&"):
|
|
3731
|
+
return None, (
|
|
3732
|
+
f"Cannot use '{fi.name}' as function reference with "
|
|
3733
|
+
f"{param_name}={type_arg}: generic functions use a different "
|
|
3734
|
+
f"C++ parameter convention than {type_arg} "
|
|
3735
|
+
f"(use a lambda instead)"
|
|
3736
|
+
)
|
|
3737
|
+
return tuple(inferred[tp] for tp in fi.type_params), None
|