From b7cfc2e950ae1ea59875b8199eceed69b63dd1cf Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Tue, 20 Oct 2026 08:49:13 -0600 Subject: [PATCH] docs: give results a page of its own (#405) - Add "The results Type" page after "The task Type", with compiled and tested snippets. - Replace the tasks page's results section with a pointer to it. - Annotate the landing-page example and add an error-code section to the quick start, both linking to the new page. - Correct Concurrent Composition: when_any returns a std::variant. --- doc/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/4.coroutines/4.intro.adoc | 3 + .../ROOT/pages/4.coroutines/4b.tasks.adoc | 16 +- .../pages/4.coroutines/4g.composition.adoc | 2 +- .../ROOT/pages/4.coroutines/4k.results.adoc | 83 +++++++ doc/modules/ROOT/pages/index.adoc | 4 +- doc/modules/ROOT/pages/quick-start.adoc | 15 ++ test/doc/programs/index_page_echo.cpp | 1 + test/doc/snippets/4b_tasks.cpp | 33 --- test/doc/snippets/4k_results.cpp | 219 ++++++++++++++++++ test/doc/snippets/quick_start.cpp | 48 ++++ 11 files changed, 375 insertions(+), 50 deletions(-) create mode 100644 doc/modules/ROOT/pages/4.coroutines/4k.results.adoc create mode 100644 test/doc/snippets/4k_results.cpp diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index e00abaeac..cf5432be5 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -4,6 +4,7 @@ * xref:4.coroutines/4.intro.adoc[Coroutines in Capy] ** xref:4.coroutines/4a.flow-control.adoc[Flow control] ** xref:4.coroutines/4b.tasks.adoc[The task Type] +** xref:4.coroutines/4k.results.adoc[The results Type] ** xref:4.coroutines/4c.launching.adoc[Starting Coroutines] ** xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] ** xref:4.coroutines/4e.context-awaitable.adoc[The ContextAwaitable Protocol] diff --git a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc index acf5f7973..0f0c71750 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -18,6 +19,8 @@ Capy's coroutine model is built around a single principle: asynchronous code sho address the needs of asynchronous I/O in Capy. * xref:4.coroutines/4b.tasks.adoc[The task Type] -- Declaring, returning values from, and awaiting cpp:task[task] coroutines. +* xref:4.coroutines/4k.results.adoc[The results Type] -- How operations report expected + failures as an error code plus payload, without throwing. * xref:4.coroutines/4c.launching.adoc[Starting Coroutines] -- Starting coroutines with cpp:run_async[], and binding child tasks with cpp:run[]. * xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] -- Executors, diff --git a/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc b/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc index edc8da5c0..cb418fd07 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc @@ -46,20 +46,8 @@ For cpp:task[task], you can either use `co_return;` explicitly or let exec [#reporting-errors] == Reporting Errors: results -I/O operations report an expected failure as a value rather than an exception. They return the error alongside the result, in a cpp:results[results]: a `std::tuple` holding a `std::error_code` followed by zero or more payload values. Exceptions remain for genuine errors, such as a failed frame allocation. - -A coroutine that reports errors this way returns cpp:task[task]`>`: - -[source,cpp] ----- -include::example$snippets/4b_tasks.cpp[tag=results] ----- - -Because cpp:results[] is a `std::tuple`, the whole standard tuple API applies: structured bindings, `std::tie`, `std::apply`, `std::get`, `std::tuple_cat`, comparisons, and tuple assignment. The error code is the first element, so `auto [ec, n] = co_await s.read_some(buf);` destructures it directly, and `std::tie` rebinds into existing variables without introducing new ones. Always test the error code before reading a payload; a payload's meaning when it is set is defined by the operation that produced it. - -For cpp:results[results<>] -- no payload -- a `std::error_code` converts implicitly, so `co_return some_ec;` compiles. With payloads present you must supply the whole result, as `count_ready` shows. - -This is the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4g.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Streams] both assume them. +I/O operations report an expected failure as a value rather than an exception, and a coroutine that reports errors this way returns cpp:task[task]`>`. +xref:4.coroutines/4k.results.adoc[The results Type] explains how to consume and produce a cpp:results[], which the stream concepts and the concurrent combinators all use. == Running a Task diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc index 189bc54ca..fe81c8084 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc @@ -3,7 +3,7 @@ Headers: `` and ``. cpp:async_waker[] on this page is ``. -Both combinators take cpp:task[task]`>` children and return their results as a single cpp:results[]. If that type is new, read xref:4.coroutines/4b.tasks.adoc#reporting-errors[Reporting Errors] first -- everything below is expressed in them. +Both combinators take cpp:task[task]`>` children. cpp:when_all[] returns a single cpp:results[], and cpp:when_any[] returns a `std::variant` of the error code and the children's payloads. If that type is new, read xref:4.coroutines/4k.results.adoc[The results Type] first -- everything below is expressed in them. == Overview diff --git a/doc/modules/ROOT/pages/4.coroutines/4k.results.adoc b/doc/modules/ROOT/pages/4.coroutines/4k.results.adoc new file mode 100644 index 000000000..56be79340 --- /dev/null +++ b/doc/modules/ROOT/pages/4.coroutines/4k.results.adoc @@ -0,0 +1,83 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += The results Type +:page-mode: explanation + +I/O operations in Capy report an expected failure, such as reaching the end of a stream, as a value. +Exceptions are kept for genuine errors, such as a failed frame allocation. +The value is a cpp:results[results], declared in: + +[source,cpp] +---- +include::example$snippets/4k_results.cpp[tag=include_results] +---- + +`results` is an alias for `std::tuple`. +The error code always comes first. Zero or more payload values follow it. +Stream operations return `results`: an error code, then a byte count. + +== Consuming results + +Unpack the result with a structured binding, then test the error code before using the payload: + +[source,cpp] +---- +include::example$snippets/4k_results.cpp[tag=consume] +---- + +Compare the error code against a condition, not an exact error value. +For example, cpp:cond::eof[cond::eof] matches the end-of-stream error that Capy's streams and Corosio's sockets report. + +== Payload Alongside an Error + +Whether a payload is meaningful when the error code is set depends on the operation that produced it. +Many I/O operations report a meaningful partial result alongside the error. +cpp:read[] and cpp:write[] report the bytes transferred before the condition stopped them: + +[source,cpp] +---- +include::example$snippets/4k_results.cpp[tag=partial] +---- + +Given a stream that holds only `"hi"`, this read stops with cpp:cond::eof[cond::eof] and `n == 2`. +Other operations leave the payload unspecified on error, so check each operation's reference page. + +== Reusing Variables with std::tie + +Because `results` is a `std::tuple`, `std::tie` assigns into variables that already exist. +This suits a loop, where a structured binding would introduce new names on every pass: + +[source,cpp] +---- +include::example$snippets/4k_results.cpp[tag=tie] +---- + +The rest of the standard tuple API applies too: `std::get`, `std::apply`, `std::tuple_cat`, comparisons, and tuple assignment. + +[#producing] +== Producing results + +A coroutine that reports errors this way returns cpp:task[task]`>`: + +[source,cpp] +---- +include::example$snippets/4k_results.cpp[tag=produce] +---- + +For `results<>`, with no payload, a `std::error_code` converts implicitly, so `co_return some_ec;` compiles. +With payloads present you must supply the whole tuple, as `count_ready` shows. + +== Where results Appears + +* xref:6.streams/6.intro.adoc[Streams] -- `read_some`, `write_some`, cpp:read[] and cpp:write[] return `results`. +* xref:4.coroutines/4g.composition.adoc[Concurrent Composition] -- cpp:when_all[] flattens its children's payloads into one `results`. + cpp:when_any[] returns a `std::variant` whose first alternative is the error code and whose others are the children's payloads. +* xref:8.examples/8l.async-mutex.adoc[Async Mutex] -- cpp:async_mutex[]'s `scoped_lock()` returns an error code and a lock guard. +* xref:A.specification-methods/Ac.contingencies.adoc[Contingencies] -- What a contingency is, and how `ec` signals one. diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 7e310cfcc..de2ccf025 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -42,7 +42,7 @@ This can be either your home-grown one or Corosio. include::example$programs/index_page_echo.cpp[tag=full] ---- <.> The cpp:task[task<>] return type defines a coroutine that starts suspended. `any_stream` is a type-erased wrapper that works with any concrete stream implementation. -<.> Each `co_await` suspends until the I/O operation completes. +<.> Each `co_await` suspends until the I/O operation completes. The result is a cpp:results[], which the structured binding unpacks into `ec` and `n`. See xref:4.coroutines/4k.results.adoc[The results Type]. <.> You can signal failures by throwing an exception, or by returning a `std::error_code`. <.> The condition `cond::eof` indicates reaching the end of the stream. @@ -184,7 +184,7 @@ Your caller supplies the stream, and an I/O library supplies the I/O awaitables. To keep the protocol out of headers, write it against the type-erased cpp:any_stream[] instead. * The executor and the stop token reach your task through `co_await`. Your functions do not take them as parameters. -* Each stream operation returns a cpp:results[] that holds an error code and a byte count. +* Each stream operation returns a cpp:results[] that holds an error code and a byte count. See xref:4.coroutines/4k.results.adoc[The results Type]. A read can return bytes and an error together. The stream only reports what happened. Your protocol decides whether the result is a success or a failure, for example whether a partial message is usable. diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index 088939430..ff04db57d 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -102,9 +103,23 @@ Exceptions propagate through coroutine chains. To handle them at the top level: include::example$snippets/quick_start.cpp[tag=errors,indent=0] ---- +== Error Codes Instead of Exceptions + +I/O operations do not throw for expected failures, such as reaching the end of a stream. +They report the failure as a value instead: a cpp:results[], holding an error code followed by the payload. +A task of your own can do the same, and the caller unpacks the error code first: + +[source,cpp] +---- +include::example$snippets/quick_start.cpp[tag=error_codes,indent=0] +---- + +See xref:4.coroutines/4k.results.adoc[The results Type]. + == Next Steps * xref:2.cpp20-coroutines/2.intro.adoc[Introduction To {cpp}20 Coroutines] — If you need to refresh the C++ coroutines basics * xref:4.coroutines/4b.tasks.adoc[Tasks] — Learn how lazy tasks work +* xref:4.coroutines/4k.results.adoc[The results Type] — How operations report errors without throwing * xref:4.coroutines/4c.launching.adoc[Starting Tasks] — Understand cpp:run_async[] in detail * xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] — Control where coroutines execute diff --git a/test/doc/programs/index_page_echo.cpp b/test/doc/programs/index_page_echo.cpp index 33e45e948..f51c83ae5 100644 --- a/test/doc/programs/index_page_echo.cpp +++ b/test/doc/programs/index_page_echo.cpp @@ -20,6 +20,7 @@ capy::task echo(capy::any_stream& stream) // <.> char buf[1024]; for(;;) { + // read_some returns results: an error code, then a byte count auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); // <.> auto [wec, _] = co_await capy::write(stream, capy::const_buffer(buf, n)); diff --git a/test/doc/snippets/4b_tasks.cpp b/test/doc/snippets/4b_tasks.cpp index d51ebe0fa..4f63e3cff 100644 --- a/test/doc/snippets/4b_tasks.cpp +++ b/test/doc/snippets/4b_tasks.cpp @@ -77,39 +77,6 @@ capy::task<> log_message(std::string msg) } // namespace returning -namespace reporting_errors { - -// tag::results[] -// `results` holds an error code `ec` plus zero or more payload -// values. A task returning one reports failure without throwing. - -capy::task> ensure_ready(bool ready) -{ - if(! ready) - co_return std::make_error_code(std::errc::not_connected); // ec converts - co_return {}; // success -} - -capy::task> count_ready(bool ready) -{ - using result = capy::results; - if(! ready) - co_return result{std::make_error_code(std::errc::not_connected), 0}; - co_return result{std::error_code(), 42}; // success, carrying a value -} - -capy::task<> use_them() -{ - // `results` models the tuple protocol: ec first, then the payloads. - auto [ec, n] = co_await count_ready(true); - if(ec) - co_return; // always check ec first - (void)n; // n is only meaningful when ec is falsy -} -// end::results[] - -} // namespace reporting_errors - namespace awaiting { // tag::awaiting[] diff --git a/test/doc/snippets/4k_results.cpp b/test/doc/snippets/4k_results.cpp new file mode 100644 index 000000000..5bd2342e4 --- /dev/null +++ b/test/doc/snippets/4k_results.cpp @@ -0,0 +1,219 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Compiled fragments shown in pages/4.coroutines/4k.results.adoc. Pages +// include the tagged regions; scaffolding stays outside the tags. + +#include "../doc_warnings.hpp" + +// GCC gives false positive -Wmaybe-uninitialized on structured bindings +// via the tuple protocol inside coroutine frames. +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + +// tag::include_results[] +#include +// end::include_results[] + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +namespace producing { + +// tag::produce[] +// `results` holds an error code `ec` plus zero or more payload +// values. A task returning one reports failure without throwing. + +capy::task> ensure_ready(bool ready) +{ + if(! ready) + co_return std::make_error_code(std::errc::not_connected); // ec converts + co_return {}; // success +} + +capy::task> count_ready(bool ready) +{ + using result = capy::results; + if(! ready) + co_return result{std::make_error_code(std::errc::not_connected), 0}; + co_return result{std::error_code(), 42}; // success, carrying a value +} + +capy::task<> use_them() +{ + // `results` models the tuple protocol: ec first, then the payloads. + auto [ec, n] = co_await count_ready(true); + if(ec) + co_return; // always check ec first + (void)n; // n is only meaningful when ec is falsy +} +// end::produce[] + +} // namespace producing + +// tag::consume[] +capy::task read_once(capy::test::read_stream& rs) +{ + char buf[64]; + // read_some returns results: an error code, then a byte count + auto [ec, n] = co_await rs.read_some(capy::make_buffer(buf)); + if(ec) // always test the error code first + co_return 0; + co_return n; // n is meaningful because ec is clear +} +// end::consume[] + +// tag::partial[] +capy::task<> read_header(capy::test::read_stream& rs) +{ + char buf[8]; + // read keeps going until buf is full, or a condition stops it + auto [ec, n] = co_await capy::read(rs, capy::make_buffer(buf)); + if(ec == capy::cond::eof) + { + // The stream ended early. n still counts the bytes + // placed in buf before it did. + std::string_view got(buf, n); + (void)got; + } +} +// end::partial[] + +// tag::tie[] +capy::task read_all(capy::test::read_stream& rs) +{ + std::string body; + char buf[64]; + std::error_code ec; + std::size_t n = 0; + for(;;) + { + // std::tie assigns into ec and n; no new bindings per iteration + std::tie(ec, n) = co_await rs.read_some(capy::make_buffer(buf)); + if(ec) + break; + body.append(buf, n); + } + // ec == capy::cond::eof when the stream ended normally + co_return body; +} +// end::tie[] + +struct results_test +{ + void + testProduce() + { + capy::test::fuse f; + auto r = f.inert([&](capy::test::fuse&) -> capy::task<> { + auto [ec0] = co_await producing::ensure_ready(false); + BOOST_TEST(ec0 == std::errc::not_connected); + + auto [ec1] = co_await producing::ensure_ready(true); + BOOST_TEST(! ec1); + + auto [ec2, n] = co_await producing::count_ready(true); + BOOST_TEST(! ec2); + BOOST_TEST(n == 42); + + auto [ec3, n3] = co_await producing::count_ready(false); + BOOST_TEST(ec3 == std::errc::not_connected); + (void)n3; + + co_await producing::use_them(); + }); + BOOST_TEST(r.success); + } + + void + testConsume() + { + capy::test::fuse f; + capy::test::read_stream rs(f); + rs.provide("hello"); + auto r = f.inert([&](capy::test::fuse&) -> capy::task<> { + std::size_t n = co_await read_once(rs); + BOOST_TEST(n == 5); + }); + BOOST_TEST(r.success); + } + + void + testEofCarriesPartialCount() + { + capy::test::fuse f; + capy::test::read_stream rs(f); + rs.provide("hi"); + auto r = f.inert([&](capy::test::fuse&) -> capy::task<> { + // The claim the page makes about the `partial` fragment. + char buf[8]; + auto [ec, n] = co_await capy::read(rs, capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST(n == 2); + BOOST_TEST(std::string_view(buf, n) == "hi"); + }); + BOOST_TEST(r.success); + + capy::test::read_stream rs2(f); + rs2.provide("hi"); + auto r2 = f.inert([&](capy::test::fuse&) -> capy::task<> { + co_await read_header(rs2); + }); + BOOST_TEST(r2.success); + } + + void + testTieRebinds() + { + capy::test::fuse f; + capy::test::read_stream rs(f, 1); + rs.provide("abc"); + auto r = f.inert([&](capy::test::fuse&) -> capy::task<> { + std::string body = co_await read_all(rs); + BOOST_TEST(body == "abc"); + + // The condition read_all's loop stopped on. + char buf[1]; + auto [ec, n] = co_await rs.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST(n == 0); + }); + BOOST_TEST(r.success); + } + + void + run() + { + testProduce(); + testConsume(); + testEofCarriesPartialCount(); + testTieRebinds(); + } +}; + +} // namespace + +TEST_SUITE(results_test, "boost.capy.doc.4k_results"); diff --git a/test/doc/snippets/quick_start.cpp b/test/doc/snippets/quick_start.cpp index 63fb59543..5280ba425 100644 --- a/test/doc/snippets/quick_start.cpp +++ b/test/doc/snippets/quick_start.cpp @@ -13,13 +13,18 @@ #include "../doc_warnings.hpp" +#include #include #include #include +#include #include #include #include +#include +#include +#include #include "test_suite.hpp" @@ -40,6 +45,28 @@ capy::task might_fail() co_return 0; } +// tag::error_codes[] +capy::task> parse_port(std::string_view s) +{ + int port = 0; + auto [end, err] = std::from_chars(s.data(), s.data() + s.size(), port); + if(err != std::errc() || end != s.data() + s.size()) + co_return {std::make_error_code(std::errc::invalid_argument), 0}; + co_return {std::error_code(), port}; +} + +capy::task<> connect_to_server() +{ + auto [ec, port] = co_await parse_port("8080"); // results: error code first, then the value + if(ec) + { + std::cerr << "Bad port: " << ec.message() << "\n"; + co_return; + } + std::cout << "Connecting on port " << port << "\n"; +} +// end::error_codes[] + struct quick_start_test { void @@ -77,11 +104,32 @@ struct quick_start_test pool.join(); } + void + testErrorCodes() + { + capy::thread_pool pool(1); + auto executor = pool.get_executor(); + std::error_code ok_ec, bad_ec; + int ok_port = 0; + capy::run_async(executor, [&](capy::results r) { + std::tie(ok_ec, ok_port) = r; + })(parse_port("8080")); + capy::run_async(executor, [&](capy::results r) { + bad_ec = std::get<0>(r); + })(parse_port("x")); + capy::run_async(executor)(connect_to_server()); + pool.join(); + BOOST_TEST(! ok_ec); + BOOST_TEST(ok_port == 8080); + BOOST_TEST(bad_ec == std::errc::invalid_argument); + } + void run() { testHandlingResults(); testHandlingErrors(); + testErrorCodes(); } };