tpy-lang 0.3.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tpy_lang-0.3.0.dev0.dist-info/METADATA +151 -0
- tpy_lang-0.3.0.dev0.dist-info/RECORD +333 -0
- tpy_lang-0.3.0.dev0.dist-info/WHEEL +4 -0
- tpy_lang-0.3.0.dev0.dist-info/entry_points.txt +3 -0
- tpyc/__init__.py +104 -0
- tpyc/__main__.py +6 -0
- tpyc/_buildinfo.py +1 -0
- tpyc/_data/docs/LANGUAGE_FEATURES.md +6278 -0
- tpyc/_data/docs/STDLIB_ROADMAP.md +1258 -0
- tpyc/_data/docs/TPY_FOR_AGENTS.md +556 -0
- tpyc/_data/lib/tpy/_bindings/__init__.py +6 -0
- tpyc/_data/lib/tpy/_bindings/pcre2.py +173 -0
- tpyc/_data/lib/tpy/_bindings/posix_socket.py +161 -0
- tpyc/_data/lib/tpy/_functools_macros.py +80 -0
- tpyc/_data/lib/tpy/_macro_helpers.py +161 -0
- tpyc/_data/lib/tpy/argparse.py +2062 -0
- tpyc/_data/lib/tpy/asyncio/__init__.py +744 -0
- tpyc/_data/lib/tpy/asyncio/_executor.py +515 -0
- tpyc/_data/lib/tpy/base64.py +410 -0
- tpyc/_data/lib/tpy/bisect.py +39 -0
- tpyc/_data/lib/tpy/builtins.py +38 -0
- tpyc/_data/lib/tpy/dataclasses.py +354 -0
- tpyc/_data/lib/tpy/enum.py +23 -0
- tpyc/_data/lib/tpy/functools.py +33 -0
- tpyc/_data/lib/tpy/hashlib.py +206 -0
- tpyc/_data/lib/tpy/heapq.py +118 -0
- tpyc/_data/lib/tpy/io.py +395 -0
- tpyc/_data/lib/tpy/json.py +221 -0
- tpyc/_data/lib/tpy/math.py +406 -0
- tpyc/_data/lib/tpy/random.py +597 -0
- tpyc/_data/lib/tpy/re.py +467 -0
- tpyc/_data/lib/tpy/socket.py +379 -0
- tpyc/_data/lib/tpy/struct.py +178 -0
- tpyc/_data/lib/tpy/sys.py +40 -0
- tpyc/_data/lib/tpy/time.py +39 -0
- tpyc/_data/lib/tpy/tpy/__init__.py +78 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/__init__.py +10 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/_decorators.py +37 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/_extern.py +64 -0
- tpyc/_data/lib/tpy/tpy/_builtins/__init__.py +11 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_bytes.py +378 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_dict.py +151 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_exceptions.py +125 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_funcs.py +681 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_io.py +97 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_list.py +127 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_range.py +52 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_set.py +139 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_super.py +11 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_types.py +661 -0
- tpyc/_data/lib/tpy/tpy/_core/__init__.py +23 -0
- tpyc/_data/lib/tpy/tpy/_core/_bytes_view.py +129 -0
- tpyc/_data/lib/tpy/tpy/_core/_containers.py +137 -0
- tpyc/_data/lib/tpy/tpy/_core/_functions.py +40 -0
- tpyc/_data/lib/tpy/tpy/_core/_types.py +2061 -0
- tpyc/_data/lib/tpy/tpy/_typing/__init__.py +77 -0
- tpyc/_data/lib/tpy/tpy/_version.py +29 -0
- tpyc/_data/lib/tpy/tpy/bits.py +28 -0
- tpyc/_data/lib/tpy/tpy/coro/__init__.py +127 -0
- tpyc/_data/lib/tpy/tpy/extern.py +8 -0
- tpyc/_data/lib/tpy/tpy/mem.py +49 -0
- tpyc/_data/lib/tpy/tpy/unsafe.py +195 -0
- tpyc/_data/lib/tpy/tpy/version.py +21 -0
- tpyc/_data/lib/tpy/typing.py +13 -0
- tpyc/_data/runtime/cpp/include/tpy/any.hpp +461 -0
- tpyc/_data/runtime/cpp/include/tpy/as_ostream.hpp +117 -0
- tpyc/_data/runtime/cpp/include/tpy/async.hpp +76 -0
- tpyc/_data/runtime/cpp/include/tpy/bigint.hpp +1343 -0
- tpyc/_data/runtime/cpp/include/tpy/builtins.hpp +400 -0
- tpyc/_data/runtime/cpp/include/tpy/bytes_ops.hpp +469 -0
- tpyc/_data/runtime/cpp/include/tpy/container_ops.hpp +487 -0
- tpyc/_data/runtime/cpp/include/tpy/copy_iter.hpp +82 -0
- tpyc/_data/runtime/cpp/include/tpy/core.hpp +558 -0
- tpyc/_data/runtime/cpp/include/tpy/dict_ops.hpp +289 -0
- tpyc/_data/runtime/cpp/include/tpy/dunder.hpp +750 -0
- tpyc/_data/runtime/cpp/include/tpy/dynamic.hpp +44 -0
- tpyc/_data/runtime/cpp/include/tpy/enum.hpp +40 -0
- tpyc/_data/runtime/cpp/include/tpy/file.hpp +245 -0
- tpyc/_data/runtime/cpp/include/tpy/fixed_int.hpp +317 -0
- tpyc/_data/runtime/cpp/include/tpy/format.hpp +954 -0
- tpyc/_data/runtime/cpp/include/tpy/frame_slot.hpp +120 -0
- tpyc/_data/runtime/cpp/include/tpy/generator.hpp +47 -0
- tpyc/_data/runtime/cpp/include/tpy/iterable_ops.hpp +122 -0
- tpyc/_data/runtime/cpp/include/tpy/itertools.hpp +749 -0
- tpyc/_data/runtime/cpp/include/tpy/next_iter.hpp +82 -0
- tpyc/_data/runtime/cpp/include/tpy/ordered_map.hpp +518 -0
- tpyc/_data/runtime/cpp/include/tpy/ordered_set.hpp +337 -0
- tpyc/_data/runtime/cpp/include/tpy/own_iter.hpp +54 -0
- tpyc/_data/runtime/cpp/include/tpy/pascal_graph_sdl.hpp +192 -0
- tpyc/_data/runtime/cpp/include/tpy/printing.hpp +302 -0
- tpyc/_data/runtime/cpp/include/tpy/protocols.hpp +61 -0
- tpyc/_data/runtime/cpp/include/tpy/range.hpp +115 -0
- tpyc/_data/runtime/cpp/include/tpy/ranges.hpp +212 -0
- tpyc/_data/runtime/cpp/include/tpy/set_ops.hpp +265 -0
- tpyc/_data/runtime/cpp/include/tpy/slice.hpp +47 -0
- tpyc/_data/runtime/cpp/include/tpy/span_iter.hpp +42 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/math.hpp +41 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/pcre2_h.hpp +96 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/random.hpp +25 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/socket_h.hpp +145 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/time.hpp +62 -0
- tpyc/_data/runtime/cpp/include/tpy/system.hpp +121 -0
- tpyc/_data/runtime/cpp/include/tpy/throwable.hpp +55 -0
- tpyc/_data/runtime/cpp/include/tpy/tpy.hpp +156 -0
- tpyc/_data/runtime/cpp/include/tpy/type_name.hpp +77 -0
- tpyc/_data/runtime/cpp/include/tpy/type_traits.hpp +240 -0
- tpyc/_data/runtime/cpp/include/tpy/uninit_array_storage.hpp +250 -0
- tpyc/_data/runtime/cpp/include/tpy/uninit_heap_storage.hpp +277 -0
- tpyc/_data/runtime/cpp/include/tpy/varargs.hpp +174 -0
- tpyc/_data/runtime/cpp/include/tpy/variant_ref.hpp +118 -0
- tpyc/_data/runtime/cpp/src/stdlib/socket_impl.cpp +104 -0
- tpyc/_data/runtime/cpp/third_party/README.md +58 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/AUTHORS +36 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/CMakeLists.txt +1233 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/COPYING +5 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/ChangeLog +3097 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/HACKING +853 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/INSTALL +368 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/LICENCE +94 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/NEWS +492 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/NON-AUTOTOOLS-BUILD +430 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/README +956 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/COPYING-CMAKE-SCRIPTS +22 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindEditline.cmake +16 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindPackageHandleStandardArgs.cmake +58 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindReadline.cmake +29 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config-version.cmake.in +15 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config.cmake.in +148 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/config-cmake.h.in +56 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-16.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-32.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-8.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-posix.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/pcre2-config.in +121 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h +483 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.generic +483 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.in +460 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.generic +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.in +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_auto_possess.c +1371 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c +196 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c.dist +196 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chkdint.c +96 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_compile.c +11001 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_config.c +252 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_context.c +510 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_convert.c +1189 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dfa_match.c +4119 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dftables.c +297 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_error.c +345 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_extuni.c +162 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_find_bracket.c +219 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_fuzzsupport.c +792 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_internal.h +2084 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_intmodedep.h +940 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_compile.c +14972 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_match.c +200 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_misc.c +234 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_neon_inc.h +354 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_simd_inc.h +2355 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_test.c +2528 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_maketables.c +165 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match.c +7777 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match_data.c +185 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_newline.c +243 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ord2utf.c +120 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_pattern_info.c +432 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_printint.c +886 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_script_run.c +344 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_serialize.c +286 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_string_utils.c +237 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_study.c +1915 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substitute.c +1009 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substring.c +550 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_tables.c +234 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucd.c +5460 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucp.h +396 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucptables.c +1533 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_valid_utf.c +398 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_xclass.c +308 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2demo.c +497 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2grep.c +4606 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.c +425 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.h +187 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix_test.c +209 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2test.c +9708 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorApple.c +137 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorCore.c +327 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorFreeBSD.c +89 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorPosix.c +62 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorWindows.c +40 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorNetBSD.c +72 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorPosix.c +172 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorPosix.c +141 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorWindows.c +102 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfig.h +142 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigCPU.h +188 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigInternal.h +907 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.c +3561 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.h +2466 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_32.c +4636 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_64.c +3491 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_T2_32.c +4302 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeLOONGARCH_64.c +3765 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_32.c +472 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_64.c +387 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_common.c +4259 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_32.c +485 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_64.c +719 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_common.c +3161 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_32.c +142 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_64.c +222 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_common.c +3121 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeS390X.c +4526 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_32.c +1685 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_64.c +1398 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_common.c +5001 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitSerialize.c +516 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitUtils.c +344 -0
- tpyc/_data/runtime/cpp/third_party/pcre2.sources.txt +54 -0
- tpyc/_data/runtime/cpp/third_party/pcre2.vendor.json +7 -0
- tpyc/build/__init__.py +7 -0
- tpyc/build/pcre2.py +122 -0
- tpyc/build/third_party.py +413 -0
- tpyc/cli.py +822 -0
- tpyc/codegen_cpp/__init__.py +18 -0
- tpyc/codegen_cpp/builtins.py +484 -0
- tpyc/codegen_cpp/context.py +2064 -0
- tpyc/codegen_cpp/expressions.py +5940 -0
- tpyc/codegen_cpp/functions.py +1913 -0
- tpyc/codegen_cpp/gen_async.py +3258 -0
- tpyc/codegen_cpp/gen_generators.py +657 -0
- tpyc/codegen_cpp/generator.py +2258 -0
- tpyc/codegen_cpp/match.py +1997 -0
- tpyc/codegen_cpp/param_const.py +172 -0
- tpyc/codegen_cpp/protocols.py +907 -0
- tpyc/codegen_cpp/records.py +1654 -0
- tpyc/codegen_cpp/resumable_cfg.py +1651 -0
- tpyc/codegen_cpp/statements.py +4963 -0
- tpyc/codegen_cpp/string_dispatch.py +76 -0
- tpyc/codegen_cpp/test_context.py +46 -0
- tpyc/codegen_cpp/test_param_const.py +113 -0
- tpyc/codegen_cpp/test_resumable_cfg.py +182 -0
- tpyc/codegen_cpp/type_resolution.py +53 -0
- tpyc/codegen_cpp/types.py +436 -0
- tpyc/codegen_cpp/variant_access.py +135 -0
- tpyc/coercions.py +749 -0
- tpyc/compilation_context.py +57 -0
- tpyc/compiler.py +3945 -0
- tpyc/cycle_detection.py +358 -0
- tpyc/diagnostics.py +135 -0
- tpyc/dump_types.py +353 -0
- tpyc/frontend_diagnostics.py +47 -0
- tpyc/frontend_ir/__init__.py +140 -0
- tpyc/frontend_ir/lower.py +1098 -0
- tpyc/frontend_ir/nodes.py +718 -0
- tpyc/frontend_ir/resolver_adapter.py +151 -0
- tpyc/frontend_plugin.py +209 -0
- tpyc/install_docs.py +81 -0
- tpyc/liveness.py +756 -0
- tpyc/macro_api.py +1724 -0
- tpyc/macro_loader.py +497 -0
- tpyc/module_names.py +64 -0
- tpyc/modules/__init__.py +31 -0
- tpyc/modules/defs.py +89 -0
- tpyc/modules/registry.py +36 -0
- tpyc/modules/resolver.py +192 -0
- tpyc/modules/type_resolution.py +629 -0
- tpyc/namespace.py +172 -0
- tpyc/parse/__init__.py +84 -0
- tpyc/parse/imports.py +490 -0
- tpyc/parse/nodes.py +1732 -0
- tpyc/parse/parser.py +4043 -0
- tpyc/parse/resolve_refs.py +466 -0
- tpyc/parse/type_resolver.py +1060 -0
- tpyc/prescan.py +254 -0
- tpyc/qnames.py +149 -0
- tpyc/repl.py +529 -0
- tpyc/repl_backends.py +848 -0
- tpyc/sema/__init__.py +21 -0
- tpyc/sema/analyzer.py +3625 -0
- tpyc/sema/bound_check.py +72 -0
- tpyc/sema/builder_trace.py +684 -0
- tpyc/sema/calls.py +5406 -0
- tpyc/sema/compatibility.py +2107 -0
- tpyc/sema/context.py +1243 -0
- tpyc/sema/expressions.py +3737 -0
- tpyc/sema/flow_facts.py +199 -0
- tpyc/sema/init_tracker.py +150 -0
- tpyc/sema/list_literals.py +69 -0
- tpyc/sema/literal_utils.py +27 -0
- tpyc/sema/local_deduction.py +1088 -0
- tpyc/sema/macros.py +179 -0
- tpyc/sema/match.py +1177 -0
- tpyc/sema/method_expansion.py +347 -0
- tpyc/sema/methods.py +2197 -0
- tpyc/sema/mutation_propagation.py +268 -0
- tpyc/sema/narrowing.py +857 -0
- tpyc/sema/numeric_lattice.py +160 -0
- tpyc/sema/operators.py +402 -0
- tpyc/sema/overloads.py +841 -0
- tpyc/sema/protocols.py +1209 -0
- tpyc/sema/reach_analysis.py +202 -0
- tpyc/sema/registration.py +3156 -0
- tpyc/sema/scope_tracker.py +193 -0
- tpyc/sema/statements.py +4426 -0
- tpyc/sema/type_ops.py +1879 -0
- tpyc/sema/value_range.py +181 -0
- tpyc/symbol_binding.py +259 -0
- tpyc/test_c3_mro.py +208 -0
- tpyc/test_cli_argv.py +52 -0
- tpyc/test_compiler.py +559 -0
- tpyc/test_contains_type_param.py +101 -0
- tpyc/test_cycle_detection.py +221 -0
- tpyc/test_dump_types.py +225 -0
- tpyc/test_install_docs.py +65 -0
- tpyc/test_local_cpp_form.py +135 -0
- tpyc/test_macro_loader.py +76 -0
- tpyc/test_method_expansion.py +254 -0
- tpyc/test_nominal_identity.py +182 -0
- tpyc/test_overloads.py +410 -0
- tpyc/test_parse.py +303 -0
- tpyc/test_parse_type_ref.py +506 -0
- tpyc/test_parse_version_info.py +58 -0
- tpyc/test_reach_analysis.py +72 -0
- tpyc/test_ref_type.py +216 -0
- tpyc/test_send_sync_substitution.py +276 -0
- tpyc/test_tuple_mutation_propagation.py +206 -0
- tpyc/test_type_def_registry.py +1729 -0
- tpyc/test_union_types.py +195 -0
- tpyc/type_def_registry.py +975 -0
- tpyc/typesys.py +5104 -0
tpyc/sema/statements.py
ADDED
|
@@ -0,0 +1,4426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Statement Analysis
|
|
3
|
+
|
|
4
|
+
Statement analysis including variable declarations, assignments, and control flow.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from ..typesys import (
|
|
11
|
+
TpyType, IntLiteralType, FloatLiteralType, OwnType, ReadonlyType,
|
|
12
|
+
FinalType,
|
|
13
|
+
PendingListType, PendingDictType, make_list, PendingSetType, PendingStrType, PendingBytesType, PendingViewType, NominalType, TypeParamRef,
|
|
14
|
+
ListLiteralInfo, DictLiteralInfo, SetLiteralInfo, ViewVarInfo, PtrType, is_readonly_ptr, NoneType, OptionalType, AnyType, UnionType, UnknownElementType,
|
|
15
|
+
unwrap_readonly, unwrap_own, unwrap_qualifiers, is_any_str_type, is_any_bytes_type, TupleType, own_tuple_target,
|
|
16
|
+
LiteralType,
|
|
17
|
+
ViewTypeFamily, view_family_for_type,
|
|
18
|
+
PendingGenericInstanceType, contains_fn_type,
|
|
19
|
+
INT32, VOID, BIGINT, FLOAT, STRVIEW, BYTES, BYTESVIEW, BOOL, is_protocol_type, is_protocol_union, final_type_str_to_strview,
|
|
20
|
+
is_final_allowed_inner, FINAL_INNER_TYPE_ERROR,
|
|
21
|
+
qualify_exception_name, is_return_exception, is_exception_type, error_return_matches,
|
|
22
|
+
FunctionInfo, ParamInfo, RecordInfo,
|
|
23
|
+
make_ref, unwrap_ref_type, RefType, param_has_mutable_borrow_surface,
|
|
24
|
+
is_integer_type, is_any_int_type, is_numeric_type, is_readonly_span,
|
|
25
|
+
is_float_type, is_any_float_type, is_polymorphic_subclass_fact,
|
|
26
|
+
resolve_int_literals,
|
|
27
|
+
bare_name)
|
|
28
|
+
from ..parse import (
|
|
29
|
+
TpyExpr,
|
|
30
|
+
TpyStmt, TpyVarDecl, TpyTupleUnpack, TpyAssign, TpyAugAssign, TpyDelItem, TpyDelVar, TpyDelAttr, TpyExprStmt, TpyReturn, TpyYield,
|
|
31
|
+
TpyIf, TpyWhile, TpyForEach, TpyBreak, TpyContinue, TpyAssert,
|
|
32
|
+
TpyRaise, TpyExceptHandler, TpyTry, TpyWith,
|
|
33
|
+
TpyGlobal, TpyNonlocal, TpyNestedDef,
|
|
34
|
+
TpyCall, TpyMethodCall, TpyArrayLiteral, TpyListComprehension, TpyCoerce,
|
|
35
|
+
TpySubscript, TpySlice, TpyStrLiteral, TpyName, TpyTupleLiteral,
|
|
36
|
+
TpyIntLiteral, TpyFloatLiteral, TpyBoolLiteral, TpyUnaryOp,
|
|
37
|
+
TpyNoneLiteral,
|
|
38
|
+
TpyFieldAccess, TpyFunction, TupleElemCapture,
|
|
39
|
+
TpyMatch, TpyBinOp, TpyIfExpr,
|
|
40
|
+
)
|
|
41
|
+
from ..coercions import CoercionContext
|
|
42
|
+
from ..namespace import BindingKind
|
|
43
|
+
from ..symbol_binding import SymbolKind, lookup_imported
|
|
44
|
+
from ..prescan import ScanResult, scan_reassigned_vars, parse_deref_view_key
|
|
45
|
+
from ..liveness import analyze_last_uses
|
|
46
|
+
from ..parse.nodes import VarLinkage
|
|
47
|
+
from .context import addr_taken_roots, expr_yields_non_null_ptr
|
|
48
|
+
from ..diagnostics import SemanticError, NOCOPY_REMEDIATION_HINT
|
|
49
|
+
from .match import MatchAnalyzer
|
|
50
|
+
from .narrowing import NarrowingTracker
|
|
51
|
+
from .scope_tracker import ScopeTracker
|
|
52
|
+
from .init_tracker import InitTracker
|
|
53
|
+
from .value_range import ValueRange
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from .context import SemanticContext, BorrowTracker
|
|
56
|
+
from .type_ops import TypeOperations
|
|
57
|
+
from .compatibility import TypeCompatibility
|
|
58
|
+
from .local_deduction import LocalTypeDeduction
|
|
59
|
+
from .list_literals import IterableHelper
|
|
60
|
+
from .expressions import ExpressionAnalyzer
|
|
61
|
+
from .protocols import ProtocolChecker
|
|
62
|
+
|
|
63
|
+
from .context import BorrowKind, MODULE_INIT_CONTEXT, PENDING_CONTAINER_TYPES, _storage_key, _storage_root, _borrow_storage_root
|
|
64
|
+
from .expressions import _collect_body_name_refs, _collect_body_local_defs
|
|
65
|
+
from .local_deduction import collect_pending_source_types
|
|
66
|
+
from tpyc import modules as builtin_modules
|
|
67
|
+
from tpyc import qnames
|
|
68
|
+
from ..type_def_registry import (
|
|
69
|
+
is_dict, is_array, is_span, is_list,
|
|
70
|
+
is_char_type, is_str_type, is_string_type, is_str_view_type,
|
|
71
|
+
is_bytes_type, is_bytearray_type, is_bytes_view_type,
|
|
72
|
+
is_borrowing_view_type,
|
|
73
|
+
is_fixed_int_type, is_big_int_type,
|
|
74
|
+
find_factory_by_simple_name, protocol_info_of,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _needs_provenance_tracking(t: TpyType) -> bool:
|
|
79
|
+
"""Whether a local of this type participates in param-provenance / trusted-call tracking.
|
|
80
|
+
|
|
81
|
+
Non-value types are reference types and always track provenance.
|
|
82
|
+
PtrType is a value type but carries an address. Borrowing views
|
|
83
|
+
(StrView / BytesView / Span / SpanIter) are value types with an
|
|
84
|
+
interior pointer into their source storage. PendingViewType covers
|
|
85
|
+
unresolved str/bytes locals -- if they settle into view semantics
|
|
86
|
+
the tracking is required; if they settle into owned (String/bytes)
|
|
87
|
+
the extra tracking is harmless.
|
|
88
|
+
"""
|
|
89
|
+
return (not t.is_value_type()
|
|
90
|
+
or isinstance(t, PtrType)
|
|
91
|
+
or is_borrowing_view_type(t)
|
|
92
|
+
or isinstance(t, PendingViewType))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_dangling_temporary_arg(expr: TpyExpr) -> bool:
|
|
96
|
+
"""Check if an expression is a temporary whose storage won't survive.
|
|
97
|
+
|
|
98
|
+
Function calls and binary ops produce temporaries destroyed at
|
|
99
|
+
end-of-statement. Literals (string, int, array, etc.) are either
|
|
100
|
+
static or materialized by codegen into named locals. Names and field
|
|
101
|
+
accesses have addressable storage.
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(expr, TpyCoerce):
|
|
104
|
+
return _is_dangling_temporary_arg(expr.expr)
|
|
105
|
+
if isinstance(expr, (TpyCall, TpyMethodCall, TpyBinOp)):
|
|
106
|
+
return True
|
|
107
|
+
if isinstance(expr, TpyIfExpr):
|
|
108
|
+
return (_is_dangling_temporary_arg(expr.then_expr)
|
|
109
|
+
or _is_dangling_temporary_arg(expr.else_expr))
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _register_call_result_borrow(ctx: SemanticContext, borrower: str, expr: TpyExpr) -> None:
|
|
114
|
+
"""Register borrow from function call return value (8b).
|
|
115
|
+
|
|
116
|
+
When a function has return_borrows_from facts, the result variable
|
|
117
|
+
borrows from the indicated argument(s). None means unanalyzed -- skip.
|
|
118
|
+
"""
|
|
119
|
+
if isinstance(expr, TpyCall):
|
|
120
|
+
fi = expr.resolved_function_info
|
|
121
|
+
args = expr.args
|
|
122
|
+
obj = None
|
|
123
|
+
elif isinstance(expr, TpyMethodCall):
|
|
124
|
+
fi = expr.resolved_function_info
|
|
125
|
+
args = expr.args
|
|
126
|
+
obj = expr.obj
|
|
127
|
+
elif isinstance(expr, TpyFieldAccess) and expr.property_getter_call is not None:
|
|
128
|
+
# Property getter is a method call; use its return_borrows_from facts
|
|
129
|
+
fi = expr.property_getter_call.resolved_function_info
|
|
130
|
+
args = expr.property_getter_call.args
|
|
131
|
+
obj = expr.property_getter_call.obj
|
|
132
|
+
else:
|
|
133
|
+
return
|
|
134
|
+
if fi is None or fi.return_borrows_from is None:
|
|
135
|
+
return
|
|
136
|
+
bt = ctx.func.borrow_tracker
|
|
137
|
+
for idx in fi.return_borrows_from:
|
|
138
|
+
if idx == -1 and obj is not None:
|
|
139
|
+
root = _borrow_storage_root(obj)
|
|
140
|
+
if root is not None:
|
|
141
|
+
bt.add_borrow(root, borrower, BorrowKind.ELEMENT)
|
|
142
|
+
elif _is_dangling_temporary_arg(obj):
|
|
143
|
+
ctx.warning(
|
|
144
|
+
f"Result borrows from temporary receiver object; "
|
|
145
|
+
f"the temporary is destroyed at end-of-statement",
|
|
146
|
+
expr,
|
|
147
|
+
)
|
|
148
|
+
elif idx >= 0 and idx < len(args):
|
|
149
|
+
root = _borrow_storage_root(args[idx])
|
|
150
|
+
if root is not None:
|
|
151
|
+
bt.add_borrow(root, borrower, BorrowKind.ELEMENT)
|
|
152
|
+
elif _is_dangling_temporary_arg(args[idx]):
|
|
153
|
+
ctx.warning(
|
|
154
|
+
f"Result borrows from temporary argument '{fi.params[idx].name}'; "
|
|
155
|
+
f"the temporary is destroyed at end-of-statement",
|
|
156
|
+
expr,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _format_aug_target(target: TpyExpr) -> str:
|
|
162
|
+
"""Format an aug-assign target as a short label for error messages.
|
|
163
|
+
|
|
164
|
+
e.g. items[0] -> 'items[...]', obj.x -> 'obj.x', otherwise 'target'.
|
|
165
|
+
"""
|
|
166
|
+
if isinstance(target, TpyFieldAccess):
|
|
167
|
+
obj = target.obj.name if isinstance(target.obj, TpyName) else "..."
|
|
168
|
+
return f"'{obj}.{target.field}'"
|
|
169
|
+
if isinstance(target, TpySubscript):
|
|
170
|
+
obj = target.obj.name if isinstance(target.obj, TpyName) else "..."
|
|
171
|
+
return f"'{obj}[...]'"
|
|
172
|
+
return "target"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _root_name_of_expr(expr: TpyExpr) -> str | None:
|
|
176
|
+
"""Extract the root TpyName from a chain of field/subscript accesses.
|
|
177
|
+
|
|
178
|
+
e.g. p.inner.v -> "p", c.items[0] -> "c", x -> "x".
|
|
179
|
+
Returns None for non-name roots (calls, literals, etc.).
|
|
180
|
+
|
|
181
|
+
Unbound-self field access (BaseN.field, set by sema) reports "self"
|
|
182
|
+
since the implicit receiver is `this` -- the syntactic root name is
|
|
183
|
+
the ancestor class, but the mutation travels through self.
|
|
184
|
+
"""
|
|
185
|
+
while isinstance(expr, (TpyFieldAccess, TpySubscript)):
|
|
186
|
+
if (isinstance(expr, TpyFieldAccess)
|
|
187
|
+
and expr.unbound_self_parent_type is not None):
|
|
188
|
+
return "self"
|
|
189
|
+
expr = expr.obj
|
|
190
|
+
return expr.name if isinstance(expr, TpyName) else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _local_traces_to_self(borrow_tracker: 'BorrowTracker', name: str) -> bool:
|
|
194
|
+
"""True when a local borrow-traces to self or a self.<field> storage key.
|
|
195
|
+
|
|
196
|
+
Used to recognize a method call receiver whose root is a local alias of
|
|
197
|
+
self-owned storage (e.g. ``frame = self.frame; frame.get().cancel()``)
|
|
198
|
+
so the call's self-mutation flows back through the enclosing method.
|
|
199
|
+
"""
|
|
200
|
+
ultimate = borrow_tracker.effective_storage_through_borrows(name)
|
|
201
|
+
return ultimate == "self" or ultimate.startswith("self.")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _is_self_call_deferred(
|
|
205
|
+
expr_obj: TpyExpr, obj_root: str | None,
|
|
206
|
+
loop_var_iterable: dict[str, str],
|
|
207
|
+
borrow_tracker: 'BorrowTracker',
|
|
208
|
+
) -> bool:
|
|
209
|
+
"""Check if a method call receiver traces to self through field accesses or loop vars.
|
|
210
|
+
|
|
211
|
+
When True, self-mutation is deferred to Phase 2 via call edges
|
|
212
|
+
(receiver_is_self=True) instead of being marked directly in Phase 1.
|
|
213
|
+
This enables readonly inference for methods that call non-mutating
|
|
214
|
+
methods on fields or loop elements.
|
|
215
|
+
"""
|
|
216
|
+
if obj_root == "self":
|
|
217
|
+
# Verify the chain is purely field accesses (no subscripts like
|
|
218
|
+
# self.items[0].method()). _root_name_of_expr strips both FieldAccess
|
|
219
|
+
# and Subscript, so obj_root=="self" doesn't rule out subscripts.
|
|
220
|
+
# Subscript-rooted calls are not deferred because the call edge in
|
|
221
|
+
# calls.py also only walks TpyFieldAccess.
|
|
222
|
+
chain = expr_obj
|
|
223
|
+
while isinstance(chain, TpyFieldAccess):
|
|
224
|
+
chain = chain.obj
|
|
225
|
+
return isinstance(chain, TpyName) and chain.name == "self"
|
|
226
|
+
if obj_root is not None:
|
|
227
|
+
# loop_var.method() where loop_var iterates over self.field
|
|
228
|
+
iterable = loop_var_iterable.get(obj_root)
|
|
229
|
+
if iterable is not None and _storage_root(iterable) == "self":
|
|
230
|
+
return True
|
|
231
|
+
# local.method() where `local = self.<field>` registered a FIELD borrow.
|
|
232
|
+
# Without this, the call is treated as a regular non-self call and the
|
|
233
|
+
# callee's self-mutation never propagates back to the enclosing method,
|
|
234
|
+
# so non-const methods reached through a local alias of a self field
|
|
235
|
+
# leave self_mutated unset and auto-readonly wrongly marks the method
|
|
236
|
+
# const. Deferring lets Phase 2 propagate precisely.
|
|
237
|
+
if _local_traces_to_self(borrow_tracker, obj_root):
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# view_family_for_type is in typesys (alongside ViewTypeFamily / VIEW_TYPE_FAMILIES).
|
|
243
|
+
# Re-imported at the top of this module.
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _handle_pinned_view_rebind(ctx: SemanticContext, name: str, stmt: TpyStmt) -> None:
|
|
247
|
+
"""Handle the two pinned-view tracker effects of rebinding ``name``.
|
|
248
|
+
|
|
249
|
+
1. If ``name`` is a source with pinned-view borrowers, those views dangle
|
|
250
|
+
(str/bytes is value-type storage with no slot/pointer indirection) --
|
|
251
|
+
warn for each and drop the entry. ``sorted`` for deterministic order.
|
|
252
|
+
2. If ``name`` is itself a pinned view, remove it from its source's set
|
|
253
|
+
so the source no longer warns about it on subsequent rebinds.
|
|
254
|
+
"""
|
|
255
|
+
aliases = ctx.func.pinned_view_aliases
|
|
256
|
+
if not aliases:
|
|
257
|
+
return
|
|
258
|
+
pinned_views = aliases.pop(name, None)
|
|
259
|
+
if pinned_views:
|
|
260
|
+
for view_name in sorted(pinned_views):
|
|
261
|
+
ctx.warning(
|
|
262
|
+
f"Mutation of '{name}' while borrowed"
|
|
263
|
+
f" (reassignment invalidates view '{view_name}')",
|
|
264
|
+
stmt
|
|
265
|
+
)
|
|
266
|
+
for source in list(aliases):
|
|
267
|
+
views = aliases[source]
|
|
268
|
+
if name in views:
|
|
269
|
+
views.discard(name)
|
|
270
|
+
if not views:
|
|
271
|
+
del aliases[source]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class StatementAnalyzer:
|
|
275
|
+
"""Statement analysis."""
|
|
276
|
+
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
ctx: SemanticContext,
|
|
280
|
+
type_ops: TypeOperations,
|
|
281
|
+
compat: TypeCompatibility,
|
|
282
|
+
deduction: LocalTypeDeduction,
|
|
283
|
+
iterable: IterableHelper,
|
|
284
|
+
protocols: ProtocolChecker,
|
|
285
|
+
narrowing: NarrowingTracker,
|
|
286
|
+
expr: ExpressionAnalyzer,
|
|
287
|
+
):
|
|
288
|
+
self.ctx = ctx
|
|
289
|
+
self.type_ops = type_ops
|
|
290
|
+
self.compat = compat
|
|
291
|
+
self.deduction = deduction
|
|
292
|
+
self.iterable = iterable
|
|
293
|
+
self.protocols = protocols
|
|
294
|
+
self.narrowing = narrowing
|
|
295
|
+
self.scopes = ScopeTracker(ctx, compat)
|
|
296
|
+
self.init = InitTracker(ctx)
|
|
297
|
+
self.expr = expr
|
|
298
|
+
self.match = MatchAnalyzer(ctx, self, expr)
|
|
299
|
+
|
|
300
|
+
def _resolve_obj_storage(self, obj: TpyExpr) -> str | None:
|
|
301
|
+
"""Resolve the borrow-tracker storage key for a mutation target's object.
|
|
302
|
+
|
|
303
|
+
For simple names, resolves aliases via effective_storage().
|
|
304
|
+
For single-level field access (self.items), returns the dotted key directly
|
|
305
|
+
(no alias resolution -- field paths are not aliased in the tracker).
|
|
306
|
+
"""
|
|
307
|
+
if isinstance(obj, TpyName):
|
|
308
|
+
return self.ctx.func.borrow_tracker.effective_storage(obj.name)
|
|
309
|
+
return _storage_key(obj)
|
|
310
|
+
|
|
311
|
+
def _warn_all_caps_without_final(self, name: str, type_hint: str, node: TpyStmt) -> None:
|
|
312
|
+
"""Warn on ALL_CAPS module-level variables without Final annotation."""
|
|
313
|
+
if (not name.startswith("_")
|
|
314
|
+
and name.replace("_", "").isalpha()
|
|
315
|
+
and name == name.upper()
|
|
316
|
+
and len(name) >= 2):
|
|
317
|
+
self.ctx.warning(
|
|
318
|
+
f"ALL_CAPS variable '{name}' without Final annotation; "
|
|
319
|
+
f"use Final[{type_hint}] if this is a constant",
|
|
320
|
+
node
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _check_no_bare_async_call(self, stmt: TpyStmt) -> None:
|
|
324
|
+
"""Sema rule: a call to an async def whose result is dropped or
|
|
325
|
+
bound to a non-consuming local is rejected. The caller must
|
|
326
|
+
consume the coroutine via `await`, `asyncio.run`, `create_task`,
|
|
327
|
+
or pass it as an arg to a function that does.
|
|
328
|
+
|
|
329
|
+
Rejected (caught at TpyStmt analysis time):
|
|
330
|
+
c = f() (TpyVarDecl / TpyAssign whose RHS is a bare async call)
|
|
331
|
+
f() (TpyExprStmt whose expr is a bare async call)
|
|
332
|
+
return f() (TpyReturn whose value is a bare async call)
|
|
333
|
+
Allowed:
|
|
334
|
+
await f() -- TpyAwait, not TpyCall
|
|
335
|
+
x = await f() -- RHS is TpyAwait
|
|
336
|
+
asyncio.run(f()) -- f() is an arg of another call
|
|
337
|
+
asyncio.create_task(f())
|
|
338
|
+
"""
|
|
339
|
+
target_expr: TpyExpr | None = None
|
|
340
|
+
if isinstance(stmt, TpyVarDecl):
|
|
341
|
+
target_expr = stmt.init
|
|
342
|
+
elif isinstance(stmt, TpyAssign):
|
|
343
|
+
target_expr = stmt.value
|
|
344
|
+
elif isinstance(stmt, TpyExprStmt):
|
|
345
|
+
target_expr = stmt.expr
|
|
346
|
+
elif isinstance(stmt, TpyReturn):
|
|
347
|
+
target_expr = stmt.value
|
|
348
|
+
if not isinstance(target_expr, TpyCall):
|
|
349
|
+
return
|
|
350
|
+
func_name = target_expr.maybe_func_name
|
|
351
|
+
if not func_name:
|
|
352
|
+
return
|
|
353
|
+
overloads = self.ctx.registry.get_function(func_name)
|
|
354
|
+
if not any(getattr(fi, "is_async", False) for fi in (overloads or ())):
|
|
355
|
+
return
|
|
356
|
+
raise self.ctx.error(
|
|
357
|
+
f"Coroutine value from async def '{func_name}' must be consumed: "
|
|
358
|
+
f"use `await {func_name}(...)` inside an async def, or "
|
|
359
|
+
f"`asyncio.run({func_name}(...))` at top level. "
|
|
360
|
+
f"Coroutines are single-use, must-use values.",
|
|
361
|
+
stmt)
|
|
362
|
+
|
|
363
|
+
def _warn_unnecessary_return_copy(self, value: TpyExpr) -> None:
|
|
364
|
+
"""Warn when return copy(x) is used but x is at last use (auto-move suffices)."""
|
|
365
|
+
if not (isinstance(value, TpyCall) and len(value.args) == 1
|
|
366
|
+
and value.resolved_function_info
|
|
367
|
+
and value.resolved_function_info.qualified_name == "tpy.copy"):
|
|
368
|
+
return
|
|
369
|
+
inner = value.args[0]
|
|
370
|
+
if (isinstance(inner, TpyName)
|
|
371
|
+
and id(inner) in self.ctx.all_last_uses
|
|
372
|
+
and self.compat._is_owned_var(inner.name)):
|
|
373
|
+
self.ctx.warning(
|
|
374
|
+
f"unnecessary copy() -- '{inner.name}' is at its last use and would be moved automatically",
|
|
375
|
+
value,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def _warn_str_field_return_copy(self, value: TpyExpr, expected: TpyType) -> None:
|
|
379
|
+
"""Warn when a method returns self.field where field is str.
|
|
380
|
+
|
|
381
|
+
This copies the string; suggest StrView (zero-copy view) or String
|
|
382
|
+
(explicit owned) so the user makes an intentional choice.
|
|
383
|
+
Suppressed for dunder methods (__str__, __repr__).
|
|
384
|
+
"""
|
|
385
|
+
if not is_str_type(expected):
|
|
386
|
+
return
|
|
387
|
+
func = self.ctx.func.current_function
|
|
388
|
+
if func is None or not func.is_method:
|
|
389
|
+
return
|
|
390
|
+
# Dunder methods (__str__, __repr__) have a fixed str contract
|
|
391
|
+
if func.name.startswith("__") and func.name.endswith("__"):
|
|
392
|
+
return
|
|
393
|
+
inner = value
|
|
394
|
+
while isinstance(inner, TpyCoerce):
|
|
395
|
+
inner = inner.expr
|
|
396
|
+
if not (isinstance(inner, TpyFieldAccess)
|
|
397
|
+
and isinstance(inner.obj, TpyName)
|
|
398
|
+
and inner.obj.name == "self"):
|
|
399
|
+
return
|
|
400
|
+
field_name = inner.field
|
|
401
|
+
self.ctx.warning(
|
|
402
|
+
f"returns a copy of str field 'self.{field_name}'; "
|
|
403
|
+
f"use -> StrView for zero-copy access or -> String to silence this warning",
|
|
404
|
+
value,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def _check_own_lvalue_return(self, own_type: OwnType, expr: TpyExpr, context: str) -> None:
|
|
408
|
+
"""Check that an lvalue returned as Own[T] has explicit copy() or is auto-moved."""
|
|
409
|
+
self.compat.check_own_lvalue_into_own(own_type, expr, context, action="return")
|
|
410
|
+
|
|
411
|
+
def _is_in_constructor(self) -> bool:
|
|
412
|
+
"""Check if currently analyzing an __init__ method body."""
|
|
413
|
+
func = self.ctx.func.current_function
|
|
414
|
+
return (func is not None
|
|
415
|
+
and getattr(func, 'name', None) == "__init__"
|
|
416
|
+
and getattr(func, 'is_method', False))
|
|
417
|
+
|
|
418
|
+
def _annotate_tuple_elem_capture(
|
|
419
|
+
self, literal: TpyTupleLiteral, tuple_type: TupleType,
|
|
420
|
+
*, is_return: bool = False, is_field: bool = False
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Annotate each element of a tuple literal with its capture mode.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
literal: The tuple literal AST node to annotate.
|
|
426
|
+
tuple_type: The resolved TupleType for the literal.
|
|
427
|
+
is_return: True if this literal is in a return statement.
|
|
428
|
+
is_field: True if this literal is assigned to a class field.
|
|
429
|
+
"""
|
|
430
|
+
V = TupleElemCapture.VALUE
|
|
431
|
+
R = TupleElemCapture.REF
|
|
432
|
+
CR = TupleElemCapture.CONST_REF
|
|
433
|
+
|
|
434
|
+
is_readonly = (self.ctx.func.current_function is not None
|
|
435
|
+
and getattr(self.ctx.func.current_function, 'is_readonly', False))
|
|
436
|
+
|
|
437
|
+
literal.elem_capture = []
|
|
438
|
+
for i, et in enumerate(tuple_type.element_types):
|
|
439
|
+
if i >= len(literal.elements):
|
|
440
|
+
literal.elem_capture.append(V)
|
|
441
|
+
continue
|
|
442
|
+
elem = literal.elements[i]
|
|
443
|
+
|
|
444
|
+
# Value types, Own[T], and TypeParamRef are always VALUE
|
|
445
|
+
if et.is_value_type() or isinstance(et, (OwnType, TypeParamRef)):
|
|
446
|
+
literal.elem_capture.append(V)
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
# Field context: all reference-type elements are owned (VALUE)
|
|
450
|
+
# Warn if not an explicit copy() -- same as scalar field assignment
|
|
451
|
+
if is_field:
|
|
452
|
+
elem_val_type = self.ctx.get_raw_expr_type(elem)
|
|
453
|
+
# Last-use of an owned local moves into the slot (matches
|
|
454
|
+
# scalar `self.f = local` auto-move) -- no copy, no warning.
|
|
455
|
+
is_auto_moved = (isinstance(elem, TpyName)
|
|
456
|
+
and id(elem) in self.ctx.all_last_uses
|
|
457
|
+
and self.compat._is_owned_var(elem.name))
|
|
458
|
+
should_warn = False
|
|
459
|
+
if elem_val_type is not None and isinstance(elem_val_type, (RefType, OwnType)):
|
|
460
|
+
if not self.compat.is_copy_call(elem) and not is_auto_moved:
|
|
461
|
+
should_warn = isinstance(elem_val_type, RefType) or isinstance(elem, TpyName)
|
|
462
|
+
elif self._is_non_owned_var_copy(elem, et):
|
|
463
|
+
should_warn = True
|
|
464
|
+
else:
|
|
465
|
+
elem_stripped = self.ctx.get_expr_type(elem)
|
|
466
|
+
if not self.compat.is_copy_call(elem):
|
|
467
|
+
if (isinstance(elem_stripped, OptionalType) and not elem_stripped.inner.is_value_type()):
|
|
468
|
+
should_warn = True
|
|
469
|
+
elif (isinstance(elem_stripped, UnionType) and elem_stripped.uses_pointer_repr()
|
|
470
|
+
and not elem_stripped.needs_wrapper()):
|
|
471
|
+
should_warn = True
|
|
472
|
+
if should_warn:
|
|
473
|
+
if self.ctx.is_type_non_copyable(et):
|
|
474
|
+
raise self.ctx.error(
|
|
475
|
+
f"cannot copy non-copyable type '{et}' into field "
|
|
476
|
+
f"(tuple element {i}){NOCOPY_REMEDIATION_HINT}",
|
|
477
|
+
elem
|
|
478
|
+
)
|
|
479
|
+
self.ctx.warning(
|
|
480
|
+
f"copies {et} into field (tuple element {i}); "
|
|
481
|
+
f"use copy() to make this explicit",
|
|
482
|
+
elem
|
|
483
|
+
)
|
|
484
|
+
literal.elem_capture.append(V)
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
# Return context
|
|
488
|
+
if is_return:
|
|
489
|
+
if self.compat.is_dangling_return(elem):
|
|
490
|
+
# Will error separately in check_dangling_reference
|
|
491
|
+
literal.elem_capture.append(V)
|
|
492
|
+
elif self.compat.is_const_ref_source(elem):
|
|
493
|
+
if is_readonly:
|
|
494
|
+
literal.elem_capture.append(CR)
|
|
495
|
+
else:
|
|
496
|
+
raise self.ctx.error(
|
|
497
|
+
f"Cannot return readonly source as tuple element {i}. "
|
|
498
|
+
f"Type '{et}' would be returned by mutable reference, "
|
|
499
|
+
f"but the source is readonly. "
|
|
500
|
+
f"Use Own[{et}] with copy() to return by value.",
|
|
501
|
+
elem
|
|
502
|
+
)
|
|
503
|
+
elif is_readonly:
|
|
504
|
+
literal.elem_capture.append(CR)
|
|
505
|
+
else:
|
|
506
|
+
literal.elem_capture.append(R)
|
|
507
|
+
# Returning a non-value element by reference takes its address.
|
|
508
|
+
# Mark source params as needing T& (not const T&).
|
|
509
|
+
for tup_root in addr_taken_roots(elem):
|
|
510
|
+
self.ctx.mark_param_mutated(tup_root)
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
# Local context: is_const_ref_source handles ReadonlyType
|
|
514
|
+
# (including constructor params which are typed as ReadonlyType)
|
|
515
|
+
# Pointer-form Optional: force REF for all sources (including
|
|
516
|
+
# non-lvalue None) so the tuple has a uniform slot shape. A
|
|
517
|
+
# mixed `(t, None)` would otherwise yield std::tuple<T*,
|
|
518
|
+
# std::optional<T>> and not match a uniformly pointer-form
|
|
519
|
+
# downstream type.
|
|
520
|
+
if isinstance(et, OptionalType) and et.uses_pointer_repr():
|
|
521
|
+
if (not isinstance(elem, TpyNoneLiteral)
|
|
522
|
+
and not self.compat.is_lvalue(elem)
|
|
523
|
+
and self.ctx.is_type_non_copyable(et.inner)):
|
|
524
|
+
raise self.ctx.error(
|
|
525
|
+
f"cannot bind tuple element {i} of non-copyable type "
|
|
526
|
+
f"'{et.inner}' from rvalue. Bind to a local first, or "
|
|
527
|
+
f"place the literal directly at its consumer"
|
|
528
|
+
f"{NOCOPY_REMEDIATION_HINT}",
|
|
529
|
+
elem,
|
|
530
|
+
)
|
|
531
|
+
if is_readonly or self.compat.is_const_ref_source(elem):
|
|
532
|
+
literal.elem_capture.append(CR)
|
|
533
|
+
else:
|
|
534
|
+
literal.elem_capture.append(R)
|
|
535
|
+
continue
|
|
536
|
+
if not self.compat.is_lvalue(elem):
|
|
537
|
+
literal.elem_capture.append(V)
|
|
538
|
+
else:
|
|
539
|
+
# A ref-tuple of a non-copyable element (`tuple<T&, ...>`) has
|
|
540
|
+
# no path to a value tuple later -- the C++ conversion fails
|
|
541
|
+
# with cryptic <tuple> template errors. Reject with a clean
|
|
542
|
+
# diagnostic; users either annotate the local for ownership
|
|
543
|
+
# transfer or place the literal directly at its consumer.
|
|
544
|
+
if self.ctx.is_type_non_copyable(et):
|
|
545
|
+
raise self.ctx.error(
|
|
546
|
+
f"cannot bind tuple element {i} of non-copyable type "
|
|
547
|
+
f"'{et}' by reference. Annotate the local as "
|
|
548
|
+
f"'tuple[Own[{et}], ...]' to consume the source, or "
|
|
549
|
+
f"place the literal directly at its consumer (return, "
|
|
550
|
+
f"call, outer literal) without an intermediate "
|
|
551
|
+
f"variable{NOCOPY_REMEDIATION_HINT}",
|
|
552
|
+
elem,
|
|
553
|
+
)
|
|
554
|
+
if self.compat.is_const_ref_source(elem):
|
|
555
|
+
literal.elem_capture.append(CR)
|
|
556
|
+
else:
|
|
557
|
+
literal.elem_capture.append(R)
|
|
558
|
+
|
|
559
|
+
def _save_ns_var_types(self) -> dict[str, TpyType]:
|
|
560
|
+
"""Save namespace variable types for later restoration."""
|
|
561
|
+
result: dict[str, TpyType] = {}
|
|
562
|
+
if self.ctx.func.current_ns:
|
|
563
|
+
for name, binding in self.ctx.func.current_ns.all_bindings().items():
|
|
564
|
+
if binding.kind == BindingKind.VARIABLE:
|
|
565
|
+
result[name] = binding.type
|
|
566
|
+
return result
|
|
567
|
+
|
|
568
|
+
def _restore_ns_var_types(self, saved: dict[str, TpyType]) -> None:
|
|
569
|
+
"""Restore namespace variable types from a saved snapshot."""
|
|
570
|
+
if self.ctx.func.current_ns:
|
|
571
|
+
for name, typ in saved.items():
|
|
572
|
+
self.ctx.func.current_ns.update_variable_type(name, typ)
|
|
573
|
+
|
|
574
|
+
def _sync_ns_var_type(self, name: str, typ: TpyType) -> None:
|
|
575
|
+
"""Sync a single variable's namespace type to match scope."""
|
|
576
|
+
if self.ctx.func.current_ns:
|
|
577
|
+
self.ctx.func.current_ns.update_variable_type(name, typ)
|
|
578
|
+
|
|
579
|
+
def _sync_promoted_var_types(self, names: set[str] | None = None) -> None:
|
|
580
|
+
"""Sync scope and namespace with var_types after a control-flow restore.
|
|
581
|
+
|
|
582
|
+
After restoring scope/namespace to a pre-block state (if-branch,
|
|
583
|
+
while, for-each), variables whose canonical declaration type was
|
|
584
|
+
widened inside the block (e.g., Int32 promoted to BigInt, or None
|
|
585
|
+
promoted to Optional[T]) need to be re-synced so that subsequent
|
|
586
|
+
analysis sees the correct type.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
names: Variable names to check. If None, checks all tracked
|
|
590
|
+
variable declarations in the current function.
|
|
591
|
+
"""
|
|
592
|
+
items = (
|
|
593
|
+
self.ctx.func.var_decl_by_name.items() if names is None
|
|
594
|
+
else ((n, self.ctx.func.var_decl_by_name[n]) for n in names if n in self.ctx.func.var_decl_by_name)
|
|
595
|
+
)
|
|
596
|
+
for name, var_decl in items:
|
|
597
|
+
canonical = self.ctx.var_types.get(id(var_decl))
|
|
598
|
+
if canonical is None:
|
|
599
|
+
continue
|
|
600
|
+
current = self.ctx.func.current_scope.lookup(name)
|
|
601
|
+
# Preserve ReadonlyType from branch merge: var_types stores
|
|
602
|
+
# unwrapped types, so re-wrap with ReadonlyType if the merge
|
|
603
|
+
# determined this variable should be readonly.
|
|
604
|
+
if isinstance(current, ReadonlyType):
|
|
605
|
+
if unwrap_readonly(current) == canonical:
|
|
606
|
+
continue
|
|
607
|
+
target = ReadonlyType(canonical)
|
|
608
|
+
else:
|
|
609
|
+
target = canonical
|
|
610
|
+
if target != current:
|
|
611
|
+
self.ctx.func.current_scope.define(name, target)
|
|
612
|
+
self._sync_ns_var_type(name, target)
|
|
613
|
+
|
|
614
|
+
def _is_class_name_receiver(self, expr: TpyExpr) -> bool:
|
|
615
|
+
"""True when `expr` is a class-name reference (e.g. `MyClass`), not
|
|
616
|
+
an instance reference (`self`, `obj`, `f()`, ...). Mirrors the
|
|
617
|
+
binding-kind discrimination in `_try_class_constant_access`.
|
|
618
|
+
"""
|
|
619
|
+
if not isinstance(expr, TpyName):
|
|
620
|
+
return False
|
|
621
|
+
ns = self.ctx.func.current_ns
|
|
622
|
+
if ns is None:
|
|
623
|
+
return False
|
|
624
|
+
binding = ns.lookup(expr.name)
|
|
625
|
+
if binding is None:
|
|
626
|
+
return False
|
|
627
|
+
if binding.kind == BindingKind.RECORD:
|
|
628
|
+
return True
|
|
629
|
+
if binding.kind == BindingKind.IMPORTED_NAME:
|
|
630
|
+
import_info = self.ctx.imported_names.get(expr.name)
|
|
631
|
+
if import_info is None:
|
|
632
|
+
return False
|
|
633
|
+
return bool(self.ctx.registry.find_record_by_qname(
|
|
634
|
+
f"{import_info[0]}.{import_info[1]}"))
|
|
635
|
+
return False
|
|
636
|
+
|
|
637
|
+
def _check_class_constant_write(self, target: TpyExpr, stmt: TpyStmt) -> None:
|
|
638
|
+
"""Reject `=` / `+=` on Final class constants; warn on instance-side
|
|
639
|
+
writes to mutable ClassVar (CPython would create an instance attribute
|
|
640
|
+
instead of writing through to class storage -- mypy/pyright already
|
|
641
|
+
flag this; matching them keeps the tooling story consistent).
|
|
642
|
+
"""
|
|
643
|
+
if not isinstance(target, TpyFieldAccess):
|
|
644
|
+
return
|
|
645
|
+
owner = target.class_constant_owner
|
|
646
|
+
if owner is None:
|
|
647
|
+
return
|
|
648
|
+
if owner.is_final_class_constant(target.field):
|
|
649
|
+
raise self.ctx.error(
|
|
650
|
+
f"Cannot reassign Final class constant "
|
|
651
|
+
f"'{owner.name}.{target.field}'",
|
|
652
|
+
stmt,
|
|
653
|
+
)
|
|
654
|
+
if not self._is_class_name_receiver(target.obj):
|
|
655
|
+
self.ctx.warning(
|
|
656
|
+
f"Assigning to ClassVar '{owner.name}.{target.field}' via "
|
|
657
|
+
f"instance writes to class storage in TPy (CPython creates "
|
|
658
|
+
f"an instance attribute); use "
|
|
659
|
+
f"'{owner.name}.{target.field} = ...' for portability",
|
|
660
|
+
stmt,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
def _enforce_readonly_assignment_target(self, target: TpyExpr) -> None:
|
|
664
|
+
"""Reject assignments through readonly references, frozen fields, and readonly field declarations."""
|
|
665
|
+
# ClassVar writes always go to class-scoped `static inline` storage,
|
|
666
|
+
# never to instance fields, so receiver const-ness/freezing is
|
|
667
|
+
# irrelevant -- the write doesn't touch the instance.
|
|
668
|
+
if (isinstance(target, TpyFieldAccess)
|
|
669
|
+
and target.class_constant_owner is not None):
|
|
670
|
+
return
|
|
671
|
+
# BaseN.field = v goes through `this`, but the syntactic receiver
|
|
672
|
+
# (BaseN) has no value type, so the obj_type branch below can't
|
|
673
|
+
# catch it -- gate on the current method's @readonly flag directly.
|
|
674
|
+
if (isinstance(target, TpyFieldAccess)
|
|
675
|
+
and target.unbound_self_parent_type is not None):
|
|
676
|
+
cur = self.ctx.func.current_function
|
|
677
|
+
if isinstance(cur, TpyFunction) and cur.is_readonly:
|
|
678
|
+
raise self.ctx.error("Cannot mutate readonly reference", target)
|
|
679
|
+
if isinstance(target, (TpyFieldAccess, TpySubscript)):
|
|
680
|
+
obj_type = self.ctx.get_expr_type(target.obj)
|
|
681
|
+
if obj_type is not None:
|
|
682
|
+
check_type = obj_type
|
|
683
|
+
if isinstance(check_type, OptionalType):
|
|
684
|
+
check_type = check_type.inner
|
|
685
|
+
if isinstance(check_type, ReadonlyType):
|
|
686
|
+
raise self.ctx.error("Cannot mutate readonly reference", target)
|
|
687
|
+
# isinstance narrowing strips ReadonlyType from the expr type;
|
|
688
|
+
# the scope binding preserves it, so check there.
|
|
689
|
+
if isinstance(target.obj, TpyName) and self.ctx.is_readonly_name(target.obj.name):
|
|
690
|
+
raise self.ctx.error("Cannot mutate readonly reference", target)
|
|
691
|
+
# Frozen dataclass / readonly field: reject assignment except self.field in __init__
|
|
692
|
+
if isinstance(target, TpyFieldAccess):
|
|
693
|
+
actual = unwrap_readonly(check_type)
|
|
694
|
+
if isinstance(actual, NominalType):
|
|
695
|
+
info = self.ctx.registry.get_record(actual.name)
|
|
696
|
+
if info is not None:
|
|
697
|
+
cur = self.ctx.func.current_function
|
|
698
|
+
rec = self.ctx.record_ctx.record
|
|
699
|
+
in_own_init = (
|
|
700
|
+
isinstance(cur, TpyFunction) and cur.name == "__init__"
|
|
701
|
+
and isinstance(target.obj, TpyName) and target.obj.name == "self"
|
|
702
|
+
and rec is not None and rec.name == actual.name
|
|
703
|
+
)
|
|
704
|
+
if info.is_frozen and not in_own_init:
|
|
705
|
+
raise self.ctx.error(
|
|
706
|
+
f"Cannot assign to field '{target.field}' of frozen dataclass '{actual.name}'",
|
|
707
|
+
target,
|
|
708
|
+
)
|
|
709
|
+
# Walk the class hierarchy to find readonly fields
|
|
710
|
+
# (own and inherited). __init__ of the declaring
|
|
711
|
+
# class or any subclass is exempt.
|
|
712
|
+
in_any_init = (
|
|
713
|
+
isinstance(cur, TpyFunction) and cur.name == "__init__"
|
|
714
|
+
and isinstance(target.obj, TpyName) and target.obj.name == "self"
|
|
715
|
+
and rec is not None
|
|
716
|
+
)
|
|
717
|
+
if not in_any_init:
|
|
718
|
+
# Walk self + MRO ancestors for a readonly field declaration.
|
|
719
|
+
records_to_check = [info, *self.ctx.registry.iter_ancestor_records(info)]
|
|
720
|
+
for check in records_to_check:
|
|
721
|
+
for fld in check.fields:
|
|
722
|
+
if fld.name == target.field and isinstance(fld.type, ReadonlyType):
|
|
723
|
+
raise self.ctx.error(
|
|
724
|
+
f"Cannot assign to readonly field '{target.field}'",
|
|
725
|
+
target,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def _find_consuming_iter(self, iterable_type: TpyType) -> FunctionInfo | None:
|
|
729
|
+
"""Find the consuming __iter__ overload for a type, if any.
|
|
730
|
+
|
|
731
|
+
Only matches concrete types (list, user records with auto_own __iter__).
|
|
732
|
+
Skips pending/unresolved types to avoid mismatched codegen.
|
|
733
|
+
"""
|
|
734
|
+
# Only match concrete types, not pending/unresolved
|
|
735
|
+
if isinstance(iterable_type, (PendingListType, PendingGenericInstanceType)):
|
|
736
|
+
return None
|
|
737
|
+
# Value-type elements: moving is identical to copying, so consuming
|
|
738
|
+
# the container's internal structure is pure overhead.
|
|
739
|
+
elem = builtin_modules.get_iterable_element_type(iterable_type, registry=self.ctx.registry)
|
|
740
|
+
if elem is not None and elem.is_value_type():
|
|
741
|
+
return None
|
|
742
|
+
record_info = self.ctx.registry.get_record_for_type(iterable_type)
|
|
743
|
+
if record_info is None:
|
|
744
|
+
return None
|
|
745
|
+
overloads = record_info.get_method_overloads("__iter__")
|
|
746
|
+
for fi in overloads:
|
|
747
|
+
if fi.is_consuming:
|
|
748
|
+
# Build type substitution for generic types
|
|
749
|
+
type_subst = self.type_ops.build_type_substitution(iterable_type)
|
|
750
|
+
if type_subst:
|
|
751
|
+
fi = self.type_ops.substitute_method_type_params(fi, type_subst)
|
|
752
|
+
return fi
|
|
753
|
+
return None
|
|
754
|
+
|
|
755
|
+
def _resolve_enum_iterable(self, stmt: TpyForEach) -> NominalType | None:
|
|
756
|
+
"""Check if for-each iterates over an enum type (e.g. `for c in Color`).
|
|
757
|
+
|
|
758
|
+
Returns the enum NominalType if so, None otherwise.
|
|
759
|
+
"""
|
|
760
|
+
iterable = stmt.iterable
|
|
761
|
+
if not isinstance(iterable, TpyName):
|
|
762
|
+
return None
|
|
763
|
+
binding = self.ctx.func.current_ns.lookup(iterable.name) if self.ctx.func.current_ns else None
|
|
764
|
+
if binding is None:
|
|
765
|
+
return None
|
|
766
|
+
if binding.kind == BindingKind.ENUM and binding.enum_type is not None:
|
|
767
|
+
return binding.enum_type
|
|
768
|
+
if binding.kind == BindingKind.IMPORTED_NAME and binding.import_source:
|
|
769
|
+
src_mod, original_name = binding.import_source
|
|
770
|
+
enum_type = self.ctx.registry.get_enum(original_name)
|
|
771
|
+
if enum_type is not None:
|
|
772
|
+
return enum_type
|
|
773
|
+
return None
|
|
774
|
+
|
|
775
|
+
def analyze_stmt(self, stmt: TpyStmt) -> None:
|
|
776
|
+
"""Analyze a statement."""
|
|
777
|
+
try:
|
|
778
|
+
self._analyze_stmt_dispatch(stmt)
|
|
779
|
+
finally:
|
|
780
|
+
# Flush post-access ptr narrowing queued during expression analysis.
|
|
781
|
+
# See FunctionTrackingState.pending_non_null_ptr_vars for rationale
|
|
782
|
+
# (deferred to statement boundary so within-statement sibling
|
|
783
|
+
# accesses keep their individual null checks).
|
|
784
|
+
pending = self.ctx.func.pending_non_null_ptr_vars
|
|
785
|
+
if pending:
|
|
786
|
+
self.ctx.func.non_null_ptr_vars |= pending
|
|
787
|
+
pending.clear()
|
|
788
|
+
|
|
789
|
+
def _analyze_stmt_dispatch(self, stmt: TpyStmt) -> None:
|
|
790
|
+
# Coroutine[T] single-use: a bare call to an async def whose
|
|
791
|
+
# result is dropped/stored without consumption is a sema error.
|
|
792
|
+
# See docs/ASYNC_DESIGN.md "Coroutine value model". Allowed:
|
|
793
|
+
# await f() (TpyAwait wraps the call)
|
|
794
|
+
# x = await f()
|
|
795
|
+
# asyncio.run(f()), asyncio.create_task(f()), Task wrap (the
|
|
796
|
+
# coro is the arg of another call, which consumes it)
|
|
797
|
+
# Rejected:
|
|
798
|
+
# c = f() (drops the coro into a non-consuming local)
|
|
799
|
+
# f() (statement-level call, no consumption)
|
|
800
|
+
# return f() (returns the coro instead of awaiting)
|
|
801
|
+
self._check_no_bare_async_call(stmt)
|
|
802
|
+
|
|
803
|
+
if isinstance(stmt, TpyVarDecl):
|
|
804
|
+
self._analyze_var_decl(stmt)
|
|
805
|
+
elif isinstance(stmt, TpyTupleUnpack):
|
|
806
|
+
self._analyze_tuple_unpack(stmt)
|
|
807
|
+
elif isinstance(stmt, TpyAssign):
|
|
808
|
+
self._analyze_assign(stmt)
|
|
809
|
+
elif isinstance(stmt, TpyAugAssign):
|
|
810
|
+
self._analyze_aug_assign(stmt)
|
|
811
|
+
elif isinstance(stmt, TpyDelItem):
|
|
812
|
+
self._analyze_del_item(stmt)
|
|
813
|
+
elif isinstance(stmt, TpyDelVar):
|
|
814
|
+
self._analyze_del_var(stmt)
|
|
815
|
+
elif isinstance(stmt, TpyDelAttr):
|
|
816
|
+
self._analyze_del_attr(stmt)
|
|
817
|
+
elif isinstance(stmt, TpyExprStmt):
|
|
818
|
+
self.expr.analyze_expr(stmt.expr)
|
|
819
|
+
elif isinstance(stmt, TpyReturn):
|
|
820
|
+
if stmt.value:
|
|
821
|
+
expected = unwrap_ref_type(self.ctx.func.current_function.return_type) if self.ctx.func.current_function else VOID
|
|
822
|
+
ret_type = self.expr.analyze_expr_with_hint(stmt.value, expected)
|
|
823
|
+
stmt.value_type = ret_type
|
|
824
|
+
stmt.value = self.compat.coerce_expr(stmt.value, ret_type, expected, "return value",
|
|
825
|
+
coercion_ctx=CoercionContext.RETURN, is_return=True)
|
|
826
|
+
# Track return-type context for pending list deduction
|
|
827
|
+
self.deduction.mark_list_return_context(stmt.value, expected)
|
|
828
|
+
# Warn when a method copies a str field on return
|
|
829
|
+
self._warn_str_field_return_copy(stmt.value, expected)
|
|
830
|
+
# Check for lvalue returned as Own[T] without explicit copy()
|
|
831
|
+
if isinstance(expected, OwnType):
|
|
832
|
+
if self.compat.is_copy_call(stmt.value):
|
|
833
|
+
self._warn_unnecessary_return_copy(stmt.value)
|
|
834
|
+
else:
|
|
835
|
+
self._check_own_lvalue_return(expected, stmt.value, "return type")
|
|
836
|
+
# Check Own[T] elements in tuple literals.
|
|
837
|
+
if isinstance(stmt.value, TpyTupleLiteral):
|
|
838
|
+
tuple_target = own_tuple_target(expected)
|
|
839
|
+
if tuple_target is not None:
|
|
840
|
+
for i, et in enumerate(tuple_target.element_types):
|
|
841
|
+
if isinstance(et, OwnType) and i < len(stmt.value.elements):
|
|
842
|
+
self._check_own_lvalue_return(et, stmt.value.elements[i],
|
|
843
|
+
f"tuple element {i}")
|
|
844
|
+
# Annotate per-element capture mode (ref/value/const_ref)
|
|
845
|
+
self._annotate_tuple_elem_capture(
|
|
846
|
+
stmt.value, tuple_target, is_return=True)
|
|
847
|
+
# Check for dangling reference (returning local/temporary as reference)
|
|
848
|
+
self.compat.check_dangling_reference(stmt.value, expected, stmt.loc)
|
|
849
|
+
# Returning a non-value type by reference takes the source's address.
|
|
850
|
+
# Mark both loop vars and params so they keep T& (not const T&/const T*).
|
|
851
|
+
if isinstance(stmt.value, TpyName):
|
|
852
|
+
self.ctx.mark_loop_var_mutated(stmt.value.name)
|
|
853
|
+
# Escape tracking: returning a nested def marks it as escaping
|
|
854
|
+
if stmt.value.name in self.ctx.func.nested_def_names:
|
|
855
|
+
self.ctx.func.nested_def_escapes.add(stmt.value.name)
|
|
856
|
+
# Returning a borrowing view does not give the caller write
|
|
857
|
+
# access to the source, so we record borrow provenance but
|
|
858
|
+
# must not raise self_mutated -- that would suppress auto-const
|
|
859
|
+
# on the enclosing method and break const callers.
|
|
860
|
+
if expected is not None:
|
|
861
|
+
returns_borrowing_view = is_borrowing_view_type(expected)
|
|
862
|
+
if not expected.is_value_type() or returns_borrowing_view:
|
|
863
|
+
for ret_root in addr_taken_roots(stmt.value):
|
|
864
|
+
if not returns_borrowing_view:
|
|
865
|
+
self.ctx.mark_param_mutated(ret_root)
|
|
866
|
+
self.ctx.mark_param_returned(ret_root) # 8b: track which param storage the return borrows
|
|
867
|
+
# 8b rule 3: transitive return -- if returning the result of a call
|
|
868
|
+
# whose return_borrows_from is known, propagate the borrow contract.
|
|
869
|
+
# e.g. `return inner(items)` where inner borrows param 0 -> mark items.
|
|
870
|
+
ret_inner = stmt.value.expr if isinstance(stmt.value, TpyCoerce) else stmt.value
|
|
871
|
+
if isinstance(ret_inner, (TpyCall, TpyMethodCall)):
|
|
872
|
+
fi_ret = ret_inner.resolved_function_info
|
|
873
|
+
if fi_ret is not None and fi_ret.return_borrows_from:
|
|
874
|
+
ret_args = ret_inner.args
|
|
875
|
+
ret_obj = getattr(ret_inner, 'obj', None)
|
|
876
|
+
for idx in fi_ret.return_borrows_from:
|
|
877
|
+
if idx == -1 and ret_obj is not None:
|
|
878
|
+
src = _borrow_storage_root(ret_obj)
|
|
879
|
+
elif idx >= 0 and idx < len(ret_args):
|
|
880
|
+
src = _borrow_storage_root(ret_args[idx])
|
|
881
|
+
else:
|
|
882
|
+
src = None
|
|
883
|
+
if src is not None:
|
|
884
|
+
# Read-only sources (readonly callees, view returns)
|
|
885
|
+
# don't propagate mutation to their borrowed-from arg.
|
|
886
|
+
if not returns_borrowing_view and not fi_ret.is_readonly:
|
|
887
|
+
self.ctx.mark_param_mutated(src)
|
|
888
|
+
self.ctx.mark_param_returned(src)
|
|
889
|
+
self.init.mark_terminated()
|
|
890
|
+
elif isinstance(stmt, TpyYield):
|
|
891
|
+
self._analyze_yield(stmt)
|
|
892
|
+
elif isinstance(stmt, TpyIf):
|
|
893
|
+
assigned_before_cond = frozenset(self.ctx.func.definitely_assigned)
|
|
894
|
+
saved_sc_and = self.ctx.sc_and_walrus.copy()
|
|
895
|
+
saved_sc_or = self.ctx.sc_or_walrus.copy()
|
|
896
|
+
self.ctx.sc_and_walrus = set()
|
|
897
|
+
self.ctx.sc_or_walrus = set()
|
|
898
|
+
self.expr.analyze_expr(stmt.condition)
|
|
899
|
+
always_walrus = self.ctx.func.definitely_assigned - assigned_before_cond
|
|
900
|
+
sc_and = self.ctx.sc_and_walrus # safe in then-body
|
|
901
|
+
sc_or = self.ctx.sc_or_walrus # safe in else-body
|
|
902
|
+
self.ctx.sc_and_walrus = saved_sc_and
|
|
903
|
+
self.ctx.sc_or_walrus = saved_sc_or
|
|
904
|
+
self.narrowing.warn_truthy_value_optionals(stmt.condition)
|
|
905
|
+
then_type_facts, else_type_facts = self.narrowing.condition_type_facts(stmt.condition)
|
|
906
|
+
ptr_nn_then, ptr_nn_else = self.narrowing.condition_ptr_null_facts(stmt.condition)
|
|
907
|
+
range_true, range_false = self.narrowing.condition_range_facts(stmt.condition)
|
|
908
|
+
stmt.then_type_facts = self._filter_union_codegen_facts(then_type_facts)
|
|
909
|
+
stmt.else_type_facts = self._filter_union_codegen_facts(else_type_facts)
|
|
910
|
+
scope_before = set(self.ctx.func.current_scope.bindings.keys())
|
|
911
|
+
assigned_before = frozenset(self.ctx.func.definitely_assigned)
|
|
912
|
+
before = self.init.save()
|
|
913
|
+
consumed_before = self.ctx.func.current_consumed_own_params.copy()
|
|
914
|
+
# Save binding types for ReadonlyType merge after branches
|
|
915
|
+
bindings_before = dict(self.ctx.func.current_scope.bindings)
|
|
916
|
+
ns_types_before = self._save_ns_var_types()
|
|
917
|
+
# Then-body: condition was true -> && operands all evaluated,
|
|
918
|
+
# but || RHS may have been skipped (LHS alone was truthy)
|
|
919
|
+
self.ctx.func.definitely_assigned |= always_walrus | sc_and
|
|
920
|
+
self.ctx.func.narrowed_types.update(then_type_facts)
|
|
921
|
+
self.ctx.func.non_null_ptr_vars |= ptr_nn_then
|
|
922
|
+
self._apply_range_facts(range_true)
|
|
923
|
+
for s in stmt.then_body:
|
|
924
|
+
self.analyze_stmt(s)
|
|
925
|
+
then_state = self.init.save()
|
|
926
|
+
consumed_after_then = self.ctx.func.current_consumed_own_params.copy()
|
|
927
|
+
then_terminated = self.ctx.func.init_terminated
|
|
928
|
+
bindings_after_then = dict(self.ctx.func.current_scope.bindings)
|
|
929
|
+
# Restore bindings for else branch
|
|
930
|
+
self.ctx.func.current_scope.bindings.update(bindings_before)
|
|
931
|
+
self._restore_ns_var_types(ns_types_before)
|
|
932
|
+
self.init.restore(before)
|
|
933
|
+
# Else-body: condition was false -> || operands all evaluated,
|
|
934
|
+
# but && RHS may have been skipped (LHS alone was falsy)
|
|
935
|
+
self.ctx.func.definitely_assigned |= always_walrus | sc_or
|
|
936
|
+
self.ctx.func.current_consumed_own_params = consumed_before.copy()
|
|
937
|
+
self.ctx.func.narrowed_types.update(else_type_facts)
|
|
938
|
+
self.ctx.func.non_null_ptr_vars |= ptr_nn_else
|
|
939
|
+
self._apply_range_facts(range_false)
|
|
940
|
+
for s in stmt.else_body:
|
|
941
|
+
self.analyze_stmt(s)
|
|
942
|
+
else_state = self.init.save()
|
|
943
|
+
consumed_after_else = self.ctx.func.current_consumed_own_params.copy()
|
|
944
|
+
else_terminated = self.ctx.func.init_terminated
|
|
945
|
+
bindings_after_else = dict(self.ctx.func.current_scope.bindings)
|
|
946
|
+
self.init.merge_branches(then_state, else_state)
|
|
947
|
+
# Merge consumed Own[T] params: must be consumed on ALL non-terminated paths
|
|
948
|
+
if then_terminated and else_terminated:
|
|
949
|
+
self.ctx.func.current_consumed_own_params = consumed_after_then | consumed_after_else
|
|
950
|
+
elif then_terminated:
|
|
951
|
+
self.ctx.func.current_consumed_own_params = consumed_after_else
|
|
952
|
+
elif else_terminated:
|
|
953
|
+
self.ctx.func.current_consumed_own_params = consumed_after_then
|
|
954
|
+
else:
|
|
955
|
+
self.ctx.func.current_consumed_own_params = consumed_after_then & consumed_after_else
|
|
956
|
+
# Merge ReadonlyType: if readonly on EITHER branch, keep readonly
|
|
957
|
+
for name in set(bindings_after_then) | set(bindings_after_else):
|
|
958
|
+
then_type = bindings_after_then.get(name)
|
|
959
|
+
else_type = bindings_after_else.get(name)
|
|
960
|
+
if then_type is not None and else_type is not None:
|
|
961
|
+
then_is_ro = isinstance(then_type, ReadonlyType)
|
|
962
|
+
else_is_ro = isinstance(else_type, ReadonlyType)
|
|
963
|
+
if then_is_ro and not else_is_ro:
|
|
964
|
+
merged = ReadonlyType(unwrap_readonly(else_type))
|
|
965
|
+
self.ctx.func.current_scope.define(name, merged)
|
|
966
|
+
self._sync_ns_var_type(name, merged)
|
|
967
|
+
elif else_is_ro and not then_is_ro:
|
|
968
|
+
merged = ReadonlyType(unwrap_readonly(then_type))
|
|
969
|
+
self.ctx.func.current_scope.define(name, merged)
|
|
970
|
+
self._sync_ns_var_type(name, merged)
|
|
971
|
+
# Sync scope/namespace with var_types for variables whose
|
|
972
|
+
# declaration type was promoted inside a branch.
|
|
973
|
+
self._sync_promoted_var_types(
|
|
974
|
+
set(bindings_after_then) | set(bindings_after_else)
|
|
975
|
+
)
|
|
976
|
+
# Detect variables first declared inside branches that need
|
|
977
|
+
# pre-declaration. Skip when both branches terminate (no code
|
|
978
|
+
# after the if needs the variable).
|
|
979
|
+
if not self.ctx.func.init_terminated:
|
|
980
|
+
branch_new = set(self.ctx.func.current_scope.bindings.keys()) - scope_before
|
|
981
|
+
newly_assigned = self.ctx.func.definitely_assigned - assigned_before
|
|
982
|
+
predecl = (branch_new & newly_assigned) - self.ctx.func.global_declarations
|
|
983
|
+
else:
|
|
984
|
+
predecl = set()
|
|
985
|
+
if predecl:
|
|
986
|
+
self.ctx.if_branch_decls[id(stmt)] = {
|
|
987
|
+
name: self.ctx.func.current_scope.lookup(name)
|
|
988
|
+
for name in sorted(predecl)
|
|
989
|
+
}
|
|
990
|
+
elif isinstance(stmt, TpyWhile):
|
|
991
|
+
assigned_before_cond = frozenset(self.ctx.func.definitely_assigned)
|
|
992
|
+
saved_sc_and = self.ctx.sc_and_walrus.copy()
|
|
993
|
+
saved_sc_or = self.ctx.sc_or_walrus.copy()
|
|
994
|
+
self.ctx.sc_and_walrus = set()
|
|
995
|
+
self.ctx.sc_or_walrus = set()
|
|
996
|
+
self.expr.analyze_expr(stmt.condition)
|
|
997
|
+
always_walrus_w = self.ctx.func.definitely_assigned - assigned_before_cond
|
|
998
|
+
all_walrus_w = always_walrus_w | self.ctx.sc_and_walrus | self.ctx.sc_or_walrus
|
|
999
|
+
self.ctx.sc_and_walrus = saved_sc_and
|
|
1000
|
+
self.ctx.sc_or_walrus = saved_sc_or
|
|
1001
|
+
self.narrowing.warn_truthy_value_optionals(stmt.condition)
|
|
1002
|
+
then_type_facts, _ = self.narrowing.condition_type_facts(stmt.condition)
|
|
1003
|
+
ptr_nn, _ = self.narrowing.condition_ptr_null_facts(stmt.condition)
|
|
1004
|
+
range_true, _ = self.narrowing.condition_range_facts(stmt.condition)
|
|
1005
|
+
stmt.then_type_facts = self._filter_union_codegen_facts(then_type_facts)
|
|
1006
|
+
before = self.init.save()
|
|
1007
|
+
consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
|
|
1008
|
+
# Save namespace types -- loop_scope() restores scope bindings
|
|
1009
|
+
# automatically, but namespace mutations inside the loop persist.
|
|
1010
|
+
ns_types_before_while = self._save_ns_var_types()
|
|
1011
|
+
with self.scopes.loop_scope():
|
|
1012
|
+
self.init.apply_loop_entry_facts(
|
|
1013
|
+
before,
|
|
1014
|
+
condition_type_facts=then_type_facts,
|
|
1015
|
+
)
|
|
1016
|
+
# Applied separately from apply_loop_entry_facts because
|
|
1017
|
+
# that method only handles type narrowing, not ptr non-null.
|
|
1018
|
+
self.ctx.func.non_null_ptr_vars |= ptr_nn
|
|
1019
|
+
self._apply_range_facts(range_true)
|
|
1020
|
+
for s in stmt.body:
|
|
1021
|
+
self.analyze_stmt(s)
|
|
1022
|
+
self.init.apply_loop_exit_facts(before)
|
|
1023
|
+
# Re-add all walrus vars (condition always evaluates fully)
|
|
1024
|
+
self.ctx.func.definitely_assigned |= all_walrus_w
|
|
1025
|
+
# Loop might not execute — consumption inside is not definite
|
|
1026
|
+
self.ctx.func.current_consumed_own_params = consumed_before_loop
|
|
1027
|
+
# Restore namespace to pre-loop state (scope was already restored
|
|
1028
|
+
# by loop_scope context manager)
|
|
1029
|
+
self._restore_ns_var_types(ns_types_before_while)
|
|
1030
|
+
self._sync_promoted_var_types()
|
|
1031
|
+
for s in stmt.orelse:
|
|
1032
|
+
self.analyze_stmt(s)
|
|
1033
|
+
elif isinstance(stmt, TpyForEach):
|
|
1034
|
+
if stmt.is_async:
|
|
1035
|
+
self._analyze_async_for(stmt)
|
|
1036
|
+
return
|
|
1037
|
+
# Check for enum iteration: `for c in Color`
|
|
1038
|
+
enum_type = self._resolve_enum_iterable(stmt)
|
|
1039
|
+
if enum_type is not None:
|
|
1040
|
+
stmt.enum_iterable = enum_type
|
|
1041
|
+
elem_type = enum_type
|
|
1042
|
+
self._record_for_loop_var_type(stmt, elem_type)
|
|
1043
|
+
before = self.init.save()
|
|
1044
|
+
consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
|
|
1045
|
+
ns_types_before_foreach = self._save_ns_var_types()
|
|
1046
|
+
with self.scopes.loop_scope() as inner_scope:
|
|
1047
|
+
self.init.apply_loop_entry_facts(before)
|
|
1048
|
+
with self.scopes.loop_var(inner_scope, stmt.var, elem_type, inner_scope.depth, is_foreach=True):
|
|
1049
|
+
for s in stmt.body:
|
|
1050
|
+
self.analyze_stmt(s)
|
|
1051
|
+
self.init.apply_loop_exit_facts(before)
|
|
1052
|
+
self.ctx.func.current_consumed_own_params = consumed_before_loop
|
|
1053
|
+
self._restore_ns_var_types(ns_types_before_foreach)
|
|
1054
|
+
self._sync_promoted_var_types()
|
|
1055
|
+
self._propagate_for_loop_scope(stmt, inner_scope, elem_type)
|
|
1056
|
+
for s in stmt.orelse:
|
|
1057
|
+
self.analyze_stmt(s)
|
|
1058
|
+
else:
|
|
1059
|
+
iterable_type = self.expr.analyze_expr(stmt.iterable)
|
|
1060
|
+
is_readonly_iterable = isinstance(iterable_type, ReadonlyType)
|
|
1061
|
+
inner_iterable_type = unwrap_readonly(unwrap_own(unwrap_ref_type(iterable_type)))
|
|
1062
|
+
# Resolve TypeParamRef to its bound for element type extraction
|
|
1063
|
+
resolved_for_iter = inner_iterable_type
|
|
1064
|
+
if isinstance(inner_iterable_type, TypeParamRef):
|
|
1065
|
+
bound = self.type_ops.get_type_param_bound(inner_iterable_type.name)
|
|
1066
|
+
if bound is not None and is_protocol_type(bound):
|
|
1067
|
+
resolved_for_iter = bound
|
|
1068
|
+
elem_type = self.iterable.get_iterable_element_type(resolved_for_iter, loc=stmt.loc)
|
|
1069
|
+
# Elements from a readonly iterable inherit readonly status
|
|
1070
|
+
if is_readonly_iterable and not elem_type.is_value_type():
|
|
1071
|
+
elem_type = ReadonlyType(unwrap_readonly(elem_type))
|
|
1072
|
+
elem_type = self._infer_new_local_type(
|
|
1073
|
+
stmt.var, elem_type, None, None,
|
|
1074
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
1075
|
+
)
|
|
1076
|
+
stmt.elem_type = make_ref(elem_type)
|
|
1077
|
+
|
|
1078
|
+
# Auto-consuming decision is deferred until after body analysis
|
|
1079
|
+
# (see below) so we know whether the loop var is mutated.
|
|
1080
|
+
|
|
1081
|
+
is_direct_next_iter = builtin_modules.get_error_return_next_element_type(inner_iterable_type, registry=self.ctx.registry) is not None
|
|
1082
|
+
is_iter_based = builtin_modules.get_iter_element_type(inner_iterable_type, registry=self.ctx.registry) is not None
|
|
1083
|
+
is_protocol_iter = is_protocol_type(resolved_for_iter) and resolved_for_iter.qualified_name() in ("typing.Iterator", "typing.Iterable")
|
|
1084
|
+
before = self.init.save()
|
|
1085
|
+
consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
|
|
1086
|
+
ns_types_before_foreach = self._save_ns_var_types()
|
|
1087
|
+
with self.scopes.loop_scope() as inner_scope:
|
|
1088
|
+
self.init.apply_loop_entry_facts(before)
|
|
1089
|
+
# Track range facts for loop variable from range() calls
|
|
1090
|
+
self._track_for_range_facts(stmt)
|
|
1091
|
+
bt = self.ctx.func.borrow_tracker
|
|
1092
|
+
if isinstance(stmt.iterable, TpyName):
|
|
1093
|
+
bt.add_borrow(stmt.iterable.name, "__for_iter", BorrowKind.ITER)
|
|
1094
|
+
self.ctx.func.loop_var_iterable[stmt.var] = stmt.iterable.name
|
|
1095
|
+
# Protocol-typed and TypeParamRef params used as for-loop iterables
|
|
1096
|
+
# require mutable access: .__next__() mutates iterator state.
|
|
1097
|
+
# Mark them mutated so the generated param gets T& not const T&.
|
|
1098
|
+
inner = unwrap_ref_type(unwrap_readonly(iterable_type))
|
|
1099
|
+
if (isinstance(inner, (TypeParamRef,)) or is_protocol_type(inner)):
|
|
1100
|
+
self.ctx.mark_param_mutated(stmt.iterable.name)
|
|
1101
|
+
elif isinstance(stmt.iterable, TpyFieldAccess):
|
|
1102
|
+
# Property getter: register ITER borrow via return_borrows_from
|
|
1103
|
+
# on the receiver object (more precise than the field-path key).
|
|
1104
|
+
gc = stmt.iterable.property_getter_call
|
|
1105
|
+
if gc is not None:
|
|
1106
|
+
fi_gc = gc.resolved_function_info
|
|
1107
|
+
if fi_gc is not None and fi_gc.return_borrows_from:
|
|
1108
|
+
for idx in fi_gc.return_borrows_from:
|
|
1109
|
+
if idx == -1 and gc.obj is not None:
|
|
1110
|
+
src = _borrow_storage_root(gc.obj)
|
|
1111
|
+
if src is not None:
|
|
1112
|
+
bt.add_borrow(src, "__for_iter", BorrowKind.ITER)
|
|
1113
|
+
self.ctx.func.loop_var_iterable[stmt.var] = src
|
|
1114
|
+
elif idx >= 0 and idx < len(gc.args):
|
|
1115
|
+
src = _borrow_storage_root(gc.args[idx])
|
|
1116
|
+
if src is not None:
|
|
1117
|
+
bt.add_borrow(src, "__for_iter", BorrowKind.ITER)
|
|
1118
|
+
self.ctx.func.loop_var_iterable[stmt.var] = src
|
|
1119
|
+
else:
|
|
1120
|
+
key = _storage_key(stmt.iterable)
|
|
1121
|
+
if key is not None:
|
|
1122
|
+
bt.add_borrow(key, "__for_iter", BorrowKind.ITER)
|
|
1123
|
+
self.ctx.func.loop_var_iterable[stmt.var] = key
|
|
1124
|
+
elif isinstance(stmt.iterable, (TpyCall, TpyMethodCall)):
|
|
1125
|
+
# 8b: iterable is a call whose return borrows from source arg(s).
|
|
1126
|
+
# Register ITER borrow directly on those source containers so that
|
|
1127
|
+
# structural mutations during the loop generate conflict warnings.
|
|
1128
|
+
fi_iter = stmt.iterable.resolved_function_info
|
|
1129
|
+
if fi_iter is not None and fi_iter.return_borrows_from:
|
|
1130
|
+
call_args = stmt.iterable.args
|
|
1131
|
+
call_obj = getattr(stmt.iterable, 'obj', None)
|
|
1132
|
+
for idx in fi_iter.return_borrows_from:
|
|
1133
|
+
arg = None
|
|
1134
|
+
if idx == -1 and call_obj is not None:
|
|
1135
|
+
src = _borrow_storage_root(call_obj)
|
|
1136
|
+
arg = call_obj
|
|
1137
|
+
elif idx >= 0 and idx < len(call_args):
|
|
1138
|
+
src = _borrow_storage_root(call_args[idx])
|
|
1139
|
+
arg = call_args[idx]
|
|
1140
|
+
else:
|
|
1141
|
+
src = None
|
|
1142
|
+
if src is not None:
|
|
1143
|
+
bt.add_borrow(src, "__for_iter", BorrowKind.ITER)
|
|
1144
|
+
self.ctx.func.loop_var_iterable[stmt.var] = src
|
|
1145
|
+
elif arg is not None and _is_dangling_temporary_arg(arg):
|
|
1146
|
+
# Call results returning non-value types are
|
|
1147
|
+
# materialized into named variables by codegen
|
|
1148
|
+
# (for by-reference passing), so they survive
|
|
1149
|
+
# the for-loop. Only warn for value-type temporaries
|
|
1150
|
+
# (e.g. str -> string_view conversion) where the
|
|
1151
|
+
# underlying storage is truly destroyed.
|
|
1152
|
+
is_materialized = False
|
|
1153
|
+
if isinstance(arg, (TpyCall, TpyMethodCall)):
|
|
1154
|
+
arg_fi = arg.resolved_function_info
|
|
1155
|
+
if arg_fi is not None and not arg_fi.return_type.is_value_type():
|
|
1156
|
+
is_materialized = True
|
|
1157
|
+
if not is_materialized:
|
|
1158
|
+
if idx == -1:
|
|
1159
|
+
detail = "temporary receiver object"
|
|
1160
|
+
else:
|
|
1161
|
+
detail = f"temporary argument '{fi_iter.params[idx].name}'"
|
|
1162
|
+
self.ctx.warning(
|
|
1163
|
+
f"Iterator borrows from {detail}; "
|
|
1164
|
+
f"the temporary is destroyed before iteration begins",
|
|
1165
|
+
stmt.iterable,
|
|
1166
|
+
)
|
|
1167
|
+
if is_direct_next_iter or is_protocol_iter:
|
|
1168
|
+
iter_depth = inner_scope.depth
|
|
1169
|
+
elif is_iter_based:
|
|
1170
|
+
# __iter__() either references the container's storage
|
|
1171
|
+
# (NativeIterable types -- builtins like list/dict/str
|
|
1172
|
+
# and span-backed types like SpanIter) or creates a
|
|
1173
|
+
# fresh owned iterator (user-defined iterators).
|
|
1174
|
+
# For the former, the loop-var lifetime is the
|
|
1175
|
+
# container's; for the latter, it's loop-body scope.
|
|
1176
|
+
#
|
|
1177
|
+
# Known gap (no failing test): user records whose
|
|
1178
|
+
# `__iter__()` returns `SpanIter` (e.g. ArrayList,
|
|
1179
|
+
# Stack with `__iter__(self) -> SpanIter[T]`) are NOT
|
|
1180
|
+
# `is_native_iterable`, but their iterator does
|
|
1181
|
+
# reference the container's storage. Provenance
|
|
1182
|
+
# tracking only activates for non-value loop vars
|
|
1183
|
+
# from param-derived iterables, so the gap is
|
|
1184
|
+
# currently invisible. If it surfaces, recover by
|
|
1185
|
+
# also checking whether `__iter__()` returns a
|
|
1186
|
+
# `SpanIter` here.
|
|
1187
|
+
references_container = builtin_modules.is_native_iterable(
|
|
1188
|
+
inner_iterable_type, registry=self.ctx.registry
|
|
1189
|
+
)
|
|
1190
|
+
if references_container:
|
|
1191
|
+
if self.compat.is_lvalue(stmt.iterable):
|
|
1192
|
+
iter_depth = self.scopes.get_expr_scope_depth(stmt.iterable)
|
|
1193
|
+
else:
|
|
1194
|
+
iter_depth = inner_scope.depth
|
|
1195
|
+
else:
|
|
1196
|
+
iter_depth = inner_scope.depth
|
|
1197
|
+
elif self.compat.is_lvalue(stmt.iterable):
|
|
1198
|
+
# For-each var references container's storage -- use container's depth.
|
|
1199
|
+
iter_depth = self.scopes.get_expr_scope_depth(stmt.iterable)
|
|
1200
|
+
else:
|
|
1201
|
+
# For rvalue iterables (calls), C++ extends the temporary's lifetime
|
|
1202
|
+
# to the for statement, but it dies when the loop ends. Use body depth
|
|
1203
|
+
# so that escaping to any outer-scoped variable is caught.
|
|
1204
|
+
iter_depth = inner_scope.depth
|
|
1205
|
+
# Track provenance for loop vars whose type participates in
|
|
1206
|
+
# provenance tracking, when iterating over a param-derived iterable.
|
|
1207
|
+
track_loop_prov = (
|
|
1208
|
+
_needs_provenance_tracking(unwrap_readonly(elem_type))
|
|
1209
|
+
and self.compat.is_param_derived_expr(stmt.iterable)
|
|
1210
|
+
)
|
|
1211
|
+
if track_loop_prov:
|
|
1212
|
+
self.init.add_loop_var_provenance(stmt.var)
|
|
1213
|
+
self.ctx.func.mutated_loop_vars.discard(stmt.var)
|
|
1214
|
+
self.ctx.func.consumed_loop_vars.discard(stmt.var)
|
|
1215
|
+
self.ctx.func.deferred_loop_copy_warnings.pop(stmt.var, None)
|
|
1216
|
+
self._record_for_loop_var_type(stmt, elem_type)
|
|
1217
|
+
with self.scopes.loop_var(inner_scope, stmt.var, elem_type, iter_depth, is_foreach=True):
|
|
1218
|
+
for s in stmt.body:
|
|
1219
|
+
self.analyze_stmt(s)
|
|
1220
|
+
if track_loop_prov:
|
|
1221
|
+
self.init.remove_loop_var_provenance(stmt.var)
|
|
1222
|
+
# Set const-ref binding when the loop var was never mutated.
|
|
1223
|
+
# mutated_loop_vars was cleared for stmt.var before entering the
|
|
1224
|
+
# loop body, so it only reflects mutations from this loop.
|
|
1225
|
+
# Only for non-value types or expensive-to-copy value types
|
|
1226
|
+
# (BigInt, String, tuples with expensive elements). Cheap
|
|
1227
|
+
# primitives (int32_t, bool, double, etc.) are better copied
|
|
1228
|
+
# into a register than referenced through a pointer.
|
|
1229
|
+
# For synthetic tuple-unpack loop vars, skip const binding when
|
|
1230
|
+
# the unpack has elements that need mutable references --
|
|
1231
|
+
# const tuple prevents T& bindings via std::get.
|
|
1232
|
+
unwrapped = unwrap_qualifiers(elem_type)
|
|
1233
|
+
worth_const_ref = (not unwrapped.is_value_type()
|
|
1234
|
+
or unwrapped.is_expensive_copy())
|
|
1235
|
+
needs_mut_unpack = False
|
|
1236
|
+
if stmt.is_tuple_unpack and stmt.body:
|
|
1237
|
+
first = stmt.body[0]
|
|
1238
|
+
if isinstance(first, TpyTupleUnpack) and any(first.is_ref):
|
|
1239
|
+
needs_mut_unpack = True
|
|
1240
|
+
if (worth_const_ref
|
|
1241
|
+
and stmt.var not in self.ctx.func.mutated_loop_vars
|
|
1242
|
+
and stmt.var not in self.ctx.func.consumed_loop_vars
|
|
1243
|
+
and not needs_mut_unpack):
|
|
1244
|
+
stmt.const_loop_var = True
|
|
1245
|
+
# Auto-consuming iteration: use consuming __iter__ when the
|
|
1246
|
+
# container is at last use and elements are either mutated
|
|
1247
|
+
# or consumed (copied into owned storage via Own[T] params,
|
|
1248
|
+
# append, etc.). The container is dead after the loop, so
|
|
1249
|
+
# moving it into OwnIter is free (pointer swap). Consumed
|
|
1250
|
+
# elements become movable at last use, avoiding copies.
|
|
1251
|
+
loop_var_needs_ownership = (
|
|
1252
|
+
stmt.var in self.ctx.func.mutated_loop_vars
|
|
1253
|
+
or stmt.var in self.ctx.func.consumed_loop_vars
|
|
1254
|
+
)
|
|
1255
|
+
if (loop_var_needs_ownership
|
|
1256
|
+
and not stmt.hoist_loop_var
|
|
1257
|
+
and not unwrapped.is_value_type()
|
|
1258
|
+
and isinstance(stmt.iterable, TpyName)
|
|
1259
|
+
and id(stmt.iterable) in self.ctx.all_last_uses
|
|
1260
|
+
and self.compat._is_owned_var(stmt.iterable.name)):
|
|
1261
|
+
consuming_fi = self._find_consuming_iter(inner_iterable_type)
|
|
1262
|
+
if consuming_fi is not None:
|
|
1263
|
+
stmt.consuming_iter_fi = consuming_fi
|
|
1264
|
+
# Suppress copy warnings for the loop variable --
|
|
1265
|
+
# elements will be moved, not copied.
|
|
1266
|
+
deferred = self.ctx.func.deferred_loop_copy_warnings.pop(stmt.var, None)
|
|
1267
|
+
if deferred:
|
|
1268
|
+
for idx in sorted(deferred, reverse=True):
|
|
1269
|
+
del self.ctx.diagnostics[idx]
|
|
1270
|
+
# Also suppress copy warnings when elem_type is Own[T]
|
|
1271
|
+
# (e.g. Iterable[Own[T]] params) -- elements will be moved.
|
|
1272
|
+
if (stmt.consuming_iter_fi is None
|
|
1273
|
+
and isinstance(elem_type, OwnType)):
|
|
1274
|
+
deferred = self.ctx.func.deferred_loop_copy_warnings.pop(stmt.var, None)
|
|
1275
|
+
if deferred:
|
|
1276
|
+
for idx in sorted(deferred, reverse=True):
|
|
1277
|
+
del self.ctx.diagnostics[idx]
|
|
1278
|
+
self.init.apply_loop_exit_facts(before)
|
|
1279
|
+
# Loop might not execute — consumption inside is not definite
|
|
1280
|
+
self.ctx.func.current_consumed_own_params = consumed_before_loop
|
|
1281
|
+
self._restore_ns_var_types(ns_types_before_foreach)
|
|
1282
|
+
self._sync_promoted_var_types()
|
|
1283
|
+
self._propagate_for_loop_scope(stmt, inner_scope, elem_type)
|
|
1284
|
+
for s in stmt.orelse:
|
|
1285
|
+
self.analyze_stmt(s)
|
|
1286
|
+
elif isinstance(stmt, TpyBreak):
|
|
1287
|
+
if self.ctx.func.loop_depth == 0:
|
|
1288
|
+
raise self.ctx.error("'break' outside loop", stmt)
|
|
1289
|
+
self.init.mark_terminated()
|
|
1290
|
+
elif isinstance(stmt, TpyContinue):
|
|
1291
|
+
if self.ctx.func.loop_depth == 0:
|
|
1292
|
+
raise self.ctx.error("'continue' outside loop", stmt)
|
|
1293
|
+
self.init.mark_terminated()
|
|
1294
|
+
elif isinstance(stmt, TpyAssert):
|
|
1295
|
+
self.expr.analyze_expr(stmt.condition)
|
|
1296
|
+
self.narrowing.warn_truthy_value_optionals(stmt.condition)
|
|
1297
|
+
if stmt.message is not None:
|
|
1298
|
+
msg_type = self.expr.analyze_expr(stmt.message)
|
|
1299
|
+
if not is_any_str_type(msg_type):
|
|
1300
|
+
raise self.ctx.error("assert message must be a string", stmt)
|
|
1301
|
+
then_type_facts, _ = self.narrowing.condition_type_facts(stmt.condition)
|
|
1302
|
+
ptr_nn, _ = self.narrowing.condition_ptr_null_facts(stmt.condition)
|
|
1303
|
+
range_true, _ = self.narrowing.condition_range_facts(stmt.condition)
|
|
1304
|
+
stmt.then_type_facts = self._filter_union_codegen_facts(then_type_facts)
|
|
1305
|
+
self.ctx.func.narrowed_types.update(then_type_facts)
|
|
1306
|
+
self.ctx.func.non_null_ptr_vars |= ptr_nn
|
|
1307
|
+
self._apply_range_facts(range_true)
|
|
1308
|
+
elif isinstance(stmt, TpyGlobal):
|
|
1309
|
+
self._analyze_global_stmt(stmt)
|
|
1310
|
+
elif isinstance(stmt, TpyRaise):
|
|
1311
|
+
self._analyze_raise(stmt)
|
|
1312
|
+
elif isinstance(stmt, TpyTry):
|
|
1313
|
+
self._analyze_try(stmt)
|
|
1314
|
+
elif isinstance(stmt, TpyMatch):
|
|
1315
|
+
self.match.analyze_match(stmt)
|
|
1316
|
+
elif isinstance(stmt, TpyWith):
|
|
1317
|
+
self._analyze_with(stmt)
|
|
1318
|
+
elif isinstance(stmt, TpyNonlocal):
|
|
1319
|
+
self._analyze_nonlocal(stmt)
|
|
1320
|
+
elif isinstance(stmt, TpyNestedDef):
|
|
1321
|
+
self._analyze_nested_def(stmt)
|
|
1322
|
+
|
|
1323
|
+
def _apply_range_facts(self, facts: dict[str, 'ValueRange']) -> None:
|
|
1324
|
+
"""Apply integer range facts, intersecting with any existing ranges."""
|
|
1325
|
+
for name, new_range in facts.items():
|
|
1326
|
+
existing = self.ctx.func.value_ranges.get(name)
|
|
1327
|
+
if existing is not None:
|
|
1328
|
+
self.ctx.func.value_ranges[name] = ValueRange.intersect(existing, new_range)
|
|
1329
|
+
else:
|
|
1330
|
+
self.ctx.func.value_ranges[name] = new_range
|
|
1331
|
+
|
|
1332
|
+
def _track_for_range_facts(self, stmt: TpyForEach) -> None:
|
|
1333
|
+
"""Set range facts for loop variable when iterating over range().
|
|
1334
|
+
|
|
1335
|
+
Detects: range(len(arr)), range(N).
|
|
1336
|
+
"""
|
|
1337
|
+
iterable = stmt.iterable
|
|
1338
|
+
if not isinstance(iterable, TpyCall) or iterable.func_name != "range":
|
|
1339
|
+
return
|
|
1340
|
+
|
|
1341
|
+
args = iterable.args
|
|
1342
|
+
if len(args) == 1:
|
|
1343
|
+
arg = args[0]
|
|
1344
|
+
# range(len(arr)) -- symbolic bound
|
|
1345
|
+
if (isinstance(arg, TpyCall) and arg.func_name == "len"
|
|
1346
|
+
and len(arg.args) == 1 and isinstance(arg.args[0], TpyName)):
|
|
1347
|
+
self.ctx.func.value_ranges[stmt.var] = ValueRange.for_range_index(
|
|
1348
|
+
stop_len_of=arg.args[0].name,
|
|
1349
|
+
)
|
|
1350
|
+
return
|
|
1351
|
+
# range(N) -- literal bound
|
|
1352
|
+
if isinstance(arg, TpyIntLiteral):
|
|
1353
|
+
self.ctx.func.value_ranges[stmt.var] = ValueRange.for_range_index(
|
|
1354
|
+
stop_literal=arg.value,
|
|
1355
|
+
)
|
|
1356
|
+
return
|
|
1357
|
+
# range(n) -- unknown bound, but still non-negative
|
|
1358
|
+
self.ctx.func.value_ranges[stmt.var] = ValueRange.for_range_index()
|
|
1359
|
+
|
|
1360
|
+
def _filter_union_codegen_facts(
|
|
1361
|
+
self, facts: dict[str, TpyType],
|
|
1362
|
+
) -> dict[str, TpyType]:
|
|
1363
|
+
"""Keep narrowing facts that drive codegen extraction.
|
|
1364
|
+
|
|
1365
|
+
Optional narrowing is handled implicitly by std::optional in C++,
|
|
1366
|
+
so only UnionType variables need explicit std::get<T> extraction.
|
|
1367
|
+
LiteralType facts are passed through for dead branch elimination.
|
|
1368
|
+
Protocol facts (e.g. Iterable[T] -> NativeIterable[T] via isinstance)
|
|
1369
|
+
feed protocol_narrowings so downstream dispatch sees the refined type.
|
|
1370
|
+
Polymorphic-class facts (e.g. isinstance(opt_exc, OSError) where
|
|
1371
|
+
opt_exc: Optional[BaseException]) drive cast-and-cache codegen so
|
|
1372
|
+
subclass-typed reads in the true branch route through a dynamic_cast'd
|
|
1373
|
+
local rather than the base pointer.
|
|
1374
|
+
"""
|
|
1375
|
+
def _peel(t: TpyType) -> TpyType:
|
|
1376
|
+
# Strip Own / readonly so Own[A|B] params surface as UnionType
|
|
1377
|
+
# for the union-fact check; mirrors _isinstance_unwrap in
|
|
1378
|
+
# calls.py.
|
|
1379
|
+
t = unwrap_readonly(t)
|
|
1380
|
+
if isinstance(t, OwnType):
|
|
1381
|
+
t = unwrap_readonly(t.wrapped)
|
|
1382
|
+
return t
|
|
1383
|
+
|
|
1384
|
+
def _polymorphic_subclass_fact(name: str, ty: TpyType) -> bool:
|
|
1385
|
+
# Strict subclass only -- `is not None` narrowing keeps the source's
|
|
1386
|
+
# inner class and is gated out by the shared predicate. `_peel` strips
|
|
1387
|
+
# Own/readonly so `Own[A | B]` params surface their inner shape.
|
|
1388
|
+
return is_polymorphic_subclass_fact(
|
|
1389
|
+
_peel(self.narrowing.declared_type_for_name(name)),
|
|
1390
|
+
ty, self.ctx.registry)
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
name: ty for name, ty in facts.items()
|
|
1394
|
+
if (isinstance(_peel(self.narrowing.declared_type_for_name(name)), (UnionType, AnyType))
|
|
1395
|
+
or isinstance(ty, LiteralType)
|
|
1396
|
+
or is_protocol_type(ty)
|
|
1397
|
+
or _polymorphic_subclass_fact(name, ty)
|
|
1398
|
+
# Deref-view facts (key carries the deref suffix) drive the
|
|
1399
|
+
# if-init cast-and-cache for owning wrappers; the wrapper var
|
|
1400
|
+
# itself is never retyped, so they pass through verbatim.
|
|
1401
|
+
or parse_deref_view_key(name) is not None)
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
def _analyze_raise(self, stmt: TpyRaise) -> None:
|
|
1405
|
+
"""Analyze a raise statement (return-tier, throw-tier, or bare re-raise)."""
|
|
1406
|
+
# Bare raise (re-raise)
|
|
1407
|
+
if stmt.exception_type is None and stmt.raise_expr is None:
|
|
1408
|
+
if self.ctx.in_except_tier is None:
|
|
1409
|
+
raise self.ctx.error(
|
|
1410
|
+
"bare 'raise' is only valid inside an 'except' block", stmt)
|
|
1411
|
+
if self.ctx.in_except_tier == "return" and not self.ctx.in_except_has_binding:
|
|
1412
|
+
raise self.ctx.error(
|
|
1413
|
+
"bare 'raise' in return-tier except requires 'as' binding "
|
|
1414
|
+
"(e.g. 'except E as e') to capture the error value",
|
|
1415
|
+
stmt)
|
|
1416
|
+
self.init.mark_terminated()
|
|
1417
|
+
return
|
|
1418
|
+
|
|
1419
|
+
# Expression raise (general expression, e.g. raise <expr>)
|
|
1420
|
+
if stmt.raise_expr is not None:
|
|
1421
|
+
self._analyze_raise_expr(stmt)
|
|
1422
|
+
return
|
|
1423
|
+
|
|
1424
|
+
func = self.ctx.func.current_function
|
|
1425
|
+
if not isinstance(func, TpyFunction):
|
|
1426
|
+
raise self.ctx.error(
|
|
1427
|
+
f"'raise {stmt.exception_type}' can only be used inside a function", stmt)
|
|
1428
|
+
|
|
1429
|
+
bare_exc = bare_name(stmt.exception_type)
|
|
1430
|
+
record = self.ctx.registry.find_record(bare_exc)
|
|
1431
|
+
if not record:
|
|
1432
|
+
# Not a type -- check if it's a variable of exception type
|
|
1433
|
+
self._analyze_raise_name_as_expr(stmt)
|
|
1434
|
+
return
|
|
1435
|
+
|
|
1436
|
+
qualified_exc = qualify_exception_name(
|
|
1437
|
+
stmt.exception_type, self.ctx.registry, self.ctx.module_name)
|
|
1438
|
+
is_cf = is_return_exception(qualified_exc)
|
|
1439
|
+
|
|
1440
|
+
if is_cf:
|
|
1441
|
+
# Return-tier: must be inside @error_return(E) function with matching E
|
|
1442
|
+
if func.error_return is None:
|
|
1443
|
+
raise self.ctx.error(
|
|
1444
|
+
f"'raise {stmt.exception_type}' requires "
|
|
1445
|
+
f"@error_return({stmt.exception_type}) on the enclosing function",
|
|
1446
|
+
stmt)
|
|
1447
|
+
if not error_return_matches(func.error_return, qualified_exc):
|
|
1448
|
+
raise self.ctx.error(
|
|
1449
|
+
f"'raise {stmt.exception_type}' does not match "
|
|
1450
|
+
f"@error_return({stmt.exception_type})", stmt)
|
|
1451
|
+
else:
|
|
1452
|
+
# Throw-tier: must inherit from Exception
|
|
1453
|
+
if not is_exception_type(stmt.exception_type, self.ctx.registry):
|
|
1454
|
+
raise self.ctx.error(
|
|
1455
|
+
f"'{stmt.exception_type}' is not an exception type; "
|
|
1456
|
+
f"it must inherit from Exception",
|
|
1457
|
+
stmt)
|
|
1458
|
+
|
|
1459
|
+
# Type-check constructor arguments against __init__ params
|
|
1460
|
+
if stmt.args:
|
|
1461
|
+
if record.has_init:
|
|
1462
|
+
min_args = sum(1 for _, _, d in record.init_params if d is None)
|
|
1463
|
+
max_args = len(record.init_params)
|
|
1464
|
+
if len(stmt.args) < min_args or len(stmt.args) > max_args:
|
|
1465
|
+
expected = (f"{max_args}" if min_args == max_args
|
|
1466
|
+
else f"{min_args} to {max_args}")
|
|
1467
|
+
raise self.ctx.error(
|
|
1468
|
+
f"'raise {stmt.exception_type}()' expects "
|
|
1469
|
+
f"{expected} arguments, got {len(stmt.args)}",
|
|
1470
|
+
stmt)
|
|
1471
|
+
for i, (arg, (pname, ptype, _)) in enumerate(
|
|
1472
|
+
zip(stmt.args, record.init_params)):
|
|
1473
|
+
arg_type = self.expr.analyze_expr_with_hint(arg, ptype)
|
|
1474
|
+
stmt.args[i] = self.compat.coerce_expr(
|
|
1475
|
+
arg, arg_type, ptype, f"argument '{pname}'",
|
|
1476
|
+
coercion_ctx=CoercionContext.ARG)
|
|
1477
|
+
elif record.fields:
|
|
1478
|
+
raise self.ctx.error(
|
|
1479
|
+
f"'{stmt.exception_type}' has data fields but no __init__; "
|
|
1480
|
+
f"add __init__ to use 'raise {stmt.exception_type}(...)'",
|
|
1481
|
+
stmt)
|
|
1482
|
+
else:
|
|
1483
|
+
raise self.ctx.error(
|
|
1484
|
+
f"'raise {stmt.exception_type}()' does not accept arguments",
|
|
1485
|
+
stmt)
|
|
1486
|
+
# Propagate qualified name to AST
|
|
1487
|
+
stmt.exception_type = qualified_exc
|
|
1488
|
+
self.init.mark_terminated()
|
|
1489
|
+
|
|
1490
|
+
def _analyze_raise_expr(self, stmt: TpyRaise) -> None:
|
|
1491
|
+
"""Analyze 'raise <expr>' where expr is a general expression.
|
|
1492
|
+
|
|
1493
|
+
Phase 20 Stage 3: accepts a `Box[Throwable]` (or any other type
|
|
1494
|
+
implementing the `Deref` protocol whose peeled type is Throwable)
|
|
1495
|
+
in addition to plain Exception subclasses. Codegen lowers via
|
|
1496
|
+
`<expr>.__raise__()` which auto-derefs through Box's vtable.
|
|
1497
|
+
"""
|
|
1498
|
+
func = self.ctx.func.current_function
|
|
1499
|
+
if not isinstance(func, TpyFunction):
|
|
1500
|
+
raise self.ctx.error(
|
|
1501
|
+
"'raise' can only be used inside a function", stmt)
|
|
1502
|
+
expr_type = self.expr.analyze_expr(stmt.raise_expr)
|
|
1503
|
+
# Peel __deref__ until we hit a non-Deref type. Box[Throwable]
|
|
1504
|
+
# peels once to Throwable; an Optional[Box[T]] field already
|
|
1505
|
+
# narrows the Optional away before this point. Stores the depth on
|
|
1506
|
+
# the AST so codegen can insert the matching `.__deref__()` chain.
|
|
1507
|
+
peeled_source = unwrap_qualifiers(expr_type)
|
|
1508
|
+
depth = 0
|
|
1509
|
+
while True:
|
|
1510
|
+
next_peeled = self.expr.get_deref_target_type(peeled_source)
|
|
1511
|
+
if next_peeled is None:
|
|
1512
|
+
break
|
|
1513
|
+
depth += 1
|
|
1514
|
+
peeled_source = unwrap_qualifiers(next_peeled)
|
|
1515
|
+
if depth > 8:
|
|
1516
|
+
raise self.ctx.error(
|
|
1517
|
+
f"raise expression __deref__ chain exceeds 8 levels",
|
|
1518
|
+
stmt)
|
|
1519
|
+
if depth > 0:
|
|
1520
|
+
stmt.deref_depth = depth
|
|
1521
|
+
t = peeled_source
|
|
1522
|
+
if isinstance(t, NominalType) and t.is_protocol and t.qualified_name() == "tpy.Throwable":
|
|
1523
|
+
self.init.mark_terminated()
|
|
1524
|
+
return
|
|
1525
|
+
if isinstance(t, NominalType) and not t.is_protocol:
|
|
1526
|
+
if is_exception_type(t.name, self.ctx.registry):
|
|
1527
|
+
self.init.mark_terminated()
|
|
1528
|
+
return
|
|
1529
|
+
raise self.ctx.error(
|
|
1530
|
+
f"cannot raise expression of type '{expr_type}'; "
|
|
1531
|
+
f"its __deref__ target '{t}' is not Throwable",
|
|
1532
|
+
stmt)
|
|
1533
|
+
type_name = self._raise_expr_type_name(expr_type, stmt)
|
|
1534
|
+
if is_return_exception(
|
|
1535
|
+
qualify_exception_name(type_name, self.ctx.registry, self.ctx.module_name)):
|
|
1536
|
+
raise self.ctx.error(
|
|
1537
|
+
f"'raise <expr>' cannot be used with ReturnException type "
|
|
1538
|
+
f"'{type_name}'; use direct 'raise {type_name}' inside "
|
|
1539
|
+
f"an @error_return function instead",
|
|
1540
|
+
stmt)
|
|
1541
|
+
if not is_exception_type(type_name, self.ctx.registry):
|
|
1542
|
+
raise self.ctx.error(
|
|
1543
|
+
f"cannot raise expression of type '{type_name}'; "
|
|
1544
|
+
f"it must inherit from Exception",
|
|
1545
|
+
stmt)
|
|
1546
|
+
self.init.mark_terminated()
|
|
1547
|
+
|
|
1548
|
+
def _analyze_raise_name_as_expr(self, stmt: TpyRaise) -> None:
|
|
1549
|
+
"""Handle 'raise Name' or 'raise Name(args)' where Name is not a type.
|
|
1550
|
+
|
|
1551
|
+
Converts to expression raise if Name is a variable/call of exception type.
|
|
1552
|
+
"""
|
|
1553
|
+
name = stmt.exception_type
|
|
1554
|
+
if stmt.args or stmt.is_call_form:
|
|
1555
|
+
# raise func() or raise func(args) -- convert to call expression
|
|
1556
|
+
call_expr = TpyCall(func=TpyName(name, loc=stmt.loc), args=stmt.args, loc=stmt.loc)
|
|
1557
|
+
stmt.raise_expr = call_expr
|
|
1558
|
+
stmt.exception_type = None
|
|
1559
|
+
stmt.args = []
|
|
1560
|
+
self._analyze_raise_expr(stmt)
|
|
1561
|
+
return
|
|
1562
|
+
# raise name -- convert to variable reference
|
|
1563
|
+
name_expr = TpyName(name=name, loc=stmt.loc)
|
|
1564
|
+
stmt.raise_expr = name_expr
|
|
1565
|
+
stmt.exception_type = None
|
|
1566
|
+
self._analyze_raise_expr(stmt)
|
|
1567
|
+
|
|
1568
|
+
def _raise_expr_type_name(self, expr_type: TpyType, stmt: TpyRaise) -> str:
|
|
1569
|
+
"""Extract the type name from a raise expression's type for validation."""
|
|
1570
|
+
t = unwrap_qualifiers(expr_type)
|
|
1571
|
+
if isinstance(t, NominalType) and not t.is_protocol:
|
|
1572
|
+
return t.name
|
|
1573
|
+
raise self.ctx.error(
|
|
1574
|
+
f"cannot raise expression of type '{expr_type}'; "
|
|
1575
|
+
f"expected an exception type",
|
|
1576
|
+
stmt)
|
|
1577
|
+
|
|
1578
|
+
def _classify_try_tier(self, stmt: TpyTry) -> str:
|
|
1579
|
+
"""Classify a try statement as 'return', 'throw', or 'finally_only'."""
|
|
1580
|
+
if not stmt.handlers:
|
|
1581
|
+
return "finally_only"
|
|
1582
|
+
|
|
1583
|
+
has_cf = False
|
|
1584
|
+
has_throw = False
|
|
1585
|
+
has_bare = False
|
|
1586
|
+
for h in stmt.handlers:
|
|
1587
|
+
if h.exception_type is None:
|
|
1588
|
+
has_bare = True
|
|
1589
|
+
continue
|
|
1590
|
+
proto = self.ctx.registry.scan_by_short_name(h.exception_type)
|
|
1591
|
+
is_cf_catch_all = (
|
|
1592
|
+
proto is not None
|
|
1593
|
+
and f"{proto.module}.{proto.name}" == qnames.RETURN_EXCEPTION
|
|
1594
|
+
)
|
|
1595
|
+
if is_cf_catch_all or is_return_exception(
|
|
1596
|
+
qualify_exception_name(
|
|
1597
|
+
h.exception_type, self.ctx.registry, self.ctx.module_name)):
|
|
1598
|
+
has_cf = True
|
|
1599
|
+
else:
|
|
1600
|
+
has_throw = True
|
|
1601
|
+
|
|
1602
|
+
if has_cf and has_throw:
|
|
1603
|
+
raise self.ctx.error(
|
|
1604
|
+
"cannot mix ReturnException and non-ReturnException exception types "
|
|
1605
|
+
"in the same try/except block", stmt)
|
|
1606
|
+
if has_cf and has_bare:
|
|
1607
|
+
raise self.ctx.error(
|
|
1608
|
+
"bare 'except:' cannot be mixed with ReturnException handlers", stmt)
|
|
1609
|
+
|
|
1610
|
+
return "return" if has_cf else "throw"
|
|
1611
|
+
|
|
1612
|
+
def _analyze_try(self, stmt: TpyTry) -> None:
|
|
1613
|
+
"""Analyze a try/except/else/finally statement.
|
|
1614
|
+
|
|
1615
|
+
Classifies the try block into tiers:
|
|
1616
|
+
- 'return': ReturnException handlers -> goto-based dispatch (existing)
|
|
1617
|
+
- 'throw': non-ReturnException handlers -> C++ try/catch
|
|
1618
|
+
- 'finally_only': no handlers, just finally cleanup
|
|
1619
|
+
"""
|
|
1620
|
+
tier = self._classify_try_tier(stmt)
|
|
1621
|
+
stmt.tier = tier
|
|
1622
|
+
|
|
1623
|
+
if tier == "finally_only":
|
|
1624
|
+
self._analyze_try_finally_only(stmt)
|
|
1625
|
+
elif tier == "return":
|
|
1626
|
+
self._analyze_try_return(stmt)
|
|
1627
|
+
else:
|
|
1628
|
+
self._analyze_try_throw(stmt)
|
|
1629
|
+
|
|
1630
|
+
def _analyze_try_finally_only(self, stmt: TpyTry) -> None:
|
|
1631
|
+
"""Analyze try/finally with no except handlers."""
|
|
1632
|
+
scope_before = set(self.ctx.func.current_scope.bindings.keys())
|
|
1633
|
+
for s in stmt.try_body:
|
|
1634
|
+
self.analyze_stmt(s)
|
|
1635
|
+
try_bindings = dict(self.ctx.func.current_scope.bindings)
|
|
1636
|
+
prev_in_finally = self.ctx.in_finally
|
|
1637
|
+
self.ctx.in_finally = True
|
|
1638
|
+
for s in stmt.finally_body:
|
|
1639
|
+
self.analyze_stmt(s)
|
|
1640
|
+
self.ctx.in_finally = prev_in_finally
|
|
1641
|
+
# Hoist try-body variables so they're accessible in the finally body.
|
|
1642
|
+
# Mark as hoisted so non-value types use pointer indirection.
|
|
1643
|
+
branch_new = set(try_bindings.keys()) - scope_before
|
|
1644
|
+
predecl = branch_new - self.ctx.func.global_declarations
|
|
1645
|
+
if predecl:
|
|
1646
|
+
self.ctx.if_branch_decls[id(stmt)] = {
|
|
1647
|
+
name: try_bindings[name]
|
|
1648
|
+
for name in sorted(predecl)
|
|
1649
|
+
if name in try_bindings
|
|
1650
|
+
}
|
|
1651
|
+
self.ctx.func.hoisted_vars |= predecl
|
|
1652
|
+
|
|
1653
|
+
def _analyze_try_return(self, stmt: TpyTry) -> None:
|
|
1654
|
+
"""Analyze return-tier try/except (ReturnException, goto-based)."""
|
|
1655
|
+
# Return tier supports single handler or ReturnException catch-all
|
|
1656
|
+
if len(stmt.handlers) != 1:
|
|
1657
|
+
raise self.ctx.error(
|
|
1658
|
+
"return-tier (ReturnException) try/except supports only a single handler", stmt)
|
|
1659
|
+
handler = stmt.handlers[0]
|
|
1660
|
+
|
|
1661
|
+
scope_before = set(self.ctx.func.current_scope.bindings.keys())
|
|
1662
|
+
before = self.init.save()
|
|
1663
|
+
consumed_before = self.ctx.func.current_consumed_own_params.copy()
|
|
1664
|
+
bindings_before = dict(self.ctx.func.current_scope.bindings)
|
|
1665
|
+
ns_types_before = self._save_ns_var_types()
|
|
1666
|
+
|
|
1667
|
+
# Detect except ReturnException catch-all
|
|
1668
|
+
proto = self.ctx.registry.scan_by_short_name(handler.exception_type)
|
|
1669
|
+
is_return_exception_catch_all = (
|
|
1670
|
+
proto is not None
|
|
1671
|
+
and f"{proto.module}.{proto.name}" == qnames.RETURN_EXCEPTION
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
if is_return_exception_catch_all and handler.binding:
|
|
1675
|
+
raise self.ctx.error(
|
|
1676
|
+
"'except ReturnException as' binding is not supported", stmt)
|
|
1677
|
+
|
|
1678
|
+
if not is_return_exception_catch_all:
|
|
1679
|
+
bare_exc = bare_name(handler.exception_type)
|
|
1680
|
+
if not self.ctx.registry.find_record(bare_exc):
|
|
1681
|
+
raise self.ctx.error(
|
|
1682
|
+
f"Unknown error type '{handler.exception_type}'", stmt)
|
|
1683
|
+
|
|
1684
|
+
# Set try context so call analysis can allow error_return calls
|
|
1685
|
+
prev_try_error = self.ctx.try_except_error_type
|
|
1686
|
+
if is_return_exception_catch_all:
|
|
1687
|
+
self.ctx.try_except_error_type = "*"
|
|
1688
|
+
else:
|
|
1689
|
+
self.ctx.try_except_error_type = qualify_exception_name(
|
|
1690
|
+
handler.exception_type, self.ctx.registry, self.ctx.module_name)
|
|
1691
|
+
|
|
1692
|
+
for s in stmt.try_body:
|
|
1693
|
+
self.analyze_stmt(s)
|
|
1694
|
+
|
|
1695
|
+
self.ctx.try_except_error_type = prev_try_error
|
|
1696
|
+
|
|
1697
|
+
for s in stmt.else_body:
|
|
1698
|
+
self.analyze_stmt(s)
|
|
1699
|
+
then_state = self.init.save()
|
|
1700
|
+
consumed_after_then = self.ctx.func.current_consumed_own_params.copy()
|
|
1701
|
+
then_terminated = self.ctx.func.init_terminated
|
|
1702
|
+
try_bindings = dict(self.ctx.func.current_scope.bindings)
|
|
1703
|
+
|
|
1704
|
+
# Restore to pre-try state for except branch
|
|
1705
|
+
self.ctx.func.current_scope.bindings = dict(bindings_before)
|
|
1706
|
+
self._restore_ns_var_types(ns_types_before)
|
|
1707
|
+
self.init.restore(before)
|
|
1708
|
+
self.ctx.func.current_consumed_own_params = consumed_before.copy()
|
|
1709
|
+
|
|
1710
|
+
# Register except binding -- use find_record_by_qname because
|
|
1711
|
+
# handler.exception_type may be module-qualified (e.g. from macros).
|
|
1712
|
+
if handler.binding:
|
|
1713
|
+
exc_record = self.ctx.registry.find_record_by_qname(handler.exception_type)
|
|
1714
|
+
if exc_record:
|
|
1715
|
+
exc_type = NominalType(exc_record.name, _module_qname=exc_record.qualified_name())
|
|
1716
|
+
self.ctx.func.current_scope.bindings[handler.binding] = exc_type
|
|
1717
|
+
self.init.mark_assigned(handler.binding)
|
|
1718
|
+
|
|
1719
|
+
# Set in_except_tier for bare raise validation
|
|
1720
|
+
prev_except_tier = self.ctx.in_except_tier
|
|
1721
|
+
prev_has_binding = self.ctx.in_except_has_binding
|
|
1722
|
+
self.ctx.in_except_tier = "return"
|
|
1723
|
+
self.ctx.in_except_has_binding = handler.binding is not None
|
|
1724
|
+
for s in handler.body:
|
|
1725
|
+
self.analyze_stmt(s)
|
|
1726
|
+
self.ctx.in_except_tier = prev_except_tier
|
|
1727
|
+
self.ctx.in_except_has_binding = prev_has_binding
|
|
1728
|
+
|
|
1729
|
+
if handler.binding and handler.binding in self.ctx.func.current_scope.bindings:
|
|
1730
|
+
del self.ctx.func.current_scope.bindings[handler.binding]
|
|
1731
|
+
else_state = self.init.save()
|
|
1732
|
+
consumed_after_else = self.ctx.func.current_consumed_own_params.copy()
|
|
1733
|
+
else_terminated = self.ctx.func.init_terminated
|
|
1734
|
+
|
|
1735
|
+
self.init.merge_branches(then_state, else_state)
|
|
1736
|
+
self._merge_consumed_own(
|
|
1737
|
+
then_terminated, else_terminated,
|
|
1738
|
+
consumed_after_then, consumed_after_else)
|
|
1739
|
+
|
|
1740
|
+
# Analyze finally body (runs on all paths, doesn't affect branch merging)
|
|
1741
|
+
prev_in_finally = self.ctx.in_finally
|
|
1742
|
+
self.ctx.in_finally = True
|
|
1743
|
+
for s in stmt.finally_body:
|
|
1744
|
+
self.analyze_stmt(s)
|
|
1745
|
+
self.ctx.in_finally = prev_in_finally
|
|
1746
|
+
|
|
1747
|
+
# Hoist all declarations for goto-based dispatch
|
|
1748
|
+
all_bindings = dict(try_bindings)
|
|
1749
|
+
all_bindings.update(self.ctx.func.current_scope.bindings)
|
|
1750
|
+
branch_new = set(all_bindings.keys()) - scope_before
|
|
1751
|
+
if handler.binding:
|
|
1752
|
+
branch_new.discard(handler.binding)
|
|
1753
|
+
predecl = branch_new - self.ctx.func.global_declarations
|
|
1754
|
+
if predecl:
|
|
1755
|
+
self.ctx.if_branch_decls[id(stmt)] = {
|
|
1756
|
+
name: all_bindings[name]
|
|
1757
|
+
for name in sorted(predecl)
|
|
1758
|
+
if name in all_bindings
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
def _analyze_try_throw(self, stmt: TpyTry) -> None:
|
|
1762
|
+
"""Analyze throw-tier try/except (C++ try/catch)."""
|
|
1763
|
+
scope_before = set(self.ctx.func.current_scope.bindings.keys())
|
|
1764
|
+
before = self.init.save()
|
|
1765
|
+
consumed_before = self.ctx.func.current_consumed_own_params.copy()
|
|
1766
|
+
bindings_before = dict(self.ctx.func.current_scope.bindings)
|
|
1767
|
+
ns_types_before = self._save_ns_var_types()
|
|
1768
|
+
|
|
1769
|
+
# Validate all handlers and save resolved records for binding.
|
|
1770
|
+
# Qualify before lookup so dotted forms (`re.error`) bypass any local
|
|
1771
|
+
# class with the same bare name.
|
|
1772
|
+
handler_records: list[RecordInfo | None] = []
|
|
1773
|
+
for h in stmt.handlers:
|
|
1774
|
+
if h.exception_type is not None:
|
|
1775
|
+
qualified = qualify_exception_name(
|
|
1776
|
+
h.exception_type, self.ctx.registry, self.ctx.module_name)
|
|
1777
|
+
record = self.ctx.registry.find_record_by_qname(qualified)
|
|
1778
|
+
if not record:
|
|
1779
|
+
raise self.ctx.error(
|
|
1780
|
+
f"Unknown exception type '{h.exception_type}'", stmt)
|
|
1781
|
+
if not is_exception_type(qualified, self.ctx.registry):
|
|
1782
|
+
raise self.ctx.error(
|
|
1783
|
+
f"'{h.exception_type}' is not an exception type; "
|
|
1784
|
+
f"it must inherit from Exception", stmt)
|
|
1785
|
+
handler_records.append(record)
|
|
1786
|
+
h.exception_type = qualified
|
|
1787
|
+
else:
|
|
1788
|
+
handler_records.append(None)
|
|
1789
|
+
|
|
1790
|
+
# Analyze try body
|
|
1791
|
+
for s in stmt.try_body:
|
|
1792
|
+
self.analyze_stmt(s)
|
|
1793
|
+
# Capture try-body bindings BEFORE else (else vars are scoped to the
|
|
1794
|
+
# if(__ok) block in C++ and must not leak into post-try scope)
|
|
1795
|
+
try_bindings = dict(self.ctx.func.current_scope.bindings)
|
|
1796
|
+
|
|
1797
|
+
# Analyze else body
|
|
1798
|
+
for s in stmt.else_body:
|
|
1799
|
+
self.analyze_stmt(s)
|
|
1800
|
+
then_state = self.init.save()
|
|
1801
|
+
consumed_after_then = self.ctx.func.current_consumed_own_params.copy()
|
|
1802
|
+
then_terminated = self.ctx.func.init_terminated
|
|
1803
|
+
|
|
1804
|
+
# Analyze each except handler as a separate branch from pre-try state
|
|
1805
|
+
handler_states: list[tuple] = []
|
|
1806
|
+
for i, h in enumerate(stmt.handlers):
|
|
1807
|
+
self.ctx.func.current_scope.bindings = dict(bindings_before)
|
|
1808
|
+
self._restore_ns_var_types(ns_types_before)
|
|
1809
|
+
self.init.restore(before)
|
|
1810
|
+
self.ctx.func.current_consumed_own_params = consumed_before.copy()
|
|
1811
|
+
|
|
1812
|
+
record = handler_records[i]
|
|
1813
|
+
if h.binding and record is not None:
|
|
1814
|
+
exc_type = NominalType(record.name, _module_qname=record.qualified_name())
|
|
1815
|
+
self.ctx.func.current_scope.bindings[h.binding] = exc_type
|
|
1816
|
+
self.init.mark_assigned(h.binding)
|
|
1817
|
+
|
|
1818
|
+
prev_except_tier = self.ctx.in_except_tier
|
|
1819
|
+
self.ctx.in_except_tier = "throw"
|
|
1820
|
+
for s in h.body:
|
|
1821
|
+
self.analyze_stmt(s)
|
|
1822
|
+
self.ctx.in_except_tier = prev_except_tier
|
|
1823
|
+
|
|
1824
|
+
if h.binding and h.binding in self.ctx.func.current_scope.bindings:
|
|
1825
|
+
del self.ctx.func.current_scope.bindings[h.binding]
|
|
1826
|
+
|
|
1827
|
+
handler_states.append((
|
|
1828
|
+
self.init.save(),
|
|
1829
|
+
self.ctx.func.current_consumed_own_params.copy(),
|
|
1830
|
+
self.ctx.func.init_terminated,
|
|
1831
|
+
))
|
|
1832
|
+
|
|
1833
|
+
# Merge all branches: success path + all handler paths
|
|
1834
|
+
all_states = [then_state] + [s for s, _, _ in handler_states]
|
|
1835
|
+
all_terminated = [then_terminated] + [t for _, _, t in handler_states]
|
|
1836
|
+
all_consumed = [consumed_after_then] + [c for _, c, _ in handler_states]
|
|
1837
|
+
|
|
1838
|
+
# Multi-branch merge: start from first, merge pairwise
|
|
1839
|
+
merged = all_states[0]
|
|
1840
|
+
for s in all_states[1:]:
|
|
1841
|
+
self.init.merge_branches(merged, s)
|
|
1842
|
+
merged = self.init.save()
|
|
1843
|
+
|
|
1844
|
+
# Merge consumed Own[T] params
|
|
1845
|
+
if all(all_terminated):
|
|
1846
|
+
result_consumed: set[str] = set()
|
|
1847
|
+
for c in all_consumed:
|
|
1848
|
+
result_consumed |= c
|
|
1849
|
+
elif any(all_terminated):
|
|
1850
|
+
result_consumed = set()
|
|
1851
|
+
for t, c in zip(all_terminated, all_consumed):
|
|
1852
|
+
if not t:
|
|
1853
|
+
result_consumed = result_consumed & c if result_consumed else c.copy()
|
|
1854
|
+
else:
|
|
1855
|
+
result_consumed = all_consumed[0].copy()
|
|
1856
|
+
for c in all_consumed[1:]:
|
|
1857
|
+
result_consumed &= c
|
|
1858
|
+
self.ctx.func.current_consumed_own_params = result_consumed
|
|
1859
|
+
|
|
1860
|
+
# Analyze finally body
|
|
1861
|
+
prev_in_finally = self.ctx.in_finally
|
|
1862
|
+
self.ctx.in_finally = True
|
|
1863
|
+
for s in stmt.finally_body:
|
|
1864
|
+
self.analyze_stmt(s)
|
|
1865
|
+
self.ctx.in_finally = prev_in_finally
|
|
1866
|
+
|
|
1867
|
+
# Throw-tier uses C++ try/catch with proper scoping -- no goto hoisting
|
|
1868
|
+
# needed in general. BUT try-body variables must be hoisted when:
|
|
1869
|
+
# - finally body exists: finally code runs after the try/catch block
|
|
1870
|
+
# - else body exists: else code is emitted after the try/catch block
|
|
1871
|
+
# - code continues after try/except and try-body vars are in scope
|
|
1872
|
+
# (all handlers terminate, so post-try code uses try-body vars)
|
|
1873
|
+
all_handlers_terminate = all(t for _, _, t in handler_states)
|
|
1874
|
+
needs_hoist = (stmt.finally_body or stmt.else_body
|
|
1875
|
+
or (all_handlers_terminate and not self.ctx.func.init_terminated))
|
|
1876
|
+
self.ctx.func.current_scope.bindings = dict(try_bindings)
|
|
1877
|
+
if needs_hoist:
|
|
1878
|
+
branch_new = set(try_bindings.keys()) - scope_before
|
|
1879
|
+
for h in stmt.handlers:
|
|
1880
|
+
if h.binding:
|
|
1881
|
+
branch_new.discard(h.binding)
|
|
1882
|
+
predecl = branch_new - self.ctx.func.global_declarations
|
|
1883
|
+
if predecl:
|
|
1884
|
+
self.ctx.if_branch_decls[id(stmt)] = {
|
|
1885
|
+
name: try_bindings[name]
|
|
1886
|
+
for name in sorted(predecl)
|
|
1887
|
+
if name in try_bindings
|
|
1888
|
+
}
|
|
1889
|
+
self.ctx.func.hoisted_vars |= predecl
|
|
1890
|
+
|
|
1891
|
+
def _merge_consumed_own(self, then_terminated: bool, else_terminated: bool,
|
|
1892
|
+
consumed_then: set[str], consumed_else: set[str]) -> None:
|
|
1893
|
+
"""Merge consumed Own[T] params from two branches."""
|
|
1894
|
+
if then_terminated and else_terminated:
|
|
1895
|
+
self.ctx.func.current_consumed_own_params = consumed_then | consumed_else
|
|
1896
|
+
elif then_terminated:
|
|
1897
|
+
self.ctx.func.current_consumed_own_params = consumed_else
|
|
1898
|
+
elif else_terminated:
|
|
1899
|
+
self.ctx.func.current_consumed_own_params = consumed_then
|
|
1900
|
+
else:
|
|
1901
|
+
self.ctx.func.current_consumed_own_params = consumed_then & consumed_else
|
|
1902
|
+
|
|
1903
|
+
def _analyze_async_for(self, stmt: TpyForEach) -> None:
|
|
1904
|
+
"""Analyze `async for x in ait: <body>` (v1.5 M6).
|
|
1905
|
+
|
|
1906
|
+
Resolves `<ait>.__aiter__()` (sync method, returns the async
|
|
1907
|
+
iterator) and the iterator's `__anext__()` (async method, returns
|
|
1908
|
+
`Awaitable[T]`); unwraps T as the loop var's element type.
|
|
1909
|
+
Only allowed inside `async def`.
|
|
1910
|
+
"""
|
|
1911
|
+
cur = self.ctx.func.current_function
|
|
1912
|
+
if not (isinstance(cur, TpyFunction) and cur.is_async):
|
|
1913
|
+
raise self.ctx.error(
|
|
1914
|
+
"`async for` is only allowed inside an `async def` "
|
|
1915
|
+
"function body", stmt)
|
|
1916
|
+
|
|
1917
|
+
iterable_type = self.expr.analyze_expr(stmt.iterable)
|
|
1918
|
+
iterable_inner = unwrap_own(unwrap_ref_type(iterable_type))
|
|
1919
|
+
|
|
1920
|
+
record_info = self.ctx.registry.get_record_for_type(iterable_inner)
|
|
1921
|
+
if record_info is None:
|
|
1922
|
+
raise self.ctx.error(
|
|
1923
|
+
f"Type '{iterable_inner}' cannot be used as an async "
|
|
1924
|
+
f"iterable (not a record type)", stmt.iterable)
|
|
1925
|
+
|
|
1926
|
+
aiter_overloads = record_info.methods.get("__aiter__")
|
|
1927
|
+
if not aiter_overloads:
|
|
1928
|
+
raise self.ctx.error(
|
|
1929
|
+
f"Type '{iterable_inner}' cannot be used as an async "
|
|
1930
|
+
f"iterable (missing __aiter__ method)", stmt.iterable)
|
|
1931
|
+
aiter_info = aiter_overloads[0]
|
|
1932
|
+
if aiter_info.is_async:
|
|
1933
|
+
raise self.ctx.error(
|
|
1934
|
+
f"`__aiter__` on '{iterable_inner}' must be a sync "
|
|
1935
|
+
f"`def` (returns the async iterator); only `__anext__` "
|
|
1936
|
+
f"is `async def`", stmt.iterable)
|
|
1937
|
+
|
|
1938
|
+
aiter_type = unwrap_own(unwrap_ref_type(aiter_info.return_type))
|
|
1939
|
+
if not isinstance(aiter_type, NominalType):
|
|
1940
|
+
raise self.ctx.error(
|
|
1941
|
+
f"`__aiter__` on '{iterable_inner}' returns "
|
|
1942
|
+
f"'{aiter_type}', which is not a record type and cannot "
|
|
1943
|
+
f"provide `__anext__`", stmt.iterable)
|
|
1944
|
+
aiter_record = self.ctx.registry.get_record_for_type(aiter_type)
|
|
1945
|
+
if aiter_record is None:
|
|
1946
|
+
raise self.ctx.error(
|
|
1947
|
+
f"`__aiter__` on '{iterable_inner}' returns "
|
|
1948
|
+
f"'{aiter_type}', which is not a record type and cannot "
|
|
1949
|
+
f"provide `__anext__`", stmt.iterable)
|
|
1950
|
+
# Codegen reads this to find the __anext__ sub-coro struct name
|
|
1951
|
+
# without re-walking the registry.
|
|
1952
|
+
stmt.async_aiter_type = aiter_type
|
|
1953
|
+
|
|
1954
|
+
anext_overloads = aiter_record.methods.get("__anext__")
|
|
1955
|
+
if not anext_overloads:
|
|
1956
|
+
raise self.ctx.error(
|
|
1957
|
+
f"Async iterator '{aiter_type}' is missing `__anext__` "
|
|
1958
|
+
f"method", stmt.iterable)
|
|
1959
|
+
anext_info = anext_overloads[0]
|
|
1960
|
+
if not anext_info.is_async:
|
|
1961
|
+
raise self.ctx.error(
|
|
1962
|
+
f"`__anext__` on '{aiter_type}' must be `async def` "
|
|
1963
|
+
f"for use in `async for`", stmt.iterable)
|
|
1964
|
+
|
|
1965
|
+
anext_ret = unwrap_ref_type(anext_info.return_type)
|
|
1966
|
+
if (isinstance(anext_ret, NominalType)
|
|
1967
|
+
and anext_ret.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
|
|
1968
|
+
and len(anext_ret.type_args) == 1):
|
|
1969
|
+
elem_type = anext_ret.type_args[0]
|
|
1970
|
+
else:
|
|
1971
|
+
raise self.ctx.error(
|
|
1972
|
+
f"`__anext__` on '{aiter_type}' must return "
|
|
1973
|
+
f"`Awaitable[T]` (got '{anext_ret}')", stmt.iterable)
|
|
1974
|
+
|
|
1975
|
+
elem_type = self._infer_new_local_type(
|
|
1976
|
+
stmt.var, elem_type, None, None,
|
|
1977
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
1978
|
+
)
|
|
1979
|
+
stmt.elem_type = make_ref(elem_type)
|
|
1980
|
+
self._record_for_loop_var_type(stmt, elem_type)
|
|
1981
|
+
|
|
1982
|
+
before = self.init.save()
|
|
1983
|
+
consumed_before_loop = self.ctx.func.current_consumed_own_params.copy()
|
|
1984
|
+
ns_types_before = self._save_ns_var_types()
|
|
1985
|
+
with self.scopes.loop_scope() as inner_scope:
|
|
1986
|
+
self.init.apply_loop_entry_facts(before)
|
|
1987
|
+
self.ctx.func.mutated_loop_vars.discard(stmt.var)
|
|
1988
|
+
self.ctx.func.consumed_loop_vars.discard(stmt.var)
|
|
1989
|
+
# Register an ITER borrow on a named iterable so structural
|
|
1990
|
+
# mutations of it inside the loop body generate conflict
|
|
1991
|
+
# warnings (e.g. `async for x in items: items.append(...)`).
|
|
1992
|
+
# Limited to TpyName today: __aiter__ returns the aiter by
|
|
1993
|
+
# value into a frame slot, so call/field-access iterables
|
|
1994
|
+
# don't share storage with the loop var (unlike sync for,
|
|
1995
|
+
# where the iterator references the iterable's storage).
|
|
1996
|
+
if isinstance(stmt.iterable, TpyName):
|
|
1997
|
+
bt = self.ctx.func.borrow_tracker
|
|
1998
|
+
bt.add_borrow(stmt.iterable.name, "__for_iter", BorrowKind.ITER)
|
|
1999
|
+
self.ctx.func.loop_var_iterable[stmt.var] = stmt.iterable.name
|
|
2000
|
+
# Protocol-typed or generic-typed iterables: `__aiter__`
|
|
2001
|
+
# is called on the param and may mutate self, so the
|
|
2002
|
+
# generated signature must be `T&` not `const T&`.
|
|
2003
|
+
# Concrete-typed iterables: the analyzer infers
|
|
2004
|
+
# mutability from the call itself, so no extra hint
|
|
2005
|
+
# needed here.
|
|
2006
|
+
inner = unwrap_ref_type(unwrap_readonly(iterable_type))
|
|
2007
|
+
if isinstance(inner, TypeParamRef) or is_protocol_type(inner):
|
|
2008
|
+
self.ctx.mark_param_mutated(stmt.iterable.name)
|
|
2009
|
+
with self.scopes.loop_var(inner_scope, stmt.var, elem_type,
|
|
2010
|
+
inner_scope.depth, is_foreach=True):
|
|
2011
|
+
for s in stmt.body:
|
|
2012
|
+
self.analyze_stmt(s)
|
|
2013
|
+
self.init.apply_loop_exit_facts(before)
|
|
2014
|
+
# Loop body might not execute; consumption inside isn't definite.
|
|
2015
|
+
self.ctx.func.current_consumed_own_params = consumed_before_loop
|
|
2016
|
+
self._restore_ns_var_types(ns_types_before)
|
|
2017
|
+
self._sync_promoted_var_types()
|
|
2018
|
+
self._propagate_for_loop_scope(stmt, inner_scope, elem_type)
|
|
2019
|
+
# `async for` parser already rejects orelse; nothing to analyze.
|
|
2020
|
+
|
|
2021
|
+
def _analyze_with(self, stmt: TpyWith) -> None:
|
|
2022
|
+
"""Analyze a with statement (context managers).
|
|
2023
|
+
|
|
2024
|
+
Checks that each context expression has __enter__ and __exit__ methods.
|
|
2025
|
+
Types the as-binding from __enter__ return type. The as-variable is
|
|
2026
|
+
visible after the with block (matching CPython semantics).
|
|
2027
|
+
|
|
2028
|
+
`async with` (stmt.is_async): looks for __aenter__ / __aexit__,
|
|
2029
|
+
validates both are async methods, and unwraps the
|
|
2030
|
+
`Awaitable[T]` wrapper that async-method registration applies to
|
|
2031
|
+
their return types. Only allowed inside `async def`.
|
|
2032
|
+
"""
|
|
2033
|
+
if stmt.is_async:
|
|
2034
|
+
cur = self.ctx.func.current_function
|
|
2035
|
+
if not (isinstance(cur, TpyFunction) and cur.is_async):
|
|
2036
|
+
raise self.ctx.error(
|
|
2037
|
+
"`async with` is only allowed inside an `async def` "
|
|
2038
|
+
"function body", stmt)
|
|
2039
|
+
|
|
2040
|
+
enter_method = "__aenter__" if stmt.is_async else "__enter__"
|
|
2041
|
+
exit_method = "__aexit__" if stmt.is_async else "__exit__"
|
|
2042
|
+
kind_label = "async context manager" if stmt.is_async else "context manager"
|
|
2043
|
+
|
|
2044
|
+
for item in stmt.items:
|
|
2045
|
+
ctx_type = unwrap_own(unwrap_ref_type(self.expr.analyze_expr(item.context_expr)))
|
|
2046
|
+
|
|
2047
|
+
# Look up __[a]enter__ / __[a]exit__ on the context manager type.
|
|
2048
|
+
record_info = self.ctx.registry.get_record_for_type(ctx_type)
|
|
2049
|
+
err_node = item.context_expr
|
|
2050
|
+
if record_info is None:
|
|
2051
|
+
raise self.ctx.error(
|
|
2052
|
+
f"Type '{ctx_type}' cannot be used as a {kind_label}"
|
|
2053
|
+
f" (not a record type)", err_node)
|
|
2054
|
+
|
|
2055
|
+
enter_overloads = record_info.methods.get(enter_method)
|
|
2056
|
+
if not enter_overloads:
|
|
2057
|
+
raise self.ctx.error(
|
|
2058
|
+
f"Type '{ctx_type}' cannot be used as a {kind_label}"
|
|
2059
|
+
f" (missing {enter_method} method)", err_node)
|
|
2060
|
+
|
|
2061
|
+
exit_overloads = record_info.methods.get(exit_method)
|
|
2062
|
+
if not exit_overloads:
|
|
2063
|
+
raise self.ctx.error(
|
|
2064
|
+
f"Type '{ctx_type}' cannot be used as a {kind_label}"
|
|
2065
|
+
f" (missing {exit_method} method)", err_node)
|
|
2066
|
+
|
|
2067
|
+
# Validate async-ness: in `async with`, both methods must be
|
|
2068
|
+
# `async def`. In sync `with`, neither should be.
|
|
2069
|
+
enter_info = enter_overloads[0]
|
|
2070
|
+
exit_info = exit_overloads[0]
|
|
2071
|
+
if stmt.is_async:
|
|
2072
|
+
if not enter_info.is_async:
|
|
2073
|
+
raise self.ctx.error(
|
|
2074
|
+
f"`{enter_method}` on '{ctx_type}' must be `async def` "
|
|
2075
|
+
f"for use in `async with`", err_node)
|
|
2076
|
+
if not exit_info.is_async:
|
|
2077
|
+
raise self.ctx.error(
|
|
2078
|
+
f"`{exit_method}` on '{ctx_type}' must be `async def` "
|
|
2079
|
+
f"for use in `async with`", err_node)
|
|
2080
|
+
# v1.5 M5 limitation: cleanup-only `__aexit__` (all-None
|
|
2081
|
+
# args). Inspecting `exc_val: Optional[BaseException]`
|
|
2082
|
+
# requires polymorphic exception storage across the
|
|
2083
|
+
# suspension (E9 / Phase 20).
|
|
2084
|
+
if len(exit_info.params) >= 2 and isinstance(
|
|
2085
|
+
exit_info.params[1].type, OptionalType):
|
|
2086
|
+
raise self.ctx.error(
|
|
2087
|
+
f"`{exit_method}` with `exc_val: Optional["
|
|
2088
|
+
f"BaseException]` is not yet supported in "
|
|
2089
|
+
f"`async with` -- v1.5 M5 only handles "
|
|
2090
|
+
f"cleanup-only managers. Annotate exc_val as "
|
|
2091
|
+
f"`None` to discard the exception. Full "
|
|
2092
|
+
f"polymorphic-exception inspection is tracked "
|
|
2093
|
+
f"as E9 / Phase 20.", err_node)
|
|
2094
|
+
|
|
2095
|
+
# v1.5 M1: tag whether __exit__ / __aexit__ may suppress
|
|
2096
|
+
# exceptions, and whether exc_val is typed Optional[BaseException]
|
|
2097
|
+
# (vs None). Registration rejects multi-overload `__exit__`;
|
|
2098
|
+
# async methods don't participate in @overload, so each has
|
|
2099
|
+
# exactly one entry.
|
|
2100
|
+
# For async methods, the FunctionInfo return type is wrapped
|
|
2101
|
+
# in `Cancellable[T]` for async-def results (or `Awaitable[T]`
|
|
2102
|
+
# for user types with bare `__poll__`); unwrap one level to
|
|
2103
|
+
# see the user's declared T.
|
|
2104
|
+
exit_ret = exit_info.return_type
|
|
2105
|
+
if (stmt.is_async and isinstance(exit_ret, NominalType)
|
|
2106
|
+
and exit_ret.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
|
|
2107
|
+
and len(exit_ret.type_args) == 1):
|
|
2108
|
+
exit_ret = exit_ret.type_args[0]
|
|
2109
|
+
item.exit_can_suppress = exit_ret == BOOL
|
|
2110
|
+
item.exit_takes_exc_val = (
|
|
2111
|
+
len(exit_info.params) >= 2
|
|
2112
|
+
and isinstance(exit_info.params[1].type, OptionalType)
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
# Get return type of __[a]enter__() -- use the first overload.
|
|
2116
|
+
# For async, unwrap Awaitable[T] / Cancellable[T] so the
|
|
2117
|
+
# as-binding sees T.
|
|
2118
|
+
enter_type = unwrap_ref_type(enter_info.return_type)
|
|
2119
|
+
if (stmt.is_async and isinstance(enter_type, NominalType)
|
|
2120
|
+
and enter_type.qualified_name() in (qnames.AWAITABLE, qnames.CANCELLABLE)
|
|
2121
|
+
and len(enter_type.type_args) == 1):
|
|
2122
|
+
enter_type = enter_type.type_args[0]
|
|
2123
|
+
item.enter_type = enter_type
|
|
2124
|
+
|
|
2125
|
+
# Register the as-variable if present
|
|
2126
|
+
if item.target is not None:
|
|
2127
|
+
resolved = self._infer_new_local_type(
|
|
2128
|
+
item.target, enter_type, None, None,
|
|
2129
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
2130
|
+
)
|
|
2131
|
+
item.enter_type = resolved
|
|
2132
|
+
self.ctx.func.current_scope.define(item.target, resolved)
|
|
2133
|
+
self.init.mark_assigned(item.target)
|
|
2134
|
+
|
|
2135
|
+
# Analyze the body -- track new variable declarations so codegen
|
|
2136
|
+
# can pre-declare them outside the guard {} scope (C++ scoping).
|
|
2137
|
+
scope_before = set(self.ctx.func.current_scope.bindings.keys())
|
|
2138
|
+
|
|
2139
|
+
for s in stmt.body:
|
|
2140
|
+
self.analyze_stmt(s)
|
|
2141
|
+
|
|
2142
|
+
# All variables first declared inside the body need pre-declaration
|
|
2143
|
+
# since codegen wraps the body in try {} for the with's cleanup pattern.
|
|
2144
|
+
branch_new = set(self.ctx.func.current_scope.bindings.keys()) - scope_before
|
|
2145
|
+
predecl = branch_new - self.ctx.func.global_declarations
|
|
2146
|
+
if predecl:
|
|
2147
|
+
self.ctx.if_branch_decls[id(stmt)] = {
|
|
2148
|
+
name: self.ctx.func.current_scope.lookup(name)
|
|
2149
|
+
for name in sorted(predecl)
|
|
2150
|
+
if name in self.ctx.func.current_scope.bindings
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
# --- Nested def / nonlocal ---
|
|
2154
|
+
|
|
2155
|
+
def _analyze_nonlocal(self, stmt: TpyNonlocal) -> None:
|
|
2156
|
+
"""Analyze a nonlocal declaration."""
|
|
2157
|
+
if not self.ctx.func.in_nested_def:
|
|
2158
|
+
raise self.ctx.error(
|
|
2159
|
+
"'nonlocal' is only valid inside a nested function", stmt)
|
|
2160
|
+
for name in stmt.names:
|
|
2161
|
+
if name not in self.ctx.func.outer_scope_locals:
|
|
2162
|
+
raise self.ctx.error(
|
|
2163
|
+
f"No binding for nonlocal '{name}' found in enclosing scope",
|
|
2164
|
+
stmt)
|
|
2165
|
+
self.ctx.func.current_nonlocal_names.add(name)
|
|
2166
|
+
|
|
2167
|
+
def _prescan_and_analyze_body(
|
|
2168
|
+
self,
|
|
2169
|
+
func: TpyFunction,
|
|
2170
|
+
params: list[tuple[str, TpyType]],
|
|
2171
|
+
scope: 'Scope',
|
|
2172
|
+
ns: 'Namespace | None',
|
|
2173
|
+
) -> ScanResult:
|
|
2174
|
+
"""Shared function body analysis: bind params, prescan, analyze statements.
|
|
2175
|
+
|
|
2176
|
+
Used by both _analyze_function (analyzer.py) and _analyze_nested_def.
|
|
2177
|
+
Callers handle scope creation, type resolution, and post-processing.
|
|
2178
|
+
"""
|
|
2179
|
+
# Bind params to scope with Ref for non-value types.
|
|
2180
|
+
# analyze_expr strips Ref from return values, so downstream sema
|
|
2181
|
+
# sees bare types. Ref on scope types enables provenance-aware
|
|
2182
|
+
# code (assignment warnings, local scope propagation).
|
|
2183
|
+
param_names: set[str] = set()
|
|
2184
|
+
for pname, ptype in params:
|
|
2185
|
+
param_names.add(pname)
|
|
2186
|
+
scope_type = make_ref(ptype)
|
|
2187
|
+
scope.define(pname, scope_type)
|
|
2188
|
+
self.ctx.func.var_scope_depth[pname] = scope.depth
|
|
2189
|
+
self.ctx.func.definitely_assigned.add(pname)
|
|
2190
|
+
if ns:
|
|
2191
|
+
ns.bind_variable(pname, scope_type)
|
|
2192
|
+
|
|
2193
|
+
# Param tracking for mutation analysis
|
|
2194
|
+
self.ctx.func.current_param_names = param_names
|
|
2195
|
+
self.ctx.func.current_param_name_to_idx = {p: i for i, (p, _) in enumerate(params)}
|
|
2196
|
+
# For non-static methods, include "self" in the param index map with
|
|
2197
|
+
# sentinel -1 so that _record_mutation_call_edges can track self
|
|
2198
|
+
# flowing through free function calls (e.g., helper(self)).
|
|
2199
|
+
# Phase 2 (_resolve_single) converts caller_idx == -1 to self_mutated.
|
|
2200
|
+
if func.is_method and not func.is_staticmethod:
|
|
2201
|
+
self.ctx.func.current_param_name_to_idx["self"] = -1
|
|
2202
|
+
|
|
2203
|
+
# Prescan for reassigned variables + last-use liveness
|
|
2204
|
+
scan = scan_reassigned_vars(func.body, pre_declared=param_names)
|
|
2205
|
+
self.ctx.all_last_uses |= analyze_last_uses(func.body, scan.alias_sources)
|
|
2206
|
+
self.ctx.func.current_reassigned_vars = scan.reassigned.copy()
|
|
2207
|
+
self.ctx.func.current_lvalue_reassigned = scan.lvalue_reassigned.copy()
|
|
2208
|
+
self.ctx.func.current_aug_assigned_vars = scan.aug_assigned.copy()
|
|
2209
|
+
|
|
2210
|
+
# Analyze body
|
|
2211
|
+
for stmt in func.body:
|
|
2212
|
+
self.analyze_stmt(stmt)
|
|
2213
|
+
|
|
2214
|
+
return scan
|
|
2215
|
+
|
|
2216
|
+
def _analyze_nested_def(self, stmt: TpyNestedDef) -> None:
|
|
2217
|
+
"""Analyze a nested function definition."""
|
|
2218
|
+
func = stmt.func
|
|
2219
|
+
|
|
2220
|
+
if self.ctx.func.in_nested_def:
|
|
2221
|
+
raise self.ctx.error(
|
|
2222
|
+
"Nested functions cannot contain further nested functions",
|
|
2223
|
+
stmt)
|
|
2224
|
+
|
|
2225
|
+
# Collect outer locals available for capture
|
|
2226
|
+
outer_locals = self.ctx.func.definitely_assigned.copy()
|
|
2227
|
+
|
|
2228
|
+
# Pre-scan nonlocal declarations (at any nesting depth) to bind
|
|
2229
|
+
# them in the inner scope before body analysis begins.
|
|
2230
|
+
nonlocal_names: set[str] = set()
|
|
2231
|
+
self._collect_nonlocal_names(func.body, nonlocal_names)
|
|
2232
|
+
|
|
2233
|
+
# Resolve param and return types
|
|
2234
|
+
resolved_params_bare = [(p, self.type_ops.resolve_type(t)) for p, t in func.params]
|
|
2235
|
+
func.params = [(p, make_ref(t)) for p, t in resolved_params_bare]
|
|
2236
|
+
params = resolved_params_bare
|
|
2237
|
+
param_names = {p for p, _ in params}
|
|
2238
|
+
return_type = self.type_ops.resolve_type(func.return_type)
|
|
2239
|
+
func.return_type = make_ref(return_type)
|
|
2240
|
+
|
|
2241
|
+
# Analyze body in isolated scope
|
|
2242
|
+
with self.scopes.nested_def_scope(func) as inner_scope:
|
|
2243
|
+
self.ctx.func.outer_scope_locals = outer_locals
|
|
2244
|
+
|
|
2245
|
+
# Add nonlocal names to inner scope with types from outer
|
|
2246
|
+
for name in nonlocal_names:
|
|
2247
|
+
outer_type = inner_scope.parent.lookup(name) if inner_scope.parent else None
|
|
2248
|
+
if outer_type is not None:
|
|
2249
|
+
inner_scope.define(name, outer_type)
|
|
2250
|
+
self.ctx.func.definitely_assigned.add(name)
|
|
2251
|
+
if self.ctx.func.current_ns:
|
|
2252
|
+
self.ctx.func.current_ns.bind_variable(name, outer_type)
|
|
2253
|
+
|
|
2254
|
+
self._prescan_and_analyze_body(func, params, inner_scope, self.ctx.func.current_ns)
|
|
2255
|
+
|
|
2256
|
+
# Use the authoritative nonlocal set from body analysis
|
|
2257
|
+
# (covers nonlocal declarations at any nesting depth)
|
|
2258
|
+
nonlocal_names = self.ctx.func.current_nonlocal_names.copy()
|
|
2259
|
+
stmt.nonlocal_names = nonlocal_names
|
|
2260
|
+
|
|
2261
|
+
# Compute captures: free variables that come from outer scope
|
|
2262
|
+
free_names = _collect_body_name_refs(func.body)
|
|
2263
|
+
local_defs = _collect_body_local_defs(func.body)
|
|
2264
|
+
captured = sorted(
|
|
2265
|
+
(free_names - param_names - local_defs - nonlocal_names) & outer_locals
|
|
2266
|
+
)
|
|
2267
|
+
# Nonlocal names are also captures (mutable references)
|
|
2268
|
+
for name in sorted(nonlocal_names):
|
|
2269
|
+
if name not in captured:
|
|
2270
|
+
captured.append(name)
|
|
2271
|
+
stmt.captured_names = captured
|
|
2272
|
+
|
|
2273
|
+
# Create FunctionInfo and register as local function
|
|
2274
|
+
param_infos = [ParamInfo(name=pname, type=ptype) for pname, ptype in params]
|
|
2275
|
+
fi = FunctionInfo(
|
|
2276
|
+
name=func.name,
|
|
2277
|
+
params=param_infos,
|
|
2278
|
+
return_type=return_type,
|
|
2279
|
+
)
|
|
2280
|
+
|
|
2281
|
+
# Bind as FUNCTION in the local namespace
|
|
2282
|
+
if self.ctx.func.current_ns:
|
|
2283
|
+
self.ctx.func.current_ns.bind_function(fi)
|
|
2284
|
+
self.ctx.func.definitely_assigned.add(func.name)
|
|
2285
|
+
|
|
2286
|
+
# Track for escape analysis
|
|
2287
|
+
self.ctx.func.nested_def_names.add(func.name)
|
|
2288
|
+
self.ctx.func.nested_def_nodes[func.name] = stmt
|
|
2289
|
+
|
|
2290
|
+
def _collect_nonlocal_names(self, stmts: list, names: set[str]) -> None:
|
|
2291
|
+
"""Recursively collect nonlocal declarations from all nesting depths."""
|
|
2292
|
+
for s in stmts:
|
|
2293
|
+
if isinstance(s, TpyNonlocal):
|
|
2294
|
+
names.update(s.names)
|
|
2295
|
+
elif not isinstance(s, TpyNestedDef):
|
|
2296
|
+
for body in s.sub_bodies():
|
|
2297
|
+
self._collect_nonlocal_names(body, names)
|
|
2298
|
+
|
|
2299
|
+
def _resolve_literal_type(self, t: TpyType) -> TpyType:
|
|
2300
|
+
"""Resolve IntLiteralType/FloatLiteralType to concrete types."""
|
|
2301
|
+
if isinstance(t, IntLiteralType):
|
|
2302
|
+
return self.ctx.default_int_for_literal(t)
|
|
2303
|
+
if isinstance(t, FloatLiteralType):
|
|
2304
|
+
return FLOAT
|
|
2305
|
+
return t
|
|
2306
|
+
|
|
2307
|
+
def _record_for_loop_var_type(self, stmt: TpyForEach, elem_type: TpyType) -> None:
|
|
2308
|
+
"""Record the resolved loop var type for `# tpyc: type()` validation
|
|
2309
|
+
and IDE display. The actual `loop_var` binding keeps `elem_type` as-is
|
|
2310
|
+
so the body's overload resolution retains IntLiteralType-to-BigInt
|
|
2311
|
+
flexibility (heapq pattern: `for v in [literals]: heappush(h: list[int], v)`).
|
|
2312
|
+
Tuple-unpack for-loops have a synthetic `stmt.var` -- the user-facing
|
|
2313
|
+
names are recorded by the body's TpyTupleUnpack handler instead.
|
|
2314
|
+
|
|
2315
|
+
Limitation: if the body retro-widens the loop var to BigInt (heapq
|
|
2316
|
+
pattern), this recording isn't updated -- local_deduction's retro-widen
|
|
2317
|
+
path keys on var_decl_by_name, which only contains TpyVarDecl-backed
|
|
2318
|
+
names. `# tpyc: type()` on such a loop var would show the literal-
|
|
2319
|
+
defaulted Int32, not the widened BigInt.
|
|
2320
|
+
"""
|
|
2321
|
+
if not stmt.loc or stmt.is_tuple_unpack:
|
|
2322
|
+
return
|
|
2323
|
+
self.ctx.declared_var_types[(stmt.loc.line, stmt.var)] = (
|
|
2324
|
+
resolve_int_literals(elem_type, self.ctx.default_int_for_literal))
|
|
2325
|
+
|
|
2326
|
+
def _propagate_loop_body_vars(self, stmt: TpyStmt,
|
|
2327
|
+
inner_scope: 'Scope',
|
|
2328
|
+
skip_var: str | None = None) -> None:
|
|
2329
|
+
"""Store body-declared variables from a for-loop scope as pending.
|
|
2330
|
+
|
|
2331
|
+
Variables are lazily promoted to the parent scope when first
|
|
2332
|
+
referenced after the loop.
|
|
2333
|
+
|
|
2334
|
+
skip_var: loop variable name to exclude (already handled by caller).
|
|
2335
|
+
"""
|
|
2336
|
+
for name, var_type in inner_scope.bindings.items():
|
|
2337
|
+
if name == skip_var:
|
|
2338
|
+
continue
|
|
2339
|
+
if name in self.ctx.func.global_declarations:
|
|
2340
|
+
continue
|
|
2341
|
+
resolved = self._resolve_literal_type(var_type)
|
|
2342
|
+
self.ctx.func.pending_loop_vars[name] = (resolved, stmt, None)
|
|
2343
|
+
|
|
2344
|
+
# Re-parent any pending vars from nested loops to this (outer) loop,
|
|
2345
|
+
# so they get pre-declared before this loop if referenced after it.
|
|
2346
|
+
for name, (vtype, loop_stmt, orig_stmt) in list(self.ctx.func.pending_loop_vars.items()):
|
|
2347
|
+
if loop_stmt is not stmt and name not in inner_scope.bindings:
|
|
2348
|
+
self.ctx.func.pending_loop_vars[name] = (vtype, stmt, orig_stmt)
|
|
2349
|
+
|
|
2350
|
+
def _propagate_for_loop_scope(self, stmt: TpyForEach,
|
|
2351
|
+
inner_scope: 'Scope',
|
|
2352
|
+
elem_type: TpyType) -> None:
|
|
2353
|
+
"""Store for-loop variable and body-declared variables as pending."""
|
|
2354
|
+
# Loop variable (skip synthetic tuple-unpack vars in non-generators;
|
|
2355
|
+
# generators need the synthetic var as a struct field)
|
|
2356
|
+
var_name = stmt.var
|
|
2357
|
+
is_generator = (isinstance(self.ctx.func.current_function, TpyFunction)
|
|
2358
|
+
and self.ctx.func.current_function.is_generator)
|
|
2359
|
+
if ((not stmt.is_tuple_unpack or is_generator)
|
|
2360
|
+
and var_name not in self.ctx.func.global_declarations):
|
|
2361
|
+
resolved = self._resolve_literal_type(elem_type)
|
|
2362
|
+
self.ctx.func.pending_loop_vars[var_name] = (resolved, stmt, stmt)
|
|
2363
|
+
|
|
2364
|
+
self._propagate_loop_body_vars(stmt, inner_scope, skip_var=var_name)
|
|
2365
|
+
|
|
2366
|
+
def _analyze_global_stmt(self, stmt: TpyGlobal) -> None:
|
|
2367
|
+
"""Analyze a `global x, y` statement."""
|
|
2368
|
+
# Must be inside a function, not at module level
|
|
2369
|
+
if self.ctx.is_top_level or isinstance(self.ctx.func.current_function, type(MODULE_INIT_CONTEXT)):
|
|
2370
|
+
raise self.ctx.error("'global' declaration is only allowed inside a function", stmt)
|
|
2371
|
+
for name in stmt.names:
|
|
2372
|
+
# Cannot use 'global' with Final variables
|
|
2373
|
+
if name in self.ctx.final_globals:
|
|
2374
|
+
raise self.ctx.error(
|
|
2375
|
+
f"Cannot use 'global' with Final variable '{name}'", stmt)
|
|
2376
|
+
# Name must exist in global scope
|
|
2377
|
+
global_type = self.ctx.global_scope.lookup(name)
|
|
2378
|
+
if global_type is None:
|
|
2379
|
+
raise self.ctx.error(f"name '{name}' is not defined at module level", stmt)
|
|
2380
|
+
# Must not shadow a function parameter
|
|
2381
|
+
func = self.ctx.func.current_function
|
|
2382
|
+
if isinstance(func, TpyFunction):
|
|
2383
|
+
for pname, _ in func.params:
|
|
2384
|
+
if pname == name:
|
|
2385
|
+
raise self.ctx.error(
|
|
2386
|
+
f"name '{name}' is a parameter and cannot be declared global", stmt)
|
|
2387
|
+
self.ctx.func.global_declarations.add(name)
|
|
2388
|
+
|
|
2389
|
+
def validate_compile_time_constant(
|
|
2390
|
+
self, init: TpyExpr, target_type: 'TpyType | None', label: str, loc: object,
|
|
2391
|
+
) -> None:
|
|
2392
|
+
"""Validate that `init` is a compile-time constant; raise a uniform
|
|
2393
|
+
diagnostic otherwise. Shared between module-level `Final` decls and
|
|
2394
|
+
class constants -- callers pass `label` (e.g. `"Final variable 'X'"`
|
|
2395
|
+
or `"class constant 'C.X'"`) to customize the noun phrase.
|
|
2396
|
+
"""
|
|
2397
|
+
bad = self._find_nonconstant_leaf(init, target_type)
|
|
2398
|
+
if bad is None:
|
|
2399
|
+
return
|
|
2400
|
+
prefix = f"{label} requires a compile-time constant initializer"
|
|
2401
|
+
if isinstance(bad, TpyName):
|
|
2402
|
+
imp = lookup_imported(
|
|
2403
|
+
self.ctx.module_attributes, bad.name, SymbolKind.VARIABLE)
|
|
2404
|
+
if imp is not None:
|
|
2405
|
+
src_mod, _ = imp
|
|
2406
|
+
raise self.ctx.error(
|
|
2407
|
+
f"{prefix}; cross-module Final references are not yet supported "
|
|
2408
|
+
f"('{bad.name}' is imported from '{src_mod}')",
|
|
2409
|
+
loc,
|
|
2410
|
+
)
|
|
2411
|
+
if bad is not init and isinstance(bad, TpyName):
|
|
2412
|
+
raise self.ctx.error(
|
|
2413
|
+
f"{prefix}; '{bad.name}' is not a Final constant",
|
|
2414
|
+
loc,
|
|
2415
|
+
)
|
|
2416
|
+
raise self.ctx.error(prefix, loc)
|
|
2417
|
+
|
|
2418
|
+
def _find_nonconstant_leaf(self, expr: TpyExpr, target_type: 'TpyType | None' = None) -> 'TpyExpr | None':
|
|
2419
|
+
"""Find the first non-constant sub-expression in a Final initializer.
|
|
2420
|
+
|
|
2421
|
+
Returns None if the expression is a valid compile-time constant,
|
|
2422
|
+
or the offending sub-expression otherwise.
|
|
2423
|
+
|
|
2424
|
+
Accepts: literals, unary ops on constant operands, references to other
|
|
2425
|
+
Final globals, @call_macro expansions that reduce to a constant,
|
|
2426
|
+
primitive type constructor calls with constant args, binary ops on
|
|
2427
|
+
constant operands (when target_type is numeric/bool), and tuple
|
|
2428
|
+
literals with all-constant elements.
|
|
2429
|
+
"""
|
|
2430
|
+
# Unwrap compile-time macro expansions
|
|
2431
|
+
macro_exp = getattr(expr, "macro_expansion", None)
|
|
2432
|
+
if macro_exp is not None:
|
|
2433
|
+
return self._find_nonconstant_leaf(macro_exp, target_type)
|
|
2434
|
+
if isinstance(expr, (TpyIntLiteral, TpyFloatLiteral, TpyBoolLiteral, TpyStrLiteral)):
|
|
2435
|
+
return None
|
|
2436
|
+
if isinstance(expr, TpyUnaryOp):
|
|
2437
|
+
return self._find_nonconstant_leaf(expr.operand, target_type)
|
|
2438
|
+
if isinstance(expr, TpyName) and expr.name in self.ctx.analyzed_finals:
|
|
2439
|
+
return None
|
|
2440
|
+
if isinstance(expr, TpyBinOp) and is_numeric_type(target_type):
|
|
2441
|
+
return (self._find_nonconstant_leaf(expr.left, target_type)
|
|
2442
|
+
or self._find_nonconstant_leaf(expr.right, target_type))
|
|
2443
|
+
# Primitive type constructor: Float32(0.5), Int64(SOME_FINAL), etc.
|
|
2444
|
+
# Identified by: resolved to an __init__ method, result is a primitive.
|
|
2445
|
+
if isinstance(expr, TpyCall) and len(expr.args) == 1:
|
|
2446
|
+
fi = expr.resolved_function_info
|
|
2447
|
+
if fi is not None and fi.name == '__init__':
|
|
2448
|
+
result_type = expr.call_type or self.ctx.get_expr_type(expr)
|
|
2449
|
+
if is_numeric_type(result_type) or is_char_type(result_type):
|
|
2450
|
+
return self._find_nonconstant_leaf(expr.args[0], result_type)
|
|
2451
|
+
if isinstance(expr, TpyTupleLiteral):
|
|
2452
|
+
if isinstance(target_type, TupleType) and len(target_type.element_types) == len(expr.elements):
|
|
2453
|
+
for e, t in zip(expr.elements, target_type.element_types):
|
|
2454
|
+
bad = self._find_nonconstant_leaf(e, t)
|
|
2455
|
+
if bad is not None:
|
|
2456
|
+
return bad
|
|
2457
|
+
return None
|
|
2458
|
+
for e in expr.elements:
|
|
2459
|
+
bad = self._find_nonconstant_leaf(e)
|
|
2460
|
+
if bad is not None:
|
|
2461
|
+
return bad
|
|
2462
|
+
return None
|
|
2463
|
+
return expr
|
|
2464
|
+
|
|
2465
|
+
def _check_nonvalue_rebinding(self, name: str, node: TpyStmt) -> None:
|
|
2466
|
+
"""Error if reassigning a non-value-type param, loop variable, or global."""
|
|
2467
|
+
existing_type = self.ctx.func.current_scope.lookup(name)
|
|
2468
|
+
if existing_type is None or unwrap_readonly(existing_type).is_value_type():
|
|
2469
|
+
return
|
|
2470
|
+
# Check function parameters
|
|
2471
|
+
func = self.ctx.func.current_function
|
|
2472
|
+
if isinstance(func, TpyFunction):
|
|
2473
|
+
for pname, ptype in func.params:
|
|
2474
|
+
ptype_bare = unwrap_ref_type(ptype)
|
|
2475
|
+
if pname == name and not unwrap_readonly(ptype_bare).is_value_type():
|
|
2476
|
+
raise self.ctx.error(
|
|
2477
|
+
f"Cannot reassign parameter '{name}' of type '{unwrap_readonly(ptype_bare)}'; "
|
|
2478
|
+
f"assign to a new local variable instead",
|
|
2479
|
+
node
|
|
2480
|
+
)
|
|
2481
|
+
# Check for-each loop variables
|
|
2482
|
+
if name in self.ctx.func.loop_vars:
|
|
2483
|
+
raise self.ctx.error(
|
|
2484
|
+
f"Cannot reassign loop variable '{name}' of type '{existing_type}'; "
|
|
2485
|
+
f"assign to a new local variable instead",
|
|
2486
|
+
node
|
|
2487
|
+
)
|
|
2488
|
+
# Check global-declared non-value-type variables
|
|
2489
|
+
if name in self.ctx.func.global_declarations:
|
|
2490
|
+
raise self.ctx.error(
|
|
2491
|
+
f"Cannot reassign global variable '{name}' of non-value type '{existing_type}'",
|
|
2492
|
+
node
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
def _infer_new_local_view_type(
|
|
2496
|
+
self, name: str, var_type: TpyType,
|
|
2497
|
+
init_expr: TpyExpr | None, init_type: TpyType | None,
|
|
2498
|
+
line: int | None, family: ViewTypeFamily,
|
|
2499
|
+
) -> TpyType:
|
|
2500
|
+
"""Create a pending view type for a new local of the given family."""
|
|
2501
|
+
var_id = self.ctx.next_view_var_id(family)
|
|
2502
|
+
vars_reg = self.ctx.view_vars(family)
|
|
2503
|
+
var_map = self.ctx.view_var_map(family)
|
|
2504
|
+
|
|
2505
|
+
if isinstance(var_type, PendingViewType):
|
|
2506
|
+
# Alias from another pending view type -- collect ALL leaf sources.
|
|
2507
|
+
# For `x = a or b` both a and b are sources; if either resolves to
|
|
2508
|
+
# owned, x must too (otherwise x would be a view into a
|
|
2509
|
+
# potentially-reallocated buffer).
|
|
2510
|
+
if init_expr is not None:
|
|
2511
|
+
source_ids = [t.var_id for t in collect_pending_source_types(self.ctx, init_expr)
|
|
2512
|
+
if isinstance(t, family.pending_type_class)]
|
|
2513
|
+
else:
|
|
2514
|
+
source_ids = [var_type.var_id]
|
|
2515
|
+
info = ViewVarInfo(var_id=var_id, variable_name=name,
|
|
2516
|
+
decl_line=line, source_var_ids=source_ids)
|
|
2517
|
+
else:
|
|
2518
|
+
# Fresh from owned type (str or bytes)
|
|
2519
|
+
if init_expr is not None:
|
|
2520
|
+
is_owned = not self.deduction.is_view_compatible_source(init_expr, init_type)
|
|
2521
|
+
else:
|
|
2522
|
+
is_owned = False
|
|
2523
|
+
# Track source storage for subscript/field views so that
|
|
2524
|
+
# mutations on the source fall back to the owned type.
|
|
2525
|
+
source_storage: str | None = None
|
|
2526
|
+
if not is_owned and init_expr is not None:
|
|
2527
|
+
unwrapped_init = init_expr.expr if isinstance(init_expr, TpyCoerce) else init_expr
|
|
2528
|
+
if isinstance(unwrapped_init, (TpySubscript, TpyFieldAccess)):
|
|
2529
|
+
root = _borrow_storage_root(unwrapped_init)
|
|
2530
|
+
if root is not None:
|
|
2531
|
+
source_storage = self.ctx.func.borrow_tracker.effective_storage(root)
|
|
2532
|
+
info = ViewVarInfo(var_id=var_id, variable_name=name,
|
|
2533
|
+
decl_line=line, initialized_from_owned=is_owned,
|
|
2534
|
+
source_storage=source_storage)
|
|
2535
|
+
if source_storage is not None:
|
|
2536
|
+
self.ctx.view_source_borrows_map(family).setdefault(source_storage, set()).add(var_id)
|
|
2537
|
+
|
|
2538
|
+
vars_reg[var_id] = info
|
|
2539
|
+
var_map[name] = var_id
|
|
2540
|
+
self.ctx.view_pending_resolutions(family).append(var_id)
|
|
2541
|
+
return family.pending_type_class(var_id)
|
|
2542
|
+
|
|
2543
|
+
def _infer_new_local_type(
|
|
2544
|
+
self, name: str, var_type: TpyType,
|
|
2545
|
+
init_expr: TpyExpr | None, init_type: TpyType | None,
|
|
2546
|
+
line: int | None,
|
|
2547
|
+
) -> TpyType:
|
|
2548
|
+
"""Apply deferred type inference for a new local variable.
|
|
2549
|
+
|
|
2550
|
+
Handles view-type families (str/bytes -> PendingViewType) and
|
|
2551
|
+
PendingListType alias logic. Skipped at module top-level.
|
|
2552
|
+
|
|
2553
|
+
When init_expr is None (for-loop var, tuple unpack), the source is
|
|
2554
|
+
considered view-compatible (initialized_from_owned=False) because
|
|
2555
|
+
the container outlives the loop/unpack scope.
|
|
2556
|
+
"""
|
|
2557
|
+
if self.ctx.is_top_level:
|
|
2558
|
+
return var_type
|
|
2559
|
+
# Float literals resolve to float64 as new local variables
|
|
2560
|
+
if isinstance(var_type, FloatLiteralType):
|
|
2561
|
+
return FLOAT
|
|
2562
|
+
|
|
2563
|
+
# View-type families (str/bytes): register a pending view-vars entry for
|
|
2564
|
+
# deferred view-vs-owned storage resolution. For LiteralType[str/bytes] the
|
|
2565
|
+
# entry is registered for codegen but `stmt.type` stays LiteralType so
|
|
2566
|
+
# OOS rejection / dispatch / narrowing keep seeing the annotation.
|
|
2567
|
+
family = view_family_for_type(var_type)
|
|
2568
|
+
if family is not None:
|
|
2569
|
+
pending = self._infer_new_local_view_type(name, var_type, init_expr, init_type, line, family)
|
|
2570
|
+
return var_type if isinstance(var_type, LiteralType) else pending
|
|
2571
|
+
elif (isinstance(var_type, PendingListType)
|
|
2572
|
+
and init_expr is not None and isinstance(init_expr, TpyName)):
|
|
2573
|
+
return self.deduction.register_list_alias(
|
|
2574
|
+
name, var_type,
|
|
2575
|
+
decl_line=line,
|
|
2576
|
+
)
|
|
2577
|
+
|
|
2578
|
+
return var_type
|
|
2579
|
+
|
|
2580
|
+
def _analyze_yield(self, stmt: TpyYield) -> None:
|
|
2581
|
+
"""Analyze a yield statement in a generator function."""
|
|
2582
|
+
func = self.ctx.func.current_function
|
|
2583
|
+
if func is None or not func.is_generator:
|
|
2584
|
+
raise self.ctx.error("'yield' can only be used inside a generator function", stmt)
|
|
2585
|
+
elem_type = func.generator_yield_type
|
|
2586
|
+
assert elem_type is not None
|
|
2587
|
+
yield_type = self.expr.analyze_expr_with_hint(stmt.value, elem_type)
|
|
2588
|
+
stmt.value = self.compat.coerce_expr(
|
|
2589
|
+
stmt.value, yield_type, elem_type, "yield value",
|
|
2590
|
+
coercion_ctx=CoercionContext.RETURN)
|
|
2591
|
+
self.compat.check_view_return_dangle(stmt.value, elem_type, stmt.loc)
|
|
2592
|
+
|
|
2593
|
+
def _analyze_var_decl(self, stmt: TpyVarDecl) -> None:
|
|
2594
|
+
"""Analyze a variable declaration."""
|
|
2595
|
+
# In nested defs, assigning to an outer variable requires nonlocal
|
|
2596
|
+
if (self.ctx.func.in_nested_def
|
|
2597
|
+
and stmt.name in self.ctx.func.outer_scope_locals
|
|
2598
|
+
and stmt.name not in self.ctx.func.current_nonlocal_names):
|
|
2599
|
+
raise self.ctx.error(
|
|
2600
|
+
f"Cannot assign to '{stmt.name}' in nested function"
|
|
2601
|
+
f" without 'nonlocal' declaration",
|
|
2602
|
+
stmt)
|
|
2603
|
+
# Type alias expansion: replace NominalType("Shape") with the alias
|
|
2604
|
+
# target (one level; recurses into the target's inner types; _seen
|
|
2605
|
+
# guards self-referential aliases). Must run before `resolve_type`
|
|
2606
|
+
# because alias targets can contain further type references that
|
|
2607
|
+
# resolve_type wants to process uniformly. Enum substitution is
|
|
2608
|
+
# handled by `resolve_type` itself -- it replaces bare
|
|
2609
|
+
# NominalType("Color") placeholders with the registered enum
|
|
2610
|
+
# NominalType (which carries `_module_qname`), whether the enum
|
|
2611
|
+
# appears directly or is the target of a resolved alias.
|
|
2612
|
+
if stmt.type and self.ctx.registry.type_aliases:
|
|
2613
|
+
registry = self.ctx.registry
|
|
2614
|
+
recursive_names = self.ctx.recursive_union_names
|
|
2615
|
+
|
|
2616
|
+
def _expand_aliases(t: TpyType, _seen: frozenset[str] = frozenset()) -> TpyType:
|
|
2617
|
+
# Alias references are bare parser NominalType placeholders
|
|
2618
|
+
# (no _module_qname, no TypeDef entry) -- the qname guard
|
|
2619
|
+
# enforces "unresolved only," matching `_resolve_alias` in
|
|
2620
|
+
# analyzer.py. `get_type_alias` is the specific check.
|
|
2621
|
+
if (isinstance(t, NominalType)
|
|
2622
|
+
and not t.is_protocol
|
|
2623
|
+
and not t._module_qname
|
|
2624
|
+
and t.name not in _seen
|
|
2625
|
+
and t.name not in recursive_names):
|
|
2626
|
+
alias = registry.get_type_alias(t.name)
|
|
2627
|
+
if alias is not None:
|
|
2628
|
+
new_seen = _seen | {t.name}
|
|
2629
|
+
return alias.map_inner_types(
|
|
2630
|
+
lambda inner: _expand_aliases(inner, new_seen)
|
|
2631
|
+
)
|
|
2632
|
+
return t.map_inner_types(lambda inner: _expand_aliases(inner, _seen))
|
|
2633
|
+
|
|
2634
|
+
stmt.type = _expand_aliases(stmt.type)
|
|
2635
|
+
|
|
2636
|
+
# resolve_type sets is_protocol flag, upgrades NominalType -> TypeParamRef
|
|
2637
|
+
# in generic scopes, substitutes enum placeholders, and handles
|
|
2638
|
+
# compile-time-only aliases (FStr etc.).
|
|
2639
|
+
if stmt.type:
|
|
2640
|
+
stmt.type = self.type_ops.resolve_type(stmt.type)
|
|
2641
|
+
|
|
2642
|
+
# Validate the type annotation if present
|
|
2643
|
+
# Allow TypeParamRef inside generic functions or generic record methods
|
|
2644
|
+
if stmt.type:
|
|
2645
|
+
# Own[T] / readonly[T] are only valid for parameters and return
|
|
2646
|
+
# types, not variables. Run these context-specific rejections
|
|
2647
|
+
# BEFORE validate_type so a richer-context diagnostic wins over
|
|
2648
|
+
# the generic shape rejection (e.g. `Own[Optional[Polymorphic]]`
|
|
2649
|
+
# at a local should surface "Own[T] not allowed as variable",
|
|
2650
|
+
# not "use Optional[T] or Box[T]" -- the user can't use either
|
|
2651
|
+
# in a local context either).
|
|
2652
|
+
if isinstance(stmt.type, OwnType):
|
|
2653
|
+
raise self.ctx.error(
|
|
2654
|
+
f"Own[{stmt.type.wrapped}] cannot be used as a variable type. "
|
|
2655
|
+
f"Use '{stmt.type.wrapped}' instead (Own[T] is for parameters and return types only)",
|
|
2656
|
+
stmt
|
|
2657
|
+
)
|
|
2658
|
+
if isinstance(stmt.type, ReadonlyType):
|
|
2659
|
+
raise self.ctx.error(
|
|
2660
|
+
f"readonly[{stmt.type.wrapped}] cannot be used as a variable type. "
|
|
2661
|
+
f"Readonly on locals is deduced from initialization",
|
|
2662
|
+
stmt
|
|
2663
|
+
)
|
|
2664
|
+
try:
|
|
2665
|
+
in_generic = bool(
|
|
2666
|
+
(isinstance(self.ctx.func.current_function, TpyFunction) and self.ctx.func.current_function.type_params)
|
|
2667
|
+
or self.ctx.record_ctx.type_params
|
|
2668
|
+
)
|
|
2669
|
+
# An init-only function-local `p: Optional[Pet] = Dog()` lowers
|
|
2670
|
+
# to a `const Pet*` pointing at the materialized rvalue, same
|
|
2671
|
+
# shape as the parameter position. Allow the pointer-repr
|
|
2672
|
+
# @dynamic Optional here; the rvalue-rebind reject (shared slot
|
|
2673
|
+
# can't retype) and definite-assignment keep it sound. Globals
|
|
2674
|
+
# (module-init sentinel) and no-init declarations stay rejected
|
|
2675
|
+
# -- a static slot has no value representation for an abstract
|
|
2676
|
+
# base.
|
|
2677
|
+
allow_ptr_repr_dyn = (
|
|
2678
|
+
stmt.init is not None
|
|
2679
|
+
and isinstance(self.ctx.func.current_function, TpyFunction)
|
|
2680
|
+
)
|
|
2681
|
+
self.type_ops.validate_type(
|
|
2682
|
+
stmt.type, allow_type_param_ref=in_generic, loc=stmt.loc,
|
|
2683
|
+
allow_forward_ref=False,
|
|
2684
|
+
allow_pointer_repr_dynamic=allow_ptr_repr_dyn)
|
|
2685
|
+
except SemanticError as e:
|
|
2686
|
+
raise self.ctx.error(str(e), stmt)
|
|
2687
|
+
|
|
2688
|
+
# Fn[...] is only valid in parameter position
|
|
2689
|
+
if stmt.type and contains_fn_type(stmt.type):
|
|
2690
|
+
raise self.ctx.error(
|
|
2691
|
+
"Fn type is only valid in parameter position. "
|
|
2692
|
+
"Use Callable for fields, returns, and locals",
|
|
2693
|
+
stmt
|
|
2694
|
+
)
|
|
2695
|
+
|
|
2696
|
+
# Final[T] validation
|
|
2697
|
+
# Detect FinalType from annotation (covers function-level where register_globals didn't run)
|
|
2698
|
+
if stmt.type and isinstance(stmt.type, FinalType):
|
|
2699
|
+
stmt.type = final_type_str_to_strview(stmt.type.wrapped)
|
|
2700
|
+
stmt.is_final = True
|
|
2701
|
+
if stmt.is_final:
|
|
2702
|
+
if not self.ctx.is_top_level:
|
|
2703
|
+
raise self.ctx.error(
|
|
2704
|
+
f"Final can only be used at module level",
|
|
2705
|
+
stmt
|
|
2706
|
+
)
|
|
2707
|
+
if stmt.name in self.ctx.analyzed_finals:
|
|
2708
|
+
raise self.ctx.error(
|
|
2709
|
+
f"Cannot re-declare Final variable '{stmt.name}'",
|
|
2710
|
+
stmt
|
|
2711
|
+
)
|
|
2712
|
+
if not stmt.init:
|
|
2713
|
+
raise self.ctx.error(
|
|
2714
|
+
f"Final variable '{stmt.name}' must have an initializer",
|
|
2715
|
+
stmt
|
|
2716
|
+
)
|
|
2717
|
+
inner = stmt.type
|
|
2718
|
+
if not is_final_allowed_inner(inner):
|
|
2719
|
+
raise self.ctx.error(
|
|
2720
|
+
f"Final[{inner}] is not supported; {FINAL_INNER_TYPE_ERROR}",
|
|
2721
|
+
stmt
|
|
2722
|
+
)
|
|
2723
|
+
|
|
2724
|
+
if self.ctx.is_top_level and not stmt.is_final:
|
|
2725
|
+
type_hint = str(stmt.type) if stmt.type else "<type>"
|
|
2726
|
+
self._warn_all_caps_without_final(stmt.name, type_hint, stmt)
|
|
2727
|
+
|
|
2728
|
+
# Protocol types can only be used for function parameters, not variables
|
|
2729
|
+
# Exception: @dynamic protocols can be used as variable types
|
|
2730
|
+
if stmt.type and is_protocol_type(stmt.type):
|
|
2731
|
+
protocol_info = protocol_info_of(stmt.type)
|
|
2732
|
+
if not protocol_info or not protocol_info.is_dynamic:
|
|
2733
|
+
raise self.ctx.error(
|
|
2734
|
+
f"Protocol type '{stmt.type.name}' cannot be used as a variable type. "
|
|
2735
|
+
f"Only @dynamic protocols can be used as variable types",
|
|
2736
|
+
stmt
|
|
2737
|
+
)
|
|
2738
|
+
|
|
2739
|
+
# Detect native global import: x: T = native_global("name")
|
|
2740
|
+
if isinstance(stmt.init, TpyCall) and isinstance(stmt.init.func, TpyName) and self.ctx.func.current_ns:
|
|
2741
|
+
binding = self.ctx.func.current_ns.lookup(stmt.init.func_name)
|
|
2742
|
+
if (binding and binding.kind == BindingKind.IMPORTED_NAME
|
|
2743
|
+
and binding.import_source
|
|
2744
|
+
and binding.import_source[0] == "tpy.extern"
|
|
2745
|
+
and binding.import_source[1] == "native_global"):
|
|
2746
|
+
func_name = binding.import_source[1]
|
|
2747
|
+
if not self.ctx.is_top_level:
|
|
2748
|
+
raise self.ctx.error(
|
|
2749
|
+
f"{func_name}() can only be used at module level",
|
|
2750
|
+
stmt
|
|
2751
|
+
)
|
|
2752
|
+
if stmt.type is None:
|
|
2753
|
+
raise self.ctx.error(
|
|
2754
|
+
f"{func_name}() requires a type annotation",
|
|
2755
|
+
stmt
|
|
2756
|
+
)
|
|
2757
|
+
native_name = None
|
|
2758
|
+
if len(stmt.init.args) == 1:
|
|
2759
|
+
if not isinstance(stmt.init.args[0], TpyStrLiteral):
|
|
2760
|
+
raise self.ctx.error(
|
|
2761
|
+
f"{func_name}() argument must be a string literal",
|
|
2762
|
+
stmt
|
|
2763
|
+
)
|
|
2764
|
+
native_name = stmt.init.args[0].value
|
|
2765
|
+
elif len(stmt.init.args) > 1:
|
|
2766
|
+
raise self.ctx.error(
|
|
2767
|
+
f"{func_name}() takes 0 or 1 arguments",
|
|
2768
|
+
stmt
|
|
2769
|
+
)
|
|
2770
|
+
# Determine linkage from kwargs
|
|
2771
|
+
kw_binding = stmt.init.kwargs.get("binding")
|
|
2772
|
+
kw_array = stmt.init.kwargs.get("array")
|
|
2773
|
+
is_c_binding = (isinstance(kw_binding, TpyStrLiteral)
|
|
2774
|
+
and kw_binding.value == "C")
|
|
2775
|
+
is_array = (isinstance(kw_array, TpyBoolLiteral)
|
|
2776
|
+
and kw_array.value is True)
|
|
2777
|
+
if is_c_binding and is_array:
|
|
2778
|
+
stmt.linkage = VarLinkage.NATIVE_C_ARRAY
|
|
2779
|
+
elif is_c_binding:
|
|
2780
|
+
stmt.linkage = VarLinkage.NATIVE_C
|
|
2781
|
+
else:
|
|
2782
|
+
stmt.linkage = VarLinkage.NATIVE
|
|
2783
|
+
stmt.native_name = native_name
|
|
2784
|
+
stmt.init = None
|
|
2785
|
+
return
|
|
2786
|
+
|
|
2787
|
+
# Handle `global x` declarations: treat as reassignment of the global variable
|
|
2788
|
+
is_global_declared = stmt.name in self.ctx.func.global_declarations
|
|
2789
|
+
if is_global_declared:
|
|
2790
|
+
existing_type = self.ctx.global_scope.lookup(stmt.name)
|
|
2791
|
+
if stmt.type is not None:
|
|
2792
|
+
raise self.ctx.error(
|
|
2793
|
+
f"Cannot add type annotation to global variable '{stmt.name}' from inside a function",
|
|
2794
|
+
stmt
|
|
2795
|
+
)
|
|
2796
|
+
else:
|
|
2797
|
+
# Check if this is a reassignment (variable already exists in scope)
|
|
2798
|
+
existing_type = self.ctx.func.current_scope.lookup(stmt.name)
|
|
2799
|
+
# Top-level typed globals are pre-registered before statement analysis.
|
|
2800
|
+
# For an earlier unannotated write to the same name, treat this as a
|
|
2801
|
+
# fresh local write and let a later annotation retro-validate history.
|
|
2802
|
+
is_preregistered_global_write = (
|
|
2803
|
+
self.ctx.is_top_level
|
|
2804
|
+
and stmt.type is None
|
|
2805
|
+
and stmt.init is not None
|
|
2806
|
+
and stmt.name not in self.ctx.func.current_scope.bindings
|
|
2807
|
+
and stmt.name in self.ctx.global_scope.bindings
|
|
2808
|
+
and stmt.name not in self.ctx.func.authoritative_types
|
|
2809
|
+
)
|
|
2810
|
+
if is_preregistered_global_write:
|
|
2811
|
+
existing_type = None
|
|
2812
|
+
|
|
2813
|
+
# Block reassignment of Final globals at module level
|
|
2814
|
+
# (inside functions, local shadowing is allowed)
|
|
2815
|
+
if self.ctx.is_top_level and not stmt.is_final and stmt.name in self.ctx.final_globals:
|
|
2816
|
+
raise self.ctx.error(
|
|
2817
|
+
f"Cannot reassign Final variable '{stmt.name}'",
|
|
2818
|
+
stmt
|
|
2819
|
+
)
|
|
2820
|
+
|
|
2821
|
+
# Disallow reassignment of non-value-type params and loop vars
|
|
2822
|
+
if existing_type is not None:
|
|
2823
|
+
self._check_nonvalue_rebinding(stmt.name, stmt)
|
|
2824
|
+
|
|
2825
|
+
init_type: TpyType | None = None
|
|
2826
|
+
if stmt.init:
|
|
2827
|
+
# Handle empty list literal or generic type constructor with explicit type annotation
|
|
2828
|
+
# Note: [] * N is collapsed to [] in the parser
|
|
2829
|
+
is_empty_literal = isinstance(stmt.init, TpyArrayLiteral) and not stmt.init.elements
|
|
2830
|
+
_generic_td = (find_factory_by_simple_name(stmt.init.func_name)
|
|
2831
|
+
if isinstance(stmt.init, TpyCall) and isinstance(stmt.init.func, TpyName) else None)
|
|
2832
|
+
is_generic_constructor = (isinstance(stmt.init, TpyCall) and
|
|
2833
|
+
not stmt.init.args and
|
|
2834
|
+
stmt.init.call_type is None and
|
|
2835
|
+
_generic_td is not None and
|
|
2836
|
+
bool(_generic_td.param_kinds))
|
|
2837
|
+
|
|
2838
|
+
if (is_empty_literal or is_generic_constructor) and stmt.type:
|
|
2839
|
+
# Check if annotation matches the constructor's generic type
|
|
2840
|
+
annotation_matches = False
|
|
2841
|
+
if is_generic_constructor:
|
|
2842
|
+
td = find_factory_by_simple_name(stmt.init.func_name)
|
|
2843
|
+
annotation_matches = (td is not None and
|
|
2844
|
+
stmt.type.qualified_name() == td.qname)
|
|
2845
|
+
else:
|
|
2846
|
+
# Empty literal [] can match list[T] annotation
|
|
2847
|
+
annotation_matches = is_list(stmt.type)
|
|
2848
|
+
|
|
2849
|
+
if annotation_matches:
|
|
2850
|
+
if is_list(stmt.type):
|
|
2851
|
+
# list[T]: Use PendingListType for potential Array optimization
|
|
2852
|
+
elem_type = stmt.type.type_args[0]
|
|
2853
|
+
# Set call_type so codegen generates explicit type (e.g., std::vector<int>())
|
|
2854
|
+
if is_generic_constructor:
|
|
2855
|
+
stmt.init.call_type = stmt.type # type: ignore
|
|
2856
|
+
if self.ctx.func.current_function is None:
|
|
2857
|
+
# Global context: return ListType directly
|
|
2858
|
+
init_type = make_list(elem_type)
|
|
2859
|
+
else:
|
|
2860
|
+
# Function-local context: create PendingListType
|
|
2861
|
+
literal_id = self.ctx.literal_counter
|
|
2862
|
+
self.ctx.literal_counter += 1
|
|
2863
|
+
info = ListLiteralInfo(
|
|
2864
|
+
literal_id=literal_id,
|
|
2865
|
+
expr=stmt.init,
|
|
2866
|
+
element_type=elem_type,
|
|
2867
|
+
size=0,
|
|
2868
|
+
is_global=self.ctx.is_top_level,
|
|
2869
|
+
has_explicit_annotation=True,
|
|
2870
|
+
explicit_type=stmt.type
|
|
2871
|
+
)
|
|
2872
|
+
self.ctx.list_literals[literal_id] = info
|
|
2873
|
+
self.ctx.func.pending_resolutions.append(literal_id)
|
|
2874
|
+
init_type = PendingListType(elem_type, 0, literal_id)
|
|
2875
|
+
else:
|
|
2876
|
+
# Other generic types (Array, etc.): use annotation directly
|
|
2877
|
+
init_type = stmt.type
|
|
2878
|
+
# Set call_type so codegen knows the concrete template type
|
|
2879
|
+
if is_generic_constructor:
|
|
2880
|
+
stmt.init.call_type = stmt.type # type: ignore
|
|
2881
|
+
# Cache expr_type since we bypassed _analyze_expr
|
|
2882
|
+
self.ctx.set_expr_type(stmt.init, init_type)
|
|
2883
|
+
else:
|
|
2884
|
+
func_name = stmt.init.func_name if is_generic_constructor else "[]"
|
|
2885
|
+
raise self.ctx.error(
|
|
2886
|
+
f"{func_name} requires matching type annotation, got {stmt.type}", stmt
|
|
2887
|
+
)
|
|
2888
|
+
else:
|
|
2889
|
+
# Use annotation as hint, or existing type for reassignments
|
|
2890
|
+
type_hint = stmt.type if stmt.type else existing_type
|
|
2891
|
+
init_type = self.expr.analyze_expr_with_hint(stmt.init, type_hint)
|
|
2892
|
+
|
|
2893
|
+
# Deferred Final constant check: runs after init analysis so that
|
|
2894
|
+
# @call_macro expansions are available via macro_expansion attr.
|
|
2895
|
+
if stmt.is_final:
|
|
2896
|
+
self.validate_compile_time_constant(
|
|
2897
|
+
stmt.init, stmt.type, f"Final variable '{stmt.name}'", stmt,
|
|
2898
|
+
)
|
|
2899
|
+
self.ctx.analyzed_finals.add(stmt.name)
|
|
2900
|
+
|
|
2901
|
+
# Track container literal to variable mapping for mutation/type inference.
|
|
2902
|
+
# Only bind when the init is an actual literal or empty constructor,
|
|
2903
|
+
# not a name reference (aliases are handled by register_list_alias).
|
|
2904
|
+
if isinstance(init_type, PENDING_CONTAINER_TYPES) and not isinstance(stmt.init, TpyName):
|
|
2905
|
+
self.ctx.track_container_variable(
|
|
2906
|
+
stmt.name, init_type, stmt.loc.line if stmt.loc else None,
|
|
2907
|
+
)
|
|
2908
|
+
# List-specific: if explicit annotation is provided, record it
|
|
2909
|
+
if isinstance(init_type, PendingListType):
|
|
2910
|
+
if stmt.type and isinstance(stmt.init, (TpyArrayLiteral, TpyListComprehension)):
|
|
2911
|
+
info = self.ctx.list_literals[init_type.literal_id]
|
|
2912
|
+
info.has_explicit_annotation = True
|
|
2913
|
+
info.explicit_type = stmt.type
|
|
2914
|
+
|
|
2915
|
+
# Track pending generic instance to variable mapping
|
|
2916
|
+
if isinstance(init_type, PendingGenericInstanceType):
|
|
2917
|
+
self.ctx.func.variable_to_generic_instance[stmt.name] = init_type.instance_id
|
|
2918
|
+
info = self.ctx.func.pending_generic_instances.get(init_type.instance_id)
|
|
2919
|
+
if info is not None:
|
|
2920
|
+
info.variable_name = stmt.name
|
|
2921
|
+
info.decl_line = stmt.loc.line if stmt.loc else None
|
|
2922
|
+
|
|
2923
|
+
if stmt.type:
|
|
2924
|
+
if existing_type is not None:
|
|
2925
|
+
self.deduction.check_conflicting_annotation(
|
|
2926
|
+
stmt.name,
|
|
2927
|
+
stmt.type,
|
|
2928
|
+
stmt,
|
|
2929
|
+
new_line=(stmt.loc.line if stmt.loc else None),
|
|
2930
|
+
)
|
|
2931
|
+
ann_line = stmt.loc.line if stmt.loc else None
|
|
2932
|
+
self.deduction.retro_validate_against_annotation(stmt.name, stmt.type, annotation_line=ann_line)
|
|
2933
|
+
# Special case: single-char string literal can be assigned to Char
|
|
2934
|
+
if (is_char_type(stmt.type) and is_any_str_type(init_type) and
|
|
2935
|
+
isinstance(stmt.init, TpyStrLiteral) and len(stmt.init.value) == 1):
|
|
2936
|
+
pass # Allow str literal -> Char
|
|
2937
|
+
else:
|
|
2938
|
+
# Unwrap ReadonlyType for coercion -- readonly is tracked
|
|
2939
|
+
# via type deduction, not the compatibility check.
|
|
2940
|
+
inner_init = unwrap_readonly(init_type)
|
|
2941
|
+
stmt.init = self.compat.coerce_expr(stmt.init, inner_init, stmt.type,
|
|
2942
|
+
f"variable '{stmt.name}'",
|
|
2943
|
+
coercion_ctx=CoercionContext.INIT)
|
|
2944
|
+
var_type = stmt.type
|
|
2945
|
+
# Inherit ReadonlyType from init expression
|
|
2946
|
+
if isinstance(init_type, ReadonlyType) and not var_type.is_value_type():
|
|
2947
|
+
var_type = ReadonlyType(var_type)
|
|
2948
|
+
self.deduction.set_authoritative_annotation(
|
|
2949
|
+
stmt.name,
|
|
2950
|
+
stmt.type,
|
|
2951
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
2952
|
+
)
|
|
2953
|
+
elif existing_type:
|
|
2954
|
+
# If RHS analysis triggered a retro-widen on this name
|
|
2955
|
+
# (literal-seeded local promoted to a fixed-int target by
|
|
2956
|
+
# any typed-slot use), the existing_type captured before
|
|
2957
|
+
# the RHS is stale -- refresh from the scope so the
|
|
2958
|
+
# reassignment's compat check sees the now-final declared
|
|
2959
|
+
# type.
|
|
2960
|
+
if (stmt.name in self.ctx.func.retro_widened_locs
|
|
2961
|
+
and self.ctx.func.current_scope is not None):
|
|
2962
|
+
fresh = self.ctx.func.current_scope.lookup(stmt.name)
|
|
2963
|
+
if fresh is not None and fresh != existing_type:
|
|
2964
|
+
existing_type = fresh
|
|
2965
|
+
# Unwrap ReadonlyType for reassignment type resolution and
|
|
2966
|
+
# coercion -- this is a binding, not passing by reference.
|
|
2967
|
+
inner_existing = unwrap_readonly(existing_type)
|
|
2968
|
+
inner_init = unwrap_readonly(init_type)
|
|
2969
|
+
# PendingGenericInstanceType: reject reassignment while pending
|
|
2970
|
+
if isinstance(inner_existing, PendingGenericInstanceType):
|
|
2971
|
+
raise self.ctx.error(
|
|
2972
|
+
f"Cannot reassign '{stmt.name}' while its generic type is still "
|
|
2973
|
+
f"being inferred; add explicit type arguments to the constructor",
|
|
2974
|
+
stmt,
|
|
2975
|
+
)
|
|
2976
|
+
# PendingListType reassignment: different sizes force list
|
|
2977
|
+
if isinstance(inner_existing, PendingListType):
|
|
2978
|
+
if isinstance(inner_init, PendingListType):
|
|
2979
|
+
if inner_existing.size != inner_init.size:
|
|
2980
|
+
self.deduction.mark_list_different_size(inner_existing.literal_id)
|
|
2981
|
+
self.deduction.mark_list_different_size(inner_init.literal_id)
|
|
2982
|
+
else:
|
|
2983
|
+
self.deduction.link_list_literals(inner_existing.literal_id, inner_init.literal_id)
|
|
2984
|
+
var_type = existing_type
|
|
2985
|
+
# View-family reassignment (PendingViewType or LiteralType[str/bytes]).
|
|
2986
|
+
# Pre-filter with cheap isinstance tuple so non-view reassignments
|
|
2987
|
+
# (records / lists / dicts) skip the qualified_name() lookup inside
|
|
2988
|
+
# view_family_for_type.
|
|
2989
|
+
elif (isinstance(inner_existing, (PendingViewType, LiteralType))
|
|
2990
|
+
and (vf := view_family_for_type(inner_existing)) is not None):
|
|
2991
|
+
if vf.is_any_member(inner_init):
|
|
2992
|
+
if not self.deduction.is_view_compatible_source(stmt.init, inner_init):
|
|
2993
|
+
self.deduction.mark_view_reassigned_from_owned(stmt.name, vf)
|
|
2994
|
+
else:
|
|
2995
|
+
self.deduction.track_view_reassign_source(stmt.name, inner_init, vf)
|
|
2996
|
+
var_type = existing_type
|
|
2997
|
+
if isinstance(inner_existing, LiteralType):
|
|
2998
|
+
# LiteralType additionally runs coerce_expr so out-of-set
|
|
2999
|
+
# literal RHS is rejected. (PendingViewType silently widens --
|
|
3000
|
+
# tracked in BUGS.md.)
|
|
3001
|
+
stmt.init = self.compat.coerce_expr(stmt.init, inner_init, var_type,
|
|
3002
|
+
f"reassignment to '{stmt.name}'",
|
|
3003
|
+
coercion_ctx=CoercionContext.ASSIGN)
|
|
3004
|
+
else:
|
|
3005
|
+
var_type = self.deduction.resolve_reassignment_target_type(
|
|
3006
|
+
stmt.name, inner_existing, inner_init, init_expr=stmt.init
|
|
3007
|
+
)
|
|
3008
|
+
# Reassignment: check if we need to upgrade IntLiteralType
|
|
3009
|
+
if isinstance(inner_existing, IntLiteralType) and is_integer_type(var_type):
|
|
3010
|
+
# Upgrade from IntLiteralType to concrete type
|
|
3011
|
+
# Update var_types so codegen knows the resolved type
|
|
3012
|
+
orig_decl = self.ctx.func.var_decl_by_name.get(stmt.name)
|
|
3013
|
+
if orig_decl:
|
|
3014
|
+
self.ctx.var_types[id(orig_decl)] = var_type
|
|
3015
|
+
else:
|
|
3016
|
+
# Normal reassignment: use existing type, check compatibility
|
|
3017
|
+
stmt.init = self.compat.coerce_expr(stmt.init, inner_init, var_type,
|
|
3018
|
+
f"reassignment to '{stmt.name}'",
|
|
3019
|
+
coercion_ctx=CoercionContext.ASSIGN)
|
|
3020
|
+
# Readonly status flows from the value expression
|
|
3021
|
+
if isinstance(init_type, ReadonlyType) and not var_type.is_value_type():
|
|
3022
|
+
if isinstance(var_type, OptionalType):
|
|
3023
|
+
var_type = OptionalType(ReadonlyType(var_type.inner))
|
|
3024
|
+
else:
|
|
3025
|
+
var_type = ReadonlyType(var_type)
|
|
3026
|
+
if var_type != existing_type:
|
|
3027
|
+
# Keep original declaration's resolved type in sync for codegen.
|
|
3028
|
+
resolved = unwrap_readonly(var_type)
|
|
3029
|
+
orig_decl = self.ctx.func.var_decl_by_name.get(stmt.name)
|
|
3030
|
+
if orig_decl:
|
|
3031
|
+
self.ctx.var_types[id(orig_decl)] = resolved
|
|
3032
|
+
# Retroactively update declared_var_types for earlier lines
|
|
3033
|
+
# so # tpyc: type() reflects the final variable type.
|
|
3034
|
+
for key in self.ctx.declared_var_types:
|
|
3035
|
+
if key[1] == stmt.name:
|
|
3036
|
+
self.ctx.declared_var_types[key] = resolved
|
|
3037
|
+
else:
|
|
3038
|
+
# New variable: resolve IntLiteralType/FloatLiteralType.
|
|
3039
|
+
if isinstance(init_type, IntLiteralType):
|
|
3040
|
+
var_type = self.ctx.default_int_for_literal(init_type, warn_node=stmt.init)
|
|
3041
|
+
self.ctx.func.literal_default_vars.add(stmt.name)
|
|
3042
|
+
elif isinstance(init_type, FloatLiteralType):
|
|
3043
|
+
var_type = FLOAT # float literals always default to float64
|
|
3044
|
+
# Preserve OwnType on variables -- Own[T] indicates the variable
|
|
3045
|
+
# owns its storage and can be moved at last use.
|
|
3046
|
+
# Exceptions: union types need the raw type for isinstance/
|
|
3047
|
+
# narrowing codegen; optional pointer types use T* storage.
|
|
3048
|
+
elif isinstance(init_type, OwnType):
|
|
3049
|
+
inner = init_type.wrapped
|
|
3050
|
+
if (isinstance(inner, UnionType)
|
|
3051
|
+
or (isinstance(inner, OptionalType) and inner.uses_pointer_repr())):
|
|
3052
|
+
var_type = inner
|
|
3053
|
+
else:
|
|
3054
|
+
var_type = init_type
|
|
3055
|
+
# None literal without annotation -- can't infer the Optional type
|
|
3056
|
+
elif isinstance(init_type, NoneType):
|
|
3057
|
+
var_type = init_type
|
|
3058
|
+
self.ctx.func.unresolved_none_vars.add(stmt.name)
|
|
3059
|
+
else:
|
|
3060
|
+
var_type = init_type
|
|
3061
|
+
# Resolve IntLiteralType nested inside TupleType / Array / list
|
|
3062
|
+
# so `t = (1, 2)` records `tuple[Int32, Int32]` rather than
|
|
3063
|
+
# `tuple[IntLiteral(1), IntLiteral(2)]`. Codegen already lowers
|
|
3064
|
+
# the storage to concrete types via downstream passes; this
|
|
3065
|
+
# aligns sema's view so type queries return the same answer.
|
|
3066
|
+
var_type = resolve_int_literals(var_type, self.ctx.default_int_for_literal)
|
|
3067
|
+
# Strip OwnType from init_type: ownership of the source variable
|
|
3068
|
+
# doesn't transfer to the target. The target determines its own
|
|
3069
|
+
# ownership via the OwnType wrapping logic below (line ~2570).
|
|
3070
|
+
if isinstance(var_type, OwnType):
|
|
3071
|
+
var_type = var_type.wrapped
|
|
3072
|
+
# Preserve Ref on non-reassigned function locals from reference
|
|
3073
|
+
# sources (call returns, field access, subscript, params).
|
|
3074
|
+
# Strip for: reassigned locals (T* codegen), top-level globals.
|
|
3075
|
+
if (stmt.name in self.ctx.func.current_reassigned_vars
|
|
3076
|
+
or self.ctx.is_top_level):
|
|
3077
|
+
var_type = unwrap_ref_type(var_type)
|
|
3078
|
+
# Track inferred writes for potential future retro-validation.
|
|
3079
|
+
self.deduction.record_write(stmt.name, stmt.init, init_type)
|
|
3080
|
+
# Annotate tuple literal element capture modes (local context)
|
|
3081
|
+
if isinstance(stmt.init, TpyTupleLiteral) and isinstance(var_type, TupleType):
|
|
3082
|
+
self._annotate_tuple_elem_capture(stmt.init, var_type)
|
|
3083
|
+
elif stmt.type:
|
|
3084
|
+
if isinstance(stmt.type, OptionalType):
|
|
3085
|
+
# Optional without initializer is allowed (defaults to None/nullptr)
|
|
3086
|
+
var_type = stmt.type
|
|
3087
|
+
elif not stmt.type.is_value_type():
|
|
3088
|
+
raise self.ctx.error(
|
|
3089
|
+
f"Variable '{stmt.name}' of type '{stmt.type}' must have an initializer",
|
|
3090
|
+
stmt
|
|
3091
|
+
)
|
|
3092
|
+
else:
|
|
3093
|
+
var_type = stmt.type
|
|
3094
|
+
self.deduction.set_authoritative_annotation(
|
|
3095
|
+
stmt.name,
|
|
3096
|
+
stmt.type,
|
|
3097
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
3098
|
+
)
|
|
3099
|
+
else:
|
|
3100
|
+
raise self.ctx.error(f"Variable '{stmt.name}' has no type annotation and no initializer", stmt)
|
|
3101
|
+
|
|
3102
|
+
# Deferred type inference for new locals (PendingViewType, list alias, etc.)
|
|
3103
|
+
if not is_global_declared and existing_type is None:
|
|
3104
|
+
var_type = self._infer_new_local_type(
|
|
3105
|
+
stmt.name, var_type, stmt.init, init_type,
|
|
3106
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
3107
|
+
)
|
|
3108
|
+
if isinstance(var_type, PendingViewType):
|
|
3109
|
+
stmt.type = var_type
|
|
3110
|
+
|
|
3111
|
+
if is_global_declared:
|
|
3112
|
+
# Update global scope type; bind in current scope for local reads
|
|
3113
|
+
self.ctx.global_scope.define(stmt.name, var_type)
|
|
3114
|
+
self.ctx.func.current_scope.define(stmt.name, var_type)
|
|
3115
|
+
else:
|
|
3116
|
+
self.ctx.func.current_scope.define(stmt.name, var_type)
|
|
3117
|
+
# Reassignment revives a consumed variable
|
|
3118
|
+
self.ctx.func.consumed_vars.discard(stmt.name)
|
|
3119
|
+
# Reassigning a loop variable prevents const-ref binding
|
|
3120
|
+
if existing_type is not None:
|
|
3121
|
+
self.ctx.mark_loop_var_mutated(stmt.name)
|
|
3122
|
+
# Assigning a loop var to a non-value-type local takes &(var) in codegen
|
|
3123
|
+
if (stmt.init is not None and isinstance(stmt.init, TpyName)
|
|
3124
|
+
and var_type is not None and not var_type.is_value_type()):
|
|
3125
|
+
self.ctx.mark_loop_var_mutated(stmt.init.name)
|
|
3126
|
+
# Borrow tracking: reassignment breaks aliases in both directions.
|
|
3127
|
+
# `retarget_storage_borrows` runs before `remove_borrower` so that
|
|
3128
|
+
# borrowers of `name` get re-pointed to `name`'s former upstream
|
|
3129
|
+
# (with kind promoted to the most-restrictive in the chain) rather
|
|
3130
|
+
# than silently dropped. Required for chains through reassigned vars
|
|
3131
|
+
# (e.g. `view = s; s = items[1]`) to keep tracking the source.
|
|
3132
|
+
self.ctx.mark_all_view_borrowers_mutated(stmt.name)
|
|
3133
|
+
_handle_pinned_view_rebind(self.ctx, stmt.name, stmt)
|
|
3134
|
+
bt = self.ctx.func.borrow_tracker
|
|
3135
|
+
bt.retarget_storage_borrows(stmt.name)
|
|
3136
|
+
bt.remove_borrower(stmt.name)
|
|
3137
|
+
# Create borrow when the target aliases another variable's storage.
|
|
3138
|
+
# Reassigned vars register too: codegen uses T* pointer-locals, so
|
|
3139
|
+
# `view = s` aliases the storage `s` currently points into; the
|
|
3140
|
+
# retarget logic above keeps the chain valid across reassignments.
|
|
3141
|
+
if (stmt.init is not None
|
|
3142
|
+
and var_type is not None):
|
|
3143
|
+
if not var_type.is_value_type():
|
|
3144
|
+
# Non-value lvalue: y = x, v = items[i], v = obj.field
|
|
3145
|
+
init_unwrapped = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
|
|
3146
|
+
root = _borrow_storage_root(stmt.init)
|
|
3147
|
+
if root is not None:
|
|
3148
|
+
if isinstance(init_unwrapped, TpySubscript):
|
|
3149
|
+
kind = BorrowKind.ELEMENT
|
|
3150
|
+
elif isinstance(init_unwrapped, TpyFieldAccess):
|
|
3151
|
+
kind = BorrowKind.FIELD
|
|
3152
|
+
else:
|
|
3153
|
+
kind = BorrowKind.ALIAS
|
|
3154
|
+
bt.add_borrow(root, stmt.name, kind)
|
|
3155
|
+
# 8a.5: defer marking the source as mutated until the borrower is
|
|
3156
|
+
# actually written through. Deferral applies to:
|
|
3157
|
+
# - ELEMENT borrows (v = items[i])
|
|
3158
|
+
# - ALIAS/FIELD borrows whose root traces back to an ELEMENT borrow
|
|
3159
|
+
# (w = v, x = w where v = items[i]) -- checked transitively.
|
|
3160
|
+
# PTR/ITER borrows and chains not rooted at an ELEMENT mark immediately.
|
|
3161
|
+
if not (kind == BorrowKind.ELEMENT
|
|
3162
|
+
or bt.is_deferred_borrow(root)):
|
|
3163
|
+
self.ctx.mark_param_mutated(root)
|
|
3164
|
+
else:
|
|
3165
|
+
# _borrow_storage_root returned None: init is not a simple name/
|
|
3166
|
+
# subscript/field (e.g. or/and/ternary, or a deep chain like
|
|
3167
|
+
# outer.inner[i]). Use addr_taken_roots to find all root params
|
|
3168
|
+
# and mark them T& (not const T&) since we're aliasing into them.
|
|
3169
|
+
for alias_root in addr_taken_roots(stmt.init):
|
|
3170
|
+
self.ctx.mark_param_mutated(alias_root)
|
|
3171
|
+
elif isinstance(var_type, PtrType):
|
|
3172
|
+
# take_ptr(x) / Ptr(x) borrows x's storage even though Ptr is a value type
|
|
3173
|
+
init_inner = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
|
|
3174
|
+
if isinstance(init_inner, TpyCall) and len(init_inner.args) > 0:
|
|
3175
|
+
is_ptr_ctor = (init_inner.call_type is not None
|
|
3176
|
+
and init_inner.call_type.is_pointer())
|
|
3177
|
+
fi = init_inner.resolved_function_info
|
|
3178
|
+
is_vpc = fi is not None and fi.value_ptr_coercion
|
|
3179
|
+
if is_ptr_ctor or is_vpc:
|
|
3180
|
+
root = _borrow_storage_root(init_inner.args[0])
|
|
3181
|
+
if root is not None:
|
|
3182
|
+
bt.add_borrow(root, stmt.name, BorrowKind.PTR)
|
|
3183
|
+
# Direct value-to-Ptr coercion (`pp: Ptr[T] = x`): same borrow
|
|
3184
|
+
# semantics as the take_ptr / Ptr(x) explicit forms. The mutation
|
|
3185
|
+
# signal is already propagated in coerce_expr; this block adds
|
|
3186
|
+
# the borrow registration so warn_borrow_* diagnostics fire.
|
|
3187
|
+
if isinstance(stmt.init, TpyCoerce) and stmt.init.coercion.name in (
|
|
3188
|
+
"record_to_ptr", "record_to_const_ptr",
|
|
3189
|
+
"upcast_to_ptr", "upcast_to_const_ptr"):
|
|
3190
|
+
root = _borrow_storage_root(stmt.init.expr)
|
|
3191
|
+
if root is not None:
|
|
3192
|
+
bt.add_borrow(root, stmt.name, BorrowKind.PTR)
|
|
3193
|
+
elif is_span(var_type):
|
|
3194
|
+
# Span from slicing borrows the source container
|
|
3195
|
+
# (StrView excluded: str is immutable, no mutations to warn about)
|
|
3196
|
+
init_inner = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
|
|
3197
|
+
if isinstance(init_inner, TpySubscript) and isinstance(init_inner.index, TpySlice):
|
|
3198
|
+
root = _borrow_storage_root(init_inner)
|
|
3199
|
+
if root is not None:
|
|
3200
|
+
bt.add_borrow(root, stmt.name, BorrowKind.ELEMENT)
|
|
3201
|
+
elif is_str_view_type(var_type) or is_bytes_view_type(var_type):
|
|
3202
|
+
# Pinned view annotation aliasing a name source: register so
|
|
3203
|
+
# source reassignment warns. Pending views handle this via
|
|
3204
|
+
# source_mutated fall-back; the explicit annotation can't
|
|
3205
|
+
# fall back so we warn at the mutation site instead.
|
|
3206
|
+
init_inner = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
|
|
3207
|
+
if isinstance(init_inner, TpyName):
|
|
3208
|
+
self.ctx.func.pinned_view_aliases.setdefault(
|
|
3209
|
+
init_inner.name, set()).add(stmt.name)
|
|
3210
|
+
# 8b: Register call result borrow for ALL assignments (including reassignments).
|
|
3211
|
+
# Unlike general alias tracking, borrow contracts use precise return_borrows_from
|
|
3212
|
+
# facts and don't need pointer-alias analysis -- safe to apply to reassigned vars.
|
|
3213
|
+
if (stmt.init is not None
|
|
3214
|
+
and var_type is not None
|
|
3215
|
+
and not var_type.is_value_type()):
|
|
3216
|
+
init_unwrapped = stmt.init.expr if isinstance(stmt.init, TpyCoerce) else stmt.init
|
|
3217
|
+
_register_call_result_borrow(self.ctx, stmt.name, init_unwrapped)
|
|
3218
|
+
# Reassigned non-value locals generate T* local = &(source) in C++.
|
|
3219
|
+
# Mark source param as mutated so it stays T& (not const T&), regardless
|
|
3220
|
+
# of whether the borrow-tracking block above ran.
|
|
3221
|
+
if (stmt.init is not None
|
|
3222
|
+
and stmt.name in self.ctx.func.current_reassigned_vars
|
|
3223
|
+
and var_type is not None
|
|
3224
|
+
and not var_type.is_value_type()):
|
|
3225
|
+
for alias_root in addr_taken_roots(stmt.init):
|
|
3226
|
+
self.ctx.mark_param_mutated(alias_root)
|
|
3227
|
+
if stmt.init:
|
|
3228
|
+
self.init.mark_assigned(stmt.name)
|
|
3229
|
+
self.narrowing.update_after_write(stmt.name, var_type, init_type if stmt.init else None, stmt.init)
|
|
3230
|
+
# Union assignment narrowing: narrow to concrete member on initial declaration
|
|
3231
|
+
if existing_type is None:
|
|
3232
|
+
inner_var = unwrap_readonly(var_type)
|
|
3233
|
+
if (isinstance(inner_var, UnionType) and init_type is not None
|
|
3234
|
+
and init_type in inner_var.members):
|
|
3235
|
+
self.ctx.func.narrowed_types[stmt.name] = init_type
|
|
3236
|
+
facts = {stmt.name: init_type}
|
|
3237
|
+
stmt.then_type_facts = self._filter_union_codegen_facts(facts)
|
|
3238
|
+
# Record scope depth for new variables (not reassignments of outer-scope
|
|
3239
|
+
# vars). Uses scope lookup rather than var_scope_depth existence, so that
|
|
3240
|
+
# stale entries from discarded inner scopes get overwritten correctly.
|
|
3241
|
+
#
|
|
3242
|
+
# Note: when an outer-scoped variable is reassigned with an rvalue inside
|
|
3243
|
+
# an inner scope (e.g. `p = Point()` in a loop body where `p` was declared
|
|
3244
|
+
# outside), the depth stays at the outer scope. This is safe because the
|
|
3245
|
+
# codegen uses a rebind slot at the declaration scope for rvalue rebinds.
|
|
3246
|
+
if existing_type is None:
|
|
3247
|
+
self.ctx.func.var_scope_depth[stmt.name] = self.ctx.func.current_scope.depth
|
|
3248
|
+
# Update rvalue status for hoist eligibility (both new vars and reassignments)
|
|
3249
|
+
if stmt.init:
|
|
3250
|
+
if self.compat.is_lvalue(stmt.init):
|
|
3251
|
+
# Move-through: lvalue alias at last use of source promotes to rvalue.
|
|
3252
|
+
# Both target and source must be non-reassigned Tier 1 locals
|
|
3253
|
+
# (reassigned vars become T* pointer-locals in codegen).
|
|
3254
|
+
if (isinstance(stmt.init, TpyName)
|
|
3255
|
+
and existing_type is None
|
|
3256
|
+
and stmt.name not in self.ctx.func.current_reassigned_vars
|
|
3257
|
+
and stmt.init.name not in self.ctx.func.current_reassigned_vars
|
|
3258
|
+
and id(stmt.init) in self.ctx.all_last_uses
|
|
3259
|
+
and self.compat._is_owned_var(stmt.init.name)
|
|
3260
|
+
and var_type is not None
|
|
3261
|
+
and not var_type.is_value_type()
|
|
3262
|
+
and not (isinstance(var_type, OptionalType) and var_type.uses_pointer_repr())
|
|
3263
|
+
and not is_protocol_union(var_type)):
|
|
3264
|
+
self.ctx.func.rvalue_vars.add(stmt.name)
|
|
3265
|
+
self.ctx.func.owned_locals.add(stmt.name)
|
|
3266
|
+
self.ctx.func.ever_owned_locals.add(stmt.name)
|
|
3267
|
+
self.ctx.func.move_through_vars.add(stmt.name)
|
|
3268
|
+
else:
|
|
3269
|
+
self.ctx.func.rvalue_vars.discard(stmt.name)
|
|
3270
|
+
self.ctx.func.owned_locals.discard(stmt.name)
|
|
3271
|
+
else:
|
|
3272
|
+
self.ctx.func.rvalue_vars.add(stmt.name)
|
|
3273
|
+
# Track ownership: rvalue-init from value-creating expression
|
|
3274
|
+
# (constructor, Own return) vs reference-returning call.
|
|
3275
|
+
if not self._is_reference_returning_call(stmt.init):
|
|
3276
|
+
self.ctx.func.owned_locals.add(stmt.name)
|
|
3277
|
+
self.ctx.func.ever_owned_locals.add(stmt.name)
|
|
3278
|
+
else:
|
|
3279
|
+
self.ctx.func.owned_locals.discard(stmt.name)
|
|
3280
|
+
if stmt.init and _needs_provenance_tracking(var_type):
|
|
3281
|
+
self.init.mark_provenance(stmt.name, self.compat.is_param_derived_expr(stmt.init))
|
|
3282
|
+
self.init.mark_safe_to_return(
|
|
3283
|
+
stmt.name, self.compat.is_safe_to_return_expr(stmt.init))
|
|
3284
|
+
# Track non-null pointer provenance for null-check elision
|
|
3285
|
+
if stmt.init and isinstance(var_type, PtrType):
|
|
3286
|
+
is_non_null = expr_yields_non_null_ptr(stmt.init, self.ctx.func.non_null_ptr_vars)
|
|
3287
|
+
self.init.mark_non_null_ptr(stmt.name, is_non_null)
|
|
3288
|
+
|
|
3289
|
+
# Scope escape check for variable declarations (new and reassignment)
|
|
3290
|
+
if stmt.init and not var_type.is_value_type():
|
|
3291
|
+
self.scopes.check_escape(stmt.name, stmt.init, stmt)
|
|
3292
|
+
if self.ctx.func.current_ns:
|
|
3293
|
+
self.ctx.func.current_ns.bind_variable(stmt.name, var_type)
|
|
3294
|
+
# Track top-level declarations with line number for order-aware codegen
|
|
3295
|
+
# Use earliest declaration line (min) so uses between redeclarations work
|
|
3296
|
+
if self.ctx.is_top_level:
|
|
3297
|
+
decl_line = stmt.loc.line if stmt.loc else 0
|
|
3298
|
+
if stmt.name not in self.ctx.top_level_decls:
|
|
3299
|
+
self.ctx.top_level_decls[stmt.name] = decl_line
|
|
3300
|
+
else:
|
|
3301
|
+
self.ctx.top_level_decls[stmt.name] = min(self.ctx.top_level_decls[stmt.name], decl_line)
|
|
3302
|
+
# Track first var_decl for later type updates on reassignment-driven inference.
|
|
3303
|
+
if existing_type is None:
|
|
3304
|
+
self.ctx.func.var_decl_by_name[stmt.name] = stmt
|
|
3305
|
+
# Record declared type for test type-annotation validation.
|
|
3306
|
+
# Strip Own[T] and Ref[T] for display -- internal annotations, not user-facing.
|
|
3307
|
+
if stmt.loc:
|
|
3308
|
+
display_type = unwrap_ref_type(unwrap_own(var_type)) if var_type else var_type
|
|
3309
|
+
self.ctx.declared_var_types[(stmt.loc.line, stmt.name)] = display_type
|
|
3310
|
+
|
|
3311
|
+
def _is_reference_returning_call(self, expr: TpyExpr | None) -> bool:
|
|
3312
|
+
"""Check if an expression is a function call that returns a reference.
|
|
3313
|
+
|
|
3314
|
+
Returns True for function/method calls that return non-Own non-value
|
|
3315
|
+
types (i.e., they return references to existing storage).
|
|
3316
|
+
Returns False for constructors, Own[T] returns, value returns, and
|
|
3317
|
+
non-call expressions.
|
|
3318
|
+
"""
|
|
3319
|
+
if expr is None:
|
|
3320
|
+
return False
|
|
3321
|
+
if isinstance(expr, TpyCoerce):
|
|
3322
|
+
return self._is_reference_returning_call(expr.expr)
|
|
3323
|
+
if isinstance(expr, (TpyCall, TpyMethodCall)):
|
|
3324
|
+
# Constructor calls always create new values
|
|
3325
|
+
if isinstance(expr, TpyCall) and expr.call_type is not None:
|
|
3326
|
+
return False
|
|
3327
|
+
fi = expr.resolved_function_info
|
|
3328
|
+
if fi is not None and fi.return_type is not None:
|
|
3329
|
+
rt = fi.return_type
|
|
3330
|
+
# Own[T] returns are value-creating (ownership transfer)
|
|
3331
|
+
if isinstance(rt, OwnType):
|
|
3332
|
+
return False
|
|
3333
|
+
# Value types are always by-value
|
|
3334
|
+
if rt.is_value_type():
|
|
3335
|
+
return False
|
|
3336
|
+
# User-defined record constructors: call_type is None (not set
|
|
3337
|
+
# by the constructor resolution path) but fi resolves to __init__
|
|
3338
|
+
# or has the record's name as the function name.
|
|
3339
|
+
if fi.name == "__init__":
|
|
3340
|
+
return False
|
|
3341
|
+
if (isinstance(expr, TpyCall) and isinstance(expr.func, TpyName)
|
|
3342
|
+
and self.ctx.registry.find_record(expr.func.name) is not None):
|
|
3343
|
+
return False
|
|
3344
|
+
# Non-Own non-value return = reference
|
|
3345
|
+
return True
|
|
3346
|
+
return False
|
|
3347
|
+
|
|
3348
|
+
def _analyze_tuple_unpack(self, stmt: TpyTupleUnpack) -> None:
|
|
3349
|
+
"""Analyze tuple unpacking: a, b = expr."""
|
|
3350
|
+
rhs_type = self.expr.analyze_expr(stmt.value)
|
|
3351
|
+
rhs_check = rhs_type.wrapped if isinstance(rhs_type, OwnType) else rhs_type
|
|
3352
|
+
|
|
3353
|
+
if not isinstance(rhs_check, TupleType):
|
|
3354
|
+
raise self.ctx.error(
|
|
3355
|
+
f"Cannot unpack non-tuple type {rhs_type}", stmt)
|
|
3356
|
+
|
|
3357
|
+
rhs_type = rhs_check
|
|
3358
|
+
n_targets = len(stmt.targets)
|
|
3359
|
+
n_elems = len(rhs_type.element_types)
|
|
3360
|
+
if n_targets != n_elems:
|
|
3361
|
+
raise self.ctx.error(
|
|
3362
|
+
f"Cannot unpack tuple of {n_elems} elements into "
|
|
3363
|
+
f"{n_targets} targets", stmt)
|
|
3364
|
+
|
|
3365
|
+
# Per-element expressions for narrowing (range facts, etc.)
|
|
3366
|
+
has_elem_exprs = isinstance(stmt.value, TpyTupleLiteral)
|
|
3367
|
+
for i, name in enumerate(stmt.targets):
|
|
3368
|
+
elem_type = rhs_type.element_types[i]
|
|
3369
|
+
elem_expr = stmt.value.elements[i] if has_elem_exprs and i < len(stmt.value.elements) else None
|
|
3370
|
+
owned = isinstance(elem_type, OwnType)
|
|
3371
|
+
stmt.is_owned.append(owned)
|
|
3372
|
+
if owned:
|
|
3373
|
+
elem_type = elem_type.wrapped
|
|
3374
|
+
is_ref = (not owned and not elem_type.is_value_type()
|
|
3375
|
+
and not isinstance(elem_type, TypeParamRef))
|
|
3376
|
+
stmt.is_ref.append(is_ref)
|
|
3377
|
+
|
|
3378
|
+
stmt.target_types.append(elem_type)
|
|
3379
|
+
|
|
3380
|
+
if name is None:
|
|
3381
|
+
stmt.is_new.append(True)
|
|
3382
|
+
continue
|
|
3383
|
+
|
|
3384
|
+
if self.ctx.is_top_level:
|
|
3385
|
+
# Block reassignment of Final globals at module level
|
|
3386
|
+
if name in self.ctx.final_globals:
|
|
3387
|
+
raise self.ctx.error(
|
|
3388
|
+
f"Cannot reassign Final variable '{name}'",
|
|
3389
|
+
stmt)
|
|
3390
|
+
# At module level, targets become globals with namespace-scope
|
|
3391
|
+
# definitions. Mark is_new=False so codegen emits assignment
|
|
3392
|
+
# (the declaration is handled by gen_global_decl).
|
|
3393
|
+
# Resolve IntLiteralType so the global's declared type aligns
|
|
3394
|
+
# with the int32_t storage codegen emits (sema/codegen parity).
|
|
3395
|
+
elem_type = resolve_int_literals(elem_type, self.ctx.default_int_for_literal)
|
|
3396
|
+
self.ctx.global_scope.define(name, elem_type)
|
|
3397
|
+
self.ctx.func.current_scope.define(name, elem_type)
|
|
3398
|
+
self.init.mark_assigned(name)
|
|
3399
|
+
self.narrowing.update_after_write(name, elem_type, elem_type, elem_expr)
|
|
3400
|
+
if self.ctx.func.current_ns:
|
|
3401
|
+
self.ctx.func.current_ns.bind_variable(name, elem_type)
|
|
3402
|
+
decl_line = stmt.loc.line if stmt.loc else 0
|
|
3403
|
+
if name not in self.ctx.top_level_decls:
|
|
3404
|
+
self.ctx.top_level_decls[name] = decl_line
|
|
3405
|
+
self._warn_all_caps_without_final(name, str(elem_type), stmt)
|
|
3406
|
+
if stmt.loc:
|
|
3407
|
+
display_type = unwrap_own(elem_type) if elem_type else elem_type
|
|
3408
|
+
self.ctx.declared_var_types[(stmt.loc.line, name)] = display_type
|
|
3409
|
+
stmt.is_new.append(False)
|
|
3410
|
+
continue
|
|
3411
|
+
|
|
3412
|
+
existing = self.ctx.func.current_scope.lookup(name)
|
|
3413
|
+
# Don't treat globals as existing unless explicitly declared
|
|
3414
|
+
# with 'global' -- unpack should create locals by default
|
|
3415
|
+
if (existing is not None
|
|
3416
|
+
and name not in self.ctx.func.global_declarations
|
|
3417
|
+
and name not in self.ctx.func.current_scope.bindings
|
|
3418
|
+
and name in self.ctx.global_scope.bindings):
|
|
3419
|
+
existing = None
|
|
3420
|
+
if existing is not None:
|
|
3421
|
+
self._check_nonvalue_rebinding(name, stmt)
|
|
3422
|
+
self.compat.check_type_compatible(
|
|
3423
|
+
elem_type, existing, "tuple unpacking", source_expr=stmt)
|
|
3424
|
+
self.narrowing.update_after_write(name, existing, elem_type, elem_expr)
|
|
3425
|
+
stmt.is_new.append(False)
|
|
3426
|
+
else:
|
|
3427
|
+
elem_type = self._infer_new_local_type(
|
|
3428
|
+
name, elem_type, None, None,
|
|
3429
|
+
line=(stmt.loc.line if stmt.loc else None),
|
|
3430
|
+
)
|
|
3431
|
+
# Commit IntLiteralType to default_int_type at the new-local
|
|
3432
|
+
# binding site -- otherwise codegen emits invalid C++ like
|
|
3433
|
+
# `1 a = std::get<0>(...)` since IntLiteralType.to_cpp()
|
|
3434
|
+
# returns the literal value, not a type. Applied here (not in
|
|
3435
|
+
# _infer_new_local_type) so for-loop var binding still leaves
|
|
3436
|
+
# IntLiteralType in place: `for v in [...]: f(v)` where f
|
|
3437
|
+
# takes BigInt needs that flexibility (heapq pattern).
|
|
3438
|
+
elem_type = resolve_int_literals(elem_type, self.ctx.default_int_for_literal)
|
|
3439
|
+
stmt.target_types[i] = elem_type
|
|
3440
|
+
self.ctx.func.current_scope.define(name, elem_type)
|
|
3441
|
+
self.init.mark_assigned(name)
|
|
3442
|
+
self.narrowing.update_after_write(name, elem_type, elem_type, elem_expr)
|
|
3443
|
+
stmt.is_new.append(True)
|
|
3444
|
+
if stmt.loc:
|
|
3445
|
+
display_type = unwrap_own(elem_type) if elem_type else elem_type
|
|
3446
|
+
self.ctx.declared_var_types[(stmt.loc.line, name)] = display_type
|
|
3447
|
+
|
|
3448
|
+
# Register element-borrow edges so a later mutation through an
|
|
3449
|
+
# unpacked target (a, b = p; a.x = ...) traces back to the source
|
|
3450
|
+
# tuple's storage. Mirrors the deferred-element-ref pattern used for
|
|
3451
|
+
# `a = items[0]`. Two source shapes:
|
|
3452
|
+
# - name source `a, b = p` -> edges p -> a, p -> b
|
|
3453
|
+
# - tuple-literal source `a, b = (x, y)` -> edges x -> a, y -> b
|
|
3454
|
+
# Only register for slots whose unpacked type exposes a mutable
|
|
3455
|
+
# borrow surface; value-only elements never need the edge.
|
|
3456
|
+
if not self.ctx.is_top_level:
|
|
3457
|
+
bt = self.ctx.func.borrow_tracker
|
|
3458
|
+
literal_src = stmt.value if isinstance(stmt.value, TpyTupleLiteral) else None
|
|
3459
|
+
name_src_root = (_root_name_of_expr(stmt.value)
|
|
3460
|
+
if literal_src is None else None)
|
|
3461
|
+
for i, name in enumerate(stmt.targets):
|
|
3462
|
+
if name is None or not stmt.is_ref[i]:
|
|
3463
|
+
continue
|
|
3464
|
+
# Skip value-type elements (BigInt, str, ...) bound by
|
|
3465
|
+
# const-ref for copy avoidance: they have no borrow surface
|
|
3466
|
+
# the call-edge gate would care about, and registering an
|
|
3467
|
+
# edge here just bloats the borrow tracker.
|
|
3468
|
+
if not param_has_mutable_borrow_surface(stmt.target_types[i]):
|
|
3469
|
+
continue
|
|
3470
|
+
if literal_src is not None and i < len(literal_src.elements):
|
|
3471
|
+
src_root = _root_name_of_expr(literal_src.elements[i])
|
|
3472
|
+
else:
|
|
3473
|
+
src_root = name_src_root
|
|
3474
|
+
if src_root is None or src_root == name:
|
|
3475
|
+
continue
|
|
3476
|
+
bt.add_borrow(src_root, name, BorrowKind.ELEMENT)
|
|
3477
|
+
|
|
3478
|
+
# Determine const-ref eligibility per element for expensive value types.
|
|
3479
|
+
# Safe because tuples are immutable -- no in-place mutation possible.
|
|
3480
|
+
if not self.ctx.is_top_level:
|
|
3481
|
+
source_is_lvalue = isinstance(stmt.value, TpyName)
|
|
3482
|
+
source_safe = True
|
|
3483
|
+
if source_is_lvalue:
|
|
3484
|
+
source_name = stmt.value.name
|
|
3485
|
+
source_safe = (source_name not in self.ctx.func.current_reassigned_vars)
|
|
3486
|
+
for i, name in enumerate(stmt.targets):
|
|
3487
|
+
if name is None:
|
|
3488
|
+
stmt.is_const_ref.append(False)
|
|
3489
|
+
continue
|
|
3490
|
+
target_type = stmt.target_types[i]
|
|
3491
|
+
eligible = (
|
|
3492
|
+
stmt.is_new[i]
|
|
3493
|
+
and not stmt.is_ref[i]
|
|
3494
|
+
and not stmt.is_owned[i]
|
|
3495
|
+
and not isinstance(target_type, PendingViewType)
|
|
3496
|
+
and target_type.is_value_type()
|
|
3497
|
+
and target_type.is_expensive_copy()
|
|
3498
|
+
and name not in self.ctx.func.current_reassigned_vars
|
|
3499
|
+
and name not in self.ctx.func.current_aug_assigned_vars
|
|
3500
|
+
and source_safe
|
|
3501
|
+
)
|
|
3502
|
+
stmt.is_const_ref.append(eligible)
|
|
3503
|
+
|
|
3504
|
+
def _analyze_slice_assign(self, stmt: TpyAssign) -> None:
|
|
3505
|
+
"""Analyze a slice assignment: a[x:y] = rhs or a[x:y:z] = rhs."""
|
|
3506
|
+
assert isinstance(stmt.target, TpySubscript)
|
|
3507
|
+
sl = stmt.target.index
|
|
3508
|
+
assert isinstance(sl, TpySlice)
|
|
3509
|
+
|
|
3510
|
+
stepped = sl.step is not None
|
|
3511
|
+
|
|
3512
|
+
obj_type = self.expr.analyze_expr(stmt.target.obj)
|
|
3513
|
+
actual_type = unwrap_own(unwrap_readonly(unwrap_ref_type(obj_type)))
|
|
3514
|
+
|
|
3515
|
+
# Look up __setitem__(basic_slice/slice, value) overload via .py stubs
|
|
3516
|
+
result = self.expr._find_slice_setitem(actual_type, stepped=stepped)
|
|
3517
|
+
if result is None:
|
|
3518
|
+
raise self.ctx.error(
|
|
3519
|
+
f"Slice assignment not supported for type '{obj_type}'", stmt)
|
|
3520
|
+
|
|
3521
|
+
_value_param_type, fi = result
|
|
3522
|
+
stmt.target.slice_function_info = fi
|
|
3523
|
+
stmt.target.is_stepped_slice = stepped
|
|
3524
|
+
|
|
3525
|
+
self._enforce_readonly_assignment_target(stmt.target)
|
|
3526
|
+
|
|
3527
|
+
# Analyze slice bounds and validate they are integer types
|
|
3528
|
+
for bound in (sl.lower, sl.upper, sl.step):
|
|
3529
|
+
if bound is not None:
|
|
3530
|
+
bound_type = self.expr.analyze_expr(bound)
|
|
3531
|
+
if not is_any_int_type(bound_type):
|
|
3532
|
+
raise self.ctx.error(
|
|
3533
|
+
f"Slice bound must be an integer, got '{bound_type}'", bound)
|
|
3534
|
+
|
|
3535
|
+
# Use the container's element type as hint so array literals infer correctly.
|
|
3536
|
+
# Use the stub's value param type (e.g. Iterable[Own[T]]) for coercion,
|
|
3537
|
+
# which accepts any iterable and triggers copy warnings for lvalue sources.
|
|
3538
|
+
elem_type = actual_type.get_element_type()
|
|
3539
|
+
rhs_hint = make_list(elem_type) if elem_type is not None else _value_param_type
|
|
3540
|
+
self.ctx.set_expr_type(stmt.target, rhs_hint)
|
|
3541
|
+
|
|
3542
|
+
value_type = self.expr.analyze_expr_with_hint(stmt.value, rhs_hint)
|
|
3543
|
+
stmt.value = self.compat.coerce_expr(stmt.value, value_type, _value_param_type, "slice assignment",
|
|
3544
|
+
coercion_ctx=CoercionContext.ASSIGN,
|
|
3545
|
+
target_is_storage_form=True)
|
|
3546
|
+
|
|
3547
|
+
# Mutation tracking
|
|
3548
|
+
root = _root_name_of_expr(stmt.target)
|
|
3549
|
+
if root is not None:
|
|
3550
|
+
self.ctx.mark_loop_var_mutated(root)
|
|
3551
|
+
self.ctx.mark_param_mutated(root)
|
|
3552
|
+
# Slice assignment replaces a subrange -- structural mutation.
|
|
3553
|
+
self.ctx.mark_param_structurally_mutated(root)
|
|
3554
|
+
storage = self._resolve_obj_storage(stmt.target.obj)
|
|
3555
|
+
if storage is not None:
|
|
3556
|
+
if self.ctx.func.borrow_tracker.has_borrow_of_kinds(storage, (BorrowKind.ELEMENT, BorrowKind.PTR, BorrowKind.ITER)):
|
|
3557
|
+
self.ctx.warning(
|
|
3558
|
+
f"Mutation of '{storage}' while borrowed"
|
|
3559
|
+
" (slice assignment may invalidate references)", stmt)
|
|
3560
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
3561
|
+
|
|
3562
|
+
def _analyze_assign(self, stmt: TpyAssign) -> None:
|
|
3563
|
+
"""Analyze an assignment."""
|
|
3564
|
+
# In nested defs, assigning to an outer variable requires nonlocal
|
|
3565
|
+
if (self.ctx.func.in_nested_def
|
|
3566
|
+
and isinstance(stmt.target, TpyName)
|
|
3567
|
+
and stmt.target.name in self.ctx.func.outer_scope_locals
|
|
3568
|
+
and stmt.target.name not in self.ctx.func.current_nonlocal_names):
|
|
3569
|
+
raise self.ctx.error(
|
|
3570
|
+
f"Cannot assign to '{stmt.target.name}' in nested function"
|
|
3571
|
+
f" without 'nonlocal' declaration",
|
|
3572
|
+
stmt)
|
|
3573
|
+
# Slice assignment: a[x:y] = rhs -- handled separately
|
|
3574
|
+
if isinstance(stmt.target, TpySubscript) and isinstance(stmt.target.index, TpySlice):
|
|
3575
|
+
self._analyze_slice_assign(stmt)
|
|
3576
|
+
return
|
|
3577
|
+
# D16 dyn-attr write: if the target is `obj.foo` where foo is undeclared
|
|
3578
|
+
# AND the class has __setattr__, the read-side analysis would either
|
|
3579
|
+
# resolve via __getattr__ (handled below) or raise "no field". Detect
|
|
3580
|
+
# the latter by analyzing the target with a recovery path: pre-check
|
|
3581
|
+
# for an undeclared FieldAccess target on a dyn-writable class.
|
|
3582
|
+
if (isinstance(stmt.target, TpyFieldAccess)
|
|
3583
|
+
and self._target_is_dyn_writable_only(stmt.target)):
|
|
3584
|
+
self._analyze_dyn_setattr_assign(stmt)
|
|
3585
|
+
return
|
|
3586
|
+
target_type = self.expr.analyze_expr(stmt.target)
|
|
3587
|
+
self._check_class_constant_write(stmt.target, stmt)
|
|
3588
|
+
# D16 dyn-attr write detection: if the read-side analysis resolved the
|
|
3589
|
+
# target via __getattr__ (static lookup miss + dyn-readable class),
|
|
3590
|
+
# reinterpret as a __setattr__ write. Synthesize a method call and
|
|
3591
|
+
# delegate to the normal method-call analyzer so all standard arg
|
|
3592
|
+
# coercion (e.g. into-Any wrapping) is applied uniformly.
|
|
3593
|
+
if (isinstance(stmt.target, TpyFieldAccess)
|
|
3594
|
+
and stmt.target.dyn_getattr_call is not None):
|
|
3595
|
+
obj_type = self.ctx.get_expr_type(stmt.target.obj)
|
|
3596
|
+
actual = unwrap_qualifiers(obj_type) if obj_type is not None else None
|
|
3597
|
+
record = (self.ctx.registry.get_record_for_type(actual)
|
|
3598
|
+
if isinstance(actual, NominalType) else None)
|
|
3599
|
+
sa_overloads, _sa_subst = (
|
|
3600
|
+
self.protocols.lookup_record_method_overloads(record, "__setattr__")
|
|
3601
|
+
if record is not None else ([], {}))
|
|
3602
|
+
if not sa_overloads:
|
|
3603
|
+
rec_name = record.name if record is not None else str(actual)
|
|
3604
|
+
raise self.ctx.error(
|
|
3605
|
+
f"Record '{rec_name}' has no field '{stmt.target.field}' "
|
|
3606
|
+
f"and does not define __setattr__",
|
|
3607
|
+
stmt,
|
|
3608
|
+
)
|
|
3609
|
+
# Clear the read-side resolution; this is a write. Then synthesize
|
|
3610
|
+
# a __setattr__ call with the original value AST and analyze it
|
|
3611
|
+
# via the method-call path -- coerce_expr applies arg coercion
|
|
3612
|
+
# (including into-Any wrapping) on the value automatically.
|
|
3613
|
+
stmt.target.dyn_getattr_call = None
|
|
3614
|
+
setter_call = TpyMethodCall(
|
|
3615
|
+
obj=stmt.target.obj,
|
|
3616
|
+
method="__setattr__",
|
|
3617
|
+
args=[TpyStrLiteral(value=stmt.target.field), stmt.value],
|
|
3618
|
+
loc=stmt.loc,
|
|
3619
|
+
)
|
|
3620
|
+
self.expr.analyze_expr(setter_call)
|
|
3621
|
+
stmt.target.dyn_setattr_call = setter_call
|
|
3622
|
+
# Mark mutation: writing through a dyn-attr is mutating self/obj.
|
|
3623
|
+
root = _root_name_of_expr(stmt.target.obj)
|
|
3624
|
+
if root is not None:
|
|
3625
|
+
self.ctx.mark_loop_var_mutated(root)
|
|
3626
|
+
self.ctx.mark_param_mutated(root)
|
|
3627
|
+
self._enforce_readonly_assignment_target(stmt.target)
|
|
3628
|
+
return
|
|
3629
|
+
value_type = self.expr.analyze_expr_with_hint(stmt.value, target_type)
|
|
3630
|
+
# Property setter: validate and tag for codegen
|
|
3631
|
+
if isinstance(stmt.target, TpyFieldAccess) and stmt.target.is_property_access:
|
|
3632
|
+
obj_type = self.ctx.get_expr_type(stmt.target.obj)
|
|
3633
|
+
actual = unwrap_readonly(obj_type) if obj_type else None
|
|
3634
|
+
record = self.ctx.registry.get_record_for_type(actual) if isinstance(actual, NominalType) else None
|
|
3635
|
+
prop = self.protocols.lookup_record_property(record, stmt.target.field) if record else None
|
|
3636
|
+
if prop and prop.setter:
|
|
3637
|
+
stmt.target.property_setter = True
|
|
3638
|
+
# Construct setter TpyMethodCall for codegen delegation
|
|
3639
|
+
setter_name = f"set_{stmt.target.field}"
|
|
3640
|
+
setter_call = TpyMethodCall(
|
|
3641
|
+
obj=stmt.target.obj, method=setter_name, args=[stmt.value])
|
|
3642
|
+
setter_call.resolved_function_info = prop.setter
|
|
3643
|
+
stmt.target.property_setter_call = setter_call
|
|
3644
|
+
elif prop and not prop.setter:
|
|
3645
|
+
raise self.ctx.error(
|
|
3646
|
+
f"Property '{stmt.target.field}' is read-only (no setter defined)",
|
|
3647
|
+
stmt,
|
|
3648
|
+
)
|
|
3649
|
+
self._enforce_readonly_assignment_target(stmt.target)
|
|
3650
|
+
# Track mutation of for-each loop variables (prevents const-ref binding)
|
|
3651
|
+
root = _root_name_of_expr(stmt.target)
|
|
3652
|
+
if root is not None:
|
|
3653
|
+
self.ctx.mark_loop_var_mutated(root)
|
|
3654
|
+
# Through-reference writes (field/subscript) mutate the param's object;
|
|
3655
|
+
# plain name reassignment just rebinds the local.
|
|
3656
|
+
if isinstance(stmt.target, (TpyFieldAccess, TpySubscript)):
|
|
3657
|
+
self.ctx.mark_param_mutated(root)
|
|
3658
|
+
copy_warning_fired = False
|
|
3659
|
+
if isinstance(stmt.target, (TpyFieldAccess, TpySubscript)):
|
|
3660
|
+
declared_target_type = self.narrowing.declared_type_for_expr(stmt.target)
|
|
3661
|
+
if declared_target_type is not None:
|
|
3662
|
+
target_type = declared_target_type
|
|
3663
|
+
# Unified copy detection: Ref (borrowed), Own (owned at non-last-use),
|
|
3664
|
+
# or compound borrowed types (Optional/Union with pointer repr) on
|
|
3665
|
+
# the value expression type means storing it into a field/container
|
|
3666
|
+
# will copy. Ref covers params, call returns, field access, subscript.
|
|
3667
|
+
# Own covers owned locals at non-last-use.
|
|
3668
|
+
# Optional[NonValue] and Union[pointer-repr] use pointer representation
|
|
3669
|
+
# (T*, variant<A*,B*>) which is semantically borrowed -- copying into
|
|
3670
|
+
# storage (std::optional<T>, variant<A,B>) is a pointer-to-value copy.
|
|
3671
|
+
# Skip explicit copy() calls (caller acknowledged the copy) and
|
|
3672
|
+
# OwnType from non-name sources (explicit Own return from function).
|
|
3673
|
+
is_own_from_name = isinstance(value_type, OwnType) and isinstance(stmt.value, TpyName)
|
|
3674
|
+
stripped_value = self.ctx.get_expr_type(stmt.value)
|
|
3675
|
+
is_compound_ref = (
|
|
3676
|
+
(isinstance(stripped_value, OptionalType) and not stripped_value.inner.is_value_type())
|
|
3677
|
+
or (isinstance(stripped_value, UnionType) and stripped_value.uses_pointer_repr()
|
|
3678
|
+
and not stripped_value.needs_wrapper())
|
|
3679
|
+
)
|
|
3680
|
+
# Ptr[T] target takes the address of the source (`_a(&a)`), no copy.
|
|
3681
|
+
target_is_ptr = isinstance(unwrap_qualifiers(target_type), PtrType)
|
|
3682
|
+
# Any target: INTO_ANY in compatibility.py emits its own copy
|
|
3683
|
+
# warning for reference-type sources -- skip the generic
|
|
3684
|
+
# field/container warning here to avoid a double-fire.
|
|
3685
|
+
target_is_any = isinstance(unwrap_qualifiers(target_type), AnyType)
|
|
3686
|
+
if ((isinstance(value_type, RefType) or is_own_from_name or is_compound_ref)
|
|
3687
|
+
and stmt.loc is not None
|
|
3688
|
+
and not target_is_ptr
|
|
3689
|
+
and not target_is_any
|
|
3690
|
+
and not self.compat.is_copy_call(stmt.value)):
|
|
3691
|
+
inner = unwrap_qualifiers(value_type)
|
|
3692
|
+
dest = "field" if isinstance(stmt.target, TpyFieldAccess) else "container"
|
|
3693
|
+
if self.ctx.is_type_non_copyable(target_type):
|
|
3694
|
+
verb = "may copy" if isinstance(inner, TypeParamRef) else "cannot copy"
|
|
3695
|
+
if inner == target_type:
|
|
3696
|
+
msg = (f"{verb} non-copyable type '{target_type}' into "
|
|
3697
|
+
f"{dest}{NOCOPY_REMEDIATION_HINT}")
|
|
3698
|
+
else:
|
|
3699
|
+
msg = (f"{verb} {inner} into {dest} of type '{target_type}'; "
|
|
3700
|
+
f"target is non-copyable{NOCOPY_REMEDIATION_HINT}")
|
|
3701
|
+
raise self.ctx.error(msg, stmt)
|
|
3702
|
+
if isinstance(inner, TypeParamRef):
|
|
3703
|
+
msg = f"may copy {inner} into {dest} if not a value type; use copy() to make this explicit"
|
|
3704
|
+
else:
|
|
3705
|
+
msg = f"copies {inner} into {dest}; use copy() to make this explicit"
|
|
3706
|
+
self.ctx.warning(msg, stmt)
|
|
3707
|
+
copy_warning_fired = True
|
|
3708
|
+
# Own[T] param stored in a field/container — mark as consumed
|
|
3709
|
+
if isinstance(stmt.value, TpyName) and stmt.value.name in self.ctx.func.current_param_names:
|
|
3710
|
+
self.ctx.mark_own_param_consumed(stmt.value.name)
|
|
3711
|
+
# Address-escape tracking: storing a param into a mutable Ptr[T]
|
|
3712
|
+
# takes its address. Suppress the perf-default `const T&` param
|
|
3713
|
+
# emission so the `¶m -> T*` store type-checks.
|
|
3714
|
+
tgt_inner = unwrap_qualifiers(target_type)
|
|
3715
|
+
if (isinstance(stmt.target, TpyFieldAccess)
|
|
3716
|
+
and isinstance(tgt_inner, PtrType)
|
|
3717
|
+
and not tgt_inner.is_readonly):
|
|
3718
|
+
self.ctx.func.current_addr_escape_param_names.add(stmt.value.name)
|
|
3719
|
+
if isinstance(stmt.target, TpyName):
|
|
3720
|
+
# Track param rebinding (subsequent mutations target the new local, not the arg)
|
|
3721
|
+
if stmt.target.name in self.ctx.func.current_param_names:
|
|
3722
|
+
self.ctx.func.current_rebound_params.add(stmt.target.name)
|
|
3723
|
+
# Block reassignment of Final globals at module level
|
|
3724
|
+
if self.ctx.is_top_level and stmt.target.name in self.ctx.final_globals:
|
|
3725
|
+
raise self.ctx.error(
|
|
3726
|
+
f"Cannot reassign Final variable '{stmt.target.name}'",
|
|
3727
|
+
stmt
|
|
3728
|
+
)
|
|
3729
|
+
# Match var-decl flow: reject forbidden rebinding before any type mutation.
|
|
3730
|
+
self._check_nonvalue_rebinding(stmt.target.name, stmt)
|
|
3731
|
+
declared_target_type = self.ctx.func.current_scope.lookup(stmt.target.name)
|
|
3732
|
+
if declared_target_type is not None:
|
|
3733
|
+
target_type = declared_target_type
|
|
3734
|
+
# Unwrap Ref, Own, and ReadonlyType for reassignment type resolution --
|
|
3735
|
+
# this is a binding, not passing by reference.
|
|
3736
|
+
inner_target = unwrap_own(unwrap_ref_type(unwrap_readonly(target_type)))
|
|
3737
|
+
inner_value = unwrap_own(unwrap_ref_type(unwrap_readonly(value_type)))
|
|
3738
|
+
# PendingListType reassignment: different sizes force list
|
|
3739
|
+
if isinstance(inner_target, PendingListType):
|
|
3740
|
+
if isinstance(inner_value, PendingListType):
|
|
3741
|
+
if inner_target.size != inner_value.size:
|
|
3742
|
+
self.deduction.mark_list_different_size(inner_target.literal_id)
|
|
3743
|
+
self.deduction.mark_list_different_size(inner_value.literal_id)
|
|
3744
|
+
else:
|
|
3745
|
+
self.deduction.link_list_literals(inner_target.literal_id, inner_value.literal_id)
|
|
3746
|
+
# target_type stays PendingListType
|
|
3747
|
+
# PendingDictType/PendingSetType reassignment: keep pending
|
|
3748
|
+
elif isinstance(inner_target, (PendingDictType, PendingSetType)):
|
|
3749
|
+
pass # target_type stays pending
|
|
3750
|
+
# PendingViewType reassignment: track view-compatibility, keep pending
|
|
3751
|
+
elif isinstance(inner_target, PendingViewType):
|
|
3752
|
+
vf = inner_target.family
|
|
3753
|
+
if vf.is_any_member(inner_value):
|
|
3754
|
+
if not self.deduction.is_view_compatible_source(stmt.value, inner_value):
|
|
3755
|
+
self.deduction.mark_view_reassigned_from_owned(stmt.target.name, vf)
|
|
3756
|
+
else:
|
|
3757
|
+
self.deduction.track_view_reassign_source(stmt.target.name, inner_value, vf)
|
|
3758
|
+
# target_type stays pending
|
|
3759
|
+
else:
|
|
3760
|
+
target_type = self.deduction.resolve_reassignment_target_type(
|
|
3761
|
+
stmt.target.name, inner_target, inner_value, init_expr=stmt.value
|
|
3762
|
+
)
|
|
3763
|
+
# Readonly status flows from the value expression
|
|
3764
|
+
if isinstance(value_type, ReadonlyType) and not target_type.is_value_type():
|
|
3765
|
+
target_type = ReadonlyType(target_type)
|
|
3766
|
+
self.ctx.func.current_scope.define(stmt.target.name, target_type)
|
|
3767
|
+
# Reassignment revives a consumed variable
|
|
3768
|
+
self.ctx.func.consumed_vars.discard(stmt.target.name)
|
|
3769
|
+
# Borrow tracking: reassignment breaks aliases in both directions.
|
|
3770
|
+
# Retarget runs before remove_borrower so borrowers of the target
|
|
3771
|
+
# get re-pointed to the upstream source (with promoted kind),
|
|
3772
|
+
# keeping chains through reassigned vars valid.
|
|
3773
|
+
self.ctx.mark_all_view_borrowers_mutated(stmt.target.name)
|
|
3774
|
+
_handle_pinned_view_rebind(self.ctx, stmt.target.name, stmt)
|
|
3775
|
+
bt = self.ctx.func.borrow_tracker
|
|
3776
|
+
bt.retarget_storage_borrows(stmt.target.name)
|
|
3777
|
+
bt.remove_borrower(stmt.target.name)
|
|
3778
|
+
# Rebinding a non-value pointer-local generates local = &(source) in C++,
|
|
3779
|
+
# requiring source param to be T& (not const T&).
|
|
3780
|
+
if not inner_target.is_value_type() and self.compat.is_lvalue(stmt.value):
|
|
3781
|
+
for rebind_root in addr_taken_roots(stmt.value):
|
|
3782
|
+
self.ctx.mark_param_mutated(rebind_root)
|
|
3783
|
+
if self.ctx.func.current_ns:
|
|
3784
|
+
self.ctx.func.current_ns.update_variable_type(stmt.target.name, target_type)
|
|
3785
|
+
self.ctx.set_expr_type(stmt.target, target_type)
|
|
3786
|
+
self.deduction.record_write(stmt.target.name, stmt.value, inner_value)
|
|
3787
|
+
if not isinstance(inner_target, (*PENDING_CONTAINER_TYPES, PendingViewType)):
|
|
3788
|
+
resolved = unwrap_readonly(target_type)
|
|
3789
|
+
var_decl = self.ctx.func.var_decl_by_name.get(stmt.target.name)
|
|
3790
|
+
if var_decl:
|
|
3791
|
+
self.ctx.var_types[id(var_decl)] = resolved
|
|
3792
|
+
# Retroactively update declared_var_types for earlier lines
|
|
3793
|
+
# so # tpyc: type() reflects the final variable type.
|
|
3794
|
+
if resolved != unwrap_readonly(inner_target):
|
|
3795
|
+
for key in self.ctx.declared_var_types:
|
|
3796
|
+
if key[1] == stmt.target.name:
|
|
3797
|
+
self.ctx.declared_var_types[key] = resolved
|
|
3798
|
+
|
|
3799
|
+
# Disallow reassignment of non-value-type params and loop vars
|
|
3800
|
+
if isinstance(stmt.target, TpyName):
|
|
3801
|
+
# Update rvalue/ownership status for hoist eligibility and copy detection
|
|
3802
|
+
if self.compat.is_lvalue(stmt.value):
|
|
3803
|
+
self.ctx.func.rvalue_vars.discard(stmt.target.name)
|
|
3804
|
+
self.ctx.func.owned_locals.discard(stmt.target.name)
|
|
3805
|
+
else:
|
|
3806
|
+
self.ctx.func.rvalue_vars.add(stmt.target.name)
|
|
3807
|
+
if not self._is_reference_returning_call(stmt.value):
|
|
3808
|
+
self.ctx.func.owned_locals.add(stmt.target.name)
|
|
3809
|
+
self.ctx.func.ever_owned_locals.add(stmt.target.name)
|
|
3810
|
+
else:
|
|
3811
|
+
self.ctx.func.owned_locals.discard(stmt.target.name)
|
|
3812
|
+
|
|
3813
|
+
# Tuples are immutable -- reject element assignment
|
|
3814
|
+
if isinstance(stmt.target, TpySubscript):
|
|
3815
|
+
obj_type = self.ctx.get_expr_type(stmt.target.obj)
|
|
3816
|
+
if isinstance(obj_type, TupleType):
|
|
3817
|
+
raise self.ctx.error("Tuples are immutable; cannot assign to tuple elements", stmt)
|
|
3818
|
+
# Dict/TypedDict subscript assignment is always allowed
|
|
3819
|
+
actual_obj = unwrap_readonly(obj_type)
|
|
3820
|
+
is_typed_dict_target = (
|
|
3821
|
+
isinstance(actual_obj, NominalType) and actual_obj.is_record
|
|
3822
|
+
and stmt.target.typed_dict_field is not None
|
|
3823
|
+
)
|
|
3824
|
+
if not (is_dict(actual_obj) or isinstance(actual_obj, PendingDictType)) and not is_typed_dict_target:
|
|
3825
|
+
elem_type = obj_type.get_element_type()
|
|
3826
|
+
if elem_type is not None:
|
|
3827
|
+
# Span[readonly[T]] always rejects element assignment
|
|
3828
|
+
if is_span(obj_type) and is_readonly_span(obj_type):
|
|
3829
|
+
raise self.ctx.error(f"Cannot assign to elements of {obj_type} (read-only)", stmt)
|
|
3830
|
+
# Check if type conforms to MutableSequence[elem_type]
|
|
3831
|
+
mutable_seq = NominalType("MutableSequence", (elem_type,), is_protocol=True)
|
|
3832
|
+
if not self.protocols.type_conforms_to_protocol(obj_type, mutable_seq):
|
|
3833
|
+
raise self.ctx.error(f"Cannot assign to elements of {obj_type} (read-only)", stmt)
|
|
3834
|
+
|
|
3835
|
+
# Prevent assignment through read-only pointer
|
|
3836
|
+
if isinstance(stmt.target, TpyName):
|
|
3837
|
+
pass # TpyName targets are fine
|
|
3838
|
+
else:
|
|
3839
|
+
if isinstance(stmt.target, TpyFieldAccess):
|
|
3840
|
+
obj_type = self.ctx.get_expr_type(stmt.target.obj)
|
|
3841
|
+
if is_readonly_ptr(obj_type):
|
|
3842
|
+
raise self.ctx.error("Cannot assign through read-only pointer", stmt)
|
|
3843
|
+
|
|
3844
|
+
# Borrow conflict: field write on borrowed storage.
|
|
3845
|
+
# Resolves aliases so alias.field = val warns when the underlying storage
|
|
3846
|
+
# has field/element/ptr borrows.
|
|
3847
|
+
# Subscript assignment (items[i] = val, d[k] = val) is in-place and does
|
|
3848
|
+
# NOT invalidate element references: list element replacement doesn't
|
|
3849
|
+
# reallocate, and ordered_map is node-based so insertion is stable
|
|
3850
|
+
# (confirmed by @native_preserves_refs on dict.__setitem__).
|
|
3851
|
+
if isinstance(stmt.target, TpySubscript):
|
|
3852
|
+
storage = self._resolve_obj_storage(stmt.target.obj)
|
|
3853
|
+
if storage is not None:
|
|
3854
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
3855
|
+
elif isinstance(stmt.target, TpyFieldAccess):
|
|
3856
|
+
storage = self._resolve_obj_storage(stmt.target.obj)
|
|
3857
|
+
# Also check the field-path key itself (e.g. "self.items" for self.items = [...])
|
|
3858
|
+
# since borrows may be registered on the dotted key.
|
|
3859
|
+
field_storage = _storage_key(stmt.target)
|
|
3860
|
+
_BORROW_KINDS = (BorrowKind.FIELD, BorrowKind.ELEMENT, BorrowKind.PTR, BorrowKind.ITER)
|
|
3861
|
+
has_conflict = False
|
|
3862
|
+
bt = self.ctx.func.borrow_tracker
|
|
3863
|
+
if storage is not None and bt.has_borrow_of_kinds(storage, _BORROW_KINDS):
|
|
3864
|
+
has_conflict = True
|
|
3865
|
+
if not has_conflict and field_storage is not None and bt.has_borrow_of_kinds(field_storage, _BORROW_KINDS):
|
|
3866
|
+
storage = field_storage
|
|
3867
|
+
has_conflict = True
|
|
3868
|
+
if has_conflict:
|
|
3869
|
+
msg = (f"Mutation of '{storage}' while borrowed"
|
|
3870
|
+
" (field assignment may invalidate references)")
|
|
3871
|
+
self.ctx.warning(msg, stmt)
|
|
3872
|
+
if storage is not None:
|
|
3873
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
3874
|
+
if field_storage is not None and field_storage != storage:
|
|
3875
|
+
self.ctx.mark_all_view_borrowers_mutated(field_storage)
|
|
3876
|
+
|
|
3877
|
+
# PendingDictType subscript assignment: d[k] = v -- infer key/value types
|
|
3878
|
+
if isinstance(stmt.target, TpySubscript):
|
|
3879
|
+
obj_type_for_dict = self.ctx.get_expr_type(stmt.target.obj)
|
|
3880
|
+
if isinstance(obj_type_for_dict, PendingDictType):
|
|
3881
|
+
index_type = self.ctx.get_expr_type(stmt.target.index)
|
|
3882
|
+
self.deduction.infer_dict_key_value_types(
|
|
3883
|
+
stmt.target.obj, index_type, value_type)
|
|
3884
|
+
# Update obj_type and target_type if types were inferred
|
|
3885
|
+
dict_info = self.ctx.dict_literals.get(obj_type_for_dict.literal_id)
|
|
3886
|
+
if dict_info and not isinstance(dict_info.key_type, UnknownElementType):
|
|
3887
|
+
new_pending = PendingDictType(dict_info.key_type, dict_info.value_type, obj_type_for_dict.literal_id)
|
|
3888
|
+
if new_pending.key_type != obj_type_for_dict.key_type or new_pending.value_type != obj_type_for_dict.value_type:
|
|
3889
|
+
self.ctx.set_expr_type(stmt.target.obj, new_pending)
|
|
3890
|
+
if isinstance(stmt.target.obj, TpyName):
|
|
3891
|
+
if self.ctx.func.current_scope:
|
|
3892
|
+
self.ctx.func.current_scope.define(stmt.target.obj.name, new_pending)
|
|
3893
|
+
if self.ctx.func.current_ns:
|
|
3894
|
+
self.ctx.func.current_ns.bind_variable(stmt.target.obj.name, new_pending)
|
|
3895
|
+
target_type = dict_info.value_type
|
|
3896
|
+
self.ctx.set_expr_type(stmt.target, target_type)
|
|
3897
|
+
|
|
3898
|
+
# Field / container element targets store the value (storage form);
|
|
3899
|
+
# the `T -> Optional[T]` address-take that fires for borrow-form
|
|
3900
|
+
# destinations doesn't apply here.
|
|
3901
|
+
target_is_storage = isinstance(stmt.target, (TpyFieldAccess, TpySubscript))
|
|
3902
|
+
stmt.value = self.compat.coerce_expr(stmt.value, value_type, target_type, "assignment",
|
|
3903
|
+
coercion_ctx=CoercionContext.ASSIGN,
|
|
3904
|
+
target_is_storage_form=target_is_storage)
|
|
3905
|
+
# Annotate tuple literal element capture modes
|
|
3906
|
+
if isinstance(stmt.value, TpyTupleLiteral):
|
|
3907
|
+
tuple_target = own_tuple_target(target_type)
|
|
3908
|
+
if tuple_target is not None:
|
|
3909
|
+
is_field = isinstance(stmt.target, TpyFieldAccess)
|
|
3910
|
+
self._annotate_tuple_elem_capture(
|
|
3911
|
+
stmt.value, tuple_target, is_field=is_field)
|
|
3912
|
+
# Residual copy warning: reassigned vars without OwnType in scope
|
|
3913
|
+
if isinstance(stmt.target, (TpyFieldAccess, TpySubscript)):
|
|
3914
|
+
if stmt.loc is not None and not copy_warning_fired:
|
|
3915
|
+
if self._is_non_owned_var_copy(stmt.value, target_type):
|
|
3916
|
+
dest = "field" if isinstance(stmt.target, TpyFieldAccess) else "container"
|
|
3917
|
+
if self.ctx.is_type_non_copyable(target_type):
|
|
3918
|
+
verb = "may copy" if isinstance(target_type, TypeParamRef) else "cannot copy"
|
|
3919
|
+
if value_type == target_type:
|
|
3920
|
+
msg = (f"{verb} non-copyable type '{target_type}' into "
|
|
3921
|
+
f"{dest}{NOCOPY_REMEDIATION_HINT}")
|
|
3922
|
+
else:
|
|
3923
|
+
msg = (f"{verb} {value_type} into {dest} of type '{target_type}'; "
|
|
3924
|
+
f"target is non-copyable{NOCOPY_REMEDIATION_HINT}")
|
|
3925
|
+
raise self.ctx.error(msg, stmt)
|
|
3926
|
+
if isinstance(target_type, TypeParamRef):
|
|
3927
|
+
msg = f"may copy {target_type} into {dest} if not a value type; use copy() to make this explicit"
|
|
3928
|
+
else:
|
|
3929
|
+
msg = f"copies {value_type} into {dest}; use copy() to make this explicit"
|
|
3930
|
+
self.ctx.warning(msg, stmt)
|
|
3931
|
+
|
|
3932
|
+
# Scope escape check for assignments to named variables
|
|
3933
|
+
if isinstance(stmt.target, TpyName) and not target_type.is_value_type():
|
|
3934
|
+
self.scopes.check_escape(stmt.target.name, stmt.value, stmt)
|
|
3935
|
+
# Assigning a loop var to a pointer-local takes &(var) in codegen
|
|
3936
|
+
if isinstance(stmt.value, TpyName):
|
|
3937
|
+
self.ctx.mark_loop_var_mutated(stmt.value.name)
|
|
3938
|
+
|
|
3939
|
+
if isinstance(stmt.target, TpyName) and _needs_provenance_tracking(target_type):
|
|
3940
|
+
self.init.mark_provenance(stmt.target.name, self.compat.is_param_derived_expr(stmt.value))
|
|
3941
|
+
self.init.mark_safe_to_return(
|
|
3942
|
+
stmt.target.name, self.compat.is_safe_to_return_expr(stmt.value))
|
|
3943
|
+
# Track non-null pointer provenance for null-check elision
|
|
3944
|
+
if isinstance(stmt.target, TpyName) and isinstance(target_type, PtrType):
|
|
3945
|
+
is_non_null = expr_yields_non_null_ptr(stmt.value, self.ctx.func.non_null_ptr_vars)
|
|
3946
|
+
self.init.mark_non_null_ptr(stmt.target.name, is_non_null)
|
|
3947
|
+
|
|
3948
|
+
# Mark as definitely assigned for plain name targets
|
|
3949
|
+
if isinstance(stmt.target, TpyName):
|
|
3950
|
+
self.init.mark_assigned(stmt.target.name)
|
|
3951
|
+
self.narrowing.update_after_write(stmt.target.name, target_type, value_type, stmt.value)
|
|
3952
|
+
elif isinstance(stmt.target, TpyFieldAccess):
|
|
3953
|
+
self.narrowing.invalidate_for_field_write(stmt.target)
|
|
3954
|
+
|
|
3955
|
+
def _analyze_del_item(self, stmt: TpyDelItem) -> None:
|
|
3956
|
+
"""Analyze del obj[key] statement."""
|
|
3957
|
+
for subscript in stmt.targets:
|
|
3958
|
+
# Analyze obj and index separately to avoid triggering __getitem__
|
|
3959
|
+
# validation (del doesn't read the element, only deletes it).
|
|
3960
|
+
self.expr.analyze_expr(subscript.obj)
|
|
3961
|
+
# Track mutation of for-each loop variables and parameters
|
|
3962
|
+
del_root = _root_name_of_expr(subscript.obj)
|
|
3963
|
+
if del_root is not None:
|
|
3964
|
+
self.ctx.mark_loop_var_mutated(del_root)
|
|
3965
|
+
self.ctx.mark_param_mutated(del_root)
|
|
3966
|
+
# del item removes an element from the container -- structural mutation.
|
|
3967
|
+
self.ctx.mark_param_structurally_mutated(del_root)
|
|
3968
|
+
# Borrow conflict: del on a container with element-level borrows
|
|
3969
|
+
storage = self._resolve_obj_storage(subscript.obj)
|
|
3970
|
+
if storage is not None:
|
|
3971
|
+
bt = self.ctx.func.borrow_tracker
|
|
3972
|
+
if bt.has_element_borrow(storage):
|
|
3973
|
+
if bt.has_iter_borrow(storage):
|
|
3974
|
+
msg = (f"Mutation of '{storage}' while iterating over it"
|
|
3975
|
+
" ('del' invalidates the iterator)")
|
|
3976
|
+
else:
|
|
3977
|
+
msg = (f"Mutation of '{storage}' while borrowed"
|
|
3978
|
+
" ('del' may invalidate references)")
|
|
3979
|
+
self.ctx.warning(msg, stmt)
|
|
3980
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
3981
|
+
self._enforce_readonly_assignment_target(subscript)
|
|
3982
|
+
obj_type = self.ctx.get_expr_type(subscript.obj)
|
|
3983
|
+
actual = unwrap_readonly(obj_type)
|
|
3984
|
+
# Reject known-immutable/fixed-size types before analyzing the index
|
|
3985
|
+
if isinstance(actual, TupleType):
|
|
3986
|
+
raise self.ctx.error(
|
|
3987
|
+
"Tuples are immutable; cannot delete tuple elements", stmt)
|
|
3988
|
+
if is_array(actual):
|
|
3989
|
+
raise self.ctx.error(
|
|
3990
|
+
"Arrays are fixed-size; cannot delete array elements", stmt)
|
|
3991
|
+
if is_span(actual):
|
|
3992
|
+
raise self.ctx.error(
|
|
3993
|
+
"Spans are read-only views; cannot delete span elements", stmt)
|
|
3994
|
+
# Check that the type has __delitem__
|
|
3995
|
+
record_info = self.ctx.registry.get_record_for_type(actual)
|
|
3996
|
+
if record_info:
|
|
3997
|
+
overloads = record_info.get_method_overloads("__delitem__")
|
|
3998
|
+
if not overloads:
|
|
3999
|
+
raise self.ctx.error(
|
|
4000
|
+
f"'del' is not supported for type {actual}; "
|
|
4001
|
+
f"define __delitem__ to enable element deletion", stmt)
|
|
4002
|
+
else:
|
|
4003
|
+
raise self.ctx.error(
|
|
4004
|
+
f"'del' is not supported for type {actual}", stmt)
|
|
4005
|
+
# Analyze the index expression only after confirming __delitem__ exists
|
|
4006
|
+
self.expr.analyze_expr(subscript.index)
|
|
4007
|
+
|
|
4008
|
+
def _target_is_dyn_writable_only(self, target: TpyFieldAccess) -> bool:
|
|
4009
|
+
"""D16: True if `target` is `obj.foo` where foo is undeclared on the
|
|
4010
|
+
receiver's class AND the class has __setattr__ but not __getattr__.
|
|
4011
|
+
|
|
4012
|
+
In that combination the read-side analyzer would raise "no field"
|
|
4013
|
+
before reaching the assign-time dunder routing, so the assign path
|
|
4014
|
+
needs to take over BEFORE calling analyze_expr on the target.
|
|
4015
|
+
"""
|
|
4016
|
+
try:
|
|
4017
|
+
obj_type = self.expr.analyze_expr(target.obj)
|
|
4018
|
+
except SemanticError:
|
|
4019
|
+
return False
|
|
4020
|
+
actual = unwrap_qualifiers(obj_type) if obj_type is not None else None
|
|
4021
|
+
if not (isinstance(actual, NominalType) and actual.is_record):
|
|
4022
|
+
return False
|
|
4023
|
+
record = self.ctx.registry.get_record_for_type(actual)
|
|
4024
|
+
if record is None:
|
|
4025
|
+
return False
|
|
4026
|
+
# Cheap reject first: if the class has __getattr__, the standard path
|
|
4027
|
+
# (dyn_getattr_call) handles it -- this fast-path is for the
|
|
4028
|
+
# __setattr__-only case. If there is also no __setattr__, irrelevant.
|
|
4029
|
+
if self.protocols.lookup_record_method_overloads(record, "__getattr__")[0]:
|
|
4030
|
+
return False
|
|
4031
|
+
if not self.protocols.lookup_record_method_overloads(record, "__setattr__")[0]:
|
|
4032
|
+
return False
|
|
4033
|
+
# Static lookup must miss for the dyn-setattr fallback to fire.
|
|
4034
|
+
if self.protocols.lookup_record_field(record, target.field) is not None:
|
|
4035
|
+
return False
|
|
4036
|
+
if self.protocols.lookup_record_property(record, target.field) is not None:
|
|
4037
|
+
return False
|
|
4038
|
+
if self.protocols.lookup_record_method_overloads(record, target.field)[0]:
|
|
4039
|
+
return False
|
|
4040
|
+
if target.field in record.class_constants:
|
|
4041
|
+
return False
|
|
4042
|
+
return True
|
|
4043
|
+
|
|
4044
|
+
def _analyze_dyn_setattr_assign(self, stmt: TpyAssign) -> None:
|
|
4045
|
+
"""D16: write-only dyn-setattr path used when the class has __setattr__
|
|
4046
|
+
but no __getattr__ (so the read-side analyzer can't resolve the target).
|
|
4047
|
+
"""
|
|
4048
|
+
assert isinstance(stmt.target, TpyFieldAccess)
|
|
4049
|
+
setter_call = TpyMethodCall(
|
|
4050
|
+
obj=stmt.target.obj,
|
|
4051
|
+
method="__setattr__",
|
|
4052
|
+
args=[TpyStrLiteral(value=stmt.target.field), stmt.value],
|
|
4053
|
+
loc=stmt.loc,
|
|
4054
|
+
)
|
|
4055
|
+
self.expr.analyze_expr(setter_call)
|
|
4056
|
+
stmt.target.dyn_setattr_call = setter_call
|
|
4057
|
+
# Mutation tracking parallel to plain field assignment.
|
|
4058
|
+
root = _root_name_of_expr(stmt.target.obj)
|
|
4059
|
+
if root is not None:
|
|
4060
|
+
self.ctx.mark_loop_var_mutated(root)
|
|
4061
|
+
self.ctx.mark_param_mutated(root)
|
|
4062
|
+
self._enforce_readonly_assignment_target(stmt.target)
|
|
4063
|
+
|
|
4064
|
+
def _analyze_del_attr(self, stmt: 'TpyDelAttr') -> None:
|
|
4065
|
+
"""D16 Phase 3: del obj.foo, obj2.bar -- route through __delattr__.
|
|
4066
|
+
|
|
4067
|
+
Declared fields, properties, methods, and class constants are not
|
|
4068
|
+
deletable in TPy regardless of whether the class defines __delattr__
|
|
4069
|
+
(record layout is fixed). For undeclared names, route through the
|
|
4070
|
+
dunder if present.
|
|
4071
|
+
"""
|
|
4072
|
+
for target in stmt.targets:
|
|
4073
|
+
obj_type = self.expr.analyze_expr(target.obj)
|
|
4074
|
+
actual = unwrap_qualifiers(obj_type) if obj_type is not None else None
|
|
4075
|
+
if not (isinstance(actual, NominalType) and actual.is_record):
|
|
4076
|
+
raise self.ctx.error(
|
|
4077
|
+
f"Cannot delete attribute '{target.field}' on type {obj_type}",
|
|
4078
|
+
stmt,
|
|
4079
|
+
)
|
|
4080
|
+
record = self.ctx.registry.get_record_for_type(actual)
|
|
4081
|
+
if record is None:
|
|
4082
|
+
raise self.ctx.error(
|
|
4083
|
+
f"Cannot delete attribute '{target.field}' on type {obj_type}",
|
|
4084
|
+
stmt,
|
|
4085
|
+
)
|
|
4086
|
+
field_name = target.field
|
|
4087
|
+
# Reject declared-member targets uniformly.
|
|
4088
|
+
if self.protocols.lookup_record_field(record, field_name) is not None:
|
|
4089
|
+
raise self.ctx.error(
|
|
4090
|
+
f"Cannot delete declared field '{field_name}' from "
|
|
4091
|
+
f"'{record.name}' (record layout is fixed)",
|
|
4092
|
+
stmt,
|
|
4093
|
+
)
|
|
4094
|
+
if self.protocols.lookup_record_property(record, field_name) is not None:
|
|
4095
|
+
raise self.ctx.error(
|
|
4096
|
+
f"Cannot delete declared property '{field_name}' from "
|
|
4097
|
+
f"'{record.name}'",
|
|
4098
|
+
stmt,
|
|
4099
|
+
)
|
|
4100
|
+
method_overloads, _ = self.protocols.lookup_record_method_overloads(record, field_name)
|
|
4101
|
+
if method_overloads:
|
|
4102
|
+
raise self.ctx.error(
|
|
4103
|
+
f"Cannot delete method '{field_name}' from '{record.name}'",
|
|
4104
|
+
stmt,
|
|
4105
|
+
)
|
|
4106
|
+
if field_name in record.class_constants:
|
|
4107
|
+
raise self.ctx.error(
|
|
4108
|
+
f"Cannot delete class constant '{field_name}' from '{record.name}'",
|
|
4109
|
+
stmt,
|
|
4110
|
+
)
|
|
4111
|
+
# Route through __delattr__ if defined.
|
|
4112
|
+
da_overloads, _da_subst = self.protocols.lookup_record_method_overloads(
|
|
4113
|
+
record, "__delattr__")
|
|
4114
|
+
if not da_overloads:
|
|
4115
|
+
# Only mention __delattr__ when the class has already opted
|
|
4116
|
+
# into dyn-attrs (any of __getattr__/__setattr__ defined);
|
|
4117
|
+
# for a plain record, just say the field doesn't exist.
|
|
4118
|
+
has_dyn_attrs = bool(
|
|
4119
|
+
self.protocols.lookup_record_method_overloads(record, "__getattr__")[0]
|
|
4120
|
+
or self.protocols.lookup_record_method_overloads(record, "__setattr__")[0]
|
|
4121
|
+
)
|
|
4122
|
+
if has_dyn_attrs:
|
|
4123
|
+
raise self.ctx.error(
|
|
4124
|
+
f"Record '{record.name}' has no field '{field_name}' and does not "
|
|
4125
|
+
f"define __delattr__",
|
|
4126
|
+
stmt,
|
|
4127
|
+
)
|
|
4128
|
+
raise self.ctx.error(
|
|
4129
|
+
f"Record '{record.name}' has no field '{field_name}'", stmt)
|
|
4130
|
+
synth = TpyMethodCall(
|
|
4131
|
+
obj=target.obj,
|
|
4132
|
+
method="__delattr__",
|
|
4133
|
+
args=[TpyStrLiteral(value=field_name)],
|
|
4134
|
+
loc=stmt.loc,
|
|
4135
|
+
)
|
|
4136
|
+
self.expr.analyze_expr(synth)
|
|
4137
|
+
target.dyn_delattr_call = synth
|
|
4138
|
+
# Mark mutation: deletion through dunder mutates the receiver.
|
|
4139
|
+
root = _root_name_of_expr(target.obj)
|
|
4140
|
+
if root is not None:
|
|
4141
|
+
self.ctx.mark_loop_var_mutated(root)
|
|
4142
|
+
self.ctx.mark_param_mutated(root)
|
|
4143
|
+
self._enforce_readonly_assignment_target(target)
|
|
4144
|
+
|
|
4145
|
+
def _analyze_del_var(self, stmt: TpyDelVar) -> None:
|
|
4146
|
+
"""Analyze a variable deletion statement (del x)."""
|
|
4147
|
+
for name in stmt.names:
|
|
4148
|
+
# Global-declared and nonlocal vars are always reachable;
|
|
4149
|
+
# locals/params must be definitely assigned.
|
|
4150
|
+
is_external = (name in self.ctx.func.global_declarations
|
|
4151
|
+
or name in self.ctx.func.current_nonlocal_names)
|
|
4152
|
+
if not is_external and name not in self.ctx.func.definitely_assigned:
|
|
4153
|
+
raise self.ctx.error(
|
|
4154
|
+
f"variable '{name}' may not be assigned at this point", stmt)
|
|
4155
|
+
# Remove from definitely_assigned so use-after-del is caught
|
|
4156
|
+
self.ctx.func.definitely_assigned.discard(name)
|
|
4157
|
+
# Clear narrowing facts
|
|
4158
|
+
self.ctx.func.narrowed_types.pop(name, None)
|
|
4159
|
+
self.ctx.func.non_null_ptr_vars.discard(name)
|
|
4160
|
+
|
|
4161
|
+
def _apply_aug_assign_writeback(
|
|
4162
|
+
self,
|
|
4163
|
+
target: TpyExpr,
|
|
4164
|
+
target_type: TpyType,
|
|
4165
|
+
result_type: TpyType,
|
|
4166
|
+
op: str,
|
|
4167
|
+
stmt: TpyAugAssign,
|
|
4168
|
+
) -> None:
|
|
4169
|
+
"""Check and apply the write-back step of an augmented assignment.
|
|
4170
|
+
|
|
4171
|
+
After the binop is resolved with result_type, verifies result_type is
|
|
4172
|
+
compatible with the target and updates variable caches when widening applies.
|
|
4173
|
+
Uses the same type rules as regular assignment (resolve_reassignment_target_type
|
|
4174
|
+
+ check_type_compatible), so annotated variables and non-wideneable pairs
|
|
4175
|
+
produce a standard type mismatch error.
|
|
4176
|
+
"""
|
|
4177
|
+
if result_type == target_type:
|
|
4178
|
+
return
|
|
4179
|
+
if isinstance(target, TpyName):
|
|
4180
|
+
name = target.name
|
|
4181
|
+
effective_type = self.deduction.resolve_reassignment_target_type(
|
|
4182
|
+
name, target_type, result_type,
|
|
4183
|
+
)
|
|
4184
|
+
# check_type_compatible errors when effective_type refused widening
|
|
4185
|
+
# (e.g. annotated variable, or mixed-sign fixed-int pair).
|
|
4186
|
+
self.compat.check_type_compatible(
|
|
4187
|
+
result_type, effective_type, f"'{op}=' to '{name}'", loc=stmt.loc,
|
|
4188
|
+
)
|
|
4189
|
+
if effective_type != target_type:
|
|
4190
|
+
if self.ctx.func.current_scope:
|
|
4191
|
+
self.ctx.func.current_scope.define(name, effective_type)
|
|
4192
|
+
var_decl = self.ctx.func.var_decl_by_name.get(name)
|
|
4193
|
+
if var_decl:
|
|
4194
|
+
self.ctx.var_types[id(var_decl)] = effective_type
|
|
4195
|
+
for key in self.ctx.declared_var_types:
|
|
4196
|
+
if key[1] == name:
|
|
4197
|
+
self.ctx.declared_var_types[key] = effective_type
|
|
4198
|
+
else:
|
|
4199
|
+
# Subscript/field target: element type is fixed, cannot widen.
|
|
4200
|
+
self.compat.check_type_compatible(
|
|
4201
|
+
result_type, target_type,
|
|
4202
|
+
f"'{op}=' to {_format_aug_target(target)}", loc=stmt.loc,
|
|
4203
|
+
target_is_storage_form=True,
|
|
4204
|
+
)
|
|
4205
|
+
|
|
4206
|
+
def _is_non_owned_var_copy(self, expr: TpyExpr, target_type: TpyType) -> bool:
|
|
4207
|
+
"""Check if storing a non-OwnType variable copies into storage.
|
|
4208
|
+
|
|
4209
|
+
Covers locals excluded from OwnType wrapping: reassigned vars,
|
|
4210
|
+
union types, Optional with pointer repr. These are not caught by
|
|
4211
|
+
the unified Ref/Own check.
|
|
4212
|
+
"""
|
|
4213
|
+
if target_type.is_value_type():
|
|
4214
|
+
return False
|
|
4215
|
+
if self.compat._is_value_type_param(target_type):
|
|
4216
|
+
return False
|
|
4217
|
+
if self.compat.is_copy_call(expr):
|
|
4218
|
+
return False
|
|
4219
|
+
if not isinstance(expr, TpyName):
|
|
4220
|
+
return False
|
|
4221
|
+
scope_type = self.ctx.func.current_scope.lookup(expr.name) if self.ctx.func.current_scope else None
|
|
4222
|
+
if scope_type is not None and isinstance(scope_type, (RefType, OwnType)):
|
|
4223
|
+
return False # already caught by the Ref/Own check
|
|
4224
|
+
if scope_type is not None and scope_type.is_value_type():
|
|
4225
|
+
return False
|
|
4226
|
+
# Skip at last-use of movable var
|
|
4227
|
+
if id(expr) in self.ctx.all_last_uses and self.compat._is_owned_var(expr.name):
|
|
4228
|
+
return False
|
|
4229
|
+
if scope_type is None:
|
|
4230
|
+
return False
|
|
4231
|
+
return True
|
|
4232
|
+
|
|
4233
|
+
def _analyze_aug_assign(self, stmt: TpyAugAssign) -> None:
|
|
4234
|
+
"""Analyze an augmented assignment (+=, -=, etc.)."""
|
|
4235
|
+
# In nested defs, aug-assign to an outer variable requires nonlocal
|
|
4236
|
+
if (self.ctx.func.in_nested_def
|
|
4237
|
+
and isinstance(stmt.target, TpyName)
|
|
4238
|
+
and stmt.target.name in self.ctx.func.outer_scope_locals
|
|
4239
|
+
and stmt.target.name not in self.ctx.func.current_nonlocal_names):
|
|
4240
|
+
raise self.ctx.error(
|
|
4241
|
+
f"Cannot modify '{stmt.target.name}' in nested function"
|
|
4242
|
+
f" without 'nonlocal' declaration",
|
|
4243
|
+
stmt)
|
|
4244
|
+
# Block augmented assignment of Final globals at module level
|
|
4245
|
+
if isinstance(stmt.target, TpyName) and self.ctx.is_top_level and stmt.target.name in self.ctx.final_globals:
|
|
4246
|
+
raise self.ctx.error(
|
|
4247
|
+
f"Cannot reassign Final variable '{stmt.target.name}'",
|
|
4248
|
+
stmt
|
|
4249
|
+
)
|
|
4250
|
+
target_type = unwrap_own(unwrap_ref_type(self.expr.analyze_expr(stmt.target)))
|
|
4251
|
+
# Augmented assignment on properties not yet supported
|
|
4252
|
+
if isinstance(stmt.target, TpyFieldAccess) and stmt.target.is_property_access:
|
|
4253
|
+
raise self.ctx.error(
|
|
4254
|
+
f"Augmented assignment on property '{stmt.target.field}' is not yet supported",
|
|
4255
|
+
stmt,
|
|
4256
|
+
)
|
|
4257
|
+
self._check_class_constant_write(stmt.target, stmt)
|
|
4258
|
+
# Aug-assign replaces the target's value with a freshly computed one
|
|
4259
|
+
# (owned str/bytes concat, reallocated list, etc.), so any prior
|
|
4260
|
+
# param-derived / safe-to-return provenance is now stale and must be
|
|
4261
|
+
# cleared -- otherwise a later `return` as a view would pass the
|
|
4262
|
+
# dangling check despite pointing into local storage.
|
|
4263
|
+
if isinstance(stmt.target, TpyName) and _needs_provenance_tracking(target_type):
|
|
4264
|
+
self.init.mark_provenance(stmt.target.name, False)
|
|
4265
|
+
self.init.mark_safe_to_return(stmt.target.name, False)
|
|
4266
|
+
value_type = self.expr.analyze_expr_with_hint(stmt.value, target_type)
|
|
4267
|
+
# Track mutation of for-each loop variables and parameters
|
|
4268
|
+
aug_root = _root_name_of_expr(stmt.target)
|
|
4269
|
+
if aug_root is not None:
|
|
4270
|
+
self.ctx.mark_loop_var_mutated(aug_root)
|
|
4271
|
+
self.ctx.mark_param_mutated(aug_root)
|
|
4272
|
+
# items += other_list extends the container in-place (structural mutation).
|
|
4273
|
+
# items[i] += x and obj.field += x are in-place element/field writes -- not structural.
|
|
4274
|
+
if isinstance(stmt.target, TpyName):
|
|
4275
|
+
self.ctx.mark_param_structurally_mutated(aug_root)
|
|
4276
|
+
self._enforce_readonly_assignment_target(stmt.target)
|
|
4277
|
+
# Borrow conflict: augmented assignment may mutate borrowed storage.
|
|
4278
|
+
# Subscript aug-assign (items[i] += x) modifies element in-place -- same as
|
|
4279
|
+
# subscript assign, no reallocation, element/ptr borrows remain valid.
|
|
4280
|
+
if isinstance(stmt.target, TpySubscript):
|
|
4281
|
+
storage = self._resolve_obj_storage(stmt.target.obj)
|
|
4282
|
+
if storage is not None:
|
|
4283
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
4284
|
+
elif isinstance(stmt.target, TpyFieldAccess):
|
|
4285
|
+
storage = self._resolve_obj_storage(stmt.target.obj)
|
|
4286
|
+
field_storage = _storage_key(stmt.target)
|
|
4287
|
+
_BORROW_KINDS = (BorrowKind.FIELD, BorrowKind.ELEMENT, BorrowKind.PTR, BorrowKind.ITER)
|
|
4288
|
+
has_conflict = False
|
|
4289
|
+
bt = self.ctx.func.borrow_tracker
|
|
4290
|
+
if storage is not None and bt.has_borrow_of_kinds(storage, _BORROW_KINDS):
|
|
4291
|
+
has_conflict = True
|
|
4292
|
+
if not has_conflict and field_storage is not None and bt.has_borrow_of_kinds(field_storage, _BORROW_KINDS):
|
|
4293
|
+
storage = field_storage
|
|
4294
|
+
has_conflict = True
|
|
4295
|
+
if has_conflict:
|
|
4296
|
+
self.ctx.warning(
|
|
4297
|
+
f"Mutation of '{storage}' while borrowed"
|
|
4298
|
+
" (field assignment may invalidate references)",
|
|
4299
|
+
stmt,
|
|
4300
|
+
)
|
|
4301
|
+
if storage is not None:
|
|
4302
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
4303
|
+
if field_storage is not None and field_storage != storage:
|
|
4304
|
+
self.ctx.mark_all_view_borrowers_mutated(field_storage)
|
|
4305
|
+
# Borrow conflict: aug-assign on a name target that has element borrows.
|
|
4306
|
+
# Any structural aug-assign (list +=, set |=, user-defined __iadd__ that
|
|
4307
|
+
# reallocates) is a mutation -- check the borrow state, not the container type.
|
|
4308
|
+
elif isinstance(stmt.target, TpyName):
|
|
4309
|
+
bt = self.ctx.func.borrow_tracker
|
|
4310
|
+
storage = bt.effective_storage(stmt.target.name)
|
|
4311
|
+
if bt.has_element_borrow(storage):
|
|
4312
|
+
if bt.has_iter_borrow(storage):
|
|
4313
|
+
self.ctx.warning(
|
|
4314
|
+
f"Mutation of '{storage}' while iterating over it"
|
|
4315
|
+
f" ('{stmt.op}=' invalidates the iterator)",
|
|
4316
|
+
stmt,
|
|
4317
|
+
)
|
|
4318
|
+
else:
|
|
4319
|
+
self.ctx.warning(
|
|
4320
|
+
f"Mutation of '{storage}' while borrowed"
|
|
4321
|
+
f" ('{stmt.op}=' may invalidate references)",
|
|
4322
|
+
stmt,
|
|
4323
|
+
)
|
|
4324
|
+
self.ctx.mark_all_view_borrowers_mutated(storage)
|
|
4325
|
+
if (
|
|
4326
|
+
isinstance(stmt.target, TpyName)
|
|
4327
|
+
and is_big_int_type(target_type)
|
|
4328
|
+
and is_fixed_int_type(value_type)
|
|
4329
|
+
and stmt.target.name in self.ctx.func.literal_default_vars
|
|
4330
|
+
):
|
|
4331
|
+
type_name = str(value_type)
|
|
4332
|
+
self.ctx.warning(
|
|
4333
|
+
f"Augmented assignment does not narrow '{stmt.target.name}' from int to {type_name}; "
|
|
4334
|
+
f"variable remains int (BigInt). Annotate or initialize '{stmt.target.name}' as {type_name} "
|
|
4335
|
+
f"to keep {type_name} arithmetic.",
|
|
4336
|
+
stmt,
|
|
4337
|
+
)
|
|
4338
|
+
# Literal-typed targets reject augmented assignment outright: the result
|
|
4339
|
+
# of `x += y` is rarely in the declared value set, and Literal[str] locals
|
|
4340
|
+
# use std::string_view storage which can't hold a new owned string anyway.
|
|
4341
|
+
if isinstance(target_type, LiteralType):
|
|
4342
|
+
raise self.ctx.error(
|
|
4343
|
+
f"Augmented assignment is not supported for Literal[...] -- the "
|
|
4344
|
+
f"result is not guaranteed to be in the declared value set",
|
|
4345
|
+
stmt,
|
|
4346
|
+
)
|
|
4347
|
+
# Target must be numeric, owned string, or a type with registered operators.
|
|
4348
|
+
# StrView is excluded -- it's non-owning, so += would dangle.
|
|
4349
|
+
is_numeric_target = is_any_int_type(target_type) or is_float_type(target_type)
|
|
4350
|
+
is_str_target = (is_str_type(target_type) or is_string_type(target_type)
|
|
4351
|
+
or isinstance(target_type, PendingStrType))
|
|
4352
|
+
is_bytes_target = (is_bytes_type(target_type) or is_bytearray_type(target_type)
|
|
4353
|
+
or isinstance(target_type, PendingBytesType))
|
|
4354
|
+
# PendingViewType += promotes to owned
|
|
4355
|
+
if isinstance(target_type, PendingViewType) and isinstance(stmt.target, TpyName):
|
|
4356
|
+
self.deduction.mark_view_augassign(stmt.target.name, target_type.family)
|
|
4357
|
+
if not is_numeric_target and not is_str_target and not is_bytes_target:
|
|
4358
|
+
# StrView/BytesView += would dangle (result is a temporary assigned to a view)
|
|
4359
|
+
if is_str_view_type(target_type):
|
|
4360
|
+
raise self.ctx.error(
|
|
4361
|
+
f"Augmented assignment is not supported for StrView (result would dangle)",
|
|
4362
|
+
stmt,
|
|
4363
|
+
)
|
|
4364
|
+
if is_bytes_view_type(target_type):
|
|
4365
|
+
raise self.ctx.error(
|
|
4366
|
+
f"Augmented assignment is not supported for BytesView (result would dangle)",
|
|
4367
|
+
stmt,
|
|
4368
|
+
)
|
|
4369
|
+
# Try in-place method first (e.g. __iadd__, __ior__), then binary operator
|
|
4370
|
+
operators = self.expr.operators
|
|
4371
|
+
protocol_checker = self.protocols.type_conforms_to_protocol if self.protocols else None
|
|
4372
|
+
if result := operators.resolve_aug_inplace(
|
|
4373
|
+
target_type, stmt.op, value_type,
|
|
4374
|
+
protocol_checker=protocol_checker, loc_node=stmt,
|
|
4375
|
+
):
|
|
4376
|
+
stmt.resolved_inplace = result
|
|
4377
|
+
if result.method.params:
|
|
4378
|
+
_, param_type = result.method.params[0]
|
|
4379
|
+
self.compat.check_type_compatible(
|
|
4380
|
+
value_type, param_type, f"'{stmt.op}=' operand", source_expr=stmt.value,
|
|
4381
|
+
)
|
|
4382
|
+
return
|
|
4383
|
+
# Method exists but arg type mismatches -- produce a specific type error.
|
|
4384
|
+
expected_param = operators.get_aug_inplace_param_type(target_type, stmt.op)
|
|
4385
|
+
if expected_param is not None:
|
|
4386
|
+
self.compat.check_type_compatible(
|
|
4387
|
+
value_type, expected_param, f"'{stmt.op}=' operand",
|
|
4388
|
+
loc=stmt.loc, source_expr=stmt.value,
|
|
4389
|
+
)
|
|
4390
|
+
if result := operators.resolve_binop(target_type, stmt.op, value_type, loc_node=stmt):
|
|
4391
|
+
stmt.resolved_binop = result
|
|
4392
|
+
return
|
|
4393
|
+
raise self.ctx.error(
|
|
4394
|
+
f"Operator '{stmt.op}=' is not supported for {target_type}",
|
|
4395
|
+
stmt,
|
|
4396
|
+
)
|
|
4397
|
+
check_value_type = value_type.wrapped if isinstance(value_type, OwnType) else value_type
|
|
4398
|
+
if is_numeric_target and not (is_any_int_type(check_value_type) or is_any_float_type(check_value_type)):
|
|
4399
|
+
raise self.ctx.error(
|
|
4400
|
+
f"Augmented assignment value must be a numeric type, got {check_value_type}",
|
|
4401
|
+
stmt,
|
|
4402
|
+
)
|
|
4403
|
+
if is_str_target and not is_any_str_type(check_value_type):
|
|
4404
|
+
raise self.ctx.error(
|
|
4405
|
+
f"Augmented assignment value must be a string type, got {check_value_type}",
|
|
4406
|
+
stmt,
|
|
4407
|
+
)
|
|
4408
|
+
# Special case: FixedInt += BigInt should use the target's ops (value gets converted)
|
|
4409
|
+
# This preserves checked arithmetic and avoids unnecessary promotion to BigInt
|
|
4410
|
+
resolve_value_type = check_value_type
|
|
4411
|
+
if is_fixed_int_type(target_type) and is_big_int_type(check_value_type):
|
|
4412
|
+
resolve_value_type = target_type
|
|
4413
|
+
# Invalidate range facts for the target (value has changed)
|
|
4414
|
+
if isinstance(stmt.target, TpyName):
|
|
4415
|
+
self.ctx.func.value_ranges.pop(stmt.target.name, None)
|
|
4416
|
+
# Resolve the binary operation for codegen
|
|
4417
|
+
operators = self.expr.operators
|
|
4418
|
+
if result := operators.resolve_binop(target_type, stmt.op, resolve_value_type, loc_node=stmt):
|
|
4419
|
+
stmt.resolved_binop = result
|
|
4420
|
+
if is_numeric_target:
|
|
4421
|
+
self._apply_aug_assign_writeback(stmt.target, target_type, result.method.return_type, stmt.op, stmt)
|
|
4422
|
+
elif is_numeric_target:
|
|
4423
|
+
raise self.ctx.error(
|
|
4424
|
+
f"Operator '{stmt.op}=' is not supported between {target_type} and {value_type}",
|
|
4425
|
+
stmt,
|
|
4426
|
+
)
|