diff --git a/.gitignore b/.gitignore index a376cafa..166fdc85 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ CMakeFiles/ *.log docs/html docs/latex +docs/code/*.cpp +docs/tutorial.md diff --git a/CMakeLists.txt b/CMakeLists.txt index e01b3774..409e197c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ endif() if(BEMAN_EXECUTION_BUILD_EXAMPLES) add_subdirectory(examples) + add_subdirectory(docs/code) endif() if(NOT BEMAN_EXECUTION_ENABLE_INSTALL OR CMAKE_SKIP_INSTALL_RULES) diff --git a/Makefile b/Makefile index b956d026..83545dd7 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,8 @@ ifeq (${hostSystemName},Darwin) export GCC_DIR:=$(shell realpath ${GCC_PREFIX}) export CMAKE_CXX_STDLIB_MODULES_JSON=${GCC_DIR}/lib/gcc/current/libstdc++.modules.json - export CXX:=g++-15 - export CXXFLAGS:=-stdlib=libstdc++ + # export CXX:=g++-15 + # export CXXFLAGS:=-stdlib=libstdc++ export GCOV="gcov" else ifeq (${hostSystemName},Linux) export LLVM_DIR=/usr/lib/llvm-20 @@ -105,6 +105,7 @@ run: test ./$(BUILD)/examples/$(EXAMPLE) doc: + ./bin/mk-doc.py docs/*.mds doxygen docs/Doxyfile # $(SANITIZERS): @@ -171,14 +172,14 @@ cmake-format: pre-commit run gersemi clang-format: - pre-commit run $@ - # XXX TBD: git clang-format main + # pre-commit run $@ + git clang-format main todo: bin/mk-todo.py unstage: - git restore --staged tests/beman/execution/CMakeLists.txt + git restore --staged tests/beman/execution/CMakeLists.txt docs/code/CMakeLists.txt .PHONY: clean-doc clean-doc: diff --git a/bin/mk-doc.py b/bin/mk-doc.py new file mode 100755 index 00000000..2b15fd23 --- /dev/null +++ b/bin/mk-doc.py @@ -0,0 +1,78 @@ +#!/usr/bin/python3 + +import re +import sys + +prestart_re = re.compile('\s*
')
+preend_re = re.compile("\s*")
+append_re = re.compile("list\(APPEND EXAMPLES")
+appendp_re = re.compile("list\(APPEND EXAMPLES\)")
+paren_re = re.compile("\)")
+
+
+def update_cmake(list):
+ text = ""
+ skipping = False
+ with open("docs/code/CMakeLists.txt") as cmake:
+ for line in cmake:
+ if skipping:
+ if paren_re.match(line):
+ skipping = False
+ text += line
+ else:
+ if append_re.match(line):
+ text += "list(APPEND EXAMPLES\n"
+ text += "\n".join(list) + "\n"
+ skipping = not appendp_re.match(line)
+ if not skipping:
+ text += ")\n"
+ else:
+ text += line
+
+ with open("docs/code/CMakeLists.txt", "w") as cmake:
+ cmake.write(text)
+
+
+def transform(text, output):
+ capturing = False
+ code = ""
+ name = ""
+ names = {}
+ for line in text:
+ match = prestart_re.match(line)
+ if match:
+ print(f"example: {match.group(1)}")
+ name = match.group(1)
+ code = ""
+ output.write(f"```c++\n")
+ capturing = True
+ elif capturing and preend_re.match(line):
+ if name in names:
+ print(f"skipping code with duplicate name '{name}'")
+ else:
+ names[name] = code
+ with open("docs/code/" + name + ".cpp", "w") as codeout:
+ codeout.write(code)
+ capturing = False
+ output.write(f"```\n")
+ else:
+ if capturing:
+ code += line
+ output.write(f"{line}")
+ update_cmake(names.keys())
+
+
+def process(root):
+ print(f"processing {root}")
+ text = []
+ with open(root + ".mds") as input:
+ text = input.readlines()
+ with open(root + ".md", "w") as output:
+ transform(text, output)
+
+
+sys.argv.pop(0)
+for file in sys.argv:
+ match = re.match("(.*)\.mds$", file)
+ if match:
+ process(match.group(1))
diff --git a/docs/code/CMakeLists.txt b/docs/code/CMakeLists.txt
new file mode 100644
index 00000000..2d2752d3
--- /dev/null
+++ b/docs/code/CMakeLists.txt
@@ -0,0 +1,20 @@
+# gersemi: off
+# docs/code/CMakeLists.txt -*-makefile-*-
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+# gersemi: on
+
+list(APPEND EXAMPLES)
+
+if(BEMAN_USE_MODULES)
+ list(APPEND EXAMPLES modules) # modules.cpp
+endif()
+
+foreach(EXAMPLE ${EXAMPLES})
+ set(EXAMPLE_TARGET ${TARGET_PREFIX}.tutorial.${EXAMPLE})
+ add_executable(${EXAMPLE_TARGET})
+ target_sources(${EXAMPLE_TARGET} PRIVATE ${EXAMPLE}.cpp)
+ target_link_libraries(
+ ${EXAMPLE_TARGET}
+ PRIVATE ${TARGET_NAMESPACE}::${TARGET_NAME}
+ )
+endforeach()
diff --git a/docs/code/tst-basic-timer.cpp b/docs/code/tst-basic-timer.cpp
new file mode 100644
index 00000000..3779e482
--- /dev/null
+++ b/docs/code/tst-basic-timer.cpp
@@ -0,0 +1,38 @@
+// examples/tst-basic-timer.cpp -*-C++-*-
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+// ----------------------------------------------------------------------------
+
+#include get_allocator(env) returns an allocator for any memory allocations in the respective context. If env doesn’t support this query any attempt to access it will result in a compilation error. The value of the expression get_allocator(env) is the result of as_const(env).query(get_allocator) if
noexcept;simple-allocator.get_completion_scheduler<>(env) -> scheduler get_completion_scheduler<Tag>(env) -> schedulerget_complet_scheduler<Tag>(env) yields the completion scheduler for the completion signal Tag associated with env. This query can be used to determine the scheduler a sender sender completes on for a given completion signal Tag by using get_completion_scheduler<Tag>(get_env(sender)). The value of the expression is equivalent to as_const(env).query(get_completion_scheduler<Tag>) if
@@ -485,7 +485,7 @@ To determine the result the sender is first transformed usin
new-sender.get_completion_signatures(env) if this expression is valid;remove_cvref_t<New-Sender-Type>::completion_signatures if this type exists;completion_signatures<set_value_t(T), set_error_t(exception_ptr), set_stopped_t()> if New-Sender-Type is an awaitable type which would yield an object of type T when it is co_awaited;get_delegation_scheduler(env) yields the sche
Otherwise the expression is invalid.
get_domain(env) -> domain
-get_domain(env) -> domainget_domain(env) yields the domain associated with env. The value of the expression is equivalent to as_const(env).query(get_domain) if
just(value...) creates a sender which sends <
just_error(error) -> sender-of<set_error_t(Error)>just_error(error) -> sender-of<set_error_t(Error)>just_error(error) creates a sender which sends error on the `set_error` (failure) channel when started.
Completions
@@ -604,7 +603,7 @@ The expression just_error(error) creates a sender which send
just_stopped() -> sender-of<set_stopped_t()>just_stopped() -> sender-of<set_stopped_t()>just_stopped() creates a sender which sends a completion on the `set_stopped` (cancellation) channel when started.
Completions
@@ -613,7 +612,7 @@ The expression just_stopped() creates a sender which sends a comple
read_env(query) -> sender-of<set_value_t(query-result)>read_env(query) -> sender-of<set_value_t(query-result)>read_env(query) creates a sender which sends the result of querying query the environment of the receiver it gets connected to on the `set_value` channel when started. Put differently, it calls set_value(move(receiver), query(get_env(receiver))). For example, in a coroutine it may be useful to extra the stop token associated with the coroutine which can be done using read_env:
```c++\
@@ -626,7 +625,7 @@ auto token = co_await read_env(get_stop_token);
schedule(scheduler) -> sender-of<set_value_t()>schedule(scheduler) -> sender-of<set_value_t()>schedule(scheduler) creates a sender which upon success completes on the set_value channel without any arguments running on the execution context associated with scheduler. Depending on the scheduler it is possible that the sender can complete with an error if the scheduling fails or using `set_stopped()` if the operation gets cancelled before it is successful.
Completions
@@ -667,7 +666,7 @@ The expression into_variant(sender) creates a sender which t
let_value(upstream, fun) -> senderon(_sched_, _sndr_)schedule_from(scheduler, sender) -> sender+ It was pointed out for similar documents that useful information was surprisingly available. On the other hand the intent is to avoid clutter which isn't necessary but can be helpful. +
++ There are or will be multiple implementations of + [`std::execution`](https://wg21.link/exec) and the examples + should work with these implementations assuming the declarations + are suitably made available: +
++ #include++ namespace ex = beman::execution; +
+ import std; + namespace ex = std::execution; ++
+ #include++ namespace ex = stdexec; +
+ #include++ namespace ex = unifex; +
+The key abstraction of [`std::execution`](https://wg21.link/exec) are [_senders_](https://wg21.link/exec.snd). A sender is a fully curried asynchronous function representing some work. There are a few steps needed to get the work represented by a sender executed which are relevant when creating library components using senders. The details will be provided when explaining how to create senders. For now, it is sufficient to know that ex::sync_wait(_sender_) can execute _sender_ and obtain the result of that execution (senders are normally executed differently).
+
+To get going with senders, it is necessary to get some sender. One
+fundamental sender is one which [synchronously] produces a particular
+result when it gets started. The expression
+ex::just(_args_...) is used to get a sender which
+completes successful and produces the values _args_...
+when it gets `start`ed. Likewise, the expression
+ex::just_error(e) is used to get a sender which
+completes with the error _e_ when it gets `start`ed.
+Asynchronous operations can also be stopped (cancelled) without
+either producing a value or an error and the expression
+`ex::just_stopped()` produces a sender which completes indicating
+that it got stopped when it gets `start`ed.
+
+Once you got a sender it needs to be `start`ed and the result needs
+to be awaited to get the sender effect. The function
+ex::sync_wait(_sender_) `start`s the argument
+_sender_, awaits the results, and `return`s them. The
+result of `ex::sync_wait` is an
+std::optional<std::tuple<_T_...>> to cope
+with the kinds of results an asynchronous function can produce:
+
+ #include++ #include + namespace ex = beman::execution; + + int main() { + auto[b, i] = *ex::sync_wait(ex::just(true, 1)); + std::cout << "b=" << b << ", i=" << i << "\n"; + } +
tst::sync_wait(_sndr_) wraps the
+ _sndr_ into a new sender which claims to complete
+ successfully if _sndr_ doesn't already declare
+ how it can succeed. How to do that will be shown later in the
+ tutorial.
+ + #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + try { tst::sync_wait(ex::just_error(17)); } + catch (int e) { std::cout << "error=" << e << "\n"; } + } +
tst::sync_wait(_sndr_) wraps the _sndr_
+ into a new sender which claims to complete successfully if
+ _sndr_ doesn't already declare how it can succeed.
+ How to do that will be shown later in the tutorial .
+ + #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + if (not tst::sync_wait(ex::just_stopped())) { + std::cout << "the operation was cancelled\n"; + } + } +
+The `just` family of sender algorithms and `sync_wait` are basic +building blocks for creating and executing senders. The standard +library ships with various algorithms to compose senders: these +algorithms take one or more senders as well as other parameters and +produce a new sender. This composition is a major objective of the +sender interface. This section describes a few algorithms used in +the examples below. +
+
+Some of the algorithms are _pipeable_ which means that instead of
+writing _algo_(_sender_, _args_...) it is possible to
+write _sender_ | _algo_(_args_...). This alternative
+notation results in composing the algorithms in processing order.
+Using the call notation causes the starting point to be the most
+nested algorithm. Where applicable, the summary below shows the
+pipe notation.
+
_fun_:
+ _sender_ completes according to the name_sender_
+ completes according to the name of the algorithm (`then`
+ => success; `upon_error` => error; `upon_stopped` =>
+ stop), _fun_ is invoked with the value
+ produced by _sender_ and the [successful]
+ result of the algorithm is the value returned from this
+ invocation (if any). If the invocation of _fun_
+ throws, the algorithm completes with an error
+ provding an `std::exception_ptr` to caught exception. If
+ _sender_ doesn't complete accordingly to the
+ algorithm's name, the completion is forwarded.
+ _sender_ | ex::then(_fun_)_fun_.
+ + #include++ #include + namespace ex = beman::execution; + + int main() { + auto[r] = *ex::sync_wait(ex::just(true, 17) | ex::then([](bool, int i){ return 2 * i; })); + std::cout << "r=" << r << "\n"; + } +
+ #include++ #include + namespace ex = beman::execution; + + int main() { + try { ex::sync_wait(ex::just(true, 17) | ex::then([](bool, int i){ throw 2 * i; })); } + catch (int e) { std::cout << "e=" << e << "\n"; } + } +
+ #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + try { + tst::sync_wait(ex::just_error(17) | ex::then([](bool, int i){ return 2 * i; })); + } + catch (int e) { + std::cout << "e=" << e << "\n"; + } + } +
+ #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + auto o = tst::sync_wait(ex::just_stopped() | ex::then([](bool, int i){ return 2 * i; })); + std::cout << "o=" << (o? "": "not ") << "set\n"; + } +
_sender_ | ex::upon_error(_fun_)+ #include++ #include + namespace ex = beman::execution; + + int main() { + auto[r] = *ex::sync_wait(ex::just_error(17) | ex::upon_error([](int e){ return e; })); + std::cout << "r=" << r << "\n"; + } +
+ #include++ #include + #include + namespace ex = beman::execution; + + int main() { + auto[e] = *ex::sync_wait( + ex::just_error(17) + | ex::then([](bool, int i) noexcept { return std::expected (i); }) + | ex::upon_error([](int e) noexcept { return std::expected (std::unexpected(e)); }) + ); + std::cout << (e? "success": "failure") << " e=" << (e? e.value(): e.error()) << "\n"; + } +
_sender_ | ex::upon_stopped(_fun_)+ #include++ #include + namespace ex = beman::execution; + + int main() { + auto[r] = *ex::sync_wait(ex::just_stopped() | ex::upon_stopped([](){ return 42; })); + std::cout << "r=" << r << "\n"; + } +
_fun_:
+ _fun__fun_ with the results from
+ _sender_ returns a sender _s_
+ when the result of _sender_ match algorithm
+ name (`let_value` => success; `let_error` => error,
+ `let_stopped` => stopped). In that case the result becomes
+ the result of executing this sender _s_.
+ If _sender_ completes differently, the result is
+ forwarded. If the invocation of _fun_ exits with
+ an exception the algorithm completes with an error passing an
+ std::exception_ptr to the caught exception.
+ _sender_ | ex::let_value(_fun_)+ #include++ #include + namespace ex = beman::execution; + + int main() { + int f{3}; + auto[r] = *ex::sync_wait(ex::just(17) | ex::let_value([f](int i){ return ex::just(f * i); })); + std::cout << "r=" << r << "\n"; + } +
+ #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + int f{3}; + try { tst::sync_wait(ex::just(17) | ex::let_value([f](int i){ return ex::just_error(f * i); })); } + catch (int e) { + std::cout << "e=" << e << "\n"; + } + } +
+ #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + int f{3}; + try { tst::sync_wait(tst::just_error(17) | ex::let_value([f](auto...){ return ex::just(f); })); } + catch (int e) { + std::cout << "e=" << e << "\n"; + } + } +
std::exception_ptr to the caught exception. Note
+ the function object still needs to return a sender to satisfy the requirement
+ of `let_value`'s interface.
+ + #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + int f{3}; + try { *tst::sync_wait(ex::just(17) | ex::let_value([f](int i){ throw f * i; return ex::just(); })); } + catch (int e) { + std::cout << "e=" << e << "\n"; + } + } +
_sender_ | ex::let_error(_fun_)+ #include++ #include + namespace ex = beman::execution; + + int main() { + auto[r] = *ex::sync_wait(ex::just_error(17) | ex::let_error([](int e){ return ex::just(e); })); + std::cout << "r=" << r << "\n"; + } +
_sender_ | ex::let_stopped(_fun_)+ #include++ #include + namespace ex = beman::execution; + + int main() { + auto[r] = *ex::sync_wait(ex::just_error(17) | ex::let_error([](int e){ return ex::just(e); })); + std::cout << "r=" << r << "\n"; + } +
ex::when_all(_s_...)_s_... a `std::tuple` with all
+ results is produced. If an error or a cancellation is
+ encountered all outstanding senders are stopped and
+ once all senders are completed the unsuccessful result
+ is reported.
+ + #include++ #include + namespace ex = beman::execution; + + int main() { + auto[b, i, f] = *ex::sync_wait(ex::when_all(ex::just(true), ex::just(), ex::just(17, 2.5))); + std::cout << "b=" << b << " i=" << i << " f=" << f << "\n"; + } +
+ #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + try { ex::sync_wait(tst::when_all(ex::just(true), ex::just_error(1), ex::just(17, 2.5))); } + catch (int e) { + std::cout << "e=" << e << "\n"; + } + } +
+ #include++ #include + #include "tst.hpp" + namespace ex = beman::execution; + + int main() { + if (not ex::sync_wait(tst::when_all(ex::just(true), ex::just_stopped(), ex::just(17, 2.5)))) { + std::cout << "no result set => operation was stopped\n"; + } + } +
+For example, `tst::resume_after(500ms)` is a concrete example of a sender whose work consists of doing nothing and completing after `500ms`. One way to `start` this sender and awaiting its completion is using ex::sync_wait(_sender_):
+
+ ex::sync_wait(tst::resume_after(500ms)); ++
+ #include <beman/execution.hpp>
+ namespace ex = beman::execution;
+ #include <tst.hpp>
+ #include <chrono>
+ using std::chrono::literals;
+
+ int main() {
+ ex::sync_wait(tst::resume_after(500ms));
+ }
+
+
+Normally senders aren't `start` directly like that. Instead they are composed into using sender algorithms. For a simple example the sender above could be composed with the ex::then algorithm which executes a function with the results of the sender it is composed with:
+
+ ex::sync_wait(
+ tst::resume_after(500ms)
+ | ex::then([]{ std::cout << "waited 500ms\n"; })
+ );
+
+ +A composed sender is a sender which can be further composed. For example, the `ex::when_all` algorithm takes multiple senders as argument and completes when all senders completed: +
+
+ auto work = [](auto dur) {
+ return tst::resume_after(dur)
+ | ex::then([]{ std::cout << "waited " << dur << "\n"; });
+ };
+ ex::sync_wait(
+ ex::when_all(work(500ms), work(200ms), work(100ms))
+ );
+
+ +Senders can be composed using `ex::task<>` which can `co_await` senders and is itself a sender: +
+
+ auto work = [](auto dur, auto fun)->ex::task<> {
+ co_await tst::resume_after(dur);
+ for (int i{}; i < 100; ++) {
+ co_await tst::resume_after(100ms);
+ fun(i);
+ }
+ };
+ ex::sync_wait(
+ ex::when_all(
+ work(3ms, [](int i){
+ (i % 3) && (i % 5) && (std::cout << i);
+ std::cout << '\n';
+ )}),
+ work(1ms, [](int i){ (i % 3) || (std::cout << "fizz"); }),
+ work(2ms, [](int i){ (i % 5) || (std::cout << "buzz"); })
+ );
+
+ +If the work isn't known in advanced something is needed to `start` new work and await its completion. This is the purpose of `ex::counting_scope`: it tracks outstanding work and provides a sender which completes when it becomes empty: +
+
+ auto work = [](auto fun)->ex::task<> {
+ for (int i{}; i < 100; ++i) {
+ co_await ex::resume_after(100ms);
+ fun(i);
+ }
+ };
+ ex::counting_scope scope;
+ ex::sync_wait([](auto& s)->ex::task<> {
+ co_await tst::resume_after(1ms);
+ ex::spawn(s.get_token(), work([](int i) {
+ (i % 3) && (i % 5) && (std::cout << i);
+ std::cout << '\n';
+ }));
+ co_await tst::resume_after(1ms);
+ ex::spawn(s.get_token(), work([](int i) {
+ (i % 3) || (std::cout << "fizz");
+ }));
+ co_await tst::resume_after(1ms);
+ ex::spawn(s.get_token(), work([](int i) {
+ (i % 5) || (std::cout << "buzz");
+ }));
+ co_await scope.when_empty();
+ }(scope));
+
+ +The above are the key tools needed to create an application using asynchronous approaches: +
++ The above doesn't explain how sender are implemented or what tools are available. There are more advanced needs +
diff --git a/include/beman/execution/detail/just.hpp b/include/beman/execution/detail/just.hpp index 6486e298..e5e2b264 100644 --- a/include/beman/execution/detail/just.hpp +++ b/include/beman/execution/detail/just.hpp @@ -61,7 +61,7 @@ using just_error_t = ::beman::execution::detail::just_t<::beman::execution::se using just_stopped_t = ::beman::execution::detail::just_t<::beman::execution::set_stopped_t>; /*! - * \briefjust(_arg_...)` yields a sender completing with set_value_t(_Arg_...)
+ * \brief just(_arg_...) yields a sender completing with set_value_t(_Arg_...)
* \headerfile beman/execution/execution.hpp sync_wait(_sender_):
* for a value completion of _sender_ it will yield an
- * std::optional<std::tuple<_Args_...>>` with the
+ * std::optional<std::tuple<_Args_...>> with the
* `tuple` containing the value copied/moved from the original arguments
* (an `optional` is returned to indicate cancellation).
*
@@ -170,7 +170,7 @@ inline constexpr ::beman::execution::just_t just{};
inline constexpr ::beman::execution::just_error_t just_error{};
/*!
- * \brief just_stopped(_) yields a sender completing with set_stopped_t()
+ * \brief just_stopped() yields a sender completing with set_stopped_t()
* \headerfile beman/execution/execution.hpp
*
* \details
diff --git a/include/beman/execution/detail/sync_wait.hpp b/include/beman/execution/detail/sync_wait.hpp
index 36964ef1..6d38b213 100644
--- a/include/beman/execution/detail/sync_wait.hpp
+++ b/include/beman/execution/detail/sync_wait.hpp
@@ -108,6 +108,53 @@ struct sync_wait_t {
namespace beman::execution {
using sync_wait_t = ::beman::execution::detail::sync_wait_t;
+/*!
+ * \brief sync_wait(_sender_) starts _sender_ and waits for its completion.
+ * \headerfile beman/execution/execution.hpp
+ *
+ * \details
+ * `sync_wait` is a callable object of type `sync_wait_t`. Invoking
+ * sync_wait(_sender_) starts _sender_ and
+ * waits for its completion. This involves a few steps:
+ * 1. A run_loop is created to provide a scheduler.
+ * 2. The _sender_ is `connect`ed to a receiver capturing
+ * the results and providing an environment with access to the
+ * `run_loop`'s scheduler.
+ * 3. The operation state returned from `connect` is `start`ed.
+ * 4. The `run_loop` is run to process any work scheduled.
+ *
+ * Once the _sender_ completes, the result is provided by `sync_wait`:
+ * - If the _sender_ completes with set_value(_arg_...), `sync_wait` returns
+ * an std::optional> containing the results
+ * _arg_....
+ * - If the _sender_ completes with `set_stopped()`, `sync_wait` returns a
+ * disengaged std::optional> .
+ * - If the _sender_ completes with
+ * set_error(_error_), `sync_wait` throw _error_ or rethrows the exception if
+ * _error_ is an std::exception_ptr.
+ *
+ * Usage
+ *
+ * sync_wait(sender...)
+ *
+ *
+ * Example
+ *
+ * The use of sync_wait(_sender_) is in `main`
+ * to synchronously wait for the completion of the asynchronous work
+ * of the program represented by _sender_.
+ *
+ *
+ * #include
+ * #include
+ *
+ * int main() {
+ * auto result = ex::sync_wait(ex::just(17));
+ * assert(result);
+ * assert(*result == std::tuple(17));
+ * }
+ *
+ */
inline constexpr ::beman::execution::sync_wait_t sync_wait{};
} // namespace beman::execution
diff --git a/include/beman/execution/detail/then.hpp b/include/beman/execution/detail/then.hpp
index 67891260..1a342201 100644
--- a/include/beman/execution/detail/then.hpp
+++ b/include/beman/execution/detail/then.hpp
@@ -139,12 +139,177 @@ struct completion_signatures_for_impl<
#include
namespace beman::execution {
-using then_t = ::beman::execution::detail::then_t<::beman::execution::set_value_t>;
-using upon_error_t = ::beman::execution::detail::then_t<::beman::execution::set_error_t>;
+/*!
+ * \brief then_t is the type of then.
+ * \headerfile beman/execution/execution.hpp
+ */
+using then_t = ::beman::execution::detail::then_t<::beman::execution::set_value_t>;
+/*!
+ * \brief upon_error_t is the type of upon_error.
+ * \headerfile beman/execution/execution.hpp
+ */
+using upon_error_t = ::beman::execution::detail::then_t<::beman::execution::set_error_t>;
+/*!
+ * \brief upon_stopped_t is the type of upon_stopped.
+ * \headerfile beman/execution/execution.hpp
+ */
using upon_stopped_t = ::beman::execution::detail::then_t<::beman::execution::set_stopped_t>;
-inline constexpr ::beman::execution::then_t then{};
-inline constexpr ::beman::execution::upon_error_t upon_error{};
+/*!
+ * \brief then(_sender_, _fun_) yields a sender transforming a set_value_t(_A_...) completion
+ * \headerfile beman/execution/execution.hpp
+ *
+ * \details
+ * `then` is a callable object of type `then_t`. Invoking then(_sender_, _fun_) or
+ * _sender_ | then(_fun_) yields a sender
+ * which, when `start`ed starts _sender_ and awaits its completion. When
+ * _sender_ completes `then` proceeds according to this completion:
+ * - If the completion is set_value(_a_...), _fun_(_a_...) is invoked:
+ * - if that invocation throws, `then` completes with set_error(_r_, std::current_exception());
+ * otherwise
+ * - if the invocation returns `void`, `then` completes with set_value(_r_); otherwise
+ * - if the invocation returns _v_, `then` completes with set_value(_r_, _v_).
+ * - Otherwise, if the completion is set_error(_e_), `then` completes with set_error(_r_,
+ * _e_)
+ * - Otherwise, if the completion is set_stopped(), `then` completes with set_stopped(_r_).
+ *
+ * Usage
+ *
+ * then(sender, fun)
+ * sender | then(fun)
+ *
+ *
+ * Completions Signatures
+ * The completion signatures depends on the completion signatures _CS_ of _sender_ (the
+ * completion signatures will be deduplicated):
+ * - For each set_value_t(_A_...) in _CS_,
+ * there is a completion signature
+ * set_value_t(decltype(_fun_(std::declval<_A_>()...))).
+ * - If for any of the set_value_t(_A_...) in
+ * _CS_ the expression noexcept(_fun_(std::declval<_A_>()...))
+ * is `false` there is a completion signature set_error_t(std::exception_ptr).
+ * - Each set_error_t(_Error_) in _CS_ is copied.
+ * - If set_stopped_t() is in _CS_ it is copied.
+ *
+ * Example
+ *
+ *
+ * #include
+ * #include
+ * namespace ex = beman::execution;
+ *
+ * int main() {
+ * auto result = ex::sync_wait(ex::just(10) | ex::then([](int v) { return v == 3; }));
+ * assert(result);
+ * assert(*result == std::tuple(false));
+ * }
+ *
+ */
+inline constexpr ::beman::execution::then_t then{};
+
+/*!
+ * \brief upon_error(_sender_, _fun_) yields a sender transforming a set_error_t(_E_)
+ * completion
+ * \headerfile beman/execution/execution.hpp
+ *
+ * \details
+ * `upon_error` is a callable object of type `upon_error_t`. Invoking upon_error(_sender_, _fun_) or
+ * _sender_ | upon_error(_fun_) yields a sender
+ * which, when `start`ed starts _sender_ and awaits its completion. When
+ * _sender_ completes `upon_error` proceeds according to this completion:
+ * - If the completion is set_error(_e_), _fun_(_e_) is invoked:
+ * - if that invocation throws, `upon_error` completes with set_error(_r_, std::current_exception());
+ * otherwise
+ * - if the invocation returns `void`, `upon_error` completes with set_value(_r_); otherwise
+ * - if the invocation returns _v_, `upon_error` completes with set_value(_r_, _v_).
+ * - Otherwise, if the completion is set_value(_a_...), `upon_error` completes with set_value(_r_,
+ * _a_...)
+ * - Otherwise, if the completion is set_stopped(), `upon_error` completes with
+ * set_stopped(_r_).
+ *
+ * Usage
+ *
+ * upon_error(sender, fun)
+ * sender | upon_error(fun)
+ *
+ *
+ * Completions Signatures
+ * The completion signatures depend on the completion signatures _CS_ of _sender_ (the
+ * completion signatures will be deduplicated):
+ * - For each set_error_t(_E_) in _CS_,
+ * there is a completion signature
+ * set_value_t(decltype(_fun_(std::declval<_E_>()))).
+ * - If for any of the set_error_t(_E_) in
+ * _CS_ the expression noexcept(_fun_(_std::declval()_))
+ * is `false` there is a completion signature set_error_t(std::exception_ptr).
+ * - Each set_value_t(_A_...) in _CS_ is copied.
+ * - If set_stopped_t() is in _CS_ it is copied.
+ *
+ * Example
+ *
+ *
+ * #include
+ * #include
+ * namespace ex = beman::execution;
+ *
+ * int main() {
+ * auto result = ex::sync_wait(ex::just_error(10) | ex::upon_error([](int v) { return v == 3; }));
+ * assert(result);
+ * assert(*result == std::tuple(false));
+ * }
+ *
+ */
+inline constexpr ::beman::execution::upon_error_t upon_error{};
+
+/*!
+ * \brief upon_stopped(_sender_, _fun_) yields a sender transforming a set_stopped_t()
+ * completion
+ * \headerfile beman/execution/execution.hpp
+ *
+ * \details
+ * `upon_stopped` is a callable object of type `upon_stopped_t`. Invoking upon_stopped(_sender_, _fun_) or
+ * _sender_ | upon_stopped(_fun_) yields a sender
+ * which, when `start`ed starts _sender_ and awaits its completion. When
+ * _sender_ completes `upon_stopped` proceeds according to this completion:
+ * - If the completion is set_stopped(_e_), _fun_(_e_) is invoked:
+ * - if that invocation throws, `upon_stopped` completes with set_error(_r_,
+ * std::current_exception()); otherwise
+ * - if the invocation returns `void`, `upon_stopped` completes with set_value(_r_); otherwise
+ * - if the invocation returns _v_, `upon_stopped` completes with set_value(_r_, _v_).
+ * - Otherwise, if the completion is set_value(_a_...), `upon_stopped` completes with set_value(_r_,
+ * _a_...)
+ * - Otherwise, if the completion is set_error(_e_), `upon_stopped` completes with set_error(_r_,
+ * _e_).
+ *
+ * Usage
+ *
+ * upon_stopped(sender, fun)
+ * sender | upon_stopped(fun)
+ *
+ *
+ * Completions Signatures
+ * The completion signatures depend on the completion signatures _CS_ of _sender_ (the
+ * completion signatures will be deduplicated):
+ * - There is a completion signature set_value_t(decltype(_fun_())).
+ * - If the expression noexcept(_fun_()) is `false` there is a completion signature
+ * set_error_t(std::exception_ptr).
+ * - Each set_value_t(_A_...) in _CS_ is copied.
+ * - Each set_error_t(_A_...) in _CS_ is copied.
+ *
+ * Example
+ *
+ *
+ * #include
+ * #include
+ * namespace ex = beman::execution;
+ *
+ * int main() {
+ * auto result = ex::sync_wait(ex::just_stopped() | ex::upon_stopped([]() { return true; }));
+ * assert(result);
+ * assert(*result == std::tuple(true));
+ * }
+ *
+ */
inline constexpr ::beman::execution::upon_stopped_t upon_stopped{};
} // namespace beman::execution