tpy-lang 0.3.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tpy_lang-0.3.0.dev0.dist-info/METADATA +151 -0
- tpy_lang-0.3.0.dev0.dist-info/RECORD +333 -0
- tpy_lang-0.3.0.dev0.dist-info/WHEEL +4 -0
- tpy_lang-0.3.0.dev0.dist-info/entry_points.txt +3 -0
- tpyc/__init__.py +104 -0
- tpyc/__main__.py +6 -0
- tpyc/_buildinfo.py +1 -0
- tpyc/_data/docs/LANGUAGE_FEATURES.md +6278 -0
- tpyc/_data/docs/STDLIB_ROADMAP.md +1258 -0
- tpyc/_data/docs/TPY_FOR_AGENTS.md +556 -0
- tpyc/_data/lib/tpy/_bindings/__init__.py +6 -0
- tpyc/_data/lib/tpy/_bindings/pcre2.py +173 -0
- tpyc/_data/lib/tpy/_bindings/posix_socket.py +161 -0
- tpyc/_data/lib/tpy/_functools_macros.py +80 -0
- tpyc/_data/lib/tpy/_macro_helpers.py +161 -0
- tpyc/_data/lib/tpy/argparse.py +2062 -0
- tpyc/_data/lib/tpy/asyncio/__init__.py +744 -0
- tpyc/_data/lib/tpy/asyncio/_executor.py +515 -0
- tpyc/_data/lib/tpy/base64.py +410 -0
- tpyc/_data/lib/tpy/bisect.py +39 -0
- tpyc/_data/lib/tpy/builtins.py +38 -0
- tpyc/_data/lib/tpy/dataclasses.py +354 -0
- tpyc/_data/lib/tpy/enum.py +23 -0
- tpyc/_data/lib/tpy/functools.py +33 -0
- tpyc/_data/lib/tpy/hashlib.py +206 -0
- tpyc/_data/lib/tpy/heapq.py +118 -0
- tpyc/_data/lib/tpy/io.py +395 -0
- tpyc/_data/lib/tpy/json.py +221 -0
- tpyc/_data/lib/tpy/math.py +406 -0
- tpyc/_data/lib/tpy/random.py +597 -0
- tpyc/_data/lib/tpy/re.py +467 -0
- tpyc/_data/lib/tpy/socket.py +379 -0
- tpyc/_data/lib/tpy/struct.py +178 -0
- tpyc/_data/lib/tpy/sys.py +40 -0
- tpyc/_data/lib/tpy/time.py +39 -0
- tpyc/_data/lib/tpy/tpy/__init__.py +78 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/__init__.py +10 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/_decorators.py +37 -0
- tpyc/_data/lib/tpy/tpy/_bootstrap/_extern.py +64 -0
- tpyc/_data/lib/tpy/tpy/_builtins/__init__.py +11 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_bytes.py +378 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_dict.py +151 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_exceptions.py +125 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_funcs.py +681 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_io.py +97 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_list.py +127 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_range.py +52 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_set.py +139 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_super.py +11 -0
- tpyc/_data/lib/tpy/tpy/_builtins/_types.py +661 -0
- tpyc/_data/lib/tpy/tpy/_core/__init__.py +23 -0
- tpyc/_data/lib/tpy/tpy/_core/_bytes_view.py +129 -0
- tpyc/_data/lib/tpy/tpy/_core/_containers.py +137 -0
- tpyc/_data/lib/tpy/tpy/_core/_functions.py +40 -0
- tpyc/_data/lib/tpy/tpy/_core/_types.py +2061 -0
- tpyc/_data/lib/tpy/tpy/_typing/__init__.py +77 -0
- tpyc/_data/lib/tpy/tpy/_version.py +29 -0
- tpyc/_data/lib/tpy/tpy/bits.py +28 -0
- tpyc/_data/lib/tpy/tpy/coro/__init__.py +127 -0
- tpyc/_data/lib/tpy/tpy/extern.py +8 -0
- tpyc/_data/lib/tpy/tpy/mem.py +49 -0
- tpyc/_data/lib/tpy/tpy/unsafe.py +195 -0
- tpyc/_data/lib/tpy/tpy/version.py +21 -0
- tpyc/_data/lib/tpy/typing.py +13 -0
- tpyc/_data/runtime/cpp/include/tpy/any.hpp +461 -0
- tpyc/_data/runtime/cpp/include/tpy/as_ostream.hpp +117 -0
- tpyc/_data/runtime/cpp/include/tpy/async.hpp +76 -0
- tpyc/_data/runtime/cpp/include/tpy/bigint.hpp +1343 -0
- tpyc/_data/runtime/cpp/include/tpy/builtins.hpp +400 -0
- tpyc/_data/runtime/cpp/include/tpy/bytes_ops.hpp +469 -0
- tpyc/_data/runtime/cpp/include/tpy/container_ops.hpp +487 -0
- tpyc/_data/runtime/cpp/include/tpy/copy_iter.hpp +82 -0
- tpyc/_data/runtime/cpp/include/tpy/core.hpp +558 -0
- tpyc/_data/runtime/cpp/include/tpy/dict_ops.hpp +289 -0
- tpyc/_data/runtime/cpp/include/tpy/dunder.hpp +750 -0
- tpyc/_data/runtime/cpp/include/tpy/dynamic.hpp +44 -0
- tpyc/_data/runtime/cpp/include/tpy/enum.hpp +40 -0
- tpyc/_data/runtime/cpp/include/tpy/file.hpp +245 -0
- tpyc/_data/runtime/cpp/include/tpy/fixed_int.hpp +317 -0
- tpyc/_data/runtime/cpp/include/tpy/format.hpp +954 -0
- tpyc/_data/runtime/cpp/include/tpy/frame_slot.hpp +120 -0
- tpyc/_data/runtime/cpp/include/tpy/generator.hpp +47 -0
- tpyc/_data/runtime/cpp/include/tpy/iterable_ops.hpp +122 -0
- tpyc/_data/runtime/cpp/include/tpy/itertools.hpp +749 -0
- tpyc/_data/runtime/cpp/include/tpy/next_iter.hpp +82 -0
- tpyc/_data/runtime/cpp/include/tpy/ordered_map.hpp +518 -0
- tpyc/_data/runtime/cpp/include/tpy/ordered_set.hpp +337 -0
- tpyc/_data/runtime/cpp/include/tpy/own_iter.hpp +54 -0
- tpyc/_data/runtime/cpp/include/tpy/pascal_graph_sdl.hpp +192 -0
- tpyc/_data/runtime/cpp/include/tpy/printing.hpp +302 -0
- tpyc/_data/runtime/cpp/include/tpy/protocols.hpp +61 -0
- tpyc/_data/runtime/cpp/include/tpy/range.hpp +115 -0
- tpyc/_data/runtime/cpp/include/tpy/ranges.hpp +212 -0
- tpyc/_data/runtime/cpp/include/tpy/set_ops.hpp +265 -0
- tpyc/_data/runtime/cpp/include/tpy/slice.hpp +47 -0
- tpyc/_data/runtime/cpp/include/tpy/span_iter.hpp +42 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/math.hpp +41 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/pcre2_h.hpp +96 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/random.hpp +25 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/socket_h.hpp +145 -0
- tpyc/_data/runtime/cpp/include/tpy/stdlib/time.hpp +62 -0
- tpyc/_data/runtime/cpp/include/tpy/system.hpp +121 -0
- tpyc/_data/runtime/cpp/include/tpy/throwable.hpp +55 -0
- tpyc/_data/runtime/cpp/include/tpy/tpy.hpp +156 -0
- tpyc/_data/runtime/cpp/include/tpy/type_name.hpp +77 -0
- tpyc/_data/runtime/cpp/include/tpy/type_traits.hpp +240 -0
- tpyc/_data/runtime/cpp/include/tpy/uninit_array_storage.hpp +250 -0
- tpyc/_data/runtime/cpp/include/tpy/uninit_heap_storage.hpp +277 -0
- tpyc/_data/runtime/cpp/include/tpy/varargs.hpp +174 -0
- tpyc/_data/runtime/cpp/include/tpy/variant_ref.hpp +118 -0
- tpyc/_data/runtime/cpp/src/stdlib/socket_impl.cpp +104 -0
- tpyc/_data/runtime/cpp/third_party/README.md +58 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/AUTHORS +36 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/CMakeLists.txt +1233 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/COPYING +5 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/ChangeLog +3097 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/HACKING +853 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/INSTALL +368 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/LICENCE +94 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/NEWS +492 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/NON-AUTOTOOLS-BUILD +430 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/README +956 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/COPYING-CMAKE-SCRIPTS +22 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindEditline.cmake +16 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindPackageHandleStandardArgs.cmake +58 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/FindReadline.cmake +29 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config-version.cmake.in +15 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/cmake/pcre2-config.cmake.in +148 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/config-cmake.h.in +56 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-16.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-32.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-8.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/libpcre2-posix.pc.in +13 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/pcre2-config.in +121 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h +483 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.generic +483 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/config.h.in +460 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.generic +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2.h.in +1010 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_auto_possess.c +1371 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c +196 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chartables.c.dist +196 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_chkdint.c +96 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_compile.c +11001 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_config.c +252 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_context.c +510 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_convert.c +1189 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dfa_match.c +4119 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_dftables.c +297 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_error.c +345 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_extuni.c +162 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_find_bracket.c +219 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_fuzzsupport.c +792 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_internal.h +2084 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_intmodedep.h +940 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_compile.c +14972 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_match.c +200 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_misc.c +234 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_neon_inc.h +354 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_simd_inc.h +2355 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_jit_test.c +2528 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_maketables.c +165 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match.c +7777 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_match_data.c +185 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_newline.c +243 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ord2utf.c +120 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_pattern_info.c +432 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_printint.c +886 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_script_run.c +344 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_serialize.c +286 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_string_utils.c +237 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_study.c +1915 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substitute.c +1009 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_substring.c +550 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_tables.c +234 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucd.c +5460 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucp.h +396 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_ucptables.c +1533 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_valid_utf.c +398 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2_xclass.c +308 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2demo.c +497 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2grep.c +4606 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.c +425 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix.h +187 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2posix_test.c +209 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/pcre2test.c +9708 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorApple.c +137 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorCore.c +327 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorFreeBSD.c +89 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorPosix.c +62 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitExecAllocatorWindows.c +40 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorNetBSD.c +72 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitProtExecAllocatorPosix.c +172 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorPosix.c +141 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/allocator_src/sljitWXExecAllocatorWindows.c +102 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfig.h +142 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigCPU.h +188 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitConfigInternal.h +907 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.c +3561 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitLir.h +2466 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_32.c +4636 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_64.c +3491 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeARM_T2_32.c +4302 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeLOONGARCH_64.c +3765 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_32.c +472 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_64.c +387 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeMIPS_common.c +4259 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_32.c +485 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_64.c +719 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativePPC_common.c +3161 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_32.c +142 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_64.c +222 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeRISCV_common.c +3121 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeS390X.c +4526 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_32.c +1685 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_64.c +1398 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitNativeX86_common.c +5001 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitSerialize.c +516 -0
- tpyc/_data/runtime/cpp/third_party/pcre2/src/sljit/sljitUtils.c +344 -0
- tpyc/_data/runtime/cpp/third_party/pcre2.sources.txt +54 -0
- tpyc/_data/runtime/cpp/third_party/pcre2.vendor.json +7 -0
- tpyc/build/__init__.py +7 -0
- tpyc/build/pcre2.py +122 -0
- tpyc/build/third_party.py +413 -0
- tpyc/cli.py +822 -0
- tpyc/codegen_cpp/__init__.py +18 -0
- tpyc/codegen_cpp/builtins.py +484 -0
- tpyc/codegen_cpp/context.py +2064 -0
- tpyc/codegen_cpp/expressions.py +5940 -0
- tpyc/codegen_cpp/functions.py +1913 -0
- tpyc/codegen_cpp/gen_async.py +3258 -0
- tpyc/codegen_cpp/gen_generators.py +657 -0
- tpyc/codegen_cpp/generator.py +2258 -0
- tpyc/codegen_cpp/match.py +1997 -0
- tpyc/codegen_cpp/param_const.py +172 -0
- tpyc/codegen_cpp/protocols.py +907 -0
- tpyc/codegen_cpp/records.py +1654 -0
- tpyc/codegen_cpp/resumable_cfg.py +1651 -0
- tpyc/codegen_cpp/statements.py +4963 -0
- tpyc/codegen_cpp/string_dispatch.py +76 -0
- tpyc/codegen_cpp/test_context.py +46 -0
- tpyc/codegen_cpp/test_param_const.py +113 -0
- tpyc/codegen_cpp/test_resumable_cfg.py +182 -0
- tpyc/codegen_cpp/type_resolution.py +53 -0
- tpyc/codegen_cpp/types.py +436 -0
- tpyc/codegen_cpp/variant_access.py +135 -0
- tpyc/coercions.py +749 -0
- tpyc/compilation_context.py +57 -0
- tpyc/compiler.py +3945 -0
- tpyc/cycle_detection.py +358 -0
- tpyc/diagnostics.py +135 -0
- tpyc/dump_types.py +353 -0
- tpyc/frontend_diagnostics.py +47 -0
- tpyc/frontend_ir/__init__.py +140 -0
- tpyc/frontend_ir/lower.py +1098 -0
- tpyc/frontend_ir/nodes.py +718 -0
- tpyc/frontend_ir/resolver_adapter.py +151 -0
- tpyc/frontend_plugin.py +209 -0
- tpyc/install_docs.py +81 -0
- tpyc/liveness.py +756 -0
- tpyc/macro_api.py +1724 -0
- tpyc/macro_loader.py +497 -0
- tpyc/module_names.py +64 -0
- tpyc/modules/__init__.py +31 -0
- tpyc/modules/defs.py +89 -0
- tpyc/modules/registry.py +36 -0
- tpyc/modules/resolver.py +192 -0
- tpyc/modules/type_resolution.py +629 -0
- tpyc/namespace.py +172 -0
- tpyc/parse/__init__.py +84 -0
- tpyc/parse/imports.py +490 -0
- tpyc/parse/nodes.py +1732 -0
- tpyc/parse/parser.py +4043 -0
- tpyc/parse/resolve_refs.py +466 -0
- tpyc/parse/type_resolver.py +1060 -0
- tpyc/prescan.py +254 -0
- tpyc/qnames.py +149 -0
- tpyc/repl.py +529 -0
- tpyc/repl_backends.py +848 -0
- tpyc/sema/__init__.py +21 -0
- tpyc/sema/analyzer.py +3625 -0
- tpyc/sema/bound_check.py +72 -0
- tpyc/sema/builder_trace.py +684 -0
- tpyc/sema/calls.py +5406 -0
- tpyc/sema/compatibility.py +2107 -0
- tpyc/sema/context.py +1243 -0
- tpyc/sema/expressions.py +3737 -0
- tpyc/sema/flow_facts.py +199 -0
- tpyc/sema/init_tracker.py +150 -0
- tpyc/sema/list_literals.py +69 -0
- tpyc/sema/literal_utils.py +27 -0
- tpyc/sema/local_deduction.py +1088 -0
- tpyc/sema/macros.py +179 -0
- tpyc/sema/match.py +1177 -0
- tpyc/sema/method_expansion.py +347 -0
- tpyc/sema/methods.py +2197 -0
- tpyc/sema/mutation_propagation.py +268 -0
- tpyc/sema/narrowing.py +857 -0
- tpyc/sema/numeric_lattice.py +160 -0
- tpyc/sema/operators.py +402 -0
- tpyc/sema/overloads.py +841 -0
- tpyc/sema/protocols.py +1209 -0
- tpyc/sema/reach_analysis.py +202 -0
- tpyc/sema/registration.py +3156 -0
- tpyc/sema/scope_tracker.py +193 -0
- tpyc/sema/statements.py +4426 -0
- tpyc/sema/type_ops.py +1879 -0
- tpyc/sema/value_range.py +181 -0
- tpyc/symbol_binding.py +259 -0
- tpyc/test_c3_mro.py +208 -0
- tpyc/test_cli_argv.py +52 -0
- tpyc/test_compiler.py +559 -0
- tpyc/test_contains_type_param.py +101 -0
- tpyc/test_cycle_detection.py +221 -0
- tpyc/test_dump_types.py +225 -0
- tpyc/test_install_docs.py +65 -0
- tpyc/test_local_cpp_form.py +135 -0
- tpyc/test_macro_loader.py +76 -0
- tpyc/test_method_expansion.py +254 -0
- tpyc/test_nominal_identity.py +182 -0
- tpyc/test_overloads.py +410 -0
- tpyc/test_parse.py +303 -0
- tpyc/test_parse_type_ref.py +506 -0
- tpyc/test_parse_version_info.py +58 -0
- tpyc/test_reach_analysis.py +72 -0
- tpyc/test_ref_type.py +216 -0
- tpyc/test_send_sync_substitution.py +276 -0
- tpyc/test_tuple_mutation_propagation.py +206 -0
- tpyc/test_type_def_registry.py +1729 -0
- tpyc/test_union_types.py +195 -0
- tpyc/type_def_registry.py +975 -0
- tpyc/typesys.py +5104 -0
|
@@ -0,0 +1,2064 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboPython Code Generation Context
|
|
3
|
+
|
|
4
|
+
Shared state and utilities for C++ code generation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import Callable, Iterator, Literal, TextIO, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from ..typesys import (
|
|
14
|
+
TpyType, PtrType, OwnType, ReadonlyType, OptionalType, NominalType, SelfType,
|
|
15
|
+
IntLiteralType, TypeParamRef, UnionType, TupleType, FunctionInfo, ModuleInfo,
|
|
16
|
+
AliasRef, RecursiveUnionInfo,
|
|
17
|
+
is_protocol_type, unwrap_readonly, unwrap_qualifiers, ensure_qualified, unwrap_ref_type,
|
|
18
|
+
is_union_or_optional_type, is_own_pointer_repr_optional,
|
|
19
|
+
polymorphic_source_is_pointer,
|
|
20
|
+
)
|
|
21
|
+
from ..parse import (
|
|
22
|
+
SourceLocation, TpyExpr, TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral,
|
|
23
|
+
TpyBoolLiteral, TpyNoneLiteral, TpyArrayLiteral, TpyListRepeat, TpyListComprehension,
|
|
24
|
+
TpyDictLiteral, TpySetLiteral, TpyDictComprehension, TpySetComprehension,
|
|
25
|
+
TpyGeneratorExpression,
|
|
26
|
+
TpyCoerce, TpyBinOp, TpyUnaryOp, TpyMethodCall, TpySubscript, TpySlice, TpyCall, TpyName, TpyFieldAccess,
|
|
27
|
+
TpyIfExpr,
|
|
28
|
+
)
|
|
29
|
+
from ..namespace import Namespace, BindingKind
|
|
30
|
+
from ..type_def_registry import (
|
|
31
|
+
is_bool_type, is_dict, is_set, is_bytes_view_type, is_str_view_type,
|
|
32
|
+
)
|
|
33
|
+
from ..symbol_binding import lookup_imported, SymbolKind
|
|
34
|
+
from ..compilation_context import get_current_compiler
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from ..sema import SemanticAnalyzer
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
INDENT = " "
|
|
41
|
+
|
|
42
|
+
# C++ reserved keywords and common type names that conflict with Python identifiers.
|
|
43
|
+
# When a Python name matches one of these, codegen appends '_' to avoid C++ errors.
|
|
44
|
+
_CPP_RESERVED_WORDS: frozenset[str] = frozenset({
|
|
45
|
+
# C++ keywords
|
|
46
|
+
"alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", "bitor",
|
|
47
|
+
"bool", "break", "case", "catch", "char", "char8_t", "char16_t", "char32_t",
|
|
48
|
+
"class", "compl", "concept", "const", "consteval", "constexpr", "constinit",
|
|
49
|
+
"const_cast", "continue", "co_await", "co_return", "co_yield",
|
|
50
|
+
"decltype", "default", "delete", "do", "double", "dynamic_cast",
|
|
51
|
+
"else", "enum", "explicit", "export", "extern",
|
|
52
|
+
"false", "float", "for", "friend", "goto",
|
|
53
|
+
"if", "inline", "int", "long", "mutable",
|
|
54
|
+
"namespace", "new", "noexcept", "not", "not_eq", "nullptr",
|
|
55
|
+
"operator", "or", "or_eq",
|
|
56
|
+
"private", "protected", "public",
|
|
57
|
+
"register", "reinterpret_cast", "requires", "return",
|
|
58
|
+
"short", "signed", "sizeof", "static", "static_assert", "static_cast",
|
|
59
|
+
"struct", "switch",
|
|
60
|
+
"template", "this", "thread_local", "throw", "true", "try", "typedef",
|
|
61
|
+
"typeid", "typename",
|
|
62
|
+
"union", "unsigned", "using",
|
|
63
|
+
"virtual", "void", "volatile",
|
|
64
|
+
"wchar_t", "while",
|
|
65
|
+
"xor", "xor_eq",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def escape_cpp_name(name: str) -> str:
|
|
70
|
+
"""Escape a Python identifier that clashes with a C++ reserved word.
|
|
71
|
+
|
|
72
|
+
Appends '_' to names that collide with C++ keywords or built-in type names.
|
|
73
|
+
Leaves other names unchanged. Dunder names (__x__) and internal names
|
|
74
|
+
(starting with __tpy) are never escaped -- they are compiler-generated.
|
|
75
|
+
"""
|
|
76
|
+
if name in _CPP_RESERVED_WORDS:
|
|
77
|
+
return name + "_"
|
|
78
|
+
return name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_TEMPLATE_PLACEHOLDER = re.compile(r"\{(self|cpp|\d+)\}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def expand_cpp_template(template: str, self_val: str, *args: str,
|
|
85
|
+
self_type: 'TpyType | None' = None) -> str:
|
|
86
|
+
"""Expand a C++ template, substituting {self}, {cpp}, and positional {0}, {1}, etc.
|
|
87
|
+
|
|
88
|
+
{cpp} is replaced with self_type.to_cpp() when available -- used for
|
|
89
|
+
@builtin_type methods that reference their own C++ type name.
|
|
90
|
+
"""
|
|
91
|
+
# Validate before substitution: check that all placeholder indices are in range.
|
|
92
|
+
# Post-substitution scanning would false-positive on C++ braces in values
|
|
93
|
+
# (e.g. std::vector<int>{30} looks like {30} placeholder).
|
|
94
|
+
for m in _TEMPLATE_PLACEHOLDER.finditer(template):
|
|
95
|
+
token = m.group(1)
|
|
96
|
+
if token in ("self", "cpp"):
|
|
97
|
+
continue
|
|
98
|
+
if int(token) >= len(args):
|
|
99
|
+
raise CodeGenError(
|
|
100
|
+
f"Unreplaced placeholder {m.group()} in C++ template: {template}"
|
|
101
|
+
)
|
|
102
|
+
if self_type and "{cpp}" in template:
|
|
103
|
+
template = template.replace("{cpp}", self_type.to_cpp())
|
|
104
|
+
result = template.replace("{self}", self_val)
|
|
105
|
+
for i, arg in enumerate(args):
|
|
106
|
+
result = result.replace(f"{{{i}}}", arg)
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def escape_cpp_string(value: str) -> str:
|
|
111
|
+
"""Escape a Python string for use in a C++ string literal (double-quoted)."""
|
|
112
|
+
return (value.replace('\\', '\\\\')
|
|
113
|
+
.replace('"', '\\"')
|
|
114
|
+
.replace('\n', '\\n')
|
|
115
|
+
.replace('\r', '\\r')
|
|
116
|
+
.replace('\t', '\\t')
|
|
117
|
+
.replace('\x00', '\\000'))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cpp_bytes_literal_span(value: bytes) -> str:
|
|
121
|
+
"""Render a bytes literal as a `::tpy::bytes_literal(...)` call --
|
|
122
|
+
a `std::span<const uint8_t>` over a C++ string literal (static
|
|
123
|
+
storage), avoiding the heap allocation of a temporary vector."""
|
|
124
|
+
escaped = "".join(f"\\x{b:02x}" for b in value)
|
|
125
|
+
return f'::tpy::bytes_literal("{escaped}", {len(value)})'
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cpp_string_literal_expr(value: str) -> str:
|
|
129
|
+
"""Render a Python string as a C++ string_view expression.
|
|
130
|
+
|
|
131
|
+
NUL-free literals emit as bare `"..."` (decay; string_view ctor uses
|
|
132
|
+
strlen). Embedded NUL needs `std::string_view{"...", N}` with explicit
|
|
133
|
+
UTF-8 byte length, otherwise strlen truncates at the first NUL.
|
|
134
|
+
"""
|
|
135
|
+
escaped = escape_cpp_string(value)
|
|
136
|
+
if '\x00' in value:
|
|
137
|
+
nbytes = len(value.encode('utf-8'))
|
|
138
|
+
return f'std::string_view{{"{escaped}", {nbytes}}}'
|
|
139
|
+
return f'"{escaped}"'
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def view_key_target(container_type) -> "TpyType | None":
|
|
143
|
+
"""For dict/set with a view-typed key (BytesView/StrView), return that
|
|
144
|
+
key type so callers can thread it as a target_type into key-position
|
|
145
|
+
codegen. Lets bytes/str literals pin to static storage instead of
|
|
146
|
+
being stored as dangling spans/string_views."""
|
|
147
|
+
if not (is_dict(container_type) or is_set(container_type)):
|
|
148
|
+
return None
|
|
149
|
+
type_args = getattr(container_type, "type_args", None)
|
|
150
|
+
if not type_args:
|
|
151
|
+
return None
|
|
152
|
+
k = type_args[0]
|
|
153
|
+
if is_bytes_view_type(k) or is_str_view_type(k):
|
|
154
|
+
return k
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def escape_cpp_char(value: str) -> str:
|
|
159
|
+
"""Escape a Python char for use in a C++ char literal (single-quoted)."""
|
|
160
|
+
return (value.replace('\\', '\\\\').replace("'", "\\'")
|
|
161
|
+
.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
|
|
162
|
+
.replace('\x00', '\\000'))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_include_path(module_name: str) -> str | None:
|
|
166
|
+
"""Get the include path override for a module, or None for default.
|
|
167
|
+
|
|
168
|
+
Returns None when no compilation is active (matches the previous
|
|
169
|
+
empty-default-dict behavior for callers like BuildLayout's pure
|
|
170
|
+
path-shape tests).
|
|
171
|
+
"""
|
|
172
|
+
compiler = get_current_compiler()
|
|
173
|
+
if compiler is None:
|
|
174
|
+
return None
|
|
175
|
+
return compiler.include_path_map.get(module_name)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def module_to_include_path(module_name: str) -> str:
|
|
179
|
+
"""Resolve include path for a module: override if set, else default from name."""
|
|
180
|
+
override = get_include_path(module_name)
|
|
181
|
+
if override is not None:
|
|
182
|
+
return override
|
|
183
|
+
parts = module_name.split('.')
|
|
184
|
+
if len(parts) == 1:
|
|
185
|
+
return f"{parts[0]}.hpp"
|
|
186
|
+
return '/'.join(parts[:-1]) + f"/{parts[-1]}.hpp"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Mapping from Python dunder methods to C++ binary operators.
|
|
190
|
+
# Both __truediv__ and __floordiv__ map to / in C++: for integer types, C++ /
|
|
191
|
+
# is truncating division (like Python //); user types should implement the
|
|
192
|
+
# appropriate semantics in their __truediv__/__floordiv__ methods.
|
|
193
|
+
def module_to_cpp_namespace(module_name: str) -> str:
|
|
194
|
+
"""Convert a dotted module name to a C++ namespace.
|
|
195
|
+
|
|
196
|
+
Checks the namespace map first (for # tpy: namespace overrides),
|
|
197
|
+
falls back to "tpyapp::{module_name}".
|
|
198
|
+
"""
|
|
199
|
+
compiler = get_current_compiler()
|
|
200
|
+
if compiler is not None and module_name in compiler.namespace_map:
|
|
201
|
+
return compiler.namespace_map[module_name]
|
|
202
|
+
return f"tpyapp::{module_name.replace('.', '::')}"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def module_has_cpp_namespace_override(module_name: str) -> bool:
|
|
206
|
+
"""True if `module_name` has an explicit `# tpy: cpp_namespace` directive
|
|
207
|
+
(i.e. its C++ namespace differs from the default `tpyapp::<module>`)."""
|
|
208
|
+
compiler = get_current_compiler()
|
|
209
|
+
return compiler is not None and module_name in compiler.namespace_map
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def qualified_cpp_name(module_name: str, name: str) -> str:
|
|
213
|
+
"""Build an absolute-qualified C++ name for cross-module references.
|
|
214
|
+
|
|
215
|
+
Example: ("shapes", "Circle") -> "::tpyapp::shapes::Circle"
|
|
216
|
+
Example: ("shapes", "Container.Inner") -> "::tpyapp::shapes::Container::Inner"
|
|
217
|
+
"""
|
|
218
|
+
cpp_name = "::".join(escape_cpp_name(part) for part in name.split("."))
|
|
219
|
+
return f"::{module_to_cpp_namespace(module_name)}::{cpp_name}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def qualify_native_name(name: str) -> str:
|
|
223
|
+
"""Force @native call-site emission to absolute global scope.
|
|
224
|
+
|
|
225
|
+
Any non-empty name gets a leading `::` so C++ unqualified lookup can't
|
|
226
|
+
bind it to a member function, enclosing-namespace symbol, or ADL hit
|
|
227
|
+
before finding the intended native symbol. Idempotent: names that
|
|
228
|
+
already start with `::` are left alone.
|
|
229
|
+
|
|
230
|
+
Example: "socket" -> "::socket", "tpy::__len__" -> "::tpy::__len__",
|
|
231
|
+
"::already_global" -> "::already_global".
|
|
232
|
+
"""
|
|
233
|
+
if not name or name.startswith("::"):
|
|
234
|
+
return name
|
|
235
|
+
return f"::{name}"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def enum_cpp_name(enum_type: TpyType, current_module: str, *,
|
|
239
|
+
absolute: bool = False, einfo=None) -> str:
|
|
240
|
+
"""Authoritative C++ spelling for an enum type.
|
|
241
|
+
|
|
242
|
+
Resolution order:
|
|
243
|
+
- @native qname (canonical, sema-normalized to ::-prefixed form);
|
|
244
|
+
- cross-module imported qualification (driven by EnumInfo.module_name);
|
|
245
|
+
- local / nested form.
|
|
246
|
+
|
|
247
|
+
`absolute=False` (default): bare short / nested form for local
|
|
248
|
+
enums, which works inside the declaring module's user namespace.
|
|
249
|
+
`absolute=True`: fully-qualify local enums as
|
|
250
|
+
`::tpyapp::<module>::E` for sites that emit at global scope
|
|
251
|
+
(EnumUtil specializations, std::ostream operators outside the
|
|
252
|
+
user namespace block).
|
|
253
|
+
|
|
254
|
+
`einfo` lets callers hoist a previously-resolved EnumInfo so this
|
|
255
|
+
helper doesn't repeat the lookup (hot in member-access codegen).
|
|
256
|
+
"""
|
|
257
|
+
if einfo is None:
|
|
258
|
+
from ..type_def_registry import enum_info_of
|
|
259
|
+
einfo = enum_info_of(enum_type)
|
|
260
|
+
if einfo is not None and einfo.is_native and einfo.native_name:
|
|
261
|
+
return einfo.native_name
|
|
262
|
+
if (einfo is not None and einfo.module_name is not None
|
|
263
|
+
and einfo.module_name != current_module):
|
|
264
|
+
return qualified_cpp_name(einfo.module_name, enum_type.name)
|
|
265
|
+
if absolute:
|
|
266
|
+
return qualified_cpp_name(current_module, enum_type.name)
|
|
267
|
+
if "." in enum_type.name:
|
|
268
|
+
return enum_type.name.replace(".", "::")
|
|
269
|
+
return enum_type.name
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def loop_var_binding(
|
|
273
|
+
elem_type: TpyType, cpp_var: str, deref_expr: str,
|
|
274
|
+
const_loop_var: bool, hoisted: bool = False,
|
|
275
|
+
consuming: bool = False,
|
|
276
|
+
) -> str:
|
|
277
|
+
"""Return the C++ loop variable binding line (no trailing newline).
|
|
278
|
+
|
|
279
|
+
Shared by for-loop and comprehension codegen to avoid duplicating the
|
|
280
|
+
const_loop_var / value_type / auto&& decision tree.
|
|
281
|
+
|
|
282
|
+
consuming=True uses auto&& to bind into OwnIter's move-iterator
|
|
283
|
+
storage. Zero cost (no per-element move), but move-ready: codegen
|
|
284
|
+
can later emit std::move(var) for per-element ownership transfer.
|
|
285
|
+
"""
|
|
286
|
+
# Own[T] from consuming iterators uses the same binding as T.
|
|
287
|
+
if isinstance(elem_type, OwnType):
|
|
288
|
+
elem_type = elem_type.wrapped
|
|
289
|
+
if hoisted:
|
|
290
|
+
return f"{cpp_var} = {deref_expr};"
|
|
291
|
+
if consuming:
|
|
292
|
+
# Forwarding ref into OwnIter storage: zero-cost, move-ready.
|
|
293
|
+
return f"auto&& {cpp_var} = {deref_expr};"
|
|
294
|
+
# Composite types (variants, tuples) use reference binding -- they may
|
|
295
|
+
# contain heap-allocated members, making copies expensive.
|
|
296
|
+
if isinstance(elem_type, (UnionType, TupleType)):
|
|
297
|
+
if const_loop_var:
|
|
298
|
+
return f"const auto& {cpp_var} = {deref_expr};"
|
|
299
|
+
return f"auto&& {cpp_var} = {deref_expr};"
|
|
300
|
+
if const_loop_var and elem_type.is_value_type():
|
|
301
|
+
if elem_type.is_expensive_copy():
|
|
302
|
+
return f"const {elem_type.to_cpp()}& {cpp_var} = {deref_expr};"
|
|
303
|
+
return f"{elem_type.to_cpp()} {cpp_var} = {deref_expr};"
|
|
304
|
+
if const_loop_var:
|
|
305
|
+
return f"const auto& {cpp_var} = {deref_expr};"
|
|
306
|
+
if elem_type.is_value_type():
|
|
307
|
+
return f"{elem_type.to_cpp()} {cpp_var} = {deref_expr};"
|
|
308
|
+
return f"auto&& {cpp_var} = {deref_expr};"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def is_lvalue_iterable(
|
|
312
|
+
expr: TpyExpr,
|
|
313
|
+
get_record: Callable[[str], object | None],
|
|
314
|
+
get_type: Callable[[TpyExpr], TpyType],
|
|
315
|
+
) -> bool:
|
|
316
|
+
"""Check if an iterable expression is a C++ lvalue.
|
|
317
|
+
|
|
318
|
+
Lvalue expressions get ``auto&`` to preserve consumption semantics.
|
|
319
|
+
Rvalue expressions (constructors, value-returning calls, literals)
|
|
320
|
+
get ``auto`` to own the temporary safely.
|
|
321
|
+
|
|
322
|
+
get_record: look up a record by name (e.g. registry.get_record).
|
|
323
|
+
get_type: resolve the C++ result type of an expression.
|
|
324
|
+
"""
|
|
325
|
+
while isinstance(expr, TpyCoerce):
|
|
326
|
+
expr = expr.expr
|
|
327
|
+
if isinstance(expr, TpyName):
|
|
328
|
+
return True
|
|
329
|
+
if isinstance(expr, TpyFieldAccess):
|
|
330
|
+
return is_lvalue_iterable(expr.obj, get_record, get_type)
|
|
331
|
+
if isinstance(expr, TpySubscript):
|
|
332
|
+
# Slicing (a[1:]) returns an rvalue span/view, not a reference into obj.
|
|
333
|
+
# Only single-index subscripting (a[0]) returns T& and inherits lvalueness from obj.
|
|
334
|
+
if isinstance(expr.index, TpySlice):
|
|
335
|
+
return False
|
|
336
|
+
return is_lvalue_iterable(expr.obj, get_record, get_type)
|
|
337
|
+
if isinstance(expr, (TpyMethodCall, TpyCall)):
|
|
338
|
+
if isinstance(expr, TpyCall) and expr.call_type is not None:
|
|
339
|
+
return False
|
|
340
|
+
if isinstance(expr, TpyCall) and isinstance(expr.func, TpyName) and get_record(expr.func_name):
|
|
341
|
+
return False
|
|
342
|
+
# Own[T] returns are by-value rvalues even when T is a reference type;
|
|
343
|
+
# get_type strips OwnType, so consult resolved_function_info to see it.
|
|
344
|
+
rfi = expr.resolved_function_info
|
|
345
|
+
if rfi is not None and isinstance(rfi.return_type, OwnType):
|
|
346
|
+
return False
|
|
347
|
+
ret_type = get_type(expr)
|
|
348
|
+
# Protocol return types (e.g. Iterator[T] from generators) are
|
|
349
|
+
# value types in practice -- the C++ return is a concrete struct.
|
|
350
|
+
if is_protocol_type(ret_type):
|
|
351
|
+
return False
|
|
352
|
+
return (not ret_type.is_value_type()
|
|
353
|
+
and not is_union_or_optional_type(ret_type))
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
DUNDER_TO_BINARY_OP: dict[str, str] = {
|
|
358
|
+
"__add__": "+", "__sub__": "-", "__mul__": "*",
|
|
359
|
+
"__truediv__": "/", "__floordiv__": "/", "__mod__": "%",
|
|
360
|
+
"__eq__": "==", "__ne__": "!=",
|
|
361
|
+
"__lt__": "<", "__le__": "<=", "__gt__": ">", "__ge__": ">=",
|
|
362
|
+
"__and__": "&", "__or__": "|", "__xor__": "^",
|
|
363
|
+
"__lshift__": "<<", "__rshift__": ">>",
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Container/generator-shaped expressions whose gen_expr emits a value
|
|
367
|
+
# (`vector<T>{...}`, `ordered_map<K, V>{...}`, generator state struct, ...)
|
|
368
|
+
# regardless of the target type. Distinct from pointer-emit rvalues
|
|
369
|
+
# (function calls returning T*, pointer-local names) which already yield
|
|
370
|
+
# stable pointer storage. Codegen sites initializing a pointer-form slot
|
|
371
|
+
# from an Optional source use this to decide whether to materialize a
|
|
372
|
+
# named slot before taking address.
|
|
373
|
+
_CONTAINER_LITERAL_NODES: tuple = (
|
|
374
|
+
TpyArrayLiteral, TpyListRepeat, TpyListComprehension,
|
|
375
|
+
TpyDictLiteral, TpySetLiteral,
|
|
376
|
+
TpyDictComprehension, TpySetComprehension,
|
|
377
|
+
TpyGeneratorExpression,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class SlotState:
|
|
382
|
+
"""Manages unique slot names for pointer-local backing storage."""
|
|
383
|
+
|
|
384
|
+
def __init__(self):
|
|
385
|
+
self._counter: int = 0
|
|
386
|
+
self._prefix: str = "__slot"
|
|
387
|
+
self._global_scope: bool = False
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def global_scope(self) -> bool:
|
|
391
|
+
return self._global_scope
|
|
392
|
+
|
|
393
|
+
def next_slot(self) -> str:
|
|
394
|
+
"""Return a fresh slot name (__slot_N or __global_slot_N)."""
|
|
395
|
+
self._counter += 1
|
|
396
|
+
return f"{self._prefix}_{self._counter}"
|
|
397
|
+
|
|
398
|
+
def reset(self, *, global_scope: bool = False) -> None:
|
|
399
|
+
self._counter = 0
|
|
400
|
+
self._global_scope = global_scope
|
|
401
|
+
self._prefix = "__global_slot" if global_scope else "__slot"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class TempState:
|
|
405
|
+
"""Manages temporary variables for array literals passed to mutable reference params."""
|
|
406
|
+
|
|
407
|
+
def __init__(self):
|
|
408
|
+
self._pending: list[tuple[str, str, str, bool]] = []
|
|
409
|
+
self._pending_named: list[tuple[str, str, str | None, bool]] = []
|
|
410
|
+
self._counter: int = 0
|
|
411
|
+
|
|
412
|
+
def create(self, param_type: TpyType, init_expr: str) -> str:
|
|
413
|
+
"""Create a temp variable and return its name for use in the call."""
|
|
414
|
+
param_type = unwrap_ref_type(param_type)
|
|
415
|
+
self._counter += 1
|
|
416
|
+
temp_name = f"__tmp_{self._counter}"
|
|
417
|
+
is_protocol = is_protocol_type(param_type)
|
|
418
|
+
type_cpp = "auto" if is_protocol or isinstance(param_type, TypeParamRef) else param_type.to_cpp()
|
|
419
|
+
self._pending.append((temp_name, type_cpp, init_expr, False))
|
|
420
|
+
return temp_name
|
|
421
|
+
|
|
422
|
+
def create_typed(self, cpp_type: str, init_expr: str, *, brace_init: bool = False) -> str:
|
|
423
|
+
"""Create a temp variable with an explicit C++ type."""
|
|
424
|
+
self._counter += 1
|
|
425
|
+
temp_name = f"__tmp_{self._counter}"
|
|
426
|
+
self._pending.append((temp_name, cpp_type, init_expr, brace_init))
|
|
427
|
+
return temp_name
|
|
428
|
+
|
|
429
|
+
def declare_named(self, name: str, cpp_type: str, *,
|
|
430
|
+
init: str | None = None, brace_init: bool = False) -> None:
|
|
431
|
+
"""Register a named pre-declaration (for walrus operator variables)."""
|
|
432
|
+
self._pending_named.append((name, cpp_type, init, brace_init))
|
|
433
|
+
|
|
434
|
+
def checkpoint(self) -> tuple[int, int]:
|
|
435
|
+
"""Snapshot the current pending-temp queue lengths.
|
|
436
|
+
|
|
437
|
+
Pair with `rollback_to` to discard temps registered between the two
|
|
438
|
+
calls -- needed when emitting into a context that has no place to
|
|
439
|
+
flush declarations (e.g. a C++ member-initializer-list expression).
|
|
440
|
+
"""
|
|
441
|
+
return (len(self._pending), len(self._pending_named))
|
|
442
|
+
|
|
443
|
+
def rollback_to(self, checkpoint: tuple[int, int]) -> bool:
|
|
444
|
+
"""Discard temps registered after `checkpoint`. Returns True if any were dropped.
|
|
445
|
+
|
|
446
|
+
`_counter` is intentionally not rolled back: the caller may emit the same
|
|
447
|
+
expression again later (in a different context that can flush temps), and
|
|
448
|
+
reusing counter values would produce duplicate `__tmp_N` names. Skipping a
|
|
449
|
+
number is harmless -- temp names only need to be unique within a pass.
|
|
450
|
+
"""
|
|
451
|
+
pending, pending_named = checkpoint
|
|
452
|
+
if len(self._pending) == pending and len(self._pending_named) == pending_named:
|
|
453
|
+
return False
|
|
454
|
+
del self._pending[pending:]
|
|
455
|
+
del self._pending_named[pending_named:]
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
def flush(self, out: TextIO, indent: str) -> None:
|
|
459
|
+
"""Emit any pending temp variable declarations."""
|
|
460
|
+
for name, cpp_type, init_val, brace_init in self._pending_named:
|
|
461
|
+
if init_val is not None:
|
|
462
|
+
out.write(f"{indent}{cpp_type} {name} = {init_val};\n")
|
|
463
|
+
elif brace_init:
|
|
464
|
+
out.write(f"{indent}{cpp_type} {name}{{}};\n")
|
|
465
|
+
else:
|
|
466
|
+
out.write(f"{indent}{cpp_type} {name};\n")
|
|
467
|
+
self._pending_named.clear()
|
|
468
|
+
for temp_name, type_cpp, init_expr, brace_init in self._pending:
|
|
469
|
+
if brace_init:
|
|
470
|
+
out.write(f"{indent}{type_cpp} {temp_name}{{{init_expr}}};\n")
|
|
471
|
+
else:
|
|
472
|
+
out.write(f"{indent}{type_cpp} {temp_name} = {init_expr};\n")
|
|
473
|
+
self._pending.clear()
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class CodeGenError(Exception):
|
|
477
|
+
"""Error during C++ code generation."""
|
|
478
|
+
def __init__(self, message: str, loc: SourceLocation | None = None,
|
|
479
|
+
filename: str | None = None):
|
|
480
|
+
self.message = message
|
|
481
|
+
self.loc = loc
|
|
482
|
+
self.filename = filename
|
|
483
|
+
super().__init__(message)
|
|
484
|
+
|
|
485
|
+
def format(self, filename: str = "<unknown>") -> str:
|
|
486
|
+
"""Format error with file:line prefix."""
|
|
487
|
+
name = self.filename or filename
|
|
488
|
+
if self.loc:
|
|
489
|
+
return f"{name}:{self.loc.line}: error: {self.message}"
|
|
490
|
+
return f"{name}: error: {self.message}"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@dataclass
|
|
494
|
+
class CodeGenOptions:
|
|
495
|
+
"""Options for C++ code generation."""
|
|
496
|
+
emit_source_comments: bool = False # Embed Python source as comments in generated C++
|
|
497
|
+
comment_line_numbers: bool = True # Include .py line numbers in source comments
|
|
498
|
+
no_main: bool = False # Skip main() generation, emit __tpy_main() only
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class LocalCppForm(Enum):
|
|
502
|
+
"""Coarse C++ representation of a local/param name.
|
|
503
|
+
|
|
504
|
+
The codegen tracks several parallel `set[str]` fields (`pointer_locals`,
|
|
505
|
+
`optional_locals`, `ptr_variant_locals`, `storage_form_tuple_locals`,
|
|
506
|
+
`storage_form_optional_locals`) plus a `current_func_params` lookup
|
|
507
|
+
for `Own[Union]` params. Every boundary-handling site (call arg,
|
|
508
|
+
return, var-decl init, rebind, tuple-unpack, etc.) needs to ask
|
|
509
|
+
"what shape is this name?" and route to the right lift / wrap
|
|
510
|
+
helper. `local_cpp_form` is the single classifier those sites consult.
|
|
511
|
+
|
|
512
|
+
Variants:
|
|
513
|
+
* `POINTER` -- `T*` / `const T*`. Pointer-form Optional or hoisted
|
|
514
|
+
non-value local. Membership in `pointer_locals` minus
|
|
515
|
+
`optional_locals`.
|
|
516
|
+
* `OPTIONAL_STORAGE` -- `std::optional<T>` storage form. `Own[Opt[T_ref]]`
|
|
517
|
+
params (also in `pointer_locals` for arrow field access). Lifts via
|
|
518
|
+
`tpy::optional_to_ptr` when consumed as a `T*` slot.
|
|
519
|
+
* `STORAGE_OPTIONAL` -- `std::optional<T>` storage form, but from
|
|
520
|
+
an iteration source (for-loop var iterating `list[P|None]` /
|
|
521
|
+
`dict[K, P|None]`, comprehension/genexpr unpack var bound from
|
|
522
|
+
a storage-form-tuple slot). Same lift as `OPTIONAL_STORAGE`
|
|
523
|
+
(`tpy::optional_to_ptr`); separate variant because these names
|
|
524
|
+
are NOT in `pointer_locals` (their access doesn't route through
|
|
525
|
+
`->`) and may be in `const_storage_form_optional_locals` for
|
|
526
|
+
const-source iteration.
|
|
527
|
+
* `VALUE_VARIANT` -- `std::variant<A, B>` storage form. `Own[Union nonvalue]`
|
|
528
|
+
params at the ABI. Lifts via `tpy::to_ptr_variant` when consumed
|
|
529
|
+
as a pointer-variant slot.
|
|
530
|
+
* `PTR_VARIANT` -- `std::variant<T*, ...>` borrow form. Non-value
|
|
531
|
+
union local already in pointer-variant shape; no lift needed.
|
|
532
|
+
* `STORAGE_TUPLE` -- `std::tuple<std::optional<T>, ...>` storage form.
|
|
533
|
+
For-loop variables iterating storage containers, locals initialized
|
|
534
|
+
from another storage-form source, `Own[tuple[T|None, ...]]` params.
|
|
535
|
+
Wraps via `tpy::tuple_to_pointer` when feeding pointer-form tuple
|
|
536
|
+
params/destructure targets.
|
|
537
|
+
* `VALUE` -- everything else (value types, T& ref-bound locals,
|
|
538
|
+
plain non-value locals rendered via T&).
|
|
539
|
+
"""
|
|
540
|
+
POINTER = auto()
|
|
541
|
+
OPTIONAL_STORAGE = auto()
|
|
542
|
+
STORAGE_OPTIONAL = auto()
|
|
543
|
+
VALUE_VARIANT = auto()
|
|
544
|
+
PTR_VARIANT = auto()
|
|
545
|
+
STORAGE_TUPLE = auto()
|
|
546
|
+
VALUE = auto()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@dataclass
|
|
550
|
+
class LocalScopeSnap:
|
|
551
|
+
"""Snapshot of the C++ local-variable declaration state inside a function body.
|
|
552
|
+
|
|
553
|
+
Covers every field that tracks which locals exist and what C++ representation
|
|
554
|
+
they use (pointer-local, const-indirect, movable, rebind slot), plus the
|
|
555
|
+
two isinstance-narrowing maps -- `narrowed_vars` (per-source-variable
|
|
556
|
+
alias bindings) and `declared_persistent_aliases` (the scope-global set of
|
|
557
|
+
persistent alias names, used by `_fresh_alias_local` for collision
|
|
558
|
+
avoidance). Both maps reference C++ locals that live only within the
|
|
559
|
+
block that declared them, so they must be revoked when the surrounding
|
|
560
|
+
C++ block closes. Used to restore scope between if/else branches so that
|
|
561
|
+
declarations inside one branch don't bleed into sibling branches.
|
|
562
|
+
|
|
563
|
+
If a new "what locals exist" field is added to CodeGenContext, add it here too.
|
|
564
|
+
|
|
565
|
+
Note: hoisted_vars and branch_hoisted_vars are NOT snapshotted -- they are
|
|
566
|
+
function-scoped accumulators. A stale entry from a prior branch's nested if
|
|
567
|
+
causes unnecessary hoisting but not incorrect code. Similarly,
|
|
568
|
+
frame_field_shadows is NOT snapshotted -- entries are either discarded by
|
|
569
|
+
the emit site that introduced them (for-loop iter var, scoped to the loop
|
|
570
|
+
body) or persist to function exit where `reset_scope` clears them (with-as
|
|
571
|
+
/ tuple-unpack, which outlive their source statement in Python scoping).
|
|
572
|
+
Both lifetimes are correct relative to branch-snapshot boundaries.
|
|
573
|
+
"""
|
|
574
|
+
declared_vars: set[str]
|
|
575
|
+
var_types: dict[str, TpyType]
|
|
576
|
+
local_scope_names: set[str]
|
|
577
|
+
pointer_locals: set[str]
|
|
578
|
+
optional_locals: set[str]
|
|
579
|
+
ptr_variant_locals: set[str]
|
|
580
|
+
const_indirect_locals: set[str]
|
|
581
|
+
storage_form_tuple_locals: set[str]
|
|
582
|
+
const_storage_form_tuple_locals: set[str]
|
|
583
|
+
storage_form_optional_locals: set[str]
|
|
584
|
+
const_storage_form_optional_locals: set[str]
|
|
585
|
+
movable_locals: set[str]
|
|
586
|
+
ref_bound_locals: set[str]
|
|
587
|
+
rebind_slots: dict[str, str]
|
|
588
|
+
plain_rebind_slots: set[str]
|
|
589
|
+
assign_narrowed_types: dict[str, 'TpyType']
|
|
590
|
+
narrowed_vars: dict[str, str]
|
|
591
|
+
declared_persistent_aliases: set[str]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@dataclass
|
|
595
|
+
class FinallyContext:
|
|
596
|
+
"""Tracks an active try/finally (or with) block.
|
|
597
|
+
|
|
598
|
+
The finally body is emitted inline at each exit site: before any
|
|
599
|
+
fall-through, return, break, or continue out of the try block, and
|
|
600
|
+
inside the catch(...) wrapper for the throw path. The unified pattern
|
|
601
|
+
is `try { body } catch (...) { <finally>; throw; } <finally>;` with
|
|
602
|
+
explicit `<finally>;` calls inserted before any non-throw exit while
|
|
603
|
+
the frame is on the stack.
|
|
604
|
+
|
|
605
|
+
The same shape is used by the resumable-frame async codegen, where
|
|
606
|
+
each suspension's case body re-establishes the active try/finally
|
|
607
|
+
structure -- making the lowering shared across sync and async.
|
|
608
|
+
"""
|
|
609
|
+
emit_finally: Callable[[TextIO, str], None]
|
|
610
|
+
"""Emit the finally body inline at the given (out, indent)."""
|
|
611
|
+
|
|
612
|
+
terminates: bool
|
|
613
|
+
"""True if the finally body's last reachable statement is raise/return.
|
|
614
|
+
When set, callers must suppress any trailing `throw;` / `return ...;`
|
|
615
|
+
after invoking emit_finally, since the body itself transferred control."""
|
|
616
|
+
|
|
617
|
+
loop_depth: int
|
|
618
|
+
"""len(loop_else_labels) at frame push time. break/continue walk
|
|
619
|
+
finally frames whose loop_depth >= current loop count, since those
|
|
620
|
+
are the frames pushed inside the innermost active loop."""
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@dataclass
|
|
624
|
+
class CodeGenContext:
|
|
625
|
+
"""Shared state for C++ code generation."""
|
|
626
|
+
|
|
627
|
+
# --- Core ---
|
|
628
|
+
analyzer: SemanticAnalyzer
|
|
629
|
+
options: CodeGenOptions
|
|
630
|
+
module_name: str = "generated"
|
|
631
|
+
source_lines: list[str] = field(default_factory=list)
|
|
632
|
+
# Peer modules in the same import-graph SCC. When a `<peer>.hpp`
|
|
633
|
+
# would be included from this module's header (vs cpp file), the
|
|
634
|
+
# codegen swaps it for `<peer>_fwd.hpp` to break the cyclic
|
|
635
|
+
# complete-type include. Empty for non-cycle modules.
|
|
636
|
+
cycle_peers: frozenset[str] = field(default_factory=frozenset)
|
|
637
|
+
|
|
638
|
+
# --- Scope tracking ---
|
|
639
|
+
indent_level: int = 0
|
|
640
|
+
declared_vars: set[str] = field(default_factory=set)
|
|
641
|
+
var_types: dict[str, TpyType] = field(default_factory=dict)
|
|
642
|
+
local_scope_names: set[str] = field(default_factory=set)
|
|
643
|
+
nested_def_locals: set[str] = field(default_factory=set)
|
|
644
|
+
# Walrus pre-declarations already emitted (not snapshot/restored across branches)
|
|
645
|
+
walrus_pre_declared: set[str] = field(default_factory=set)
|
|
646
|
+
global_names: set[str] = field(default_factory=set)
|
|
647
|
+
current_ns: Namespace | None = None
|
|
648
|
+
in_method: bool = False
|
|
649
|
+
in_consuming_method: bool = False
|
|
650
|
+
in_property_getter: bool = False
|
|
651
|
+
# The NominalType of the record whose method is currently being emitted.
|
|
652
|
+
# Set by records.py / functions.py around method bodies and read by
|
|
653
|
+
# `lookup_var_type` so `self` resolves to the enclosing class type --
|
|
654
|
+
# enables `isinstance(self, Sub)` polymorphic-dispatch routing in codegen.
|
|
655
|
+
current_method_record_type: 'TpyType | None' = None
|
|
656
|
+
current_return_type: TpyType | None = None
|
|
657
|
+
# Generator yield type. Set at generator-body entry points (state-machine
|
|
658
|
+
# __next__, simple-for/simple-while inline lambdas) and read by all yield
|
|
659
|
+
# emission sites so they share one source of truth instead of threading
|
|
660
|
+
# the type through call signatures.
|
|
661
|
+
current_yield_type: TpyType | None = None
|
|
662
|
+
current_error_return: str | None = None
|
|
663
|
+
error_return_stmt_handled: bool = False
|
|
664
|
+
current_func_params: dict[str, TpyType] = field(default_factory=dict)
|
|
665
|
+
current_type_param_bounds: dict[str, TpyType] = field(default_factory=dict)
|
|
666
|
+
const_ref_params: set[str] = field(default_factory=set)
|
|
667
|
+
# Param names whose declared const-inferred surface goes deep -- inner
|
|
668
|
+
# tuple slots, pointer-form Optional inner, etc. Drives body codegen
|
|
669
|
+
# decisions for tuple unpack, alias propagation, call-site lowering.
|
|
670
|
+
# Subset of const_ref_params for ordinary T& params; also includes
|
|
671
|
+
# Optional/Tuple param names that const_ref_params doesn't track.
|
|
672
|
+
deep_const_borrow_params: set[str] = field(default_factory=set)
|
|
673
|
+
|
|
674
|
+
# --- Temporary variable management ---
|
|
675
|
+
temps: TempState = field(default_factory=TempState)
|
|
676
|
+
|
|
677
|
+
# --- Global declaration tracking (from `global x` statements) ---
|
|
678
|
+
global_declared_vars: set[str] = field(default_factory=set)
|
|
679
|
+
|
|
680
|
+
# --- Pointer-local tracking ---
|
|
681
|
+
pointer_locals: set[str] = field(default_factory=set)
|
|
682
|
+
# Locals whose C++ shape is std::optional<T> (storage form) rather than
|
|
683
|
+
# T* (pointer form). Two populations:
|
|
684
|
+
# - Non-value hoisted vars not reassigned (branch-emitted as
|
|
685
|
+
# std::optional<T> to avoid a separate T*+slot pair).
|
|
686
|
+
# - Own[OptionalType[P_ref]] params (rendered as std::optional<P>&&).
|
|
687
|
+
# Both also live in pointer_locals so the field-access dispatch routes
|
|
688
|
+
# via optional<T>::operator-> (correct arrow). The is_storage_form_-
|
|
689
|
+
# optional_source predicate deliberately excludes them; consumer sites
|
|
690
|
+
# that need an optional_to_ptr lift to feed a T* slot check
|
|
691
|
+
# optional_locals directly (_optional_pointer_form_value,
|
|
692
|
+
# _gen_optional_ptr_arg, the var-decl and rebind paths in statements.py).
|
|
693
|
+
optional_locals: set[str] = field(default_factory=set)
|
|
694
|
+
const_indirect_locals: set[str] = field(default_factory=set)
|
|
695
|
+
# Pre-bound polymorphic-isinstance cast locals for the C++17 if-init form
|
|
696
|
+
# (var -> ptr local name). Populated by `_gen_if` before emitting the
|
|
697
|
+
# condition; the isinstance bool-check and cast-and-cache extraction both
|
|
698
|
+
# consult this map and reuse the local instead of re-emitting dynamic_cast.
|
|
699
|
+
isinstance_init_locals: dict[str, str] = field(default_factory=dict)
|
|
700
|
+
# Same idea for deref-view narrowing through an owning wrapper (Box/Rc):
|
|
701
|
+
# keyed by the wrapper receiver name (e.g. `rc`), the value is the pre-bound
|
|
702
|
+
# `Sub*` local cast from the deref payload pointer, so the condition and the
|
|
703
|
+
# narrowed member accesses share one cast instead of re-deriving it.
|
|
704
|
+
deref_view_init_locals: dict[str, str] = field(default_factory=dict)
|
|
705
|
+
# Locals whose tuple type contains pointer-repr Optional but whose C++
|
|
706
|
+
# representation is the storage form (std::tuple<std::optional<T>, ...>):
|
|
707
|
+
# for-loop variables iterating storage-form containers, and locals
|
|
708
|
+
# initialized from a storage-form source (field, subscript, global).
|
|
709
|
+
# Read sites need a tuple_to_pointer wrap to flow into pointer-form
|
|
710
|
+
# tuple params/destructure targets.
|
|
711
|
+
storage_form_tuple_locals: set[str] = field(default_factory=set)
|
|
712
|
+
# Subset of storage_form_tuple_locals: locals iterated from a const-bound
|
|
713
|
+
# source (const list&, dict.values(), self.field in readonly method, ...).
|
|
714
|
+
# When unpacking these, the storage->pointer wrap must produce const slots
|
|
715
|
+
# because optional_to_ptr returns `const T*` from a const optional<T>&.
|
|
716
|
+
const_storage_form_tuple_locals: set[str] = field(default_factory=set)
|
|
717
|
+
# Locals whose C++ shape is `std::optional<T>` (storage form) because they
|
|
718
|
+
# bind a pointer-repr-Optional element of a storage-form source:
|
|
719
|
+
# * for-loop variable iterating `list[P|None]` / `dict[K, P|None]`
|
|
720
|
+
# * comprehension/genexpr unpack variable whose tuple slot is
|
|
721
|
+
# pointer-repr-Optional from a storage-form source
|
|
722
|
+
# `is_storage_form_optional_source(TpyName(...))` returns True for these;
|
|
723
|
+
# consumer sites needing `P*` insert `optional_to_ptr` based on it.
|
|
724
|
+
# Disjoint from `optional_locals` (which tracks Own[Opt[T_ref]] params).
|
|
725
|
+
storage_form_optional_locals: set[str] = field(default_factory=set)
|
|
726
|
+
# Subset of storage_form_optional_locals: locals bound from a const-bound
|
|
727
|
+
# source (mirror of const_storage_form_tuple_locals -- when the producing
|
|
728
|
+
# iteration yields const optional<T>&, the lift must produce const T*).
|
|
729
|
+
const_storage_form_optional_locals: set[str] = field(default_factory=set)
|
|
730
|
+
# Names where codegen has emitted a C++-scoped local declaration in the
|
|
731
|
+
# current scope that shadows a putative frame field of the same name.
|
|
732
|
+
# Sema treats every function-level local as a potential frame field
|
|
733
|
+
# (added to func.generator_locals -> generator_optional_fields), but
|
|
734
|
+
# for-loop iter vars on loops without internal yield/await are emitted
|
|
735
|
+
# as `auto&& it = *__beg_n` C++ locals instead. Body-emit consults this
|
|
736
|
+
# set to suppress the `(*name)` peel that would otherwise apply to a
|
|
737
|
+
# putative storage-form-optional frame field. NOT in LocalScopeSnap --
|
|
738
|
+
# the set is body-emit scoped; entries are added and discarded around
|
|
739
|
+
# the emit site that introduced the shadow.
|
|
740
|
+
frame_field_shadows: set[str] = field(default_factory=set)
|
|
741
|
+
|
|
742
|
+
# --- Pointer-variant locals (non-value union variables) ---
|
|
743
|
+
# Variables that are std::variant<T*...> instead of std::variant<T...>.
|
|
744
|
+
ptr_variant_locals: set[str] = field(default_factory=set)
|
|
745
|
+
pointer_globals: set[str] = field(default_factory=set)
|
|
746
|
+
final_globals: set[str] = field(default_factory=set)
|
|
747
|
+
slots: SlotState = field(default_factory=SlotState)
|
|
748
|
+
rebind_slots: dict[str, str] = field(default_factory=dict)
|
|
749
|
+
plain_rebind_slots: set[str] = field(default_factory=set)
|
|
750
|
+
reassigned_vars: set[str] = field(default_factory=set)
|
|
751
|
+
rvalue_reassigned_vars: set[str] = field(default_factory=set)
|
|
752
|
+
lvalue_reassigned_vars: set[str] = field(default_factory=set)
|
|
753
|
+
|
|
754
|
+
# --- Auto-move tracking (last-use -> std::move) ---
|
|
755
|
+
movable_locals: set[str] = field(default_factory=set)
|
|
756
|
+
sema_movable_locals: set[str] = field(default_factory=set)
|
|
757
|
+
|
|
758
|
+
# --- Reference-bound locals (T& aliases -- del must not move-sink) ---
|
|
759
|
+
# ref_bound_locals grows during codegen as T& decls are emitted -> in LocalScopeSnap.
|
|
760
|
+
# aliased_vars/alias_names are set once per function from prescan -> NOT in LocalScopeSnap.
|
|
761
|
+
ref_bound_locals: set[str] = field(default_factory=set)
|
|
762
|
+
aliased_vars: set[str] = field(default_factory=set)
|
|
763
|
+
alias_names: set[str] = field(default_factory=set)
|
|
764
|
+
|
|
765
|
+
# --- Move-through vars (lvalue alias promoted to owned via std::move) ---
|
|
766
|
+
move_through_vars: set[str] = field(default_factory=set)
|
|
767
|
+
|
|
768
|
+
# --- Hoisted variable tracking (scope escape phase 2) ---
|
|
769
|
+
hoisted_vars: set[str] = field(default_factory=set)
|
|
770
|
+
# Branch-hoisted pointer-locals: declared by _emit_branch_decls for if/match/try.
|
|
771
|
+
# Rvalue slots for these vars must go to pending_hoist_decls, not block scope.
|
|
772
|
+
branch_hoisted_vars: set[str] = field(default_factory=set)
|
|
773
|
+
|
|
774
|
+
# --- Comprehension-local variable names ---
|
|
775
|
+
# Loop variables inside comprehensions shadow globals during element
|
|
776
|
+
# expression codegen. Checked early in is_global_name().
|
|
777
|
+
comp_local_names: set[str] = field(default_factory=set)
|
|
778
|
+
pending_hoist_decls: list[str] = field(default_factory=list)
|
|
779
|
+
|
|
780
|
+
# True while generating container element expressions (dict/list/set/tuple
|
|
781
|
+
# literals, comprehension elements). Ternary codegen checks this to produce
|
|
782
|
+
# std::optional<T> instead of T*/nullptr -- containers always store
|
|
783
|
+
# std::optional<T>, while locals/returns use T*.
|
|
784
|
+
in_container_element: bool = False
|
|
785
|
+
|
|
786
|
+
# --- Union type narrowing (isinstance -> std::get) ---
|
|
787
|
+
narrowed_vars: dict[str, str] = field(default_factory=dict)
|
|
788
|
+
# Protocol isinstance narrowing: var -> narrowed protocol type.
|
|
789
|
+
# For `if isinstance(x, SomeProtocol):` on a union parameter, sema narrows x
|
|
790
|
+
# to the protocol type within the branch. No std::get extraction is emitted
|
|
791
|
+
# (the C++ template param is unchanged; the narrower concept constraint
|
|
792
|
+
# holds via `if constexpr`). This map lets get_resolved_type surface the
|
|
793
|
+
# narrower type to for-loop dispatch, `in` operator, etc.
|
|
794
|
+
protocol_narrowings: dict[str, 'TpyType'] = field(default_factory=dict)
|
|
795
|
+
# Assignment narrowing: var -> narrowed concrete type (for inline std::get at access points)
|
|
796
|
+
assign_narrowed_types: dict[str, 'TpyType'] = field(default_factory=dict)
|
|
797
|
+
# Literal type narrowing: var -> single-value LiteralType for dead branch elimination
|
|
798
|
+
literal_facts: dict[str, 'TpyType'] = field(default_factory=dict)
|
|
799
|
+
# Literal overload specialization: injected facts that survive reset_scope
|
|
800
|
+
# (managed by _gen_literal_specialized_function, same pattern as overload_param_types)
|
|
801
|
+
literal_overload_facts: dict[str, 'TpyType'] = field(default_factory=dict)
|
|
802
|
+
|
|
803
|
+
# --- @overload specialization ---
|
|
804
|
+
# When generating code for a specific @overload stub, maps parameter names
|
|
805
|
+
# to their concrete (non-union) types. Used for dead branch elimination:
|
|
806
|
+
# isinstance checks on specialized params resolve statically.
|
|
807
|
+
overload_param_types: dict[str, 'TpyType'] = field(default_factory=dict)
|
|
808
|
+
# For short-arity stubs, the missing impl params that must be emitted as
|
|
809
|
+
# locals (with their default expressions) at the start of the body.
|
|
810
|
+
# Each entry: (param_name, param_type, default_expr). Consumed by
|
|
811
|
+
# StatementGenerator.gen_body right after the opening brace and then
|
|
812
|
+
# cleared so inner bodies don't re-emit them.
|
|
813
|
+
overload_missing_param_locals: list[tuple[str, 'TpyType', object]] = field(default_factory=list)
|
|
814
|
+
|
|
815
|
+
# --- Iterator loop counter ---
|
|
816
|
+
iter_counter: int = 0
|
|
817
|
+
|
|
818
|
+
# --- Tuple unpacking counter ---
|
|
819
|
+
unpack_counter: int = 0
|
|
820
|
+
|
|
821
|
+
# --- try/except ---
|
|
822
|
+
try_except_counter: int = 0
|
|
823
|
+
# When set, we're inside a try body -- error_return calls should goto this label
|
|
824
|
+
try_except_label: str | None = None
|
|
825
|
+
# When set (except E as e), error_return calls should move error into this var before goto
|
|
826
|
+
try_except_err_opt: str | None = None
|
|
827
|
+
in_except_tier: Literal["return", "throw"] | None = None
|
|
828
|
+
|
|
829
|
+
# --- finally (inline emit pattern) ---
|
|
830
|
+
# Stack of active try/finally (and with) blocks. Codegen invokes
|
|
831
|
+
# frame.emit_finally inline before each non-throw exit (return/break/
|
|
832
|
+
# continue/fall-through) and inside each try block's catch(...) wrapper.
|
|
833
|
+
# No goto labels, no shared __retval variable.
|
|
834
|
+
finally_stack: list[FinallyContext] = field(default_factory=list)
|
|
835
|
+
|
|
836
|
+
# --- with statement ---
|
|
837
|
+
with_counter: int = 0
|
|
838
|
+
# Variables pre-declared by _emit_branch_decls for for-loop hoisting
|
|
839
|
+
loop_hoisted_vars: set[str] = field(default_factory=set)
|
|
840
|
+
|
|
841
|
+
# --- Match/case label counter (for goto-based guard fallthrough) ---
|
|
842
|
+
match_counter: int = 0
|
|
843
|
+
|
|
844
|
+
# --- Generator function codegen ---
|
|
845
|
+
in_generator_body: bool = False
|
|
846
|
+
generator_field_names: set[str] = field(default_factory=set)
|
|
847
|
+
# Fields read through an outer wrap (`(*name)` deref) -- the union
|
|
848
|
+
# of user locals stored as `tpy::frame_slot<T>` and synthetic for-
|
|
849
|
+
# loop / async-with / async-for / try-finally fields still stored
|
|
850
|
+
# as `std::optional<T>`. Read sites can use the same `(*name)`
|
|
851
|
+
# access for both.
|
|
852
|
+
generator_optional_fields: set[str] = field(default_factory=set)
|
|
853
|
+
# Subset of generator_optional_fields whose C++ slot is the new
|
|
854
|
+
# `tpy::frame_slot<T>` (user locals from `func.generator_locals`,
|
|
855
|
+
# non-value, non-pointer-form). Writes here must go through
|
|
856
|
+
# `.emplace(value)` -- the helper has no operator= for arbitrary T.
|
|
857
|
+
generator_frame_slot_locals: set[str] = field(default_factory=set)
|
|
858
|
+
# Subset of pointer_locals that are borrow-form for-loop vars (a `T*`
|
|
859
|
+
# aliasing a live container element). Yielding one by name needs a deref
|
|
860
|
+
# to the value yield slot -- but pointer-repr Optional/Union locals (also
|
|
861
|
+
# in pointer_locals) must NOT be deref'd that way, hence a dedicated set.
|
|
862
|
+
generator_borrow_form_loop_vars: set[str] = field(default_factory=set)
|
|
863
|
+
# For-loops with yields in state machine generators: keyed by id(TpyForEach)
|
|
864
|
+
# Values are GeneratorForInfo (not imported here to avoid circular dep)
|
|
865
|
+
generator_for_loop_info: dict[int, object] = field(default_factory=dict)
|
|
866
|
+
# When generating a generator method's __next__() body, self -> __self
|
|
867
|
+
generator_self_ref: str | None = None
|
|
868
|
+
|
|
869
|
+
# --- Async coroutine function codegen ---
|
|
870
|
+
# When generating an `async def` body inside its struct's poll() method,
|
|
871
|
+
# `return v` is rewritten to `__state = <done>; return Poll<T>::ready(v);`.
|
|
872
|
+
# The flag piggybacks on in_generator_body for the field-rewrite path
|
|
873
|
+
# (locals -> this->field) -- both share the resumable-frame shape -- but
|
|
874
|
+
# has its own return-rewrite handling in statements.py.
|
|
875
|
+
in_async_coro_body: bool = False
|
|
876
|
+
async_coro_return_cpp: str | None = None # C++ return type for Poll<T>::ready
|
|
877
|
+
async_coro_done_state: str | None = None # name of the DONE state enumerator
|
|
878
|
+
# Generator body lowered onto the resumable frame (yield -> __next__ ->
|
|
879
|
+
# expected<T, StopIteration>). Distinct from in_generator_body, which is
|
|
880
|
+
# the legacy goto-label generator codegen: there a bare `return` emits
|
|
881
|
+
# `goto __done`, but the resumable while/switch has no such label, so a
|
|
882
|
+
# bare `return` / fall-off-end lowers to `__state = S_DONE; return
|
|
883
|
+
# make_unexpected(StopIteration{})` instead (see statements.py).
|
|
884
|
+
in_generator_resumable_body: bool = False
|
|
885
|
+
generator_resumable_done_state: str | None = None
|
|
886
|
+
# Set when emitting a helper-based finally body for a generator on the
|
|
887
|
+
# resumable frame. `return` here sets `this->__finally_stop = true` and
|
|
888
|
+
# void-returns; callers check the flag and emit StopIteration.
|
|
889
|
+
in_generator_finally_helper: bool = False
|
|
890
|
+
# Resumable-frame `match` decomposition (H1): when a suspending `match`
|
|
891
|
+
# is emitted, the existing `gen_match` dispatch is reused unchanged, but
|
|
892
|
+
# each arm BODY is routed back through the resumable walker instead of
|
|
893
|
+
# emitted inline. `resumable_arm_emitter(arm_bb)` emits one arm's body
|
|
894
|
+
# via the state machine; `resumable_arm_bb_by_body` maps `id(case.body)`
|
|
895
|
+
# to its arm BB. Consulted in the single shared `_emit_case_body`
|
|
896
|
+
# chokepoint. None outside a suspending-match emit.
|
|
897
|
+
resumable_arm_emitter: 'Callable[[int], None] | None' = None
|
|
898
|
+
resumable_arm_bb_by_body: dict[int, int] = field(default_factory=dict)
|
|
899
|
+
# Whether the `match` currently being emitted has an lvalue subject (set
|
|
900
|
+
# by `gen_match`). A non-lvalue subject is bound to a dispatch-local copy,
|
|
901
|
+
# so a pointer-form (pointer-repr `Optional`) arm binding would alias that
|
|
902
|
+
# copy and dangle across a suspension -- the resumable binding emit rejects
|
|
903
|
+
# that combination. Default True (lvalue) for non-match / non-resumable.
|
|
904
|
+
resumable_match_subject_is_lvalue: bool = True
|
|
905
|
+
# Source location of the `match` currently being emitted, used to locate
|
|
906
|
+
# the resumable binding-emit reject (the binding emit is deep in the
|
|
907
|
+
# dispatch and has no stmt in hand otherwise). Set by `gen_match`.
|
|
908
|
+
resumable_match_loc: 'SourceLocation | None' = None
|
|
909
|
+
# True iff the current generator's struct has a `__finally_stop` field
|
|
910
|
+
# (i.e. at least one helper-based finally body contains a `return`).
|
|
911
|
+
# Controls whether _emit_finally_helper_call appends the stop check.
|
|
912
|
+
generator_has_finally_stop: bool = False
|
|
913
|
+
# Pending-return routing inside a CFG-based finally region
|
|
914
|
+
#: when set, `return v` inside the active case saves `v`
|
|
915
|
+
# to the slot, sets the flag, walks finally frames up to
|
|
916
|
+
# `async_pending_return_boundary` (the position in finally_stack
|
|
917
|
+
# belonging to regions OUTSIDE the CFG-based finally -- they're
|
|
918
|
+
# walked later by AsyncFinallyExit), then transitions state to
|
|
919
|
+
# the finally entry. `slot` is None for void async defs.
|
|
920
|
+
async_pending_return_flag: str | None = None
|
|
921
|
+
async_pending_return_slot: str | None = None
|
|
922
|
+
async_pending_return_target_state: str | None = None
|
|
923
|
+
async_pending_return_boundary: int = 0
|
|
924
|
+
|
|
925
|
+
# --- for/else, while/else label stack ---
|
|
926
|
+
# When generating a loop with an else clause, the goto label name is
|
|
927
|
+
# pushed here so TpyBreak codegen can emit `goto label` instead of `break`.
|
|
928
|
+
loop_else_labels: list[str] = field(default_factory=list)
|
|
929
|
+
|
|
930
|
+
# --- Cross-module import tracking ---
|
|
931
|
+
user_module_imports: set[str] = field(default_factory=set)
|
|
932
|
+
all_user_modules: set[str] = field(default_factory=set)
|
|
933
|
+
implicit_stdlib_modules: set[str] = field(default_factory=set)
|
|
934
|
+
macro_dep_modules: set[str] = field(default_factory=set)
|
|
935
|
+
emitted_tpy_inits: set[str] = field(default_factory=set)
|
|
936
|
+
top_level_decls: dict[str, int] = field(default_factory=dict)
|
|
937
|
+
current_stmt_line: int = 0
|
|
938
|
+
|
|
939
|
+
# --- Native global variable imports ---
|
|
940
|
+
# TODO: replace with dict[str, NativeGlobalInfo] holding c_name, linkage, var_type
|
|
941
|
+
# instead of a flat name->name mapping (the TpyVarDecl nodes already carry this)
|
|
942
|
+
native_global_names: dict[str, str] = field(default_factory=dict)
|
|
943
|
+
|
|
944
|
+
def iter_imported_recursive_unions(self) -> 'Iterator[RecursiveUnionInfo]':
|
|
945
|
+
"""Yield each cross-module recursive alias once.
|
|
946
|
+
|
|
947
|
+
Skips aliases whose origin is the current module. The compiler-wide
|
|
948
|
+
index contains each alias under both its full member tuple and the
|
|
949
|
+
non-None subset, so id-based de-dup is needed -- both keys point
|
|
950
|
+
at the same RecursiveUnionInfo instance produced by `_register`.
|
|
951
|
+
"""
|
|
952
|
+
compiler = get_current_compiler()
|
|
953
|
+
if compiler is None:
|
|
954
|
+
return
|
|
955
|
+
seen: set[int] = set()
|
|
956
|
+
for info in compiler.union_wrapper_index.values():
|
|
957
|
+
if info.origin == self.module_name:
|
|
958
|
+
continue
|
|
959
|
+
if id(info) in seen:
|
|
960
|
+
continue
|
|
961
|
+
seen.add(id(info))
|
|
962
|
+
yield info
|
|
963
|
+
|
|
964
|
+
def is_ptr_variant_union(self, typ: 'TpyType') -> bool:
|
|
965
|
+
"""Check if a union type uses pointer-variant representation.
|
|
966
|
+
|
|
967
|
+
Returns True for non-value unions (e.g. Dog | Cat with records)
|
|
968
|
+
that are NOT recursive union aliases. Recursive unions use wrapper
|
|
969
|
+
structs which are value types, so they skip pointer-variant form.
|
|
970
|
+
"""
|
|
971
|
+
from ..typesys import UnionType
|
|
972
|
+
return (isinstance(typ, UnionType)
|
|
973
|
+
and typ.uses_pointer_repr()
|
|
974
|
+
and not typ.needs_wrapper())
|
|
975
|
+
|
|
976
|
+
def is_ptr_variant_source(self, expr: TpyExpr) -> bool:
|
|
977
|
+
"""Check if an expression produces a pointer variant (vs value variant).
|
|
978
|
+
|
|
979
|
+
Pointer-variant sources: ptr_variant locals, union params, function
|
|
980
|
+
calls returning non-value unions. Value-variant sources: constructors,
|
|
981
|
+
Own returns, field access, container subscript, bare alternative values.
|
|
982
|
+
|
|
983
|
+
Note: only `ReadonlyType` is unwrapped on params. `OwnType[A | B]`
|
|
984
|
+
params lower to a value-variant `std::variant<A, B>&&` (the caller
|
|
985
|
+
gave up ownership), so they should NOT take the ptr-variant path.
|
|
986
|
+
"""
|
|
987
|
+
if isinstance(expr, TpyCoerce):
|
|
988
|
+
return self.is_ptr_variant_source(expr.expr)
|
|
989
|
+
if isinstance(expr, TpyName):
|
|
990
|
+
if expr.name in self.ptr_variant_locals:
|
|
991
|
+
return True
|
|
992
|
+
# __init__ codegen sets current_func_params but doesn't populate
|
|
993
|
+
# ptr_variant_locals (only the regular function-body path does).
|
|
994
|
+
# Consult the param table directly so ctor-init MIL conversions
|
|
995
|
+
# match the body-assignment behaviour.
|
|
996
|
+
ptype = self.current_func_params.get(expr.name)
|
|
997
|
+
if ptype is not None and self.is_ptr_variant_union(unwrap_readonly(ptype)):
|
|
998
|
+
return True
|
|
999
|
+
return False
|
|
1000
|
+
if isinstance(expr, (TpyCall, TpyMethodCall)):
|
|
1001
|
+
fi = expr.resolved_function_info
|
|
1002
|
+
if fi is not None and self.is_ptr_variant_union(fi.return_type):
|
|
1003
|
+
return True
|
|
1004
|
+
return False
|
|
1005
|
+
|
|
1006
|
+
def reset_scope(self) -> None:
|
|
1007
|
+
"""Reset all per-scope state for a new function/method/module-init body."""
|
|
1008
|
+
self.declared_vars = set()
|
|
1009
|
+
self.var_types = {}
|
|
1010
|
+
self.local_scope_names = set()
|
|
1011
|
+
self.nested_def_locals = set()
|
|
1012
|
+
self.global_declared_vars = set()
|
|
1013
|
+
self.pointer_locals = set()
|
|
1014
|
+
self.optional_locals = set()
|
|
1015
|
+
self.ptr_variant_locals = set()
|
|
1016
|
+
self.const_indirect_locals = set()
|
|
1017
|
+
self.storage_form_tuple_locals = set()
|
|
1018
|
+
self.const_storage_form_tuple_locals = set()
|
|
1019
|
+
self.storage_form_optional_locals = set()
|
|
1020
|
+
self.const_storage_form_optional_locals = set()
|
|
1021
|
+
self.slots.reset()
|
|
1022
|
+
self.rebind_slots = {}
|
|
1023
|
+
self.plain_rebind_slots = set()
|
|
1024
|
+
self.reassigned_vars = set()
|
|
1025
|
+
self.rvalue_reassigned_vars = set()
|
|
1026
|
+
self.lvalue_reassigned_vars = set()
|
|
1027
|
+
self.movable_locals = set()
|
|
1028
|
+
self.ref_bound_locals = set()
|
|
1029
|
+
self.aliased_vars = set()
|
|
1030
|
+
self.alias_names = set()
|
|
1031
|
+
self.move_through_vars = set()
|
|
1032
|
+
self.hoisted_vars = set()
|
|
1033
|
+
self.branch_hoisted_vars = set()
|
|
1034
|
+
self.comp_local_names = set()
|
|
1035
|
+
self.frame_field_shadows = set()
|
|
1036
|
+
self.pending_hoist_decls = []
|
|
1037
|
+
self.current_ns = None
|
|
1038
|
+
self.indent_level = 0
|
|
1039
|
+
self.current_return_type = None
|
|
1040
|
+
self.current_yield_type = None
|
|
1041
|
+
self.current_error_return = None
|
|
1042
|
+
self.error_return_stmt_handled = False
|
|
1043
|
+
self.current_func_params = {}
|
|
1044
|
+
self.current_type_param_bounds = {}
|
|
1045
|
+
self.in_method = False
|
|
1046
|
+
self.narrowed_vars = {}
|
|
1047
|
+
# All persistent cast-and-cache aliases declared in the current C++
|
|
1048
|
+
# scope. narrowed_vars holds only the *most recent* alias per source
|
|
1049
|
+
# variable, so it loses earlier aliases after a bump (`__p` -> `__p_2`
|
|
1050
|
+
# rewrites narrowed_vars[p] and the `__p` declaration becomes
|
|
1051
|
+
# untracked even though it's still live). This set retains every
|
|
1052
|
+
# emitted alias name in the current scope so `_fresh_alias_local`'s
|
|
1053
|
+
# collision check spans the full history.
|
|
1054
|
+
self.declared_persistent_aliases: set[str] = set()
|
|
1055
|
+
self.protocol_narrowings = {}
|
|
1056
|
+
self.assign_narrowed_types = {}
|
|
1057
|
+
self.literal_facts = {}
|
|
1058
|
+
self.walrus_pre_declared = set()
|
|
1059
|
+
self.overload_terminated = False
|
|
1060
|
+
# Note: overload_param_types and literal_overload_facts are NOT reset
|
|
1061
|
+
# here -- they're managed by the caller (set before gen_body, cleared
|
|
1062
|
+
# in a finally block).
|
|
1063
|
+
self.match_counter = 0
|
|
1064
|
+
self.iter_counter = 0
|
|
1065
|
+
self.unpack_counter = 0
|
|
1066
|
+
self.loop_else_labels = []
|
|
1067
|
+
self.loop_hoisted_vars = set()
|
|
1068
|
+
self.finally_stack = []
|
|
1069
|
+
|
|
1070
|
+
def indent(self) -> str:
|
|
1071
|
+
"""Get current indentation string."""
|
|
1072
|
+
return INDENT * self.indent_level
|
|
1073
|
+
|
|
1074
|
+
def snapshot_local_scope(self) -> LocalScopeSnap:
|
|
1075
|
+
"""Snapshot the local-variable declaration state (see LocalScopeSnap)."""
|
|
1076
|
+
return LocalScopeSnap(
|
|
1077
|
+
declared_vars=self.declared_vars.copy(),
|
|
1078
|
+
var_types=dict(self.var_types),
|
|
1079
|
+
local_scope_names=self.local_scope_names.copy(),
|
|
1080
|
+
pointer_locals=self.pointer_locals.copy(),
|
|
1081
|
+
optional_locals=self.optional_locals.copy(),
|
|
1082
|
+
ptr_variant_locals=self.ptr_variant_locals.copy(),
|
|
1083
|
+
const_indirect_locals=self.const_indirect_locals.copy(),
|
|
1084
|
+
storage_form_tuple_locals=self.storage_form_tuple_locals.copy(),
|
|
1085
|
+
const_storage_form_tuple_locals=self.const_storage_form_tuple_locals.copy(),
|
|
1086
|
+
storage_form_optional_locals=self.storage_form_optional_locals.copy(),
|
|
1087
|
+
const_storage_form_optional_locals=self.const_storage_form_optional_locals.copy(),
|
|
1088
|
+
movable_locals=self.movable_locals.copy(),
|
|
1089
|
+
ref_bound_locals=self.ref_bound_locals.copy(),
|
|
1090
|
+
rebind_slots=dict(self.rebind_slots),
|
|
1091
|
+
plain_rebind_slots=self.plain_rebind_slots.copy(),
|
|
1092
|
+
assign_narrowed_types=dict(self.assign_narrowed_types),
|
|
1093
|
+
narrowed_vars=dict(self.narrowed_vars),
|
|
1094
|
+
declared_persistent_aliases=self.declared_persistent_aliases.copy(),
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
def restore_local_scope(self, snap: LocalScopeSnap) -> None:
|
|
1098
|
+
"""Restore local-variable declaration state from a snapshot."""
|
|
1099
|
+
self.declared_vars = snap.declared_vars.copy()
|
|
1100
|
+
self.var_types = dict(snap.var_types)
|
|
1101
|
+
self.local_scope_names = snap.local_scope_names.copy()
|
|
1102
|
+
self.pointer_locals = snap.pointer_locals.copy()
|
|
1103
|
+
self.optional_locals = snap.optional_locals.copy()
|
|
1104
|
+
self.ptr_variant_locals = snap.ptr_variant_locals.copy()
|
|
1105
|
+
self.const_indirect_locals = snap.const_indirect_locals.copy()
|
|
1106
|
+
self.storage_form_tuple_locals = snap.storage_form_tuple_locals.copy()
|
|
1107
|
+
self.const_storage_form_tuple_locals = snap.const_storage_form_tuple_locals.copy()
|
|
1108
|
+
self.storage_form_optional_locals = snap.storage_form_optional_locals.copy()
|
|
1109
|
+
self.const_storage_form_optional_locals = snap.const_storage_form_optional_locals.copy()
|
|
1110
|
+
self.movable_locals = snap.movable_locals.copy()
|
|
1111
|
+
self.ref_bound_locals = snap.ref_bound_locals.copy()
|
|
1112
|
+
self.rebind_slots = dict(snap.rebind_slots)
|
|
1113
|
+
self.plain_rebind_slots = snap.plain_rebind_slots.copy()
|
|
1114
|
+
self.assign_narrowed_types = dict(snap.assign_narrowed_types)
|
|
1115
|
+
self.narrowed_vars = dict(snap.narrowed_vars)
|
|
1116
|
+
self.declared_persistent_aliases = snap.declared_persistent_aliases.copy()
|
|
1117
|
+
|
|
1118
|
+
def restore_narrowed_vars(self, saved: dict[str, str | None]) -> None:
|
|
1119
|
+
"""Restore narrowed_vars after a branch block."""
|
|
1120
|
+
for var_name, prev in saved.items():
|
|
1121
|
+
if prev is not None:
|
|
1122
|
+
self.narrowed_vars[var_name] = prev
|
|
1123
|
+
else:
|
|
1124
|
+
self.narrowed_vars.pop(var_name, None)
|
|
1125
|
+
|
|
1126
|
+
def save_protocol_narrowings(self) -> dict[str, 'TpyType']:
|
|
1127
|
+
"""Snapshot protocol_narrowings before entering a branch."""
|
|
1128
|
+
return dict(self.protocol_narrowings)
|
|
1129
|
+
|
|
1130
|
+
def restore_protocol_narrowings(self, saved: dict[str, 'TpyType']) -> None:
|
|
1131
|
+
"""Restore protocol_narrowings after a branch block."""
|
|
1132
|
+
self.protocol_narrowings = dict(saved)
|
|
1133
|
+
|
|
1134
|
+
def save_literal_facts(self) -> dict[str, 'TpyType']:
|
|
1135
|
+
"""Snapshot literal_facts before entering a branch."""
|
|
1136
|
+
return dict(self.literal_facts)
|
|
1137
|
+
|
|
1138
|
+
def restore_literal_facts(self, saved: dict[str, 'TpyType']) -> None:
|
|
1139
|
+
"""Restore literal_facts after a branch block."""
|
|
1140
|
+
self.literal_facts = saved
|
|
1141
|
+
|
|
1142
|
+
def any_ancestor_has_del(self, record_name: str) -> bool:
|
|
1143
|
+
"""Check if any ancestor of the named record has __del__."""
|
|
1144
|
+
record_info = self.analyzer.registry.get_record(record_name)
|
|
1145
|
+
if record_info is None:
|
|
1146
|
+
return False
|
|
1147
|
+
return any(
|
|
1148
|
+
anc_rec.has_del
|
|
1149
|
+
for anc_rec in self.analyzer.registry.iter_ancestor_records(record_info)
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
def record_or_ancestor_has_del(self, record_name: str) -> bool:
|
|
1153
|
+
"""Check if the named record or any ancestor has __del__."""
|
|
1154
|
+
record_info = self.analyzer.registry.get_record(record_name)
|
|
1155
|
+
if record_info is None:
|
|
1156
|
+
return False
|
|
1157
|
+
if record_info.has_del:
|
|
1158
|
+
return True
|
|
1159
|
+
return self.any_ancestor_has_del(record_name)
|
|
1160
|
+
|
|
1161
|
+
def _write_source_comment(self, out: TextIO, line_no: int, source: str, indent: str = "") -> None:
|
|
1162
|
+
"""Write a source comment line, optionally including the .py line number."""
|
|
1163
|
+
source = source.lstrip()
|
|
1164
|
+
if self.options.comment_line_numbers:
|
|
1165
|
+
out.write(f"{indent}// {line_no}: {source}\n")
|
|
1166
|
+
else:
|
|
1167
|
+
out.write(f"{indent}// {source}\n")
|
|
1168
|
+
|
|
1169
|
+
def emit_source_comment(self, out: TextIO, loc: SourceLocation | None, indent: str = "") -> None:
|
|
1170
|
+
"""Emit the original Python source line(s) as a comment if enabled.
|
|
1171
|
+
|
|
1172
|
+
Walks subsequent lines until the logical line ends -- detected as the
|
|
1173
|
+
first `NEWLINE` token at bracket-depth zero. Lets multi-line
|
|
1174
|
+
signatures (`def f(\\n a: T,\\n) -> T:`) and other paren-continued
|
|
1175
|
+
expressions render their full source instead of just the opening line.
|
|
1176
|
+
"""
|
|
1177
|
+
if not self.options.emit_source_comments:
|
|
1178
|
+
return
|
|
1179
|
+
if loc is None:
|
|
1180
|
+
return
|
|
1181
|
+
start_idx = loc.line - 1
|
|
1182
|
+
if not (0 <= start_idx < len(self.source_lines)):
|
|
1183
|
+
return
|
|
1184
|
+
end_idx = start_idx
|
|
1185
|
+
# Tokenize from this line forward to find the logical-line boundary.
|
|
1186
|
+
# 32-line cap is a safety bound; real signatures fit easily. Track
|
|
1187
|
+
# paren depth via OP tokens so the boundary is the first NEWLINE/NL
|
|
1188
|
+
# at depth 0 -- `NEWLINE` ends a statement; `NL` at depth 0 ends a
|
|
1189
|
+
# comment-only or blank line (and we shouldn't pull the next stmt's
|
|
1190
|
+
# source into this comment block).
|
|
1191
|
+
snippet = "".join(
|
|
1192
|
+
line if line.endswith("\n") else line + "\n"
|
|
1193
|
+
for line in self.source_lines[start_idx:start_idx + 32]
|
|
1194
|
+
)
|
|
1195
|
+
try:
|
|
1196
|
+
import io
|
|
1197
|
+
import tokenize
|
|
1198
|
+
depth = 0
|
|
1199
|
+
for tok in tokenize.generate_tokens(io.StringIO(snippet).readline):
|
|
1200
|
+
if tok.type == tokenize.OP:
|
|
1201
|
+
if tok.string in "([{":
|
|
1202
|
+
depth += 1
|
|
1203
|
+
elif tok.string in ")]}":
|
|
1204
|
+
depth -= 1
|
|
1205
|
+
elif tok.type in (tokenize.NEWLINE, tokenize.NL) and depth <= 0:
|
|
1206
|
+
end_idx = start_idx + tok.start[0] - 1
|
|
1207
|
+
break
|
|
1208
|
+
except (tokenize.TokenError, IndentationError):
|
|
1209
|
+
pass # Fall back: just the start line.
|
|
1210
|
+
end_idx = min(end_idx, len(self.source_lines) - 1)
|
|
1211
|
+
for i in range(start_idx, end_idx + 1):
|
|
1212
|
+
self._write_source_comment(out, i + 1, self.source_lines[i].rstrip(), indent)
|
|
1213
|
+
|
|
1214
|
+
def emit_else_comment(self, out: TextIO, orelse: list, indent: str = "") -> None:
|
|
1215
|
+
"""Emit the 'else:' source line as a comment.
|
|
1216
|
+
|
|
1217
|
+
Scans backward from the first orelse statement, skipping blank lines
|
|
1218
|
+
and comment lines. Stops on any other non-empty line that is not 'else:'.
|
|
1219
|
+
"""
|
|
1220
|
+
if not self.options.emit_source_comments:
|
|
1221
|
+
return
|
|
1222
|
+
if not orelse or not hasattr(orelse[0], 'loc') or orelse[0].loc is None:
|
|
1223
|
+
return
|
|
1224
|
+
first_line = orelse[0].loc.line
|
|
1225
|
+
for line_num in range(first_line - 1, 0, -1):
|
|
1226
|
+
line_idx = line_num - 1
|
|
1227
|
+
if 0 <= line_idx < len(self.source_lines):
|
|
1228
|
+
stripped = self.source_lines[line_idx].strip()
|
|
1229
|
+
if stripped.startswith('else:'):
|
|
1230
|
+
self._write_source_comment(out, line_num, self.source_lines[line_idx].rstrip(), indent)
|
|
1231
|
+
return
|
|
1232
|
+
if stripped and not stripped.startswith('#'):
|
|
1233
|
+
return
|
|
1234
|
+
|
|
1235
|
+
def emit_inline_comments(self, out: TextIO, loc: SourceLocation | None, indent: str = "") -> None:
|
|
1236
|
+
"""Emit Python comment lines immediately preceding a statement.
|
|
1237
|
+
|
|
1238
|
+
Walks backwards from the line before loc, skipping blank lines,
|
|
1239
|
+
then collecting consecutive comment lines (#...) at the same or
|
|
1240
|
+
shallower indentation as the current statement (deeper-indented
|
|
1241
|
+
comments belong to inner blocks and are handled by
|
|
1242
|
+
emit_block_trailing_comments).
|
|
1243
|
+
Emits them in source order.
|
|
1244
|
+
"""
|
|
1245
|
+
if not self.options.emit_source_comments:
|
|
1246
|
+
return
|
|
1247
|
+
if loc is None:
|
|
1248
|
+
return
|
|
1249
|
+
stmt_line = self.source_lines[loc.line - 1]
|
|
1250
|
+
stmt_indent = len(stmt_line) - len(stmt_line.lstrip())
|
|
1251
|
+
collected: list[tuple[int, str]] = []
|
|
1252
|
+
idx = loc.line - 2 # 0-indexed line before current statement
|
|
1253
|
+
# Skip blank lines
|
|
1254
|
+
while idx >= 0 and not self.source_lines[idx].strip():
|
|
1255
|
+
idx -= 1
|
|
1256
|
+
# Collect consecutive comment lines at same or shallower indentation
|
|
1257
|
+
while idx >= 0:
|
|
1258
|
+
stripped = self.source_lines[idx].strip()
|
|
1259
|
+
if stripped.startswith("#"):
|
|
1260
|
+
line_indent = len(self.source_lines[idx]) - len(self.source_lines[idx].lstrip())
|
|
1261
|
+
if line_indent > stmt_indent:
|
|
1262
|
+
break
|
|
1263
|
+
collected.append((idx + 1, self.source_lines[idx].rstrip()))
|
|
1264
|
+
idx -= 1
|
|
1265
|
+
else:
|
|
1266
|
+
break
|
|
1267
|
+
# Emit in source order
|
|
1268
|
+
for line_no, source_line in reversed(collected):
|
|
1269
|
+
self._write_source_comment(out, line_no, source_line, indent)
|
|
1270
|
+
|
|
1271
|
+
def emit_block_trailing_comments(self, out: TextIO, body: list, indent: str = "") -> None:
|
|
1272
|
+
"""Emit trailing comment lines after the last statement in a block.
|
|
1273
|
+
|
|
1274
|
+
Scans forward from the last statement's line, emitting Python comment
|
|
1275
|
+
lines that maintain the same or deeper indentation. Stops at
|
|
1276
|
+
non-comment code or dedented lines.
|
|
1277
|
+
"""
|
|
1278
|
+
if not self.options.emit_source_comments:
|
|
1279
|
+
return
|
|
1280
|
+
if not body:
|
|
1281
|
+
return
|
|
1282
|
+
last_stmt = body[-1]
|
|
1283
|
+
if not hasattr(last_stmt, 'loc') or last_stmt.loc is None:
|
|
1284
|
+
return
|
|
1285
|
+
last_line = last_stmt.loc.line
|
|
1286
|
+
# Determine expected indentation from the last statement's source
|
|
1287
|
+
ref_idx = last_line - 1
|
|
1288
|
+
if ref_idx < 0 or ref_idx >= len(self.source_lines):
|
|
1289
|
+
return
|
|
1290
|
+
ref_line = self.source_lines[ref_idx]
|
|
1291
|
+
block_indent = len(ref_line) - len(ref_line.lstrip())
|
|
1292
|
+
|
|
1293
|
+
line_no = last_line + 1
|
|
1294
|
+
while line_no <= len(self.source_lines):
|
|
1295
|
+
line_idx = line_no - 1
|
|
1296
|
+
source_line = self.source_lines[line_idx]
|
|
1297
|
+
stripped = source_line.strip()
|
|
1298
|
+
if not stripped:
|
|
1299
|
+
line_no += 1
|
|
1300
|
+
continue
|
|
1301
|
+
line_indent = len(source_line) - len(source_line.lstrip())
|
|
1302
|
+
if line_indent < block_indent:
|
|
1303
|
+
break
|
|
1304
|
+
if stripped.startswith("#"):
|
|
1305
|
+
self._write_source_comment(out, line_no, source_line.rstrip(), indent)
|
|
1306
|
+
line_no += 1
|
|
1307
|
+
else:
|
|
1308
|
+
break
|
|
1309
|
+
|
|
1310
|
+
def emit_preceding_comments(self, out: TextIO, loc: SourceLocation | None, indent: str = "") -> None:
|
|
1311
|
+
"""Emit comments and decorators preceding a definition as C++ comments.
|
|
1312
|
+
|
|
1313
|
+
Walks backwards from the line before loc, skipping blank lines,
|
|
1314
|
+
collecting decorator lines (@...) and comment lines (#...) at the
|
|
1315
|
+
same indentation as the definition. Deeper-indented comments belong
|
|
1316
|
+
to the body of a preceding definition and are skipped.
|
|
1317
|
+
Emits them in source order.
|
|
1318
|
+
"""
|
|
1319
|
+
if not self.options.emit_source_comments:
|
|
1320
|
+
return
|
|
1321
|
+
if loc is None:
|
|
1322
|
+
return
|
|
1323
|
+
# Determine the definition's indentation
|
|
1324
|
+
def_line = self.source_lines[loc.line - 1]
|
|
1325
|
+
def_indent = len(def_line) - len(def_line.lstrip())
|
|
1326
|
+
collected: list[tuple[int, str]] = []
|
|
1327
|
+
idx = loc.line - 2 # 0-indexed line before definition
|
|
1328
|
+
# Skip blank lines between definition and block above
|
|
1329
|
+
while idx >= 0 and not self.source_lines[idx].strip():
|
|
1330
|
+
idx -= 1
|
|
1331
|
+
# Collect decorator lines
|
|
1332
|
+
while idx >= 0:
|
|
1333
|
+
stripped = self.source_lines[idx].strip()
|
|
1334
|
+
if stripped.startswith("@"):
|
|
1335
|
+
collected.append((idx + 1, self.source_lines[idx].rstrip()))
|
|
1336
|
+
idx -= 1
|
|
1337
|
+
else:
|
|
1338
|
+
break
|
|
1339
|
+
# Skip blank lines between decorators and comments
|
|
1340
|
+
while idx >= 0 and not self.source_lines[idx].strip():
|
|
1341
|
+
idx -= 1
|
|
1342
|
+
# Collect comment lines at same indentation as the definition
|
|
1343
|
+
while idx >= 0:
|
|
1344
|
+
stripped = self.source_lines[idx].strip()
|
|
1345
|
+
if stripped.startswith("#"):
|
|
1346
|
+
line_indent = len(self.source_lines[idx]) - len(self.source_lines[idx].lstrip())
|
|
1347
|
+
if line_indent > def_indent:
|
|
1348
|
+
break
|
|
1349
|
+
collected.append((idx + 1, self.source_lines[idx].rstrip()))
|
|
1350
|
+
idx -= 1
|
|
1351
|
+
else:
|
|
1352
|
+
break
|
|
1353
|
+
# Emit in source order (collected is reversed)
|
|
1354
|
+
for line_no, source_line in reversed(collected):
|
|
1355
|
+
self._write_source_comment(out, line_no, source_line, indent)
|
|
1356
|
+
|
|
1357
|
+
def lookup_var_type(self, var_name: str) -> 'TpyType | None':
|
|
1358
|
+
"""Resolve a variable's declared type in any visible scope.
|
|
1359
|
+
|
|
1360
|
+
Function-locals and parameters live in `var_types`; module-level
|
|
1361
|
+
globals are bound on the analyzer's `global_ns`. `self` inside a
|
|
1362
|
+
method body resolves to the enclosing record's type via
|
|
1363
|
+
`current_method_record_type`. Returns None when the name isn't
|
|
1364
|
+
found anywhere.
|
|
1365
|
+
"""
|
|
1366
|
+
typ = self.var_types.get(var_name)
|
|
1367
|
+
if typ is not None:
|
|
1368
|
+
return typ
|
|
1369
|
+
if (var_name == "self"
|
|
1370
|
+
and self.in_method
|
|
1371
|
+
and "self" not in self.current_func_params
|
|
1372
|
+
and self.current_method_record_type is not None):
|
|
1373
|
+
return self.current_method_record_type
|
|
1374
|
+
global_ns = self.analyzer.ctx.global_ns
|
|
1375
|
+
if global_ns is not None:
|
|
1376
|
+
binding = global_ns.lookup(var_name)
|
|
1377
|
+
if binding is not None and binding.type is not None:
|
|
1378
|
+
return binding.type
|
|
1379
|
+
return None
|
|
1380
|
+
|
|
1381
|
+
def is_global_name(self, expr: TpyExpr) -> bool:
|
|
1382
|
+
"""Check if expression is a reference to a global variable.
|
|
1383
|
+
|
|
1384
|
+
Returns False if the name is shadowed by a local variable or parameter.
|
|
1385
|
+
"""
|
|
1386
|
+
if not isinstance(expr, TpyName):
|
|
1387
|
+
return False
|
|
1388
|
+
|
|
1389
|
+
if expr.name in self.comp_local_names:
|
|
1390
|
+
return False
|
|
1391
|
+
|
|
1392
|
+
# Use namespace if available
|
|
1393
|
+
if self.current_ns:
|
|
1394
|
+
# Check if it's a global variable
|
|
1395
|
+
global_binding = self.analyzer.global_ns.lookup_local(expr.name)
|
|
1396
|
+
if global_binding is None or global_binding.kind != BindingKind.VARIABLE:
|
|
1397
|
+
return False # Not a global variable
|
|
1398
|
+
|
|
1399
|
+
# Check if locally shadowed (only if we have a local namespace, not global_ns itself)
|
|
1400
|
+
if self.current_ns is not self.analyzer.global_ns:
|
|
1401
|
+
# Traverse local namespace chain (up to but not including global_ns)
|
|
1402
|
+
ns: Namespace | None = self.current_ns
|
|
1403
|
+
while ns is not None and ns is not self.analyzer.global_ns:
|
|
1404
|
+
if ns.lookup_local(expr.name):
|
|
1405
|
+
return False # Locally shadowed
|
|
1406
|
+
ns = ns.parent
|
|
1407
|
+
|
|
1408
|
+
return True
|
|
1409
|
+
|
|
1410
|
+
# Fallback: use old tracking
|
|
1411
|
+
# local_scope_names contains function params and locally-declared variables
|
|
1412
|
+
return expr.name in self.global_names and expr.name not in self.local_scope_names
|
|
1413
|
+
|
|
1414
|
+
def is_pointer_local(self, expr: TpyExpr) -> bool:
|
|
1415
|
+
"""Check if expression is a reference to a pointer-local variable."""
|
|
1416
|
+
if not isinstance(expr, TpyName):
|
|
1417
|
+
return False
|
|
1418
|
+
return expr.name in self.pointer_locals
|
|
1419
|
+
|
|
1420
|
+
def is_storage_form_optional_source(self, expr: TpyExpr) -> bool:
|
|
1421
|
+
"""True when expr renders as a storage-form Optional[T] lvalue
|
|
1422
|
+
(`std::optional<T>&` for a non-value T) -- i.e. needs the same
|
|
1423
|
+
downstream handling at consumer sites as a record's storage-form
|
|
1424
|
+
Optional field: `optional_to_ptr` at borrow sites, double-deref
|
|
1425
|
+
at narrowing sites, `ptr_to_optional` at assignments.
|
|
1426
|
+
|
|
1427
|
+
Covers three source shapes that produce the same C++ shape:
|
|
1428
|
+
|
|
1429
|
+
* TpyFieldAccess on a storage-form Optional[T] field of a
|
|
1430
|
+
record (`obj.maybe_p` where the field is stored as
|
|
1431
|
+
`std::optional<T>`).
|
|
1432
|
+
* TpySubscript yielding a pointer-repr Optional element of a
|
|
1433
|
+
storage-form container (`pairs[i]` where
|
|
1434
|
+
`pairs: list[P|None]`, `d[k]` where `d: dict[K, P|None]`).
|
|
1435
|
+
`__getitem__` returns `std::optional<P>` (storage form).
|
|
1436
|
+
* TpyName whose `local_cpp_form` is `STORAGE_OPTIONAL` -- a
|
|
1437
|
+
for-loop variable iterating such a container, or a
|
|
1438
|
+
comprehension/genexpr unpack variable bound from a
|
|
1439
|
+
storage-form-tuple slot whose element type is pointer-repr
|
|
1440
|
+
Optional. Routed through the classifier rather than direct
|
|
1441
|
+
set lookup so all name-keyed shape dispatch goes through one
|
|
1442
|
+
place; the backing set is `storage_form_optional_locals`.
|
|
1443
|
+
|
|
1444
|
+
Note: `optional_locals` names (`Own[OptionalType[P_ref]]` params
|
|
1445
|
+
rendered as `std::optional<P>&&`) are NOT included. They share
|
|
1446
|
+
the storage-form C++ shape but are also in `pointer_locals`,
|
|
1447
|
+
and the downstream field-access dispatch already routes via
|
|
1448
|
+
`optional<T>::operator->`. The consumer sites that DO need a
|
|
1449
|
+
`optional_to_ptr` lift for these names (call-arg, return into
|
|
1450
|
+
pointer-form, pointer-local rebind) check `optional_locals`
|
|
1451
|
+
directly.
|
|
1452
|
+
"""
|
|
1453
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1454
|
+
val_type = self.get_expr_type(expr)
|
|
1455
|
+
return (isinstance(val_type, OptionalType)
|
|
1456
|
+
and val_type.uses_pointer_repr())
|
|
1457
|
+
if isinstance(expr, TpySubscript):
|
|
1458
|
+
val_type = self.get_expr_type(expr)
|
|
1459
|
+
if not (isinstance(val_type, OptionalType)
|
|
1460
|
+
and val_type.uses_pointer_repr()):
|
|
1461
|
+
return False
|
|
1462
|
+
# Tuple-subscript codegen pre-lifts via `optional_to_ptr` (see
|
|
1463
|
+
# `_gen_subscript` tuple branch) when the object is a storage-form
|
|
1464
|
+
# tuple source. The rendered expression is already `T*`, so
|
|
1465
|
+
# consumers must NOT lift again.
|
|
1466
|
+
obj_type = self.get_expr_type(expr.obj)
|
|
1467
|
+
if obj_type is not None and isinstance(unwrap_qualifiers(obj_type), TupleType):
|
|
1468
|
+
return False
|
|
1469
|
+
return True
|
|
1470
|
+
if isinstance(expr, TpyName):
|
|
1471
|
+
if self.local_cpp_form(expr.name) is LocalCppForm.STORAGE_OPTIONAL:
|
|
1472
|
+
return True
|
|
1473
|
+
return False
|
|
1474
|
+
|
|
1475
|
+
def _is_pointer_global(self, expr: TpyExpr) -> bool:
|
|
1476
|
+
"""Check if expression is a reference to a non-value-type global (T*)."""
|
|
1477
|
+
if not isinstance(expr, TpyName):
|
|
1478
|
+
return False
|
|
1479
|
+
return expr.name in self.pointer_globals and self.is_global_name(expr)
|
|
1480
|
+
|
|
1481
|
+
def is_storage_form_source(self, expr: TpyExpr) -> bool:
|
|
1482
|
+
"""True when `expr` reads a value from a storage location.
|
|
1483
|
+
|
|
1484
|
+
Storage locations are fields, container subscripts, globals,
|
|
1485
|
+
locals flagged as storage-form (loop vars iterating storage
|
|
1486
|
+
containers, locals initialized from another storage-form source),
|
|
1487
|
+
and function calls whose return type is `Own[tuple[T_ref,...]]`
|
|
1488
|
+
(the storage form is the function's return ABI). Used by
|
|
1489
|
+
tuple-of-pointer-Optional callers to decide whether to emit an
|
|
1490
|
+
element-wise tuple_to_pointer wrap; the value's actual type /
|
|
1491
|
+
shape is the caller's responsibility to validate.
|
|
1492
|
+
"""
|
|
1493
|
+
if isinstance(expr, (TpyFieldAccess, TpySubscript)):
|
|
1494
|
+
return True
|
|
1495
|
+
if isinstance(expr, TpyName):
|
|
1496
|
+
if expr.name in self.storage_form_tuple_locals:
|
|
1497
|
+
return True
|
|
1498
|
+
if self.is_global_name(expr):
|
|
1499
|
+
return True
|
|
1500
|
+
if isinstance(expr, (TpyCall, TpyMethodCall)):
|
|
1501
|
+
fi = expr.resolved_function_info
|
|
1502
|
+
if fi is not None:
|
|
1503
|
+
rt = unwrap_readonly(fi.return_type)
|
|
1504
|
+
# Two shapes both produce a storage-form tuple return:
|
|
1505
|
+
# * Own[tuple[T,...]] (pre-lowering form, may persist in
|
|
1506
|
+
# some call paths).
|
|
1507
|
+
# * tuple[Own[T_ref], T_value, ...] (post-own_tuple_target
|
|
1508
|
+
# synthesis -- the per-element Own pushes storage form
|
|
1509
|
+
# into each ref-typed slot).
|
|
1510
|
+
if isinstance(rt, OwnType) and isinstance(rt.wrapped, TupleType):
|
|
1511
|
+
return True
|
|
1512
|
+
if isinstance(rt, TupleType) and any(
|
|
1513
|
+
isinstance(et, OwnType) for et in rt.element_types):
|
|
1514
|
+
return True
|
|
1515
|
+
return False
|
|
1516
|
+
|
|
1517
|
+
def callee_returns_own_ptr_optional(self, init: 'TpyExpr') -> bool:
|
|
1518
|
+
"""True when `init` is a function call returning
|
|
1519
|
+
`Own[OptionalType[T_ref]]` (storage form). Codegen sites bridging
|
|
1520
|
+
the function's `std::optional<T>` return into a pointer-form local
|
|
1521
|
+
check this to know they need the `optional_to_ptr` lift.
|
|
1522
|
+
"""
|
|
1523
|
+
if not isinstance(init, (TpyCall, TpyMethodCall)):
|
|
1524
|
+
return False
|
|
1525
|
+
fi = init.resolved_function_info
|
|
1526
|
+
if fi is None:
|
|
1527
|
+
return False
|
|
1528
|
+
return is_own_pointer_repr_optional(fi.return_type)
|
|
1529
|
+
|
|
1530
|
+
def is_own_ptr_variant_param(self, name: str) -> bool:
|
|
1531
|
+
"""True when `name` is a function parameter typed
|
|
1532
|
+
`Own[UnionType non-value]` (pointer-variant Union, storage form
|
|
1533
|
+
at the param ABI). Mirror of `optional_locals` for unions;
|
|
1534
|
+
consumers needing the `to_ptr_variant` lift check this.
|
|
1535
|
+
"""
|
|
1536
|
+
ptype = self.current_func_params.get(name)
|
|
1537
|
+
if ptype is None:
|
|
1538
|
+
return False
|
|
1539
|
+
peeled = unwrap_readonly(ptype)
|
|
1540
|
+
return (isinstance(peeled, OwnType)
|
|
1541
|
+
and isinstance(peeled.wrapped, UnionType)
|
|
1542
|
+
and self.is_ptr_variant_union(peeled.wrapped))
|
|
1543
|
+
|
|
1544
|
+
def local_cpp_form(self, name: str) -> LocalCppForm:
|
|
1545
|
+
"""Classify a name's C++ representation -- see `LocalCppForm`.
|
|
1546
|
+
|
|
1547
|
+
Priority order matters: `optional_locals` is a subset of
|
|
1548
|
+
`pointer_locals` (Own[Opt[T_ref]] params live in both for arrow
|
|
1549
|
+
access vs. lift dispatch), so OPTIONAL_STORAGE must take
|
|
1550
|
+
precedence over POINTER. STORAGE_OPTIONAL tracks iteration-source
|
|
1551
|
+
loop/unpack vars (populated only by
|
|
1552
|
+
`register_loop_var_storage_form`); its population is
|
|
1553
|
+
producer-disjoint from `optional_locals` today, so the priority
|
|
1554
|
+
slot is conventional, not load-bearing -- a same-name shadow
|
|
1555
|
+
between a param and a loop var would still resolve consistently
|
|
1556
|
+
because both forms lift the same way via `optional_to_ptr`.
|
|
1557
|
+
VALUE_VARIANT (Own[Union] param) is looked up via
|
|
1558
|
+
`current_func_params`, not a set; it precedes PTR_VARIANT
|
|
1559
|
+
because a single name cannot match both.
|
|
1560
|
+
|
|
1561
|
+
For arrow-vs-deref field access use `is_pointer_local(expr)`
|
|
1562
|
+
instead -- it covers both POINTER and OPTIONAL_STORAGE.
|
|
1563
|
+
"""
|
|
1564
|
+
if name in self.optional_locals:
|
|
1565
|
+
return LocalCppForm.OPTIONAL_STORAGE
|
|
1566
|
+
if name in self.storage_form_optional_locals:
|
|
1567
|
+
return LocalCppForm.STORAGE_OPTIONAL
|
|
1568
|
+
if self.is_own_ptr_variant_param(name):
|
|
1569
|
+
return LocalCppForm.VALUE_VARIANT
|
|
1570
|
+
if name in self.ptr_variant_locals:
|
|
1571
|
+
return LocalCppForm.PTR_VARIANT
|
|
1572
|
+
if name in self.storage_form_tuple_locals:
|
|
1573
|
+
return LocalCppForm.STORAGE_TUPLE
|
|
1574
|
+
if name in self.pointer_locals:
|
|
1575
|
+
return LocalCppForm.POINTER
|
|
1576
|
+
return LocalCppForm.VALUE
|
|
1577
|
+
|
|
1578
|
+
def iteration_yields_const(self, iterable: TpyExpr) -> bool:
|
|
1579
|
+
"""True when iterating `iterable` binds the loop var as const.
|
|
1580
|
+
|
|
1581
|
+
Detects const-source iteration patterns where the resulting tuple
|
|
1582
|
+
elements come out as `const optional<T>&` -- so the storage->pointer
|
|
1583
|
+
wrap must use `to_cpp_return_const()` to match optional_to_ptr's
|
|
1584
|
+
`const T*` output.
|
|
1585
|
+
"""
|
|
1586
|
+
# field-of-self in a readonly method: self is const, field is const ref
|
|
1587
|
+
if isinstance(iterable, TpyFieldAccess) and isinstance(iterable.obj, TpyName):
|
|
1588
|
+
if iterable.obj.name == "self" and "self" in self.const_ref_params:
|
|
1589
|
+
return True
|
|
1590
|
+
# const-inferred param or alias of one
|
|
1591
|
+
if isinstance(iterable, TpyName):
|
|
1592
|
+
return (iterable.name in self.const_indirect_locals
|
|
1593
|
+
or iterable.name in self.const_ref_params)
|
|
1594
|
+
return False
|
|
1595
|
+
|
|
1596
|
+
def register_frame_field_shadow(self, name: str) -> bool:
|
|
1597
|
+
"""Mark `name` as a C++-local shadow of a frame field for the
|
|
1598
|
+
rest of the current scope. Body-emit consults `frame_field_shadows`
|
|
1599
|
+
to suppress the `(*name)` peel that would otherwise apply to a
|
|
1600
|
+
`generator_optional_fields` member. No-op outside a resumable
|
|
1601
|
+
body or if `name` is already shadowed.
|
|
1602
|
+
|
|
1603
|
+
Returns True iff this call added the entry. Callers whose
|
|
1604
|
+
C++-local binding is block-scoped (e.g. for-loop iter var) use
|
|
1605
|
+
the return value to gate a paired discard at scope exit; callers
|
|
1606
|
+
whose binding outlives its source statement (with-as, tuple-
|
|
1607
|
+
unpack -- Python scoping) ignore the return and rely on
|
|
1608
|
+
`reset_scope` to clear the set at function exit.
|
|
1609
|
+
"""
|
|
1610
|
+
if not self.in_generator_body or name in self.frame_field_shadows:
|
|
1611
|
+
return False
|
|
1612
|
+
self.frame_field_shadows.add(name)
|
|
1613
|
+
return True
|
|
1614
|
+
|
|
1615
|
+
def register_loop_var_storage_form(self, name: str, elem_type: 'TpyType | None',
|
|
1616
|
+
iterable: 'TpyExpr',
|
|
1617
|
+
detect_const_source: bool = True) -> None:
|
|
1618
|
+
"""Register a loop / unpack variable into the storage-form tracking
|
|
1619
|
+
sets based on its sema element type.
|
|
1620
|
+
|
|
1621
|
+
Shared by the producer-side sites: `_gen_loop_body` (regular
|
|
1622
|
+
for-loop) and `_enter_comp_scope` (comprehension/genexpr scope). The
|
|
1623
|
+
sites differ
|
|
1624
|
+
only in whether they detect a const-bound iteration source:
|
|
1625
|
+
- regular for-loop + comp/genexpr: default
|
|
1626
|
+
`detect_const_source=True` runs `iteration_yields_const`
|
|
1627
|
+
against `iterable`; the const-variant set gets populated
|
|
1628
|
+
when the source is const-bound.
|
|
1629
|
+
- generator-body for-loop: pass `detect_const_source=False`
|
|
1630
|
+
(no const-source channel in generator bodies today; tracked
|
|
1631
|
+
separately).
|
|
1632
|
+
|
|
1633
|
+
Peels `ReadonlyType` from `elem_type` -- const-source iteration
|
|
1634
|
+
yields `ReadonlyType(OptionalType(P))` / `ReadonlyType(TupleType(...))`
|
|
1635
|
+
at the sema level, but the C++ shape is the same storage form;
|
|
1636
|
+
const-ness rides on `iteration_yields_const`.
|
|
1637
|
+
"""
|
|
1638
|
+
if elem_type is None:
|
|
1639
|
+
return
|
|
1640
|
+
elem_peeled = unwrap_readonly(elem_type)
|
|
1641
|
+
is_const_source = (detect_const_source
|
|
1642
|
+
and self.iteration_yields_const(iterable))
|
|
1643
|
+
if (isinstance(elem_peeled, TupleType)
|
|
1644
|
+
and elem_peeled.has_pointer_repr_optional_element()):
|
|
1645
|
+
self.storage_form_tuple_locals.add(name)
|
|
1646
|
+
if is_const_source:
|
|
1647
|
+
self.const_storage_form_tuple_locals.add(name)
|
|
1648
|
+
if (isinstance(elem_peeled, OptionalType)
|
|
1649
|
+
and elem_peeled.uses_pointer_repr()):
|
|
1650
|
+
self.storage_form_optional_locals.add(name)
|
|
1651
|
+
if is_const_source:
|
|
1652
|
+
self.const_storage_form_optional_locals.add(name)
|
|
1653
|
+
|
|
1654
|
+
def needs_optional_to_ptr_lift(self, name: str) -> bool:
|
|
1655
|
+
"""True when `name` is in `OPTIONAL_STORAGE` form -- an
|
|
1656
|
+
`Own[Opt[T_ref]]` param whose C++ shape is `std::optional<T>`
|
|
1657
|
+
and must be lifted via `tpy::optional_to_ptr` to feed a `T*`
|
|
1658
|
+
slot.
|
|
1659
|
+
|
|
1660
|
+
For call-expression sources (function returning `Own[Opt[T_ref]]`),
|
|
1661
|
+
use `callee_returns_own_ptr_optional` instead -- those paths
|
|
1662
|
+
also allocate a slot to hold the rvalue and aren't a pure lift.
|
|
1663
|
+
"""
|
|
1664
|
+
return self.local_cpp_form(name) is LocalCppForm.OPTIONAL_STORAGE
|
|
1665
|
+
|
|
1666
|
+
def needs_to_ptr_variant_lift(self, name: str) -> bool:
|
|
1667
|
+
"""True when `name` is in `VALUE_VARIANT` form -- an
|
|
1668
|
+
`Own[Union nonvalue]` param whose C++ shape is `std::variant<A, B>`
|
|
1669
|
+
and must be lifted via `tpy::to_ptr_variant` to feed a
|
|
1670
|
+
`std::variant<A*, B*>` slot.
|
|
1671
|
+
"""
|
|
1672
|
+
return self.local_cpp_form(name) is LocalCppForm.VALUE_VARIANT
|
|
1673
|
+
|
|
1674
|
+
def polymorphic_cast_arg(self, var_name: str, var_decl: 'TpyType | None') -> str:
|
|
1675
|
+
"""C++ expression to use as the input to dynamic_cast<Sub*>(...)
|
|
1676
|
+
when narrowing the polymorphic source `var_name`.
|
|
1677
|
+
|
|
1678
|
+
Polymorphic isinstance/dynamic_cast needs a pointer input; the right
|
|
1679
|
+
prefix depends on the variable's C++ binding shape:
|
|
1680
|
+
- `self` inside a generator/async body -> `&{generator_self_ref}`
|
|
1681
|
+
(the body sees self via `__self: T&` for async/multi-yield or
|
|
1682
|
+
`(*this)` for simple generators -- both yield `T*` when address-
|
|
1683
|
+
of'd; the `&(*this)` form is folded by the optimizer)
|
|
1684
|
+
- `self` in a regular method -> `this` (already a pointer)
|
|
1685
|
+
- pointer-globals, pointer-locals, imported pointers
|
|
1686
|
+
(`is_indirect_name`) -> the bare name (already a pointer)
|
|
1687
|
+
- declared `Optional[Polymorphic]` (pointer-repr) param -> the bare
|
|
1688
|
+
name (the C++ binding is `T*`)
|
|
1689
|
+
- bare polymorphic param (`T&`) or anything else not above
|
|
1690
|
+
-> `&name` (take the address)
|
|
1691
|
+
"""
|
|
1692
|
+
name_expr = TpyName(var_name)
|
|
1693
|
+
if var_name == "self" and self.generator_self_ref is not None:
|
|
1694
|
+
# In simple-generator bodies generator_self_ref is `(*this)`;
|
|
1695
|
+
# &(*this) == this. In async/multi-yield bodies it's `__self`
|
|
1696
|
+
# (a T& field on the frame struct); &__self gives T*.
|
|
1697
|
+
if self.generator_self_ref == "(*this)":
|
|
1698
|
+
return "this"
|
|
1699
|
+
return f"&{self.generator_self_ref}"
|
|
1700
|
+
if var_name == "self" and self.is_indirect_name(name_expr):
|
|
1701
|
+
return "this"
|
|
1702
|
+
escaped = escape_cpp_name(var_name)
|
|
1703
|
+
if self.is_indirect_name(name_expr):
|
|
1704
|
+
return escaped
|
|
1705
|
+
if polymorphic_source_is_pointer(var_decl):
|
|
1706
|
+
return escaped
|
|
1707
|
+
return f"&{escaped}"
|
|
1708
|
+
|
|
1709
|
+
def deref_view_cast_arg(self, var_name: str, depth: int) -> str:
|
|
1710
|
+
"""C++ payload pointer for a deref-view isinstance/narrowing on owning
|
|
1711
|
+
wrapper `var_name`: `&(<v>.__deref__()...)` peeled `depth` times. The
|
|
1712
|
+
wrapper's reference-returning __deref__ yields a `[const] P&`, so the
|
|
1713
|
+
address-of is the same `[const] P*` dynamic_cast input the bare/Ptr/
|
|
1714
|
+
Optional sources expose directly. Const-ness tracks the wrapper
|
|
1715
|
+
receiver (auto_readonly __deref__), decided by the caller."""
|
|
1716
|
+
name_expr = TpyName(var_name)
|
|
1717
|
+
escaped = escape_cpp_name(var_name)
|
|
1718
|
+
base = f"(*{escaped})" if self.is_indirect_name(name_expr) else escaped
|
|
1719
|
+
chain = ".__deref__()" * depth
|
|
1720
|
+
return f"&({base}{chain})"
|
|
1721
|
+
|
|
1722
|
+
def deref_dispatch_inner(self, var_decl: 'TpyType | None', depth: int) -> 'TpyType | None':
|
|
1723
|
+
"""Peel `depth` __deref__ steps off `var_decl` to recover the
|
|
1724
|
+
dispatch inner (the @dynamic protocol / polymorphic class behind an
|
|
1725
|
+
owning wrapper), so codegen can pick dynamic_cast vs dyn_adapter_cast
|
|
1726
|
+
for a deref-view narrowing."""
|
|
1727
|
+
cur = unwrap_readonly(var_decl) if var_decl is not None else None
|
|
1728
|
+
type_ops = self.analyzer.type_ops
|
|
1729
|
+
for _ in range(depth):
|
|
1730
|
+
if cur is None:
|
|
1731
|
+
return None
|
|
1732
|
+
cur = type_ops.get_deref_target_type(cur)
|
|
1733
|
+
if cur is not None:
|
|
1734
|
+
cur = unwrap_readonly(cur)
|
|
1735
|
+
return cur
|
|
1736
|
+
|
|
1737
|
+
def deref_dispatch_source(self, var_decl: 'TpyType | None') -> 'tuple[TpyType, int] | None':
|
|
1738
|
+
"""Peel __deref__ steps off `var_decl` until reaching a @dynamic
|
|
1739
|
+
dispatch inner; return (inner, depth). Mirrors sema's
|
|
1740
|
+
CallAnalyzer._deref_dispatch_inner so the if-init cast-and-cache can
|
|
1741
|
+
recover the depth a deref-view fact omits (the fact carries only the
|
|
1742
|
+
narrowed subclass, not the wrapper or its deref depth)."""
|
|
1743
|
+
from ..typesys import is_dynamic_dispatch_inner
|
|
1744
|
+
cur = unwrap_readonly(var_decl) if var_decl is not None else None
|
|
1745
|
+
type_ops = self.analyzer.type_ops
|
|
1746
|
+
for depth in range(1, 9):
|
|
1747
|
+
if cur is None:
|
|
1748
|
+
return None
|
|
1749
|
+
target = type_ops.get_deref_target_type(cur)
|
|
1750
|
+
if target is None:
|
|
1751
|
+
return None
|
|
1752
|
+
inner = unwrap_readonly(target)
|
|
1753
|
+
if (isinstance(inner, NominalType)
|
|
1754
|
+
and is_dynamic_dispatch_inner(inner, self.analyzer.registry)):
|
|
1755
|
+
return inner, depth
|
|
1756
|
+
cur = inner
|
|
1757
|
+
return None
|
|
1758
|
+
|
|
1759
|
+
def is_indirect_name(self, expr: TpyExpr) -> bool:
|
|
1760
|
+
"""Check if expression needs indirect access (-> / deref).
|
|
1761
|
+
|
|
1762
|
+
Unifies pointer-globals (T*), pointer-locals (T*), cross-module
|
|
1763
|
+
imported pointer variables, and `self` (this pointer in methods)
|
|
1764
|
+
-- all use -> for field/method access and (*x) for value dereference.
|
|
1765
|
+
"""
|
|
1766
|
+
if isinstance(expr, TpyName) and expr.name in self.comp_local_names:
|
|
1767
|
+
return False
|
|
1768
|
+
# self -> this (pointer) in instance methods, unless inside a generator
|
|
1769
|
+
# where generator_self_ref is already a dereferenced reference
|
|
1770
|
+
if (isinstance(expr, TpyName) and expr.name == "self"
|
|
1771
|
+
and self.in_method and "self" not in self.current_func_params
|
|
1772
|
+
and self.generator_self_ref is None):
|
|
1773
|
+
return True
|
|
1774
|
+
if self._is_pointer_global(expr) or self.is_pointer_local(expr):
|
|
1775
|
+
return True
|
|
1776
|
+
if isinstance(expr, TpyName):
|
|
1777
|
+
imp = lookup_imported(
|
|
1778
|
+
self.analyzer.ctx.module_attributes,
|
|
1779
|
+
expr.name, SymbolKind.VARIABLE)
|
|
1780
|
+
if imp is not None:
|
|
1781
|
+
source_module, original_name = imp
|
|
1782
|
+
module_info = self.analyzer.registry.get_module(source_module)
|
|
1783
|
+
if module_info and original_name in module_info.variables:
|
|
1784
|
+
return module_info.variables[original_name].is_pointer
|
|
1785
|
+
return False
|
|
1786
|
+
|
|
1787
|
+
def setup_resumable_frame_locals(self, func: 'TpyFunction') -> None:
|
|
1788
|
+
"""Populate `pointer_locals`, `generator_optional_fields`, and
|
|
1789
|
+
`generator_frame_slot_locals` for a resumable-frame body
|
|
1790
|
+
(generator __next__ or async __poll__).
|
|
1791
|
+
|
|
1792
|
+
For each local in `func.generator_locals`:
|
|
1793
|
+
- value type: not tracked (frame slot is `T name;`, no peel)
|
|
1794
|
+
- pointer-form (pointer-repr Optional[NonValue] OR a for-loop
|
|
1795
|
+
iter var with a stable lvalue source): added to
|
|
1796
|
+
`pointer_locals` -- same dispatch as sync pointer-locals
|
|
1797
|
+
- other non-value: added to both `generator_optional_fields`
|
|
1798
|
+
and `generator_frame_slot_locals` (frame slot is
|
|
1799
|
+
`tpy::frame_slot<T> name;`, reads via `(*name)`,
|
|
1800
|
+
writes via `.emplace(value)`)
|
|
1801
|
+
|
|
1802
|
+
Caller's contract: all three sets are cleared at body entry and
|
|
1803
|
+
restored on exit (matches the existing `generator_*` save/restore
|
|
1804
|
+
dance). `generator_for_loop_info` must already be populated.
|
|
1805
|
+
"""
|
|
1806
|
+
# Pointer-form iter vars are seeded up-front so the for-loop
|
|
1807
|
+
# emit path doesn't need to mutate `pointer_locals` mid-emission.
|
|
1808
|
+
# Both the legacy struct path and the resumable for-loop emit
|
|
1809
|
+
# (`gen_async._prescan_resumable_for_loops`) populate
|
|
1810
|
+
# `generator_for_loop_info`; bodies with no for-loop leave it empty
|
|
1811
|
+
# (the loop is then a no-op).
|
|
1812
|
+
for info in self.generator_for_loop_info.values():
|
|
1813
|
+
if info.pointer_form_loop_var is not None:
|
|
1814
|
+
self.pointer_locals.add(info.pointer_form_loop_var)
|
|
1815
|
+
self.generator_borrow_form_loop_vars.add(info.pointer_form_loop_var)
|
|
1816
|
+
# Tuple-unpack targets aliasing a non-value container member:
|
|
1817
|
+
# stored as `T*` (alias into the live element), same dispatch
|
|
1818
|
+
# as a pointer-form loop var.
|
|
1819
|
+
self.pointer_locals.update(info.pointer_form_unpack_targets)
|
|
1820
|
+
self.generator_borrow_form_loop_vars.update(info.pointer_form_unpack_targets)
|
|
1821
|
+
|
|
1822
|
+
if not func.generator_locals:
|
|
1823
|
+
return
|
|
1824
|
+
|
|
1825
|
+
for lname, ltype in func.generator_locals:
|
|
1826
|
+
ltype_inner = unwrap_ref_type(ltype)
|
|
1827
|
+
if ltype_inner.is_value_type():
|
|
1828
|
+
continue
|
|
1829
|
+
if (isinstance(ltype_inner, OptionalType)
|
|
1830
|
+
and ltype_inner.uses_pointer_repr()):
|
|
1831
|
+
self.pointer_locals.add(lname)
|
|
1832
|
+
continue
|
|
1833
|
+
if lname in self.pointer_locals:
|
|
1834
|
+
continue
|
|
1835
|
+
self.generator_optional_fields.add(lname)
|
|
1836
|
+
# User-local non-pointer-form non-value frame fields are backed by
|
|
1837
|
+
# `tpy::frame_slot<T>` (emitted by the resumable struct field-decl
|
|
1838
|
+
# path in `gen_async.gen_coro_struct`). Writes route through
|
|
1839
|
+
# `.emplace(...)`; reads use the `(*name)` access.
|
|
1840
|
+
self.generator_frame_slot_locals.add(lname)
|
|
1841
|
+
|
|
1842
|
+
def is_already_pointer_source(self, expr: TpyExpr) -> bool:
|
|
1843
|
+
"""True when `expr` renders as a `T*` value with no further lifting.
|
|
1844
|
+
|
|
1845
|
+
Composes `is_indirect_name` (pointer locals/globals/self/imports --
|
|
1846
|
+
names whose rendering is already `T*`) with the `Ptr[T]` source
|
|
1847
|
+
case (any expression whose sema type is `PtrType`). Used by the
|
|
1848
|
+
codegen sites that lift other source shapes via `&(...)` or
|
|
1849
|
+
`optional_to_ptr`: when this returns True, the bare rendering is
|
|
1850
|
+
already in the right shape and the lift is a bug (`T**` / wrong type).
|
|
1851
|
+
"""
|
|
1852
|
+
if self.is_indirect_name(expr):
|
|
1853
|
+
return True
|
|
1854
|
+
return isinstance(self.get_expr_type(expr), PtrType)
|
|
1855
|
+
|
|
1856
|
+
def get_expr_type(self, expr: TpyExpr) -> TpyType | None:
|
|
1857
|
+
"""Get the sema-analyzed type of an expression, unwrapping ReadonlyType.
|
|
1858
|
+
|
|
1859
|
+
Codegen doesn't need ReadonlyType (C++ handles const via signatures),
|
|
1860
|
+
so this always strips it.
|
|
1861
|
+
"""
|
|
1862
|
+
typ = self.analyzer.get_expr_type(expr)
|
|
1863
|
+
return unwrap_readonly(typ) if typ is not None else None
|
|
1864
|
+
|
|
1865
|
+
def pointer_value_expr(self, expr: TpyExpr, rendered: str) -> str:
|
|
1866
|
+
"""Return expression yielding raw pointer value for pointer names.
|
|
1867
|
+
|
|
1868
|
+
Most pointer-globals are wrapped and need one dereference (`*name`) to get
|
|
1869
|
+
`T*`, but globals whose declared type is already pointer-like (Ptr, Ptr[readonly[T]],
|
|
1870
|
+
or Optional non-value) are already `T*` and must be returned as-is.
|
|
1871
|
+
"""
|
|
1872
|
+
if not self._is_pointer_global(expr):
|
|
1873
|
+
return rendered
|
|
1874
|
+
expr_type = self.get_expr_type(expr)
|
|
1875
|
+
if isinstance(expr_type, PtrType):
|
|
1876
|
+
return rendered
|
|
1877
|
+
if isinstance(expr_type, OptionalType) and expr_type.uses_pointer_repr():
|
|
1878
|
+
return rendered
|
|
1879
|
+
return f"(*{rendered})"
|
|
1880
|
+
|
|
1881
|
+
def _call_returns_cpp_ref(self, fi: FunctionInfo | None, obj: TpyExpr | None = None) -> bool:
|
|
1882
|
+
"""True if this function/method call returns a C++ lvalue reference (T&).
|
|
1883
|
+
|
|
1884
|
+
User-defined functions/methods with non-value, non-generic, non-owning
|
|
1885
|
+
return types emit T& in C++ (via to_cpp_return()). Everything else --
|
|
1886
|
+
native imports, @native record methods, TypeParamRef (val_or_ref_t<T>),
|
|
1887
|
+
Own[T], Optional[T] -- uses value semantics.
|
|
1888
|
+
"""
|
|
1889
|
+
if fi is None or fi.is_native_import:
|
|
1890
|
+
return False
|
|
1891
|
+
# Record constructors return rvalue temporaries, never C++ T&. The
|
|
1892
|
+
# bare `fi.return_type is the record class` test below would otherwise
|
|
1893
|
+
# mis-classify a constructor for a non-value-type class as returning
|
|
1894
|
+
# by reference -- triggering `T& var = T()` indirection at the call
|
|
1895
|
+
# site, which fails to bind. Set in _set_record_constructor_info.
|
|
1896
|
+
if fi.is_constructor:
|
|
1897
|
+
return False
|
|
1898
|
+
if obj is not None:
|
|
1899
|
+
# Methods on @native records have unknown C++ return convention.
|
|
1900
|
+
raw_obj_type = self.analyzer.get_expr_type(obj)
|
|
1901
|
+
obj_type = unwrap_readonly(raw_obj_type) if raw_obj_type is not None else None
|
|
1902
|
+
rec = self.analyzer.registry.get_record_for_type(obj_type) if obj_type else None
|
|
1903
|
+
if rec is None and isinstance(obj, TpyName):
|
|
1904
|
+
rec = self.analyzer.registry.get_record(obj.name)
|
|
1905
|
+
if rec is not None and rec.is_native:
|
|
1906
|
+
return False
|
|
1907
|
+
rt = unwrap_ref_type(fi.return_type)
|
|
1908
|
+
return (rt is not None
|
|
1909
|
+
and not rt.is_value_type()
|
|
1910
|
+
and not isinstance(rt, (TypeParamRef, OwnType, OptionalType, UnionType))
|
|
1911
|
+
and not is_protocol_type(rt))
|
|
1912
|
+
|
|
1913
|
+
def is_rvalue_source(self, expr: TpyExpr) -> bool:
|
|
1914
|
+
"""Check if an expression produces an rvalue (needs a stack slot).
|
|
1915
|
+
|
|
1916
|
+
Rvalues: constructor calls, Own[T] returns, literals, binop/unop results,
|
|
1917
|
+
field access on rvalue objects (member of temporary).
|
|
1918
|
+
Lvalues: variable names, field access on lvalues, subscript, function returning T&.
|
|
1919
|
+
|
|
1920
|
+
For pointer-local init: rvalue -> new slot, lvalue -> take address.
|
|
1921
|
+
"""
|
|
1922
|
+
# Names are lvalues (either pointer-locals, params, or globals)
|
|
1923
|
+
if isinstance(expr, TpyName):
|
|
1924
|
+
return False
|
|
1925
|
+
# Field access: rvalue iff the object is rvalue (member of temporary)
|
|
1926
|
+
if isinstance(expr, TpyFieldAccess):
|
|
1927
|
+
return self.is_rvalue_source(expr.obj)
|
|
1928
|
+
# Subscript into containers is an lvalue (returns T&).
|
|
1929
|
+
# Exception: slice calls (e.g. list_stepped_slice) may return by value.
|
|
1930
|
+
if isinstance(expr, TpySubscript):
|
|
1931
|
+
if expr.slice_function_info is not None:
|
|
1932
|
+
return not self._call_returns_cpp_ref(expr.slice_function_info)
|
|
1933
|
+
return False
|
|
1934
|
+
# Ternary: lvalue iff both arms are lvalues (C++ ternary with two lvalue
|
|
1935
|
+
# arms is itself an lvalue). Uses OR semantics: rvalue if either arm is
|
|
1936
|
+
# rvalue, since _gen_if_expr emits arms inline with no temp materialization
|
|
1937
|
+
# (unlike _gen_logical_value which materializes rvalue arms into auto&& temps).
|
|
1938
|
+
if isinstance(expr, TpyIfExpr):
|
|
1939
|
+
result_type = self.analyzer.get_expr_type(expr)
|
|
1940
|
+
if result_type and not result_type.is_value_type():
|
|
1941
|
+
return (self.is_rvalue_source(expr.then_expr)
|
|
1942
|
+
or self.is_rvalue_source(expr.else_expr))
|
|
1943
|
+
# Logical and/or with operand-return semantics: rvalue temps are
|
|
1944
|
+
# materialized into named variables by _gen_logical_value, so the
|
|
1945
|
+
# result is only an rvalue when both operands are rvalues.
|
|
1946
|
+
if isinstance(expr, TpyBinOp) and expr.op in ("&&", "||"):
|
|
1947
|
+
result_type = self.analyzer.get_expr_type(expr)
|
|
1948
|
+
if not is_bool_type(result_type):
|
|
1949
|
+
return self.is_rvalue_source(expr.left) and self.is_rvalue_source(expr.right)
|
|
1950
|
+
# Constructor calls, literals, ops are rvalues. Listed explicitly to
|
|
1951
|
+
# avoid relying on the `return True` fallthrough below for the
|
|
1952
|
+
# container-literal family -- preserves the listing as the canonical
|
|
1953
|
+
# set of rvalue-yielding expression shapes.
|
|
1954
|
+
if isinstance(expr, (TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral,
|
|
1955
|
+
TpyBoolLiteral, TpyNoneLiteral,
|
|
1956
|
+
TpyBinOp, TpyUnaryOp)):
|
|
1957
|
+
return True
|
|
1958
|
+
if isinstance(expr, _CONTAINER_LITERAL_NODES):
|
|
1959
|
+
return True
|
|
1960
|
+
if isinstance(expr, TpyMethodCall):
|
|
1961
|
+
return not self._call_returns_cpp_ref(expr.resolved_function_info, expr.obj)
|
|
1962
|
+
# Coercions: depends on inner expr
|
|
1963
|
+
if isinstance(expr, TpyCoerce):
|
|
1964
|
+
return self.is_rvalue_source(expr.expr)
|
|
1965
|
+
# Function calls
|
|
1966
|
+
if isinstance(expr, TpyCall):
|
|
1967
|
+
# Expression callees -> rvalue
|
|
1968
|
+
if not isinstance(expr.func, TpyName):
|
|
1969
|
+
return True
|
|
1970
|
+
# Record constructors -> rvalue
|
|
1971
|
+
if self.analyzer.registry.get_record(expr.func_name):
|
|
1972
|
+
return True
|
|
1973
|
+
# Generic type constructors -> rvalue
|
|
1974
|
+
if expr.call_type is not None:
|
|
1975
|
+
return True
|
|
1976
|
+
if self.analyzer.registry.get_function(expr.func_name) is not None:
|
|
1977
|
+
return not self._call_returns_cpp_ref(expr.resolved_function_info)
|
|
1978
|
+
return True # Default: treat unknown calls as rvalue
|
|
1979
|
+
return True # Default: rvalue
|
|
1980
|
+
|
|
1981
|
+
def is_container_literal_expr(self, expr: TpyExpr) -> bool:
|
|
1982
|
+
"""Predicate form of `_CONTAINER_LITERAL_NODES`; see the constant
|
|
1983
|
+
comment for the value-emit-rvalue rationale."""
|
|
1984
|
+
return isinstance(expr, _CONTAINER_LITERAL_NODES)
|
|
1985
|
+
|
|
1986
|
+
def is_value_emit_rvalue(self, expr: TpyExpr) -> bool:
|
|
1987
|
+
"""Rvalue source whose `gen_expr` yields a value (not a `T*`), so a
|
|
1988
|
+
pointer-form Optional sink needs to materialize a named slot before
|
|
1989
|
+
taking address. Container/comprehension/generator literals plus
|
|
1990
|
+
rvalue field accesses (`temp.field` on a moved-from object) are the
|
|
1991
|
+
exhaustive set under the pointer-repr-Optional outer guard at the
|
|
1992
|
+
two `_gen_pointer_local_init`/`_rebind` call sites.
|
|
1993
|
+
"""
|
|
1994
|
+
return self.is_container_literal_expr(expr) or (
|
|
1995
|
+
isinstance(expr, TpyFieldAccess) and self.is_rvalue_source(expr))
|
|
1996
|
+
|
|
1997
|
+
def is_temporary_expr(self, expr: TpyExpr) -> bool:
|
|
1998
|
+
"""Check if an expression produces a temporary (rvalue).
|
|
1999
|
+
|
|
2000
|
+
Temporaries can't bind to non-const lvalue references, so they need
|
|
2001
|
+
to be stored in a temp variable when passed to mutable ref params.
|
|
2002
|
+
|
|
2003
|
+
Lvalues (don't need temps): variable names, field access
|
|
2004
|
+
Rvalues (need temps): literals, binary/unary ops, calls, coercions, subscripts on records
|
|
2005
|
+
"""
|
|
2006
|
+
# Scalar literals: 1, 3.14, "x", True, None
|
|
2007
|
+
if isinstance(expr, (TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBoolLiteral, TpyNoneLiteral)):
|
|
2008
|
+
return True
|
|
2009
|
+
# Container literals, comprehensions, generator expressions.
|
|
2010
|
+
if self.is_container_literal_expr(expr):
|
|
2011
|
+
return True
|
|
2012
|
+
# Explicit coercions: Ptr[T]->T is dereference (lvalue), others produce temporaries
|
|
2013
|
+
if isinstance(expr, TpyCoerce):
|
|
2014
|
+
# Pointer dereference is an lvalue, not a temporary
|
|
2015
|
+
if isinstance(expr.actual_type, PtrType):
|
|
2016
|
+
return False
|
|
2017
|
+
return True
|
|
2018
|
+
# Binary and unary ops always produce temporaries
|
|
2019
|
+
if isinstance(expr, (TpyBinOp, TpyUnaryOp)):
|
|
2020
|
+
return True
|
|
2021
|
+
if isinstance(expr, TpyMethodCall):
|
|
2022
|
+
return self.is_rvalue_source(expr)
|
|
2023
|
+
# Subscript on user records returns by value (rvalue)
|
|
2024
|
+
# std::vector/array operator[] returns lvalue ref, but user __getitem__ returns by value
|
|
2025
|
+
if isinstance(expr, TpySubscript):
|
|
2026
|
+
raw_ct = self.analyzer.get_expr_type(expr.obj)
|
|
2027
|
+
container_type = unwrap_readonly(raw_ct) if raw_ct is not None else None
|
|
2028
|
+
if isinstance(container_type, NominalType) and container_type.is_user_record:
|
|
2029
|
+
return True
|
|
2030
|
+
if isinstance(expr, TpyCall):
|
|
2031
|
+
return self.is_rvalue_source(expr)
|
|
2032
|
+
return False
|
|
2033
|
+
|
|
2034
|
+
def unwrap_copy(self, expr: TpyExpr) -> TpyExpr:
|
|
2035
|
+
"""If expr is tpy.copy(x), return x; otherwise return expr as-is."""
|
|
2036
|
+
if isinstance(expr, TpyCoerce):
|
|
2037
|
+
inner = self.unwrap_copy(expr.expr)
|
|
2038
|
+
return inner if inner is not expr.expr else expr
|
|
2039
|
+
if isinstance(expr, TpyCall) and len(expr.args) == 1 and isinstance(expr.func, TpyName):
|
|
2040
|
+
if expr.func_name in self.analyzer.imported_names:
|
|
2041
|
+
mod, fn = self.analyzer.imported_names[expr.func_name]
|
|
2042
|
+
if mod == "tpy" and fn == "copy":
|
|
2043
|
+
return expr.args[0]
|
|
2044
|
+
return expr
|
|
2045
|
+
|
|
2046
|
+
def contains_protocol_type(self, typ: TpyType) -> bool:
|
|
2047
|
+
"""Check if a type contains a protocol or SelfType anywhere in its structure.
|
|
2048
|
+
|
|
2049
|
+
Used to determine if a variable should use 'auto' in C++ codegen because
|
|
2050
|
+
the actual type depends on template parameters.
|
|
2051
|
+
"""
|
|
2052
|
+
if isinstance(typ, SelfType):
|
|
2053
|
+
return True
|
|
2054
|
+
if is_protocol_type(typ):
|
|
2055
|
+
return True
|
|
2056
|
+
elif isinstance(typ, OwnType):
|
|
2057
|
+
return self.contains_protocol_type(typ.wrapped)
|
|
2058
|
+
elif isinstance(typ, ReadonlyType):
|
|
2059
|
+
return self.contains_protocol_type(typ.wrapped)
|
|
2060
|
+
elif isinstance(typ, PtrType):
|
|
2061
|
+
return self.contains_protocol_type(typ.pointee)
|
|
2062
|
+
elif (elem_type := typ.get_element_type()) is not None:
|
|
2063
|
+
return self.contains_protocol_type(elem_type)
|
|
2064
|
+
return False
|