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
|
@@ -0,0 +1,2107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Type Compatibility
|
|
3
|
+
|
|
4
|
+
Type compatibility checking, coercions, and lvalue analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from dataclasses import replace as dc_replace
|
|
9
|
+
from typing import TYPE_CHECKING, Optional
|
|
10
|
+
|
|
11
|
+
from ..typesys import (
|
|
12
|
+
TpyType, IntLiteralType, FloatLiteralType, ListRepeatType,
|
|
13
|
+
PendingListType, PendingDictType, PendingSetType, PendingStrType, PendingBytesType, UnknownElementType,
|
|
14
|
+
LiteralType, LiteralValue, LiteralTag, FLOAT, make_list, make_dict, make_set,
|
|
15
|
+
OwnType, ReadonlyType, VoidType, PtrType, is_readonly_ptr, TupleType,
|
|
16
|
+
NominalType, AliasRef, RecursiveAliasInstanceType, TypeParamRef, NoneType, AnyType, OptionalType, UnionType,
|
|
17
|
+
is_protocol_type, is_dyn_protocol, unwrap_own, unwrap_readonly, unwrap_optional_own,
|
|
18
|
+
is_any_str_type, get_covariant_params, PendingGenericInstanceType,
|
|
19
|
+
CallableType, is_fn_type, RefType, unwrap_ref_type,
|
|
20
|
+
is_callable_type, is_integer_type, is_any_float_type, is_readonly_span,
|
|
21
|
+
is_polymorphic_class_type)
|
|
22
|
+
from ..parse import (
|
|
23
|
+
TpyExpr, TpyName, TpyFieldAccess, TpySubscript, TpyArrayLiteral,
|
|
24
|
+
TpyDictLiteral, TpySetLiteral, TpyListRepeat, TpyCall, TpyMethodCall, TpyUnaryOp,
|
|
25
|
+
TpyBinOp, TpyCoerce, TpyNoneLiteral, TpyIntLiteral, TpyStrLiteral, TpyBytesLiteral,
|
|
26
|
+
TpyFunction, TpyIfExpr, TpyTupleLiteral, SourceLocation
|
|
27
|
+
)
|
|
28
|
+
from .literal_utils import literal_value_from_expr
|
|
29
|
+
from ..coercions import resolve_coercion, Coercion, CoercionContext, DEREF_COERCION, UPCAST_TO_PTR, UPCAST_TO_CONST_PTR, SPAN_METHOD_TO_SPAN_ARG, SPAN_METHOD_TO_SPAN, INTO_ANY, FROM_ANY
|
|
30
|
+
from ..modules import get_span_return_type
|
|
31
|
+
from .context import addr_taken_roots
|
|
32
|
+
from .numeric_lattice import fixed_int_range_contains, numeric_info
|
|
33
|
+
from ..diagnostics import SemanticError, NOCOPY_REMEDIATION_HINT
|
|
34
|
+
from ..type_def_registry import (
|
|
35
|
+
is_set, is_dict, is_array, is_span, is_span_iter, is_list,
|
|
36
|
+
is_str_view_type, is_bytes_view_type, is_borrowing_view_type, int_traits_of,
|
|
37
|
+
is_big_int_type, is_str_category, is_bytes_category, is_str_type, is_string_type,
|
|
38
|
+
protocol_info_of,
|
|
39
|
+
)
|
|
40
|
+
from .overloads import type_matches_numeric
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _literal_value_from_source(
|
|
44
|
+
source_expr: TpyExpr | None, expected: LiteralType,
|
|
45
|
+
) -> LiteralValue | None:
|
|
46
|
+
"""LiteralValue from a literal AST source, gated on tag-vs-base agreement."""
|
|
47
|
+
lv = literal_value_from_expr(source_expr)
|
|
48
|
+
if lv is None:
|
|
49
|
+
return None
|
|
50
|
+
if lv.tag is LiteralTag.STR and expected.is_str_base():
|
|
51
|
+
return lv
|
|
52
|
+
if lv.tag is LiteralTag.BOOL and expected.is_bool_base():
|
|
53
|
+
return lv
|
|
54
|
+
if lv.tag is LiteralTag.INT and expected.is_int_base():
|
|
55
|
+
return lv
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _dangling_view_message(return_type: TpyType) -> str | None:
|
|
60
|
+
"""Error message for returning a view that borrows from a local, or None
|
|
61
|
+
if return_type is not a borrowing-view type.
|
|
62
|
+
"""
|
|
63
|
+
if is_str_view_type(return_type):
|
|
64
|
+
return ("Cannot return StrView referencing a local or temporary; "
|
|
65
|
+
"use str or String to return an owned copy")
|
|
66
|
+
if is_bytes_view_type(return_type):
|
|
67
|
+
return ("Cannot return BytesView referencing a local or temporary; "
|
|
68
|
+
"use bytes or bytearray to return an owned copy")
|
|
69
|
+
if is_span(return_type):
|
|
70
|
+
return ("Cannot return Span referencing a local or temporary; "
|
|
71
|
+
"use list or Array to return an owned copy")
|
|
72
|
+
if is_span_iter(return_type):
|
|
73
|
+
return ("Cannot return SpanIter referencing a local or temporary; "
|
|
74
|
+
"the underlying Span would dangle after the function returns")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _container_elem_matches(actual_elem: TpyType, expected_elem: TpyType) -> bool:
|
|
79
|
+
"""Strict element type check for container assignment.
|
|
80
|
+
|
|
81
|
+
Allows Own[T] stripping and numeric literal coercions only.
|
|
82
|
+
Subclass coercion is excluded: C++ containers are non-converting templates
|
|
83
|
+
(invariant T) -- set[Child] cannot be used where set[Base] is expected.
|
|
84
|
+
"""
|
|
85
|
+
check = expected_elem.wrapped if isinstance(expected_elem, OwnType) else expected_elem
|
|
86
|
+
return actual_elem == check or type_matches_numeric(actual_elem, check)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _contains_ref_type(t: TpyType) -> bool:
|
|
90
|
+
"""True if t or any nested type is or may be a reference type.
|
|
91
|
+
|
|
92
|
+
Recurses into inner_types() so that structural types like TupleType
|
|
93
|
+
(whose is_value_type() is hardcoded True) are checked element by element.
|
|
94
|
+
TypeParamRef with no ValueType bound is treated as potentially a reference type.
|
|
95
|
+
"""
|
|
96
|
+
if not t.is_value_type():
|
|
97
|
+
return True
|
|
98
|
+
# is_value_type() True -- recurse into inner types to catch e.g. tuple[str, Node]
|
|
99
|
+
return any(_contains_ref_type(inner) for inner in t.inner_types())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_definitely_ref_type(t: TpyType) -> bool:
|
|
103
|
+
"""True if t definitely contains a reference type (no unresolved TypeParamRefs).
|
|
104
|
+
|
|
105
|
+
Returns False when the ref-ness depends on a TypeParamRef, in which case
|
|
106
|
+
the caller should use a "may copy" warning instead of "copies".
|
|
107
|
+
"""
|
|
108
|
+
if isinstance(t, TypeParamRef):
|
|
109
|
+
return False # unknown until instantiated
|
|
110
|
+
if not t.is_value_type():
|
|
111
|
+
return True
|
|
112
|
+
return any(_is_definitely_ref_type(inner) for inner in t.inner_types())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _contains_semantic_ref(t: TpyType) -> bool:
|
|
116
|
+
"""True if t contains a RefType wrapper (explicit borrowed reference).
|
|
117
|
+
|
|
118
|
+
Unlike _contains_ref_type which checks is_value_type() (structural),
|
|
119
|
+
this checks for RefType specifically -- the semantic marker that sema
|
|
120
|
+
inserts for borrowed references. Used to detect iterator-to-container
|
|
121
|
+
copies where the source is an rvalue but yields borrowed elements.
|
|
122
|
+
"""
|
|
123
|
+
if isinstance(t, RefType):
|
|
124
|
+
return True
|
|
125
|
+
return any(_contains_semantic_ref(inner) for inner in t.inner_types())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _inner_compat_with_ro_widening(source_inner: TpyType, dest_inner: TpyType) -> bool:
|
|
129
|
+
"""True when `source_inner` is compatible with `dest_inner` at a Ptr <-> Optional
|
|
130
|
+
pointer-repr boundary, allowing mutable->readonly inner widening only.
|
|
131
|
+
|
|
132
|
+
Used for both `Ptr[T] -> T | None` (source_inner = ptr pointee, dest_inner =
|
|
133
|
+
optional inner) and `T | None -> Ptr[T]` (source_inner = optional inner,
|
|
134
|
+
dest_inner = ptr pointee). The two directions are complementary: in each,
|
|
135
|
+
`source_inner` is on the source side and `dest_inner` is on the destination
|
|
136
|
+
side, regardless of which side is the Ptr.
|
|
137
|
+
|
|
138
|
+
- Same inner: always allowed.
|
|
139
|
+
- Mutable source -> readonly destination: allowed (adding const is safe).
|
|
140
|
+
- Readonly source -> mutable destination: rejected (dropping const is unsafe).
|
|
141
|
+
"""
|
|
142
|
+
if source_inner == dest_inner:
|
|
143
|
+
return True
|
|
144
|
+
if isinstance(dest_inner, ReadonlyType) and not isinstance(source_inner, ReadonlyType):
|
|
145
|
+
return dest_inner.wrapped == source_inner
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _ptr_readonly_compatible(actual: PtrType, expected: PtrType) -> bool:
|
|
150
|
+
"""True if `actual`'s readonly-ness may flow into `expected` -- a
|
|
151
|
+
mutable target from a readonly source would launder away the const."""
|
|
152
|
+
return expected.is_readonly or not actual.is_readonly
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_natural_union_member(actual: TpyType, a_info, member: TpyType) -> bool:
|
|
156
|
+
"""True if `member` is the natural target for `actual` in a union match,
|
|
157
|
+
i.e. selecting it does not require a category-crossing widening.
|
|
158
|
+
|
|
159
|
+
`a_info` must be `numeric_info(actual)` (passed in to avoid recomputing).
|
|
160
|
+
"""
|
|
161
|
+
if member == actual:
|
|
162
|
+
return True
|
|
163
|
+
m_info = numeric_info(member)
|
|
164
|
+
if a_info is None or m_info is None:
|
|
165
|
+
return False
|
|
166
|
+
if a_info.family == m_info.family:
|
|
167
|
+
return True
|
|
168
|
+
# Literal families naturally land on their concrete counterpart.
|
|
169
|
+
if a_info.family == "int_literal" and m_info.family == "int":
|
|
170
|
+
return True
|
|
171
|
+
if a_info.family == "float_literal" and m_info.family == "float":
|
|
172
|
+
return True
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class CompatError:
|
|
177
|
+
"""Type compatibility check failure (returned by _check_compat, not raised)."""
|
|
178
|
+
__slots__ = ('message', 'loc')
|
|
179
|
+
|
|
180
|
+
def __init__(self, message: str, loc: SourceLocation | None = None):
|
|
181
|
+
self.message = message
|
|
182
|
+
self.loc = loc
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# Result type for _check_compat: Coercion | None (compatible) or CompatError (incompatible)
|
|
186
|
+
CompatResult = Coercion | CompatError | None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if TYPE_CHECKING:
|
|
190
|
+
from .context import SemanticContext
|
|
191
|
+
from .type_ops import TypeOperations
|
|
192
|
+
from .protocols import ProtocolChecker
|
|
193
|
+
from .methods import MethodAnalyzer
|
|
194
|
+
from .local_deduction import LocalTypeDeduction
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TypeCompatibility:
|
|
198
|
+
"""Type compatibility checking, coercions, and lvalue analysis."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, ctx: SemanticContext):
|
|
201
|
+
self.ctx = ctx
|
|
202
|
+
# Set after construction (compat is created before these exist)
|
|
203
|
+
self.type_ops: TypeOperations
|
|
204
|
+
self.protocols: ProtocolChecker
|
|
205
|
+
self.methods: MethodAnalyzer
|
|
206
|
+
self.deduction: 'LocalTypeDeduction'
|
|
207
|
+
|
|
208
|
+
def _mark_addr_taken(self, expr: TpyExpr) -> None:
|
|
209
|
+
"""Mark all param roots of expr as mutated because their address is taken.
|
|
210
|
+
|
|
211
|
+
Resolves alias / element / field / ptr borrow chains via
|
|
212
|
+
`effective_storage` so element-borrowing aliases (`v = items[i]`)
|
|
213
|
+
trace back to their underlying param. Loop variables also propagate
|
|
214
|
+
to their source iterables -- both are storage that the address-take
|
|
215
|
+
could mutate via the resulting pointer.
|
|
216
|
+
"""
|
|
217
|
+
for name in addr_taken_roots(expr):
|
|
218
|
+
root = self.ctx.func.borrow_tracker.effective_storage(name)
|
|
219
|
+
self.ctx.mark_param_mutated(root)
|
|
220
|
+
self.ctx.mark_loop_var_mutated(root)
|
|
221
|
+
|
|
222
|
+
def is_type_compatible(self, actual: TpyType, expected: TpyType) -> bool:
|
|
223
|
+
"""Non-raising check: is actual assignable to expected?"""
|
|
224
|
+
return not isinstance(self._check_compat(actual, expected, ""), CompatError)
|
|
225
|
+
|
|
226
|
+
def _check_polymorphic_slicing(
|
|
227
|
+
self,
|
|
228
|
+
actual: TpyType,
|
|
229
|
+
expected: TpyType,
|
|
230
|
+
source_expr: TpyExpr | None,
|
|
231
|
+
context: str,
|
|
232
|
+
loc: SourceLocation | None,
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Reject the Phase-20 slicing shape: a borrow of a polymorphic class
|
|
235
|
+
being stored into an owned slot of the same (or strict-subclass-of)
|
|
236
|
+
type. Without this rejection the dynamic type is silently dropped
|
|
237
|
+
when the value is copied into the slot. Recommended fix points at
|
|
238
|
+
`Box[Throwable]` (or `Box[Polymorphic]` for non-exception bases).
|
|
239
|
+
|
|
240
|
+
Allowed shapes (no slicing risk):
|
|
241
|
+
- fresh-rvalue constructor call of the exact destination type
|
|
242
|
+
- `None` assigned to an Optional slot
|
|
243
|
+
- any non-polymorphic destination
|
|
244
|
+
"""
|
|
245
|
+
if source_expr is None:
|
|
246
|
+
return
|
|
247
|
+
# Own[T] source carries move semantics -- the rvalue's dynamic type
|
|
248
|
+
# transfers fully to the destination. Skip the borrow-shape check.
|
|
249
|
+
if isinstance(actual, OwnType):
|
|
250
|
+
return
|
|
251
|
+
# Unwrap to find the owned slot's inner concrete type, if any.
|
|
252
|
+
target_inner = expected
|
|
253
|
+
if isinstance(target_inner, OwnType):
|
|
254
|
+
target_inner = target_inner.wrapped
|
|
255
|
+
if isinstance(target_inner, OptionalType):
|
|
256
|
+
target_inner = target_inner.inner
|
|
257
|
+
if not isinstance(target_inner, NominalType):
|
|
258
|
+
return
|
|
259
|
+
if not is_polymorphic_class_type(target_inner, self.ctx.registry):
|
|
260
|
+
return
|
|
261
|
+
# Fresh-rvalue construction (TpyCall with a class-name func). The
|
|
262
|
+
# rvalue's dynamic type matches its static type at the construction
|
|
263
|
+
# site, so codegen's Phase-18/19 subclass-into-Optional machinery
|
|
264
|
+
# can materialize the temp at the concrete class with no slicing.
|
|
265
|
+
# Covers both exact-type construction and concrete-subclass
|
|
266
|
+
# construction of a polymorphic root.
|
|
267
|
+
if isinstance(source_expr, TpyCall) and isinstance(source_expr.func, TpyName):
|
|
268
|
+
if self.ctx.registry.get_record(source_expr.func.name) is not None:
|
|
269
|
+
return
|
|
270
|
+
# None into Optional -- trivially fine.
|
|
271
|
+
if isinstance(source_expr, TpyNoneLiteral):
|
|
272
|
+
return
|
|
273
|
+
# Source is a borrow-shape: name, attribute, method call, function
|
|
274
|
+
# call result. The dynamic type could differ from the static type
|
|
275
|
+
# (caught exception, field of a base-class-typed slot, ...).
|
|
276
|
+
is_borrow_shape = isinstance(
|
|
277
|
+
source_expr,
|
|
278
|
+
(TpyName, TpyFieldAccess, TpyMethodCall, TpyCall),
|
|
279
|
+
)
|
|
280
|
+
if not is_borrow_shape:
|
|
281
|
+
return
|
|
282
|
+
# The actual type must itself be a polymorphic class (or Optional[Poly])
|
|
283
|
+
# for slicing to be possible. Strip qualifiers + Optional + Ref.
|
|
284
|
+
src_inner = actual
|
|
285
|
+
for _ in range(4):
|
|
286
|
+
if isinstance(src_inner, RefType):
|
|
287
|
+
src_inner = src_inner.wrapped
|
|
288
|
+
elif isinstance(src_inner, OwnType):
|
|
289
|
+
src_inner = src_inner.wrapped
|
|
290
|
+
elif isinstance(src_inner, ReadonlyType):
|
|
291
|
+
src_inner = src_inner.wrapped
|
|
292
|
+
elif isinstance(src_inner, OptionalType):
|
|
293
|
+
src_inner = src_inner.inner
|
|
294
|
+
else:
|
|
295
|
+
break
|
|
296
|
+
if not isinstance(src_inner, NominalType):
|
|
297
|
+
return
|
|
298
|
+
if not is_polymorphic_class_type(src_inner, self.ctx.registry):
|
|
299
|
+
return
|
|
300
|
+
raise SemanticError(
|
|
301
|
+
f"cannot store '{src_inner.name}' borrow as owned "
|
|
302
|
+
f"'{target_inner.name}' in {context} -- the dynamic type may "
|
|
303
|
+
f"be a subclass and would be lost (slicing). Use "
|
|
304
|
+
f"`Box[Throwable]` (or `Box[{target_inner.name}]` for "
|
|
305
|
+
f"non-exception roots) for owned polymorphic storage: "
|
|
306
|
+
f"`slot = Box(e.clone())`.",
|
|
307
|
+
loc,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def check_type_compatible(
|
|
311
|
+
self, actual: TpyType, expected: TpyType, context: str,
|
|
312
|
+
loc: SourceLocation | None = None,
|
|
313
|
+
source_expr: TpyExpr | None = None,
|
|
314
|
+
is_return: bool = False,
|
|
315
|
+
coercion_ctx: CoercionContext | None = None,
|
|
316
|
+
target_is_storage_form: bool = False,
|
|
317
|
+
) -> Optional[Coercion]:
|
|
318
|
+
"""Check if actual type is compatible with expected type.
|
|
319
|
+
|
|
320
|
+
Raises SemanticError on incompatible types.
|
|
321
|
+
Returns a Coercion if a conversion should be applied at codegen time,
|
|
322
|
+
or None if compatible with no coercion needed.
|
|
323
|
+
"""
|
|
324
|
+
# Phase 20 Stage 5a: reject the slicing shape -- storing a borrow
|
|
325
|
+
# of a polymorphic class into an owned slot of the same type would
|
|
326
|
+
# silently drop the dynamic type. Fires before _check_compat so the
|
|
327
|
+
# user sees a targeted "use Box[Throwable]" diagnostic rather than
|
|
328
|
+
# a generic type mismatch.
|
|
329
|
+
self._check_polymorphic_slicing(actual, expected, source_expr, context, loc)
|
|
330
|
+
# check_type_compatible raises on failure, so all callers are
|
|
331
|
+
# commit points -- safe to apply the retro-widen side effect here
|
|
332
|
+
# without leaking it into overload probes (which use _check_compat
|
|
333
|
+
# / is_type_compatible directly).
|
|
334
|
+
if isinstance(source_expr, TpyName):
|
|
335
|
+
new_actual = self.deduction.try_retro_widen_literal_arg(
|
|
336
|
+
source_expr.name, actual, expected,
|
|
337
|
+
getattr(source_expr, "loc", None) or loc,
|
|
338
|
+
)
|
|
339
|
+
if new_actual is not None:
|
|
340
|
+
actual = new_actual
|
|
341
|
+
self.ctx.set_expr_type(source_expr, new_actual)
|
|
342
|
+
else:
|
|
343
|
+
self._maybe_raise_literal_local_range(source_expr, actual, expected, context)
|
|
344
|
+
result = self._check_compat(actual, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
345
|
+
if isinstance(result, CompatError):
|
|
346
|
+
# Surface where a previously retro-widened local's type got
|
|
347
|
+
# pinned, so the user sees why a non-default type appears in
|
|
348
|
+
# the message even though they wrote `a = 0`.
|
|
349
|
+
prior_loc = (self.ctx.func.retro_widened_locs.get(source_expr.name)
|
|
350
|
+
if isinstance(source_expr, TpyName) else None)
|
|
351
|
+
if prior_loc is not None:
|
|
352
|
+
hint = (f" ('{source_expr.name}' was promoted to '{actual}' by "
|
|
353
|
+
f"earlier use at line {prior_loc.line})")
|
|
354
|
+
raise SemanticError(result.message + hint, result.loc)
|
|
355
|
+
raise SemanticError(result.message, result.loc)
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
def _resolve_recursive_refs(self, typ: TpyType) -> TpyType:
|
|
359
|
+
"""Resolve AliasRef self-references to recursive union aliases.
|
|
360
|
+
|
|
361
|
+
Handles both bare AliasRef("Tree") and AliasRef nested inside
|
|
362
|
+
containers (e.g. list[AliasRef("Tree")] -> list[UnionType(...)]).
|
|
363
|
+
Treats recursive union types as opaque (does not recurse into their
|
|
364
|
+
members) to prevent infinite expansion of self-referencing placeholders.
|
|
365
|
+
"""
|
|
366
|
+
if isinstance(typ, AliasRef):
|
|
367
|
+
alias = self.ctx.registry.get_type_alias(typ.name)
|
|
368
|
+
if alias is not None:
|
|
369
|
+
return alias
|
|
370
|
+
# Generic recursive alias instances are opaque, like the non-generic
|
|
371
|
+
# UnionType wrapper below -- their alternatives live behind the
|
|
372
|
+
# wrapper and must not be expanded here.
|
|
373
|
+
if isinstance(typ, RecursiveAliasInstanceType):
|
|
374
|
+
return typ
|
|
375
|
+
if not self.ctx.recursive_union_names:
|
|
376
|
+
return typ
|
|
377
|
+
# Don't recurse into recursive union aliases -- their AliasRef
|
|
378
|
+
# placeholders are structural and must not be expanded.
|
|
379
|
+
if isinstance(typ, UnionType) and typ.needs_wrapper():
|
|
380
|
+
return typ
|
|
381
|
+
inner = typ.inner_types()
|
|
382
|
+
if not inner:
|
|
383
|
+
return typ
|
|
384
|
+
new_inner = tuple(self._resolve_recursive_refs(t) for t in inner)
|
|
385
|
+
if all(new is old for new, old in zip(new_inner, inner)):
|
|
386
|
+
return typ
|
|
387
|
+
return typ.with_inner_types(new_inner)
|
|
388
|
+
|
|
389
|
+
def _check_compat(
|
|
390
|
+
self, actual: TpyType, expected: TpyType, context: str,
|
|
391
|
+
loc: SourceLocation | None = None,
|
|
392
|
+
source_expr: TpyExpr | None = None,
|
|
393
|
+
is_return: bool = False,
|
|
394
|
+
coercion_ctx: CoercionContext | None = None,
|
|
395
|
+
target_is_storage_form: bool = False,
|
|
396
|
+
) -> CompatResult:
|
|
397
|
+
"""Core type compatibility check.
|
|
398
|
+
|
|
399
|
+
Returns Coercion or None on success, CompatError on failure.
|
|
400
|
+
|
|
401
|
+
target_is_storage_form: True when the destination is a field or
|
|
402
|
+
container element (value-storage form). Used to suppress address-
|
|
403
|
+
take mutation marking that only applies to borrow-form destinations
|
|
404
|
+
(params/locals/returns); storage-form assignments copy the value.
|
|
405
|
+
"""
|
|
406
|
+
# Resolve NominalType self-references from recursive union members.
|
|
407
|
+
# e.g. NominalType("Tree") -> UnionType, and also inside containers:
|
|
408
|
+
# list[NominalType("Tree")] -> list[UnionType(...)].
|
|
409
|
+
# Uses seen-set guard to prevent infinite recursion.
|
|
410
|
+
actual = self._resolve_recursive_refs(actual)
|
|
411
|
+
expected = self._resolve_recursive_refs(expected)
|
|
412
|
+
|
|
413
|
+
# OwnType from name lookup (implicit owned local) should not
|
|
414
|
+
# shortcircuit the Own[T] coercion path -- that path emits copy
|
|
415
|
+
# warnings when storing at non-last-use.
|
|
416
|
+
if actual == expected:
|
|
417
|
+
if not (isinstance(actual, OwnType) and isinstance(source_expr, TpyName)):
|
|
418
|
+
return None
|
|
419
|
+
# CallableType (Fn and Callable): inner param/return types may carry
|
|
420
|
+
# Own/Ref qualifiers from FI that don't affect callable contract compatibility.
|
|
421
|
+
if (is_callable_type(actual)
|
|
422
|
+
and is_callable_type(expected)
|
|
423
|
+
and len(actual.param_types) == len(expected.param_types)):
|
|
424
|
+
stripped_actual = type(actual)(
|
|
425
|
+
tuple(unwrap_ref_type(unwrap_own(p)) for p in actual.param_types),
|
|
426
|
+
unwrap_ref_type(unwrap_own(actual.return_type)))
|
|
427
|
+
stripped_expected = type(expected)(
|
|
428
|
+
tuple(unwrap_ref_type(unwrap_own(p)) for p in expected.param_types),
|
|
429
|
+
unwrap_ref_type(unwrap_own(expected.return_type)))
|
|
430
|
+
if stripped_actual == stripped_expected:
|
|
431
|
+
return None
|
|
432
|
+
# None literal (NoneType) is compatible with None annotation (VoidType)
|
|
433
|
+
if isinstance(actual, NoneType) and isinstance(expected, VoidType):
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
# Pending generic instance: try to resolve from the expected type
|
|
437
|
+
if isinstance(actual, PendingGenericInstanceType):
|
|
438
|
+
resolved = self.methods.try_resolve_pending_from_expected_type(
|
|
439
|
+
actual, expected, loc)
|
|
440
|
+
if resolved is not None:
|
|
441
|
+
if source_expr is not None:
|
|
442
|
+
self.ctx.set_expr_type(source_expr, resolved)
|
|
443
|
+
return self._check_compat(
|
|
444
|
+
resolved, expected, context, loc, source_expr,
|
|
445
|
+
is_return, coercion_ctx, target_is_storage_form)
|
|
446
|
+
return CompatError(
|
|
447
|
+
f"Type mismatch in {context}: '{actual.record_name}' has unresolved type "
|
|
448
|
+
f"arguments; call a constraining method first or add explicit type arguments",
|
|
449
|
+
loc,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Ref[T] in expected: strip Ref and check inner type.
|
|
453
|
+
# Ref is a codegen annotation (reference semantics); for type
|
|
454
|
+
# compatibility, T is compatible with Ref[T].
|
|
455
|
+
if isinstance(expected, RefType):
|
|
456
|
+
return self._check_compat(
|
|
457
|
+
unwrap_ref_type(actual), expected.wrapped, context, loc,
|
|
458
|
+
source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
459
|
+
# Strip Ref from actual too (Ref[T] is compatible with T)
|
|
460
|
+
if isinstance(actual, RefType):
|
|
461
|
+
return self._check_compat(
|
|
462
|
+
actual.wrapped, expected, context, loc,
|
|
463
|
+
source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
464
|
+
|
|
465
|
+
# readonly[T] -> readonly[T]: unwrap and check inner types
|
|
466
|
+
# T -> readonly[T]: always OK (adding const is safe)
|
|
467
|
+
if isinstance(expected, ReadonlyType):
|
|
468
|
+
actual_inner = unwrap_readonly(actual)
|
|
469
|
+
return self._check_compat(
|
|
470
|
+
actual_inner, expected.wrapped, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# readonly[T] -> T: error for non-value types (stripping const is unsafe)
|
|
474
|
+
# Exceptions: value types (copies), readonly protocols (Sized, Sequence),
|
|
475
|
+
# return values (C++ const method handles safety via const propagation)
|
|
476
|
+
# Note: mutable Span is NOT excepted even for returns -- you cannot
|
|
477
|
+
# construct a mutable span from a const container.
|
|
478
|
+
if isinstance(actual, ReadonlyType) and not isinstance(expected, ReadonlyType):
|
|
479
|
+
is_mutable_span = is_span(expected) and not is_readonly_span(expected)
|
|
480
|
+
if (not expected.is_value_type() or is_mutable_span) and (not is_return or is_mutable_span):
|
|
481
|
+
allow = False
|
|
482
|
+
if is_protocol_type(expected) and self.protocols:
|
|
483
|
+
proto_info = protocol_info_of(expected)
|
|
484
|
+
if proto_info and self.protocols.is_all_readonly(proto_info):
|
|
485
|
+
allow = True
|
|
486
|
+
if not allow:
|
|
487
|
+
return CompatError(
|
|
488
|
+
f"Cannot return readonly[{actual.wrapped}] as mutable {expected}; "
|
|
489
|
+
f"use Span[readonly[T]] or annotate return type with auto_readonly[T]"
|
|
490
|
+
if is_return else
|
|
491
|
+
f"Cannot pass readonly[{actual.wrapped}] as mutable {expected} in {context}",
|
|
492
|
+
loc,
|
|
493
|
+
)
|
|
494
|
+
return self._check_compat(
|
|
495
|
+
actual.wrapped, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# None -> Optional[T] / Ptr[T] / Ptr[readonly[T]]: always compatible
|
|
499
|
+
if isinstance(actual, NoneType) and isinstance(expected, (OptionalType, PtrType)):
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
# T -> Any: any copyable type stores into Any. Move-only sources
|
|
503
|
+
# (Own[T] / @nocopy records / containers thereof) are rejected --
|
|
504
|
+
# v1 supports copyable contents only. The INTO_ANY coercion wraps
|
|
505
|
+
# the source as `tpy::Any{std::any{value}, &any_ops_for<T>}` and
|
|
506
|
+
# upgrades view types (StrView, BytesView) to their owning form.
|
|
507
|
+
if isinstance(expected, AnyType) and not isinstance(actual, AnyType):
|
|
508
|
+
actual_for_check = unwrap_ref_type(actual)
|
|
509
|
+
if self.ctx.is_type_nocopy(actual_for_check):
|
|
510
|
+
return CompatError(
|
|
511
|
+
f"cannot store move-only type '{actual}' in Any "
|
|
512
|
+
f"(v1 supports copyable contents only)",
|
|
513
|
+
loc,
|
|
514
|
+
)
|
|
515
|
+
# Storing a reference type (record/list/dict/set) into Any
|
|
516
|
+
# silently copies (Principle #1: Any owns its contents).
|
|
517
|
+
# That contradicts TPy's reference-type baseline -- elsewhere a
|
|
518
|
+
# copy of a record/container requires explicit copy(). Suppressed
|
|
519
|
+
# for rvalue sources (no other handle), copy()-wrapped sources,
|
|
520
|
+
# and last-use auto-move (matches the dict.update convention).
|
|
521
|
+
inner = unwrap_own(unwrap_readonly(actual_for_check))
|
|
522
|
+
if (source_expr is not None
|
|
523
|
+
and self.is_lvalue(source_expr)
|
|
524
|
+
and not self.is_copy_call(source_expr)
|
|
525
|
+
and not self._is_auto_moved(source_expr)
|
|
526
|
+
and self._warns_copy_into_any(inner)):
|
|
527
|
+
self.ctx.warning(
|
|
528
|
+
f"copies {inner} into Any; use copy() to make this explicit",
|
|
529
|
+
source_expr,
|
|
530
|
+
)
|
|
531
|
+
return INTO_ANY
|
|
532
|
+
|
|
533
|
+
# Any -> T: runtime-checked auto-coerce. Target T must be a concrete
|
|
534
|
+
# type -- Union / Optional / generic-type-param / protocol targets
|
|
535
|
+
# are too ambiguous (which member to extract? what concrete T to
|
|
536
|
+
# any_cast against?). Users narrow first via isinstance or call
|
|
537
|
+
# typing.cast(T, x) explicitly. Codegen emits any_cast_or_panic<T_cpp>;
|
|
538
|
+
# typeid mismatches panic at runtime with the documented message.
|
|
539
|
+
# Protocol targets fall through to the structural-conformance path
|
|
540
|
+
# (Any satisfies Hashable / Equatable / Stringable / Representable
|
|
541
|
+
# at the type-system level; runtime ops may panic if the contained
|
|
542
|
+
# T lacks the capability). Own[Any] / readonly[Any] targets also
|
|
543
|
+
# fall through (Any -> Any is a no-op, not an extraction).
|
|
544
|
+
if isinstance(actual, AnyType):
|
|
545
|
+
expected_inner = expected
|
|
546
|
+
if isinstance(expected_inner, OwnType):
|
|
547
|
+
expected_inner = expected_inner.wrapped
|
|
548
|
+
expected_inner = unwrap_readonly(expected_inner)
|
|
549
|
+
if isinstance(expected_inner, AnyType):
|
|
550
|
+
return None
|
|
551
|
+
if isinstance(expected_inner, (UnionType, OptionalType)):
|
|
552
|
+
return CompatError(
|
|
553
|
+
f"cannot auto-coerce Any to {expected} -- "
|
|
554
|
+
f"narrow first via isinstance(x, T) or typing.cast(T, x)",
|
|
555
|
+
loc,
|
|
556
|
+
)
|
|
557
|
+
if isinstance(expected_inner, TypeParamRef):
|
|
558
|
+
return CompatError(
|
|
559
|
+
f"cannot auto-coerce Any to generic type parameter "
|
|
560
|
+
f"'{expected_inner.name}' -- typing.cast(ConcreteT, x) first",
|
|
561
|
+
loc,
|
|
562
|
+
)
|
|
563
|
+
if not is_protocol_type(expected_inner):
|
|
564
|
+
return FROM_ANY
|
|
565
|
+
|
|
566
|
+
# Union[A, B] -> Union[A, B, C]: each actual member must match some expected member
|
|
567
|
+
if isinstance(actual, UnionType) and isinstance(expected, UnionType):
|
|
568
|
+
for member in actual.members:
|
|
569
|
+
result = self._check_compat(member, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
570
|
+
if isinstance(result, CompatError):
|
|
571
|
+
return result
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
# T -> Union[T, ...] (and T -> a generic recursive alias wrapper, whose
|
|
575
|
+
# alternatives stand in for the union members): actual must match at
|
|
576
|
+
# least one member. Iterate twice so a category-crossing widening (e.g.
|
|
577
|
+
# int -> float) never wins when an in-category member exists. Without
|
|
578
|
+
# this, an int going into `int | float` would silently coerce to float
|
|
579
|
+
# when float happens to be earlier in the canonical member order.
|
|
580
|
+
union_members: 'tuple[TpyType, ...] | None' = None
|
|
581
|
+
if isinstance(expected, UnionType):
|
|
582
|
+
union_members = expected.members
|
|
583
|
+
elif isinstance(expected, RecursiveAliasInstanceType):
|
|
584
|
+
union_members = expected.alternatives()
|
|
585
|
+
if union_members is not None:
|
|
586
|
+
actual_unwrapped = unwrap_own(actual)
|
|
587
|
+
a_info = numeric_info(actual_unwrapped)
|
|
588
|
+
for member in union_members:
|
|
589
|
+
if not _is_natural_union_member(actual_unwrapped, a_info, member):
|
|
590
|
+
continue
|
|
591
|
+
result = self._check_compat(actual, member, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
592
|
+
if not isinstance(result, CompatError):
|
|
593
|
+
return result
|
|
594
|
+
for member in union_members:
|
|
595
|
+
if _is_natural_union_member(actual_unwrapped, a_info, member):
|
|
596
|
+
continue
|
|
597
|
+
result = self._check_compat(actual, member, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
598
|
+
if not isinstance(result, CompatError):
|
|
599
|
+
return result
|
|
600
|
+
return CompatError(f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
|
|
601
|
+
|
|
602
|
+
# T -> Optional[T]: implicit wrapping
|
|
603
|
+
if isinstance(expected, OptionalType):
|
|
604
|
+
if isinstance(actual, OwnType):
|
|
605
|
+
actual_inner = actual.wrapped
|
|
606
|
+
else:
|
|
607
|
+
actual_inner = actual
|
|
608
|
+
# Own[Optional[T]] -> Optional[T]: exact match after unwrap
|
|
609
|
+
if actual_inner == expected:
|
|
610
|
+
return None
|
|
611
|
+
# Optional[Own[T]] -> Optional[T]: unwrap Own inside Optional
|
|
612
|
+
if isinstance(actual_inner, OptionalType) and isinstance(actual_inner.inner, OwnType):
|
|
613
|
+
actual_inner = OptionalType(actual_inner.inner.wrapped)
|
|
614
|
+
if actual_inner == expected:
|
|
615
|
+
return None
|
|
616
|
+
# For pointer-repr Optional[T], codegen takes &(source) when source is a
|
|
617
|
+
# non-value record (not already Optional/Ptr). Mark source params as needing
|
|
618
|
+
# T& so &(param) stays valid (not const T&). Skip when the destination
|
|
619
|
+
# is in storage form (field / container element) -- the Optional there
|
|
620
|
+
# lowers to std::optional<T> (value-storage) and the assignment is a
|
|
621
|
+
# copy; no address-take happens, so the source's const-ness is fine.
|
|
622
|
+
if (expected.uses_pointer_repr()
|
|
623
|
+
and source_expr is not None
|
|
624
|
+
and not actual_inner.is_value_type()
|
|
625
|
+
and not isinstance(actual_inner, (OptionalType, PtrType, NoneType))
|
|
626
|
+
and not target_is_storage_form):
|
|
627
|
+
self._mark_addr_taken(source_expr)
|
|
628
|
+
# Ptr[T] -> T | None (pointer-repr): byte-identical T* at C++ level.
|
|
629
|
+
# Per-context idiom: Ptr[T] for storage, T | None for nullable returns.
|
|
630
|
+
if (isinstance(actual_inner, PtrType)
|
|
631
|
+
and expected.uses_pointer_repr()
|
|
632
|
+
and _inner_compat_with_ro_widening(actual_inner.pointee, expected.inner)):
|
|
633
|
+
return None
|
|
634
|
+
# Optional[A] -> Optional[B] via a whole-Optional coercion rule
|
|
635
|
+
# (e.g. str <-> StrView at arg position, where both lower to
|
|
636
|
+
# `std::optional<std::string_view>`). Try before stripping to the
|
|
637
|
+
# inner, since the inner check can't see that the wrappers share
|
|
638
|
+
# a C++ representation.
|
|
639
|
+
if isinstance(actual_inner, OptionalType):
|
|
640
|
+
ctx_for_coerce = coercion_ctx or context
|
|
641
|
+
if isinstance(ctx_for_coerce, CoercionContext):
|
|
642
|
+
whole = resolve_coercion(actual_inner, expected, ctx_for_coerce)
|
|
643
|
+
if whole is not None:
|
|
644
|
+
return whole
|
|
645
|
+
result = self._check_compat(actual_inner, expected.inner, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
646
|
+
# Rewrap inner-mismatch errors with the declared Optional types so
|
|
647
|
+
# the diagnostic reads `expected str | None, got StrView | None`
|
|
648
|
+
# rather than the truncated `expected str, got StrView | None`.
|
|
649
|
+
if isinstance(result, CompatError) and isinstance(actual, OptionalType):
|
|
650
|
+
return CompatError(
|
|
651
|
+
f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
|
|
652
|
+
return result
|
|
653
|
+
|
|
654
|
+
# Optional[T] -> Optional[T] already handled by == check above
|
|
655
|
+
# Optional[T] -> T: error (cannot implicitly unwrap)
|
|
656
|
+
|
|
657
|
+
# T | None (pointer-repr) -> Ptr[T]: byte-identical T* at C++ level.
|
|
658
|
+
# Per-context idiom: T | None for nullable returns, Ptr[T] for storage.
|
|
659
|
+
# Storage-form Optional sources (field access, container element) need
|
|
660
|
+
# an `optional_to_ptr` lift at codegen, mirroring the existing
|
|
661
|
+
# storage-form-to-borrow-form lift for OptionalType destinations.
|
|
662
|
+
if isinstance(expected, PtrType):
|
|
663
|
+
actual_for_opt = unwrap_own(actual)
|
|
664
|
+
if (isinstance(actual_for_opt, OptionalType)
|
|
665
|
+
and actual_for_opt.uses_pointer_repr()
|
|
666
|
+
and _inner_compat_with_ro_widening(actual_for_opt.inner, expected.pointee)):
|
|
667
|
+
return None
|
|
668
|
+
|
|
669
|
+
# NominalType with Own[T] in type args signals copy semantics -- applies to both
|
|
670
|
+
# protocols (Iterable[Own[T]]) and concrete containers (dict[K, Own[V]]).
|
|
671
|
+
# Strip Own for conformance/equality check; warn when elements are implicitly copied.
|
|
672
|
+
if (isinstance(expected, NominalType)
|
|
673
|
+
and expected.inner_types()
|
|
674
|
+
and any(isinstance(a, OwnType) for a in expected.inner_types())):
|
|
675
|
+
stripped_inner = tuple(a.wrapped if isinstance(a, OwnType) else a for a in expected.inner_types())
|
|
676
|
+
stripped_expected = expected.with_inner_types(stripped_inner)
|
|
677
|
+
# Own[T] actual means caller acknowledged ownership transfer -- no warning.
|
|
678
|
+
actual_inner = actual.wrapped if isinstance(actual, OwnType) else actual
|
|
679
|
+
if is_protocol_type(expected):
|
|
680
|
+
structurally_ok = bool(
|
|
681
|
+
self.protocols and self.protocols.type_conforms_to_protocol(actual_inner, stripped_expected)
|
|
682
|
+
)
|
|
683
|
+
else:
|
|
684
|
+
# Concrete type: exact match after Own stripping
|
|
685
|
+
structurally_ok = (actual_inner == stripped_expected)
|
|
686
|
+
if structurally_ok:
|
|
687
|
+
# Own[T] from explicit return/param means caller acknowledged
|
|
688
|
+
# ownership -- skip warning. Own[T] from name lookup (owned
|
|
689
|
+
# local) is implicit and still needs the warning.
|
|
690
|
+
explicit_own = (isinstance(actual, OwnType)
|
|
691
|
+
and not isinstance(source_expr, TpyName))
|
|
692
|
+
if (source_expr is not None
|
|
693
|
+
and not explicit_own
|
|
694
|
+
and not self.is_copy_call(source_expr)):
|
|
695
|
+
is_lvalue_src = self.is_lvalue(source_expr)
|
|
696
|
+
# Suppress at last use: no observable semantic divergence from
|
|
697
|
+
# CPython when the source is dead after this point (Framing A).
|
|
698
|
+
is_auto_moved = self._is_auto_moved(source_expr)
|
|
699
|
+
if not is_auto_moved:
|
|
700
|
+
for inner_t in expected.inner_types():
|
|
701
|
+
if not isinstance(inner_t, OwnType):
|
|
702
|
+
continue
|
|
703
|
+
elem_type = inner_t.wrapped
|
|
704
|
+
# Rvalue iterators that yield Ref elements (e.g.
|
|
705
|
+
# map(identity, pts)) copy on materialization.
|
|
706
|
+
# RefType in the Own-wrapped element is the proof.
|
|
707
|
+
has_ref_elements = _contains_semantic_ref(elem_type)
|
|
708
|
+
if is_lvalue_src or has_ref_elements:
|
|
709
|
+
if not _contains_ref_type(elem_type):
|
|
710
|
+
continue
|
|
711
|
+
display_type = unwrap_ref_type(elem_type)
|
|
712
|
+
# Lvalue source (container): suggest copy_iter or copy.
|
|
713
|
+
# Rvalue with Ref elements (iterator): only copy_iter.
|
|
714
|
+
if is_lvalue_src:
|
|
715
|
+
hint = "use copy_iter() to make this explicit (or copy() to copy the entire container)"
|
|
716
|
+
else:
|
|
717
|
+
hint = "use copy_iter() to make this explicit"
|
|
718
|
+
if _is_definitely_ref_type(elem_type):
|
|
719
|
+
self.ctx.warning(
|
|
720
|
+
f"copies {display_type} elements; {hint}",
|
|
721
|
+
source_expr,
|
|
722
|
+
)
|
|
723
|
+
else:
|
|
724
|
+
self.ctx.warning(
|
|
725
|
+
f"may copy {display_type} elements if not a value type; {hint}",
|
|
726
|
+
source_expr,
|
|
727
|
+
)
|
|
728
|
+
return None
|
|
729
|
+
if is_protocol_type(expected):
|
|
730
|
+
return CompatError(
|
|
731
|
+
f"Type {actual_inner} does not conform to protocol {stripped_expected} in {context}",
|
|
732
|
+
source_expr.loc if source_expr is not None else loc
|
|
733
|
+
)
|
|
734
|
+
# Concrete type mismatch: fall through to generic error handling
|
|
735
|
+
|
|
736
|
+
# Protocol matching (structural subtyping)
|
|
737
|
+
if is_protocol_type(expected):
|
|
738
|
+
# Own[T] conforms to any protocol that T conforms to
|
|
739
|
+
check_actual = actual.wrapped if isinstance(actual, OwnType) else actual
|
|
740
|
+
if self.protocols and self.protocols.type_conforms_to_protocol(check_actual, expected):
|
|
741
|
+
return None # No coercion needed, structural match
|
|
742
|
+
return CompatError(
|
|
743
|
+
f"Type {check_actual} does not conform to protocol {expected} in {context}",
|
|
744
|
+
loc
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Callable object -> Fn/Callable: record with __call__ matching the signature
|
|
748
|
+
if isinstance(actual, NominalType) and is_callable_type(expected):
|
|
749
|
+
record = self.ctx.registry.get_record_for_type(actual)
|
|
750
|
+
if record:
|
|
751
|
+
overloads = self.ctx.registry.get_method_overloads_with_parents(record, "__call__")
|
|
752
|
+
if overloads:
|
|
753
|
+
assert len(overloads) == 1, f"multiple __call__ overloads not supported"
|
|
754
|
+
fi = overloads[0]
|
|
755
|
+
if (len(fi.params) == len(expected.param_types)
|
|
756
|
+
and all(p.type == e for (p, e) in zip(fi.params, expected.param_types))
|
|
757
|
+
and (fi.return_type == expected.return_type
|
|
758
|
+
or isinstance(expected.return_type, VoidType))):
|
|
759
|
+
return None
|
|
760
|
+
return CompatError(
|
|
761
|
+
f"'__call__' signature ({', '.join(str(p.type) for p in fi.params)}) -> {fi.return_type} "
|
|
762
|
+
f"does not match {expected} in {context}",
|
|
763
|
+
loc,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
# Callable -> Fn: std::function satisfies template requires clauses in C++
|
|
767
|
+
if isinstance(actual, CallableType) and is_fn_type(expected):
|
|
768
|
+
if (len(actual.param_types) == len(expected.param_types)
|
|
769
|
+
and all(self._check_compat(a, e, "param", loc) is None
|
|
770
|
+
for a, e in zip(actual.param_types, expected.param_types))
|
|
771
|
+
and (self._check_compat(actual.return_type, expected.return_type, "return", loc) is None
|
|
772
|
+
or isinstance(expected.return_type, VoidType))):
|
|
773
|
+
return None
|
|
774
|
+
|
|
775
|
+
# Inheritance: Child -> Parent (implicit value upcast, C++ handles slicing/ref binding)
|
|
776
|
+
if (isinstance(actual, NominalType) and actual.is_user_record
|
|
777
|
+
and isinstance(expected, NominalType) and expected.is_user_record):
|
|
778
|
+
if self.ctx.registry.is_subclass_of(actual, expected):
|
|
779
|
+
if coercion_ctx in (CoercionContext.ASSIGN,
|
|
780
|
+
CoercionContext.INIT,
|
|
781
|
+
CoercionContext.RETURN):
|
|
782
|
+
msg = (f"upcast narrows '{actual}' to '{expected}' -- "
|
|
783
|
+
f"only '{expected}' fields and methods will be accessible; "
|
|
784
|
+
f"keep the concrete type or make '{expected}' a @dynamic protocol")
|
|
785
|
+
if source_expr is not None:
|
|
786
|
+
self.ctx.warning(msg, source_expr)
|
|
787
|
+
else:
|
|
788
|
+
self.ctx.warning_from_loc(msg, loc)
|
|
789
|
+
return None
|
|
790
|
+
|
|
791
|
+
# Inheritance: Ptr[Child] -> Ptr[Parent] / Ptr[readonly[Parent]]
|
|
792
|
+
# Ptr[readonly[Child]] -> Ptr[readonly[Parent]]
|
|
793
|
+
# Covers both class-to-class inheritance and `@dynamic` protocol
|
|
794
|
+
# implementation; C++ upcasts the pointer implicitly at the call
|
|
795
|
+
# site via virtual inheritance.
|
|
796
|
+
if isinstance(actual, PtrType) and isinstance(actual.inner_pointee, NominalType):
|
|
797
|
+
if isinstance(expected, PtrType) and isinstance(expected.inner_pointee, NominalType):
|
|
798
|
+
if _ptr_readonly_compatible(actual, expected):
|
|
799
|
+
if self._is_covariant_target(actual.inner_pointee, expected.inner_pointee):
|
|
800
|
+
return None
|
|
801
|
+
|
|
802
|
+
# Ptr[U] -> Ptr[B] where U is a type param whose subtype bound reaches
|
|
803
|
+
# B (`def f[U: B]`). Borrow-form pointer upcast only; the C++ upcast is
|
|
804
|
+
# only valid when the concrete U is a real subtype of B, which the call
|
|
805
|
+
# site enforces (see methods/calls bound validation).
|
|
806
|
+
if (isinstance(actual, PtrType) and isinstance(expected, PtrType)
|
|
807
|
+
and isinstance(actual.inner_pointee, TypeParamRef)
|
|
808
|
+
and _ptr_readonly_compatible(actual, expected)
|
|
809
|
+
and self._is_representational_subtype(
|
|
810
|
+
actual.inner_pointee, expected.inner_pointee)):
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
# Covariant generic coercion: Box[Child] -> Box[Parent]
|
|
814
|
+
# when Box extends Covariant[T] and Child conforms to @dynamic Parent.
|
|
815
|
+
# C++ converting move ctor handles the actual conversion.
|
|
816
|
+
if self.is_covariant_generic_upcast(actual, expected):
|
|
817
|
+
return None
|
|
818
|
+
|
|
819
|
+
# Union-member coercion through generic containers:
|
|
820
|
+
# Container[Lit] -> Container[Expr] when Expr is a recursive union alias
|
|
821
|
+
# and Lit is a valid member. Works for any generic container.
|
|
822
|
+
# The C++ Expr wrapper struct's template constructor handles implicit
|
|
823
|
+
# conversion from Lit to Expr, so Box<Expr>(Lit{...}) compiles.
|
|
824
|
+
if (isinstance(actual, NominalType) and isinstance(expected, NominalType)
|
|
825
|
+
and actual.name == expected.name
|
|
826
|
+
and actual.type_args and expected.type_args
|
|
827
|
+
and len(actual.type_args) == len(expected.type_args)
|
|
828
|
+
and actual.type_args != expected.type_args):
|
|
829
|
+
if self._check_union_member_type_args(
|
|
830
|
+
actual, expected, context, loc, source_expr, is_return, coercion_ctx
|
|
831
|
+
):
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
# Allow T -> Own[T] coercion (ownership transfer)
|
|
835
|
+
if isinstance(expected, OwnType):
|
|
836
|
+
# Warn when lvalue is implicitly copied into owned storage
|
|
837
|
+
# (returns are handled separately as errors in statements.py)
|
|
838
|
+
# Skip warning when auto-move applies (last use of an owned variable).
|
|
839
|
+
# Only locals and Own[T] params are movable -- regular params are borrowed.
|
|
840
|
+
is_auto_moved = self._is_auto_moved(source_expr)
|
|
841
|
+
self.check_own_consumption(source_expr)
|
|
842
|
+
# Mark loop variables as consumed for auto-consuming heuristic
|
|
843
|
+
if isinstance(source_expr, TpyName):
|
|
844
|
+
self.ctx.mark_loop_var_consumed(source_expr.name)
|
|
845
|
+
if (not is_return and source_expr is not None
|
|
846
|
+
and not expected.wrapped.is_value_type()
|
|
847
|
+
and not self._is_value_type_param(expected.wrapped)
|
|
848
|
+
and self.is_lvalue(source_expr)
|
|
849
|
+
and not self.is_copy_call(source_expr)
|
|
850
|
+
and not is_auto_moved):
|
|
851
|
+
value_type = self.ctx.get_expr_type(source_expr)
|
|
852
|
+
if self.ctx.is_type_non_copyable(expected.wrapped):
|
|
853
|
+
verb = "may copy" if isinstance(expected.wrapped, TypeParamRef) else "cannot copy"
|
|
854
|
+
if value_type == expected.wrapped:
|
|
855
|
+
msg = (f"{verb} non-copyable type '{expected.wrapped}' "
|
|
856
|
+
f"into owned storage{NOCOPY_REMEDIATION_HINT}")
|
|
857
|
+
else:
|
|
858
|
+
msg = (f"{verb} {value_type} into owned storage of type "
|
|
859
|
+
f"'{expected.wrapped}'; target is non-copyable{NOCOPY_REMEDIATION_HINT}")
|
|
860
|
+
raise self.ctx.error(msg, source_expr)
|
|
861
|
+
if isinstance(expected.wrapped, TypeParamRef):
|
|
862
|
+
self.ctx.warning(
|
|
863
|
+
f"may copy {value_type} into owned storage if not a value type; use copy() to make this explicit",
|
|
864
|
+
source_expr
|
|
865
|
+
)
|
|
866
|
+
else:
|
|
867
|
+
self.ctx.warning(
|
|
868
|
+
f"copies {value_type} into owned storage; use copy() to make this explicit",
|
|
869
|
+
source_expr
|
|
870
|
+
)
|
|
871
|
+
# For loop variables at last use, the consuming decision is
|
|
872
|
+
# made post-body. Record the diagnostic index so it can be
|
|
873
|
+
# suppressed if the loop activates consuming iteration
|
|
874
|
+
# (elements at last use will be moved, not copied).
|
|
875
|
+
if (isinstance(source_expr, TpyName)
|
|
876
|
+
and source_expr.name in self.ctx.func.loop_vars
|
|
877
|
+
and id(source_expr) in self.ctx.all_last_uses):
|
|
878
|
+
diag_idx = len(self.ctx.diagnostics) - 1
|
|
879
|
+
self.ctx.func.deferred_loop_copy_warnings.setdefault(
|
|
880
|
+
source_expr.name, []).append(diag_idx)
|
|
881
|
+
# Subclass coercion excluded: Child -> Own[Base] stores Child by value as
|
|
882
|
+
# Base, silently slicing the object. Same invariance as container elements.
|
|
883
|
+
# Only applies when record names differ (different types, not parametric covariance).
|
|
884
|
+
if (isinstance(actual, NominalType) and actual.is_user_record
|
|
885
|
+
and isinstance(expected.wrapped, NominalType) and expected.wrapped.is_user_record
|
|
886
|
+
and actual.name != expected.wrapped.name):
|
|
887
|
+
return CompatError(
|
|
888
|
+
f"Type mismatch in {context}: expected {expected.wrapped}, got {actual}", loc)
|
|
889
|
+
return self._check_compat(actual, expected.wrapped, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
890
|
+
|
|
891
|
+
# Allow Own[T] -> T coercion (receiving an owned value)
|
|
892
|
+
if isinstance(actual, OwnType):
|
|
893
|
+
return self._check_compat(actual.wrapped, expected, context, loc, source_expr, is_return, coercion_ctx, target_is_storage_form)
|
|
894
|
+
|
|
895
|
+
# Tuple-to-tuple: same length, element-wise compatible
|
|
896
|
+
if isinstance(actual, TupleType) and isinstance(expected, TupleType):
|
|
897
|
+
if len(actual.element_types) != len(expected.element_types):
|
|
898
|
+
return CompatError(
|
|
899
|
+
f"Type mismatch in {context}: expected {expected}, got {actual} "
|
|
900
|
+
f"(different tuple lengths)", loc
|
|
901
|
+
)
|
|
902
|
+
for i, (a, e) in enumerate(zip(actual.element_types, expected.element_types)):
|
|
903
|
+
result = self._check_compat(
|
|
904
|
+
a, e, f"{context} (tuple element {i})", loc,
|
|
905
|
+
source_expr=source_expr, is_return=is_return,
|
|
906
|
+
coercion_ctx=coercion_ctx, target_is_storage_form=target_is_storage_form
|
|
907
|
+
)
|
|
908
|
+
if isinstance(result, CompatError):
|
|
909
|
+
return result
|
|
910
|
+
return None
|
|
911
|
+
|
|
912
|
+
# IntLiteral can coerce to BigInt or stay unresolved
|
|
913
|
+
if isinstance(actual, IntLiteralType):
|
|
914
|
+
if is_big_int_type(expected) or isinstance(expected, IntLiteralType):
|
|
915
|
+
return None
|
|
916
|
+
|
|
917
|
+
# FloatLiteral can coerce to float/Float32 or stay unresolved
|
|
918
|
+
if isinstance(actual, FloatLiteralType):
|
|
919
|
+
if is_any_float_type(expected):
|
|
920
|
+
return None
|
|
921
|
+
|
|
922
|
+
# Allow Array element type coercion if sizes match
|
|
923
|
+
if is_array(actual) and is_array(expected):
|
|
924
|
+
if actual.type_args[1] == expected.type_args[1]:
|
|
925
|
+
if isinstance(actual.type_args[0], IntLiteralType) and is_integer_type(expected.type_args[0]):
|
|
926
|
+
return None
|
|
927
|
+
|
|
928
|
+
# Allow list element type coercion
|
|
929
|
+
if is_list(actual) and is_list(expected):
|
|
930
|
+
if isinstance(actual.type_args[0], IntLiteralType) and is_integer_type(expected.type_args[0]):
|
|
931
|
+
return None
|
|
932
|
+
|
|
933
|
+
# Allow list -> Array only for literal expressions
|
|
934
|
+
# (global array literals and list repeats become list[T] but can be assigned to Array variables)
|
|
935
|
+
# List *variables* cannot be coerced to Array - codegen can't handle std::vector -> std::array
|
|
936
|
+
if is_list(actual) and is_array(expected):
|
|
937
|
+
a_elem = actual.type_args[0]
|
|
938
|
+
e_elem, e_size = expected.type_args[0], expected.type_args[1]
|
|
939
|
+
if isinstance(source_expr, (TpyArrayLiteral, TpyListRepeat)):
|
|
940
|
+
# Validate size for list repeats with known count
|
|
941
|
+
if isinstance(source_expr, TpyListRepeat) and isinstance(source_expr.count, TpyIntLiteral):
|
|
942
|
+
repeat_size = len(source_expr.elements) * source_expr.count.value
|
|
943
|
+
if repeat_size != e_size:
|
|
944
|
+
return CompatError(
|
|
945
|
+
f"List repeat produces {repeat_size} elements but Array[..., {e_size}] expects {e_size}",
|
|
946
|
+
loc
|
|
947
|
+
)
|
|
948
|
+
if a_elem == e_elem:
|
|
949
|
+
return None
|
|
950
|
+
if isinstance(a_elem, IntLiteralType) and is_integer_type(e_elem):
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
# Allow PendingListType compatibility during first phase (before resolution)
|
|
954
|
+
if isinstance(actual, PendingListType):
|
|
955
|
+
# Compatible with list[T] if element types are compatible
|
|
956
|
+
if is_list(expected):
|
|
957
|
+
e_elem = expected.type_args[0]
|
|
958
|
+
if actual.element_type == e_elem:
|
|
959
|
+
return None
|
|
960
|
+
if isinstance(actual.element_type, IntLiteralType) and is_integer_type(e_elem):
|
|
961
|
+
return None
|
|
962
|
+
# Element type widening (e.g. Int32 -> Int32|None, Int32 -> Int64).
|
|
963
|
+
# Subclass coercion excluded: storing Child in list[Base] silently
|
|
964
|
+
# slices objects (same invariance as dict/set).
|
|
965
|
+
both_records = (
|
|
966
|
+
isinstance(actual.element_type, NominalType) and actual.element_type.is_user_record
|
|
967
|
+
and isinstance(e_elem, NominalType) and e_elem.is_user_record
|
|
968
|
+
)
|
|
969
|
+
if both_records:
|
|
970
|
+
# Explicit error: avoid leaking PendingList internal repr in the generic message.
|
|
971
|
+
return CompatError(
|
|
972
|
+
f"Type mismatch in {context}: expected {e_elem}, got {actual.element_type}", loc)
|
|
973
|
+
else:
|
|
974
|
+
# Element type widening (e.g. Int32 -> Int32|None, Int32 -> Int64).
|
|
975
|
+
# Container element slot is storage form -- no address-take
|
|
976
|
+
# mark should fire even if the inner type is pointer-repr Optional.
|
|
977
|
+
result = self._check_compat(
|
|
978
|
+
actual.element_type, e_elem,
|
|
979
|
+
context, loc, source_expr, is_return, coercion_ctx,
|
|
980
|
+
target_is_storage_form=True,
|
|
981
|
+
)
|
|
982
|
+
if not isinstance(result, CompatError):
|
|
983
|
+
return None # element coercion is a probe, not propagated
|
|
984
|
+
# Compatible with Array[T, N] if element types and sizes match
|
|
985
|
+
if is_array(expected):
|
|
986
|
+
if self.type_ops and self.type_ops.pending_list_matches_array(actual, expected):
|
|
987
|
+
return None
|
|
988
|
+
# Specific error for list repeat size mismatch
|
|
989
|
+
e_size = expected.type_args[1]
|
|
990
|
+
if (isinstance(source_expr, TpyListRepeat)
|
|
991
|
+
and actual.size >= 0 and actual.size != e_size):
|
|
992
|
+
return CompatError(
|
|
993
|
+
f"List repeat produces {actual.size} elements but "
|
|
994
|
+
f"Array[..., {e_size}] expects {e_size}",
|
|
995
|
+
loc,
|
|
996
|
+
)
|
|
997
|
+
# Inline repeat cannot be passed directly to Span -- assign to a variable first
|
|
998
|
+
if is_span(expected) and isinstance(source_expr, TpyListRepeat):
|
|
999
|
+
return CompatError(
|
|
1000
|
+
f"Cannot pass list repeat directly to {expected}: "
|
|
1001
|
+
f"assign to a variable first",
|
|
1002
|
+
loc,
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
# ListRepeatType materializes to list[T] or Array[T, N]
|
|
1006
|
+
if isinstance(actual, ListRepeatType):
|
|
1007
|
+
if is_list(expected):
|
|
1008
|
+
e_elem = expected.type_args[0]
|
|
1009
|
+
if actual.element_type == e_elem:
|
|
1010
|
+
return None
|
|
1011
|
+
if isinstance(actual.element_type, IntLiteralType) and is_integer_type(e_elem):
|
|
1012
|
+
return None
|
|
1013
|
+
if is_array(expected):
|
|
1014
|
+
if actual.element_type == expected.type_args[0]:
|
|
1015
|
+
return None
|
|
1016
|
+
if isinstance(actual.element_type, IntLiteralType) and is_integer_type(expected.type_args[0]):
|
|
1017
|
+
return None
|
|
1018
|
+
|
|
1019
|
+
# Allow PendingDictType compatibility during first phase (before resolution)
|
|
1020
|
+
if isinstance(actual, PendingDictType) and is_dict(expected):
|
|
1021
|
+
e_k, e_v = expected.type_args[0], expected.type_args[1]
|
|
1022
|
+
key_ok = isinstance(actual.key_type, UnknownElementType) or _container_elem_matches(actual.key_type, e_k)
|
|
1023
|
+
val_ok = isinstance(actual.value_type, UnknownElementType) or _container_elem_matches(actual.value_type, e_v)
|
|
1024
|
+
if key_ok and val_ok:
|
|
1025
|
+
return None
|
|
1026
|
+
|
|
1027
|
+
# Allow PendingSetType compatibility during first phase (before resolution)
|
|
1028
|
+
if isinstance(actual, PendingSetType) and is_set(expected):
|
|
1029
|
+
if isinstance(actual.element_type, UnknownElementType) or _container_elem_matches(actual.element_type, expected.type_args[0]):
|
|
1030
|
+
return None
|
|
1031
|
+
|
|
1032
|
+
# DictType compatibility: key and value types must match exactly.
|
|
1033
|
+
# Own[V] stripping is handled by _container_elem_matches.
|
|
1034
|
+
# Subclass coercion is intentionally excluded: dict values are stored by value,
|
|
1035
|
+
# and tpy::dict_update/tpy::dict_ctor are non-converting templates (invariant V).
|
|
1036
|
+
if is_dict(actual) and is_dict(expected):
|
|
1037
|
+
a_k, a_v = actual.type_args[0], actual.type_args[1]
|
|
1038
|
+
e_k, e_v = expected.type_args[0], expected.type_args[1]
|
|
1039
|
+
key_ok = _container_elem_matches(a_k, e_k)
|
|
1040
|
+
val_ok = _container_elem_matches(a_v, e_v)
|
|
1041
|
+
if key_ok and val_ok:
|
|
1042
|
+
return None
|
|
1043
|
+
# Raise a specific error pointing at the mismatching element type.
|
|
1044
|
+
# Strip Own[V] from the message to avoid leaking implementation details.
|
|
1045
|
+
check_val = e_v.wrapped if isinstance(e_v, OwnType) else e_v
|
|
1046
|
+
if not key_ok:
|
|
1047
|
+
return CompatError(
|
|
1048
|
+
f"Type mismatch in {context}: expected {e_k}, got {a_k}", loc)
|
|
1049
|
+
return CompatError(
|
|
1050
|
+
f"Type mismatch in {context}: expected {check_val}, got {a_v}", loc)
|
|
1051
|
+
|
|
1052
|
+
# SetType compatibility: element types must match exactly.
|
|
1053
|
+
# Subclass coercion is intentionally excluded: tpy::ordered_set<T> is a
|
|
1054
|
+
# non-converting template (invariant T).
|
|
1055
|
+
if is_set(actual) and is_set(expected):
|
|
1056
|
+
a_elem, e_elem = actual.type_args[0], expected.type_args[0]
|
|
1057
|
+
if _container_elem_matches(a_elem, e_elem):
|
|
1058
|
+
return None
|
|
1059
|
+
check_elem = e_elem.wrapped if isinstance(e_elem, OwnType) else e_elem
|
|
1060
|
+
return CompatError(
|
|
1061
|
+
f"Type mismatch in {context}: expected {check_elem}, got {a_elem}", loc)
|
|
1062
|
+
|
|
1063
|
+
# Allow PendingStrType compatibility during first phase (before resolution)
|
|
1064
|
+
if isinstance(actual, PendingStrType):
|
|
1065
|
+
if is_any_str_type(expected):
|
|
1066
|
+
return None
|
|
1067
|
+
if isinstance(expected, LiteralType) and expected.is_str_base():
|
|
1068
|
+
return None
|
|
1069
|
+
if isinstance(expected, PendingStrType):
|
|
1070
|
+
if is_str_category(actual):
|
|
1071
|
+
return None
|
|
1072
|
+
# LiteralType is compatible with its base type family
|
|
1073
|
+
if isinstance(actual, LiteralType) and isinstance(expected, LiteralType):
|
|
1074
|
+
if all(v in expected.values for v in actual.values):
|
|
1075
|
+
return None
|
|
1076
|
+
# Reject explicitly: skipping the permissive str/int branches below
|
|
1077
|
+
# is what enforces value-set membership.
|
|
1078
|
+
return CompatError(
|
|
1079
|
+
f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
|
|
1080
|
+
if isinstance(expected, LiteralType):
|
|
1081
|
+
src_lit = _literal_value_from_source(source_expr, expected)
|
|
1082
|
+
if src_lit is not None:
|
|
1083
|
+
if src_lit in expected.values:
|
|
1084
|
+
return None
|
|
1085
|
+
return CompatError(
|
|
1086
|
+
f"Type mismatch in {context}: expected {expected}, "
|
|
1087
|
+
f"got {src_lit}", loc)
|
|
1088
|
+
if isinstance(actual, IntLiteralType) and actual.value is not None and expected.is_int_base():
|
|
1089
|
+
if expected.contains(LiteralTag.INT, actual.value):
|
|
1090
|
+
return None
|
|
1091
|
+
return CompatError(
|
|
1092
|
+
f"Type mismatch in {context}: expected {expected}, "
|
|
1093
|
+
f"got {actual.value}", loc)
|
|
1094
|
+
# Non-literal source flowing into a Literal[...] LHS is rejected:
|
|
1095
|
+
# sema cannot prove the runtime value is in the declared set, and
|
|
1096
|
+
# the LHS's narrower type drives downstream Literal-specialized
|
|
1097
|
+
# dispatch (which would run the wrong arm on an out-of-set value).
|
|
1098
|
+
if isinstance(actual, LiteralType):
|
|
1099
|
+
if actual.is_str_base() and is_str_category(expected):
|
|
1100
|
+
return None
|
|
1101
|
+
if actual.base_type == expected:
|
|
1102
|
+
return None
|
|
1103
|
+
if actual.is_int_base() and is_integer_type(expected):
|
|
1104
|
+
return None
|
|
1105
|
+
# Allow PendingBytesType compatibility during first phase (before resolution)
|
|
1106
|
+
if isinstance(actual, PendingBytesType):
|
|
1107
|
+
if is_bytes_category(expected) or isinstance(expected, PendingBytesType):
|
|
1108
|
+
return None
|
|
1109
|
+
if isinstance(expected, PendingBytesType):
|
|
1110
|
+
if is_bytes_category(actual):
|
|
1111
|
+
return None
|
|
1112
|
+
|
|
1113
|
+
ctx = coercion_ctx or context
|
|
1114
|
+
coercion = resolve_coercion(actual, expected, ctx)
|
|
1115
|
+
if coercion is None:
|
|
1116
|
+
# Inheritance: Child -> Ptr[Parent] / Ptr[readonly[Parent]] (address-of with upcast).
|
|
1117
|
+
# `_is_covariant_target` covers both class inheritance (Child -> Ptr[ParentRecord])
|
|
1118
|
+
# and @dynamic-protocol implementation (Child -> Ptr[@dynamic Protocol]).
|
|
1119
|
+
#
|
|
1120
|
+
# Also accepts `actual` being a @dynamic protocol value (lowered as
|
|
1121
|
+
# `Base&` already): `&handle` produces `Base*` natively, matching
|
|
1122
|
+
# `Ptr[same protocol]`. Identity is the common case (`P -> Ptr[P]`
|
|
1123
|
+
# inside a function that took the protocol value as a param);
|
|
1124
|
+
# protocol-to-parent-protocol upcasts also work via the existing
|
|
1125
|
+
# covariance check.
|
|
1126
|
+
actual_is_addr_taker = (
|
|
1127
|
+
isinstance(actual, NominalType)
|
|
1128
|
+
and (actual.is_user_record or is_dyn_protocol(actual))
|
|
1129
|
+
)
|
|
1130
|
+
if actual_is_addr_taker:
|
|
1131
|
+
# Identity shortcut (actual == inner_pointee) only fires for
|
|
1132
|
+
# @dynamic protocol values: their `Base&`-style lowering needs
|
|
1133
|
+
# the value->Ptr path without going through resolve_coercion
|
|
1134
|
+
# (no `protocol_to_ptr` rule exists). User-record identity is
|
|
1135
|
+
# handled by the named `record_to_ptr` rule in resolve_coercion;
|
|
1136
|
+
# gating the shortcut keeps the coercion AST name stable so
|
|
1137
|
+
# downstream code can rely on it.
|
|
1138
|
+
actual_is_dyn = is_dyn_protocol(actual)
|
|
1139
|
+
if isinstance(expected, PtrType) and not expected.is_readonly and isinstance(expected.inner_pointee, NominalType):
|
|
1140
|
+
if ((actual_is_dyn and actual == expected.inner_pointee)
|
|
1141
|
+
or self._is_covariant_target(actual, expected.inner_pointee)):
|
|
1142
|
+
coercion = UPCAST_TO_PTR
|
|
1143
|
+
elif is_readonly_ptr(expected) and isinstance(expected.inner_pointee, NominalType):
|
|
1144
|
+
if ((actual_is_dyn and actual == expected.inner_pointee)
|
|
1145
|
+
or self._is_covariant_target(actual, expected.inner_pointee)):
|
|
1146
|
+
coercion = UPCAST_TO_CONST_PTR
|
|
1147
|
+
if coercion is None:
|
|
1148
|
+
# __span__() method coercion: type with __span__() -> Span[T] coerces to Span/Span[readonly[T]]
|
|
1149
|
+
if isinstance(actual, NominalType) and is_span(expected):
|
|
1150
|
+
span_ret = get_span_return_type(actual, registry=self.ctx.registry)
|
|
1151
|
+
if span_ret is not None and unwrap_readonly(span_ret.type_args[0]) == unwrap_readonly(expected.type_args[0]):
|
|
1152
|
+
# Span[readonly[T]] cannot coerce to mutable Span
|
|
1153
|
+
if not is_readonly_span(span_ret) or is_readonly_span(expected):
|
|
1154
|
+
if ctx == CoercionContext.ARG:
|
|
1155
|
+
coercion = SPAN_METHOD_TO_SPAN_ARG
|
|
1156
|
+
else:
|
|
1157
|
+
coercion = SPAN_METHOD_TO_SPAN
|
|
1158
|
+
if coercion is None:
|
|
1159
|
+
# Spannable[T] protocol -> Span[readonly[T]] coercion via __span__()
|
|
1160
|
+
if (is_protocol_type(actual) and actual.qualified_name() == "tpy.Spannable"
|
|
1161
|
+
and actual.type_args and is_span(expected)
|
|
1162
|
+
and is_readonly_span(expected) and actual.type_args[0] == unwrap_readonly(expected.type_args[0])):
|
|
1163
|
+
if ctx == CoercionContext.ARG:
|
|
1164
|
+
coercion = SPAN_METHOD_TO_SPAN_ARG
|
|
1165
|
+
else:
|
|
1166
|
+
coercion = SPAN_METHOD_TO_SPAN
|
|
1167
|
+
if coercion is None:
|
|
1168
|
+
# Generic deref coercion: any type with __deref__() -> T coerces to T
|
|
1169
|
+
if self.type_ops:
|
|
1170
|
+
deref_target = self.type_ops.get_deref_coercion_target(actual)
|
|
1171
|
+
if deref_target is None and is_readonly_ptr(actual):
|
|
1172
|
+
# Ptr[readonly[T]] -> T via deref is safe for returns and
|
|
1173
|
+
# assignments (the value is copied); reject only in ARG
|
|
1174
|
+
# context where the callee may need a mutable reference.
|
|
1175
|
+
if ctx != CoercionContext.ARG:
|
|
1176
|
+
deref_target = self.type_ops.get_deref_target_type(actual)
|
|
1177
|
+
if deref_target is not None and unwrap_readonly(deref_target) == expected:
|
|
1178
|
+
coercion = DEREF_COERCION
|
|
1179
|
+
if coercion is None:
|
|
1180
|
+
return CompatError(f"Type mismatch in {context}: expected {expected}, got {actual}", loc)
|
|
1181
|
+
|
|
1182
|
+
if isinstance(actual, PendingListType) and is_span(expected):
|
|
1183
|
+
info = self.ctx.list_literals.get(actual.literal_id)
|
|
1184
|
+
if info:
|
|
1185
|
+
info.coerced_element_type = expected.type_args[0]
|
|
1186
|
+
info.passed_to_span_param = True
|
|
1187
|
+
|
|
1188
|
+
if coercion.check_range and not coercion.check_range(actual, expected):
|
|
1189
|
+
tr = int_traits_of(expected)
|
|
1190
|
+
return CompatError(
|
|
1191
|
+
f"Integer literal {actual.value} is outside {expected} range "
|
|
1192
|
+
f"[{tr.min_value}, {tr.max_value}] in {context}",
|
|
1193
|
+
loc
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
if coercion.requires_mutable_lvalue:
|
|
1197
|
+
if source_expr is None or not self.is_mutable_lvalue(source_expr):
|
|
1198
|
+
return CompatError(
|
|
1199
|
+
f"Cannot take mutable pointer to read-only or temporary value in {context}; "
|
|
1200
|
+
f"use a read-only pointer for read-only access, or assign to a variable first",
|
|
1201
|
+
loc
|
|
1202
|
+
)
|
|
1203
|
+
# Address-taking coercion (record -> Ptr[Record]) requires T& binding.
|
|
1204
|
+
# Track so that codegen can't safely emit const T& for this param.
|
|
1205
|
+
if isinstance(source_expr, TpyName):
|
|
1206
|
+
root = self.ctx.func.borrow_tracker.effective_storage(source_expr.name)
|
|
1207
|
+
self.ctx.mark_param_mutated(root)
|
|
1208
|
+
elif coercion.requires_lvalue:
|
|
1209
|
+
if source_expr is None or not self.is_lvalue(source_expr):
|
|
1210
|
+
return CompatError(
|
|
1211
|
+
f"Cannot take address of a temporary or expression in {context}; "
|
|
1212
|
+
f"assign to a variable first",
|
|
1213
|
+
loc
|
|
1214
|
+
)
|
|
1215
|
+
# Mutable Span from a lvalue container (e.g. Array -> Span[T])
|
|
1216
|
+
# requires non-const source; codegen calls as_mut_span().
|
|
1217
|
+
# Mark source param as T&. Unlike the pointer-repr Optional
|
|
1218
|
+
# storage-form gate above, this mark applies even for
|
|
1219
|
+
# storage-form Span destinations -- the assignment still
|
|
1220
|
+
# emits as_mut_span(source), which can't bind to a const
|
|
1221
|
+
# source.
|
|
1222
|
+
if (is_span(expected) and not is_readonly_span(expected)
|
|
1223
|
+
and source_expr is not None):
|
|
1224
|
+
self._mark_addr_taken(source_expr)
|
|
1225
|
+
|
|
1226
|
+
if coercion.forbid_return_local and is_return:
|
|
1227
|
+
if source_expr is not None and self.is_dangling_return(source_expr):
|
|
1228
|
+
return CompatError(
|
|
1229
|
+
f"Cannot return local or temporary value; "
|
|
1230
|
+
f"the returned pointer/reference would dangle",
|
|
1231
|
+
loc
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
return coercion
|
|
1235
|
+
|
|
1236
|
+
def coerce_expr(
|
|
1237
|
+
self, expr: TpyExpr, actual: TpyType, expected: TpyType, context: str,
|
|
1238
|
+
coercion_ctx: CoercionContext, is_return: bool = False,
|
|
1239
|
+
target_is_storage_form: bool = False,
|
|
1240
|
+
) -> TpyExpr:
|
|
1241
|
+
"""Wrap expr in a coercion node if a conversion is needed.
|
|
1242
|
+
|
|
1243
|
+
target_is_storage_form: see _check_compat docstring. Set by callers
|
|
1244
|
+
that know the destination is a field / container element so the
|
|
1245
|
+
pointer-repr-Optional address-take mark is suppressed.
|
|
1246
|
+
"""
|
|
1247
|
+
coercion = self.check_type_compatible(
|
|
1248
|
+
actual, expected, context,
|
|
1249
|
+
getattr(expr, "loc", None),
|
|
1250
|
+
source_expr=expr,
|
|
1251
|
+
is_return=is_return,
|
|
1252
|
+
coercion_ctx=coercion_ctx,
|
|
1253
|
+
target_is_storage_form=target_is_storage_form,
|
|
1254
|
+
)
|
|
1255
|
+
if coercion is None:
|
|
1256
|
+
return expr
|
|
1257
|
+
# Value-to-mutable-Ptr coercions (`record_to_ptr`, `upcast_to_ptr`)
|
|
1258
|
+
# emit `&expr` and require the source storage to stay mutable -- same
|
|
1259
|
+
# propagation the explicit `take_ptr(x)` path gets via the
|
|
1260
|
+
# `value_ptr_coercion` handler in calls.py and the vararg pack handler.
|
|
1261
|
+
if coercion.name in ("record_to_ptr", "upcast_to_ptr"):
|
|
1262
|
+
self._mark_addr_taken(expr)
|
|
1263
|
+
runtime_bigint = False
|
|
1264
|
+
if coercion.name == "int_literal_to_fixed_int":
|
|
1265
|
+
runtime_bigint = self.is_runtime_bigint_expr(expr)
|
|
1266
|
+
# INTO_ANY needs a concrete actual_type for codegen (the storage
|
|
1267
|
+
# typeid). PendingListType / PendingDictType / PendingSetType
|
|
1268
|
+
# would normally resolve later via coerced_element_type, but that
|
|
1269
|
+
# signal is set by typed targets -- Any provides no element
|
|
1270
|
+
# constraint. Default-resolve here so the cell's typeid is stable.
|
|
1271
|
+
if coercion.name == "into_any":
|
|
1272
|
+
actual = self._resolve_pending_for_any_storage(actual)
|
|
1273
|
+
coerced = TpyCoerce(
|
|
1274
|
+
expr=expr,
|
|
1275
|
+
actual_type=actual,
|
|
1276
|
+
expected_type=expected,
|
|
1277
|
+
coercion=coercion,
|
|
1278
|
+
context_kind=coercion_ctx,
|
|
1279
|
+
context_msg=context,
|
|
1280
|
+
runtime_bigint=runtime_bigint,
|
|
1281
|
+
loc=expr.loc
|
|
1282
|
+
)
|
|
1283
|
+
self.ctx.set_expr_type(coerced, expected)
|
|
1284
|
+
return coerced
|
|
1285
|
+
|
|
1286
|
+
def _maybe_raise_literal_local_range(
|
|
1287
|
+
self, expr: TpyName, actual: TpyType, expected: TpyType, context: str,
|
|
1288
|
+
) -> None:
|
|
1289
|
+
"""Raise a range error when a literal-seeded local would have
|
|
1290
|
+
retro-widened to `expected` except a recorded literal value falls
|
|
1291
|
+
outside the target's range. Surfaces the literal value (e.g. -1,
|
|
1292
|
+
or 300 against UInt8) instead of the bare "got Int32" mismatch.
|
|
1293
|
+
"""
|
|
1294
|
+
cand = self.deduction.literal_retro_candidate(expr.name, actual, expected)
|
|
1295
|
+
if cand is None:
|
|
1296
|
+
return
|
|
1297
|
+
target, values = cand
|
|
1298
|
+
bad = [v for v in values if not fixed_int_range_contains(target, v)]
|
|
1299
|
+
if not bad:
|
|
1300
|
+
return
|
|
1301
|
+
tr = int_traits_of(target)
|
|
1302
|
+
raise self.ctx.error(
|
|
1303
|
+
f"Integer literal {bad[0]} assigned to '{expr.name}' is outside "
|
|
1304
|
+
f"{target} range [{tr.min_value}, {tr.max_value}] in {context}",
|
|
1305
|
+
expr,
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
def _resolve_pending_for_any_storage(self, actual: TpyType) -> TpyType:
|
|
1309
|
+
"""Convert Pending{List,Dict,Set}Type to its concrete container
|
|
1310
|
+
form using the literal-info's inferred element types, defaulting
|
|
1311
|
+
IntLiteralType / FloatLiteralType to the configured defaults.
|
|
1312
|
+
"""
|
|
1313
|
+
if isinstance(actual, PendingListType):
|
|
1314
|
+
elem = self._default_resolve_element(actual.element_type)
|
|
1315
|
+
info = self.ctx.list_literals.get(actual.literal_id)
|
|
1316
|
+
if info is not None and info.coerced_element_type is None:
|
|
1317
|
+
info.coerced_element_type = elem
|
|
1318
|
+
return make_list(elem)
|
|
1319
|
+
if isinstance(actual, PendingDictType):
|
|
1320
|
+
k = self._default_resolve_element(actual.key_type)
|
|
1321
|
+
v = self._default_resolve_element(actual.value_type)
|
|
1322
|
+
return make_dict(k, v)
|
|
1323
|
+
if isinstance(actual, PendingSetType):
|
|
1324
|
+
elem = self._default_resolve_element(actual.element_type)
|
|
1325
|
+
return make_set(elem)
|
|
1326
|
+
return actual
|
|
1327
|
+
|
|
1328
|
+
def _default_resolve_element(self, t: TpyType) -> TpyType:
|
|
1329
|
+
if isinstance(t, IntLiteralType):
|
|
1330
|
+
return self.ctx.default_int_type
|
|
1331
|
+
if isinstance(t, FloatLiteralType):
|
|
1332
|
+
return FLOAT
|
|
1333
|
+
return t
|
|
1334
|
+
|
|
1335
|
+
def is_runtime_bigint_expr(self, expr: TpyExpr) -> bool:
|
|
1336
|
+
"""Check if an IntLiteralType expression could be BigInt at runtime."""
|
|
1337
|
+
if isinstance(expr, TpyCoerce):
|
|
1338
|
+
return self.is_runtime_bigint_expr(expr.expr)
|
|
1339
|
+
if isinstance(expr, TpyName):
|
|
1340
|
+
return True
|
|
1341
|
+
if isinstance(expr, TpyIntLiteral):
|
|
1342
|
+
return False
|
|
1343
|
+
if isinstance(expr, TpyBinOp):
|
|
1344
|
+
return self.is_runtime_bigint_expr(expr.left) or self.is_runtime_bigint_expr(expr.right)
|
|
1345
|
+
if isinstance(expr, TpyUnaryOp):
|
|
1346
|
+
return self.is_runtime_bigint_expr(expr.operand)
|
|
1347
|
+
if isinstance(expr, (TpyCall, TpyMethodCall)):
|
|
1348
|
+
return True
|
|
1349
|
+
return True
|
|
1350
|
+
|
|
1351
|
+
def is_lvalue(self, expr: TpyExpr) -> bool:
|
|
1352
|
+
"""Check if an expression is an lvalue (can have its address taken)."""
|
|
1353
|
+
if isinstance(expr, TpyCoerce):
|
|
1354
|
+
return self.is_lvalue(expr.expr)
|
|
1355
|
+
# Named variables are lvalues
|
|
1356
|
+
if isinstance(expr, TpyName):
|
|
1357
|
+
return True
|
|
1358
|
+
# Field access on an lvalue is also an lvalue (e.g., obj.field)
|
|
1359
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1360
|
+
return self.is_lvalue(expr.obj)
|
|
1361
|
+
# Subscript on an lvalue is also an lvalue (e.g., arr[i])
|
|
1362
|
+
if isinstance(expr, TpySubscript):
|
|
1363
|
+
return self.is_lvalue(expr.obj)
|
|
1364
|
+
# Everything else (calls, literals, operators) are rvalues
|
|
1365
|
+
return False
|
|
1366
|
+
|
|
1367
|
+
def is_const_ref_source(self, expr: TpyExpr) -> bool:
|
|
1368
|
+
"""Check if expression provides a const reference (can't be captured as T&).
|
|
1369
|
+
|
|
1370
|
+
Returns True for sources that are inherently const in C++:
|
|
1371
|
+
- Field access / subscript through a readonly-typed object
|
|
1372
|
+
"""
|
|
1373
|
+
if isinstance(expr, TpyCoerce):
|
|
1374
|
+
return self.is_const_ref_source(expr.expr)
|
|
1375
|
+
if isinstance(expr, TpySubscript):
|
|
1376
|
+
obj_type = self.ctx.get_expr_type(expr.obj)
|
|
1377
|
+
if obj_type is not None:
|
|
1378
|
+
unwrapped = unwrap_readonly(obj_type)
|
|
1379
|
+
if is_span(unwrapped) and is_readonly_span(unwrapped):
|
|
1380
|
+
return True
|
|
1381
|
+
return self.is_const_ref_source(expr.obj)
|
|
1382
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1383
|
+
obj_type = self.ctx.get_expr_type(expr.obj)
|
|
1384
|
+
if obj_type is not None and isinstance(obj_type, ReadonlyType):
|
|
1385
|
+
return True
|
|
1386
|
+
return self.is_const_ref_source(expr.obj)
|
|
1387
|
+
if isinstance(expr, TpyName):
|
|
1388
|
+
expr_type = self.ctx.get_expr_type(expr)
|
|
1389
|
+
if expr_type is not None and isinstance(expr_type, ReadonlyType):
|
|
1390
|
+
return True
|
|
1391
|
+
return False
|
|
1392
|
+
|
|
1393
|
+
def _is_auto_moved(self, source_expr: 'TpyExpr | None') -> bool:
|
|
1394
|
+
"""Last-use of an owned local: auto-move makes the copy invisible."""
|
|
1395
|
+
return (isinstance(source_expr, TpyName)
|
|
1396
|
+
and id(source_expr) in self.ctx.all_last_uses
|
|
1397
|
+
and self._is_owned_var(source_expr.name))
|
|
1398
|
+
|
|
1399
|
+
def _warns_copy_into_any(self, inner: TpyType) -> bool:
|
|
1400
|
+
"""Reference types whose into-Any storage silently copies.
|
|
1401
|
+
|
|
1402
|
+
Caller must pre-unwrap Ref/Readonly/Own. Returns True for non-value
|
|
1403
|
+
records, list, dict, set; False for primitives, value-type records,
|
|
1404
|
+
str (collapses to std::string), bytes/BytesView, Ptr[T].
|
|
1405
|
+
"""
|
|
1406
|
+
if isinstance(inner, NominalType):
|
|
1407
|
+
return not inner.is_value_type() and not inner.is_protocol
|
|
1408
|
+
return is_list(inner) or is_dict(inner) or is_set(inner)
|
|
1409
|
+
|
|
1410
|
+
def is_copy_call(self, expr: TpyExpr) -> bool:
|
|
1411
|
+
"""Check if expression is a copy() or copy_iter() call from the tpy module."""
|
|
1412
|
+
if isinstance(expr, TpyCoerce):
|
|
1413
|
+
return self.is_copy_call(expr.expr)
|
|
1414
|
+
if not isinstance(expr, TpyCall):
|
|
1415
|
+
return False
|
|
1416
|
+
# Check if this function name maps to tpy.copy or tpy.copy_iter
|
|
1417
|
+
# (handles aliases like "from tpy import copy as c")
|
|
1418
|
+
if not isinstance(expr.func, TpyName):
|
|
1419
|
+
return False
|
|
1420
|
+
if expr.func_name in self.ctx.imported_names:
|
|
1421
|
+
module_name, func_name = self.ctx.imported_names[expr.func_name]
|
|
1422
|
+
return module_name == "tpy" and func_name in ("copy", "copy_iter")
|
|
1423
|
+
return False
|
|
1424
|
+
|
|
1425
|
+
def check_own_consumption(self, expr: TpyExpr) -> None:
|
|
1426
|
+
"""Mark Own[T] param as consumed if expr transfers ownership.
|
|
1427
|
+
|
|
1428
|
+
Handles two patterns:
|
|
1429
|
+
- Auto-move: bare name at last use of a movable variable
|
|
1430
|
+
- copy(): explicit copy transfers ownership of the param's value
|
|
1431
|
+
"""
|
|
1432
|
+
if isinstance(expr, TpyName):
|
|
1433
|
+
if (id(expr) in self.ctx.all_last_uses
|
|
1434
|
+
and self._is_owned_var(expr.name)):
|
|
1435
|
+
self.ctx.mark_own_param_consumed(expr.name)
|
|
1436
|
+
return
|
|
1437
|
+
if self.is_copy_call(expr) and isinstance(expr, TpyCall) and expr.args:
|
|
1438
|
+
inner = expr.args[0]
|
|
1439
|
+
if isinstance(inner, TpyName):
|
|
1440
|
+
self.ctx.mark_own_param_consumed(inner.name)
|
|
1441
|
+
|
|
1442
|
+
def check_own_lvalue_into_own(
|
|
1443
|
+
self, own_type: OwnType, expr: TpyExpr, context: str,
|
|
1444
|
+
*, action: str = "return",
|
|
1445
|
+
) -> None:
|
|
1446
|
+
"""Check that an lvalue feeding an Own[T] slot is movable, an explicit
|
|
1447
|
+
copy(), or otherwise safe to consume.
|
|
1448
|
+
|
|
1449
|
+
Used by:
|
|
1450
|
+
- return-statement Own[T] / per-element Own[tuple[T,...]] checks
|
|
1451
|
+
(action="return"); error message says "Cannot return borrowed
|
|
1452
|
+
value as ...".
|
|
1453
|
+
- call-arg Own[tuple[T,...]] per-element check (action="pass");
|
|
1454
|
+
error message says "Cannot pass borrowed value as ...".
|
|
1455
|
+
|
|
1456
|
+
Args:
|
|
1457
|
+
own_type: The Own[T] slot type.
|
|
1458
|
+
expr: The source expression occupying the slot.
|
|
1459
|
+
context: Slot description for error messages, e.g. "return type",
|
|
1460
|
+
"tuple element 1", "argument 'pname' element 0".
|
|
1461
|
+
action: "return" or "pass" -- selects the verb in error messages.
|
|
1462
|
+
"""
|
|
1463
|
+
if self.is_copy_call(expr):
|
|
1464
|
+
self.check_own_consumption(expr)
|
|
1465
|
+
return
|
|
1466
|
+
if not self.is_lvalue(expr):
|
|
1467
|
+
return
|
|
1468
|
+
# Value types are always safe -- copied, not aliased. OwnType.is_value_type
|
|
1469
|
+
# returns True (Own represents a moved value), so we have to peel Own
|
|
1470
|
+
# first to see whether the underlying T is genuinely a value type.
|
|
1471
|
+
inner = expr
|
|
1472
|
+
while isinstance(inner, TpyCoerce):
|
|
1473
|
+
inner = inner.expr
|
|
1474
|
+
raw_type = self.ctx.get_raw_expr_type(inner)
|
|
1475
|
+
if raw_type is not None:
|
|
1476
|
+
unwrapped = unwrap_ref_type(raw_type)
|
|
1477
|
+
inner_after_own = unwrapped.wrapped if isinstance(unwrapped, OwnType) else unwrapped
|
|
1478
|
+
if inner_after_own.is_value_type():
|
|
1479
|
+
return
|
|
1480
|
+
# action="return": Own-typed lvalues at non-last-use are tolerated
|
|
1481
|
+
# (codegen falls back to copy, paired with the coercion-path warning).
|
|
1482
|
+
# action="pass": enforce last-use even for Own locals -- the
|
|
1483
|
+
# per-element check exists precisely because the user opted in to
|
|
1484
|
+
# ownership semantics by writing Own[tuple[...]] and the silent copy
|
|
1485
|
+
# is the bug being fixed.
|
|
1486
|
+
if action == "return" and isinstance(unwrapped, OwnType):
|
|
1487
|
+
return
|
|
1488
|
+
# Consuming method: self.field is owned and movable out of the struct.
|
|
1489
|
+
if (self.ctx.in_consuming_method
|
|
1490
|
+
and isinstance(expr, TpyFieldAccess)
|
|
1491
|
+
and isinstance(expr.obj, TpyName) and expr.obj.name == "self"):
|
|
1492
|
+
return
|
|
1493
|
+
is_auto_moved = (isinstance(expr, TpyName)
|
|
1494
|
+
and id(expr) in self.ctx.all_last_uses
|
|
1495
|
+
and self._is_owned_var(expr.name))
|
|
1496
|
+
if is_auto_moved:
|
|
1497
|
+
self.check_own_consumption(expr)
|
|
1498
|
+
return
|
|
1499
|
+
expr_type = self.ctx.get_expr_type(expr)
|
|
1500
|
+
is_nocopy = expr_type is not None and self.ctx.is_type_nocopy(expr_type)
|
|
1501
|
+
if is_nocopy:
|
|
1502
|
+
reason = self.ctx.nocopy_reason(expr_type)
|
|
1503
|
+
is_movable = (isinstance(expr, TpyName)
|
|
1504
|
+
and self._is_owned_var(expr.name))
|
|
1505
|
+
if is_movable:
|
|
1506
|
+
raise self.ctx.error(
|
|
1507
|
+
f"{reason} is used after this point "
|
|
1508
|
+
f"and cannot be moved into {context} Own[{own_type.wrapped}]. "
|
|
1509
|
+
f"Remove later uses or restructure the code.",
|
|
1510
|
+
expr
|
|
1511
|
+
)
|
|
1512
|
+
verb = "returned" if action == "return" else "passed"
|
|
1513
|
+
raise self.ctx.error(
|
|
1514
|
+
f"{reason} cannot be {verb} as "
|
|
1515
|
+
f"{context} Own[{own_type.wrapped}]. "
|
|
1516
|
+
f"Only the original owner can be moved at its last use.",
|
|
1517
|
+
expr
|
|
1518
|
+
)
|
|
1519
|
+
verb = "return" if action == "return" else "pass"
|
|
1520
|
+
raise self.ctx.error(
|
|
1521
|
+
f"Cannot {verb} borrowed value as {context} Own[{own_type.wrapped}] "
|
|
1522
|
+
f"without explicit copy(). The source is borrowed (parameter, "
|
|
1523
|
+
f"attribute, or non-last-use variable); use 'copy(...)' to make "
|
|
1524
|
+
f"an owned copy.",
|
|
1525
|
+
expr
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
def _is_value_type_param(self, typ: TpyType) -> bool:
|
|
1529
|
+
"""Check if a TypeParamRef has a ValueType bound in the current context."""
|
|
1530
|
+
if not isinstance(typ, TypeParamRef):
|
|
1531
|
+
return False
|
|
1532
|
+
bound = self.type_ops.get_type_param_bound(typ.name)
|
|
1533
|
+
return bound is not None and isinstance(bound, NominalType) and bound.qualified_name() == "tpy.ValueType"
|
|
1534
|
+
|
|
1535
|
+
def _check_union_member_type_args(
|
|
1536
|
+
self, actual: NominalType, expected: NominalType,
|
|
1537
|
+
context: str, loc: object,
|
|
1538
|
+
source_expr: 'TpyExpr | None',
|
|
1539
|
+
is_return: bool, coercion_ctx: CoercionContext,
|
|
1540
|
+
) -> bool:
|
|
1541
|
+
"""Check Container[Lit] -> Container[Expr] union-member coercion.
|
|
1542
|
+
|
|
1543
|
+
Returns True if all differing type args have expected = recursive union
|
|
1544
|
+
and actual is a valid member of that union.
|
|
1545
|
+
"""
|
|
1546
|
+
for a_arg, e_arg in zip(actual.type_args, expected.type_args):
|
|
1547
|
+
if a_arg == e_arg:
|
|
1548
|
+
continue
|
|
1549
|
+
if not isinstance(a_arg, TpyType) or not isinstance(e_arg, TpyType):
|
|
1550
|
+
return False
|
|
1551
|
+
# Resolve recursive-alias placeholders (AliasRef from parser, or
|
|
1552
|
+
# forward NominalType refs from cross-module aliases) into their
|
|
1553
|
+
# underlying UnionType so the union-coercion path below applies.
|
|
1554
|
+
e_resolved = (self._resolve_recursive_refs(e_arg)
|
|
1555
|
+
if isinstance(e_arg, (NominalType, AliasRef)) else e_arg)
|
|
1556
|
+
if isinstance(e_resolved, UnionType) and e_resolved.needs_wrapper():
|
|
1557
|
+
# Container type-arg slot is storage form (the container stores
|
|
1558
|
+
# values, no address-take fires for pointer-repr Optional).
|
|
1559
|
+
result = self._check_compat(
|
|
1560
|
+
a_arg, e_resolved, context, loc, None, is_return, coercion_ctx,
|
|
1561
|
+
target_is_storage_form=True,
|
|
1562
|
+
)
|
|
1563
|
+
if isinstance(result, CompatError):
|
|
1564
|
+
return False
|
|
1565
|
+
else:
|
|
1566
|
+
return False
|
|
1567
|
+
# Rewrite expression type so codegen emits correct template args
|
|
1568
|
+
# (e.g. Box<Expr> not Box<Lit>)
|
|
1569
|
+
if source_expr is not None:
|
|
1570
|
+
self.ctx.set_expr_type(source_expr, expected)
|
|
1571
|
+
# Also rewrite call_type on constructor calls (codegen uses this
|
|
1572
|
+
# for template args: Box<Expr> vs Box<Lit>)
|
|
1573
|
+
if isinstance(source_expr, TpyCall) and source_expr.call_type is not None:
|
|
1574
|
+
source_expr.call_type = expected
|
|
1575
|
+
return True
|
|
1576
|
+
|
|
1577
|
+
def is_covariant_generic_upcast(self, actual: TpyType, expected: TpyType) -> bool:
|
|
1578
|
+
"""True if `actual -> expected` is a covariant-generic wrapper upcast
|
|
1579
|
+
(e.g. `Box[Child] -> Box[Parent]`): same user-record generic, differing
|
|
1580
|
+
type args, every differing position covariant and a valid C++ upcast
|
|
1581
|
+
target. This is a representation-preserving converting move, NOT
|
|
1582
|
+
slicing. Shared by `check_type_compatible` and the container-literal
|
|
1583
|
+
element guards so the covariant-vs-slice line is drawn in one place."""
|
|
1584
|
+
if not (isinstance(actual, NominalType) and actual.is_user_record
|
|
1585
|
+
and isinstance(expected, NominalType) and expected.is_user_record
|
|
1586
|
+
and actual.name == expected.name
|
|
1587
|
+
and actual.type_args and expected.type_args
|
|
1588
|
+
and actual.type_args != expected.type_args):
|
|
1589
|
+
return False
|
|
1590
|
+
record_info = self.ctx.registry.get_record(actual.name)
|
|
1591
|
+
if record_info is None or not record_info.type_params:
|
|
1592
|
+
return False
|
|
1593
|
+
covariant = get_covariant_params(record_info)
|
|
1594
|
+
return bool(covariant and self._check_covariant_args(
|
|
1595
|
+
record_info, covariant, actual, expected))
|
|
1596
|
+
|
|
1597
|
+
def _check_covariant_args(
|
|
1598
|
+
self, record_info: 'RecordInfo', covariant: set[str],
|
|
1599
|
+
actual: NominalType, expected: NominalType
|
|
1600
|
+
) -> bool:
|
|
1601
|
+
"""Check if all type args are compatible under covariance rules."""
|
|
1602
|
+
for i, param_name in enumerate(record_info.type_params):
|
|
1603
|
+
if i >= len(actual.type_args) or i >= len(expected.type_args):
|
|
1604
|
+
return False
|
|
1605
|
+
actual_arg = actual.type_args[i]
|
|
1606
|
+
expected_arg = expected.type_args[i]
|
|
1607
|
+
if actual_arg == expected_arg:
|
|
1608
|
+
continue
|
|
1609
|
+
if param_name not in covariant:
|
|
1610
|
+
return False # invariant position must match exactly
|
|
1611
|
+
if not isinstance(actual_arg, TpyType) or not isinstance(expected_arg, TpyType):
|
|
1612
|
+
return False
|
|
1613
|
+
# Target must be a @dynamic protocol or class parent for C++ pointer upcast
|
|
1614
|
+
if not self._is_covariant_target(actual_arg, expected_arg):
|
|
1615
|
+
return False
|
|
1616
|
+
return True
|
|
1617
|
+
|
|
1618
|
+
def _is_representational_subtype(self, child: TpyType, parent: TpyType) -> bool:
|
|
1619
|
+
"""True if a `Ptr[child] -> Ptr[parent]` C++ pointer upcast is valid,
|
|
1620
|
+
where `child` is a bounded type parameter.
|
|
1621
|
+
|
|
1622
|
+
Matches `child`'s IMMEDIATE bound against the actual `parent` at each
|
|
1623
|
+
hop -- never flattens a bound to bound(parent), which would wrongly
|
|
1624
|
+
admit a sibling subtype (`U: T`, `T: Pet` does NOT make `U` any
|
|
1625
|
+
`Pet`). A `U: V`, `V: U` cycle is guarded by a seen-set.
|
|
1626
|
+
|
|
1627
|
+
A protocol `parent` is declined: a structural conformer satisfies the
|
|
1628
|
+
bound without inheriting, so the plain C++ pointer upcast would be
|
|
1629
|
+
invalid. Those need the adapter path (out of scope here). Only a class
|
|
1630
|
+
or a (further) type-param bound proves the upcast. The concrete
|
|
1631
|
+
instantiation's soundness is enforced at the call site.
|
|
1632
|
+
"""
|
|
1633
|
+
if is_protocol_type(parent):
|
|
1634
|
+
return False
|
|
1635
|
+
# Record the originating type-param as representational so codegen at
|
|
1636
|
+
# call sites knows to materialize structural conformers via Adapter.
|
|
1637
|
+
origin_param = child.name if isinstance(child, TypeParamRef) else None
|
|
1638
|
+
seen: set[str] = set()
|
|
1639
|
+
cur = child
|
|
1640
|
+
while isinstance(cur, TypeParamRef) and cur.name not in seen:
|
|
1641
|
+
seen.add(cur.name)
|
|
1642
|
+
bound = self.type_ops.get_type_param_bound(cur.name)
|
|
1643
|
+
if bound is None:
|
|
1644
|
+
return False
|
|
1645
|
+
if bound == parent:
|
|
1646
|
+
if origin_param is not None:
|
|
1647
|
+
self.ctx.func.current_representational_params.add(origin_param)
|
|
1648
|
+
return True
|
|
1649
|
+
if isinstance(bound, NominalType) and bound.is_user_record:
|
|
1650
|
+
if self._is_covariant_target(bound, parent):
|
|
1651
|
+
if origin_param is not None:
|
|
1652
|
+
self.ctx.func.current_representational_params.add(origin_param)
|
|
1653
|
+
return True
|
|
1654
|
+
return False
|
|
1655
|
+
if isinstance(bound, TypeParamRef):
|
|
1656
|
+
cur = bound
|
|
1657
|
+
continue
|
|
1658
|
+
return False
|
|
1659
|
+
return False
|
|
1660
|
+
|
|
1661
|
+
def _is_covariant_target(self, child: TpyType, parent: TpyType) -> bool:
|
|
1662
|
+
"""Check if child -> parent is valid for covariant conversion.
|
|
1663
|
+
|
|
1664
|
+
Requires C++ struct inheritance: @dynamic protocol implementation
|
|
1665
|
+
or class inheritance.
|
|
1666
|
+
"""
|
|
1667
|
+
if not isinstance(child, NominalType) or not isinstance(parent, NominalType):
|
|
1668
|
+
return False
|
|
1669
|
+
# @dynamic protocol: child implements parent
|
|
1670
|
+
if is_protocol_type(parent):
|
|
1671
|
+
proto_info = protocol_info_of(parent)
|
|
1672
|
+
if proto_info and proto_info.is_dynamic:
|
|
1673
|
+
if self.protocols and self.protocols.type_conforms_to_protocol(child, parent):
|
|
1674
|
+
return True
|
|
1675
|
+
return False
|
|
1676
|
+
# Class inheritance
|
|
1677
|
+
if child.is_user_record and parent.is_user_record:
|
|
1678
|
+
return self.ctx.registry.is_subclass_of(child, parent)
|
|
1679
|
+
return False
|
|
1680
|
+
|
|
1681
|
+
def _is_local_shadow(self, name: str) -> bool:
|
|
1682
|
+
"""Check if a name is bound in a local scope, shadowing a global."""
|
|
1683
|
+
scope = self.ctx.func.current_scope
|
|
1684
|
+
while scope and scope is not self.ctx.global_scope:
|
|
1685
|
+
if name in scope.bindings:
|
|
1686
|
+
return True
|
|
1687
|
+
scope = scope.parent
|
|
1688
|
+
return False
|
|
1689
|
+
|
|
1690
|
+
def _is_owned_var(self, name: str) -> bool:
|
|
1691
|
+
"""Check if a variable has owned storage (eligible for auto-move).
|
|
1692
|
+
|
|
1693
|
+
Uses OwnType in scope as the primary authority. Falls back to
|
|
1694
|
+
rvalue_vars for variables excluded from OwnType wrapping
|
|
1695
|
+
(reassigned vars, move-through vars).
|
|
1696
|
+
"""
|
|
1697
|
+
func = self.ctx.func.current_function
|
|
1698
|
+
if isinstance(func, TpyFunction):
|
|
1699
|
+
# Own[T] params
|
|
1700
|
+
for pname, ptype in func.params:
|
|
1701
|
+
if pname == name:
|
|
1702
|
+
return unwrap_optional_own(unwrap_readonly(ptype)) is not None
|
|
1703
|
+
# Locals: check scope type.
|
|
1704
|
+
# Exclude for-loop vars: they get Own[T] from consuming iteration
|
|
1705
|
+
# element types but are not "rvalue-initialized" in the same sense
|
|
1706
|
+
# as constructor calls. Their movability is handled by the
|
|
1707
|
+
# consuming iteration system (consumed_loop_vars).
|
|
1708
|
+
scope_type = self.ctx.func.current_scope.lookup(name) if self.ctx.func.current_scope else None
|
|
1709
|
+
if scope_type and isinstance(scope_type, OwnType) and name not in self.ctx.func.loop_vars:
|
|
1710
|
+
return True
|
|
1711
|
+
# Fallback: rvalue_vars covers move-through vars and
|
|
1712
|
+
# reassigned-but-all-rvalue vars not wrapped with OwnType.
|
|
1713
|
+
# Hoisted vars are not movable (T* pointer-locals).
|
|
1714
|
+
if name in self.ctx.func.rvalue_vars and name not in self.ctx.func.hoisted_vars:
|
|
1715
|
+
if name in self.ctx.func.current_reassigned_vars:
|
|
1716
|
+
return name not in self.ctx.func.current_lvalue_reassigned
|
|
1717
|
+
return True
|
|
1718
|
+
return False
|
|
1719
|
+
# Top-level: non-value-type vars become pointer-globals, can't be moved
|
|
1720
|
+
var_type = self.ctx.func.current_scope.lookup(name) if self.ctx.func.current_scope else None
|
|
1721
|
+
if var_type:
|
|
1722
|
+
inner = var_type.wrapped if isinstance(var_type, OwnType) else var_type
|
|
1723
|
+
if not inner.is_value_type():
|
|
1724
|
+
return False
|
|
1725
|
+
return True
|
|
1726
|
+
|
|
1727
|
+
def _view_constructor_arg(self, expr: TpyExpr) -> TpyExpr | None:
|
|
1728
|
+
"""If expr is a borrowing-view constructor call, return the borrowed-from arg.
|
|
1729
|
+
|
|
1730
|
+
StrView / BytesView / Span[T] / SpanIter[T] all borrow from their
|
|
1731
|
+
first argument, so provenance/dangling predicates should recurse
|
|
1732
|
+
into that argument rather than treating the constructor call as
|
|
1733
|
+
an opaque value.
|
|
1734
|
+
"""
|
|
1735
|
+
if not isinstance(expr, TpyCall) or not expr.args:
|
|
1736
|
+
return None
|
|
1737
|
+
if expr.call_type is not None and is_borrowing_view_type(expr.call_type):
|
|
1738
|
+
return expr.args[0]
|
|
1739
|
+
return None
|
|
1740
|
+
|
|
1741
|
+
def _name_is_param_or_global(self, name: str) -> bool:
|
|
1742
|
+
"""True if the name refers to caller-owned storage that outlives the call.
|
|
1743
|
+
|
|
1744
|
+
Used by is_safe_to_return_expr and is_dangling_return to keep the
|
|
1745
|
+
self/param/non-shadowed-global checks in one place. is_param_derived_expr
|
|
1746
|
+
intentionally diverges by omitting the self check (self is the receiver,
|
|
1747
|
+
not a "parameter" for borrow-contract purposes).
|
|
1748
|
+
"""
|
|
1749
|
+
if name == "self":
|
|
1750
|
+
return True
|
|
1751
|
+
func = self.ctx.func.current_function
|
|
1752
|
+
if isinstance(func, TpyFunction):
|
|
1753
|
+
for pname, _ptype in func.params:
|
|
1754
|
+
if pname == name:
|
|
1755
|
+
return True
|
|
1756
|
+
if name in self.ctx.global_scope.bindings:
|
|
1757
|
+
if not self._is_local_shadow(name):
|
|
1758
|
+
return True
|
|
1759
|
+
return False
|
|
1760
|
+
|
|
1761
|
+
def is_param_derived_expr(self, expr: TpyExpr) -> bool:
|
|
1762
|
+
"""Check if an expression's root storage derives from parameters or globals."""
|
|
1763
|
+
if isinstance(expr, TpyCoerce):
|
|
1764
|
+
return self.is_param_derived_expr(expr.expr)
|
|
1765
|
+
# String / bytes literals live in rodata; None literal is a nullptr.
|
|
1766
|
+
# All three have non-local, permanent storage -- safe to borrow from.
|
|
1767
|
+
if isinstance(expr, (TpyStrLiteral, TpyBytesLiteral, TpyNoneLiteral)):
|
|
1768
|
+
return True
|
|
1769
|
+
if isinstance(expr, TpyName):
|
|
1770
|
+
# Parameters are param-derived
|
|
1771
|
+
func = self.ctx.func.current_function
|
|
1772
|
+
if isinstance(func, TpyFunction):
|
|
1773
|
+
for pname, _ptype in func.params:
|
|
1774
|
+
if pname == expr.name:
|
|
1775
|
+
return True
|
|
1776
|
+
# Globals are param-derived (live forever), but only if
|
|
1777
|
+
# the name isn't shadowed by a local binding
|
|
1778
|
+
if expr.name in self.ctx.global_scope.bindings:
|
|
1779
|
+
if not self._is_local_shadow(expr.name):
|
|
1780
|
+
return True
|
|
1781
|
+
# Variables tracked as param-derived
|
|
1782
|
+
if expr.name in self.ctx.func.param_provenance_vars:
|
|
1783
|
+
return True
|
|
1784
|
+
return False
|
|
1785
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1786
|
+
return self.is_param_derived_expr(expr.obj)
|
|
1787
|
+
if isinstance(expr, TpySubscript):
|
|
1788
|
+
return self.is_param_derived_expr(expr.obj)
|
|
1789
|
+
if isinstance(expr, TpyIfExpr):
|
|
1790
|
+
return (self.is_param_derived_expr(expr.then_expr)
|
|
1791
|
+
and self.is_param_derived_expr(expr.else_expr))
|
|
1792
|
+
# Pointer constructors derive provenance from their argument (address-taking)
|
|
1793
|
+
if isinstance(expr, TpyCall) and expr.call_type is not None and expr.call_type.is_pointer() and expr.args:
|
|
1794
|
+
return self.is_param_derived_expr(expr.args[0])
|
|
1795
|
+
view_arg = self._view_constructor_arg(expr)
|
|
1796
|
+
if view_arg is not None:
|
|
1797
|
+
return self.is_param_derived_expr(view_arg)
|
|
1798
|
+
# Function/method calls with return_borrows_from: result is param-derived if
|
|
1799
|
+
# the borrowed-from argument(s) are themselves param-derived.
|
|
1800
|
+
# None = unanalyzed (skip); frozenset() = returns new value (loop body never
|
|
1801
|
+
# runs, falls through to return False below -- correct).
|
|
1802
|
+
if isinstance(expr, (TpyCall, TpyMethodCall)):
|
|
1803
|
+
fi = expr.resolved_function_info
|
|
1804
|
+
if fi is not None and fi.return_borrows_from is not None:
|
|
1805
|
+
args = expr.args
|
|
1806
|
+
obj = expr.obj if isinstance(expr, TpyMethodCall) else None
|
|
1807
|
+
for idx in fi.return_borrows_from:
|
|
1808
|
+
if idx == -1 and obj is not None:
|
|
1809
|
+
if self.is_param_derived_expr(obj):
|
|
1810
|
+
return True
|
|
1811
|
+
elif 0 <= idx < len(args):
|
|
1812
|
+
if self.is_param_derived_expr(args[idx]):
|
|
1813
|
+
return True
|
|
1814
|
+
# Constructors, function calls, literals -- local storage
|
|
1815
|
+
return False
|
|
1816
|
+
|
|
1817
|
+
def is_safe_to_return_expr(self, expr: TpyExpr) -> bool:
|
|
1818
|
+
"""Whether binding a local to this RHS makes the local safe to return.
|
|
1819
|
+
|
|
1820
|
+
Recurses through composite forms so a per-call-site OR doesn't miss
|
|
1821
|
+
``sv = p if cond else pick(q)`` (neither arm uniformly param-derived
|
|
1822
|
+
nor uniformly a trusted call). Must be a superset of
|
|
1823
|
+
is_param_derived_expr to maintain the subset invariant on which
|
|
1824
|
+
flow-merge intersection relies.
|
|
1825
|
+
"""
|
|
1826
|
+
if isinstance(expr, TpyCoerce):
|
|
1827
|
+
return self.is_safe_to_return_expr(expr.expr)
|
|
1828
|
+
# String / bytes / None literals live in rodata / are nullptr --
|
|
1829
|
+
# permanent storage, safe to return. Mirrors is_param_derived_expr.
|
|
1830
|
+
if isinstance(expr, (TpyStrLiteral, TpyBytesLiteral, TpyNoneLiteral)):
|
|
1831
|
+
return True
|
|
1832
|
+
if isinstance(expr, TpyName):
|
|
1833
|
+
return (self._name_is_param_or_global(expr.name)
|
|
1834
|
+
or expr.name in self.ctx.func.safe_to_return_vars)
|
|
1835
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1836
|
+
return self.is_safe_to_return_expr(expr.obj)
|
|
1837
|
+
if isinstance(expr, TpySubscript):
|
|
1838
|
+
return self.is_safe_to_return_expr(expr.obj)
|
|
1839
|
+
if isinstance(expr, TpyIfExpr):
|
|
1840
|
+
return (self.is_safe_to_return_expr(expr.then_expr)
|
|
1841
|
+
and self.is_safe_to_return_expr(expr.else_expr))
|
|
1842
|
+
# Pointer constructors derive safety from their argument (address-taking).
|
|
1843
|
+
if isinstance(expr, TpyCall) and expr.call_type is not None and expr.call_type.is_pointer() and expr.args:
|
|
1844
|
+
return self.is_safe_to_return_expr(expr.args[0])
|
|
1845
|
+
view_arg = self._view_constructor_arg(expr)
|
|
1846
|
+
if view_arg is not None:
|
|
1847
|
+
return self.is_safe_to_return_expr(view_arg)
|
|
1848
|
+
# Calls with return_borrows_from are not handled explicitly here:
|
|
1849
|
+
# is_dangling_return already recurses into the borrowed args, so a
|
|
1850
|
+
# call whose return borrows from a param-derived (or otherwise safe)
|
|
1851
|
+
# arg is non-dangling and lands in this branch as safe. If
|
|
1852
|
+
# is_dangling_return is ever tightened for return_borrows_from, mirror
|
|
1853
|
+
# the explicit arm from is_param_derived_expr to preserve the
|
|
1854
|
+
# superset invariant.
|
|
1855
|
+
if isinstance(expr, (TpyCall, TpyMethodCall)):
|
|
1856
|
+
return not self.is_dangling_return(expr)
|
|
1857
|
+
return False
|
|
1858
|
+
|
|
1859
|
+
def is_mutable_lvalue(self, expr: TpyExpr) -> bool:
|
|
1860
|
+
"""Check if an expression is a mutable lvalue (can get a mutable Ptr).
|
|
1861
|
+
|
|
1862
|
+
This is like is_lvalue but also rejects read-only sources like Span elements.
|
|
1863
|
+
"""
|
|
1864
|
+
if isinstance(expr, TpyCoerce):
|
|
1865
|
+
return self.is_mutable_lvalue(expr.expr)
|
|
1866
|
+
# Named variables: mutable unless the binding's type is `readonly[T]`
|
|
1867
|
+
# (parameter declared as `readonly[T]`, or `self` inside a @readonly
|
|
1868
|
+
# method). Without this check, a readonly binding could launder
|
|
1869
|
+
# away its const via `&x` / `take_ptr(x)` / record-to-Ptr upcasts.
|
|
1870
|
+
if isinstance(expr, TpyName):
|
|
1871
|
+
expr_type = self.ctx.get_expr_type(expr)
|
|
1872
|
+
if isinstance(expr_type, ReadonlyType):
|
|
1873
|
+
return False
|
|
1874
|
+
return True
|
|
1875
|
+
# Field access on a mutable lvalue is also mutable
|
|
1876
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1877
|
+
return self.is_mutable_lvalue(expr.obj)
|
|
1878
|
+
# Subscript: check if the base is a read-only type (Span[readonly[T]], str)
|
|
1879
|
+
if isinstance(expr, TpySubscript):
|
|
1880
|
+
obj_type = self.ctx.get_expr_type(expr.obj)
|
|
1881
|
+
if is_span(obj_type) and is_readonly_span(obj_type):
|
|
1882
|
+
return False # Span[readonly[T]] elements are read-only
|
|
1883
|
+
if is_any_str_type(obj_type):
|
|
1884
|
+
return False
|
|
1885
|
+
return self.is_mutable_lvalue(expr.obj)
|
|
1886
|
+
return False
|
|
1887
|
+
|
|
1888
|
+
def is_dangling_return(self, expr: TpyExpr) -> bool:
|
|
1889
|
+
"""Check if returning this expression would create a dangling reference."""
|
|
1890
|
+
if isinstance(expr, TpyCoerce):
|
|
1891
|
+
return self.is_dangling_return(expr.expr)
|
|
1892
|
+
# Array/dict literal - creates temporary
|
|
1893
|
+
if isinstance(expr, (TpyArrayLiteral, TpyDictLiteral, TpySetLiteral)):
|
|
1894
|
+
return True
|
|
1895
|
+
|
|
1896
|
+
# List repeat - creates temporary
|
|
1897
|
+
if isinstance(expr, TpyListRepeat):
|
|
1898
|
+
return True
|
|
1899
|
+
|
|
1900
|
+
# Constructor call - creates temporary
|
|
1901
|
+
if isinstance(expr, TpyCall):
|
|
1902
|
+
# Pointer constructors: dangling depends on the argument, not the pointer itself
|
|
1903
|
+
if isinstance(expr.call_type, PtrType):
|
|
1904
|
+
if not expr.args:
|
|
1905
|
+
return False # Ptr[T]() -> nullptr, always safe
|
|
1906
|
+
return self.is_dangling_return(expr.args[0])
|
|
1907
|
+
|
|
1908
|
+
view_arg = self._view_constructor_arg(expr)
|
|
1909
|
+
if view_arg is not None:
|
|
1910
|
+
return self.is_dangling_return(view_arg)
|
|
1911
|
+
|
|
1912
|
+
# @value_ptr_coercion functions (e.g. take_ptr): result borrows
|
|
1913
|
+
# from the arg value, so dangling depends on the arg.
|
|
1914
|
+
fi = expr.resolved_function_info
|
|
1915
|
+
if fi is not None and fi.value_ptr_coercion and expr.args:
|
|
1916
|
+
return self.is_dangling_return(expr.args[0])
|
|
1917
|
+
|
|
1918
|
+
# Generic type constructor creates a temporary
|
|
1919
|
+
if expr.call_type is not None:
|
|
1920
|
+
return True
|
|
1921
|
+
|
|
1922
|
+
# Expression callees return temporaries (not dangling)
|
|
1923
|
+
if not isinstance(expr.func, TpyName):
|
|
1924
|
+
return True
|
|
1925
|
+
|
|
1926
|
+
# Record constructor
|
|
1927
|
+
if expr.func_name in self.ctx.registry.records:
|
|
1928
|
+
return True
|
|
1929
|
+
|
|
1930
|
+
# Function returning Own[T] creates a temporary (by-value return).
|
|
1931
|
+
# Only check user-defined functions (builtins don't appear in
|
|
1932
|
+
# registry.functions).
|
|
1933
|
+
if self.ctx.registry.get_function(expr.func_name) is not None:
|
|
1934
|
+
fi = expr.resolved_function_info
|
|
1935
|
+
if fi and isinstance(fi.return_type, OwnType):
|
|
1936
|
+
return True
|
|
1937
|
+
|
|
1938
|
+
# Function whose return borrows from args (e.g. generators storing
|
|
1939
|
+
# non-value params as T& references): dangles if any borrowed arg dangles
|
|
1940
|
+
if fi is not None and fi.return_borrows_from:
|
|
1941
|
+
for idx in fi.return_borrows_from:
|
|
1942
|
+
if 0 <= idx < len(expr.args):
|
|
1943
|
+
if self.is_dangling_return(expr.args[idx]):
|
|
1944
|
+
return True
|
|
1945
|
+
|
|
1946
|
+
# A function returning owned str/String creates a temporary
|
|
1947
|
+
# std::string that dangles if returned as StrView.
|
|
1948
|
+
if fi is not None and (is_str_type(fi.return_type) or is_string_type(fi.return_type)):
|
|
1949
|
+
return True
|
|
1950
|
+
|
|
1951
|
+
# A function returning a recursive-union wrapper struct is an
|
|
1952
|
+
# rvalue by value (the wrapper is value-shape). Taking its
|
|
1953
|
+
# address (the Optional pointer-repr return path) would yield
|
|
1954
|
+
# `&(call())` -- ill-formed C++. Force the diagnostic so the
|
|
1955
|
+
# user wraps the return type in Own[Optional[Wrapper]] (or
|
|
1956
|
+
# binds to a local first).
|
|
1957
|
+
if fi is not None:
|
|
1958
|
+
ret_unwrapped = unwrap_ref_type(unwrap_readonly(fi.return_type))
|
|
1959
|
+
if ret_unwrapped.needs_wrapper():
|
|
1960
|
+
return True
|
|
1961
|
+
|
|
1962
|
+
# Regular function call - assume it returns something safe
|
|
1963
|
+
# (the callee is responsible for not returning dangling refs)
|
|
1964
|
+
return False
|
|
1965
|
+
|
|
1966
|
+
# Locals dangle unless their root or current binding is safe.
|
|
1967
|
+
if isinstance(expr, TpyName):
|
|
1968
|
+
if self._name_is_param_or_global(expr.name):
|
|
1969
|
+
return False
|
|
1970
|
+
if expr.name in self.ctx.func.safe_to_return_vars:
|
|
1971
|
+
return False
|
|
1972
|
+
return True
|
|
1973
|
+
|
|
1974
|
+
# Field access - safe only if the object itself is safe
|
|
1975
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1976
|
+
return self.is_dangling_return(expr.obj)
|
|
1977
|
+
|
|
1978
|
+
# Subscript - safe only if the container itself is safe
|
|
1979
|
+
if isinstance(expr, TpySubscript):
|
|
1980
|
+
return self.is_dangling_return(expr.obj)
|
|
1981
|
+
|
|
1982
|
+
# Method call returning owned str/String creates a temporary
|
|
1983
|
+
# std::string that dangles if returned as StrView.
|
|
1984
|
+
if isinstance(expr, TpyMethodCall):
|
|
1985
|
+
fi = expr.resolved_function_info
|
|
1986
|
+
if fi is not None and (is_str_type(fi.return_type) or is_string_type(fi.return_type)):
|
|
1987
|
+
return True
|
|
1988
|
+
# A method returning a recursive-union wrapper struct yields a
|
|
1989
|
+
# value-shape rvalue; taking its address (the Optional pointer-repr
|
|
1990
|
+
# return path) would emit `&(call())`. Reuses the free-function
|
|
1991
|
+
# branch's wrapper-shape check so `return obj.method()` through
|
|
1992
|
+
# Optional[Wrapper] is rejected (use Own[Optional[Wrapper]]). The
|
|
1993
|
+
# free-function branch's return_borrows_from handling is not yet
|
|
1994
|
+
# mirrored here -- see BUGS.md.
|
|
1995
|
+
if fi is not None and unwrap_ref_type(unwrap_readonly(fi.return_type)).needs_wrapper():
|
|
1996
|
+
return True
|
|
1997
|
+
return False
|
|
1998
|
+
|
|
1999
|
+
# Ternary - dangles if either branch dangles
|
|
2000
|
+
if isinstance(expr, TpyIfExpr):
|
|
2001
|
+
return (self.is_dangling_return(expr.then_expr)
|
|
2002
|
+
or self.is_dangling_return(expr.else_expr))
|
|
2003
|
+
|
|
2004
|
+
# Unary/Binary ops - might create temporaries, be conservative
|
|
2005
|
+
if isinstance(expr, (TpyUnaryOp, TpyBinOp)):
|
|
2006
|
+
return True
|
|
2007
|
+
|
|
2008
|
+
# Default: assume safe
|
|
2009
|
+
return False
|
|
2010
|
+
|
|
2011
|
+
def check_view_return_dangle(self, expr: TpyExpr, return_type: TpyType,
|
|
2012
|
+
loc: SourceLocation | None) -> None:
|
|
2013
|
+
"""Variant of check_dangling_reference for value-return contexts
|
|
2014
|
+
(lambda bodies, yield values). The full check_dangling_reference
|
|
2015
|
+
rejects local/temporary returns when the return type is a non-value
|
|
2016
|
+
object -- a false positive for value-return contexts where the C++
|
|
2017
|
+
callable/generator machinery moves/copies by value. Only the
|
|
2018
|
+
view-dangling and pointer-dangling sub-rules apply here.
|
|
2019
|
+
"""
|
|
2020
|
+
if isinstance(return_type, PtrType) or is_borrowing_view_type(return_type):
|
|
2021
|
+
self.check_dangling_reference(expr, return_type, loc)
|
|
2022
|
+
|
|
2023
|
+
def check_dangling_reference(self, expr: TpyExpr, return_type: TpyType, loc: SourceLocation | None) -> None:
|
|
2024
|
+
"""Check if returning expr as a reference would be a dangling reference.
|
|
2025
|
+
|
|
2026
|
+
Object types are returned by reference. Returning a local variable or
|
|
2027
|
+
newly constructed object would create a dangling reference.
|
|
2028
|
+
"""
|
|
2029
|
+
# Only check object types (value types are returned by value)
|
|
2030
|
+
# OwnType returns by value (ownership transfer), so no dangling risk
|
|
2031
|
+
# Pointer types need dangling checks (the pointer value may point to a local)
|
|
2032
|
+
if isinstance(return_type, PtrType):
|
|
2033
|
+
if self.is_dangling_return(expr):
|
|
2034
|
+
raise self.ctx.error(
|
|
2035
|
+
"Cannot return pointer to local or temporary value; "
|
|
2036
|
+
"the returned pointer would dangle",
|
|
2037
|
+
expr
|
|
2038
|
+
)
|
|
2039
|
+
return
|
|
2040
|
+
# Value types that hold an interior pointer -- returning one that
|
|
2041
|
+
# borrows from a local would dangle after the function returns.
|
|
2042
|
+
view_msg = _dangling_view_message(return_type)
|
|
2043
|
+
if view_msg is not None:
|
|
2044
|
+
inner = expr.expr if isinstance(expr, TpyCoerce) else expr
|
|
2045
|
+
view_arg = self._view_constructor_arg(inner)
|
|
2046
|
+
if view_arg is not None:
|
|
2047
|
+
if self.is_dangling_return(view_arg):
|
|
2048
|
+
raise self.ctx.error(view_msg, inner)
|
|
2049
|
+
elif self.is_dangling_return(expr):
|
|
2050
|
+
raise self.ctx.error(view_msg, expr)
|
|
2051
|
+
return
|
|
2052
|
+
if isinstance(return_type, TupleType):
|
|
2053
|
+
if isinstance(expr, TpyTupleLiteral):
|
|
2054
|
+
for i, et in enumerate(return_type.element_types):
|
|
2055
|
+
if (not et.is_value_type() and not isinstance(et, (OwnType, TypeParamRef))
|
|
2056
|
+
and not et.needs_wrapper()
|
|
2057
|
+
and i < len(expr.elements)):
|
|
2058
|
+
if self.is_dangling_return(expr.elements[i]):
|
|
2059
|
+
raise self.ctx.error(
|
|
2060
|
+
f"Cannot return local or temporary as tuple element {i}. "
|
|
2061
|
+
f"Type '{et}' is returned by reference. "
|
|
2062
|
+
f"Use Own[{et}] to return by value.",
|
|
2063
|
+
expr.elements[i]
|
|
2064
|
+
)
|
|
2065
|
+
return
|
|
2066
|
+
if (return_type.is_value_type()
|
|
2067
|
+
or isinstance(return_type, (VoidType, OwnType))
|
|
2068
|
+
or unwrap_readonly(return_type).needs_wrapper()):
|
|
2069
|
+
# Recursive-union wrappers (non-generic `UnionType` form + the
|
|
2070
|
+
# generic `RecursiveAliasInstanceType`) are returned by value at
|
|
2071
|
+
# the C++ level even though `is_value_type()` is False (the
|
|
2072
|
+
# alternatives may be reference types) -- codegen emits the
|
|
2073
|
+
# wrapper struct as a value-returned type, so no dangling concern.
|
|
2074
|
+
# The `unwrap_readonly` mirrors codegen's `to_cpp_return_const`
|
|
2075
|
+
# delegation through ReadonlyType.
|
|
2076
|
+
return
|
|
2077
|
+
|
|
2078
|
+
# Optional[T] for non-value T returns T* -- returning a local would dangle.
|
|
2079
|
+
# But `return None` is always safe (returns nullptr).
|
|
2080
|
+
if isinstance(return_type, OptionalType):
|
|
2081
|
+
if isinstance(expr, TpyNoneLiteral):
|
|
2082
|
+
return
|
|
2083
|
+
if self.is_dangling_return(expr):
|
|
2084
|
+
raise self.ctx.error(
|
|
2085
|
+
f"Cannot return local or temporary as '{return_type}'. "
|
|
2086
|
+
f"The returned pointer would dangle. "
|
|
2087
|
+
f"Return a reference to parameter data, or use Own[{return_type}] "
|
|
2088
|
+
f"to return by value.",
|
|
2089
|
+
expr
|
|
2090
|
+
)
|
|
2091
|
+
return
|
|
2092
|
+
|
|
2093
|
+
# Check if the expression is safe to return as a reference
|
|
2094
|
+
if self.is_dangling_return(expr):
|
|
2095
|
+
if is_protocol_type(return_type):
|
|
2096
|
+
raise self.ctx.error(
|
|
2097
|
+
f"Cannot return local or temporary as '{return_type}'. "
|
|
2098
|
+
f"Dynamic protocol return requires a value that outlives the caller "
|
|
2099
|
+
f"(parameter or global).",
|
|
2100
|
+
expr
|
|
2101
|
+
)
|
|
2102
|
+
raise self.ctx.error(
|
|
2103
|
+
f"Cannot return local or temporary as reference. "
|
|
2104
|
+
f"Object type '{return_type}' is returned by reference. "
|
|
2105
|
+
f"Use Own[{return_type}] to return by value.",
|
|
2106
|
+
expr
|
|
2107
|
+
)
|