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,3258 @@
|
|
|
1
|
+
"""Code generation for `async def` functions (state-machine struct conforming
|
|
2
|
+
to Awaitable[T]).
|
|
3
|
+
|
|
4
|
+
Mirrors gen_generators.py but with the resumable-frame shape from
|
|
5
|
+
docs/ASYNC_DESIGN.md: each async def lowers to a struct with a
|
|
6
|
+
`poll(Waker) -> Poll<T>` method instead of `__next__() -> std::expected`.
|
|
7
|
+
|
|
8
|
+
Implementation status:
|
|
9
|
+
- Commit 1: no-await async defs.
|
|
10
|
+
- Commit 3 (this): single/multi-await, statically-resolved sub-coroutines.
|
|
11
|
+
Awaits are restricted to top-level statement positions:
|
|
12
|
+
* `x = await sub()` (assign / vardecl)
|
|
13
|
+
* `await sub()` (statement-level expression)
|
|
14
|
+
* `return await sub()` (return value)
|
|
15
|
+
- Subsequent commits: try/finally re-establishment, cancellation, await
|
|
16
|
+
inside loops/conditionals.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import contextlib
|
|
21
|
+
import copy
|
|
22
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
23
|
+
from enum import IntEnum
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from ..namespace import Namespace
|
|
27
|
+
from ..parse.nodes import (
|
|
28
|
+
TpyFunction, TpyAwait, TpyStmt, TpyAssign, TpyVarDecl, TpyReturn,
|
|
29
|
+
TpyExprStmt, TpyName, TpyExpr, TpyTry, TpyExceptHandler, TpyCall,
|
|
30
|
+
TpyMethodCall, TpyFieldAccess, TpyForEach, TpyWith,
|
|
31
|
+
TpyIntLiteral, TpyFloatLiteral, TpyStrLiteral, TpyBoolLiteral,
|
|
32
|
+
TpyNoneLiteral, TpyCoerce,
|
|
33
|
+
is_stable_address_lvalue,
|
|
34
|
+
)
|
|
35
|
+
from ..typesys import IntLiteralType, NominalType, OptionalType, OwnType, TypeParamRef, unwrap_readonly, unwrap_ref_type, unwrap_own, VoidType
|
|
36
|
+
from .gen_generators import GeneratorCodegen, GeneratorForInfo
|
|
37
|
+
from ..type_def_registry import is_str_type, is_str_category, is_big_int_type
|
|
38
|
+
from .context import INDENT, escape_cpp_name, CodeGenError, FinallyContext, module_to_cpp_namespace, qualified_cpp_name
|
|
39
|
+
from . import resumable_cfg as rcfg
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _stmts_have_return(stmts: "list[TpyStmt]") -> bool:
|
|
43
|
+
"""Recursively check whether any TpyReturn appears in a statement list."""
|
|
44
|
+
for stmt in stmts:
|
|
45
|
+
if isinstance(stmt, TpyReturn):
|
|
46
|
+
return True
|
|
47
|
+
if hasattr(stmt, "sub_bodies"):
|
|
48
|
+
for body in stmt.sub_bodies():
|
|
49
|
+
if _stmts_have_return(body):
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _regions_have_pending_cleanup(regions: tuple) -> bool:
|
|
55
|
+
"""Return True if any region in the tuple will emit cleanup code.
|
|
56
|
+
|
|
57
|
+
Used by _emit_exit_region_finallies to decide whether to defer the
|
|
58
|
+
__finally_stop check: if the destination region stack still has pending
|
|
59
|
+
cleanup (with.__exit__ or finally helpers), the stop-check must wait
|
|
60
|
+
until those later states have run their cleanup."""
|
|
61
|
+
for r in regions:
|
|
62
|
+
if isinstance(r, rcfg.WithRegion):
|
|
63
|
+
return True
|
|
64
|
+
if isinstance(r, rcfg.FinallyRegion):
|
|
65
|
+
# BBs inside a CFG-based finally body: the finally body itself
|
|
66
|
+
# is the pending cleanup; don't stop before it runs.
|
|
67
|
+
return True
|
|
68
|
+
if isinstance(r, rcfg.TryRegion) and r.finally_helper_name is not None:
|
|
69
|
+
return True
|
|
70
|
+
if isinstance(r, rcfg.ExceptRegion) and r.parent_finally is not None:
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if TYPE_CHECKING:
|
|
76
|
+
from io import TextIO
|
|
77
|
+
from .context import CodeGenContext
|
|
78
|
+
from .types import TypeMapper
|
|
79
|
+
from .expressions import ExpressionGenerator
|
|
80
|
+
from .statements import StatementGenerator
|
|
81
|
+
from .functions import FunctionGenerator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _CoroParamKind(IntEnum):
|
|
85
|
+
"""Storage form for a coro-struct captured param.
|
|
86
|
+
|
|
87
|
+
REF: non-value type bound by reference; field is `T&`, ctor takes `T&`,
|
|
88
|
+
init binds the reference. Caller must keep the source alive across
|
|
89
|
+
polls.
|
|
90
|
+
VALUE: value type (or string view) stored by value; field is `T`,
|
|
91
|
+
ctor takes `T x_` and the init moves into the field.
|
|
92
|
+
POINTER: pointer-form Optional[NonValue] param; field is `T*` (or
|
|
93
|
+
`const T*`), ctor takes the same. Init is a direct copy -- raw
|
|
94
|
+
pointers are trivially copyable, so std::move would just add
|
|
95
|
+
noise.
|
|
96
|
+
TYPE_PARAM: TypeParamRef param whose value-vs-reference resolution
|
|
97
|
+
happens at instantiation; field is `::tpy::val_or_ref_t<T>` (T for
|
|
98
|
+
value types, T& for object types), ctor takes
|
|
99
|
+
`::tpy::param_val_or_ref_t<T>` (const T& or T&), init directly
|
|
100
|
+
binds/copies. No std::move (the param is already a reference).
|
|
101
|
+
STATIC_PROTOCOL: static-protocol-typed param (e.g. `Own[Awaitable[T]]`)
|
|
102
|
+
whose concrete type is deduced as an extra template arg `T_<pname>`
|
|
103
|
+
with a concept constraint -- mirrors `gen_params_with_protocols`
|
|
104
|
+
in `functions.py`. Field stores by value (the concrete deduced
|
|
105
|
+
type); ctor takes `T_<pname>&&` (forwarding ref) and moves in;
|
|
106
|
+
the factory forwards the same way.
|
|
107
|
+
OWNED_VALUE: `Own[T]` non-value, non-static-protocol type (e.g.
|
|
108
|
+
`Own[Cancellable[T]]` -- a @dynamic protocol, so the C++ shape is
|
|
109
|
+
`unique_ptr<P>`, not a deduced template arg). Field stores by
|
|
110
|
+
value; ctor takes `T&&` and moves in; the factory forwards via
|
|
111
|
+
`std::move(name)`. Parallels STATIC_PROTOCOL minus the
|
|
112
|
+
template-arg dance.
|
|
113
|
+
"""
|
|
114
|
+
REF = 0
|
|
115
|
+
VALUE = 1
|
|
116
|
+
POINTER = 2
|
|
117
|
+
TYPE_PARAM = 3
|
|
118
|
+
STATIC_PROTOCOL = 4
|
|
119
|
+
OWNED_VALUE = 5
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class _CoroParam:
|
|
124
|
+
cpp_name: str
|
|
125
|
+
field_type: str # type spelling for the frame-field declaration
|
|
126
|
+
ctor_param_type: str # type spelling for the constructor's parameter
|
|
127
|
+
kind: _CoroParamKind
|
|
128
|
+
|
|
129
|
+
def field_decl(self) -> str:
|
|
130
|
+
if self.kind is _CoroParamKind.REF:
|
|
131
|
+
return f"{self.field_type}& {self.cpp_name}"
|
|
132
|
+
return f"{self.field_type} {self.cpp_name}"
|
|
133
|
+
|
|
134
|
+
def factory_param_decl(self) -> str:
|
|
135
|
+
# Factory function signature param: same C++ type as the ctor's
|
|
136
|
+
# param (matters for TYPE_PARAM, where it's `param_val_or_ref_t<T>`
|
|
137
|
+
# rather than the field's `val_or_ref_t<T>`), but with the bare
|
|
138
|
+
# name -- the factory body forwards by bare name.
|
|
139
|
+
if self.kind is _CoroParamKind.REF:
|
|
140
|
+
return f"{self.ctor_param_type}& {self.cpp_name}"
|
|
141
|
+
if self.kind is _CoroParamKind.STATIC_PROTOCOL:
|
|
142
|
+
return f"{self.ctor_param_type}&& {self.cpp_name}"
|
|
143
|
+
if self.kind is _CoroParamKind.OWNED_VALUE:
|
|
144
|
+
return f"{self.ctor_param_type} {self.cpp_name}"
|
|
145
|
+
return f"{self.ctor_param_type} {self.cpp_name}"
|
|
146
|
+
|
|
147
|
+
def ctor_param_decl(self) -> str:
|
|
148
|
+
if self.kind is _CoroParamKind.REF:
|
|
149
|
+
return f"{self.ctor_param_type}& {self.cpp_name}"
|
|
150
|
+
if self.kind is _CoroParamKind.STATIC_PROTOCOL:
|
|
151
|
+
return f"{self.ctor_param_type}&& {self.cpp_name}_"
|
|
152
|
+
if self.kind is _CoroParamKind.OWNED_VALUE:
|
|
153
|
+
return f"{self.ctor_param_type}&& {self.cpp_name}_"
|
|
154
|
+
# VALUE / POINTER / TYPE_PARAM: `_` suffix disambiguates from the
|
|
155
|
+
# field name in the init list.
|
|
156
|
+
return f"{self.ctor_param_type} {self.cpp_name}_"
|
|
157
|
+
|
|
158
|
+
def ctor_init(self) -> str:
|
|
159
|
+
if self.kind is _CoroParamKind.REF:
|
|
160
|
+
return f"{self.cpp_name}({self.cpp_name})"
|
|
161
|
+
if self.kind in (_CoroParamKind.POINTER, _CoroParamKind.TYPE_PARAM):
|
|
162
|
+
# POINTER: raw pointer, trivially copyable -- std::move is noise.
|
|
163
|
+
# TYPE_PARAM: `param_val_or_ref_t<T>` is already a reference type;
|
|
164
|
+
# std::move on it yields an rvalue that won't bind to the field
|
|
165
|
+
# type for non-value Ts. Direct bind/copy is uniformly correct.
|
|
166
|
+
return f"{self.cpp_name}({self.cpp_name}_)"
|
|
167
|
+
return f"{self.cpp_name}(std::move({self.cpp_name}_))"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _StateKind(IntEnum):
|
|
171
|
+
INITIAL = 0
|
|
172
|
+
RESUME = 1
|
|
173
|
+
JOIN = 2
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True, order=True)
|
|
177
|
+
class _StateLabel:
|
|
178
|
+
"""Typed switch-case label. Orders naturally (INITIAL < RESUME < JOIN
|
|
179
|
+
by `IntEnum` value, then by `idx`); formats to its C++ identifier
|
|
180
|
+
via `cpp_name()`."""
|
|
181
|
+
kind: _StateKind
|
|
182
|
+
idx: int = 0
|
|
183
|
+
|
|
184
|
+
def cpp_name(self) -> str:
|
|
185
|
+
if self.kind is _StateKind.INITIAL:
|
|
186
|
+
return "S_INITIAL"
|
|
187
|
+
if self.kind is _StateKind.RESUME:
|
|
188
|
+
# Resume point after suspension `idx`. Shape-neutral name: the
|
|
189
|
+
# same emitter serves both `await` and `yield` suspensions.
|
|
190
|
+
return f"S_RESUME_{self.idx}"
|
|
191
|
+
return f"S_JOIN_{self.idx}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Single C++ spelling of `Ready(unit)` for void-returning async defs.
|
|
195
|
+
# `async def -> None` lowers Poll[None]'s type-arg slot to std::monostate
|
|
196
|
+
# (the value-bearing-None unit type); the `ready` factory needs an actual
|
|
197
|
+
# value of that type, so we construct `std::monostate{}` explicitly.
|
|
198
|
+
# Centralised here so the 3 emit sites in this module + 1 in statements.py
|
|
199
|
+
# can't drift apart.
|
|
200
|
+
POLL_VOID_READY_RETURN = (
|
|
201
|
+
"return ::tpystd::tpy::Poll<::std::monostate>::ready(::std::monostate{});"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _same_elements(a: list, b: list) -> bool:
|
|
206
|
+
"""True iff `a` and `b` have identical length and element-by-element
|
|
207
|
+
identity. Used by the async lift pre-pass to decide whether a
|
|
208
|
+
sub-body actually changed -- distinct list objects with all-`is`
|
|
209
|
+
elements are treated as unchanged so the parent stmt's id stays
|
|
210
|
+
stable."""
|
|
211
|
+
return len(a) == len(b) and all(x is y for x, y in zip(a, b))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class AsyncCoroCodegen:
|
|
215
|
+
"""Generates C++ code for `async def` functions as state-machine structs."""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
ctx: "CodeGenContext",
|
|
220
|
+
types: "TypeMapper",
|
|
221
|
+
expressions: "ExpressionGenerator",
|
|
222
|
+
statements: "StatementGenerator",
|
|
223
|
+
functions: "FunctionGenerator",
|
|
224
|
+
):
|
|
225
|
+
self.ctx = ctx
|
|
226
|
+
self.types = types
|
|
227
|
+
self.expressions = expressions
|
|
228
|
+
self.statements = statements
|
|
229
|
+
self.functions = functions
|
|
230
|
+
# Set by CodeGenerator after init; the resumable for-loop emit reuses
|
|
231
|
+
# the legacy strategy analysis (`_analyze_for_strategy`).
|
|
232
|
+
self.gen_generators: GeneratorCodegen
|
|
233
|
+
# Resumable-shape discriminator read by the `_resumable_*` policy
|
|
234
|
+
# seams. ASYNC (await -> __poll__ -> Poll<T>) is the default; the
|
|
235
|
+
# generator migration sets GENERATOR (yield -> __next__ ->
|
|
236
|
+
# expected<T, StopIteration>) transiently via `_resumable_shape`.
|
|
237
|
+
self._shape: rcfg.ResumableShape = rcfg.ResumableShape.ASYNC
|
|
238
|
+
|
|
239
|
+
@contextlib.contextmanager
|
|
240
|
+
def _resumable_shape(self, shape: 'rcfg.ResumableShape'):
|
|
241
|
+
"""Temporarily select the resumable shape (async / generator) the
|
|
242
|
+
policy seams emit for, restoring the prior shape on exit."""
|
|
243
|
+
prev = self._shape
|
|
244
|
+
self._shape = shape
|
|
245
|
+
try:
|
|
246
|
+
yield
|
|
247
|
+
finally:
|
|
248
|
+
self._shape = prev
|
|
249
|
+
|
|
250
|
+
def _is_generator_shape(self) -> bool:
|
|
251
|
+
return self._shape is rcfg.ResumableShape.GENERATOR
|
|
252
|
+
|
|
253
|
+
def gen_struct_name(self, func: TpyFunction,
|
|
254
|
+
record_name: str | None = None) -> str:
|
|
255
|
+
"""Resumable-frame struct name for the CURRENT function. Shape-aware:
|
|
256
|
+
`__gen_<funcname>` (/ `__gen_<Record>_<funcname>`) for the generator
|
|
257
|
+
shape, `__coro_<funcname>` (/ `__coro_<Record>_<funcname>`) for the
|
|
258
|
+
async shape -- so the struct name reflects what it is (the
|
|
259
|
+
`operator<<` repr and the legacy `GeneratorCodegen.gen_struct_name`
|
|
260
|
+
use the same `__gen_` convention). Awaited-callee sub-coro structs
|
|
261
|
+
are named via `_sub_struct_name` (always `__coro_`)."""
|
|
262
|
+
prefix = "__gen_" if self._is_generator_shape() else "__coro_"
|
|
263
|
+
return AsyncCoroCodegen._sub_struct_name(func.name, record_name, prefix)
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _sub_struct_name(name: str, owner_record: str | None = None,
|
|
267
|
+
prefix: str = "__coro_") -> str:
|
|
268
|
+
"""Resumable-frame struct name from raw strings: `<prefix><name>` for
|
|
269
|
+
free functions, `<prefix><Owner>_<name>` for methods. Single source
|
|
270
|
+
of truth for `gen_struct_name`, the await payload factory (sub-
|
|
271
|
+
coroutine of a statically-resolved await -- always `__coro_`), and
|
|
272
|
+
the async-with prescan (which only has the CM's `NominalType.name`)."""
|
|
273
|
+
if owner_record:
|
|
274
|
+
return (f"{prefix}{escape_cpp_name(owner_record)}_"
|
|
275
|
+
f"{escape_cpp_name(name)}")
|
|
276
|
+
return f"{prefix}{escape_cpp_name(name)}"
|
|
277
|
+
|
|
278
|
+
def _classify_params(self, func: TpyFunction,
|
|
279
|
+
record_name: str | None = None
|
|
280
|
+
) -> list[_CoroParam]:
|
|
281
|
+
"""Classify async-def params for the coro struct field/ctor shape.
|
|
282
|
+
|
|
283
|
+
Returns a list of `_CoroParam` records, one per captured param.
|
|
284
|
+
When `record_name` is set, prepends `__self: <Record>&` (or
|
|
285
|
+
`const <Record>&` for @readonly methods) so async methods
|
|
286
|
+
capture their receiver -- parallels `GeneratorCodegen`
|
|
287
|
+
self-capture.
|
|
288
|
+
|
|
289
|
+
Generic params (`T` as TypeParamRef) use the
|
|
290
|
+
`param_val_or_ref_t<T>` / `val_or_ref_t<T>` trait so each
|
|
291
|
+
instantiation picks the right value-vs-reference shape -- a
|
|
292
|
+
value-typed T (e.g. Int32) stores by value (so literal /
|
|
293
|
+
rvalue call-site args don't dangle), while an object-typed T
|
|
294
|
+
stores by reference (matching Python semantics and the
|
|
295
|
+
non-template ref path).
|
|
296
|
+
|
|
297
|
+
v1 conservative rule for str: passes by string_view (caller's
|
|
298
|
+
storage, same lifetime model as generators -- coros that
|
|
299
|
+
escape via Task will need to copy out, but until that lands
|
|
300
|
+
the borrow holds across await boundaries within a single
|
|
301
|
+
asyncio.run).
|
|
302
|
+
"""
|
|
303
|
+
out: list[_CoroParam] = []
|
|
304
|
+
if record_name:
|
|
305
|
+
# Match the factory-site spelling (generator.py:1177, :1252):
|
|
306
|
+
# convert dotted nested-class names to C++ scope syntax (`Outer.Inner`
|
|
307
|
+
# -> `Outer::Inner`) before escaping, then template-qualify for a
|
|
308
|
+
# generic class (`Box[T]` -> `Box<T>&`) -- the bare class name in
|
|
309
|
+
# a type position outside the class body is ill-formed for
|
|
310
|
+
# templates.
|
|
311
|
+
cpp_record = escape_cpp_name(record_name.replace(".", "::"))
|
|
312
|
+
record_tps = self._record_template_args(record_name)
|
|
313
|
+
if record_tps:
|
|
314
|
+
cpp_record = f"{cpp_record}<{', '.join(record_tps)}>"
|
|
315
|
+
const_prefix = "const " if func.is_readonly else ""
|
|
316
|
+
recv_type = f"{const_prefix}{cpp_record}"
|
|
317
|
+
out.append(_CoroParam(
|
|
318
|
+
cpp_name="__self",
|
|
319
|
+
field_type=recv_type,
|
|
320
|
+
ctor_param_type=recv_type,
|
|
321
|
+
kind=_CoroParamKind.REF,
|
|
322
|
+
))
|
|
323
|
+
for pname, ptype in func.params:
|
|
324
|
+
cpp_name = escape_cpp_name(pname)
|
|
325
|
+
ptype_inner = unwrap_ref_type(ptype)
|
|
326
|
+
actual = unwrap_readonly(ptype_inner)
|
|
327
|
+
if is_str_type(ptype_inner):
|
|
328
|
+
out.append(_CoroParam(
|
|
329
|
+
cpp_name=cpp_name,
|
|
330
|
+
field_type="std::string_view",
|
|
331
|
+
ctor_param_type="std::string_view",
|
|
332
|
+
kind=_CoroParamKind.VALUE,
|
|
333
|
+
))
|
|
334
|
+
elif isinstance(ptype_inner, TypeParamRef):
|
|
335
|
+
# to_cpp_return / to_cpp_param_type already encode the
|
|
336
|
+
# val_or_ref_t<T> / param_val_or_ref_t<T> traits (and
|
|
337
|
+
# collapse to std::size_t for INT-kind params).
|
|
338
|
+
out.append(_CoroParam(
|
|
339
|
+
cpp_name=cpp_name,
|
|
340
|
+
field_type=ptype_inner.to_cpp_return(),
|
|
341
|
+
ctor_param_type=ptype_inner.to_cpp_param_type(),
|
|
342
|
+
kind=_CoroParamKind.TYPE_PARAM,
|
|
343
|
+
))
|
|
344
|
+
elif self.functions.protocols.is_static_protocol_param(ptype):
|
|
345
|
+
# Static-protocol param (e.g. `Own[Awaitable[T]]`): the
|
|
346
|
+
# concrete operand type is deduced as an extra template
|
|
347
|
+
# arg `T_<pname>` with a concept constraint, declared in
|
|
348
|
+
# `_emit_template_header`. Field stores by value;
|
|
349
|
+
# ctor/factory forward via T_<pname>&&. Checked before
|
|
350
|
+
# the OptionalType-pointer-repr branch below so
|
|
351
|
+
# `Own[Awaitable[T]] | None` doesn't get mis-routed to
|
|
352
|
+
# POINTER -- `_protocol_template_parts` will reject the
|
|
353
|
+
# nullable shape with a clear diagnostic.
|
|
354
|
+
#
|
|
355
|
+
# `T_{pname}` (raw, not escaped) keeps the template-arg
|
|
356
|
+
# name aligned with `_protocol_template_parts` (which
|
|
357
|
+
# declares the template arg) -- `_struct_name_templated`
|
|
358
|
+
# reads the same field back to spell instantiations.
|
|
359
|
+
# Using `cpp_name` here would diverge when `pname`
|
|
360
|
+
# collides with a C++ keyword (e.g. `class` -> `class_`).
|
|
361
|
+
template_arg = f"T_{pname}"
|
|
362
|
+
out.append(_CoroParam(
|
|
363
|
+
cpp_name=cpp_name,
|
|
364
|
+
field_type=template_arg,
|
|
365
|
+
ctor_param_type=template_arg,
|
|
366
|
+
kind=_CoroParamKind.STATIC_PROTOCOL,
|
|
367
|
+
))
|
|
368
|
+
elif isinstance(actual, OptionalType) and actual.uses_pointer_repr():
|
|
369
|
+
cpp_type = ptype_inner.to_cpp_param_type()
|
|
370
|
+
out.append(_CoroParam(
|
|
371
|
+
cpp_name=cpp_name,
|
|
372
|
+
field_type=cpp_type,
|
|
373
|
+
ctor_param_type=cpp_type,
|
|
374
|
+
kind=_CoroParamKind.POINTER,
|
|
375
|
+
))
|
|
376
|
+
elif self.ctx.is_ptr_variant_union(actual):
|
|
377
|
+
# Non-value union: the pointer-variant borrow form
|
|
378
|
+
# (`std::variant<A*, B*>`) is the shape every other param
|
|
379
|
+
# boundary uses -- ordinary functions, simple generators,
|
|
380
|
+
# plain locals. Storing it by value in the frame keeps the
|
|
381
|
+
# factory's signature in step with what the call site (and
|
|
382
|
+
# the emplace path) already build; a value-variant-by-ref
|
|
383
|
+
# field would be the lone divergence. Deep-const for a
|
|
384
|
+
# readonly union, matching `_gen_union_arg`.
|
|
385
|
+
is_readonly_param = actual is not ptype_inner
|
|
386
|
+
cpp_type = (self.types.type_to_cpp_const_ptr_variant(actual)
|
|
387
|
+
if is_readonly_param
|
|
388
|
+
else self.types.type_to_cpp_ptr_variant(actual))
|
|
389
|
+
out.append(_CoroParam(
|
|
390
|
+
cpp_name=cpp_name,
|
|
391
|
+
field_type=cpp_type,
|
|
392
|
+
ctor_param_type=cpp_type,
|
|
393
|
+
kind=_CoroParamKind.POINTER,
|
|
394
|
+
))
|
|
395
|
+
else:
|
|
396
|
+
cpp_type = self.types.type_to_cpp(ptype_inner)
|
|
397
|
+
if isinstance(ptype_inner, OwnType):
|
|
398
|
+
# Owned non-value (e.g. `Own[Cancellable[T]]` -- a
|
|
399
|
+
# @dynamic protocol whose C++ shape is unique_ptr<P>,
|
|
400
|
+
# so the param can't be re-bound through REF and the
|
|
401
|
+
# factory can't forward by bare name without
|
|
402
|
+
# tripping the deleted copy ctor). Store by value;
|
|
403
|
+
# ctor moves in; factory moves out. Checked before
|
|
404
|
+
# `is_value_type()` because `OwnType.is_value_type()`
|
|
405
|
+
# returns True (Own[T] uses T&& at param boundaries
|
|
406
|
+
# so it's value-like for *most* purposes) but the
|
|
407
|
+
# move-only semantics still need explicit forwarding.
|
|
408
|
+
out.append(_CoroParam(
|
|
409
|
+
cpp_name=cpp_name,
|
|
410
|
+
field_type=cpp_type,
|
|
411
|
+
ctor_param_type=cpp_type,
|
|
412
|
+
kind=_CoroParamKind.OWNED_VALUE,
|
|
413
|
+
))
|
|
414
|
+
elif ptype_inner.is_value_type():
|
|
415
|
+
out.append(_CoroParam(
|
|
416
|
+
cpp_name=cpp_name,
|
|
417
|
+
field_type=cpp_type,
|
|
418
|
+
ctor_param_type=cpp_type,
|
|
419
|
+
kind=_CoroParamKind.VALUE,
|
|
420
|
+
))
|
|
421
|
+
else:
|
|
422
|
+
out.append(_CoroParam(
|
|
423
|
+
cpp_name=cpp_name,
|
|
424
|
+
field_type=cpp_type,
|
|
425
|
+
ctor_param_type=cpp_type,
|
|
426
|
+
kind=_CoroParamKind.REF,
|
|
427
|
+
))
|
|
428
|
+
return out
|
|
429
|
+
|
|
430
|
+
def _protocol_template_parts(self, func: TpyFunction) -> list[str]:
|
|
431
|
+
"""Return template-header parts for any static-protocol-typed
|
|
432
|
+
params on `func`. Each part has the form `<Concept> T_<pname>`
|
|
433
|
+
(or `<Concept><type_args> T_<pname>` when the protocol is
|
|
434
|
+
generic). Mirrors `gen_combined_template_header` in protocols.py
|
|
435
|
+
for the single-required-protocol case; multi-protocol / nullable
|
|
436
|
+
shapes are deferred until a concrete need surfaces.
|
|
437
|
+
"""
|
|
438
|
+
parts: list[str] = []
|
|
439
|
+
for pname, ptype in func.params:
|
|
440
|
+
if not self.functions.protocols.is_static_protocol_param(ptype):
|
|
441
|
+
continue
|
|
442
|
+
infos = self.functions.protocols.get_all_protocol_params(
|
|
443
|
+
[(pname, ptype)])
|
|
444
|
+
if not infos:
|
|
445
|
+
continue
|
|
446
|
+
info = infos[0]
|
|
447
|
+
if len(info.protocols) != 1 or info.has_none:
|
|
448
|
+
raise CodeGenError(
|
|
449
|
+
f"async def param {pname!r}: multi-protocol or "
|
|
450
|
+
"optional-protocol shape is not yet supported in "
|
|
451
|
+
"async-def coro codegen (only single required "
|
|
452
|
+
"protocols like `Own[Awaitable[T]]`)",
|
|
453
|
+
loc=func.loc)
|
|
454
|
+
proto = info.protocols[0]
|
|
455
|
+
concept_name = self.functions.protocols.get_concept_name(proto)
|
|
456
|
+
if proto.type_args:
|
|
457
|
+
targs = ", ".join(t.to_cpp() for t in proto.type_args)
|
|
458
|
+
parts.append(f"{concept_name}<{targs}> T_{pname}")
|
|
459
|
+
else:
|
|
460
|
+
parts.append(f"{concept_name} T_{pname}")
|
|
461
|
+
return parts
|
|
462
|
+
|
|
463
|
+
def _record_template_args(self, record_name: str | None) -> tuple[str, ...]:
|
|
464
|
+
"""Type params of the enclosing record for a method, or () for a free
|
|
465
|
+
function / method on a non-generic class. A method on `class Box[T]`
|
|
466
|
+
contributes T to the coro struct's template header so the struct can
|
|
467
|
+
reference `Box<T>` in the self-capture field."""
|
|
468
|
+
if not record_name:
|
|
469
|
+
return ()
|
|
470
|
+
info = self.ctx.analyzer.registry.get_record(record_name)
|
|
471
|
+
if info is None or not info.type_params:
|
|
472
|
+
return ()
|
|
473
|
+
return tuple(info.type_params)
|
|
474
|
+
|
|
475
|
+
def _record_template_parts(self, record_name: str | None) -> list[str]:
|
|
476
|
+
"""Rendered template-header parts for the enclosing record's type
|
|
477
|
+
params, mirroring `protocols.gen_record_template_parts` so out-of-class
|
|
478
|
+
member-def headers spell the same constrained form as the class
|
|
479
|
+
declaration (`Iterable<int32_t> T`, not bare `typename T`). Without
|
|
480
|
+
this, an out-of-class definition's parameter-list is non-equivalent to
|
|
481
|
+
the in-class declaration's and C++ rejects with a constraint
|
|
482
|
+
mismatch."""
|
|
483
|
+
if not record_name:
|
|
484
|
+
return []
|
|
485
|
+
info = self.ctx.analyzer.registry.get_record(record_name)
|
|
486
|
+
if info is None or not info.type_params:
|
|
487
|
+
return []
|
|
488
|
+
return self.functions.protocols.gen_record_template_parts(
|
|
489
|
+
list(info.type_params),
|
|
490
|
+
info.type_param_bounds or {},
|
|
491
|
+
info.type_param_kinds)
|
|
492
|
+
|
|
493
|
+
def _is_templated_coro(self, func: TpyFunction,
|
|
494
|
+
record_name: str | None = None) -> bool:
|
|
495
|
+
"""True iff the coro/generator struct is a C++ template -- because
|
|
496
|
+
of explicit `[T, ...]` type params, a static-protocol-typed param
|
|
497
|
+
(`T_<pname>` template arg), OR (for a method) the enclosing record's
|
|
498
|
+
type params. Templated structs must emit their poll/__next__ body +
|
|
499
|
+
factory inline in the header; a non-template struct's body lands in
|
|
500
|
+
the .cpp. Single source of truth for the header-vs-cpp placement
|
|
501
|
+
decision across both async and generator shapes."""
|
|
502
|
+
return (bool(func.type_params)
|
|
503
|
+
or bool(self._protocol_template_parts(func))
|
|
504
|
+
or bool(self._record_template_parts(record_name)))
|
|
505
|
+
|
|
506
|
+
def _emit_template_header(self, out: "TextIO", func: TpyFunction,
|
|
507
|
+
*, indent: str = "",
|
|
508
|
+
record_name: str | None = None) -> bool:
|
|
509
|
+
proto_parts = self._protocol_template_parts(func)
|
|
510
|
+
record_parts = self._record_template_parts(record_name)
|
|
511
|
+
if not func.type_params and not proto_parts and not record_parts:
|
|
512
|
+
return False
|
|
513
|
+
# Record's [T...] first so an outer Box<T> reads naturally; then the
|
|
514
|
+
# method's own [U...]; then protocol-typed-param template args. The
|
|
515
|
+
# combined-flat form is correct for free types (structs at namespace
|
|
516
|
+
# scope) -- the coro/gen struct itself is a free `template<typename
|
|
517
|
+
# T, typename U> struct __coro_...`. For an out-of-class member
|
|
518
|
+
# function definition of a class template (`Box<T>::method`) where
|
|
519
|
+
# the method is itself a template, C++ requires NESTED headers
|
|
520
|
+
# instead -- see `_emit_member_template_headers`.
|
|
521
|
+
parts = list(record_parts)
|
|
522
|
+
parts.extend(f"typename {tp}" for tp in func.type_params)
|
|
523
|
+
parts.extend(proto_parts)
|
|
524
|
+
out.write(f"{indent}template <{', '.join(parts)}>\n")
|
|
525
|
+
return True
|
|
526
|
+
|
|
527
|
+
def _emit_member_template_headers(self, out: "TextIO",
|
|
528
|
+
func: TpyFunction,
|
|
529
|
+
*, record_name: str,
|
|
530
|
+
indent: str = "") -> None:
|
|
531
|
+
"""Emit C++ template headers for the out-of-class definition of a
|
|
532
|
+
member function of a class template: `template <record_tps>` then
|
|
533
|
+
`template <method_type_params + proto_parts>`. Either level may
|
|
534
|
+
collapse to no-op if that level has no params (e.g. a non-template
|
|
535
|
+
method on a class template emits only the class header; a method
|
|
536
|
+
template on a non-generic class emits only the method header). The
|
|
537
|
+
nested form is the **only** C++-legal spelling for "member function
|
|
538
|
+
template of a class template"; the flat form `_emit_template_header`
|
|
539
|
+
produces declares a different entity and triggers
|
|
540
|
+
no-declaration-matches at the in-class declaration."""
|
|
541
|
+
record_parts = self._record_template_parts(record_name)
|
|
542
|
+
if record_parts:
|
|
543
|
+
out.write(f"{indent}template <{', '.join(record_parts)}>\n")
|
|
544
|
+
proto_parts = self._protocol_template_parts(func)
|
|
545
|
+
if func.type_params or proto_parts:
|
|
546
|
+
parts_list = [f"typename {tp}" for tp in func.type_params]
|
|
547
|
+
parts_list.extend(proto_parts)
|
|
548
|
+
out.write(f"{indent}template <{', '.join(parts_list)}>\n")
|
|
549
|
+
|
|
550
|
+
def _struct_name_templated(self, func: TpyFunction,
|
|
551
|
+
record_name: str | None = None) -> str:
|
|
552
|
+
"""Return the coro struct name suffixed with `<T1, T2, ...>` when the
|
|
553
|
+
function is generic, else the bare name. Use this whenever the
|
|
554
|
+
struct name appears in a type-name position (return types,
|
|
555
|
+
out-of-line method qualifiers, parameter types) -- C++ rejects
|
|
556
|
+
the injected-class-name there. The bare `gen_struct_name` is
|
|
557
|
+
still correct inside the struct body (constructors) and for
|
|
558
|
+
forward decls (`struct X;`).
|
|
559
|
+
|
|
560
|
+
Template-arg order matches `_emit_template_header`: record's type
|
|
561
|
+
params first (for a method on a generic class), then the function's
|
|
562
|
+
own type params, then per-param `T_<pname>` static-protocol args.
|
|
563
|
+
"""
|
|
564
|
+
bare = self.gen_struct_name(func, record_name)
|
|
565
|
+
extras = [p.field_type for p in self._classify_params(func, record_name)
|
|
566
|
+
if p.kind is _CoroParamKind.STATIC_PROTOCOL]
|
|
567
|
+
all_args = (list(self._record_template_args(record_name))
|
|
568
|
+
+ list(func.type_params) + extras)
|
|
569
|
+
if not all_args:
|
|
570
|
+
return bare
|
|
571
|
+
return f"{bare}<{', '.join(all_args)}>"
|
|
572
|
+
|
|
573
|
+
def _emit_params_decl(self, func: TpyFunction) -> str:
|
|
574
|
+
return ", ".join(p.factory_param_decl() for p in self._classify_params(func))
|
|
575
|
+
|
|
576
|
+
def _ret_cpp(self, func: TpyFunction) -> str:
|
|
577
|
+
return self.types.type_to_cpp(unwrap_ref_type(func.return_type))
|
|
578
|
+
|
|
579
|
+
def _is_void_return(self, func: TpyFunction) -> bool:
|
|
580
|
+
"""True iff func's declared return is `None` -- i.e. the top-level
|
|
581
|
+
`-> None` annotation that lowers to C++ `void`. Distinguished from
|
|
582
|
+
`Task[None]` / `Poll[None]` consumers, where the inner None is at
|
|
583
|
+
a type-arg position and routes to `std::monostate`."""
|
|
584
|
+
return isinstance(unwrap_ref_type(func.return_type), VoidType)
|
|
585
|
+
|
|
586
|
+
def _poll_ret_cpp(self, func: TpyFunction) -> str:
|
|
587
|
+
# The state-machine's __poll__ returns Poll[T], a type-arg slot,
|
|
588
|
+
# so a `-> None` async def flips its inner T to the unit type --
|
|
589
|
+
# matching how Task[None] / Future[None] / Awaitable[None] lower
|
|
590
|
+
# elsewhere. Without the flip, `Task<std::monostate>::poll_frame()`
|
|
591
|
+
# would call a `Poll<void> __poll__()` and the C++ compiler would
|
|
592
|
+
# reject the return-type mismatch.
|
|
593
|
+
if self._is_void_return(func):
|
|
594
|
+
return "::tpystd::tpy::Poll<::std::monostate>"
|
|
595
|
+
return f"::tpystd::tpy::Poll<{self._ret_cpp(func)}>"
|
|
596
|
+
|
|
597
|
+
# -- Forward declarations -------------------------------------------------
|
|
598
|
+
|
|
599
|
+
def gen_coro_forward_decl(self, out: "TextIO", func: TpyFunction,
|
|
600
|
+
record_name: str | None = None) -> bool:
|
|
601
|
+
struct_name = self.gen_struct_name(func, record_name)
|
|
602
|
+
self._emit_template_header(out, func, record_name=record_name)
|
|
603
|
+
out.write(f"struct {struct_name};\n")
|
|
604
|
+
return True
|
|
605
|
+
|
|
606
|
+
def gen_factory_forward_decl(self, out: "TextIO", func: TpyFunction) -> bool:
|
|
607
|
+
return_type_name = self._struct_name_templated(func)
|
|
608
|
+
self._emit_template_header(out, func)
|
|
609
|
+
params = self._emit_params_decl(func)
|
|
610
|
+
out.write(f"{return_type_name} {escape_cpp_name(func.name)}({params});\n")
|
|
611
|
+
return True
|
|
612
|
+
|
|
613
|
+
# -- Body partitioning ----------------------------------------------------
|
|
614
|
+
|
|
615
|
+
def _effective_body(self, func: TpyFunction) -> list[TpyStmt]:
|
|
616
|
+
"""Return the function body with the await-lift pass applied so
|
|
617
|
+
awaits embedded in expressions become top-level vardecls. The
|
|
618
|
+
CFG builder treats compound statements (including any
|
|
619
|
+
wrapping try/finally) uniformly, so no unwrap is needed here.
|
|
620
|
+
|
|
621
|
+
Cached on the TpyFunction node so multiple codegen passes
|
|
622
|
+
(struct emission + poll body) see the same rewrite.
|
|
623
|
+
"""
|
|
624
|
+
state = rcfg.resumable_state(func)
|
|
625
|
+
if state.lifted_body is not None:
|
|
626
|
+
return state.lifted_body
|
|
627
|
+
lifted = self._lift_nested_awaits(func, func.body)
|
|
628
|
+
state.lifted_body = lifted
|
|
629
|
+
return lifted
|
|
630
|
+
|
|
631
|
+
def _lift_nested_awaits(self, func: TpyFunction,
|
|
632
|
+
body: list[TpyStmt]) -> list[TpyStmt]:
|
|
633
|
+
"""Rewrite each statement that has awaits buried in expressions
|
|
634
|
+
into a sequence of statements where every await is at top level.
|
|
635
|
+
|
|
636
|
+
Recurses into compound statement bodies (if/while/for/try/with)
|
|
637
|
+
so awaits buried inside loop/branch bodies are also lifted.
|
|
638
|
+
|
|
639
|
+
Example:
|
|
640
|
+
x = (await a()) + (await b())
|
|
641
|
+
becomes:
|
|
642
|
+
__await_lift_0 = await a()
|
|
643
|
+
__await_lift_1 = await b()
|
|
644
|
+
x = __await_lift_0 + __await_lift_1
|
|
645
|
+
|
|
646
|
+
Each lifted name is registered as a hoisted local so the bind
|
|
647
|
+
slot is a frame field.
|
|
648
|
+
"""
|
|
649
|
+
out: list[TpyStmt] = []
|
|
650
|
+
for stmt in body:
|
|
651
|
+
top_await = rcfg._top_level_await_in(stmt)
|
|
652
|
+
new_stmts = self._lift_awaits_in_stmt(func, stmt, top_await)
|
|
653
|
+
# Recurse compound bodies AFTER the surface lift so a
|
|
654
|
+
# substituted TpyName isn't re-scanned. The surface lift
|
|
655
|
+
# mutates `stmt` in place (see BUGS.md "Await lifter
|
|
656
|
+
# mutates parse AST"); the compound recurse returns a
|
|
657
|
+
# shallow copy so nested bodies don't.
|
|
658
|
+
new_stmts = [
|
|
659
|
+
self._lift_compound_subbodies(func, s) for s in new_stmts
|
|
660
|
+
]
|
|
661
|
+
out.extend(new_stmts)
|
|
662
|
+
return out
|
|
663
|
+
|
|
664
|
+
def _lift_compound_subbodies(self, func: TpyFunction,
|
|
665
|
+
stmt: TpyStmt) -> TpyStmt:
|
|
666
|
+
"""If `stmt` is a compound (if/while/for/try/with) and any of
|
|
667
|
+
its sub-bodies needs lifting, return a shallow copy with the
|
|
668
|
+
sub-body lists replaced by lifted versions. Otherwise return
|
|
669
|
+
`stmt` unchanged. Does not mutate the input."""
|
|
670
|
+
if not hasattr(stmt, "sub_bodies"):
|
|
671
|
+
return stmt
|
|
672
|
+
if not is_dataclass(stmt):
|
|
673
|
+
return stmt
|
|
674
|
+
# Preserve the original stmt object (and its id) when sub-body
|
|
675
|
+
# lifting produced no actual changes -- any analyzer-side
|
|
676
|
+
# id(stmt)-keyed dict (e.g. if_branch_decls) would otherwise be
|
|
677
|
+
# orphaned by a spurious clone.
|
|
678
|
+
replacements: dict[str, list[TpyStmt]] = {}
|
|
679
|
+
for f in fields(stmt):
|
|
680
|
+
v = getattr(stmt, f.name, None)
|
|
681
|
+
if isinstance(v, list) and v and isinstance(v[0], TpyStmt):
|
|
682
|
+
lifted = self._lift_nested_awaits(func, v)
|
|
683
|
+
if not _same_elements(lifted, v):
|
|
684
|
+
replacements[f.name] = lifted
|
|
685
|
+
# TpyTry: handlers list isn't TpyStmt-typed but each handler
|
|
686
|
+
# carries its own body. Build new handler instances if any
|
|
687
|
+
# handler body needed lifting.
|
|
688
|
+
new_handlers: list[TpyExceptHandler] | None = None
|
|
689
|
+
if isinstance(stmt, TpyTry) and stmt.handlers:
|
|
690
|
+
rebuilt: list[TpyExceptHandler] = []
|
|
691
|
+
any_changed = False
|
|
692
|
+
for h in stmt.handlers:
|
|
693
|
+
lifted_body = self._lift_nested_awaits(func, h.body) if h.body else h.body
|
|
694
|
+
if not _same_elements(lifted_body, h.body):
|
|
695
|
+
rebuilt.append(TpyExceptHandler(
|
|
696
|
+
exception_type=h.exception_type,
|
|
697
|
+
binding=h.binding,
|
|
698
|
+
body=lifted_body,
|
|
699
|
+
loc=h.loc,
|
|
700
|
+
))
|
|
701
|
+
any_changed = True
|
|
702
|
+
else:
|
|
703
|
+
rebuilt.append(h)
|
|
704
|
+
if any_changed:
|
|
705
|
+
new_handlers = rebuilt
|
|
706
|
+
if not replacements and new_handlers is None:
|
|
707
|
+
return stmt
|
|
708
|
+
new_stmt = copy.copy(stmt)
|
|
709
|
+
for fname, lifted in replacements.items():
|
|
710
|
+
setattr(new_stmt, fname, lifted)
|
|
711
|
+
if new_handlers is not None:
|
|
712
|
+
new_stmt.handlers = new_handlers
|
|
713
|
+
return new_stmt
|
|
714
|
+
|
|
715
|
+
def _lift_awaits_in_stmt(self, func: TpyFunction, stmt: TpyStmt,
|
|
716
|
+
top_await: 'TpyAwait | None') -> list[TpyStmt]:
|
|
717
|
+
"""Lift any TpyAwait inside `stmt`'s expressions out into preceding
|
|
718
|
+
VarDecls. The top-level await (if any) stays in place. Returns the
|
|
719
|
+
new statement list; if no rewrite needed, returns [stmt].
|
|
720
|
+
"""
|
|
721
|
+
# Collect nested awaits (excluding the top-level one).
|
|
722
|
+
lifts: list[TpyAwait] = []
|
|
723
|
+
self._collect_nested_awaits(stmt, top_await, lifts)
|
|
724
|
+
if not lifts:
|
|
725
|
+
return [stmt]
|
|
726
|
+
new_stmts: list[TpyStmt] = []
|
|
727
|
+
for await_node in lifts:
|
|
728
|
+
name = f"__await_lift_{self._next_lift_id(func)}"
|
|
729
|
+
self._bump_lift_id(func)
|
|
730
|
+
await_t = self.ctx.get_expr_type(await_node)
|
|
731
|
+
if await_t is None:
|
|
732
|
+
raise CodeGenError(
|
|
733
|
+
"await result has no analyzed type (lift)", loc=stmt.loc)
|
|
734
|
+
await_t = unwrap_ref_type(await_t)
|
|
735
|
+
new_decl = TpyVarDecl(
|
|
736
|
+
name=name, type=await_t, init=await_node, loc=stmt.loc)
|
|
737
|
+
new_stmts.append(new_decl)
|
|
738
|
+
# Register as hoisted local; codegen emits a frame field.
|
|
739
|
+
# Append in place to avoid an O(N) copy per lift (which,
|
|
740
|
+
# paired with _next_lift_id's linear scan, would otherwise
|
|
741
|
+
# be O(N^2) over nested awaits in one function).
|
|
742
|
+
if func.generator_locals is None:
|
|
743
|
+
func.generator_locals = []
|
|
744
|
+
func.generator_locals.append((name, await_t))
|
|
745
|
+
# Replace the await in `stmt`'s expression tree with a
|
|
746
|
+
# TpyName referring to the lifted local. Use the analyzer's
|
|
747
|
+
# set_expr_type so codegen's get_expr_type sees it.
|
|
748
|
+
replacement = TpyName(name=name, loc=await_node.loc)
|
|
749
|
+
self.ctx.analyzer.ctx.set_expr_type(replacement, await_t)
|
|
750
|
+
self._replace_expr_in_stmt(stmt, await_node, replacement)
|
|
751
|
+
new_stmts.append(stmt)
|
|
752
|
+
return new_stmts
|
|
753
|
+
|
|
754
|
+
def _collect_nested_awaits(self, stmt: TpyStmt, top_await: 'TpyAwait | None',
|
|
755
|
+
out: list[TpyAwait]) -> None:
|
|
756
|
+
"""Walk stmt's expressions; collect TpyAwait nodes that are NOT the
|
|
757
|
+
top-level await (which is handled by region partitioning). Skips
|
|
758
|
+
nested defs / sub_bodies."""
|
|
759
|
+
|
|
760
|
+
def walk_expr(e):
|
|
761
|
+
if e is None:
|
|
762
|
+
return
|
|
763
|
+
if isinstance(e, TpyAwait):
|
|
764
|
+
if e is not top_await:
|
|
765
|
+
out.append(e)
|
|
766
|
+
# Don't recurse into the await's own value; that becomes
|
|
767
|
+
# the call processed by the lifted VarDecl.
|
|
768
|
+
return
|
|
769
|
+
for c in (e.children() if hasattr(e, "children") else ()):
|
|
770
|
+
walk_expr(c)
|
|
771
|
+
|
|
772
|
+
if hasattr(stmt, "exprs"):
|
|
773
|
+
for e in stmt.exprs():
|
|
774
|
+
walk_expr(e)
|
|
775
|
+
|
|
776
|
+
def _next_lift_id(self, func: TpyFunction) -> int:
|
|
777
|
+
"""Return the next free index for __await_lift_<n> names.
|
|
778
|
+
Tracked as a per-function counter so the lift pass is linear
|
|
779
|
+
in the total number of lifts."""
|
|
780
|
+
return rcfg.resumable_state(func).next_lift_id
|
|
781
|
+
|
|
782
|
+
def _bump_lift_id(self, func: TpyFunction) -> None:
|
|
783
|
+
rcfg.resumable_state(func).next_lift_id += 1
|
|
784
|
+
|
|
785
|
+
def _replace_expr_in_stmt(self, stmt: TpyStmt, old_expr,
|
|
786
|
+
new_expr) -> None:
|
|
787
|
+
"""Walk stmt's dataclass fields and replace `old_expr` with
|
|
788
|
+
`new_expr` (by identity). Catches both primary-expression slots
|
|
789
|
+
(init/value/expr/condition for if/while/etc.) and nested
|
|
790
|
+
expression children."""
|
|
791
|
+
if not is_dataclass(stmt):
|
|
792
|
+
return
|
|
793
|
+
for f in fields(stmt):
|
|
794
|
+
v = getattr(stmt, f.name, None)
|
|
795
|
+
if v is old_expr:
|
|
796
|
+
setattr(stmt, f.name, new_expr)
|
|
797
|
+
return
|
|
798
|
+
if isinstance(v, list):
|
|
799
|
+
for i, item in enumerate(v):
|
|
800
|
+
if item is old_expr:
|
|
801
|
+
v[i] = new_expr
|
|
802
|
+
return
|
|
803
|
+
if hasattr(item, "children"):
|
|
804
|
+
self._replace_in_expr(item, old_expr, new_expr)
|
|
805
|
+
elif hasattr(v, "children"):
|
|
806
|
+
self._replace_in_expr(v, old_expr, new_expr)
|
|
807
|
+
|
|
808
|
+
def _replace_in_expr(self, expr, old_expr, new_expr) -> None:
|
|
809
|
+
"""Recursively replace `old_expr` with `new_expr` inside `expr`."""
|
|
810
|
+
if not hasattr(expr, "children") or not is_dataclass(expr):
|
|
811
|
+
return
|
|
812
|
+
for f in fields(expr):
|
|
813
|
+
v = getattr(expr, f.name, None)
|
|
814
|
+
if v is old_expr:
|
|
815
|
+
setattr(expr, f.name, new_expr)
|
|
816
|
+
return
|
|
817
|
+
if isinstance(v, list):
|
|
818
|
+
for i, item in enumerate(v):
|
|
819
|
+
if item is old_expr:
|
|
820
|
+
v[i] = new_expr
|
|
821
|
+
return
|
|
822
|
+
if hasattr(item, "children"):
|
|
823
|
+
self._replace_in_expr(item, old_expr, new_expr)
|
|
824
|
+
elif hasattr(v, "children"):
|
|
825
|
+
self._replace_in_expr(v, old_expr, new_expr)
|
|
826
|
+
|
|
827
|
+
# -- Struct definition ----------------------------------------------------
|
|
828
|
+
|
|
829
|
+
def gen_coro_struct(self, out: "TextIO", func: TpyFunction,
|
|
830
|
+
record_name: str | None = None) -> None:
|
|
831
|
+
"""Emit the full `__FCoro` struct definition. When `record_name`
|
|
832
|
+
is given, the struct captures `__self: <Record>&` and the struct
|
|
833
|
+
name is `__coro_<Record>_<func>` (mirrors generator-method
|
|
834
|
+
codegen).
|
|
835
|
+
"""
|
|
836
|
+
struct_name = self.gen_struct_name(func, record_name)
|
|
837
|
+
ctor_params = self._classify_params(func, record_name)
|
|
838
|
+
cfg = self._build_resumable_cfg(func)
|
|
839
|
+
yields = cfg.yield_sites
|
|
840
|
+
|
|
841
|
+
label = f"{record_name}.{func.name}" if record_name else func.name
|
|
842
|
+
kind = "Generator" if self._is_generator_shape() else "Async coroutine"
|
|
843
|
+
out.write(f"// {kind}: {label}\n")
|
|
844
|
+
self._emit_template_header(out, func, record_name=record_name)
|
|
845
|
+
out.write(f"struct {struct_name} {{\n")
|
|
846
|
+
|
|
847
|
+
# State integer (shared) + per-shape extra state (async adds the
|
|
848
|
+
# cancel flag).
|
|
849
|
+
out.write(f"{INDENT}int32_t __state;\n")
|
|
850
|
+
self._emit_resumable_extra_state_fields(out)
|
|
851
|
+
|
|
852
|
+
# Captured param fields
|
|
853
|
+
for p in ctor_params:
|
|
854
|
+
out.write(f"{INDENT}{p.field_decl()};\n")
|
|
855
|
+
|
|
856
|
+
state = rcfg.resumable_state(func)
|
|
857
|
+
# Hoisted local fields (mirrors generator behavior).
|
|
858
|
+
if func.generator_locals:
|
|
859
|
+
owning_str = state.with_owning_str_targets
|
|
860
|
+
pointer_form_names: set[str] = set()
|
|
861
|
+
for info in state.for_loop_info.values():
|
|
862
|
+
if info.pointer_form_loop_var is not None:
|
|
863
|
+
pointer_form_names.add(info.pointer_form_loop_var)
|
|
864
|
+
pointer_form_names.update(info.pointer_form_unpack_targets)
|
|
865
|
+
for lname, ltype in func.generator_locals:
|
|
866
|
+
ltype_inner = unwrap_ref_type(ltype)
|
|
867
|
+
cpp_name = escape_cpp_name(lname)
|
|
868
|
+
if lname in owning_str:
|
|
869
|
+
# `with X() as label:` -- `__enter__` returns by
|
|
870
|
+
# value; storing the view across suspensions
|
|
871
|
+
# would dangle. Use owning storage. See
|
|
872
|
+
# `_prescan_with_stmts`.
|
|
873
|
+
out.write(f"{INDENT}std::string {cpp_name};\n")
|
|
874
|
+
elif lname in pointer_form_names:
|
|
875
|
+
# Pointer-form alias: a for-loop var (non-value element
|
|
876
|
+
# over a stable source) or a tuple-unpack target bound to
|
|
877
|
+
# a non-value container member. Stored as `T*` (alias the
|
|
878
|
+
# live element) rather than `frame_slot<T>` (value-copy),
|
|
879
|
+
# so mutations propagate and resume preserves aliasing;
|
|
880
|
+
# also works for @nocopy / move-only elements. Bound by
|
|
881
|
+
# address in `_emit_async_for_advance` / the tuple-unpack
|
|
882
|
+
# emit. Checked before is_value_type because the loop var
|
|
883
|
+
# of a tuple-unpack (`__for_tup`) is itself a value tuple.
|
|
884
|
+
inner_cpp = self.types.type_to_cpp(ltype_inner)
|
|
885
|
+
out.write(f"{INDENT}{inner_cpp}* {cpp_name} = nullptr;\n")
|
|
886
|
+
elif ltype_inner.is_value_type():
|
|
887
|
+
cpp_type = self.types.type_to_cpp(ltype_inner)
|
|
888
|
+
out.write(f"{INDENT}{cpp_type} {cpp_name};\n")
|
|
889
|
+
elif (isinstance(ltype_inner, OptionalType)
|
|
890
|
+
and ltype_inner.uses_pointer_repr()):
|
|
891
|
+
# Pointer-repr Optional: bare `T* = nullptr` aliases
|
|
892
|
+
# the source and uses nullptr as both "uninitialized"
|
|
893
|
+
# and "None"; no outer `std::optional<...>` wrap.
|
|
894
|
+
inner_cpp = self.types.type_to_cpp(ltype_inner.inner)
|
|
895
|
+
out.write(f"{INDENT}{inner_cpp}* {cpp_name} = nullptr;\n")
|
|
896
|
+
else:
|
|
897
|
+
cpp_type = self.types.type_to_cpp(ltype_inner)
|
|
898
|
+
out.write(f"{INDENT}::tpy::frame_slot<{cpp_type}> {cpp_name};\n")
|
|
899
|
+
|
|
900
|
+
# Synthetic fields for CFG-decomposed for-loops: one
|
|
901
|
+
# iterator + one __next__-result slot per for-with-await,
|
|
902
|
+
# stored as frame_slot<T> -- same memory shape as the hoisted
|
|
903
|
+
# user-local fields, so the whole frame uses one slot type.
|
|
904
|
+
for fname, ftype in state.for_fields:
|
|
905
|
+
out.write(f"{INDENT}::tpy::frame_slot<{ftype}> {fname};\n")
|
|
906
|
+
|
|
907
|
+
# Context-manager fields for CFG-decomposed `with`-with-await
|
|
908
|
+
# bodies. One frame_slot<T> per WithItem.
|
|
909
|
+
for fname, ftype in state.with_fields:
|
|
910
|
+
out.write(f"{INDENT}::tpy::frame_slot<{ftype}> {fname};\n")
|
|
911
|
+
|
|
912
|
+
# In-flight exception slots for CFG-decomposed try-finally-with-
|
|
913
|
+
# await bodies. std::exception_ptr default-constructs
|
|
914
|
+
# to null; the catch arm sets it via std::current_exception().
|
|
915
|
+
# bool pending-return flags need explicit init: NSDMI
|
|
916
|
+
# = false. Value slots default-init via their own type's ctor.
|
|
917
|
+
is_gen = self._is_generator_shape()
|
|
918
|
+
for fname, ftype in state.try_finally_fields:
|
|
919
|
+
# Generators always return StopIteration; no pending-return value
|
|
920
|
+
# slot is needed. The field may be allocated by the prescan when
|
|
921
|
+
# the trial build ran under the async shape -- skip it here.
|
|
922
|
+
if is_gen and fname.startswith("__finally_ret_"):
|
|
923
|
+
continue
|
|
924
|
+
if ftype == "bool":
|
|
925
|
+
out.write(f"{INDENT}{ftype} {fname} = false;\n")
|
|
926
|
+
else:
|
|
927
|
+
out.write(f"{INDENT}{ftype} {fname};\n")
|
|
928
|
+
|
|
929
|
+
self._emit_resumable_sub_future_fields(out, func, yields)
|
|
930
|
+
|
|
931
|
+
# Generators with helper-based finallies that contain a `return` need a
|
|
932
|
+
# stop flag: the helper sets it so `__next__()` emits StopIteration
|
|
933
|
+
# instead of re-throwing (Python: `return` in `finally` suppresses exc).
|
|
934
|
+
if is_gen and any(_stmts_have_return(body)
|
|
935
|
+
for _, body in cfg.finally_helpers):
|
|
936
|
+
out.write(f"{INDENT}bool __finally_stop = false;\n")
|
|
937
|
+
|
|
938
|
+
out.write(f"\n")
|
|
939
|
+
|
|
940
|
+
# State enum: S_INITIAL, S_RESUME_<i>, S_JOIN_<n>, S_DONE.
|
|
941
|
+
# State numbering matches _compute_case_entries' assignment.
|
|
942
|
+
case_entries = self._compute_case_entries(cfg)
|
|
943
|
+
ordered = sorted(case_entries.items(), key=lambda kv: kv[1])
|
|
944
|
+
out.write(f"{INDENT}enum : int32_t {{\n")
|
|
945
|
+
next_val = 0
|
|
946
|
+
for _bb_id, label in ordered:
|
|
947
|
+
out.write(f"{INDENT}{INDENT}{label.cpp_name()} = {next_val},\n")
|
|
948
|
+
next_val += 1
|
|
949
|
+
out.write(f"{INDENT}{INDENT}S_DONE = {next_val},\n")
|
|
950
|
+
out.write(f"{INDENT}}};\n\n")
|
|
951
|
+
|
|
952
|
+
# Constructor.
|
|
953
|
+
ctor_param_list = ", ".join(p.ctor_param_decl() for p in ctor_params)
|
|
954
|
+
init_parts = ["__state(S_INITIAL)", *self._resumable_extra_ctor_inits()]
|
|
955
|
+
init_parts.extend(p.ctor_init() for p in ctor_params)
|
|
956
|
+
out.write(f"{INDENT}{struct_name}({ctor_param_list})\n")
|
|
957
|
+
out.write(f"{INDENT}{INDENT}: {', '.join(init_parts)} {{}}\n\n")
|
|
958
|
+
|
|
959
|
+
# Body-method forward declaration + per-shape extra methods.
|
|
960
|
+
out.write(f"{INDENT}{self._resumable_body_method_fwd_decl(func)};\n")
|
|
961
|
+
self._emit_resumable_extra_methods(out, struct_name)
|
|
962
|
+
|
|
963
|
+
# Finally-helper forward declarations: one per TryRegion with a
|
|
964
|
+
# finally body.
|
|
965
|
+
for helper_name, _body in cfg.finally_helpers:
|
|
966
|
+
out.write(f"{INDENT}void {helper_name}();\n")
|
|
967
|
+
|
|
968
|
+
repr_label = (f"{record_name}.{func.name}" if record_name
|
|
969
|
+
else func.name)
|
|
970
|
+
repr_kind = "generator" if self._is_generator_shape() else "coroutine"
|
|
971
|
+
param_struct_name = self._struct_name_templated(func, record_name)
|
|
972
|
+
out.write(f"\n{INDENT}friend std::ostream& operator<<("
|
|
973
|
+
f"std::ostream& os, const {param_struct_name}&) {{\n")
|
|
974
|
+
out.write(f"{INDENT}{INDENT}return os << "
|
|
975
|
+
f"\"<{repr_kind} {repr_label}>\";\n")
|
|
976
|
+
out.write(f"{INDENT}}}\n")
|
|
977
|
+
out.write(f"}};\n")
|
|
978
|
+
|
|
979
|
+
# -- Factory function -----------------------------------------------------
|
|
980
|
+
|
|
981
|
+
def gen_factory(self, out: "TextIO", func: TpyFunction) -> None:
|
|
982
|
+
"""Emit the factory function: `__FCoro f(args) { return __FCoro(args); }`.
|
|
983
|
+
|
|
984
|
+
Uses the templated struct name explicitly so zero-param generic
|
|
985
|
+
async defs (no ctor args for CTAD to deduce T from) compile."""
|
|
986
|
+
struct_name = self._struct_name_templated(func)
|
|
987
|
+
self.ctx.emit_source_comment(out, func.loc)
|
|
988
|
+
self._emit_template_header(out, func)
|
|
989
|
+
params = self._emit_params_decl(func)
|
|
990
|
+
out.write(f"{struct_name} {escape_cpp_name(func.name)}({params}) {{\n")
|
|
991
|
+
args = self._factory_args_forwarded(func)
|
|
992
|
+
out.write(f"{INDENT}return {struct_name}({args});\n")
|
|
993
|
+
out.write(f"}}\n")
|
|
994
|
+
|
|
995
|
+
def _factory_args_forwarded(
|
|
996
|
+
self, func: TpyFunction,
|
|
997
|
+
*, receiver: tuple[str, str] | None = None) -> str:
|
|
998
|
+
"""Format the arg list for a coro-struct factory call.
|
|
999
|
+
|
|
1000
|
+
Static-protocol params (`Own[Awaitable[T]]` etc.) take a
|
|
1001
|
+
forwarding-ref `T_<pname>&&` -- which, inside the factory body,
|
|
1002
|
+
is an lvalue -- so the call to the struct ctor (also `T_<pname>&&`)
|
|
1003
|
+
must wrap the name in `std::move(...)` to bind. Other kinds pass
|
|
1004
|
+
by bare name.
|
|
1005
|
+
|
|
1006
|
+
`receiver=(record_name, recv_expr)` supports async methods: the
|
|
1007
|
+
receiver is prepended to the arg list as `recv_expr` (typically
|
|
1008
|
+
`"*this"`), and `__self` is filtered out of the classified
|
|
1009
|
+
params so it isn't double-emitted. None for free async fns.
|
|
1010
|
+
"""
|
|
1011
|
+
record_name, recv_expr = receiver if receiver is not None else (None, None)
|
|
1012
|
+
parts: list[str] = []
|
|
1013
|
+
if recv_expr is not None:
|
|
1014
|
+
parts.append(recv_expr)
|
|
1015
|
+
for cparam in self._classify_params(func, record_name):
|
|
1016
|
+
if cparam.cpp_name == "__self":
|
|
1017
|
+
continue
|
|
1018
|
+
if cparam.kind in (_CoroParamKind.STATIC_PROTOCOL,
|
|
1019
|
+
_CoroParamKind.OWNED_VALUE):
|
|
1020
|
+
parts.append(f"std::move({cparam.cpp_name})")
|
|
1021
|
+
else:
|
|
1022
|
+
parts.append(cparam.cpp_name)
|
|
1023
|
+
return ", ".join(parts)
|
|
1024
|
+
|
|
1025
|
+
# -- poll() body ----------------------------------------------------------
|
|
1026
|
+
|
|
1027
|
+
@contextlib.contextmanager
|
|
1028
|
+
def _resumable_frame_ctx(self, func: TpyFunction, record_name: str | None):
|
|
1029
|
+
"""Set up + tear down resumable-frame ctx state for an async body.
|
|
1030
|
+
|
|
1031
|
+
Routes through `StatementGenerator.setup_body_scope` so async bodies
|
|
1032
|
+
get the same per-scope state setup as sync (reassigned_vars,
|
|
1033
|
+
aliased_vars, movable_locals, etc. populated from sema scan).
|
|
1034
|
+
Layers the resumable-frame fields (`generator_field_names`,
|
|
1035
|
+
`generator_self_ref`, etc.) plus `in_generator_body=True` on top.
|
|
1036
|
+
"""
|
|
1037
|
+
old_in_gen = self.ctx.in_generator_body
|
|
1038
|
+
old_field_names = self.ctx.generator_field_names
|
|
1039
|
+
old_optional_fields = self.ctx.generator_optional_fields
|
|
1040
|
+
old_frame_slot_locals = self.ctx.generator_frame_slot_locals
|
|
1041
|
+
old_borrow_form_loop_vars = self.ctx.generator_borrow_form_loop_vars
|
|
1042
|
+
old_for_info = self.ctx.generator_for_loop_info
|
|
1043
|
+
old_self_ref = self.ctx.generator_self_ref
|
|
1044
|
+
old_movable_locals = self.ctx.movable_locals
|
|
1045
|
+
old_in_method = self.ctx.in_method
|
|
1046
|
+
old_method_record = self.ctx.current_method_record_type
|
|
1047
|
+
# `setup_body_scope` -> `reset_scope` wipes `pointer_locals` and
|
|
1048
|
+
# `current_type_param_bounds` on entry, so the inner body sees a
|
|
1049
|
+
# clean state. Save/restore here so a future caller that nests sync
|
|
1050
|
+
# body emission around a coro doesn't see inner-coro state leak out
|
|
1051
|
+
# on exit.
|
|
1052
|
+
old_pointer_locals = self.ctx.pointer_locals
|
|
1053
|
+
old_type_param_bounds = self.ctx.current_type_param_bounds
|
|
1054
|
+
|
|
1055
|
+
# Reset frame-specific fields before setup_body_scope, since the
|
|
1056
|
+
# `setup_resumable_frame_locals` call inside it reads
|
|
1057
|
+
# `generator_for_loop_info` and writes to `generator_optional_fields`.
|
|
1058
|
+
self.ctx.generator_field_names = set()
|
|
1059
|
+
self.ctx.generator_optional_fields = set()
|
|
1060
|
+
self.ctx.generator_frame_slot_locals = set()
|
|
1061
|
+
self.ctx.generator_borrow_form_loop_vars = set()
|
|
1062
|
+
# Populated by `_prescan_resumable_for_loops`; carries
|
|
1063
|
+
# `pointer_form_loop_var` so `setup_resumable_frame_locals` (called
|
|
1064
|
+
# inside `setup_body_scope` below) seeds non-value loop vars into
|
|
1065
|
+
# `pointer_locals`. Empty for bodies with no CFG-decomposed for-loop.
|
|
1066
|
+
self.ctx.generator_for_loop_info = rcfg.resumable_state(func).for_loop_info
|
|
1067
|
+
|
|
1068
|
+
local_ns = Namespace(parent=self.ctx.analyzer.global_ns)
|
|
1069
|
+
for pname, ptype in func.params:
|
|
1070
|
+
local_ns.bind_variable(pname, ptype)
|
|
1071
|
+
|
|
1072
|
+
crp, dcbp = self.functions.compute_body_const_sets(func, record_name)
|
|
1073
|
+
# Mirror sync `_gen_method`: a generator/async method on a generic
|
|
1074
|
+
# record needs the record's type-param bounds in scope so for-loop
|
|
1075
|
+
# over `self.items: T` resolves T to its protocol bound (otherwise
|
|
1076
|
+
# falls back to the universal `::tpy::__iter__` path, missing the
|
|
1077
|
+
# direct-iterator / native-iterable peepholes and breaking move-only
|
|
1078
|
+
# `Iterator[X]` params).
|
|
1079
|
+
record_bounds = None
|
|
1080
|
+
if record_name:
|
|
1081
|
+
rec_info = self.ctx.analyzer.registry.get_record(record_name)
|
|
1082
|
+
if rec_info is not None and rec_info.type_param_bounds:
|
|
1083
|
+
record_bounds = rec_info.type_param_bounds
|
|
1084
|
+
self.statements.setup_body_scope(
|
|
1085
|
+
func.params, func.return_type, func, local_ns,
|
|
1086
|
+
indent_level=1, is_method=bool(record_name),
|
|
1087
|
+
const_ref_params=crp, deep_const_borrow_params=dcbp,
|
|
1088
|
+
owning_record_name=record_name,
|
|
1089
|
+
record_type_param_bounds=record_bounds,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
self.ctx.in_generator_body = True
|
|
1093
|
+
# Belt-and-suspenders: `setup_body_scope` registers Own[T] params
|
|
1094
|
+
# as movable when `T.is_value_type()` is False, which already
|
|
1095
|
+
# covers most static-protocol shapes. Static-protocol frame
|
|
1096
|
+
# fields (e.g. `coro: Own[Awaitable[T]]` stored as the deduced
|
|
1097
|
+
# `T_coro`) are move-only by construction; force-register them
|
|
1098
|
+
# so the call-arg generator emits `std::move(coro)` rather than
|
|
1099
|
+
# a copy-into-temp + move, independent of how the protocol's
|
|
1100
|
+
# `is_value_type` resolves.
|
|
1101
|
+
for cparam in self._classify_params(func, record_name):
|
|
1102
|
+
if cparam.kind in (_CoroParamKind.STATIC_PROTOCOL,
|
|
1103
|
+
_CoroParamKind.OWNED_VALUE):
|
|
1104
|
+
self.ctx.movable_locals.add(cparam.cpp_name)
|
|
1105
|
+
if record_name:
|
|
1106
|
+
self.ctx.generator_self_ref = "__self"
|
|
1107
|
+
self.ctx.generator_field_names.add("__self")
|
|
1108
|
+
else:
|
|
1109
|
+
self.ctx.generator_self_ref = None
|
|
1110
|
+
for pname, _ in func.params:
|
|
1111
|
+
self.ctx.generator_field_names.add(pname)
|
|
1112
|
+
if func.generator_locals:
|
|
1113
|
+
for lname, _ltype in func.generator_locals:
|
|
1114
|
+
self.ctx.generator_field_names.add(lname)
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
yield
|
|
1118
|
+
finally:
|
|
1119
|
+
self.ctx.in_generator_body = old_in_gen
|
|
1120
|
+
self.ctx.generator_field_names = old_field_names
|
|
1121
|
+
self.ctx.generator_optional_fields = old_optional_fields
|
|
1122
|
+
self.ctx.generator_frame_slot_locals = old_frame_slot_locals
|
|
1123
|
+
self.ctx.generator_borrow_form_loop_vars = old_borrow_form_loop_vars
|
|
1124
|
+
self.ctx.generator_for_loop_info = old_for_info
|
|
1125
|
+
self.ctx.generator_self_ref = old_self_ref
|
|
1126
|
+
self.ctx.movable_locals = old_movable_locals
|
|
1127
|
+
self.ctx.in_method = old_in_method
|
|
1128
|
+
self.ctx.current_method_record_type = old_method_record
|
|
1129
|
+
self.ctx.pointer_locals = old_pointer_locals
|
|
1130
|
+
self.ctx.current_type_param_bounds = old_type_param_bounds
|
|
1131
|
+
|
|
1132
|
+
def gen_coro_finally_top_def(self, out: "TextIO", func: TpyFunction,
|
|
1133
|
+
record_name: str | None = None) -> None:
|
|
1134
|
+
"""Emit member-function bodies for every `__finally_<n>()` helper
|
|
1135
|
+
the CFG produced (one per TryRegion with a finally body). No-op
|
|
1136
|
+
if the function has no finally bodies.
|
|
1137
|
+
"""
|
|
1138
|
+
cfg = self._build_resumable_cfg(func)
|
|
1139
|
+
if not cfg.finally_helpers:
|
|
1140
|
+
return
|
|
1141
|
+
struct_name = self._struct_name_templated(func, record_name)
|
|
1142
|
+
|
|
1143
|
+
with self._resumable_frame_ctx(func, record_name):
|
|
1144
|
+
for helper_name, body_stmts in cfg.finally_helpers:
|
|
1145
|
+
self._emit_template_header(out, func, record_name=record_name)
|
|
1146
|
+
out.write(f"void {struct_name}::{helper_name}() {{\n")
|
|
1147
|
+
self.ctx.indent_level = 1
|
|
1148
|
+
with self._generator_finally_helper_scope():
|
|
1149
|
+
for stmt in body_stmts:
|
|
1150
|
+
self.statements.gen_stmt(out, stmt)
|
|
1151
|
+
self.ctx.indent_level = 0
|
|
1152
|
+
out.write(f"}}\n")
|
|
1153
|
+
|
|
1154
|
+
# =====================================================================
|
|
1155
|
+
# Resumable-shape policy seam. These methods isolate the decisions
|
|
1156
|
+
# that differ between the async (`await` -> `__poll__`) shape and the
|
|
1157
|
+
# future generator (`yield` -> `__next__`) shape so the generator
|
|
1158
|
+
# migration can plug in without forking the state-machine emitter.
|
|
1159
|
+
# Each seam branches on `self._shape`; async output must stay
|
|
1160
|
+
# byte-identical (async never enters the generator branch).
|
|
1161
|
+
# =====================================================================
|
|
1162
|
+
|
|
1163
|
+
def _resumable_ret_type_cpp(self, func: TpyFunction) -> str:
|
|
1164
|
+
"""The frame body method's return type. Async: `Poll<T>`.
|
|
1165
|
+
Generator: `std::expected<T_slot, ::tpy::StopIteration>` where
|
|
1166
|
+
T_slot is the iterator slot type from
|
|
1167
|
+
`gen_generators._iter_slot_for_yield` -- borrow form
|
|
1168
|
+
(`std::tuple<T&, ...>` / `std::tuple<P*, ...>`) for tuple yields,
|
|
1169
|
+
bare cpp type otherwise. `gen_yield_value` already bridges
|
|
1170
|
+
storage-form sources into the borrow-form slot when
|
|
1171
|
+
`ctx.current_yield_type` is set (done by
|
|
1172
|
+
`_resumable_return_lowering`)."""
|
|
1173
|
+
if self._is_generator_shape():
|
|
1174
|
+
yt = func.generator_yield_type
|
|
1175
|
+
elem_cpp = self.types.type_to_cpp(yt)
|
|
1176
|
+
slot_cpp = GeneratorCodegen._iter_slot_for_yield(yt, elem_cpp)
|
|
1177
|
+
return f"std::expected<{slot_cpp}, ::tpy::StopIteration>"
|
|
1178
|
+
return self._poll_ret_cpp(func)
|
|
1179
|
+
|
|
1180
|
+
def _resumable_body_method_decl(self, func: TpyFunction,
|
|
1181
|
+
struct_name: str) -> str:
|
|
1182
|
+
"""Full declarator of the frame's body method: return type +
|
|
1183
|
+
qualified name + params. Async: `Poll<T> <struct>::__poll__(Waker
|
|
1184
|
+
waker)`. Generator: `expected<T, StopIteration> <struct>::__next__()`
|
|
1185
|
+
(no waker)."""
|
|
1186
|
+
if self._is_generator_shape():
|
|
1187
|
+
return (f"{self._resumable_ret_type_cpp(func)} "
|
|
1188
|
+
f"{struct_name}::__next__()")
|
|
1189
|
+
return (f"{self._poll_ret_cpp(func)} {struct_name}::__poll__("
|
|
1190
|
+
f"::tpystd::coro::Waker waker)")
|
|
1191
|
+
|
|
1192
|
+
def _emit_resumable_body_prelude(self, out: "TextIO",
|
|
1193
|
+
has_suspensions: bool) -> None:
|
|
1194
|
+
"""Per-shape body prelude. Async silences the unused `waker`
|
|
1195
|
+
param when the frame has no suspensions; the generator shape has
|
|
1196
|
+
no waker param, so it emits nothing."""
|
|
1197
|
+
if self._is_generator_shape():
|
|
1198
|
+
return
|
|
1199
|
+
if not has_suspensions:
|
|
1200
|
+
out.write(f"{INDENT}(void)waker;\n")
|
|
1201
|
+
|
|
1202
|
+
def _emit_resumable_done_case(self, out: "TextIO", inner: str) -> None:
|
|
1203
|
+
"""The terminal `S_DONE` switch case. Async panics on a re-poll
|
|
1204
|
+
after Ready; the generator returns `StopIteration` (a repeat
|
|
1205
|
+
`__next__()` after exhaustion is well-defined in Python)."""
|
|
1206
|
+
if self._is_generator_shape():
|
|
1207
|
+
out.write(f"{inner}case S_DONE: return "
|
|
1208
|
+
f"::tpy::make_unexpected(::tpy::StopIteration{{}});\n")
|
|
1209
|
+
return
|
|
1210
|
+
out.write(f"{inner}case S_DONE: "
|
|
1211
|
+
f"::tpy::tpy_panic(\"poll after Ready\");\n")
|
|
1212
|
+
|
|
1213
|
+
@contextlib.contextmanager
|
|
1214
|
+
def _resumable_return_lowering(self, func: TpyFunction):
|
|
1215
|
+
"""Configure how a source-level `return` lowers inside the frame
|
|
1216
|
+
body, then restore (policy seam). Async: the statement emitter
|
|
1217
|
+
consults `ctx.in_async_coro_body` to rewrite `return v` into
|
|
1218
|
+
`__state = S_DONE; return Poll::ready(v)`. The generator shape
|
|
1219
|
+
lowers a bare `return` to the StopIteration/done path and rejects
|
|
1220
|
+
return-with-value."""
|
|
1221
|
+
if self._is_generator_shape():
|
|
1222
|
+
old_in_gen = self.ctx.in_generator_resumable_body
|
|
1223
|
+
old_done = self.ctx.generator_resumable_done_state
|
|
1224
|
+
old_yt = self.ctx.current_yield_type
|
|
1225
|
+
self.ctx.in_generator_resumable_body = True
|
|
1226
|
+
self.ctx.generator_resumable_done_state = "S_DONE"
|
|
1227
|
+
self.ctx.current_yield_type = func.generator_yield_type
|
|
1228
|
+
try:
|
|
1229
|
+
yield
|
|
1230
|
+
finally:
|
|
1231
|
+
self.ctx.in_generator_resumable_body = old_in_gen
|
|
1232
|
+
self.ctx.generator_resumable_done_state = old_done
|
|
1233
|
+
self.ctx.current_yield_type = old_yt
|
|
1234
|
+
return
|
|
1235
|
+
old_in_async = getattr(self.ctx, "in_async_coro_body", False)
|
|
1236
|
+
old_async_ret_cpp = getattr(self.ctx, "async_coro_return_cpp", None)
|
|
1237
|
+
old_async_done_label = getattr(self.ctx, "async_coro_done_state", None)
|
|
1238
|
+
self.ctx.in_async_coro_body = True
|
|
1239
|
+
self.ctx.async_coro_return_cpp = self._ret_cpp(func)
|
|
1240
|
+
self.ctx.async_coro_done_state = "S_DONE"
|
|
1241
|
+
self.ctx.current_return_type = func.return_type
|
|
1242
|
+
try:
|
|
1243
|
+
yield
|
|
1244
|
+
finally:
|
|
1245
|
+
self.ctx.in_async_coro_body = old_in_async
|
|
1246
|
+
self.ctx.async_coro_return_cpp = old_async_ret_cpp
|
|
1247
|
+
self.ctx.async_coro_done_state = old_async_done_label
|
|
1248
|
+
|
|
1249
|
+
@contextlib.contextmanager
|
|
1250
|
+
def _generator_finally_helper_scope(self):
|
|
1251
|
+
"""While emitting a `__finally_<n>()` helper body of a generator,
|
|
1252
|
+
`ctx.in_generator_finally_helper` makes `return` lower to
|
|
1253
|
+
`this->__finally_stop = true; return;` (void) instead of `goto __done`.
|
|
1254
|
+
No-op for the async shape (helpers there return Poll)."""
|
|
1255
|
+
old_in_helper = self.ctx.in_generator_finally_helper
|
|
1256
|
+
if self._is_generator_shape():
|
|
1257
|
+
self.ctx.in_generator_finally_helper = True
|
|
1258
|
+
try:
|
|
1259
|
+
yield
|
|
1260
|
+
finally:
|
|
1261
|
+
self.ctx.in_generator_finally_helper = old_in_helper
|
|
1262
|
+
|
|
1263
|
+
@contextlib.contextmanager
|
|
1264
|
+
def _generator_finally_stop_scope(self, has_finally_stop: bool):
|
|
1265
|
+
"""While emitting a generator state-machine body, expose whether any
|
|
1266
|
+
finally helper contains a `return` (so the helper call sites append the
|
|
1267
|
+
`if (this->__finally_stop) return StopIteration;` check), then restore."""
|
|
1268
|
+
old_has_finally_stop = self.ctx.generator_has_finally_stop
|
|
1269
|
+
self.ctx.generator_has_finally_stop = has_finally_stop
|
|
1270
|
+
try:
|
|
1271
|
+
yield
|
|
1272
|
+
finally:
|
|
1273
|
+
self.ctx.generator_has_finally_stop = old_has_finally_stop
|
|
1274
|
+
|
|
1275
|
+
def _emit_resumable_extra_state_fields(self, out: "TextIO") -> None:
|
|
1276
|
+
"""Per-shape state fields beyond the shared `int32_t __state`.
|
|
1277
|
+
Async adds the cancellation flag; the generator shape adds
|
|
1278
|
+
nothing."""
|
|
1279
|
+
if self._is_generator_shape():
|
|
1280
|
+
return
|
|
1281
|
+
out.write(f"{INDENT}bool __cancel_pending;\n")
|
|
1282
|
+
|
|
1283
|
+
def _resumable_extra_ctor_inits(self) -> list[str]:
|
|
1284
|
+
"""Per-shape ctor member-init entries beyond `__state(S_INITIAL)`.
|
|
1285
|
+
Async initializes the cancel flag; the generator shape adds none.
|
|
1286
|
+
"""
|
|
1287
|
+
if self._is_generator_shape():
|
|
1288
|
+
return []
|
|
1289
|
+
return ["__cancel_pending(false)"]
|
|
1290
|
+
|
|
1291
|
+
def _emit_resumable_sub_future_fields(
|
|
1292
|
+
self, out: "TextIO", func: TpyFunction,
|
|
1293
|
+
yields: 'list[rcfg.Yield]') -> None:
|
|
1294
|
+
"""Sub-future frame fields -- async-only (policy seam). One per
|
|
1295
|
+
Yield (suspension_index = field ordinal): Inline ->
|
|
1296
|
+
optional<sub-coro struct>, Erased -> optional<value awaitable>,
|
|
1297
|
+
Borrowed -> raw pointer. Async-with's synthetic yields override
|
|
1298
|
+
sub_field_cpp_type via the state.async_with_struct_names map (the CM's
|
|
1299
|
+
__aenter__/__aexit__ coro struct, computed at prescan time). The
|
|
1300
|
+
generator shape stores nothing at a `yield`, so it emits nothing.
|
|
1301
|
+
"""
|
|
1302
|
+
if self._is_generator_shape():
|
|
1303
|
+
return
|
|
1304
|
+
state = rcfg.resumable_state(func)
|
|
1305
|
+
struct_names = state.async_with_struct_names
|
|
1306
|
+
for_struct_names = state.async_for_struct_names
|
|
1307
|
+
for y in yields:
|
|
1308
|
+
p = y.payload
|
|
1309
|
+
sub_cpp = p.sub_field_cpp_type
|
|
1310
|
+
if p.async_with_kind is not None and p.async_with_ctx_n is not None:
|
|
1311
|
+
entry = struct_names.get(p.async_with_ctx_n)
|
|
1312
|
+
if entry is not None:
|
|
1313
|
+
sub_cpp = entry[0] if p.async_with_kind is rcfg.AsyncWithKind.AENTER else entry[1]
|
|
1314
|
+
elif p.async_for_uid is not None:
|
|
1315
|
+
entry = for_struct_names.get(p.async_for_uid)
|
|
1316
|
+
if entry is not None:
|
|
1317
|
+
sub_cpp = entry
|
|
1318
|
+
if p.mode is rcfg.AwaitMode.BORROWED:
|
|
1319
|
+
out.write(f"{INDENT}{sub_cpp}* "
|
|
1320
|
+
f"__sub_{y.suspension_index} = nullptr;\n")
|
|
1321
|
+
else:
|
|
1322
|
+
out.write(f"{INDENT}std::optional<{sub_cpp}> "
|
|
1323
|
+
f"__sub_{y.suspension_index};\n")
|
|
1324
|
+
|
|
1325
|
+
def _resumable_body_method_fwd_decl(self, func: TpyFunction) -> str:
|
|
1326
|
+
"""In-struct forward declaration of the body method (unqualified).
|
|
1327
|
+
Async: `Poll<T> __poll__(Waker waker)`. Generator:
|
|
1328
|
+
`expected<T, StopIteration> __next__()`."""
|
|
1329
|
+
if self._is_generator_shape():
|
|
1330
|
+
return f"{self._resumable_ret_type_cpp(func)} __next__()"
|
|
1331
|
+
return (f"{self._poll_ret_cpp(func)} "
|
|
1332
|
+
f"__poll__(::tpystd::coro::Waker waker)")
|
|
1333
|
+
|
|
1334
|
+
def _emit_resumable_extra_methods(self, out: "TextIO",
|
|
1335
|
+
struct_name: str) -> None:
|
|
1336
|
+
"""Per-shape inline methods on the frame struct. Async emits
|
|
1337
|
+
`cancel()`; the generator emits `__iter__()` (returns `*this`, so
|
|
1338
|
+
the frame is its own iterator -- parallels gen_generators)."""
|
|
1339
|
+
if self._is_generator_shape():
|
|
1340
|
+
out.write(f"{INDENT}{struct_name}& __iter__() "
|
|
1341
|
+
f"{{ return *this; }}\n")
|
|
1342
|
+
return
|
|
1343
|
+
out.write(f"{INDENT}void cancel() {{ __cancel_pending = true; }}\n")
|
|
1344
|
+
|
|
1345
|
+
def gen_coro_poll_def(self, out: "TextIO", func: TpyFunction,
|
|
1346
|
+
record_name: str | None = None) -> None:
|
|
1347
|
+
"""Emit the `poll()` method body in the .cpp file (or inline-in-hpp
|
|
1348
|
+
for templates -- the caller handles placement). When `record_name`
|
|
1349
|
+
is given, the struct is `__coro_<Record>_<func>` and the body
|
|
1350
|
+
sees `self.X` as `__self.X` (parallels generator methods).
|
|
1351
|
+
"""
|
|
1352
|
+
struct_name = self._struct_name_templated(func, record_name)
|
|
1353
|
+
cfg = self._build_resumable_cfg(func)
|
|
1354
|
+
has_yields = bool(cfg.yield_sites)
|
|
1355
|
+
|
|
1356
|
+
self.ctx.emit_source_comment(out, func.loc)
|
|
1357
|
+
self._emit_template_header(out, func, record_name=record_name)
|
|
1358
|
+
out.write(f"{self._resumable_body_method_decl(func, struct_name)} {{\n")
|
|
1359
|
+
self._emit_resumable_body_prelude(out, has_yields)
|
|
1360
|
+
|
|
1361
|
+
# Return-rewrite setup (the C++ Poll<T> type and DONE state
|
|
1362
|
+
# label so `return v` lowers correctly inside the state machine)
|
|
1363
|
+
# is the resumable-shape policy's responsibility.
|
|
1364
|
+
has_finally_stop = (
|
|
1365
|
+
self._is_generator_shape()
|
|
1366
|
+
and any(_stmts_have_return(body) for _, body in cfg.finally_helpers))
|
|
1367
|
+
with self._generator_finally_stop_scope(has_finally_stop):
|
|
1368
|
+
with self._resumable_frame_ctx(func, record_name):
|
|
1369
|
+
with self._resumable_return_lowering(func):
|
|
1370
|
+
self._emit_state_machine(out, func, cfg)
|
|
1371
|
+
|
|
1372
|
+
out.write(f"}}\n")
|
|
1373
|
+
|
|
1374
|
+
# =====================================================================
|
|
1375
|
+
# CFG-based state-machine emitter (replaces _emit_switch_body).
|
|
1376
|
+
# =====================================================================
|
|
1377
|
+
|
|
1378
|
+
def _build_resumable_cfg(self, func: TpyFunction) -> 'rcfg.CFG':
|
|
1379
|
+
"""Apply the await-lift pre-pass, then build the CFG. The CFG
|
|
1380
|
+
builder handles any wrapping try/finally uniformly with all
|
|
1381
|
+
other compound statements -- no special unwrap-and-rewrap pass
|
|
1382
|
+
is needed.
|
|
1383
|
+
|
|
1384
|
+
A `_CFGNotYetSupported` (a shape the resumable lowering can't yet
|
|
1385
|
+
handle) is turned into a `CodeGenError` at the offending location.
|
|
1386
|
+
This is the authoritative backstop so no unsupported shape silently
|
|
1387
|
+
miscompiles. The result is cached on the func for the emit pass."""
|
|
1388
|
+
state = rcfg.resumable_state(func)
|
|
1389
|
+
if state.cfg is not None:
|
|
1390
|
+
return state.cfg
|
|
1391
|
+
body = self._effective_body(func)
|
|
1392
|
+
try:
|
|
1393
|
+
for_uid_map = self._prescan_resumable_for_loops(func, body)
|
|
1394
|
+
with_uid_map = self._prescan_with_stmts(func, body)
|
|
1395
|
+
try_finally_uid_map = self._prescan_resumable_try_finally(func, body)
|
|
1396
|
+
builder = rcfg.CFGBuilder(
|
|
1397
|
+
payload_factory=self._make_await_payload,
|
|
1398
|
+
for_uid_map=for_uid_map,
|
|
1399
|
+
with_uid_map=with_uid_map,
|
|
1400
|
+
try_finally_uid_map=try_finally_uid_map,
|
|
1401
|
+
func_returns_void=self._is_void_return(func),
|
|
1402
|
+
)
|
|
1403
|
+
cfg = builder.build(body)
|
|
1404
|
+
except rcfg._CFGNotYetSupported as e:
|
|
1405
|
+
raise CodeGenError(e.msg, loc=e.loc)
|
|
1406
|
+
# Stash the builder so callers (emit) can look up handler
|
|
1407
|
+
# entries via builder.get_handler_entry().
|
|
1408
|
+
state.cfg_builder = builder
|
|
1409
|
+
state.cfg = cfg
|
|
1410
|
+
return cfg
|
|
1411
|
+
|
|
1412
|
+
def _prescan_resumable_for_loops(
|
|
1413
|
+
self, func: TpyFunction,
|
|
1414
|
+
body: list[TpyStmt]) -> dict[int, int]:
|
|
1415
|
+
"""Find for-loops whose body contains await OR are `async for`
|
|
1416
|
+
(M6). For each, allocate a uid, register frame-field declarations
|
|
1417
|
+
and register the loop variable as a hoisted local so it persists
|
|
1418
|
+
across suspensions.
|
|
1419
|
+
|
|
1420
|
+
Sync for-with-await (M3.1): two slots,
|
|
1421
|
+
`(__for_itr_N: decltype(::tpy::__iter__(it)),
|
|
1422
|
+
__for_r_N: decltype(itr.__next__()))`.
|
|
1423
|
+
|
|
1424
|
+
Async-for (M6): one slot,
|
|
1425
|
+
`__for_itr_N: decltype(it.__aiter__())`. No `__for_r_N` slot --
|
|
1426
|
+
the advance is a Yield(await __anext__()) and the unwrapped
|
|
1427
|
+
value goes straight to the loop var via the standard resume-bind
|
|
1428
|
+
path. Also populates `state.async_for_struct_names[uid]` with
|
|
1429
|
+
the C++ name of the `__anext__` sub-coro struct so the Yield
|
|
1430
|
+
emit can size `__sub_<i>` and emplace it.
|
|
1431
|
+
|
|
1432
|
+
Returns {id(TpyForEach) -> uid} for the CFG builder. Frame fields
|
|
1433
|
+
live on `state.for_fields` as `[(name, cpp_type)]` consumed
|
|
1434
|
+
by gen_coro_struct (see `ResumableFuncState`).
|
|
1435
|
+
"""
|
|
1436
|
+
state = rcfg.resumable_state(func)
|
|
1437
|
+
if state.for_prescanned:
|
|
1438
|
+
return state.for_uid_map
|
|
1439
|
+
uid_map: dict[int, int] = {}
|
|
1440
|
+
fields_out: list[tuple[str, str]] = []
|
|
1441
|
+
struct_names_out: dict[int, str] = {}
|
|
1442
|
+
# {id(TpyForEach) -> GeneratorForInfo}: carries pointer_form_loop_var
|
|
1443
|
+
# so `setup_resumable_frame_locals` + the struct field emit render a
|
|
1444
|
+
# non-value loop var as an aliasing `T*` rather than a `frame_slot<T>`
|
|
1445
|
+
# value copy.
|
|
1446
|
+
for_loop_info: dict[int, GeneratorForInfo] = {}
|
|
1447
|
+
info_by_uid: dict[int, GeneratorForInfo] = {}
|
|
1448
|
+
counter = [0]
|
|
1449
|
+
# Loop-var hoists are accumulated here and applied to
|
|
1450
|
+
# func.generator_locals only after the walk completes -- if the walk
|
|
1451
|
+
# raises `_CFGNotYetSupported` mid-way (an unsupported for-loop
|
|
1452
|
+
# shape) the func falls back to the legacy emitter, which must not
|
|
1453
|
+
# inherit partially-appended loop-var entries (they would emit as
|
|
1454
|
+
# duplicate frame fields alongside the legacy for-loop fields).
|
|
1455
|
+
local_hoists: list[tuple[str, "TpyType"]] = []
|
|
1456
|
+
|
|
1457
|
+
def walk(stmts: list[TpyStmt]) -> None:
|
|
1458
|
+
for s in stmts:
|
|
1459
|
+
if isinstance(s, TpyForEach) and (
|
|
1460
|
+
s.is_async
|
|
1461
|
+
or rcfg._stmts_have_any_suspension(s.body)
|
|
1462
|
+
or rcfg._stmts_have_any_suspension(s.orelse)):
|
|
1463
|
+
cur_uid = counter[0]
|
|
1464
|
+
counter[0] += 1
|
|
1465
|
+
uid_map[id(s)] = cur_uid
|
|
1466
|
+
if s.is_async:
|
|
1467
|
+
# async-for (M6): one `__aiter__` iterator slot; the
|
|
1468
|
+
# advance is a Yield(await __anext__), not a strategy.
|
|
1469
|
+
iter_t = self.types.get_resolved_type(s.iterable)
|
|
1470
|
+
if iter_t is None:
|
|
1471
|
+
raise CodeGenError(
|
|
1472
|
+
"for-loop iterable has no resolved type "
|
|
1473
|
+
"(async for pre-scan)", loc=s.loc)
|
|
1474
|
+
src_cpp = self.types.type_to_cpp(unwrap_ref_type(iter_t))
|
|
1475
|
+
iter_field_type = (
|
|
1476
|
+
f"std::decay_t<decltype(std::declval<"
|
|
1477
|
+
f"{src_cpp}&>().__aiter__())>")
|
|
1478
|
+
fields_out.append(
|
|
1479
|
+
(f"__for_itr_{cur_uid}", iter_field_type))
|
|
1480
|
+
if s.async_aiter_type is None:
|
|
1481
|
+
raise CodeGenError(
|
|
1482
|
+
"async-for missing resolved aiter type "
|
|
1483
|
+
"(internal: sema didn't populate "
|
|
1484
|
+
"stmt.async_aiter_type)", loc=s.loc)
|
|
1485
|
+
struct_names_out[cur_uid] = self._sub_struct_qualname(
|
|
1486
|
+
s.async_aiter_type, "__anext__")
|
|
1487
|
+
info = GeneratorForInfo(
|
|
1488
|
+
uid=cur_uid, strategy="async_for", fields=[])
|
|
1489
|
+
else:
|
|
1490
|
+
# Sync for-loop: reuse the legacy strategy analysis so
|
|
1491
|
+
# range / begin_end peepholes (a plain counter / begin-
|
|
1492
|
+
# end iterators) carry over instead of the slower
|
|
1493
|
+
# universal `__iter__`/`__next__` shape. The fields it
|
|
1494
|
+
# returns are emitted as `tpy::frame_slot<T>` in
|
|
1495
|
+
# gen_coro_struct (matching hoisted user locals).
|
|
1496
|
+
info = self.gen_generators._analyze_for_strategy(
|
|
1497
|
+
s, cur_uid)
|
|
1498
|
+
if info is None:
|
|
1499
|
+
raise rcfg._CFGNotYetSupported(
|
|
1500
|
+
"yield inside this for-loop shape is not yet "
|
|
1501
|
+
"supported on the resumable path.", loc=s.loc)
|
|
1502
|
+
fields_out.extend(info.fields)
|
|
1503
|
+
for_loop_info[id(s)] = info
|
|
1504
|
+
info_by_uid[cur_uid] = info
|
|
1505
|
+
elem_t = (unwrap_ref_type(s.elem_type)
|
|
1506
|
+
if s.elem_type else None)
|
|
1507
|
+
if elem_t is None:
|
|
1508
|
+
raise CodeGenError(
|
|
1509
|
+
"for-with-suspension: loop var has no "
|
|
1510
|
+
"resolved elem_type", loc=s.loc)
|
|
1511
|
+
local_hoists.append((s.var, elem_t))
|
|
1512
|
+
if hasattr(s, "sub_bodies"):
|
|
1513
|
+
for b in s.sub_bodies():
|
|
1514
|
+
walk(b)
|
|
1515
|
+
|
|
1516
|
+
walk(body)
|
|
1517
|
+
# Walk succeeded: commit the loop-var hoists.
|
|
1518
|
+
if local_hoists:
|
|
1519
|
+
if func.generator_locals is None:
|
|
1520
|
+
func.generator_locals = []
|
|
1521
|
+
existing = {n for n, _ in func.generator_locals}
|
|
1522
|
+
for name, elem_t in local_hoists:
|
|
1523
|
+
if name not in existing:
|
|
1524
|
+
func.generator_locals.append((name, elem_t))
|
|
1525
|
+
existing.add(name)
|
|
1526
|
+
state.for_uid_map = uid_map
|
|
1527
|
+
state.for_fields = fields_out
|
|
1528
|
+
state.async_for_struct_names = struct_names_out
|
|
1529
|
+
state.for_loop_info = for_loop_info
|
|
1530
|
+
state.for_info_by_uid = info_by_uid
|
|
1531
|
+
state.for_prescanned = True
|
|
1532
|
+
return uid_map
|
|
1533
|
+
|
|
1534
|
+
def _sub_struct_qualname(
|
|
1535
|
+
self, owner: 'NominalType | None', method: str,
|
|
1536
|
+
inferred_type_args: 'tuple[TpyType, ...] | None' = None,
|
|
1537
|
+
*, module_qual: str | None = None,
|
|
1538
|
+
extra_template_args: 'list[str] | None' = None) -> str:
|
|
1539
|
+
"""Build the C++ name of the sub-coro struct generated for a
|
|
1540
|
+
statically-resolved await (free function or method).
|
|
1541
|
+
|
|
1542
|
+
Free function (owner=None, same module): `__coro_<name>[<inferred_args>]`.
|
|
1543
|
+
Free function (owner=None, cross-module via module_qual):
|
|
1544
|
+
`<callee_ns>::__coro_<name>[<inferred_args>]`.
|
|
1545
|
+
Method on non-generic class: `<ns>::__coro_<Record>_<name>
|
|
1546
|
+
[<inferred_args>]`.
|
|
1547
|
+
Method on generic class: `<ns>::__coro_<Record>_<name>
|
|
1548
|
+
<owner_type_args>`. (The class-generic + method-generic
|
|
1549
|
+
case is currently rejected at sema, so owner_type_args and
|
|
1550
|
+
inferred_type_args are not composed today.)
|
|
1551
|
+
|
|
1552
|
+
`extra_template_args` appends concrete C++ types (typically
|
|
1553
|
+
`std::remove_cvref_t<decltype(arg)>`) for each static-protocol
|
|
1554
|
+
param on the callee -- these correspond to the `T_<pname>`
|
|
1555
|
+
template args declared on the callee's struct.
|
|
1556
|
+
"""
|
|
1557
|
+
ns_qual = ""
|
|
1558
|
+
owner_name = None
|
|
1559
|
+
owner_args_suffix = ""
|
|
1560
|
+
if owner is not None:
|
|
1561
|
+
owner_cpp = self.types.type_to_cpp(owner)
|
|
1562
|
+
ns_prefix = owner_cpp.split("<", 1)[0]
|
|
1563
|
+
ns_qual = (ns_prefix.rsplit("::", 1)[0] + "::"
|
|
1564
|
+
if "::" in ns_prefix else "")
|
|
1565
|
+
owner_name = owner.name
|
|
1566
|
+
if owner.type_args:
|
|
1567
|
+
inner_cpps = [self.types.type_to_cpp(ta)
|
|
1568
|
+
for ta in owner.type_args]
|
|
1569
|
+
owner_args_suffix = "<" + ", ".join(inner_cpps) + ">"
|
|
1570
|
+
elif module_qual is not None:
|
|
1571
|
+
# Cross-module free-function await: qualify with the
|
|
1572
|
+
# callee module's C++ namespace.
|
|
1573
|
+
ns_qual = f"::{module_to_cpp_namespace(module_qual)}::"
|
|
1574
|
+
bare = AsyncCoroCodegen._sub_struct_name(method, owner_name)
|
|
1575
|
+
# Combined template-arg list: callee's explicit `[T1, ...]` from
|
|
1576
|
+
# the call's inferred substitution, followed by any
|
|
1577
|
+
# `T_<pname>` extras deduced from static-protocol args.
|
|
1578
|
+
all_args: list[str] = []
|
|
1579
|
+
if inferred_type_args and not (owner is not None and owner.type_args):
|
|
1580
|
+
all_args.extend(self.types.type_to_cpp(ta)
|
|
1581
|
+
for ta in inferred_type_args)
|
|
1582
|
+
if extra_template_args:
|
|
1583
|
+
all_args.extend(extra_template_args)
|
|
1584
|
+
suffix = ("<" + ", ".join(all_args) + ">") if all_args else ""
|
|
1585
|
+
return f"{ns_qual}{bare}{owner_args_suffix}{suffix}"
|
|
1586
|
+
|
|
1587
|
+
def _extra_template_args_for_await(self, call: TpyExpr) -> list[str]:
|
|
1588
|
+
"""Compute the per-static-protocol `T_<pname>` template-arg
|
|
1589
|
+
spellings for the sub-coro struct of an inline await.
|
|
1590
|
+
|
|
1591
|
+
Each callee static-protocol param gets one extra template arg
|
|
1592
|
+
on the struct (see `_protocol_template_parts`); at the call
|
|
1593
|
+
site that arg is the concrete type of the corresponding
|
|
1594
|
+
argument, spelled as `std::remove_cvref_t<decltype(<arg>)>` so
|
|
1595
|
+
the compiler deduces it without us having to spell it.
|
|
1596
|
+
Temps queued by gen_expr are discarded -- decltype doesn't
|
|
1597
|
+
evaluate, and the same arg's gen_expr will re-run at emplace
|
|
1598
|
+
time when the temps are actually needed.
|
|
1599
|
+
"""
|
|
1600
|
+
if not isinstance(call, (TpyCall, TpyMethodCall)):
|
|
1601
|
+
return []
|
|
1602
|
+
fi = call.resolved_function_info
|
|
1603
|
+
if fi is None or not fi.params:
|
|
1604
|
+
return []
|
|
1605
|
+
if not any(
|
|
1606
|
+
self.functions.protocols.is_static_protocol_param(p.type)
|
|
1607
|
+
for p in fi.params):
|
|
1608
|
+
return []
|
|
1609
|
+
out: list[str] = []
|
|
1610
|
+
checkpoint = self.ctx.temps.checkpoint()
|
|
1611
|
+
for i, pinfo in enumerate(fi.params):
|
|
1612
|
+
if not self.functions.protocols.is_static_protocol_param(pinfo.type):
|
|
1613
|
+
continue
|
|
1614
|
+
if i >= len(call.args):
|
|
1615
|
+
# Defensive: callee param without a corresponding arg
|
|
1616
|
+
# at the call site (defaults aren't supported on async
|
|
1617
|
+
# static-protocol params today; bail rather than emit
|
|
1618
|
+
# a malformed template arg).
|
|
1619
|
+
self.ctx.temps.rollback_to(checkpoint)
|
|
1620
|
+
return []
|
|
1621
|
+
arg_cpp = self.expressions.gen_expr(call.args[i])
|
|
1622
|
+
out.append(f"std::remove_cvref_t<decltype({arg_cpp})>")
|
|
1623
|
+
self.ctx.temps.rollback_to(checkpoint)
|
|
1624
|
+
return out
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
def _prescan_with_stmts(
|
|
1628
|
+
self, func: TpyFunction,
|
|
1629
|
+
body: list[TpyStmt]) -> dict[int, list[int]]:
|
|
1630
|
+
"""Find `with` stmts whose body contains await. For each, allocate
|
|
1631
|
+
a ctx_n per WithItem, declare a `__with_ctx_<n>` frame field of
|
|
1632
|
+
the context-manager's C++ type, and register any `as` target
|
|
1633
|
+
names as hoisted locals.
|
|
1634
|
+
|
|
1635
|
+
Returns {id(TpyWith) -> [ctx_n_per_item]}; frame fields land on
|
|
1636
|
+
`state.with_fields` as `[(name, cpp_type)]`.
|
|
1637
|
+
"""
|
|
1638
|
+
state = rcfg.resumable_state(func)
|
|
1639
|
+
if state.with_prescanned:
|
|
1640
|
+
return state.with_uid_map
|
|
1641
|
+
uid_map: dict[int, list[int]] = {}
|
|
1642
|
+
fields_out: list[tuple[str, str]] = []
|
|
1643
|
+
counter = [0]
|
|
1644
|
+
|
|
1645
|
+
# Async-with sub-coro struct names by ctx_n. Populated alongside
|
|
1646
|
+
# the per-ctx frame field. Emit uses this to size __sub_<i>
|
|
1647
|
+
# slots and to write the inline factory call.
|
|
1648
|
+
struct_names_out: dict[int, tuple[str, str]] = {}
|
|
1649
|
+
|
|
1650
|
+
def walk(stmts: list[TpyStmt]) -> None:
|
|
1651
|
+
for s in stmts:
|
|
1652
|
+
# Sync `with` only needs the frame slot when its body
|
|
1653
|
+
# contains an await (M3.2). `async with` always needs
|
|
1654
|
+
# one because `__aenter__` / `__aexit__` are themselves
|
|
1655
|
+
# the suspensions, regardless of whether the body has
|
|
1656
|
+
# any other await.
|
|
1657
|
+
if (isinstance(s, TpyWith)
|
|
1658
|
+
and (s.is_async
|
|
1659
|
+
or rcfg._stmts_have_any_suspension(s.body))):
|
|
1660
|
+
per_item: list[int] = []
|
|
1661
|
+
for item in s.items:
|
|
1662
|
+
cur_n = counter[0]
|
|
1663
|
+
counter[0] += 1
|
|
1664
|
+
per_item.append(cur_n)
|
|
1665
|
+
ctx_t = self.types.get_resolved_type(item.context_expr)
|
|
1666
|
+
if ctx_t is None:
|
|
1667
|
+
raise CodeGenError(
|
|
1668
|
+
"with-stmt context manager has no resolved "
|
|
1669
|
+
"type (async with-with-await pre-scan)",
|
|
1670
|
+
loc=s.loc)
|
|
1671
|
+
ctx_cpp = self.types.type_to_cpp(unwrap_ref_type(ctx_t))
|
|
1672
|
+
fields_out.append((f"__with_ctx_{cur_n}", ctx_cpp))
|
|
1673
|
+
# For async-with, compute the qualified C++ names
|
|
1674
|
+
# of the __aenter__ / __aexit__ coro structs so
|
|
1675
|
+
# struct-emit and _emit_suspend can look them up
|
|
1676
|
+
# by ctx_n without needing the types helper at
|
|
1677
|
+
# the CFG layer.
|
|
1678
|
+
if s.is_async:
|
|
1679
|
+
ctx_inner = unwrap_ref_type(ctx_t)
|
|
1680
|
+
if not isinstance(ctx_inner, NominalType):
|
|
1681
|
+
raise CodeGenError(
|
|
1682
|
+
"async-with context manager type is "
|
|
1683
|
+
"not a record (internal)",
|
|
1684
|
+
loc=s.loc)
|
|
1685
|
+
# Build the aenter/aexit sub-coro struct
|
|
1686
|
+
# names using the canonical struct namer
|
|
1687
|
+
# (same shape as M4 async-method coro
|
|
1688
|
+
# structs). The C++ namespace is derived
|
|
1689
|
+
# from the CM's qualified type by
|
|
1690
|
+
# stripping any template args first (so
|
|
1691
|
+
# `::ns::Foo<T>` yields a `::ns::` prefix,
|
|
1692
|
+
# not `::ns::Foo<T>::`). Type args are
|
|
1693
|
+
# re-attached as a suffix on the coro
|
|
1694
|
+
# struct reference -- the struct itself is
|
|
1695
|
+
# templated over the same T as the CM.
|
|
1696
|
+
# Generic CMs are rejected at sema today (see
|
|
1697
|
+
# registration.py); the helper handles type_args
|
|
1698
|
+
# so this prescan stays robust if that lifts.
|
|
1699
|
+
struct_names_out[cur_n] = (
|
|
1700
|
+
self._sub_struct_qualname(ctx_inner, "__aenter__"),
|
|
1701
|
+
self._sub_struct_qualname(ctx_inner, "__aexit__"),
|
|
1702
|
+
)
|
|
1703
|
+
if item.target is not None:
|
|
1704
|
+
enter_t = (unwrap_ref_type(item.enter_type)
|
|
1705
|
+
if item.enter_type else None)
|
|
1706
|
+
if enter_t is None:
|
|
1707
|
+
raise CodeGenError(
|
|
1708
|
+
"with-stmt target has no resolved "
|
|
1709
|
+
"enter_type", loc=s.loc)
|
|
1710
|
+
if func.generator_locals is None:
|
|
1711
|
+
func.generator_locals = []
|
|
1712
|
+
if not any(n == item.target
|
|
1713
|
+
for n, _ in func.generator_locals):
|
|
1714
|
+
func.generator_locals.append(
|
|
1715
|
+
(item.target, enter_t))
|
|
1716
|
+
# For str-typed as-targets, the C++
|
|
1717
|
+
# `__enter__()` / `__aenter__()` returns
|
|
1718
|
+
# `std::string` by value but sema's `str`
|
|
1719
|
+
# lowers to `std::string_view`. Tracking
|
|
1720
|
+
# the name here promotes the frame field's
|
|
1721
|
+
# storage type to `std::string` (owning)
|
|
1722
|
+
# so the field doesn't alias a temporary
|
|
1723
|
+
# that dies at the assignment's semicolon.
|
|
1724
|
+
# Applies to both sync `with` and async
|
|
1725
|
+
# `with`: the Poll<std::string>::value()
|
|
1726
|
+
# extraction in async-with's resume case
|
|
1727
|
+
# produces the same temporary shape as the
|
|
1728
|
+
# sync __enter__() return.
|
|
1729
|
+
resolved_enter_t = self.types.resolve_type(enter_t)
|
|
1730
|
+
if is_str_category(resolved_enter_t):
|
|
1731
|
+
state.with_owning_str_targets.add(item.target)
|
|
1732
|
+
uid_map[id(s)] = per_item
|
|
1733
|
+
if hasattr(s, "sub_bodies"):
|
|
1734
|
+
for b in s.sub_bodies():
|
|
1735
|
+
walk(b)
|
|
1736
|
+
|
|
1737
|
+
walk(body)
|
|
1738
|
+
state.with_uid_map = uid_map
|
|
1739
|
+
state.with_fields = fields_out
|
|
1740
|
+
state.async_with_struct_names = struct_names_out
|
|
1741
|
+
state.with_prescanned = True
|
|
1742
|
+
return uid_map
|
|
1743
|
+
|
|
1744
|
+
def _prescan_resumable_try_finally(
|
|
1745
|
+
self, func: TpyFunction,
|
|
1746
|
+
body: list[TpyStmt]) -> dict[int, int]:
|
|
1747
|
+
"""Find try-stmts whose finally body contains an await. For
|
|
1748
|
+
each, allocate a uid and declare:
|
|
1749
|
+
* `__finally_exc_<n>` -- `std::exception_ptr` (always).
|
|
1750
|
+
* `__finally_pending_<n>` -- `bool` (only when try / handler /
|
|
1751
|
+
finally body contains a reachable `return`).
|
|
1752
|
+
* `__finally_ret_<n>` -- function's return type, for the
|
|
1753
|
+
same reason and only when the async def is non-void.
|
|
1754
|
+
Returns {id(TpyTry) -> uid}; field declarations land on
|
|
1755
|
+
`state.try_finally_fields`."""
|
|
1756
|
+
state = rcfg.resumable_state(func)
|
|
1757
|
+
if state.try_finally_prescanned:
|
|
1758
|
+
return state.try_finally_uid_map
|
|
1759
|
+
uid_map: dict[int, int] = {}
|
|
1760
|
+
fields_out: list[tuple[str, str]] = []
|
|
1761
|
+
counter = [0]
|
|
1762
|
+
is_void = self._is_void_return(func)
|
|
1763
|
+
ret_cpp = self._ret_cpp(func) if not is_void else None
|
|
1764
|
+
|
|
1765
|
+
def walk(stmts: list[TpyStmt]) -> None:
|
|
1766
|
+
for s in stmts:
|
|
1767
|
+
if (isinstance(s, TpyTry)
|
|
1768
|
+
and s.finally_body
|
|
1769
|
+
and rcfg._stmts_have_any_suspension(s.finally_body)):
|
|
1770
|
+
cur_uid = counter[0]
|
|
1771
|
+
counter[0] += 1
|
|
1772
|
+
uid_map[id(s)] = cur_uid
|
|
1773
|
+
fields_out.append(
|
|
1774
|
+
(f"__finally_exc_{cur_uid}", "std::exception_ptr"))
|
|
1775
|
+
has_return = (rcfg._stmts_have_any_return(s.try_body)
|
|
1776
|
+
or any(rcfg._stmts_have_any_return(h.body)
|
|
1777
|
+
for h in s.handlers)
|
|
1778
|
+
or rcfg._stmts_have_any_return(s.finally_body))
|
|
1779
|
+
if has_return:
|
|
1780
|
+
fields_out.append(
|
|
1781
|
+
(f"__finally_pending_{cur_uid}", "bool"))
|
|
1782
|
+
# Generator pending returns are always StopIteration;
|
|
1783
|
+
# no value slot needed (_ret_cpp would give Iterator[T]).
|
|
1784
|
+
if ret_cpp is not None and not self._is_generator_shape():
|
|
1785
|
+
fields_out.append(
|
|
1786
|
+
(f"__finally_ret_{cur_uid}", ret_cpp))
|
|
1787
|
+
# `async with` desugars to a try/finally where the
|
|
1788
|
+
# finally body is `await __cm.__aexit__(...)`. The
|
|
1789
|
+
# synthetic finally needs the same set of frame slots
|
|
1790
|
+
# as a user-written try/finally-with-await; allocate
|
|
1791
|
+
# them in the same shared uid pool here.
|
|
1792
|
+
if isinstance(s, TpyWith) and s.is_async:
|
|
1793
|
+
cur_uid = counter[0]
|
|
1794
|
+
counter[0] += 1
|
|
1795
|
+
uid_map[id(s)] = cur_uid
|
|
1796
|
+
fields_out.append(
|
|
1797
|
+
(f"__finally_exc_{cur_uid}", "std::exception_ptr"))
|
|
1798
|
+
if rcfg._stmts_have_any_return(s.body):
|
|
1799
|
+
fields_out.append(
|
|
1800
|
+
(f"__finally_pending_{cur_uid}", "bool"))
|
|
1801
|
+
if ret_cpp is not None:
|
|
1802
|
+
fields_out.append(
|
|
1803
|
+
(f"__finally_ret_{cur_uid}", ret_cpp))
|
|
1804
|
+
if hasattr(s, "sub_bodies"):
|
|
1805
|
+
for b in s.sub_bodies():
|
|
1806
|
+
walk(b)
|
|
1807
|
+
|
|
1808
|
+
walk(body)
|
|
1809
|
+
state.try_finally_uid_map = uid_map
|
|
1810
|
+
state.try_finally_fields = fields_out
|
|
1811
|
+
state.try_finally_prescanned = True
|
|
1812
|
+
return uid_map
|
|
1813
|
+
|
|
1814
|
+
def _make_await_payload(self, await_node: TpyAwait, host_stmt: TpyStmt,
|
|
1815
|
+
kind, bind_target, return_stmt) -> 'rcfg.AwaitPayload':
|
|
1816
|
+
"""CFGBuilder payload factory: derives mode + sub_field_cpp_type
|
|
1817
|
+
from the await's sema annotations."""
|
|
1818
|
+
if await_node.awaited_async_func_name is not None:
|
|
1819
|
+
mode = rcfg.AwaitMode.INLINE
|
|
1820
|
+
inferred_type_args = getattr(
|
|
1821
|
+
await_node.value, "inferred_type_args", None)
|
|
1822
|
+
# Cross-module free-function await spelled `mod.func(...)`:
|
|
1823
|
+
# operand is a TpyMethodCall whose receiver is the module
|
|
1824
|
+
# (no class owner). Use the module qualifier to namespace
|
|
1825
|
+
# the sub-coro struct name.
|
|
1826
|
+
module_qual = None
|
|
1827
|
+
if (isinstance(await_node.value, TpyMethodCall)
|
|
1828
|
+
and await_node.awaited_method_owner_type is None):
|
|
1829
|
+
module_qual = (await_node.value.user_module_call
|
|
1830
|
+
or await_node.value.builtin_module_call)
|
|
1831
|
+
extra_template_args = self._extra_template_args_for_await(
|
|
1832
|
+
await_node.value)
|
|
1833
|
+
sub_cpp = self._sub_struct_qualname(
|
|
1834
|
+
await_node.awaited_method_owner_type,
|
|
1835
|
+
await_node.awaited_async_func_name,
|
|
1836
|
+
inferred_type_args,
|
|
1837
|
+
module_qual=module_qual,
|
|
1838
|
+
extra_template_args=extra_template_args)
|
|
1839
|
+
elif await_node.awaited_task_inner is not None:
|
|
1840
|
+
operand_type = self.ctx.get_expr_type(await_node.value)
|
|
1841
|
+
if operand_type is None:
|
|
1842
|
+
raise CodeGenError(
|
|
1843
|
+
"await operand has no analyzed type",
|
|
1844
|
+
loc=host_stmt.loc)
|
|
1845
|
+
operand_inner = unwrap_own(unwrap_ref_type(operand_type))
|
|
1846
|
+
sub_cpp = self.types.type_to_cpp(operand_inner)
|
|
1847
|
+
if operand_inner.is_value_type():
|
|
1848
|
+
mode = rcfg.AwaitMode.ERASED
|
|
1849
|
+
elif is_stable_address_lvalue(await_node.value):
|
|
1850
|
+
mode = rcfg.AwaitMode.BORROWED
|
|
1851
|
+
else:
|
|
1852
|
+
mode = rcfg.AwaitMode.ERASED
|
|
1853
|
+
else:
|
|
1854
|
+
raise CodeGenError(
|
|
1855
|
+
"await reached codegen without sema-resolved sub-future shape",
|
|
1856
|
+
loc=host_stmt.loc)
|
|
1857
|
+
return rcfg.AwaitPayload(
|
|
1858
|
+
mode=mode,
|
|
1859
|
+
sub_field_cpp_type=sub_cpp,
|
|
1860
|
+
operand_expr=await_node.value,
|
|
1861
|
+
kind=kind,
|
|
1862
|
+
bind_target=bind_target,
|
|
1863
|
+
return_stmt=return_stmt,
|
|
1864
|
+
host_stmt=host_stmt,
|
|
1865
|
+
await_node=await_node,
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
def _compute_case_entries(self, cfg: 'rcfg.CFG') -> dict[int, _StateLabel]:
|
|
1869
|
+
"""Return mapping bb_id -> StateLabel for every BB that needs its
|
|
1870
|
+
own case label in the emitted switch. Cached on the CFG so the
|
|
1871
|
+
struct-emit and poll-emit passes share the result.
|
|
1872
|
+
|
|
1873
|
+
Rules: a BB is a case entry iff it is
|
|
1874
|
+
- the entry BB (S_INITIAL),
|
|
1875
|
+
- the resume_bb of a Yield (S_RESUME_<i>),
|
|
1876
|
+
- reached via Fall/Branch by 2+ predecessors (multi-pred join), or
|
|
1877
|
+
- reached via Fall/Branch from a predecessor with a different
|
|
1878
|
+
region_stack (region-crossing edge).
|
|
1879
|
+
|
|
1880
|
+
Handler entries (reached only via C++ catch) are inlined into
|
|
1881
|
+
their enclosing try-region's catch and never get a case label.
|
|
1882
|
+
"""
|
|
1883
|
+
if cfg._case_entries_cache is not None:
|
|
1884
|
+
return cfg._case_entries_cache
|
|
1885
|
+
case_entries: dict[int, _StateLabel] = {
|
|
1886
|
+
cfg.entry_bb: _StateLabel(_StateKind.INITIAL)
|
|
1887
|
+
}
|
|
1888
|
+
for y in cfg.yield_sites:
|
|
1889
|
+
case_entries[y.resume_bb] = _StateLabel(
|
|
1890
|
+
_StateKind.RESUME, y.suspension_index)
|
|
1891
|
+
# Compute predecessors via Fall/Branch/Yield-resume edges.
|
|
1892
|
+
# Yield.resume_bb already a case entry; tracking helps detect
|
|
1893
|
+
# multi-pred / region-crossing.
|
|
1894
|
+
preds: dict[int, list[int]] = {bid: [] for bid in cfg.blocks}
|
|
1895
|
+
for bid, bb in cfg.blocks.items():
|
|
1896
|
+
t = bb.terminator
|
|
1897
|
+
if isinstance(t, rcfg.Fall):
|
|
1898
|
+
preds[t.next_bb].append(bid)
|
|
1899
|
+
elif isinstance(t, rcfg.Branch):
|
|
1900
|
+
preds[t.then_bb].append(bid)
|
|
1901
|
+
preds[t.else_bb].append(bid)
|
|
1902
|
+
elif isinstance(t, rcfg.AsyncForAdvance):
|
|
1903
|
+
preds[t.has_value_bb].append(bid)
|
|
1904
|
+
preds[t.exhausted_bb].append(bid)
|
|
1905
|
+
elif isinstance(t, rcfg.Yield):
|
|
1906
|
+
preds[t.resume_bb].append(bid)
|
|
1907
|
+
# Multi-pred (excluding yield-resume, which is already case entry).
|
|
1908
|
+
join_idx = 0
|
|
1909
|
+
for bid, ps in preds.items():
|
|
1910
|
+
if bid in case_entries:
|
|
1911
|
+
continue
|
|
1912
|
+
if len(ps) >= 2:
|
|
1913
|
+
case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
|
|
1914
|
+
join_idx += 1
|
|
1915
|
+
# Region-crossing predecessors.
|
|
1916
|
+
for bid, bb in cfg.blocks.items():
|
|
1917
|
+
if bid in case_entries:
|
|
1918
|
+
continue
|
|
1919
|
+
for p in preds.get(bid, ()):
|
|
1920
|
+
p_bb = cfg.blocks[p]
|
|
1921
|
+
if p_bb.region_stack != bb.region_stack:
|
|
1922
|
+
case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
|
|
1923
|
+
join_idx += 1
|
|
1924
|
+
break
|
|
1925
|
+
# WithRegion suppression targets: any with whose __exit__ may
|
|
1926
|
+
# suppress an exception transitions state directly to
|
|
1927
|
+
# post_with_bb from inside the catch (not a normal CFG edge).
|
|
1928
|
+
# Mark every such post_with_bb as a case entry so the catch's
|
|
1929
|
+
# `__state = ...; continue;` lands on a labeled case.
|
|
1930
|
+
with_post_bbs: set[int] = set()
|
|
1931
|
+
for bb in cfg.blocks.values():
|
|
1932
|
+
for r in bb.region_stack:
|
|
1933
|
+
if (isinstance(r, rcfg.WithRegion)
|
|
1934
|
+
and r.item.exit_can_suppress):
|
|
1935
|
+
with_post_bbs.add(r.post_with_bb)
|
|
1936
|
+
for bid in with_post_bbs:
|
|
1937
|
+
if bid not in case_entries:
|
|
1938
|
+
case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
|
|
1939
|
+
join_idx += 1
|
|
1940
|
+
# CFG-based finally entry BBs: the TryRegion's catch-all
|
|
1941
|
+
# transitions state directly to `finally_entry_bb` from inside
|
|
1942
|
+
# the catch (not a normal CFG edge). The Fall on the normal
|
|
1943
|
+
# exit path region-crosses out of the TryRegion which already
|
|
1944
|
+
# makes this BB a case entry, but mark explicitly to cover the
|
|
1945
|
+
# "try body always raises" case (no Fall predecessor).
|
|
1946
|
+
# Same for CFG-based finally EXIT BBs: a `return` inside the
|
|
1947
|
+
# finally body transitions state directly here without a CFG
|
|
1948
|
+
# edge, so the BB must have a case label even if the
|
|
1949
|
+
# Fall-from-finally-end region-crossing already picked it up.
|
|
1950
|
+
finally_entry_bbs: set[int] = set()
|
|
1951
|
+
finally_exit_bbs: set[int] = set()
|
|
1952
|
+
for bb in cfg.blocks.values():
|
|
1953
|
+
for r in bb.region_stack:
|
|
1954
|
+
if (isinstance(r, rcfg.TryRegion)
|
|
1955
|
+
and r.finally_entry_bb is not None):
|
|
1956
|
+
finally_entry_bbs.add(r.finally_entry_bb)
|
|
1957
|
+
elif (isinstance(r, rcfg.FinallyRegion)
|
|
1958
|
+
and r.finally_exit_bb is not None):
|
|
1959
|
+
finally_exit_bbs.add(r.finally_exit_bb)
|
|
1960
|
+
for bid in finally_entry_bbs | finally_exit_bbs:
|
|
1961
|
+
if bid not in case_entries:
|
|
1962
|
+
case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
|
|
1963
|
+
join_idx += 1
|
|
1964
|
+
# MatchDispatch join BBs: the dispatch transitions to its join
|
|
1965
|
+
# directly (a non-CFG edge from the suspension-free dispatch's
|
|
1966
|
+
# fall-through), and each arm body Falls there. The arm bodies
|
|
1967
|
+
# are INLINED by the dispatch (not their own cases), so arm_bbs
|
|
1968
|
+
# are deliberately NOT marked here; only the shared join needs a
|
|
1969
|
+
# label.
|
|
1970
|
+
match_join_bbs: set[int] = set()
|
|
1971
|
+
for bb in cfg.blocks.values():
|
|
1972
|
+
t = bb.terminator
|
|
1973
|
+
if isinstance(t, rcfg.MatchDispatch) and t.join_bb is not None:
|
|
1974
|
+
match_join_bbs.add(t.join_bb)
|
|
1975
|
+
for bid in match_join_bbs:
|
|
1976
|
+
if bid not in case_entries:
|
|
1977
|
+
case_entries[bid] = _StateLabel(_StateKind.JOIN, join_idx)
|
|
1978
|
+
join_idx += 1
|
|
1979
|
+
cfg._case_entries_cache = case_entries
|
|
1980
|
+
return case_entries
|
|
1981
|
+
|
|
1982
|
+
def _emit_state_machine(self, out: "TextIO", func: TpyFunction,
|
|
1983
|
+
cfg: 'rcfg.CFG') -> None:
|
|
1984
|
+
"""Emit `while (true) switch (state) { ... }` for the CFG.
|
|
1985
|
+
For zero-yield async defs the loop is omitted (no state
|
|
1986
|
+
transitions can fire, so the switch runs once)."""
|
|
1987
|
+
case_entries = self._compute_case_entries(cfg)
|
|
1988
|
+
self.ctx.indent_level = 1
|
|
1989
|
+
inner = self.ctx.indent()
|
|
1990
|
+
if cfg.yield_sites:
|
|
1991
|
+
out.write(f"{inner}while (true) switch (__state) {{\n")
|
|
1992
|
+
else:
|
|
1993
|
+
out.write(f"{inner}switch (__state) {{\n")
|
|
1994
|
+
# Case-label order matches the enum in gen_coro_struct.
|
|
1995
|
+
order = sorted(case_entries.items(), key=lambda kv: kv[1])
|
|
1996
|
+
for bb_id, label in order:
|
|
1997
|
+
out.write(f"{inner}case {label.cpp_name()}: {{\n")
|
|
1998
|
+
self.ctx.indent_level = 2
|
|
1999
|
+
self._emit_case(out, cfg, bb_id, case_entries, func)
|
|
2000
|
+
self.ctx.indent_level = 1
|
|
2001
|
+
out.write(f"{inner}}}\n")
|
|
2002
|
+
self._emit_resumable_done_case(out, inner)
|
|
2003
|
+
out.write(f"{inner}}}\n")
|
|
2004
|
+
out.write(f"{inner}__builtin_unreachable();\n")
|
|
2005
|
+
self.ctx.indent_level = 0
|
|
2006
|
+
|
|
2007
|
+
def _case_is_no_throw(self, cfg: 'rcfg.CFG', entry_bb: int) -> bool:
|
|
2008
|
+
"""True iff the case body that starts at `entry_bb` provably
|
|
2009
|
+
cannot throw anything an in-scope handler would catch.
|
|
2010
|
+
Conservative: requires the BB to consist solely of a Yield
|
|
2011
|
+
terminator with literal / simple-name emplace args, with no
|
|
2012
|
+
user stmts and not a resume entry (resume cases emit a cancel
|
|
2013
|
+
check + sub poll that can both throw)."""
|
|
2014
|
+
bb = cfg.blocks[entry_bb]
|
|
2015
|
+
if bb.stmts:
|
|
2016
|
+
return False
|
|
2017
|
+
if cfg.resume_to_yield().get(entry_bb) is not None:
|
|
2018
|
+
return False
|
|
2019
|
+
t = bb.terminator
|
|
2020
|
+
if not isinstance(t, rcfg.Yield):
|
|
2021
|
+
return False
|
|
2022
|
+
return self._payload_args_no_throw(t.payload)
|
|
2023
|
+
|
|
2024
|
+
def _payload_args_no_throw(self,
|
|
2025
|
+
payload: 'rcfg.SuspensionPayload') -> bool:
|
|
2026
|
+
"""True if emitting the Yield cannot throw. For a generator yield,
|
|
2027
|
+
the emitted action is `return <value>;` -- no-throw iff the value
|
|
2028
|
+
is a literal / simple name."""
|
|
2029
|
+
if isinstance(payload, rcfg.YieldPayload):
|
|
2030
|
+
return (payload.value_expr is None
|
|
2031
|
+
or self._expr_is_simple(payload.value_expr))
|
|
2032
|
+
# Async-with synthetic yields emplace from the CM frame slot
|
|
2033
|
+
# (`*__with_ctx_<n>`) plus monostate literals -- provably
|
|
2034
|
+
# no-throw and never touch the payload's `operand_expr`.
|
|
2035
|
+
if payload.async_with_kind is not None:
|
|
2036
|
+
return True
|
|
2037
|
+
if payload.mode is rcfg.AwaitMode.BORROWED:
|
|
2038
|
+
return self._expr_is_simple(payload.operand_expr)
|
|
2039
|
+
if payload.mode is rcfg.AwaitMode.ERASED:
|
|
2040
|
+
return self._expr_is_simple(payload.operand_expr)
|
|
2041
|
+
# INLINE: emplace args are the call's args.
|
|
2042
|
+
call = payload.operand_expr
|
|
2043
|
+
if not isinstance(call, TpyCall):
|
|
2044
|
+
return False
|
|
2045
|
+
return all(self._expr_is_simple(a) for a in call.args)
|
|
2046
|
+
|
|
2047
|
+
@staticmethod
|
|
2048
|
+
def _expr_is_simple(expr: TpyExpr) -> bool:
|
|
2049
|
+
"""True for literals, bare names, and shallow coercions of
|
|
2050
|
+
either. These can be emitted without invoking constructors /
|
|
2051
|
+
function calls that could throw."""
|
|
2052
|
+
if isinstance(expr, TpyCoerce):
|
|
2053
|
+
return AsyncCoroCodegen._expr_is_simple(expr.expr)
|
|
2054
|
+
return isinstance(expr, (TpyIntLiteral, TpyFloatLiteral,
|
|
2055
|
+
TpyStrLiteral, TpyBoolLiteral,
|
|
2056
|
+
TpyNoneLiteral, TpyName))
|
|
2057
|
+
|
|
2058
|
+
def _emit_case(self, out: "TextIO", cfg: 'rcfg.CFG',
|
|
2059
|
+
entry_bb: int, case_entries: dict[int, _StateLabel],
|
|
2060
|
+
func: TpyFunction) -> None:
|
|
2061
|
+
"""Emit the body of one switch case: wrap in region_stack, then
|
|
2062
|
+
walk the BB graph inline until hitting another case entry or a
|
|
2063
|
+
terminator that exits poll()."""
|
|
2064
|
+
bb = cfg.blocks[entry_bb]
|
|
2065
|
+
body_indent = self.ctx.indent()
|
|
2066
|
+
# Push FinallyContext entries so a `return` inside this case
|
|
2067
|
+
# body walks the right finally chain via _emit_finally_chain.
|
|
2068
|
+
# ExceptRegion contributes its parent try's finally because
|
|
2069
|
+
# Python runs finally after the handler completes.
|
|
2070
|
+
pushed_finally = self._push_finally_helpers(
|
|
2071
|
+
self._finally_helpers_for_region_stack(bb.region_stack))
|
|
2072
|
+
# Pending-return ctx: when this case is inside the try
|
|
2073
|
+
# body or handler body of a CFG-based finally, install ctx
|
|
2074
|
+
# state so `return` inside the body routes through the
|
|
2075
|
+
# pending-return slot.
|
|
2076
|
+
prev_pending = self._snapshot_pending_return_ctx()
|
|
2077
|
+
pending = self._pending_return_info_for_region_stack(bb.region_stack)
|
|
2078
|
+
if pending is not None:
|
|
2079
|
+
flag, slot, finally_entry_bb, boundary = pending
|
|
2080
|
+
target_label = case_entries[finally_entry_bb].cpp_name()
|
|
2081
|
+
self.ctx.async_pending_return_flag = flag
|
|
2082
|
+
self.ctx.async_pending_return_slot = slot
|
|
2083
|
+
self.ctx.async_pending_return_target_state = target_label
|
|
2084
|
+
self.ctx.async_pending_return_boundary = boundary
|
|
2085
|
+
try:
|
|
2086
|
+
# Emit `try {` for each TryRegion / WithRegion. For each
|
|
2087
|
+
# TryRegion, collect the list of "inter-region finally
|
|
2088
|
+
# helpers" between THIS TryRegion and the next-inner
|
|
2089
|
+
# try-emitting region (TryRegion or WithRegion): each
|
|
2090
|
+
# ExceptRegion's parent_finally goes in the TryRegion's
|
|
2091
|
+
# catch because its source-level try frame is no longer on
|
|
2092
|
+
# the C++ try stack but is still logically active.
|
|
2093
|
+
# WithRegions inside the gap get their own C++ try and
|
|
2094
|
+
# don't contribute extras.
|
|
2095
|
+
tryctx_stack: list[tuple[rcfg.Region, tuple[str, ...]]] = []
|
|
2096
|
+
# Skip the try/catch wrap entirely for case bodies provably
|
|
2097
|
+
# no-throw (typical setup case: empty stmts + Yield with
|
|
2098
|
+
# literal/name emplace args). The matching live catch sits
|
|
2099
|
+
# on the resume case where the sub-future's poll runs and
|
|
2100
|
+
# CAN throw.
|
|
2101
|
+
no_throw = self._case_is_no_throw(cfg, entry_bb)
|
|
2102
|
+
try_emitting = ([] if no_throw else
|
|
2103
|
+
[i for i, r in enumerate(bb.region_stack)
|
|
2104
|
+
if isinstance(r, (rcfg.TryRegion,
|
|
2105
|
+
rcfg.WithRegion))])
|
|
2106
|
+
for j, i in enumerate(try_emitting):
|
|
2107
|
+
region = bb.region_stack[i]
|
|
2108
|
+
if isinstance(region, rcfg.TryRegion):
|
|
2109
|
+
next_emit = (try_emitting[j + 1]
|
|
2110
|
+
if j + 1 < len(try_emitting)
|
|
2111
|
+
else len(bb.region_stack))
|
|
2112
|
+
extras: list[str] = []
|
|
2113
|
+
for k in range(i + 1, next_emit):
|
|
2114
|
+
mid = bb.region_stack[k]
|
|
2115
|
+
if isinstance(mid, rcfg.ExceptRegion):
|
|
2116
|
+
if mid.parent_finally is not None:
|
|
2117
|
+
extras.append(mid.parent_finally)
|
|
2118
|
+
extras.reverse()
|
|
2119
|
+
else:
|
|
2120
|
+
extras = []
|
|
2121
|
+
out.write(f"{body_indent}try {{\n")
|
|
2122
|
+
tryctx_stack.append((region, tuple(extras)))
|
|
2123
|
+
self.ctx.indent_level += 1
|
|
2124
|
+
body_indent = self.ctx.indent()
|
|
2125
|
+
|
|
2126
|
+
# Emit the case body: resume step if this is a yield-resume,
|
|
2127
|
+
# then BB statements, then terminator.
|
|
2128
|
+
self._emit_case_body(out, cfg, entry_bb, case_entries, func)
|
|
2129
|
+
|
|
2130
|
+
# Close regions innermost first.
|
|
2131
|
+
for region, extras in reversed(tryctx_stack):
|
|
2132
|
+
self.ctx.indent_level -= 1
|
|
2133
|
+
body_indent = self.ctx.indent()
|
|
2134
|
+
out.write(f"{body_indent}}}")
|
|
2135
|
+
if isinstance(region, rcfg.TryRegion):
|
|
2136
|
+
self._emit_try_region_catches(
|
|
2137
|
+
out, body_indent, region, cfg, case_entries,
|
|
2138
|
+
entry_bb, func, extras)
|
|
2139
|
+
else:
|
|
2140
|
+
self._emit_with_region_catches(
|
|
2141
|
+
out, body_indent, region, case_entries)
|
|
2142
|
+
out.write("\n")
|
|
2143
|
+
finally:
|
|
2144
|
+
for _ in range(pushed_finally):
|
|
2145
|
+
self.ctx.finally_stack.pop()
|
|
2146
|
+
self._restore_pending_return_ctx(prev_pending)
|
|
2147
|
+
|
|
2148
|
+
def _snapshot_pending_return_ctx(self) -> tuple:
|
|
2149
|
+
return (self.ctx.async_pending_return_flag,
|
|
2150
|
+
self.ctx.async_pending_return_slot,
|
|
2151
|
+
self.ctx.async_pending_return_target_state,
|
|
2152
|
+
self.ctx.async_pending_return_boundary)
|
|
2153
|
+
|
|
2154
|
+
def _restore_pending_return_ctx(self, snap: tuple) -> None:
|
|
2155
|
+
(self.ctx.async_pending_return_flag,
|
|
2156
|
+
self.ctx.async_pending_return_slot,
|
|
2157
|
+
self.ctx.async_pending_return_target_state,
|
|
2158
|
+
self.ctx.async_pending_return_boundary) = snap
|
|
2159
|
+
|
|
2160
|
+
def _finally_helpers_for_region_stack(
|
|
2161
|
+
self, region_stack: tuple) -> list:
|
|
2162
|
+
"""Compute the finally-emit closures for BBs with this
|
|
2163
|
+
region_stack. Each TryRegion / ExceptRegion contributes its
|
|
2164
|
+
finally helper name (called via `this->name()`); each WithRegion
|
|
2165
|
+
contributes a closure that emits `(*__with_ctx_<n>).__exit__(
|
|
2166
|
+
{}, nullptr/{}, {})`. Order: outermost first (innermost ends up
|
|
2167
|
+
on top of the stack)."""
|
|
2168
|
+
helpers: list = []
|
|
2169
|
+
for region in region_stack:
|
|
2170
|
+
if isinstance(region, rcfg.TryRegion):
|
|
2171
|
+
if region.finally_helper_name is not None:
|
|
2172
|
+
helpers.append(("helper", region.finally_helper_name))
|
|
2173
|
+
elif isinstance(region, rcfg.ExceptRegion):
|
|
2174
|
+
if region.parent_finally is not None:
|
|
2175
|
+
helpers.append(("helper", region.parent_finally))
|
|
2176
|
+
elif isinstance(region, rcfg.WithRegion):
|
|
2177
|
+
helpers.append(("with", region))
|
|
2178
|
+
return helpers
|
|
2179
|
+
|
|
2180
|
+
def _pending_return_info_for_region_stack(
|
|
2181
|
+
self, region_stack: tuple) -> 'tuple[str, str | None, int, int] | None':
|
|
2182
|
+
"""If a CFG-based finally is active for this region_stack, return
|
|
2183
|
+
`(flag, slot, target_bb, boundary)` for the INNERMOST one --
|
|
2184
|
+
boundary is the count of finally_stack frames contributed by
|
|
2185
|
+
helper-based regions BELOW it (those defer to AsyncFinallyExit
|
|
2186
|
+
instead of running at the return site). Returns None otherwise.
|
|
2187
|
+
|
|
2188
|
+
Walking innermost-first is what makes nested CFG finally work:
|
|
2189
|
+
a return inside the inner try body parks into the inner's slot;
|
|
2190
|
+
the inner's AsyncFinallyExit later forwards into the outer.
|
|
2191
|
+
At the inner's `finally_exit_bb`, the inner FinallyRegion is no
|
|
2192
|
+
longer on the stack, so the same lookup naturally finds the
|
|
2193
|
+
outer (the new innermost) -- which is how AsyncFinallyExit
|
|
2194
|
+
learns whether to replay locally or forward outward.
|
|
2195
|
+
|
|
2196
|
+
For try/handler-body returns: target_bb = finally_entry_bb (the
|
|
2197
|
+
return parks + jumps to the finally entry).
|
|
2198
|
+
For finally-body returns: target_bb = finally_exit_bb (the return
|
|
2199
|
+
parks + jumps straight to AsyncFinallyExit; running the finally
|
|
2200
|
+
body again would re-execute it from the top)."""
|
|
2201
|
+
innermost: 'tuple[str, str | None, int, int] | None' = None
|
|
2202
|
+
boundary = 0
|
|
2203
|
+
for region in region_stack:
|
|
2204
|
+
if isinstance(region, rcfg.TryRegion):
|
|
2205
|
+
if region.pending_return_flag is not None:
|
|
2206
|
+
innermost = (region.pending_return_flag,
|
|
2207
|
+
region.pending_return_slot,
|
|
2208
|
+
region.finally_entry_bb,
|
|
2209
|
+
boundary)
|
|
2210
|
+
elif region.finally_helper_name is not None:
|
|
2211
|
+
boundary += 1
|
|
2212
|
+
elif isinstance(region, rcfg.ExceptRegion):
|
|
2213
|
+
if region.parent_pending_return_flag is not None:
|
|
2214
|
+
innermost = (region.parent_pending_return_flag,
|
|
2215
|
+
region.parent_pending_return_slot,
|
|
2216
|
+
region.parent_finally_entry_bb,
|
|
2217
|
+
boundary)
|
|
2218
|
+
elif region.parent_finally is not None:
|
|
2219
|
+
boundary += 1
|
|
2220
|
+
elif isinstance(region, rcfg.FinallyRegion):
|
|
2221
|
+
if region.pending_return_flag is not None:
|
|
2222
|
+
innermost = (region.pending_return_flag,
|
|
2223
|
+
region.pending_return_slot,
|
|
2224
|
+
region.finally_exit_bb,
|
|
2225
|
+
boundary)
|
|
2226
|
+
elif isinstance(region, rcfg.WithRegion):
|
|
2227
|
+
boundary += 1
|
|
2228
|
+
return innermost
|
|
2229
|
+
|
|
2230
|
+
def _emit_finally_helper_call(self, out: "TextIO", indent: str,
|
|
2231
|
+
helper_name: str) -> None:
|
|
2232
|
+
"""Emit `this->helper_name();`.
|
|
2233
|
+
|
|
2234
|
+
The stop-check (`if __finally_stop`) is NOT emitted here. Callers
|
|
2235
|
+
that need to act on __finally_stop (e.g. to suppress a rethrow or
|
|
2236
|
+
skip a state transition) call _emit_generator_stop_check AFTER all
|
|
2237
|
+
cleanup for the current exit event has run, so that outer region
|
|
2238
|
+
cleanup (with.__exit__, outer finallies) is never skipped."""
|
|
2239
|
+
out.write(f"{indent}this->{helper_name}();\n")
|
|
2240
|
+
|
|
2241
|
+
def _emit_generator_stop_check(self, out: "TextIO", indent: str) -> None:
|
|
2242
|
+
"""Emit `if (this->__finally_stop) { return StopIteration; }`.
|
|
2243
|
+
|
|
2244
|
+
Only emitted when in generator shape and the struct has __finally_stop.
|
|
2245
|
+
Call AFTER all cleanup for an exit event (region loop, catch handler)
|
|
2246
|
+
has run, so outer cleanups are never skipped by an early return."""
|
|
2247
|
+
if not (self._is_generator_shape() and self.ctx.generator_has_finally_stop):
|
|
2248
|
+
return
|
|
2249
|
+
done_state = self.ctx.generator_resumable_done_state or "S_DONE"
|
|
2250
|
+
out.write(f"{indent}if (this->__finally_stop) {{\n")
|
|
2251
|
+
out.write(f"{indent}{INDENT}__state = {done_state};\n")
|
|
2252
|
+
out.write(f"{indent}{INDENT}return ::tpy::make_unexpected("
|
|
2253
|
+
f"::tpy::StopIteration{{}});\n")
|
|
2254
|
+
out.write(f"{indent}}}\n")
|
|
2255
|
+
|
|
2256
|
+
def _push_finally_helpers(self, helpers: list) -> int:
|
|
2257
|
+
"""Push FinallyContext entries for each helper. Returns the
|
|
2258
|
+
count pushed for matching pop in a `finally:` clause."""
|
|
2259
|
+
count = 0
|
|
2260
|
+
for kind, payload in helpers:
|
|
2261
|
+
if kind == "helper":
|
|
2262
|
+
helper_name = payload
|
|
2263
|
+
def _emit_finally(o: "TextIO", ind: str,
|
|
2264
|
+
n=helper_name) -> None:
|
|
2265
|
+
self._emit_finally_helper_call(o, ind, n)
|
|
2266
|
+
else: # "with"
|
|
2267
|
+
region = payload
|
|
2268
|
+
def _emit_finally(o: "TextIO", ind: str,
|
|
2269
|
+
r=region) -> None:
|
|
2270
|
+
self._emit_with_exit(o, ind, r, on_exception=False)
|
|
2271
|
+
fctx = FinallyContext(
|
|
2272
|
+
emit_finally=_emit_finally, terminates=False, loop_depth=0)
|
|
2273
|
+
self.ctx.finally_stack.append(fctx)
|
|
2274
|
+
count += 1
|
|
2275
|
+
return count
|
|
2276
|
+
|
|
2277
|
+
def _emit_with_region_catches(self, out: "TextIO", indent: str,
|
|
2278
|
+
region: 'rcfg.WithRegion',
|
|
2279
|
+
case_entries: dict[int, _StateLabel]
|
|
2280
|
+
) -> None:
|
|
2281
|
+
"""Emit `} catch (...) { __exit__(...); throw; }` for a
|
|
2282
|
+
WithRegion at the close of a case body. When
|
|
2283
|
+
`item.exit_can_suppress` is True, a `catch (BaseException&)`
|
|
2284
|
+
clause first calls __exit__ with the exception and, on a True
|
|
2285
|
+
return, transitions to the WithRegion's post_with_bb state
|
|
2286
|
+
(suppressing the exception). The foreign catch-all always
|
|
2287
|
+
cleanup-calls __exit__ with `nullptr` exc and rethrows."""
|
|
2288
|
+
item = region.item
|
|
2289
|
+
n = region.ctx_n
|
|
2290
|
+
can_suppress = item.exit_can_suppress
|
|
2291
|
+
takes_exc_val = item.exit_takes_exc_val
|
|
2292
|
+
emit_tpy_catch = can_suppress or takes_exc_val
|
|
2293
|
+
# post_with_bb is only marked a case entry when this WithRegion
|
|
2294
|
+
# may suppress; without suppression the catch unconditionally
|
|
2295
|
+
# rethrows and never needs the label.
|
|
2296
|
+
post_label = (case_entries[region.post_with_bb].cpp_name()
|
|
2297
|
+
if can_suppress else None)
|
|
2298
|
+
if emit_tpy_catch:
|
|
2299
|
+
out.write(f" catch (::tpy::BaseException& __exc_{n}) {{\n")
|
|
2300
|
+
self.ctx.indent_level += 1
|
|
2301
|
+
catch_ind = self.ctx.indent()
|
|
2302
|
+
if can_suppress:
|
|
2303
|
+
out.write(
|
|
2304
|
+
f"{catch_ind}if (!(*__with_ctx_{n}).__exit__("
|
|
2305
|
+
f"{{}}, &__exc_{n}, {{}})) throw;\n")
|
|
2306
|
+
out.write(f"{catch_ind}__state = {post_label};\n")
|
|
2307
|
+
out.write(f"{catch_ind}continue;\n")
|
|
2308
|
+
else:
|
|
2309
|
+
self._emit_with_exit(out, catch_ind, region,
|
|
2310
|
+
on_exception=True)
|
|
2311
|
+
out.write(f"{catch_ind}throw;\n")
|
|
2312
|
+
self.ctx.indent_level -= 1
|
|
2313
|
+
out.write(f"{indent}}} catch (...) {{\n")
|
|
2314
|
+
else:
|
|
2315
|
+
out.write(f" catch (...) {{\n")
|
|
2316
|
+
self.ctx.indent_level += 1
|
|
2317
|
+
catch_ind = self.ctx.indent()
|
|
2318
|
+
# Foreign exception (or non-suppressing path): cleanup-only call,
|
|
2319
|
+
# then rethrow. Don't pass &exc since the C++ side hasn't bound
|
|
2320
|
+
# one in this catch arm.
|
|
2321
|
+
out.write(f"{catch_ind}(*__with_ctx_{n}).__exit__({{}}, "
|
|
2322
|
+
f"{'nullptr' if takes_exc_val else '{}'}, {{}});\n")
|
|
2323
|
+
out.write(f"{catch_ind}throw;\n")
|
|
2324
|
+
self.ctx.indent_level -= 1
|
|
2325
|
+
out.write(f"{indent}}}")
|
|
2326
|
+
|
|
2327
|
+
def _emit_try_region_catches(self, out: "TextIO", indent: str,
|
|
2328
|
+
region: 'rcfg.TryRegion',
|
|
2329
|
+
cfg: 'rcfg.CFG',
|
|
2330
|
+
case_entries: dict[int, _StateLabel],
|
|
2331
|
+
case_entry_bb: int,
|
|
2332
|
+
func: TpyFunction,
|
|
2333
|
+
extra_finallies: tuple[str, ...] = ()) -> None:
|
|
2334
|
+
"""Emit `} catch (...) { ... }` clauses for a TryRegion at the
|
|
2335
|
+
close of a case body. Each handler's body is emitted inline
|
|
2336
|
+
inside its catch (walks the handler entry BB).
|
|
2337
|
+
|
|
2338
|
+
`case_entry_bb` is the case-entry BB this region wraps; used to
|
|
2339
|
+
determine which __sub_<n> to reset (the in-flight sub-future
|
|
2340
|
+
for this resume case).
|
|
2341
|
+
|
|
2342
|
+
`extra_finallies` are helper-fn names (innermost first) for any
|
|
2343
|
+
ExceptRegion / FinallyRegion in the case's region_stack that
|
|
2344
|
+
sit *between* this TryRegion and the next-inner TryRegion --
|
|
2345
|
+
their Python-level try frames are no longer C++ try wraps here
|
|
2346
|
+
but are still logically active. They run in the catch-all (and
|
|
2347
|
+
each handler's body, if the handler completes normally is the
|
|
2348
|
+
Fall-edge case handled by `_emit_exit_region_finallies`; the
|
|
2349
|
+
throw escape is what we cover here)."""
|
|
2350
|
+
builder = rcfg.resumable_state(func).cfg_builder
|
|
2351
|
+
yield_for_case = self._yield_at_resume(cfg, case_entry_bb)
|
|
2352
|
+
# Helpers to invoke on throw from inside the handler body
|
|
2353
|
+
# (innermost first): each extra_finally + this region's own
|
|
2354
|
+
# finally. Mirrors the catch-all unwind order.
|
|
2355
|
+
handler_throw_finallies: tuple[str, ...] = tuple(extra_finallies)
|
|
2356
|
+
if region.finally_helper_name is not None:
|
|
2357
|
+
handler_throw_finallies = handler_throw_finallies + (region.finally_helper_name,)
|
|
2358
|
+
for handler in region.handlers:
|
|
2359
|
+
self.statements._emit_except_handler_header(out, handler)
|
|
2360
|
+
self.ctx.indent_level += 1
|
|
2361
|
+
catch_indent = self.ctx.indent()
|
|
2362
|
+
# Reset the in-flight sub-future first action in catch.
|
|
2363
|
+
# Generators have no sub-futures; only reset for async shape.
|
|
2364
|
+
if (yield_for_case is not None
|
|
2365
|
+
and isinstance(yield_for_case.payload, rcfg.AwaitPayload)):
|
|
2366
|
+
self._emit_sub_reset(out, catch_indent,
|
|
2367
|
+
yield_for_case.payload,
|
|
2368
|
+
yield_for_case.suspension_index)
|
|
2369
|
+
# Wrap handler body in `try { ... } catch (...) {
|
|
2370
|
+
# finallies; throw; }` so a `raise` from inside the
|
|
2371
|
+
# handler runs this try's finally (and any inter-region
|
|
2372
|
+
# finallies) before propagating to the outer try. For
|
|
2373
|
+
# CFG-based finally, the inner catch saves
|
|
2374
|
+
# `std::current_exception()` to the parent's captured-exc
|
|
2375
|
+
# slot and transitions state to its finally entry instead
|
|
2376
|
+
# of plain rethrow -- mirrors the outer catch-all path.
|
|
2377
|
+
cfg_finally = region.captured_exc_field is not None
|
|
2378
|
+
has_throw_unwind = bool(handler_throw_finallies) or cfg_finally
|
|
2379
|
+
if has_throw_unwind:
|
|
2380
|
+
out.write(f"{catch_indent}try {{\n")
|
|
2381
|
+
self.ctx.indent_level += 1
|
|
2382
|
+
# Swap ctx.finally_stack to match the handler entry BB's
|
|
2383
|
+
# region_stack. The case-entry's stack contains finallies
|
|
2384
|
+
# for regions that are no longer active inside the handler
|
|
2385
|
+
# body (the try whose handler we're in is gone). Without
|
|
2386
|
+
# the swap, a return inside the handler walks finallies
|
|
2387
|
+
# that should not apply (e.g. a sibling inner try's finally
|
|
2388
|
+
# that has already run on the throw path).
|
|
2389
|
+
old_finally_stack = self.ctx.finally_stack
|
|
2390
|
+
handler_entry: int | None = None
|
|
2391
|
+
if builder is not None:
|
|
2392
|
+
handler_entry = builder.get_handler_entry(region, handler)
|
|
2393
|
+
handler_stack_helpers: list = []
|
|
2394
|
+
if handler_entry is not None:
|
|
2395
|
+
handler_bb = cfg.blocks[handler_entry]
|
|
2396
|
+
handler_stack_helpers = self._finally_helpers_for_region_stack(
|
|
2397
|
+
handler_bb.region_stack)
|
|
2398
|
+
self.ctx.finally_stack = []
|
|
2399
|
+
self._push_finally_helpers(handler_stack_helpers)
|
|
2400
|
+
old_except_tier = self.ctx.in_except_tier
|
|
2401
|
+
self.ctx.in_except_tier = "throw"
|
|
2402
|
+
# Pending-return ctx for the handler body: a return inside
|
|
2403
|
+
# the handler routes through the parent CFG-based finally's
|
|
2404
|
+
# pending-return slot.
|
|
2405
|
+
prev_pending = self._snapshot_pending_return_ctx()
|
|
2406
|
+
if handler_entry is not None:
|
|
2407
|
+
pending = self._pending_return_info_for_region_stack(
|
|
2408
|
+
cfg.blocks[handler_entry].region_stack)
|
|
2409
|
+
if pending is not None:
|
|
2410
|
+
flag, slot, finally_entry_bb, boundary = pending
|
|
2411
|
+
target_label = case_entries[finally_entry_bb].cpp_name()
|
|
2412
|
+
self.ctx.async_pending_return_flag = flag
|
|
2413
|
+
self.ctx.async_pending_return_slot = slot
|
|
2414
|
+
self.ctx.async_pending_return_target_state = target_label
|
|
2415
|
+
self.ctx.async_pending_return_boundary = boundary
|
|
2416
|
+
try:
|
|
2417
|
+
if handler_entry is not None:
|
|
2418
|
+
self._walk_inline(out, cfg, handler_entry,
|
|
2419
|
+
case_entries, func)
|
|
2420
|
+
finally:
|
|
2421
|
+
self.ctx.in_except_tier = old_except_tier
|
|
2422
|
+
self.ctx.finally_stack = old_finally_stack
|
|
2423
|
+
self._restore_pending_return_ctx(prev_pending)
|
|
2424
|
+
if has_throw_unwind:
|
|
2425
|
+
self.ctx.indent_level -= 1
|
|
2426
|
+
inner_close = self.ctx.indent()
|
|
2427
|
+
out.write(f"{inner_close}}} catch (...) {{\n")
|
|
2428
|
+
self.ctx.indent_level += 1
|
|
2429
|
+
inner_catch = self.ctx.indent()
|
|
2430
|
+
for helper in handler_throw_finallies:
|
|
2431
|
+
self._emit_finally_helper_call(out, inner_catch, helper)
|
|
2432
|
+
if cfg_finally:
|
|
2433
|
+
assert region.finally_entry_bb is not None
|
|
2434
|
+
fe_label = case_entries[region.finally_entry_bb].cpp_name()
|
|
2435
|
+
out.write(f"{inner_catch}this->{region.captured_exc_field}"
|
|
2436
|
+
f" = std::current_exception();\n")
|
|
2437
|
+
out.write(f"{inner_catch}__state = {fe_label};\n")
|
|
2438
|
+
out.write(f"{inner_catch}continue;\n")
|
|
2439
|
+
else:
|
|
2440
|
+
self._emit_generator_stop_check(out, inner_catch)
|
|
2441
|
+
out.write(f"{inner_catch}throw;\n")
|
|
2442
|
+
self.ctx.indent_level -= 1
|
|
2443
|
+
out.write(f"{inner_close}}}\n")
|
|
2444
|
+
self.ctx.indent_level -= 1
|
|
2445
|
+
out.write(f"{indent}}}")
|
|
2446
|
+
# Catch-all: reset sub, run inter-region finallies (innermost
|
|
2447
|
+
# first). For helper-based finally: call helper, re-throw. For
|
|
2448
|
+
# CFG-based finally: save current exception to the
|
|
2449
|
+
# captured-exc field and transition state to the finally entry
|
|
2450
|
+
# so the finally body runs in the state machine (with possible
|
|
2451
|
+
# suspensions); the saved exception is rethrown by the
|
|
2452
|
+
# AsyncFinallyExit stmt at the finally tail.
|
|
2453
|
+
out.write(" catch (...) {\n")
|
|
2454
|
+
self.ctx.indent_level += 1
|
|
2455
|
+
catch_indent = self.ctx.indent()
|
|
2456
|
+
if (yield_for_case is not None
|
|
2457
|
+
and isinstance(yield_for_case.payload, rcfg.AwaitPayload)):
|
|
2458
|
+
self._emit_sub_reset(out, catch_indent,
|
|
2459
|
+
yield_for_case.payload,
|
|
2460
|
+
yield_for_case.suspension_index)
|
|
2461
|
+
for helper in extra_finallies:
|
|
2462
|
+
self._emit_finally_helper_call(out, catch_indent, helper)
|
|
2463
|
+
if region.captured_exc_field is not None:
|
|
2464
|
+
assert region.finally_entry_bb is not None
|
|
2465
|
+
label = case_entries[region.finally_entry_bb].cpp_name()
|
|
2466
|
+
out.write(f"{catch_indent}this->{region.captured_exc_field} "
|
|
2467
|
+
f"= std::current_exception();\n")
|
|
2468
|
+
out.write(f"{catch_indent}__state = {label};\n")
|
|
2469
|
+
out.write(f"{catch_indent}continue;\n")
|
|
2470
|
+
else:
|
|
2471
|
+
if region.finally_helper_name is not None:
|
|
2472
|
+
self._emit_finally_helper_call(
|
|
2473
|
+
out, catch_indent, region.finally_helper_name)
|
|
2474
|
+
# All cleanup done; Python `return` in finally suppresses the
|
|
2475
|
+
# exception (return StopIteration rather than rethrowing).
|
|
2476
|
+
self._emit_generator_stop_check(out, catch_indent)
|
|
2477
|
+
out.write(f"{catch_indent}throw;\n")
|
|
2478
|
+
self.ctx.indent_level -= 1
|
|
2479
|
+
out.write(f"{indent}}}")
|
|
2480
|
+
|
|
2481
|
+
def _resume_index_for_case(self, cfg: 'rcfg.CFG',
|
|
2482
|
+
case_entry_bb: int) -> int | None:
|
|
2483
|
+
y = cfg.resume_to_yield().get(case_entry_bb)
|
|
2484
|
+
return y.suspension_index if y is not None else None
|
|
2485
|
+
|
|
2486
|
+
def _yield_at_resume(self, cfg: 'rcfg.CFG',
|
|
2487
|
+
resume_bb: int) -> 'rcfg.Yield | None':
|
|
2488
|
+
return cfg.resume_to_yield().get(resume_bb)
|
|
2489
|
+
|
|
2490
|
+
def _emit_case_body(self, out: "TextIO", cfg: 'rcfg.CFG',
|
|
2491
|
+
entry_bb: int, case_entries: dict[int, _StateLabel],
|
|
2492
|
+
func: TpyFunction) -> None:
|
|
2493
|
+
"""Emit the body of a case starting at entry_bb. Begins with the
|
|
2494
|
+
resume step (if entry_bb is a yield-resume), then walks BBs
|
|
2495
|
+
inline until a terminator exits the case."""
|
|
2496
|
+
body_indent = self.ctx.indent()
|
|
2497
|
+
# Resume step. Generators have no sub-future to poll and bind
|
|
2498
|
+
# nothing back in, so their resume case is a bare continuation.
|
|
2499
|
+
y = self._yield_at_resume(cfg, entry_bb)
|
|
2500
|
+
if y is not None and not self._is_generator_shape():
|
|
2501
|
+
self._emit_resume_core(out, body_indent, y.payload,
|
|
2502
|
+
y.suspension_index, func)
|
|
2503
|
+
if y.payload.kind is rcfg.AwaitKind.RETURN:
|
|
2504
|
+
# Resume core already emitted the return.
|
|
2505
|
+
return
|
|
2506
|
+
# Walk BBs inline from entry_bb.
|
|
2507
|
+
self._walk_inline(out, cfg, entry_bb, case_entries, func)
|
|
2508
|
+
|
|
2509
|
+
def _emit_exit_region_finallies(self, out: "TextIO", indent: str,
|
|
2510
|
+
from_regions: tuple,
|
|
2511
|
+
to_regions: tuple) -> None:
|
|
2512
|
+
"""Emit cleanup (finally helpers + with __exit__ calls) for each
|
|
2513
|
+
region that exists in `from_regions` but not in `to_regions`,
|
|
2514
|
+
in innermost-first order. Used when control transitions from a
|
|
2515
|
+
deeper region stack to a shallower one (normal exit from
|
|
2516
|
+
try/with) -- C++ try/catch doesn't run finally on normal exit,
|
|
2517
|
+
so we run them explicitly here."""
|
|
2518
|
+
if not from_regions:
|
|
2519
|
+
return
|
|
2520
|
+
# Identify the common prefix length.
|
|
2521
|
+
common = 0
|
|
2522
|
+
while (common < len(from_regions) and common < len(to_regions)
|
|
2523
|
+
and from_regions[common] is to_regions[common]):
|
|
2524
|
+
common += 1
|
|
2525
|
+
exited = list(from_regions[common:])
|
|
2526
|
+
# Innermost first.
|
|
2527
|
+
for region in reversed(exited):
|
|
2528
|
+
if isinstance(region, rcfg.TryRegion):
|
|
2529
|
+
if region.finally_helper_name is not None:
|
|
2530
|
+
self._emit_finally_helper_call(
|
|
2531
|
+
out, indent, region.finally_helper_name)
|
|
2532
|
+
elif isinstance(region, rcfg.ExceptRegion):
|
|
2533
|
+
# Leaving an except handler normally: run the parent
|
|
2534
|
+
# try's finally body (Python semantics).
|
|
2535
|
+
if region.parent_finally is not None:
|
|
2536
|
+
self._emit_finally_helper_call(
|
|
2537
|
+
out, indent, region.parent_finally)
|
|
2538
|
+
elif isinstance(region, rcfg.WithRegion):
|
|
2539
|
+
# Leaving a with-region normally: __exit__(None, None, None).
|
|
2540
|
+
self._emit_with_exit(out, indent, region,
|
|
2541
|
+
on_exception=False)
|
|
2542
|
+
# Emit stop-check only when to_regions has no pending cleanup of its
|
|
2543
|
+
# own. If to_regions still has with/__exit__ or finally helpers, the
|
|
2544
|
+
# state machine will run those in subsequent states; the stop-check
|
|
2545
|
+
# in those later transitions fires after ALL cleanup is done.
|
|
2546
|
+
if not _regions_have_pending_cleanup(to_regions):
|
|
2547
|
+
self._emit_generator_stop_check(out, indent)
|
|
2548
|
+
|
|
2549
|
+
def _walk_inline(self, out: "TextIO", cfg: 'rcfg.CFG',
|
|
2550
|
+
start_bb: int, case_entries: dict[int, _StateLabel],
|
|
2551
|
+
func: TpyFunction) -> None:
|
|
2552
|
+
"""Walk BBs starting from start_bb, emitting their statements
|
|
2553
|
+
and following Fall/Branch terminators inline. Stops when the
|
|
2554
|
+
terminator is Yield/Return/Raise/Unreachable, or when a
|
|
2555
|
+
Fall/Branch target is a case_entry (then emits a state
|
|
2556
|
+
transition)."""
|
|
2557
|
+
body_indent = self.ctx.indent()
|
|
2558
|
+
cur = start_bb
|
|
2559
|
+
while True:
|
|
2560
|
+
bb = cfg.blocks[cur]
|
|
2561
|
+
# Emit BB statements.
|
|
2562
|
+
for stmt in bb.stmts:
|
|
2563
|
+
if isinstance(stmt, rcfg.AsyncForIterSetup):
|
|
2564
|
+
self._emit_async_for_iter_setup(out, body_indent, stmt, func)
|
|
2565
|
+
elif isinstance(stmt, rcfg.WithEnter):
|
|
2566
|
+
self._emit_with_enter(out, body_indent, stmt)
|
|
2567
|
+
elif isinstance(stmt, rcfg.AsyncWithSetup):
|
|
2568
|
+
self._emit_async_with_setup(out, body_indent, stmt)
|
|
2569
|
+
elif isinstance(stmt, rcfg.AsyncFinallyExit):
|
|
2570
|
+
self._emit_async_finally_exit(out, body_indent, stmt)
|
|
2571
|
+
else:
|
|
2572
|
+
self.statements.gen_stmt(out, stmt)
|
|
2573
|
+
t = bb.terminator
|
|
2574
|
+
if isinstance(t, rcfg.Yield):
|
|
2575
|
+
self._emit_yield_terminator(out, body_indent, t, func)
|
|
2576
|
+
return
|
|
2577
|
+
if isinstance(t, rcfg.ReturnT):
|
|
2578
|
+
# gen_stmt routes a TpyReturn inside async-coro context
|
|
2579
|
+
# through _make_async_return, which walks the active
|
|
2580
|
+
# finally chain (ctx.finally_stack) before emitting the
|
|
2581
|
+
# Poll<T>::ready(...).
|
|
2582
|
+
self.statements.gen_stmt(out, t.return_stmt)
|
|
2583
|
+
return
|
|
2584
|
+
if isinstance(t, rcfg.RaiseT):
|
|
2585
|
+
# Emit the raise as an ordinary TpyRaise statement; the
|
|
2586
|
+
# finally chain is run via C++ exception unwinding.
|
|
2587
|
+
self.statements.gen_stmt(out, t.raise_stmt)
|
|
2588
|
+
return
|
|
2589
|
+
if isinstance(t, rcfg.Unreachable):
|
|
2590
|
+
self._emit_unreachable_tail(out, body_indent, func)
|
|
2591
|
+
return
|
|
2592
|
+
if isinstance(t, rcfg.Fall):
|
|
2593
|
+
if t.next_bb in case_entries:
|
|
2594
|
+
self._emit_exit_region_finallies(
|
|
2595
|
+
out, body_indent,
|
|
2596
|
+
cfg.blocks[cur].region_stack,
|
|
2597
|
+
cfg.blocks[t.next_bb].region_stack)
|
|
2598
|
+
out.write(f"{body_indent}__state = "
|
|
2599
|
+
f"{case_entries[t.next_bb].cpp_name()};\n")
|
|
2600
|
+
out.write(f"{body_indent}continue;\n")
|
|
2601
|
+
return
|
|
2602
|
+
cur = t.next_bb
|
|
2603
|
+
continue
|
|
2604
|
+
if isinstance(t, rcfg.Branch):
|
|
2605
|
+
cond_cpp = self.expressions.gen_expr(t.cond)
|
|
2606
|
+
self.ctx.temps.flush(out, body_indent)
|
|
2607
|
+
out.write(f"{body_indent}if ({cond_cpp}) {{\n")
|
|
2608
|
+
self.ctx.indent_level += 1
|
|
2609
|
+
self._walk_inline_or_jump(out, cfg, t.then_bb, case_entries,
|
|
2610
|
+
func, from_bb=cur)
|
|
2611
|
+
self.ctx.indent_level -= 1
|
|
2612
|
+
out.write(f"{body_indent}}} else {{\n")
|
|
2613
|
+
self.ctx.indent_level += 1
|
|
2614
|
+
self._walk_inline_or_jump(out, cfg, t.else_bb, case_entries,
|
|
2615
|
+
func, from_bb=cur)
|
|
2616
|
+
self.ctx.indent_level -= 1
|
|
2617
|
+
out.write(f"{body_indent}}}\n")
|
|
2618
|
+
return
|
|
2619
|
+
if isinstance(t, rcfg.AsyncForAdvance):
|
|
2620
|
+
self._emit_async_for_advance(
|
|
2621
|
+
out, body_indent, cfg, t, case_entries, func, from_bb=cur)
|
|
2622
|
+
return
|
|
2623
|
+
if isinstance(t, rcfg.MatchDispatch):
|
|
2624
|
+
self._emit_match_dispatch(
|
|
2625
|
+
out, cfg, t, case_entries, func, from_bb=cur)
|
|
2626
|
+
return
|
|
2627
|
+
raise CodeGenError(
|
|
2628
|
+
f"internal: unknown terminator {type(t).__name__}",
|
|
2629
|
+
loc=None)
|
|
2630
|
+
|
|
2631
|
+
def _emit_match_dispatch(self, out: "TextIO", cfg: 'rcfg.CFG',
|
|
2632
|
+
t: 'rcfg.MatchDispatch',
|
|
2633
|
+
case_entries: dict[int, _StateLabel],
|
|
2634
|
+
func: TpyFunction, from_bb: int) -> None:
|
|
2635
|
+
"""Emit a suspending `match` (H1) by reusing the ordinary
|
|
2636
|
+
`gen_match` for the type-aware dispatch, with each arm body routed
|
|
2637
|
+
back through `_walk_inline` via the `_emit_case_body` hook. Arm
|
|
2638
|
+
bodies are inlined here (not their own cases); their internal
|
|
2639
|
+
suspensions split out resume cases as usual. After the dispatch,
|
|
2640
|
+
the (non-exhaustive) fall-through transitions to `join_bb`."""
|
|
2641
|
+
body_indent = self.ctx.indent()
|
|
2642
|
+
# The arm emitter inlines one arm body in place. `match` adds no
|
|
2643
|
+
# region, so the arm BB shares `from_bb`'s region stack -- the
|
|
2644
|
+
# inline walk handles its own fall-to-join / suspension exits.
|
|
2645
|
+
def emit_arm(arm_bb: int) -> None:
|
|
2646
|
+
self._walk_inline(out, cfg, arm_bb, case_entries, func)
|
|
2647
|
+
old_emitter = self.ctx.resumable_arm_emitter
|
|
2648
|
+
old_map = self.ctx.resumable_arm_bb_by_body
|
|
2649
|
+
self.ctx.resumable_arm_emitter = emit_arm
|
|
2650
|
+
self.ctx.resumable_arm_bb_by_body = {
|
|
2651
|
+
id(case.body): arm_bb
|
|
2652
|
+
for case, arm_bb in zip(t.match_stmt.cases, t.arm_bbs)
|
|
2653
|
+
}
|
|
2654
|
+
try:
|
|
2655
|
+
self.statements.match.gen_match(out, t.match_stmt, body_indent)
|
|
2656
|
+
finally:
|
|
2657
|
+
self.ctx.resumable_arm_emitter = old_emitter
|
|
2658
|
+
self.ctx.resumable_arm_bb_by_body = old_map
|
|
2659
|
+
# The dispatch case MUST end in a terminating statement: a switch
|
|
2660
|
+
# whose cases all return/continue still "may fall through" to the
|
|
2661
|
+
# GCC eye (no default), so without this the next state's `case`
|
|
2662
|
+
# label trips -Werror=implicit-fallthrough. When a fall-through is
|
|
2663
|
+
# reachable (`join_bb` set) emit the state transition to join;
|
|
2664
|
+
# otherwise the match is exhaustive AND every arm terminates, so
|
|
2665
|
+
# the post-dispatch point is genuinely unreachable.
|
|
2666
|
+
if t.join_bb is not None:
|
|
2667
|
+
self._walk_inline_or_jump(
|
|
2668
|
+
out, cfg, t.join_bb, case_entries, func, from_bb=from_bb)
|
|
2669
|
+
else:
|
|
2670
|
+
out.write(f"{body_indent}__builtin_unreachable();\n")
|
|
2671
|
+
|
|
2672
|
+
def _walk_inline_or_jump(self, out: "TextIO", cfg: 'rcfg.CFG',
|
|
2673
|
+
target_bb: int,
|
|
2674
|
+
case_entries: dict[int, _StateLabel],
|
|
2675
|
+
func: TpyFunction,
|
|
2676
|
+
from_bb: int) -> None:
|
|
2677
|
+
body_indent = self.ctx.indent()
|
|
2678
|
+
if target_bb in case_entries:
|
|
2679
|
+
self._emit_exit_region_finallies(
|
|
2680
|
+
out, body_indent,
|
|
2681
|
+
cfg.blocks[from_bb].region_stack,
|
|
2682
|
+
cfg.blocks[target_bb].region_stack)
|
|
2683
|
+
out.write(f"{body_indent}__state = "
|
|
2684
|
+
f"{case_entries[target_bb].cpp_name()};\n")
|
|
2685
|
+
out.write(f"{body_indent}continue;\n")
|
|
2686
|
+
else:
|
|
2687
|
+
self._walk_inline(out, cfg, target_bb, case_entries, func)
|
|
2688
|
+
|
|
2689
|
+
def _emit_async_finally_exit(self, out: "TextIO", indent: str,
|
|
2690
|
+
stmt: 'rcfg.AsyncFinallyExit') -> None:
|
|
2691
|
+
"""Emit the deferred-return check + saved-exception rethrow at
|
|
2692
|
+
the dedicated finally exit BB. Both fields are cleared before
|
|
2693
|
+
extraction so a re-entry to the same try (e.g. in a loop)
|
|
2694
|
+
starts with no carried-over state.
|
|
2695
|
+
|
|
2696
|
+
Order matters: the pending-return check runs FIRST so a `return`
|
|
2697
|
+
in the finally body wins over an in-flight exception captured
|
|
2698
|
+
from the try (Python: return-in-finally swallows). The deferred
|
|
2699
|
+
return clears the captured-exc field on the way out so the
|
|
2700
|
+
rethrow check is skipped automatically.
|
|
2701
|
+
|
|
2702
|
+
The pending branch has two outcomes depending on whether an
|
|
2703
|
+
enclosing CFG-finally region is active at this exit site (set
|
|
2704
|
+
in `ctx.async_pending_return_*` by `_emit_case` from this BB's
|
|
2705
|
+
region_stack):
|
|
2706
|
+
* Not active: replay locally -- walk outer helpers, then
|
|
2707
|
+
emit Poll::ready / StopIteration as the coro's exit.
|
|
2708
|
+
* Active: forward -- move this exit's parked value into the
|
|
2709
|
+
outer's slot, set the outer's flag, walk helpers down to
|
|
2710
|
+
the outer boundary, transition to the outer's
|
|
2711
|
+
`finally_entry_bb`. The outer's `AsyncFinallyExit`
|
|
2712
|
+
eventually performs the local replay (or forwards again
|
|
2713
|
+
for deeper nesting)."""
|
|
2714
|
+
f = stmt.captured_exc_field
|
|
2715
|
+
inner = indent + INDENT
|
|
2716
|
+
if stmt.pending_return_flag is not None:
|
|
2717
|
+
flag = stmt.pending_return_flag
|
|
2718
|
+
out.write(f"{indent}if (this->{flag}) {{\n")
|
|
2719
|
+
out.write(f"{inner}this->{flag} = false;\n")
|
|
2720
|
+
# Swallow any in-flight exception captured by the try
|
|
2721
|
+
# region's catch: Python semantics say return-in-finally
|
|
2722
|
+
# wins, including over a propagating raise. For a return
|
|
2723
|
+
# from the try body (no in-flight exception), this is a
|
|
2724
|
+
# no-op.
|
|
2725
|
+
out.write(f"{inner}this->{f} = nullptr;\n")
|
|
2726
|
+
# If an enclosing CFG-based finally is active (set in ctx
|
|
2727
|
+
# by `_emit_case` from this BB's region_stack), forward the
|
|
2728
|
+
# parked state into the outer's slots and transition to its
|
|
2729
|
+
# finally_entry instead of replaying locally -- otherwise
|
|
2730
|
+
# the outer finally body would be skipped.
|
|
2731
|
+
outer_flag = self.ctx.async_pending_return_flag
|
|
2732
|
+
if outer_flag is not None:
|
|
2733
|
+
outer_slot = self.ctx.async_pending_return_slot
|
|
2734
|
+
outer_target = self.ctx.async_pending_return_target_state
|
|
2735
|
+
outer_boundary = self.ctx.async_pending_return_boundary
|
|
2736
|
+
inner_slot = stmt.pending_return_slot
|
|
2737
|
+
if outer_slot is not None and inner_slot is not None:
|
|
2738
|
+
out.write(f"{inner}this->{outer_slot} = "
|
|
2739
|
+
f"std::move(this->{inner_slot});\n")
|
|
2740
|
+
out.write(f"{inner}this->{outer_flag} = true;\n")
|
|
2741
|
+
# Walk helpers between this exit and the outer finally
|
|
2742
|
+
# entry; helpers BELOW the outer (`outer_boundary`) stay
|
|
2743
|
+
# on the stack to run inside the outer's finally region.
|
|
2744
|
+
self.statements._emit_finally_chain(out, inner,
|
|
2745
|
+
stop_at=outer_boundary)
|
|
2746
|
+
out.write(f"{inner}__state = {outer_target};\n")
|
|
2747
|
+
out.write(f"{inner}continue;\n")
|
|
2748
|
+
else:
|
|
2749
|
+
# No outer CFG finally: replay locally. Walk any outer
|
|
2750
|
+
# helpers (an enclosing helper-based finally outside
|
|
2751
|
+
# this CFG-based one) before emitting the actual return.
|
|
2752
|
+
self.statements._emit_finally_chain(out, inner)
|
|
2753
|
+
if self._is_generator_shape():
|
|
2754
|
+
done_state = self.ctx.generator_resumable_done_state or "S_DONE"
|
|
2755
|
+
out.write(f"{inner}__state = {done_state};\n")
|
|
2756
|
+
out.write(f"{inner}return ::tpy::make_unexpected("
|
|
2757
|
+
f"::tpy::StopIteration{{}});\n")
|
|
2758
|
+
else:
|
|
2759
|
+
done_state = self.ctx.async_coro_done_state or "S_DONE"
|
|
2760
|
+
out.write(f"{inner}__state = {done_state};\n")
|
|
2761
|
+
slot = stmt.pending_return_slot
|
|
2762
|
+
ret_cpp = self.ctx.async_coro_return_cpp
|
|
2763
|
+
if slot is None or ret_cpp == "void":
|
|
2764
|
+
out.write(f"{inner}{POLL_VOID_READY_RETURN}\n")
|
|
2765
|
+
else:
|
|
2766
|
+
out.write(f"{inner}return ::tpystd::tpy::Poll<{ret_cpp}>::ready"
|
|
2767
|
+
f"(std::move(this->{slot}));\n")
|
|
2768
|
+
out.write(f"{indent}}}\n")
|
|
2769
|
+
out.write(f"{indent}if (this->{f}) {{\n")
|
|
2770
|
+
out.write(f"{inner}std::exception_ptr __tmp = this->{f};\n")
|
|
2771
|
+
out.write(f"{inner}this->{f} = nullptr;\n")
|
|
2772
|
+
out.write(f"{inner}std::rethrow_exception(__tmp);\n")
|
|
2773
|
+
out.write(f"{indent}}}\n")
|
|
2774
|
+
|
|
2775
|
+
def _emit_with_enter(self, out: "TextIO", indent: str,
|
|
2776
|
+
stmt: 'rcfg.WithEnter') -> None:
|
|
2777
|
+
"""Emit the with-stmt setup sequence:
|
|
2778
|
+
__with_ctx_<n>.emplace(<context_expr>);
|
|
2779
|
+
<target> = (*__with_ctx_<n>).__enter__(); # if target
|
|
2780
|
+
(*__with_ctx_<n>).__enter__(); # else
|
|
2781
|
+
"""
|
|
2782
|
+
ctx_n = stmt.ctx_n
|
|
2783
|
+
item = stmt.item
|
|
2784
|
+
ctx_expr = self.expressions.gen_expr(item.context_expr)
|
|
2785
|
+
self.ctx.temps.flush(out, indent)
|
|
2786
|
+
out.write(f"{indent}__with_ctx_{ctx_n}.emplace({ctx_expr});\n")
|
|
2787
|
+
if item.target is not None:
|
|
2788
|
+
target = escape_cpp_name(item.target)
|
|
2789
|
+
enter_call = f"(*__with_ctx_{ctx_n}).__enter__()"
|
|
2790
|
+
if item.target in self.ctx.generator_frame_slot_locals:
|
|
2791
|
+
# A non-value `as`-target hoisted across a yield is backed by
|
|
2792
|
+
# `tpy::frame_slot<T>` (deleted operator=); construct via
|
|
2793
|
+
# emplace, mirroring the var-decl / reassign frame-slot paths.
|
|
2794
|
+
# Pointer-form Optional targets stay `T*` and bind with `=`.
|
|
2795
|
+
out.write(f"{indent}{target}.emplace({enter_call});\n")
|
|
2796
|
+
else:
|
|
2797
|
+
out.write(f"{indent}{target} = {enter_call};\n")
|
|
2798
|
+
else:
|
|
2799
|
+
out.write(f"{indent}(*__with_ctx_{ctx_n}).__enter__();\n")
|
|
2800
|
+
|
|
2801
|
+
def _emit_async_with_setup(self, out: "TextIO", indent: str,
|
|
2802
|
+
stmt: 'rcfg.AsyncWithSetup') -> None:
|
|
2803
|
+
"""Emit the async-with frame-slot population:
|
|
2804
|
+
__with_ctx_<n>.emplace(<context_expr>);
|
|
2805
|
+
(Construction into a `tpy::frame_slot<CM>` frame field;
|
|
2806
|
+
`frame_slot::operator=` is deleted, so writes go through
|
|
2807
|
+
`.emplace(...)`.) Subsequent Yield BBs (aenter/aexit) emplace
|
|
2808
|
+
`__sub_<i>` with `(*__with_ctx_<n>, ...)`."""
|
|
2809
|
+
ctx_n = stmt.ctx_n
|
|
2810
|
+
item = stmt.item
|
|
2811
|
+
ctx_expr = self.expressions.gen_expr(item.context_expr)
|
|
2812
|
+
self.ctx.temps.flush(out, indent)
|
|
2813
|
+
out.write(f"{indent}__with_ctx_{ctx_n}.emplace({ctx_expr});\n")
|
|
2814
|
+
|
|
2815
|
+
def _emit_with_exit(self, out: "TextIO", indent: str,
|
|
2816
|
+
region: 'rcfg.WithRegion',
|
|
2817
|
+
on_exception: bool) -> None:
|
|
2818
|
+
"""Emit a single `__exit__` call. `on_exception=True` passes the
|
|
2819
|
+
catch-bound `__exc_<n>` (or `nullptr` when sema marked
|
|
2820
|
+
exit_takes_exc_val=False); otherwise passes the "normal exit"
|
|
2821
|
+
args (all empty / nullptr)."""
|
|
2822
|
+
item = region.item
|
|
2823
|
+
n = region.ctx_n
|
|
2824
|
+
if on_exception and item.exit_takes_exc_val:
|
|
2825
|
+
exc_arg = f"&__exc_{n}"
|
|
2826
|
+
elif item.exit_takes_exc_val:
|
|
2827
|
+
exc_arg = "nullptr"
|
|
2828
|
+
else:
|
|
2829
|
+
exc_arg = "{}"
|
|
2830
|
+
out.write(f"{indent}(*__with_ctx_{n}).__exit__("
|
|
2831
|
+
f"{{}}, {exc_arg}, {{}});\n")
|
|
2832
|
+
|
|
2833
|
+
def _for_info(self, func: TpyFunction, uid: int) -> 'GeneratorForInfo | None':
|
|
2834
|
+
return rcfg.resumable_state(func).for_info_by_uid.get(uid)
|
|
2835
|
+
|
|
2836
|
+
def _for_src_access(self, out: "TextIO", indent: str,
|
|
2837
|
+
iterable_expr: 'TpyExpr', uid: int,
|
|
2838
|
+
info: 'GeneratorForInfo') -> str:
|
|
2839
|
+
"""Resolve the for-loop source expression for the begin_end / next
|
|
2840
|
+
strategies. When the iterable is a temporary (the pre-scan allocated
|
|
2841
|
+
`__for_src_<uid>`), store it once here and return the stored access;
|
|
2842
|
+
otherwise return the (re-evaluable) named expression."""
|
|
2843
|
+
src_cpp = self.expressions.gen_expr(iterable_expr)
|
|
2844
|
+
self.ctx.temps.flush(out, indent)
|
|
2845
|
+
if any(fn == f"__for_src_{uid}" for fn, _ in info.fields):
|
|
2846
|
+
out.write(f"{indent}__for_src_{uid}.emplace({src_cpp});\n")
|
|
2847
|
+
return f"(*__for_src_{uid})"
|
|
2848
|
+
return src_cpp
|
|
2849
|
+
|
|
2850
|
+
def _emit_async_for_iter_setup(self, out: "TextIO", indent: str,
|
|
2851
|
+
stmt: 'rcfg.AsyncForIterSetup',
|
|
2852
|
+
func: TpyFunction) -> None:
|
|
2853
|
+
"""Initialize for-loop iteration state into the frame, per strategy
|
|
2854
|
+
(mirrors the legacy `_gen_generator_for_*` peephole init so range /
|
|
2855
|
+
begin_end generators keep their fast shape):
|
|
2856
|
+
async_for: `__for_itr.emplace((it).__aiter__())`
|
|
2857
|
+
range: `__for_i`/`__for_stop`[/`__for_step`] counters
|
|
2858
|
+
begin_end: `[__for_src.emplace(...);] __for_it.emplace(src.begin()); __for_end.emplace(...end())`
|
|
2859
|
+
next: `[__for_src.emplace(...);]` (the source IS the iterator)
|
|
2860
|
+
iter_next: `__for_itr.emplace(::tpy::__iter__(src))`
|
|
2861
|
+
"""
|
|
2862
|
+
uid = stmt.uid
|
|
2863
|
+
if stmt.is_async:
|
|
2864
|
+
iter_cpp = self.expressions.gen_expr(stmt.iterable_expr)
|
|
2865
|
+
self.ctx.temps.flush(out, indent)
|
|
2866
|
+
out.write(f"{indent}__for_itr_{uid}.emplace(({iter_cpp}).__aiter__());\n")
|
|
2867
|
+
return
|
|
2868
|
+
info = self._for_info(func, uid)
|
|
2869
|
+
strat = info.strategy if info else "iter_next"
|
|
2870
|
+
if strat == "range":
|
|
2871
|
+
self._emit_for_range_setup(out, indent, stmt, uid)
|
|
2872
|
+
elif strat == "begin_end":
|
|
2873
|
+
src = self._for_src_access(out, indent, stmt.iterable_expr, uid, info)
|
|
2874
|
+
out.write(f"{indent}__for_it_{uid}.emplace(({src}).begin());\n")
|
|
2875
|
+
out.write(f"{indent}__for_end_{uid}.emplace(({src}).end());\n")
|
|
2876
|
+
elif strat == "next":
|
|
2877
|
+
# Source IS the iterator; just stash a temporary if needed.
|
|
2878
|
+
self._for_src_access(out, indent, stmt.iterable_expr, uid, info)
|
|
2879
|
+
else: # iter_next (universal)
|
|
2880
|
+
iter_cpp = self.expressions.gen_expr(stmt.iterable_expr)
|
|
2881
|
+
self.ctx.temps.flush(out, indent)
|
|
2882
|
+
out.write(f"{indent}__for_itr_{uid}.emplace(::tpy::__iter__({iter_cpp}));\n")
|
|
2883
|
+
|
|
2884
|
+
def _emit_for_range_setup(self, out: "TextIO", indent: str,
|
|
2885
|
+
stmt: 'rcfg.AsyncForIterSetup', uid: int) -> None:
|
|
2886
|
+
"""range() counter init into the frame counters."""
|
|
2887
|
+
range_call = stmt.iterable_expr
|
|
2888
|
+
elem_type = stmt.elem_type
|
|
2889
|
+
if elem_type and isinstance(elem_type, IntLiteralType):
|
|
2890
|
+
elem_type = self.ctx.analyzer.ctx.default_int_type
|
|
2891
|
+
cpp_elem = self.types.type_to_cpp(elem_type) if elem_type else "int32_t"
|
|
2892
|
+
gen_args = self.statements.builtins.gen_range_args(range_call)
|
|
2893
|
+
self.ctx.temps.flush(out, indent)
|
|
2894
|
+
nargs = len(gen_args)
|
|
2895
|
+
ci, st = f"__for_i_{uid}", f"__for_stop_{uid}"
|
|
2896
|
+
if nargs == 1:
|
|
2897
|
+
out.write(f"{indent}{ci}.emplace({cpp_elem}(0));\n")
|
|
2898
|
+
out.write(f"{indent}{st}.emplace(static_cast<{cpp_elem}>({gen_args[0]}));\n")
|
|
2899
|
+
else:
|
|
2900
|
+
out.write(f"{indent}{ci}.emplace(static_cast<{cpp_elem}>({gen_args[0]}));\n")
|
|
2901
|
+
out.write(f"{indent}{st}.emplace(static_cast<{cpp_elem}>({gen_args[1]}));\n")
|
|
2902
|
+
if nargs == 3:
|
|
2903
|
+
sp = f"__for_step_{uid}"
|
|
2904
|
+
out.write(f"{indent}{sp}.emplace(static_cast<{cpp_elem}>({gen_args[2]}));\n")
|
|
2905
|
+
if self.statements._extract_int_literal(range_call.args[2]) is None:
|
|
2906
|
+
out.write(f"{indent}::tpy::range_check_step_nonzero(*{sp});\n")
|
|
2907
|
+
if not is_big_int_type(elem_type):
|
|
2908
|
+
self.statements._gen_range_overflow_check(
|
|
2909
|
+
out, indent, f"*{ci}", f"*{st}", f"*{sp}", elem_type)
|
|
2910
|
+
|
|
2911
|
+
def _for_advance_parts(self, func: TpyFunction,
|
|
2912
|
+
t: 'rcfg.AsyncForAdvance') -> tuple[str, str, list[str]]:
|
|
2913
|
+
"""Return (pre, exhausted_test, bind_post) for the AsyncForAdvance,
|
|
2914
|
+
per strategy. `pre` runs before the exhaustion test; the loop exits
|
|
2915
|
+
when `exhausted_test` is true; `bind_post` is the list of statements
|
|
2916
|
+
that bind the loop var (and advance the cursor) on the live path."""
|
|
2917
|
+
uid = t.uid
|
|
2918
|
+
stmt = t.stmt
|
|
2919
|
+
info = self._for_info(func, uid)
|
|
2920
|
+
strat = info.strategy if info else "iter_next"
|
|
2921
|
+
cpp_var = escape_cpp_name(stmt.var)
|
|
2922
|
+
if strat == "range":
|
|
2923
|
+
ci, st = f"(*__for_i_{uid})", f"(*__for_stop_{uid})"
|
|
2924
|
+
range_call = stmt.iterable
|
|
2925
|
+
nargs = len(range_call.args)
|
|
2926
|
+
if nargs == 3:
|
|
2927
|
+
sp = f"(*__for_step_{uid})"
|
|
2928
|
+
step_lit = self.statements._extract_int_literal(range_call.args[2])
|
|
2929
|
+
if step_lit is not None and step_lit > 0:
|
|
2930
|
+
cont = f"{ci} < {st}"
|
|
2931
|
+
elif step_lit is not None and step_lit < 0:
|
|
2932
|
+
cont = f"{ci} > {st}"
|
|
2933
|
+
else:
|
|
2934
|
+
cont = f"({sp} > 0 ? {ci} < {st} : {ci} > {st})"
|
|
2935
|
+
bind_post = [f"{cpp_var} = {ci};", f"{ci} += {sp};"]
|
|
2936
|
+
else:
|
|
2937
|
+
cont = f"{ci} < {st}"
|
|
2938
|
+
bind_post = [f"{cpp_var} = ({ci})++;"]
|
|
2939
|
+
return ("", f"!({cont})", bind_post)
|
|
2940
|
+
if strat == "begin_end":
|
|
2941
|
+
it, end = f"(*__for_it_{uid})", f"(*__for_end_{uid})"
|
|
2942
|
+
if info.pointer_form_loop_var == stmt.var:
|
|
2943
|
+
bind_post = [f"{cpp_var} = &(*({it})++);"]
|
|
2944
|
+
else:
|
|
2945
|
+
bind_post = [f"{cpp_var} = *({it})++;"]
|
|
2946
|
+
return ("", f"{it} == {end}", bind_post)
|
|
2947
|
+
# next / iter_next: __next__() into __for_r, exhaust on !has_value.
|
|
2948
|
+
r = f"(*__for_r_{uid})"
|
|
2949
|
+
if strat == "next":
|
|
2950
|
+
src = self._for_src_expr(stmt, uid, info)
|
|
2951
|
+
pre = f"__for_r_{uid}.emplace({src}.__next__());"
|
|
2952
|
+
else: # iter_next
|
|
2953
|
+
pre = f"__for_r_{uid}.emplace((*__for_itr_{uid}).__next__());"
|
|
2954
|
+
elem = f"::tpy::unwrap_ref(*{r})"
|
|
2955
|
+
# Bind form mirrors the loop var's frame storage shape (D2a):
|
|
2956
|
+
# pointer-form `T*` (alias), frame_slot `.emplace`, or value assign.
|
|
2957
|
+
if info is not None and info.pointer_form_loop_var == stmt.var:
|
|
2958
|
+
bind_post = [f"{cpp_var} = &({elem});"]
|
|
2959
|
+
elif stmt.var in self.ctx.generator_frame_slot_locals:
|
|
2960
|
+
bind_post = [f"{cpp_var}.emplace({elem});"]
|
|
2961
|
+
else:
|
|
2962
|
+
bind_post = [f"{cpp_var} = {elem};"]
|
|
2963
|
+
return (pre, f"!{r}.has_value()", bind_post)
|
|
2964
|
+
|
|
2965
|
+
def _for_src_expr(self, stmt: 'TpyForEach', uid: int,
|
|
2966
|
+
info: 'GeneratorForInfo') -> str:
|
|
2967
|
+
"""The iterator source for the `next` strategy advance: the stored
|
|
2968
|
+
`__for_src` (temporary) or the re-referencable named expression."""
|
|
2969
|
+
if any(fn == f"__for_src_{uid}" for fn, _ in info.fields):
|
|
2970
|
+
return f"(*__for_src_{uid})"
|
|
2971
|
+
return self.expressions.gen_expr(stmt.iterable)
|
|
2972
|
+
|
|
2973
|
+
def _emit_async_for_advance(self, out: "TextIO", indent: str,
|
|
2974
|
+
cfg: 'rcfg.CFG',
|
|
2975
|
+
t: 'rcfg.AsyncForAdvance',
|
|
2976
|
+
case_entries: dict[int, _StateLabel],
|
|
2977
|
+
func: TpyFunction,
|
|
2978
|
+
from_bb: int) -> None:
|
|
2979
|
+
"""Emit the per-iteration advance for an AsyncForAdvance terminator
|
|
2980
|
+
(strategy-specific check + bind), then transfer to the body
|
|
2981
|
+
(`has_value_bb`) or loop exit (`exhausted_bb`)."""
|
|
2982
|
+
pre, exhausted_test, bind_post = self._for_advance_parts(func, t)
|
|
2983
|
+
if pre:
|
|
2984
|
+
out.write(f"{indent}{pre}\n")
|
|
2985
|
+
out.write(f"{indent}if ({exhausted_test}) {{\n")
|
|
2986
|
+
self.ctx.indent_level += 1
|
|
2987
|
+
inner = self.ctx.indent()
|
|
2988
|
+
self._emit_exit_region_finallies(
|
|
2989
|
+
out, inner,
|
|
2990
|
+
cfg.blocks[from_bb].region_stack,
|
|
2991
|
+
cfg.blocks[t.exhausted_bb].region_stack)
|
|
2992
|
+
if t.exhausted_bb in case_entries:
|
|
2993
|
+
out.write(f"{inner}__state = "
|
|
2994
|
+
f"{case_entries[t.exhausted_bb].cpp_name()};\n")
|
|
2995
|
+
out.write(f"{inner}continue;\n")
|
|
2996
|
+
else:
|
|
2997
|
+
# Exit BB isn't a case entry (shouldn't happen given
|
|
2998
|
+
# _compute_case_entries counts AsyncForAdvance successors,
|
|
2999
|
+
# but stay defensive: walk inline).
|
|
3000
|
+
self._walk_inline(out, cfg, t.exhausted_bb, case_entries, func)
|
|
3001
|
+
self.ctx.indent_level -= 1
|
|
3002
|
+
out.write(f"{indent}}}\n")
|
|
3003
|
+
# has_value path -- bind loop var (and advance the cursor) then body.
|
|
3004
|
+
for line in bind_post:
|
|
3005
|
+
out.write(f"{indent}{line}\n")
|
|
3006
|
+
self._walk_inline_or_jump(
|
|
3007
|
+
out, cfg, t.has_value_bb, case_entries, func, from_bb=from_bb)
|
|
3008
|
+
|
|
3009
|
+
def _emit_unreachable_tail(self, out: "TextIO", indent: str,
|
|
3010
|
+
func: TpyFunction) -> None:
|
|
3011
|
+
"""Tail emission for a BB whose end is statically unreachable
|
|
3012
|
+
(no explicit return/raise). Generators that fall off the end stop
|
|
3013
|
+
iterating (StopIteration). For void async defs, emit Ready(unit);
|
|
3014
|
+
otherwise panic."""
|
|
3015
|
+
if self._is_generator_shape():
|
|
3016
|
+
# Fell off the end of the generator body -> StopIteration.
|
|
3017
|
+
self.statements._emit_finally_chain(out, indent)
|
|
3018
|
+
out.write(f"{indent}__state = S_DONE;\n")
|
|
3019
|
+
out.write(f"{indent}return ::tpy::make_unexpected("
|
|
3020
|
+
f"::tpy::StopIteration{{}});\n")
|
|
3021
|
+
return
|
|
3022
|
+
if self._is_void_return(func):
|
|
3023
|
+
# Walk any active finally frames before returning.
|
|
3024
|
+
self.statements._emit_finally_chain(out, indent)
|
|
3025
|
+
out.write(f"{indent}__state = S_DONE;\n")
|
|
3026
|
+
out.write(f"{indent}{POLL_VOID_READY_RETURN}\n")
|
|
3027
|
+
else:
|
|
3028
|
+
out.write(f"{indent}::tpy::tpy_panic(\"async def fell "
|
|
3029
|
+
f"through without returning a value\");\n")
|
|
3030
|
+
|
|
3031
|
+
@staticmethod
|
|
3032
|
+
def _sub_field_name(suspension_index: int) -> str:
|
|
3033
|
+
return f"__sub_{suspension_index}"
|
|
3034
|
+
|
|
3035
|
+
def _gen_coro_emplace_arg(self, arg: 'TpyExpr', arg_index: int,
|
|
3036
|
+
call: 'TpyCall | TpyMethodCall') -> str:
|
|
3037
|
+
"""Generate one arg for `__sub_N.emplace(...)` constructing a
|
|
3038
|
+
sub-coroutine. Mirrors the param-type-driven coercions sync call
|
|
3039
|
+
codegen applies in `_gen_call`: pointer-form `Optional[NonValue]`
|
|
3040
|
+
lift (P -> &P) when the callee param is the `T*` shape, the
|
|
3041
|
+
@dynamic-protocol Adapter wrap when the callee takes `Own[P]`
|
|
3042
|
+
(needed for `await wait_for(coro, ...)` so the concrete coro
|
|
3043
|
+
gets boxed into `unique_ptr<Cancellable<T>>`), plus the full
|
|
3044
|
+
`Own[T]` move-out machinery (`std::move(name)` for last-use
|
|
3045
|
+
movable lvalues bound to `Own[T]` params) via `gen_call_arg`.
|
|
3046
|
+
"""
|
|
3047
|
+
fi = call.resolved_function_info
|
|
3048
|
+
if fi is None or arg_index >= len(fi.params):
|
|
3049
|
+
return self.expressions.gen_expr(arg)
|
|
3050
|
+
ptype = fi.params[arg_index].type
|
|
3051
|
+
opt_arg = self.expressions._gen_optional_ptr_arg(arg, ptype)
|
|
3052
|
+
if opt_arg is not None:
|
|
3053
|
+
return opt_arg
|
|
3054
|
+
dynamic_arg = self.expressions._gen_dynamic_protocol_arg(arg, ptype)
|
|
3055
|
+
if dynamic_arg is not None:
|
|
3056
|
+
return dynamic_arg
|
|
3057
|
+
union_arg = self.expressions._gen_union_arg(
|
|
3058
|
+
arg, ptype, is_readonly_target=fi.is_readonly)
|
|
3059
|
+
if union_arg is not None:
|
|
3060
|
+
return union_arg
|
|
3061
|
+
return self.expressions.gen_call_arg(arg, ptype)
|
|
3062
|
+
|
|
3063
|
+
def _emit_sub_reset(self, out: "TextIO", indent: str,
|
|
3064
|
+
payload: 'rcfg.AwaitPayload',
|
|
3065
|
+
suspension_index: int) -> None:
|
|
3066
|
+
sub = self._sub_field_name(suspension_index)
|
|
3067
|
+
if payload.mode is rcfg.AwaitMode.BORROWED:
|
|
3068
|
+
out.write(f"{indent}{sub} = nullptr;\n")
|
|
3069
|
+
elif (payload.mode is rcfg.AwaitMode.INLINE
|
|
3070
|
+
or payload.mode is rcfg.AwaitMode.ERASED):
|
|
3071
|
+
out.write(f"{indent}{sub}.reset();\n")
|
|
3072
|
+
else:
|
|
3073
|
+
raise CodeGenError(f"unknown await mode {payload.mode!r}",
|
|
3074
|
+
loc=None)
|
|
3075
|
+
|
|
3076
|
+
def _emit_resume_core(self, out: "TextIO", indent: str,
|
|
3077
|
+
payload: 'rcfg.AwaitPayload',
|
|
3078
|
+
suspension_index: int,
|
|
3079
|
+
func: TpyFunction) -> None:
|
|
3080
|
+
"""Emit the cancel check + poll + bind step at the start of a
|
|
3081
|
+
resume case body. Returns: caller continues with post-resume
|
|
3082
|
+
statements; for RETURN-kind, this function fully terminates the
|
|
3083
|
+
case body (walks finally chain and returns Ready)."""
|
|
3084
|
+
ret_cpp = self._poll_ret_cpp(func)
|
|
3085
|
+
sub = self._sub_field_name(suspension_index)
|
|
3086
|
+
# `::tpy::poll_with_cancel` propagates the outer's cancel into
|
|
3087
|
+
# the in-flight sub before polling (so the sub observes the
|
|
3088
|
+
# cancel at its own suspension point and can run
|
|
3089
|
+
# `finally`-with-await cleanup) and throws CancelledError if
|
|
3090
|
+
# the sub races past the cancel. See runtime/cpp/include/tpy/
|
|
3091
|
+
# async.hpp for the full semantics.
|
|
3092
|
+
out.write(f"{indent}auto __r{suspension_index} = "
|
|
3093
|
+
f"::tpy::poll_with_cancel({sub}, __cancel_pending, "
|
|
3094
|
+
f"waker);\n")
|
|
3095
|
+
out.write(f"{indent}if (__r{suspension_index}.is_pending()) "
|
|
3096
|
+
f"return {ret_cpp}::pending();\n")
|
|
3097
|
+
moved = f"std::move(__r{suspension_index}).value()"
|
|
3098
|
+
if (payload.kind is rcfg.AwaitKind.ASSIGN
|
|
3099
|
+
or payload.kind is rcfg.AwaitKind.VARDECL):
|
|
3100
|
+
target = escape_cpp_name(payload.bind_target)
|
|
3101
|
+
if payload.bind_target in self.ctx.generator_optional_fields:
|
|
3102
|
+
out.write(f"{indent}{target}.emplace({moved});\n")
|
|
3103
|
+
else:
|
|
3104
|
+
out.write(f"{indent}{target} = {moved};\n")
|
|
3105
|
+
elif payload.kind is rcfg.AwaitKind.RETURN:
|
|
3106
|
+
out.write(f"{indent}auto __ret{suspension_index} = {moved};\n")
|
|
3107
|
+
self._emit_sub_reset(out, indent, payload, suspension_index)
|
|
3108
|
+
# When a CFG-based finally is active, route the
|
|
3109
|
+
# `return await X` through the pending-return slot + flag
|
|
3110
|
+
# and transition to the finally entry instead of emitting
|
|
3111
|
+
# Poll::ready directly; AsyncFinallyExit at the finally
|
|
3112
|
+
# tail will emit the deferred Poll::ready after the
|
|
3113
|
+
# rethrow check.
|
|
3114
|
+
pending_flag = self.ctx.async_pending_return_flag
|
|
3115
|
+
if pending_flag is not None:
|
|
3116
|
+
pending_slot = self.ctx.async_pending_return_slot
|
|
3117
|
+
target_state = self.ctx.async_pending_return_target_state
|
|
3118
|
+
boundary = self.ctx.async_pending_return_boundary
|
|
3119
|
+
assert target_state is not None
|
|
3120
|
+
if pending_slot is not None and not self._is_void_return(func):
|
|
3121
|
+
out.write(f"{indent}this->{pending_slot} = "
|
|
3122
|
+
f"std::move(__ret{suspension_index});\n")
|
|
3123
|
+
else:
|
|
3124
|
+
out.write(f"{indent}(void)__ret{suspension_index};\n")
|
|
3125
|
+
out.write(f"{indent}this->{pending_flag} = true;\n")
|
|
3126
|
+
terminated = self.statements._emit_finally_chain(
|
|
3127
|
+
out, indent, stop_at=boundary)
|
|
3128
|
+
if not terminated:
|
|
3129
|
+
out.write(f"{indent}__state = {target_state};\n")
|
|
3130
|
+
out.write(f"{indent}continue;\n")
|
|
3131
|
+
return
|
|
3132
|
+
self.statements._emit_finally_chain(out, indent)
|
|
3133
|
+
if self._is_void_return(func):
|
|
3134
|
+
out.write(f"{indent}(void)__ret{suspension_index};\n")
|
|
3135
|
+
out.write(f"{indent}__state = S_DONE;\n")
|
|
3136
|
+
out.write(f"{indent}{POLL_VOID_READY_RETURN}\n")
|
|
3137
|
+
else:
|
|
3138
|
+
out.write(f"{indent}__state = S_DONE;\n")
|
|
3139
|
+
out.write(f"{indent}return ::tpystd::tpy::Poll<"
|
|
3140
|
+
f"{self._ret_cpp(func)}>::ready("
|
|
3141
|
+
f"std::move(__ret{suspension_index}));\n")
|
|
3142
|
+
return
|
|
3143
|
+
elif payload.kind is rcfg.AwaitKind.DISCARD:
|
|
3144
|
+
out.write(f"{indent}(void){moved};\n")
|
|
3145
|
+
else:
|
|
3146
|
+
raise CodeGenError(f"unknown await kind {payload.kind!r}",
|
|
3147
|
+
loc=None)
|
|
3148
|
+
self._emit_sub_reset(out, indent, payload, suspension_index)
|
|
3149
|
+
|
|
3150
|
+
def _emit_yield_terminator(self, out: "TextIO", indent: str,
|
|
3151
|
+
t: 'rcfg.Yield', func: TpyFunction) -> None:
|
|
3152
|
+
"""Emit the full suspend action for a Yield terminator (policy
|
|
3153
|
+
seam). Async stores the sub-future + advances state, then
|
|
3154
|
+
`continue`s to re-enter the switch at the new state -- the resume
|
|
3155
|
+
case's poll() returns Pending iff the sub-future is genuinely
|
|
3156
|
+
not-yet-ready; continuing (rather than returning Pending
|
|
3157
|
+
unconditionally) lets a synchronously-ready sub-future complete
|
|
3158
|
+
in one poll. The generator shape emits the yielded value +
|
|
3159
|
+
advances state + returns the value to the caller (the next
|
|
3160
|
+
`__next__()` call resumes at the new state)."""
|
|
3161
|
+
if self._is_generator_shape():
|
|
3162
|
+
self._emit_generator_yield(out, indent, t)
|
|
3163
|
+
return
|
|
3164
|
+
self._emit_suspend(out, indent, t.payload, t.suspension_index, func)
|
|
3165
|
+
out.write(f"{indent}continue;\n")
|
|
3166
|
+
|
|
3167
|
+
def _emit_generator_yield(self, out: "TextIO", indent: str,
|
|
3168
|
+
t: 'rcfg.Yield') -> None:
|
|
3169
|
+
"""Generator suspension: compute the yielded value, advance to the
|
|
3170
|
+
resume state, and return it to the caller. The next `__next__()`
|
|
3171
|
+
call re-enters the switch at the resume state, whose case header
|
|
3172
|
+
is a no-op (no poll/bind) and simply continues after the yield."""
|
|
3173
|
+
payload = t.payload
|
|
3174
|
+
assert isinstance(payload, rcfg.YieldPayload), \
|
|
3175
|
+
"generator shape requires a YieldPayload terminator"
|
|
3176
|
+
ys = payload.yield_stmt
|
|
3177
|
+
assert ys is not None, \
|
|
3178
|
+
"YieldPayload built from a generator body always carries yield_stmt"
|
|
3179
|
+
if ys.loc is not None:
|
|
3180
|
+
self.ctx.emit_source_comment(out, ys.loc, indent)
|
|
3181
|
+
yield_expr = self.statements.gen_yield_value(ys)
|
|
3182
|
+
self.ctx.temps.flush(out, indent)
|
|
3183
|
+
resume = _StateLabel(_StateKind.RESUME, t.suspension_index).cpp_name()
|
|
3184
|
+
out.write(f"{indent}__state = {resume};\n")
|
|
3185
|
+
out.write(f"{indent}return {yield_expr};\n")
|
|
3186
|
+
|
|
3187
|
+
def _emit_suspend(self, out: "TextIO", indent: str,
|
|
3188
|
+
payload: 'rcfg.AwaitPayload',
|
|
3189
|
+
suspension_index: int,
|
|
3190
|
+
func: TpyFunction) -> None:
|
|
3191
|
+
"""Emit the emplace + state-advance step at a Yield terminator.
|
|
3192
|
+
|
|
3193
|
+
Inline mode: emplace the sub-coro struct directly via its ctor.
|
|
3194
|
+
Erased mode: move the operand value into the optional field.
|
|
3195
|
+
Borrowed mode: store the operand's address in the pointer field.
|
|
3196
|
+
"""
|
|
3197
|
+
if payload.host_stmt is not None and payload.host_stmt.loc is not None:
|
|
3198
|
+
self.ctx.emit_source_comment(out, payload.host_stmt.loc, indent)
|
|
3199
|
+
sub = self._sub_field_name(suspension_index)
|
|
3200
|
+
if payload.mode is rcfg.AwaitMode.INLINE:
|
|
3201
|
+
if payload.async_with_kind is not None:
|
|
3202
|
+
# Async-with synthetic yield. Receiver is the CM frame
|
|
3203
|
+
# slot; args differ by kind.
|
|
3204
|
+
ctx_n = payload.async_with_ctx_n
|
|
3205
|
+
recv = f"(*__with_ctx_{ctx_n})"
|
|
3206
|
+
if payload.async_with_kind is rcfg.AsyncWithKind.AENTER:
|
|
3207
|
+
out.write(f"{indent}{sub}.emplace({recv});\n")
|
|
3208
|
+
else: # AEXIT -- cleanup-only call with all-None args
|
|
3209
|
+
out.write(f"{indent}{sub}.emplace({recv}, "
|
|
3210
|
+
f"::std::monostate{{}}, "
|
|
3211
|
+
f"::std::monostate{{}}, "
|
|
3212
|
+
f"::std::monostate{{}});\n")
|
|
3213
|
+
elif payload.async_for_uid is not None:
|
|
3214
|
+
uid = payload.async_for_uid
|
|
3215
|
+
out.write(f"{indent}{sub}.emplace(*__for_itr_{uid});\n")
|
|
3216
|
+
else:
|
|
3217
|
+
call = payload.operand_expr
|
|
3218
|
+
if isinstance(call, TpyCall):
|
|
3219
|
+
args = [self._gen_coro_emplace_arg(arg, i, call)
|
|
3220
|
+
for i, arg in enumerate(call.args)]
|
|
3221
|
+
self.ctx.temps.flush(out, indent)
|
|
3222
|
+
out.write(f"{indent}{sub}.emplace({', '.join(args)});\n")
|
|
3223
|
+
elif isinstance(call, TpyMethodCall):
|
|
3224
|
+
is_module_call = (call.user_module_call is not None
|
|
3225
|
+
or call.builtin_module_call is not None)
|
|
3226
|
+
if is_module_call:
|
|
3227
|
+
# `module.func(...)` -- receiver is a namespace,
|
|
3228
|
+
# not a value, so the sub-coro ctor takes only
|
|
3229
|
+
# the function args.
|
|
3230
|
+
args = [self._gen_coro_emplace_arg(a, i, call)
|
|
3231
|
+
for i, a in enumerate(call.args)]
|
|
3232
|
+
self.ctx.temps.flush(out, indent)
|
|
3233
|
+
out.write(f"{indent}{sub}.emplace({', '.join(args)});\n")
|
|
3234
|
+
else:
|
|
3235
|
+
# Bound async method: prepend receiver as __self ctor arg.
|
|
3236
|
+
recv_cpp = self.expressions.gen_expr(call.obj)
|
|
3237
|
+
arg_cpps = [self._gen_coro_emplace_arg(a, i, call)
|
|
3238
|
+
for i, a in enumerate(call.args)]
|
|
3239
|
+
self.ctx.temps.flush(out, indent)
|
|
3240
|
+
joined = ", ".join([recv_cpp] + arg_cpps)
|
|
3241
|
+
out.write(f"{indent}{sub}.emplace({joined});\n")
|
|
3242
|
+
else:
|
|
3243
|
+
raise CodeGenError(
|
|
3244
|
+
"internal: inline-mode await operand is not a call",
|
|
3245
|
+
loc=None)
|
|
3246
|
+
elif payload.mode is rcfg.AwaitMode.ERASED:
|
|
3247
|
+
operand_cpp = self.expressions.gen_expr(payload.operand_expr)
|
|
3248
|
+
self.ctx.temps.flush(out, indent)
|
|
3249
|
+
out.write(f"{indent}{sub}.emplace(std::move({operand_cpp}));\n")
|
|
3250
|
+
elif payload.mode is rcfg.AwaitMode.BORROWED:
|
|
3251
|
+
operand_cpp = self.expressions.gen_expr(payload.operand_expr)
|
|
3252
|
+
self.ctx.temps.flush(out, indent)
|
|
3253
|
+
out.write(f"{indent}{sub} = &({operand_cpp});\n")
|
|
3254
|
+
else:
|
|
3255
|
+
raise CodeGenError(f"unknown await mode {payload.mode!r}",
|
|
3256
|
+
loc=None)
|
|
3257
|
+
out.write(f"{indent}__state = "
|
|
3258
|
+
f"{_StateLabel(_StateKind.RESUME, suspension_index).cpp_name()};\n")
|