1 /*
2  * Copyright (c) 2021-2024 NVIDIA Corporation
3  *
4  * Licensed under the Apache License Version 2.0 with LLVM Exceptions
5  * (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  *
8  *   https://llvm.org/LICENSE.txt
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 #pragma once
17 
18 #include "__execution_fwd.hpp"
19 
20 // include these after __execution_fwd.hpp
21 #include "__basic_sender.hpp"
22 #include "__diagnostics.hpp"
23 #include "__domain.hpp"
24 #include "__env.hpp"
25 #include "__inline_scheduler.hpp"
26 #include "__meta.hpp"
27 #include "__receiver_ref.hpp"
28 #include "__sender_adaptor_closure.hpp"
29 #include "__senders.hpp"
30 #include "__tag_invoke.hpp"
31 #include "__transform_completion_signatures.hpp"
32 #include "__transform_sender.hpp"
33 #include "__variant.hpp"
34 
35 #include <exception>
36 
37 namespace stdexec
38 {
39 //////////////////////////////////////////////////////////////////////////////
40 // [exec.let]
41 namespace __let
42 {
43 // A dummy scheduler that is used by the metaprogramming below when the input
44 // sender doesn't have a completion scheduler.
45 struct __unknown_scheduler
46 {
47     struct __env
48     {
querystdexec::__let::__unknown_scheduler::__env49         static constexpr bool query(__is_scheduler_affine_t) noexcept
50         {
51             return true;
52         }
53 
54         constexpr auto
querystdexec::__let::__unknown_scheduler::__env55             query(get_completion_scheduler_t<set_value_t>) const noexcept
56         {
57             return __unknown_scheduler{};
58         }
59     };
60 
61     struct __sender
62     {
63         using sender_concept = sender_t;
64 
get_envstdexec::__let::__unknown_scheduler::__sender65         constexpr auto get_env() const noexcept -> __env
66         {
67             return __env();
68         }
69     };
70 
schedulestdexec::__let::__unknown_scheduler71     auto schedule() const noexcept
72     {
73         return __sender();
74     }
75 
76     bool operator==(const __unknown_scheduler&) const noexcept = default;
77 };
78 
79 inline constexpr auto __get_rcvr =
80     [](auto& __op_state) noexcept -> decltype(auto) {
81     return (__op_state.__rcvr_);
82 };
83 
84 inline constexpr auto __get_env =
85     [](auto& __op_state) noexcept -> decltype(auto) {
86     return __op_state.__state_.__get_env(__op_state.__rcvr_);
87 };
88 
89 template <class _Set, class _Domain = dependent_domain>
90 struct __let_t;
91 
92 template <class _Set>
93 inline constexpr __mstring __in_which_let_msg{
94     "In stdexec::let_value(Sender, Function)..."};
95 
96 template <>
97 inline constexpr __mstring __in_which_let_msg<set_error_t>{
98     "In stdexec::let_error(Sender, Function)..."};
99 
100 template <>
101 inline constexpr __mstring __in_which_let_msg<set_stopped_t>{
102     "In stdexec::let_stopped(Sender, Function)..."};
103 
104 template <class _Set>
105 using __on_not_callable = __callable_error<__in_which_let_msg<_Set>>;
106 
107 template <class _Receiver, class _Scheduler>
108 struct __receiver_with_sched
109 {
110     using receiver_concept = receiver_t;
111     _Receiver __rcvr_;
112     _Scheduler __sched_;
113 
114     template <class... _As>
set_valuestdexec::__let::__receiver_with_sched115     void set_value(_As&&... __as) noexcept
116     {
117         stdexec::set_value(static_cast<_Receiver&&>(__rcvr_),
118                            static_cast<_As&&>(__as)...);
119     }
120 
121     template <class _Error>
set_errorstdexec::__let::__receiver_with_sched122     void set_error(_Error&& __err) noexcept
123     {
124         stdexec::set_error(static_cast<_Receiver&&>(__rcvr_),
125                            static_cast<_Error&&>(__err));
126     }
127 
set_stoppedstdexec::__let::__receiver_with_sched128     void set_stopped() noexcept
129     {
130         stdexec::set_stopped(static_cast<_Receiver&&>(__rcvr_));
131     }
132 
get_envstdexec::__let::__receiver_with_sched133     auto get_env() const noexcept
134     {
135         return __env::__join(
136             prop{get_scheduler, __sched_},
137             __env::__without(stdexec::get_env(__rcvr_), get_domain));
138     }
139 };
140 
141 template <class _Receiver, class _Scheduler>
142 __receiver_with_sched(_Receiver, _Scheduler)
143     -> __receiver_with_sched<_Receiver, _Scheduler>;
144 
145 // If the input sender knows its completion scheduler, make it the current
146 // scheduler in the environment seen by the result sender.
147 template <class _Scheduler, class _Env>
148 using __result_env_t =
149     __if_c<__is_scheduler_affine<schedule_result_t<_Scheduler>>, _Env,
150            __env::__join_t< //
151                prop<get_scheduler_t, _Scheduler>,
152                __env::__without_t<_Env, get_domain_t>>>;
153 
154 template <__mstring _Where, __mstring _What>
155 struct _FUNCTION_MUST_RETURN_A_VALID_SENDER_IN_THE_CURRENT_ENVIRONMENT_
156 {};
157 
158 #if STDEXEC_NVHPC()
159 template <class _Sender, class _Set, class... _Env>
160 struct __bad_result_sender_
161 {
162     using __t = __mexception<
163         _FUNCTION_MUST_RETURN_A_VALID_SENDER_IN_THE_CURRENT_ENVIRONMENT_<
164             __in_which_let_msg<_Set>,
165             "The function must return a valid sender for the current environment"_mstr>,
166         _WITH_SENDER_<_Sender>, _WITH_ENVIRONMENT_<_Env>...>;
167 };
168 template <class _Sender, class _Set, class... _Env>
169 using __bad_result_sender = __t<__bad_result_sender_<_Sender, _Set, _Env...>>;
170 #else
171 template <class _Sender, class _Set, class... _Env>
172 using __bad_result_sender = __mexception<
173     _FUNCTION_MUST_RETURN_A_VALID_SENDER_IN_THE_CURRENT_ENVIRONMENT_<
174         __in_which_let_msg<_Set>,
175         "The function must return a valid sender for the current environment"_mstr>,
176     _WITH_SENDER_<_Sender>, _WITH_ENVIRONMENT_<_Env>...>;
177 #endif
178 
179 template <class _Sender, class... _Env>
180 concept __potentially_valid_sender_in =
181     sender_in<_Sender, _Env...> || (sender<_Sender> && (sizeof...(_Env) == 0));
182 
183 template <class _Set, class _Sender, class... _Env>
184 using __ensure_sender = //
185     __minvoke_if_c<__potentially_valid_sender_in<_Sender, _Env...>,
186                    __q<__midentity>,
187                    __mbind_back_q<__bad_result_sender, _Set, _Env...>, _Sender>;
188 
189 // A metafunction that computes the result sender type for a given set of
190 // argument types
191 template <class _Set, class _Fun, class _Sched, class... _Env>
192 struct __result_sender_fn
193 {
194     template <class... _Args>
195     using __f = //
196         __meval<
197             __ensure_sender, _Set,
198             __mcall<__mtry_catch_q<__call_result_t, __on_not_callable<_Set>>,
199                     _Fun, __decay_t<_Args>&...>,
200             __result_env_t<_Sched, _Env>...>;
201 };
202 
203 // The receiver that gets connected to the result sender is the input receiver,
204 // possibly augmented with the input sender's completion scheduler (which is
205 // where the result sender will be started).
206 template <class _Receiver, class _Scheduler>
207 using __result_receiver_t =
208     __if_c<__is_scheduler_affine<schedule_result_t<_Scheduler>>, _Receiver,
209            __receiver_with_sched<_Receiver, _Scheduler>>;
210 
211 template <class _ResultSender, class _Scheduler, class... _Env>
212 using __receiver_ref_t = //
213     __meval<__any_::__receiver_ref,
214             __completion_signatures_of_t<_ResultSender,
215                                          __result_env_t<_Scheduler, _Env>...>,
216             __result_env_t<_Scheduler, _Env>...>;
217 
218 template <class _ResultSender, class _Scheduler, class _Receiver>
219 concept __needs_receiver_ref =
220     __nothrow_connectable<
221         _ResultSender,
222         __receiver_ref_t<_ResultSender, _Scheduler, env_of_t<_Receiver>>> &&
223     !__nothrow_connectable<_ResultSender,
224                            __result_receiver_t<_Receiver, _Scheduler>>;
225 
226 template <class _Sender, class _Receiver>
227 using __nothrow_connectable_t =
228     __mbool<__nothrow_connectable<_Sender, _Receiver>>;
229 
230 template <class _ResultSender, class _Scheduler, class... _Env>
231 using __nothrow_connectable_receiver_ref_t =
232     __meval<__nothrow_connectable_t, _ResultSender,
233             __receiver_ref_t<_ResultSender, _Scheduler, _Env...>>;
234 
235 template <class _ResultSender, class _Scheduler, class _Receiver>
236 using __checked_result_receiver_t = //
237     __if_c<__needs_receiver_ref<_ResultSender, _Scheduler, _Receiver>,
238            __receiver_ref_t<_ResultSender, _Scheduler, env_of_t<_Receiver>>,
239            __result_receiver_t<_Receiver, _Scheduler>>;
240 
241 template <class _ResultSender, class _Scheduler, class _Receiver>
242 using __op_state_t = connect_result_t<
243     _ResultSender,
244     __checked_result_receiver_t<_ResultSender, _Scheduler, _Receiver>>;
245 
246 template <class _SetTag, class _Fun, class _Sched, class... _Env>
247 struct __transform_signal_fn
248 {
249     template <class... _Args>
250     using __nothrow_connect =
251         __mand<__mbool<(__nothrow_decay_copyable<_Args> && ...) &&
252                        __nothrow_callable<_Fun, _Args...>>,
253                __nothrow_connectable_receiver_ref_t<
254                    __mcall<__result_sender_fn<_SetTag, _Fun, _Sched, _Env...>,
255                            _Args...>,
256                    _Sched, _Env...>>;
257 
258     template <class... _Args>
259     using __f = //
260         __mcall<__mtry_q<__concat_completion_signatures>,
261                 __completion_signatures_of_t<
262                     __mcall<__result_sender_fn<_SetTag, _Fun, _Sched, _Env...>,
263                             _Args...>,
264                     __result_env_t<_Sched, _Env>...>,
265                 __eptr_completion_if_t<__nothrow_connect<_Args...>>>;
266 };
267 
268 template <class _Sender, class _Set>
269 using __completion_sched =
270     __query_result_or_t<get_completion_scheduler_t<_Set>, env_of_t<_Sender>,
271                         __unknown_scheduler>;
272 
273 template <class _LetTag, class _Fun, class _CvrefSender, class... _Env>
274 using __completions = //
275     __gather_completion_signatures<
276         __completion_signatures_of_t<_CvrefSender, _Env...>, __t<_LetTag>,
277         __transform_signal_fn<__t<_LetTag>, _Fun,
278                               __completion_sched<_CvrefSender, __t<_LetTag>>,
279                               _Env...>::template __f,
280         __sigs::__default_completion,
281         __mtry_q<__concat_completion_signatures>::__f>;
282 
283 template <__mstring _Where, __mstring _What>
284 struct _NO_COMMON_DOMAIN_
285 {};
286 
287 template <class _Set>
288 using __no_common_domain_t = //
289     _NO_COMMON_DOMAIN_<
290         __in_which_let_msg<_Set>,
291         "The senders returned by Function do not all share a common domain"_mstr>;
292 
293 template <class _Set>
294 struct __try_common_domain_fn
295 {
296     struct __error_fn
297     {
298         template <class... _Senders>
299         using __f = __mexception<__no_common_domain_t<_Set>,
300                                  _WITH_SENDERS_<_Senders...>>;
301     };
302 
303     template <class... _Senders>
304     using __f = __mcall<__mtry_catch_q<__domain::__common_domain_t, __error_fn>,
305                         _Senders...>;
306 };
307 
308 // Compute all the domains of all the result senders and make sure they're all
309 // the same
310 template <class _Set, class _Child, class _Fun, class _Env, class _Sched>
311 using __result_domain_t = //
312     __gather_completions<_Set, __completion_signatures_of_t<_Child, _Env>,
313                          __result_sender_fn<_Set, _Fun, _Sched, _Env>,
314                          __try_common_domain_fn<_Set>>;
315 
316 template <class _LetTag, class _Env>
__mk_transform_env_fn(_Env && __env)317 auto __mk_transform_env_fn(_Env&& __env) noexcept
318 {
319     using _Set = __t<_LetTag>;
320     return [&]<class _Fun, class _Child>(__ignore, _Fun&&,
321                                          _Child&& __child) -> decltype(auto) {
322         using __completions_t = __completion_signatures_of_t<_Child, _Env>;
323         if constexpr (__merror<__completions_t>)
324         {
325             return __completions_t();
326         }
327         else
328         {
329             using _Scheduler = __completion_sched<_Child, _Set>;
330             if constexpr (__is_scheduler_affine<schedule_result_t<_Scheduler>>)
331             {
332                 return (__env);
333             }
334             else
335             {
336                 return __env::__join(
337                     prop{get_scheduler, get_completion_scheduler<_Set>(
338                                             stdexec::get_env(__child))},
339                     __env::__without(static_cast<_Env&&>(__env), get_domain));
340             }
341         }
342     };
343 }
344 
345 template <class _LetTag, class _Env>
__mk_transform_sender_fn(_Env &&)346 auto __mk_transform_sender_fn(_Env&&) noexcept
347 {
348     using _Set = __t<_LetTag>;
349 
350     return []<class _Fun, class _Child>(__ignore, _Fun&& __fun,
351                                         _Child&& __child) {
352         using __completions_t = __completion_signatures_of_t<_Child, _Env>;
353 
354         if constexpr (__merror<__completions_t>)
355         {
356             return __completions_t();
357         }
358         else
359         {
360             using _Sched = __completion_sched<_Child, _Set>;
361             using _Domain = __result_domain_t<_Set, _Child, _Fun, _Env, _Sched>;
362 
363             if constexpr (__merror<_Domain>)
364             {
365                 return _Domain();
366             }
367             else if constexpr (same_as<_Domain, dependent_domain>)
368             {
369                 using _Domain2 = __late_domain_of_t<_Child, _Env>;
370                 return __make_sexpr<__let_t<_Set, _Domain2>>(
371                     static_cast<_Fun&&>(__fun), static_cast<_Child&&>(__child));
372             }
373             else
374             {
375                 static_assert(!same_as<_Domain, __unknown_scheduler>);
376                 return __make_sexpr<__let_t<_Set, _Domain>>(
377                     static_cast<_Fun&&>(__fun), static_cast<_Child&&>(__child));
378             }
379         }
380     };
381 }
382 
383 template <class _Receiver, class _Fun, class _Set, class _Sched>
384 struct __op_state_for
385 {
386     template <class... _Args>
387     using __f = __op_state_t<
388         __mcall<__result_sender_fn<_Set, _Fun, _Sched, env_of_t<_Receiver>>,
389                 _Args...>,
390         _Sched, _Receiver>;
391 };
392 
393 template <class _Receiver, class _Fun, class _Set, class _Sched,
394           class... _Tuples>
395 struct __let_state
396 {
397     using __fun_t = _Fun;
398     using __sched_t = _Sched;
399     using __env_t = __result_env_t<_Sched, env_of_t<_Receiver>>;
400     using __result_variant = __variant_for<__monostate, _Tuples...>;
401     using __op_state_variant = //
402         __variant_for<__monostate,
403                       __mapply<__op_state_for<_Receiver, _Fun, _Set, _Sched>,
404                                _Tuples>...>;
405 
406     template <class _ResultSender, class _OpState>
__get_result_receiverstdexec::__let::__let_state407     auto __get_result_receiver(const _ResultSender&,
408                                _OpState& __op_state) -> decltype(auto)
409     {
410         if constexpr (__needs_receiver_ref<_ResultSender, _Sched, _Receiver>)
411         {
412             using __receiver_ref =
413                 __receiver_ref_t<_ResultSender, _Sched, env_of_t<_Receiver>>;
414             return __receiver_ref{__op_state, __let::__get_env,
415                                   __let::__get_rcvr};
416         }
417         else
418         {
419             _Receiver& __rcvr = __op_state.__rcvr_;
420             if constexpr (__is_scheduler_affine<schedule_result_t<_Sched>>)
421             {
422                 return static_cast<_Receiver&&>(__rcvr);
423             }
424             else
425             {
426                 return __receiver_with_sched{static_cast<_Receiver&&>(__rcvr),
427                                              this->__sched_};
428             }
429         }
430     }
431 
__get_envstdexec::__let::__let_state432     auto __get_env(const _Receiver& __rcvr) const noexcept -> __env_t
433     {
434         if constexpr (__is_scheduler_affine<schedule_result_t<_Sched>>)
435         {
436             return stdexec::get_env(__rcvr);
437         }
438         else
439         {
440             return __env::__join(
441                 prop{get_scheduler, __sched_},
442                 __env::__without(stdexec::get_env(__rcvr), get_domain));
443         }
444     }
445 
446     STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS _Fun __fun_;
447     STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS _Sched __sched_;
448     __result_variant __args_{};
449     __op_state_variant __op_state3_{};
450 };
451 
452 template <class _Set, class _Domain>
453 struct __let_t
454 {
455     using __domain_t = _Domain;
456     using __t = _Set;
457 
458     template <sender _Sender, __movable_value _Fun>
operator ()stdexec::__let::__let_t459     auto operator()(_Sender&& __sndr, _Fun __fun) const -> __well_formed_sender
460         auto
461     {
462         auto __domain = __get_early_domain(__sndr);
463         return stdexec::transform_sender(
464             __domain,
465             __make_sexpr<__let_t<_Set>>(static_cast<_Fun&&>(__fun),
466                                         static_cast<_Sender&&>(__sndr)));
467     }
468 
469     template <class _Fun>
470     STDEXEC_ATTRIBUTE((always_inline))
operator ()stdexec::__let::__let_t471     auto operator()(_Fun __fun) const -> __binder_back<__let_t, _Fun>
472     {
473         return {{static_cast<_Fun&&>(__fun)}, {}, {}};
474     }
475 
476     using _Sender = __1;
477     using _Function = __0;
478     using __legacy_customizations_t =
479         __types<tag_invoke_t(__let_t,
480                              get_completion_scheduler_t<set_value_t>(
481                                  get_env_t(const _Sender&)),
482                              _Sender, _Function),
483                 tag_invoke_t(__let_t, _Sender, _Function)>;
484 
485     template <sender_expr_for<__let_t<_Set>> _Sender, class _Env>
transform_envstdexec::__let::__let_t486     static auto transform_env(_Sender&& __sndr,
487                               const _Env& __env) -> decltype(auto)
488     {
489         return __sexpr_apply(static_cast<_Sender&&>(__sndr),
490                              __mk_transform_env_fn<__let_t<_Set>>(__env));
491     }
492 
493     template <sender_expr_for<__let_t<_Set>> _Sender, class _Env>
494         requires same_as<__early_domain_of_t<_Sender>, dependent_domain>
495     static auto
transform_senderstdexec::__let::__let_t496         transform_sender(_Sender&& __sndr, const _Env& __env) -> decltype(auto)
497     {
498         return __sexpr_apply(static_cast<_Sender&&>(__sndr),
499                              __mk_transform_sender_fn<__let_t<_Set>>(__env));
500     }
501 };
502 
503 template <class _Set, class _Domain>
504 struct __let_impl : __sexpr_defaults
505 {
506     static constexpr auto get_attrs = //
507         []<class _Child>(__ignore, const _Child& __child) noexcept {
508             return __env::__join(prop{get_domain, _Domain()},
509                                  stdexec::get_env(__child));
510         };
511 
512     static constexpr auto get_completion_signatures = //
513         []<class _Self, class... _Env>(_Self&&, _Env&&...) noexcept
514         -> __completions<__let_t<_Set, _Domain>, __data_of<_Self>,
515                          __child_of<_Self>, _Env...> {
516         static_assert(sender_expr_for<_Self, __let_t<_Set, _Domain>>);
517         return {};
518     };
519 
520     static constexpr auto get_state = //
521         []<class _Sender, class _Receiver>(_Sender&& __sndr, _Receiver&) {
522             static_assert(sender_expr_for<_Sender, __let_t<_Set, _Domain>>);
523             using _Fun = __data_of<_Sender>;
524             using _Child = __child_of<_Sender>;
525             using _Sched = __completion_sched<_Child, _Set>;
526             using __mk_let_state =
527                 __mbind_front_q<__let_state, _Receiver, _Fun, _Set, _Sched>;
528 
529             using __let_state_t =
530                 __gather_completions_of<_Set, _Child, env_of_t<_Receiver>,
531                                         __q<__decayed_tuple>, __mk_let_state>;
532 
533             return __sndr.apply(
534                 static_cast<_Sender&&>(__sndr),
535                 [&]<class _Fn, class _Child>(__ignore, _Fn&& __fn,
536                                              _Child&& __child) {
537                     _Sched __sched = query_or(get_completion_scheduler<_Set>,
538                                               stdexec::get_env(__child),
539                                               __unknown_scheduler());
540                     return __let_state_t{static_cast<_Fn&&>(__fn), __sched};
541                 });
542         };
543 
544     template <class _State, class _OpState, class... _As>
__bind_stdexec::__let::__let_impl545     static void __bind_(_State& __state, _OpState& __op_state, _As&&... __as)
546     {
547         auto& __args = __state.__args_.emplace_from(
548             __tup::__mktuple, static_cast<_As&&>(__as)...);
549         auto __sndr2 = __args.apply(std::move(__state.__fun_), __args);
550         auto __rcvr2 = __state.__get_result_receiver(__sndr2, __op_state);
551         auto& __op2 = __state.__op_state3_.emplace_from(
552             stdexec::connect, std::move(__sndr2), std::move(__rcvr2));
553         stdexec::start(__op2);
554     }
555 
556     template <class _OpState, class... _As>
__bindstdexec::__let::__let_impl557     static void __bind(_OpState& __op_state, _As&&... __as) noexcept
558     {
559         using _State = decltype(__op_state.__state_);
560         using _Receiver = decltype(__op_state.__rcvr_);
561         using _Fun = typename _State::__fun_t;
562         using _Sched = typename _State::__sched_t;
563         using _ResultSender =
564             __mcall<__result_sender_fn<_Set, _Fun, _Sched, env_of_t<_Receiver>>,
565                     _As...>;
566 
567         _State& __state = __op_state.__state_;
568         _Receiver& __rcvr = __op_state.__rcvr_;
569 
570         if constexpr ((__nothrow_decay_copyable<_As> && ...) &&
571                       __nothrow_callable<_Fun, _As...> &&
572                       __v<__nothrow_connectable_receiver_ref_t<
573                           _ResultSender, _Sched, env_of_t<_Receiver>>>)
574         {
575             __bind_(__state, __op_state, static_cast<_As&&>(__as)...);
576         }
577         else
578         {
579             try
580             {
581                 __bind_(__state, __op_state, static_cast<_As&&>(__as)...);
582             }
583             catch (...)
584             {
585                 using _Receiver = decltype(__op_state.__rcvr_);
586                 stdexec::set_error(static_cast<_Receiver&&>(__rcvr),
587                                    std::current_exception());
588             }
589         }
590     }
591 
592     static constexpr auto complete = //
593         []<class _OpState, class _Tag, class... _As>(
594             __ignore, _OpState& __op_state, _Tag,
595             _As&&... __as) noexcept -> void {
596         if constexpr (__same_as<_Tag, _Set>)
597         {
598             __bind(__op_state, static_cast<_As&&>(__as)...);
599         }
600         else
601         {
602             using _Receiver = decltype(__op_state.__rcvr_);
603             _Tag()(static_cast<_Receiver&&>(__op_state.__rcvr_),
604                    static_cast<_As&&>(__as)...);
605         }
606     };
607 };
608 } // namespace __let
609 
610 using let_value_t = __let::__let_t<set_value_t>;
611 inline constexpr let_value_t let_value{};
612 
613 using let_error_t = __let::__let_t<set_error_t>;
614 inline constexpr let_error_t let_error{};
615 
616 using let_stopped_t = __let::__let_t<set_stopped_t>;
617 inline constexpr let_stopped_t let_stopped{};
618 
619 template <class _Set, class _Domain>
620 struct __sexpr_impl<__let::__let_t<_Set, _Domain>> :
621     __let::__let_impl<_Set, _Domain>
622 {};
623 } // namespace stdexec
624