From 972bd51a97e3799f0e69811befb7e7a7d91a234f Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Wed, 26 Aug 2026 12:14:16 -0600 Subject: [PATCH] docs: source duplicated reference snippets from the library itself Several pages showed a library definition by keeping a hand-maintained copy of it in a snippet translation unit, free to drift from what ships -- and it had: #383 changed IoAwaitable while both copies still showed the older one-requirement version. Closes #381. Six definitions now carry tag::/end:: markers in their headers: Executor, ReadStream, WriteStream, IoRunnable, the two buffer sequence concepts and safe_resume. An antora.yml collector entry scans those headers into the examples tree, and ten include:: directives target the header instead of the snippet, so a page can no longer show a definition the library does not have. IoAwaitable is left out deliberately. #383 replaced both of its code blocks with prose, so no page shows the concept and a marker would have no consumer. Each copy sat in a definition/concept_sketch namespace beside a pair of static_asserts, one against the copy and one against the real concept, under a comment claiming the two must match. The pair never established that: both hold whenever a single type satisfies both concepts, however far apart the definitions have drifted. The copies and the comments are gone, the asserts against the real concepts stay, and seven includes left unused by the deletions are removed. 9n's abbreviated WriteStream is replaced by the whole concept rather than pinned to it. A nested tag could only mark the opening lines; the elision marker and closing brace are not in the header, so the page would have to supply them as literal text, which doc-lint's B2 rule forbids. Showing all of it also serves the argument, since the page contrasts Capy's concept with Cobalt's concrete parameter and hiding half of Capy's requirements understates what the concept asks of a type. doc/lint/check-include-tags.mjs is new and blocking. Sourcing from headers removes the drift but leaves a quieter failure: Asciidoctor treats a missing include tag as a warning and exits 0, rendering an empty block, and the Antora CI leg only asserts that build/site exists because Antora also exits 0 on failure. A marker deleted during refactoring would empty a page with nothing going red. The check resolves every include::example$ directive through antora.yml's own scan config and fails on an unresolved file or a missing tag, across all 355 tagged includes rather than only the ten moved here. It stays outside the baseline/comparator posture the other lint steps share: it is a structural check, not a style rule with a backlog to burn down, and it stands at zero violations. The markers sit on their own line after the closing */ because MrDocs passes -fparse-all-comments, which merges a comment adjacent to a docstring into it. --- .github/workflows/docs.yml | 16 ++ doc/antora.yml | 11 ++ doc/lint/check-include-tags.mjs | 145 ++++++++++++++++++ .../pages/4.coroutines/4g.allocators.adoc | 2 +- .../ROOT/pages/5.buffers/5a.buffers.adoc | 4 +- .../ROOT/pages/6.streams/6b.streams.adoc | 4 +- .../ROOT/pages/9.design/9c.ReadStream.adoc | 2 +- .../ROOT/pages/9.design/9f.WriteStream.adoc | 2 +- .../ROOT/pages/9.design/9k.Executor.adoc | 2 +- .../9.design/9n.WhyNotCobaltConcepts.adoc | 6 +- include/boost/capy/buffers.hpp | 4 + include/boost/capy/concept/executor.hpp | 2 + include/boost/capy/concept/io_runnable.hpp | 2 + include/boost/capy/concept/read_stream.hpp | 2 + include/boost/capy/concept/write_stream.hpp | 2 + include/boost/capy/ex/frame_allocator.hpp | 3 + test/doc/snippets/4d_io_awaitable.cpp | 18 +-- test/doc/snippets/5c_sequences.cpp | 39 +---- test/doc/snippets/6b_streams.cpp | 34 +--- test/doc/snippets/9c_read_stream.cpp | 19 +-- test/doc/snippets/9f_write_stream.cpp | 22 +-- test/doc/snippets/9k_executor.cpp | 53 +------ .../snippets/9n_why_not_cobalt_concepts.cpp | 89 +---------- 23 files changed, 217 insertions(+), 266 deletions(-) create mode 100644 doc/lint/check-include-tags.mjs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ca68ba5c0..c99f2a345 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,7 @@ # # Copyright (c) 2023 Christian Mazakas # Copyright (c) 2023 Alan de Freitas +# 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) @@ -249,6 +250,21 @@ jobs: working-directory: boost-root/libs/capy/doc run: node lint/doc-lint.mjs + # BLOCKING, and deliberately outside the baseline/comparator posture above: + # this is not a style rule with a backlog to burn down, it is a structural + # check that every `include::` still resolves. Pages take code straight from + # the compiled snippets and, since #381, from the public headers, so an + # include naming a tag that no longer exists renders an EMPTY block rather + # than failing. Asciidoctor exits 0 on that (warning only) and the Antora + # leg above only asserts that build/site exists, so nothing else here would + # catch a marker deleted during ordinary refactoring. It stands at zero + # violations, so there is nothing to grandfather. + - name: "Lint: include tags resolve" + if: always() + continue-on-error: false + working-directory: boost-root/libs/capy/doc + run: node lint/check-include-tags.mjs + - name: "Lint: accessibility (pa11y-ci)" if: always() continue-on-error: true diff --git a/doc/antora.yml b/doc/antora.yml index cf151276e..f06147b2b 100644 --- a/doc/antora.yml +++ b/doc/antora.yml @@ -1,5 +1,6 @@ # # Copyright (c) 2025 Mohammad Nejati +# 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) @@ -28,6 +29,16 @@ ext: into: modules/ROOT/examples/snippets - dir: test/doc/programs into: modules/ROOT/examples/programs + # Public headers carrying `tag::`/`end::` markers, so pages can include a + # definition straight from the library instead of keeping a copy that + # drifts. Scanned under their real path, so an `example$include/...` + # target in a page reads as the header it actually is. + - dir: include + files: + - boost/capy/concept/*.hpp + - boost/capy/buffers.hpp + - boost/capy/ex/frame_allocator.hpp + into: modules/ROOT/examples/include cpp-reference: config: doc/mrdocs.yml cpp-tagfiles: diff --git a/doc/lint/check-include-tags.mjs b/doc/lint/check-include-tags.mjs new file mode 100644 index 000000000..c367729d5 --- /dev/null +++ b/doc/lint/check-include-tags.mjs @@ -0,0 +1,145 @@ +#!/usr/bin/env node +// +// check-include-tags.mjs — every `include::example$...[tag=...]` in a page +// must resolve to a real file that really carries that tag. Node built-ins +// only, no dependencies. +// +// Why this exists: pages pull code out of compiled snippets and, since +// issue #381, straight out of the public headers, so a definition shown on +// a page cannot drift from the definition that ships. That moves the risk +// rather than removing it. Asciidoctor treats a missing include tag as a +// WARNING and still exits 0 — verified locally: a page referencing a +// nonexistent tag renders an empty listing block and the build succeeds. +// The Antora CI leg cannot catch it either; it only asserts that +// `build/site` exists, precisely because Antora also exits 0 on failure. +// +// So without this gate, deleting a `tag::`/`end::` marker from a header +// during ordinary refactoring silently empties whatever page included it, +// and nothing goes red. That is a worse failure than the drift it replaced: +// drift is at least visible on the page. +// +// The example$ -> repo-path mapping is read out of doc/antora.yml's +// collector scan config rather than hardcoded here, so adding a scan entry +// cannot leave this check behind. +// +// Blocking: exits 1 on any unresolved include target or missing tag. +// +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const DOC_DIR = path.resolve(SCRIPT_DIR, '..'); +const REPO_ROOT = path.resolve(DOC_DIR, '..'); +const PAGES_DIR = path.join(DOC_DIR, 'modules', 'ROOT', 'pages'); +const EXAMPLES_PREFIX = 'modules/ROOT/examples'; + +// Parse the `dir:`/`into:` pairs under ext.collector.scan in antora.yml. +// A full YAML parser would be a dependency; the block is a flat list of +// two- and three-key entries, so an indentation-aware line scan is enough. +function readScanMap(yamlPath) { + const lines = fs.readFileSync(yamlPath, 'utf8').split('\n'); + const entries = []; + let cur = null; + for (const line of lines) { + if (/^\s*#/.test(line)) continue; + const dir = line.match(/^\s*-\s*dir:\s*(\S+)\s*$/); + if (dir) { + if (cur) entries.push(cur); + cur = { dir: dir[1], into: null }; + continue; + } + const into = line.match(/^\s*into:\s*(\S+)\s*$/); + if (into && cur) { + cur.into = into[1]; + entries.push(cur); + cur = null; + } + } + if (cur) entries.push(cur); + + // example$/ -> /. Longest prefix wins, so a nested + // mapping such as examples/snippets is preferred over bare examples. + return entries + .filter((e) => e.into && e.into.startsWith(EXAMPLES_PREFIX)) + .map((e) => ({ + prefix: e.into.slice(EXAMPLES_PREFIX.length).replace(/^\//, ''), + dir: e.dir, + })) + .sort((a, b) => b.prefix.length - a.prefix.length); +} + +function resolveTarget(resource, scanMap) { + for (const { prefix, dir } of scanMap) { + if (prefix === '') return path.join(REPO_ROOT, dir, resource); + if (resource === prefix || resource.startsWith(prefix + '/')) { + return path.join(REPO_ROOT, dir, resource.slice(prefix.length).replace(/^\//, '')); + } + } + return null; +} + +// tag=a | tags=a;b;!c | tags=a,b — negations and wildcards select +// nothing on their own, so they are not names this check can verify. +function tagsOf(attrs) { + const m = attrs.match(/\btags?=([^,\]]*)/); + if (!m) return []; + return m[1] + .split(/[;,]/) + .map((t) => t.trim()) + .filter((t) => t && !t.startsWith('!') && !t.includes('*')); +} + +function walk(dir, out = []) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p, out); + else if (e.name.endsWith('.adoc')) out.push(p); + } + return out; +} + +const scanMap = readScanMap(path.join(DOC_DIR, 'antora.yml')); +if (scanMap.length === 0) { + console.error('check-include-tags: no collector scan entries found in antora.yml'); + process.exit(1); +} + +const violations = []; +let checked = 0; + +for (const page of walk(PAGES_DIR)) { + const rel = path.relative(REPO_ROOT, page); + const lines = fs.readFileSync(page, 'utf8').split('\n'); + lines.forEach((line, i) => { + const m = line.match(/^include::example\$(\S+?)\[([^\]]*)\]/); + if (!m) return; + const [, resource, attrs] = m; + const where = `${rel}:${i + 1}`; + const target = resolveTarget(resource, scanMap); + if (!target) { + violations.push(`${where}: example$${resource} matches no collector scan entry`); + return; + } + if (!fs.existsSync(target)) { + violations.push(`${where}: example$${resource} resolves to a missing file (${path.relative(REPO_ROOT, target)})`); + return; + } + const body = fs.readFileSync(target, 'utf8'); + for (const tag of tagsOf(attrs)) { + checked++; + const has = body.includes(`tag::${tag}[]`) && body.includes(`end::${tag}[]`); + if (!has) { + violations.push(`${where}: tag '${tag}' not found in ${path.relative(REPO_ROOT, target)}`); + } + } + }); +} + +if (violations.length) { + console.error(`check-include-tags: ${violations.length} violation(s)\n`); + for (const v of violations) console.error(` ${v}`); + process.exit(1); +} + +console.log(`check-include-tags: OK — ${checked} tagged include(s) resolve to a live tag.`); diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc index 44cb3af54..5f22fd406 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc @@ -238,7 +238,7 @@ cpp:safe_resume[]'s implementation: [source,cpp] ---- -include::example$snippets/9k_executor.cpp[tag=safe_resume] +include::example$include/boost/capy/ex/frame_allocator.hpp[tag=safe_resume] ---- The cost is two TLS accesses (one read, one write) per `.resume()` call, negligible compared to the cost of resuming a coroutine. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc index 11295e81c..79e43f1d4 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc @@ -111,14 +111,14 @@ Treating a single buffer as a one-element sequence is deliberate. It lets one co [source,cpp] ---- -include::example$snippets/5c_sequences.cpp[tag=const_buffer_sequence_concept,indent=0] +include::example$include/boost/capy/buffers.hpp[tag=const_buffer_sequence_concept,indent=0] ---- A type satisfies cpp:ConstBufferSequence[] if it converts to cpp:const_buffer[] directly, or if it is a bidirectional range whose elements convert to cpp:const_buffer[]. [source,cpp] ---- -include::example$snippets/5c_sequences.cpp[tag=mutable_buffer_sequence_concept,indent=0] +include::example$include/boost/capy/buffers.hpp[tag=mutable_buffer_sequence_concept,indent=0] ---- cpp:MutableBufferSequence[] follows the same pattern for cpp:mutable_buffer[]. diff --git a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc index 96f09e72e..65383cf42 100644 --- a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc +++ b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc @@ -7,7 +7,7 @@ A type satisfies cpp:ReadStream[] if it provides partial read operations via `re [source,cpp] ---- -include::example$snippets/6b_streams.cpp[tag=read_stream_concept] +include::example$include/boost/capy/concept/read_stream.hpp[tag=read_stream_concept] ---- The `requires` clause names a single representative buffer (cpp:mutable_buffer_archetype[]) because a {cpp} concept cannot say "works with every buffer sequence." The real contract is that `read_some` accepts *any* cpp:MutableBufferSequence[]—one buffer or a range; the archetype only samples that requirement. @@ -40,7 +40,7 @@ A type satisfies cpp:WriteStream[] if it provides partial write operations via ` [source,cpp] ---- -include::example$snippets/6b_streams.cpp[tag=write_stream_concept] +include::example$include/boost/capy/concept/write_stream.hpp[tag=write_stream_concept] ---- As with cpp:ReadStream[], the cpp:const_buffer_archetype[] is only a representative: the real contract is that `write_some` accepts *any* cpp:ConstBufferSequence[], which a {cpp} concept cannot fully express. diff --git a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc index c24fb0aba..9ff8211cc 100644 --- a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc @@ -9,7 +9,7 @@ cpp:ReadStream[] is the fundamental partial-read primitive among Capy's stream c [source,cpp] ---- -include::example$snippets/9c_read_stream.cpp[tag=concept_definition,indent=0] +include::example$include/boost/capy/concept/read_stream.hpp[tag=read_stream_concept,indent=0] ---- The `requires` clause checks `read_some` against a single representative buffer, cpp:mutable_buffer_archetype[], because a {cpp} concept cannot quantify over "every buffer sequence." The contract is stronger than what the compiler verifies: a cpp:ReadStream[] must accept *any* cpp:MutableBufferSequence[]—a single buffer or a range of them—and the archetype stands in for that universally-quantified requirement. diff --git a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc index 2025b5640..e0d3dcfaf 100644 --- a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc @@ -9,7 +9,7 @@ cpp:WriteStream[] is the fundamental partial-write primitive among Capy's stream [source,cpp] ---- -include::example$snippets/9f_write_stream.cpp[tag=write_stream_concept] +include::example$include/boost/capy/concept/write_stream.hpp[tag=write_stream_concept] ---- The `requires` clause checks `write_some` against a single representative buffer, cpp:const_buffer_archetype[], because a {cpp} concept cannot quantify over "every buffer sequence." The contract is stronger than what the compiler verifies: a cpp:WriteStream[] must accept *any* cpp:ConstBufferSequence[]—a single buffer or a range of them—and the archetype stands in for that universally-quantified requirement. diff --git a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc index 429313bb3..00644ddc5 100644 --- a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc +++ b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc @@ -9,7 +9,7 @@ The cpp:Executor[] concept exists to answer one question: when a coroutine is re [source,cpp] ---- -include::example$snippets/9k_executor.cpp[tag=executor_concept] +include::example$include/boost/capy/concept/executor.hpp[tag=executor_concept] ---- An cpp:Executor[] provides exactly two scheduling operations: diff --git a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc index 7ec33d334..c876e8c85 100644 --- a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc +++ b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc @@ -41,7 +41,7 @@ cpp:IoRunnable[] refines cpp:IoAwaitable[] with operations needed to start a tas [source,cpp] ---- -include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=io_runnable_concept] +include::example$include/boost/capy/concept/io_runnable.hpp[tag=io_runnable_concept] ---- Context injection (cpp:IoRunnable::set_environment[set_environment], cpp:IoRunnable::set_continuation[set_continuation]) consists of `noexcept` requirements that cpp:IoRunnable[] places on `T::promise_type`; launcher functions invoke them through the typed handle returned by cpp:IoRunnable::handle[handle()] before resuming the frame. @@ -215,7 +215,7 @@ Capy's cpp:WriteStream[] concept requires `write_some` to accept any cpp:ConstBu [source,cpp] ---- -include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=write_stream_concept_short] +include::example$include/boost/capy/concept/write_stream.hpp[tag=write_stream_concept] ---- The type-erased wrapper cpp:any_write_stream[] also models the cpp:WriteStream[] concept. Its `write_some` is a template that accepts any cpp:ConstBufferSequence[]: @@ -438,7 +438,7 @@ Capy separates the abstraction from the wrapper. cpp:WriteStream[] is a {cpp}20 [source,cpp] ---- -include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=write_stream_concept] +include::example$include/boost/capy/concept/write_stream.hpp[tag=write_stream_concept] ---- cpp:any_write_stream[] is a type-erased wrapper that satisfies this concept. It is one possible reification, not the only one. Users can: diff --git a/include/boost/capy/buffers.hpp b/include/boost/capy/buffers.hpp index 8c5385b8c..9185d9934 100644 --- a/include/boost/capy/buffers.hpp +++ b/include/boost/capy/buffers.hpp @@ -235,11 +235,13 @@ class const_buffer @see const_buffer, MutableBufferSequence */ +// tag::const_buffer_sequence_concept[] template concept ConstBufferSequence = std::is_convertible_v || ( std::ranges::bidirectional_range && std::is_convertible_v, const_buffer>); +// end::const_buffer_sequence_concept[] /** Requires a type to convert to `mutable_buffer`, or be a range of such buffers. @@ -258,11 +260,13 @@ concept ConstBufferSequence = @see mutable_buffer, ConstBufferSequence */ +// tag::mutable_buffer_sequence_concept[] template concept MutableBufferSequence = std::is_convertible_v || ( std::ranges::bidirectional_range && std::is_convertible_v, mutable_buffer>); +// end::mutable_buffer_sequence_concept[] /** Return an iterator to the first buffer in a sequence. diff --git a/include/boost/capy/concept/executor.hpp b/include/boost/capy/concept/executor.hpp index 21e4cc4bd..adf9cd385 100644 --- a/include/boost/capy/concept/executor.hpp +++ b/include/boost/capy/concept/executor.hpp @@ -140,6 +140,7 @@ class execution_context; @see ExecutionContext, execution_context */ +// tag::executor_concept[] template concept Executor = std::is_nothrow_copy_constructible_v && @@ -157,6 +158,7 @@ concept Executor = { ce.dispatch(c) } -> std::same_as>; { ce.post(c) }; }; +// end::executor_concept[] } // capy } // boost diff --git a/include/boost/capy/concept/io_runnable.hpp b/include/boost/capy/concept/io_runnable.hpp index 661847b70..acc9a47bc 100644 --- a/include/boost/capy/concept/io_runnable.hpp +++ b/include/boost/capy/concept/io_runnable.hpp @@ -86,6 +86,7 @@ namespace capy { @see IoAwaitable, run, run_async */ +// tag::io_runnable_concept[] template concept IoRunnable = IoAwaitable && @@ -102,6 +103,7 @@ concept IoRunnable = requires(typename T::promise_type& p) { p.result(); }); +// end::io_runnable_concept[] } // namespace capy } // namespace boost diff --git a/include/boost/capy/concept/read_stream.hpp b/include/boost/capy/concept/read_stream.hpp index 1b8b02d70..d2b05f236 100644 --- a/include/boost/capy/concept/read_stream.hpp +++ b/include/boost/capy/concept/read_stream.hpp @@ -104,6 +104,7 @@ namespace capy { @see IoAwaitable, MutableBufferSequence, awaitable_decomposes_to */ +// tag::read_stream_concept[] template concept ReadStream = requires(T& stream, mutable_buffer_archetype buffers) @@ -113,6 +114,7 @@ concept ReadStream = decltype(stream.read_some(buffers)), std::error_code, std::size_t>; }; +// end::read_stream_concept[] } // namespace capy } // namespace boost diff --git a/include/boost/capy/concept/write_stream.hpp b/include/boost/capy/concept/write_stream.hpp index 97b9554b0..fc3b3fbcc 100644 --- a/include/boost/capy/concept/write_stream.hpp +++ b/include/boost/capy/concept/write_stream.hpp @@ -113,6 +113,7 @@ namespace capy { @see IoAwaitable, ConstBufferSequence, awaitable_decomposes_to */ +// tag::write_stream_concept[] template concept WriteStream = requires(T& stream, const_buffer_archetype buffers) @@ -122,6 +123,7 @@ concept WriteStream = decltype(stream.write_some(buffers)), std::error_code, std::size_t>; }; +// end::write_stream_concept[] } // namespace capy } // namespace boost diff --git a/include/boost/capy/ex/frame_allocator.hpp b/include/boost/capy/ex/frame_allocator.hpp index b3837515c..2d66c6a26 100644 --- a/include/boost/capy/ex/frame_allocator.hpp +++ b/include/boost/capy/ex/frame_allocator.hpp @@ -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) @@ -135,6 +136,7 @@ set_current_frame_allocator( @see get_current_frame_allocator, set_current_frame_allocator */ +// tag::safe_resume[] inline void safe_resume(std::coroutine_handle<> h) noexcept { @@ -142,6 +144,7 @@ safe_resume(std::coroutine_handle<> h) noexcept h.resume(); set_current_frame_allocator(saved); } +// end::safe_resume[] } // namespace capy } // namespace boost diff --git a/test/doc/snippets/4d_io_awaitable.cpp b/test/doc/snippets/4d_io_awaitable.cpp index 02f731982..2249a0553 100644 --- a/test/doc/snippets/4d_io_awaitable.cpp +++ b/test/doc/snippets/4d_io_awaitable.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -91,24 +92,7 @@ struct io_awaiter_signature // end::two_arg_await_suspend[] }; -// The real definition lives in ; -// the sketch is checked below against types that satisfy (and fail) -// the real concept. -namespace concept_sketch { - -// tag::io_awaitable_concept[] -template -concept IoAwaitable = - requires(A a, std::coroutine_handle<> h, io_env const* env) { - a.await_suspend(h, env); - }; -// end::io_awaitable_concept[] - -} // namespace concept_sketch - -static_assert(concept_sketch::IoAwaitable>); static_assert(capy::IoAwaitable>); -static_assert(!concept_sketch::IoAwaitable); static_assert(!capy::IoAwaitable); struct caller_promise diff --git a/test/doc/snippets/5c_sequences.cpp b/test/doc/snippets/5c_sequences.cpp index 6c1a90001..ab4ea1a01 100644 --- a/test/doc/snippets/5c_sequences.cpp +++ b/test/doc/snippets/5c_sequences.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -52,7 +53,6 @@ #include #include -#include #include #include #include @@ -68,38 +68,11 @@ namespace { using namespace boost::capy; -// The page's concept definitions; the sketch namespace keeps them from -// clashing with the real ones, and the asserts below prove they match. -namespace concept_sketch { - -// tag::const_buffer_sequence_concept[] -template -concept ConstBufferSequence = - std::is_convertible_v || ( - std::ranges::bidirectional_range && - std::is_convertible_v, const_buffer>); -// end::const_buffer_sequence_concept[] - -// tag::mutable_buffer_sequence_concept[] -template -concept MutableBufferSequence = - std::is_convertible_v || ( - std::ranges::bidirectional_range && - std::is_convertible_v, mutable_buffer>); -// end::mutable_buffer_sequence_concept[] - -} // namespace concept_sketch - -static_assert(concept_sketch::ConstBufferSequence == - ConstBufferSequence); -static_assert(concept_sketch::ConstBufferSequence> == - ConstBufferSequence>); -static_assert(concept_sketch::ConstBufferSequence == - ConstBufferSequence); -static_assert(concept_sketch::MutableBufferSequence == - MutableBufferSequence); -static_assert(concept_sketch::MutableBufferSequence == - MutableBufferSequence); +static_assert(ConstBufferSequence); +static_assert(ConstBufferSequence>); +static_assert(!ConstBufferSequence); +static_assert(MutableBufferSequence); +static_assert(!MutableBufferSequence); // tag::send_signature[] template diff --git a/test/doc/snippets/6b_streams.cpp b/test/doc/snippets/6b_streams.cpp index 75df9c9f3..fa1673433 100644 --- a/test/doc/snippets/6b_streams.cpp +++ b/test/doc/snippets/6b_streams.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -38,8 +39,6 @@ #include #include -#include -#include #include #include // tag::any_read_stream_include[] @@ -72,40 +71,9 @@ namespace { using namespace boost::capy; -namespace definition { - -// tag::read_stream_concept[] -template -concept ReadStream = - requires(T& stream, mutable_buffer_archetype buffers) - { - { stream.read_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.read_some(buffers)), - std::error_code, std::size_t>; - }; -// end::read_stream_concept[] - -// tag::write_stream_concept[] -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.write_some(buffers)), - std::error_code, std::size_t>; - }; -// end::write_stream_concept[] - -// The page's definitions must match the library's. -static_assert(definition::ReadStream); static_assert(capy::ReadStream); -static_assert(definition::WriteStream); static_assert(capy::WriteStream); -} // namespace definition - task<> partial_read(test::stream& stream) { // tag::read_partial[] diff --git a/test/doc/snippets/9c_read_stream.cpp b/test/doc/snippets/9c_read_stream.cpp index 1da770a53..91ccf385f 100644 --- a/test/doc/snippets/9c_read_stream.cpp +++ b/test/doc/snippets/9c_read_stream.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -62,26 +63,8 @@ namespace { using namespace boost::capy; -namespace definition { - -// tag::concept_definition[] -template -concept ReadStream = - requires(T& stream, mutable_buffer_archetype buffers) - { - { stream.read_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.read_some(buffers)), - std::error_code, std::size_t>; - }; -// end::concept_definition[] - -// The page's definition must match the library's. -static_assert(definition::ReadStream); static_assert(capy::ReadStream); -} // namespace definition - namespace composed { // tag::read_signature[] diff --git a/test/doc/snippets/9f_write_stream.cpp b/test/doc/snippets/9f_write_stream.cpp index 9b301bdfa..5eade1038 100644 --- a/test/doc/snippets/9f_write_stream.cpp +++ b/test/doc/snippets/9f_write_stream.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -37,7 +38,6 @@ #endif #include -#include #include #include #include @@ -61,27 +61,7 @@ namespace { using namespace boost::capy; -// The real definition lives in ; -// the sketch is checked below against the real concept. -namespace concept_sketch { - -// tag::write_stream_concept[] -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.write_some(buffers)), - std::error_code, std::size_t>; - }; -// end::write_stream_concept[] - -} // namespace concept_sketch - -static_assert(concept_sketch::WriteStream); static_assert(capy::WriteStream); -static_assert(!concept_sketch::WriteStream); static_assert(!capy::WriteStream); // The real algorithms live in and diff --git a/test/doc/snippets/9k_executor.cpp b/test/doc/snippets/9k_executor.cpp index 92e68f0ff..e903dc2fb 100644 --- a/test/doc/snippets/9k_executor.cpp +++ b/test/doc/snippets/9k_executor.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -40,7 +41,6 @@ #include #include #include -#include #include #include @@ -58,40 +58,9 @@ namespace { using namespace boost::capy; -// The page shows the concept exactly as defined in -// ; compiling a copy keeps the page -// in sync with the real definition. -namespace concept_def { - -// tag::executor_concept[] -template -concept Executor = - std::is_nothrow_copy_constructible_v && - std::is_nothrow_move_constructible_v && - requires(E& e, E const& ce, E const& ce2, - continuation c) - { - { ce == ce2 } noexcept -> std::convertible_to; - { ce.context() } noexcept; - requires std::is_lvalue_reference_v< - decltype(ce.context())> && - std::derived_from< - std::remove_reference_t< - decltype(ce.context())>, - execution_context>; - { ce.on_work_started() } noexcept; - { ce.on_work_finished() } noexcept; - - { ce.dispatch(c) } -> std::same_as>; - { ce.post(c) }; - }; -// end::executor_concept[] - -} // namespace concept_def - -static_assert(concept_def::Executor); -static_assert(concept_def::Executor); -static_assert(!concept_def::Executor); +static_assert(capy::Executor); +static_assert(capy::Executor); +static_assert(!capy::Executor); // Scaffolding context so the conforming dispatch shown on the page // compiles. @@ -215,20 +184,6 @@ struct io_awaitable_sketch } }; -namespace safe_resume_def { - -// tag::safe_resume[] -inline void -safe_resume(std::coroutine_handle<> h) noexcept -{ - auto* saved = get_current_frame_allocator(); - h.resume(); - set_current_frame_allocator(saved); -} -// end::safe_resume[] - -} // namespace safe_resume_def - // Declarations are enough for the concept check; the definitions are // a real implementation's concern. // tag::minimal_executor[] diff --git a/test/doc/snippets/9n_why_not_cobalt_concepts.cpp b/test/doc/snippets/9n_why_not_cobalt_concepts.cpp index c4557b071..f6621ab2a 100644 --- a/test/doc/snippets/9n_why_not_cobalt_concepts.cpp +++ b/test/doc/snippets/9n_why_not_cobalt_concepts.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// 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) @@ -37,8 +38,6 @@ #endif #include -#include -#include #include #include #include @@ -63,46 +62,9 @@ namespace { capy::task<> my_algo(capy::any_write_stream& stream); // end::capy_signature[] -// The concept definitions the page reproduces must stay in sync with -// the shipped ones; local mirrors compile them and the static_asserts -// below compare them against the real concepts. -namespace task_requirements { - -using boost::capy::io_env; - -// tag::io_awaitable_concept[] -template -concept IoAwaitable = - requires(A a, std::coroutine_handle<> h, io_env const* env) - { - a.await_suspend(h, env); - }; -// end::io_awaitable_concept[] - -// tag::io_runnable_concept[] -template -concept IoRunnable = - IoAwaitable && - requires { typename T::promise_type; } && - requires(T& t, T const& ct, typename T::promise_type const& cp, - typename T::promise_type& p) - { - { ct.handle() } noexcept - -> std::same_as>; - { cp.exception() } noexcept -> std::same_as; - { t.release() } noexcept; - { p.set_continuation(std::coroutine_handle<>{}) } noexcept; - { p.set_environment(static_cast(nullptr)) } noexcept; - } && - (std::is_void_v().await_resume())> || - requires(typename T::promise_type& p) { p.result(); }); -// end::io_runnable_concept[] - -static_assert(IoAwaitable> == capy::IoAwaitable>); -static_assert(IoRunnable> == capy::IoRunnable>); -static_assert(IoRunnable> == capy::IoRunnable>); - -} // namespace task_requirements +static_assert(capy::IoAwaitable>); +static_assert(capy::IoRunnable>); +static_assert(capy::IoRunnable>); namespace context_propagation { @@ -118,25 +80,6 @@ struct child_operation } // namespace context_propagation -namespace concept_short { - -using boost::capy::const_buffer_archetype; -using boost::capy::IoAwaitable; - -// tag::write_stream_concept_short[] -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - // ... - }; -// end::write_stream_concept_short[] - -static_assert(WriteStream); - -} // namespace concept_short - // tag::semantics_quote[] // From capy/concept/write_stream.hpp @@ -246,28 +189,6 @@ any_write_stream::any_write_stream(S s) } // namespace awaitable_storage -namespace concept_full { - -using boost::capy::awaitable_decomposes_to; -using boost::capy::const_buffer_archetype; -using boost::capy::IoAwaitable; - -// tag::write_stream_concept[] -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.write_some(buffers)), - std::error_code, std::size_t>; - }; -// end::write_stream_concept[] - -static_assert(WriteStream == - capy::WriteStream); -static_assert(WriteStream); - -} // namespace concept_full +static_assert(capy::WriteStream); } // namespace