diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e524fb73e..b6ce3eacf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,28 +15,66 @@ on: branches: - master - develop + - develop-2 + - 'pr/**' paths: - 'doc/**' + - 'include/**' - '*.adoc' - 'README.adoc' + - '.github/workflows/docs.yml' pull_request: paths: - 'doc/**' + - 'include/**' - '*.adoc' - 'README.adoc' + - '.github/workflows/docs.yml' + # Manual trigger, for authoring a replacement doc/lint/baseline.json in the CI + # environment — see the "Baseline reseed" steps at the end of the antora job. + # A reseed REWRITES the reference point of the blocking doc-quality gate, so it + # must never happen on push or pull_request: an automatic reseed would absorb + # real regressions into the grandfathered backlog, which is precisely what the + # gate exists to prevent. workflow_dispatch is the only trigger that reaches + # those steps (they are additionally guarded on github.event_name), and they + # only upload an artifact for a human to review and commit. + workflow_dispatch: jobs: antora: name: Antora Docs runs-on: 'ubuntu-latest' + # The a11y contrast gate (E4) is Review tier, not blocking (DOC_STYLE_GUIDE.md + # Part F.0 — demoted alongside E2: all gated failures were color-contrast on + # shared Antora theme nav chrome, not Capy-fixable). pa11y still needs a + # launchable browser to run the scan as a non-blocking report: .pa11yci.json + # defaults to /usr/bin/chromium (local), but ubuntu-latest ships google-chrome + # at /usr/bin/google-chrome and has no chromium. run-a11y.mjs reads + # PA11Y_CHROME_PATH to override the config, so point it at the runner's + # pre-installed Chrome for every step that runs the a11y scan (the scan step + # and the omnibus report). + env: + PA11Y_CHROME_PATH: /usr/bin/google-chrome defaults: run: shell: bash steps: + # asciidoctor here is the Ruby CLI that Vale 3.x shells out to when + # linting .adoc files (its lintAdoc scope). It is NOT the same as the JS + # @asciidoctor/core that Antora/build_antora.sh pulls in via `npm ci` — + # that has no `asciidoctor` binary on PATH. Without the Ruby CLI, `vale + # modules` and baseline.mjs's vale_adoc error with "asciidoctor not found" + # → the check is marked skipped → and because vale_adoc AND vale_docstrings + # are both GATED checks (A7 Capy.PartHeadings on the pages; C4/C9/C10 on both + # surfaces since Phase-4 exit) the gate's gated-skip path would fail the job on + # missing infra. Note the docstring corpus needs asciidoctor too: the extracted + # files are `.adoc`, so a missing Ruby CLI skips that slice as well. The apt package + # installs /usr/bin/asciidoctor, on PATH for every later step. This first + # step runs before the Antora build, the Vale steps, and the blocking gate. - name: Install packages uses: alandefreitas/cpp-actions/package-install@v1.9.0 with: - apt-get: git cmake + apt-get: git cmake asciidoctor - name: Clone Boost.Capy uses: actions/checkout@v4 @@ -113,3 +151,264 @@ jobs: with: name: antora-docs path: boost-root/libs/capy/doc/build/site + + # --- Doc-quality checks (Style Guide Part F.0 / doc improvement plan Task 2) --- + # Individual check steps below stay non-blocking (continue-on-error: true): they + # print each tool's raw findings for review. Enforcement is done by the + # no-new-violations comparator, which diffs a fresh run against baseline.json. + # At Phase-4 exit the gated rules are A1/A6/A7/B2/D2/ANCHOR + MrDocs-no-warnings + + # C2/C4/C9/C10: the blocking gate step below runs the comparator with --strict + # --gate and IS blocking (continue-on-error: false) — it fails the job on any NEW + # A1/A6/A7/B2/D2/ANCHOR violation, any NEW MrDocs reference-surface warning, or any NEW + # C2/C4/C9/C10 wording violation on either surface. E4 (a11y contrast) is Review + # tier, not gated (DOC_STYLE_GUIDE.md Part F.0); the a11y scan below stays a + # non-blocking report. All other rules remain warning-only via the non-blocking + # "no-new-violations report" step (see DOC_IMPROVEMENT_PLAN.md). + # The accuracy gate for .adoc example code (B2/B3/D2 correctness) is separate and + # stays a hard gate: the boost_capy_doc_tests b2 target defined in + # test/doc/Jamfile, run via `./b2 libs/capy/test` in ci.yml, not this job + # (test/doc/CMakeLists.txt defines the equivalent CMake target). + + - name: Install Vale + if: always() + continue-on-error: true + run: | + mkdir -p "$RUNNER_TEMP/vale-bin" + curl -sSL https://github.com/errata-ai/vale/releases/download/v3.15.1/vale_3.15.1_Linux_64-bit.tar.gz \ + | tar -xz -C "$RUNNER_TEMP/vale-bin" vale + echo "$RUNNER_TEMP/vale-bin" >> "$GITHUB_PATH" + echo "$(pwd)/boost-root/libs/capy/doc/node_modules/.bin" >> "$GITHUB_PATH" + + - name: Doc-quality - Vale sync (Google style package) + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: vale sync + + - name: Doc-quality - Vale over .adoc pages + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: vale modules + + - name: Doc-quality - extract + Vale over header docstrings + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: | + node lint/extract-docstrings.mjs + vale lint/.docstrings + + - name: Doc-quality - structural lint (doc-lint.mjs) + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: node lint/doc-lint.mjs + + - name: Doc-quality - accessibility contrast scan (pa11y-ci) + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: node lint/run-a11y.mjs + + - name: Doc-quality - MrDocs no-warnings scan + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: node lint/mrdocs-warnings.mjs + + - name: Doc-quality - no-new-violations report (all rules, non-blocking) + if: always() + continue-on-error: true + working-directory: boost-root/libs/capy/doc + run: node lint/check-no-new-violations.mjs + + # BLOCKING Phase-4 gate: fails the job on any NEW A1/A6/A7/B2/D2/ANCHOR violation, any + # NEW MrDocs reference-surface warning, and — promoted at Phase-4 exit — any NEW + # C2/C4/C9/C10 wording violation on EITHER surface. A1/A6/B2/D2 are doc_lint + # fingerprints; A7 is the Vale rule Capy.PartHeadings; MrDocs-no-warnings gates + # the whole mrdocs_warnings check; C4/C9/C10 are the Vale rules + # Capy.SimpleTense / Capy.NoFluff / Capy.Terminology, gated over both the .adoc + # pages (vale_adoc) and the extracted header docstrings (vale_docstrings), so + # vale_docstrings is now a GATED check too. C2's authority is + # lint/sentence-length.mjs, not a Vale rule — Capy.SentenceLength is + # `level: suggestion` and enforces nothing (see + # .vale/styles/Capy/SentenceLength.yml) — so C2 gates that script's hard slice. + # E4 (a11y contrast) is Review tier, not gated — see DOC_STYLE_GUIDE.md Part F.0 + # (demoted like E2: the gated failures were color-contrast on shared Antora theme + # nav chrome, which Capy cannot fix). A skip of ANY gated check (doc_lint / + # vale_adoc / vale_docstrings / sentence_length / mrdocs_warnings) fails the gate + # (can't verify a gated rule = not a pass); a skip of the non-blocking a11y scan + # does not. The pre-existing backlog is grandfathered by baseline.json. + # + # THE TWO GATE-SPEC SHAPES DIFFER, AND THE DIFFERENCE IS LOAD-BEARING. + # check-no-new-violations.mjs tests each regex against the WHOLE fingerprint. + # * Vale fingerprints are `file:#N:Check.Name` — check name at the TAIL. So the + # Vale specs tail-anchor with `$` and MUST NOT carry a leading `^`. An + # `^`-anchored Vale spec matches nothing and reports `gated: true, + # gatedNew: 0` — a gate that says it is gating while checking nothing. That + # was measured twice on this branch; it is why the A7 spec is written + # `Capy\.PartHeadings$` and not `^Capy\.PartHeadings$`. + # * sentence_length fingerprints are `C2:file:#N:message` — rule at the HEAD. So + # `^C2:` is the correct shape THERE, and it deliberately cannot reach the + # `advisory-C2` design-essay slice (DOC_STYLE_GUIDE.md Part C2 makes the + # 25-word limit soft in essays; 67 findings under + # modules/ROOT/pages/9.design/ and .../A.specification-methods/ are an + # explicit carve-out, not a backlog). + # Fingerprint-shape contract: doc/lint/README.md. Never promote a rule here on the + # strength of a green run — plant a violation and watch this step fail first. + # + # !!! THIS STEP IS RED TODAY, ON PURPOSE, AND THE FIX IS A POST-MERGE RESEED. + # `sentence_length` has NO entry in the committed baseline.json (the check was + # added after that snapshot was taken), so nothing in its slice is grandfathered + # and `--gate 'sentence_length:^C2:'` exits 1 on the whole hard slice. That slice + # is exactly TWO findings, both in include/boost/capy/when_any.hpp + # (lint/.docstrings/when_any.hpp.adoc), a 27-word and a 31-word sentence of the + # form "If at least one child await-returned a zero `ec`, the result holds …, + # unless producing the winner's payload threw, in which case that exception is + # rethrown." They are ACCEPTED REFUSALS, not defects: a Phase-4 rewrite that split + # them made a false claim against the code and was reverted verbatim, and the + # maintainer's content review carries that text. Zero .adoc fingerprints remain + # under `^C2:`. + # The maintainer chose visible debt over new machinery: the in-source + # refusal-marker option was declined. Do NOT add a suppression mechanism and do + # NOT reseed baseline.json locally (a local run grandfathers ~357 local-vs-CI + # drift fingerprints). The fix is the `workflow_dispatch` reseed at the end of this + # job, run AFTER merge, per doc/lint/README.md. + # By contrast the C4/C9/C10 gates (both surfaces) are GREEN today with no reseed + # needed: their three residual .adoc findings sit inside two verbatim third-party + # quoted passages and are already grandfathered by baseline.json. + - name: Doc-quality - Phase-4 gate (A1/A6/A7/B2/D2/ANCHOR + MrDocs + C2/C4/C9/C10, blocking) + if: always() + continue-on-error: false + working-directory: boost-root/libs/capy/doc + run: | + # Expected state until the post-merge reseed: EXIT 1 with exactly two gated + # findings, both C2:lint/.docstrings/when_any.hpp.adoc (see the note above). + # Any OTHER gated finding is a real regression. + node lint/check-no-new-violations.mjs --strict \ + --gate 'doc_lint:^(A1|A6|B2|D2|ANCHOR):' \ + --gate 'vale_adoc:Capy\.PartHeadings$' \ + --gate 'mrdocs_warnings:.*' \ + --gate 'sentence_length:^C2:' \ + --gate 'vale_adoc:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$' \ + --gate 'vale_docstrings:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$' + + # --- Baseline reseed (workflow_dispatch only) --------------------------- + # doc/lint/baseline.json is the gate's reference point: anything in it is + # grandfathered. It goes stale as the backlog is worked down (a fix removes + # findings but not their baseline entries), and a stale-high baseline + # grandfathers findings that no longer exist — so they can be reintroduced + # and the gate stays green. Retiring them needs a regenerated baseline. + # + # Regenerating on a developer machine is NOT safe: a local run differs from + # a CI run by hundreds of fingerprints (measured: 297 — a different MrDocs + # 0.8.0 build hash, chromium vs google-chrome, file-processing order), and + # committing those differences would grandfather environment drift as if it + # were the real backlog. So the candidate is authored HERE, by the same job, + # on the same runner image, with the same PATH (Ruby asciidoctor for Vale's + # .adoc scope, doc/node_modules/.bin, the RUNNER_TEMP vale binary) and the + # same PA11Y_CHROME_PATH the blocking gate above just used. Reusing the gate's + # own job — rather than a second job that re-creates its setup — is + # deliberate: an imitated environment is exactly the bug this avoids, and it + # cannot drift from the gate's environment because it IS the gate's + # environment. + # + # Two safety properties of the ordering and paths below: + # * these steps run AFTER the blocking gate, and + # * the candidate is written to RUNNER_TEMP, never to the checked-out + # doc/lint/baseline.json, + # so the gate in this same run still compares against the COMMITTED + # baseline. A candidate that overwrote it first would make the gate compare + # a run against itself and pass unconditionally. + # + # The job never commits or pushes. It uploads a candidate for review; a + # human reads the diff and commits it. Maintainer procedure, including how + # to read the report and when NOT to reseed: doc/lint/README.md. + - name: Baseline reseed - regenerate a candidate in the CI environment + if: always() && github.event_name == 'workflow_dispatch' + working-directory: boost-root/libs/capy/doc + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/baseline-candidate" + node lint/baseline.mjs "$RUNNER_TEMP/baseline-candidate/baseline.json" + + # Reports per-check counts before/after and, per check and rule, which + # fingerprints the candidate would ADD (grandfather) and REMOVE (retire). + # Any ADDED fingerprint matching the gate spec is a finding a reseed would + # silently un-gate; those are named individually and fail this step. So do a + # SKIPPED check and a GATED check that collapsed to zero findings — both + # would wipe a merge-blocking check's whole grandfathered backlog. + # + # The gate spec is EXTRACTED from this workflow file rather than restated + # here. A second verbatim copy is a rot hazard with a silent failure mode: + # promote a rule in the blocking step above, forget this one, and the report + # keeps printing "none gated" for a rule that now blocks merges — the safety + # net stops covering exactly the rule that was just deemed important enough + # to gate. Extraction means there is one copy, in the blocking step, and this + # step cannot disagree with it. If extraction yields nothing (someone + # reformatted the blocking step's arguments), the step FAILS rather than + # reporting against an empty gate spec, which would look identical to "no + # gated additions." + - name: Baseline reseed - report what the candidate would change + if: always() && github.event_name == 'workflow_dispatch' + working-directory: boost-root/libs/capy/doc + run: | + set -uo pipefail + out="$RUNNER_TEMP/baseline-candidate" + workflow=../.github/workflows/docs.yml + + # Read ONLY the blocking step's run block: from its comparator invocation + # to the blank line that ends it, then stop. `awk ... {exit}` rather than a + # sed range because a sed range restarts at every match of its start + # pattern — including the copy of that pattern in this very extractor, + # which silently pulled the extractor's own quoting into the gate spec. + # Stopping at the first block also makes the source of truth unambiguous: + # the step that actually blocks merges. `grep -o` takes every occurrence + # per line, in case the arguments are ever reflowed onto one. + gate_args=() + while IFS= read -r spec; do + gate_args+=(--gate "$spec") + done < <( + awk '/check-no-new-violations\.mjs --strict/ { inblock = 1 } + inblock && /^[[:space:]]*$/ { exit } + inblock' "$workflow" \ + | grep -o -- "--gate '[^']*'" \ + | sed "s/^--gate '//; s/'\$//" \ + | sort -u + ) + if [ "${#gate_args[@]}" -eq 0 ]; then + echo "::error title=Gate spec not found::could not extract any --gate spec from $workflow; refusing to report against an empty gate spec" + exit 1 + fi + echo "gate spec extracted from $workflow: ${gate_args[*]}" + + status=0 + node lint/baseline-diff.mjs lint/baseline.json "$out/baseline.json" \ + "${gate_args[@]}" | tee "$out/baseline-diff.txt" || status=$? + # Full text diff of the file itself: the only place a single changed + # fingerprint is visible verbatim. `diff` exits 1 when files differ. + diff -u lint/baseline.json "$out/baseline.json" > "$out/baseline.json.diff" || true + # GitHub rejects a step summary over 1 MiB. A real report is ~14 KB, but a + # pathological candidate must not turn a reporting step into an infra + # failure, so cap it and point at the artifact for the full text. + { + echo '## Candidate doc/lint/baseline.json' + echo + echo 'Download the `doc-lint-baseline-candidate` artifact. Do not commit it' + echo 'without accounting for every ADDED fingerprint below. Full untruncated' + echo 'report: `baseline-diff.txt` in that artifact.' + echo + echo '```' + head -c 900000 "$out/baseline-diff.txt" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + exit "$status" + + - name: Baseline reseed - upload the candidate for review + if: always() && github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: doc-lint-baseline-candidate + path: ${{ runner.temp }}/baseline-candidate + if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index cc6dfa9b4..a36c3404c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,7 @@ endfunction() if (BOOST_CAPY_MRDOCS_BUILD) file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/mrdocs.cpp" - "#include \n") + "#include \n#include \n") add_library(boost_capy_mrdocs "${CMAKE_CURRENT_BINARY_DIR}/mrdocs.cpp") boost_capy_setup_properties(boost_capy_mrdocs) target_compile_definitions(boost_capy_mrdocs PUBLIC BOOST_CAPY_MRDOCS) diff --git a/DOC_AUDIT_PHASE4_EXIT.md b/DOC_AUDIT_PHASE4_EXIT.md new file mode 100644 index 000000000..4d8f29066 --- /dev/null +++ b/DOC_AUDIT_PHASE4_EXIT.md @@ -0,0 +1,402 @@ +# Capy Documentation Audit — Phase-4 Exit + +**Scope:** all 65 `.adoc` pages under `doc/modules/ROOT/pages`, one sub-agent per page, each +running the `doc-prompts/doc-audit.md` pipeline (Classify → Score → self-Challenge) against +`DOC_STYLE_GUIDE.md`. The reference (docstring) surface was **not** audited — this is the +exposition surface only. + +**Headline:** **430 findings survived self-challenge across 65 pages; ~345 more were dropped. +Zero pages scored clean on all five axes.** 34 pages carry a `major` Accuracy grade and 13 +carry a `major` Structure grade. The wording axis — the one Phase 4 just closed — is the +*healthiest* axis in the corpus: 1 `major` (why-capy.adoc) and no page where wording is the +top defect. Phase 4 did its job. What it exposed is that Phases 1 and 2 did not finish. + +--- + +## The one-line conclusion + +**Accuracy, not wording, is the corpus's dominant defect class, and the gates that were +promoted to protect it do not reach the places the defects live.** Nine of the twelve +systemic patterns below are gate-coverage holes, not writing problems. Corosio (Phase 5) +should not start until the gate holes are closed, because Phase 5 will replicate them. + +--- + +## Part 1 — Systemic patterns + +Ranked by blast radius. Each was verified in the repo, not taken from a sub-agent's word. + +### S1. `Capy.PartHeadings` (rule A7) has never matched anything — and A7 is a promoted gate + +`doc/.vale/styles/Capy/PartHeadings.yml` is `raw: '^Part\s+\d+\b'`. Every "Part N" title in +the corpus is a **Roman numeral**: + +``` +2a "Part I: Foundations" 2b "Part II: {cpp}20 Syntax" 2c "Part III: …" 2d "Part IV: …" +3a "Part I: Foundations" 3b "Part II: Synchronization" 3c "Part III: …" 3d "Part IV: …" +``` + +`\d+` cannot match `I`/`II`/`III`/`IV`. **`doc/lint/baseline.json` contains 0 +`Capy.PartHeadings` fingerprints** against a corpus with 8 A7 violations — the CI-authored +baseline is the proof, and it is exactly the shape F4 documents for this same file +(`b54fe6c8` fixed the *scope*; the *pattern* was never fixed). + +This is a **gate**, promoted at Phase-1 exit. It has been reporting clean since the day it +was written. Six independent sub-agents found it without being told to look. + +Fix: `raw: '^Part\s+([0-9]+|[IVXLC]+)\b'`, then bite-test both forms per F4. + +> **Bite-tested, per F4.** Planting `= Part 3:` / `== Part 5:` produced 3 `Capy.PartHeadings` +> alerts; planting `= Part III:` / `== Part IV:` produced **0**. A control page with +> `Note that … simply … utilize … spawn` fired `Capy.NoFluff` ×3 and `Capy.Terminology` ×1, +> proving the Capy styles were loaded and running. The rule works for Arabic numerals and is +> blind to Roman; the corpus is 100% Roman. +> +> **Tooling note that cost this audit real time.** `vale` *does* run here, but only with +> `cd doc && export PATH="$PWD/node_modules/.bin:$PATH"` — `asciidoctor` is `asciidoctor.js` +> from node, not Ruby, and Vale shells out to it for every `.adoc` (including the extracted +> docstring corpus). Without that PATH prepend, Vale exits 2 with `asciidoctor not found` +> and **prints nothing**, which greps identically to "clean". Several audit sub-agents hit +> this and fell back to reading rule YAML instead of running the check — the precise failure +> F4 exists to prevent. This invocation belongs in `DOC_STYLE_GUIDE.md` step 3, which today +> just says "run `vale` locally". + +### S2. `doc-lint.mjs`'s B2 check only sees `[source,cpp]` blocks + +`doc/lint/doc-lint.mjs:63` matches `/^\[source\s*,\s*(cpp|c\+\+)\b[^\]]*\]/i`. Anything else +is invisible to the B2 gate. Measured escapes: + +| Escape route | Count | Example | +|---|---|---| +| Bare `----` listing holding real C++ | ≥2 confirmed | `9k.Executor.adoc:281`, `9l.RunApi.adoc:260` | +| `[source,c]` | 2 | `5d.system-io.adoc` (`iovec`, `WSABUF`) | +| `[source,cmake]` | 12 | every example page's Build block | +| `[source,bash]` | 1 | `quick-start.adoc` | + +B2 is a **promoted gate**. It is enforcing on one language token. + +### S3. The hand-pasted CMake Build block is wrong on 7 pages — and B2 cannot see it + +12 of 15 `8.examples/` pages plus `quick-start.adoc` hand-paste a build recipe. The link +target has drifted into two camps: + +- **Broken (7):** `quick-start` (`-lcapy`), `8a`, `8b`, `8c`, `8d`, `8f`, `8g` + (`target_link_libraries(... PRIVATE capy)`) +- **Correct (5):** `8k`, `8l`, `8m`, `8n`, `8q` (`PRIVATE Boost::capy`) + +`CMakeLists.txt:125-126` defines `boost_capy` with `add_library(Boost::capy ALIAS boost_capy)`. +**No target or library named `capy` exists.** The very first command a quick-start reader runs +does not link. Every `example/*/` directory already ships a real `CMakeLists.txt` that could +be `include::`d. + +### S4. A phantom `Source`/`Sink` concept tier is documented as shipped + +`include/boost/capy/concept/` contains **three** stream concepts: `ReadStream`, `WriteStream`, +`Stream`. `grep -rn 'ReadSource\|WriteSink\|BufferSource\|BufferSink' include/` returns **0**. +Five pages document them as present fact: + +| Page | Claim | +|---|---| +| `6.intro.adoc` | "six concepts, arranged in three complementary pairs"; "_sources_ and _sinks_" | +| `6b.streams.adoc` | prerequisite: "the six stream concept categories" | +| `index.adoc` | "the seven stream concepts" (and "three" 15 lines earlier) | +| `9m.WhyNotCobalt.adoc` | "seven coroutine-only stream concepts"; 4 phantom wrapper + 4 phantom mock table rows | +| `9c` / `9f` | full `ReadSource`/`WriteSink` hierarchy diagrams; 9f links a "WriteSink design document" that does not exist | +| `6f.isolation.adoc` | lists `any_buffer_source`, `any_buffer_sink` as living in `` | + +`include/boost/capy/io/` holds exactly `any_read_stream.hpp`, `any_stream.hpp`, +`any_write_stream.hpp`, `write_now.hpp`. Related: `9i.TypeEraseAwaitable.adoc` describes +`any_read_source`/`any_buffer_source` that *do* exist — **in Boost.Http** — without saying so, +so a Capy reader searches Capy's reference for them. + +This is one decision, not six edits: either the tier ships, or every page marks it planned. + +### S5. Claimed program output is compile-gated, never run-gated + +19 pages carry 21 `*Output:*` / `== Output` blocks, all hand-typed. `test/doc/WriteProgramTest.cmake` +emits a bare `add_test("${TEST_NAME}" "${TEST_EXECUTABLE}")`; `grep -rn +'PASS_REGULAR_EXPRESSION\|expected_output' test/doc/` returns **nothing**. A doc program passes +on exit status alone. Sub-agents hand-verified several blocks as correct today and found two +that are not reproducible as printed: + +- `8g.parallel-fetch.adoc` — three top-level `run_async` tasks on a default `thread_pool` + with unsynchronized `std::cout`; the shown interleaving is one of many. +- `3b.synchronization.adoc` — "180,000, 195,327, maybe occasionally 200,000" from a program + in `COMPILE_ONLY_PROGRAMS`, which never runs at all. + +The single-source pipeline stops at the code block and does not cover the output beneath it. + +### S6. Asciidoctor concatenates same-named tag regions; the compile gate cannot see the result + +Asciidoctor merges every region sharing a tag name into one rendered block. The snippet file +compiles per-region; the merged block on the page can be garbage. Confirmed against +**rendered HTML** by the `3c.advanced.adoc` agent: + +- `tag=wait_variants` — two regions from different scopes concatenate into a block that + declares `auto status` twice and would not compile as shown. +- `tag=shared_mutex` — the tag opens above the pragma preamble, so the rendered block is + ~30 lines of `#pragma GCC diagnostic ignored` before any shared-mutex code. + +Same leaked-preamble defect at `4a.tasks.adoc` (`tag=include_task` renders ~28 lines of +pragmas under the caption "The `task` type is defined in:"). + +Cheap gate: for each `tag=X` a page references, extract the *concatenated* region and compile +it standalone; or warn when a tag name has more than one region in a file. + +### S7. `role=external` is being used as a general compile-gate escape hatch + +14 uses: 13 on `9.design/WhyNot*` pages (legitimate — other-library comparison), and: + +- `7a.drivers.adoc` — a block of **Capy's own** `fuse::armed(run_one, fn)` API. +- `9m.WhyNotCobalt.adoc` — a 75-line serializer written in **Capy's own** `task<>` and + `capy::test::run_blocking`; `serialize_capy_task` appears nowhere else in the repo. + +B3 reserves the role for other-library code. Used this way it turns off B2 for first-party +code that could be compiled. + +### S8. A "compiled sketch" pattern satisfies B2 while defeating A2/B1 + +`test/doc/snippets/` contains deliberate sketch namespaces — `executor_concept_sketch`, +`api_sketch`, `concept_def`, `concept_layer`, `composed`, `synopsis`, `definition` — that +re-declare a real library entity so a hand-typed signature compiles. The compile gate is +satisfied; the declaration is a *copy* and drifts freely. Confirmed drift: + +- `4c.executors.adoc` — the reproduced `thread_pool` constructor lost `explicit`. +- `5b.types.adoc` — the `const_buffer` sketch lost `constexpr` on four members and both copy + operations. +- `7e.buffer-inspection.adoc` — table row says `operator bool() const`; real is + `explicit … noexcept`. +- `5e.algorithms.adoc` — `namespace synopsis` declares `buffer_size`/`buffer_empty` as plain + function templates; both are `constexpr noexcept` anonymous-struct function objects. The + page's own prose says `buffer_copy` *is* a function object, then shows a free function. +- `9a.CapyLayering.adoc` — `namespace concept_layer` re-declares `capy::write`. + +Where a `static_assert` does guard the sketch (`9n`, `9f`), it only asserts that 2–3 known +types satisfy both — it passes when a requirement is added that those types happen to meet. + +**Any page including a `*_sketch` / `synopsis` / `concept_def` tag is an A2 candidate.** + +### S9. Exposition reproduces the reference contract verbatim — 13 pages, `major` St + +The highest-value structural rule (A2) is the most violated. The worst cases copy a docstring +near sentence-for-sentence: + +| Page | What is reproduced | +|---|---| +| `9c.ReadStream` / `9f.WriteStream` | `Semantics` → `Conforming Signatures` copied from `read_stream.hpp`/`write_stream.hpp`. 9f's copy **has already drifted** (narrows the by-value buffer rule to coroutines only; the header requires it unconditionally). 9c states an after-error precondition the docstring does not contain — the two surfaces now disagree. | +| `6b.streams` | the same contract a **third** time | +| `7a` / `7b` / `7e` | hand-typed member tables: 14 rows for `fuse`, 6 for `run_blocking`, 3 tables in 7b, 2 in 7e | +| `4c.executors` | `Executor` concept body + `thread_pool` ctor | +| `5b.types` | full `const_buffer` / `mutable_buffer` class declarations | +| `5e.algorithms` | 4 API synopses | +| `9k.Executor` | concept body + a duplicate `Conforming Signatures` section | + +`9.intro.adoc` **sanctions** it: it promises each design page carries the concept's "formal +definition". Fixing the pages without fixing the intro leaves the section describing itself +incorrectly. + +Also: 16 pages carry a hand-maintained `== Reference` header table with no `xref` or `cpp:` +link. Individually below the A2 bar; collectively a 16-page ungated drift surface. + +### S10. `cpp:` adoption is half-applied *within* pages + +Phase-4 measurement: 51/65 pages use `cpp:`, 555 uses. But **174 bare-backticked mentions of +linkable Capy symbols remain on pages that already link elsewhere** — often in the same +sentence (`"…`run_async` for entry and cpp:run[] for hopping"`), and disproportionately in +**summary and comparison tables**, which three agents independently called out as the blind +spot. Worst: `4f.composition` (19), `9f.WriteStream` (18), `5b.types` (18), `9c` (11). + +This is mechanically greppable: *a symbol linked with `cpp:` anywhere on a page but bare +elsewhere on that same page.* It is the cheapest large win in the report. + +### S11. Section-intro roadmaps drift from the actual page inventory + +Every `*.intro.adoc` hand-summarises its nav children in closing prose. Four have drifted: + +- `5.intro` promises "dynamic buffer abstractions" — no such page, no such entity. +- `6.intro` promises "transfer algorithms" — no such page; plus the S4 phantom taxonomy. +- `4.intro` says topics end at allocators — `4h.lambda-captures` follows. +- `8.intro` promises "fully featured servers, covering real-world integration with Corosio" — + no such page exists in the section. + +`7.intro` also mis-states `fuse`'s run loop (one extra run vs. a second full sweep), while +`7a` states it correctly. Related: section landing pages have **two incompatible conventions** +— `7.intro` xrefs each child with a gloss; `5.intro`, `6.intro`, `9.intro`, `A.intro` link +nothing. `A.intro` is 21 words. No rule settles which wins. + +### S12. Repeated cross-page claims that are wrong in every copy + +Fix once, fix everywhere: + +| Claim | Where | Reality | +|---|---|---| +| "one virtual call per I/O operation" | `why-capy`, `9a`, `9m`, `9o` | `any_read_stream::read_some` dispatches through 5 vtable pointers | +| Corosio has 3 or 4 backends | `9b` ("four"), `9k` (twice, "three"), `9o` ("io_uring planned") | 5: epoll, kqueue, io_uring, IOCP, select. `9o` also names only WolfSSL; OpenSSL ships too | +| "coroutine frame is heap-allocated" (unconditional) | `2a`, `2b`, `2c`, `2d`, `4h` | contradicted by these pages' own HALO sections | +| `epoll_context`/`iocp_context`/`select_context` types | `9k` | no such types; it is `corosio::io_context` + a backend tag | +| Buffer layout matches `WSABUF` | `5a`, `5b` ("often just a reinterpret_cast") | `WSABUF` orders members oppositely with a 32-bit length; `5d` says Capy *copies* | +| "GCC 10+, Clang 14+, MSVC 2019 16.8+" | `2a` | README says GCC 12+/Clang 17+/MSVC 14.34+; CI's oldest is GCC 13 | + +Also single-site but flatly wrong and worth listing: `9a` names a `right_now` pattern — the +type is `write_now`; `9c` calls `read(stream, buffer(buf,100))` — the factory is `make_buffer`; +`5d` says Corosio "exposes" registered-buffer optimizations — it exposes none; `3a` says +`std::thread` without `std::ref` "modifies a copy" — it is a hard compile error. + +--- + +## Part 2 — Cross-cutting question the audit cannot decide + +**Does the chapter page template make every concept page mode-mixed?** 12 pages classified +`mixed` with `mode_mismatch: true`. In nearly every case the sub-agent traced it to the shared +skeleton, not to a local edit: `== Prerequisites` (39 pages) + body + `== Reference` header +table (16 pages) + a "You have now learned … Continue to …" closer. Tutorial scaffolding wraps +bodies that are reference specification or design explanation. + +Five agents independently declined to file it per-page and asked for a corpus-level ruling. +Either bless the skeleton in A1/A3, or split the pages. Do not let 12 pages carry a mismatch +no one intends to act on. + +**Secondary:** `3.concurrency/` (5 pages) uses **zero** Capy types — it is a deliberate +standard-C++ primer per `3.intro`, but D2 reads "the library's *own* type — not only of the +standard-library types it resembles", and Capy ships `async_mutex`/`async_event` as the +coroutine analogues of exactly what 3b/3c teach. Ratify the carve-out or reverse it. + +--- + +## Part 3 — Undefined terms (C7), corpus-wide + +Used unglossed, defined nowhere, no glossary page exists: + +`await-return` (3 pages: `6b`, `9i`, `9n`) · `SBO` (3: `9k`, `9m`, `9n`) · `launcher` (7: +`why-capy`, `4d`, `4e`, `8a`, `9k`, `9l`, `9n` — self-consistent, so this is a C.1 table +decision, and Vale's `\b(launch|spawn|…)\b` cannot match the noun) · `trampoline` (`4d`, first +use precedes its only definition in `9l`) · `contingency` (`Ab` uses it; `Ac` defines it, and +nav puts `Ac` **after** `Ab`) · `reactor` (`4e`, `9k`, `9o`) · `proactor` (`9k`, one corpus +use) · `TLS` unexpanded ~15× before first expansion (`4g`) · `IIFE` in a heading (`4h`) · +`HFT` (`9a`) · `Capy-coroutine` (`Ab`, one corpus use) · `lock-free` (`3c`, `8k` — and `8k`'s +usage is wrong: `strand` serializes with a pooled mutex). + +Related reference-surface gap: `Ab.cancellation.adoc` is the sole definition of "_supports +IoAwaitable cancellation_", italicised as a defined term in **12 docstrings**, none of which +can link to it (per `ef789cea`, MrDocs escapes docstring punctuation). Nothing in the corpus +xrefs `Ab.cancellation.adoc` except `nav.adoc`. + +--- + +## Part 4 — Per-page grades + +`St Ac Wo Co Pr` · `M`=major `m`=minor `.`=clean · `⚠`=mode mismatch · Rank = 3×major+minor + +| Page | St | Ac | Wo | Co | Pr | ⚠ | Rank | +|---|:-:|:-:|:-:|:-:|:-:|:-:|--:| +| 4.coroutines/4e.cancellation | M | M | m | m | m | | 9 | +| 4.coroutines/4c.executors | M | M | m | m | m | ⚠ | 9 | +| 5.buffers/5e.algorithms | M | M | m | m | m | ⚠ | 9 | +| 6.streams/6b.streams | M | M | m | m | m | ⚠ | 9 | +| 7.testing/7b.mock-streams | M | M | m | m | m | ⚠ | 9 | +| 9.design/9c.ReadStream | M | M | m | m | m | ⚠ | 9 | +| 9.design/9f.WriteStream | M | M | m | m | m | | 9 | +| 9.design/9m.WhyNotCobalt | M | M | m | m | m | | 9 | +| 5.buffers/5b.types | M | M | m | m | . | ⚠ | 8 | +| 9.design/9k.Executor | M | M | m | . | m | | 8 | +| why-capy | m | M | M | m | . | | 8 | +| index | m | M | m | m | m | | 7 | +| 7.testing/7a.drivers | M | m | m | m | m | ⚠ | 7 | +| 7.testing/7e.buffer-inspection | M | m | m | m | m | ⚠ | 7 | +| 9.design/9l.RunApi | M | m | m | . | m | | 7 | +| 5.buffers/5d.system-io | m | M | m | m | m | | 7 | +| 9.design/9a.CapyLayering | m | M | m | m | m | | 7 | +| 9.design/9o.WhyNotTMC | m | M | m | m | m | | 7 | +| 2.cpp20-coroutines/2d.advanced | m | M | m | . | m | | 6 | +| 3.concurrency/3c.advanced | m | M | m | m | . | | 6 | +| 4.coroutines/4a.tasks | m | M | m | m | m | | 6 | +| 5.buffers/5a.overview | . | M | m | m | m | | 6 | +| 6.streams/6f.isolation | m | M | m | m | . | | 6 | +| 8.examples/8c.buffer-composition | m | M | m | m | m | | 6 | +| 8.examples/8f.timeout-cancellation | m | M | m | . | m | | 6 | +| 9.design/9b.Separation | . | M | m | m | m | | 6 | +| 9.design/9n.WhyNotCobaltConcepts | m | M | m | . | . | | 5 | +| 2.cpp20-coroutines/2a.foundations | m | M | . | . | . | | 4 | +| 3.concurrency/3a.foundations | m | M | m | . | . | | 5 | +| 6.streams/6.intro | . | M | m | . | . | | 4 | +| 8.examples/8d.mock-stream-testing | m | M | . | . | m | | 5 | +| 8.examples/8b.producer-consumer | m | M | . | m | . | | 5 | +| 8.examples/8g.parallel-fetch | . | M | m | m | m | | 6 | +| 8.examples/8a.hello-task | . | M | . | m | m | | 5 | +| 8.examples/8l.async-mutex | . | M | . | m | m | | 5 | +| 9.design/9i.TypeEraseAwaitable | . | M | . | m | m | | 5 | +| 7.testing/7.intro | . | M | m | . | . | | 4 | +| quick-start | . | M | m | m | m | | 6 | +| 4.coroutines/4f.composition | m | m | m | m | m | | 5 | +| 4.coroutines/4g.allocators | m | m | m | . | m | ⚠ | 4 | +| 4.coroutines/4d.io-awaitable | m | . | m | . | m | ⚠ | 3 | +| 6.streams/6a.overview | m | m | m | . | m | | 4 | +| 5.buffers/5c.sequences | m | m | . | m | m | | 4 | +| 3.concurrency/3b.synchronization | m | m | m | m | . | | 4 | +| 2.cpp20-coroutines/2b.syntax | m | m | m | m | . | | 4 | +| 2.cpp20-coroutines/2c.machinery | m | m | m | . | . | | 3 | +| 4.coroutines/4b.launching | . | . | m | m | m | | 3 | +| 4.coroutines/4h.lambda-captures | . | m | m | m | . | | 3 | +| 5.buffers/5.intro | m | m | m | . | . | | 3 | +| 8.examples/8e.type-erased-echo | m | m | . | m | m | | 4 | +| 8.examples/8n.custom-executor | m | m | . | m | m | | 4 | +| 8.examples/8k.strand-serialization | . | . | m | m | m | | 3 | +| 8.examples/8m.parallel-tasks | . | m | . | . | . | | 1 | +| 8.examples/8o.sender-bridge | . | m | . | m | m | | 3 | +| A.spec-methods/Ac.contingencies | m | . | m | m | m | | 4 | +| A.spec-methods/Ab.cancellation | m | . | m | m | . | | 3 | +| 3.concurrency/3d.patterns | m | . | m | . | . | | 2 | +| 4.coroutines/4.intro | . | m | m | . | . | | 2 | +| 3.concurrency/3.intro | . | . | m | m | . | | 2 | +| 2.cpp20-coroutines/2.intro | . | . | m | . | . | | 1 | +| 9.design/9.intro | m | . | m | . | . | | 2 | +| 8.examples/8q.gui-integration | . | . | m | . | m | | 2 | +| 8.examples/8p.asio-use-capy | . | . | . | m | . | | 1 | +| 8.examples/8.intro | . | m | . | . | . | | 1 | +| A.spec-methods/A.intro | . | . | . | m | . | | 1 | + +**No page scored clean on all five axes.** Closest: `8p.asio-use-capy` and `A.intro` +(one minor each), then `8.intro` and `8m.parallel-tasks`. + +--- + +## Part 5 — Recommended order before Phase 5 + +Structure-and-accuracy first, exactly as the plan's architecture says — Phase 4 finished the +wrong-order-proof: wording is clean and it did not help. + +1. **Close the gate holes** (S1, S2, S6, S7) — every hour spent fixing pages before this is + revertible without failing CI. Bite-test each per F4. **Install asciidoctor first**, or + the bite-test discipline cannot be executed. +2. **Reseed the baseline** — the owed Phase-4 action. ~3,700 dead grandfather clauses mean + every Phase-3/4 fix is currently revertible. Do this before adding new findings to the + pile. +3. **Decide S4** (phantom Source/Sink tier) — one maintainer ruling unblocks 6 pages, and it + is the corpus's single largest accuracy defect. +4. **Sweep S3** (7 broken build recipes) — smallest effort, highest reader impact; the + quick-start's build line does not work. +5. **Sweep S10** (`cpp:` half-adoption, 174 mechanical fixes) — greppable, low risk. +6. **Rule the mode-mixing question** (Part 2) before touching the 12 `mixed` pages. +7. **A2 sweep** (S9) — 13 pages, but do `9.intro` first so the section stops sanctioning it. +8. **Then** Phase 5. Corosio's playbook inherits every gate hole above; fixing them here is + the only thing that keeps Phase 5 from re-earning them. + +--- + +## Method notes and limits + +- One sub-agent per page; each read the style guide, the audit prompt, and its page, scored + five axes, then re-read to refute its own findings. ~345 findings were dropped on + self-challenge — a 45% drop rate, which is the noise floor working. +- Agents verified accuracy against `include/boost/capy/**`, `test/doc/`, and `example/`; + several compiled or ran binaries. Unverifiable claims were recorded as notes, not findings. +- **The reference (docstring) surface was not audited.** `doc-audit`'s reference mode exists + and was not run. Given that S9 found the two surfaces already disagreeing on the + `ReadStream` contract, a reference-mode pass is warranted before Phase 5. +- **`doc-prompts/doc-audit.md` needs two corrections.** Its calibration parentheticals are + pre-Phase-1 measurements: it says "zero of 65 pages declare `:page-mode:`" (now 1 — + `8q.gui-integration`) and "zero of 65 pages use the `cpp:` macro" (now 51 pages, 555 uses). + I overrode both in the sub-agent briefing; the file itself is still stale. +- The style guide has **no rule id for a plain factual error**. Agents cited `B4` (written for + reference briefs) or `D5` as the nearest fit, and two flagged the stretch explicitly. An + accuracy rule would make the corpus's dominant defect class citable without straining. diff --git a/DOC_AUDIT_REFERENCE.md b/DOC_AUDIT_REFERENCE.md new file mode 100644 index 000000000..b16e8ee46 --- /dev/null +++ b/DOC_AUDIT_REFERENCE.md @@ -0,0 +1,271 @@ +# Capy Reference-Surface Audit — Docstrings + +**Scope:** the **reference** surface — Doxygen docstrings in the 64 public headers under +`include/boost/capy/**` (excluding `detail/` and `impl/`). 28 sub-agents, ~450 documented +public declarations, running `doc-prompts/doc-audit.md`'s **reference mode** (Diátaxis mode +fixed to `reference`; the five axes remapped to the docstring contract). + +Companion to [DOC_AUDIT_PHASE4_EXIT.md](DOC_AUDIT_PHASE4_EXIT.md), which covers the +exposition surface. **Read R1 first — it invalidates a chunk of this tool's own contract.** + +**Headline:** the reference surface is in **better** shape than the pages. Prose quality is +genuinely good — the `Wo` axis is clean on 20 of 28 assignments and produced no `major` +anywhere. But **`Ac` is `major` on 14 of 28 assignments**, and the concentration is stark: +**the single largest defect class is hand-written `@par Example` code, which no gate compiles +and which has rotted in at least 12 places.** + +--- + +## R1. The audit contract's own St rule is wrong — findings discarded + +`doc-audit.md`'s reference-mode remap requires the section order *brief → description → +`@param` → `@return` → `@par` → `@throws` → `@note` → `@see`*. Six agents dutifully filed St +findings against it before I measured the premise. Both halves of the rule fail: + +- **It is not the house convention.** Across every non-detail public header: **65 doc blocks + put `@par` before the first `@param`/`@tparam`/`@return`; 26 put it after.** The + "violating" form wins 71% to 29%, spanning `task`, `quitter`, `read`, `write`, `when_all`, + `when_any`, `async_mutex`, `strand`, `execution_context`. Enforcing the rule would flag the + library, not fix it. +- **It has zero reader impact.** **MrDocs 0.8.0 normalizes section order in the rendered + output.** Verified: `task::await_resume` writes `@return` before `@par Exception Safety` + and renders *Description → Exception Safety → Return Value*. + +**I discarded every section-ordering finding and re-briefed the remaining 22 agents to skip +the rule.** The correction is a fix owed to `doc-prompts/doc-audit.md`, not to any header. +A real St finding still exists where a paragraph is *stranded* inside or after a `@par` block +(`async_event`/`async_waker` both do this) — that changes what renders where. + +This is the audit tool doing exactly what it warns about: a rule stated confidently, never +checked against the corpus it governs. + +--- + +## R2. Docstring `@par Example` code is ungated, and ~12 of 103 blocks are broken + +**103 `@code` blocks across 50 public headers.** Nothing compiles them: + +- The snippet-compile job (`F2`, the accuracy gate) covers only `include::example$` on + `.adoc` pages. +- `doc/lint/extract-docstrings.mjs:105` **explicitly strips them** before Vale sees the + prose — `Drop @code ... @endcode samples entirely — not prose.` +- Phase-0 Task 5 studied exactly this and **recommended DEFER** (`doc/lint/RESEARCH-docstring-examples.md:196`). + +That deferral was reasonable without evidence. **This audit is the evidence.** Confirmed +broken, several by actually compiling them: + +| Symbol | Defect | How confirmed | +|---|---|---| +| `run_async_wrapper` | `@warning` says `auto w = run_async(ex);` "does not compile" — it **does**; C++17 guaranteed copy elision never considers the deleted ctors | compiled | +| `test::run_blocking_wrapper` | copy of the same claim: "can only be used as a temporary" — `auto w = …; std::move(w)(t());` compiles and runs | compiled, with negative control | +| `work_guard` | `make_work_guard(ctx)` passes an execution **context** to a function constrained on `Executor` | read + constraint check | +| `buffer_param` | Virtual Interface Pattern: CTAD yields `span`, needs `span` | compiled (`g++ -std=c++20`) | +| `test::buffer_to_string` | calls `.data()` on bufgrind halves; returns `void const*`, satisfies no sequence concept | read + tests | +| `ExecutionContext` | `ex.post([]{})` — every capy `post` takes `continuation&`, which no closure converts to | read | +| `async_mutex` | defines `task<> protected_operation()` **twice** in one TU | read | +| `cond` | `if(…)` with a comment-only body followed by `else` — syntax error | read | +| `io_task` | uses `route_params` / `route::next`, which exist in **no** repo (capy, corosio, burl) | grep ×3 trees | +| `test::stream`, `test::write_stream` | object constructed **outside** `f.armed`, so state carries across the ~2N rounds and the trailing `// buf contains "hello"` comment is unreachable / wrong | read + unit tests | +| `Stream` concept | "echo" example discards `write_some`'s result, silently dropping bytes under the partial-write contract it inherits | read | +| `any_read_stream` / `any_write_stream` | redeclare `stream` in one scope; use undeclared `ioc`, `data`, `size` | read | + +Several of these sit **next to a compiled snippet that has the correct form** — the +`buffer_to_string` example is wrong while `test/doc/snippets/7e_buffer_inspection.cpp:81` +does it right. That is the prime directive's drift, on the one surface the pipeline does not +cover. **Recommend reopening Task 5.** + +--- + +## R3. `@see executor` names a symbol that does not exist — 19 times + +The concept is `boost::capy::Executor` (`concept/executor.hpp:165`). There is no +`boost::capy::executor`. Yet: + +- `ex/run_async.hpp` — **18** occurrences (one per overload) +- `ex/run.hpp` — **1** + +(`ex/this_coro.hpp`'s single `@see executor` is **legitimate** — `this_coro::executor` is a +real awaitable tag object.) + +**This is worse than a dead link:** because `this_coro::executor` exists, a fuzzy resolver can +silently point all 19 at the tag object instead of the concept. `continuation.hpp` gets it +right (`@see Executor, executor_ref`), so the correct form is already in the tree. One `sed`. + +Related and unresolved: **MrDocs 0.8.0 appears to render every `@see` as plain unlinked +text** (observed on `task`, `io_env`, `quitter`, `when_any`, `read`). If so, `warn-broken-ref` +never sees `@see` targets at all — which would explain how 19 bad ones survived a gate that +is supposed to be blocking. **Worth a bite-test before trusting `warn-broken-ref`.** + +--- + +## R4. Where the two surfaces disagree, the winner is not always the same + +The exposition audit flagged five page↔docstring conflicts. Adjudicated against code: + +| Conflict | Correct surface | Fix goes in | +|---|---|---| +| `stream::provide` direction (7b table says "this stream", code appends to peer) | **docstring** | `7b.mock-streams.adoc:224` (the page even self-contradicts at line 172) | +| `fuse` run loop (7.intro says "one extra run", code runs two full sweeps) | **docstring** | `7.intro.adoc:47` | +| `strand::dispatch` inline condition (4c says "if the strand is idle") | **docstring** | `4c.executors.adoc:122` | +| by-value buffer rule narrowed to coroutines | **docstring** | `9c.ReadStream.adoc:56` **and** `9f.WriteStream.adoc:52` — the exposition audit found only 9f | +| `ReadStream` after-error precondition (9c says UB) | **neither** — see below | both | +| `ExecutionContext` "executes function objects" / "destroys unexecuted work" | **the page** (`9k.Executor.adoc`) | `concept/execution_context.hpp` | + +Two of these are worth dwelling on. + +**The after-error rule.** 9c.ReadStream.adoc asserts "Once `read_some` returns an error the +caller must not call `read_some` again … the behavior after an error is undefined." That is +**refuted by the library's own conforming stream**: `test::read_stream::read_some` returns +`{error::eof, 0}` on *every* subsequent call (fully defined), and under a `fuse` it returns an +injected error while leaving `pos_` untouched, so the next call **resumes delivering data**. +9c also contradicts itself — line 149 endorses zero-length probes whose whole purpose is to +return an `ec`. But the docstring is not right either: it makes "a subsequent read" +load-bearing and never says what such a call may do. **Fix: state the permissive rule in the +concept header; delete 9c's UB text. Do not sync the copies.** + +**`ExecutionContext` is Asio residue.** `concept/execution_context.hpp` still says a context +provides what is "needed to execute function objects" and that destroying it "destroys all +unexecuted work". Capy executors take `continuation&`, and `thread_pool::stop` documents the +real behavior as *abandons*. Here the **design page is correct and the header is stale** — +the reverse of every other row. Any "pages defer to headers" sweep would propagate the error. + +--- + +## R5. Systemic patterns + +- **`@par Thread Safety` is applied per-overload, not per-symbol — and MrDocs emits one page + per overload.** `run_async` documents it on 4 of 18 overloads; `when_any` on 0 of 3; + `when_all` on 1 of 3; `thread_pool::stop` omits it while documenting cross-thread use. Same + for preconditions: `run_async`'s memory-resource lifetime rule appears on 1 of 6 `mr` + overloads. **A reader landing on overload 12 gets a page with no safety contract at all.** +- **The two-call `run_async` warning reaches one page of nineteen.** MrDocs renders 19 + separate `run_async-*.html`; the full `@warning` lives only on `run_async_wrapper`, and + **no overload carries `@see run_async_wrapper`**. `run_async-04.html` contains zero + occurrences of "two-call". This is Phase-2 finding #3's remaining half. +- **Markdown `**bold**` does not render.** `run_async_wrapper.html` shows literal + `**` in the header's most important admonition. Single `*italic*` works. +- **`@li` lists terminate the enclosing `@warning`.** In `run_async_wrapper.html` the warning + box closes before the `
    `, so all three hazardous patterns render as ordinary body text — + the framing is stripped from exactly the content that needs it. +- **`@note` is silently demoted to body prose** by MrDocs 0.8.0 (verified on `task::handle`, + `task::release`) — indistinguishable from ordinary text library-wide. +- **Near-clone headers duplicate every defect.** `any_read_stream`/`any_write_stream`, + `async_event`/`async_mutex`, `error`/`cond`, `executor_ref`/`any_executor`, + `read`/`write`/`read_at_least`/`write_at_least`, `task`/`quitter`. Fix in pairs or the pair + re-diverges. `run_blocking_wrapper` is a copy of `run_async_wrapper` that inherited a false + claim **and dropped its rationale clause**. +- **Blanket "operations on a default-constructed X are undefined" is false in five places** — + the same classes document `has_value()`, `operator bool()`, `operator==`, and `target_type()` + as working on the empty state (`any_read_stream`, `any_write_stream`, `any_executor`). +- **Missing `@tparam` on member function templates is entirely ungated** — MrDocs emits zero + tparam warnings across all 214 baseline fingerprints, despite `warn-no-paramdoc: true`. +- **`detail::` vocabulary leaks into public prose** with no resolvable target: "trampoline", + "chain", "the internal work guard" (names an object that does not exist — `thread_pool` + uses a `joined_` flag), `slice_of` (public alias is `slice_type`), `stop_requested_exception`, + `frame_memory_resource`. +- **No docstring anywhere links to an `.adoc` page.** `grep 'xref:\|specification-methods' + include/` returns nothing. So every term defined only in `A.specification-methods/` is + unreachable from the reference — including *"contingency"* (24 uses) and + *"supports IoAwaitable cancellation"* (11 uses, italicised as a defined term in 12 + docstrings). Per `ef789cea`, MrDocs escapes docstring punctuation, so a docstring **cannot** + carry an xref. That is a structural dead end, not an oversight. +- **`Capy.Terminology`'s swap list is narrower than C10.** It covers only + launch/spawn/cancel-token/boxed. It cannot see `scheduler` used for `executor` + (`io_awaitable.hpp`), `wrapper` vs `launcher` (all 18 `run_async` `@return` lines), + `IoAwaitables` vs "I/O awaitable", or `@pre` vs `@par Preconditions` (17 uses each). + +### The "contingency" question, resolved + +The four algorithm headers say **contingency** (6× each, 24 total, and nowhere else in +`include/`); the concept headers say **condition**. **"Contingency" should win**, and the +algorithm headers are the correct side: + +1. `A.specification-methods/Ac.contingencies.adoc` formally defines it. +2. "Condition" is already taken — `cond.hpp` is *"Portable error conditions"*, and the + standard reserves *error condition* for `std::error_condition`. +3. The algorithm headers already maintain both terms as *distinct* concepts — + `Contingencies:` heads the when-an-error-is-reported list, `Notable conditions:` heads the + `cond::` enumerator list. Two concepts, not synonyms, so C10 does not bite them. + +Fix direction: extend "contingency" into `concept/read_stream.hpp:47` and +`concept/write_stream.hpp:51`. Vale cannot help — neither word is in the swap list, and +adding one requires picking the direction. + +--- + +## R6. Grades by assignment + +`St Ac Wo Co Pr` · `M`=major `m`=minor `.`=clean · St excludes the retired ordering rule + +| Header(s) | St | Ac | Wo | Co | Pr | +|---|:-:|:-:|:-:|:-:|:-:| +| `ex/run_async.hpp` | . | M | m | M | M | +| `test/stream.hpp` + `read_stream` + `write_stream` | . | M | . | m | . | +| `concept/read_stream` + `write_stream` + `stream` | . | M | . | m | m | +| `concept/executor` + `execution_context` + buffer concepts | . | M | m | m | . | +| `concept/io_awaitable` + `io_runnable` + `decomposes_to` | . | M | m | m | . | +| `buffers/consuming_buffers` + `buffer_param` + `asio` | . | M | . | m | m | +| `io/any_stream.hpp` + `write_now.hpp` | . | M | . | m | . | +| `ex/executor_ref.hpp` + `any_executor.hpp` | . | M | m | m | . | +| `ex/thread_pool.hpp` | . | M | m | m | m | +| `test/run_blocking.hpp` | . | M | . | m | . | +| `test/fuse.hpp` | . | M | m | . | . | +| `test/bufgrind` + `buffer_to_string` + `thread_name` + `test.hpp` | . | M | . | m | . | +| `io_result` + `io_task` + `error` + `cond` | . | M | . | m | m | +| `ex/this_coro` + `io_env` + `immediate` + `work_guard` + `continuation` | m | M | . | m | . | +| `ex/async_mutex.hpp` | . | M | m | M | m | +| `task.hpp` | m | M | m | m | m | +| `quitter.hpp` | m | m | m | M | . | +| `io/any_read_stream.hpp` + `any_write_stream.hpp` | . | m | . | m | . | +| `ex/strand.hpp` | m | m | . | m | . | +| `ex/async_event.hpp` + `async_waker.hpp` | m | m | m | m | m | +| `ex/execution_context.hpp` + `system_context.hpp` | m | m | m | m | . | +| `ex/run.hpp` | m | m | . | m | m | +| `when_all.hpp` | m | m | m | m | . | +| `when_any.hpp` | . | m | . | m | m | +| `ex/frame_allocator` + `frame_alloc_mixin` + `recycling_memory_resource` + `io_awaitable_promise_base` | . | m | m | m | m | +| `read` + `write` + `read_at_least` + `write_at_least` | . | m | m | . | . | +| `buffers/make_buffer` + `buffer_slice` + `buffer_copy` + `front` | . | m | m | m | m | +| `buffers.hpp` | . | m | . | . | m | + +**Cleanest:** `concept/read_stream.hpp` (clean on all five in isolation), `front.hpp`, +`test/read_stream.hpp`, `buffers.hpp` (one Ac, one Pr). +**`Wo` clean on 20 of 28** — Phase 4's wording pass held on this surface. + +--- + +## R7. Recommended order + +1. **Fix `doc-audit.md`'s St ordering rule** (R1) and its two stale corpus calibrations + (`:page-mode:` 0→1 page, `cpp:` 0→51 pages). The prompt collection is the generation + engine for Phase 5; shipping it with a rule that flags 71% of the library is a + force-multiplier for noise. +2. **`sed` the 19 `@see executor` → `@see Executor`** (R3), then **bite-test + `warn-broken-ref`** — if `@see` is unlinked plain text, that gate is fail-open and belongs + on the F4 list next to `Capy.PartHeadings`. +3. **Reopen Task 5** with R2's evidence and gate docstring `@code`. 12 confirmed-broken + examples out of 103, two of which assert a compile failure that does not happen, is a + different input than the research doc had. +4. **Sweep `@par Thread Safety` and preconditions across full overload sets** (R5) — MrDocs + emits a page per overload, so per-overload coverage is the only coverage that exists. +5. **Add `@see run_async_wrapper` to all 18 `run_async` overloads** and repeat the two-call + constraint in each brief. Closes the remaining half of Phase-2 finding #3. +6. **Apply R4's rulings** — noting that two of them run *page → header*, so a one-directional + sweep is wrong. +7. Only then Phase 5. + +## Limits + +- Sub-agents were told not to report undocumented declarations (the MrDocs gate's job) and + not to re-report C2/C4/C9/C10 (promoted, clean). Findings here are what those gates cannot + see. +- Several agents could not run MrDocs (not on PATH) and judged `Pr` from the already-built + `doc/build/site`, which may lag the headers. Where a `Pr` finding rests on rendered output + it says so. +- `doc/lint/baseline.json` was authored 2026-07-30T16:45Z, before `a806892c` touched + `frame_alloc_mixin.hpp` — baseline-based reasoning about that header is partly stale. +- ~230 candidate findings were dropped on self-challenge, plus all section-ordering findings + discarded under R1. The dominant drop reason was **identity-shaped class briefs** (B4): + agents measured it as house convention across 17+ headers and declined to file it per + header. If B4 is meant to bind class briefs, that is one library-wide decision, not 28 + findings. diff --git a/DOC_IMPROVEMENT_PLAN.md b/DOC_IMPROVEMENT_PLAN.md new file mode 100644 index 000000000..624388fbc --- /dev/null +++ b/DOC_IMPROVEMENT_PLAN.md @@ -0,0 +1,452 @@ +# Capy / Corosio Documentation Improvement — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: use `superpowers:subagent-driven-development` +> (recommended) or `superpowers:executing-plans` to work task-by-task. Steps use checkbox +> (`- [ ]`) syntax. This is a **documentation** plan: a task's "test" is *docs build clean + +> Vale passes + snippets compile + page matches its acceptance checklist*, not a unit test. + +**Goal:** Bring Capy and Corosio documentation to Boost re-review quality by fixing the +verified feedback in [DOC_REVIEW_FEEDBACK.md](DOC_REVIEW_FEEDBACK.md), and lock in +guardrails ([DOC_STYLE_GUIDE.md](DOC_STYLE_GUIDE.md) + Vale) so it cannot re-drift. + +**Architecture:** Guardrails first, then outside-in — Structure → Accuracy → +Completeness → Wording — because polishing prose before fixing structure just creates +work to redo. Single source runs **three levels: code → docstrings (the MrDocs reference) +→ exposition** (which *links* the reference, never restates it). Two doc surfaces exist — +`.adoc` pages and header docstrings — and both get the five axes in every phase. + +**Tech stack:** Antora (AsciiDoc), `antora-cpp-reference`/`-tagfiles` extensions, +`@antora/collector-extension` snippet pipeline (`test/doc/`), MrDocs reference (generated +from `include/boost/capy/**` docstrings), Vale (to be added), and the `doc-prompts/` +structured prompt collection (generation/repair engine). + +## Global Constraints + +- **Capy first**; Corosio follows the identical playbook (Phase 5). +- **The doc corpus is two surfaces:** exposition `.adoc` pages **and** reference docstrings + in `include/boost/capy/**` (MrDocs generates the reference from them). Every phase covers + both for the symbols in scope. **Reference edits land in the `.hpp`, never the generated + page.** Reference accuracy means *docstring ↔ code*, not prose ↔ reference. Follow the + `boost-docs` skill's Doxygen conventions for docstrings. +- **Never hand-type an API signature** in exposition prose — link with `cpp:` (Style Guide B1). +- **Never paste example code** — `include::example$...[tag=]` from a compiled source + (Style Guide B2). Non-compiling blocks carry `role=pseudocode`/`role=external`. +- **One Diátaxis mode per page** (Style Guide A1); reference content stays in the reference. +- **Holistic adoption, not piecemeal.** The approach (Diátaxis + anti-drift guardrails + + the five axes) is adopted across the doc set as a coherent whole and integrated together, + not merged one task at a time. Tasks remain the unit of *work and review* (each still ends + build-clean + `vale`-clean + commit), but they accumulate on the branch and land as one + cohesive change rather than per-task PRs. +- **Edits are produced by the `doc-prompts/` collection**, not hand-authored ad hoc: + `doc-write` (new/rewritten pages or docstrings), `doc-fix` (repairs from `doc-audit` + findings), `doc-sync` (code-change drift, incl. the changed symbol's own docstring). The + CI gates (Task 2) are the backstop, not the author. +- Doc build (local): `cd doc && npx antora --fetch local-playbook.yml` — must finish with + **zero broken-xref warnings**. +- Snippet build (local): the `test/doc/{snippets,programs}` CMake targets must compile + (confirm exact target name from `.github/workflows/ci.yml` before first run). +- Every task ends: **build clean → `vale doc/modules` clean at error-level → commit.** + +--- + +## Phase 0 — Re-baseline & guardrails + +### Task 1: Re-baseline the feedback against current `develop` + +**Files:** update `DOC_REVIEW_FEEDBACK.md` status columns; produce `doc-worklist.md`. + +**Why first:** develop has moved since the review. Already confirmed done: #2 (positioning, +`708f0d34`), #8 (snippet compile, `aa1a38c7`), signal-safety (`3dc32e8a`), much of #9 TLS +(`19d76f37`,`71040d78`). Re-checking prevents reworking items that are already closed. + +- [ ] **Step 1:** For each finding #1–#30, open the current file(s) named in its + `verify_hint` and mark it done / partial / open. Command per finding, e.g.: + `grep -rn "Asynchronously launch a lazy task" include/boost/capy/ex/run_async.hpp` +- [ ] **Step 2:** Write `doc-worklist.md`: one row per still-open finding → + `{finding#, library, surface (adoc|docstring), pages/headers, phase, owner}`. This + worklist, not this plan, is the authoritative per-item task list for Phases 1–4. +- [ ] **Step 3:** Commit: `docs: re-baseline review feedback against develop`. + +**Acceptance:** every finding has a current status and a surface. + +### Task 2: Stand up all enforcement tiers (warning mode + baseline) + +**Files:** Create `doc/.vale.ini`, `doc/.vale/styles/Capy/{Terminology,NoFluff,SentenceLength,PartHeadings,SimpleTense}.yml`; +create `doc/lint/doc-lint.mjs` and `doc/lint/baseline.json`; modify `.github/workflows/ci.yml`. + +Enforcement has three tiers (Style Guide Part F.0): **gate** (blocks merge), **warning** +(flags for review), **review** (PR checklist). This task installs the machinery for all +three but runs every automated check **non-blocking at first** — the un-cleaned docs would +otherwise turn CI red across the board. Gates are promoted per phase (see +schedule below). Of the 30 style-guide rules: ~12 are gate-able, ~9 run as warnings, ~9 stay +on the PR checklist. + +- [ ] **Step 1:** Create the Vale rule files from Style Guide Part F (Terminology, NoFluff, + SentenceLength) plus two trivial adds: `PartHeadings` (regex `^==+\s+Part\s+\d+` → rule A7) + and `SimpleTense` (existence: "will ", "has been" → rule C4). +- [ ] **Step 2:** Write `doc/lint/doc-lint.mjs` — the ~50-line structural linter for the + gates Vale cannot express. It checks: + - **A1** — every page under `pages/` declares `:page-mode:` + - **A6** — `quick-start` is within the first 3 nav entries + - **B2** — no `[source,cpp]` block holds raw code (must be `include::example$…` or carry a + `role=pseudocode`/`role=external`) + - **D2** — every tutorial/concept page has ≥1 `include::example$` + Emit findings as JSON; exit 0 while in warning mode. +- [ ] **Step 3:** Add the accessibility contrast check (rule E4): build the site, run + `pa11y-ci` (or `axe`) against the output with contrast rules enabled. +- [ ] **Step 4:** Add the **reference-surface gates** — (a) the **MrDocs build must emit no + warnings** (undocumented parameters, mismatched `@param` names, unresolved references); + (b) **Vale runs over docstring prose** extracted from `include/boost/capy/**`, not only the + `.adoc` pages. (Docstring `@code` compilation is deliberately *not* here — see Task 5.) +- [ ] **Step 5:** Wire all automated checks into the CI doc job **non-blocking** + (`continue-on-error: true`): `vale` (adoc + docstrings), `node doc/lint/doc-lint.mjs`, the + a11y scan, and the MrDocs no-warnings check. The existing **snippet-compile job stays a + hard gate** — it is the accuracy tier for `.adoc` example code (B2/B3/D2 correctness). +- [ ] **Step 6:** Snapshot current violations to `doc/lint/baseline.json` so CI can gate + "no *new* violations" while the backlog is worked down. +- [ ] **Step 7:** Decide the guide's permanent home (recommend `doc/CONTRIBUTING-docs.md` or + a contributing page) and move it there; keep a single terminology source (Style Guide C.1). +- [ ] **Step 8:** Commit: `docs: add style guide + enforcement tooling (warning mode)`. + +**Acceptance:** CI runs Vale (adoc + docstrings) + doc-lint + a11y + MrDocs-no-warnings and +reports findings **without** failing the build; introducing a *new* banned word ("utilize") +or an undocumented `@param` fails the no-new-violations check; the snippet-compile job still +hard-fails on a broken `.adoc` example. + +**Gate-promotion schedule.** A rule flips from warning → hard gate at the exit of the phase +that cleans it (a `continue-on-error: false` change + baseline reset): + +| Promote at end of | Rules that become blocking gates | +|---|---| +| Phase 1 (Structure) | A1, A6, A7, B2, D2 | +| Phase 2 (Accuracy) | MrDocs-no-warnings (B3 already gated via the compile job); E4 stays Review tier — theme-controlled, not Capy-fixable, same as E2 | +| Phase 4 (Wording) | C2, C4, C9, C10 (over both `.adoc` and docstrings) | + +Two corrections this schedule earned in practice, recorded here so a later phase does not +re-learn them. **"A baseline reset" is not what a gate promotion needs** — see the Phase-4 exit +below; the operative criterion is *the promoted rule's gated slice is empty*, and only the CI +`workflow_dispatch` job may author a baseline. **And C2 is not a Vale gate**: `Capy.SentenceLength` +is `level: suggestion` and enforces nothing, so C2's authority is `doc/lint/sentence-length.mjs` +and its gate spec is head-anchored (`sentence_length:^C2:`) while the Vale specs are tail-anchored +(`…Capy\.NoFluff)$`). The two shapes are not interchangeable and a `^`-anchored Vale spec silently +gates nothing — `doc/lint/README.md` holds the fingerprint contract. + +### Task 3: Audit `cpp:`-macro / reference-link coverage (finding #7) + +**Files:** produce `doc-xref-gaps.md`. + +- [ ] **Step 1:** Grep exposition pages for hand-typed signatures that should be `cpp:` + links: `grep -rnE '\b(run_async|task<|io_task|thread_pool|strand)\b' doc/modules/ROOT/pages | grep -v 'cpp:'` +- [ ] **Step 2:** Confirm the `cpp:` macro resolves in a build (pick one page, convert one + signature, `npx antora` build, verify the link renders to the reference). +- [ ] **Step 3:** Record gap count per page in `doc-xref-gaps.md` (feeds Phase 1). +- [ ] **Step 4:** Commit the audit. + +### Task 4: Harden the documentation prompt collection + +**Files:** `doc-prompts/*.md` (`doc-write`, `doc-fix`, `doc-sync`, `doc-audit`, `README`). +No repo docs change in this task. + +The `doc-prompts/` collection is the generation/repair engine for Phases 1–4. Prove it +against real inputs and wire it to the sub-agent harness before relying on it. + +- [ ] **Step 1:** Dry-run `doc-audit` on three representative pages (a tutorial, a + reference-heavy exposition page, a design essay). Confirm findings cite real verbatim + spans and map to the five axes; tune the noise floor if it over- or under-reports. +- [ ] **Step 2:** Dry-run `doc-sync` on a recent public-header diff (e.g. the `run_async` + brief or a TLS commit). Confirm it locates the stale spans — **including the changed + symbol's own docstring** — and grounds each edit in the **new** declaration. +- [ ] **Step 3:** Dry-run `doc-write` on one small symbol. Confirm it grounds claims in the + fact sheet, sources the example from a compiled snippet, and links the reference via `cpp:`. +- [ ] **Step 4 (reference mode):** Extend each tool for the reference surface — Step 0 + inventory includes `include/boost/capy/**` docstrings; `doc-audit` gains a reference mode + (fixed Diátaxis mode = reference; axes remap to the docstring contract: brief/`@param`/ + `@return`/`@throws`/thread-safety/template-constraint completeness, docstring↔code accuracy, + MrDocs render check); `doc-sync` treats the changed symbol's co-located docstring as + drift-hit #1; `doc-write`/`doc-fix` accept a header `target_file`. Align with the + `boost-docs` skill. +- [ ] **Step 5:** Wire the tools to the sub-agent harness (the mechanism his `code-review.md` + uses); confirm raw code/prose never enters the main context. +- [ ] **Step 6:** Commit: `docs: harden documentation prompt collection`. + +**Acceptance:** each tool runs end-to-end on a real input (a page **and** a docstring) and +returns valid typed records; a known-stale page is flagged by `doc-audit`; a known code +drift — including a stale `@param` — is caught by `doc-sync`. + +### Task 5: Research — gating docstring `@code` examples + +**Files:** produce `doc/lint/RESEARCH-docstring-examples.md`. No code change. + +The `@par Example` / `@code` blocks in header docstrings are hand-typed and compiled by +**no** gate today (the snippet-compile job covers only the `.adoc` pages). Research how to +bring them under a compile gate. **The outcome may be to defer implementation** — this task +produces a recommendation, not necessarily a gate. + +- [ ] **Step 1:** Enumerate options — (a) MrDocs `@snippet`/include directive pulling from a + compiled `test/doc/` source; (b) a preprocessor that extracts `@code` blocks into a + generated TU compiled in CI; (c) rely on `doc-sync` to catch drift at change-time with no + standing gate; (d) any MrDocs-native example verification, if the current version supports it. +- [ ] **Step 2:** For each, note feasibility against the pinned MrDocs version, effort, and + whether it round-trips cleanly into the rendered reference. +- [ ] **Step 3:** Recommend one — or recommend **defer** — with the decision and rationale + written to the research doc. +- [ ] **Step 4:** Commit: `docs: research docstring @code example gating`. + +**Acceptance:** a written recommendation with the options evaluated and a clear go/defer +decision. Any implementation is a separate follow-up, out of this plan's Phase 0. + +--- + +## Phase 1 — Capy: Structure (macro) + +Work the worklist rows tagged `structure`. Template per row below; worked examples first. +For each page touched, the **docstrings of the symbols it documents** get the same Structure +pass (contract shape); those edits land in the `.hpp`. + +### Task 6 (worked example): Relocate `8i.echo-server-corosio` out of Capy (finding #19) + +**Files:** Delete `doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc`; modify +`doc/modules/ROOT/nav.adoc`; move content to the Corosio examples in Phase 5. + +- [ ] **Step 1:** Move the page's compiled snippet source to Corosio's `example/` tree + (or note it for Phase 5 if Corosio isn't checked out). +- [ ] **Step 2:** Remove the nav entry and the page; `grep -rn "8i.echo-server-corosio" doc` + to find and fix inbound xrefs. +- [ ] **Step 3:** `cd doc && npx antora --fetch local-playbook.yml` → **zero broken xrefs**. +- [ ] **Step 4:** Commit: `docs: move corosio echo-server example to Corosio (#19)`. + +**Acceptance:** build clean, no dangling xref, example lives with Corosio. + +### Task 7 (worked example): Consolidate rationale placement (finding #10) + +**Files:** `doc/modules/ROOT/pages/9.design/*`, `A.specification-methods/*`, target pages. + +- [ ] **Step 1:** In the worklist, classify each `9.design`/`A.specification-methods` page as + *cross-cutting* (stays in Explanation) or *local* (moves to an admonition on the relevant + how-to/tutorial page) per Style Guide A3. +- [ ] **Step 2:** Move one local-rationale block into a `[NOTE]` on its home page; leave an + xref stub if inbound links exist. +- [ ] **Step 3:** Build clean; `vale` clean. +- [ ] **Step 4:** Commit per page moved. + +### Task 8 (template): flatten "Part N" headings (finding #11) & fix ordering (finding #12) + +**Files:** `4.coroutines/4e.cancellation.adoc` (Part 1–9), `2.cpp20-coroutines/*`. + +- [ ] **Step 1:** Replace `== Part N: X` with `== X` descriptive headings; drop the "Part" + ceremony (Style Guide A7). For a one-paragraph "Part", fold it into a sibling section. +- [ ] **Step 2:** Verify no section depends on a concept introduced later (A5); reorder if so. +- [ ] **Step 3:** Build clean; commit. + +> **Remaining Phase-1 rows** (from worklist): findings #1 (exposition-replicates-reference — +> per page, apply Style Guide B1/A2), #13/#14 (Corosio → Phase 5). Each is one task using the +> Task 7/8 template: reclassify → edit → build → `vale` → commit. + +- [ ] **Phase 1 exit — promote gates:** flip A1, A6, A7, B2, D2 to blocking + (`continue-on-error: false`), reset `doc/lint/baseline.json`, confirm CI is green. + +--- + +## Phase 2 — Capy: Accuracy + +Reference is central to this phase: `doc-sync` sweeps **docstring ↔ code** accuracy for the +symbols in scope, and the MrDocs no-warnings gate is promoted at phase exit. Reference and +exposition accuracy are fixed together, not in separate tracks. + +### Task 9 (worked example): Fix `run_async` reference brief (finding #3) + +**Files:** `include/boost/capy/ex/run_async.hpp:460,500,545,591,628,656,686,710,732,757…` +(this is a **docstring** fix — reference surface). + +- [ ] **Step 1:** Replace the brief "Asynchronously launch a lazy task on the given + executor" — `run_async` returns a launcher and takes **no** task. Rewrite per Style Guide + B4, e.g.: `/** Bind an executor (and options) to produce a launcher; invoke the launcher + with a task to start it. */` Keep wording consistent across all overloads (C10). +- [ ] **Step 2:** Rebuild the MrDocs reference (`doc/mrdocs.yml` pipeline); confirm the new + brief renders **and MrDocs emits no warnings** for these symbols. +- [ ] **Step 3:** Expand the two-call **warning admonition** (findings #3, Gennaro/Rainer): + add the *preconstructed-task* case and the *wrapper-function* case; link the Frame + Allocators page for rationale (Style Guide D3). +- [ ] **Step 4:** Build clean; `vale` clean; commit: `docs: correct run_async brief and expand two-call warning (#3)`. + +**Acceptance:** reference brief no longer claims a task argument; warning lists ≥3 dangerous +patterns with rationale link; MrDocs clean for the symbol. + +> **Remaining Phase-2 rows:** #17 (UDP text → Phase 5), #9 residual TLS-warning mirroring in +> tutorials, #16 executor-affinity docs, #21/#22 (io_result / buffer-concept specs). +> Template = Task 9: fix → rebuild reference → verify → commit. Docstring-surface rows run +> the same template against the `.hpp`. + +- [ ] **Phase 2 exit — promote gates:** flip MrDocs-no-warnings to blocking; confirm CI green. + (E4 stays Review tier — a11y contrast findings were all shared-theme nav chrome, not + Capy-fixable, consistent with E2's demotion.) + +--- + +## Phase 3 — Capy: Completeness & Pedagogy (human-led) + +> These tasks need judgment and must not be run unsupervised (the review's core warning). +> Use Alan's "fresh agent, no session context" technique to draft explanations, then a human +> reviews. Each task is still gated by build + `vale`. Docstring completeness (every +> `@param`/`@return`/`@throws` present) is part of this phase for the symbols in scope. + +### Task 10 (worked example): Add a runnable example to the `task` page (finding #5, D2) + +**Files:** `4.coroutines/4a.tasks.adoc`; new compiled source `test/doc/snippets/tasks_run.cpp`. + +- [ ] **Step 1:** Write a minimal compiled snippet that *runs* a `task` (e.g. via + `run_blocking` or `run_async` inline) with an assertion on the result; tag a region. +- [ ] **Step 2:** `include::example$snippets/tasks_run.cpp[tag=run]` in the page, replacing + any hand-typed block (B2). +- [ ] **Step 3:** Build the snippet target → compiles & passes; `npx antora` → renders. +- [ ] **Step 4:** Commit: `docs: give the task page a runnable example (#5)`. + +**Acceptance:** the page a reader lands on to learn `task` now shows a task actually running, +and the snippet is compiled in CI. + +> **Remaining Phase-3 rows:** #5 across other concept pages (IoAwaitable, executors, +> allocators), #12 (balance coroutine-intro depth: expand symmetric transfer, trim threads), +> #28 callback-interop examples, #29 GUI example, #30 platform-issues page; plus docstring +> completeness for changed symbols. Template = Task 10. + +- [ ] **Phase-3 exit — reseed `doc/lint/baseline.json` from CI (owed follow-up).** Phase 3 closed + 195 `mrdocs_warnings` findings (214 → 19) but the committed baseline still records 214, so all + 195 stay grandfathered and can be reintroduced without failing the gate. The reseed mechanism + is the `workflow_dispatch` reseed steps in `.github/workflows/docs.yml`; the procedure is + `doc/lint/README.md`. GitHub only offers `workflow_dispatch` for workflows present on the + **default branch**, so the order is: merge the branch → dispatch **Documentation** → review the + candidate's diff → commit it. Never regenerate the baseline locally (measured: a local run adds + 357 fingerprints a CI run would not). + +--- + +## Phase 4 — Capy: Wording (STE pass, last) + +### Task 11 (template): STE/Vale cleanup, page by page (and docstrings) + +**Files:** each surviving page, in worklist order, **and the docstrings of the symbols it +documents**. + +- [x] **Step 1:** Run `vale` on the page **and** its symbols' docstrings; fix every finding: + split long sentences (C1/C2), remove unnecessary negatives (C5), cut fluff/clichés (C6/C9), + apply terminology (C10). *Done for the four rules this phase promotes, both surfaces — final + state below. Two scope rulings narrowed "every finding": the `Google.*` house-style pack is + demoted below warning level with the reasons in `doc/.vale.ini` (Capy writes Title Case + headings; Google style mandates sentence case), and the residual `Vale.Spelling` findings are + bare C++ identifiers wanting backticks — real Style-Guide **B1** defects, not C-rule defects, + tracked in `doc-worklist.md` instead.* +- [ ] **Step 2:** Build clean; re-run `vale` → clean at warning level for that page and its + docstrings. *Left unticked deliberately: "`vale` clean at warning level" was never achievable + on this branch — 2825 error-level alerts at `4bea4edf`, 722 after the phase — and chasing it + would have meant adopting Google's house style. The operative per-task criterion actually used + was **the blocking gate is green AND the four C-rule slices are empty for the touched files**, + with the doc build clean at zero broken xrefs (688 pages). That was met by every task.* +- [x] **Step 3:** Commit per page: `docs: STE wording pass on `. *Commits are per + page-group rather than strictly per page, disclosed by each task: prose that chains across + pages (the "In the next section you will learn …" closers, a section summary and the pages it + summarises) has to move together or intermediate commits contradict themselves.* + +> Wording is last on purpose — never polish prose on a page or docstring that Phase 1–3 might +> delete or rewrite. + +- [x] **Phase 4 exit — promote gates** *— CI is red on that one step until the post-merge + reseed*: C2, C4, C9 and C10 are blocking over **both** surfaces. + The blocking step in `.github/workflows/docs.yml` now carries six `--gate` specs; the + three added at this exit are `sentence_length:^C2:`, + `vale_adoc:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$` and the `vale_docstrings` + equivalent, so `vale_docstrings` becomes a gated check too. Each was bite-tested with a planted + violation before being believed. + + **Two of the three original clauses were wrong and were superseded by maintainer rulings.** + + * *"reset `doc/lint/baseline.json` to empty (backlog cleared)"* — **infeasible, and abandoned.** + 16 permanently-grandfathered MrDocs findings, 70 Review-tier a11y theme findings that are + shared-Antora-theme contrast and not Capy-fixable, and the whole non-gated backlog all live in + that file; emptying it would also violate the standing rule that only the CI + `workflow_dispatch` job may author a baseline. **The substitute criterion, ruled and met, is + per-rule gated slices empty.** Measured at `620fdf2c`: C4 `Capy.SimpleTense` gated-new 0, C9 + `Capy.NoFluff` 0, C10 `Capy.Terminology` 0, on both surfaces. + * *"confirm CI green"* — **not achievable for C2 until a post-merge reseed.** `sentence_length` + has no entry in the committed baseline, so nothing in its slice is grandfathered and + `--gate 'sentence_length:^C2:'` **exits 1** on exactly two findings, both in + `include/boost/capy/when_any.hpp` (a 27-word and a 31-word sentence). They are **accepted + refusals, not defects**: a rewrite that split them made a false claim against the code and was + reverted verbatim. The maintainer declined an in-source refusal marker and chose **visible debt + over new machinery**, so the Documentation job stays red on that one step until the owed + action below. The C4/C9/C10 gate needs no reseed and is green today. + + **Final measured state, both surfaces, at `620fdf2c`:** + + | Rule | `.adoc` | docstrings | + |---|---|---| + | C2 hard (`sentence_length`) | **0** | **2** (accepted refusals) | + | C2 advisory (essay carve-out) | **67** | — | + | C4 `Capy.SimpleTense` | **2** | **0** | + | C9 `Capy.NoFluff` | **1** | **0** | + | C10 `Capy.Terminology` | **0** | **0** | + + The 3 residual `.adoc` findings are all inside **two verbatim third-party quoted passages** + (`9n.WhyNotCobaltConcepts.adoc:347` carries two `will` tokens on one line and Vale raises one + alert per token; `9k.Executor.adoc:125` is a P0913R1 `[quote]` block), grandfathered by + fingerprints verified to describe those same sentences. + + **The 67 advisory C2 findings are a deliberate carve-out, not a backlog.** Part C2 of the style + guide says the 25-word limit is "hard in API docs; soft in essays", so `sentence-length.mjs` + keys findings under `modules/ROOT/pages/9.design/` and `.../A.specification-methods/` as + `advisory-C2` — a key that deliberately does not begin with `C2`, so even a mis-written + head-anchored spec cannot reach them. Verified: the gated slice contains 0 `advisory-C2` + fingerprints. + +- [ ] **Owed after merge — dispatch the reseed.** `workflow_dispatch` is only offered for a + workflow present on the **default branch**, so the order is: merge → dispatch **Documentation** → + review the candidate → commit it. Procedure and the one pre-authorised exception (the reseed + report *will* exit 1 naming those two `when_any.hpp` fingerprints, and only those two) are in + `doc/lint/README.md`. **Never reseed locally** — a local run adds 357 fingerprints a CI run + would not. + + **Turning the C2 gate green is the smaller half of what this buys.** The same reseed retires + roughly **3,700 dead grandfather clauses** — baseline entries for findings that no longer exist, + measured locally at `620fdf2c` as 2348 `vale_adoc` + 1189 `vale_docstrings` + 195 + `mrdocs_warnings` (recorded counts 2441 / 1477 / 214 against actual 97 / 296 / 19; a CI-authored + candidate will differ by some of the known 357-fingerprint local-vs-CI drift). A dead + grandfather clause means the gate will not object if the finding comes back. **So until this + lands, every Phase-4 wording fix and Phase-3's 195 MrDocs fixes are revertible without failing + CI** — the promoted gates catch newly *introduced* findings, not the re-introduction of a + grandfathered one. That, not the red step, is the reason to reseed promptly. + +--- + +## Phase 5 — Corosio (same playbook) + +Repeat Phases 0–4 against Corosio (both surfaces), driven by its worklist rows. +Highest-value, verified-open Corosio items: + +- **Structure:** collapse the three-tier duplication — delete/merge `4.guide/4a.tcp-networking` + (duplicates the Networking Tutorial) and relocate `4.guide/4b.concurrent-programming` + (Capy content) (finding #4); move Quick Start to the top of nav (finding #14); regroup the + Reference by functionality, document operators with their types, separate sync/async + (finding #13). +- **Accuracy:** mirror the TLS "not-wired-up" warning into `3.tutorials/3b.http-client` + (finding #9); correct the UDP fragmentation text to state guaranteed-reassembly limits + (finding #17); docstring↔code sweep via `doc-sync`. +- **Completeness:** document executor affinity (finding #16); glossary A–Z nav table + the + missing coroutine terms (finding #23). +- **Presentation:** dark-mode contrast check (finding #27). +- **Already done — verify only:** #24 (signal/resolver overview prose), #25 (note placement). + +Each is one task on the Task 6/7/9/10/11 templates. + +--- + +## Self-review checklist (run before handing off each phase) +- [ ] Every still-open finding in DOC_REVIEW_FEEDBACK.md maps to a worklist row and a task. +- [ ] Both surfaces covered: for each symbol in scope, its docstring got the same axis pass + as the `.adoc` (reference woven in, not deferred). +- [ ] No task hand-types a signature or pastes code in exposition (Style Guide B1/B2). +- [ ] Every task ends with build-clean + `vale`-clean + commit. +- [ ] Wording (Phase 4) runs only after structure/accuracy/completeness on that page. + +## Execution options +1. **Subagent-driven (recommended):** fresh subagent per task, human review between tasks — + fits the "human in the loop, no unsupervised drift" mandate. +2. **Inline execution:** batch with checkpoints via `superpowers:executing-plans`. diff --git a/DOC_REVIEW_FEEDBACK.md b/DOC_REVIEW_FEEDBACK.md new file mode 100644 index 000000000..c46257430 --- /dev/null +++ b/DOC_REVIEW_FEEDBACK.md @@ -0,0 +1,370 @@ +# Capy + Corosio — Documentation Feedback from the Boost Review + +**Prepared:** 2026-07-24 +**Scope:** All documentation feedback from the joint Boost formal review of Capy and +Corosio (mailing-list thread, June 23 – July 7 2026, extended), plus open GitHub issues +on `cppalliance/capy` and `cppalliance/corosio` (including pre-review issues). + +**Sources mined:** +- Boost ML review thread — 73 messages, 19 participants + ([thread](https://lists.boost.org/archives/list/boost@lists.boost.org/thread/5RXXUC7XHL7JGSFPCKWMFHTRPMMLXRTC/)). +- Alan de Freitas's Corosio feedback filed as `corosio#324` + ([issue](https://github.com/cppalliance/corosio/issues/324)). +- 12 Capy issues (#356 #353 #297 #296 #287 #283 #273 #266 #207 #170 #159 #71) and + 5 Corosio issues (#324 #285 #284 #283 #23) reviewed for documentation angle. + +> ## ⚠️ Correction (2026-07-24, after pulling develop) +> Several top findings have **already been addressed on `develop`** since the review, and +> the status columns below are correspondingly stale. Confirmed via git history + file +> inspection: +> - **#2 (what Capy is / core invariant) — now largely addressed.** Commit `708f0d34` +> (closes #341/#349) added "What Capy Is / Is Not" to `index.adoc`, the same-executor +> invariant up front + a `4c.executors.adoc` section, and reframed `4d.io-awaitable.adoc` +> as interop vocabulary with a "Bridging a Foreign Awaitable" escape hatch. +> - **#8 (examples don't compile) — closed at the tooling level.** Commit `aa1a38c7` +> replaced ~480 hand-typed blocks with includes of compiled sources under `test/doc/`; +> intentionally-non-compiling blocks now carry `role=pseudocode`/`role=external` (this +> also addresses Alan's "pseudocode mixed with real syntax"). +> - **#9 (TLS) / signal-safety** — substantially advanced: `19d76f37`/`71040d78` wired TLS +> trust-store/verify/ALPN; `3dc32e8a` made POSIX signal handling async-signal-safe. +> +> The **re-baseline pass is the first unit of the improvement plan** for exactly this +> reason. Treat the tables below as the review-time snapshot, not current state. + +**Method.** 181 raw documentation-feedback items were extracted from the sources, then +clustered into distinct findings. Each checkable finding was verified against the +**current local `develop` docs** to determine whether it is still open, partially +addressed, or already fixed. Confidence = **signal strength** (number of distinct +reviewers, author self-diagnosis counted as strong corroboration) **× accuracy** (still +open ranks higher than already-fixed). + +> A note on attribution: reviewers cited the live site paths (e.g. `8.design/…`); the +> local `develop` tree has renumbered some chapters (now `9.design/…`). Verification +> maps by topic, not by URL. + +--- + +## 1. Who gave documentation feedback, and their stance + +| Reviewer | Doc feedback weight | Overall stance (my reading) | +|---|---|---| +| **Alan de Freitas** | Very high — a documentation-centric Capy review (msg 64) + all of `corosio#324` | Accept (Capy), non-conditional | +| **Rainer Deyke** | High — Capy review (msg 8) + a Corosio doc read-through (msg 71) | **Reject** (Capy) | +| **Andrzej Krzemienski** | High — msg 67 + issues #207 #170 #273 #287 #297 #356 #283 | Lean reject / "encourage and reconvene" | +| **Gennaro Prota** | Medium — praised docs, TLS-mirroring nitpick, concept naming (msg 55) | Conditional accept | +| **Peter Turcan** | Medium — three focused Corosio polish issues (#283 #284 #285) | (tech-writer polish, not a vote) | +| **toast27** | Low-medium — wants callback-interop examples; "people skip docs" (msg 57) | Lean accept (Capy) | +| **Vinnie Falco** (author) | — self-diagnosis (msg 18, msg 50) | Author | +| LegalizeAdulthood, MungoG | Low — single GH example/platform-doc requests | — | + +The documentation was a **decisive factor** in the review. Rainer's reject and Andrzej's +reluctance both trace substantially to documentation problems, and the author himself +(msg 50) concluded *"This review has surfaced a documentation problem… several reviewers +have arrived at different (incompatible) conclusions about what Capy is, because we never +stated it plainly."* + +--- + +## 2. Master list — findings ranked by confidence + +Legend — **Cat:** St=Structure, Ac=Accuracy, Wo=Wording, Co=Completeness/Pedagogy, +Pr=Presentation/Tooling. **Status:** 🔴 still open · 🟡 partially addressed · 🟢 fixed · +⚪ deferred (Corosio repo not present in this checkout — see Phase 5). + +> **Re-baselined 2026-07-24 against current `develop`** (Phase 0, Task 1). Every status +> below reflects a file:line or grep verification recorded in +> `.superpowers/sdd/DOC_IMPROVEMENT_PLAN/task-1-report.md`, not the review-time snapshot. +> Still-open/partial findings have a row in `doc-worklist.md`. Corosio-scoped findings +> (`Lib=corosio`) are marked deferred regardless of prior status, because the Corosio repo +> is not checked out alongside Capy here and cannot be re-verified — including #9, #24, #25 +> which the review-time doc had rated 🟡/🟢 on the strength of the Corosio-side commits/PRs +> cited (those changes live in the Corosio repo, not this one). + +| # | Finding | Lib | Cat | Parties | Status | +|---|---|---|---|---|---| +| **1** | **Exposition replicates the reference and drifts from it** — prose restates full signatures/concepts that then diverge from the generated reference | both | Ac/St/Pr | Alan, Andrzej, Rainer (3) | 🟡 — *re-baseline: `antora-cpp-reference`/`-tagfiles` extensions are now installed and configured (`doc/antora.yml`, `doc/package.json`) but the `cpp:` macro has zero uses in `doc/modules/ROOT/pages` and only 3 `xref:reference:` links exist across ~65 pages; ~160 hand-typed API-term hits remain (Task-3 grep). Tooling exists, prose doesn't use it yet.* | +| **2** | **The docs never state plainly *what Capy is* / its scope + core invariant up front** → reviewers reach incompatible conclusions | capy | St/Co | Vinnie(author), Rainer, Alan, Andrzej (4) | 🟢 — *re-baseline: fixed by `708f0d34`. `index.adoc:5` "What Capy Is", `:23` "What Capy Is Not"; `4c.executors.adoc:10` anchors a full "same-executor invariant" section.* | +| **3** | **`run_async` two-call syntax under-documented** — warning lists too few dangerous cases; rationale/trade-offs not explained; reference text is wrong ("launch a lazy task" — it takes no task) | capy | Ac/Co | Rainer, Gennaro, Alan (3) | 🟡 — *re-baseline: `4b.launching.adoc:28-52` now explains the {cpp}17 evaluation-order rationale and one dangerous case (rvalue-qualified wrapper, compile-time caught). But `include/boost/capy/ex/run_async.hpp` still reads "Asynchronously launch a lazy task on the given executor" in all 18 overloads — the reference-text bug is unchanged.* | +| **4** | **Three-tier content duplication** — same material taught up to 3× (Networking Tutorial → Tutorials → Guide); Guide *TCP/IP Networking* duplicates the tutorial; Guide *Concurrent Programming* is Capy content; *UDP Sockets* mispositioned | corosio | St | Alan, Rainer (2) | ⚪ deferred (Phase 5) | +| **5** | **Not goal-oriented; no runnable examples for the library's own types** — `task` page never runs a task; IoAwaitable page has no working example; coroutine intro is syntax-first not use-case-first | capy | Co | Alan, toast27, Rainer (3) | 🟡 — *re-baseline: examples now compile (`aa1a38c7`), but `test/doc/snippets/4a_tasks.cpp` and `4d_io_awaitable.cpp` have no `main()` — no page shows a `task` actually running with output; the full runnable program (`test/doc/programs/4b_launching_run_async.cpp`) lives one page later.* | +| **6** | **"AI fluff": verbose, repetitive, unnecessary negatives ("This is X. Not Y. Not Z"), clichés/metaphors, terms undefined** — docs "orders of magnitude longer than needed" | both | Wo | Alan, Andrzej (2) | 🔴 — *re-baseline confirmed: `why-capy.adoc:275` and `9.design/9a.CapyLayering.adoc:61` are textbook unnecessary-negative patterns; 11 pages still contain banned filler words (simply/basically/essentially/obviously/of course/note that/in order to).* | +| **7** | **Missing reference cross-links** (the `cpp:` macro / `antora-cpp-reference` + `tagfiles` extensions) — hard to navigate and the root cause of finding #1's drift | both | Pr | Alan (1, stated for both libs) | 🟡 — *re-baseline: extensions are installed & configured in `doc/antora.yml`/`doc/package.json` (progress since review), but zero `cpp:` macro usages found in the page tree — the linking work itself hasn't started.* | +| **8** | **Example code that does not compile / unsafe example patterns** — IoAwaitable example was broken; many examples pass args by reference/view (dangling risk); pseudocode mixed with real syntax | both | Ac/Co | Rainer, Alan (2) | 🟡 (IoAwaitable 🟢 confirmed via spot-check; dangling-ref pattern 🔴 confirmed still present, e.g. `test/doc/snippets/5c_sequences.cpp:142`, `4f_composition.cpp:342`) | +| **9** | **TLS docs signal not-ready and are not fail-safe** — red "not wired up" boxes; unimplemented features silently ignored instead of refusing; TLS warning not mirrored in HTTPS-client tutorial | corosio | Ac/Co | Gennaro, Rainer (+author agreed) (2) | ⚪ deferred (Phase 5) — *no TLS/HTTPS-client tutorial pages exist anywhere in this Capy checkout; the cited progress commits (`3dc32e8a`,`19d76f37`,`71040d78`) do not exist in this repo's git history (confirmed `unknown revision`) — they are Corosio-repo commits.* | +| **10** | **Design-rationale placement is inconsistent** — dedicated `9.design` pages *and* `A.specification-methods` *and* interleaved prose; pick one (preference: interleaved admonitions) | capy | St | Alan (1) | 🔴 — *re-baseline confirmed: both `9.design/` (10 files) and `A.specification-methods/` (3 files) still exist as separate rationale channels.* | +| **11** | **Over-use of "Part N" mega-headings** — cancellation page runs Part 1–9; "Part 4" is a single paragraph | capy | St/Wo | Alan (1) | 🔴 — *re-baseline confirmed: `4e.cancellation.adoc` still has Part 1 through Part 9; "Part 4: Beyond Cancellation" is lines 132-140 (9 lines).* | +| **12** | **Intro chapters inconsistent** — coroutine intro under-explains hard ideas (symmetric transfer: 2 short paragraphs) while over-explaining threads; "Advanced Topics" precedes the `await_suspend` explanation it depends on | capy | St/Co | Alan (1) | 🟢 — *re-baseline (disagrees with review-time 🔴): Symmetric Transfer in `2d.advanced.adoc` is now 67 lines / 4 subsections, not "2 short paragraphs". Ordering is correct: `await_suspend`/awaiter protocol is explained in `2b.syntax.adoc`/`2c.machinery.adoc`, both prerequisites of Part IV (`2d.advanced.adoc`).* | +| **13** | **Reference organized alphabetically, not by functionality** — no backend-tag overview; operators documented via "Friends"; sync and async operations not separated | corosio | St/Pr | Rainer (1) | ⚪ deferred (Phase 5) | +| **14** | **Quick Start mispositioned** — sits at the bottom, between Glossary and Reference | corosio | St | Rainer (1) | ⚪ deferred (Phase 5) | +| **15** | **Naming clarity flagged in docs** — `execution_context` vs `ExecutionContext` (case-only), `executor_ref` (vs Boost `*_ref` convention), `buffer_length`/`buffer_size` | capy | Wo | Alan, Gennaro (concept naming) (2) | 🔴 (design-adjacent) — *re-baseline confirmed unchanged: `concept/execution_context.hpp:73` vs `ex/execution_context.hpp` class; `ex/executor_ref.hpp`; `buffers.hpp:335` `buffer_size` / `:409` `buffer_length` both still present.* | +| **16** | **Executor affinity not documented** though class-level thread-safety is | corosio | Co | Rainer (1) | ⚪ deferred (Phase 5) | +| **17** | **UDP fragmentation text misleading** — frames fragmentation as loss-amplification only; omits guaranteed-reassembly limits (oversized fragmented datagrams can be dropped outright) | corosio | Ac | Rainer (1) | ⚪ deferred (Phase 5) | +| **18** | **Missing right-rail ToC; some pages very long** | both | Pr | Alan (1) | 🔴 — *re-baseline confirmed: no `:page-toc:` control found anywhere; long pages confirmed (`9m.WhyNotCobalt.adoc` 616 lines, `9n` 506, `9o` 466, `7a.drivers` 352). Likely needs the shared boost-website UI bundle, which lives outside this repo.* | +| **19** | **`echo-server-corosio` example lives in Capy docs** — breaks the library separation; move to Corosio | capy | St | Rainer (1) | 🔴 — *re-baseline confirmed: `8.examples/8i.echo-server-corosio.adoc` and its nav entry still present.* | +| **20** | **Awaitable-returning functions need a standard description method** — `Await-effects` / `Await-returns` / `Await-error-conditions` / `Await-postconditions` | capy | Co | Andrzej (#170) (1) | 🟡 — *re-baseline confirmed: pattern used in only 4 of ~14 top-level public headers (`read.hpp`, `write.hpp`, `read_at_least.hpp`, `write_at_least.hpp`); `when_all.hpp`, `when_any.hpp`, `task.hpp`, `quitter.hpp` lack it entirely.* | +| **21** | **Meaning of `error_code` in `io_result` is under-specified / "on success" reads backwards** | capy | Ac/Co | Andrzej (#207) (1) | 🟢 — *re-baseline (disagrees with review-time 🟡): `io_result.hpp:24-44` docstring + `Ac.contingencies.adoc` fully specify the contract with no "on success" phrasing (0 grep hits in either file).* | +| **22** | **Buffer & stream concept specs need precision** — buffer-handle lifetime contract, `void*` rationale, `buffer_slice` naming/semiregular status, `Slice` concept unnecessary, `IoAwaitable` definition too loose | capy | Ac/Co | Andrzej (#273 #287 #297 #356) (1) | 🔴 — *re-baseline confirmed still open: `concept/io_awaitable.hpp:110-117` `IoAwaitable` is still just `requires(A a, h, env){ a.await_suspend(h,env); }` — no `await_ready`/`await_resume` requirement; `buffers.hpp:63-70` `void*` ctor/`data()` still has no rationale docstring. (`buffer_slice.hpp:38-60` lifetime contract has improved — partial progress on one sub-point.)* | +| **23** | **Glossary needs an A–Z nav table + more coroutine terms** (`co_await`, `co_return`, coroutine frame, promise type, …) | corosio | Pr/Co | Peter Turcan (#283) (1) | ⚪ deferred (Phase 5) | +| **24** | **Signal Handling / Name Resolution "Overview" was code-only** — needs a sentence of prose | corosio | Co | Peter Turcan (#285) (1) | ⚪ deferred (Phase 5) — *the Section-4 "already fixed" note cites Corosio-side pages/commits not present in this checkout; cannot be re-verified here (see box above).* | +| **25** | **"Code snippets assume" note placed outside the NOTE box** in Hash Server + Reconnect tutorials | corosio | St | Peter Turcan (#284) (1) | ⚪ deferred (Phase 5) — *same caveat as #24.* | +| **26** | **HALO "(Clang extension)" renders blank** where the attribute name belongs | capy | Ac/Pr | Alan (1) | 🟢 — *spot-verified: `2d.advanced.adoc:102` renders `[[clang::coro_await_elidable]]` (Clang extension).* | +| **27** | **Dark-mode contrast** — black text on dark-blue background | corosio | Pr | Rainer (1) | ⚪ deferred (Phase 5) | +| **28** | **Add callback-based-API interop examples** (using a callback API *inside* a Capy coroutine) | capy | Co | toast27 (1) | 🟢 — *re-baseline (disagrees with review-time 🔴): `8.examples/8p.asio-use-capy.adoc` (added in `708f0d34`) demonstrates calling Boost.Asio's callback/completion-token API from inside a Capy coroutine via a `use_capy` token — this is exactly the requested pattern.* | +| **29** | **GUI event-loop integration example** | capy | Co | LegalizeAdulthood (#159) (1) | 🟡 — *re-baseline (upgrade from 🔴): `8.examples/8n.custom-executor.adoc` implements a generic single-threaded run-loop executor "analogous to a GUI event loop", but its Exercises section (line 98) explicitly defers actual GUI-framework integration to the reader — no worked GUI example exists yet.* | +| **30** | **Platform-specific issues need a documentation home** (e.g. Unix-sockets Windows behavior; undefined "IOCP") | corosio | Co | MungoG (#23), Alan (2) | ⚪ deferred (Phase 5) | + +--- + +## 3. Findings in detail (grouped by category) + +### 3.1 Structure — the shape and order of the material + +- **[#1, #4] Duplication is the dominant structural complaint.** In Capy the *exposition + replicates the reference*; in Corosio the *whole doc set repeats itself* across three + parent sections. Alan (`corosio#324`): the reader "has to read the same content three + times." Rainer (msg 71): "Guide > TCP/IP Networking is already covered by the Networking + Tutorial… Guide > TLS Encryption and Tutorials > TLS Context cover basically the same + subject twice." **Verified:** `4.guide/4a.tcp-networking.adoc` and + `4.guide/4b.concurrent-programming.adoc` both exist alongside the 12-page + `2.networking-tutorial`; `4b` is Capy-domain concurrency content. +- **[#2] The framing failure.** The docs do not open by saying what Capy *is*. The author's + own core invariant — *"a coroutine is always resumed by the same executor that launched + it"* (msg 18) — is, in his words, something that "should probably be stated in the Capy + docs up front." Because it isn't, Rainer read Capy as a restrictive framework and voted + reject; Alan spent 35% of the docs before seeing any mention of I/O. This is the + highest-leverage single fix. +- **[#10] Rationale is scattered** across dedicated `9.design/` pages (11 files), the + `A.specification-methods/` chapter (3 files), *and* inline prose. **Verified present.** + Alan's preference: interleaved admonitions. +- **[#11] "Part N" headings** — **verified:** `4e.cancellation.adoc` has Part 1 through + Part 9; "Part 4: Beyond Cancellation" is a single short section. +- **[#13, #14] Corosio navigation.** Reference is a flat alphabetical heap; **Quick Start + sits at nav line 48**, after Glossary and before Reference (Capy's is correctly at the + top, line 3). +- **[#19] `8i.echo-server-corosio` still lives in Capy examples** — **verified present.** + +### 3.2 Accuracy — is the documentation correct? + +- **[#3] `run_async` reference is wrong.** **Verified:** all ten overloads' briefs read + *"Asynchronously launch a lazy task on the given executor"* — but `run_async` receives + no task; it returns the launcher object. Alan flagged exactly this. +- **[#8] Examples that don't compile.** Rainer's reject hinged partly on the IoAwaitable + example (`capy#296`), which used `coroutine_handle<>` where a `continuation` was + required. **Verified fixed:** `4d.io-awaitable.adoc` now uses a `continuation cont_` + member and explains passing `continuation&` to `post`/`dispatch`. Alan's broader + "examples that don't compile" and Rainer's dangling-by-reference example pattern remain + open. +- **[#9] TLS is documented as not-ready and is not fail-safe.** Gennaro and Rainer both + flagged it; the maintainers agreed and are demoting the SSL implementation to `detail` + (msg 73) / wiring it to fail safely (msg 72). The HTTPS-client tutorial still calls + `set_default_verify_paths()` / `set_verify_mode(peer)` (lines 277-279) **without** the + warning that the TLS guide carries — **verified.** +- **[#17] UDP text.** **Verified:** `2g.udp.adoc:63-67` frames fragmentation purely as a + loss-amplification problem and advises small datagrams, but does not state that oversized + fragmented datagrams may be discarded regardless of loss (Rainer's safety point). +- **[#26] HALO blank render** — **verified fixed** in source (`2d.advanced.adoc:128`). + +### 3.3 Wording — technical-writing quality + +- **[#6] "AI fluff."** Alan's most emphatic theme, endorsed by Andrzej. Concrete patterns + he named: (i) the same point re-phrased repeatedly with no new information; (ii) + *unnecessary negatives* — "This is X. Not Y. Not Z" instead of "This is X"; (iii) + clichés/metaphors that force the reader to reverse-engineer them; (iv) expressions used + without definition (leaked from the authoring agent's context). He judged the docs + "orders of magnitude longer than they need to be." +- **[#15] Naming surfaced as doc-clarity confusion** — `execution_context` vs + `ExecutionContext` (case-only distinction), `executor_ref` vs the Boost `*_ref` + convention, and `buffer_length` vs `buffer_size`. (Design-adjacent, but every reviewer + hit it while reading the docs.) + +### 3.4 Completeness & Pedagogy — does it teach? + +- **[#5] Not goal-oriented.** The single most-repeated substantive complaint. Alan: the + `task` page has coroutines that return `task` but "no example where the task is + executed… a user following the documentation and compiling small examples as they learn + has nothing they can run." toast27: people want practical examples and "a lot of people + skip documentation." The IoAwaitable page is "full of implementation details… but not a + single example of a task running and benefiting from any of this." +- **[#12] Uneven depth.** Threads get a whole page with working examples; symmetric + transfer (far more consequential to Capy) gets two short paragraphs. +- **[#20, #21, #22] Andrzej's specification requests** (GitHub, pre- and mid-review): + a standard method for documenting awaitable-returning functions (`Await-effects` etc.), + the meaning of `error_code`/"on success" in `io_result`, and precise buffer/stream + concept specs. The new `A.specification-methods` chapter partially addresses the first + two; the buffer/stream issues (#273 #287 #297 #356) remain open. +- **[#16, #28, #29, #30]** executor-affinity documentation, callback-interop examples, + GUI integration example, platform-specific issues page. + +### 3.5 Presentation & Tooling — the doc system + +- **[#7] Reference cross-links.** Alan (both libraries): the exposition should use the + `cpp:` macro and the `antora-cpp-reference-extension` / `antora-cpp-tagfiles-extension` + so prose links into the generated reference instead of re-typing signatures. This is + **cheap, high-value, and structurally fixes the drift in #1.** +- **[#18] No right-rail ToC**, and several pages are long enough to need one. +- **[#13] Reference generation** — group by functionality; document operators with their + types, not as free "Friends"; separate sync from async. +- **[#27] Dark-mode contrast** (not verified locally; Antora theme). + +--- + +## 4. Already addressed — do **not** redo these + +| Finding | Evidence | +|---|---| +| IoAwaitable example bug (`capy#296`, Rainer's "Fatal Flaw") | `4d.io-awaitable.adoc` now uses `continuation` correctly | +| "Code snippets assume" note placement (`corosio#284`) | includes now inside `[NOTE]` in `3e.hash-server`, `3f.reconnect` | +| Signal/Resolver overview code-only (`corosio#285`) | `4i.signals.adoc`, `4j.resolver.adoc` now have prose intros | +| HALO "(Clang extension)" blank | attribute name present in `2d.advanced.adoc:128` | + +Partially addressed (started, not finished): TLS fail-safety (#9), awaitable/`io_result` +specification method (#20/#21), glossary terms (#23). + +> **Re-baseline note (2026-07-24):** The Corosio-scoped rows in this table (#9 TLS, +> "Code snippets assume" #25, Signal/Resolver overview #24) cite pages/commits that live in +> the Corosio repo, which is not checked out alongside Capy here — they are marked +> ⚪ deferred (Phase 5) in Section 2 pending re-verification against that repo, not because +> they regressed. The two Capy-side rows (IoAwaitable example, HALO blank render) were +> spot-verified and remain 🟢 fixed. + +--- + +## 5. Review of your three-category model (Structure / Accuracy / Wording) + +**Verdict: the three axes are correct and well-chosen for *prose*, but incomplete for a +documentation *site*. They cleanly hold roughly half of this review's findings; the other +half fall into two axes the model omits, and one cross-cutting concern.** + +What the model captures well: +- **Accuracy** and **Wording** are real and independently supported (the `run_async` + reference error; the "AI fluff" cluster). Neither is overstated. +- **Structure** captures the duplication and ordering complaints. + +Where it falls short — three gaps, in order of how loudly the review demanded them: + +1. **Completeness / Pedagogy is missing, and it was the #1 substantive theme.** The + loudest complaint — "looks complete but I didn't learn it," "no runnable example," + "exposition replicates the reference instead of teaching," "rationale assumed, not + explained" — is not about structure (nothing is mis-ordered), not accuracy (nothing is + *wrong*), and not wording (the sentences are fine). It is about whether the right + content *exists and teaches*. This needs its own axis. +2. **Presentation / Tooling is missing.** Reference cross-links, right-rail ToC, + dark-mode contrast, mrdocs reference organization, signature rendering — these are + properties of the doc *system*, orthogonal to prose. Several concrete findings live + only here. +3. **"Structure" conflates macro and micro.** The review's structural pain is almost all + *macro* (which pages exist, cross-page duplication, rationale scattered across three + places) rather than *within-page flow*. Worth naming both so a fix targets the right + level. + +One overstatement to guard against: treating **Wording as mere surface polish**. Alan and +Andrzej framed the "fluff" not as typos but as a *symptom of an authoring process that +drifts* — which is exactly why you want a style guide. Wording is where the drift becomes +visible, not where it originates. + +**Recommended categorization (five axes + one cross-cutting concern):** + +1. **Structure** — split into *macro* (page set, cross-page duplication, section order) + and *micro* (in-page flow). +2. **Accuracy** — explicitly include (a) **example-code correctness** (compiles, safe, no + dangling refs) and (b) **prose ↔ reference drift**. +3. **Wording / Style** — sentence-level technical-writing quality. +4. **Completeness / Pedagogy** — is the right content present, goal-oriented, and backed + by runnable examples and rationale? +5. **Presentation / Tooling** — rendering, cross-links, navigation chrome, reference + generation, theming. + +Cross-cutting: **Drift / Maintainability.** Nearly every reviewer's deepest worry (Alan +and Andrzej explicitly) is that the docs and reference *keep* diverging. This is a process +property, not a document property — and it is the reason a style guide is worth writing. +It should be a first-class lens: every rule in the guide should be justified by "does this +reduce drift?" + +--- + +## 6. Standards & tooling recommendations for an AI-followable style guide + +The goal you described — a guide an AI agent can follow to keep quality high and prevent +drift — is best served by combining a **structural framework**, a **prose style standard**, +and an **enforcement mechanism**. Recommendations, most-impactful first: + +1. **Diátaxis (structural framework) — adopt this first.** Diátaxis partitions + documentation into four modes with distinct purposes that must not be mixed: + *tutorials* (learning), *how-to guides* (tasks), *reference* (information), and + *explanation* (understanding/rationale). Nearly every structural finding in this report + is a textbook Diátaxis violation: tutorials that are really examples (#4), exposition + that replicates the reference (#1), rationale scattered instead of confined to + explanation (#10), a reference doing a tutorial's job (#5). Making mode boundaries + explicit is the single highest-leverage structural rule, and it is easy for an agent to + check ("which mode is this page? does its content match that mode?"). — https://diataxis.fr + +2. **A single-source-of-truth rule (directly kills the #1 finding).** Exposition must + **never restate reference signatures or concept definitions**; it links to the + generated reference via the `cpp:` macro and the + `antora-cpp-reference-extension` / `antora-cpp-tagfiles-extension` (which Alan + explicitly asked for). This is both a style rule and a tooling change, and it removes + the mechanism by which prose drifts from code. + +3. **A mainstream prose style guide as the base — Google or Microsoft.** Both the + [Google developer documentation style guide](https://developers.google.com/style) and + the [Microsoft Writing Style Guide](https://learn.microsoft.com/style-guide/) cover + voice, tone, task-orientation, terminology, and code formatting; both explicitly favor + *goal-oriented, task-based* writing (addresses #5), and both are heavily represented in + model training data, so an agent follows them reliably. Recommend adopting one wholesale + as the base and layering a short project-specific delta (C++/coroutine terminology, + Boost conventions). + +4. **ASD-STE100 (Simplified Technical English) — as a *pragmatic subset*, not verbatim.** + STE's core rules map precisely onto the "AI fluff" findings (#6): short sentences, one + idea per sentence, active voice, approved single-term-per-concept vocabulary, and a ban + on metaphor/wordiness. **Caveat:** strict STE was designed for aerospace maintenance + *procedures* — it bans most gerunds and restricts vocabulary in ways that suit how-to + steps and reference docstrings but actively harm conceptual tutorials and design + essays. Recommendation: apply an STE-derived subset **hard** in reference docstrings and + how-to steps, and **relaxed** (short-sentence/active-voice spirit only) in tutorials and + explanation. This matches how the review itself distinguished dense reference prose from + teaching prose. + +5. **A prose linter in CI — Vale — to make the guide *enforceable and AI-followable*.** + [Vale](https://vale.sh) encodes style rules (sentence length, passive voice, banned + words, one-term-per-concept, "unnecessary negative" patterns) as version-controlled YAML + and runs in CI. This is the concrete anti-drift mechanism: it turns the style guide from + a document an agent *might* follow into a gate that *fails the build* when prose drifts. + Vale ships importable Google and Microsoft styles as starting points; add custom rules + for the terminology table and the "don't restate the reference" heuristic where + detectable. (Alternatives: textlint, `write-good` — Vale is the most capable.) + +6. **A terminology table (one term per concept).** Several findings are terminology drift + (#15, and the launch/start/spawn/run family that recurred through the thread). A short + controlled-vocabulary table, enforced by Vale, prevents synonym drift and is trivial for + an agent to apply. + +7. **Worth knowing, not adopting wholesale:** ISO/IEC/IEEE **26514** (design & development + of user documentation) and **26515** (documentation in agile) are the formal standards + in this space. They are heavyweight and process-oriented; cite them for credibility if + needed, but Diátaxis + a mainstream style guide + Vale will deliver far more value per + unit effort. + +**Suggested stack:** *Diátaxis* (structure) + *Google or Microsoft style guide* (prose) + +*STE-derived subset for reference/how-to* (precision) + *single-source-of-truth linking +rule* (anti-drift) + *Vale in CI* (enforcement). That combination directly answers each +category of feedback in this report and gives an agent a checkable contract. + +--- + +## 7. Appendix — confidence rationale + +- **Highest confidence** goes to findings raised by ≥3 distinct reviewers *and* verified + still-open: #1 (Alan+Andrzej+Rainer), #3 (Rainer+Gennaro+Alan), #5 (Alan+toast27+Rainer). +- **Finding #2** is rated very high despite being partly one reviewer's framing because the + **library author independently reached the same diagnosis** (msg 50) and it is the + proximate cause of the only reject vote — author self-corroboration is strong signal. +- **Single-party findings** (#10–#30) are ranked lower on *signal* but several are ranked + up on *accuracy* because they are concrete and verified (e.g. #11 Part-headings, #14 + Quick Start position, #19 echo-server placement). Peter Turcan's items are single-party + but carry weight as professional technical-writing review. +- **Already-fixed items** (#24, #25, #26, and IoAwaitable within #8) are retained for the + record but should not consume rework effort. +- Note on double-counting: Andrzej's msg 67 quotes Alan's entire appendix and explicitly + *endorses* it. His agreement is counted as genuine second-party corroboration of Alan's + points, but not as independent discovery. \ No newline at end of file diff --git a/DOC_STYLE_GUIDE.md b/DOC_STYLE_GUIDE.md new file mode 100644 index 000000000..fbf3cfa15 --- /dev/null +++ b/DOC_STYLE_GUIDE.md @@ -0,0 +1,348 @@ +# Capy / Corosio Documentation Style Guide + +**Audience:** human editors and AI agents writing or editing the documentation. + +This guide is a **checkable contract**: every rule is phrased so an agent can apply it, and +most can be checked automatically. Enforcement falls into three tiers — a CI **gate** that +blocks a merge, a CI **warning** that flags candidates for a human to judge, or **review** +via the PR checklist when no tool can decide (see Part F for the per-rule mapping). It is +organized by five documentation axes — Structure, Accuracy, Wording, Completeness, +Presentation — plus the cross-cutting concern that motivates all of them: **drift**. + +> **The prime directive — prevent drift.** Prose and generated reference tend to diverge +> over time; hand-copied signatures and pasted examples rot silently. Every rule below +> exists to make the docs *self-correcting*: single-sourced, compiled, and linted. When a +> rule trades elegance for drift-resistance, drift-resistance wins. + +--- + +## Part A — Structure (Diátaxis) + +Follow **[Diátaxis](https://diataxis.fr)**. Every page is exactly one of four modes, and +**modes must not mix**: + +| Mode | Purpose | Answers | +|---|---|---| +| **Tutorial** | learning, by doing | "teach me" | +| **How-to** | a single task, start→finish | "how do I X?" | +| **Reference** | information, dry and complete | "what is the signature of X?" | +| **Explanation** | understanding, rationale | "why is it this way?" | + +**Rules:** +- **A1.** Each page declares its mode; an edit keeps content within that mode. +- **A2. Reference belongs in the reference.** Exposition pages never reproduce full + signatures or concept definitions — they *link* (Part B). This is the highest-value + structural rule. +- **A3. Rationale belongs in Explanation.** Use interleaved admonitions (`[NOTE]`/`[TIP]`) + for *local* rationale on a how-to/tutorial page; reserve dedicated design/explanation + pages for *cross-cutting* rationale. Do not run multiple parallel rationale channels for + the same material. +- **A4. One concept, one home.** Before adding a page, find where the concept already + lives. If two pages teach the same thing, merge them. +- **A5. Ordering follows dependency.** A page may not rely on a concept introduced only on + a *later* page. "Advanced" material comes after the basics it builds on. +- **A6. Quick-start / getting-started content sits near the top of the navigation**, not + buried near the reference. +- **A7. Headings describe content, not ceremony.** No numbered "Part N" mega-headings for + short sections; use plain descriptive headings. + +## Part B — Single-source-of-truth (anti-drift core) + +The Antora pipeline provides two mechanisms; use them instead of hand-authoring: + +- **B1. Never hand-type an API signature in prose.** Reference a symbol with the `cpp:` + macro so it links to the generated reference and cannot drift + (e.g. `cpp:boost::capy::run_async[]`). To describe what a function does, link it; do not + restate its declaration. +- **B2. Never paste example code.** Every code block is an `include::example$...[tag=...]` + of a compiled source file. New examples are written as compiled sources with tagged + regions, not typed into the page. +- **B3. Intentionally-non-compiling blocks are tagged**, not silently pasted: use a + pseudocode role for sketches/rejected designs and an external role for other-library + comparisons, so the compile gate knows to skip them. These two apply to a `[source,*]` + block only — role=pseudocode/external on a bare listing does nothing (B2 doesn't look for + them there). + A bare listing (`----` with no `[source,*]` attribute, or `....`) that holds literal + program output or a hand-drawn figure — never code — is tagged `[role=output]` or + `[role=figure]` respectively, so B2 does not mistake it for an unmarked code block. This is + the one constraint that makes the design safe, and it is load-bearing: **role=output/ + role=figure exempt only a bare listing, never a `[source,*]` block** — a block that + actually compiles is tagged `[source,*]` and cleared through pseudocode/external, full + stop, however output-shaped its content looks. Confusing the two would let `role=output` + launder real code past B2. `doc-lint.mjs`'s SHAPE check runs a content heuristic over every + role=output/role=figure block and reports (non-gated, advisory) any whose content looks + like code, so a wrong tag is not silently permanent — see the check's header comment. + Note `role=` on a bare listing emits a real CSS class (`class="listingblock output"` / + `...figure`) with no stylesheet rule behind it today; a future UI bundle that styles + `.output`/`.figure` will change how every such block renders, project-wide, in one step. +- **B4. A brief describes behavior, not identity — classes included.** A reference brief + says what the entity *does*, not what it *is*, and never restates its declaration or claims + parameters it does not take. This binds class briefs as much as function briefs. *(Reversal: + both documentation audits read identity-shaped class briefs — "A test utility for…", "Result + type for…", "Recycling memory resource with size-class buckets" — as house convention, + attested across 17+ headers, and on that reading dropped roughly 230 findings apiece. The + maintainer ruled that B4 binds them anyway: describing what a class *is* is not a licensed + house style, it is the defect B4 exists to catch. Treat both audits' B4 dispositions on class + briefs as superseded; this is what authorizes re-opening that finding set.)* + +## Part C — Wording (pragmatic Simplified Technical English) + +Apply an **ASD-STE100–derived subset**: STE's *spirit* — short sentences, active voice, one +idea per sentence, simple tense, one term per concept — **not** its strict word-ban. Enforce +**hard** in reference briefs and how-to steps; **relax** to spirit-only in tutorials and +design essays. + +- **C1. One idea per sentence.** Split compounds joined by "and/but/;/—". +- **C2. Length.** ≤ 20 words for instructions, ≤ 25 for descriptive text. Hard in API docs; + soft in essays. +- **C3. Active voice; name the actor.** "The task receives the executor," not "the executor + is received." +- **C4. Present simple.** Avoid needless "will" and "has been". +- **C5. No unnecessary negatives.** Write "This is X," not "This is X. Not Y. Not Z." +- **C6. No decorative figurative language.** At most one analogy per page, only when it + carries real explanatory weight. Cut clichés and metaphors the reader must + reverse-engineer. +- **C7. Define terms before use.** No expression enters the text without a definition or a + glossary link. +- **C8. Keep articles.** "the task", "a coroutine" — never drop *the/a* to save words. +- **C9. Plain words.** *use* (not utilize/leverage), *to* (not in order to), *before* (not + prior to), *because* (not due to the fact that); delete + *simply/basically/obviously/of course/note that*. +- **C10. One term per concept** (Part C.1). Never alternate synonyms. +- **C11. One documentation command per concept — docstring tags included.** Doxygen offers + both `@pre` and `@par Preconditions` for the same concept, a precondition. Use `@pre`; never + `@par Preconditions`. *(Placement: this is C10's "one term per concept" applied to command + choice rather than word choice, not a drift risk, so it sits in Part C rather than Part B — + neither tag can go stale relative to the code; they only differ in which markup an author + reaches for. It is deliberately not a C.1 table row: C.1 governs English words chosen while + writing prose, enforced by matching that prose after docstring extraction; these are Doxygen + commands consumed *by* the extractor itself, and the two do not even survive extraction in + the same shape — `@par Preconditions` re-emits as a bare "Preconditions" prose line, `@pre` + re-emits with no label at all — so a C.1-style substitution rule could not enforce this as + written. Evidence: the docstring corpus was split exactly 17/17 between the two forms when + this was ruled — a genuine tie, not two conventions living in different files; `thread_pool.hpp` + alone contains both (`@pre` once, `@par Preconditions` twice). There was no house rule to + preserve; the maintainer broke the tie in favor of `@pre`. Treat `@par Preconditions` as the + form to replace wherever a docstring is touched.)* + +### C.1 Terminology table (controlled vocabulary) + +Use the **Use** column everywhere; never the **Avoid** synonyms. API identifiers are +technical names and never change. *(This table is the one part of the guide expected to grow +as vocabulary is added — extend it rather than letting synonyms drift.)* + +| Concept | Use | Avoid | +|---|---|---| +| begin executing a coroutine | **start** | launch, spawn, fire off, kick off, run (verb) | +| the `co_await` operation | **await** | wait on, waiting for | +| value a coroutine yields at completion | **result** | return value (except naming the C++ type) | +| object that schedules work | **executor** | scheduler (reserve for P2300) | +| context owning threads/executors | **execution context** | context (bare), backend context | +| concrete I/O impl behind a type-erased type | **I/O backend** | backend, provider, engine | +| type that hides its concrete type | **type-erased** | erased, opaque, boxed | +| `stop_token`-based cancellation | **stop token** / **cancellation** | cancel token, cancellation token | +| callback passed to `run_async` | **completion handler** | handler (bare), callback | +| a `task` value | **task** | coroutine (the language feature), coro | +| the C++20 language feature | **coroutine** | coro, async function | +| awaitable satisfying `IoAwaitable` | **I/O awaitable** | awaitable (bare, when the concept is meant) | + +Approved technical names (need no paraphrase): coroutine, task, promise, awaiter, +awaitable, executor, execution context, strand, thread pool, allocator, frame, buffer, +buffer sequence, stream, stop token, sender, receiver, scheduler, mutex, event, waker. +Where a name here also appears in the Avoid column above, the table row governs: use +it only in the sense the row names. **scheduler** is approved only in its P2300 sense +(the `scheduler` concept); it is never a synonym for **executor**. + +## Part D — Completeness & Pedagogy + +- **D1. Goal-oriented, not syntax-first.** Open a concept with a use case ("you want to + X"), then introduce the machinery that achieves it. Do not enumerate syntax before + motivation. +- **D2. Every concept page has a runnable example** of the library's *own* type — not only + of the standard-library types it resembles. A page introducing a type shows that type in + use, actually running. *(Primer carve-out: the "library's own type" clause does not bind a + section that declares itself background material rather than a Capy concept page. + `doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc` through `3d.patterns.adoc` each + carry several runnable examples, all deliberately of standard-library types, because + `3.intro.adoc` frames the whole section as first-principles concurrency taught before Capy is + introduced, not as an introduction to a Capy type — this carve-out is what excuses `3a`–`3d` + from D2, not from having examples at all. `3.intro.adoc` itself has no `include::example$` of + any kind and is not covered by this carve-out; its D2 finding is a separate, already-baselined + gap (a prose-only introduction page), not evidence for "deliberately standard-library." Both + prior audits flagged `3a`–`3d`'s std-lib examples as a D2 violation and both were wrong to; it + does not recur, and this note exists only to stop a third pass from re-filing it. No page + changes follow from this carve-out.)* + *(Landing-page carve-out: every `*.intro.adoc` — all nine, `:page-mode: explanation` — is a + motivating essay plus a mechanical child list, never an introduction of a Capy type. `3.intro`'s + gap above is one instance of this general case, not a special case of its own. `doc-lint.mjs`'s + D2 check is scoped to `CONCEPT_DIRS` (a pedagogical category — "this chapter teaches + progressively") and deliberately does **not** read `:page-mode:` at all, so a page cannot leave + D2's scope by declaring a mode, correct or not — the six chapter-intro pages + (`2`–`7.intro.adoc`) stay in D2's scope and keep failing it for the same reason `3.intro.adoc` + always did: they introduce no type, so there is no example to add. That six-finding count is a + documented, intentional consequence of D2's own scope, not a regression to chase to zero by + adding decorative `include::example$` blocks to pages that don't need one.)* +- **D3. Every non-obvious design choice states its rationale** (or links to the explanation + page that does). "Because it is" is not documentation. +- **D4. Document thread-safety *and* executor affinity** at the class level where relevant. +- **D5. No unexplained qualifiers.** Hedges like "even on X" or "where available" either get + explained or get cut. + +## Part E — Presentation & Tooling + +- **E1.** Prose links to the reference via `cpp:` (Part B1) so a first-time reader can see a + type inline. +- **E2.** A right-rail table of contents is enabled; long pages are split at natural mode + boundaries. *(Review tier: verify by eye, do not gate.)* The ToC is switched on by the + `page-toc` attribute in `doc/antora.yml`, not by the theme. +- **E3.** The reference is grouped by functionality where the generator allows; operators are + documented with their types; asynchronous operations are distinguishable from synchronous. +- **E4.** The theme passes a contrast check in both light and dark mode. *(Review tier: the + gated failures were all `color-contrast` on shared Antora theme nav chrome — an external UI + bundle Capy cannot fix, the same rationale that demoted E2; scan runs non-blocking, verify by + eye, do not gate.)* + +## Part F — Enforcement (makes this guide checkable) + +The guide is only anti-drift if CI checks it. Add **[Vale](https://vale.sh)** and wire it +into the CI documentation job. + +### F.0 Enforcement tier by rule + +Not every rule is machine-checkable. Each rule sits in one of three tiers: + +- **Gate** — CI blocks the merge. Checked by Vale, the snippet-compile job, a small custom + AsciiDoc/nav lint script, or an accessibility scan. +- **Warning** — CI flags candidates, a human decides. Heuristic checks with real + false-positive/negative rates; never block on these. +- **Review** — no tool can judge; enforced by the PR checklist (F3). + +| Tier | Rules | +|---|---| +| **Gate** | A1, A6, A7, B2, B3, C2, C4, C9, C10, D2 | +| **Warning** | A2, B1, C1, C3, C5, C6, D4, D5, E1 | +| **Review** | A3, A4, A5, B4, C7, C8, C11, D1, D3, E2, E3, E4 | + +The accuracy gates (B2, B3, D2 correctness) are enforced by the snippet-compile job, not by +Vale — that job is what makes examples unable to drift. + +`doc/.vale.ini`: +```ini +StylesPath = .vale/styles +MinAlertLevel = warning +Packages = Google +[*.adoc] +BasedOnStyles = Vale, Google, Capy +; AsciiDoc source/callout blocks are code, not prose: +BlockIgnores = (?s) *(\[source.*?----.*?----) +TokenIgnores = (\x60[^\x60]+\x60) +``` + +`doc/.vale/styles/Capy/Terminology.yml` (enforces Part C.1): +```yaml +extends: substitution +message: "Use '%s' for one-term-per-concept consistency (style guide C.1)." +level: warning +ignorecase: false +swap: + '\b(launch|spawn|fire off|kick off)\b': start + '\bcancellation token\b': stop token + '\bcancel token\b': stop token + '\bboxed\b': type-erased +``` + +`doc/.vale/styles/Capy/NoFluff.yml` (enforces C5/C9): +```yaml +extends: existence +message: "Filler/fluff — delete or rewrite (style guide C5/C9): '%s'." +level: warning +ignorecase: true +tokens: + - simply + - basically + - essentially + - obviously + - of course + - note that + - in order to + - due to the fact that + - utilize +``` + +`doc/.vale/styles/Capy/SentenceLength.yml` (retired to `suggestion`; does not enforce C2 — +see F1): +```yaml +extends: occurrence +message: "Sentence over 25 words — split it (style guide C1/C2)." +level: suggestion +scope: sentence +token: \b(\w+)\b +max: 25 +``` + +- **F1.** CI runs `vale doc/modules` and fails on `error`-level findings, except **C2**: its + authority is `doc/lint/sentence-length.mjs` (`doc/lint/README.md`), hard on docstrings and + non-essay `.adoc` pages, advisory on `9.design/` and `A.specification-methods/`. + `Capy.SentenceLength` is `level: suggestion` and enforces nothing. +- **F2.** The snippet-compile job is the accuracy gate; keep every example sourced from a + compiled file (Part B2). +- **F3.** Doc PR checklist: mode declared (A1)? no hand-typed signatures (B1)? example + compiled (B2)? terminology clean (`vale`)? rationale present (D3)? + +### F4 — A check is not adopted until a planted violation has failed it + +**The rule: before promoting a rule to a gate — or believing a gate you just wired — plant a +violation of that exact rule and watch the check fail. A green run is not evidence.** Twelve times +during the documentation-improvement work a check looked healthy while checking less than it +appeared to, and every one of them read as a pass. Three, all recoverable from this repository's +history: + +- **A rule that could not match any input.** `Capy/PartHeadings.yml` (A7) was written + `scope: heading` with the pattern `^==+\s+Part\s+\d+`. Vale's heading scope hands the rule the + heading *text*, with the `==` markers already stripped, so the anchor guaranteed zero matches. + It reported clean over a corpus full of `== Part 3:` headings until `b54fe6c8` fixed it. +- **A gate spec that matched no fingerprint.** + `--gate 'vale_adoc:^(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$'` reports + `gated: true, gatedNew: 0` at exit 0, because the check name lives at the *tail* of a Vale + fingerprint and the leading `^` anchors to the file name. The un-anchored form fails on the same + input. Found twice, the second time by bite-testing rather than by reading. +- **A gated check that collapsed to zero without being marked skipped.** A crashing check emitted + no findings and reported `count: 0, skipped: false`; the comparator then computed *zero new + violations* from an empty current set and passed. Zero looks exactly like success. Both + comparators now carry an explicit fail-closed rule for it — a gated check with zero findings + against a non-empty baseline is fatal unless `--allow-emptied` names it, in + `doc/lint/baseline-diff.mjs` (reseed candidates) and in + `doc/lint/check-no-new-violations.mjs` (the blocking gate). The reachable case that motivated + the second one, and the reason it is whole-check rather than per-gate-regex, is recorded at the + rule itself: `cd doc && vale --output=JSON lint/.nonexistent-corpus` prints `{}` and exits 0. + `doc/lint/baseline.mjs` additionally checks `extract-docstrings.mjs`'s exit status, because the + docstring corpus is generated and the generator never clears its output directory, so a crashed + extractor used to leave a stale corpus that linted clean. + +The shared shape is that all three failures are **silent and reassuring**: the machinery reports +success, and the only way to distinguish "nothing is wrong" from "nothing is being checked" is to +introduce something wrong and confirm it is caught. `doc/lint/selftest.mjs` exists for the same +reason, and `doc/lint/README.md` records the fingerprint shapes a gate spec has to match. + +--- + +### How an agent uses this guide +1. Identify the page's Diátaxis mode; keep edits in-mode (A1). +2. Never type a signature or paste code — link (B1) or include a compiled snippet (B2). +3. Run Vale locally over both corpora, from `doc/`, before proposing the change, and fix all + `error`s. Vale must run from `doc/` with `node_modules/.bin` on `PATH` — this project's Vale + needs `asciidoctor` (the asciidoctor.js build under `node_modules`, not a Ruby install; there + is no Ruby on a stock dev machine here) to parse AsciiDoc, and without both of those it exits + 2 having printed nothing, which greps identical to a clean run and has already misled two + audit sub-agents this way: + ``` + cd doc && export PATH="$PWD/node_modules/.bin:$PATH" + vale --output=JSON modules + node lint/extract-docstrings.mjs && vale --output=JSON lint/.docstrings + ``` + A `0` in the output is not evidence of a clean run by itself — it is at least as often + evidence the run never happened (F.4's silent-and-reassuring failures are exactly this + shape). Confirm a non-zero total somewhere before trusting a zero. Vale does not enforce + C2 (sentence length) either way — its authority is `doc/lint/sentence-length.mjs`, not Vale + (F1); do not look to `vale`'s exit code for C2. +4. For every new claim, either link the rationale or add it (D3). \ No newline at end of file diff --git a/doc-prompts/README.md b/doc-prompts/README.md new file mode 100644 index 000000000..272c1927d --- /dev/null +++ b/doc-prompts/README.md @@ -0,0 +1,106 @@ +# Documentation Prompt Collection + +A collection of structured prompts (in the `tools-public` house style) that generate, +repair, and audit Capy/Corosio documentation. The prompts do the work a linter cannot: +they own the **judgment rules** — Diátaxis mode purity, duplication, objectiveness, +pedagogy, and code↔doc drift — while deterministic tooling owns the mechanical rules. + +**Two surfaces.** Documentation lives as exposition `.adoc` pages *and* as reference +docstrings in the headers (MrDocs generates the reference from them). Every tool applies the +five axes to both: `doc-audit` gains a reference mode (fixed Diátaxis mode = `reference`, +axes remapped to the docstring contract); `doc-write`/`doc-fix` accept a header `target_file` +and write/repair the docstring in the `.hpp`; `doc-sync` is the natural home for reference +drift — a changed symbol and its docstring share one diff, so the co-located docstring is +always its first drift hit. **A reference edit lands in the `.hpp`, never a generated page.** +Docstring conventions follow the `boost-docs` skill. + +## The two ends of the pipeline + +``` + GENERATE ─────────────────────────────► DETECT + doc-write doc-sync doc-audit + (new page to spec) (repair code-change (score existing + drift) pages on 5 axes) + │ │ + └──────► doc-fix ◄──────┘ + (apply grounded edits + from findings) +``` + +| Tool | End | Trigger | Output | +|---|---|---|---| +| **doc-write** | generate | a symbol/feature/topic to document | a new `.adoc` page + compiled snippet(s) + nav entry | +| **doc-sync** | detect + fix | a code change (diff / commit range) | edits that repair docs the change made stale | +| **doc-audit** | detect | existing or new pages | ranked findings on the five axes | +| **doc-fix** | fix | findings (from `doc-audit` or `doc-sync`) | a minimal, grounded patch set | + +`doc-sync` and `doc-audit` both hand their findings to `doc-fix` for repair, so the edit +contract lives in one place. + +## Shared rubric + +Every tool references one source of truth: the project **documentation style guide** +(`DOC_STYLE_GUIDE.md`). The style guide defines: + +- the **five axes** — **St**ructure, **Ac**curacy, **Wo**rding, **Co**mpleteness & Pedagogy, + **Pr**esentation & Tooling; +- the **Diátaxis** mode taxonomy (tutorial / how-to / reference / explanation); +- the **single-source-of-truth** rules (link the reference via `cpp:`; include compiled + snippets, never paste code); +- the **terminology table** (one term per concept). + +The tools cite the guide by section (e.g. "style guide C.1" for terminology) rather than +restating it, so the rubric never drifts from the guide. + +## Division of labor — what these tools do NOT do + +Deterministic tooling owns the mechanical rules and is a separate CI gate: + +- **Vale** — banned words, sentence length, terminology substitutions. +- **snippet-compile job** — every documentation code block compiles against the real API. +- **structural lint script** — mode-attribute presence, nav position, "no raw `[source]` + blocks", "every concept page has an `include::example$`". + +These prompts assume those gates exist and target only what they cannot check. + +## Shared invariants (every tool obeys) + +1. **Raw code and raw page prose never enter the main context.** Sub-agents read from disk; + the main context orchestrates over structured JSON records only. +2. **Every claim is grounded.** A statement about the API is backed by a verbatim quote of + the real declaration or reference; no claim is invented. +3. **Single source.** Code blocks are `include::example$…[tag=…]` of compiled sources, never + hand-typed. Signatures are `cpp:` links, never restated in prose. +4. **Noise floor.** A finding or edit without a verbatim span is discarded. Subjective + preference is not a finding. Doing nothing is a valid outcome. + +## Sub-agent dispatch contract + +Invariant 1 above is a promise; this section is the mechanism that keeps it, stated fully +inside this collection (no external tool or file is required to understand or run it). + +1. **Step 0 is deterministic and runs in the orchestrator** (what each tool calls "the main + context") — no LLM call. It only computes paths, an inventory, a change set, or a brief + from arguments and file **listings** (names, diff stats) — never file **contents**. +2. **One sub-agent per unit** — one page, one symbol, one doc hit, one finding, one edit. The + orchestrator's dispatch to that sub-agent carries only identifiers already produced by a + prior step: a `path`/`symbol`, and the prior step's typed JSON record. It never carries + file contents, because the orchestrator never held any to begin with. +3. **The sub-agent is the only actor that reads raw content.** It opens the file(s) itself, + from disk, using its own tools. Whatever it reads exists only inside that sub-agent's own + context — the orchestrator has no channel into it. +4. **The sub-agent's only return value is its step's typed JSON record**, validated against + that step's schema before the orchestrator accepts it. A record's only raw-text fields are + the short, capped verbatim spans the schema itself demands (`span`, `source_span`, + `evidence` — ≤ 200–300 chars, with the single stated C1/C2 exception) — never the + surrounding page, docstring, or diff hunk. +5. **The adversarial challenge/verify stage is a separate sub-agent dispatch**, not a + continuation of the authoring sub-agent's context. It receives the same kind of + identifier-plus-record input, re-reads the file from disk **independently**, and returns + its own typed verdict record. It never receives the first sub-agent's raw reading — only + its claim. +6. **Consequence:** the orchestrator's own context, across an entire run, contains nothing + but paths, counts, and validated JSON records carrying capped verbatim spans. There is no + step at which an orchestrator instruction says "read this file and show me its contents" — + every read happens inside a sub-agent whose one output channel is the schema. Raw code and + raw prose structurally cannot reach the orchestrator under this contract. diff --git a/doc-prompts/doc-audit.md b/doc-prompts/doc-audit.md new file mode 100644 index 000000000..91908495c --- /dev/null +++ b/doc-prompts/doc-audit.md @@ -0,0 +1,244 @@ +--- +description: Documentation audit against the five documentation axes +--- + +# Documentation Audit + +Audits Antora documentation pages **and** public-header docstrings against the five axes +defined in the documentation style guide — **Structure, Accuracy, Wording, Completeness & +Pedagogy, Presentation & Tooling**. Takes one or more `.adoc` pages (**exposition mode**, +Diátaxis-classified) or `include/boost/capy/**` headers (**reference mode**, Diátaxis mode +fixed to `reference`), and runs a structured analysis pipeline. Sub-agents read all prose or +docstrings and perform all scoring. The main context orchestrates, filters, and renders the +report. Raw page prose and raw docstring text never enter the main context. + +**Noise philosophy:** The tool goes out of its way not to find anything. Every finding must +justify its existence against a style-guide rule with a verbatim quoted span. The default +posture is: this page is fine until proven otherwise. A page that scores clean on all axes +is a valid and desirable outcome, not a failure of the tool. Subjective preference is not a +finding. + +\newpage + +```mermaid +flowchart TD + Paths --> Inventory + Inventory --> Classify + Classify --> Score + Score --> Challenge + Challenge --> Synthesize + Synthesize --> Report +``` + +\newpage + +--- + +## Core Rule + +Raw page prose or docstring text NEVER enters the main context. All reading and scoring +happen inside sub-agents; the main context receives only structured JSON records. A finding +without a verbatim quoted span is discarded. Non-negotiable. + +The five axes are the ONLY axes. The authoritative definitions live in the style guide; the +summaries below are for the sub-agent's convenience. Cite the specific rule (e.g. "C5", +"B1", "D2") in each finding. + +- **St — Structure.** Diátaxis mode purity (one mode per page); dependency-correct ordering; + no duplication of another page; no reproduction of reference signatures (style guide A, B). +- **Ac — Accuracy.** Every claim correct and verifiable against code/reference; no drift. + (Example *compilation* is a CI gate, not this tool.) +- **Wo — Wording.** Judgment-level prose rules a linter cannot check: undefined terms, + decorative metaphor/cliché, tone, unnecessary negatives (style guide C5–C7). +- **Co — Completeness & Pedagogy.** Goal-oriented not syntax-first; a concept page shows the + library's own type running; non-obvious choices state rationale; thread-safety/affinity + documented (style guide D). +- **Pr — Presentation & Tooling.** Prose links the reference via `cpp:` rather than restating + it (style guide E1). Nav/ToC/theme/reference-grouping are owned by the build, not here. + +**Reference mode remaps every axis to the docstring contract** (fixed Diátaxis mode = +`reference`); see "Reference Mode" below for the concrete per-axis rules. + +--- + +## Step 0 - Inventory + +Runs in main context. No LLM. Deterministic. + +**Input:** paths — files/directories under `doc/modules/ROOT/pages` (**exposition mode**), +or files/directories under `include/boost/capy/**` (**reference mode**). + +**Actions (exposition mode):** expand directories to `.adoc`; exclude partials (`_*.adoc`), +nav, generated reference. Attach `nav_position` and `declared_mode` (the `:page-mode:` +attribute, or null). + +**Actions (reference mode):** expand directories to headers; exclude `detail/`, `impl/`, +`experimental/detail/`, and any public declaration with **no** docstring at all (an +undocumented declaration is the MrDocs no-warnings gate's job, not this tool's — see "Not +this tool's job"). One entry per documented public declaration, keyed by its qualified +`symbol` and the verbatim Doxygen block immediately preceding it. + +**Output:** `DiscoveryResult` — `page_entries[]` of `{ path, unit_kind, nav_position, +declared_mode }` for `unit_kind=page`, or `{ path, unit_kind, symbol }` for +`unit_kind=docstring`. Inform the user: "[N] pages / [M] docstrings under audit." + +--- + +## Step 1 - Classify + +One sub-agent per page. Determine the page's true Diátaxis mode from content. + +**Reference mode (`unit_kind=docstring`) skips this step.** A docstring's Diátaxis mode is +fixed to `reference` by definition (style guide Part A) — set `inferred_mode=reference`, +`declared_mode=reference`, `mode_mismatch=false`, and go straight to Step 2. + +**Return:** `ClassifyRecord` + +- `path`: string +- `inferred_mode`: one of `tutorial`, `how-to`, `reference`, `explanation`, `mixed` +- `declared_mode`: string or null +- `mode_mismatch`: boolean — `true` only when `declared_mode` is **non-null** and disagrees + with `inferred_mode`, or `inferred_mode` is `mixed`. **A `null` `declared_mode` is never a + mismatch** — an undeclared `:page-mode:` is the deterministic lint script's gate (A1; see + README "Division of labor"), not this tool's judgment call, regardless of how many pages in + the target corpus currently declare one. (Capy's own corpus went from 1 of 65 pages declaring + `:page-mode:` to 65 of 65 over the course of this plan; the rule above did not change and must + not be re-tuned to a snapshot count — a corpus in either state defers presence-checking to A1, + never to this tool's judgment.) +- `topic`: string, **one sentence** — the concept the page teaches +- `approx_word_count`: integer + +**Validation:** reject invalid JSON, unknown enum, `topic` > 200 chars. If two pages share a +`topic`, flag a duplication candidate for Step 2. + +--- + +## Step 2 - Score + +One sub-agent per page. Score the five axes in a single read. + +**Input:** `ClassifyRecord` + any duplication-candidate paths. Sub-agent reads from disk. + +**Return:** `PageScore` + +- `path`: string +- `axes[]`: exactly five, one per `St`,`Ac`,`Wo`,`Co`,`Pr`, each: + - `axis`: one of `St`,`Ac`,`Wo`,`Co`,`Pr` + - `grade`: one of `clean`, `minor`, `major` + - `findings[]`: **at most 4 per axis**, each: + - `span`: **verbatim quote** (≤ 200 chars, **except** a `C1`/`C2` sentence-length finding, + whose `span` is the full offending sentence even past 200 chars — truncating a run-on + sentence deletes the clause that proves the violation). Required. + - `rule`: string — the style-guide rule id (e.g. `C5`, `D2`). + - `problem`: string, **one sentence**. + - `fix`: string, **one sentence** — the concrete edit. + - `confidence`: one of `high`, `medium`, `low`. +- `runnable_example_present`: boolean — feeds the `Co` grade for concept pages. **D2 scope:** + applies hard to tutorial/how-to concept pages (a type must be shown *actually running*, and + a claimed program output must trace to a compiled `main`/test harness — see the dry-run + finding below). For `explanation`/design-essay pages, compiled snippets that illustrate + mechanism (no claimed program output) satisfy the single-source rule without D2's stronger + "shown running" bar — do not force-fail an essay for lacking one. + +**Wording exemption:** text inside an attributed `[quote]` block or a `role=external`/ +`role=pseudocode` code block is not the page author's prose — exclude it from `Wo`-axis and +terminology checks (rewriting a citation to match house terminology misquotes the source). + +**Presentation scope (`Pr`/E1/B1):** flag a hand-typed **signature** (a restated parameter +list or return type) or a restated concept definition, not every backtick-quoted type name in +casual prose. (Confirmed against the corpus: `cpp:` is established house convention — 51 of +65 pages use it, 555 uses across 93 distinct targets. This is no longer an adoption backlog, +so the noise floor tightens: a bare backtick-quoted reference to a type or member that has a +resolvable `cpp:` target is in scope for a finding, same as a hand-typed signature. Casual +prose that names a concept with no corresponding reference target remains out of scope — +still don't flag every backtick-quoted word.) + +**Validation:** reject if any finding lacks a `span`; if a `span` is not verbatim in the +file; if > 4 findings per axis; if the axes are not exactly `St,Ac,Wo,Co,Pr`. + +### Reference mode — axis remap (docstrings) + +Fixed Diátaxis mode = `reference`. The five axes remap to the docstring contract (align with +the `boost-docs` skill's Doxygen conventions): + +- **St** — structural completeness: brief (implicit first sentence) present; no stray + `\`-commands mixed with `@`-commands; no paragraph stranded inside or after a `@par` block. + **No section-ordering rule** — retired: the corpus splits 65/26 in favor of `@par` *before* + the first `@param`/`@tparam`/`@return` (house convention is the "violating" form, 71/29), + and MrDocs 0.8.0 normalizes section order in the rendered output regardless of source order + (e.g. `task.hpp`'s `await_resume` writes `@return` before `@par Exception Safety`, but the + rendered page shows Exception Safety before Return Value) — so there is no stable target to + score source order against. +- **Ac** — docstring↔code accuracy: every actual parameter has a matching `@param`, same + name, same order; `@return` present iff the function returns non-`void`; `@throws` matches + what the code can actually throw (a `noexcept` function carries no `@throws`); a + `requires`/concept constraint on a template parameter is reflected in prose or `@par + Requires`. +- **Wo** — same STE-derived prose rules (C1–C10), scoped to the docstring's own sentences + (excluding `@code`/`@endcode`). +- **Co** — completeness & pedagogy remapped to docstring-contract completeness: `@param`/ + `@return`/`@throws` coverage; thread-safety documented (`@par Thread Safety`) where not + obviously single-threaded; the template constraint's *purpose* is explained, not only + stated; `@par Example` present where feasible. +- **Pr** — **MrDocs render check**: valid Doxygen command syntax (no unclosed `@code`, no + `@param` naming a parameter that does not exist); the brief describes behavior, not + identity, and does not restate the declaration (style guide B4). + +**Known gap — `Ac` has no lettered rule.** Unlike `St`, `Co`, and `Pr` above, reference-mode +`Ac` doesn't map onto any lettered clause in the style guide: Part A/B are written for +exposition-mode structure and briefs, not a per-axis accuracy contract for docstrings. Past +audits have cited `B4` (written for class-brief identity-vs-behavior, not factual accuracy) +to justify a plain factual-error finding under `Ac` — that's a stretch, not a real citation. +Until the style guide settles this (a possible Part B accuracy rule, or an explicit blessing +of the axis id as its own citation — out of scope here, see Task 1b), citing the bare axis id +(`Ac`) as a reference-mode finding's `rule` is acceptable. Do not discard an otherwise-valid +`Ac` finding for lacking a lettered citation the style guide does not currently provide. + +A docstring finding's `span` is a verbatim quote from the header (same 200-char rule and the +same C1/C2 exception as exposition mode). Reference-mode findings feed `doc-fix`/`doc-sync` +exactly like exposition findings — the repair still lands in the `.hpp`, never a generated +page (see `doc-fix`/`doc-write`). + +--- + +## Step 3 - Challenge + +One adversarial sub-agent per page with any `major` grade. Its job is to REFUTE findings. + +**Return:** `ChallengeRecord` — `verdicts[]` of `{ span, survives, reason }`. +**Rule:** default `survives=false` when uncertain. Main drops every non-surviving finding. + +--- + +## Step 4 - Synthesize + +Main context, surviving records only. Roll up per-page grades to an axis line +(`St:major Ac:clean Wo:minor Co:major Pr:minor`); rank pages by `3×major + minor`; merge +duplication candidates into one cross-page Structure finding. + +**Output — Report** (feeds `doc-fix` directly): + +``` +## Documentation Audit +| Page / Symbol | St | Ac | Wo | Co | Pr | Mode ok? | +|------|----|----|----|----|----|----------| + +### — St:major, Co:major +- **[St · A2]** "" — -> (high) +``` + +Reference-mode rows use `::` (e.g. `include/boost/capy/ex/run_async.hpp:: +run_async(Ex,H1)`) in the Page/Symbol column; `Mode ok?` is always `yes` (mode is fixed). + +End with: "[P] pages audited, [D] docstrings audited, [C] clean, [F] findings across the five +axes." + +--- + +## Not this tool's job + +Compilation (CI), mechanical prose lint and structural/nav checks (Vale + lint script), and +**rewriting** (`doc-fix` consumes this report). This tool judges only what a linter cannot. +For the reference surface: a public declaration with **no** docstring at all is the MrDocs +no-warnings gate's job (Task 2), not a finding here — this tool judges whether an *existing* +docstring is complete, accurate, and well-worded, not whether one exists. diff --git a/doc-prompts/doc-fix.md b/doc-prompts/doc-fix.md new file mode 100644 index 000000000..b64a959b2 --- /dev/null +++ b/doc-prompts/doc-fix.md @@ -0,0 +1,136 @@ +--- +description: Apply minimal, grounded repairs to documentation from a findings list +--- + +# Documentation Fix + +Consumes a findings list (from `doc-audit` or `doc-sync`, or a single page path) and produces +a minimal, grounded patch set. Each edit is tied to one finding and grounded in the real +code/reference. Sub-agents read and edit; the main context orchestrates over records. Raw +code and raw prose never enter the main context. + +**Noise philosophy:** The smallest edit that resolves the finding. Preserve the author's +voice and intent. Do not rewrite beyond the finding's span. If resolving a finding requires a +judgment the finding does not authorize (restructuring, changing an argument), escalate +instead of guessing. + +\newpage + +```mermaid +flowchart TD + Findings --> Intake + Intake --> Ground + Ground --> Edit + Edit --> Verify + Verify --> Patch +``` + +\newpage + +--- + +## Core Rule + +An edit is emitted only when (a) it is tied to a specific finding, (b) it changes only the +finding's span or its minimal enclosing block, and (c) any factual content is grounded in a +verbatim quote of the real code/reference. Code fixes edit the **compiled snippet source**, +not the page. Signature restatements are replaced with `cpp:` links (style guide B). Raw code +and prose never enter the main context. + +--- + +## Step 0 - Intake + +Runs in main context. Deterministic. + +**Input:** a findings array (the `doc-audit`/`doc-sync` report), or `{ page_path }` (in which +case run `doc-audit` on that page first). `page_path` may be an `.adoc` page **or** a header +(`include/boost/capy/**`) — a header target runs `doc-audit` reference mode. + +**Actions:** group findings by `path` (a header path groups its docstring findings same as a +page groups its prose findings); within a page, order by axis severity (`major` before +`minor`) and by document position. Drop findings without a `span`. + +**Output:** `FixQueue` — `pages[]` of `{ path, findings[] }`. + +--- + +## Step 1 - Ground + +One sub-agent per page. Reads the page and the real code/reference for each finding's span. + +**Return:** `GroundRecord` + +- `path`: string +- `items[]`: each `{ finding_ref, current_span, corrected_fact, source_span }` where + `source_span` is a **verbatim quote** of the code/reference that justifies the correction, + or `null` if the fix is purely stylistic (wording). + +**Validation:** reject any item whose `corrected_fact` is factual but has no `source_span`. + +--- + +## Step 2 - Edit + +One sub-agent per finding. Produces the concrete edit. + +**Return:** `Edit` + +- `finding_ref`: string +- `edit_kind`: one of `prose`, `link` (restated signature → `cpp:`), `snippet` (fix the + compiled source), `admonition` (move/insert rationale), `docstring` (repair a Doxygen block + in a header), `escalate`. +- `target_file`: string — the `.adoc` page; the snippet source for `edit_kind=snippet`; or the + `.hpp` for `edit_kind=docstring`. A reference-mode edit always lands in the header, never a + generated reference page. +- `before`: **verbatim** current text (≤ 300 chars). +- `after`: replacement text. Conforms to the style guide (C1–C10 for prose; B1/B2 for + links/snippets) for `target_file=.adoc`, or to the `boost-docs` skill's Doxygen conventions + (brief/`@param`/`@return`/`@throws`/thread-safety order) for `edit_kind=docstring`. +- `escalation_reason`: string or null — set when the fix needs unauthorized judgment. + +**Validation:** reject if `before` is not verbatim in `target_file`; reject if `after` +introduces a factual claim absent from Step 1's `source_span`. + +--- + +## Step 3 - Verify + +One adversarial sub-agent per edit. Confirms the edit resolves the finding without collateral. + +**Return:** `Verdict` — `{ finding_ref, resolves, introduces_new_claim, style_conformant, +accept }`. `accept=true` requires `resolves && !introduces_new_claim && style_conformant`. +Default to `accept=false` when uncertain. + +For `edit_kind=snippet`, `accept` additionally requires the edited source to **compile**. For +`edit_kind=docstring`, `accept` additionally requires the header to still compile and every +`@param` name to match the declaration, in order. + +--- + +## Step 4 - Patch + +Main context, accepted edits only. Emit an ordered patch set per file (unified-diff style), +then the escalations as a separate human-review list. + +**Output:** + +``` +## Documentation Fix — patch set +### (N edits) +- [St · A2] link: "run_async(ex)(task)" -> cpp:boost::capy::run_async[] +- [Wo · C5] prose: "" -> "" +### (N edits) +- [Ac · B4] docstring: "" -> "" +### Escalations (human judgment required) +- : +``` + +End with: "[E] edits across [P] pages, [S] escalations." + +--- + +## Not this tool's job + +Finding the problems (`doc-audit`/`doc-sync` do that); deciding structure or argument +(escalated); the mechanical prose lint pass (Vale) runs afterward and should come back clean. diff --git a/doc-prompts/doc-sync.md b/doc-prompts/doc-sync.md new file mode 100644 index 000000000..5af81c3b9 --- /dev/null +++ b/doc-prompts/doc-sync.md @@ -0,0 +1,164 @@ +--- +description: Repair documentation that a code change made stale, including silent drift +--- + +# Documentation Sync + +Given a code change (a diff or commit range), finds and repairs the documentation the change +made stale — **including drift that breaks no deterministic rule**. Vale still passes, the +structure is still valid, and any snippet that does not touch the changed API still compiles; +yet a prose description, a rationale, or a reference brief can now be silently wrong. This +tool keys off the *diff*, not off doc-internal rules, so it catches exactly that drift. +Sub-agents read code and docs; the main context orchestrates over records. Raw code and prose +never enter the main context. + +**Noise philosophy:** Only the changed surface can cause drift. Start from the diff and reach +outward. A change that touches nothing documented produces zero edits — a valid outcome. No +edit without a changed symbol and a stale doc span. + +\newpage + +```mermaid +flowchart TD + Diff --> ChangeSurface + ChangeSurface --> Locate + Locate --> AssessDrift + AssessDrift --> Repair + Repair --> Verify + Verify --> Synthesize +``` + +\newpage + +--- + +## Core Rule + +An edit is proposed only when tied to a specific changed symbol AND a specific documentation +span, and the correction is verifiable against the **new** code (quote the new declaration or +behavior). No speculative rewrites. Repairs follow the `doc-fix` edit contract (compiled +snippet sources for code; `cpp:` links for signatures; style guide for prose). Raw code and +prose never enter the main context. + +--- + +## Step 0 - Change surface + +Runs in main context. No LLM. Deterministic. + +**Input:** a diff or commit range (e.g. `git diff A..B` over the public headers). + +**Actions:** compute the changed **public** API surface — restrict to declarations under the +public include path; ignore `detail/` and tests. + +**Output:** `ChangeSet` — `changes[]` of: +- `symbol`: string (qualified) +- `change_kind`: one of `added`, `removed`, `renamed`, `signature_changed`, + `semantics_changed` (docstring/behavior changed but signature stable) +- `old_decl`, `new_decl`: strings (verbatim, or null for added/removed) + +Inform the user: "[K] public symbols changed." + +--- + +## Step 1 - Locate + +One sub-agent per changed symbol. Finds every documentation location that mentions or depends +on it — prose references, reference briefs, example sources, compiled snippets, and prose that +*assumes old behavior* without naming the symbol. + +**Drift-hit #1 is mandatory and unconditional.** Before searching anywhere else, the +sub-agent emits one hit for the changed symbol's **co-located docstring** — the Doxygen block +immediately preceding its declaration in the header — as `hit_kind=own_docstring`. This hit +is always produced, whether or not the diff touched the docstring's own text: the docstring +sits beside the symbol the diff just changed, so it is inspected on every run, and Step 2 +assesses it against the *new* declaration like any other hit. (This is the reference-mode +entry point for `doc-sync`: reference drift almost always surfaces here first, because a +changed signature or a newly added constraint is exactly the kind of thing an existing brief +or `@param` block silently stops matching.) + +**Return:** `DocHits` — `hits[]` of `{ symbol, path, span, hit_kind }` where `hit_kind` is one +of `own_docstring` (the co-located Doxygen block; always hit #1), `signature_mention`, +`prose_reference`, `example_use`, `rationale_dependency`, `xref`; `span` is a **verbatim +quote** from the doc. + +--- + +## Step 2 - Assess drift + +One sub-agent per hit. Compares the hit against the `new_decl` / new semantics. + +**Return:** `DriftRecord` + +- `path`, `symbol`, `span` +- `status`: one of `stale_signature`, `stale_behavior`, `stale_example`, `stale_rationale`, + `stale_xref`, `still_correct` +- `evidence`: **verbatim quote** of the new declaration/behavior that proves staleness +- `axis`: the style-guide axis the drift violates (usually `Ac`; `stale_rationale` may be + `Co`) + +**Validation:** reject `status != still_correct` without `evidence`. Drop `still_correct`. + +--- + +## Step 3 - Repair + +For every stale `DriftRecord`, produce an edit under the **`doc-fix` Step 2 contract** (same +`Edit` record: `edit_kind`, `target_file`, `before`, `after`, grounded in `evidence`). Code +drift fixes the compiled snippet source; signature drift *in exposition prose* becomes a +`cpp:` link; `stale_rationale` is flagged `escalate` (a changed *why* usually needs human +judgment). + +A stale `own_docstring` hit is repaired **in the header itself** — `edit_kind=docstring`, +`target_file` is the `.hpp`, `after` conforms to the `boost-docs` skill's Doxygen conventions +(brief/`@param`/`@return`/`@throws`/thread-safety, in that order). The reference edit lands +in the `.hpp`, never a generated page — the generated reference is a build artifact of the +docstring, not a document to edit directly. + +**Return:** `Edit[]` (as defined by `doc-fix`). + +--- + +## Step 4 - Verify + +One adversarial sub-agent per edit. Confirms the edit matches the **new** code and introduces +no claim the new code does not support. For snippet edits, the source must compile against the +new API. For `edit_kind=docstring`, `accept` additionally requires the header still compiles +and every `@param` name still matches the declaration. Default `accept=false` when uncertain. +(Same `Verdict` record as `doc-fix`.) + +--- + +## Step 5 - Synthesize + +Main context, accepted edits only. + +**Output:** + +``` +## Documentation Sync — +[K] symbols changed, [H] doc hits, [D] stale, [E] edits, [X] escalations. + +### (signature_changed) +- [Ac] own_docstring stale_signature: "" -> (accepted) +- [Ac] stale_signature: "" -> cpp:... (accepted) +- [Co] stale_rationale: "" — ESCALATE: behavior changed, rewrite the "why" +``` + +The `own_docstring` row is the co-located docstring hit (Step 1) — it always appears first +when the changed symbol's own brief/`@param` block no longer matches the new declaration. + +Escalations (renames touching many pages, changed rationale, removed symbols still taught) +are listed for human review, never auto-applied. + +--- + +## Not this tool's job + +Judging pages the change did not touch (`doc-audit` does that); applying edits beyond the +changed surface; the mechanical lint/compile gates, which run afterward as the backstop. + +## Intended use + +Run in the PR that changes a public header, or in a scheduled job over the merge range, so +docs cannot silently drift from code between releases. diff --git a/doc-prompts/doc-write.md b/doc-prompts/doc-write.md new file mode 100644 index 000000000..e4894a27b --- /dev/null +++ b/doc-prompts/doc-write.md @@ -0,0 +1,163 @@ +--- +description: Generate a new documentation page to spec, grounded in the real API +--- + +# Documentation Write + +Generates a new Antora documentation page (or a section) for a symbol, feature, or topic. +The page is written in the correct Diátaxis mode, conforms to the documentation style guide, +grounds every claim in the real code and reference, and sources every code block from a +compiled snippet. Sub-agents read the code and author the prose; the main context +orchestrates over structured records. Raw code never enters the main context. + +**Noise philosophy:** Write the minimum that teaches the reader the concept and lets them +run something. Do not pad. Do not restate the reference. If a fact is not in the code or the +reference, it is not written — it is flagged as a gap for a human. + +\newpage + +```mermaid +flowchart TD + Target --> Brief + Brief --> Ground + Ground --> Outline + Outline --> Snippet + Snippet --> Draft + Draft --> SelfCheck + SelfCheck --> Page +``` + +\newpage + +--- + +## Core Rule + +Every claim about the API is grounded in a verbatim quote of the real declaration, docstring, +or reference — no invented behavior. Every code block is an `include::example$…[tag=…]` of a +compiled source authored in Step 4, never hand-typed. Every signature mentioned in prose is a +`cpp:` link, never restated (style guide B). Raw code never enters the main context. + +--- + +## Step 0 - Brief + +Runs in main context. Deterministic. + +**Input:** `{ target, mode, audience, target_file }` — the symbol/feature/topic, the intended +Diátaxis mode (`tutorial`|`how-to`|`reference`|`explanation`), the reader assumed, and an +optional `target_file`. + +**Actions:** resolve `target` to its set of public symbols; determine the nav location and +the pages it must cross-link. **If `target_file` names a header** (`include/boost/capy/**`), +this is a **docstring write**, not a page write: `mode` is forced to `reference`, +`symbols[]` is the single declaration in `target_file`, and `nav_parent`/`related_pages[]` are +`null` (a docstring has no nav entry — Steps 2/4 skip nav-shaped output accordingly). + +**Output:** `Brief` — `{ target, mode, audience, symbols[], nav_parent, related_pages[], +target_file }` (`target_file` is `null` for a page write). + +--- + +## Step 1 - Ground + +One sub-agent. Reads the real declarations, docstrings, and existing reference for +`symbols[]`. Produces the fact sheet the page may draw on — nothing outside it may be +claimed. + +**Return:** `FactSheet` + +- `facts[]`: each `{ symbol, kind, signature_ref, behavior, preconditions[], errors[], + thread_safety, affinity, source_span }` where `source_span` is a **verbatim quote** of the + declaration/docstring that grounds `behavior`. +- `gaps[]`: strings — facts the reader needs that the code/reference does not state + (escalated to a human; never invented). + +**Validation:** reject any `fact` whose `behavior` is not supported by its `source_span`. + +--- + +## Step 2 - Outline + +One sub-agent. Produces a mode-appropriate outline; each section maps to `facts[]`. + +**Return:** `Outline` + +- `sections[]`: each `{ heading, purpose_one_sentence, fact_ids[], needs_runnable_example }` +- Mode contract: `tutorial`/`how-to` are goal-oriented (open with the use case, not the + syntax; style guide D1) and every concept section sets `needs_runnable_example=true`; + `reference` is complete and dry; `explanation` is argued and may carry rationale. **A + docstring write (`target_file` set) has exactly one section — the declaration itself — and + always sets `needs_runnable_example` from whether the symbol is example-worthy, not from + the mode contract above.** + +**Validation:** reject if any section maps to no fact; reject a `mixed` outline (one mode +per page, style guide A1). + +--- + +## Step 3 - Snippet + +One sub-agent + deterministic build. For each `needs_runnable_example`, first check for an +**existing** compiled snippet already covering the same fact (grep `test/doc/snippets/` and +`example/` for the symbol); reuse its `source_path`/`tag` rather than authoring a duplicate. +Otherwise author a new compiled source file with a tagged region under `example/` or +`test/doc/`, then compile it. + +**Return:** `Snippets` — `snippets[]` of `{ section_heading, source_path, tag, compiles }`. + +**Validation:** reject any snippet where `compiles=false`. A section that needs an example +but has no compiling snippet blocks the draft. + +--- + +## Step 4 - Draft + +One sub-agent. Writes the `.adoc` from the outline, fact sheet, and snippets — **or**, when +`target_file` is set, writes the Doxygen docstring block for the header instead. + +**Rules applied while drafting a page (style guide):** one idea per sentence, active voice, +simple tense (C1–C4); terminology table (C.1); `cpp:` links for every symbol (B1, E1); code +via `include::example$…[tag=…]` only (B2); rationale in interleaved admonitions (A3, D3); +`:page-mode:` attribute set (A1). + +**Rules applied while drafting a docstring** (`target_file` set; follow the `boost-docs` +skill): implicit one-sentence brief describing behavior, not identity (B4); section order +brief → description → `@param` (one per parameter, in declaration order) → `@return` (omit +for `void`) → `@par` blocks (Thread Safety, Complexity, Example, as applicable) → `@throws` → +`@see`; a `requires`/concept constraint on a template parameter is explained in prose, not +only named; match the local file's comment style (`/** */` vs Asio's `///` + `/** */`) — +never introduce a second convention into a file. **The edit lands in the `.hpp` — a docstring +write never produces or touches a generated reference page.** + +**Return:** `Draft` — `{ page_path, adoc_text, nav_entry }` for a page write, or +`{ target_file, symbol, docstring_text }` for a docstring write. + +--- + +## Step 5 - Self-check + +One adversarial sub-agent. Verifies the draft before it is emitted. + +**Return:** `CheckRecord` + +- `claim_checks[]`: each `{ span, grounded_by_fact_id, supported }` — every API claim in the + prose maps to a fact; `supported=false` marks a hallucination. +- `mode_pure`: boolean; `all_code_is_include`: boolean; `all_signatures_linked`: boolean. +- For a docstring write: `param_names_match`: boolean — every `@param` name matches the + actual parameter, in order; `brief_describes_behavior`: boolean — the brief is not a + restated declaration (B4). + +**Rule:** if any `supported=false`, or any boolean is false, the draft is returned to Step 4 +with the specific failures. Nothing is emitted until all pass. + +**Output:** the `.adoc` page, the compiled snippet source(s), the nav entry, and the `gaps[]` +list for human follow-up — or, for a docstring write, the edited `.hpp` and the `gaps[]` list. +Never a generated reference page. + +--- + +## Not this tool's job + +Deciding the *information architecture* across many pages (a human, or a structure pass, owns +mode assignment and nav shape); mechanical prose lint (Vale) runs afterward as the backstop. diff --git a/doc-rationale-classification.md b/doc-rationale-classification.md new file mode 100644 index 000000000..29bcb7d24 --- /dev/null +++ b/doc-rationale-classification.md @@ -0,0 +1,127 @@ +# Rationale Placement Classification (Style Guide A3) + +**Purpose:** Task 7 (finding #10) worked example. Classifies every page under +`doc/modules/ROOT/pages/9.design/` and `doc/modules/ROOT/pages/A.specification-methods/` +as *cross-cutting* (rationale stays on its dedicated Explanation page), *local* (rationale +belongs in an admonition on a specific how-to/tutorial page), or *landing* (nav-only intro +page, not audited). Method: `doc-prompts/doc-audit.md` Step-1 structure axis, applying +Style Guide A3 by hand (this classification is a manual application of the rule, not a +sub-agent JSON pipeline run). + +**Legend:** `verdict` — `cross-cutting` | `local` | `landing`. `home` — the how-to/tutorial +page the local rationale belongs on (only set when `verdict=local`). + +| page | verdict | home | reason | +|---|---|---|---| +| `9.design/9.intro.adoc` | landing | — | Nav-only landing page for the Design section; no prose to classify. | +| `9.design/9a.CapyLayering.adoc` | cross-cutting | — | Whole-library layering essay (concepts / type-erased wrappers / `task<>` type erasure / compilation-boundary economics / symmetric transfer). Spans every abstraction layer in the library; no single tutorial owns this scope. | +| `9.design/9b.Separation.adoc` | cross-cutting | — | Capy-vs-Corosio physical-design essay (Lakos levelization, CCD, Ousterhout deep modules). By definition cross-cutting — it argues about the boundary between two libraries, not one page's mechanism. | +| `9.design/9c.ReadStream.adoc` | cross-cutting | — | The "Design Foundations: Why a Full Buffer Is Always Success" rationale (full-buffer-is-success, EOF-as-error, canonical advance-then-check loop, the conforming-sources survey across TCP/TLS/HTTP/QUIC/compression/memory/mock streams) justifies a contract consumed by every concrete stream and by composed algorithms (`read`, `when_all`, `when_any`) — not local to one tutorial. *Note (out of scope for A3, flagged for a follow-on Structure/A2 pass): this page also restates definitions/semantics that `6.streams/6b.streams.adoc` already teaches — a duplication concern, not a rationale-placement one.* | +| `9.design/9f.WriteStream.adoc` | cross-cutting | — | Same reasoning as 9c: "Buffer Top-Up: Why `write_some` Can Outperform `write_now`" is the general throughput-vs-convenience trade-off behind the primitive, with no single how-to page that teaches `write_now`/buffer top-up elsewhere to serve as its "home." Same duplication note vs `6b.streams.adoc` as above, same out-of-scope caveat. | +| `9.design/9i.TypeEraseAwaitable.adoc` | cross-cutting | — | vtable-layout rationale (flat vs per-construct-ops, cache-line analysis) spans all `any_*` wrapper types (`any_read_stream`, `any_write_stream`, `any_read_source`, `any_buffer_source`, `any_buffer_sink`, `any_write_sink`). Genuine cross-cutting design essay. | +| `9.design/9k.Executor.adoc` | **mixed** — page overall cross-cutting; **one section was local** | `4.coroutines/4g.allocators.adoc` (`TLS Preservation`) | The page as a whole (Asio comparison, `dispatch`/`post`/`defer` rationale, `continuation`/`executor_ref` design, P2300 comparison) is a genuine cross-cutting Executor-concept essay and stays. Its **"Frame Allocator Preservation" section** (former lines 275–300: "The Save/Restore Protocol" + "Where It Applies") was local rationale about one specific mechanism (`safe_resume`'s TLS save/restore around `.resume()`) that is already taught as a how-to on `4g.allocators.adoc`'s "TLS Preservation" section — a duplicate parallel rationale channel for the same material, forbidden by A3. **This is the block moved in this task** (see below). | +| `9.design/9l.RunApi.adoc` | cross-cutting | — | Two-phase-invocation rationale, naming alternatives considered (builder pattern, single-call, named method), P4003/P2300 comparison. This *is* the dedicated home for `run`/`run_async` rationale — `9k.Executor.adoc` itself xrefs here rather than duplicating. Correctly placed already. | +| `9.design/9m.WhyNotCobalt.adoc` | cross-cutting | — | Whole-library comparison essay (11 sections: streams, type erasure, mock streams, threading, context propagation, cancellation, buffers, allocators, platform separation, coroutine overhead). Textbook cross-cutting. | +| `9.design/9n.WhyNotCobaltConcepts.adoc` | cross-cutting | — | Side-by-side design analysis of Capy's vs. Cobalt's write-stream abstraction (task requirements, context propagation, buffers, semantic specification, allocation, concept-vs-ABC). Comparative essay against another library — cross-cutting by construction. | +| `9.design/9o.WhyNotTMC.adoc` | cross-cutting | — | Whole-library comparison essay (Capy vs. TooManyCooks) helping readers choose between two libraries. Cross-cutting by construction. | +| `A.specification-methods/A.intro.adoc` | landing | — | Nav-only landing page ("Methods of API Description... in the following Reference section"); no rationale to classify. | +| `A.specification-methods/Ab.cancellation.adoc` | cross-cutting | — | Defines the term "supports IoAwaitable cancellation" used to describe conformance across the entire generated Reference section, not one tutorial's mechanism. Terminology backing many reference entries, not a single how-to page. | +| `A.specification-methods/Ac.contingencies.adoc` | cross-cutting | — | Defines "contingency" and the `io_result` destructuring convention (`[ec, n]`) used across every stream operation's specification in the Reference. Foundational vocabulary for the whole API-description methodology, not local to one page. | + +## Counts + +- Cross-cutting: 10 pages fully cross-cutting, plus 1 page (`9k.Executor.adoc`) cross-cutting + overall with exactly one local section. +- Local: **1 rationale block found** (within `9k.Executor.adoc`), moved in this task. +- Landing: 2 (`9.intro.adoc`, `A.intro.adoc`). + +Most `9x.WhyNot*`/comparison essays and the two `A.specification-methods` glossary-style +pages are cross-cutting, matching the brief's expectation. The corpus is overwhelmingly +already A3-compliant: `9.design`/`A.specification-methods` pages are, by and large, +legitimately dedicated Explanation pages rather than a channel duplicating some how-to +page's local mechanism. The one clear exception — `9k.Executor.adoc`'s "Frame Allocator +Preservation" section duplicating `4g.allocators.adoc`'s "TLS Preservation" section — is +the block relocated below. + +## The one move performed + +**Source:** `doc/modules/ROOT/pages/9.design/9k.Executor.adoc`, former "Frame Allocator +Preservation" section (heading + "The Save/Restore Protocol" + "Where It Applies" +subsections). + +**Destination:** `doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc`, inside the +existing "TLS Preservation" section, as a `[NOTE]` admonition block immediately after the +existing `safe_resume` usage paragraph. + +**What moved verbatim:** the `safe_resume` implementation code include +(`9k_executor.cpp[tag=safe_resume]` — the *definition*, distinct from `4g.allocators.adoc`'s +existing `4g_allocators.cpp[tag=safe_resume]` include, which shows *usage*), the TLS-stack +explanation and per-call cost sentence, and the two-call-sites-exempt list +(`symmetric_transfer`, `run_async_wrapper::operator()`) verbatim. + +**What was intentionally not carried over (and why):** two lead-in sentences from `9k` +("Capy propagates frame allocators via thread-local storage...", "If that user code +resumes a coroutine from a different chain...") and one summary sentence ("All executor +event loops and strand dispatch loops must use `safe_resume`...") were near-verbatim +restatements of sentences already present one paragraph above the insertion point on +`4g.allocators.adoc` (same page, adjacent). Pasting them again immediately below their own +twins would itself be the parallel-rationale-channel problem A3 forbids, just intra-page +instead of inter-page. Everything else moved is genuinely new information at the +destination. The `=== The Save/Restore Protocol` / `=== Where It Applies` sub-headings +were flattened to plain paragraphs because AsciiDoc section headings cannot nest inside a +delimited admonition block — a structural necessity, not a wording edit. + +**Xref stub left at the source:** `9k.Executor.adoc` now has, in place of the removed +section, a one-line pointer: +`NOTE: For the TLS save/restore protocol required around .resume() calls (safe_resume) -- +including which two call sites are deliberately exempt -- see +xref:../4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation].` +No page held an inbound xref to the old section (it had no `[[anchor]]`, and grepping the +whole `doc/` tree for `9k.Executor` turns up only `nav.adoc`'s whole-page link and +`9l.RunApi.adoc`'s and `9m.WhyNotCobalt.adoc`'s whole-page links elsewhere — none target +this section specifically), so the stub is precautionary for reader continuity, not a +required broken-link fix. + +**Source-page fragment risk:** none. `9k.Executor.adoc` was 332 lines before the edit and +remains a substantial, complete page (Definition, Relationship to Asio, dispatch/post +rationale, `continuation` design, nothrow-copy rationale, work-tracking, `executor_ref` +design, I/O completion pattern, P2300 comparison, Summary) — it is nowhere close to empty +or a fragment. + +## Verification + +**Build:** `cd doc && BOOST_SRC_DIR=/home/michael/git/boost npx antora --fetch +local-playbook.yml` — exit 0, zero matches for `broken`/`target of xref not found`, and the +moved-in `xref:../4.coroutines/4g.allocators.adoc#_tls_preservation[...]` resolves to a +real anchor in the rendered HTML (`build/site/capy/4.coroutines/4g.allocators.html`, +`

    `). + +**Vale:** `vale sync && vale --output=JSON` on the two touched pages, cross-checked with +`node doc/lint/check-no-new-violations.mjs` (the repo's own baseline-diff gate). Raw +line-fingerprint diff reports ~55 "new" findings on the two files, but a content-based +comparison (Check + Match text, ignoring line number) against the pre-edit versions shows +this is almost entirely a line-shift artifact of the line-based fingerprint scheme, not new +prose problems: + +- `9k.Executor.adoc`: **0 findings added** by content; 7 findings *removed* (the deleted + section's own pre-existing violations went away with it). +- `4g.allocators.adoc`: **0 findings added** by content, after a Fix-round-1 pass trimmed + the NOTE's intra-page duplication (see below) and swapped one leftover `--` for a comma. + An earlier draft of the moved NOTE briefly reintroduced 2 findings by content — 1× + `Google.EmDash` and 1× `Vale.Spelling` ("coroutine") — both pre-existing, massively + backlogged patterns already present dozens of times on this exact page and thousands of + times across the corpus (Vale's dictionary doesn't recognize "coroutine"; the codebase's + house style uses ASCII `--` for em dashes, which `Google.EmDash` doesn't recognize as + one). This was not a new class of problem — the same two already-backlogged violations + reappearing on relocated text — but Fix round 1 removed it anyway once the redundant + sentences it lived in were trimmed. +- One genuinely new finding was caught and fixed during this task: my own newly-authored + xref stub sentence on `9k.Executor.adoc` originally used `--` for a parenthetical, which + is a real net-new `Google.EmDash` occurrence (new authored text, not moved). Reworded to + use commas instead, eliminating it before commit. +- `vale_docstrings`, `doc_lint`, `mrdocs_warnings`, `a11y`: 0 new findings (unaffected — + no headers or nav/lint-script-checked structure touched). + +(Note: even a from-`develop`, unedited checkout reports 7 "new" `Vale.Spelling` findings on +`modules/ROOT/nav.adoc` under this same baseline-diff tool — confirmed by stashing this +task's edits and re-running. That drift is pre-existing and unrelated to this task.) diff --git a/doc-worklist.md b/doc-worklist.md new file mode 100644 index 000000000..a20412e2a --- /dev/null +++ b/doc-worklist.md @@ -0,0 +1,221 @@ +# Documentation Worklist + +**Purpose:** authoritative per-item task list for Phases 1–4 of `DOC_IMPROVEMENT_PLAN.md`. +Generated by the Phase 0 / Task 1 re-baseline (`docs: re-baseline review feedback against +develop`) against `DOC_REVIEW_FEEDBACK.md` Section 2. One row per finding that is still +**open** or **partial** after re-verification against current `develop`, plus every +**deferred** (Corosio) finding so Phase 5 has its list. Findings verified fully **done** +(#2, #12, #21, #26, #28) have no row here — see `DOC_REVIEW_FEEDBACK.md` for their evidence. + +**Legend** +- `surface` — `adoc` (exposition pages under `doc/modules/ROOT/pages/**`), `docstring` + (Doxygen comments in `include/boost/capy/**` headers, source for the MrDocs reference), + or `both`. +- `phase` — maps to `DOC_IMPROVEMENT_PLAN.md`: `1`=Structure, `2`=Accuracy, + `3`=Completeness/Pedagogy, `4`=Wording, `5`=Corosio (repeat of Phases 0–4 against the + Corosio repo, not available in this checkout). +- `owner` — `unassigned` for Phase 1–4 rows (picked up task-by-task per the plan); + `Phase 5` for Corosio-deferred rows (require the Corosio repo checked out). + +## Open / Partial findings (Capy — Phases 1–4) + +| finding# | library | surface | pages/headers | phase | owner | +|---|---|---|---|---|---| +| 15 | capy | docstring | **OUT OF PHASE 4 — maintainer API-naming decision, not a doc task (ruled during Phase-4 pre-flight).** `include/boost/capy/concept/execution_context.hpp:73` (`concept ExecutionContext`) vs `include/boost/capy/ex/execution_context.hpp` (`class execution_context`); `include/boost/capy/ex/executor_ref.hpp`; `include/boost/capy/buffers.hpp:397` (`buffer_size`) vs `:475` (`buffer_length`) — a doc-side change here would either document the inconsistency as intentional or silently pick a winner, so the row stays open pending a maintainer naming decision outside this plan. | 4 | unassigned | +| 18 | both | adoc | **DEFERRED — Review-tier (E2 reclassified Gate->Review in `6944132f`); verify-by-eye only, NOT actionable in this repo.** Verified in the built site (2026-07-28): long design pages (e.g. `9m.WhyNotCobalt.adoc`, 616 lines) render only the left page-tree nav, no right-rail per-page section ToC. `.toc-menu` styling ships in the shared boost-website UI bundle assets (`_/css/site.css`, `_/js/site.js`) but no page activates it — the right-rail ToC is a set-once theme format owned by that external UI bundle. | 1 | unassigned | + +## Deferred items found during Phase 4 (Capy — not review findings) + +Discovered while working Phase 4, out of its scope, and tracked here so they are not lost. +These have no `DOC_REVIEW_FEEDBACK.md` finding number. Every count re-measured at `620fdf2c` +(`cwd=doc`, `PATH` including `node_modules/.bin`, Vale 3.15.1). **P4-D6 was closed by the +Phase-4 final fix wave and removed — see the dated note at the end of this file.** + +| id | surface | what | count | why deferred | +|---|---|---|---|---| +| P4-D1 | both | Residual `Vale.Spelling` findings — bare C++ identifiers used as running prose that want backticks or a `cpp:` link. Re-derived on **both** surfaces at the Phase-4 final fix wave (`cwd=doc`, `PATH` incl. `node_modules/.bin`, Vale 3.15.1): **`.adoc` 73 alerts / 49 distinct terms / 28 pages** (top: `when_all` 5, `await_suspend` 3, `impl` 3, `io_context` 3, `run_async` 3, `when_any` 3) and **docstrings 308 alerts / 89 distinct terms / 58 headers** (top: `io_result` 32, `run_async` 23, `io_awaitable_promise_base` 19, `error_code` 18, `run_blocking` 10, `when_all` 10). The docstring figure includes the 22 alerts the `///` extractor extension made visible in this same wave; before it, the docstring surface measured 286 / 84 / 54. | **381** = adoc 73 + docstrings 308 | Style-Guide **B1** (code font / reference links), not one of the four C-rules Phase 4 gates; the plan's Task 11 names C1/C2/C5/C6/C9/C10. Mechanical backtick/`cpp:` work that belongs with the B1 family. Maintainer ruling, recorded with its count rather than absorbed into a wording task. **The row originally recorded only the `.adoc` half** (73, `surface: adoc`), leaving four fifths of the same defect class untracked; the plan's Global Constraints require both surfaces to get every axis, so it is now `both`. Note the docstring half needs a different remedy from the `.adoc` half: a header docstring cannot carry a `cpp:` macro (MrDocs escapes docstring punctuation, so `xref:`/`cpp:` arrive as literal text — see the ledger's P4-ds-A entry), so backticks are the only available fix there. | +| P4-D2 | comments | Genuine Avoid-column residuals in `test/doc/**` and `example/**` **comments**: 6 C10 (`test/doc/programs/4c_executors_executor_ref.cpp:51`, `test/doc/snippets/3d_patterns.cpp:83`, `test/doc/snippets/4e_cancellation.cpp:322`, `test/doc/snippets/9l_run_api.cpp:108`, `example/asio/any_stream.cpp:99`, `example/asio/use_capy_example.cpp:127`) + 2 C4 (`example/asio/api/uni_stream.hpp:199,226`). | **8** across 106 files | **No phase has ever scoped those comments.** None of the 8 is rendered into a page, which is why they were correctly out of scope. The argument for scoping them: `uni_stream.hpp:199,226` still read "at least one byte **has been** read/written" — the exact sentence P4-ds-C already fixed in the library headers (`io/any_read_stream.hpp:200`, `io/any_write_stream.hpp:201`, now "**is** read/written") — while `example/gui-integration/gui_integration.cpp:345` already carries post-C10 wording, so the two trees are measurably drifting in opposite directions. Any such sweep must also cover **string literals whose output is pasted into a page** (a comment-shaped grep cannot see them; `custom_executor.cpp:146` was one) and exclude by construction the 4 look-alikes: the two `4b.launching.adoc` file-path references and `example/hello-task/hello_task.cpp:29,31`'s `tag::launch[]`/`end::launch[]` include markers. | +| P4-D3 | adoc + build | **Nothing in CI couples the `== Output` blocks to a real run.** The example programs are not registered as ctest tests (`ctest -N` lists none) and no lint regenerates or compares those blocks, so a captured-output block can silently stop matching its program. | **14** pages carry an `== Output` block (all under `8.examples/`) | Structural and pre-existing; it became load-bearing when a Phase-4 fix had to regenerate one such block by hand-running the program. Recommended fix: register the examples as ctest tests and add a lint check that regenerates each block from a real run, turning the convention into a gate. | +| P4-D4 | both | Literal backticks leak into rendered HTML from spans whose closing delimiter is followed by a word character, e.g. `` `main`'s `` and — a different, worse shape — `` `co_await`s `` at `9k.Executor.adoc:253`, which mis-pairs the span and pulls following prose into code font. | **9** source spans on **6** `.adoc` pages (`2b.syntax:93`, `2d.advanced:31` ×3, `4g.allocators:56`, `5b.types:29` ×2, `7b.mock-streams:166`, `9k.Executor:253`) → **10** literal backticks rendered. Site-wide there are **22** literal backticks in rendered article bodies across **12** pages; the other 6 are generated **reference** pages, where some are legitimate (inside a rendered `@code` block, e.g. `read.html`) and at least one is a real docstring-side leak of the same class (`test/fuse.hpp:768`'s ``non-`io_context` ``, plus `quitter.hpp:333` and `ex/run_async.hpp:354`). | Each needs a prose edit (AsciiDoc has no way to make a constrained monospace span abut a word character), so it is per-instance rewording on **both** surfaces, not a config change. | +| P4-D5 | tooling | `Google.OxfordComma`'s **left anchor is a bare `,\s`**, which cannot distinguish a list comma from a sentence-initial adverbial comma. Any edit that changes where a sentence ends can therefore trip it — one did during Phase 4, and was reverted. | — | Not gated (`vale_adoc` gates only `Capy\.PartHeadings$` plus the three C-rules), so it can raise `newCount` but never fail CI. **A three-item guard would break the rule** — comma-plus-two-items is exactly the missing-Oxford-comma shape it exists to catch ("Apples, pears or bananas"). Fixing the left anchor is a real change to a Google-package rule and needs its own bite-tested fixture. Interim playbook note: re-run `Google.OxfordComma` after every sentence split. | + +## Deferred findings (Corosio — Phase 5) + +Corosio is not checked out alongside Capy in this workspace, so these cannot be +code-verified here. Surface is taken from the feedback text; paths are the Corosio-side +page names cited in `DOC_REVIEW_FEEDBACK.md` Section 3, unverified against that repo. + +| finding# | library | surface | pages/headers | phase | owner | +|---|---|---|---|---|---| +| 4 | corosio | adoc | `4.guide/4a.tcp-networking.adoc`, `4.guide/4b.concurrent-programming.adoc`, `2.networking-tutorial/*` (Corosio repo) | 5 | Phase 5 | +| 9 | corosio | adoc | HTTPS-client tutorial (lines ~277-279 per review), TLS guide page (Corosio repo) | 5 | Phase 5 | +| 13 | corosio | both | Reference generation config + nav (Corosio repo) | 5 | Phase 5 | +| 14 | corosio | adoc | `nav.adoc` (Quick Start entry, Corosio repo) | 5 | Phase 5 | +| 16 | corosio | docstring | executor-affinity class docs (Corosio repo) | 5 | Phase 5 | +| 17 | corosio | adoc | `2g.udp.adoc:63-67` (Corosio repo) | 5 | Phase 5 | +| 23 | corosio | adoc | Glossary page (Corosio repo) | 5 | Phase 5 | +| 24 | corosio | adoc | `4i.signals.adoc`, `4j.resolver.adoc` (Corosio repo; Section 4 of the feedback doc says fixed — re-verify against the Corosio repo before treating as closed) | 5 | Phase 5 | +| 25 | corosio | adoc | `3e.hash-server.adoc`, `3f.reconnect.adoc` (Corosio repo; Section 4 says fixed — re-verify against the Corosio repo before treating as closed) | 5 | Phase 5 | +| 27 | corosio | adoc | presentation/theme: Antora UI theme / dark-mode CSS (Corosio repo or shared UI bundle) | 5 | Phase 5 | +| 30 | corosio | adoc | Platform-specific issues page (does not yet exist; Corosio repo) | 5 | Phase 5 | + +- **2026-07-27:** Finding #19 resolved on the Capy side — `8i.echo-server-corosio.adoc` + and its nav entry were deleted (content preserved in git history at + `6944132fb5e20726c468f6b05783190a250b2e4c`). Compiled snippet source still to move to + Corosio's `example/` tree is `example/echo-server-corosio/` (left in place, unmodified). +- **2026-07-28 (Phase-1 close):** Reconciled the Open/Partial table to the true + post-Phase-1 state. Rows removed as **fully done** (Phase-1 findings now closed): + **#10** rationale consolidation (worked example moved + full classification; + `doc-rationale-classification.md` found only 1 local block, the rest cross-cutting), + **#11** Part-N heading flattening (commit `1c21187e`), **#19** echo-server relocate + (commit `daba154b`). Rows kept but re-marked: **#1 / #7** cpp: links — Phase-1 scope + complete, bare-mention long tail deferred to Phase 4 (partial, not closed); **#18** + right-rail ToC — Review-tier, verify-by-eye, not actionable in this repo (deferred). + Phase 2–4 rows (#3, #5, #6, #8, #15, #20, #22, #29) left unchanged. +- **2026-07-30 (Phase-3 close):** Reconciled the Open/Partial table to the true + post-Phase-3 state, same precedent as the Phase-1 entry above. Rows removed as + **fully done**: **#5** runnable page examples — `4a.tasks`, `4d.io-awaitable` and + `4g.frame-allocators` each gained a compiled snippet whose `main()` runs a `task` + (commits `b12065bf`, `0864c991`, `57e7aa5c`); **#20** await-contract triads — the + `@par Await-effects`/`Await-returns`/`Await-postcondition` pattern now covers + `when_all.hpp`, `when_any.hpp`, `task.hpp` and `quitter.hpp` (commit `df68f9bc`, + corrected by `ca5dca52` and `18e1c261`; reclassified from `phase = 2` to phase 3 by + controller ruling); **#29** GUI integration — `8q.gui-integration.adoc` plus the + compiled `example/gui-integration/` (commits `d482285f`, `9740adde`). Rows kept but + re-marked: **#1 / #7** cpp: links — Phase 3 closed the `7b.mock-streams` `test::` + tail via the `boost/capy/test.hpp` umbrella, but the bare-mention long tail is + still deferred to Phase 4 (partial, not closed). +- **2026-07-30:** `DOC_REVIEW_FEEDBACK.md`'s Status columns have not been touched + since `b4ba0daf` ("docs: re-baseline review feedback against develop") and are + stale for every finding closed in Phases 1–3. This file, not that one, is the + authoritative status for Phases 1–4. +- **2026-08-03 (Phase-4 pre-flight re-verification):** Supersedes the now-deleted + 2026-07-30 "not verified in this wave" note. Row **#22** re-verified as **fully + done** against current code and removed: commit `2cbd29ad` added the + await_suspend-only rationale to `IoAwaitable` + (`include/boost/capy/concept/io_awaitable.hpp:28-34,71-75`) and the `void*`/`void + const*` pointer-and-storage rationale to `mutable_buffer`/`const_buffer`'s + constructor and `data()` (`include/boost/capy/buffers.hpp:70-80,88-94` and + `:159-169,188-194`); no signature change, docs match code. Finding #22's parent + (`DOC_REVIEW_FEEDBACK.md:106`) lists five sub-points — buffer-handle lifetime, + `void*` rationale, `buffer_slice` naming/semiregular, "`Slice` concept + unnecessary", `IoAwaitable` looseness — and this row tracked only the two + docstring sub-points named in its own line anchors, so the row closes only + those two; the other three were checked and found substantively addressed + elsewhere, not overlooked: `include/boost/capy/buffers/buffer_slice.hpp:37-68` + documents the buffer-handle lifetime contract (`@par Lifetime`) and deletes the + rvalue overload that would otherwise dangle; no `concept Slice` exists anywhere + in `include/` (confirmed by grep), so there is no such concept to remove. + Row **#3**'s **docstring** leg re-verified as **fully done** and closed: + commit `cfa41dcb` rewrote all 18 `run_async` overload briefs (now e.g. "Bind an + executor to produce a launcher; invoke the launcher with a task to start it.", + `include/boost/capy/ex/run_async.hpp:514` et al.) and expanded the + `run_async_wrapper` `@warning` to cover all three defeat patterns — stored + wrapper, preconstructed task, wrapper-function forwarding — with a link to the + Frame Allocators guide (`include/boost/capy/ex/run_async.hpp:343-366`, guide + confirmed to exist at `doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc`). + **Finding #3 is NOT wholly closed**: it has an **adoc** leg (Rainer/Gennaro/Alan's + "warning lists too few dangerous cases; rationale/trade-offs not explained" per + `DOC_REVIEW_FEEDBACK.md:87`) that the docstring fix never reached — see row #3, + re-added with `surface = adoc` to track it rather than let it be rediscovered at + the Boost re-review. Row **#8** re-verified as **partial, kept, and reclassified + from phase = 2 to phase 4 by controller ruling** (its residual is Phase-4 wording + work, so a phase-4 sweep must be able to select it): the underlying hazard + (dangling-by-reference in the fan-out example, commit `289cf6bc`) was a real, + if latent, defect — nothing dangled in the snippet as written, but the commit + body correctly identifies that a caller passing a temporary would dangle — and + it is closed; the companion `read_all` reference parameter was correctly left + alone as the safe caller-keeps-stream-alive idiom — Phase 2's + genuine-bug-vs-safe-in-context investigation happened and reached the right + call — but the rationale for why one takes a reference and the other takes a + value exists only in the `289cf6bc` commit message, not on either adoc page; + see the row for the residual. Also recorded per Phase-4 pre-flight maintainer + ruling: row **#15** is out of Phase 4 (API-naming decision, not a doc fix); + rows **#1**/**#7**'s deferred bare-mention tail is confirmed as its own Phase-4 + task with a first-mention-per-page linking rule. +- **2026-08-10 (Phase-4 close):** Reconciled the Open/Partial table to the true + post-Phase-4 state, same precedent as the Phase-1 and Phase-3 entries above. + Rows removed as **fully done**: + **#1** and **#7** exposition-replicates-reference / `cpp:` link coverage — the + bare-mention long tail Phase 1 deferred is worked (commits `41ce7a19..a2b9eb72`, + 22 commits, **215 conversions across 44 pages**; `cpp:` macros in the tree + **342 → 557**, all 557 verified resolving to a real anchor whose text equals the + display label, **0 unresolved**; re-measured at close: 557 macros over 51 pages). + The linking rule was first-mention-per-page, as ruled. The only bare `test::`-spelled + occurrences left tree-wide are the 6 Cobalt `write_stream` mentions on + `9n.WhyNotCobaltConcepts.adoc` (L180, 251, 342, 397, 435, 458), which name + **Cobalt's** class and are correctly refused. + **#6** fluff/filler — discharged: `Capy.NoFluff` (C9) is **0** on the header + docstrings (commits `db8ac8cd..1b4c0e02`) and **1** on the `.adoc` pages + (commits `104f6f82..620fdf2c`), and that single residual is + `9.design/9k.Executor.adoc:125`'s "In order to", inside a **verbatim `[quote]` block + of P0913R1** — someone else's words, grandfathered by a `baseline.json` fingerprint + verified to describe that same sentence. Of the eleven pages the finding originally + named, ten carry no filler at all; the eleventh is `9k.Executor.adoc`, and its only + remaining hit is that quoted passage. + Rows kept, with the reason restated: **#3** — the **adoc leg is still open**; + `4b.launching.adoc`'s `[WARNING]` block still lists only the rvalue-qualified-wrapper + case, the one caught at compile time, and neither it nor `4g.allocators.adoc` carries + the two silent-failure cases that `run_async.hpp:343-366` documents. Its anchor is + corrected in this pass from `40-49` to **`40-52`** (the block delimiter opens at 40 and + closes at 52; the old anchor omitted "Always use the two-call pattern…"). + **#8** — still **partial**: the hazard is closed, but the residual is a wording note + explaining the by-value/by-reference asymmetry between `fan_out`'s `item` and + `read_all`'s `Stream&`, and **no page carries it**. + **#15** — stays open and stays **out of Phase 4**: it is a maintainer **API-naming + decision** (`ExecutionContext` vs `execution_context`, `buffer_size` vs + `buffer_length`), and a doc-side change would either document the inconsistency as + intentional or silently pick a winner. + **#18** — unchanged (Review-tier, not actionable in this repo). + Also added: a new **"Deferred items found during Phase 4"** section with six tracked + items (P4-D1…P4-D6), each with its measured count, for work Phase 4 surfaced but did + not own. + **Correction to the count carried in the Phase-4 handoff:** the `test/doc/**` + + `example/**` comment residual is **8**, not 9. The ninth + (`example/mock-stream-testing/mock_stream_testing.cpp:126`, "fuse will inject errors") + was fixed inside Phase 4 because it renders into `8d.mock-stream-testing`; re-derived + here with the documented exclusion filter over all 106 files — 6 C10 + 2 C4 = 8. +- **2026-08-10 (Phase-4 final fix wave):** Rows closed and removed, same precedent as the + Phase-1/Phase-3/Phase-4-close entries above. + **P4-D6** — CLOSED. The four `include/boost/capy/test/stream.hpp` docstrings now say the + **pair** is closed, not "the peer" / "the other end": `:49-50` ("the pair is automatically + closed. Any suspended reader on either end is resumed with `error::eof`"), `:235`, `:441` + and `:602`. Comment-only, proven mechanically (`gcc -fpreprocessed -dD -E -P` + + whitespace-strip sha256 identical to the pre-fix file, and the check bite-tested by + appending a declaration, which flips the hash). The page (`7b.mock-streams.adoc:185`) and + the in-code comment (`:141-143`) were already right, so nothing there changed. + **`doc/lint/extract-docstrings.mjs` now covers `///` doc comments** (86 published doc + lines across 25 headers that MrDocs does render — `io_result.hpp:49` appears on both + `reference/boost/capy/io_result.html` and `io_result/ec.html` — and that no gate could + see). Docstring corpus 69 → 73 files, Vale 296 → 321. **The 25 newly visible findings + are NOT fixed here** (they are wording work, outside a fix wave) and none is in a gated + rule: 22 `Vale.Spelling` (tracked by P4-D1 below, re-derived in this pass), 2 + `Google.OxfordComma` on `task.hpp`/`quitter.hpp`'s identical "The wrapped awaitable, + decayed and stored by value." — the P4-D5 left-anchor defect, comma plus a two-item + coordination — and 1 `Google.OptionalPlurals` on `detail/io_result_combinators.hpp`'s + `value(s)`, a rule that had no occurrence on either surface before. **C2 hard stays 2, + C2 advisory stays 67, and Capy.SimpleTense / NoFluff / Terminology stay 0 on the + docstring surface**, so the four gates Phase 4 promoted are unaffected. + **#3** — CLOSED, both legs. The docstring leg closed on 2026-08-03 (`cfa41dcb`); the + adoc leg closes here. `4b.launching.adoc`'s `[WARNING]` block now lists all three + patterns that split the two calls apart -- stored wrapper (the compile-time-caught one), + preconstructed task, and wrapper function -- says explicitly that the last two produce no + diagnostic, links `cpp:run_async_wrapper[]` for the per-pattern detail, and xrefs + `4g.allocators.adoc` for the {cpp}17-evaluation-order rationale. No signature is typed + out and no code is pasted (B1/B2); the one `role=pseudocode` block is the pre-existing + compile-error illustration, kept and relabelled as the first pattern. + **`4g.allocators.adoc` gets a two-sentence pointer, not the list** (chosen deliberately): + it is the rationale page and already owns the timing constraint and the two-call + explanation, so duplicating the defeat list there would give two places to keep in sync. + The pointer sits directly under its two-call snippet, where a reader who arrives for the + rationale meets it. + **#8** — CLOSED. The by-value/by-reference asymmetry is now explained on **both** pages: + `4f.composition.adoc` (after the `fan_out` snippet) and `5c.sequences.adoc` (after the + `consuming_buffers` bullets). Both state the rule -- a coroutine reads its parameters + when its body runs, so a task that is stored and awaited later outlives its call + expression -- and both cross-link the other page's case. The hazard is stated as + **latent**, matching `289cf6bc`'s own commit body: in the snippet as written nothing + dangles because `items` outlives the `when_all`; the hazard is what a caller passing a + temporary would create. + Both notes are Vale-clean: the `.adoc` surface is unmoved at **97** across all ten rules + with zero new findings on any of the four edited pages, and C2 stays hard 0 / advisory 67. diff --git a/doc-xref-gaps.md b/doc-xref-gaps.md new file mode 100644 index 000000000..74d2306b3 --- /dev/null +++ b/doc-xref-gaps.md @@ -0,0 +1,216 @@ +# Doc `cpp:` / reference-link coverage audit (finding #7) + +Phase 0, Task 3 of `DOC_IMPROVEMENT_PLAN.md`. This is an **audit only** — no +exposition pages are fixed here. It measures, per page, how many hand-typed +API mentions in prose *should* become `cpp:` reference-macro links (provided +by `@cppalliance/antora-cpp-tagfiles-extension`, configured in +`doc/local-playbook.yml` with `using-namespaces: [boost::]`, e.g. +`cpp:run_async[]`). The output feeds Phase 1 prioritization. + +**Current state confirmed:** the 65 exposition pages under +`doc/modules/ROOT/pages/**` use **zero** `cpp:` macro links today (`grep -rl +'cpp:' doc/modules/ROOT/pages` → no hits). All API mentions in prose are +hand-typed backtick spans. The gaps below are therefore the full backlog for +Phase 1, not incremental noise. + +## Method + +1. Built a symbol list (54 names) from public headers under + `include/boost/capy/**` (excluding `detail/` and example-placeholder names + like `MyReadable`/`my_task` that appear only inside header doc-comments), + plus the brief's example symbols (`run_async`, `task`, `io_task`, + `thread_pool`, `strand`). Full list: see "Symbol list" below. +2. Scanned all 65 `.adoc` pages line-by-line, **excluding**: + - content inside `[source...] ---- ... ----` listing/output blocks + (covers `include::example$...[]` snippets, inline pseudocode blocks, + and `*Output:*` blocks — these are compiled/verbatim, not prose), + - heading lines (`=`, `==`, ...), + - backtick spans that are header/file-path references (e.g. + `` `` ``) — those aren't symbol-in-prose + mentions needing a `cpp:` link. +3. For each remaining backtick span `` `...` `` containing a symbol from the + list, classified it as: + - **symbol-mention gap** — a bare (or near-bare) symbol name used as a + noun in a sentence, e.g. `` `run_async` ``, `` `task` ``. Linkable + as-is via `cpp:symbol[]`. + - **signature-restatement gap** — a full or partial declaration/call + retyped in prose, e.g. + `` `explicit read_stream(fuse f = {}, std::size_t max_read_size = std::size_t(-1))` `` + or `` `run_async(executor, allocator)(my_task())` ``. These are the + worst Style Guide B1 offenders — Phase 1 should replace them with a + `cpp:` link plus, where useful, a real compiled example rather than a + hand-typed signature. + - Classification heuristic: spans containing `(`/`)`, `->`, or an + embedded declaration pattern (`type name(`) were bucketed as + signature-restatement; bare identifiers (optionally with a single + `<...>` template argument or `::` qualification) were bucketed as + symbol-mention. This is a pragmatic heuristic, not a parser — see + "Known imprecision" below. + +Grep used to build/cross-check the symbol source list (public headers, +non-`detail`): + +``` +find include/boost/capy -type f -name "*.hpp" | grep -v '/detail/' +grep -rhE '^[[:space:]]*(class|struct|concept)[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' \ + include/boost/capy/**/*.hpp include/boost/capy/*.hpp +``` + +Cross-check against the brief's sample grep (all matches are a subset of the +table below, confirming no page was missed): + +``` +grep -rnE '\b(run_async|task<|io_task|thread_pool|strand)\b' doc/modules/ROOT/pages | grep -v 'cpp:' +``` + +Symbol list used (54 names): `run_async`, `task`, `io_task`, `thread_pool`, +`strand`, `executor`, `executor_ref`, `any_executor`, `io_context`, +`execution_context`, `buffer`, `const_buffer`, `mutable_buffer`, +`buffer_param`, `buffer_slice`, `consuming_buffers`, `buffer_copy`, +`make_buffer`, `read_stream`, `write_stream`, `any_read_stream`, +`any_write_stream`, `any_stream`, `ReadStream`, `WriteStream`, `Stream`, +`Executor`, `ExecutionContext`, `IoAwaitable`, `IoAwaitableRange`, +`IoRunnable`, `ConstBufferSequence`, `MutableBufferSequence`, `io_result`, +`io_env`, `continuation`, `quitter`, `async_mutex`, `async_event`, +`async_waker`, `frame_allocator`, `frame_alloc_mixin`, +`recycling_memory_resource`, `work_guard`, `when_all`, `when_any`, +`read_at_least`, `write_at_least`, `this_coro`, `immediate`, `cond`, `error`, +`write_now`, `system_context`. + +## Step 2 proof: the `cpp:` macro resolves + +To confirm the mechanism actually works before recommending it for Phase 1, +one hand-typed mention was converted, built, and checked — then reverted +(this is the only edit made during the audit; it is **not** part of the +committed change). + +- **Page:** `doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc`, line 137 +- **Change:** `` `run_async` `` → `cpp:run_async[]` +- **Build command:** `cd doc && BOOST_SRC_DIR=/home/michael/git/boost npx antora --fetch local-playbook.yml` (output: `doc/build/site`, gitignored) +- **Result:** build succeeded (exit 0). Rendered HTML at + `doc/build/site/capy/4.coroutines/4a.tasks.html` contains: + + ```html + run_async + ``` + + and the target file `doc/build/site/capy/reference/boost/capy/run_async-0e.html` + exists in the built site (the MrDocs-generated reference page for the + `run_async` overload set). **PASS.** +- **Revert:** `git checkout -- doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc`; + confirmed `git diff HEAD -- ` is empty afterward. + +## Known imprecision + +- The mention/signature split is a heuristic (paren/arrow detection), not a + C++ parser. Spot-checked the top offenders (`why-capy.adoc`, + `9m.WhyNotCobalt.adoc`, `9k.Executor.adoc`, `7b.mock-streams.adoc`, + `9l.RunApi.adoc`, `4h.lambda-captures.adoc`) by hand; classifications held + up (e.g. `` `explicit read_stream(fuse f = {}, ...)` `` correctly bucketed + as signature-restatement, `` `ReadStream` `` correctly bucketed as + mention). A handful of borderline calls remain, e.g. usage-syntax spans + like `` `run_async(executor)(task)` `` are counted as + signature-restatement even though they show call syntax rather than a + formal declaration — Phase 1 should treat these as "convert the callable + name to `cpp:`, keep or move the demonstrative syntax to a compiled + example" rather than assuming a literal signature needs deleting. +- The 9.design pages (`WhyNotCobalt*`, `Executor`, `RunApi`, `WriteStream`, + `ReadStream`, `TypeEraseAwaitable`, `CapyLayering`) and `why-capy.adoc` are + narrative/rationale pages that reference many symbols densely in + comparison prose — their high counts are real, not an artifact of the + symbol list. +- Pages with 0 gaps (`2.cpp20-coroutines/*`, `3.concurrency/{.intro,3a,3b,3c}`, + and the various `*.intro.adoc` section landing pages) were manually spot + checked: they either discuss generic C++20 coroutine mechanics + (`co_await`, `promise_type`) that have no Capy public-API symbol to link, + or are short landing/TOC pages with no prose signatures. +- Not every conceivable Capy public symbol is in the 54-name list (e.g. some + narrow buffer/concept helpers like `buffer_archetype`, + `decomposes_to`), per the task's "pragmatic, not a perfect census" + scoping. Re-running with an expanded list would likely raise a few counts + slightly on buffer-heavy pages (`5.buffers/*`) but is unlikely to change + the ranking of the top offenders. + +## Per-page gap counts + +| Page | Symbol-mention gaps | Signature-restatement gaps | Total | Notes / worst offenders | +|---|---|---|---|---| +| `why-capy.adoc` | 64 | 1 | 65 | worst: `run_async(executor)(my_task())` (L158) | +| `9.design/9n.WhyNotCobaltConcepts.adoc` | 49 | 0 | 49 | e.g. `IoAwaitable` | +| `9.design/9m.WhyNotCobalt.adoc` | 46 | 2 | 48 | worst: `run_async(executor, allocator)(my_task())` (L394) | +| `9.design/9k.Executor.adoc` | 41 | 1 | 42 | worst: `run_async(ex, alloc)(my_task())` (L315) | +| `9.design/9f.WriteStream.adoc` | 30 | 0 | 30 | e.g. `WriteStream` | +| `4.coroutines/4f.composition.adoc` | 26 | 1 | 27 | worst: `task>` (L28) | +| `4.coroutines/4d.io-awaitable.adoc` | 22 | 3 | 25 | worst: `env->executor` (L48) | +| `7.testing/7b.mock-streams.adoc` | 16 | 9 | 25 | worst: `read_stream rs(f)` (L25); also `explicit read_stream(fuse f = {}, std::size_t max_read_size = std::size_t(-1))` (L60) | +| `5.buffers/5b.types.adoc` | 24 | 0 | 24 | e.g. `const_buffer` | +| `9.design/9a.CapyLayering.adoc` | 21 | 0 | 21 | e.g. `ReadStream` | +| `9.design/9c.ReadStream.adoc` | 18 | 1 | 19 | worst: `read(stream, buffer(buf, 100))` (L155) | +| `9.design/9l.RunApi.adoc` | 10 | 9 | 19 | worst: `f(context)(task)` (L5); also `io_context::run()` (L112) | +| `5.buffers/5c.sequences.adoc` | 15 | 2 | 17 | worst: `buffer_slice(seq, offset, length)` (L89) | +| `4.coroutines/4e.cancellation.adoc` | 11 | 5 | 16 | worst: `task::handle()` (L169) | +| `9.design/9i.TypeEraseAwaitable.adoc` | 15 | 1 | 16 | worst: `io_result>` (L92) | +| `9.design/9o.WhyNotTMC.adoc` | 13 | 3 | 16 | worst: `run_async(ex, allocator)` (L140) | +| `index.adoc` | 16 | 0 | 16 | e.g. `IoAwaitable` | +| `7.testing/7a.drivers.adoc` | 15 | 0 | 15 | e.g. `error::canceled` | +| `8.examples/8f.timeout-cancellation.adoc` | 11 | 3 | 14 | worst: `async_waker::wait()` (L86) | +| `4.coroutines/4b.launching.adoc` | 11 | 2 | 13 | worst: `run_async(executor)(task)` (L30) | +| `4.coroutines/4g.allocators.adoc` | 13 | 0 | 13 | e.g. `run_async` | +| `4.coroutines/4a.tasks.adoc` | 12 | 0 | 12 | e.g. `task` | +| `4.coroutines/4c.executors.adoc` | 11 | 1 | 12 | worst: `run_async(ex)` (L32) | +| `6.streams/6a.overview.adoc` | 12 | 0 | 12 | e.g. `ReadStream` | +| `6.streams/6b.streams.adoc` | 11 | 0 | 11 | e.g. `ReadStream` | +| `5.buffers/5a.overview.adoc` | 10 | 0 | 10 | e.g. `ConstBufferSequence` | +| `8.examples/8a.hello-task.adoc` | 9 | 1 | 10 | worst: `run_async(pool.get_executor())` (L64) | +| `8.examples/8g.parallel-fetch.adoc` | 9 | 0 | 9 | e.g. `when_all` | +| `8.examples/8l.async-mutex.adoc` | 8 | 0 | 8 | e.g. `async_mutex` | +| `8.examples/8n.custom-executor.adoc` | 8 | 0 | 8 | e.g. `Executor` | +| `9.design/9b.Separation.adoc` | 8 | 0 | 8 | e.g. `when_all` | +| `7.testing/7e.buffer-inspection.adoc` | 7 | 0 | 7 | e.g. `ConstBufferSequence` | +| `8.examples/8b.producer-consumer.adoc` | 7 | 0 | 7 | e.g. `async_event` | +| `8.examples/8c.buffer-composition.adoc` | 7 | 0 | 7 | e.g. `std::array` | +| `8.examples/8k.strand-serialization.adoc` | 7 | 0 | 7 | e.g. `strand` | +| `3.concurrency/3d.patterns.adoc` | 6 | 0 | 6 | e.g. `thread_pool` | +| `8.examples/8d.mock-stream-testing.adoc` | 6 | 0 | 6 | e.g. `test::read_stream` | +| `5.buffers/5e.algorithms.adoc` | 5 | 0 | 5 | e.g. `ConstBufferSequence` | +| `8.examples/8m.parallel-tasks.adoc` | 5 | 0 | 5 | e.g. `thread_pool` | +| `8.examples/8o.sender-bridge.adoc` | 5 | 0 | 5 | e.g. `io_result` | +| `4.coroutines/4h.lambda-captures.adoc` | 0 | 4 | 4 | worst: `[x]() -> task<> { use(x); }()` (L100) — all 4 are lambda/task usage snippets, not classic signatures | +| `A.specification-methods/Ab.cancellation.adoc` | 4 | 0 | 4 | e.g. `IoAwaitable` | +| `6.streams/6f.isolation.adoc` | 3 | 0 | 3 | e.g. `any_stream` | +| `8.examples/8e.type-erased-echo.adoc` | 3 | 0 | 3 | e.g. `any_stream` | +| `8.examples/8i.echo-server-corosio.adoc` | 3 | 0 | 3 | e.g. `io_context` | +| `8.examples/8p.asio-use-capy.adoc` | 3 | 0 | 3 | e.g. `IoAwaitable` | +| `A.specification-methods/Ac.contingencies.adoc` | 3 | 0 | 3 | e.g. `capy::io_result` | +| `5.buffers/5d.system-io.adoc` | 2 | 0 | 2 | e.g. `ConstBufferSequence` | +| `7.testing/7.intro.adoc` | 2 | 0 | 2 | e.g. `read_stream` | +| `quick-start.adoc` | 1 | 1 | 2 | worst: `run_async(executor)(greet())` (L46) | +| `2.cpp20-coroutines/2d.advanced.adoc` | 1 | 0 | 1 | e.g. `task` | +| `2.cpp20-coroutines/2.intro.adoc` | 0 | 0 | 0 | generic-coroutine content only | +| `2.cpp20-coroutines/2a.foundations.adoc` | 0 | 0 | 0 | generic-coroutine content only | +| `2.cpp20-coroutines/2b.syntax.adoc` | 0 | 0 | 0 | generic-coroutine content only | +| `2.cpp20-coroutines/2c.machinery.adoc` | 0 | 0 | 0 | generic-coroutine content only | +| `3.concurrency/3.intro.adoc` | 0 | 0 | 0 | landing page | +| `3.concurrency/3a.foundations.adoc` | 0 | 0 | 0 | generic concurrency content only | +| `3.concurrency/3b.synchronization.adoc` | 0 | 0 | 0 | generic concurrency content only | +| `3.concurrency/3c.advanced.adoc` | 0 | 0 | 0 | generic concurrency content only | +| `4.coroutines/4.intro.adoc` | 0 | 0 | 0 | landing page | +| `5.buffers/5.intro.adoc` | 0 | 0 | 0 | landing page | +| `6.streams/6.intro.adoc` | 0 | 0 | 0 | landing page | +| `8.examples/8.intro.adoc` | 0 | 0 | 0 | landing page | +| `9.design/9.intro.adoc` | 0 | 0 | 0 | landing page | +| `A.specification-methods/A.intro.adoc` | 0 | 0 | 0 | landing page | +| **TOTAL** | **665** | **50** | **715** | | + +## Phase 1 prioritization suggestion + +By raw volume: `why-capy.adoc`, `9n.WhyNotCobaltConcepts.adoc`, +`9m.WhyNotCobalt.adoc`, `9k.Executor.adoc`, `9f.WriteStream.adoc` are the top +five and are all narrative/rationale pages dense with symbol mentions — +mostly bare-mention gaps, mechanically convertible to `cpp:` almost 1:1. + +By worst-offender severity (signature-restatement density, the true B1 +violations): `7b.mock-streams.adoc` (9 of 25), `9l.RunApi.adoc` (9 of 19), +and `4e.cancellation.adoc` (5 of 16) contain full constructor/method +signatures retyped in prose and should be prioritized for rewriting (link + +compiled example) over pages that only need mechanical `cpp:` substitution. diff --git a/doc/.gitignore b/doc/.gitignore index dd87e2d73..c9bddcf2d 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1,2 +1,6 @@ node_modules build +lint/.docstrings/ +# Vale-managed style packages (regenerated by `vale sync`); Capy/ is ours and is tracked. +.vale/styles/Google/ +.vale/styles/Vale/ diff --git a/doc/.pa11yci.json b/doc/.pa11yci.json new file mode 100644 index 000000000..c208cc99d --- /dev/null +++ b/doc/.pa11yci.json @@ -0,0 +1,20 @@ +{ + "_comment": "Style Guide E4 — contrast check (light + dark), Part F.0 'a11y contrast' gate. As of Phase-2 exit the contrast subset is BLOCKING (see docs.yml Phase-2 gate). Serve doc/build/site (e.g. `npx http-server doc/build/site -p 8088`) before running pa11y-ci against these paths, or point urls at file:// paths directly. chromeLaunchConfig.executablePath below is the LOCAL default (/usr/bin/chromium); run-a11y.mjs overrides it from the PA11Y_CHROME_PATH env var when set — CI sets that to the runner's browser (ubuntu-latest ships google-chrome at /usr/bin/google-chrome, not chromium).", + "defaults": { + "timeout": 30000, + "wait": 250, + "runners": ["axe"], + "standard": "WCAG2AA", + "chromeLaunchConfig": { + "executablePath": "/usr/bin/chromium", + "args": ["--no-sandbox"] + } + }, + "urls": [ + "http://localhost:8088/capy/index.html", + "http://localhost:8088/capy/why-capy.html", + "http://localhost:8088/capy/quick-start.html", + "http://localhost:8088/capy/4.coroutines/4a.tasks.html", + "http://localhost:8088/capy/reference/boost/capy.html" + ] +} diff --git a/doc/.vale.ini b/doc/.vale.ini new file mode 100644 index 000000000..f78aa00a7 --- /dev/null +++ b/doc/.vale.ini @@ -0,0 +1,102 @@ +StylesPath = .vale/styles +MinAlertLevel = warning +Packages = Google +; Capy domain vocabulary: .vale/styles/config/vocabularies/Capy/accept.txt. +; It holds genuine prose words and proper nouns ONLY. Bare C++ identifiers used as +; running text stay unlisted on purpose — they are style-guide B1 defects and must +; keep showing up as Vale.Spelling alerts until the prose is fixed. +Vocab = Capy +[*.adoc] +BasedOnStyles = Vale, Google, Capy +; NO BlockIgnores here, on purpose. Vale's native AsciiDoc handling (it shells out to +; asciidoctor and only lints extracted prose nodes) already excludes delimited listing +; blocks — `[source,cpp]`/`----`, bare `----`, `....` literal blocks, blocks nested in +; list items or admonitions, `role=pseudocode`/`role=external`, and callout markers are +; all skipped natively. A `BlockIgnores = (?s) *(\[source.*?----.*?----)` substitution +; used to sit here; it does NOT additionally protect anything — it destroys the `----` +; delimiters before asciidoctor sees them, which corrupts the block structure and hands +; the code inside to the linter as if it were a paragraph. Measured on the real corpus +; (Phase-4 blockignores task): removing that line dropped `.adoc` warning-level alerts +; 503 -> 376 (Vale.Spelling 189 -> 73, Capy.SentenceLength 140 -> 135, Capy.Terminology +; 17 -> 12, Google.Units 2 -> 1). A fingerprint-level audit attributed all but one of the +; retired findings to code. +; +; The one exception, and a second, pre-existing instance a position-independent control +; run also turned up (both confirmed in the fix-round-1 report, not "an isolated loss" as +; an earlier draft of this comment claimed): a correctly-excluded code block can suppress +; an UNRELATED, later Vale.Spelling/Google.Colons alert if the block's own text shares a +; SUBSTRING with the flagged word, and only if the block comes BEFORE the flagged prose in +; the file (a block after it has no effect; distance within the file does not matter). +; Reduced fixture: `// token` before a paragraph containing `foo_token` suppresses the +; alert; `// hello` before it does not; either half of `// zzqq_wwrr` suppresses a later +; bare `zzqq_wwrr`. This is a Vale/asciidoctor position-resolution artifact, not something +; any `BlockIgnores`/`TokenIgnores` value here controls — do not try to "fix" it with a +; config change without a fresh bite-tested fixture proving the change does something. +; Two known-affected spots as of the fix-round-1 report, neither currently flagged live by +; Vale, both still real defects to fix from the worklist rather than from a fresh Vale run: +; `stop_token` bare in prose at 9l.RunApi.adoc:171, and `Intentionally` after a colon at +; 9o.WhyNotTMC.adoc:68. If you are tempted to re-add a BlockIgnores line for source blocks: +; don't. Confirm first, with an isolated fixture, that Vale is actually failing to skip +; something. +; Ignore inline code spans (backticks) AND `cpp:target[...]` reference macros +; (the B1 conversion replaced backtick symbol spans with cpp: macros; their +; symbol text must stay unlinted, exactly as the backtick spans were). +; The third clause is the fixed label of the boost-wide thread-safety idiom +; ("Distinct objects: Safe." / "Shared objects: Unsafe."). Each instance is a +; genuine Google.Colons hit, but the form is boost-wide and Capy does not get to +; rewrite it; 16 of the 30 Colons hits at 4bea4edf were this one idiom, all in +; header docstrings (`grep -rn 'objects:' --include='*.hpp' include` → 18; no .adoc +; page uses it). Only the LABEL and its colon are blanked, so whatever follows +; stays fully linted by every other rule; the pattern deliberately does NOT spell +; out "Safe."/"Unsafe." because one instance continues into a longer clause +; ("Shared objects: Safe for copy, comparison, and `context()`.") that the +; phrase-exact form left exposed while suppressing its 16 siblings. +; This is NOT a Google.Colons demotion, on purpose: demoting the rule would also +; hide the genuine non-idiom Colons hits. Measured after: 11 survive (8 .adoc, all +; in 9o.WhyNotTMC/9b.Separation/3b.synchronization; 3 docstring "Pass ..." hits in +; io/any_*_stream.hpp). 30 - 16 does not equal 11 because the Vocab above also +; removed 3 .adoc Colons hits — Vale treats vocabulary terms as capitalisation +; exceptions, and "Capy"/"Corosio"/"Coroutine" after a colon were never defects. +TokenIgnores = (\x60[^\x60]+\x60), (cpp:[^\s\[]*\[[^\]]*\]), ((?:Distinct|Shared) objects:) + +; --- Google house-style pack: deliberately demoted, not abandoned ---------------- +; Maintainer ruling (Phase 4): these eight rules encode GOOGLE's house style, not +; Capy defects, and are scoped out of Phase 4's "vale clean" criterion. They are +; demoted to `suggestion` (below MinAlertLevel) rather than removed, so a curious +; reader can still run `vale --minAlertLevel=suggestion` and see them. +; Counts measured at 4bea4edf with `vale --output=JSON --minAlertLevel=suggestion` +; over `modules` (.adoc) and `lint/.docstrings` (header docstrings), i.e. BEFORE the +; Vocab above existed. Re-running now shows slightly lower numbers for two of them +; (Headings 597 -> 569, WordListCase 44 -> 43) because Vale treats vocabulary terms +; as exceptions to its capitalisation rules; that is a side effect of Step 1, not a +; fix, and it does not change the ruling. +; +; Google.Headings 597 .adoc / 0 docstrings — Capy writes Title Case section +; headings; Google style mandates sentence case. Retitling 597 headings is a +; user-visible house-style change, not a defect fix. +; Google.WordListCase 44 .adoc / 49 docstrings — Google's capitalisation list for +; words like "Internet"/"email"; disagrees with Boost usage, not with Part C. +; Google.EmDash 56 .adoc / 18 docstrings — bans spaced em dashes. Capy uses +; ` -- ` (AsciiDoc's em-dash form) as a deliberate typographic convention. +; Google.We 10 .adoc / 0 docstrings — bans first-person plural. The +; design essays under 9.design argue in the first person by design (D3). +; Google.FirstPerson 6 .adoc / 0 docstrings — same ruling as Google.We. +; Google.Latin 10 .adoc / 17 docstrings — bans "e.g."/"i.e."; both are +; standard in Boost reference documentation. +; Google.Quotes 12 .adoc / 2 docstrings — demands commas and periods inside +; quotation marks (US convention). Capy quotes code-like strings, where moving +; punctuation inside the quotes would misstate the string's contents. +; Google.Spacing 11 .adoc / 3 docstrings — flags spacing around punctuation +; in prose that is mostly quoted code. +; +; NOT demoted, on purpose: Google.Will (a genuine C4 signal, and the plan promotes +; C4), Google.Colons, Google.OxfordComma, Google.LyHyphens, Google.Units, +; Google.Ordinal. +Google.Headings = suggestion +Google.WordListCase = suggestion +Google.EmDash = suggestion +Google.We = suggestion +Google.FirstPerson = suggestion +Google.Latin = suggestion +Google.Quotes = suggestion +Google.Spacing = suggestion diff --git a/doc/.vale/styles/Capy/NoFluff.yml b/doc/.vale/styles/Capy/NoFluff.yml new file mode 100644 index 000000000..fc28c22d3 --- /dev/null +++ b/doc/.vale/styles/Capy/NoFluff.yml @@ -0,0 +1,14 @@ +extends: existence +message: "Filler/fluff — delete or rewrite (style guide C5/C9): '%s'." +level: warning +ignorecase: true +tokens: + - simply + - basically + - essentially + - obviously + - of course + - note that + - in order to + - due to the fact that + - utilize diff --git a/doc/.vale/styles/Capy/PartHeadings.yml b/doc/.vale/styles/Capy/PartHeadings.yml new file mode 100644 index 000000000..3aadb3e84 --- /dev/null +++ b/doc/.vale/styles/Capy/PartHeadings.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Heading uses 'Part N' ceremony instead of a descriptive title (style guide A7): '%s'." +level: warning +scope: heading +ignorecase: true +raw: + - '^Part\s+([0-9]+|[IVXLC]+)\b' diff --git a/doc/.vale/styles/Capy/SentenceLength.yml b/doc/.vale/styles/Capy/SentenceLength.yml new file mode 100644 index 000000000..77d8b5008 --- /dev/null +++ b/doc/.vale/styles/Capy/SentenceLength.yml @@ -0,0 +1,68 @@ +# RETIRED AS AN AUTHORITY — demoted rather than deleted, the same treatment +# .vale.ini gives the Google house-style pack. `doc/lint/sentence-length.mjs` is +# the authority for C2 on both surfaces; this rule is kept only so that +# `vale --minAlertLevel=suggestion` can still show what Vale made of a page. +# `suggestion` is below .vale.ini's `MinAlertLevel = warning`, so it no longer +# reaches baseline.json, the gate, or a default `vale modules` run. +# +# ============================================================================ +# DO NOT GATE THIS RULE. `--gate 'vale_adoc:Capy\.SentenceLength$'` — the +# obvious spec, by analogy with the working `vale_adoc:Capy\.PartHeadings$` — +# is VACUOUS. At `suggestion` this rule never enters a Vale fingerprint set, so +# the spec matches nothing and the gate reports `gated: true, gatedNew: 0` and +# exits 0 while checking NOTHING. Measured, exactly that. That is the same +# fail-open shape as the vacuous `Capy.PartHeadings` rule this branch already +# had to fix. Gate the script instead: +# +# --gate 'sentence_length:^C2:' +# +# which is live in docs.yml since the Phase-4 exit. Re-measured at the Phase-4 +# final fix wave: EXIT=1 / gatedNew=2, both findings the grandfathered +# `when_any.hpp` refusals, so it still needs a baseline reseed before it can go +# green. (An earlier version of this comment said gatedNew=135, the figure from +# before the .adoc hard slice was worked to zero; `doc/lint/README.md` carried +# the identical staleness and was corrected, this copy was missed.) +# `^C2:` binds the HARD slice only; the design-essay +# findings are keyed `advisory-C2` and are deliberately unreachable from a +# `C2`-prefixed spec (DOC_STYLE_GUIDE.md Part C2: hard in API docs, soft in +# essays). +# ============================================================================ +# +# Three measured reasons this rule cannot be the authority, none fixable inside +# a Vale rule (all `cwd=doc`, vale 3.15.1, target `modules`): +# +# 1. UNDER-COUNTS. It runs after .vale.ini's `TokenIgnores` blanks inline code +# spans, so a span contributes ZERO words where a reader counts one. Three +# measurements of the size of that blind spot, all agreeing: +# * task P4-prereq, Vale with rewritten TokenIgnores, on the +# pre-BlockIgnores-fix config: 140 -> 170 (+30) +# * this task, Vale with the committed TokenIgnores, every backtick span +# (2115) and `cpp:` macro (557) outside code blocks replaced by one +# word, `--minAlertLevel=suggestion`: 135 -> 164 (+29) +# * sentence-length.mjs, spans blanked versus spans as one word, all +# slices of `.adoc`: 125 -> 152 (+27) +# An earlier version of this comment claimed "+35, measured twice" by +# substituting today's 135 for prereq's 140 and by counting words with +# Vale's tokenizer. Both were wrong; the real figure is +27 to +30. +# 2. MIS-ATTRIBUTES, which is worse: the missed block produces no alert to +# chase, so re-running to a fixpoint never finds it. The blanking corrupts +# Vale's position mapping for `scope: sentence` rules. Hand-verified case: +# `5.buffers/5b.types.adoc` holds two over-limit sentences in list items +# (27 and 34 words) and Vale reports NONE. The artifact is not confined to +# `scope: sentence` either — a `Capy.Terminology` alert on +# `4.coroutines/4b.launching.adoc` is reported at line 25, an `include::` +# line inside a `[source,cpp]` block, when the text that matched is at +# line 68. +# 3. FALSE-POSITIVES on under-segmentation. `4.coroutines/4f.composition.adoc:93` +# is flagged as one sentence; its four real sentences are 23, 14, 11 and 6 +# words. Reproduced with that paragraph alone in a file. +# +# Do NOT re-promote this to warning/error without first showing, on a fixture, +# that Vale's position mapping for `scope: sentence` is fixed. Two checkers +# reporting different C2 numbers is how a gate loses credibility. +extends: occurrence +message: "Sentence over 25 words — split it (style guide C1/C2)." +level: suggestion +scope: sentence +token: \b(\w+)\b +max: 25 diff --git a/doc/.vale/styles/Capy/SimpleTense.yml b/doc/.vale/styles/Capy/SimpleTense.yml new file mode 100644 index 000000000..fa7d026c7 --- /dev/null +++ b/doc/.vale/styles/Capy/SimpleTense.yml @@ -0,0 +1,40 @@ +# Deliberately scoped to the two literal tokens DOC_STYLE_GUIDE.md Part C4 names +# ("Avoid needless 'will' and 'has been'") — NOT extended to every perfect-tense +# form. What this rule does and does not see, re-measured at commit 620fdf2c with +# cwd=doc and PATH including node_modules/.bin (Vale shells out to asciidoctor for +# .adoc, and the extracted docstrings are .adoc too): +# +# `will\s` IS wrap-tolerant, and that is deliberate. Commit 30464086 changed the +# token from the space-literal `'will '` to `'will\s'`, so a `will` at the end of a +# wrapped source line now matches. Fixture proof: `The buffer will\nbe consumed.` +# is FLAGGED, Match `"will\n"`. Do not "simplify" this back to a literal space. +# +# `has been` is NOT wrap-tolerant — a known, open hole. Fixture proof: `The buffer +# has\nbeen consumed.` is NOT flagged, while the same text on one line is. It costs +# nothing today (0 occurrences of `has\s*\n\s*been` in either corpus at 620fdf2c), +# so it is recorded rather than fixed; the fix is the same one-character change +# (`has\sbeen`) if an instance ever appears. +# +# The perfect-tense family this rule deliberately excludes — 17 occurrences on the +# docstring corpus (`doc/lint/.docstrings/`), plus 4 on the `.adoc` pages: +# have been x12 (ex/this_coro.hpp x4, read_at_least.hpp x3, +# write_at_least.hpp x3, ex/thread_pool.hpp, +# write.hpp) +# has already been x3 (when_any.hpp x2, ex/async_mutex.hpp) +# has not been x1 (write_at_least.hpp) +# has nowbeen x1 (ex/async_waker.hpp — invisible for BOTH reasons: +# an intervening adverb and a line wrap) +# (.adoc: have been x2 in 4h.lambda-captures, 9l.RunApi; has been x2 in +# 9b.Separation, why-capy) +# Maintainer ruling: these stay excluded. Part C4 names only "will" and "has been", +# so the rule is FAITHFUL to the guide as written; extending `tokens` would be a +# style-guide change smuggled in as a lint fix, and would surface ~17 new prose +# findings at once. A future editor who wants broader coverage changes Part C4 +# first, then this file. +extends: existence +message: "Avoid needless future/perfect tense — prefer present simple (style guide C4): '%s'." +level: warning +ignorecase: true +tokens: + - 'will\s' + - 'has been' diff --git a/doc/.vale/styles/Capy/Terminology.yml b/doc/.vale/styles/Capy/Terminology.yml new file mode 100644 index 000000000..2cddf18eb --- /dev/null +++ b/doc/.vale/styles/Capy/Terminology.yml @@ -0,0 +1,36 @@ +# C10 / C.1 one-term-per-concept. Two things this rule has to get right at once, +# and it got both wrong before: it must catch the verb in every form a writer +# actually uses, and it must NOT touch API identifiers or the noun `launcher`. +# +# `ignorecase: false` plus bare stems meant only the exact lowercase stem was +# seen. Measured on a fixture: `Launch the task for execution.`, `launches it on +# the executor`, `launched`, `launching`, `Spawn`, `spawns`, `spawned`, +# `spawning`, `Fire off`, `fires off`, `Kick off`, `kicked off` and `Boxed` all +# passed; only `launch`, `cancellation token`, `cancel token` and `boxed` were +# caught. That left the majority of real C10 prose invisible to the C10 checker. +# +# `ignorecase: true` is safe here, MEASURED rather than assumed. Every +# `launch`/`spawn`-bearing identifier in the library and the pages is either +# snake_case (`launch_one`, `launch_all`, `spawn_work`, `co_spawn`, +# `launch_policies`, the snippet tag `4b_launching`) or a `launcher` compound +# (`when_any_io_launcher`, `launchable`, `relaunch`, `launchers`). `_` is a word +# character, so the `\b` after the stem already excludes the snake_case forms, +# and the inflection list below is deliberately limited to VERB endings so it +# cannot reach `launcher`. On top of that, `.vale.ini`'s `TokenIgnores` blanks +# backtick spans and `cpp:` macros, so an identifier written as code is invisible +# to this rule anyway. +# +# The noun `launcher` STAYS, and must not be flagged: it is the role name of the +# `run_async` wrapper object, and plan Task 11's approved brief for all 18 +# overloads uses it — "Bind to produce a launcher; invoke the launcher +# with a task to start it." The verb `launch` is the violation, the noun +# `launcher` is not. Verify both directions if you touch the pattern. +extends: substitution +message: "Use '%s' for one-term-per-concept consistency (style guide C.1)." +level: warning +ignorecase: true +swap: + '\b(launch(?:es|ed|ing)?|spawn(?:s|ed|ing)?|fire[sd]? off|firing off|kick(?:s|ed)? off|kicking off)\b': start + '\bcancellation token\b': stop token + '\bcancel token\b': stop token + '\bboxed\b': type-erased diff --git a/doc/.vale/styles/config/vocabularies/Capy/accept.txt b/doc/.vale/styles/config/vocabularies/Capy/accept.txt new file mode 100644 index 000000000..0c9902533 --- /dev/null +++ b/doc/.vale/styles/config/vocabularies/Capy/accept.txt @@ -0,0 +1,156 @@ +# +# 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 +# + +# Capy documentation vocabulary — class (a) terms only. +# +# Every line is a case-insensitive regex, matched with word boundaries. `(?i)` is +# deliberate: a case-SENSITIVE entry makes Vale synthesise an error-level Vale.Terms +# alert on every other casing of the term, which would turn this file into a silent +# new casing rule. Possessives ("Capy's") are handled by Vale; plurals are not, so +# inflections are spelled out. +# +# ONLY genuine prose words and proper nouns belong here. A bare C++ identifier +# sitting unbackticked in running prose is a real defect (style guide B1) and must +# stay visible as a Vale.Spelling alert — do not add one to silence it. + +# --- Projects, libraries, tools, publishers ------------------------------------- +(?i)capy +(?i)corosio +(?i)asio +(?i)tmc +(?i)http +(?i)traccc +(?i)tcmalloc +(?i)wxwidgets +(?i)javadoc + +# --- People and cited sources --------------------------------------------------- +(?i)carruth +(?i)cern +(?i)chuanqi +(?i)dimovian +(?i)dobb +(?i)kohlhoff +(?i)lakos +(?i)nostrand +(?i)ousterhout +(?i)parnas +(?i)stepanov +(?i)yaknyam + +# --- Coroutine and asynchrony vocabulary (style guide C.1) ---------------------- +(?i)coroutines? +(?i)awaitables? +(?i)awaiters? +(?i)wakers? +(?i)wakeup +(?i)resumers? +(?i)combinators? +(?i)async +(?i)asynchrony + +# --- Types, idioms and roles named in prose ------------------------------------- +(?i)mutex(es)? +(?i)vtables? +(?i)typelists? +(?i)mixins? +(?i)serializers? +(?i)associators? +(?i)accessors? +(?i)functors? +(?i)callables? +(?i)callees? +(?i)requestors? +(?i)decompressors? +(?i)unchunkers? +(?i)proactors? +(?i)toolkits? +(?i)namespaces? +(?i)destructors? +(?i)virtuals? +(?i)freelists? +(?i)allocators? +(?i)implementors? +(?i)pimpl +(?i)data + +# --- Platform and systems terms ------------------------------------------------- +(?i)epoll +(?i)kqueue +(?i)io_uring +(?i)iovec +(?i)syscalls? +(?i)apis? +(?i)abis? +(?i)cpus? + +# --- C++ terms used adjectivally or nominally in running prose ------------------ +(?i)const +(?i)constness +(?i)nullptr +(?i)nothrow +(?i)enum +(?i)bool +(?i)boolean +(?i)lvalues? +(?i)rvalues? +(?i)variadic +(?i)templated +(?i)invocable +(?i)arity +(?i)config +(?i)codegen +(?i)devirtualization +(?i)prefetch +(?i)pseudocode +(?i)subtree +(?i)postconditions? + +# --- Coined adjectives and nouns the guide uses --------------------------------- +(?i)joinable +(?i)schedulable +(?i)launchable +(?i)buildable +(?i)greppable +(?i)greppability +(?i)composable +(?i)composability +(?i)reusability +(?i)discoverability +(?i)levelization +(?i)levelized +(?i)swappable +(?i)untyped +(?i)unbuffered +(?i)uncancellable +(?i)unclamped +(?i)uncontended +(?i)unexecuted +(?i)preconstructed +(?i)walkthrough + +# --- Verbs and participles ------------------------------------------------------ +(?i)rethrow(s|n|ing)? +(?i)preallocat(e|es|ed|ion) +(?i)deallocat(e|es|ed|ion) +(?i)dequeue[sd]? +(?i)enqueue[sd]? +(?i)dereferences? +(?i)destructuring +(?i)disambiguates? +(?i)deregisters? +(?i)unregisters? +(?i)unlinks? +(?i)downcasts? +(?i)recompiles? +(?i)reframed +(?i)reinstalls? +(?i)interop +(?i)interoperate +(?i)interoperation diff --git a/doc/antora.yml b/doc/antora.yml index ad9990c7a..cf151276e 100644 --- a/doc/antora.yml +++ b/doc/antora.yml @@ -15,6 +15,8 @@ asciidoc: attributes: source-language: asciidoc@ table-caption: false + page-toc: '' + toclevels: 2 nav: - modules/ROOT/nav.adoc ext: diff --git a/doc/lint/README.md b/doc/lint/README.md new file mode 100644 index 000000000..7844f4aee --- /dev/null +++ b/doc/lint/README.md @@ -0,0 +1,349 @@ + +# `doc/lint` — the documentation-quality toolkit + +These scripts implement the enforcement tiers in `DOC_STYLE_GUIDE.md` Part F.0. They run in +the **Documentation** workflow (`.github/workflows/docs.yml`), in the `antora` job, after the +site build. Node built-ins only, no dependencies of their own. + +| Script | What it checks | +|---|---| +| `doc-lint.mjs` | Structural AsciiDoc/nav rules (A1, A6, B2, ANCHOR, D2, …). JSON on stdout. | +| `extract-docstrings.mjs` | Extracts header docstrings into `.docstrings/*.adoc` so Vale can lint them. | +| `sentence-length.mjs` | **The authority for C2** (no sentence over 25 words), over both corpora. JSON on stdout. | +| ↳ | Emits `C2` (hard slice), `advisory-C2` (design essays) and `BACKTICK` (unbalanced code span). | +| `selftest.mjs` | Asserts `sentence-length.mjs` and `doc-lint.mjs`'s B2 and ANCHOR checks still detect what they claim. Exit 1 on regression. | +| `mrdocs-warnings.mjs` | Runs the pinned MrDocs 0.8.0 directly and parses its reference-surface warnings. | +| `run-a11y.mjs` | pa11y-ci contrast/a11y scan over the built site (`doc/build/site`). | +| `baseline.mjs` | Runs every check and snapshots their findings to `baseline.json`. | +| `check-no-new-violations.mjs` | **The gate.** Diffs a fresh run against `baseline.json` and fails on NEW findings. | +| `baseline-diff.mjs` | Explains what replacing `baseline.json` with a candidate would change. | + +## How the gate works + +`baseline.json` is a snapshot of every finding that already existed when it was taken. +`check-no-new-violations.mjs` runs a fresh scan and reports only fingerprints that are **not** +in the snapshot. Everything in the snapshot is grandfathered. + +Which findings actually *block* a merge is the `--gate :` spec in the blocking +step of `.github/workflows/docs.yml`. Each regex is tested against the **whole** fingerprint. + +Fingerprints are built by `baseline.mjs` and their shape is load-bearing: + +| Check | Fingerprint | +|---|---| +| `doc_lint` | `rule:file:#N:message` — rule at the **head** | +| `sentence_length` | `C2:file:#N:message` (hard), `advisory-C2:…`, `BACKTICK:…` — rule at the **head** | +| `vale_adoc`, `vale_docstrings` | `file:#N:Check.Name` — check name at the **tail** | +| `mrdocs_warnings` | `file:#N:message` | +| `a11y` | `url:code:selector` | + +`#N` is the Nth occurrence of that (head, tail) pair, **not a line number** — so inserting +text above a finding does not rename it. It sits mid-key on purpose: the gate spec +`vale_adoc:Capy\.PartHeadings$` is anchored on the tail, and anything appended after the check +name would make that gate match nothing while still exiting 0. + +### `ANCHOR` + +`doc-lint.mjs`'s `ANCHOR` rule catches a C++ attribute written as `` `[[nodiscard]]` `` in +prose. Asciidoctor's inline-anchor substitution runs *inside* a backtick span, so that source — +which reads perfectly correctly — renders as an **empty `` element**. Measured: +`` `[[clang::coro_await_elidable]]` `` produced +``. The fix in prose is a passthrough: +`` `+[[nodiscard]]+` ``. + +It is gated (`doc_lint:^(A1|A6|B2|D2|ANCHOR):`), head-anchored because doc_lint fingerprints put +the rule at the head. Verified by planting a violation and confirming the fingerprint +`ANCHOR::#1:` matches the new spec and **does not** match the previous +`^(A1|A6|B2|D2):` — so the workflow edit is load-bearing, not decorative. Only prose is scanned; +inside a delimited block `[[` is literal and renders fine, and that negative was bite-tested too. + +`doc-lint.mjs` also emits a `SHAPE` rule alongside `A1`/`A6`/`B2`/`ANCHOR`/`D2` — deliberately outside the +gate spec's `^(A1|A6|B2|D2|ANCHOR):` alternation, the same way `advisory-C2` sits outside +`sentence_length`'s `^C2:`. SHAPE is a content-shape heuristic over every `role=output`/ +`role=figure` bare listing (DOC_STYLE_GUIDE.md B3): those roles are a permanent B2 exemption, so a +block wrongly tagged non-code would otherwise be permanently invisible. A SHAPE finding never +blocks a merge — it is a signal for a human to re-check the tag, not a defect in itself. + +**A Vale gate spec must never carry a leading `^`.** The regex is tested against the whole +fingerprint, and for the Vale checks the check name is at the **tail**, so `^Capy\.NoFluff$` +matches nothing and the comparator then reports `gated: true, gatedNew: 0` — a gate that +announces it is gating while checking nothing, at exit 0. Measured twice on this branch. The +head-anchored shape is correct only for the two rule-at-the-head checks (`doc_lint`, +`sentence_length`), which is why the live spec mixes `^(A1|A6|B2|D2|ANCHOR):` and `^C2:` with +`Capy\.PartHeadings$` and `(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$`. When you add +a gate, plant a violation of that exact rule and watch the step fail before you believe it. + +**Never hand-edit `baseline.json`.** + +### C2: gate the script, not the Vale rule + +`Capy.SentenceLength` is `level: suggestion` (see the comment in +`.vale/styles/Capy/SentenceLength.yml`), so it never enters a Vale fingerprint set. The +obvious-looking spec by analogy with `vale_adoc:Capy\.PartHeadings$` is therefore **vacuous** — +measured: `--gate 'vale_adoc:Capy\.SentenceLength$'` gives `gated: true, gatedNew: 0`, exit 0, +while checking nothing. Gate the script: + +``` +--gate 'sentence_length:^C2:' # live in docs.yml since Phase-4 exit + # re-measured at the Phase-4 final fix wave: exit 1, gatedNew 2 +``` + +`^C2:` binds the **hard** slice only — the `include/**` docstrings plus every `.adoc` page outside +`modules/ROOT/pages/9.design/` and `modules/ROOT/pages/A.specification-methods/`. The design-essay +findings are keyed `advisory-C2` (`DOC_STYLE_GUIDE.md` Part C2 makes the limit soft in essays), and +that key deliberately does not begin with `C2` so a mis-written `^C2` cannot reach them. Verified: +the gated slice contains 0 `advisory-C2` fingerprints. + +### Run `selftest.mjs` after touching `sentence-length.mjs` or `doc-lint.mjs` + +``` +node lint/selftest.mjs # exit 0 = 35 assertions hold; exit 1 names what broke +``` + +It exercises the properties whose failure is **silent**, because nothing in the real corpus +reaches them: the unbalanced-backtick guard (both halves — the diagnostic *and* the count +correction), the two under-reporting cases (mid-sentence ellipsis, parenthesised abbreviation), +the bold run-in lead, reader word counting, code blocks not being linted as prose, and the +hard/advisory partition including a `9.designish/` look-alike that must stay hard. Two plausible +refactors of the backtick rule were demonstrated to break it while every corpus-level number still +looked reasonable; both fail this file. Fixtures live in `lint/fixtures/` and are not part of +either linted corpus. + +It also asserts that `extract-docstrings.mjs` extracts **both** Doxygen comment forms, which is the +same class of silent failure: `///` runs went unextracted until 2026-08, so 86 published doc lines +were outside every gate, and losing that branch again would just make the corpus smaller and every +count lower. The expectation is derived from the header tree rather than written down — the headers +whose *only* doc comments are `///` runs must each produce an output file — so it cannot go stale. + +It also covers `doc-lint.mjs`'s B2 check, against a throwaway fixture tree built at run time (not +`lint/fixtures/`, which only `sentence-length.mjs` reads): every language token B2 must reach (not +just `cpp`/`c++`), a bare `----`/`....` listing holding real code with no role marker, a 5-dash +(`-----`) listing (closer length must match opener length, not just be `>=4`), and — load-bearing — +that `role=output`/`role=figure` clears only a *bare* listing and never a `[source,*]` block. A +mutation that ORs the clearing-role and non-code-role checks together, ignoring which kind of block +it is, makes `role=output` a blanket exemption for real C++; this file catches it. It also asserts +the SHAPE advisory sidecar (DOC_STYLE_GUIDE.md B3) fires on a role=output/role=figure block whose +content looks like code and stays silent on a genuine one. + +### `sentence_length` has no baseline entry — so the C2 gate is RED, on purpose + +**Read this before you try to make the Documentation job green.** `sentence_length` was added +after the committed `baseline.json` was taken, so **nothing in its slice is grandfathered** and +every one of its findings reports as new. Phase-4 exit gated it anyway +(`--gate 'sentence_length:^C2:'`), which means the blocking step **exits 1** on the whole hard +slice. Measured at 620fdf2c, that slice is exactly **two** findings, both in +`include/boost/capy/when_any.hpp` (`lint/.docstrings/when_any.hpp.adoc`) — a 27-word and a +31-word sentence of the shape *"If at least one child await-returned a zero `ec`, the result +holds …, unless producing the winner's payload threw, in which case that exception is +rethrown."* **Zero `.adoc` fingerprints remain under `^C2:`.** + +Those two are **accepted refusals, not defects.** A Phase-4 rewrite that split them made a false +claim against the code and was reverted verbatim; the maintainer's content review carries that +exact text. The maintainer declined an in-source refusal marker and chose **visible debt over new +machinery**, so: + +* **Do not** add a suppression mechanism, and **do not** widen or head-trim the gate spec. +* **Do not** reseed `baseline.json` locally — a local run grandfathers ~357 local-vs-CI drift + fingerprints (see below). +* The fix is the **post-merge `workflow_dispatch` reseed** documented in the next section. Until + it lands, the Documentation job is red on one step with two known findings. + +By contrast the **C4/C9/C10** gates — `Capy.SimpleTense` / `Capy.NoFluff` / `Capy.Terminology` +over *both* surfaces — are **green today and need no reseed**. Their three residual `.adoc` +findings sit inside two verbatim third-party quoted passages and are already in +`baseline.json`, with fingerprints verified to describe those same sentences. + +One further consequence of gating this check: a **crash** of `sentence-length.mjs` is now fatal. +It is reported as `SKIPPED` on stderr, and a skip of a *gated* check fails the gate. + +--- + +## Reseeding `baseline.json` + +### When you need to + +The snapshot goes stale in one direction: when you **fix** findings they disappear from the +scan but stay in the snapshot. Those stale entries are dead grandfather clauses — the finding +can be reintroduced later and the gate will still pass, because the snapshot says it is a +known issue. Reseed when a batch of fixes has landed and you want the gate to start protecting +them. + +**Never reseed to make a red gate go green** — *with exactly one pre-authorised exception, the +C2 case described under "`sentence_length` has no baseline entry", above.* Outside that case, a +red gate means something new was introduced: fix that instead. The exception exists because the +C2 gate is red for findings that were never new and were never defects, and grandfathering them +is the ruled resolution rather than a way around a real regression. + +### Why not just run `baseline.mjs` locally + +Because a local run differs from a CI run by hundreds of fingerprints — a different MrDocs +0.8.0 build hash, `chromium` instead of the runner's `google-chrome`, different +file-processing order, a different `asciidoctor` implementation. Committing that grandfathers +your machine's quirks as if they were the project's backlog. Measured during Phase 3: a local +run adds **357** fingerprints a CI run would not. + +So the candidate is authored by CI, in the CI environment, and a human reviews and commits it. + +### Prerequisite + +GitHub only offers the `workflow_dispatch` trigger for a workflow whose file is present **on +the repository's default branch**. If `.github/workflows/docs.yml` on the default branch has +no `workflow_dispatch:` in its `on:` block, neither the "Run workflow" button nor +`gh workflow run` will find it, and you must merge that change first. Once it is on the +default branch you can dispatch against any ref. + +### 1. Run the job + +Web UI: **Actions → Documentation → Run workflow →** pick the branch → **Run workflow**. + +```bash +gh workflow run docs.yml --ref +gh run list --workflow=docs.yml # then: gh run watch +``` + +The run does everything a normal docs run does — including the blocking gate against the +*currently committed* baseline — and then produces a candidate baseline, **only** because you +dispatched it manually. A push or a pull request never produces one; an automatic reseed would +absorb real regressions into the grandfathered backlog. + +### 2. Read the report before downloading anything + +Open the finished run and read its **Summary** page, under "Candidate +doc/lint/baseline.json". The report ends in a `RESULT:` line; **if it does not say +`candidate retires … none gated`, the reseed step has failed and you must not commit the +file.** + +> **The one pre-authorised exception, for the first reseed after the doc-improvement branch +> merges.** The point of that reseed is to grandfather the two accepted `when_any.hpp` C2 +> refusals described above — and a gated addition is exactly what the report treats as fatal. +> So the report **will** exit 1 with `RESULT: candidate must not be committed until its gated +> additions are justified`, and it **will** list precisely: +> +> ``` +> - sentence_length :: C2:lint/.docstrings/when_any.hpp.adoc:#1:sentence over 25 words +> - sentence_length :: C2:lint/.docstrings/when_any.hpp.adoc:#2:sentence over 25 words +> ``` +> +> Because that step ends in `exit "$status"` (`.github/workflows/docs.yml:406`), **the +> "report what the candidate would change" step shows as FAILED in the Actions UI.** That is +> expected here and is not an infra problem. **The candidate is still retrievable:** the upload +> step is guarded `if: always()` (`docs.yml:408-414`), so the +> `doc-lint-baseline-candidate` artifact is attached to the run even though the report step went +> red. Download it as usual. +> +> Those **two fingerprints, and nothing else**, are the justification. There is no +> `--allow-gated` flag today; accepting them is a maintainer decision made by hand and recorded +> in the reseed commit message. **Anything else in that list is a real regression — go fix the +> documentation instead.** +> +> **Delete this blockquote once the reseed is committed.** It documents a one-shot transition; +> left in place afterwards it becomes a standing exception to a rule that should have none. + +Then check four things, in this order: + +1. **A `SKIPPED` cell in the per-check table.** Stop if you see one. A skipped check means a + tool could not run (most often Ruby `asciidoctor` missing, which breaks the `vale_adoc` + slice; or a MrDocs cache miss). The candidate would wipe that check's entire grandfathered + backlog. Fix the environment, re-run, discard this candidate. + + The same applies to a **gated check showing `0` findings** against a non-zero committed + count: a check that crashes reports zero, and zero looks like success. That is also fatal. + If a gated backlog has *genuinely* reached zero — a milestone, not an accident — confirm + the check really ran and re-run the comparator with `--allow-emptied `. + +2. **"ADDED fingerprints that the merge gate WOULD have blocked."** If it says `(none — …)`, + good. Anything listed is either **a real regression** you are about to grandfather away — + go fix the documentation instead — or **an environment difference**. Do not accept a + candidate with unexplained entries here; this section is the entire safety mechanism. Two + checks that settle which it is: + + - **Does the fingerprint's file and rule correspond to something that changed at the ref + you dispatched?** `git log --oneline .. -- `. A real + regression has a change behind it; drift does not. + - **Re-dispatch against the default branch and see whether the same fingerprint appears.** + Environment drift reproduces on a ref that contains none of your work. A regression + introduced at your ref does not. + +3. **The ADDED / REMOVED breakdown by check and rule.** ADDED becomes permanently + grandfathered; additions in ungated slices are tolerable while those slices are still being + worked down, but read the rule names and confirm they are the classes you expect. + +4. **REMOVED is what you came for — so confirm it is what you did.** The rule breakdown should + match the work that has actually landed since the last reseed. A retirement far larger than + the work, or spread across rules nobody touched, means a tool linted less than it should + have rather than that the backlog shrank. (Precedent: a malformed code span once made + `asciidoctor` swallow real prose and collapsed the linted surface from 1576 alerts to 412, + with no error anywhere.) + +### 3. Accept it + +```bash +cd "$(git rev-parse --show-toplevel)" # paths below are repo-root-relative +gh run download -n doc-lint-baseline-candidate -D /tmp/cand +cp /tmp/cand/baseline.json doc/lint/baseline.json +git diff --stat doc/lint/baseline.json +``` + +The artifact also contains `baseline-diff.txt` (the report you just read) and +`baseline.json.diff` (a `diff -u` against the committed baseline, the only place a single +changed fingerprint is visible verbatim). + +Commit on a branch, naming the run that produced it and what the ADDED entries were, so the +next person can audit the decision: + +``` +ci: reseed doc/lint/baseline.json from CI run + +Retires stale fingerprints (). Adds , all in non-gated +slices (); no added fingerprint matches the blocking gate spec. +``` + +Open a PR. The Documentation workflow on that PR runs the gate against your new baseline — +**that run is the real proof.** If the candidate was authored in a drifted environment, +`mrdocs_warnings` (gated as a whole check) red-lines immediately. It fails closed, not open. + +### Reading a candidate by hand (diagnosis only) + +`baseline-diff.mjs` compares any two snapshots. The CI step derives its `--gate` values from +the blocking step in `.github/workflows/docs.yml` so the two cannot rot apart; when you run it +by hand, copy them from that step — a report run with a stale or missing gate spec prints +`none gated` and means nothing. + +```bash +cd doc +node lint/baseline-diff.mjs lint/baseline.json /path/to/candidate.json \ + --gate 'doc_lint:^(A1|A6|B2|D2):' \ + --gate 'vale_adoc:Capy\.PartHeadings$' \ + --gate 'mrdocs_warnings:.*' \ + --gate 'sentence_length:^C2:' \ + --gate 'vale_adoc:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$' \ + --gate 'vale_docstrings:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$' +``` + +Six specs as of Phase-4 exit. The CI step extracts them, so this hand-run copy is the one that +can rot — check it against the blocking step before trusting a `none gated` result. + +Exit 0 means the candidate is explainable (it may still add *ungated* findings — read the +report). Exit 1 means it must not be committed as-is, for one of three reasons, all named in +the `RESULT:` line: a check is `skipped`, a gated check collapsed to zero findings, or an added +fingerprint matches the gate spec. + +### Running the checks locally + +`vale_adoc` and `vale_docstrings` need an `asciidoctor` on `PATH` (Vale shells out to it for +`.adoc`), and `run-a11y.mjs` needs `doc/build/site` plus a browser: + +```bash +cd doc +export PATH="$PWD/node_modules/.bin:$PATH" +node lint/baseline.mjs /tmp/local-snapshot.json # never commit this as baseline.json +``` + +`baseline.mjs` output is working-directory-independent, so it is safe to run from anywhere. diff --git a/doc/lint/RESEARCH-docstring-examples.md b/doc/lint/RESEARCH-docstring-examples.md new file mode 100644 index 000000000..08e4e9fe8 --- /dev/null +++ b/doc/lint/RESEARCH-docstring-examples.md @@ -0,0 +1,279 @@ +# Research: gating docstring `@code` examples + +**Status:** research only, no code/config change. Produced for DOC_IMPROVEMENT_PLAN Phase 0, +Task 5. + +## Problem + +Header docstrings under `include/boost/capy/**` contain hand-typed `@code`/`@par Example` +blocks. No gate compiles them today. `test/doc/CMakeLists.txt` (target +`boost_capy_doc_tests`, run via `./b2 libs/capy/test`) compiles the `.adoc` pages' examples — +commit `aa1a38c7` ("docs: compile every documentation code block") converted all ~480 +hand-typed blocks on the 45 `.adoc` pages into compiled includes precisely because a hand-typed +example can silently rot. Docstring `@code` blocks were out of scope for that commit and remain +unprotected. + +## Inventory + +``` +$ grep -rlE '@code|@par Example' include/boost/capy/ | wc -l +52 +$ grep -roE '@endcode' -r include/boost/capy/ | wc -l +103 +``` + +52 files, 103 `@code`/`@endcode` block pairs (`@par Example` headings always wrap a `@code` +block in this codebase; no bare-prose "Example" sections exist without one). For scale: the +`.adoc`-page effort in `aa1a38c7` covered ~480 blocks across 45 pages; this corpus is ~21% of +that by block count, spread across 52 files instead of 45 pages. + +Representative blocks (chosen to show the range, not cherry-picked for compilability): + +```cpp +// include/boost/capy/when_any.hpp +@code +task example() +{ + std::vector> reads; + for (auto& buf : buffers) + reads.push_back(stream.read_some(buf)); + auto result = co_await when_any(std::move(reads)); + ... +} +@endcode +``` + +```cpp +// include/boost/capy/ex/strand.hpp +@code +thread_pool pool(4); +strand strand(pool.get_executor()); // CTAD deduces the executor type +continuation c1{h1}, c2{h2}, c3{h3}; +strand.post(c1); +... +@endcode +``` + +```cpp +// include/boost/capy/ex/run_async.hpp +@code +// Correct usage - wrapper is temporary +run_async(ex)(my_task()); + +// Compile error - cannot call operator() on lvalue +auto w = run_async(ex); +w(my_task()); // Error: operator() requires rvalue +@endcode +``` + +Two findings that size the effort: + +1. **Almost none of the 103 blocks are self-contained.** `stream`, `buffers`, `h1`/`h2`/`h3` + are never declared in scope; the blocks are teaching fragments, not compilable programs. + Bringing them under a compile gate needs per-block scaffolding (declare the ambient names), + the same kind of work `test/doc/snippets/*.cpp` already does for the `.adoc` pages. +2. **Some blocks are intentionally non-compiling** (the `run_async.hpp` example above + demonstrates a *rejected* overload on purpose). `aa1a38c7` hit the identical problem on the + `.adoc` side and solved it with an explicit exemption convention: `role=pseudocode` (21 + blocks) and `role=external` (13 blocks). Doxygen `@code` has no attribute slot equivalent to + an Asciidoc role, so an equivalent marker would have to be invented (e.g. a sentinel first + line inside the block, stripped before compiling). + +## MrDocs version actually in use + +MrDocs is not a standalone install; `@cppalliance/antora-cpp-reference-extension` downloads and +runs it. `doc/lint/mrdocs-warnings.mjs` (built in Task 2) is the existing tool that invokes the +binary directly, mirroring the extension's own resolution: check `PATH`, else search +`~/.cache/antora/reference-collector/mrdocs///bin/mrdocs`. + +Two tags are cached locally (`master` and `develop`); the script's cache search (DFS, PATH +directories then the cache tree) resolves to **`master`**. Running it directly: + +``` +$ /home/michael/.cache/antora/reference-collector/mrdocs/linux/master/bin/mrdocs --version +MrDocs version 0.8.0+e9f847d8acfd +Built with LLVM 22.0.0git +Build SHA: e9f847d8acfd0a5d8381d44685d8779414a17437 +Target: x86_64-unknown-linux-gnu +``` + +Confirmed this is also the binary `mrdocs-warnings.mjs` runs in practice: invoking the script +produced 216 findings against this same binary/config. + +**Pinned version: MrDocs 0.8.0 (build `e9f847d8acfd`, matches upstream tag `v0.8.0`, released +2025-10-30).** Feasibility below is evaluated against this build's actual shipped headers and +`--help` output, not the newest upstream commit. + +## Options evaluated + +### (a) MrDocs `@snippet`/include directive pulling from a compiled `test/doc/` source + +**Not feasible at the pinned version — the command does not exist.** + +- `mrdocs --help` (run against the pinned binary) lists every CLI flag; nothing resembling + `--snippet` or file inclusion for doc comments. +- The doc-comment block-command surface is a closed, exhaustively enumerated set, straight from + the shipped header (`include/mrdocs/Metadata/DocComment/Block/BlockKind.hpp` in the cached + install): + `Admonition, Brief, Code, Heading, Paragraph, List, DefinitionList, Quote, ThematicBreak, + FootnoteDefinition, Table, Math, Param, Postcondition, Precondition, Returns, See, Throws, + TParam`. No `Snippet` or `Include` kind. +- `CodeBlock.hpp` confirms `@code`/`@endcode` stores only a `literal` string captured verbatim + from the comment — there is no file-reference field. +- Upstream docs corroborate this design intent, not just the shipped binary: the MrDocs + `commands/blocks.adoc` page (cppalliance/mrdocs, `docs/modules/ROOT/pages/commands/blocks.adoc`) + states plainly: "A code block reproduces source verbatim. Fence it with `@code` and + `@endcode`... `@verbatim`/`@endverbatim` are the same idea without highlighting." No mention + of file inclusion anywhere in the command reference (`commands/reference.adoc`). +- Confirmed independently via GitHub issue search: **cppalliance/mrdocs#620**, "Compile code + snippets in javadocs," opened 2024-06-05, still **open**, last comment 2025-12-16 from the + maintainer (`alandefreitas`) comparing it to how Rustdoc concatenates all snippets into one + file for compilation — i.e. upstream has thought about exactly this feature and has not + built it. It's tracked under an umbrella issue (`#1113`, "Explore unknowns," last updated + 2026-06-25 — active). + +Effort/round-trip are moot: the mechanism this option assumes does not exist in the pinned +MrDocs. + +### (b) A preprocessor that extracts `@code` blocks into a generated TU compiled in CI + +**Feasible, but real effort — comparable in kind to `aa1a38c7`'s `.adoc` work, scaled to ~21% +of its block count.** + +- Extraction itself is cheap and has working prior art in this repo: + `doc/lint/extract-docstrings.mjs` already isolates `@code`/`@endcode` spans with + `raw.replace(/@code\b[\s\S]*?@endcode\b/g, '')` (it currently *discards* them for the Vale + prose gate — the inverse of what a compile gate needs, but the extraction regex is a known + starting point). +- The hard cost is not extraction, it's compilability. As shown in Inventory, most blocks + reference ambient names not declared in the block. Each needs a small scaffold (declare + `stream`, `buffers`, etc.) — the same category of work `test/doc/snippets/*.cpp` already does + for `.adoc` pages, block-by-block, not a bulk automatable transform. +- Non-compiling-by-design blocks (at least the `run_async.hpp` case, likely more) need an + exemption convention. `aa1a38c7` solved this with Asciidoc `role=pseudocode` / + `role=external`, an attribute slot `@code` doesn't have; a docstring-side equivalent has to be + invented and is not a drop-in port of that convention. +- **Round-trip is clean only if designed carefully.** Since MrDocs cannot pull `@code` content + from a file (per option a), the header's hand-typed text stays the single rendered source of + truth; a gate can only *verify* that text (by wrapping the literal block in a scaffold and + compiling it), not *generate* it from an external snippet. That's an achievable design (no + divergence between what's rendered and what's compiled, because they're the same literal + text) but it means the CI target compiles ad hoc per-block scaffolding, not the tidy + `include::example$...[tag=...]` mechanism used on the `.adoc` side. +- Sizing: `aa1a38c7` converted ~480 blocks across 45 pages (a dedicated commit/task). This + corpus is 103 blocks across 52 files — meaningfully smaller by count, but each block still + needs individual scaffolding judgment, so it is not scaled down proportionally in time; the + need to invent a new non-compiling-block convention (not portable from Asciidoc roles) adds + further one-off design cost this task did not have to pay. + +### (c) Rely on `doc-sync` to catch drift at change-time, no standing gate + +**Already partially wired, weaker than a compiler check, no marginal implementation cost.** + +`doc-prompts/doc-sync.md` (Step 1) makes the co-located docstring (`own_docstring` hit) a +**mandatory, unconditional** hit for every changed public symbol — it is always inspected, not +opt-in. Step 4 (Verify) requires, for `edit_kind=docstring`: "the header still compiles and +every `@param` name still matches the declaration." + +This is real protection, but it does not compile the `@code` example itself — comments are +preprocessed away before the header compiles, so "the header still compiles" says nothing about +whether the example body inside the comment is still valid C++. The check is a human/agent +review comparing the example's prose against the new declaration, not a compiler-verified fact. +It catches signature drift (a renamed parameter, a changed return type) reliably because that's +literally what Step 4 asks the reviewer to check; it does not catch e.g. a valid-looking example +that no longer compiles for a subtler reason (removed overload, changed constraint) unless the +reviewing agent happens to notice. + +### (d) MrDocs-native example verification, if the pinned version supports it + +**Not supported — same evidence as (a).** `--help` has no `--warn-example`/`--check-example`/ +similar flag; the Warnings section of `mrdocs --help` is exhaustively: `--concurrency`, +`--ignore-failures`, `--ignore-map-errors`, `--log-level`, `--report`, `--verbose`, +`--warn-as-error`, `--warn-broken-ref`, `--warn-if-doc-error`, `--warn-if-undoc-enum-val`, +`--warn-if-undocumented`, `--warn-no-paramdoc`, `--warn-unnamed-param`, `--warnings` — nothing +inspects `@code` content. This collapses into the same open upstream issue (#620) as option (a). + +## Recommendation: DEFER + +Do not build a standing compile gate for docstring `@code` blocks in this phase. + +**Why:** Options (a) and (d) — the two that would give a clean, low-maintenance round-trip — +are blocked on a MrDocs feature that does not exist at the pinned version and is an +18-month-old open upstream issue with no committed timeline. Option (b) is feasible but is a +real, separately-sized effort (bespoke per-block scaffolding for ~103 non-self-contained +examples, plus inventing a non-compiling-block convention `aa1a38c7` didn't have to invent for +Asciidoc) — disproportionate to bolt on inside a Phase 0 *research* task, and better sized as +its own follow-up once scoped concretely. + +**Interim protection (option c), already in place, no new work:** `doc-sync`'s mandatory +`own_docstring` hit plus its Step 4 requirement that a docstring edit's `@param`s still match +the new declaration. This catches the drift class that matters most (signature/behavior drift +following a code change) at PR time, via human/agent review — just not via a compiler. + +**Concrete follow-up, if/when taken up** (sketch only, not implemented here): + +- A new script, e.g. `doc/lint/check-docstring-examples.mjs`, reusing + `extract-docstrings.mjs`'s `@code`/`@endcode` extraction regex (inverted: keep the code, + not discard it). +- An exemption marker for intentionally non-compiling blocks — e.g. a sentinel first line + (`// doc-example: no-compile`) inside the block, stripped before compiling, analogous in + intent to `aa1a38c7`'s `role=pseudocode`/`role=external` but expressed in a form `@code` can + carry. +- Per-block scaffold files under a new `test/doc/docstrings/
    /.cpp` tree (mirroring + `test/doc/snippets/`), authored by hand the first time each block is touched — realistically + folded into the Phase 1 "Structure" pass that is already rewriting each header's docstrings + file-by-file, rather than done as one big upfront sweep. +- A new CMake target (`boost_capy_docstring_doc_tests`, modeled on `test/doc/CMakeLists.txt`'s + `boost_capy_doc_tests`) added to `tests` once a meaningful fraction of the 52 files have + scaffolds. + +**Revisit triggers:** + +1. cppalliance/mrdocs#620 ("Compile code snippets in javadocs") or its umbrella #1113 ships + native `@snippet`/example-compile support — re-run this research against the new version; + options (a)/(d) would likely become the better long-term answer at that point. +2. A docstring `@code` example is found broken in review despite doc-sync running — evidence + the interim protection (c) is insufficient in practice, not just in theory. +3. Phase 1's per-file Structure pass finishes touching all 52 headers — natural checkpoint to + size a dedicated follow-up task, since every docstring will have just been freshly rewritten + and reviewed anyway. + +## Audit evidence for revisit trigger #2 (added post-hoc, DEFER still stands) + +A later documentation audit (`DOC_AUDIT_REFERENCE.md`, section R2) exercised trigger #2 above: +it read all 103 blocks and, for several, actually compiled the corrected form with +`g++ -std=c++20 -fsyntax-only`. Result: **12 of the 103 blocks were broken** — about 12%, +not a hypothetical risk. Two of the twelve were not drift but active misinformation: + +- `run_async_wrapper`'s `@warning` and `test::run_blocking_wrapper`'s copy of it both asserted + that storing the wrapper (`auto w = run_async(ex);`) "does not compile." Compiled and run: + it does compile — C++17 guaranteed copy elision constructs `w` directly from the prvalue, so + the deleted copy/move constructors are never considered. What actually fails to compile is + calling through the stored lvalue (`w(my_task())`); `std::move(w)(my_task())` compiles and + runs. Both docstrings have been corrected to state the real guarantee — the rvalue + ref-qualifier rejects calls on an lvalue — instead of the false one. +- The other ten were compile failures or semantic drift from the API they document: a context + passed where an `Executor` is required (`work_guard`), a CTAD default that silently produces + the wrong buffer const-ness (`buffer_param`'s Virtual Interface Pattern), a closure passed + where `post` requires a `continuation&` (`ExecutionContext`), a duplicate function definition + in one TU (`async_mutex`), a comment-only `if` body followed by `else` (`cond`, plain syntax + error), `.data()` called on a value that has no such member for the sequence concepts in play + (`test::buffer_to_string`), symbols (`route_params`, `route::next`) that exist in none of + capy, corosio, or burl (`io_task`), a mock object constructed outside the `fuse::armed` retry + loop so state leaked across rounds and the trailing comment went stale (`test::stream`, + `test::write_stream`), a discarded partial-write result under a concept that documents + partial writes as normal (`Stream`), and a redeclared local plus undeclared placeholder names + (`any_read_stream`, `any_write_stream`). + +None of this changes the recommendation. The DEFER rationale was about mechanism (no MrDocs +snippet-compile support, bespoke per-block scaffolding, a non-compiling-block convention to +invent) — a real defect rate does not make that mechanism appear. What it does is retire the +"reasonable without evidence" caveat the original recommendation carried: the deferred risk +was real, not just plausible, and this pass fixed the twelve found so far by hand rather than +by gate. If a future pass finds a similar rate again, that is the point to weigh option (b)'s +cost against a third hand-fix pass instead of re-deriving this analysis from scratch. + +Not fixed as part of this pass, and worth folding into the next one: `any_stream.hpp`'s +example has the same redeclared-local/undeclared-placeholder shape as `any_read_stream` and +`any_write_stream` above, but it was outside the audited list (R2 covers 12 blocks, not this +one) and so was left alone here to keep the fixed count matched to the audited evidence. diff --git a/doc/lint/baseline-diff.mjs b/doc/lint/baseline-diff.mjs new file mode 100644 index 000000000..eef429a49 --- /dev/null +++ b/doc/lint/baseline-diff.mjs @@ -0,0 +1,281 @@ +#!/usr/bin/env node +// +// 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 +// +// baseline-diff.mjs — explains what accepting a candidate doc/lint/baseline.json +// would change, so a maintainer reseeding the "no new violations" gate can tell +// "stale entries retired" from "a real new finding absorbed." +// +// Reseeding a baseline REWRITES the gate's reference point: every fingerprint the +// candidate adds becomes grandfathered forever, and every fingerprint it drops +// becomes newly reportable. That is the one operation in this toolchain that can +// silently un-gate a real regression, so it is never automatic — CI produces a +// candidate, this script explains it, and a human commits it. The reseed steps +// live in .github/workflows/docs.yml; the maintainer procedure is in +// doc/lint/README.md. +// +// Node built-ins only, no dependencies. +// +// Usage: +// node doc/lint/baseline-diff.mjs \ +// [--gate : ...] [--examples N] +// +// --gate takes the SAME specs as check-no-new-violations.mjs (split on the first +// ':', regex tested against the whole fingerprint). Pass the live gate spec and +// any added fingerprint that would have blocked a merge is reported separately, +// in full, and as a GitHub error annotation — those are the entries a reseed +// would grandfather away. The CI step derives these specs from the blocking step +// in .github/workflows/docs.yml rather than restating them, so they cannot rot +// apart; see the reseed steps there. +// +// --allow-emptied acknowledges that a gated check legitimately reached +// zero findings (its backlog is genuinely closed). Repeatable. Without it, a +// gated check that is empty in the candidate while non-empty in the committed +// baseline is FATAL — see emptiedGated below. +// +// Exit status: +// 0 candidate is explainable (it may still add ungated findings — read the report) +// 1 candidate must not be committed as-is. Three reasons, all fail-closed: +// * a check is `skipped` in it (a skipped check snapshots an empty slice, +// wiping that slice's grandfathered backlog), +// * a GATED check collapsed to zero findings without being marked skipped +// (same wipe, but arrives looking like success — see emptiedGated), +// * an added fingerprint matches the gate spec (a finding that would have +// blocked a merge is about to become grandfathered). +// Adding *ungated* findings is not by itself an error: those slices are still +// being worked down, so an intentional new backlog is legitimate. A gated +// addition never is without justification, and neither is an unverifiable check. +// +import fs from 'node:fs'; + +const argv = process.argv.slice(2); +const gateByCheck = new Map(); // check -> [RegExp] +const allowEmptied = new Set(); +const positional = []; +let examples = 5; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + let spec = null; + if (a === '--gate') spec = argv[++i]; + else if (a.startsWith('--gate=')) spec = a.slice('--gate='.length); + else if (a === '--examples') { examples = Number(argv[++i]); continue; } + else if (a.startsWith('--examples=')) { examples = Number(a.slice('--examples='.length)); continue; } + else if (a === '--allow-emptied') { allowEmptied.add(argv[++i]); continue; } + else if (a.startsWith('--allow-emptied=')) { allowEmptied.add(a.slice('--allow-emptied='.length)); continue; } + else { positional.push(a); continue; } + const idx = spec.indexOf(':'); + if (idx < 0) { + console.error(`--gate expects :, got: ${spec}`); + process.exit(2); + } + const check = spec.slice(0, idx); + if (!gateByCheck.has(check)) gateByCheck.set(check, []); + gateByCheck.get(check).push(new RegExp(spec.slice(idx + 1))); +} +if (positional.length !== 2) { + console.error('usage: baseline-diff.mjs [--gate spec ...] [--allow-emptied check ...] [--examples N]'); + process.exit(2); +} +const [committedPath, candidatePath] = positional; + +function load(p) { + try { + return JSON.parse(fs.readFileSync(p, 'utf8')); + } catch (e) { + console.error(`cannot read ${p}: ${e.message}`); + process.exit(1); + } +} +const committed = load(committedPath); +const candidate = load(candidatePath); + +// Which rule a fingerprint belongs to. The key SHAPE differs per check and is +// load-bearing (see baseline.mjs occurrenceKey): doc_lint is +// `rule:file:#N:message` (rule at the HEAD), the two Vale checks are +// `file:#N:Check.Name` (check name at the TAIL — the merge gate's +// `Capy\.PartHeadings$` is anchored on it), mrdocs_warnings is +// `file:#N:message`, a11y is `url:code:selector`. Group accordingly rather than +// guessing from the string. +const afterOccurrenceIndex = (fp) => { + const m = fp.match(/:#\d+:/); + return m ? fp.slice(m.index + m[0].length) : fp; +}; +function ruleOf(check, fp) { + switch (check) { + case 'vale_adoc': + case 'vale_docstrings': + return fp.slice(fp.lastIndexOf(':') + 1) || '(unparsed)'; + case 'doc_lint': + // sentence_length shares doc_lint's fingerprint shape (rule at the HEAD), and + // its three keys are the split a maintainer needs to see BEFORE reseeding: + // `C2` is the hard slice that a `--gate 'sentence_length:^C2:'` spec will make + // merge-blocking, `advisory-C2` is the design essays that never block, and + // `BACKTICK` is a tooling diagnostic, not a prose finding. Without this case + // all three collapsed into `(all)` and the reseed report hid exactly the + // distinction that decides what becomes gate-protected. + case 'sentence_length': { + const i = fp.indexOf(':'); + return i > 0 ? fp.slice(0, i) : '(unparsed)'; + } + case 'mrdocs_warnings': + // Collapse quoted identifiers so per-symbol findings group by warning class. + return afterOccurrenceIndex(fp).replace(/'[^']*'/g, "'…'").replace(/"[^"]*"/g, '"…"'); + case 'a11y': + return fp.split(':')[1] || '(unparsed)'; + default: + return '(all)'; + } +} + +const out = []; +const say = (s = '') => out.push(s); +const annotations = []; +const emptiedGated = []; +let fatal = false; + +say('=== doc-lint baseline candidate: what committing it would change ==='); +say(`committed: ${committedPath} (generatedAt ${committed.generatedAt ?? '?'})`); +say(`candidate: ${candidatePath} (generatedAt ${candidate.generatedAt ?? '?'})`); +say(); + +const checkNames = [...new Set([...Object.keys(committed.checks || {}), ...Object.keys(candidate.checks || {})])].sort(); +const rows = []; +const perCheck = new Map(); +for (const check of checkNames) { + const base = committed.checks?.[check] ?? {}; + const cand = candidate.checks?.[check] ?? {}; + const baseSet = new Set(base.fingerprints || []); + const candSet = new Set(cand.fingerprints || []); + const added = [...candSet].filter((fp) => !baseSet.has(fp)).sort(); + const removed = [...baseSet].filter((fp) => !candSet.has(fp)).sort(); + const gateRes = gateByCheck.get(check) || null; + const gatedAdded = gateRes ? added.filter((fp) => gateRes.some((re) => re.test(fp))) : []; + perCheck.set(check, { base, cand, added, removed, gateRes, gatedAdded }); + rows.push([ + check + (gateRes ? ' *' : ''), + cand.skipped ? 'SKIPPED' : String(cand.count ?? candSet.size), + base.skipped ? 'SKIPPED' : String(base.count ?? baseSet.size), + String(added.length), + String(removed.length), + ]); + if (cand.skipped) { + fatal = true; + annotations.push(`::error title=Baseline candidate unusable::check '${check}' is SKIPPED in the candidate (${cand.reason ?? 'no reason given'}). Committing it would wipe that check's grandfathered backlog. Fix the environment and re-run.`); + } else if (gateRes && candSet.size === 0 && baseSet.size > 0 && !allowEmptied.has(check)) { + // A GATED check reporting zero findings where the committed baseline has some + // is treated as a crash until proven otherwise. This is the fail-open that the + // `skipped` flag does NOT catch: a check that dies with empty stdout used to be + // recorded as `count: 0, skipped: false`, and the resulting candidate read + // "retires 214, grandfathers 0, none gated" — an actively reassuring report for + // a candidate that would wipe a merge-blocking check's entire backlog. + // (baseline.mjs now marks such crashes skipped; this is the independent + // second line, because it does not care WHY the slice is empty.) + // + // Emptiness, not a removal-fraction threshold: a crash produces exactly zero, + // never 40% fewer, so emptiness targets the real failure mode with no magic + // number and no arbitrary cliff. A percentage would fire on the very first + // legitimate reseed here (mrdocs_warnings drops 195 of 214 = 91%), training + // maintainers to wave it through — the worst outcome for a guard. The one + // legitimate zero, a gated backlog genuinely closing, is a milestone worth an + // explicit --allow-emptied , which records the decision in the run log. + fatal = true; + emptiedGated.push(check); + annotations.push(`::error title=Baseline candidate unusable::gated check '${check}' reports 0 findings but the committed baseline has ${baseSet.size}. A crashed check looks exactly like this. Verify the check really ran; if the backlog is genuinely closed, re-run with --allow-emptied ${check}.`); + } +} + +// Fixed-width table so before/after is skimmable in a job log. +const header = ['check', 'candidate', 'committed', 'added', 'removed']; +const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))); +const fmt = (cells) => cells.map((c, i) => (i === 0 ? c.padEnd(widths[i]) : c.padStart(widths[i]))).join(' '); +say('--- per-check counts (`*` = gated by the merge gate) ---'); +say(fmt(header)); +say(widths.map((w) => '-'.repeat(w)).join(' ')); +for (const r of rows) say(fmt(r)); +const totalAdded = [...perCheck.values()].reduce((n, v) => n + v.added.length, 0); +const totalRemoved = [...perCheck.values()].reduce((n, v) => n + v.removed.length, 0); +const totalGated = [...perCheck.values()].reduce((n, v) => n + v.gatedAdded.length, 0); +say(); +say(`TOTAL added ${totalAdded} removed ${totalRemoved} added-and-gated ${totalGated}`); +say(); + +function byRule(check, list) { + const groups = new Map(); + for (const fp of list) { + const rule = ruleOf(check, fp); + if (!groups.has(rule)) groups.set(rule, []); + groups.get(rule).push(fp); + } + return [...groups.entries()].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])); +} + +// ADDED is the dangerous direction: every one of these becomes grandfathered. +say('--- ADDED fingerprints by check and rule (these become grandfathered) ---'); +if (totalAdded === 0) say('(none)'); +for (const check of checkNames) { + const { added } = perCheck.get(check); + if (added.length === 0) continue; + say(`${check}: ${added.length} added`); + for (const [rule, list] of byRule(check, added)) { + say(` ${String(list.length).padStart(5)} ${rule}`); + for (const fp of list.slice(0, examples)) say(` e.g. ${fp}`); + if (list.length > examples) say(` ... and ${list.length - examples} more`); + } +} +say(); + +// REMOVED is the point of a reseed: retiring entries that are already fixed. +say('--- REMOVED fingerprints by check and rule (backlog being retired) ---'); +if (totalRemoved === 0) say('(none)'); +for (const check of checkNames) { + const { removed } = perCheck.get(check); + if (removed.length === 0) continue; + say(`${check}: ${removed.length} removed`); + for (const [rule, list] of byRule(check, removed)) say(` ${String(list.length).padStart(5)} ${rule}`); +} +say(); + +say('--- ADDED fingerprints that the merge gate WOULD have blocked ---'); +if (gateByCheck.size === 0) { + say('(no --gate spec passed; re-run with the gate spec from .github/workflows/docs.yml to see this)'); +} else if (totalGated === 0) { + say(`(none — no added fingerprint matches the gate spec: ${[...gateByCheck.entries()].map(([c, res]) => res.map((re) => `${c}:${re.source}`).join(' ')).join(' ')})`); +} else { + say(`${totalGated} added fingerprint(s) match the gate spec. Each is EITHER a real regression`); + say('you are about to grandfather away, OR an environment difference. Account for every one'); + say('before committing this candidate:'); + for (const check of checkNames) { + for (const fp of perCheck.get(check).gatedAdded) say(` - ${check} :: ${fp}`); + } + annotations.push(`::error title=Baseline candidate adds gated findings::${totalGated} added fingerprint(s) would have blocked the merge gate. Do not commit this candidate until each is explained.`); +} +say(); + +if (fatal) { + say('RESULT: candidate is NOT safe to commit. Do not use this file.'); + for (const check of checkNames) { + if (candidate.checks?.[check]?.skipped) say(` - ${check} is SKIPPED in the candidate: the check could not run at all.`); + } + for (const check of emptiedGated) { + say(` - ${check} is GATED and reports 0 findings against ${committed.checks?.[check]?.count ?? '?'} in the`); + say(' committed baseline. A crashed check looks exactly like this. Confirm the check really'); + say(` ran; if that backlog is genuinely closed, re-run with --allow-emptied ${check}.`); + } +} else if (totalGated > 0) { + say('RESULT: candidate must not be committed until its gated additions are justified (see above).'); +} else { + say(`RESULT: candidate retires ${totalRemoved} and grandfathers ${totalAdded} fingerprint(s), none gated.`); + if (allowEmptied.size > 0) say(`(--allow-emptied accepted a zero-finding gated check: ${[...allowEmptied].join(', ')})`); +} + +console.log(out.join('\n')); +if (process.env.GITHUB_ACTIONS) for (const a of annotations) console.log(a); +// Gated additions exit 1 too: the skip path already fails closed, and a green step +// beside a red annotation is how a warning gets skimmed past. An ungated addition +// alone is not an error (those slices are still being worked down). +process.exit(fatal || totalGated > 0 ? 1 : 0); diff --git a/doc/lint/baseline.json b/doc/lint/baseline.json new file mode 100644 index 000000000..0fdac1f23 --- /dev/null +++ b/doc/lint/baseline.json @@ -0,0 +1,4316 @@ +{ + "generatedAt": "2026-07-30T16:45:58.892Z", + "note": "Snapshot of current violations (Task 2, Style Guide Part F.0). Non-blocking: this records the backlog so a future comparator can flag NEW findings without failing on the ones already known about. Fingerprints are line-insensitive: the `#N` component is the Nth occurrence of that (file, message) pair, NOT a line number, so inserting text above a finding does not rename it.", + "checks": { + "vale_adoc": { + "count": 2441, + "skipped": false, + "fingerprints": [ + "modules/ROOT/nav.adoc:#10:Vale.Spelling", + "modules/ROOT/nav.adoc:#11:Vale.Spelling", + "modules/ROOT/nav.adoc:#12:Vale.Spelling", + "modules/ROOT/nav.adoc:#13:Vale.Spelling", + "modules/ROOT/nav.adoc:#14:Vale.Spelling", + "modules/ROOT/nav.adoc:#1:Vale.Spelling", + "modules/ROOT/nav.adoc:#2:Vale.Spelling", + "modules/ROOT/nav.adoc:#3:Vale.Spelling", + "modules/ROOT/nav.adoc:#4:Vale.Spelling", + "modules/ROOT/nav.adoc:#5:Vale.Spelling", + "modules/ROOT/nav.adoc:#6:Vale.Spelling", + "modules/ROOT/nav.adoc:#7:Vale.Spelling", + "modules/ROOT/nav.adoc:#8:Vale.Spelling", + "modules/ROOT/nav.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#1:Google.We", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#1:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#2:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#3:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#3:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#1:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#1:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#1:Google.WordList", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#2:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#2:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#3:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#4:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#5:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#6:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#7:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#8:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:Google.Colons", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:Google.Quotes", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#2:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#2:Google.Quotes", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#2:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#3:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#3:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#3:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#4:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#5:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#6:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#7:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#8:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#10:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#11:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#12:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#13:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#14:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#1:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#1:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#1:Google.WordList", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#2:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#2:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#2:Google.WordList", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#3:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#3:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#3:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#4:Capy.SimpleTense", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#4:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#4:Google.Will", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#5:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#6:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#7:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#8:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#9:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#10:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#11:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#12:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#13:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#14:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#15:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#16:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#1:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#2:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#3:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#4:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#5:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#6:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#7:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#8:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#9:Google.Headings", + "modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3.intro.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/3.concurrency/3.intro.adoc:#1:Google.Will", + "modules/ROOT/pages/3.concurrency/3.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3.intro.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3.intro.adoc:#2:Google.Will", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#10:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#11:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#1:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#1:Google.Will", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#1:Google.WordList", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#2:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#2:Google.Will", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#2:Google.WordList", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#3:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#3:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#3:Google.Will", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#4:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#5:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#6:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#7:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#8:Google.Headings", + "modules/ROOT/pages/3.concurrency/3a.foundations.adoc:#9:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#1:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#1:Google.Will", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#2:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#2:Google.Will", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#3:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#3:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#3:Google.Will", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#4:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#5:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#6:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#7:Google.Headings", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#10:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#11:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#1:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#1:Google.Latin", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#1:Google.Will", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#2:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#2:Google.Latin", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#2:Google.Will", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#3:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#4:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#5:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#6:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#7:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#8:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#9:Google.Headings", + "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#10:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#1:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#2:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#3:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#4:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#5:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#6:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#7:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#8:Google.Headings", + "modules/ROOT/pages/3.concurrency/3d.patterns.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#2:Google.Will", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4.intro.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#2:Capy.Terminology", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#2:Google.Will", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#10:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#11:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#12:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#13:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#14:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#15:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#16:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#1:Google.WordList", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#10:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#11:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#12:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#13:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#14:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#15:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#16:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Google.WordList", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#10:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#11:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#12:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#13:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#14:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#15:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#16:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#17:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#18:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#19:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#1:Google.WordList", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#20:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#21:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#22:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#23:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#24:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#25:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#26:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#27:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#28:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#2:Google.Will", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#2:Google.WordList", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#10:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#11:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#12:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#13:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#14:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#15:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#1:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#1:Google.Will", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#10:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#11:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#12:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#13:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#14:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#15:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#16:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#17:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#18:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#19:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Google.LyHyphens", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Google.WordList", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#20:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#2:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#2:Google.WordList", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#3:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#3:Google.WordList", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#4:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#4:Google.WordList", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#5:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#10:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#11:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#12:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#1:Google.EmDash", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#1:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#2:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#3:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#4:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#5:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#6:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#7:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#8:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#9:Google.Headings", + "modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#1:Google.Will", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#2:Google.Will", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5.intro.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#10:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#1:Google.FirstPerson", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#1:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#1:Google.WordList", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#2:Google.FirstPerson", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#2:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#3:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#4:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#5:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#6:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#7:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#8:Google.Headings", + "modules/ROOT/pages/5.buffers/5a.overview.adoc:#9:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#1:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#1:Google.LyHyphens", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#1:Google.WordList", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#2:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#2:Google.WordList", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#3:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#4:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#5:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#6:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#7:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#8:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#9:Google.Headings", + "modules/ROOT/pages/5.buffers/5b.types.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#10:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#11:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#1:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#2:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#3:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#4:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#5:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#6:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#7:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#8:Google.Headings", + "modules/ROOT/pages/5.buffers/5c.sequences.adoc:#9:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#10:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#11:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#12:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#13:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#14:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#15:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#16:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#1:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#2:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#3:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#4:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#5:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#6:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#7:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#8:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#9:Google.Headings", + "modules/ROOT/pages/5.buffers/5d.system-io.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#10:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#11:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#12:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#13:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#14:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#15:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#16:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#17:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#1:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#2:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#3:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#4:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#5:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#6:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#7:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#8:Google.Headings", + "modules/ROOT/pages/5.buffers/5e.algorithms.adoc:#9:Google.Headings", + "modules/ROOT/pages/6.streams/6.intro.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/6.streams/6.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/6.streams/6.intro.adoc:#1:Google.Will", + "modules/ROOT/pages/6.streams/6.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/6.streams/6.intro.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/6.streams/6.intro.adoc:#2:Google.Will", + "modules/ROOT/pages/6.streams/6.intro.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/6.streams/6.intro.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/6.streams/6.intro.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/6.streams/6.intro.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#1:Google.Headings", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#1:Google.WordList", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#2:Google.Headings", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#3:Google.Headings", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#4:Google.Headings", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#5:Google.Headings", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#6:Google.Headings", + "modules/ROOT/pages/6.streams/6a.overview.adoc:#7:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#10:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#11:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#12:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#1:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#2:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#3:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#4:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#5:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#6:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#7:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#8:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#9:Google.Headings", + "modules/ROOT/pages/6.streams/6b.streams.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#1:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#1:Google.WordList", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#2:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#3:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#4:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#5:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#6:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#7:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#8:Google.Headings", + "modules/ROOT/pages/6.streams/6f.isolation.adoc:#9:Google.Headings", + "modules/ROOT/pages/7.testing/7.intro.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/7.testing/7.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/7.testing/7.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/7.testing/7.intro.adoc:#2:Google.Headings", + "modules/ROOT/pages/7.testing/7.intro.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/7.testing/7.intro.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#10:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#11:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#12:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#13:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#14:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.EmDash", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.LyHyphens", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.Units", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.Will", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.WordList", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#2:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#3:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#4:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#5:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#6:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#7:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#8:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#9:Google.Headings", + "modules/ROOT/pages/7.testing/7a.drivers.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#10:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#11:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#12:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#1:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#2:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#3:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#4:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#5:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#6:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#7:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#8:Google.Headings", + "modules/ROOT/pages/7.testing/7b.mock-streams.adoc:#9:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#1:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#2:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#3:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#4:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#5:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#6:Google.Headings", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8.intro.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8a.hello-task.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8b.producer-consumer.adoc:#8:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8c.buffer-composition.adoc:#8:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#1:Google.Latin", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#10:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#11:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Google.Colons", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Google.FirstPerson", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Google.Units", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#8:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#9:Google.Headings", + "modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#1:Google.EmDash", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#1:Google.EmDash", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8k.strand-serialization.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#1:Google.EmDash", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#1:Google.Latin", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#2:Google.Will", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#8:Google.Headings", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8l.async-mutex.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#2:Google.Will", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#7:Google.Headings", + "modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc:#8:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#1:Google.Latin", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#6:Google.Headings", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8n.custom-executor.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8o.sender-bridge.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#1:Google.Headings", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#1:Google.Spacing", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#1:Google.Will", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#2:Google.Headings", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#2:Google.Spacing", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#3:Google.Headings", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#4:Google.Headings", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#5:Google.Headings", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9.intro.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#1:Google.We", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#2:Google.We", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#2:Google.WordList", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#3:Google.WordList", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Google.Colons", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Google.Ordinal", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Google.Spacing", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#2:Google.Spacing", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#2:Google.WordList", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#3:Google.WordList", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#59:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#60:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#61:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#62:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#63:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#64:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#65:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#66:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#67:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#68:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#69:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#70:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#71:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#72:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#73:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#74:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#75:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#76:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#77:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#78:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#79:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#80:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#81:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#82:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#83:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#84:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#85:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#86:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#87:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#88:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#89:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9b.Separation.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#10:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#11:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#12:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#13:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#14:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#15:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#16:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#17:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#18:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#19:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#1:Google.FirstPerson", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#1:Google.LyHyphens", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#20:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#2:Google.WordList", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9c.ReadStream.adoc:#9:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#10:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#11:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#12:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#13:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#14:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#15:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#1:Google.EmDash", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#1:Google.LyHyphens", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#2:Google.EmDash", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#9:Google.Headings", + "modules/ROOT/pages/9.design/9f.WriteStream.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#1:Google.EmDash", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#1:Google.Latin", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#2:Google.EmDash", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#2:Google.Latin", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#3:Google.Latin", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#10:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#10:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#11:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#11:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#12:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#13:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#14:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#15:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#16:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#17:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#18:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#19:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Google.Colons", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Google.FirstPerson", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Google.Latin", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#2:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#3:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#4:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#59:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#5:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#60:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#61:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#62:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#63:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#64:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#65:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#66:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#67:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#68:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#69:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#6:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#70:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#71:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#72:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#73:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#74:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#75:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#76:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#77:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#78:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#79:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#7:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#80:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#8:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#9:Google.EmDash", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#9:Google.Headings", + "modules/ROOT/pages/9.design/9k.Executor.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#10:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#10:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#11:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#12:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Google.FirstPerson", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Google.Will", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#2:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#2:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#3:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#3:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#4:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#4:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#5:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#5:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#6:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#6:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#7:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#7:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#8:Capy.Terminology", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#8:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#9:Google.EmDash", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#9:Google.Headings", + "modules/ROOT/pages/9.design/9l.RunApi.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#100:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#101:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#102:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#103:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#104:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#105:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#106:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#107:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#108:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#109:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#10:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#10:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#10:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#110:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#111:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#112:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#113:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#114:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#115:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#116:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#117:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#118:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#119:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#11:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#11:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#120:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#121:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#122:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#123:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#124:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#125:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#126:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#127:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#128:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#129:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#12:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#12:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#130:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#131:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#132:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#133:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#134:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#135:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#136:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#137:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#138:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#139:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#13:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#13:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#140:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#141:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#142:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#143:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#144:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#145:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#146:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#147:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#148:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#149:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#14:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#14:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#150:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#151:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#152:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#153:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#154:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#155:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#156:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#157:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#15:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#16:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Google.Spacing", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Google.We", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Google.Spacing", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Google.We", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Google.WordList", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#3:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#3:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#3:Google.Spacing", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#3:Google.We", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#4:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#4:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#4:Google.We", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#59:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#5:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#5:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#60:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#61:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#62:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#63:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#64:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#65:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#66:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#67:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#68:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#69:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#6:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#6:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#70:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#71:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#72:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#73:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#74:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#75:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#76:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#77:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#78:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#79:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#7:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#7:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#80:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#81:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#82:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#83:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#84:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#85:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#86:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#87:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#88:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#89:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#8:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#8:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#90:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#91:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#92:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#93:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#94:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#95:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#96:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#97:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#98:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#99:Vale.Spelling", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#9:Google.EmDash", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#9:Google.Headings", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#9:Google.Quotes", + "modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Google.EmDash", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Google.Latin", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Google.OxfordComma", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Google.Will", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#2:Google.EmDash", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#2:Google.Will", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#3:Google.EmDash", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#4:Google.EmDash", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#59:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#60:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#61:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#62:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#63:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#64:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#65:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#66:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#67:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#100:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#101:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#102:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#103:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#104:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#105:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#106:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#107:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#108:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#109:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#10:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#110:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#111:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#112:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#113:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#114:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#115:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#116:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#11:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#12:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#13:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#14:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#15:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#16:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#1:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#1:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#1:Google.WordList", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#2:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#2:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#2:Google.WordList", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#3:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#3:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#3:Google.WordList", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#4:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#4:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#59:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#5:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#5:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#60:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#61:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#62:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#63:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#64:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#65:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#66:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#67:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#68:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#69:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#6:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#6:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#70:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#71:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#72:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#73:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#74:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#75:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#76:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#77:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#78:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#79:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#7:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#7:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#80:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#81:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#82:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#83:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#84:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#85:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#86:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#87:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#88:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#89:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#8:Google.Colons", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#8:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#90:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#91:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#92:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#93:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#94:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#95:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#96:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#97:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#98:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#99:Vale.Spelling", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#9:Google.Headings", + "modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/A.intro.adoc:#1:Google.Headings", + "modules/ROOT/pages/A.specification-methods/Ab.cancellation.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#1:Google.Will", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#10:Google.Headings", + "modules/ROOT/pages/index.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#11:Google.Headings", + "modules/ROOT/pages/index.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#12:Google.Headings", + "modules/ROOT/pages/index.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#1:Google.Headings", + "modules/ROOT/pages/index.adoc:#1:Google.Spacing", + "modules/ROOT/pages/index.adoc:#1:Google.WordList", + "modules/ROOT/pages/index.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#2:Google.Headings", + "modules/ROOT/pages/index.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#3:Google.Headings", + "modules/ROOT/pages/index.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#4:Google.Headings", + "modules/ROOT/pages/index.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#5:Google.Headings", + "modules/ROOT/pages/index.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#6:Google.Headings", + "modules/ROOT/pages/index.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#7:Google.Headings", + "modules/ROOT/pages/index.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#8:Google.Headings", + "modules/ROOT/pages/index.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/index.adoc:#9:Google.Headings", + "modules/ROOT/pages/index.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/quick-start.adoc:#1:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#1:Google.Will", + "modules/ROOT/pages/quick-start.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/quick-start.adoc:#2:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#2:Google.Will", + "modules/ROOT/pages/quick-start.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#3:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#4:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#5:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#6:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#7:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#8:Google.Headings", + "modules/ROOT/pages/quick-start.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/quick-start.adoc:#9:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#10:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#10:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#11:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#12:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#13:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#14:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#15:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#16:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#17:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#18:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#19:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#1:Capy.SimpleTense", + "modules/ROOT/pages/why-capy.adoc:#1:Capy.Terminology", + "modules/ROOT/pages/why-capy.adoc:#1:Google.EmDash", + "modules/ROOT/pages/why-capy.adoc:#1:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#1:Google.Spacing", + "modules/ROOT/pages/why-capy.adoc:#1:Google.We", + "modules/ROOT/pages/why-capy.adoc:#1:Google.Will", + "modules/ROOT/pages/why-capy.adoc:#1:Google.WordList", + "modules/ROOT/pages/why-capy.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#20:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#21:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#22:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#23:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#24:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#25:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#26:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#27:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#28:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#29:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#2:Capy.SimpleTense", + "modules/ROOT/pages/why-capy.adoc:#2:Capy.Terminology", + "modules/ROOT/pages/why-capy.adoc:#2:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#2:Google.Spacing", + "modules/ROOT/pages/why-capy.adoc:#2:Google.We", + "modules/ROOT/pages/why-capy.adoc:#2:Google.Will", + "modules/ROOT/pages/why-capy.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#30:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#31:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#32:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#33:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#34:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#35:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#36:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#37:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#38:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#39:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#3:Capy.SimpleTense", + "modules/ROOT/pages/why-capy.adoc:#3:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#3:Google.Spacing", + "modules/ROOT/pages/why-capy.adoc:#3:Google.We", + "modules/ROOT/pages/why-capy.adoc:#3:Google.Will", + "modules/ROOT/pages/why-capy.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#40:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#41:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#42:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#43:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#44:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#45:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#46:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#47:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#48:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#49:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#4:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#50:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#51:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#52:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#53:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#54:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#55:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#56:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#57:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#58:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#59:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#5:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#5:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#60:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#61:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#62:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#63:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#64:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#65:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#66:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#67:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#68:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#69:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#6:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#6:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#70:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#71:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#72:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#73:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#74:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#75:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#76:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#77:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#78:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#79:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#7:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#7:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#80:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#81:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#82:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#83:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#8:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#8:Vale.Spelling", + "modules/ROOT/pages/why-capy.adoc:#9:Google.Headings", + "modules/ROOT/pages/why-capy.adoc:#9:Vale.Spelling" + ] + }, + "vale_docstrings": { + "count": 1477, + "skipped": false, + "fingerprints": [ + "lint/.docstrings/buffers.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/buffers.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/buffers.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/asio.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/asio.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/buffers/asio.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/buffers/asio.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/buffers/asio.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/buffers/buffer_copy.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/buffer_copy.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/buffer_copy.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/buffer_copy.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/buffer_copy.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/buffers/buffer_copy.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/buffers/buffer_param.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers/buffer_slice.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/buffers/consuming_buffers.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/consuming_buffers.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/buffers/consuming_buffers.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/consuming_buffers.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/consuming_buffers.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/front.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/front.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/front.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/front.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/front.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#12:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#13:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#14:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#15:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#16:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#17:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#18:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#19:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#1:Google.LyHyphens", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#20:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#21:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#22:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#23:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#2:Google.LyHyphens", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/buffers/make_buffer.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/concept/decomposes_to.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/concept/execution_context.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/concept/execution_context.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/execution_context.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/concept/executor.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/concept/executor.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/concept/executor.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/concept/executor.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#1:Capy.Terminology", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/concept/io_awaitable.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#1:Capy.Terminology", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#2:Capy.Terminology", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/concept/io_runnable.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/concept/read_stream.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/concept/write_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/concept/write_stream.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/cond.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/cond.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/cond.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/cond.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/continuation.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/continuation.hpp.adoc:#1:Google.LyHyphens", + "lint/.docstrings/continuation.hpp.adoc:#1:Google.OxfordComma", + "lint/.docstrings/continuation.hpp.adoc:#1:Google.WordList", + "lint/.docstrings/continuation.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#2:Google.EmDash", + "lint/.docstrings/continuation.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#3:Google.EmDash", + "lint/.docstrings/continuation.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/continuation.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#2:Google.Latin", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#3:Google.Latin", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/detail/await_suspend_helper.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/detail/buffer_array.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/detail/frame_memory_resource.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/detail/frame_memory_resource.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/detail/frame_memory_resource.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/detail/frame_memory_resource.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/detail/intrusive.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/detail/intrusive.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/detail/intrusive.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/detail/intrusive.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/detail/intrusive.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/detail/slice_of.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/detail/slice_of.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/detail/slice_of.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/detail/slice_of.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/detail/slice_of.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/detail/thread_local_ptr.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/error.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/error.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/error.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/error.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/error.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/error.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#1:Capy.NoFluff", + "lint/.docstrings/ex/any_executor.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/any_executor.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/any_executor.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/any_executor.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/any_executor.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/any_executor.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/async_event.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/async_event.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/async_event.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/async_event.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/async_event.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/async_mutex.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/async_waker.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/async_waker.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/detail/strand_service.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/execution_context.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/ex/execution_context.hpp.adoc:#1:Google.WordList", + "lint/.docstrings/ex/execution_context.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/execution_context.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/ex/execution_context.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#1:Capy.NoFluff", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/executor_ref.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/frame_alloc_mixin.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#1:Capy.Terminology", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#3:Capy.SimpleTense", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/frame_allocator.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#12:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#13:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#14:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#15:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#16:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#17:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#18:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#19:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/immediate.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/ex/immediate.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/io_awaitable_promise_base.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/io_env.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/recycling_memory_resource.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#12:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#13:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#14:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#15:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#16:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#17:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#18:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#19:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#20:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#21:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#22:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#23:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#24:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#25:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#26:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#27:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#28:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#29:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#30:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#31:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#32:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/run.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/ex/run.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#12:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#13:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#14:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#15:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#16:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#17:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#18:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#19:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/run_async.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/run_async.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#20:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#21:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#22:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#23:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#24:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#25:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#26:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#27:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#28:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#29:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#30:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#31:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#32:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#33:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#34:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#35:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#36:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#37:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#38:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#39:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#40:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#41:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#42:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#43:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#44:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#45:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#46:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#46:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#47:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#47:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#48:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#48:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#49:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#49:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#50:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#50:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#51:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#52:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#53:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#54:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#55:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#56:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#57:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#58:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#59:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#60:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#61:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#62:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#63:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#64:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#65:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#66:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#67:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#68:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#69:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/run_async.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/ex/run_async.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/strand.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/ex/strand.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/strand.hpp.adoc:#1:Google.WordList", + "lint/.docstrings/ex/strand.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/ex/strand.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#2:Google.EmDash", + "lint/.docstrings/ex/strand.hpp.adoc:#2:Google.Will", + "lint/.docstrings/ex/strand.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/ex/strand.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/strand.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/ex/this_coro.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#2:Google.Latin", + "lint/.docstrings/ex/this_coro.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#3:Google.Latin", + "lint/.docstrings/ex/this_coro.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#46:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#47:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#48:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#4:Google.Latin", + "lint/.docstrings/ex/this_coro.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/this_coro.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#1:Google.Quotes", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#2:Google.Quotes", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#2:Google.Will", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#3:Capy.SimpleTense", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/ex/thread_pool.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/ex/work_guard.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/ex/work_guard.hpp.adoc:#1:Google.Will", + "lint/.docstrings/ex/work_guard.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/ex/work_guard.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/ex/work_guard.hpp.adoc:#2:Google.Will", + "lint/.docstrings/ex/work_guard.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/ex/work_guard.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/ex/work_guard.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/ex/work_guard.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/ex/work_guard.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#1:Google.Will", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/io/any_read_stream.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/io/any_stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/io/any_stream.hpp.adoc:#1:Google.Will", + "lint/.docstrings/io/any_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/io/any_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/io/any_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/io/any_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/io/any_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/io/any_stream.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#1:Google.Will", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/io/any_write_stream.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/io/write_now.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/io/write_now.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/io/write_now.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/io/write_now.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/io/write_now.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/io/write_now.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/io/write_now.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/io/write_now.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/io_result.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/io_result.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/io_task.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/io_task.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/io_task.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/quitter.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/quitter.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/quitter.hpp.adoc:#1:Google.Spacing", + "lint/.docstrings/quitter.hpp.adoc:#1:Google.Will", + "lint/.docstrings/quitter.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/quitter.hpp.adoc:#2:Google.EmDash", + "lint/.docstrings/quitter.hpp.adoc:#2:Google.Spacing", + "lint/.docstrings/quitter.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/quitter.hpp.adoc:#3:Google.EmDash", + "lint/.docstrings/quitter.hpp.adoc:#3:Google.Spacing", + "lint/.docstrings/quitter.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#46:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#47:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#48:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#49:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#50:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#51:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#52:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/quitter.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/read.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/read.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/read.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/read.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/read.hpp.adoc:#2:Google.EmDash", + "lint/.docstrings/read.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/read.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/read_at_least.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/read_at_least.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/read_at_least.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/read_at_least.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/read_at_least.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/read_at_least.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/read_at_least.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#46:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#47:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#48:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#49:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#50:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#51:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#52:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#53:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#54:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#55:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#56:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#57:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#58:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#59:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#60:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#61:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#62:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#63:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#64:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#65:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/task.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/task.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/test.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/buffer_to_string.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/buffer_to_string.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/bufgrind.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/test/bufgrind.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/test/bufgrind.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/test/bufgrind.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/test/bufgrind.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#12:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#13:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/test/fuse.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/test/fuse.hpp.adoc:#1:Google.Will", + "lint/.docstrings/test/fuse.hpp.adoc:#1:Google.WordList", + "lint/.docstrings/test/fuse.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#2:Capy.SimpleTense", + "lint/.docstrings/test/fuse.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#2:Google.Will", + "lint/.docstrings/test/fuse.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#3:Capy.SimpleTense", + "lint/.docstrings/test/fuse.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#3:Google.Will", + "lint/.docstrings/test/fuse.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#4:Capy.SimpleTense", + "lint/.docstrings/test/fuse.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#4:Google.Will", + "lint/.docstrings/test/fuse.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/test/fuse.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/test/fuse.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/test/read_stream.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/test/read_stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/read_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/read_stream.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/test/read_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/test/read_stream.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/test/read_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/test/read_stream.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/test/read_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/test/read_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#11:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#12:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#13:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#14:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#15:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#16:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#17:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#18:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#19:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#1:Google.WordList", + "lint/.docstrings/test/run_blocking.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#20:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#21:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#22:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#23:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#24:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#25:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#26:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#27:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#28:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#29:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#2:Google.WordList", + "lint/.docstrings/test/run_blocking.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#30:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#3:Google.WordList", + "lint/.docstrings/test/run_blocking.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#46:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#47:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#48:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#4:Google.WordList", + "lint/.docstrings/test/run_blocking.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/test/run_blocking.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/test/run_blocking.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#10:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/test/stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/test/stream.hpp.adoc:#1:Google.WordList", + "lint/.docstrings/test/stream.hpp.adoc:#1:Vale.Repetition", + "lint/.docstrings/test/stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#2:Google.WordList", + "lint/.docstrings/test/stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#7:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#8:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/test/stream.hpp.adoc:#9:Google.Colons", + "lint/.docstrings/test/stream.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/test/thread_name.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/write_stream.hpp.adoc:#1:Capy.SimpleTense", + "lint/.docstrings/test/write_stream.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/test/write_stream.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/test/write_stream.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/test/write_stream.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/test/write_stream.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/test/write_stream.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/test/write_stream.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/test/write_stream.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/test/write_stream.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/test/write_stream.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/when_all.hpp.adoc:#1:Google.LyHyphens", + "lint/.docstrings/when_all.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/when_all.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/when_all.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#40:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#41:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#42:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#43:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#44:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#45:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#46:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#47:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#48:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#49:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/when_all.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#50:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#51:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#52:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#53:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#54:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#55:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#56:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#57:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#58:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#59:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#5:Google.Colons", + "lint/.docstrings/when_all.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#60:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#61:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#62:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#63:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#64:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#6:Google.Colons", + "lint/.docstrings/when_all.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/when_all.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#10:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#11:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#12:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#13:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#14:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#15:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#16:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#17:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#18:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#19:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/when_any.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/when_any.hpp.adoc:#1:Google.Latin", + "lint/.docstrings/when_any.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#20:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#21:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#22:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#23:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#24:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#25:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#26:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#27:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#28:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#29:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/when_any.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#30:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#31:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#32:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#33:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#34:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#35:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#36:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#37:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#38:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#39:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/when_any.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#4:Google.Colons", + "lint/.docstrings/when_any.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#6:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#7:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#8:Vale.Spelling", + "lint/.docstrings/when_any.hpp.adoc:#9:Vale.Spelling", + "lint/.docstrings/write.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/write.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/write.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/write.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/write.hpp.adoc:#2:Google.EmDash", + "lint/.docstrings/write.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/write.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/write.hpp.adoc:#4:Vale.Spelling", + "lint/.docstrings/write.hpp.adoc:#5:Vale.Spelling", + "lint/.docstrings/write_at_least.hpp.adoc:#1:Google.Colons", + "lint/.docstrings/write_at_least.hpp.adoc:#1:Google.EmDash", + "lint/.docstrings/write_at_least.hpp.adoc:#1:Vale.Spelling", + "lint/.docstrings/write_at_least.hpp.adoc:#2:Google.Colons", + "lint/.docstrings/write_at_least.hpp.adoc:#2:Vale.Spelling", + "lint/.docstrings/write_at_least.hpp.adoc:#3:Google.Colons", + "lint/.docstrings/write_at_least.hpp.adoc:#3:Vale.Spelling", + "lint/.docstrings/write_at_least.hpp.adoc:#4:Vale.Spelling" + ] + }, + "doc_lint": { + "count": 70, + "skipped": false, + "byRule": { + "A1": 64, + "A6": 0, + "B2": 0, + "D2": 6 + }, + "fingerprints": [ + "A1:2.cpp20-coroutines/2.intro.adoc:#1:no :page-mode: attribute", + "A1:2.cpp20-coroutines/2a.foundations.adoc:#1:no :page-mode: attribute", + "A1:2.cpp20-coroutines/2b.syntax.adoc:#1:no :page-mode: attribute", + "A1:2.cpp20-coroutines/2c.machinery.adoc:#1:no :page-mode: attribute", + "A1:2.cpp20-coroutines/2d.advanced.adoc:#1:no :page-mode: attribute", + "A1:3.concurrency/3.intro.adoc:#1:no :page-mode: attribute", + "A1:3.concurrency/3a.foundations.adoc:#1:no :page-mode: attribute", + "A1:3.concurrency/3b.synchronization.adoc:#1:no :page-mode: attribute", + "A1:3.concurrency/3c.advanced.adoc:#1:no :page-mode: attribute", + "A1:3.concurrency/3d.patterns.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4.intro.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4a.tasks.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4b.launching.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4c.executors.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4d.io-awaitable.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4e.cancellation.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4f.composition.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4g.allocators.adoc:#1:no :page-mode: attribute", + "A1:4.coroutines/4h.lambda-captures.adoc:#1:no :page-mode: attribute", + "A1:5.buffers/5.intro.adoc:#1:no :page-mode: attribute", + "A1:5.buffers/5a.overview.adoc:#1:no :page-mode: attribute", + "A1:5.buffers/5b.types.adoc:#1:no :page-mode: attribute", + "A1:5.buffers/5c.sequences.adoc:#1:no :page-mode: attribute", + "A1:5.buffers/5d.system-io.adoc:#1:no :page-mode: attribute", + "A1:5.buffers/5e.algorithms.adoc:#1:no :page-mode: attribute", + "A1:6.streams/6.intro.adoc:#1:no :page-mode: attribute", + "A1:6.streams/6a.overview.adoc:#1:no :page-mode: attribute", + "A1:6.streams/6b.streams.adoc:#1:no :page-mode: attribute", + "A1:6.streams/6f.isolation.adoc:#1:no :page-mode: attribute", + "A1:7.testing/7.intro.adoc:#1:no :page-mode: attribute", + "A1:7.testing/7a.drivers.adoc:#1:no :page-mode: attribute", + "A1:7.testing/7b.mock-streams.adoc:#1:no :page-mode: attribute", + "A1:7.testing/7e.buffer-inspection.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8.intro.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8a.hello-task.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8b.producer-consumer.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8c.buffer-composition.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8d.mock-stream-testing.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8e.type-erased-echo.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8f.timeout-cancellation.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8g.parallel-fetch.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8k.strand-serialization.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8l.async-mutex.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8m.parallel-tasks.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8n.custom-executor.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8o.sender-bridge.adoc:#1:no :page-mode: attribute", + "A1:8.examples/8p.asio-use-capy.adoc:#1:no :page-mode: attribute", + "A1:9.design/9.intro.adoc:#1:no :page-mode: attribute", + "A1:9.design/9a.CapyLayering.adoc:#1:no :page-mode: attribute", + "A1:9.design/9b.Separation.adoc:#1:no :page-mode: attribute", + "A1:9.design/9c.ReadStream.adoc:#1:no :page-mode: attribute", + "A1:9.design/9f.WriteStream.adoc:#1:no :page-mode: attribute", + "A1:9.design/9i.TypeEraseAwaitable.adoc:#1:no :page-mode: attribute", + "A1:9.design/9k.Executor.adoc:#1:no :page-mode: attribute", + "A1:9.design/9l.RunApi.adoc:#1:no :page-mode: attribute", + "A1:9.design/9m.WhyNotCobalt.adoc:#1:no :page-mode: attribute", + "A1:9.design/9n.WhyNotCobaltConcepts.adoc:#1:no :page-mode: attribute", + "A1:9.design/9o.WhyNotTMC.adoc:#1:no :page-mode: attribute", + "A1:A.specification-methods/A.intro.adoc:#1:no :page-mode: attribute", + "A1:A.specification-methods/Ab.cancellation.adoc:#1:no :page-mode: attribute", + "A1:A.specification-methods/Ac.contingencies.adoc:#1:no :page-mode: attribute", + "A1:index.adoc:#1:no :page-mode: attribute", + "A1:quick-start.adoc:#1:no :page-mode: attribute", + "A1:why-capy.adoc:#1:no :page-mode: attribute", + "D2:2.cpp20-coroutines/2.intro.adoc:#1:tutorial/concept page has no include::example$", + "D2:3.concurrency/3.intro.adoc:#1:tutorial/concept page has no include::example$", + "D2:4.coroutines/4.intro.adoc:#1:tutorial/concept page has no include::example$", + "D2:5.buffers/5.intro.adoc:#1:tutorial/concept page has no include::example$", + "D2:6.streams/6.intro.adoc:#1:tutorial/concept page has no include::example$", + "D2:7.testing/7.intro.adoc:#1:tutorial/concept page has no include::example$" + ] + }, + "mrdocs_warnings": { + "count": 214, + "skipped": false, + "fingerprints": [ + "?:#1:unsupported HTML tag ", + "?:#2:unsupported HTML tag ", + "?:#3:unsupported HTML tag ", + "?:#4:unsupported HTML tag ", + "?:#5:unsupported HTML tag ", + "?:#6:unsupported HTML tag ", + "?:#7:unsupported HTML tag ", + "?:#8:unsupported HTML tag ", + "include/boost/capy/buffers.hpp:#1:begin: variable is undocumented", + "include/boost/capy/buffers.hpp:#1:boost::capy::buffer_length: Missing documentation for parameter 'bs'", + "include/boost/capy/buffers.hpp:#1:boost::capy::buffer_length: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::const_buffer: 1st parameter is unnamed", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::const_buffer: Missing documentation for parameter 'b'", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::const_buffer: Missing documentation for parameter 'data'", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::const_buffer: Missing documentation for parameter 'size'", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::data: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::operator+=: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::operator=: Missing documentation for parameter 'other'", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::operator=: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::const_buffer::size: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::data: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::mutable_buffer: 1st parameter is unnamed", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::mutable_buffer: Missing documentation for parameter 'data'", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::mutable_buffer: Missing documentation for parameter 'size'", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::operator+=: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::operator=: 1st parameter is unnamed", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::operator=: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:boost::capy::mutable_buffer::size: Missing documentation for return value", + "include/boost/capy/buffers.hpp:#1:buffer_empty: variable is undocumented", + "include/boost/capy/buffers.hpp:#1:buffer_size: variable is undocumented", + "include/boost/capy/buffers.hpp:#1:end: variable is undocumented", + "include/boost/capy/buffers/buffer_copy.hpp:#1:buffer_copy: variable is undocumented", + "include/boost/capy/buffers/buffer_param.hpp:#1:arr_: variable is undocumented", + "include/boost/capy/buffers/buffer_param.hpp:#1:buffer_param: guide is undocumented", + "include/boost/capy/buffers/buffer_param.hpp:#1:dummy_: variable is undocumented", + "include/boost/capy/buffers/consuming_buffers.hpp:#1:boost::capy::consuming_buffers::consuming_buffers: 1st parameter is unnamed", + "include/boost/capy/buffers/consuming_buffers.hpp:#1:boost::capy::consuming_buffers::data: Missing documentation for return value", + "include/boost/capy/buffers/consuming_buffers.hpp:#1:consuming_buffers: guide is undocumented", + "include/boost/capy/buffers/front.hpp:#1:front: variable is undocumented", + "include/boost/capy/buffers/make_buffer.hpp:#1:boost::capy::make_buffer: Missing documentation for parameter 'data'", + "include/boost/capy/buffers/make_buffer.hpp:#1:boost::capy::make_buffer: Missing documentation for return value", + "include/boost/capy/buffers/make_buffer.hpp:#2:boost::capy::make_buffer: Missing documentation for parameter 'data'", + "include/boost/capy/buffers/make_buffer.hpp:#2:boost::capy::make_buffer: Missing documentation for return value", + "include/boost/capy/concept/buffer_archetype.hpp:#1:boost::capy::const_buffer_archetype_::operator const_buffer: Missing documentation for return value", + "include/boost/capy/concept/buffer_archetype.hpp:#1:boost::capy::mutable_buffer_archetype_::operator const_buffer: Missing documentation for return value", + "include/boost/capy/concept/buffer_archetype.hpp:#1:boost::capy::mutable_buffer_archetype_::operator mutable_buffer: Missing documentation for return value", + "include/boost/capy/concept/buffer_archetype.hpp:#1:const_buffer_archetype: typedef is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#1:const_buffer_archetype_: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#1:mutable_buffer_archetype: typedef is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#1:mutable_buffer_archetype_: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#1:operator=: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#2:const_buffer_archetype_: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#2:mutable_buffer_archetype_: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#2:operator=: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#3:const_buffer_archetype_: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#3:mutable_buffer_archetype_: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#3:operator=: function is undocumented", + "include/boost/capy/concept/buffer_archetype.hpp:#4:operator=: function is undocumented", + "include/boost/capy/cond.hpp:#1:boost::capy::make_error_condition: Missing documentation for parameter 'ev'", + "include/boost/capy/cond.hpp:#1:boost::capy::make_error_condition: Missing documentation for return value", + "include/boost/capy/continuation.hpp:#1:h: variable is undocumented", + "include/boost/capy/continuation.hpp:#1:reserved: variable is undocumented", + "include/boost/capy/detail/intrusive.hpp:#1:boost::capy::detail::intrusive_list: Failed to resolve reference to 'node'", + "include/boost/capy/error.hpp:#1:boost::capy::make_error_code: Missing documentation for parameter 'ev'", + "include/boost/capy/error.hpp:#1:boost::capy::make_error_code: Missing documentation for return value", + "include/boost/capy/ex/any_executor.hpp:#1:boost::capy::any_executor::any_executor: 1st parameter is unnamed", + "include/boost/capy/ex/any_executor.hpp:#1:boost::capy::any_executor::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/any_executor.hpp:#1:boost::capy::any_executor::operator=: Missing documentation for return value", + "include/boost/capy/ex/async_event.hpp:#1:await_ready: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#1:await_resume: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#1:boost::capy::async_event::async_event: 1st parameter is unnamed", + "include/boost/capy/ex/async_event.hpp:#1:boost::capy::async_event::is_set: Missing documentation for return value", + "include/boost/capy/ex/async_event.hpp:#1:boost::capy::async_event::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/async_event.hpp:#1:boost::capy::async_event::wait_awaiter::await_suspend: Missing documentation for parameter 'env'", + "include/boost/capy/ex/async_event.hpp:#1:boost::capy::async_event::wait_awaiter::await_suspend: Missing documentation for parameter 'h'", + "include/boost/capy/ex/async_event.hpp:#1:boost::capy::async_event::wait_awaiter::await_suspend: Missing documentation for return value", + "include/boost/capy/ex/async_event.hpp:#1:operator=: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#1:wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#1:~wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#2:boost::capy::async_event::async_event: 1st parameter is unnamed", + "include/boost/capy/ex/async_event.hpp:#2:boost::capy::async_event::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/async_event.hpp:#2:operator=: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#2:wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_event.hpp:#3:wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:await_ready: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:await_resume: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::async_mutex: 1st parameter is unnamed", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::is_locked: Missing documentation for return value", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::lock_awaiter::await_suspend: Missing documentation for parameter 'env'", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::lock_awaiter::await_suspend: Missing documentation for parameter 'h'", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::lock_awaiter::await_suspend: Missing documentation for return value", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::lock_guard_awaiter::await_suspend: Missing documentation for parameter 'env'", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::lock_guard_awaiter::await_suspend: Missing documentation for parameter 'h'", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::lock_guard_awaiter::await_suspend: Missing documentation for return value", + "include/boost/capy/ex/async_mutex.hpp:#1:boost::capy::async_mutex::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/async_mutex.hpp:#1:lock_awaiter: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:lock_guard: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:lock_guard_awaiter: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:operator=: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:~lock_awaiter: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#1:~lock_guard: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#2:await_ready: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#2:await_resume: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#2:boost::capy::async_mutex::async_mutex: 1st parameter is unnamed", + "include/boost/capy/ex/async_mutex.hpp:#2:boost::capy::async_mutex::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/async_mutex.hpp:#2:lock_awaiter: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#2:lock_guard: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#2:operator=: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#3:lock_awaiter: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#3:lock_guard: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#3:operator=: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#4:lock_guard: function is undocumented", + "include/boost/capy/ex/async_mutex.hpp:#4:operator=: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#1:await_resume: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#1:boost::capy::async_waker::async_waker: 1st parameter is unnamed", + "include/boost/capy/ex/async_waker.hpp:#1:boost::capy::async_waker::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/async_waker.hpp:#1:boost::capy::async_waker::wait_awaiter::await_ready: Missing documentation for return value", + "include/boost/capy/ex/async_waker.hpp:#1:boost::capy::async_waker::wait_awaiter::await_suspend: Missing documentation for parameter 'env'", + "include/boost/capy/ex/async_waker.hpp:#1:boost::capy::async_waker::wait_awaiter::await_suspend: Missing documentation for parameter 'h'", + "include/boost/capy/ex/async_waker.hpp:#1:boost::capy::async_waker::wait_awaiter::await_suspend: Missing documentation for return value", + "include/boost/capy/ex/async_waker.hpp:#1:operator=: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#1:wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#1:~wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#2:boost::capy::async_waker::async_waker: 1st parameter is unnamed", + "include/boost/capy/ex/async_waker.hpp:#2:boost::capy::async_waker::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/async_waker.hpp:#2:operator=: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#2:wait_awaiter: function is undocumented", + "include/boost/capy/ex/async_waker.hpp:#3:wait_awaiter: function is undocumented", + "include/boost/capy/ex/execution_context.hpp:#1:boost::capy::execution_context::execution_context: 1st parameter is unnamed", + "include/boost/capy/ex/execution_context.hpp:#1:execution_context: function is undocumented", + "include/boost/capy/ex/execution_context.hpp:#1:operator=: function is undocumented", + "include/boost/capy/ex/execution_context.hpp:#1:service: function is undocumented", + "include/boost/capy/ex/execution_context.hpp:#1:~service: function is undocumented", + "include/boost/capy/ex/executor_ref.hpp:#1:boost::capy::executor_ref::executor_ref: 1st parameter is unnamed", + "include/boost/capy/ex/executor_ref.hpp:#1:boost::capy::executor_ref::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/executor_ref.hpp:#1:boost::capy::executor_ref::operator=: Missing documentation for return value", + "include/boost/capy/ex/executor_ref.hpp:#1:executor_ref: function is undocumented", + "include/boost/capy/ex/frame_alloc_mixin.hpp:#1:boost::capy::frame_alloc_mixin::operator delete: Missing documentation for parameter 'ptr'", + "include/boost/capy/ex/frame_alloc_mixin.hpp:#1:boost::capy::frame_alloc_mixin::operator delete: Missing documentation for parameter 'size'", + "include/boost/capy/ex/frame_alloc_mixin.hpp:#1:boost::capy::frame_alloc_mixin::operator new: Failed to resolve reference to 'Propagates'", + "include/boost/capy/ex/immediate.hpp:#1:boost::capy::immediate::await_ready: Missing documentation for return value", + "include/boost/capy/ex/immediate.hpp:#1:boost::capy::immediate::await_resume: Missing documentation for return value", + "include/boost/capy/ex/io_awaitable_promise_base.hpp:#1:~io_awaitable_promise_base: function is undocumented", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:boost::capy::recycling_memory_resource::allocate_fast: 2nd parameter is unnamed", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:boost::capy::recycling_memory_resource::allocate_fast: Missing documentation for parameter 'bytes'", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:boost::capy::recycling_memory_resource::allocate_fast: Missing documentation for return value", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:boost::capy::recycling_memory_resource::deallocate_fast: 3rd parameter is unnamed", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:boost::capy::recycling_memory_resource::deallocate_fast: Missing documentation for parameter 'bytes'", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:boost::capy::recycling_memory_resource::deallocate_fast: Missing documentation for parameter 'p'", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:do_allocate: function is undocumented", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:do_deallocate: function is undocumented", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:do_is_equal: function is undocumented", + "include/boost/capy/ex/recycling_memory_resource.hpp:#1:~recycling_memory_resource: function is undocumented", + "include/boost/capy/ex/run.hpp:#1:boost::capy::run: Documented parameter 'alloc' does not exist", + "include/boost/capy/ex/run_async.hpp:#1:operator=: function is undocumented", + "include/boost/capy/ex/run_async.hpp:#1:run_async_wrapper: function is undocumented", + "include/boost/capy/ex/run_async.hpp:#2:operator=: function is undocumented", + "include/boost/capy/ex/run_async.hpp:#2:run_async_wrapper: function is undocumented", + "include/boost/capy/ex/strand.hpp:#1:boost::capy::strand::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/strand.hpp:#1:boost::capy::strand::operator=: Missing documentation for return value", + "include/boost/capy/ex/strand.hpp:#1:boost::capy::strand::strand: 1st parameter is unnamed", + "include/boost/capy/ex/strand.hpp:#1:strand: guide is undocumented", + "include/boost/capy/ex/strand.hpp:#2:boost::capy::strand::operator=: 1st parameter is unnamed", + "include/boost/capy/ex/strand.hpp:#2:boost::capy::strand::operator=: Missing documentation for return value", + "include/boost/capy/ex/strand.hpp:#2:boost::capy::strand::strand: 1st parameter is unnamed", + "include/boost/capy/ex/thread_pool.hpp:#1:boost::capy::thread_pool::executor_type::context: Missing documentation for return value", + "include/boost/capy/ex/thread_pool.hpp:#1:boost::capy::thread_pool::executor_type::operator==: Missing documentation for parameter 'other'", + "include/boost/capy/ex/thread_pool.hpp:#1:boost::capy::thread_pool::executor_type::operator==: Missing documentation for return value", + "include/boost/capy/ex/thread_pool.hpp:#1:operator=: function is undocumented", + "include/boost/capy/ex/thread_pool.hpp:#1:thread_pool: function is undocumented", + "include/boost/capy/ex/work_guard.hpp:#1:operator=: function is undocumented", + "include/boost/capy/io/any_read_stream.hpp:#1:boost::capy::any_read_stream::any_read_stream: 1st parameter is unnamed", + "include/boost/capy/io/any_read_stream.hpp:#1:operator=: function is undocumented", + "include/boost/capy/io/any_stream.hpp:#1:boost::capy::any_stream::any_stream: 1st parameter is unnamed", + "include/boost/capy/io/any_stream.hpp:#1:operator=: function is undocumented", + "include/boost/capy/io/any_write_stream.hpp:#1:boost::capy::any_write_stream::any_write_stream: 1st parameter is unnamed", + "include/boost/capy/io/any_write_stream.hpp:#1:operator=: function is undocumented", + "include/boost/capy/io_result.hpp:#1:boost::capy::io_result::io_result: Missing documentation for parameter 'ec_'", + "include/boost/capy/io_result.hpp:#1:boost::capy::io_result::io_result: Missing documentation for parameter 'ts'", + "include/boost/capy/io_result.hpp:#1:get: function is undocumented", + "include/boost/capy/io_result.hpp:#2:get: function is undocumented", + "include/boost/capy/io_result.hpp:#3:get: function is undocumented", + "include/boost/capy/io_result.hpp:#4:get: function is undocumented", + "include/boost/capy/quitter.hpp:#1:a_: variable is undocumented", + "include/boost/capy/quitter.hpp:#1:await_ready: function is undocumented", + "include/boost/capy/quitter.hpp:#1:await_resume: function is undocumented", + "include/boost/capy/quitter.hpp:#1:await_suspend: function is undocumented", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::await_ready: Missing documentation for return value", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::await_resume: Missing documentation for return value", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::await_suspend: Missing documentation for parameter 'cont'", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::await_suspend: Missing documentation for parameter 'env'", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::await_suspend: Missing documentation for return value", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::operator=: Missing documentation for parameter 'other'", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::operator=: Missing documentation for return value", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::promise_type::exception: Missing documentation for return value", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::promise_type::stopped: Missing documentation for return value", + "include/boost/capy/quitter.hpp:#1:boost::capy::quitter::quitter: Missing documentation for parameter 'other'", + "include/boost/capy/quitter.hpp:#1:ep_: variable is undocumented", + "include/boost/capy/quitter.hpp:#1:operator=: function is undocumented", + "include/boost/capy/quitter.hpp:#1:p_: variable is undocumented", + "include/boost/capy/quitter.hpp:#1:quitter: function is undocumented", + "include/boost/capy/read.hpp:#1:boost::capy::read: Missing documentation for return value", + "include/boost/capy/read_at_least.hpp:#1:boost::capy::read_at_least: Missing documentation for return value", + "include/boost/capy/task.hpp:#1:a_: variable is undocumented", + "include/boost/capy/task.hpp:#1:await_ready: function is undocumented", + "include/boost/capy/task.hpp:#1:await_resume: function is undocumented", + "include/boost/capy/task.hpp:#1:await_suspend: function is undocumented", + "include/boost/capy/task.hpp:#1:boost::capy::task::await_resume: Failed to resolve reference to 'The'", + "include/boost/capy/task.hpp:#1:ep_: variable is undocumented", + "include/boost/capy/task.hpp:#1:operator=: function is undocumented", + "include/boost/capy/task.hpp:#1:p_: variable is undocumented", + "include/boost/capy/task.hpp:#1:task: function is undocumented", + "include/boost/capy/write.hpp:#1:boost::capy::write: Missing documentation for return value", + "include/boost/capy/write_at_least.hpp:#1:boost::capy::write_at_least: Missing documentation for return value" + ] + }, + "a11y": { + "count": 71, + "skipped": false, + "contrastCount": 9, + "fingerprints": [ + "/capy/4.coroutines/4a.tasks.html:color-contrast:#content > article > div:nth-child(6) > div > div:nth-child(2) > div > pre > code > span:nth-child(1)", + "/capy/4.coroutines/4a.tasks.html:color-contrast:#content > article > div:nth-child(6) > div > div:nth-child(2) > div > pre > code > span:nth-child(8)", + "/capy/4.coroutines/4a.tasks.html:link-name:#_awaiting_other_tasks > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_declaring_task_coroutines > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_exception_propagation > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_lazy_execution > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_move_semantics > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_overview > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_prerequisites > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_reference > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_returning_values_with_co_return > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_running_a_task > a", + "/capy/4.coroutines/4a.tasks.html:link-name:#_symmetric_transfer > a", + "/capy/4.coroutines/4a.tasks.html:list:#toc > aside > div > div > nav > ul", + "/capy/4.coroutines/4a.tasks.html:list:#toc > aside > div > div > nav > ul > ul", + "/capy/index.html:link-name:#_assumed_knowledge > a", + "/capy/index.html:link-name:#_code_convention > a", + "/capy/index.html:link-name:#_compiler_support > a", + "/capy/index.html:link-name:#_dependencies > a", + "/capy/index.html:link-name:#_design_philosophy > a", + "/capy/index.html:link-name:#_linking > a", + "/capy/index.html:link-name:#_next_steps > a", + "/capy/index.html:link-name:#_quick_example > a", + "/capy/index.html:link-name:#_requirements > a", + "/capy/index.html:link-name:#_target_audience > a", + "/capy/index.html:link-name:#_the_library_family > a", + "/capy/index.html:link-name:#_what_capy_is > a", + "/capy/index.html:link-name:#_what_capy_is_not > a", + "/capy/index.html:link-name:#_what_this_library_does > a", + "/capy/index.html:link-name:#_what_this_library_does_not_do > a", + "/capy/index.html:list:#toc > aside > div > div > nav > ul", + "/capy/index.html:list:#toc > aside > div > div > nav > ul > ul", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(1)", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(16)", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(2)", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(3)", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(4)", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(6)", + "/capy/quick-start.html:color-contrast:#content > article > div:nth-child(4) > div > div:nth-child(2) > div > pre > code > span:nth-child(9)", + "/capy/quick-start.html:link-name:#_build_and_run > a", + "/capy/quick-start.html:link-name:#_handling_errors > a", + "/capy/quick-start.html:link-name:#_handling_results > a", + "/capy/quick-start.html:link-name:#_minimal_example > a", + "/capy/quick-start.html:link-name:#_next_steps > a", + "/capy/quick-start.html:link-name:#_what_just_happened > a", + "/capy/quick-start.html:list:#toc > aside > div > div > nav > ul", + "/capy/quick-start.html:list:#toc > aside > div > div > nav > ul > ul", + "/capy/reference/boost/capy.html:link-name:#_concepts > a", + "/capy/reference/boost/capy.html:link-name:#_deduction_guides > a", + "/capy/reference/boost/capy.html:link-name:#_enums > a", + "/capy/reference/boost/capy.html:link-name:#_functions > a", + "/capy/reference/boost/capy.html:link-name:#_namespaces > a", + "/capy/reference/boost/capy.html:link-name:#_type_aliases > a", + "/capy/reference/boost/capy.html:link-name:#_types > a", + "/capy/reference/boost/capy.html:list:#toc > aside > div > div > nav > ul", + "/capy/reference/boost/capy.html:list:#toc > aside > div > div > nav > ul > ul", + "/capy/why-capy.html:link-name:#_buffer_sequences > a", + "/capy/why-capy.html:link-name:#_comparison > a", + "/capy/why-capy.html:link-name:#_comparison_2 > a", + "/capy/why-capy.html:link-name:#_comparison_3 > a", + "/capy/why-capy.html:link-name:#_comparison_4 > a", + "/capy/why-capy.html:link-name:#_coroutine_execution_model > a", + "/capy/why-capy.html:link-name:#_coroutine_only_stream_concepts > a", + "/capy/why-capy.html:link-name:#_the_road_ahead > a", + "/capy/why-capy.html:link-name:#_type_erasing_stream_wrappers > a", + "/capy/why-capy.html:link-name:#_what_capy_offers > a", + "/capy/why-capy.html:link-name:#_what_capy_offers_2 > a", + "/capy/why-capy.html:link-name:#_what_capy_offers_3 > a", + "/capy/why-capy.html:link-name:#_what_capy_offers_4 > a", + "/capy/why-capy.html:list:#toc > aside > div > div > nav > ul", + "/capy/why-capy.html:list:#toc > aside > div > div > nav > ul > ul" + ] + } + } +} diff --git a/doc/lint/baseline.mjs b/doc/lint/baseline.mjs new file mode 100644 index 000000000..134e86872 --- /dev/null +++ b/doc/lint/baseline.mjs @@ -0,0 +1,248 @@ +#!/usr/bin/env node +// +// baseline.mjs — runs every check and snapshots current violations to +// doc/lint/baseline.json (Style Guide Part F.0, "no new violations" while the +// backlog is worked down). Node built-ins only, no dependencies. +// +// Each check contributes a `count` and a `fingerprints` array (stable +// per-finding strings) so a later comparator (check-no-new-violations.mjs) +// can diff a fresh run against this snapshot and flag genuinely new +// findings, independent of how many pre-existing ones remain. Fingerprints +// deliberately carry no line number — see occurrenceKey() below. +// +// Usage: node doc/lint/baseline.mjs [--skip-a11y] [outFile] +// outFile defaults to doc/lint/baseline.json; check-no-new-violations.mjs +// passes a temp path so a comparison run doesn't clobber the committed one. +// +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +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 cliArgs = process.argv.slice(2); +const skipA11y = cliArgs.includes('--skip-a11y'); +const outArg = cliArgs.find((a) => !a.startsWith('--')); + +function run(cmd, args, opts = {}) { + const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, ...opts }); + return r; +} + +// Fingerprints must survive line shifts. Keying on the reported line number made +// every finding BELOW an insertion point look new: commit df68f9bc, a comment-only +// docstring addition, renamed 27 grandfathered MrDocs findings and red-lined the +// blocking MrDocs-no-warnings gate without introducing a single warning. So the +// line number is replaced by a per-group occurrence index: the Nth finding sharing +// the same (head, tail) pair is keyed `#N`. That keeps multiplicity — the same +// warning appearing one MORE time in the same file is still new — while making the +// key independent of where in the file it appears. +// +// The index goes exactly where the line number was, mid-key. Do not move it to the +// tail: .github/workflows/docs.yml gates on `doc_lint:^(A1|A6|B2|D2):` (head-anchored) +// and `vale_adoc:Capy\.PartHeadings$` (TAIL-anchored), and the regexes are tested +// against the whole fingerprint, so a trailing index would make the PartHeadings +// gate match nothing and fail open. +// +// The counter is per-base-key, never a raw iteration counter, so the resulting key +// multiset is {base:#1 .. base:#k} whatever order the findings arrive in. +function occurrenceKey(seen, head, tail) { + const base = `${head}\u0000${tail}`; // NUL separator: neither part can contain it + const n = (seen.get(base) ?? 0) + 1; + seen.set(base, n); + return `${head}:#${n}:${tail}`; +} + +// Vale is spawned below with cwd=DOC_DIR, so the keys of its JSON output are +// paths relative to DOC_DIR (or absolute). `path.relative(DOC_DIR, file)` +// resolved a *relative* `file` against process.cwd(), NOT against DOC_DIR — so +// regenerating from anywhere other than doc/ prefixed every Vale path with +// `../` and silently renamed all ~3900 Vale fingerprints at once, retiring the +// entire grandfathered Vale backlog and re-minting it under new keys. Resolve +// against DOC_DIR explicitly so the key depends only on the file, never on +// where the generator happened to be invoked from. The separator normalisation +// is a no-op on POSIX (path.sep === '/') and keeps a Windows run from minting a +// parallel backslash-keyed key set. +function valeRelPath(file) { + return path.relative(DOC_DIR, path.resolve(DOC_DIR, file)).split(path.sep).join('/'); +} + +function valeFingerprints(target) { + const r = run('vale', ['--output=JSON', target], { cwd: DOC_DIR }); + if (r.error) { + return { count: 0, skipped: true, reason: `vale failed to launch: ${r.error.message}`, fingerprints: [] }; + } + // Vale's own exit codes: 0 = no alerts at MinAlertLevel, 1 = alerts found (the normal, + // expected case — NOT a failure), 2 = fatal runtime error (e.g. `asciidoctor` off PATH, + // a broken `vale sync`). On a fatal error Vale writes a single JSON error object to + // stderr and leaves stdout empty, so a plain JSON.parse(stdout || '{}') silently yields + // `{}` — indistinguishable from "ran clean, found nothing." Detect that explicitly + // instead of ever reporting a broken Vale run as `count: 0`. + let parsed = null; + try { parsed = r.stdout ? JSON.parse(r.stdout) : null; } catch { parsed = null; } + const looksLikeFileMap = parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed); + if (r.status === 2 || !looksLikeFileMap) { + const tail = (r.stderr || r.stdout || '(no output)').trim().slice(-500); + return { count: 0, skipped: true, reason: `vale on '${target}' did not produce findings (exit ${r.status}): ${tail}`, fingerprints: [] }; + } + const fingerprints = []; + const seen = new Map(); + for (const [file, alerts] of Object.entries(parsed)) { + for (const a of alerts) fingerprints.push(occurrenceKey(seen, valeRelPath(file), a.Check)); + } + return { count: fingerprints.length, fingerprints: fingerprints.sort() }; +} + +// A crashed check must report as SKIPPED, never as zero findings. doc-lint.mjs and +// mrdocs-warnings.mjs have no error path for an *uncaught throw*: they die with a +// non-zero status and an empty stdout. Defaulting that to an empty findings object +// yielded `count: 0, skipped: false` — a crashed check indistinguishable from a +// clean one. Both are GATED, and the comparator only treats a `skipped` check as +// unverifiable, so the zero sailed through as "backlog empty": the reseed reporter +// called it "0 added, none gated" and the merge gate called it "0 new". That is the +// fail-open shape this toolchain exists to prevent, so the exit status is now +// checked before the output is believed. (mrdocs-warnings.mjs's *designed* +// failures — no binary, version-pin miss, MrDocs itself failing — already emit +// `{error}` on exit 0 and are handled below; this only covers crashes.) +function crashed(r, script) { + if (!r.error && r.status === 0) return null; + const tail = (r.stderr || r.error?.message || r.stdout || '(no output)').trim().slice(-500); + return { count: 0, skipped: true, reason: `${script} failed (exit ${r.status}): ${tail}`, fingerprints: [] }; +} + +function docLintFingerprints() { + const r = run('node', [path.join(SCRIPT_DIR, 'doc-lint.mjs')]); + const bad = crashed(r, 'doc-lint.mjs'); + if (bad) return bad; + let parsed; + try { + parsed = JSON.parse(r.stdout || ''); + } catch { + return { + count: 0, skipped: true, fingerprints: [], + reason: `doc-lint.mjs produced unparseable output: ${(r.stdout || '(empty)').trim().slice(-500)}`, + }; + } + const fingerprints = []; + const seen = new Map(); + for (const [check, items] of Object.entries(parsed.findings || {})) { + // SHAPE is advisory-only and never gated (see doc-lint.mjs's header + // comment); folding it into doc_lint's fingerprint set let a single + // advisory SHAPE finding keep `currentSet.length` non-zero in + // check-no-new-violations.mjs even when A1/A6/B2/D2 — the checks the + // gate spec `doc_lint:^(A1|A6|B2|D2):` actually cares about — report + // zero, silently disarming the "gated check reports 0 against a + // non-empty baseline" backstop (check-no-new-violations.mjs:187). + if (check === 'SHAPE') continue; + for (const it of items) fingerprints.push(occurrenceKey(seen, `${check}:${it.file}`, it.message)); + } + return { count: fingerprints.length, byRule: parsed.summary, fingerprints: fingerprints.sort() }; +} + +// C2 (sentence length) is checked by our own script, not by Vale — see the +// header of sentence-length.mjs and the comment in Capy/SentenceLength.yml for +// why. The finding shape is doc-lint.mjs's, so the fingerprint is the doc_lint +// shape (`rule:file:#N:message`, rule at the HEAD) and a future gate spec reads +// `--gate 'sentence_length:^C2:'`. Note this check has no entry in the +// committed baseline.json yet, so every finding reports as NEW until the +// maintainer reseeds; it is deliberately NOT in the gate spec, so that cannot +// block a merge. +function sentenceLengthFingerprints() { + const r = run('node', [path.join(SCRIPT_DIR, 'sentence-length.mjs')], { cwd: DOC_DIR }); + const bad = crashed(r, 'sentence-length.mjs'); + if (bad) return bad; + let parsed; + try { + parsed = JSON.parse(r.stdout || ''); + } catch { + return { + count: 0, skipped: true, fingerprints: [], + reason: `sentence-length.mjs produced unparseable output: ${(r.stdout || '(empty)').trim().slice(-500)}`, + }; + } + const fingerprints = []; + const seen = new Map(); + for (const [check, items] of Object.entries(parsed.findings || {})) { + for (const it of items) fingerprints.push(occurrenceKey(seen, `${check}:${it.file}`, it.message)); + } + return { count: fingerprints.length, byRule: parsed.summary, fingerprints: fingerprints.sort() }; +} + +function mrdocsFingerprints() { + const r = run('node', [path.join(SCRIPT_DIR, 'mrdocs-warnings.mjs')]); + const bad = crashed(r, 'mrdocs-warnings.mjs'); + if (bad) return bad; + let parsed = {}; + try { parsed = JSON.parse(r.stdout || '{}'); } catch { /* fall through */ } + if (parsed.error) return { count: 0, skipped: true, reason: parsed.error, fingerprints: [] }; + const seen = new Map(); + const fingerprints = (parsed.findings || []).map((f) => occurrenceKey(seen, f.file ?? '?', f.message)); + return { count: fingerprints.length, fingerprints: fingerprints.sort() }; +} + +function a11yFingerprints() { + const r = run('node', [path.join(SCRIPT_DIR, 'run-a11y.mjs')]); + let parsed = {}; + try { parsed = JSON.parse(r.stdout || '{}'); } catch { /* fall through */ } + if (parsed.error) return { count: 0, skipped: true, reason: parsed.error, fingerprints: [] }; + const fingerprints = (parsed.findings || []).map((f) => `${f.url}:${f.code}:${f.selector}`); + return { + count: fingerprints.length, + contrastCount: parsed.summary?.contrast ?? fingerprints.filter((f) => f.includes(':color-contrast:')).length, + fingerprints: fingerprints.sort(), + }; +} + +const results = {}; +results.vale_adoc = valeFingerprints('modules'); + +// The docstring corpus is GENERATED, so the generator's exit status is part of +// the measurement. It used to be discarded: extract-docstrings.mjs never clears +// OUT_DIR, so a crash left whatever the last successful run wrote — a stale +// corpus that Vale lints happily and reports `skipped: false` over. Worse, if +// the crash happened before anything was ever written (fresh clone, renamed +// path), Vale over an absent directory exits 0 with `{}`, which valeFingerprints +// reads as `count: 0, skipped: false` — a vacuous clean for the C4/C9/C10 +// docstring gates and for sentence_length's docstring half. Both dependent +// checks are therefore marked SKIPPED, which the comparator treats as a gate +// failure, instead of being believed. +const extract = run('node', [path.join(SCRIPT_DIR, 'extract-docstrings.mjs')]); +const extractBad = crashed(extract, 'extract-docstrings.mjs'); +if (extractBad) { + const reason = `docstring corpus not regenerated: ${extractBad.reason}`; + results.vale_docstrings = { count: 0, skipped: true, reason, fingerprints: [] }; + results.sentence_length = { count: 0, skipped: true, reason, fingerprints: [] }; +} else { + results.vale_docstrings = valeFingerprints('lint/.docstrings'); + + // After extract-docstrings.mjs above: sentence-length.mjs lints BOTH corpora and + // exits non-zero rather than reporting a clean zero for one it could not read. + results.sentence_length = sentenceLengthFingerprints(); +} + +results.doc_lint = docLintFingerprints(); +results.mrdocs_warnings = mrdocsFingerprints(); +results.a11y = skipA11y ? { count: 0, skipped: true, reason: '--skip-a11y' } : a11yFingerprints(); + +const baseline = { + generatedAt: new Date().toISOString(), + note: 'Snapshot of current violations (Task 2, Style Guide Part F.0). Non-blocking: ' + + 'this records the backlog so a future comparator can flag NEW findings without ' + + 'failing on the ones already known about. Fingerprints are line-insensitive: ' + + 'the `#N` component is the Nth occurrence of that (file, message) pair, NOT a ' + + 'line number, so inserting text above a finding does not rename it.', + checks: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { + count: v.count, skipped: v.skipped || false, reason: v.reason, + byRule: v.byRule, contrastCount: v.contrastCount, + fingerprints: v.fingerprints, + }])), +}; + +const outPath = outArg ? path.resolve(outArg) : path.join(SCRIPT_DIR, 'baseline.json'); +fs.writeFileSync(outPath, JSON.stringify(baseline, null, 2) + '\n'); +console.log(JSON.stringify({ + written: path.relative(REPO_ROOT, outPath), + summary: Object.fromEntries(Object.entries(baseline.checks).map(([k, v]) => [k, v.skipped ? 'skipped' : v.count])), +}, null, 2)); diff --git a/doc/lint/check-no-new-violations.mjs b/doc/lint/check-no-new-violations.mjs new file mode 100644 index 000000000..27d5813c7 --- /dev/null +++ b/doc/lint/check-no-new-violations.mjs @@ -0,0 +1,224 @@ +#!/usr/bin/env node +// +// check-no-new-violations.mjs — the "no NEW violations" gate the Task 2 +// brief's acceptance criterion describes: a fresh check run is diffed +// against doc/lint/baseline.json (Style Guide Part F.0) per-check +// fingerprint sets, and anything not already in the baseline is reported as +// new. This is what would catch a newly introduced banned word ("utilize") +// or a newly undocumented @param, without re-flagging the existing backlog. +// +// Node built-ins only. Regenerates a fresh snapshot via baseline.mjs into a +// temp file (never overwrites the committed baseline.json) and compares. +// +// Non-blocking by default (Task 2: everything stays warning-mode). Pass +// --strict to exit 1 when new violations are found. +// +// --gate : restricts what counts as blocking to a named +// set of (check, rule-regex) pairs — the phase-exit gate-promotion mechanism. +// Repeatable. Each value is split on its FIRST ':' into a check name and a +// regex tested against that check's fingerprints. In gate mode BOTH the exit +// condition and the skip check are filtered: only NEW fingerprints of a gated +// check that match its regex block, and only a skip of a GATED check fails the +// run ("can't verify a gated rule = not a pass"); skips of non-gated checks +// (a11y, mrdocs, vale_docstrings) are reported but do not block. Without +// --gate the comparator stays omnibus (the non-blocking report step). +// +// A gated check that reports ZERO findings against a non-empty baseline also +// fails the gate — a check that silently did not run is indistinguishable from +// one that ran clean, and `skipped` does not catch it. See the rule at the +// bottom of the per-check loop for the reachable case and the reasoning. +// +// Phase-1 exit gate spec (A1/A6/A7/B2/D2): +// --gate 'doc_lint:^(A1|A6|B2|D2):' --gate 'vale_adoc:Capy\.PartHeadings$' +// (A1/A6/B2/D2 come from doc_lint; A7 is the Vale rule Capy.PartHeadings.) +// +// Phase-2 exit adds MrDocs-no-warnings to the above (full spec): +// --gate 'doc_lint:^(A1|A6|B2|D2):' --gate 'vale_adoc:Capy\.PartHeadings$' \ +// --gate 'mrdocs_warnings:.*' +// mrdocs_warnings:.* gates the whole reference-surface check. E4 (a11y contrast) +// is NOT gated — it was demoted to Review tier (DOC_STYLE_GUIDE.md Part F.0): +// the gated failures were all color-contrast on shared Antora theme nav chrome, +// which Capy cannot fix, the same rationale that demoted E2. The a11y scan +// still runs and is reported non-blocking. +// +// --allow-emptied suppresses the "gated check reports zero findings +// against a non-empty baseline" failure described below, for one check. Use it +// only when a gated backlog has genuinely closed; it records the decision in +// the run log. Not used by the committed CI invocation. +// +// Usage: node doc/lint/check-no-new-violations.mjs [--strict] [--gate spec ...] +// [--allow-emptied check ...] [--skip-a11y] +// +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const argv = process.argv.slice(2); +const strict = argv.includes('--strict'); + +// Parse --gate specs; everything else (except --strict) is passed through to +// baseline.mjs. NB: --gate values must NOT reach baseline.mjs, whose first +// non-flag arg is taken as the output path. +const gateByCheck = new Map(); // check -> [RegExp] +const allowEmptied = new Set(); // checks whose zero is an accepted milestone +const extraArgs = []; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--strict') continue; + let allow = null; + if (a === '--allow-emptied') allow = argv[++i]; + else if (a.startsWith('--allow-emptied=')) allow = a.slice('--allow-emptied='.length); + if (allow != null) { + if (!allow) { + console.error('--allow-emptied expects a check name'); + process.exit(2); + } + allowEmptied.add(allow); + continue; + } + let spec = null; + if (a === '--gate') spec = argv[++i]; + else if (a.startsWith('--gate=')) spec = a.slice('--gate='.length); + if (spec != null) { + const idx = spec.indexOf(':'); + if (idx < 0) { + console.error(`--gate expects :, got: ${spec}`); + process.exit(2); + } + const check = spec.slice(0, idx); + const re = new RegExp(spec.slice(idx + 1)); + if (!gateByCheck.has(check)) gateByCheck.set(check, []); + gateByCheck.get(check).push(re); + continue; + } + extraArgs.push(a); +} +const gated = gateByCheck.size > 0; + +const baselinePath = path.join(SCRIPT_DIR, 'baseline.json'); +if (!fs.existsSync(baselinePath)) { + console.log(JSON.stringify({ error: `no baseline.json at ${baselinePath} — run baseline.mjs first` }, null, 2)); + process.exit(0); +} +const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); + +const tmpPath = path.join(os.tmpdir(), `doc-lint-current-${process.pid}.json`); +const r = spawnSync('node', [path.join(SCRIPT_DIR, 'baseline.mjs'), ...extraArgs, tmpPath], { encoding: 'utf8' }); +if (r.status !== 0 || !fs.existsSync(tmpPath)) { + console.log(JSON.stringify({ error: 'failed to generate a current snapshot', stderr: r.stderr }, null, 2)); + process.exit(0); +} +const current = JSON.parse(fs.readFileSync(tmpPath, 'utf8')); +fs.rmSync(tmpPath, { force: true }); + +let totalNew = 0; // omnibus: new findings across ALL checks (report semantics) +let anySkipped = false; // any check skipped at all +let gatedNew = 0; // new findings in gated checks matching a gate regex +let gatedSkipped = false; // a GATED check was skipped (can't verify => gate fails) +let gatedEmptied = false; // a GATED check reported ZERO findings against a non-empty baseline +const gatedFindings = []; // the specific gated new fingerprints (named in the log) +const emptiedGated = []; // the checks that tripped the emptiness rule +const report = {}; +for (const [check, currentCheck] of Object.entries(current.checks)) { + const gateRes = gateByCheck.get(check) || null; + // A skipped check (Vale broken, MrDocs/a11y couldn't run, ...) is NOT a clean pass — it + // means no comparison happened at all. Surface it loudly (stderr, outside the JSON blob) + // so it can't be mistaken for "0 new" in a log that only skims the summary line, and + // record it distinctly (newCount: null, not 0) in the JSON report too. A skip of a GATED + // check additionally fails the gate: an unverifiable gated rule is not a pass. + if (currentCheck.skipped) { + anySkipped = true; + if (gateRes) gatedSkipped = true; + console.error(`SKIPPED: ${check} (${currentCheck.reason}) — no-new-violations comparison NOT performed for this check.${gateRes ? ' [GATED — fails the gate]' : ''}`); + report[check] = { + skipped: true, reason: currentCheck.reason, gated: !!gateRes, + baselineCount: baseline.checks[check]?.count ?? 0, currentCount: currentCheck.count, + newCount: null, newFindings: [], + }; + continue; + } + const baseSet = new Set(baseline.checks[check]?.fingerprints || []); + const currentSet = currentCheck.fingerprints || []; + const newOnes = currentSet.filter((fp) => !baseSet.has(fp)); + totalNew += newOnes.length; + const entry = { baselineCount: baseline.checks[check]?.count ?? 0, currentCount: currentCheck.count, newCount: newOnes.length, newFindings: newOnes }; + if (gateRes) { + const gatedOnes = newOnes.filter((fp) => gateRes.some((re) => re.test(fp))); + entry.gated = true; + entry.gatedNewCount = gatedOnes.length; + entry.gatedNewFindings = gatedOnes; + gatedNew += gatedOnes.length; + for (const fp of gatedOnes) gatedFindings.push(`${check} :: ${fp}`); + + // A GATED check that reports ZERO findings where the committed baseline has + // some is treated as a check that did not run, until proven otherwise. This + // is the fail-open the `skipped` flag does NOT catch, and it is reachable: + // + // $ cd doc && vale --output=JSON lint/.nonexistent-corpus + // {} + // $ echo $? + // 0 + // + // baseline.mjs marks a Vale check skipped only on exit 2 or a non-object + // parse, so exit 0 plus `{}` yields `{count: 0, skipped: false}` — and this + // comparator then computes "zero new" from an empty current set and reports + // `gated: true, gatedNew: 0`, i.e. a gate that says it is gating while + // measuring nothing. Any renamed corpus path, crashed extractor, or + // `.vale.ini` edit that stops matching the corpus lands here. + // + // The rule is the same one baseline-diff.mjs applies to a reseed candidate, + // for the same reasons: emptiness rather than a removal-fraction threshold + // (a check that did not run produces exactly zero, never 40% fewer), and + // scoped to GATED checks, whose zero is the one that decides a merge. + // + // Deliberately WHOLE-CHECK, not per-gate-regex. The gated SLICE of + // vale_docstrings is legitimately empty today — zero Capy.SimpleTense / + // NoFluff / Terminology on the docstring corpus is exactly what Phase 4 + // delivered — so a per-slice rule would fail the committed invocation on + // the phase's own success state. A whole-check zero cannot be produced by + // wording work: the residual Vale.Spelling/Google backlog on both corpora + // is not going to zero, so only a broken run gets there. + // + // The one legitimate whole-check zero, a gated backlog genuinely closing, + // is a milestone worth an explicit --allow-emptied . + if (currentSet.length === 0 && baseSet.size > 0 && !allowEmptied.has(check)) { + entry.gatedEmptied = true; + gatedEmptied = true; + emptiedGated.push(check); + } else if (currentSet.length === 0 && baseSet.size > 0) { + entry.gatedEmptiedAllowed = true; + } + } + report[check] = entry; +} + +if (anySkipped) { + console.error(`SKIPPED checks present — totalNew (${totalNew}) is only valid for the checks that actually ran.`); +} + +// The blocking condition: gated slice when --gate is present, omnibus otherwise. +const blockingNew = gated ? gatedNew : totalNew; +const blockingSkip = gated ? gatedSkipped : anySkipped; +if (gated && blockingNew > 0) { + console.error(`GATE: ${blockingNew} new gated violation(s):`); + for (const f of gatedFindings) console.error(` - ${f}`); +} +for (const check of emptiedGated) { + console.error(`GATE: gated check '${check}' reports 0 findings but the committed baseline has ` + + `${baseline.checks[check]?.fingerprints?.length ?? 0} — a check that did not run looks exactly ` + + `like this. Verify it really ran; if the backlog is genuinely closed, re-run with ` + + `--allow-emptied ${check}.`); +} + +console.log(JSON.stringify({ + totalNew, anySkipped, strict, + gated, gatedNew: gated ? gatedNew : undefined, gatedSkipped: gated ? gatedSkipped : undefined, + gatedEmptied: gated ? gatedEmptied : undefined, + emptiedGated: gated ? emptiedGated : undefined, + gatedFindings: gated ? gatedFindings : undefined, + checks: report, +}, null, 2)); +process.exit(strict && (blockingNew > 0 || blockingSkip || (gated && gatedEmptied)) ? 1 : 0); diff --git a/doc/lint/doc-lint.mjs b/doc/lint/doc-lint.mjs new file mode 100644 index 000000000..8afa671cb --- /dev/null +++ b/doc/lint/doc-lint.mjs @@ -0,0 +1,301 @@ +#!/usr/bin/env node +// +// doc-lint.mjs — structural checks Vale cannot express (Style Guide Part F). +// Node built-ins only, no dependencies. Exit 0 always (warning mode, Task 2); +// findings are emitted as JSON on stdout for `baseline.json` / CI to consume. +// +// Checks: +// A1 — every page under pages/ declares :page-mode:, and the value is one +// of DOC_STYLE_GUIDE.md Part A's four Diátaxis modes (tutorial, +// how-to, reference, explanation). Presence alone used to pass; a +// typo or a non-mode value (e.g. the former `concept`) slipped +// through and silently fell out of D2's scope (see below). Bite-test +// per style-guide F4: plant an invalid value, confirm A1 fails. +// A6 — quick-start is within the first 3 top-level nav.adoc entries +// B2 — no [source,] block, and no bare listing (`----` or +// `....`), holds raw code (must start with include::example$... or +// carry role=pseudocode/role=external — DOC_STYLE_GUIDE.md B3). A +// bare listing whose attribute line carries role=output/role=figure +// is exempt from B2 outright — it is not code, but see SHAPE below, +// which still looks at it. Originally scoped to [source,cpp]/ +// [source,c++] only, which left [source,cmake]/[source,c]/ +// [source,bash] and bare listings holding real C++ invisible to the +// gate; widened once every such block in the corpus was classified +// (see git history for the audit). Delimiters are matched 4-or-more +// repeats of the character, closer length must equal opener length +// (AsciiDoc's own rule — `-----`/`....` are not `----`, and this is +// also how AsciiDoc nests a `----` inside a `-----`); a fixed +// 4-character match let a 5-dash listing hide code from the gate. +// The attribute list read above the delimiter walks consecutive +// `[source,...]`/`[role=...]` lines, not just the nearest one — +// AsciiDoc permits a block's attribute list to be split across +// adjacent lines (e.g. `[source,cpp]` then `[role=output]` on the +// next line) and Asciidoctor merges them; reading only the nearest +// line missed `[source,...]` set on an earlier line and let a +// highlighted C++ block through as an exempt bare listing. The walk +// deliberately stops at anything else `[...]`-shaped (a block anchor +// `[[id]]`, an admonition style `[NOTE]`, a quote attribution +// `[quote,...]`) — a first cut that merged any `[...]`-shaped line +// made one of those, sitting directly above an exempt +// `[source,...,role=pseudocode]` block, defeat that exemption. +// SHAPE — advisory only, NEVER gated (not in the summary the CI gate +// spec reads by rule prefix). A role=output/role=figure block is a +// permanent B2 exemption, so a block wrongly marked non-code would +// be permanently invisible; SHAPE runs a content heuristic +// (`#include`, `co_await`, `template<`, a brace-opened struct/class, +// a `;`-terminated line, `Name::member(`) over exactly the blocks B2 +// just exempted, and flags ones that look like code. The exemption +// stops being permanent invisibility: the gate still looks, it just +// doesn't block. +// ANCHOR — no prose writes a C++ attribute as `[[...]]` inside a code +// span. Asciidoctor's inline-anchor substitution runs inside a +// backtick span, so `` `[[nodiscard]]` `` is parsed as an anchor and +// renders as an EMPTY element -- the attribute name silently +// disappears from the page. Measured: `[[clang::coro_await_elidable]]` +// rendered as ``. +// The defect is invisible in the source, which reads correctly, so it +// needs a machine check rather than a careful reader. The fix is a +// passthrough: `` `+[[nodiscard]]+` ``. Only +// prose is scanned -- inside a delimited block `[[` is literal and +// renders fine, which is why the check is line-based with a +// block-depth skip rather than a whole-file regex. Bite-test per +// style-guide F4: put `` `[[nodiscard]]` `` in a page and confirm +// ANCHOR fires. +// D2 — every page in a CONCEPT_DIRS chapter (or quick-start.adoc) has +// >=1 include::example$. D2's "concept page" is a pedagogical +// category, not a Diátaxis mode — deliberately independent of +// :page-mode:, so a page cannot leave D2's scope by declaring a +// different (even a legitimate) mode. See DOC_STYLE_GUIDE.md's D2 +// entry for why landing pages (*.intro.adoc, mode: explanation) are +// outside this scope on purpose, not by omission. +// +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +// An optional CLI arg overrides which pages tree gets walked, so a self-test +// can point this at a throwaway fixture tree instead of the real corpus. +// A6/D2's nav.adoc is unaffected — those checks are orthogonal to what a +// pages-tree override is for (exercising B2 in isolation). +const PAGES_DIR = process.argv[2] ? path.resolve(process.argv[2]) : path.join(ROOT, 'modules/ROOT/pages'); +const NAV_FILE = path.join(ROOT, 'modules/ROOT/nav.adoc'); + +// The four Diátaxis modes DOC_STYLE_GUIDE.md Part A defines. A1 rejects +// anything else, including the legacy `concept` value (never a Diátaxis +// mode — it was D2's subject noun leaking into A1's value namespace). +const VALID_MODES = new Set(['tutorial', 'how-to', 'reference', 'explanation']); + +// Directories whose pages are concept/tutorial material for D2's heuristic. +// Deliberately NOT keyed off :page-mode: — see the D2 comment above. +const CONCEPT_DIRS = [ + '2.cpp20-coroutines', '3.concurrency', '4.coroutines', + '5.buffers', '6.streams', '7.testing', +]; +const TUTORIAL_FILES = new Set(['quick-start.adoc']); + +function walk(dir) { + let out = []; + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, ent.name); + if (ent.isDirectory()) out = out.concat(walk(p)); + else if (ent.name.endsWith('.adoc')) out.push(p); + } + return out; +} + +function pageMode(text) { + const m = text.match(/^:page-mode:\s*(\S+)/m); + return m ? m[1] : null; +} + +function isConceptOrTutorial(relPath) { + const top = relPath.split(path.sep)[0]; + return TUTORIAL_FILES.has(relPath) || CONCEPT_DIRS.includes(top); +} + +// SHAPE's content heuristic (see the header comment). Deliberately narrow — +// this only needs to catch a block that is CLEARLY code, not judge style. +const CODE_SHAPE_PATTERNS = [ + /#include\b/, + /\bco_await\b/, + /\btemplate\s* CODE_SHAPE_PATTERNS.some((re) => re.test(l))); +} + +// Walk every block delimited by 4-or-more repeats of `ch` (`-` for +// listing/source blocks, `.` for literal blocks). Returns B2 and SHAPE +// findings (kind-tagged; the caller routes them to the right bucket). +function scanBlocks(lines, ch) { + const delim = new RegExp(`^\\${ch}{4,}$`); + const out = []; + let openLen = null; + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + if (!delim.test(trimmed)) continue; + if (openLen !== null) { + // The closer must repeat `ch` exactly as many times as the opener did; + // a mismatched length is content (or a nested delimiter of a + // different length) and does not close this block. + if (trimmed.length === openLen) openLen = null; + continue; + } + openLen = trimmed.length; + const openerLine = i; + + // The attribute line(s) immediately above (skipping blank lines before + // the stack begins), if any. AsciiDoc permits a block's attribute list + // to be split across multiple adjacent `[...]` lines (no blank line + // between them) and Asciidoctor merges them into one; reading only the + // single nearest line missed a role= or [source,...] marker set on an + // earlier line in the stack, e.g.: + // [source,cpp] + // [role=output] + // ---- + // which used to read attr as just `[role=output]`, miss isSource, and + // fall into the bare-listing branch below instead of B2. Walk upward + // collecting consecutive lines, but ONLY ones that look like a + // continuation of THIS block's attribute list -- `[source,...]` or + // `[role=...]`, the only two shapes isSource/hasClearingRole/ + // hasNonCodeRole below ever inspect. A bare `/^\[.*\]$/` walk is too + // wide: a block anchor (`[[id]]`), an admonition style (`[NOTE]`), or a + // quote attribution (`[quote,...]`) can legitimately sit directly above + // a block with no blank line between, and merging one of those ahead of + // a real `[source,cpp,role=pseudocode]` line made the joined string no + // longer start with `[source`, wrongly flagging an exempt block as B2. + // Stopping the walk at the first non-source/non-role line excludes them. + const ATTR_CONTINUATION = /^\[(?:source\b|role=)/i; + let a = openerLine - 1; + while (a >= 0 && lines[a].trim() === '') a--; + const attrLineIdxs = []; + while (a >= 0 && ATTR_CONTINUATION.test(lines[a].trim())) { + attrLineIdxs.unshift(a); + a--; + } + const attr = attrLineIdxs.map((idx) => lines[idx].trim()).join(' '); + const attrTopLine = attrLineIdxs.length ? attrLineIdxs[0] : openerLine; + const isSource = /^\[source\s*,\s*[^,\]]+/i.test(attr); + const hasClearingRole = /role=(pseudocode|external)\b/.test(attr); + const hasNonCodeRole = /role=(output|figure)\b/.test(attr); + + // A [source,,role=pseudocode|external] block: not a B2 candidate. + // NB this is deliberately isSource-gated — role=output/role=figure must + // NOT clear a [source,*] block (that would make role=output a blanket + // exemption for real code); only pseudocode/external do that, and only + // on a [source,*] block. + if (isSource && hasClearingRole) continue; + + if (!isSource && hasNonCodeRole) { + // Bare listing explicitly marked as program output / a figure: not a + // B2 candidate, but SHAPE still looks at its content (advisory). + const body = []; + for (let m = openerLine + 1; m < lines.length; m++) { + const t = lines[m].trim(); + if (delim.test(t) && t.length === openLen) break; + body.push(lines[m]); + } + if (looksLikeCode(body)) { + out.push({ + kind: 'SHAPE', + line: openerLine + 1, + message: `role=output/role=figure block's content looks like code, not literal output/a figure (advisory, not gated)`, + }); + } + continue; + } + + // Everything else — any [source,] block without a clearing role, + // and any bare listing without a role=output/role=figure marker — must + // open on a compiled include, or it is raw code pasted into the page. + let k = openerLine + 1; + while (k < lines.length && lines[k].trim() === '') k++; + const first = (lines[k] || '').trim(); + if (!first.startsWith('include::example$')) { + const line = attr !== '' ? attrTopLine + 1 : openerLine + 1; + const message = isSource + ? 'raw code, not include::example$/role=pseudocode/role=external' + : 'raw code in a bare listing, not include::example$/role=output/role=figure — this block must not contain code'; + out.push({ kind: 'B2', line, message }); + } + } + return out; +} + +// Prose lines only: a `[[...]]` inside a delimited block is literal and safe. +// Tracks delimiter depth the same way scanBlocks does (4-or-more repeats, the +// closer must match the opener's length) so a `----` nested in a `-----` does +// not end the outer block early and expose its body to the scan. +function scanAnchors(lines) { + const out = []; + let openLen = null; + let openCh = null; + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + const d = /^([-.=_*+])\1{3,}$/.exec(trimmed); + if (d) { + if (openLen === null) { + openLen = trimmed.length; + openCh = d[1]; + } else if (d[1] === openCh && trimmed.length === openLen) { + openLen = null; + openCh = null; + } + continue; + } + if (openLen !== null) continue; + if (/`\[\[/.test(lines[i])) { + out.push({ + line: i + 1, + message: + 'attribute written as `[[...]]` in a code span renders as an empty ' + + '(Asciidoctor reads it as an inline anchor); use a passthrough `+[[...]]+`', + }); + } + } + return out; +} + +const findings = { A1: [], A6: [], B2: [], SHAPE: [], ANCHOR: [], D2: [] }; +const files = walk(PAGES_DIR); + +for (const file of files) { + const rel = path.relative(PAGES_DIR, file); + const text = fs.readFileSync(file, 'utf8'); + const mode = pageMode(text); + + if (!mode) { + findings.A1.push({ file: rel, message: 'no :page-mode: attribute' }); + } else if (!VALID_MODES.has(mode)) { + findings.A1.push({ file: rel, message: `invalid :page-mode: value '${mode}' (must be one of ${[...VALID_MODES].join(', ')})` }); + } + + // Walk every listing (`----`) and literal (`....`) delimited block — + // source or bare — for B2 and its SHAPE advisory sidecar. + const lines = text.split('\n'); + for (const b of [...scanBlocks(lines, '-'), ...scanBlocks(lines, '.')]) { + findings[b.kind].push({ file: rel, line: b.line, message: b.message }); + } + + for (const a of scanAnchors(lines)) { + findings.ANCHOR.push({ file: rel, line: a.line, message: a.message }); + } + + if (isConceptOrTutorial(rel) && !text.includes('include::example$')) { + findings.D2.push({ file: rel, message: 'tutorial/concept page has no include::example$' }); + } +} + +const navText = fs.readFileSync(NAV_FILE, 'utf8'); +const topEntries = navText.split('\n').filter(l => /^\* xref:/.test(l)); +const qsIndex = topEntries.findIndex(l => /quick-start/.test(l)); +if (qsIndex === -1 || qsIndex > 2) { + findings.A6.push({ file: 'nav.adoc', message: `quick-start at top-level position ${qsIndex + 1}, must be <= 3` }); +} + +const summary = Object.fromEntries(Object.entries(findings).map(([k, v]) => [k, v.length])); +console.log(JSON.stringify({ summary, findings }, null, 2)); +process.exit(0); diff --git a/doc/lint/extract-docstrings.mjs b/doc/lint/extract-docstrings.mjs new file mode 100644 index 000000000..22766f636 --- /dev/null +++ b/doc/lint/extract-docstrings.mjs @@ -0,0 +1,198 @@ +#!/usr/bin/env node +// +// extract-docstrings.mjs — pulls Doxygen/MrDocs docstring prose out of +// `include/boost/capy/**/*.hpp` into plain .adoc files so Vale (Style Guide +// Part F) can lint it, not only the Antora `.adoc` pages (Style Guide Part F, +// Task 2 Step 4b). Node built-ins only, no dependencies. +// +// For each header with at least one doc comment, writes a mirrored file under +// OUT_DIR (default doc/lint/.docstrings/, gitignored — generated output, not +// source) containing just the comment prose: `@code`/`@endcode` +// samples are dropped (not prose, and full of identifiers/punctuation that +// would drown real findings); `@param`/`@tparam`/`@return`/etc. tags have +// their tag keyword (and, for `@param`/`@tparam`, the parameter name) removed +// but keep their description text, since that's the part C.2/C.9/C.10 apply +// to. The file extension is .adoc so it picks up the same `[*.adoc]` section +// of doc/.vale.ini used for the Antora pages — no separate Vale config needed. +// +// The parameter name is dropped, not re-emitted as a label. Emitting it back +// as `name: description` — which this script did until 2026-07 — made every +// `@param`/`@tparam` in the library trip Google.Colons, whose token is +// `(? 0), so `li` is the whole set. +const LIST_ITEM = /^@li\b\s*/; +// `@par Some Title` is a SECTION TITLE, not the opening words of the paragraph +// under it, and it carries no terminal punctuation. Left as a bare line it was +// joined into the paragraph's first sentence and inflated its word count — the +// same class of defect as the bold run-in lead, but one no sentence-boundary +// rule can fix, because there is no boundary character to find. Exactly 7 of 202 +// C2 findings started on such a line and two of them were not violations +// (`ex/executor_ref.hpp` "Thread Safety" reported 26 for a real 24; +// `io/any_read_stream.hpp` "Immediate Completion" reported 27 for a real 24). +// So the title is emitted as its own paragraph. A bare `@par` with no title is +// Doxygen's plain paragraph break and contributes nothing. +const PAR_TITLE = /^@par\b\s*/; +const INLINE_REFS = /@(ref|p|c)\s+(\S+)/g; + +// A Doxygen `@li` item is a sentence, and the extractor used to hand Vale a run +// of them as consecutive lines with the `@li` keyword still in the text. Two +// defects followed, both fixture-confirmed: +// +// * Vale's sentence segmenter needs `. ` to break and will not break on +// `\n@li` (`@` is not a capital), so a run of PERIOD-LESS items collapsed +// into one pseudo-sentence. Five ten-word items produced exactly one +// Capy.SentenceLength alert whose Match field was literally 'li'; the same +// five items with terminal periods produced none. C2 was therefore measuring +// missing Doxygen punctuation, not sentence length. +// * The surviving `li` keyword spent a phantom word of the 25-word budget, so +// a list item's real limit was 24. A hand-counted 25-word item alerted. +// +// Each item is now emitted as its own paragraph (blank-line delimited, which is +// what makes it a separate block to asciidoctor and so a separate sentence +// scope) with the keyword removed and continuation lines folded in. +function cleanBlock(raw) { + // Drop @code ... @endcode samples entirely — not prose. + const noCode = raw.replace(/@code\b[\s\S]*?@endcode\b/g, ''); + const lines = noCode.split('\n').map((l) => l.trim()); + const prose = []; + let item = null; // text of the `@li` item currently being accumulated + const flush = () => { if (item !== null) { prose.push(item, ''); item = null; } }; + const separate = () => { if (prose.length && prose[prose.length - 1] !== '') prose.push(''); }; + for (let line of lines) { + if (line === '') { + // The item's own trailing blank line stands in for this one. + if (item !== null) flush(); else prose.push(''); + continue; + } + const li = LIST_ITEM.exec(line); + if (li) { + if (item !== null) flush(); else separate(); + item = line.slice(li[0].length).replace(INLINE_REFS, '$2'); + continue; + } + const par = PAR_TITLE.exec(line); + if (par) { + flush(); + separate(); + const title = line.slice(par[0].length).replace(INLINE_REFS, '$2').trim(); + if (title) prose.push(title, ''); + continue; + } + // A non-blank, non-tag line under an open item is its continuation. + if (item !== null) { + if (!line.startsWith('@')) { item += ` ${line.replace(INLINE_REFS, '$2')}`; continue; } + flush(); + } + line = line.replace(NAMED_TAGS, ''); + line = line.replace(BARE_TAGS, ''); + line = line.replace(INLINE_REFS, '$2'); + prose.push(line); + } + flush(); + return prose.join('\n').trim(); +} + +// Every `/* ... */` range in the file, so a `///` line sitting INSIDE one is not +// mistaken for a doc comment of its own. There is no such line in the tree today +// (`grep -rn '^[[:space:]]*\*.*///'` finds none), but the cost of being wrong is a +// commented-out doc comment silently entering the linted corpus. +function blockCommentRanges(text) { + const ranges = []; + for (const m of text.matchAll(/\/\*[\s\S]*?\*\//g)) ranges.push([m.index, m.index + m[0].length]); + return ranges; +} + +// Doc comments in source order. `/** ... */` blocks come from a direct match; a +// run of consecutive `///` lines is collected into one block, ended by the first +// line that is not a `///` line (blank, code, or anything else) — the same rule +// Doxygen applies. `///<` (trailing member doc) and `////`-style separator rules +// are matched by neither pattern; the tree contains none of either. +function docComments(text) { + const found = []; + for (const m of text.matchAll(/\/\*\*([\s\S]*?)\*\//g)) found.push({ at: m.index, raw: m[1] }); + + const inBlock = blockCommentRanges(text); + const covered = (off) => inBlock.some(([a, b]) => off >= a && off < b); + const lineRe = /^[ \t]*\/\/\/(?!\/)(.*)$/; + let off = 0; + let run = null; // { at, lines: [] } + const flushRun = () => { if (run) { found.push({ at: run.at, raw: run.lines.join('\n') }); run = null; } }; + for (const line of text.split('\n')) { + const m = lineRe.exec(line); + if (m && !covered(off)) { + if (!run) run = { at: off, lines: [] }; + run.lines.push(m[1].replace(/^[ \t]/, '')); + } else { + flushRun(); + } + off += line.length + 1; + } + flushRun(); + + return found.sort((a, b) => a.at - b.at).map((d) => d.raw); +} + +let written = 0; +for (const file of walk(INCLUDE_ROOT)) { + const text = fs.readFileSync(file, 'utf8'); + const blocks = docComments(text).map(cleanBlock).filter(Boolean); + if (blocks.length === 0) continue; + + const rel = path.relative(INCLUDE_ROOT, file); + const outPath = path.join(OUT_DIR, `${rel}.adoc`); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, blocks.join('\n\n') + '\n'); + written++; +} + +console.log(JSON.stringify({ headersWithDocs: written, outDir: path.relative(REPO_ROOT, OUT_DIR) }, null, 2)); diff --git a/doc/lint/fixtures/modules/ROOT/pages/5.buffers/hard.adoc b/doc/lint/fixtures/modules/ROOT/pages/5.buffers/hard.adoc new file mode 100644 index 000000000..35f8c13d0 --- /dev/null +++ b/doc/lint/fixtures/modules/ROOT/pages/5.buffers/hard.adoc @@ -0,0 +1,42 @@ += Hard-slice fixtures +:page-mode: concept + +// BACKTICK GUARD. 30 words, with one STRAY backtick and one balanced span, so the +// block's backtick count is odd. If the guard breaks, the length-preserving mask +// pairs the stray backtick with the balanced one and collapses everything between +// them into a single word: this sentence measures 8, produces NO finding, and +// nothing says so. Expect: one finding at 30 words AND one BACKTICK diagnostic. +One two three four five six `seven eight nine ten eleven twelve thirteen fourteen +fifteen sixteen seventeen eighteen nineteen twenty twentyone twentytwo twentythree +twentyfour twentyfive twentysix twentyseven twentyeight twentynine `thirty`. + +// ELLIPSIS, the only known UNDER-reporting case. 34 words with a mid-sentence +// `...`. If the boundary guard breaks this splits into 16 + 18 and is missed. +// The ellipsis is kept off the start of a line on purpose: a leading `... ` is a +// legitimate AsciiDoc level-3 ordered-list marker, and treating it as one is +// correct, so a line-initial ellipsis would test the wrong thing. +Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi ... omicron +pi rho sigma tau upsilon phi chi psi omega alef bet gimel dalet he vav zayin +het tet yod. + +// PARENTHESISED ABBREVIATION, the other under-reporting case. 30 words. +Aone atwo athree afour afive asix aseven aeight anine aten (e.g.) aeleven atwelve +athirteen afourteen afifteen asixteen aseventeen aeighteen anineteen atwenty +atwentyone atwentytwo atwentythree atwentyfour atwentyfive atwentysix +atwentyseven atwentyeight atwentynine athirty. + +// BOLD RUN-IN LEAD. The lead is its own sentence; the tail is 12 words. Neither +// is over the limit, so a merge would be visible as a spurious finding here. +*The library owns the handles.* Capy creates and manages the buffer handles and the handle sequences. + +// READER WORD COUNTING. 30 tokens under the retired Vale token, 25 as a reader +// counts: five hyphen compounds, one possessive, one contraction, one slashed +// list of three, one dotted form and one qualified identifier. Expect NO finding. +The most-derived fine-grained copy-on-write single-threaded well-known caller's don't read/write/seek buffer_array.hpp this_coro::executor_tag alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu. + +// CODE BLOCKS ARE NOT PROSE. If this is ever linted, the identifier below makes +// it obvious in the finding text. +[source,cpp] +---- +auto zzq = utilize_and_leverage(one, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve, thirteen, fourteen, fifteen, sixteen, seventeen, eighteen, nineteen, twenty, twentyone, twentytwo, twentythree, twentyfour, twentyfive, twentysix); +---- diff --git a/doc/lint/fixtures/modules/ROOT/pages/9.design/advisory.adoc b/doc/lint/fixtures/modules/ROOT/pages/9.design/advisory.adoc new file mode 100644 index 000000000..30f025713 --- /dev/null +++ b/doc/lint/fixtures/modules/ROOT/pages/9.design/advisory.adoc @@ -0,0 +1,8 @@ += Advisory-slice fixture +:page-mode: concept + +// This page is under 9.design/, so its findings must be keyed `advisory-C2` and +// must NOT be reachable from a `--gate 'sentence_length:^C2:'` spec. +Advisory one two three four five six seven eight nine ten eleven twelve thirteen +fourteen fifteen sixteen seventeen eighteen nineteen twenty twentyone twentytwo +twentythree twentyfour twentyfive twentysix twentyseven. diff --git a/doc/lint/fixtures/modules/ROOT/pages/9.designish/lookalike.adoc b/doc/lint/fixtures/modules/ROOT/pages/9.designish/lookalike.adoc new file mode 100644 index 000000000..ad35a173e --- /dev/null +++ b/doc/lint/fixtures/modules/ROOT/pages/9.designish/lookalike.adoc @@ -0,0 +1,8 @@ += Look-alike directory fixture +:page-mode: concept + +// `9.designish/` is NOT `9.design/`. The advisory test matches a whole path +// segment, so this page's finding must stay in the HARD slice. +Lookalike one two three four five six seven eight nine ten eleven twelve thirteen +fourteen fifteen sixteen seventeen eighteen nineteen twenty twentyone twentytwo +twentythree twentyfour twentyfive twentysix twentyseven. diff --git a/doc/lint/mrdocs-warnings.mjs b/doc/lint/mrdocs-warnings.mjs new file mode 100644 index 000000000..73f70889c --- /dev/null +++ b/doc/lint/mrdocs-warnings.mjs @@ -0,0 +1,223 @@ +#!/usr/bin/env node +// +// mrdocs-warnings.mjs — reference-surface gate (Style Guide Part F.0, +// "MrDocs-no-warnings"). Node built-ins only, no dependencies. +// +// MrDocs has no standalone CLI package: it runs inside +// @cppalliance/antora-cpp-reference-extension during `npx antora`. Scanning +// the *captured Antora build log* does not work — the extension's runCommand +// helper only forwards MrDocs's stderr to the console; MrDocs prints its +// per-symbol "undocumented"/"missing param doc" findings to stdout, and the +// extension swallows stdout into an internal buffer it never surfaces +// (lib/extension.js, runCommand: `output` is not set for the MrDocs +// invocation, so `ps.stdout` data goes to an array, not the console). +// Verified locally: an Antora build with mrdocs.yml's warning flags on +// produced 0 visible findings in the build log, while invoking the same +// MrDocs binary/config/args directly produced 208. +// +// So this script invokes MrDocs directly, with the same config file and CLI +// arguments the extension uses (mirrored from its debug log), then parses +// combined stdout+stderr itself. It mirrors the extension's own compiler +// preference (clang++/clang over g++/gcc — the extension does this because +// this MrDocs build crashes with a stray GCC-only header path; reproduced +// locally with GCC 16.1.1) so results match what a real Antora build would +// hit if not for the stdout-swallowing bug above. +// +// Non-blocking (Task 2): always exits 0. Findings are JSON on stdout. +// +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +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 CONFIG_PATH = path.join(DOC_DIR, 'mrdocs.yml'); + +function emit(payload) { + console.log(JSON.stringify(payload, null, 2)); + process.exit(0); +} + +// Pinned MrDocs version (Task 14 / Phase-2 exit). The cache DFS below can +// surface more than one `mrdocs` binary (e.g. the `develop` and `master` +// reference-collector tags), and their `--version` strings can differ — so +// picking the FIRST one found made the "no-warnings" gate nondeterministic. +// We constrain the search to the binary whose BASE version (the `X.Y.Z` +// before any `+build` metadata) matches this pin; if none matches we error +// loudly rather than silently scan with an unexpected version. Override via +// MRDOCS_VERSION for a deliberate bump. +const PINNED_VERSION = process.env.MRDOCS_VERSION || '0.8.0'; + +function findOnPath(names) { + return findAllOnPath(names)[0] || null; +} + +function findAllOnPath(names) { + const found = []; + const dirs = (process.env.PATH || '').split(path.delimiter); + for (const name of names) { + for (const dir of dirs) { + const candidate = path.join(dir, name); + try { + fs.accessSync(candidate, fs.constants.X_OK); + found.push(candidate); + } catch { /* not here */ } + } + } + return found; +} + +// Search the Antora reference-collector cache the extension populates +// (getUserCacheDir('antora')/reference-collector/mrdocs///bin/mrdocs). +// Returns EVERY executable found so the caller can pick the pin-matching one. +function findAllMrDocsInCache() { + const found = []; + const bases = [ + process.env.MRDOCS_ROOT, + path.join(os.homedir(), '.cache/antora/reference-collector/mrdocs'), + ].filter(Boolean); + for (const base of bases) { + if (!fs.existsSync(base)) continue; + const stack = [base]; + while (stack.length) { + const dir = stack.pop(); + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; } + for (const ent of entries) { + const p = path.join(dir, ent.name); + if (ent.isDirectory()) stack.push(p); + else if (ent.name === 'mrdocs' || ent.name === 'mrdocs.exe') { + try { fs.accessSync(p, fs.constants.X_OK); found.push(p); } catch { /* skip */ } + } + } + } + } + return found; +} + +// Base version (X.Y.Z, build metadata after `+` stripped) reported by a +// candidate binary, or null if it can't be run / parsed. +function mrdocsBaseVersion(exe) { + const r = spawnSync(exe, ['--version'], { encoding: 'utf8' }); + if (r.error || (r.status !== 0 && r.status !== null)) return null; + const out = `${r.stdout || ''}${r.stderr || ''}`; + const m = out.match(/MrDocs\s+version\s+(\S+)/i) || out.match(/(\d+\.\d+\.\d+)/); + return m ? m[1].split('+')[0] : null; +} + +// Candidate search space: PATH first, then the reference-collector cache. +const candidates = [...findAllOnPath(['mrdocs', 'mrdocs.exe']), ...findAllMrDocsInCache()]; +if (candidates.length === 0) { + emit({ + error: 'mrdocs executable not found (checked PATH and the Antora reference-collector cache). ' + + 'Run the Antora build first (it downloads MrDocs), then re-run this script.', + summary: { total: 0 }, + findings: [], + }); +} + +// Select the FIRST candidate whose base version matches the pin. +let mrdocsExe = null; +const inspected = []; +for (const c of candidates) { + const v = mrdocsBaseVersion(c); + inspected.push(`${c} => ${v ?? '(version unreadable)'}`); + if (v === PINNED_VERSION) { mrdocsExe = c; break; } +} +if (!mrdocsExe) { + emit({ + error: `no MrDocs binary matching pinned version ${PINNED_VERSION} found ` + + `(set MRDOCS_VERSION to override). Candidates inspected:\n ${inspected.join('\n ')}`, + summary: { total: 0 }, + findings: [], + }); +} + +if (!fs.existsSync(CONFIG_PATH)) { + emit({ error: `mrdocs.yml not found: ${CONFIG_PATH}`, summary: { total: 0 }, findings: [] }); +} + +// Mirror CppReferenceExtension.findCXXCompilers(): clang++/clang preferred over g++/gcc. +const cxx = findOnPath(['clang++']) || process.env.CXX_COMPILER || process.env.CXX || findOnPath(['g++']); +const cc = findOnPath(['clang']) || process.env.C_COMPILER || process.env.CC || findOnPath(['gcc']); + +const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mrdocs-warnings-')); +const args = [ + `--config=${CONFIG_PATH}`, + `--output=${outDir}`, + '--generator=adoc', + '--multipage=true', + '--tagfile=reference.tag.xml', +]; + +const result = spawnSync(mrdocsExe, args, { + cwd: REPO_ROOT, + env: { ...process.env, ...(cxx ? { CXX: cxx, CMAKE_CXX_COMPILER: cxx } : {}), ...(cc ? { CC: cc, CMAKE_C_COMPILER: cc } : {}) }, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, +}); + +fs.rmSync(outDir, { recursive: true, force: true }); + +if (result.error || (result.status !== 0 && result.status !== null)) { + emit({ + error: `mrdocs exited with status ${result.status}: ${result.error?.message || '(see stderr)'}`, + stderrTail: (result.stderr || '').slice(-2000), + summary: { total: 0 }, + findings: [], + }); +} + +const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, ''); +const relIncludePath = (p) => p.replace(/^.*?(include\/boost\/capy\/.*)$/, '$1'); +const combined = stripAnsi(`${result.stdout || ''}\n${result.stderr || ''}`); +const lines = combined.split('\n'); + +// MrDocs prints ":::" then indented "N) " items for +// that location; separately it prints bare "warning: ..." lines (e.g. +// unsupported HTML tags) with no location. CMake's own "CMake Warning" noise +// is excluded — it is a build-system warning, not a reference-surface one. +const LOC_RE = /^(\/\S+\.(?:hpp|cpp|ipp)):(\d+):(\d+):\s*$/; +const ITEM_RE = /^\s*\d+\)\s*(.+)$/; +const BARE_WARNING_RE = /^warning:\s*(.+)$/; + +// `boost`/`capy` namespaces get flagged by warn-if-undocumented too, but a +// namespace isn't a documentable symbol in the sense this gate cares about +// (no @brief slot maps onto a namespace declaration the way it does onto a +// class/function), and the finding is fingerprint-unstable across +// environments (see the file header: local vs. CI MrDocs build/order +// differences). Drop these here so they never enter the gate's finding list, +// in both baseline generation and the live check (same script). All other +// MrDocs warning classes (undocumented symbol/param, broken refs, etc.) are +// left intact. +const NAMESPACE_UNDOCUMENTED_RE = /namespace is undocumented/; + +const findings = []; +let currentLoc = null; +for (const line of lines) { + const loc = line.match(LOC_RE); + if (loc) { + currentLoc = { file: relIncludePath(loc[1]), line: Number(loc[2]) }; + continue; + } + const item = line.match(ITEM_RE); + if (item && currentLoc) { + const message = item[1].trim(); + if (!NAMESPACE_UNDOCUMENTED_RE.test(message)) { + findings.push({ file: currentLoc.file, line: currentLoc.line, message }); + } + continue; + } + const bare = line.match(BARE_WARNING_RE); + if (bare) { + const message = bare[1].trim(); + if (!NAMESPACE_UNDOCUMENTED_RE.test(message)) { + findings.push({ file: null, line: null, message }); + } + } +} + +emit({ summary: { total: findings.length }, findings }); diff --git a/doc/lint/run-a11y.mjs b/doc/lint/run-a11y.mjs new file mode 100644 index 000000000..cdb3cf9c5 --- /dev/null +++ b/doc/lint/run-a11y.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// +// run-a11y.mjs — accessibility contrast gate (Style Guide rule E4 / Part F.0 +// "a11y contrast" warning). Node built-ins + the pa11y-ci devDependency +// (doc/package.json) + `python3` to serve the built site (present on +// ubuntu-latest CI runners and used here instead of a hand-rolled static +// server — a from-scratch Node server produced silent per-page navigation +// errors from pa11y/axe instead of real findings when this was tried). +// +// Requires the Antora site to already be built (doc/build/site). +// +// Usage: node doc/lint/run-a11y.mjs +// Non-blocking (Task 2): always exits 0. Findings are JSON on stdout. +// +import fs from 'node:fs'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn, spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const DOC_DIR = path.resolve(SCRIPT_DIR, '..'); + +function emit(payload) { + console.log(JSON.stringify(payload, null, 2)); + process.exit(0); +} + +const siteDir = path.join(DOC_DIR, 'build/site'); +if (!fs.existsSync(siteDir)) { + emit({ error: 'doc/build/site not built — run the Antora build first', summary: { total: 0 }, findings: [] }); +} + +const configPath = path.join(DOC_DIR, '.pa11yci.json'); +if (!fs.existsSync(configPath)) { + emit({ error: `pa11y config not found: ${configPath}`, summary: { total: 0 }, findings: [] }); +} + +const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + +// Browser path is env-overridable (PA11Y_CHROME_PATH) so the same config works +// on hosts whose browser lives elsewhere. .pa11yci.json hard-codes +// /usr/bin/chromium (the local default); GitHub's ubuntu-latest ships +// google-chrome at /usr/bin/google-chrome, not chromium, and now that the E4 +// contrast gate is BLOCKING an unlaunchable browser would fail the build on +// missing infra (or, worse, pass vacuously). CI sets PA11Y_CHROME_PATH to the +// runner's browser. We inject the override and run pa11y-ci against a derived +// config so the committed JSON stays the local default. +const chromePath = process.env.PA11Y_CHROME_PATH; +let effectiveConfigPath = configPath; +if (chromePath) { + config.defaults = config.defaults || {}; + config.defaults.chromeLaunchConfig = config.defaults.chromeLaunchConfig || {}; + config.defaults.chromeLaunchConfig.executablePath = chromePath; + effectiveConfigPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'pa11y-cfg-')), '.pa11yci.json'); + fs.writeFileSync(effectiveConfigPath, JSON.stringify(config, null, 2)); +} + +const firstUrl = (config.urls || [])[0]; +const portMatch = firstUrl && firstUrl.match(/:(\d+)\b/); +const PORT = portMatch ? Number(portMatch[1]) : 8088; + +function waitForPort(port, deadlineMs) { + const start = Date.now(); + return new Promise((resolve, reject) => { + (function attempt() { + const sock = net.connect(port, '127.0.0.1'); + sock.once('connect', () => { sock.destroy(); resolve(); }); + sock.once('error', () => { + sock.destroy(); + if (Date.now() - start > deadlineMs) reject(new Error(`nothing listening on :${port}`)); + else setTimeout(attempt, 100); + }); + })(); + }); +} + +const server = spawn('python3', ['-m', 'http.server', String(PORT), '--directory', siteDir], { stdio: 'ignore' }); +let payload; +try { + await waitForPort(PORT, 5000); + const bin = path.join(DOC_DIR, 'node_modules/.bin/pa11y-ci'); + const r = spawnSync(bin, ['--config', effectiveConfigPath, '--json'], { cwd: DOC_DIR, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); + // Distinguish a real run from a browser-launch failure. When pa11y cannot + // launch the browser (e.g. executablePath points at a missing binary) it + // writes NOTHING to stdout and the error to stderr, exiting non-zero. A naive + // `JSON.parse(stdout || '{}')` would then yield `{}` — indistinguishable from + // "ran clean, 0 findings" — and, now that the E4 contrast gate is BLOCKING, + // let the gate pass VACUOUSLY while silently guarding nothing. So an empty or + // shapeless result (no `results` object) is reported as an error → the + // baseline marks the check skipped → a skip of this GATED check fails the + // gate loudly, which is the correct outcome for "couldn't actually check". + let parsed = null; + if (r.stdout && r.stdout.trim()) { + try { parsed = JSON.parse(r.stdout); } catch { parsed = null; } + } + const looksLikeResults = parsed && typeof parsed === 'object' && parsed.results && typeof parsed.results === 'object'; + if (!looksLikeResults) { + payload = { + error: `pa11y-ci produced no usable results (exit ${r.status}) — browser failed to launch? ` + + `Check the a11y browser path (PA11Y_CHROME_PATH / .pa11yci.json executablePath).`, + stderrTail: (r.stderr || '').slice(-2000), summary: { total: 0 }, findings: [], + }; + } + if (!payload) { + const findings = []; + for (const [url, items] of Object.entries(parsed.results || {})) { + const publicUrl = url.replace(/^https?:\/\/[^/]+/, ''); + for (const it of items) findings.push({ url: publicUrl, code: it.code, type: it.type, selector: it.selector, message: it.message }); + } + payload = { summary: { total: findings.length, contrast: findings.filter((f) => f.code === 'color-contrast').length }, findings }; + } +} catch (err) { + payload = { error: `could not reach local server: ${err.message}`, summary: { total: 0 }, findings: [] }; +} finally { + server.kill(); +} +emit(payload); diff --git a/doc/lint/selftest.mjs b/doc/lint/selftest.mjs new file mode 100644 index 000000000..8acdef0dc --- /dev/null +++ b/doc/lint/selftest.mjs @@ -0,0 +1,508 @@ +#!/usr/bin/env node +// +// 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 +// +// selftest.mjs — asserts that sentence-length.mjs and doc-lint.mjs's B2 check +// still detect what they claim to, against the checked-in corpus in +// lint/fixtures/ (sentence-length.mjs) or a throwaway fixture tree built at +// run time (doc-lint.mjs's B2 section). Node built-ins only. Exit 0 = all +// assertions hold; exit 1 = at least one broke, with the failure named. Run +// it after any edit to sentence-length.mjs or doc-lint.mjs. +// +// Why this exists. The C2 checker is on its way to becoming a merge-blocking +// gate, and the properties below are exactly the ones whose failure is SILENT: +// nothing in the real corpus exercises them, so a plausible refactor can retire +// a protection and every downstream number still looks reasonable. Two such +// refactors were demonstrated on the unbalanced-backtick guard alone — making +// the backtick pattern lenient returns the diagnostic count to 0 and makes a +// 30-word sentence vanish with no finding at all, and renaming the rule's `id` +// without updating maskBlock()'s skip string keeps the diagnostic but silently +// drops the count correction. Both pass every corpus-level check. Neither passes +// this file. +// +// Fixtures live in lint/fixtures/ and are NOT part of either linted corpus: +// Vale runs on `modules` and `lint/.docstrings` only, and Antora reads +// modules/ via antora.yml, so nothing else sees them. +// +// Usage: node doc/lint/selftest.mjs +// +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const DOC_DIR = path.resolve(SCRIPT_DIR, '..'); +const FIXTURES = path.join(SCRIPT_DIR, 'fixtures', 'modules'); + +const r = spawnSync('node', [path.join(SCRIPT_DIR, 'sentence-length.mjs'), FIXTURES], + { encoding: 'utf8', cwd: DOC_DIR, maxBuffer: 16 * 1024 * 1024 }); +if (r.status !== 0) { + console.error(`selftest: sentence-length.mjs exited ${r.status}\n${r.stderr || r.stdout}`); + process.exit(1); +} +const out = JSON.parse(r.stdout); +const hard = out.findings.C2; +const advisory = out.findings['advisory-C2']; +const backtick = out.findings.BACKTICK; + +// A finding is identified by the first word of its sentence, which is unique per +// fixture and survives a change to line numbering. +const lead = (f) => f.sentence.replace(/^[^A-Za-z`]*/, '').split(/[\s`]+/)[0]; +const find = (arr, word) => arr.filter((f) => lead(f) === word); + +const failures = []; +// Counted, not hand-written: a literal here goes stale silently, and a total +// nobody can trust is worse than no total. `check` tallies itself and the +// summary reports the tally. one()/none() delegate to check(), so they are +// counted there. +let assertionCount = 0; +function check(name, cond, detail) { + assertionCount++; + if (cond) return; + failures.push(`${name}${detail ? ` — ${detail}` : ''}`); +} +function one(name, arr, word, words) { + const hits = find(arr, word); + if (hits.length !== 1) { + assertionCount++; + failures.push(`${name} — expected exactly 1 finding leading with '${word}', got ${hits.length}`); + return; + } + check(name, hits[0].words === words, `'${word}' measured ${hits[0].words} words, expected ${words}`); +} +function none(name, arr, word) { + const hits = find(arr, word); + check(name, hits.length === 0, + `expected no finding leading with '${word}', got ${hits.length} (${hits.map((h) => `${h.words}w`).join(', ')})`); +} + +// 1. The unbalanced-backtick guard. Both halves matter: the diagnostic must be +// emitted AND the sentence must still be measured at its full length. A +// lenient backtick pattern loses both; a stale skip id loses only the second. +one('backtick guard: sentence measured at full length', hard, 'One', 30); +check('backtick guard: diagnostic emitted', backtick.length === 1, + `expected 1 BACKTICK finding, got ${backtick.length}`); +check('backtick guard: summary counts it', out.summary.unbalancedBackticks === 1, + `summary.unbalancedBackticks = ${out.summary.unbalancedBackticks}`); + +// 2. The two UNDER-reporting cases. These are the only known ways a real +// violation can slip through, so they are the assertions that protect the +// gate's floor rather than its ceiling. +one('mid-sentence ellipsis does not split the sentence', hard, 'Alpha', 34); +one('parenthesised abbreviation does not split the sentence', hard, 'Aone', 31); + +// 3. Bold run-in lead is its own sentence, so neither half is over the limit. +none('bold run-in lead does not merge into the next sentence', hard, 'The'); + +// 4. Reader word counting. 30 tokens under the retired Vale token, 25 as a +// reader counts, so any regression in the connector set makes this fire. +none('reader word counting keeps a 25-word sentence under the limit', hard, 'most-derived'); + +// 5. Code blocks are not prose. +check('code blocks are not linted as prose', + !JSON.stringify(out.findings).includes('utilize_and_leverage'), + 'a [source,cpp] block reached the linter'); + +// 6. The hard/advisory partition, in both directions, including the look-alike +// directory that must NOT be treated as an essay. +one('9.design/ findings are advisory', advisory, 'Advisory', 28); +none('9.design/ findings are not in the hard slice', hard, 'Advisory'); +one('a 9.designish/ look-alike stays in the hard slice', hard, 'Lookalike', 28); + +// 7. The gate-reachability property itself, tested on the rule keys rather than +// asserted in a comment: a `^C2:` spec must reach the hard key and nothing +// else, whatever the keys are renamed to. +const GATE = /^C2:/; +const keys = Object.keys(out.findings); +check('rule keys are C2 / advisory-C2 / BACKTICK', keys.join(',') === 'C2,advisory-C2,BACKTICK', + `got '${keys.join(',')}'`); +check('only the hard key is reachable from a ^C2: gate spec', + keys.filter((k) => GATE.test(`${k}:some/file.adoc:#1:message`)).join(',') === 'C2', + `reachable keys: '${keys.filter((k) => GATE.test(`${k}:f:#1:m`)).join(',')}'`); + +// 8. extract-docstrings.mjs covers BOTH Doxygen comment forms. `///` runs were +// invisible to every gate until 2026-08 — 86 published doc lines across 25 +// headers, and the gap surfaced only because a bite test happened to plant its +// first probe in a `///` comment. A tightened regex or a reverted branch would +// retire the coverage silently: the corpus just gets smaller, every count drops, +// and nothing reads as broken. The expectations below are DERIVED from the real +// header tree rather than written down, so they cannot go stale. +const INCLUDE_ROOT = path.resolve(DOC_DIR, '..', 'include/boost/capy'); +const TMP_OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'capy-selftest-docstrings-')); +try { + const walkHpp = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { + const p = path.join(dir, e.name); + return e.isDirectory() ? walkHpp(p) : (e.name.endsWith('.hpp') ? [p] : []); + }); + // Headers whose ONLY doc comments are `///` runs: they have no output file at all + // unless the `///` branch works, which makes them the sharpest available probe. + const slashOnly = walkHpp(INCLUDE_ROOT).filter((f) => { + const t = fs.readFileSync(f, 'utf8'); + return /^[ \t]*\/\/\/(?!\/)/m.test(t) && !t.includes('/**'); + }).map((f) => `${path.relative(INCLUDE_ROOT, f)}.adoc`); + + const x = spawnSync('node', [path.join(SCRIPT_DIR, 'extract-docstrings.mjs'), TMP_OUT], + { encoding: 'utf8', cwd: DOC_DIR, maxBuffer: 16 * 1024 * 1024 }); + check('extract-docstrings.mjs exits 0', x.status === 0, `exited ${x.status}: ${(x.stderr || '').trim().slice(-200)}`); + check('extract-docstrings.mjs finds some `///`-only headers to prove the branch on', + slashOnly.length > 0, 'no header in the tree has `///` docs and no `/** */` block'); + const missing = slashOnly.filter((rel) => !fs.existsSync(path.join(TMP_OUT, rel))); + check('`///` doc comments are extracted', missing.length === 0, + `${missing.length} of ${slashOnly.length} ` + + `\`///\`-only header(s) produced no output: ${missing.slice(0, 3).join(', ')}`); +} finally { + fs.rmSync(TMP_OUT, { recursive: true, force: true }); +} + +// 9. doc-lint.mjs's B2 check ("no code block holds raw code") must reach +// every [source,] block, not just [source,cpp]/[source,c++] — that +// was the whole gap a prior widening closed — and must also reach bare +// `----`/`....` listings, while leaving role=pseudocode/external/ +// output/figure and include::example$ blocks alone. There was previously +// no self-test coverage for doc-lint.mjs at all, so a regex narrowed back +// to one language, or a bare-listing branch that stopped firing, would +// pass every corpus-level check silently. Exercised against a throwaway +// fixture tree (not lint/fixtures/, which only sentence-length.mjs reads) +// so a real corpus edit can't perturb these counts. +// +// flagged.adoc's `[source,cmake]` case is a weaker property than its name +// suggests: narrowing isSource back to cpp/c++-only does NOT clear that +// finding, because a de-recognized [source,cmake] block still falls into +// the bare-listing branch and gets flagged there instead (same result, +// different code path). The real proof that isSource covers every +// language lives on the CLEARING side, in clear.adoc: a +// [source,cmake,role=pseudocode] block is invisible to B2 only if +// isSource recognizes cmake — if it doesn't, that block falls into the +// bare-listing branch too, where role=pseudocode is NOT a recognized +// non-code role, and it lights up clear.adoc instead. That is where an +// isSource regression actually surfaces; flagged.adoc's cmake case is +// kept only because catching the "still gets flagged, for the wrong +// reason" case is itself worth asserting. +{ + const DOCLINT_TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'capy-selftest-doclint-')); + try { + const write = (rel, body) => { + const fp = path.join(DOCLINT_TMP, rel); + fs.mkdirSync(path.dirname(fp), { recursive: true }); + fs.writeFileSync(fp, body); + }; + // One [source,cmake] block with no clearing role (see the comment + // above — flagged via isSource OR the bare-listing fallback, either + // way); one bare `----` block with no role=output/role=figure marker + // and real code in it (bare listings were invisible to any + // [source,...] regex before B2 was widened); and one [source,cpp, + // role=output] block — role=output/role=figure must clear ONLY a bare + // listing, never a [source,*] block. A mutation that ORs hasClearingRole + // and hasNonCodeRole together (ignoring isSource) makes role=output a + // blanket exemption for real C++ and this block stops being flagged. + write('flagged.adoc', [ + ':page-mode: how-to', + '', + '= Flagged', + '', + '[source,cmake]', + '----', + 'add_executable(x x.cpp)', + '----', + '', + '----', + 'int x = 1;', + '----', + '', + '[source,cpp,role=output]', + '----', + 'int y = 2;', + '----', + '', + ].join('\n')); + // Every exemption B2 recognizes, one of each, all in a single page that + // must produce zero findings. role=pseudocode and role=external are + // BOTH tested here deliberately: they are two different alternatives in + // the same regex, and dropping either one independently keeps this + // fixture passing for the OTHER unless both are exercised (dropping + // `external` alone was a measured miss — the corpus is cleared mostly + // by `external`, not `pseudocode`, e.g. 9k/9l/9n/9o/5d). + write('clear.adoc', [ + ':page-mode: how-to', + '', + '= Clear', + '', + '[source,cmake,role=pseudocode]', + '----', + 'add_executable(x x.cpp)', + '----', + '', + '[source,cpp,role=external]', + '----', + 'task async_work();', + '----', + '', + '[role=output]', + '----', + 'build succeeded', + '----', + '', + '[role=figure]', + '----', + '[A] --> [B]', + '----', + '', + '[source,cpp]', + '----', + 'include::example$foo.cpp[tag=bar]', + '----', + '', + ].join('\n')); + // SHAPE: a role=output block whose content looks like code is a + // permanent B2 blind spot by design (that is what role=output is FOR), + // so SHAPE must still flag it — advisory, not gated. A genuine output + // block (no code-shaped line) must not trip SHAPE at all. + write('shape.adoc', [ + ':page-mode: how-to', + '', + '= Shape', + '', + '[role=output]', + '----', + 'int z = 3;', + '----', + '', + '[role=output]', + '----', + 'Hello from Capy!', + '----', + '', + ].join('\n')); + // I1/I2: a 5-dash listing must not hide code from B2 (closer length + // must match opener length, not just be >=4), and a `....` literal + // block is a second bare-listing syntax B2 must also reach. The + // 5-dash case above pairs a matching 5-dash closer with its 5-dash + // opener, which does NOT exercise the closer-length check at all — a + // mutation loosening it from `===` to `>=` still passes that case. + // The role=figure block below is the real regression this fixture was + // missing (measured against the real corpus, 9b.Separation.adoc's CCD + // diagram): its body contains a dash-only line LONGER than its own + // 4-dash opener. Under `===` this is correctly just content, and the + // real closer below it ends the block with zero findings. Under `>=` + // the long dash-only line is wrongly accepted as an early closer, and + // the real closing `----` is then misread as a brand-new, attribute-less + // opener with nothing after it — a false B2 finding on a block that + // never contained code. + write('delimiters.adoc', [ + ':page-mode: how-to', + '', + '= Delimiters', + '', + '-----', + 'int five_dash = 1;', + '-----', + '', + '....', + 'int four_dot = 1;', + '....', + '', + '[role=figure]', + '----', + '-------------------', + 'CCD = 5', + '----', + '', + ].join('\n')); + + // G1 (final-review fix): AsciiDoc permits a block's attribute list to be + // split across multiple adjacent `[...]` lines, and Asciidoctor merges + // them into one. scanBlocks() used to read only the single nearest + // `[...]` line above the delimiter, so a `[source,cpp]` marker one line + // further up was invisible: `isSource` came back false, the block took + // the bare-listing branch, and role=output cleared it as non-code — B2:0. + // SHAPE, which still looks at bare-listing content, ALSO missed it: its + // `;\s*$` pattern is defeated by the trailing `// running sum` comment, + // which is this corpus's own annotation idiom for [role=output] blocks. + // The result was a highlighted C++ source block invisible to both B2 and + // SHAPE. Confirmed to reproduce against the pre-fix scanBlocks() (attr + // read from the single nearest line only) before landing the fix above. + write('split-attr.adoc', [ + ':page-mode: how-to', + '', + '= Split Attr', + '', + '[source,cpp]', + '[role=output]', + '----', + 'int total = 0; // running sum', + '----', + '', + ].join('\n')); + + // G1 review-round-2 fix: widening the attribute walk to ANY consecutive + // `[...]`-shaped line (the first cut of the fix above) created a false + // positive. A block anchor (`[[id]]`), an admonition style (`[NOTE]`), + // or a quote attribution (`[quote,...]`) can legitimately sit directly + // above a block with no blank line between; merging one of those ahead + // of a real `[source,cpp,role=pseudocode]` line made the joined string + // no longer start with `[source`, so `isSource` went false and a + // legitimately-exempt pseudocode block was wrongly flagged as B2. The + // walk must stop at the first line that is not itself a `[source,...]` + // or `[role=...]` continuation. Confirmed to reproduce against the + // review-round-1 fix (any `[...]`-shaped line merged) before landing + // the ATTR_CONTINUATION restriction above. + write('anchor-above-pseudocode.adoc', [ + ':page-mode: how-to', + '', + '= Anchor Above Pseudocode', + '', + '[[my-anchor]]', + '[source,cmake,role=pseudocode]', + '----', + 'add_executable(x x.cpp)', + '----', + '', + '[NOTE]', + '[source,cpp,role=pseudocode]', + '----', + 'int y = 2;', + '----', + '', + '[quote,Someone]', + '[source,cpp,role=external]', + '----', + 'task async_work();', + '----', + '', + ].join('\n')); + + // ANCHOR fixture. Line numbers are asserted below, so keep them stable: + // 3 = the prose violation, 5-8 = the in-block negative, 10 = the + // passthrough negative. + write('attr-anchor.adoc', [ + '= Attr', // 1 + ':page-mode: explanation', // 2 + 'The wrapper is `[[nodiscard]]` here.', // 3 <- must fire + '', // 4 + '[source,cpp]', // 5 + '----', // 6 + 'struct `[[nodiscard]]` s;', // 7 <- must NOT fire + '----', // 8 + '', // 9 + 'Fixed as `+[[nodiscard]]+` instead.', // 10 <- must NOT fire + '', + ].join('\n')); + + const d = spawnSync('node', [path.join(SCRIPT_DIR, 'doc-lint.mjs'), DOCLINT_TMP], + { encoding: 'utf8', cwd: DOC_DIR, maxBuffer: 16 * 1024 * 1024 }); + check('doc-lint.mjs exits 0 against the B2 fixture tree', d.status === 0, + `exited ${d.status}: ${(d.stderr || '').trim().slice(-200)}`); + let dOut = null; + try { dOut = JSON.parse(d.stdout); } catch { /* reported below */ } + check('doc-lint.mjs prints parseable JSON', dOut !== null, `stdout: ${d.stdout.slice(0, 200)}`); + if (dOut) { + const flaggedHits = dOut.findings.B2.filter((f) => f.file === 'flagged.adoc'); + check('B2 catches an unmarked [source,cmake] block (directly, or via the bare-listing fallback)', + flaggedHits.length === 3, `flagged.adoc B2 findings: ${JSON.stringify(flaggedHits)}`); + check('B2 catches a bare `----` block holding real code with no role marker', + flaggedHits.some((f) => f.line === 10), + `expected a finding at flagged.adoc:10 (the bare block); got: ${JSON.stringify(flaggedHits)}`); + check('role=output does NOT clear a [source,cpp] block holding real code', + flaggedHits.some((f) => f.line === 14), + `expected a finding at flagged.adoc:14 ([source,cpp,role=output]); got: ${JSON.stringify(flaggedHits)}`); + const clearHits = dOut.findings.B2.filter((f) => f.file === 'clear.adoc'); + check('B2 leaves role=pseudocode/external/output/figure and include::example$ alone', + clearHits.length === 0, `clear.adoc should have 0 B2 findings, got: ${JSON.stringify(clearHits)}`); + + const shapeB2 = dOut.findings.B2.filter((f) => f.file === 'shape.adoc'); + check('SHAPE-worthy blocks stay OUT of B2 (role=output is a real exemption, just not a silent one)', + shapeB2.length === 0, `shape.adoc should have 0 B2 findings, got: ${JSON.stringify(shapeB2)}`); + const shapeHits = dOut.findings.SHAPE.filter((f) => f.file === 'shape.adoc'); + check('SHAPE flags a role=output block whose content looks like code', + shapeHits.some((f) => f.line === 6), + `expected a SHAPE finding at shape.adoc:6; got: ${JSON.stringify(shapeHits)}`); + check('SHAPE leaves a genuine role=output block alone', + !shapeHits.some((f) => f.line === 11), + `shape.adoc:11 is real output text, should not be SHAPE-flagged; got: ${JSON.stringify(shapeHits)}`); + + const delimHits = dOut.findings.B2.filter((f) => f.file === 'delimiters.adoc'); + check('B2 reaches a 5-dash (`-----`) listing, not just exactly `----`', + delimHits.some((f) => f.line === 5), + `expected a finding at delimiters.adoc:5 (the ----- block); got: ${JSON.stringify(delimHits)}`); + check('B2 reaches a `....` literal block, not just `----`', + delimHits.some((f) => f.line === 9), + `expected a finding at delimiters.adoc:9 (the .... block); got: ${JSON.stringify(delimHits)}`); + // The closer-length check is `===`, not `>=`: a role=figure block + // whose body contains a dash-only line LONGER than its own 4-dash + // opener must not be misread as closing early there (measured + // against 9b.Separation.adoc's real CCD diagram, which does exactly + // this). Exactly 2 findings total (the two above) — a third means + // the mismatched-length body line either got flagged directly or + // caused the real closer below it to be misread as a new opener. + check('B2 does not misfire on a body dash-run whose length differs from its own delimiter\'s', + delimHits.length === 2, `expected exactly 2 delimiters.adoc findings (5-dash, dot), got: ${JSON.stringify(delimHits)}`); + + // G1: a [source,cpp] block whose attribute list is split across two + // adjacent `[...]` lines must still be recognized as source, not fall + // through to the bare-listing/SHAPE branch. + const splitAttrB2 = dOut.findings.B2.filter((f) => f.file === 'split-attr.adoc'); + check('B2 catches a [source,cpp] block whose attribute list is split across two lines', + splitAttrB2.length === 1 && splitAttrB2[0].line === 5, + `expected exactly 1 split-attr.adoc B2 finding at line 5 (the [source,cpp] line), got: ${JSON.stringify(splitAttrB2)}`); + const splitAttrShape = dOut.findings.SHAPE.filter((f) => f.file === 'split-attr.adoc'); + check('split-attribute [source,cpp] block is caught by B2, not diverted into SHAPE', + splitAttrShape.length === 0, + `split-attr.adoc should have 0 SHAPE findings (B2 should catch it directly), got: ${JSON.stringify(splitAttrShape)}`); + + // G1 review-round-2: a block anchor, an admonition style, or a quote + // attribution sitting directly above a [source,...,role=pseudocode/ + // external] block (no blank line between) must NOT be merged into + // that block's attribute string -- only [source,...]/[role=...] + // continuation lines may merge. All three blocks here are otherwise + // properly exempt and must produce zero B2 findings. + const anchorHits = dOut.findings.B2.filter((f) => f.file === 'anchor-above-pseudocode.adoc'); + check('a [[anchor]]/[NOTE]/[quote,...] line above an exempt [source,...] block does not defeat its exemption', + anchorHits.length === 0, + `anchor-above-pseudocode.adoc should have 0 B2 findings, got: ${JSON.stringify(anchorHits)}`); + + // ANCHOR: a C++ attribute in a prose code span. Asciidoctor's + // inline-anchor substitution runs inside a backtick span, so + // `` `[[nodiscard]]` `` renders as an EMPTY -- the attribute + // name vanishes from the page while the source still reads correctly. + // That is why this needs a machine check: the defect is invisible to + // a human reading the .adoc. + // Both directions matter. A rule that only fired would be satisfied + // by a whole-file regex, which would then flag every legitimate + // `[[nodiscard]]` inside a code block -- so the in-block negative and + // the passthrough negative are asserted too, or the block-skip could + // be dropped in a refactor with the positive still passing. + const anchorRule = dOut.findings.ANCHOR.filter((f) => f.file === 'attr-anchor.adoc'); + check('ANCHOR catches an attribute written as `[[...]]` in prose (renders as an empty )', + anchorRule.some((f) => f.line === 3), + `expected an ANCHOR finding at attr-anchor.adoc:3; got: ${JSON.stringify(anchorRule)}`); + check('ANCHOR ignores `[[...]]` inside a delimited block (literal there, renders fine)', + !anchorRule.some((f) => f.line >= 5 && f.line <= 8), + `attr-anchor.adoc:5-8 is inside a block and must not fire; got: ${JSON.stringify(anchorRule)}`); + check('ANCHOR accepts the passthrough form `+[[...]]+` as the fix', + !anchorRule.some((f) => f.line === 10), + `attr-anchor.adoc:10 is the passthrough fix and must not fire; got: ${JSON.stringify(anchorRule)}`); + check('ANCHOR finds exactly the one planted prose violation', + anchorRule.length === 1, `expected exactly 1 ANCHOR finding, got: ${JSON.stringify(anchorRule)}`); + } + } finally { + fs.rmSync(DOCLINT_TMP, { recursive: true, force: true }); + } +} + +if (failures.length) { + console.error(`selftest: ${failures.length} assertion(s) FAILED`); + for (const f of failures) console.error(` - ${f}`); + process.exit(1); +} +console.log(JSON.stringify({ + ok: true, + assertions: assertionCount, + fixtureSummary: out.summary, +}, null, 2)); diff --git a/doc/lint/sentence-length.mjs b/doc/lint/sentence-length.mjs new file mode 100644 index 000000000..375a82dd3 --- /dev/null +++ b/doc/lint/sentence-length.mjs @@ -0,0 +1,411 @@ +#!/usr/bin/env node +// +// 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 +// +// sentence-length.mjs — the authority for Style Guide C2 ("no sentence over 25 +// words"), replacing Vale's `Capy.SentenceLength` on both documentation +// surfaces. Node built-ins only, no dependencies. JSON on stdout, in the same +// shape doc-lint.mjs uses, so baseline.mjs can fingerprint it. +// +// Why this exists rather than a Vale rule +// --------------------------------------- +// `Capy.SentenceLength` is `extends: occurrence`, `scope: sentence`, and it runs +// AFTER doc/.vale.ini's `TokenIgnores` blanks inline code spans. Two defects +// follow from that, both measured on this branch and neither fixable inside a +// Vale rule: +// +// 1. UNDER-COUNTING. A blanked span contributes ZERO words where a reader +// counts at least one, so the rule's 25-word budget is not the reader's. +// Two independent Vale-side measurements agree on the size of it: task +// P4-prereq got 140 -> 170 (+30) by rewriting `TokenIgnores` on the +// pre-BlockIgnores-fix config, and a re-measurement on today's corpus +// (every backtick span and `cpp:` macro outside code blocks replaced by one +// word) got 135 -> 164 (+29). +// 2. MIS-ATTRIBUTION, which is worse. The blanking corrupts Vale's position +// mapping for `scope: sentence` rules, so an alert can be reported against +// the wrong block — which makes a genuinely over-limit block look +// unreported, and it produces no alert of its own to chase. Iterating +// `extract + vale` to a fixpoint does NOT find it. Cleanest real case, +// hand-verified: `5.buffers/5b.types.adoc` holds two over-limit sentences in +// list items and Vale reports NONE. +// +// So this checker does its own segmentation and its own counting, and never +// asks Vale where anything is. `TokenIgnores` in .vale.ini is deliberately left +// alone: 557 `cpp:` macros depend on it for the rules that SHOULD ignore symbol +// text. The fix is to stop depending on Vale's position mapping for C2, not to +// remove the ignores. +// +// How a code span is counted +// -------------------------- +// As the one word a reader sees. A construct is masked with U+0001 runs of the +// SAME LENGTH as the original, leaving a single `x` behind, so that +// +// * the offsets of everything after it stay valid, which is what lets a +// finding quote the real source text and name the real line, and +// * the word counter sees it exactly once. +// +// `xref:`/link macros are masked around their bracketed text instead: a reader +// sees the link text, so the link text is what gets counted. +// +// Output +// ------ +// Two rule keys, because C2 is hard in API docs and soft in essays — see +// ADVISORY_DIRS below. `C2` is the hard slice and the one a gate binds; +// `advisory-C2` is the design essays, measured and reported but never blocking. +// A third key, `BACKTICK`, reports blocks whose inline code spans are unbalanced. +// +// Usage: node doc/lint/sentence-length.mjs [--max N] [corpusDir ...] +// Corpora default to `modules` (the Antora pages) and `lint/.docstrings` (the +// header docstrings, produced by extract-docstrings.mjs), both relative to +// doc/. A missing or empty corpus is a HARD ERROR (exit 1), never a clean +// zero: this branch has eight recorded instances of a check that looked +// healthy while checking less than it appeared to, and "the corpus silently +// scanned no files" is the cheapest way to add a ninth. +// +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 DEFAULT_CORPORA = ['modules', 'lint/.docstrings']; + +const argv = process.argv.slice(2); +let max = 25; +const corpora = []; +for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--max') max = Number(argv[++i]); + else if (argv[i].startsWith('--max=')) max = Number(argv[i].slice('--max='.length)); + else corpora.push(argv[i]); +} +if (!Number.isInteger(max) || max < 1) { + console.error(`--max expects a positive integer, got: ${max}`); + process.exit(2); +} +if (corpora.length === 0) corpora.push(...DEFAULT_CORPORA); + +function walk(dir) { + let out = []; + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, ent.name); + if (ent.isDirectory()) out = out.concat(walk(p)); + else if (ent.name.endsWith('.adoc')) out.push(p); + } + return out; +} + +// --- prose extraction ------------------------------------------------------ +// +// Delimited blocks whose content is NOT prose. Their content is skipped +// wholesale — handing code to a prose linter is the `BlockIgnores` mistake +// .vale.ini documents at length, and it is not repeated here. Note that +// `====` (example), `____` (quote) and `****` (sidebar) blocks DO hold prose +// and are only unit boundaries; `--` open blocks do not occur in this corpus. +const OPAQUE_OPEN = /^(-{4,}|\.{4,}|\+{4,}|\/{4,})$/; +const TABLE_DELIM = /^[|,!]===$/; + +// Lines that carry no prose of their own. Headings are included: a heading is +// not a sentence, and the longest in the corpus is nowhere near 25 words. +const SKIP_LINE = new RegExp([ + '^//', // line comment + '^:[^\\s:]+:', // document attribute + '^\\[.*\\]$', // block attribute, or [[anchor]] + '^(include|ifdef|ifndef|ifeval|endif|image|video|audio|toc)::', + '^=+\\s', // heading + '^(={4,}|_{4,}|\\*{4,}|\'{3,})$', // prose-block delimiters and thematic break + '^\\+$', // list continuation +].join('|')); + +// A leading list, callout or ordered-list marker. The item after it is its own +// block to asciidoctor, hence its own sentence scope. +const LIST_MARKER = /^(?:[*.]{1,5}|-|\d+\.|<\d+>|<\.>)\s+/; + +// Collect the prose blocks of one file as { line, text }, where `line` is the +// 1-based line the block starts on and `text` is its lines joined with a single +// space. `map` records where each source line begins inside `text`, so a +// sentence found mid-block can still be reported against the line it starts on. +function proseBlocks(text) { + const lines = text.split('\n'); + const out = []; + let cur = null; + let opaque = null; // RegExp that closes the opaque block we are inside + let inTable = false; + + const open = (lineNo, s) => { cur = { line: lineNo, text: s, map: [{ at: 0, line: lineNo }] }; }; + const append = (lineNo, s) => { + if (cur === null) return open(lineNo, s); + cur.text += ' '; + cur.map.push({ at: cur.text.length, line: lineNo }); + cur.text += s; + }; + const flush = () => { if (cur && cur.text.trim()) out.push(cur); cur = null; }; + + for (let i = 0; i < lines.length; i++) { + const lineNo = i + 1; + const t = lines[i].trim(); + if (opaque !== null) { if (opaque.test(t)) opaque = null; continue; } + if (t === '') { flush(); continue; } + if (TABLE_DELIM.test(t)) { flush(); inTable = !inTable; continue; } + if (OPAQUE_OPEN.test(t)) { + flush(); + // Close on the same delimiter character; asciidoctor wants matching + // lengths, but being lenient here cannot swallow the rest of the file. + opaque = new RegExp(`^\\${t[0]}{4,}$`); + continue; + } + if (SKIP_LINE.test(t)) { flush(); continue; } + if (inTable && t.startsWith('|')) { + // Every `|` on the row opens a cell, and a cell is its own prose block. + // The last cell stays open so a cell continued on the next line folds in. + flush(); + const cells = t.split('|').slice(1); + for (let k = 0; k < cells.length; k++) { + const cell = cells[k].trim(); + if (!cell) continue; + if (k === cells.length - 1) open(lineNo, cell); + else out.push({ line: lineNo, text: cell, map: [{ at: 0, line: lineNo }] }); + } + continue; + } + const marker = LIST_MARKER.exec(t); + if (marker) { flush(); open(lineNo, t.slice(marker[0].length)); continue; } + append(lineNo, t); + } + flush(); + return out; +} + +// --- masking --------------------------------------------------------------- +// +// Each rule replaces its match with a same-length string. `keep: false` leaves +// one `x` (the single word a reader sees); `keep: true` leaves capture group 1 +// (a link's visible text) and hides the rest. +// +// Order matters: the macros run first so that a macro's target — which may +// itself contain brackets, colons or backticks — is consumed as one unit rather +// than being carved up by a later rule, and an already-masked region cannot +// re-match because U+0001 appears in none of the patterns. Note that `keep: true` +// deliberately PRESERVES the link text, including any backticks in it, so those +// backticks stay visible to the backtick rule and to the unbalanced-backtick +// guard below. That is intended — link text is prose — and it is also how a +// backtick can pair across constructs, which is what the guard catches. +const SPANS = [ + { re: /\b(?:xref|link|kbd|btn|menu|footnote):[^\s[]*\[([^\]]*)\]/g, keep: true }, + { re: /\bhttps?:\/\/\S*?\[([^\]]*)\]/g, keep: true }, + { re: /\b(?:cpp|image|icon|pass):[^\s[]*\[[^\]]*\]/g, keep: false }, + { re: /``[^`]+``|`[^`\n]+`/g, keep: false, id: 'backtick' }, + { re: /\{[a-z][\w-]*\}/g, keep: false }, // attribute reference, e.g. {cpp} +]; + +const hide = (n) => ''.repeat(n); + +function mask(s, skip = null) { + let out = s; + for (const { re, keep, id } of SPANS) { + if (id !== undefined && id === skip) continue; + out = out.replace(re, (m, g1) => { + if (keep && g1) { + const at = m.indexOf(g1); + return hide(at) + g1 + hide(m.length - at - g1.length); + } + return `x${hide(m.length - 1)}`; + }); + } + return out; +} + +// A residual backtick in the masked text means the block held an UNBALANCED +// inline code span, and the mask has already done damage: the stray backtick +// pairs with an unrelated one and, because the mask preserves length, every word +// between them collapses into a single `x`. Measured on a fixture, one stray +// backtick turns a 30-word sentence into 6 — silent UNDER-reporting in a check +// meant to feed a merge-blocking gate, i.e. the exact failure shape this script +// exists to remove. So it is made visible instead: the block is re-masked with +// the backtick rule disabled, which counts the span text as full prose +// (over-reporting, the safe direction), and a `BACKTICK` finding names the file +// and line. The rule's own `\n` guard is not enough because proseBlocks() joins a +// block's lines with a space, so a stray backtick on one line can reach a +// backtick on another. `BACKTICK` findings fingerprint as +// `BACKTICK:file:#N:message`, which does NOT match a `^C2:` gate spec, so they +// are visible without being blocking. +function maskBlock(text) { + const masked = mask(text); + if (!masked.includes('`')) return { masked, unbalanced: false }; + return { masked: mask(text, 'backtick'), unbalanced: true }; +} + +// --- segmentation ---------------------------------------------------------- +// +// Terminal punctuation followed by whitespace or end of block, on the MASKED +// text — so a `.` inside a code span or a URL cannot open a sentence boundary. +// +// The inline-formatting marks are part of the boundary, not after it: AsciiDoc's +// bold run-in lead (`*The library owns the handles.* Capy creates ...`) and the +// same idiom in docstrings (`... `run_async(ex)(task)`.** The wrapper's ...`) +// put `*` or `_` between the period and the space. Requiring whitespace +// immediately after the period merged those leads into the following sentence and +// over-reported its length — two confirmed false positives. +// +// The two UNDER-reporting cases, both fixed here. Every other known miscount in +// this script over-reports, which is the safe direction for a length limit; these +// two let a real violation through, which a merge-blocking gate cannot afford. +// Both were pre-existing (the pre-round code splits identically), and both were +// found by adversarial fixtures rather than by the corpus. +// +// * A mid-sentence ELLIPSIS. `...` ends with a period followed by a space, so a +// 34-word sentence containing one was segmented 16 + 18 and missed at 25. The +// `(? (s.match(WORD) || []).length; + +// --- hard versus advisory slice (maintainer ruling) ------------------------- +// +// DOC_STYLE_GUIDE.md Part C2 makes the limit "hard in API docs, soft in essays". +// The flat 25 stays — a 20-word instruction limit was rejected as unimplementable, +// since nothing classifies instruction-versus-descriptive prose reliably — but the +// OUTPUT is split so a gate can bind only the hard part: +// +// hard the extracted `include/**` docstrings, plus every .adoc page NOT in +// the two essay directories below. This is the number that must reach +// zero, and the slice a `--gate 'sentence_length:^C2:'` spec binds. +// advisory doc/modules/ROOT/pages/9.design/ and .../A.specification-methods/. +// Measured and reported, never blocking. +// +// The advisory rule key deliberately does NOT begin with `C2`, so that even a +// mis-written head-anchored spec (`^C2` without the colon) cannot reach the +// essays. Keep it that way. +const ADVISORY_DIRS = [ + 'modules/ROOT/pages/9.design/', + 'modules/ROOT/pages/A.specification-methods/', +]; +// Matched as a whole path SEGMENT sequence, with the trailing slash, so a +// look-alike directory (`9.designish/`) stays in the hard slice. The `/`-prefixed +// form is also tested because an ad-hoc invocation with an absolute corpus path +// outside doc/ makes `rel` a `../`-walk-up, which no prefix test would match — +// that silently put every essay finding in the HARD slice. baseline.mjs always +// passes the relative defaults, so this only ever affected manual runs. +const ruleFor = (rel) => (ADVISORY_DIRS.some((d) => rel.startsWith(d) || rel.includes(`/${d}`)) + ? 'advisory-C2' : 'C2'); + +// --- run ------------------------------------------------------------------- + +const byRule = { C2: [], 'advisory-C2': [], BACKTICK: [] }; +const scanned = {}; +for (const corpus of corpora) { + const root = path.resolve(DOC_DIR, corpus); + if (!fs.existsSync(root)) { + console.error(`sentence-length.mjs: corpus '${corpus}' does not exist at ${root}. ` + + "For 'lint/.docstrings', run `node lint/extract-docstrings.mjs` first. " + + 'Refusing to report zero findings for a corpus that was never read.'); + process.exit(1); + } + const files = walk(root); + if (files.length === 0) { + console.error(`sentence-length.mjs: corpus '${corpus}' contains no .adoc files. ` + + 'Refusing to report zero findings for an empty corpus.'); + process.exit(1); + } + scanned[corpus] = files.length; + for (const file of files) { + // Keyed on the path relative to DOC_DIR, never a basename: `concept/read_stream.hpp` + // and `test/read_stream.hpp` are different files, and two verification scripts on + // this branch silently conflated exactly that pair. + const rel = path.relative(DOC_DIR, file).split(path.sep).join('/'); + const rule = ruleFor(rel); + for (const block of proseBlocks(fs.readFileSync(file, 'utf8'))) { + const { masked, unbalanced } = maskBlock(block.text); + const lineOf = (at) => { + let line = block.line; + for (const e of block.map) if (e.at <= at) line = e.line; + return line; + }; + if (unbalanced) { + byRule.BACKTICK.push({ + file: rel, + line: block.line, + message: 'unbalanced backtick in block; inline code spans in it are counted as prose', + sentence: block.text.slice(0, 200).trim(), + }); + } + for (const [from, to] of sentenceRanges(masked)) { + const words = countWords(masked.slice(from, to)); + if (words <= max) continue; + byRule[rule].push({ + file: rel, + line: lineOf(from), + words, + // The message is deliberately fixed text: baseline.mjs fingerprints a + // doc_lint-shaped finding as `rule:file:#N:message`, and folding the + // word count or the sentence into it would re-mint the fingerprint on + // every reword — a gate that fails because a contributor rephrased an + // already-over-limit sentence teaches contributors to distrust it. + message: `sentence over ${max} words`, + sentence: block.text.slice(from, to).trim(), + }); + } + } + } +} + +console.log(JSON.stringify({ + summary: { + hard: byRule.C2.length, + advisory: byRule['advisory-C2'].length, + unbalancedBackticks: byRule.BACKTICK.length, + max, + scanned, + advisoryDirs: ADVISORY_DIRS, + }, + findings: byRule, +}, null, 2)); +process.exit(0); diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 8c4350781..5182cdaab 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -2,18 +2,18 @@ * xref:why-capy.adoc[Why Capy?] * xref:quick-start.adoc[Quick Start] * xref:2.cpp20-coroutines/2.intro.adoc[Introduction To {cpp}20 Coroutines] -** xref:2.cpp20-coroutines/2a.foundations.adoc[Part I: Foundations] -** xref:2.cpp20-coroutines/2b.syntax.adoc[Part II: {cpp}20 Syntax] -** xref:2.cpp20-coroutines/2c.machinery.adoc[Part III: Coroutine Machinery] -** xref:2.cpp20-coroutines/2d.advanced.adoc[Part IV: Advanced Topics] +** xref:2.cpp20-coroutines/2a.foundations.adoc[Coroutine Foundations] +** xref:2.cpp20-coroutines/2b.syntax.adoc[{cpp}20 Syntax] +** xref:2.cpp20-coroutines/2c.machinery.adoc[Coroutine Machinery] +** xref:2.cpp20-coroutines/2d.advanced.adoc[Advanced Topics] * xref:3.concurrency/3.intro.adoc[Introduction to Concurrency] -** xref:3.concurrency/3a.foundations.adoc[Part I: Foundations] -** xref:3.concurrency/3b.synchronization.adoc[Part II: Synchronization] -** xref:3.concurrency/3c.advanced.adoc[Part III: Advanced Primitives] -** xref:3.concurrency/3d.patterns.adoc[Part IV: Communication & Patterns] +** xref:3.concurrency/3a.foundations.adoc[Threads] +** xref:3.concurrency/3b.synchronization.adoc[Mutexes & Deadlock] +** xref:3.concurrency/3c.advanced.adoc[Atomics, Condition Variables & Shared Locks] +** xref:3.concurrency/3d.patterns.adoc[Futures, async & Patterns] * xref:4.coroutines/4.intro.adoc[Coroutines in Capy] ** xref:4.coroutines/4a.tasks.adoc[The task Type] -** xref:4.coroutines/4b.launching.adoc[Launching Coroutines] +** xref:4.coroutines/4b.launching.adoc[Starting Coroutines] ** xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] ** xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] ** xref:4.coroutines/4e.cancellation.adoc[Stop Tokens and Cancellation] @@ -21,11 +21,6 @@ ** xref:4.coroutines/4g.allocators.adoc[Frame Allocators] ** xref:4.coroutines/4h.lambda-captures.adoc[Lambda Coroutine Captures] * xref:5.buffers/5.intro.adoc[Buffer Sequences] -** xref:5.buffers/5a.overview.adoc[Why Concepts, Not Spans] -** xref:5.buffers/5b.types.adoc[Buffer Types] -** xref:5.buffers/5c.sequences.adoc[Buffer Sequences] -** xref:5.buffers/5d.system-io.adoc[System I/O Integration] -** xref:5.buffers/5e.algorithms.adoc[Buffer Algorithms] * xref:6.streams/6.intro.adoc[Stream Concepts] ** xref:6.streams/6a.overview.adoc[Overview] ** xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] @@ -42,13 +37,13 @@ ** xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] ** xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] ** xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] -** xref:8.examples/8i.echo-server-corosio.adoc[Echo Server with Corosio] ** xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] ** xref:8.examples/8l.async-mutex.adoc[Async Mutex] ** xref:8.examples/8m.parallel-tasks.adoc[Parallel Tasks] ** xref:8.examples/8n.custom-executor.adoc[Custom Executor] ** xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] ** xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] +** xref:8.examples/8q.gui-integration.adoc[GUI Integration] * xref:9.design/9.intro.adoc[Design] ** xref:9.design/9a.CapyLayering.adoc[Layered Abstractions] ** xref:9.design/9b.Separation.adoc[Why Capy Is Separate] diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc index 771ca65f5..60d32adcd 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2.intro.adoc @@ -8,11 +8,21 @@ // = Introduction To {cpp}20 Coroutines +:page-mode: explanation -Every {cpp} function you have ever written follows the same contract: it runs from start to finish, then returns. The caller waits. The stack frame lives and dies in lockstep with that single invocation. This model has served us well for decades, but it forces a hard tradeoff when programs need to wait--for a network response, a disk read, a timer, or another thread. The function either blocks (wasting a thread) or you restructure your code into callbacks, state machines, or futures that scatter your logic across multiple places. +Every {cpp} function you have ever written follows the same contract: it runs from start to finish, then returns. The caller waits. The stack frame lives and dies in lockstep with that single invocation. This model has served us well for decades. But it forces a hard tradeoff when programs need to wait--for a network response, a disk read, a timer, or another thread. The function either blocks (wasting a thread) or you restructure your code into callbacks, state machines, or futures that scatter your logic across multiple places. {cpp}20 coroutines change the rules. A coroutine can _suspend_ its execution--saving its local state somewhere outside the stack--and _resume_ later, picking up exactly where it left off. The control flow reads top-to-bottom, like the synchronous code you already know, but the runtime behavior is asynchronous. No blocked threads. No callback chains. No lost context. This is not a minor syntactic convenience. It is a fundamental shift in how you can structure programs that wait. -This section takes you from zero to a working understanding of {cpp}20 coroutines. No prior experience with coroutines or async programming is needed. You will start with the problem that coroutines solve, move through the language syntax and compiler machinery, and finish with the performance characteristics that make coroutines practical for real systems. By the end, you will understand not only _how_ to write coroutines but _why_ they work the way they do--knowledge that will make everything in the rest of this documentation click into place. +== What This Section Covers + +* xref:2.cpp20-coroutines/2a.foundations.adoc[Coroutine Foundations] -- How normal function + calls work, and what a coroutine's suspend/resume model changes. +* xref:2.cpp20-coroutines/2b.syntax.adoc[{cpp}20 Syntax] -- The `co_await`, + `co_yield`, and `co_return` keywords, and how the compiler builds awaitables. +* xref:2.cpp20-coroutines/2c.machinery.adoc[Coroutine Machinery] -- Promise types + and coroutine handles, the machinery behind suspension and resumption. +* xref:2.cpp20-coroutines/2d.advanced.adoc[Advanced Topics] -- Symmetric transfer, + custom allocation, HALO, and exception handling across suspension points. diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc index 91e39b901..055579110 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc @@ -1,20 +1,7 @@ -= Part I: Foundations += Coroutine Foundations +:page-mode: tutorial -This section introduces the fundamental concepts you need before working with {cpp}20 coroutines. You will learn how normal functions work, what makes coroutines different, and why coroutines exist as a language feature. - -== Prerequisites - -Before beginning this tutorial, you should have: - -* A {cpp} compiler with {cpp}20 support (GCC 10+, Clang 14+, or MSVC 2019 16.8+) -* Familiarity with basic {cpp} concepts: functions, classes, templates, and lambdas -* Understanding of how function calls work: the call stack, local variables, and return values - -The examples in this tutorial use standard {cpp}20 features. Compile with: - -* GCC: `g++ -std=c++20 -fcoroutines your_file.cpp` -* Clang: `clang++ -std=c++20 your_file.cpp` -* MSVC: `cl /std:c++20 your_file.cpp` +This section introduces the fundamental concepts you need before working with {cpp}20 coroutines. It explains how normal functions work, what makes coroutines different, and why coroutines exist as a language feature. == Functions and the Call Stack @@ -38,7 +25,7 @@ This model has a fundamental constraint: *run-to-completion*. Once a function st == What Is a Coroutine? -A *coroutine* is a function that can suspend its execution and resume later from exactly where it left off. Think of it as a bookmark in a book of instructions—instead of reading the entire book in one sitting, you can mark your place, do something else, and return to continue reading. +A *coroutine* is a function that can suspend its execution and resume later from exactly where it left off. Think of it as a bookmark in a book of instructions. Instead of reading the entire book in one sitting, you can mark your place, do something else, and return to continue reading. When a coroutine suspends: @@ -51,11 +38,11 @@ When a coroutine resumes: * Local variables are restored to their previous values * Execution continues from the suspension point -This capability is implemented through a *coroutine frame*—a heap-allocated block of memory that stores the coroutine's state. Unlike stack frames, coroutine frames persist across suspension points because they live on the heap rather than the stack. +This capability is implemented through a *coroutine frame*—a block of memory, typically heap-allocated, that stores the coroutine's state. Unlike stack frames, coroutine frames persist across suspension points. xref:2.cpp20-coroutines/2d.advanced.adoc[Advanced Topics] covers when the compiler can place one elsewhere instead. +.Conceptual illustration - not real syntax [source,cpp,role=pseudocode] ---- -// Conceptual illustration (not real syntax) task fetch_and_process() { auto data = co_await fetch_from_network(); // suspends here @@ -92,7 +79,7 @@ include::example$snippets/2a_foundations.cpp[tag=callback_request,indent=0] This code does not block. Each operation starts, registers a callback, and returns immediately. When the operation completes, the callback runs. -But look what has happened to the code: three levels of nesting, logic scattered across multiple lambda functions, and local variables that cannot be shared between callbacks without careful lifetime management. A single logical operation becomes fragmented across multiple functions. +But look what has happened to the code: three levels of nesting and logic scattered across multiple lambda functions. Local variables cannot be shared between callbacks without careful lifetime management. A single logical operation becomes fragmented across multiple functions. === The Coroutine Solution @@ -112,5 +99,3 @@ Coroutines also enable: * *Generators* — Functions that produce sequences of values on demand, computing each value only when requested * *State machines* — Complex control flow expressed as linear code with suspension points * *Cooperative multitasking* — Multiple logical tasks interleaved on a single thread - -You have now learned what coroutines are and why they exist. In the next section, you will learn the {cpp}20 syntax for creating coroutines. diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc index d74508623..08e0da772 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc @@ -1,12 +1,8 @@ -= Part II: {cpp}20 Syntax += {cpp}20 Syntax +:page-mode: tutorial This section introduces the three {cpp}20 keywords that create coroutines and walks you through building your first coroutine step by step. -== Prerequisites - -* Completed xref:2.cpp20-coroutines/2a.foundations.adoc[Part I: Foundations] -* Understanding of why coroutines exist and what problem they solve - == The Three Keywords A function becomes a coroutine when its body contains any of three special keywords: `co_await`, `co_yield`, or `co_return`. The presence of any of these keywords signals to the compiler that the function requires coroutine machinery. @@ -45,7 +41,7 @@ For coroutines that do not return a value, use `co_return;` without an argument. The distinction between regular functions and coroutines matters because they behave fundamentally differently at runtime: * A regular function allocates its local variables on the stack. When it returns, those variables are gone. -* A coroutine allocates its local variables in a heap-allocated *coroutine frame*. When it suspends, those variables persist. When it resumes, they are still there. +* A coroutine allocates its local variables in a *coroutine frame*, typically on the heap. When it suspends, those variables persist. When it resumes, they are still there. Here is the minimal structure needed to create a coroutine: @@ -54,9 +50,9 @@ Here is the minimal structure needed to create a coroutine: include::example$snippets/2b_syntax.cpp[tag=simple_coroutine,indent=0] ---- -The `promise_type` nested structure provides the minimum scaffolding the compiler needs. You will learn what each method does in xref:2.cpp20-coroutines/2c.machinery.adoc[Part III: Coroutine Machinery]. +The `promise_type` nested structure provides the minimum scaffolding the compiler needs. xref:2.cpp20-coroutines/2c.machinery.adoc[Coroutine Machinery] explains what each method does. -For now, observe that the presence of `co_return` transforms what looks like a regular function into a coroutine. If you try to compile a function with coroutine keywords but without proper infrastructure, the compiler will produce errors. +For now, observe that the presence of `co_return` transforms what looks like a regular function into a coroutine. If you try to compile a function with coroutine keywords but without proper infrastructure, the compiler produces errors. == Awaitables and Awaiters @@ -75,6 +71,7 @@ include::example$programs/2b_syntax_counter.cpp[tag=full] *Output:* +[role=output] ---- counter: 0 main: resuming @@ -111,5 +108,3 @@ These are useful building blocks for promise types and custom awaitables. ---- include::example$snippets/2b_syntax.cpp[tag=standard_awaiters,indent=0] ---- - -You have now learned the three coroutine keywords and how awaitables work. In the next section, you will learn about the promise type and coroutine handle—the machinery that makes coroutines function. diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc index 4f23927da..f460b460e 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc @@ -1,16 +1,11 @@ -= Part III: Coroutine Machinery += Coroutine Machinery +:page-mode: tutorial -This section explains the promise type and coroutine handle—the core machinery that controls coroutine behavior. You will build a complete generator type by understanding how these pieces work together. - -== Prerequisites - -* Completed xref:2.cpp20-coroutines/2b.syntax.adoc[Part II: {cpp}20 Syntax] -* Understanding of the three coroutine keywords -* Familiarity with awaitables and awaiters +This section explains the promise type and coroutine handle—the core machinery that controls coroutine behavior. You build a complete generator type by understanding how these pieces work together. == The Promise Type -Every coroutine has an associated *promise type*. This type acts as a controller for the coroutine, defining how it behaves at key points in its lifecycle. The promise type is not something you pass to the coroutine—it is a nested type inside the coroutine's return type that the compiler uses automatically. +Every coroutine has an associated *promise type*. This type acts as a controller for the coroutine, defining how it behaves at key points in its lifecycle. The promise type is not something you pass to the coroutine. It is a nested type inside the coroutine's return type that the compiler uses automatically. The compiler expects to find a type named `promise_type` nested inside your coroutine's return type. If your coroutine returns `Generator`, the compiler looks for `Generator::promise_type`. @@ -19,7 +14,7 @@ The compiler expects to find a type named `promise_type` nested inside your coro The promise type must provide these methods: `get_return_object()`:: -Called to create the object that will be returned to the caller of the coroutine. This happens before the coroutine body begins executing. +Called to create the object returned to the caller of the coroutine. This happens before the coroutine body begins executing. `initial_suspend()`:: Called immediately after `get_return_object()`. Returns an awaiter that determines whether the coroutine should suspend before running any of its body. Return `std::suspend_never{}` to start executing immediately, or `std::suspend_always{}` to suspend before the first statement. @@ -39,6 +34,7 @@ The compiler transforms your coroutine body into something resembling this pseud NOTE: The `co_await` keywords below are intentional. This mirrors the {cpp} standard's own description ({cpp}20 [dcl.fct.def.coroutine]/5), which uses `co_await` to express the logical suspension points. The compiler expands each `co_await` into the full awaiter protocol (`await_ready`, `await_suspend`, `await_resume`) as described in xref:2.cpp20-coroutines/2b.syntax.adoc#_awaitables_and_awaiters[Awaitables and Awaiters]. +.What the compiler generates [source,cpp,role=pseudocode] ---- { @@ -64,7 +60,7 @@ NOTE: The `co_await` keywords below are intentional. This mirrors the {cpp} stan Important observations: * The return object is created before `initial_suspend()` runs, so it is available even if the coroutine suspends immediately -* `final_suspend()` determines whether the coroutine frame persists after completion—if it returns `suspend_always`, you must manually destroy the coroutine; if it returns `suspend_never`, the frame is destroyed automatically +* `final_suspend()` determines whether the coroutine frame persists after completion. If it returns `suspend_always`, you must manually destroy the coroutine; if it returns `suspend_never`, the frame is destroyed automatically === Tracing Promise Behavior @@ -75,6 +71,7 @@ include::example$programs/2c_machinery_trace.cpp[tag=full] *Output:* +[role=output] ---- calling coroutine promise constructed @@ -92,7 +89,7 @@ Notice that the promise is constructed first, then `get_return_object()` creates [WARNING] ==== -If your coroutine can fall off the end of its body without executing `co_return`, and your promise type lacks a `return_void()` method, the behavior is undefined. Always ensure your promise type has `return_void()` if there is any code path that might reach the end of the coroutine body without an explicit `co_return`. +A coroutine may fall off the end of its body without executing `co_return`. If its promise type lacks a `return_void()` method, the behavior is undefined. Your promise type must therefore have `return_void()` whenever any code path might reach the end of the coroutine body without an explicit `co_return`. ==== == Coroutine Handle @@ -133,6 +130,7 @@ A *generator* is a function that produces a sequence of values on demand. Instea The expression `co_yield value` is transformed by the compiler into: +.What the compiler generates [source,cpp,role=pseudocode] ---- co_await promise.yield_value(value) @@ -149,6 +147,7 @@ include::example$programs/2c_machinery_generator.cpp[tag=full] *Output:* +[role=output] ---- 1 2 @@ -182,12 +181,11 @@ include::example$programs/2c_machinery_fibonacci.cpp[tag=full] *Output:* +[role=output] ---- 0 1 1 2 3 5 8 13 21 34 ---- -The Fibonacci generator runs an infinite loop internally. It will produce values forever. But because it yields and suspends after each value, the caller controls when (and whether) to ask for more values. The generator only computes values on demand. - -The variables `a` and `b` persist across yields because they live in the coroutine frame on the heap. +The Fibonacci generator runs an infinite loop internally. It produces values forever. But because it yields and suspends after each value, the caller controls when (and whether) to ask for more values. The generator only computes values on demand. -You have now learned how promise types and coroutine handles work together to create useful abstractions like generators. In the next section, you will explore advanced topics: symmetric transfer, allocation, and exception handling. +The variables `a` and `b` persist across yields because they live in the coroutine frame, typically on the heap. diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc index 355ad9e1e..e7ab2a172 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc @@ -1,15 +1,11 @@ -= Part IV: Advanced Topics += Advanced Topics +:page-mode: tutorial This section covers advanced coroutine topics: symmetric transfer for efficient resumption, coroutine allocation strategies, and exception handling. These concepts are essential for building production-quality coroutine types. -== Prerequisites - -* Completed xref:2.cpp20-coroutines/2c.machinery.adoc[Part III: Coroutine Machinery] -* Understanding of promise types, coroutine handles, and generators - == Symmetric Transfer -When a coroutine completes or awaits another coroutine, control must transfer somewhere. The naive approach—simply calling `handle.resume()`—has a problem: each nested coroutine adds a frame to the call stack. With deep nesting, you risk stack overflow. +When a coroutine completes or awaits another coroutine, control must transfer somewhere. The naive approach—calling `handle.resume()`—has a problem: each nested coroutine adds a frame to the call stack. With deep nesting, you risk stack overflow. *Symmetric transfer* solves this by returning a coroutine handle from `await_suspend`. Instead of resuming the target coroutine via a function call, the compiler generates a tail call that transfers control without growing the stack. @@ -32,6 +28,21 @@ Without symmetric transfer, when `a` awaits `b`: Each suspension adds a stack frame. With thousands of nested coroutines, the stack overflows. +=== Return Types for await_suspend + +The fix comes from `await_suspend`'s return type, so start there. It can return three types, and each means something different: + +`void`:: +Always suspend. The coroutine is suspended and some external mechanism must resume it. + +`bool`:: +Conditional suspension. Return `true` to suspend, `false` to continue without suspending. + +`std::coroutine_handle<>`:: +Symmetric transfer. The returned handle is resumed; returning `std::noop_coroutine()` suspends without resuming anything. + +The third one is what solves the stack problem. + === The Solution: Return the Handle `await_suspend` can return a `std::coroutine_handle<>`: @@ -43,6 +54,7 @@ include::example$snippets/2d_advanced.cpp[tag=symmetric_await_suspend,indent=0] When `await_suspend` returns a handle, the compiler generates code equivalent to: +.What the compiler generates [source,cpp,role=pseudocode] ---- auto next = awaiter.await_suspend(current); @@ -52,19 +64,6 @@ if (next != std::noop_coroutine()) The key insight: returning a handle enables the compiler to implement the resumption as a tail call. The current stack frame is reused for the next coroutine. -=== Return Types for await_suspend - -`await_suspend` can return three types: - -`void`:: -Always suspend. The coroutine is suspended and some external mechanism must resume it. - -`bool`:: -Conditional suspension. Return `true` to suspend, `false` to continue without suspending. - -`std::coroutine_handle<>`:: -Symmetric transfer. The returned handle is resumed; returning `std::noop_coroutine()` suspends without resuming anything. - === Using Symmetric Transfer in Generators A production generator uses symmetric transfer at `final_suspend` to return to whoever is iterating: @@ -76,7 +75,7 @@ include::example$snippets/2d_advanced.cpp[tag=generator_final_suspend,indent=0] == Coroutine Allocation -Every coroutine needs memory for its *coroutine frame*—the heap-allocated structure holding local variables, parameters, and suspension state. +Every coroutine needs memory for its *coroutine frame*—the structure, typically heap-allocated, holding local variables, parameters, and suspension state. === Default Allocation @@ -99,7 +98,7 @@ Compilers can sometimes eliminate coroutine frame allocation entirely through *H HALO is most effective when: * Coroutines are awaited immediately after creation -* The coroutine type is marked with `[[clang::coro_await_elidable]]` (Clang extension) +* The coroutine type is marked with `+[[clang::coro_await_elidable]]+` (Clang extension) * Optimization is enabled (`-O2` or higher) [source,cpp] @@ -179,6 +178,7 @@ include::example$programs/2d_advanced_exception.cpp[tag=full] *Output:* +[role=output] ---- Starting risky operation Operation failed: Something went wrong @@ -205,15 +205,4 @@ This generator: * Manages coroutine lifetime with RAII * Supports move semantics -== Conclusion - -You have now learned the complete mechanics of {cpp}20 coroutines: - -* *Keywords* — `co_await`, `co_yield`, and `co_return` transform functions into coroutines -* *Promise types* — Control coroutine behavior at initialization, suspension, completion, and error handling -* *Coroutine handles* — Lightweight references for resuming, querying, and destroying coroutines -* *Symmetric transfer* — Efficient control flow without stack accumulation -* *Allocation* — Custom allocation and HALO optimization -* *Exception handling* — Capturing and propagating exceptions across suspension points - -These fundamentals prepare you for understanding Capy's `task` type and the IoAwaitable protocol, which build on standard coroutine machinery with executor affinity and stop token propagation. +These fundamentals prepare you for understanding Capy's cpp:task[task] type and the IoAwaitable protocol. Both build on standard coroutine machinery with executor affinity and stop token propagation. diff --git a/doc/modules/ROOT/pages/3.concurrency/3.intro.adoc b/doc/modules/ROOT/pages/3.concurrency/3.intro.adoc index 3663951cc..91080ab7c 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3.intro.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3.intro.adoc @@ -8,11 +8,24 @@ // = Introduction to Concurrency +:page-mode: explanation Your processor has multiple cores. Your operating system runs hundreds of threads. Your users expect responsive interfaces while your server handles thousands of simultaneous connections. Concurrency is not an advanced topic reserved for specialists--it is the reality of modern software. -Yet concurrent programming has a reputation for being treacherous, and that reputation is earned. Two threads reading and writing the same variable can produce results that are impossible to reproduce, impossible to debug, and impossible to reason about by staring at the code. A program that passes every test can still corrupt data in production under load. The bugs are real, and they are subtle. +Yet concurrent programming has a reputation for being treacherous, and that reputation is earned. Two threads reading and writing the same variable can produce results that are impossible to reproduce and impossible to debug. Such results are also impossible to reason about by staring at the code. A program that passes every test can still corrupt data in production under load. The bugs are real, and they are subtle. -The good news: these problems are well understood. Decades of research and practice have produced clear patterns, precise vocabulary, and reliable tools. Once you understand the fundamentals--what a data race actually is, why memory ordering matters, how synchronization primitives work--concurrent code becomes something you can reason about with confidence. +The good news: these problems are well understood. Decades of research and practice have produced clear patterns, precise vocabulary, and reliable tools. Once you understand the fundamentals, concurrent code becomes something you can reason about with confidence. Those fundamentals are what a data race actually is, why memory ordering matters, and how synchronization primitives work. -This section builds your understanding of concurrency from first principles. No prior experience with threads or parallel programming is needed. You will learn what makes concurrent code hard to reason about, how the standard synchronization tools work, and the architectural patterns that tame that complexity. When you finish, you will have the vocabulary and mental models to understand how Capy's coroutine-based concurrency works under the hood--and why it eliminates entire categories of the bugs described here. +== What This Section Covers + +* xref:3.concurrency/3a.foundations.adoc[Concurrency Foundations] -- Threads, their lifecycle, + and how to create, join, and detach them. +* xref:3.concurrency/3b.synchronization.adoc[Synchronization] -- Race conditions, + mutexes, lock guards, and how to avoid deadlock. +* xref:3.concurrency/3c.advanced.adoc[Advanced Primitives] -- Atomics, condition + variables, and shared locks for finer-grained synchronization. +* xref:3.concurrency/3d.patterns.adoc[Communication & Patterns] -- Futures, + promises, `std::async`, and practical patterns for concurrent code. + +When you finish, you have the vocabulary and mental models to understand how Capy's +coroutine-based concurrency works under the hood. diff --git a/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc b/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc index 0c80f3f49..529bf2872 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc @@ -1,14 +1,7 @@ -= Part I: Foundations += Threads +:page-mode: tutorial -This section introduces the fundamental concepts of concurrent programming. You will learn what concurrency is, why it matters, and how threads provide the foundation for parallel execution. - -== Prerequisites - -Before beginning this tutorial, you should have: - -* A {cpp} compiler with {cpp}11 or later support -* Familiarity with basic {cpp} concepts: functions, classes, and lambdas -* Understanding of how programs execute sequentially +This section introduces the fundamental concepts of concurrent programming. It explains what concurrency is, why it matters, and how threads provide the foundation for parallel execution. == Why Concurrency Matters @@ -18,7 +11,7 @@ Consider downloading a large file. Without concurrency, your application freezes The benefits compound in computationally intensive work. Image processing, scientific simulations, video encoding—these tasks can be split into independent pieces. Process them simultaneously and your program finishes in a fraction of the time. -But concurrency is not free. It introduces complexity. Multiple threads accessing the same data can corrupt it. Threads waiting on each other can freeze forever. These problems—*race conditions* and *deadlocks*—are the challenges you will learn to handle. +But concurrency is not free. It introduces complexity. Multiple threads accessing the same data can corrupt it. Threads waiting on each other can freeze forever. These problems—*race conditions* and *deadlocks*—are the challenges you learn to handle. == Threads—Your Program's Parallel Lives @@ -52,6 +45,7 @@ include::example$programs/3a_foundations_parallel.cpp[tag=full] Run this and you might see output like: +[role=output] ---- Alice: 1 Bob: 1 @@ -63,6 +57,7 @@ Alice: 3 Or perhaps: +[role=output] ---- AliceBob: : 1 1 @@ -85,7 +80,7 @@ Lambda expressions are often the clearest choice: include::example$programs/3a_foundations_lambda.cpp[tag=full] ---- -The lambda captures `x` by value—it copies `x` into the lambda. By default, `std::thread` copies all arguments passed to it. Even if your function declares a reference parameter, the thread receives a copy. +The lambda captures `x` by value—it copies `x` into the lambda. By default, `std::thread` also decays and copies every argument passed to its constructor, before invoking the callable. To pass by reference, use `std::ref()`: @@ -94,7 +89,7 @@ To pass by reference, use `std::ref()`: include::example$programs/3a_foundations_ref.cpp[tag=full] ---- -Without `std::ref()`, the thread would modify a copy, leaving `counter` unchanged. +Without `std::ref()`, this fails to compile. `std::thread` decays arguments to values, and a decayed `int` cannot bind to `increment`'s `int&` parameter. === Member Functions @@ -141,5 +136,3 @@ include::example$snippets/3a_foundations.cpp[tag=joinable,indent=0] ---- A thread is joinable if it represents an actual thread of execution. After joining or detaching, or after default construction, a `std::thread` is not joinable. - -You have now learned the basics of threads: creation, execution, and lifecycle management. In the next section, you will learn about the dangers of shared data and how to protect it with synchronization primitives. diff --git a/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc b/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc index 3ea64e651..6456789f0 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc @@ -1,11 +1,7 @@ -= Part II: Synchronization += Mutexes & Deadlock +:page-mode: tutorial -This section introduces the dangers of shared data access and the synchronization primitives that protect against them. You will learn about race conditions, mutexes, lock guards, and deadlocks. - -== Prerequisites - -* Completed xref:3.concurrency/3a.foundations.adoc[Part I: Foundations] -* Understanding of threads and their lifecycle +This section introduces the dangers of shared data access and the synchronization primitives that protect against them. It covers race conditions, mutexes, lock guards, and deadlocks. == The Danger: Race Conditions @@ -18,7 +14,7 @@ Consider this code: include::example$programs/3b_synchronization_race.cpp[tag=full] ---- -Two threads, each incrementing 100,000 times. You would expect 200,000. But run this repeatedly and you will see different results—180,000, 195,327, maybe occasionally 200,000. Something is wrong. +Two threads, each incrementing 100,000 times. You would expect 200,000. But run this repeatedly and you see different results—180,000, 195,327, maybe occasionally 200,000. Something is wrong. The `++counter` operation looks atomic—indivisible—but it is not. It actually consists of three steps: @@ -110,5 +106,3 @@ include::example$snippets/3b_synchronization.cpp[tag=scoped_lock_multi] 2. *Use std::scoped_lock for multiple mutexes* — Let the library handle deadlock avoidance 3. *Hold locks for minimal time* — Reduce the window for contention 4. *Avoid nested locks when possible* — Simpler designs prevent deadlock by construction - -You have now learned about race conditions, mutexes, lock guards, and deadlocks. In the next section, you will explore advanced synchronization primitives: atomics, condition variables, and shared locks. diff --git a/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc b/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc index a7dca8ebf..642dc8b97 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc @@ -1,12 +1,8 @@ -= Part III: Advanced Primitives += Atomics, Condition Variables & Shared Locks +:page-mode: tutorial This section covers advanced synchronization primitives: atomics for lock-free operations, condition variables for efficient waiting, and shared locks for reader/writer patterns. -== Prerequisites - -* Completed xref:3.concurrency/3b.synchronization.adoc[Part II: Synchronization] -* Understanding of mutexes, lock guards, and deadlocks - == Atomics: Lock-Free Operations For operations on individual values, mutexes might be overkill. *Atomic types* provide lock-free thread safety for single variables. @@ -62,7 +58,13 @@ The worker thread calls `cv.wait()`, which atomically releases the mutex and sus === The Predicate -The lambda `[]{ return ready; }` is the *predicate*. `wait()` will not return until this evaluates to true. This guards against *spurious wakeups*—rare events where a thread wakes without notification. Always use a predicate. +The lambda `[]{ return ready; }` is the *predicate*. `wait()` does not return until this evaluates to true. This guards against *spurious wakeups*—events where a thread wakes without a matching notification. Always use a predicate. + +Spurious wakeups are permitted on purpose. Implementations are commonly layered on operating-system primitives, such as POSIX condition variables, which themselves permit a wait to return without a matching notification. Requiring `wait` to suppress that would force every implementation to add bookkeeping the platform does not provide. + +An operating-system signal can also wake a waiting thread. On some platforms a notification wakes several waiters when only one can proceed. + +There is also a real race. Between the notification and the waiter reacquiring the mutex, another thread can take the mutex and consume the condition. The waiter then wakes correctly notified and still finds nothing to do, which is indistinguishable from a spurious wakeup. Re-checking a predicate handles both. === Notification Methods @@ -113,5 +115,3 @@ include::example$snippets/3c_advanced.cpp[tag=thread_safe_cache,indent=0] ---- Multiple threads can call `get()` simultaneously without blocking each other. Only `put()` requires exclusive access. - -You have now learned about atomics, condition variables, and shared locks. In the next section, you will explore communication patterns: futures, promises, async, and practical concurrent patterns. diff --git a/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc b/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc index 93f4d6003..55d95f50d 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc @@ -1,12 +1,8 @@ -= Part IV: Communication & Patterns += Futures, async & Patterns +:page-mode: tutorial This section covers communication mechanisms for getting results from threads and practical patterns for concurrent programming. -== Prerequisites - -* Completed xref:3.concurrency/3c.advanced.adoc[Part III: Advanced Primitives] -* Understanding of atomics, condition variables, and shared locks - == Futures and Promises: Getting Results Back Threads can perform work, but how do you get results from them? Passing references works but is clunky. {cpp} offers a cleaner abstraction: *futures* and *promises*. @@ -36,9 +32,9 @@ Creating threads manually, managing promises, joining at the end—it is mechani include::example$programs/3d_patterns_async.cpp[tag=full] ---- -`std::async` launches the function (potentially in a new thread), returning a future. No explicit thread creation, no promise management, no join call. +`std::async` starts the function (potentially in a new thread), returning a future. No explicit thread creation, no promise management, no join call. -=== Launch Policies +=== `std::launch` Policies By default, the system decides whether to run the function in a new thread or defer it until you call `get()`. You can specify: @@ -93,22 +89,7 @@ include::example$programs/3d_patterns_parallel_for.cpp[tag=full] The work is divided into chunks, each handled by its own thread. For CPU-bound work on large datasets, this can dramatically reduce execution time. -== Summary - -You have learned the fundamentals of concurrent programming: - -* *Threads* — Independent flows of execution within a process -* *Mutexes* — Mutual exclusion to prevent data races -* *Lock guards* — RAII wrappers that ensure mutexes are properly released -* *Atomics* — Lock-free safety for single operations -* *Condition variables* — Efficient waiting for events -* *Shared locks* — Multiple readers or one writer -* *Futures and promises* — Communication of results between threads -* *std::async* — Simplified launching of parallel work - -You have seen the dangers—race conditions, deadlocks—and the tools to avoid them. - -=== Best Practices +== Best Practices * *Start with std::async* when possible * *Prefer immutable data* — shared data that never changes needs no synchronization @@ -119,4 +100,4 @@ You have seen the dangers—race conditions, deadlocks—and the tools to avoid Concurrency is challenging. Bugs hide until the worst moment. Testing is hard because timing varies. But the rewards are substantial: responsive applications, full hardware utilization, and elegant solutions to naturally parallel problems. -This foundation prepares you for understanding Capy's concurrency facilities: `thread_pool`, `strand`, `when_all`, `async_event`, `async_mutex`, and `async_waker`. These build on standard primitives to provide coroutine-friendly concurrent programming. +This foundation prepares you for understanding Capy's concurrency facilities: cpp:thread_pool[], cpp:strand[], cpp:when_all[], cpp:async_event[], cpp:async_mutex[], and cpp:async_waker[]. These build on standard primitives to provide coroutine-friendly concurrent programming. diff --git a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc index 08c39b757..cacbaf7cf 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc @@ -8,11 +8,32 @@ // = Coroutines in Capy +:page-mode: explanation You know how {cpp}20 coroutines work at the language level. You understand threads, synchronization, and the problems that concurrency introduces. Now it is time to see how Capy brings these together into a practical, high-performance library. -Capy's coroutine model is built around a single principle: asynchronous code should look like synchronous code. You write a function that reads from a socket, processes the data, and writes a response--top to bottom, with local variables and normal control flow. Capy handles suspension, resumption, thread scheduling, and cancellation behind the scenes. The result is code that is both easier to read and harder to get wrong. +Capy's coroutine model is built around a single principle: asynchronous code should look like synchronous code. You write a function that reads from a socket, processes the data, and writes a response. The code reads top to bottom, with local variables and normal control flow. Capy handles suspension, resumption, thread scheduling, and cancellation behind the scenes. The result is code that is both easier to read and harder to get wrong. But this is not magic, and it is not a black box. Every piece of Capy's coroutine infrastructure is designed to be transparent. You can see how tasks are scheduled, control where they run, propagate cancellation, compose concurrent operations, and tune memory allocation. Understanding these mechanisms is what separates someone who uses the library from someone who uses it _well_. -This section is the bridge between theory and practice. You will see how Capy turns C++20 coroutines into a complete async programming model--from launching and scheduling tasks, through cancellation and concurrent composition, to fine-grained control over memory allocation. Each topic builds on the last, and by the end you will be writing real asynchronous programs with Capy. +== What This Section Covers + +* xref:4.coroutines/4a.tasks.adoc[The task Type] -- Declaring, returning values from, and + awaiting cpp:task[task] coroutines. +* xref:4.coroutines/4b.launching.adoc[Starting Coroutines] -- Starting coroutines with + cpp:run_async[], and binding child tasks with cpp:run[]. +* xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] -- Executors, + execution contexts, thread pools, and strands. +* xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] -- How the executor and + stop token propagate through a chain of awaited coroutines. +* xref:4.coroutines/4e.cancellation.adoc[Stop Tokens and Cancellation] -- Cooperative + cancellation with `std::stop_token`, and how Capy tasks observe it. +* xref:4.coroutines/4f.composition.adoc[Concurrent Composition] -- Running tasks + concurrently with cpp:when_all[] and cpp:when_any[]. +* xref:4.coroutines/4g.allocators.adoc[Frame Allocators] -- How coroutine frames are + allocated, and how to customize the allocator. +* xref:4.coroutines/4h.lambda-captures.adoc[Lambda Coroutine Captures] -- A critical + pitfall: lambda captures versus coroutine frame lifetime. + +Each topic builds on the last, and by the end you are writing real asynchronous programs +with Capy. diff --git a/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc b/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc index b73cdc849..ec999390f 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc @@ -1,15 +1,30 @@ = The task Type +:page-mode: explanation -This section introduces Capy's `task` type—the fundamental coroutine type for asynchronous programming in Capy. +This section introduces Capy's cpp:task[task] type—the fundamental coroutine type for asynchronous programming in Capy. -== Prerequisites +cpp:task[task] is declared in: -* Completed xref:../2.cpp20-coroutines/2d.advanced.adoc[{cpp}20 Coroutines Tutorial] -* Understanding of promise types, coroutine handles, and symmetric transfer +[source,cpp] +---- +include::example$snippets/4a_tasks.cpp[tag=include_task] +---- + +[NOTE] +==== +*How this manual names headers.* Each page gives the specific header for what it introduces, so you can include only what you use. The runnable examples instead include the umbrella header, which pulls in everything: + +[source,cpp] +---- +include::example$snippets/4a_tasks.cpp[tag=include_umbrella] +---- + +That keeps the examples short and focused on the behavior being shown. In your own code, prefer the specific headers. +==== == Overview -`task` is Capy's primary coroutine return type. It represents an asynchronous operation that eventually produces a value of type `T` (or nothing, for `task`). +cpp:task[task] is Capy's primary coroutine return type. It represents an asynchronous operation that eventually produces a value of type `T` (or nothing, for `task`). Key characteristics: @@ -21,14 +36,14 @@ Key characteristics: == Declaring task Coroutines -Any function that returns `task` and contains coroutine keywords (`co_await`, `co_return`) is a `task` coroutine: +Any function that returns cpp:task[task] and contains coroutine keywords (`co_await`, `co_return`) is a `task` coroutine: [source,cpp] ---- include::example$snippets/4a_tasks.cpp[tag=declaring] ---- -The syntax `task<>` is equivalent to `task` and represents a coroutine that completes without producing a value. +The syntax cpp:task[task<>] is equivalent to `task` and represents a coroutine that completes without producing a value. == Returning Values with co_return @@ -39,7 +54,36 @@ Use `co_return` to complete the coroutine and provide its result: include::example$snippets/4a_tasks.cpp[tag=returning] ---- -For `task`, you can either use `co_return;` explicitly or let execution fall off the end of the function body. +For cpp:task[task], you can either use `co_return;` explicitly or let execution fall off the end of the function body. + +[#io-result-and-io-task] +== Reporting Errors: io_result and io_task + +I/O operations report an expected failure as a value rather than an exception. They return the error alongside the result, in a cpp:io_result[io_result]. It holds a `std::error_code` named `ec`, followed by zero or more payload values. Exceptions remain for genuine errors, such as a failed frame allocation. + +cpp:io_task[io_task] names a task returning one of those results: + +[source,cpp] +---- +include::example$snippets/4a_tasks.cpp[tag=io_task] +---- + +cpp:io_result[] models the tuple protocol, with `ec` first, so structured bindings work directly. Always test `ec` before reading a payload; a payload's meaning when `ec` is set is defined by the operation that produced it. + +For cpp:io_result[io_result<>] -- 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. + +These two names are the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4f.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Stream Concepts] both assume them. + +== Running a Task + +A cpp:task[task] is lazy, so declaring one does not run it. To run a task to completion from ordinary (non-coroutine) code, pass it to cpp:run_async[] with an executor and a completion handler. The task runs on the executor, and its result is delivered to the handler: + +[source,cpp] +---- +include::example$snippets/4a_tasks.cpp[tag=run] +---- + +Here `add(2, 3)` runs on a cpp:thread_pool[] executor, and the completion handler receives the result, `5`. The call to `pool.join()` waits for the pooled work to finish before the result is read. == Awaiting Other Tasks @@ -59,7 +103,7 @@ When you `co_await` a task: == Lazy Execution -A critical property of `task` is *lazy execution*: creating a task does not start its execution. The coroutine body runs only when the task is awaited. +A critical property of cpp:task[task] is *lazy execution*: creating a task does not start its execution. The coroutine body runs only when the task is awaited. [source,cpp] ---- @@ -68,6 +112,7 @@ include::example$snippets/4a_tasks.cpp[tag=lazy] *Output:* +[role=output] ---- Task created Computing... @@ -117,21 +162,3 @@ include::example$snippets/4a_tasks.cpp[tag=exceptions] ---- The exception is stored in the promise when it occurs and rethrown in `await_resume` when the calling coroutine resumes. - -== Reference - -The `task` type is defined in: - -[source,cpp] ----- -include::example$snippets/4a_tasks.cpp[tag=include_task] ----- - -Or included via the umbrella header: - -[source,cpp] ----- -include::example$snippets/4a_tasks.cpp[tag=include_umbrella] ----- - -You have now learned how to declare, return values from, and await `task` coroutines. In the next section, you will learn how to launch tasks for execution using `run_async` and `run`. diff --git a/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc b/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc index 53bfcf161..cf2c19156 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc @@ -1,24 +1,20 @@ -= Launching Coroutines += Starting Coroutines +:page-mode: how-to -This section explains how to launch coroutines for execution. You will learn about `run_async` for entry from non-coroutine code and `run` for executor hopping within coroutine code. - -== Prerequisites - -* Completed xref:4.coroutines/4a.tasks.adoc[The task Type] -* Understanding of lazy task execution +This section explains how to start coroutines for execution. It covers cpp:run_async[] for entry from non-coroutine code and cpp:run[] for executor hopping within coroutine code. == The Execution Model Capy tasks are lazy—they do not execute until something drives them. Two mechanisms exist: * *Awaiting* — One coroutine awaits another (`co_await task`) -* *Launching* — Non-coroutine code initiates execution (`run_async`) +* *Starting* — Non-coroutine code initiates execution (cpp:run_async[]) -When a task is awaited, the awaiting coroutine provides context: an executor for dispatching completion and a stop token for cancellation. But what about the first task in a chain? That task needs explicit launching. +When a task is awaited, the awaiting coroutine provides context: an executor for dispatching completion and a stop token for cancellation. But what about the first task in a chain? Non-coroutine code must start that task explicitly. == run_async: Entry from Non-Coroutine Code -`run_async` is the bridge between regular code and coroutine code. It takes an executor, creates the necessary context, and starts the task executing. +cpp:run_async[] is the bridge between regular code and coroutine code. It takes an executor, creates the necessary context, and starts the task executing. [source,cpp] ---- @@ -27,7 +23,7 @@ include::example$programs/4b_launching_run_async.cpp[tag=full] === Two-Call Syntax -Notice the unusual syntax: `run_async(executor)(task)`. This is intentional and relates to {cpp}17 evaluation order. +Notice the unusual syntax: cpp:run_async[]`(executor)(task)`. This is intentional and relates to {cpp}17 evaluation order. {cpp}17 guarantees that in the expression `f(a)(b)`: @@ -35,25 +31,46 @@ Notice the unusual syntax: `run_async(executor)(task)`. This is intentional and 2. `b` is evaluated second 3. The callable is invoked with `b` -This ordering matters because the task's coroutine frame is allocated during step 2, and `run_async` sets up thread-local allocator state in step 1. The task inherits that allocator. +This ordering matters because the task's coroutine frame is allocated during step 2, and cpp:run_async[] sets up thread-local allocator state in step 1. The task inherits that allocator. [WARNING] ==== -Do not store the result of `run_async(executor)` and call it later. The -wrapper's call operator is rvalue-qualified, so this fails to compile: +Construct the task as the direct argument of the two-call expression. Three +patterns split the two calls apart, and only the first is caught by the +compiler. + +*Stored wrapper.* Storing the result of cpp:run_async[]`(executor)` and calling +it later does not compile, because the wrapper's call operator is +rvalue-qualified: +.Does not compile [source,cpp,role=pseudocode] ---- auto wrapper = run_async(pool.get_executor()); // Don't do this wrapper(compute()); // Error: operator() requires an rvalue ---- +*Preconstructed task.* Storing the *task* in a local and passing it in +afterwards compiles and runs. Its frame is allocated by the time the wrapper +exists, so it never sees the wrapper's allocator. A moved-from local, or a task +returned by an earlier statement, behaves the same way. + +*Wrapper function.* A helper that accepts a task and performs the two-call +pattern internally has the same effect. Its caller constructs the task as an +argument to the helper, which is before the helper's body runs. + +The two silent patterns produce no diagnostic at all: the task runs, on a +coroutine frame that came from the wrong allocator. +cpp:run_async_wrapper[] documents each pattern, and +xref:4.coroutines/4g.allocators.adoc#two-call-rationale[Frame Allocators] carries the +{cpp}17-evaluation-order rationale. + Always use the two-call pattern in a single expression. ==== === Handler Overloads -`run_async` accepts optional handlers for results and exceptions: +cpp:run_async[] accepts optional handlers for results and exceptions: [source,cpp] ---- @@ -65,11 +82,11 @@ that goes unhandled (no error handler was supplied, or a handler let one escape) calls `std::terminate`. To react to an error, pass an error handler; it receives the `std::exception_ptr` and should handle it in place rather than rethrowing. To catch an error, `co_await` the work inside a coroutine -and use `try`/`catch` rather than launching it fire-and-forget. +and use `try`/`catch` rather than starting it fire-and-forget. == run: Executor Hopping Within Coroutines -Inside a coroutine, use `run` to execute a child task on a different executor: +Inside a coroutine, use cpp:run[] to execute a child task on a different executor: [source,cpp] ---- @@ -80,7 +97,7 @@ include::example$snippets/4b_launching.cpp[tag=run_hop] By default, a task inherits its caller's executor. This means completions are dispatched through that executor, ensuring thread affinity for thread-sensitive code. -`run` overrides this inheritance for a specific child task, binding it to a different executor. The child task runs on the specified executor, and when it completes, the parent task resumes on its original executor. +cpp:run[] overrides this inheritance for a specific child task, binding it to a different executor. The child task runs on the specified executor, and when it completes, the parent task resumes on its original executor. This pattern is useful for: @@ -90,11 +107,11 @@ This pattern is useful for: == Stop Token Propagation -Both `run_async` and `run` propagate stop tokens to the launched task and all tasks it awaits. The task accesses its token via `co_await this_coro::stop_token`. +Both cpp:run_async[] and cpp:run[] propagate stop tokens to the task they start and all tasks it awaits. The task accesses its token via `co_await this_coro::stop_token`. === Injecting a Token with run_async -Since `run_async` is called from non-coroutine code, there is no caller token to inherit. Pass a stop token explicitly: +Since cpp:run_async[] is called from non-coroutine code, there is no caller token to inherit. Pass a stop token explicitly: [source,cpp] ---- @@ -103,7 +120,7 @@ include::example$snippets/4b_launching.cpp[tag=inject_token,indent=0] === Inheritance with run -`run` is called from within a coroutine, so it inherits the caller's stop token by default: +cpp:run[] is called from within a coroutine, so it inherits the caller's stop token by default: [source,cpp] ---- @@ -119,7 +136,7 @@ include::example$snippets/4b_launching.cpp[tag=override_token,indent=0] == Handler Threading -Handlers passed to `run_async` are invoked on whatever thread the executor schedules: +Handlers passed to cpp:run_async[] are invoked on whatever thread the executor schedules: [source,cpp] ---- @@ -128,17 +145,3 @@ include::example$snippets/4b_launching.cpp[tag=handler_thread,indent=0] If you need results on a specific thread, use appropriate synchronization or dispatch mechanisms. -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| Entry point for launching tasks from non-coroutine code - -| `` -| Executor binding for child tasks within coroutines -|=== - -You have now learned how to launch coroutines using `run_async` and bind child tasks to specific executors using `run`. In the next section, you will learn about executors and execution contexts in detail. diff --git a/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc b/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc index 18ad79256..a5d9c3c8a 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc @@ -1,46 +1,40 @@ = Executors and Execution Contexts +:page-mode: explanation This section explains executors and execution contexts—the mechanisms that control where and how coroutines execute. -== Prerequisites - -* Completed xref:4.coroutines/4b.launching.adoc[Launching Coroutines] -* Understanding of `run_async` and `run` - [#the-same-executor-invariant] == The Same-Executor Invariant Capy enforces one rule above all others: -*A coroutine always resumes on the executor it was launched with.* +*A coroutine always resumes on the executor it was started with.* -This rule is what keeps shared state safe by default. Consider a connection handler launched on a strand: +This rule is what keeps shared state safe by default. Consider a connection handler started on a strand: [source,cpp] ---- include::example$snippets/4c_executors.cpp[tag=handle_client] ---- -Launch this on a strand, and every resumption—after `conn.read()` and after `conn.write()`—happens on that strand. The update to `conn.stats.requests` is therefore free of data races without any mutex. +Start this on a strand, and every resumption—after `conn.read()` and after `conn.write()`—happens on that strand. The update to `conn.stats.requests` is therefore free of data races without any mutex. Without the invariant, the coroutine could resume after `co_await conn.read()` on an `io_uring` completion thread, a pool thread, or wherever the I/O subsystem completed the operation. Correct code would then need either a mutex around every access to shared state or an explicit _resume-on-this-strand_ step after every `co_await`. A mutex defeats the purpose of the strand, and a single forgotten resume step reintroduces the data race. -Because the invariant holds, the safe behavior is automatic and the unsafe behavior does not compile: awaiting a _plain_ awaitable—one that could resume the coroutine on any thread—is rejected. See xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] for why, and for the explicit escape hatch. +Because the invariant holds, the safe behavior is automatic and the unsafe behavior does not compile. Awaiting a _plain_ awaitable—one that could resume the coroutine on any thread—is rejected. See xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] for why, and for the explicit escape hatch. === How the Invariant Is Maintained -Affinity propagates forward. Launching a task with `run_async(ex)` binds it to `ex`; a child `co_await`-ed from that task inherits the same executor automatically, and so on down the chain. When a child completes, control returns to its caller _through the caller's executor_. When both share the same executor—the common case—that return is a direct symmetric transfer with no queuing; only a deliberate executor change requires a dispatch. +Affinity propagates forward. Starting a task with cpp:run_async[]`(ex)` binds it to `ex`. A child `co_await`-ed from that task inherits the same executor automatically, and so on down the chain. When a child completes, control returns to its caller _through the caller's executor_. When both share the same executor—the common case—that return is a direct symmetric transfer with no queuing. Only a deliberate executor change requires a dispatch. -That deliberate change is what `run` provides: it runs a subtree on a different executor and restores the caller's executor when the subtree completes (see xref:4.coroutines/4b.launching.adoc[Launching Coroutines]). This is also why I/O objects with executor-bound invariants—a socket tied to one `io_context`, a Windows IOCP handle, executor-specific timer state—remain safe: a coroutine holding them never resumes on the wrong executor mid-body. +That deliberate change is what `run` provides. It runs a subtree on a different executor and restores the caller's executor when the subtree completes (see xref:4.coroutines/4b.launching.adoc[Starting Coroutines]). This is also why I/O objects with executor-bound invariants—a socket tied to one `io_context`, a Windows IOCP handle, executor-specific timer state—remain safe. A coroutine holding them never resumes on the wrong executor mid-body. == The Executor Concept -An *executor* is an object that can schedule work for execution. An executor must be nothrow copy- and move-constructible and provide the following interface: - -[source,cpp] ----- -include::example$snippets/4c_executors.cpp[tag=executor_concept] ----- +An *executor* is an object that can schedule work for execution. See +cpp:Executor[] for the exact syntactic and semantic requirements, and +xref:9.design/9k.Executor.adoc[Executor Concept Design] for the +rationale behind them. === dispatch() vs post() @@ -58,20 +52,20 @@ Returns a reference to the execution context that owns this executor. The contex == executor_ref: Type-Erased Executor -`executor_ref` wraps any executor in a type-erased container, allowing code to work with executors without knowing their concrete type: +cpp:executor_ref[] wraps any executor in a type-erased container, allowing code to work with executors without knowing their concrete type: [source,cpp] ---- include::example$programs/4c_executors_executor_ref.cpp[tag=full] ---- -Here `make_suspended_work` launches a coroutine on the pool and returns the `continuation` it parked; posting that continuation through the type-erased `ex` resumes it. +Here `make_suspended_work` starts a coroutine on the pool and returns the cpp:continuation[] it parked; posting that continuation through the type-erased `ex` resumes it. -`executor_ref` stores a reference to the underlying executor—the original executor must outlive the `executor_ref`. +cpp:executor_ref[] stores a reference to the underlying executor—the original executor must outlive the `executor_ref`. == thread_pool: Multi-Threaded Execution -`thread_pool` manages a pool of worker threads that execute coroutines concurrently: +cpp:thread_pool[] manages a pool of worker threads that execute coroutines concurrently: [source,cpp] ---- @@ -80,26 +74,23 @@ include::example$programs/4c_executors_thread_pool.cpp[tag=full] === Constructor Parameters -[source,cpp] ----- -include::example$snippets/4c_executors.cpp[tag=thread_pool_ctor,indent=0] ----- +See cpp:thread_pool[] for the exact constructor signature. * `num_threads` — Number of worker threads. If 0, uses hardware concurrency. * `thread_name_prefix` — Prefix for thread names (useful for debugging). === Thread Safety -Work posted to a `thread_pool` may execute on any of its worker threads. If your coroutines access shared data, you must use appropriate synchronization. +Work posted to a cpp:thread_pool[] may execute on any of its worker threads. If your coroutines access shared data, you must use appropriate synchronization. == execution_context: Base Class -`execution_context` is the base class for execution contexts. It provides: +cpp:execution_context[] is the base class for execution contexts. It provides: * Frame allocator access via `get_frame_allocator()` * Service infrastructure for extensibility -Custom execution contexts inherit from `execution_context`: +Custom execution contexts inherit from cpp:execution_context[]: [source,cpp] ---- @@ -108,7 +99,7 @@ include::example$snippets/4c_executors.cpp[tag=my_context] == strand: Serialization Without Mutexes -A `strand` ensures that handlers are executed in order, with no two handlers executing concurrently. This eliminates the need for mutexes when all access to shared data goes through the strand. +A cpp:strand[] ensures that handlers are executed in order, with no two handlers executing concurrently. This eliminates the need for mutexes when all access to shared data goes through the strand. [source,cpp] ---- @@ -119,8 +110,8 @@ include::example$snippets/4c_executors.cpp[tag=shared_resource] The strand maintains a queue of pending work. When work is dispatched: -1. If no other work is executing on the strand, the new work runs immediately -2. If other work is executing, the new work is queued +1. If the calling thread is already executing within the strand, the new work runs immediately, inline +2. Otherwise, the new work is queued, even if the strand is idle 3. When the current work completes, the next queued item runs This provides logical single-threading without blocking physical threads. @@ -160,26 +151,3 @@ For embarrassingly parallel work with no shared state: include::example$snippets/4c_executors.cpp[tag=independent_tasks,indent=0] ---- -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| The Executor concept definition - -| `` -| Type-erased executor wrapper - -| `` -| Multi-threaded execution context - -| `` -| Base class for execution contexts - -| `` -| Serialization primitive -|=== - -You have now learned about executors, execution contexts, thread pools, and strands. In the next section, you will learn about the IoAwaitable protocol that enables context propagation. diff --git a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc b/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc index 7a159378c..58c9a3f82 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc @@ -1,12 +1,8 @@ = The IoAwaitable Protocol +:page-mode: explanation This section explains the IoAwaitable protocol—Capy's mechanism for propagating execution context through coroutine chains. -== Prerequisites - -* Completed xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] -* Understanding of standard awaiter protocol (`await_ready`, `await_suspend`, `await_resume`) - == The Problem: Context Propagation Standard {cpp}20 coroutines define awaiters with this `await_suspend` signature: @@ -16,7 +12,7 @@ Standard {cpp}20 coroutines define awaiters with this `await_suspend` signature: include::example$snippets/4d_io_awaitable.cpp[tag=std_await_suspend,indent=0] ---- -The awaiter receives only a handle to the suspended coroutine. But real I/O code needs more: +The awaiter receives only a handle to the suspended coroutine. But real applications need more: * *Executor* — Where should completions be dispatched? * *Stop token* — Should this operation support cancellation? @@ -24,6 +20,13 @@ The awaiter receives only a handle to the suspended coroutine. But real I/O code How does an awaitable get this information? +[NOTE] +==== +*Nothing on this page is specific to I/O.* An executor, a stop token, and an allocator are what any asynchronous operation needs. It makes no difference whether the operation waits on a socket, a GPU queue, a timer, or another thread. + +The `Io` in cpp:IoAwaitable[] reflects where the protocol was first used, Corosio's I/O types, rather than a restriction on where it applies. Read the name as "an awaitable that carries its execution environment." +==== + === Backward Query Approach One approach: the awaitable queries the calling coroutine's promise for context. This requires the awaitable to know the promise type, creating tight coupling. @@ -45,26 +48,26 @@ This signature receives: * `h` — The coroutine handle (as in standard awaiters) * `env` — The execution environment containing: -** `env->executor` — The caller's executor for dispatching completions -** `env->stop_token` — A stop token for cooperative cancellation -** `env->frame_allocator` — An optional frame allocator +** cpp:io_env::executor[env->executor] — The caller's executor for dispatching completions +** cpp:io_env::stop_token[env->stop_token] — A stop token for cooperative cancellation +** cpp:io_env::frame_allocator[env->frame_allocator] — An optional frame allocator Many IoAwaitables return `std::coroutine_handle<>` to enable symmetric transfer, but the concept does not require any particular return type. == IoAwaitable Concept -An awaitable satisfies `IoAwaitable` if `a.await_suspend(h, env)` is a valid expression: +An awaitable satisfies cpp:IoAwaitable[] if `a.await_suspend(h, env)` is a valid expression: [source,cpp] ---- include::example$snippets/4d_io_awaitable.cpp[tag=io_awaitable_concept] ---- -The concept constrains only the two-argument `await_suspend` that receives the `io_env`. It does not require `await_ready` or `await_resume`, nor does it constrain the return type of `await_suspend`. A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept simply does not test for them. +The concept constrains only the two-argument `await_suspend` that receives the `io_env`. It does not require `await_ready` or `await_resume`, nor does it constrain the return type of `await_suspend`. A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept does not test for them. == IoRunnable Concept -For tasks that can be launched from non-coroutine contexts, the `IoRunnable` concept refines `IoAwaitable` and requires a `promise_type` plus the following: +For tasks that can be started from non-coroutine contexts, the cpp:IoRunnable[] concept refines cpp:IoAwaitable[] and requires a `promise_type` plus the following: * `handle()`: Access the typed coroutine handle * `release()`: Transfer ownership of the frame @@ -73,15 +76,15 @@ For tasks that can be launched from non-coroutine contexts, the `IoRunnable` con * `set_continuation()`: Set the continuation handle (on the promise) * `set_environment()`: Inject the `io_env` (on the promise) -These methods exist because launch functions like `run_async` cannot `co_await` the task directly. The trampoline must be allocated before the task type is known, so it type-erases the task through function pointers and needs a common API to manage lifetime and extract results. +These methods exist because launcher functions like cpp:run_async[] cannot `co_await` the task directly. The trampoline must be allocated before the task type is known, so it type-erases the task through function pointers. The trampoline therefore needs a common API to manage lifetime and extract results. -The context injection methods `set_continuation` and `set_environment` are part of the `IoRunnable` concept: it requires them on the `promise_type`. Launch functions access them through the typed handle provided by `handle()`. +The context injection methods `set_continuation` and `set_environment` are part of the cpp:IoRunnable[] concept: it requires them on the `promise_type`. Launcher functions access them through the typed handle provided by `handle()`. -Capy's `task` satisfies this concept. +Capy's cpp:task[task] satisfies this concept. == How Context Flows -When you write `co_await child_task()` inside a `task`: +When you write `co_await child_task()` inside a cpp:task[task]: 1. The parent task's `await_transform` intercepts the awaitable 2. It wraps the child in a transform awaiter @@ -102,13 +105,13 @@ Forward propagation offers several advantages: * *Composability* — Any IoAwaitable works with any IoRunnable task * *Explicit flow* — Context flows downward through the call chain, not queried upward -This design enables Capy's type-erased wrappers (`any_stream`, etc.) to work without knowing the concrete executor type. +This design enables Capy's type-erased wrappers (cpp:any_stream[], etc.) to work without knowing the concrete executor type. == A Vocabulary for Coroutine Interop -Because the protocol is just a two-argument `await_suspend`, `IoAwaitable` is more than an internal mechanism—it is a _vocabulary type_ for interoperation. Any coroutine library that speaks the protocol can propagate execution environment across a `co_await` boundary without knowing the other side's concrete task type. +Because the protocol is just a two-argument `await_suspend`, cpp:IoAwaitable[] is more than an internal mechanism—it is a _vocabulary type_ for interoperation. Any coroutine library that speaks the protocol can propagate execution environment across a `co_await` boundary without knowing the other side's concrete task type. -This is the interop problem framed on the xref:index.adoc[home page]: a shared protocol replaces the set of pairwise adapters that separate coroutine libraries would otherwise need to talk to one another. Capy is the reference implementation of that protocol. +This is the interop problem framed on the xref:index.adoc[home page]. A shared protocol replaces the set of pairwise adapters that separate coroutine libraries would otherwise need to talk to one another. Capy is the reference implementation of that protocol. == Implementing Custom IoAwaitables @@ -121,8 +124,8 @@ include::example$snippets/4d_io_awaitable.cpp[tag=my_awaitable] The key points: -1. Store the `io_env` as a pointer (`io_env const*`), never a copy. Launch functions guarantee the `io_env` outlives the awaitable's operation. -2. To resume the caller, wrap its handle in a `continuation` and pass that to the executor's `post` (or `dispatch`) — these take a `continuation&`, not a raw `coroutine_handle`. Store the `continuation` in the awaitable so it keeps a stable address until the executor dequeues and resumes it; the executor links continuations intrusively, so a temporary would dangle. +1. Store the `io_env` as a pointer (`io_env const*`), never a copy. Launcher functions guarantee the `io_env` outlives the awaitable's operation. +2. To resume the caller, wrap its handle in a cpp:continuation[] and pass that to the executor's `post` (or `dispatch`). These take a `continuation&`, not a raw `coroutine_handle`. Store the `continuation` in the awaitable so it keeps a stable address until the executor dequeues and resumes it. The executor links continuations intrusively, so a temporary would dangle. 3. Respect the stop token for cancellation === Stop Callbacks Must Post, Not Resume @@ -150,7 +153,25 @@ include::example$snippets/4d_io_awaitable.cpp[tag=wrong_stop_callback,indent=0] See xref:4.coroutines/4e.cancellation.adoc#stoppable-awaitables[Implementing Stoppable Awaitables] for a complete example. -For a production implementation of this exact pattern, read the source of `async_waker::wait_awaiter` (xref:reference:boost/capy/async_waker/wait_awaiter.adoc[`async_waker::wait_awaiter`]): it registers a stop callback that posts the resume through the executor, and arbitrates between wakeup and cancellation with a single atomic claim. +For a production implementation of this exact pattern, read the source of cpp:async_waker::wait_awaiter[]. It registers a stop callback that posts the resume through the executor, and arbitrates between wakeup and cancellation with a single atomic claim. + +== Running a Custom IoAwaitable + +The sections above show how to _build_ an IoAwaitable; this one shows it running inside a task and delivering its result. The awaitable produces a value and resumes the caller by posting its cpp:continuation[] through the caller's executor—the pattern from the previous section, completed: + +[source,cpp] +---- +include::example$snippets/4d_io_awaitable.cpp[tag=runnable_awaitable] +---- + +To run that task from ordinary, non-coroutine code, hand it to cpp:run_async[] with an executor and a completion handler: + +[source,cpp] +---- +include::example$snippets/4d_io_awaitable.cpp[tag=run_awaitable,indent=0] +---- + +The task runs on the cpp:thread_pool[] executor. When the awaitable posts its continuation, the pool resumes the task, `await_resume` returns the value, and it arrives at the completion handler—here, `5`. [#bridging-a-foreign-awaitable] == Bridging a Foreign Awaitable @@ -166,7 +187,7 @@ include::example$snippets/4d_io_awaitable.cpp[tag=reject_plain,indent=0] This is intentional. A plain awaitable receives only the coroutine handle; it can resume the coroutine on any thread by calling `handle.resume()` directly. That silently breaks the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant]—the coroutine could wake on a foreign completion thread, leaving shared state you believed was strand-protected exposed to races. Rejecting such an awaitable at compile time prevents that. The constraint does not lock you in; it requires environment propagation to be explicit rather than silently dropped. -The escape hatch is to wrap the foreign awaitable (or callback, or future) in a small `IoAwaitable` that captures the executor and re-posts the resumption through it: +The escape hatch is to wrap the foreign awaitable (or callback, or future) in a small cpp:IoAwaitable[]. That awaitable captures the executor and re-posts the resumption through it: [source,cpp] ---- @@ -177,26 +198,12 @@ The single rule that makes any bridge correct: *on completion, post through `env === There Is No Universal Bridge -Capy does not provide a generic `co_await foreign_awaitable(x)` that adapts _arbitrary_ awaitables automatically. Such an adapter cannot work in the general case: it has no way to know how a foreign runtime schedules its completions, so it cannot guarantee the invariant. A shared protocol addresses this where a hidden adapter cannot. A small, explicit bridge per foreign runtime keeps the guarantee intact and the cost visible. +Capy does not provide a generic `co_await foreign_awaitable(x)` that adapts _arbitrary_ awaitables automatically. Such an adapter cannot work in the general case. It has no way to know how a foreign runtime schedules its completions, so it cannot guarantee the invariant. A shared protocol addresses this where a hidden adapter cannot. A small, explicit bridge per foreign runtime keeps the guarantee intact and the cost visible. === Worked Bridges Two complete, buildable bridges live in the examples: -* xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] — `await_sender` adapts a `std::execution` sender, mapping its completion channels onto `io_result` and posting the resumption through the executor. -* xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] — the `use_capy` completion token turns any Asio async operation into an `IoAwaitable`. - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| The IoAwaitable concept definition - -| `` -| The IoRunnable concept for launchable tasks -|=== +* xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] — `await_sender` adapts a `std::execution` sender, mapping its completion channels onto cpp:io_result[] and posting the resumption through the executor. +* xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] — the `use_capy` completion token turns any Asio async operation into an cpp:IoAwaitable[]. -You have now learned how the IoAwaitable protocol enables context propagation through coroutine chains. In the next section, you will learn about stop tokens and cooperative cancellation. diff --git a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc b/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc index 0799c8c07..4d4749ac8 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc @@ -1,13 +1,9 @@ = Stop Tokens and Cancellation +:page-mode: explanation -This section teaches cooperative cancellation from the ground up, explaining {cpp}20 stop tokens as a general-purpose notification mechanism and how Capy uses them for coroutine cancellation. +This section teaches cooperative cancellation from the ground up. It explains {cpp}20 stop tokens as a general-purpose notification mechanism, and how Capy uses them for coroutine cancellation. -== Prerequisites - -* Completed xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] -* Understanding of how context propagates through coroutine chains - -== Part 1: The Problem +== The Problem Cancellation matters in many scenarios: @@ -40,7 +36,7 @@ Some systems support forceful thread interruption. This is dangerous because it The solution is *cooperative cancellation*: ask nicely, let the work clean up. The cancellation requestor signals intent; the worker decides when and how to respond. -== Part 2: {cpp}20 Stop Tokens—A General-Purpose Signaling Mechanism +== {cpp}20 Stop Tokens—A General-Purpose Signaling Mechanism {cpp}20 introduces `std::stop_token`, `std::stop_source`, and `std::stop_callback`. While named for "stopping," these implement a general-purpose *Observer pattern*—a thread-safe one-to-many notification system. @@ -64,6 +60,7 @@ include::example$snippets/4e_cancellation.cpp[tag=observer_pattern,indent=0] *Output:* +[role=output] ---- Before signal Observer 1 notified @@ -94,7 +91,7 @@ Each `stop_callback` stores a different callable type `F`. Despite this, all Registration and invocation are thread-safe. You can register callbacks, request stop, and invoke callbacks from any thread without additional synchronization. -== Part 3: The One-Shot Nature +== The One-Shot Nature [WARNING] ==== @@ -119,7 +116,7 @@ To "reset," create an entirely new `stop_source`: include::example$snippets/4e_cancellation.cpp[tag=reset_workaround,indent=0] ---- -This is manual and error-prone. Any code still holding the old token will not receive new signals. +This is manual and error-prone. Any code still holding the old token does not receive new signals. === Design Implication @@ -129,7 +126,7 @@ If you need repeatable signals, `stop_token` is the wrong tool. Consider: * Atomic flags with explicit reset protocol * Custom event types -== Part 4: Beyond Cancellation +=== Beyond Cancellation The "stop" naming obscures the mechanism's generality. `stop_token` implements *one-shot broadcast notification*, useful for: @@ -138,7 +135,7 @@ The "stop" naming obscures the mechanism's generality. `stop_token` implements * * *Resource availability* — Signal when database connected or cache warmed * *Any one-shot broadcast scenario* -== Part 5: Stop Tokens in Coroutines +== Stop Tokens in Coroutines Coroutines have a propagation problem: how does a nested coroutine know to stop? If you pass a stop token explicitly to every function, your APIs become cluttered. @@ -164,16 +161,16 @@ include::example$snippets/4e_cancellation.cpp[tag=access_stop_token,indent=0] === Why Not `coroutine_handle::destroy()`? -`std::coroutine_handle::destroy()` is the {cpp}20 primitive that frees a coroutine frame. It is not a cancellation mechanism, and it has the same flaw as forceful thread interruption: the coroutine is torn down with no opportunity to complete pending I/O, release locks, or run RAII destructors in the expected order. +`std::coroutine_handle::destroy()` is the {cpp}20 primitive that frees a coroutine frame. It is not a cancellation mechanism. It has the same flaw as forceful thread interruption. The coroutine is torn down with no opportunity to complete pending I/O, release locks, or run RAII destructors in the expected order. -Capy exposes `task::handle()` and `quitter::handle()` so that Capy's own launchers (`run_async`, `run`) and custom integrations can dispatch coroutines through executors. Calling `destroy()` on such a handle while the coroutine is being awaited by a parent produces undefined behavior: the destruction cascades back through the parent's continuation, re-entering frame destruction that is already in progress. +Capy exposes cpp:task::handle[handle]`()` and cpp:quitter::handle[handle]`()` so that Capy's own launchers (cpp:run_async[], cpp:run[]) and custom integrations can dispatch coroutines through executors. Calling `destroy()` on such a handle while the coroutine is being awaited by a parent produces undefined behavior. The destruction cascades back through the parent's continuation, re-entering frame destruction that is already in progress. The rule: * To cancel work, request a stop on a `std::stop_source` whose token the work observes. The work unwinds cleanly through `final_suspend` and any RAII guards run in the correct order. -* Do not call `destroy()` on a handle returned by `task::handle()` or `quitter::handle()` while the coroutine is being awaited. +* Do not call `destroy()` on a handle returned by cpp:task::handle[handle]`()` or cpp:quitter::handle[handle]`()` while the coroutine is being awaited. -== Part 6: Responding to Cancellation +== Responding to Cancellation === Checking the Token @@ -193,21 +190,21 @@ include::example$snippets/4e_cancellation.cpp[tag=raii_cleanup,indent=0] === The canceled Convention -When cancellation causes an operation to fail, the conventional error code is `error::canceled`, which compares equal to the portable condition `cond::canceled`: +When cancellation causes an operation to fail, the conventional error code is cpp:error::canceled[error::canceled], which compares equal to the portable condition cpp:cond::canceled[cond::canceled]: [source,cpp] ---- include::example$snippets/4e_cancellation.cpp[tag=canceled_convention,indent=0] ---- -== Part 7: OS Integration +== OS Integration When Capy's I/O is provided by Corosio, requesting stop cancels work in progress rather than waiting for the operation to finish on its own. Corosio cancels the pending operation through whatever backend is active for the platform, and it resolves promptly with `std::errc::operation_canceled`. -The mechanism depends on the backend: completion-based backends (Windows IOCP, and io_uring when enabled on Linux) cancel the operation in the kernel, while readiness-based backends (Linux epoll, kqueue on the BSDs and macOS, and the portable select fallback) remove it from the reactor before its system call runs. Either way the operation is reported as cancelled instead of blocking until the I/O would have completed. +The mechanism depends on the backend. Completion-based backends (Windows IOCP, and io_uring when enabled on Linux) cancel the operation in the kernel. Readiness-based backends (Linux epoll, kqueue on the BSDs and macOS, and the portable select fallback) remove it from the reactor before its system call runs. Either way the operation is reported as cancelled instead of blocking until the I/O would have completed. [[stoppable-awaitables]] -== Part 8: Implementing Stoppable Awaitables +== Implementing Stoppable Awaitables The examples above show *polling* for cancellation with `token.stop_requested()`. For awaitables that suspend indefinitely—waiting for I/O, a lock, or an external event—you need a `std::stop_callback` to wake the coroutine when cancellation arrives. @@ -237,12 +234,12 @@ When `request_stop()` fires the callback, the coroutine handle is posted to the NOTE: Capy's built-in I/O awaitables (via Corosio) already use the post-back pattern internally. This guidance applies when writing your own custom awaitables. -== Part 9: Patterns +== Patterns === Racing a Deadline A timeout is expressed as a race: the operation you care about against -a deadline. `async_waker` provides the suspension point for both sides +a deadline. cpp:async_waker[] provides the suspension point for both sides of that race; a user thread supplies the clock: [source,cpp] @@ -250,20 +247,20 @@ of that race; a user thread supplies the clock: include::example$snippets/4e_cancellation.cpp[tag=racing_deadline,indent=0] ---- -`when_any(await_fetch(ch), deadline(waker))` returns as soon as either side -wakes; the loser's wait resolves with `cond::canceled`. See +cpp:when_any[]`(await_fetch(ch), deadline(waker))` returns as soon as either side +wakes; the loser's wait resolves with cpp:cond::canceled[cond::canceled]. See xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] for the full runnable demo, including the two `std::thread` objects that play fetch worker and clock. When the work you are racing is Corosio I/O rather than coroutine-internal work, Corosio's own timed operations take the place of the deadline -thread, scheduling against the platform event loop instead of a +thread. They schedule against the platform event loop instead of a `std::this_thread::sleep_for`. === User Cancellation -Connect UI cancellation to stop tokens. Pass the token through `run_async` so it propagates automatically via the execution environment—the task accesses it with `co_await this_coro::stop_token` instead of receiving it as a function argument: +Connect UI cancellation to stop tokens. Pass the token through cpp:run_async[] so it propagates automatically via the execution environment. The task accesses it with `co_await this_coro::stop_token` instead of receiving it as a function argument: [source,cpp] ---- @@ -281,11 +278,11 @@ include::example$snippets/4e_cancellation.cpp[tag=graceful_shutdown,indent=0] === when_any Cancellation -`when_any` uses stop tokens internally to cancel "losing" tasks when the first task completes. This is covered in xref:4.coroutines/4f.composition.adoc[Concurrent Composition]. +cpp:when_any[] uses stop tokens internally to cancel "losing" tasks when the first task completes. This is covered in xref:4.coroutines/4f.composition.adoc[Concurrent Composition]. -== Reference +== The Standard Library Types -The stop token mechanism is part of the {cpp} standard library: +The stop token mechanism is part of the {cpp} standard library, not Capy: [source,cpp] ---- @@ -297,5 +294,3 @@ Key types: * `std::stop_source` — Creates and manages stop state * `std::stop_token` — Observes stop state * `std::stop_callback` — Registers callbacks for stop notification - -You have now learned how stop tokens provide cooperative cancellation for coroutines. In the next section, you will learn about concurrent composition with `when_all` and `when_any`. diff --git a/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc index b104b4078..24632d329 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc @@ -1,11 +1,11 @@ = Concurrent Composition +:page-mode: how-to -This section explains how to run multiple tasks concurrently using `when_all` and `when_any`. +This section explains how to run multiple tasks concurrently using cpp:when_all[] and cpp:when_any[]. -== Prerequisites +Headers: `` and ``. cpp:async_waker[] on this page is ``. -* Completed xref:4.coroutines/4e.cancellation.adoc[Stop Tokens and Cancellation] -* Understanding of stop token propagation +Both combinators take cpp:io_task[] children and return their results as a single cpp:io_result[]. If those two names are new, read xref:4.coroutines/4a.tasks.adoc#io-result-and-io-task[Reporting Errors] first -- everything below is expressed in them. == Overview @@ -25,7 +25,7 @@ include::example$snippets/4f_composition.cpp[tag=concurrent,indent=0] == when_all: Wait for All Tasks -`when_all` launches multiple `io_task` children concurrently and waits for all of them to complete. It returns `task>`, a single `ec` plus the flattened payloads: +cpp:when_all[] starts multiple cpp:io_task[] children concurrently and waits for all of them to complete. It returns cpp:task[task]`>`, a single `ec` plus the flattened payloads: [source,cpp] ---- @@ -34,18 +34,18 @@ include::example$snippets/4f_composition.cpp[tag=when_all_basic,indent=0] === Result Type -`when_all` returns `io_result` where each `Ri` is the child's payload flattened: `io_result` contributes `T`, `io_result<>` contributes `tuple<>`. Check `ec` first; values are only meaningful when `!ec`. +cpp:when_all[] returns cpp:io_result[io_result]`` where each `Ri` is the child's payload flattened: `io_result` contributes `T`, `io_result<>` contributes `tuple<>`. Check `ec` first; values are only meaningful when `!ec`. === Void io_tasks -`io_task<>` children contribute `tuple<>` to the result: +cpp:io_task[io_task<>] children contribute `tuple<>` to the result: [source,cpp] ---- include::example$snippets/4f_composition.cpp[tag=when_all_void_mix,indent=0] ---- -When all children are `io_task<>`, just check `r.ec`: +When all children are cpp:io_task[io_task<>], just check `r.ec`: [source,cpp] ---- @@ -54,11 +54,11 @@ include::example$snippets/4f_composition.cpp[tag=when_all_all_void,indent=0] === Error Handling -I/O errors are reported through the `ec` field of the `io_result`. When any child returns a non-zero `ec`: +I/O errors are reported through the `ec` field of the cpp:io_result[io_result]. When any child returns a non-zero `ec`: 1. Stop is requested for sibling tasks 2. All tasks complete (or respond to stop) -3. The first `ec` (in completion order, not input order) is propagated in the outer `io_result` +3. The first `ec` (in completion order, not input order) is propagated in the outer cpp:io_result[io_result] [source,cpp] ---- @@ -74,7 +74,7 @@ include::example$snippets/4f_composition.cpp[tag=when_all_exception,indent=0] === Stop Propagation -When one task fails, `when_all` requests stop for its siblings. Well-behaved tasks should check their stop token and exit promptly: +When one task fails, cpp:when_all[] requests stop for its siblings. Well-behaved tasks should check their stop token and exit promptly: [source,cpp] ---- @@ -83,26 +83,26 @@ include::example$snippets/4f_composition.cpp[tag=stop_propagation,indent=0] == when_any: First-to-Succeed Wins -`when_any` launches multiple `io_task` children concurrently and returns when the first one *succeeds* (`!ec`): +cpp:when_any[] starts multiple cpp:io_task[] children concurrently and returns when the first one *succeeds* (`!ec`): [source,cpp] ---- include::example$snippets/4f_composition.cpp[tag=when_any_basic,indent=0] ---- -The result is a `variant` with `error_code` at index 0 (failure/no winner) and one alternative per input task at indices 1..N. Only tasks returning `!ec` can win; errors and exceptions do not count as winning. When a winner is found, stop is requested for all siblings. All tasks complete before `when_any` returns. +The result is a `variant` with `error_code` at index 0 (failure/no winner) and one alternative per input task at indices 1..N. Only tasks returning `!ec` can win; errors and exceptions do not count as winning. When a winner is found, stop is requested for all siblings. All tasks complete before cpp:when_any[] returns. -When every task fails, `when_any` reports a failure, but *which* one is unspecified: the result either carries an `error_code` at index 0 or rethrows one of the children's exceptions. Unlike `when_all`, there is no priority between error codes and exceptions, and no guarantee about which task's failure surfaces (including no guarantee that it is the first or last to complete). Do not rely on receiving the failure from any particular task. +When every task fails, cpp:when_any[] reports a failure, but *which* one is unspecified. The result either carries an `error_code` at index 0 or rethrows one of the children's exceptions. Unlike cpp:when_all[], there is no priority between error codes and exceptions, and no guarantee about which task's failure surfaces. In particular, there is no guarantee that it is the first or last to complete. Do not rely on receiving the failure from any particular task. === Errors Do Not Win (wait_for_one_success) -A child that returns a non-zero `ec` (or throws) does *not* win, and it does *not* cancel its siblings. `when_any` keeps waiting until some child succeeds or until every child has finished. Only when *all* children fail does the result settle at index 0, holding an `error_code`. +A child that returns a non-zero `ec` (or throws) does *not* win, and it does *not* cancel its siblings. cpp:when_any[] keeps waiting until some child succeeds or until every child has finished. Only when *all* children fail does the result settle at index 0, holding an `error_code`. If you need "complete on the first child to *finish*, success or error," that behavior is opt-in — wrap the child as shown below. === Treating an Error as a Win -To make a child win on an error, wrap it so the error becomes a success before `when_any` sees it. +To make a child win on an error, wrap it so the error becomes a success before cpp:when_any[] sees it. The first pattern translates a specific, benign error into success. Other errors propagate unchanged, so they still do not win: @@ -138,9 +138,22 @@ Process items in parallel, then combine results using the range overload: include::example$snippets/4f_composition.cpp[tag=fan_out,indent=0] ---- +[NOTE] +==== +`process_item` takes its item by value. The tasks it returns are collected in a +vector and awaited later. A reference parameter would be read long after the +loop that bound it. + +In the snippet above nothing dangles: `items` outlives the cpp:when_all[]. The +hazard is what a caller could do -- passing a temporary would leave the stored +task holding a reference to it. Awaiting a task immediately, as `read_all` on +xref:5.buffers/5.intro.adoc[Buffer Sequences] does, is the case where a +reference parameter is safe. +==== + === Racing a Deadline -`when_any` is the tool for expressing a deadline race directly. Two waker waits, one completed by the operation itself, the other by whatever plays the clock, race as ordinary `when_any` children: +cpp:when_any[] is the tool for expressing a deadline race directly. Two waker waits, one completed by the operation itself, the other by whatever plays the clock, race as ordinary `when_any` children: [source,cpp] ---- @@ -159,7 +172,7 @@ For timed I/O against a socket, prefer Corosio's own timed operations: they sche === Task Storage -`when_all` stores all tasks in its coroutine frame. Tasks are moved from the arguments, so the original task objects become empty after the call. +cpp:when_all[] stores all tasks in its coroutine frame. Tasks are moved from the arguments, so the original task objects become empty after the call. === Completion Tracking @@ -169,27 +182,9 @@ A shared atomic counter tracks how many tasks remain. Each task completion decre Each child task is wrapped in a "runner" coroutine that: -1. Receives context (executor, stop token) from `when_all` +1. Receives context (executor, stop token) from cpp:when_all[] 2. Awaits the child task 3. Stores the result in shared state 4. Signals completion This design ensures proper context propagation to all children. - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| Concurrent composition with when_all - -| `` -| First-completion racing with when_any - -| `` -| Single-waiter notification point for deadlines and other external events -|=== - -You have now learned how to compose tasks concurrently with `when_all` and `when_any`. In the next section, you will learn about frame allocators for customizing coroutine memory allocation. diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc index 95c041e9c..94647298a 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc @@ -1,63 +1,37 @@ = Frame Allocators +:page-mode: explanation This section explains how coroutine frames are allocated and how to customize allocation for performance. -== Prerequisites - -* Completed xref:4.coroutines/4f.composition.adoc[Concurrent Composition] -* Understanding of coroutine frame allocation from xref:../2.cpp20-coroutines/2d.advanced.adoc[{cpp}20 Coroutines Tutorial] - -== The Timing Constraint - -Coroutine frame allocation has a unique constraint: memory must be allocated *before* the coroutine body begins executing. The standard {cpp} mechanism—promise type's `operator new`—is called before the promise is constructed. - -This creates a challenge: how can a coroutine use a custom allocator when the allocator might be passed as a parameter, which is stored *in* the frame? - -== Thread-Local Propagation - -Capy solves this with thread-local propagation: +== Using Custom Allocators -1. Before evaluating the task argument, `run_async` sets a thread-local allocator -2. The task's `operator new` reads this thread-local allocator -3. The task stores the allocator in its promise for child propagation +=== With run_async -This is why `run_async` uses two-call syntax: +Pass an allocator to cpp:run_async[]: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=two_call,indent=0] +include::example$snippets/4g_allocators.cpp[tag=run_async_pmr_alloc,indent=0] ---- -== The Window - -The "window" is the interval between setting the thread-local allocator and the coroutine's first suspension point. During this window: - -* The task is allocated using the TLS allocator -* The task captures the TLS allocator in its promise -* Child tasks inherit the allocator - -After the window closes (at the first suspension), the TLS allocator may be restored to a previous value. The task retains its captured allocator regardless. - -== TLS Preservation - -Between a coroutine's `await_resume` (which sets TLS to the correct allocator) and the next child coroutine invocation (whose `operator new` reads TLS), arbitrary user code runs. If that code resumes a coroutine from a different chain on the same thread -- by calling `.resume()` directly, pumping a completion queue, or running nested dispatch -- the other coroutine's `await_resume` overwrites TLS with its own allocator. The original coroutine's next child would then allocate from the wrong resource. - -To prevent this, any code that calls `.resume()` on a coroutine handle must use `safe_resume` from ``: +Or pass a `memory_resource*` directly: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=safe_resume,indent=0] +include::example$snippets/4g_allocators.cpp[tag=run_async_memory_resource,indent=0] ---- -`safe_resume` saves the current thread-local allocator, calls `h.resume()`, then restores the saved value. This makes TLS behave like a stack: nested resumes cannot spoil the outer value. All of Capy's built-in executors (`thread_pool`, strands, `blocking_context`) use `safe_resume` internally. Custom executor event loops must do the same -- see xref:8.examples/8n.custom-executor.adoc[Custom Executor] for an example. +=== Default Allocator + +When no allocator is specified, cpp:run_async[] uses the execution context's default frame allocator, typically a recycling allocator optimized for coroutine frame sizes. == Custom Allocator Requirements Custom allocators must meet the usual {cpp} allocator requirements, or be a `std::pmr::memory_resource*`. The library does not expose a separate public concept for them; a value-type allocator works as a frame allocator when it provides, illustratively: +.Illustrative requirements - not a named concept [source,cpp,role=pseudocode] ---- -// Illustrative requirements — not a named public concept: typename A::value_type; a.allocate(n) // -> A::value_type* a.deallocate(p, n); @@ -65,56 +39,35 @@ a.deallocate(p, n); In practice, any standard allocator works. -== Using Custom Allocators - -=== With run_async - -Pass an allocator to `run_async`: - -[source,cpp] ----- -include::example$snippets/4g_allocators.cpp[tag=run_async_pmr_alloc,indent=0] ----- - -Or pass a `memory_resource*` directly: - -[source,cpp] ----- -include::example$snippets/4g_allocators.cpp[tag=run_async_memory_resource,indent=0] ----- - -=== Default Allocator - -When no allocator is specified, `run_async` uses the execution context's default frame allocator, typically a recycling allocator optimized for coroutine frame sizes. - == Recycling Allocator -Capy provides `recycling_memory_resource`, a memory resource optimized for coroutine frames: +Capy provides cpp:recycling_memory_resource[], a memory resource optimized for coroutine frames: * Maintains freelists by size class * Reuses recently freed blocks (cache-friendly) * Falls back to upstream allocator for new sizes -This allocator is used by default for `thread_pool` and other execution contexts. +This allocator is used by default for cpp:thread_pool[] and other execution contexts. -NOTE: `recycling_memory_resource` honors only the default new alignment (`__STDCPP_DEFAULT_NEW_ALIGNMENT__`, typically `alignof(std::max_align_t)`). The alignment argument passed to `do_allocate`/`do_deallocate` is ignored, so over-aligned requests are not satisfied. This is sufficient for coroutine frames but means the resource is not a drop-in replacement where over-aligned allocations are required. +NOTE: cpp:recycling_memory_resource[] honors only the default new alignment (`__STDCPP_DEFAULT_NEW_ALIGNMENT__`, typically `alignof(std::max_align_t)`). The alignment argument passed to cpp:recycling_memory_resource::do_allocate[do_allocate]/cpp:recycling_memory_resource::do_deallocate[do_deallocate] is ignored, so over-aligned requests are not satisfied. This is sufficient for coroutine frames but means the resource is not a drop-in replacement where over-aligned allocations are required. -== Frame Allocator Mixin +=== Observing Reuse -Most users never need to allocate coroutine frames manually -- `task` and the built-in awaitable types already participate in TLS frame allocation. When you write your own coroutine promise type and want it to use the same fast path, inherit from `frame_alloc_mixin`: +You can watch the reuse happen. This `memory_resource` pools each freed block by size and counts how many allocations come from the heap versus a recycled block: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=frame_alloc_mixin] +include::example$snippets/4g_allocators.cpp[tag=recycling_observe_resource,indent=0] ---- -`frame_alloc_mixin` (in ``) supplies `operator new` and `operator delete` that: +Run the same task eight times through it, one run at a time: -* Read the thread-local frame allocator set by `run_async` (falling back to `std::pmr::get_default_resource()` when none is set). -* Bypass virtual dispatch when that allocator is the default recycling memory resource. -* Store the resolved allocator pointer at the tail of each frame, so deallocation uses the correct resource even if the thread-local allocator has since changed. +[source,cpp] +---- +include::example$snippets/4g_allocators.cpp[tag=recycling_observe,indent=0] +---- -This is the same strategy used internally by `io_awaitable_promise_base`. Use the mixin directly when your promise type does not need the full environment and continuation support that `io_awaitable_promise_base` provides. The allocation fast path uses thread-local storage and needs no synchronization; the global pool fallback is mutex-protected. +The first run finds an empty pool and takes its coroutine frames from the heap. Each `pool.join()` returns those frames to the resource, so the next cpp:run_async[] call reuses them. Once the pool is warm, the upstream count stops climbing while the task keeps running -- the seven warm runs allocate nothing new. cpp:recycling_memory_resource[] does the same thing with size-class freelists and a lock-free thread-local cache. That is why cpp:thread_pool[] and the other execution contexts install that resource by default. == HALO Optimization @@ -124,7 +77,7 @@ This is the same strategy used internally by `io_awaitable_promise_base`. Use th * The frame size is known at compile time * Optimization is enabled -Capy's `task` uses the `[[clang::coro_await_elidable]]` attribute (when available) to enable HALO: +Capy's cpp:task[task] uses the `+[[clang::coro_await_elidable]]+` attribute (when available) to enable HALO: [source,cpp] ---- @@ -156,7 +109,7 @@ For most applications, the default recycling allocator provides good performance === Consider Memory Resources for Batched Work -When launching many short-lived tasks together, a monotonic buffer resource can be efficient: +When starting many short-lived tasks together, a monotonic buffer resource can be efficient: [source,cpp] ---- @@ -165,9 +118,9 @@ include::example$snippets/4g_allocators.cpp[tag=batch_allocator] === Scope Variables to Reduce Frame Size -Compilers use declaration scope (braces) to decide which variables cross suspend points and must live in the coroutine frame. Variables declared in an outer scope remain in the frame even after their last use, as long as a `co_await` follows within the same scope. +Clang decides frame layout after optimization, so declaration scope controls what it keeps. A variable whose scope spans a `co_await` stays in the frame until that scope ends. -Wrapping buffer usage in explicit braces can dramatically reduce frame size: +Wrapping buffer usage in explicit braces can therefore cut frame size sharply: [source,cpp] ---- @@ -176,7 +129,7 @@ include::example$snippets/4g_allocators.cpp[tag=frame_scope_bad] include::example$snippets/4g_allocators.cpp[tag=frame_scope_good] ---- -This technique also enables the compiler to *overlap* variables in the frame. When two variables have completely non-overlapping lifetimes (in separate scoped blocks), the compiler can reuse the same frame memory for both — even on Clang: +The same technique lets Clang *overlap* two variables. When their lifetimes cannot coexist, both can occupy one frame offset: [source,cpp] ---- @@ -185,7 +138,31 @@ include::example$snippets/4g_allocators.cpp[tag=pipeline_overlap_bad] include::example$snippets/4g_allocators.cpp[tag=pipeline_overlap_good] ---- -In the second version, `read_buf` and `write_buf` never coexist, so the compiler can place them at the same frame offset — halving the frame's buffer footprint. This optimization applies to any variables with non-overlapping lifetimes, not just arrays. +In the second version, `read_buf` and `write_buf` never coexist, so they can share storage. This applies to any variables with non-overlapping lifetimes, not just arrays. + +[IMPORTANT] +==== +*Both techniques are Clang optimizations, and both need `-O1` or higher.* GCC sizes the frame in its frontend, so neither one changes anything there. + +Frame bytes for the four coroutines above, reported by the promise's `operator new` with 4 KB buffers: + +[cols="2,1,1,1"] +|=== +| Pattern | Clang 22 `-O2` | Clang 22 `-O0` | GCC 16 `-O0` and `-O2` + +| Scoping: outer buffer, then braced +| 4128 -> *32* +| 4128 -> 4128 +| 4136 -> 4136 + +| Overlap: shared scope, then separate scopes +| 8224 -> *4128* +| 8224 -> 8224 +| 8224 -> 8224 +|=== + +On GCC, scoping does not shrink the frame. Keep large buffers out of the coroutine instead: pass them in by reference, or allocate them outside the frame. +==== === GCC vs Clang Frame Sizes @@ -196,28 +173,101 @@ GCC and Clang use fundamentally different strategies for coroutine frame layout: * **Clang** performs frame layout after middle-end optimizations. Dead variables, unused temporaries, and constant-folded intermediates are eliminated before the frame is sized. * **GCC** performs frame layout in the frontend, before optimizations. Every local variable whose scope spans a suspend point ends up in the frame, even if optimizations would later prove it dead. -The practical consequence is that GCC coroutine frames are often 5-10x larger than Clang's for the same source code. In one benchmark, the same coroutine produced a 24-byte frame on Clang and a 16,032-byte frame on GCC. +How large the gap gets depends on how much the optimizer can discard. Without optimization the two are comparable, as the table above shows. With optimization enabled, Clang's frames can be smaller by an order of magnitude or more. The cited article reports a coroutine whose frame was 24 bytes on Clang against 16,032 bytes on GCC. -For production coroutine workloads, Clang currently produces substantially better code. If you must use GCC, pay extra attention to variable scoping (above) and consider supplying a custom `memory_resource` with larger block sizes, since frames above 2048 bytes bypass the default recycling allocator's pooling. +This also explains why the scoping techniques above are Clang-only. GCC has already sized the frame before the optimizer could prove a variable dead. + +For production coroutine workloads, Clang currently produces substantially better code. On GCC, do not rely on scoping; keep large buffers outside the frame, and consider a custom `memory_resource` with larger block sizes. Frames above 2048 bytes bypass the default recycling allocator's pooling. === Profile Before Optimizing Coroutine frame allocation is rarely the bottleneck. Profile your application before investing in custom allocators. -== Reference -[cols="1,3"] -|=== -| Header | Description +== How Frame Allocation Works -| `` -| Frame allocator concept and utilities +The three sections below are the mechanism behind everything above. You do not need them to use a custom allocator, but you do need them to write an executor or a promise type. -| `` -| Mixin base for promise types that use the TLS frame allocator +[#timing-constraint] +=== The Timing Constraint -| `` -| Default recycling allocator implementation -|=== +Coroutine frame allocation has a unique constraint: memory must be allocated *before* the coroutine body begins executing. The standard {cpp} mechanism—promise type's `operator new`—is called before the promise is constructed. + +This creates a challenge. How can a coroutine use a custom allocator when the allocator might be passed as a parameter, which is stored *in* the frame? + +[#two-call-rationale] +=== Thread-Local Propagation + +Capy solves this with thread-local propagation: + +1. Before evaluating the task argument, cpp:run_async[] sets a thread-local allocator +2. The task's `operator new` reads this thread-local allocator +3. The task stores the allocator in its promise for child propagation + +This is why cpp:run_async[] uses two-call syntax: + +[source,cpp] +---- +include::example$snippets/4g_allocators.cpp[tag=two_call,indent=0] +---- + +Three patterns split those two calls apart, and two of them do it without any +diagnostic. xref:4.coroutines/4b.launching.adoc[Starting Coroutines] lists all +three. + +=== The Window + +The "window" is the interval between setting the thread-local allocator and the coroutine's first suspension point. During this window: + +* The task is allocated using the TLS allocator +* The task captures the TLS allocator in its promise +* Child tasks inherit the allocator + +After the window closes (at the first suspension), the TLS allocator may be restored to a previous value. The task retains its captured allocator regardless. + +=== TLS Preservation + +Between a coroutine's `await_resume` (which sets TLS to the correct allocator) and the next child coroutine invocation (whose `operator new` reads TLS), arbitrary user code runs. That code can call `.resume()` directly, pump a completion queue, or run nested dispatch. If it resumes a coroutine from a different chain on the same thread, the other coroutine's `await_resume` overwrites TLS with its own allocator. The original coroutine's next child would then allocate from the wrong resource. + +To prevent this, any code that calls `.resume()` on a coroutine handle must use cpp:safe_resume[] from ``: + +[source,cpp] +---- +include::example$snippets/4g_allocators.cpp[tag=safe_resume,indent=0] +---- + +cpp:safe_resume[] saves the current thread-local allocator, calls `h.resume()`, then restores the saved value. This makes TLS behave like a stack: nested resumes cannot spoil the outer value. All of Capy's built-in executors (cpp:thread_pool[], strands, cpp:test::blocking_context[blocking_context]) use `safe_resume` internally. Custom executor event loops must do the same -- see xref:8.examples/8n.custom-executor.adoc[Custom Executor] for an example. + +[NOTE] +==== +cpp:safe_resume[]'s implementation: + +[source,cpp] +---- +include::example$snippets/9k_executor.cpp[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. + +Two `.resume()` call sites intentionally do _not_ use cpp:safe_resume[]: + +* **`symmetric_transfer`** (MSVC workaround). The calling coroutine is about to suspend unconditionally. When it later resumes, `await_resume` restores TLS from the promise's stored environment. Save/restore would add overhead with no benefit. +* **`run_async_wrapper::operator()`**. TLS is already saved in the wrapper's constructor and restored in its destructor, which bracket the entire task lifetime. +==== + +== Frame Allocator Mixin + +Most users never need to allocate coroutine frames manually -- cpp:task[task] and the built-in awaitable types already participate in TLS frame allocation. When you write your own coroutine promise type and want it to use the same fast path, inherit from cpp:frame_alloc_mixin[]: + +[source,cpp] +---- +include::example$snippets/4g_allocators.cpp[tag=frame_alloc_mixin] +---- + +cpp:frame_alloc_mixin[] (in ``) supplies `operator new` and `operator delete` that: + +* Read the thread-local frame allocator set by cpp:run_async[] (falling back to `std::pmr::get_default_resource()` when none is set). +* Bypass virtual dispatch when that allocator is the default recycling memory resource. +* Store the resolved allocator pointer at the tail of each frame, so deallocation uses the correct resource even if the thread-local allocator has since changed. -You have now learned how coroutine frame allocation works and how to customize it. Continue to xref:4.coroutines/4h.lambda-captures.adoc[Lambda Coroutine Captures] to learn about a critical pitfall with lambda coroutines. +This is the same strategy used internally by cpp:io_awaitable_promise_base[]. Use the mixin directly when your promise type does not need the full environment and continuation support that `io_awaitable_promise_base` provides. The allocation fast path uses thread-local storage and needs no synchronization; the global pool fallback is mutex-protected. diff --git a/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc b/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc index 70847320b..1a5ce5c4b 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc @@ -1,15 +1,11 @@ = Lambda Coroutine Captures +:page-mode: how-to Lambda captures are a common source of undefined behavior in coroutine code. This section explains the problem and the safe patterns to use instead. -== Prerequisites - -* Completed xref:4.coroutines/4g.allocators.adoc[Frame Allocators] -* Understanding of coroutine frame lifetime from xref:../2.cpp20-coroutines/2c.machinery.adoc[Part III: Coroutine Machinery] - == The Problem -Consider this innocent-looking code: +Consider this innocent-looking code. The lambda is defined and called in one expression -- the `()` after the closing brace -- so `started` holds the task, not the lambda. That shape is an _immediately invoked function expression_, or IIFE; it matters again below. [source,cpp] ---- @@ -24,7 +20,7 @@ In {cpp}20, lambda coroutine captures are stored in the lambda closure object, * 1. The lambda closure is created, capturing `sock` by reference 2. The lambda's `operator()()` is called -3. A coroutine frame is allocated on the heap +3. A coroutine frame is allocated (typically on the heap; HALO may elide this) 4. The coroutine suspends at `initial_suspend` 5. `operator()()` returns the task 6. **The lambda closure is destroyed** — it was a temporary @@ -97,25 +93,25 @@ Member function coroutines work correctly because `this` is an implicit paramete |=== | Pattern | Safety | Notes -| `[x]() -> task<> { use(x); }()` -| UNSAFE -| Capture `x` destroyed with lambda - -| `[](auto x) -> task<> { use(x); }(val)` +| `[](auto x) ->` cpp:task[task]`<> { use(x); }(val)` | SAFE | Parameter `x` in coroutine frame -| `[&x]() -> task<> { use(x); }()` -| UNSAFE -| Accessed through dangling `this` pointer to destroyed closure +| Member function coroutine +| SAFE +| `this` is an implicit parameter -| `[](auto& x) -> task<> { use(x); }(val)` +| `[](auto& x) ->` cpp:task[task]`<> { use(x); }(val)` | SAFE* | Reference parameter; `val` must outlive coroutine -| Member function coroutine -| SAFE -| `this` is an implicit parameter +| `[x]() ->` cpp:task[task]`<> { use(x); }()` +| UNSAFE +| Capture `x` destroyed with lambda + +| `[&x]() ->` cpp:task[task]`<> { use(x); }()` +| UNSAFE +| Accessed through dangling `this` pointer to destroyed closure |=== == Why Does {cpp} Work This Way? @@ -128,7 +124,3 @@ The {cpp} standard specifies that coroutine parameters are copied to the corouti * The closure is external to the function body There have been proposals to change this behavior, but as of {cpp}23 the issue remains. - -== Next Steps - -You have now learned the major pitfalls of lambda coroutines. This completes the Coroutines in Capy section. Continue to xref:../5.buffers/5a.overview.adoc[Buffer Sequences] to learn about Capy's buffer model. diff --git a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc index 34fe1e7c5..fdfed5a36 100644 --- a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5.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) @@ -8,11 +9,440 @@ // = Buffer Sequences +:page-mode: explanation +:page-aliases: 5.buffers/5a.overview.adoc, 5.buffers/5b.types.adoc, 5.buffers/5c.sequences.adoc, 5.buffers/5d.system-io.adoc, 5.buffers/5e.algorithms.adoc -Every I/O operation ultimately comes down to moving bytes between your program and the outside world--a socket, a file, a pipe. The question is: how do you describe where those bytes live in memory? +Every I/O operation comes down to moving bytes between your program and the outside world. The question is how you describe where those bytes live. -The obvious answer is a pointer and a size. And for a single contiguous buffer, that works. But real I/O is rarely that tidy. An HTTP response has headers in one buffer and a body in another. A message might be assembled from a protocol header, a payload, and a checksum--each produced by different parts of your code, each sitting in its own memory. The operating system even supports scatter/gather I/O specifically to handle this: a single system call that reads into or writes from _multiple_ non-contiguous buffers. +A pointer and a size answers it for one contiguous region. Real I/O is rarely that tidy. An HTTP response has headers in one buffer and a body in another. A message may be a protocol header, a payload, and a checksum, each produced by different code and each in its own memory. -Capy's buffer model is designed for this reality. Instead of forcing you to copy data into a single contiguous allocation, Capy uses _buffer sequences_--lightweight, zero-copy abstractions that let you describe any arrangement of memory and pass it directly to the OS. The design is concept-driven, meaning the compiler verifies correctness at compile time with no runtime overhead. +Capy describes any such arrangement without copying it, using _buffer sequences_. This page covers the two buffer types and the sequence concepts built on them. It then shows how both reach the operating system, and the algorithms that measure and copy them. -This section covers everything you need to work with memory in Capy's I/O model. You will learn the fundamental buffer types, how to compose them into sequences for scatter/gather I/O, and how they map to operating system primitives. You will also meet the algorithms that manipulate buffer data and the dynamic buffer abstractions that grow as data arrives. Understanding buffers is essential for everything that follows--streams, I/O operations, and protocol implementations all build on the abstractions introduced here. +Everything here lives in ``. + +== Buffer Types + +=== Buffers Are Handles + +A cpp:const_buffer[] or cpp:mutable_buffer[] is a *handle*: a non-owning `(pointer, size)` view of memory it does not own. Constructing one copies no bytes, and destroying one frees nothing. + +This splits lifetime responsibility cleanly: + +* *You own the bytes.* The memory a buffer refers to--a stack array, a `std::string`, a slab from your allocator--is yours to keep alive. It must stay valid for the whole duration of any operation you hand the buffer to. That includes the suspension points of a `co_await`-ed I/O operation. +* *The library owns the handles.* Capy creates and manages buffer handles on your behalf. Those are the sub-range a cpp:buffer_slice[] produces, and the descriptors a type-erased stream passes to the OS. Each is valid only for the window its API documents. + +The library never copies or takes ownership of your bytes through a buffer. It only moves handles. This split explains every buffer-lifetime rule below. + +=== const_buffer + +cpp:const_buffer[] represents a contiguous region of read-only memory. + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=const_buffer_construct,indent=0] +---- + +Accessors: + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=const_buffer_accessors,indent=0] +---- + +The `+=` operator removes bytes from the front, which is useful when processing a buffer incrementally: + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=const_buffer_prefix,indent=0] +---- + +=== mutable_buffer + +cpp:mutable_buffer[] represents a contiguous region of writable memory. The interface mirrors cpp:const_buffer[], but `data()` returns non-const `void*`. + +A cpp:mutable_buffer[] converts implicitly to a cpp:const_buffer[]: + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=mutable_to_const,indent=0] +---- + +The reverse is not allowed. + +=== make_buffer + +cpp:make_buffer[] creates buffers from various sources: + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=make_buffer_include] + +include::example$snippets/5b_types.cpp[tag=make_buffer_sources,indent=0] +---- + +It accepts any sized, contiguous range of trivially-copyable elements, including `std::span` and `boost::span`. The returned type follows the element constness: + +* Ranges of mutable elements -> cpp:mutable_buffer[] +* Ranges of const elements, `string_view`, string literals -> cpp:const_buffer[] + +The buffer's size in bytes is `count * sizeof(element)`. + +[NOTE] +==== +*Why `void*` rather than `std::byte`?* + +Two concrete forces decide it: + +* *The platform types already use it.* The OS structures Capy maps onto are `void*` or `char*`: `iovec`'s `iov_base`, `WSABUF`'s `buf`. The pointer field never needs reinterpreting. +* *Callers supply many element types.* Data arrives as `char[]`, `unsigned char[]`, `std::byte[]`, `std::string`, and more. One neutral pointer erases all of them to a single representation. `std::span` would make every caller reinterpret first, and `std::span` is ill-formed. + +This is an argument from layout and from caller convenience, not from semantics. "Raw memory" describes `std::byte` just as well as it describes `void*`. +==== + +== Buffer Sequences + +A *buffer sequence* is any type that can produce an iteration of buffers: + +* A single buffer is a sequence of one element +* A range of buffers, such as `vector`, is a multi-element sequence +* Any bidirectional range with buffer-convertible values qualifies + +Treating a single buffer as a one-element sequence is deliberate. It lets one concept-constrained signature serve both the common single-buffer call and scatter/gather composition. There is no overload and no explicit wrap at the call site. + +=== The Concepts + +[source,cpp] +---- +include::example$snippets/5c_sequences.cpp[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] +---- + +cpp:MutableBufferSequence[] follows the same pattern for cpp:mutable_buffer[]. + +Many common types satisfy these concepts: + +[source,cpp] +---- +include::example$snippets/5c_sequences.cpp[tag=concept_models,indent=0] +---- + +`std::string` and `std::string_view` are ranges of characters, not of buffers, so they do not satisfy the concepts themselves. Wrap them with cpp:make_buffer[]. + +=== Heterogeneous Composition + +Because the concept accepts anything convertible to a buffer, you can mix types freely: + +[source,cpp] +---- +include::example$snippets/5c_sequences.cpp[tag=send_signature,indent=0] + +include::example$snippets/5c_sequences.cpp[tag=send_calls,indent=0] +---- + +A single buffer works in the same signature, with no wrapping: + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=write_data_signature,indent=0] + +include::example$snippets/5b_types.cpp[tag=write_data_calls,indent=0] +---- + +[NOTE] +==== +*Why concepts rather than one span type?* + +`std::span>` is the reflexive answer for multiple buffers, and it works. The problem is composition. Joining a two-buffer header to a three-buffer body means building a new five-element array, so every composition allocates: + +[source,cpp] +---- +include::example$snippets/5a_overview.cpp[tag=span_combine,indent=0] +---- + +A concept accepts the composite directly, with no allocation and no overload per shape. + +This is the same move the STL makes: parameterize on a concept, not on a concrete container. The parallel is loose -- STL algorithms take iterator pairs, and these concepts take ranges over a much narrower domain. The shared point is only that a concept composes where a concrete type does not. + +At type-erasure boundaries the tradeoff reverses. Virtual functions need concrete types, so Capy converts to concrete descriptors internally and keeps concepts at the user-facing edge. +==== + +=== Iterating Buffer Sequences + +Use `begin()` and `end()` from ``: + +[source,cpp] +---- +include::example$snippets/5c_sequences.cpp[tag=iterate,indent=0] +---- + +These handle both single buffers, returning pointer-to-self, and ranges, returning standard iterators: + +[source,cpp] +---- +include::example$snippets/5b_types.cpp[tag=begin_end_uniform,indent=0] +---- + +=== buffer_slice + +cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: + +[source,cpp] +---- +include::example$snippets/5c_sequences.cpp[tag=buffer_slice_include] + +include::example$snippets/5c_sequences.cpp[tag=buffer_slice,indent=0] +---- + +cpp:buffer_slice[]`(seq, offset, length)` returns a value that is itself a buffer sequence, so you can pass it to any operation expecting one. Both `offset` and `length` are optional, which makes it a general byte sub-range primitive. Except in the single-buffer case the result borrows `seq`, so the sequence must outlive the slice. + +=== consuming_buffers + +When transferring data incrementally, cpp:consuming_buffers[] is a cursor that tracks progress: + +[source,cpp] +---- +include::example$snippets/5c_sequences.cpp[tag=consuming_buffers_include] + +include::example$snippets/5c_sequences.cpp[tag=read_all,indent=0] +---- + +The cursor borrows the underlying sequence and provides: + +* cpp:consuming_buffers::data[data]`()` -- buffer sequence view of the remaining bytes +* cpp:consuming_buffers::consume[consume]`(n)` -- advance past `n` transferred bytes, in place + +[NOTE] +==== +`read_all` takes its stream by reference and its buffers by value. Both are safe +here, because the caller owns the stream and awaits the task immediately. + +A coroutine reads its parameters when its body runs, not when the call is +written. A task that is stored and awaited later outlives its call expression, +so a reference parameter can dangle by then. That is why the fan-out example on +xref:4.coroutines/4f.composition.adoc[Concurrent Composition] takes its item by +value: it collects its tasks first, then awaits them together. +==== + +=== Why Bidirectional? + +The concepts require bidirectional ranges, not merely forward ranges, for two reasons: + +1. Some algorithms traverse buffers backwards +2. The slice views from cpp:buffer_slice[] and cpp:consuming_buffers::data[]`()` must adjust the first and last buffers' bounds + +If your custom sequence offers only forward iteration, wrap it in a type that provides bidirectional access. + +== System I/O Integration + +=== Platform Buffer Structures + +[source,c,role=external] +---- +struct iovec { + void* iov_base; // Pointer to data + size_t iov_len; // Length of data +}; +---- + +POSIX uses `iovec` with `readv()`, `writev()`, `recvmsg()`, and `sendmsg()`. Capy's buffer types place the pointer first and the size second, matching `iovec` in both order and width. Filling an `iovec` is therefore a field-for-field copy, with no conversion. + +[WARNING] +==== +*Matching layout does not license a cast.* Do not `reinterpret_cast` an array of cpp:mutable_buffer[] or cpp:const_buffer[] to `iovec*` and hand it to the OS, even though the fields line up. No object of the target type exists in that storage, so the access is undefined behavior. These are not implicit-lifetime types either, so `std::start_lifetime_as_array` cannot rescue it. + +Copy field by field into a real platform array, which is what Capy does internally. +==== + +[source,c,role=external] +---- +typedef struct _WSABUF { + ULONG len; // Length (note: first!) + CHAR* buf; // Pointer +} WSABUF; +---- + +Windows uses `WSABUF` with `WSARecv()` and `WSASend()`. The field order is reversed and the length is 32-bit, so Capy copies descriptors into a `WSABUF` array rather than casting. + +=== Translation Process + +When you call an I/O function with a buffer sequence: + +[source,cpp] +---- +include::example$snippets/5d_system_io.cpp[tag=write_some_signature] +---- + +Capy counts the buffers, fills an array of platform structures with the descriptors, calls the OS function, and returns the result. + +Conversion always happens on the stack; the implementation never allocates. A fixed on-frame window of 16 descriptors is filled from the sequence and passed to the OS call. If the sequence holds more buffers than fit, the window is refilled and the call repeated: + +.Pseudocode - internal implementation +[source,cpp,role=pseudocode] +---- +template +auto platform_write(Buffers const& buffers) +{ + iovec iovecs[16]; // fixed on-frame window, never heap-allocated + + auto it = begin(buffers); + auto last = end(buffers); + while (it != last) + { + std::size_t count = fill_iovecs(iovecs, it, last, 16); // up to 16 + auto result = writev(fd, iovecs, count); + // ... advance the window past the buffers just written + } +} +---- + +The window size is implementation-defined. There is no heap fallback. + +=== Why Vectored I/O + +Consider sending an HTTP message whose headers and body sit in separate buffers. With a single-buffer API you have two options, and each costs something: + +* *Copy.* Allocate a buffer large enough for both, copy the headers in, copy the body after them, then send once. That is an allocation plus two copies of data you already have. +* *Call twice.* Send the headers, then send the body. No copy, but two system calls rather than one. On a datagram socket it also changes the result: two datagrams instead of one. + +Vectored I/O avoids both. One call transfers several non-contiguous buffers as a single logical operation: + +[source,cpp] +---- +include::example$snippets/5d_system_io.cpp[tag=two_syscalls,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5d_system_io.cpp[tag=gather_syscall,indent=0] +---- + +The data is never copied into a contiguous staging buffer; the OS reads directly from each region. The write is also atomic at the file offset level, so other processes see all of the data or none of it. + +=== Registered Buffers + +Some platforms allow buffers to be pre-registered with the kernel, removing per-operation address translation. On Linux 5.1+, io_uring supports this: + +[source,cpp] +---- +include::example$snippets/5d_system_io.cpp[tag=io_uring_fixed,indent=0] +---- + +Windows IOCP offers a comparable optimization with pre-registered memory regions. + +Corosio does not currently expose either. Every operation goes through the per-call translation described above. + +=== Writing Efficient Code + +Fewer buffers means less translation overhead: + +[source,cpp] +---- +include::example$snippets/5d_system_io.cpp[tag=minimize_buffer_count,indent=0] +---- + +For repeated I/O with the same structure, consider caching the platform array: + +[source,cpp] +---- +include::example$snippets/5d_system_io.cpp[tag=cached_iovecs] +---- + +Buffer translation is rarely the bottleneck. Profile network latency, disk time, and your own processing before optimizing descriptor copying. + +== Buffer Algorithms + +=== Measuring + +cpp:buffer_size[] returns the total number of *bytes* across every buffer in a sequence: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=buffer_size_example,indent=0] +---- + +cpp:buffer_length[] returns the *number of buffers* in the sequence: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=buffer_length_example,indent=0] +---- + +[IMPORTANT] +==== +These two names are easy to confuse, so read them as answering different questions: + +* cpp:buffer_size[] -- how many bytes of data? This is what I/O operations care about. +* cpp:buffer_length[] -- how many buffers hold it? This is the element count of the sequence, the analogue of `std::ranges::size`. + +A three-buffer sequence of 100 bytes each has a cpp:buffer_size[] of 300 and a cpp:buffer_length[] of 3. You want cpp:buffer_size[] far more often. +==== + +cpp:buffer_empty[] reports whether a sequence carries no data, either because it holds no buffers or because every buffer has size zero: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=buffer_empty_example,indent=0] +---- + +=== Copying + +cpp:buffer_copy[] copies data from one buffer sequence to another and returns the number of bytes copied: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_example,indent=0] +---- + +Its `at_most` parameter caps the transfer, which is useful for protocols with size limits: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_at_most,indent=0] +---- + +Source and target need not have the same shape: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_cross,indent=0] +---- + +The algorithm fills target buffers in order, reading from source buffers as needed. It handles a source buffer spanning several targets, and the reverse. + +=== Partial Transfer Loops + +Real transfers move some of the bytes, not all of them. Drive the loop with a cpp:consuming_buffers[] cursor, described under <<_consuming_buffers,consuming_buffers>> above. + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=read_loop] +---- + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=write_loop] +---- + +=== Custom Buffer Types + +Any memory region can be a buffer, including a memory-mapped one: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=mmap_buffer,indent=0] +---- + +You can also define your own type satisfying the concepts: + +[source,cpp] +---- +include::example$snippets/5e_algorithms.cpp[tag=custom_sequence] +---- + +Here `chunk_iterator` is a small bidirectional iterator whose `operator*` returns each chunk as a cpp:const_buffer[]. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc b/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc deleted file mode 100644 index e55518528..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc +++ /dev/null @@ -1,131 +0,0 @@ -= Why Concepts, Not Spans - -This section explains why Capy uses concept-driven buffer sequences instead of `std::span`, and why this design enables composition without allocation. - -== Prerequisites - -* Basic {cpp} experience with memory and pointers -* Familiarity with {cpp}20 concepts - -== The I/O Use Case - -Buffers exist to interface with operating system I/O. When you read from a socket, write to a file, or transfer data through any I/O channel, you work with contiguous memory regions—addresses and byte counts. - -The fundamental unit is a `(pointer, size)` pair. The OS reads bytes from or writes bytes to linear addresses. - -== The Reflexive Answer: span - -The instinctive {cpp} answer to "how should I represent a buffer?" is `std::span`: - -[source,cpp] ----- -include::example$snippets/5a_overview.cpp[tag=span_signatures,indent=0] ----- - -This works for single contiguous buffers. But I/O often involves multiple buffers—a technique called *scatter/gather I/O*. - -== Scatter/Gather I/O - -Consider assembling an HTTP message. The headers are in one buffer; the body is in another. With single-buffer APIs, you must: - -1. Allocate a new buffer large enough for both -2. Copy headers into the new buffer -3. Copy body after headers -4. Send the combined buffer - -This is wasteful. The data already exists—why copy it? - -Scatter/gather I/O solves this. Operating systems provide vectored I/O calls (`writev` on POSIX, scatter/gather with IOCP on Windows) that accept multiple buffers and transfer them as a single logical operation. - -== The Span Reflex for Multiple Buffers - -Extending the span reflex: `std::span>`: - -[source,cpp] ----- -include::example$snippets/5a_overview.cpp[tag=span_of_spans,indent=0] ----- - -This works, but introduces a composition problem. - -== The Composition Problem - -Suppose you have: - -[source,cpp] ----- -include::example$snippets/5a_overview.cpp[tag=span_aliases,indent=0] ----- - -To send headers followed by body, you need 5 buffers total. With `span>`: - -[source,cpp] ----- -include::example$snippets/5a_overview.cpp[tag=span_combine,indent=0] ----- - -Every composition allocates. This leads to: - -* Overload proliferation—separate functions for single buffer, multiple buffers, common cases -* Performance overhead—allocation on every composition -* Boilerplate—manual copying everywhere - -== The Concept-Driven Alternative - -Instead of concrete types, use concepts. Define `ConstBufferSequence` as "any type that can produce a sequence of buffers": - -[source,cpp] ----- -include::example$snippets/5a_overview.cpp[tag=concept_signature,indent=0] ----- - -This single signature accepts: - -* A single `const_buffer` -* A `span` -* A `vector` -* A `string_view` wrapped with `make_buffer` (which yields a single `const_buffer`) -* A custom composite type -* *Any composition of the above—without allocation* - -== STL Parallel - -This design follows Stepanov's insight from the STL: algorithms parameterized on concepts (iterators), not concrete types (containers), enable composition that concrete types forbid. - -The span reflex is a regression from thirty years of generic programming. Concepts restore the compositional power that concrete types lack. - -== The Middle Ground - -Concepts provide flexibility at user-facing APIs. But at type-erasure boundaries—virtual functions, library boundaries—concrete types are necessary. - -Capy's approach: - -* *User-facing APIs* — Accept concepts for maximum flexibility -* *Type-erasure boundaries* — Use concrete spans internally -* *Library handles conversion* — Users get concepts; implementation uses spans - -This gives users the composition benefits of concepts while hiding the concrete types needed for virtual dispatch. - -== Why Not std::byte? - -Even `std::byte` imposes a semantic opinion. POSIX uses `void*` for semantic neutrality—"raw memory, I move bytes without opining on contents." - -But `span` doesn't compile—{cpp} can't express type-agnostic buffer abstraction with `span`. - -Capy provides `const_buffer` and `mutable_buffer` as semantically neutral buffer types. They have known layout compatible with OS structures (`iovec`, `WSABUF`) without imposing `std::byte` semantics. - -== Summary - -The reflexive `span>` approach: - -* Forces allocation on every composition -* Leads to overload proliferation -* Loses the compositional power of generic programming - -The concept-driven approach: - -* Enables zero-allocation composition -* Provides a single signature that accepts anything buffer-like -* Follows proven STL design principles - -Continue to xref:5.buffers/5b.types.adoc[Buffer Types] to learn about `const_buffer` and `mutable_buffer`. diff --git a/doc/modules/ROOT/pages/5.buffers/5b.types.adoc b/doc/modules/ROOT/pages/5.buffers/5b.types.adoc deleted file mode 100644 index ded6be77a..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5b.types.adoc +++ /dev/null @@ -1,152 +0,0 @@ -= Buffer Types - -This section introduces Capy's fundamental buffer types: `const_buffer` and `mutable_buffer`. - -== Prerequisites - -* Completed xref:5.buffers/5a.overview.adoc[Why Concepts, Not Spans] -* Understanding of why concept-driven buffers enable composition - -== Buffers Are Handles - -A `const_buffer` or `mutable_buffer` is a *handle*: a non-owning `(pointer, size)` view of memory it does not own. Constructing one copies no bytes, and destroying one frees nothing. - -This splits lifetime responsibility cleanly: - -* *You own the bytes.* The memory a buffer refers to—a stack array, a `std::string`, a slab from your allocator—is yours to keep alive. It must remain valid for the entire duration of any operation you hand the buffer to, including across the suspension points of a `co_await`-ed I/O operation. -* *The library owns the handles.* Capy creates and manages buffer handles and handle-sequences on your behalf—the buffers a dynamic buffer exposes through `prepare`/`data`, the sub-range a `buffer_slice` produces, the descriptors a type-erased stream passes to the OS. Each such handle is valid only for the window its API documents, typically until the next call that mutates the owner. - -The library never copies or takes ownership of your bytes through a buffer; it only moves handles. This split explains every buffer-lifetime rule in this chapter. - -== Why `void*`, Not `std::byte`? - -`std::byte` imposes a semantic opinion. It says "this is raw bytes"—but that is itself an opinion about the data's nature. - -POSIX uses `void*` for buffers. This expresses semantic neutrality: "I move memory without opining on what it contains." The OS doesn't care if the bytes represent text, integers, or compressed data—it moves them. - -Two concrete forces favor `void*` specifically over `std::span`: - -* *Platform types already use it.* The OS structures Capy maps onto—`iovec`'s `iov_base`, `WSABUF`'s `buf`—are `void*`/`char*`. Erasing to `void*` makes conversion to those structures a layout match rather than a reinterpretation. -* *Callers supply many element types.* User data arrives as `char[]`, `unsigned char[]`, `std::byte[]`, `std::string`, and more. A single neutral pointer erases all of them to one representation. `std::span` would force every caller to reinterpret their bytes first, and `std::span` is ill-formed—{cpp} cannot express a type-agnostic buffer with `span`. - -Capy provides `const_buffer` and `mutable_buffer` as semantically neutral buffer types with known layout. - -== const_buffer - -`const_buffer` represents a contiguous region of read-only memory: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_interface,indent=0] ----- - -=== Construction - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_construct,indent=0] ----- - -=== Accessors - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_accessors,indent=0] ----- - -=== Prefix Removal - -The `+=` operator removes bytes from the front of the buffer: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_prefix,indent=0] ----- - -This is useful when processing a buffer incrementally. - -== mutable_buffer - -`mutable_buffer` represents a contiguous region of writable memory: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=mutable_buffer_interface,indent=0] ----- - -The interface mirrors `const_buffer`, but `data()` returns non-const `void*`. - -=== Conversion - -`mutable_buffer` implicitly converts to `const_buffer`: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=mutable_to_const,indent=0] ----- - -The reverse is not allowed—you cannot implicitly convert `const_buffer` to `mutable_buffer`. - -== make_buffer - -The `make_buffer` function creates buffers from various sources: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=make_buffer_include] - -include::example$snippets/5b_types.cpp[tag=make_buffer_sources,indent=0] ----- - -`make_buffer` accepts any sized, contiguous range of trivially-copyable -elements—including `std::span` and `boost::span`—in addition to the -sources shown above. - -The returned buffer type depends on the element constness of the range: - -* Ranges of mutable elements → `mutable_buffer` -* Ranges of const elements, `string_view`, string literals → `const_buffer` - -The buffer's size, in bytes, is `count * sizeof(element)`. - -== Layout Compatibility - -`const_buffer` and `mutable_buffer` have the same memory layout as OS buffer structures: - -* POSIX: `struct iovec { void* iov_base; size_t iov_len; }` -* Windows: `struct WSABUF { ULONG len; CHAR* buf; }` (note: different order) - -This means conversion to OS structures is efficient—often just a reinterpret_cast for arrays of buffers. - -== Single Buffers as Sequences - -A single buffer is a degenerate sequence—a sequence with one element. The `ConstBufferSequence` and `MutableBufferSequence` concepts recognize this: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=write_data_signature,indent=0] - -include::example$snippets/5b_types.cpp[tag=write_data_calls,indent=0] ----- - -The library provides `begin()` and `end()` functions that work uniformly: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=begin_end_uniform,indent=0] ----- - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| Core buffer types and concepts - -| `` -| Buffer creation utilities -|=== - -You have now learned about `const_buffer` and `mutable_buffer`. Continue to xref:5.buffers/5c.sequences.adoc[Buffer Sequences] to understand how these types compose into sequences. diff --git a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc deleted file mode 100644 index 369122465..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc +++ /dev/null @@ -1,132 +0,0 @@ -= Buffer Sequences - -This section explains buffer sequences—the concept that enables zero-allocation composition of buffers. - -== Prerequisites - -* Completed xref:5.buffers/5b.types.adoc[Buffer Types] -* Understanding of `const_buffer` and `mutable_buffer` - -== What Is a Buffer Sequence? - -A *buffer sequence* is any type that can produce an iteration of buffers. Formally: - -* A single buffer (like `const_buffer`) is a sequence of one element -* A range of buffers (like `vector`) is a multi-element sequence -* Any bidirectional range with buffer-convertible values qualifies - -Treating a single buffer as a one-element sequence is a deliberate convenience, not an accident of the definition. It lets one concept-constrained signature serve both the common single-buffer call and scatter/gather composition, with no overload and no explicit wrap at the call site. Capy favors this convenience as a primary design goal and applies it consistently—`make_buffer`, for instance, accepts any contiguous range of bytes—so that buffer-passing reads the same whether you hand over one region or many. - -== The Concepts - -=== ConstBufferSequence - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=const_buffer_sequence_concept,indent=0] ----- - -A type satisfies `ConstBufferSequence` if: - -* It converts to `const_buffer` directly (single buffer), OR -* It is a bidirectional range whose elements convert to `const_buffer` - -=== MutableBufferSequence - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=mutable_buffer_sequence_concept,indent=0] ----- - -Same pattern, but for `mutable_buffer`. - -== Satisfying the Concepts - -Many common types satisfy these concepts: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=concept_models,indent=0] ----- - -Note that `std::string` and `std::string_view` are ranges of characters, -not of buffers, so they do not satisfy the concepts themselves; wrap them -with `make_buffer` to obtain a single-buffer sequence. - -== Heterogeneous Composition - -Because the concept accepts anything convertible to buffer, you can mix types: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=send_signature,indent=0] - -include::example$snippets/5c_sequences.cpp[tag=send_calls,indent=0] ----- - -== Iterating Buffer Sequences - -Use `begin()` and `end()` from ``: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=iterate,indent=0] ----- - -These functions handle both single buffers (returning pointer-to-self) and ranges (returning standard iterators). - -== buffer_slice - -`buffer_slice` returns a byte sub-range of a buffer sequence, as a value: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=buffer_slice_include] - -include::example$snippets/5c_sequences.cpp[tag=buffer_slice,indent=0] ----- - -`buffer_slice(seq, offset, length)` returns a value that is itself a buffer sequence: pass it directly to any operation expecting one. The `offset` and `length` parameters (both optional) make `buffer_slice` a general byte sub-range primitive. Except for the single-buffer case, the result borrows `seq`, so the sequence must outlive the slice. - -== consuming_buffers - -When transferring data incrementally, `consuming_buffers` is a cursor that tracks progress: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=consuming_buffers_include] - -include::example$snippets/5c_sequences.cpp[tag=read_all,indent=0] ----- - -A `consuming_buffers` cursor borrows the underlying sequence and provides: - -* `data()` — Buffer sequence view of the remaining bytes (pass to `read_some`/`write_some`) -* `consume(n)` — Advance past `n` transferred bytes, in place - -== Why Bidirectional? - -The concepts require bidirectional ranges (not just forward ranges) for two reasons: - -1. Some algorithms traverse buffers backwards -2. The slice views produced by `buffer_slice` and `consuming_buffers::data()` need to adjust the first and last buffers' bounds - -If your custom buffer sequence only provides forward iteration, wrap it in a type that provides bidirectional access. - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| Concepts and iteration functions - -| `` -| Byte sub-range slicing algorithm - -| `` -| Incremental consumption cursor -|=== - -You have now learned how buffer sequences enable zero-allocation composition. Continue to xref:5.buffers/5d.system-io.adoc[System I/O Integration] to see how buffer sequences interface with operating system I/O. diff --git a/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc b/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc deleted file mode 100644 index 8fc2e8a4c..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc +++ /dev/null @@ -1,186 +0,0 @@ -= System I/O Integration - -This section explains how buffer sequences interface with operating system I/O operations. - -== Prerequisites - -* Completed xref:5.buffers/5c.sequences.adoc[Buffer Sequences] -* Understanding of buffer sequence concepts - -== The Virtual Boundary - -User-facing APIs use concepts for composition flexibility. But at type-erasure boundaries—where virtual functions are needed—concrete types are required. - -Capy's design: - -* *User-facing API* — Accepts `ConstBufferSequence` or `MutableBufferSequence` concepts -* *Internal boundary* — Converts to concrete arrays for virtual dispatch -* *OS interface* — Translates to platform-specific structures - -The library handles all conversions automatically. - -== Platform Buffer Structures - -=== POSIX: iovec - -[source,c] ----- -struct iovec { - void* iov_base; // Pointer to data - size_t iov_len; // Length of data -}; ----- - -Used with `readv()`, `writev()`, `recvmsg()`, `sendmsg()`. - -=== Windows: WSABUF - -[source,c] ----- -typedef struct _WSABUF { - ULONG len; // Length (note: first!) - CHAR* buf; // Pointer -} WSABUF; ----- - -Used with `WSARecv()`, `WSASend()`. - -Note the different member order—Capy handles this platform difference internally. - -== Translation Process - -When you call an I/O function with a buffer sequence: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=write_some_signature] ----- - -Internally, Capy: - -1. Counts the number of buffers in the sequence -2. Allocates space for platform buffer structures (on stack for small sequences) -3. Copies buffer descriptors (pointer/size pairs) to platform structures -4. Calls the OS function with the platform array -5. Returns the result - -== Stack-Based Conversion - -Conversion always happens on the stack—the implementation never -allocates. A fixed-size, on-frame window of buffer descriptors (16 -entries) is filled from the sequence and passed to the OS call. If the -sequence has more buffers than fit in the window, the window is refilled -and the OS call is repeated for the remaining buffers: - -[source,cpp,role=pseudocode] ----- -// Pseudocode of internal implementation -template -auto platform_write(Buffers const& buffers) -{ - iovec iovecs[16]; // fixed on-frame window, never heap-allocated - - auto it = begin(buffers); - auto last = end(buffers); - while (it != last) - { - std::size_t count = fill_iovecs(iovecs, it, last, 16); // up to 16 - auto result = writev(fd, iovecs, count); - // ... advance the window past the buffers just written - } -} ----- - -The window size (16) is fixed and implementation-defined. Sequences with -more buffers than the window are handled by refilling it across -successive OS calls; there is no heap fallback. - -== Scatter/Gather Benefits - -Using vectored I/O provides: - -=== Fewer System Calls - -Without scatter/gather: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=two_syscalls,indent=0] ----- - -With scatter/gather: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=gather_syscall,indent=0] ----- - -=== Zero-Copy Transmission - -Data doesn't need to be copied into a single contiguous buffer. The OS reads directly from each buffer in sequence. - -=== Atomic Operations - -The vectored write is atomic at the file offset level—other processes see either none or all of the data. - -== Registered Buffers - -Advanced platforms offer registered buffer optimizations: - -=== io_uring (Linux 5.1+) - -Buffers can be pre-registered with the kernel, eliminating per-operation address translation: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=io_uring_fixed,indent=0] ----- - -=== IOCP (Windows) - -Similar optimization with pre-registered memory regions for zero-copy I/O. - -Capy's Corosio library exposes these optimizations where available. - -== Writing Efficient Code - -=== Minimize Buffer Count - -Fewer buffers means less translation overhead: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=minimize_buffer_count,indent=0] ----- - -=== Reuse Buffer Structures - -For repeated I/O with the same structure, consider caching the platform buffer array: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=cached_iovecs] ----- - -=== Profile Before Optimizing - -Buffer translation is rarely the bottleneck. Focus on: - -* Network latency -* Disk I/O time -* Data processing logic - -Not buffer descriptor copying. - -== Reference - -The buffer sequence concepts and translation utilities are in: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=include_buffers] ----- - -OS-specific I/O is handled by Corosio, which builds on Capy's buffer model. - -You have now learned how buffer sequences integrate with operating system I/O. Continue to xref:5.buffers/5e.algorithms.adoc[Buffer Algorithms] to learn about measuring and copying buffers. diff --git a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc deleted file mode 100644 index a8077ff01..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc +++ /dev/null @@ -1,192 +0,0 @@ -= Buffer Algorithms - -This section covers algorithms for measuring and manipulating buffer sequences. - -== Prerequisites - -* Completed xref:5.buffers/5c.sequences.adoc[Buffer Sequences] -* Understanding of `ConstBufferSequence` and iteration - -== Measuring Buffers - -=== buffer_size - -Returns the total number of bytes across all buffers in a sequence: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_size_signature] ----- - -Example: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_size_example,indent=0] ----- - -Note: `buffer_size` returns the sum of bytes, not the count of buffers. - -=== buffer_empty - -Checks if a buffer sequence contains no data: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_empty_signature] ----- - -A buffer sequence is empty if: - -* It contains no buffers, OR -* All buffers have size zero - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_empty_example,indent=0] ----- - -=== buffer_length - -Returns the number of buffers in a sequence: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_length_signature] ----- - -Example: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_length_example,indent=0] ----- - -Note the distinction: - -* `buffer_size` — total bytes (data measurement) -* `buffer_length` — number of buffers (sequence length) - -== Copying Buffers - -=== buffer_copy - -Copies data from one buffer sequence to another: - -`buffer_copy` is a function object with a single call operator whose -`at_most` parameter defaults to copying everything available: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_signature] ----- - -Returns the number of bytes copied. - -Example: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_example,indent=0] ----- - -=== Partial Copy with at_most - -Limit the number of bytes copied: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_at_most,indent=0] ----- - -This is useful for implementing protocols with size limits. - -=== Cross-Sequence Copy - -`buffer_copy` handles sequences with different structure: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_cross,indent=0] ----- - -The algorithm fills target buffers sequentially, reading from source buffers as needed, handling cases where a single source buffer spans multiple target buffers or vice versa. - -== Real I/O Patterns - -Partial-transfer loops use a `consuming_buffers` cursor: `data()` -presents the not-yet-transferred remainder, and `consume(n)` advances -past the bytes the stream reported. - -=== Read Loop - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=read_loop] ----- - -=== Write Loop - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=write_loop] ----- - -== Practical Benefits of Concept-Based Design - -=== Zero-Copy I/O - -Data never moves unnecessarily. The buffer sequence points to existing data, and the OS reads directly from those locations: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=zero_copy,indent=0] ----- - -=== Scatter/Gather Operations - -Multiple buffers transfer in a single operation: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=scatter_gather,indent=0] ----- - -=== Custom Allocators and Memory-Mapped Buffers - -Any memory region can be a buffer: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=mmap_buffer,indent=0] ----- - -=== User-Defined Buffer Types - -Create custom types that satisfy the concepts: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=custom_sequence] ----- - -Here `chunk_iterator` is a small bidirectional iterator whose -`operator*` returns each chunk as a `const_buffer`. - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| Measurement algorithms (`buffer_size`, `buffer_empty`, `buffer_length`) - -| `` -| Copy algorithm - -| `` -| Consumption cursor for partial-transfer loops -|=== - -You have now learned how to measure and copy buffer sequences. Continue to xref:6.streams/6.intro.adoc[Stream Concepts] to learn how coroutines transfer data through streams. diff --git a/doc/modules/ROOT/pages/6.streams/6.intro.adoc b/doc/modules/ROOT/pages/6.streams/6.intro.adoc index cce486deb..bbd040272 100644 --- a/doc/modules/ROOT/pages/6.streams/6.intro.adoc +++ b/doc/modules/ROOT/pages/6.streams/6.intro.adoc @@ -8,13 +8,21 @@ // = Stream Concepts +:page-mode: explanation Data flows. It arrives from a network socket in unpredictable chunks. It leaves through a file descriptor as fast as the disk allows. It passes through encryption, compression, and framing layers--each transforming it before handing it off to the next. Modeling this flow well is one of the most important things an I/O library can do. -Capy organizes data flow around six concepts, arranged in three complementary pairs. The design reflects a truth about I/O that most libraries gloss over: _partial_ operations and _complete_ operations are fundamentally different things, and conflating them leads to bugs. +Capy organizes data flow around three concepts: cpp:ReadStream[], cpp:WriteStream[], and cpp:Stream[]. The design reflects a truth about I/O that most libraries gloss over. _Partial_ operations and _complete_ operations are fundamentally different things, and conflating them leads to bugs. -A socket might give you 47 bytes when you asked for 1024. That is not an error--it is the nature of the hardware. Some code needs to handle those 47 bytes immediately and ask for more. Other code needs exactly 1024 bytes and should not return until it has them (or an error occurs). These are different operations with different contracts, and Capy gives them different names: _streams_ for partial I/O, and _sources_ and _sinks_ for complete I/O. +A socket might give you 47 bytes when you asked for 1024. That is not an error--it is the nature of the hardware. Some code needs to handle those 47 bytes immediately and ask for more. Other code needs exactly 1024 bytes and should not return until it has them (or an error occurs). Capy's stream concepts cover the partial case directly: `read_some` and `write_some` transfer whatever the hardware allows. The complete case is a composed algorithm, not a separate concept. cpp:read[], cpp:write[], cpp:read_at_least[], and cpp:write_at_least[] loop over `read_some` or `write_some` until the buffer is satisfied or an error occurs. -On top of this, Capy adds _buffer sources_ and _buffer sinks_--concepts that work with dynamic buffers, enabling protocol parsers and message builders to grow their storage as needed without manual bookkeeping. +== What This Section Covers -This section introduces the concepts that form Capy's vocabulary for data flow. You will learn the distinction between partial and complete I/O, how the concept pairs relate to each other, and how transfer algorithms and physical isolation let you write I/O logic that is composable, testable, and independent of any particular transport. Once you understand these concepts, every I/O operation in the library will feel familiar. +* xref:6.streams/6a.overview.adoc[Overview] -- What cpp:ReadStream[], cpp:WriteStream[], and + cpp:Stream[] model, and why partial I/O needs its own concepts. +* xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] -- The cpp:ReadStream[] and + cpp:WriteStream[] concepts, and the type-erased `any_stream` wrappers. +* xref:6.streams/6f.isolation.adoc[Physical Isolation] -- Type erasure as a compilation + firewall for transport-independent, testable I/O code. + +Once you understand these concepts, every I/O operation in the library feels familiar. diff --git a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc index 3719a7d48..1013b2da7 100644 --- a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc +++ b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc @@ -1,11 +1,8 @@ = Stream Concepts Overview +:page-mode: explanation This section introduces Capy's stream concepts—the abstractions that enable data to flow through your programs. -== Prerequisites - -* Understanding of buffer sequences - == Three Concepts for Data Flow Capy defines three concepts for I/O operations: @@ -14,17 +11,17 @@ Capy defines three concepts for I/O operations: |=== | Concept | Direction | Description -| `ReadStream` +| cpp:ReadStream[] | Read | Partial reads—returns whatever is available -| `WriteStream` +| cpp:WriteStream[] | Write | Partial writes—writes as much as possible -| `Stream` +| cpp:Stream[] | Read + Write -| A connected pair—satisfies both `ReadStream` and `WriteStream` +| A connected pair—satisfies both cpp:ReadStream[] and cpp:WriteStream[] |=== == Streams: Partial I/O @@ -48,14 +45,14 @@ Each concept has a corresponding type-erasing wrapper: |=== | Concept | Wrapper -| `ReadStream` -| `any_read_stream` +| cpp:ReadStream[] +| cpp:any_read_stream[] -| `WriteStream` -| `any_write_stream` +| cpp:WriteStream[] +| cpp:any_write_stream[] | (Both) -| `any_stream` +| cpp:any_stream[] |=== These wrappers enable: @@ -89,5 +86,3 @@ include::example$snippets/6a_overview.cpp[tag=caller_decides,indent=0] ---- Same code, different transports: the in-memory test stream shown here, a TCP socket, or a TLS stream. Compile once, link anywhere. - -Continue to xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] to learn the `ReadStream` and `WriteStream` concepts in detail. diff --git a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc index 9088c9e20..d2ec0a92b 100644 --- a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc +++ b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc @@ -1,44 +1,22 @@ = Streams (Partial I/O) +:page-mode: explanation -This section explains the `ReadStream` and `WriteStream` concepts for partial I/O operations. - -== Prerequisites - -* Completed xref:6.streams/6a.overview.adoc[Stream Concepts Overview] -* Understanding of the six stream concept categories +This section explains the cpp:ReadStream[] and cpp:WriteStream[] concepts for partial I/O operations. == ReadStream -A type satisfies `ReadStream` if it provides partial read operations via `read_some`: +A type satisfies cpp:ReadStream[] if it provides partial read operations via `read_some`: [source,cpp] ---- include::example$snippets/6b_streams.cpp[tag=read_stream_concept] ---- -The `requires` clause names a single representative buffer (`mutable_buffer_archetype`) because a {cpp} concept cannot say "works with every buffer sequence." The real contract is that `read_some` accepts *any* `MutableBufferSequence`—one buffer or a range; the archetype only samples that requirement. +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. === read_some Semantics -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=read_some_signature,indent=0] ----- - -Attempts to read up to `buffer_size(buffers)` bytes from the stream into the buffer sequence. Await-returns `(error_code, std::size_t)`: - -If `buffer_size(buffers) > 0`: - -* If `!ec`, then `n >= 1 && n \<= buffer_size(buffers)`. `n` bytes were read into the buffer sequence. -* If `ec`, then `n >= 0 && n < buffer_size(buffers)`. `n` is the number of bytes read before the I/O condition arose. - -Equivalently, `n == buffer_size(buffers)` implies `!ec`: a read that fills the buffer sequence is a success even when the underlying operation also signals a condition such as end-of-stream. That condition is reported on a subsequent read. - -If `buffer_empty(buffers)` is true, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. - -I/O conditions from the underlying system are reported via `ec`. Failures in the library itself (such as allocation failure) are reported via exceptions. - -*Throws:* `std::bad_alloc` if coroutine frame allocation fails. +See cpp:ReadStream[] for the full contract: return-value semantics, error reporting, throws, and buffer lifetime. === Partial Transfer @@ -60,36 +38,18 @@ include::example$snippets/6b_streams.cpp[tag=dump_stream] == WriteStream -A type satisfies `WriteStream` if it provides partial write operations via `write_some`: +A type satisfies cpp:WriteStream[] if it provides partial write operations via `write_some`: [source,cpp] ---- include::example$snippets/6b_streams.cpp[tag=write_stream_concept] ---- -As with `ReadStream`, the `const_buffer_archetype` is only a representative: the real contract is that `write_some` accepts *any* `ConstBufferSequence`, which a {cpp} concept cannot fully express. +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. === write_some Semantics -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=write_some_signature,indent=0] ----- - -Attempts to write up to `buffer_size(buffers)` bytes from the buffer sequence to the stream. Await-returns `(error_code, std::size_t)`: - -If `buffer_size(buffers) > 0`: - -* If `!ec`, then `n >= 1 && n \<= buffer_size(buffers)`. `n` bytes were written from the buffer sequence. -* If `ec`, then `n >= 0 && n < buffer_size(buffers)`. `n` is the number of bytes written before the I/O condition arose. - -Equivalently, `n == buffer_size(buffers)` implies `!ec`: a write that drains the entire buffer sequence is a success even when the underlying operation also signals a condition. That condition is reported on a subsequent write. - -If `buffer_empty(buffers)` is true, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. - -I/O conditions from the underlying system are reported via `ec`. Failures in the library itself (such as allocation failure) are reported via exceptions. - -*Throws:* `std::bad_alloc` if coroutine frame allocation fails. +See cpp:WriteStream[] for the full contract: return-value semantics, error reporting, throws, and buffer lifetime. === Partial Transfer @@ -106,7 +66,7 @@ To write all data, loop until complete (or use the `write()` composed operation) === any_read_stream -Wraps any `ReadStream` in a type-erased container: +Wraps any cpp:ReadStream[] in a type-erased container: [source,cpp] ---- @@ -119,7 +79,7 @@ Each wrapper has two construction modes. Passing an object by value takes owners === any_write_stream -Wraps any `WriteStream`: +Wraps any cpp:WriteStream[]: [source,cpp] ---- @@ -130,7 +90,7 @@ include::example$snippets/6b_streams.cpp[tag=any_write_stream_ctors,indent=0] === any_stream -Wraps bidirectional streams (both `ReadStream` and `WriteStream`): +Wraps bidirectional streams (both cpp:ReadStream[] and cpp:WriteStream[]): [source,cpp] ---- @@ -163,27 +123,3 @@ include::example$snippets/6b_streams.cpp[tag=echo_server] ---- The implementation doesn't know the concrete stream type. It compiles once and works with any transport. - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| ReadStream concept definition - -| `` -| WriteStream concept definition - -| `` -| Type-erased read stream wrapper - -| `` -| Type-erased write stream wrapper - -| `` -| Type-erased bidirectional stream wrapper -|=== - -You have now learned the stream concepts for partial I/O. Continue to xref:6.streams/6f.isolation.adoc[Physical Isolation]. diff --git a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc index 77d6ab9b7..e9c3f83e5 100644 --- a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc +++ b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc @@ -1,12 +1,8 @@ = Physical Isolation +:page-mode: explanation This section explains how type-erased wrappers enable compilation firewalls and transport-independent APIs. -== Prerequisites - -* Completed xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] -* Understanding of type-erased wrappers - == The Compilation Firewall Pattern {cpp} templates are powerful but have a cost: every instantiation compiles in every translation unit that uses it. Change a template, and everything that includes it recompiles. @@ -128,12 +124,4 @@ When profiling shows wrapper overhead matters: 2. Use concrete types in hot paths 3. Accept the template cost for that code path -== Reference - -Type-erased wrappers are in ``: - -* `any_stream` -* `any_read_stream`, `any_write_stream` -* `any_buffer_source`, `any_buffer_sink` - -You have now completed the Stream Concepts section. These abstractions—streams and their type-erased wrappers—form the foundation for Capy's I/O model. Continue to xref:../8.examples/8a.hello-task.adoc[Example Programs] to see complete working examples. +These abstractions—streams and their type-erased wrappers—form the foundation for Capy's I/O model. diff --git a/doc/modules/ROOT/pages/7.testing/7.intro.adoc b/doc/modules/ROOT/pages/7.testing/7.intro.adoc index 95cdd87bb..949f27744 100644 --- a/doc/modules/ROOT/pages/7.testing/7.intro.adoc +++ b/doc/modules/ROOT/pages/7.testing/7.intro.adoc @@ -8,46 +8,45 @@ // = Testing +:page-mode: explanation Real I/O is a poor foundation for unit tests. Network operations are slow, non-deterministic, and do not fail on demand -- so error-handling paths go -untested until production breaks them. Capy ships a self-contained toolkit -that replaces the transport with in-memory mocks, drives coroutines to +untested until production breaks them. Capy ships a self-contained toolkit. +It replaces the transport with in-memory mocks, drives coroutines to completion on the calling thread, and injects failures at every -`maybe_fail()` site so that every error branch is exercised automatically. -Because each mock satisfies the same concept as its production counterpart, -test code reads the same as production code -- the only difference is the -type of the stream or source you pass in. +`maybe_fail()` site. The toolkit therefore exercises every error branch +automatically. Because each mock satisfies the same concept as its +production counterpart, test code reads the same as production code. The +only difference is the type of the stream you pass in. == What This Section Covers -* xref:7.testing/7a.drivers.adoc[Driving Tests] -- `run_blocking` drives a - coroutine to completion on the calling thread without a real executor; - `fuse` runs the test body repeatedly, injecting an error at each - `maybe_fail()` site in turn until every failure path has been covered; - and the `thread_name` header's `set_current_thread_name` function labels +* xref:7.testing/7a.drivers.adoc[Driving Tests] -- cpp:test::run_blocking[run_blocking] drives a + coroutine to completion on the calling thread without a real executor. + cpp:test::fuse[fuse] runs the test body repeatedly, injecting an error at each + `maybe_fail()` site in turn until every failure path is covered. + The `thread_name` header's cpp:set_current_thread_name[] function labels worker threads so that failures in multi-threaded tests are easier to attribute. -* xref:7.testing/7b.mock-streams.adoc[Mock Streams] -- `read_stream`, - `write_stream`, and `stream` (a connected pair) implement the partial-I/O +* xref:7.testing/7b.mock-streams.adoc[Mock Streams] -- cpp:test::read_stream[read_stream], + cpp:test::write_stream[write_stream], and cpp:test::stream[stream] (a connected pair) implement the partial-I/O concepts from xref:6.streams/6b.streams.adoc[Streams]. Use them to test protocol logic that calls `read_some` and `write_some` without touching a socket. -* xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection] -- `bufgrind` +* xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection] -- cpp:test::bufgrind[bufgrind] iterates every split point of a buffer sequence, exercising every - chunk-boundary condition; `buffer_to_string` concatenates buffer sequences + chunk-boundary condition; cpp:test::buffer_to_string[buffer_to_string] concatenates buffer sequences into a `std::string` for easy assertion. == How the Pieces Fit A typical test constructs one or more mocks, arms a `fuse`, and hands the mocks to the code under test inside a `run_blocking` call. The `fuse` -repeats the test body automatically -- once for each failure site and once -in exception mode -- while `run_blocking` keeps the whole thing on the -calling thread. Buffer utilities such as `bufgrind` and `buffer_to_string` -wrap the mock data for assertions, letting you verify that every split of +repeats the test body automatically in two full sweeps -- error-code mode, +then exception mode. `run_blocking` keeps the whole thing on the calling +thread. Buffer utilities such as `bufgrind` and `buffer_to_string` +wrap the mock data for assertions. They let you verify that every split of an input buffer produces the same correct output. - -Continue to xref:7.testing/7a.drivers.adoc[Driving Tests] to begin. diff --git a/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc b/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc index dd45287e3..4f97cad3f 100644 --- a/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc +++ b/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc @@ -8,22 +8,18 @@ // = Driving Tests +:page-mode: how-to Three utilities work together to run capy code synchronously inside a unit -test: `run_blocking` drives a coroutine to completion on the calling thread, -`fuse` injects errors at controlled points and reruns the test body until -every failure path is covered, and `set_current_thread_name` labels worker +test. cpp:test::run_blocking[run_blocking] drives a coroutine to completion on the calling +thread. cpp:test::fuse[fuse] injects errors at controlled points and reruns the test body +until every failure path is covered. cpp:set_current_thread_name[] labels worker threads so that multi-threaded test output is readable. -== Prerequisites - -* xref:4.coroutines/4.intro.adoc[Coroutines in Capy] -* xref:6.streams/6.intro.adoc[Stream Concepts] - == run_blocking `run_blocking` bridges async coroutine code into a synchronous test body. -It creates a single-threaded event loop on the calling thread, launches the +It creates a single-threaded event loop on the calling thread, starts the coroutine through it, and blocks until the coroutine finishes or throws. No real executor or thread pool is involved. @@ -45,7 +41,7 @@ include::example$snippets/7a_drivers.cpp[tag=run_blocking_overloads,indent=0] === How It Works -`run_blocking` creates a `blocking_context`, an internal single-threaded +`run_blocking` creates a cpp:test::blocking_context[blocking_context], an internal single-threaded execution context. Work posted to it is queued and processed on the calling thread until the coroutine signals completion, then control returns to the caller. The inline executor performs symmetric transfer for `dispatch` calls @@ -53,49 +49,27 @@ so that the coroutine chain runs without unnecessary context switches. Use this only in test code. Production code should use a real execution context such as a thread pool. -[cols="1,2"] -|=== -| Overload | Behavior - -| `run_blocking()` -| Discard result. Rethrows captured exceptions. - -| `run_blocking(on_value)` -| Invoke `on_value(v)` on success. Rethrows exceptions if `on_value` - does not accept `std::exception_ptr`. - -| `run_blocking(on_value, on_error)` -| Invoke `on_value(v)` on success or `on_error(ep)` with - `std::exception_ptr` on failure. - -| `run_blocking(stop_token)` -| Drive with an external stop token; discard result. - -| `run_blocking(stop_token, on_value)` -| Drive with an external stop token; invoke `on_value(v)` on success. - -| `run_blocking(stop_token, on_value, on_error)` -| Drive with an external stop token; invoke `on_value(v)` on success or - `on_error(ep)` on failure. -|=== +See cpp:test::run_blocking[] for the complete overload set. Each +overload optionally takes a success handler, an error handler, or an +external stop token. === Exercising Cancellation The stop token supplied to `run_blocking` propagates through the execution environment to every `capy::test` awaitable. When the token is requested, the -next awaited test operation resolves to `error::canceled` instead of -performing its work, so code under test can be driven down its cancellation -paths without real I/O. +next awaited test operation resolves to cpp:error::canceled[error::canceled] instead of +performing its work. Code under test can therefore be driven down its +cancellation paths without real I/O. [source,cpp] ---- include::example$snippets/7a_drivers.cpp[tag=run_blocking_cancellation,indent=0] ---- -The mock sources, sinks, and buffer adapters complete synchronously, so they -check the token up front: an outstanding stop request yields `error::canceled` -on the next operation. A connected `stream` whose `read_some` is *blocked* -waiting for its peer is resumed with `error::canceled` when the token fires; a +The mock streams and buffer adapters complete synchronously, so they +check the token up front. An outstanding stop request yields cpp:error::canceled[error::canceled] +on the next operation. A connected cpp:test::stream[stream] whose `read_some` is *blocked* +waiting for its peer is resumed with `error::canceled` when the token fires. A read that can satisfy from already-buffered data is unaffected. == fuse @@ -155,8 +129,8 @@ include::example$snippets/7a_drivers.cpp[tag=early_return,indent=0] === Coroutine Support -`armed()` detects when the test lambda returns an `IoRunnable` (such as -`task`) and drives it to completion via `run_blocking` internally. +`armed()` detects when the test lambda returns an cpp:IoRunnable[] (such as +cpp:task[task]) and drives it to completion via `run_blocking` internally. You do not need to call `run_blocking` yourself: [source,cpp] @@ -169,7 +143,7 @@ include::example$snippets/7a_drivers.cpp[tag=armed_coroutine,indent=0] A type that holds a `fuse` reference can call `maybe_fail()` from its own methods to declare additional fail points beyond those built into the mocks. Outside `armed()` or `inert()` the call is a no-op (returns an -empty error code immediately); inside `armed()` it participates in +empty error code immediately). Inside `armed()` the call participates in fault injection alongside every other site. [source,cpp] @@ -187,65 +161,13 @@ The default injected code is `error::test_failure`. Pass any include::example$snippets/7a_drivers.cpp[tag=custom_error_code,indent=0] ---- -[cols="1,2"] -|=== -| Member | Description - -| `fuse()` -| Construct with the default error code (`error::test_failure`). - -| `explicit fuse(std::error_code ec)` -| Construct with a custom error code delivered by `maybe_fail()`. - -| `armed(fn) -> result` -| Run `fn` repeatedly in error-code mode then exception mode, failing - at successive `maybe_fail()` sites. Accepts plain lambdas and coroutine - lambdas returning `IoRunnable`. - -| `armed(run_one, fn) -> result` -| Like `armed(fn)` for coroutine lambdas, but drives each iteration - through the caller-supplied `run_one` instead of `run_blocking`, so the - task runs on any execution context the caller chooses (for example an - `io_context`, required by operations built on `corosio::timeout`). - `run_one` runs the task to completion and returns any escaped exception - as a `std::exception_ptr` (null on success); `armed` rethrows it. - -| `inert(fn) -> result` -| Run `fn` once with no injection. `maybe_fail()` always returns `{}`. - Accepts plain lambdas and coroutine lambdas returning `IoRunnable`. - -| `operator()(fn) -> result` -| Alias for `armed(fn)`. - -| `maybe_fail() -> std::error_code` -| Return the injected error code at the active failure point, or `{}` - otherwise. In exception mode, throws `std::system_error` instead of - returning an error. Outside `armed`/`inert`, always returns `{}`. - -| `fail()` -| Signal an explicit test failure and stop execution. Records the call - site in `result::loc`. - -| `fail(std::exception_ptr)` -| Signal a test failure with an associated exception. Stored in - `result::ep`. - -| `result::success` -| `true` if the run completed without any failure. - -| `result::loc` -| Source location of the last `maybe_fail()` or `fail()` call on failure. - -| `result::ep` -| Exception pointer captured from a `fail(ep)` call, or `nullptr`. - -| `result::operator bool()` -| Returns `result::success`. -|=== +See cpp:test::fuse[] for the complete member list. It covers both +constructors, the `armed`/`inert`/`operator()` entry points, +`maybe_fail`, the `fail` overloads, and `result`'s fields. == thread_name -`set_current_thread_name` names the calling thread so that debuggers, +cpp:set_current_thread_name[] names the calling thread so that debuggers, `htop`, and core dumps show a recognizable label instead of a generic thread ID. This is most useful when a test failure occurs inside a thread pool worker and you need to identify which worker was involved. The function is a @@ -262,7 +184,7 @@ Platform limits on the name length: include::example$snippets/7a_drivers.cpp[tag=thread_name,indent=0] ---- -Note that `set_current_thread_name` lives in namespace `boost::capy`, not +cpp:set_current_thread_name[] lives in namespace `boost::capy`, not `boost::capy::test`, because the function is useful in any context, not only tests. @@ -270,7 +192,7 @@ tests. |=== | Function | Description -| `set_current_thread_name(char const* name)` +| cpp:set_current_thread_name[]`(char const* name)` | Set the OS thread name for the calling thread. Truncated to the platform limit. No-op on unsupported platforms. |=== @@ -286,15 +208,16 @@ internally, so the test body uses `co_await` directly: include::example$snippets/7a_drivers.cpp[tag=canonical_skeleton] ---- -When the operation under test needs a specific execution context -- for -example an `io_context`, which operations built on `corosio::timeout` or -`corosio::delay` require and which `run_blocking` does not provide -- use the -`armed(run_one, fn)` overload. The caller supplies `run_one`, which drives -each iteration's task on a context it owns and returns any exception the task -raised as a `std::exception_ptr` (null on success); `armed` rethrows it. +Use the `armed(run_one, fn)` overload when the operation under test needs a +specific execution context. Operations built on `corosio::timeout` or +`corosio::delay`, for example, require an `io_context`, which `run_blocking` +does not provide. The caller supplies `run_one`, which drives each iteration's +task on a context it owns. `run_one` returns any exception the task raised as a +`std::exception_ptr`, or null on success, and `armed` rethrows it. `fuse` never learns about the context -- the caller owns the drive loop: -[source,cpp,role=external] +.Excerpt - not compiled here +[source,cpp,role=pseudocode] ---- fuse f; auto io_runner = [](task t) -> std::exception_ptr { @@ -313,9 +236,10 @@ auto r = f.armed( BOOST_TEST(r.success); ---- -`run_one` must *return* the exception rather than rethrow it: an exception -escaping a `run_async` completion handler calls `std::terminate`, so the -handler captures it and the runner hands it back once the run loop is done. +`run_one` must *return* the exception rather than rethrow it. An exception +escaping a cpp:run_async[] completion handler calls `std::terminate`. The +handler therefore captures it, and the runner hands it back once the run loop +is done. A fresh `io_context` per iteration (as above) needs no `restart()`; reuse one across iterations only if you call `restart()` between rounds. @@ -325,28 +249,11 @@ across iterations only if you call `restart()` between rounds. of a `fuse` object shares the same internal state, so all copies respond to the same `armed()` or `inert()` call. This is what makes the canonical pattern work: pass a copy of `f` to each mock at construction time, then -call `f.armed(...)` once -- the injection machinery reaches every mock +call `f.armed(...)` once. The injection machinery reaches every mock because they all hold a copy pointing to the same shared state. For tests that need mocks, replace `add` with a function that takes a -`read_stream`, `write_stream`, or other mock, and construct those mocks -with the same `fuse f`. The armed loop will then exercise every I/O +cpp:test::read_stream[read_stream], cpp:test::write_stream[write_stream], or other mock. +Construct those mocks with the same `fuse f`. The armed loop then exercises every I/O failure path through both error-code and exception modes automatically. -== Reference - -[cols="1,3"] -|=== -| Header | Contents - -| `` -| Synchronous coroutine driver. - -| `` -| Systematic error injection. - -| `` -| Thread naming for diagnostics. -|=== - -Continue to xref:7.testing/7b.mock-streams.adoc[Mock Streams]. diff --git a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc index d5cc14472..ec1e16253 100644 --- a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc +++ b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc @@ -8,6 +8,7 @@ // = Mock Streams +:page-mode: how-to Concept-conforming test doubles for the partial-I/O concepts in xref:6.streams/6b.streams.adoc[Streams]. Use them to drive protocol @@ -16,9 +17,9 @@ partial-transfer paths. == read_stream -`read_stream` implements the `ReadStream` concept. Test code stages bytes +cpp:test::read_stream[] implements the cpp:ReadStream[] concept. Test code stages bytes via `provide()`, then the system under test (or the test body) calls -`read_some()` to consume them. The attached `fuse` injects errors at +`read_some()` to consume them. The attached cpp:test::fuse[] injects errors at every read call, exercising the caller's error-handling paths. Because `fuse` copies share state (see xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), @@ -44,8 +45,8 @@ include::example$snippets/7b_mock_streams.cpp[tag=read_stream_chunked,indent=0] === EOF Behavior -When all provided data has been consumed, `read_some` returns -`cond::eof` with a byte count of zero. The stream does not +When all provided data is consumed, `read_some` returns +cpp:cond::eof[cond::eof] with a byte count of zero. The stream does not suspend; the result is available immediately. [source,cpp] @@ -53,38 +54,17 @@ suspend; the result is available immediately. include::example$snippets/7b_mock_streams.cpp[tag=read_stream_eof,indent=0] ---- -[cols="1,2"] -|=== -| Member | Description - -| `explicit read_stream(fuse f = {}, std::size_t max_read_size = std::size_t(-1))` -| Construct with an optional shared `fuse` and an optional per-read byte limit. - When omitted, the fuse is inert and reads return all available data at once. - Set `max_read_size` to simulate chunked network delivery. - -| `provide(std::string_view sv)` -| Append bytes to the internal buffer for subsequent reads. Multiple - calls accumulate data. - -| `read_some(MutableBufferSequence buffers)` -| Partial read. Returns up to `max_read_size` bytes (or all available - if no limit was set). Returns `cond::eof` when the buffer is drained. - Consults the fuse before every read. - -| `available() -> std::size_t` -| Return the number of bytes remaining to be read. - -| `clear()` -| Clear all data and reset the read position. -|=== +See cpp:test::read_stream[] for the complete member list: the +constructor's `fuse` and `max_read_size` parameters, `provide`, +`read_some`, `available`, and `clear`. == write_stream -`write_stream` implements the `WriteStream` concept. The system under +cpp:test::write_stream[] implements the cpp:WriteStream[] concept. The system under test calls `write_some()` and the test inspects what was written via `data()`. Test code may also call `expect()` to register the data it -anticipates; any mismatch between written bytes and that prefix causes -`write_some()` to return `error::test_failure` directly. The fuse is a +anticipates. Any mismatch between written bytes and that prefix causes +`write_some()` to return cpp:error::test_failure[error::test_failure] directly. The fuse is a separate concern used only for error injection. Because `fuse` copies share state (see xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), @@ -112,49 +92,27 @@ include::example$snippets/7b_mock_streams.cpp[tag=write_stream_chunked,indent=0] Call `expect()` before or after writes to assert that the written data matches a prefix. Matched bytes are consumed from both sides. If written data does not match the expected prefix, the next `write_some` call -returns `error::test_failure`. +returns cpp:error::test_failure[error::test_failure]. [source,cpp] ---- include::example$snippets/7b_mock_streams.cpp[tag=write_stream_expect,indent=0] ---- -[cols="1,2"] -|=== -| Member | Description - -| `explicit write_stream(fuse f = {}, std::size_t max_write_size = std::size_t(-1))` -| Construct with an optional shared `fuse` and an optional per-write byte limit. - When omitted, the fuse is inert and writes accept all bytes at once. - Set `max_write_size` to simulate chunked network delivery. - -| `write_some(ConstBufferSequence buffers)` -| Partial write. Appends up to `max_write_size` bytes to the internal - buffer, then checks against the expected prefix. On mismatch, rolls - back the appended bytes and returns `(error::test_failure, 0)`. - Consults the fuse before every write. - -| `data() -> std::string_view` -| Return bytes written but not yet matched by `expect()`. - -| `size() -> std::size_t` -| Return the number of bytes written. - -| `expect(std::string_view sv) -> std::error_code` -| Register expected data and immediately check any already-written - bytes. Returns an error if existing data does not match. -|=== +See cpp:test::write_stream[] for the complete member list: the +constructor's `fuse` and `max_write_size` parameters, `write_some`, +`data`, `size`, and `expect`. == stream -`stream` is a connected bidirectional test double. Create a pair with -`make_stream_pair(f)`. Bytes written to one end become readable on the +cpp:test::stream[] is a connected bidirectional test double. Create a pair with +cpp:test::make_stream_pair[]`(f)`. Bytes written to one end become readable on the other. If `read_some` is called on an end with no buffered data, the calling coroutine suspends until the peer calls `write_some`. This makes `stream` useful for testing client/server code without real sockets. -Both `stream` ends satisfy `ReadStream` and `WriteStream`. +Both `stream` ends satisfy cpp:ReadStream[] and cpp:WriteStream[]. [source,cpp] ---- @@ -178,13 +136,13 @@ operation under test. Calling `close()` on one end signals EOF to the peer. The peer drains any buffered data first; once the buffer is empty, subsequent -`read_some` calls on the peer return `cond::eof`. The peer may still +`read_some` calls on the peer return cpp:cond::eof[cond::eof]. The peer may still call `write_some` after receiving EOF. When the fuse injects an error during `read_some` or `write_some`, the -pair is automatically closed: the calling end returns the injected -error, any suspended reader on the other end is resumed with -`cond::eof`, and all subsequent operations on both ends return +pair is automatically closed. The calling end returns the injected +error, and any suspended reader on the other end is resumed with +cpp:cond::eof[cond::eof]. All subsequent operations on both ends return `cond::eof`. === Thread Safety @@ -193,49 +151,14 @@ Single-threaded only. Both ends of the pair must be accessed from the same thread. Concurrent access from multiple threads or multiple concurrent coroutines is undefined behavior. -[cols="1,2"] -|=== -| Function / Member | Description - -| `make_stream_pair(fuse f = {}) -> std::pair` -| Create a connected pair sharing the supplied fuse. - -| `read_some(MutableBufferSequence buffers)` -| Partial read from the peer's outgoing data. Suspends if no data is - available. Returns `cond::eof` when the stream is closed or the peer - called `close()`. Consults the fuse before every read (unless - draining after `close()`). - -| `write_some(ConstBufferSequence buffers)` -| Partial write into the peer's incoming buffer. Resumes a suspended - peer reader if any. Returns `cond::eof` if the stream is closed. - Consults the fuse before every write. - -| `close()` -| Signal EOF to the peer's reads. Buffered data is drained first. - Writes from the peer are unaffected. - -| `set_max_read_size(std::size_t n)` -| Limit bytes returned per `read_some` call on this end, simulating - chunked network delivery (applies to this end only; the peer end has - its own independent limit). - -| `provide(std::string_view sv)` -| Inject bytes into this stream for reading, bypassing the fuse. - Resumes a suspended `read_some` if any. - -| `expect(std::string_view expected) -> std::pair` -| Read exactly `expected.size()` bytes and compare. Returns the error - code and whether the data matched. - -| `data() -> std::string_view` -| Return a view of the unread bytes buffered in this stream. -|=== +See cpp:test::stream[] and cpp:test::make_stream_pair[] for the +complete member list: pair construction, `read_some`, `write_some`, +`close`, `set_max_read_size`, `provide`, `expect`, and `data`. == Putting It Together The following snippet tests a function that reads a single line -terminated by `'\n'` from a `ReadStream`. The `fuse.armed()` loop +terminated by `'\n'` from a cpp:ReadStream[]. The `fuse.armed()` loop runs the coroutine repeatedly, failing at every `read_some` call in turn, then reruns in exception mode. Each injected failure exercises a different error-handling branch inside `read_line`. @@ -245,20 +168,3 @@ a different error-handling branch inside `read_line`. include::example$snippets/7b_mock_streams.cpp[tag=read_line_test] ---- -== Reference - -[cols="1,3"] -|=== -| Header | Contents - -| `` -| Mock ReadStream with controllable partial reads. - -| `` -| Mock WriteStream with controllable partial writes and expectations. - -| `` -| Connected bidirectional pair for client/server tests. -|=== - -Continue to xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection]. diff --git a/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc b/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc index 7daefaa24..a5a0d1288 100644 --- a/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc +++ b/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc @@ -8,10 +8,11 @@ // = Buffer Inspection +:page-mode: how-to -Two small utilities round out the toolkit. `bufgrind` iterates every split +Two small utilities round out the toolkit. cpp:test::bufgrind[bufgrind] iterates every split point of a buffer sequence, exercising every chunk-boundary condition in the -system under test. `buffer_to_string` concatenates buffer sequences into a +system under test. cpp:test::buffer_to_string[buffer_to_string] concatenates buffer sequences into a `std::string` for assertion. == bufgrind @@ -24,7 +25,7 @@ a buffer in chunks is exercised at every possible chunk boundary with a single `while` loop. `bufgrind` does not perform I/O and does not consult a fuse, so the snippets -on this page drive it under `f.inert(...)`: a single pass is sufficient to +on this page drive it under `f.inert(...)`. A single pass is sufficient to visit every split position, and there are no async failure sites to inject. [source,cpp] @@ -36,6 +37,7 @@ include::example$snippets/7e_buffer_inspection.cpp[tag=all_splits,indent=0] For a 5-byte input `"hello"`, `bufgrind` yields six positions: +[role=output] ---- pos=0: b1="" b2="hello" pos=1: b1="h" b2="ello" @@ -64,10 +66,10 @@ two positions: 0 and size. === Mutability Preservation -`bufgrind` is templated on a `ConstBufferSequence` but the slices it +`bufgrind` is templated on a cpp:ConstBufferSequence[] but the slices it produces follow the mutability of the input. Each half is itself a -buffer sequence (`slice_type`). Passing a `mutable_buffer` yields halves -that model `MutableBufferSequence`; passing a `const_buffer` yields +buffer sequence (cpp:test::bufgrind::slice_type[slice_type]). Passing a cpp:mutable_buffer[] yields halves +that model cpp:MutableBufferSequence[]; passing a cpp:const_buffer[] yields halves that model `ConstBufferSequence`. This matters for tests that need to write into the produced buffers rather than only read from them. @@ -76,23 +78,8 @@ need to write into the produced buffers rather than only read from them. include::example$snippets/7e_buffer_inspection.cpp[tag=mutability,indent=0] ---- -[cols="1,2"] -|=== -| Member | Description - -| `bufgrind(BS const& bs, std::size_t step = 1)` -| Construct over a buffer sequence. `step` controls how many bytes to - advance on each call to `next()`. A step of 0 is treated as 1. - The final split at `buffer_size(bs)` is always included. - -| `operator bool() const` -| Return `true` while more split positions remain. - -| `next()` -| Advance to the next split position. Returns an awaitable that yields - `split_type`, a `std::pair` of `slice_type` values representing - the two pieces at the current position. -|=== +See cpp:test::bufgrind[] for the complete member list: the +constructor's `step` parameter, `operator bool`, and `next`. == buffer_to_string @@ -117,19 +104,12 @@ input: include::example$snippets/7e_buffer_inspection.cpp[tag=reconstruct,indent=0] ---- -[cols="1,2"] -|=== -| Function | Description - -| `buffer_to_string(Buffers const&... bufs) -> std::string` -| Concatenate one or more `ConstBufferSequence` arguments into a single - `std::string`. Arguments are appended in the order given. -|=== +See cpp:test::buffer_to_string[] for the exact signature. == Putting It Together The following snippet tests a hypothetical parser that reads from a -`read_stream`. `bufgrind` exercises every split of the input so the parser +cpp:test::read_stream[read_stream]. `bufgrind` exercises every split of the input so the parser is run against every possible chunk boundary; `buffer_to_string` verifies the output at each split: @@ -138,19 +118,4 @@ the output at each split: include::example$snippets/7e_buffer_inspection.cpp[tag=parser_all_splits,indent=0] ---- -== Reference - -[cols="1,3"] -|=== -| Header | Contents - -| `` -| Exhaustive buffer split-point iterator. - -| `` -| Buffer-sequence to string helper. -|=== - -You have reached the end of the Testing section. Continue -to xref:8.examples/8.intro.adoc[Example Programs] for end-to-end usage -or xref:reference:boost/capy.adoc[Reference] for the API browser. +See xref:reference:boost/capy.adoc[Reference] for the full API browser. diff --git a/doc/modules/ROOT/pages/8.examples/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index 5890ab8d0..73be122d2 100644 --- a/doc/modules/ROOT/pages/8.examples/8.intro.adoc +++ b/doc/modules/ROOT/pages/8.examples/8.intro.adoc @@ -8,7 +8,42 @@ // = Example Programs +:page-mode: explanation -The best way to learn a library is to watch it solve real problems. This section is a collection of complete, working programs that demonstrate how the pieces you have learned--tasks, buffers, streams, cancellation, composition--fit together in practice. +The best way to learn a library is to watch it solve real problems. This section is a collection of complete, working programs. They demonstrate how the pieces you have learned--tasks, buffers, streams, cancellation, composition--fit together in practice. -Every example is self-contained and compiles as a standalone program. The code is followed by detailed explanations of what it does, why it is structured that way, and what happens at each step. The examples range from minimal starting points to fully featured servers, covering real-world integration with Corosio. Start with whatever interests you most, or work through them in order for a guided tour of Capy's capabilities. +Every example is self-contained and compiles as a standalone program. The code is followed +by detailed explanations of what it does, why it is structured that way, and what happens +at each step. Start with whatever interests you most, or work through them in order for a +guided tour of Capy's capabilities. + +== What This Section Covers + +* xref:8.examples/8a.hello-task.adoc[Hello Task] -- The minimal Capy program: a task that + prints a message. +* xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] -- Two tasks communicating + via an async event, with strand serialization. +* xref:8.examples/8c.buffer-composition.adoc[Buffer Composition] -- Composing buffer + sequences without allocation for scatter/gather I/O. +* xref:8.examples/8d.mock-stream-testing.adoc[Mock Stream Testing] -- Unit testing protocol + code with mock streams and error injection. +* xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] -- An echo server + demonstrating the compilation firewall pattern. +* xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] -- Racing a slow + operation against a deadline, and cancelling it with a stop token. +* xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] -- Running multiple operations + concurrently with cpp:when_all[]. +* xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] -- Protecting shared + state with a strand instead of a mutex. +* xref:8.examples/8l.async-mutex.adoc[Async Mutex] -- Fair FIFO coroutine locking with + cpp:async_mutex[]. +* xref:8.examples/8m.parallel-tasks.adoc[Parallel Tasks] -- Distributing CPU-bound work + across a thread pool and collecting results. +* xref:8.examples/8n.custom-executor.adoc[Custom Executor] -- Implementing the `Executor` + concept with a single-threaded run loop. +* xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] -- Awaiting a + `std::execution` (P2300) sender from a Capy coroutine. +* xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] -- Using + Boost.Asio operations directly through a `use_capy` completion token. +* xref:8.examples/8q.gui-integration.adoc[GUI Integration] -- Running a Capy coroutine on + a GUI event loop, resuming on the GUI thread to update widgets. diff --git a/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc b/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc index 12c848c62..34b287488 100644 --- a/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc +++ b/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc @@ -1,17 +1,13 @@ = Hello Task +:page-mode: how-to The minimal Capy program: a task that prints a message. -== What You Will Learn +== What This Example Shows -* Creating a `task<>` coroutine -* Using `thread_pool` as an execution context -* Launching tasks with `run_async` - -== Prerequisites - -* {cpp}20 compiler -* Capy library installed +* Creating a cpp:task[task<>] coroutine +* Using cpp:thread_pool[] as an execution context +* Starting tasks with cpp:run_async[] == Source Code @@ -22,10 +18,10 @@ include::example$hello-task/hello_task.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(hello_task hello_task.cpp) -target_link_libraries(hello_task PRIVATE capy) +target_link_libraries(hello_task PRIVATE Boost::capy) ---- == Walkthrough @@ -37,9 +33,9 @@ target_link_libraries(hello_task PRIVATE capy) include::example$hello-task/hello_task.cpp[tag=say_hello] ---- -`task<>` is equivalent to `task`—a coroutine that completes without returning a value. The `co_return` keyword marks this as a coroutine. +cpp:task[task<>] is equivalent to `task`—a coroutine that completes without returning a value. The `co_return` keyword marks this as a coroutine. -Tasks are lazy: calling `say_hello()` creates a task object but does not execute the body. The `"Hello"` message is not printed until the task is launched. +Tasks are lazy: calling `say_hello()` creates a task object but does not execute the body. The `"Hello"` message is not printed until the task is started. === The Thread Pool @@ -48,26 +44,27 @@ Tasks are lazy: calling `say_hello()` creates a task object but does not execute include::example$hello-task/hello_task.cpp[tag=pool,indent=0] ---- -`thread_pool` provides an execution context with worker threads. By default, it creates one thread per CPU core. +cpp:thread_pool[] provides an execution context with worker threads. By default, it creates one thread per CPU core. Call `pool.join()` before the pool is destroyed to wait for all outstanding work to finish. The destructor does *not* wait for queued work: `~thread_pool` calls `stop()`, which abandons any work that has not yet started, then joins the worker threads. Without the explicit `pool.join()`, `main` could destroy the pool and exit before `say_hello` ever runs, so `"Hello from Capy!"` might never print. -=== Launching +=== Starting the Task [source,cpp] ---- include::example$hello-task/hello_task.cpp[tag=launch,indent=0] ---- -`run_async` bridges non-coroutine code (like `main`) to coroutine code. The two-call syntax: +cpp:run_async[] bridges non-coroutine code (like `main`) to coroutine code. The two-call syntax: -1. `run_async(pool.get_executor())` — Creates a launcher with the executor +1. cpp:run_async[]`(pool.get_executor())` — Creates a launcher with the executor 2. `(say_hello())` — Accepts the task and starts execution The task runs on one of the pool's worker threads. == Output +[role=output] ---- Hello from Capy! ---- @@ -75,8 +72,8 @@ Hello from Capy! == Exercises 1. Modify `say_hello` to accept a `std::string_view` parameter and print it -2. Create multiple tasks and launch them all -3. Add a handler to `run_async` that prints when the task completes +2. Create multiple tasks and start them all +3. Add a handler to cpp:run_async[] that prints when the task completes == Next Steps diff --git a/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc index 595a9823c..5c2fd82f4 100644 --- a/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc +++ b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc @@ -1,19 +1,15 @@ = Producer-Consumer +:page-mode: how-to Two tasks communicating via an async event, with strand serialization. -== What You Will Learn +== What This Example Shows -* Using `async_event` for coroutine synchronization -* Running multiple concurrent tasks with `when_all` -* Using `strand` to serialize access to shared state +* Using cpp:async_event[] for coroutine synchronization +* Running multiple concurrent tasks with cpp:when_all[] +* Using cpp:strand[] to serialize access to shared state * Task-to-task communication patterns -== Prerequisites - -* Completed xref:8.examples/8a.hello-task.adoc[Hello Task] -* Understanding of basic task creation and launching - == Source Code [source,cpp] @@ -23,10 +19,10 @@ include::example$producer-consumer/producer_consumer.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(producer_consumer producer_consumer.cpp) -target_link_libraries(producer_consumer PRIVATE capy) +target_link_libraries(producer_consumer PRIVATE Boost::capy) ---- == Walkthrough @@ -38,7 +34,7 @@ target_link_libraries(producer_consumer PRIVATE capy) include::example$producer-consumer/producer_consumer.cpp[tag=strand,indent=0] ---- -A `strand` is an executor adaptor that serializes execution. All coroutines dispatched through a strand are guaranteed not to run concurrently, making it safe to access shared state without explicit locking. Note that `async_event` is not thread-safe, so using a strand ensures safe access. +A cpp:strand[] is an executor adaptor that serializes execution. All coroutines dispatched through a strand are guaranteed not to run concurrently, making it safe to access shared state without explicit locking. cpp:async_event[] is not thread-safe, so using a strand ensures safe access. === The Event @@ -47,7 +43,7 @@ A `strand` is an executor adaptor that serializes execution. All coroutines disp include::example$producer-consumer/producer_consumer.cpp[tag=event,indent=0] ---- -`async_event` is a manual-reset signaling mechanism. One task can `set()` it; other tasks can `wait()` for it. When set, all current waiters resume, and the event stays set (later `wait()` calls return immediately) until `clear()` is called. +cpp:async_event[] is a manual-reset signaling mechanism. One task can `set()` it; other tasks can `wait()` for it. When set, all current waiters resume, and the event stays set (later `wait()` calls return immediately) until `clear()` is called. === Producer @@ -74,7 +70,7 @@ The consumer waits until the event is set. The `co_await data_ready.wait()` susp include::example$producer-consumer/producer_consumer.cpp[tag=run_both,indent=0] ---- -`when_all` runs both tasks concurrently within the same parent coroutine context, but the strand ensures they don't run at the same time on different threads. The producer signals `data_ready` when the value is set, and the consumer waits for the signal before reading. +cpp:when_all[] runs both tasks concurrently within the same parent coroutine context, but the strand ensures they don't run at the same time on different threads. The producer signals `data_ready` when the value is set, and the consumer waits for the signal before reading. === Completion Synchronization @@ -89,6 +85,7 @@ The `std::latch` ensures `main()` waits for the tasks to complete before returni == Output +[role=output] ---- Producer: preparing data... Producer: data ready, signaling diff --git a/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc b/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc index 19093b3c9..5f864eded 100644 --- a/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc +++ b/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc @@ -1,18 +1,14 @@ = Buffer Composition +:page-mode: how-to Composing buffer sequences without allocation for scatter/gather I/O. -== What You Will Learn +== What This Example Shows * Creating buffers from different sources * Using `std::array` and `std::array` for scatter/gather I/O * Zero-allocation buffer sequence patterns -== Prerequisites - -* Completed xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] -* Understanding of buffer types from xref:../5.buffers/5b.types.adoc[Buffer Types] - == Source Code [source,cpp] @@ -22,10 +18,10 @@ include::example$buffer-composition/buffer_composition.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(buffer_composition buffer_composition.cpp) -target_link_libraries(buffer_composition PRIVATE capy) +target_link_libraries(buffer_composition PRIVATE Boost::capy) ---- == Walkthrough @@ -37,7 +33,7 @@ target_link_libraries(buffer_composition PRIVATE capy) include::example$buffer-composition/buffer_composition.cpp[tag=make_buffer,indent=0] ---- -`make_buffer` creates buffer views from various sources. No data is copied—the buffers reference the original storage. +cpp:make_buffer[] creates buffer views from various sources. No data is copied—the buffers reference the original storage. === Two-Buffer Scatter/Gather @@ -46,7 +42,7 @@ include::example$buffer-composition/buffer_composition.cpp[tag=make_buffer,inden include::example$buffer-composition/buffer_composition.cpp[tag=two_buffer,indent=0] ---- -Capy's buffer-sequence concepts accept any range of `const_buffer` or `mutable_buffer`, so `std::array` is a buffer sequence with no further wrapping required. Use `mutable_buffer` for receive paths. +Capy's buffer-sequence concepts accept any range of cpp:const_buffer[] or cpp:mutable_buffer[], so `std::array` is a buffer sequence with no further wrapping required. Use `mutable_buffer` for receive paths. === Multi-Buffer Arrays @@ -72,6 +68,7 @@ When you write a buffer sequence, the OS receives all buffers in a single system == Output +[role=output] ---- === Single Buffer Examples === @@ -102,7 +99,7 @@ Prepared 2 buffers with 128 bytes total capacity == Exercises -1. Create a function that takes any `ConstBufferSequence` and prints its contents +1. Create a function that takes any cpp:ConstBufferSequence[] and prints its contents 2. Implement a simple message framing protocol using buffer composition == Next Steps diff --git a/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc b/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc index 69e710cd4..f104e9f59 100644 --- a/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc +++ b/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc @@ -1,17 +1,13 @@ = Mock Stream Testing +:page-mode: how-to Unit testing protocol code with mock streams and error injection. -== What You Will Learn +== What This Example Shows -* Using `test::read_stream` and `test::write_stream` -* Error injection with `fuse` -* Synchronous testing with `run_blocking` - -== Prerequisites - -* Completed xref:8.examples/8c.buffer-composition.adoc[Buffer Composition] -* Understanding of streams from xref:../6.streams/6b.streams.adoc[Streams] +* Using cpp:test::read_stream[test::read_stream] and cpp:test::write_stream[test::write_stream] +* Error injection with cpp:test::fuse[fuse] +* Synchronous testing with cpp:test::run_blocking[run_blocking] == Source Code @@ -22,10 +18,10 @@ include::example$mock-stream-testing/mock_stream_testing.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(mock_stream_testing mock_stream_testing.cpp) -target_link_libraries(mock_stream_testing PRIVATE capy) +target_link_libraries(mock_stream_testing PRIVATE Boost::capy) ---- == Walkthrough @@ -37,7 +33,7 @@ target_link_libraries(mock_stream_testing PRIVATE capy) include::example$snippets/8d_mock_stream_testing.cpp[tag=mock_streams,indent=0] ---- -`test::stream` is a bidirectional mock that satisfies both `ReadStream` and `WriteStream`: +cpp:test::stream[test::stream] is a bidirectional mock that satisfies both cpp:ReadStream[] and cpp:WriteStream[]: * Streams are obtained in connected pairs from `make_stream_pair(f)`; the shared `fuse` enables error injection * `provide(data)` — Supplies data for the peer's reads @@ -51,7 +47,7 @@ include::example$snippets/8d_mock_stream_testing.cpp[tag=mock_streams,indent=0] include::example$mock-stream-testing/mock_stream_testing.cpp[tag=any_stream,indent=0] ---- -Use pointer construction (`&a`) so the `any_stream` wrapper references the stream end without taking ownership. This allows inspecting `b.data()` after operations. +Use pointer construction (`&a`) so the cpp:any_stream[] wrapper references the stream end without taking ownership. This allows inspecting `b.data()` after operations. === Synchronous Testing @@ -79,6 +75,7 @@ This systematically tests all error handling paths. == Output +[role=output] ---- Test: happy path PASSED @@ -95,7 +92,7 @@ All tests passed! 1. Add a test for EOF handling (what if input doesn't end with newline?) 2. Test with different max_read_size values -3. Add a test for write errors using `test::write_stream` +3. Add a test for write errors using cpp:test::write_stream[test::write_stream] == Next Steps diff --git a/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc b/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc index 0da7277d7..f9937a738 100644 --- a/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc +++ b/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc @@ -1,18 +1,14 @@ = Type-Erased Echo +:page-mode: how-to Echo server demonstrating the compilation firewall pattern. -== What You Will Learn +== What This Example Shows -* Using `any_stream` for transport-independent code +* Using cpp:any_stream[] for transport-independent code * Physical isolation through separate compilation * Build time benefits of type erasure -== Prerequisites - -* Completed xref:8.examples/8d.mock-stream-testing.adoc[Mock Stream Testing] -* Understanding of type erasure from xref:../6.streams/6f.isolation.adoc[Physical Isolation] - == Source Code === echo.hpp @@ -38,10 +34,10 @@ include::example$type-erased-echo/main.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_library(echo_lib echo.cpp) -target_link_libraries(echo_lib PUBLIC capy) +target_link_libraries(echo_lib PUBLIC Boost::capy) add_executable(echo_demo main.cpp) target_link_libraries(echo_demo PRIVATE echo_lib) @@ -56,7 +52,7 @@ target_link_libraries(echo_demo PRIVATE echo_lib) include::example$type-erased-echo/echo.hpp[tag=session_decl] ---- -The header declares only the signature. It includes `any_stream` and `task`, but no concrete transport types. +The header declares only the signature. It includes cpp:any_stream[] and cpp:task[], but no concrete transport types. Clients of this header: @@ -89,6 +85,7 @@ This scales: in large projects, changes to implementation files don't cascade th == Output +[role=output] ---- Echo output: Hello, World! ---- diff --git a/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc index 9bf9edc54..f02b1d937 100644 --- a/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc +++ b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc @@ -1,21 +1,17 @@ = Timeout with Cancellation +:page-mode: how-to Racing a slow operation against a deadline, and cancelling work directly with a stop token. -== What You Will Learn +== What This Example Shows * Where timed operations belong in the layering * Using `async_waker` as the escape hatch for external timing -* Racing a fetch against a deadline with `when_any` +* Racing a fetch against a deadline with cpp:when_any[] * Checking `stop_requested()` in coroutines * Direct cancellation with `std::stop_source` -== Prerequisites - -* Completed xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] -* Understanding of stop tokens from xref:../4.coroutines/4e.cancellation.adoc[Cancellation] - == Source Code [source,cpp] @@ -25,10 +21,10 @@ include::example$timeout-cancellation/timeout_cancellation.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(timeout_cancellation timeout_cancellation.cpp) -target_link_libraries(timeout_cancellation PRIVATE capy) +target_link_libraries(timeout_cancellation PRIVATE Boost::capy) ---- == Walkthrough @@ -36,10 +32,10 @@ target_link_libraries(timeout_cancellation PRIVATE capy) === Where Timing Lives Capy is the coroutine layer: no reactor, no clock, no hidden threads. -Every thread Capy touches is one you handed it, a `thread_pool` worker, -or the thread that calls `run`. Timing needs a clock and something to -sleep on it, and that belongs to the I/O layer, where a reactor and -platform clock already exist for other reasons: Corosio provides +Every thread Capy touches is one you handed it, a cpp:thread_pool[] worker, +or the thread that calls cpp:run[]. Timing needs a clock and something to +sleep on it. That belongs to the I/O layer, where a reactor and +platform clock already exist for other reasons. Corosio provides `delay()` and `timeout()` built on its event loop. In pure Capy, with no Corosio in the picture, the user's own thread @@ -49,27 +45,27 @@ plays the clock, and `async_waker` gives it a suspension point to wake. `async_waker` is Capy's answer to "how do I wait for something that isn't a coroutine?" It hands a single wakeup from any thread to one waiting -coroutine: one coroutine suspends in `wait()`; any thread, including one -you spun up yourself to play the role of a clock, wakes it with `wake()`: +coroutine. One coroutine suspends in cpp:async_waker::wait[wait]`()`. Any thread, including one +you spun up yourself to play the role of a clock, wakes it with cpp:async_waker::wake[wake]`()`: [source,cpp] ---- include::example$timeout-cancellation/timeout_cancellation.cpp[tag=fetch_channel] ---- -`wait()` returns an `io_result<>` that is empty on wakeup and carries -`cond::canceled` if the environment's stop token fires first. A wakeup +cpp:async_waker::wait[wait]`()` returns an cpp:io_result[io_result<>] that is empty on wakeup and carries +cpp:cond::canceled[cond::canceled] if the environment's stop token fires first. A wakeup with no waiter present is latched as one pending token, so the -wake-before-wait race is benign; extra wakes collapse into that single +wake-before-wait race is benign. Extra wakes collapse into that single token rather than queuing up. Because `wake()` is the only operation callable from a foreign thread, the pattern generalizes to anything -external: a timer, a hardware interrupt, a completion callback from -another library. +external. Examples are a timer, a hardware interrupt, a completion +callback from another library. === Racing a Fetch Against a Deadline -`demo_timeout` builds two `io_task` children, each just a waker wait, and -races them with `when_any`: +`demo_timeout` builds two cpp:io_task[] children, each just a waker wait, and +races them with cpp:when_any[]: [source,cpp] ---- @@ -79,12 +75,12 @@ include::example$timeout-cancellation/timeout_cancellation.cpp[tag=race,indent=0 `await_fetch` completes when the fetch worker thread finishes and wakes `fetch_ch.fetch_ready`. `deadline` completes when a second user thread, playing the clock, sleeps for the allotted duration and wakes -`deadline_waker`. Whichever wakes first wins; -`when_any` requests stop on the loser, which is exactly how `await_fetch` +`deadline_waker`. Whichever wakes first wins. +cpp:when_any[] requests stop on the loser, which is exactly how `await_fetch` learns to set `cancelled` and let the fetch worker thread bail out early. -`async_waker::wait()` must only be awaited on a single-threaded -executor, so both children run on `thread_pool(1)`. The slow, blocking +cpp:async_waker::wait[wait]`()` must only be awaited on a single-threaded +executor, so both children run on cpp:thread_pool[]`(1)`. The slow, blocking work itself still happens off that thread, on the two `std::thread` objects the demo owns directly. @@ -95,7 +91,7 @@ objects the demo owns directly. include::example$timeout-cancellation/timeout_cancellation.cpp[tag=get_stop_token,indent=0] ---- -Inside a task, `this_coro::stop_token` retrieves the stop token propagated from the caller. You can also access it through the full environment via `co_await this_coro::environment`. +Inside a task, cpp:this_coro::stop_token[this_coro::stop_token] retrieves the stop token propagated from the caller. You can also access it through the full environment via `co_await this_coro::environment`. === Checking for Cancellation @@ -126,6 +122,7 @@ Cancellation doesn't have to throw. You can return partial results or a sentinel == Output +[role=output] ---- Demo: Fetch races a deadline Completed step 0 diff --git a/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc b/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc index 02dcda3e4..5f4710273 100644 --- a/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc +++ b/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc @@ -1,18 +1,14 @@ = Parallel Fetch +:page-mode: how-to -Running multiple operations concurrently with `when_all`. +Running multiple operations concurrently with cpp:when_all[]. -== What You Will Learn +== What This Example Shows -* Using `when_all` to run tasks in parallel +* Using cpp:when_all[] to run tasks in parallel * Structured bindings for results * Error propagation in concurrent tasks -== Prerequisites - -* Completed xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] -* Understanding of `when_all` from xref:../4.coroutines/4f.composition.adoc[Composition] - == Source Code [source,cpp] @@ -22,10 +18,10 @@ include::example$parallel-fetch/parallel_fetch.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(parallel_fetch parallel_fetch.cpp) -target_link_libraries(parallel_fetch PRIVATE capy) +target_link_libraries(parallel_fetch PRIVATE Boost::capy) ---- == Walkthrough @@ -37,7 +33,7 @@ target_link_libraries(parallel_fetch PRIVATE capy) include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_dashboard,indent=0] ---- -`when_all` requires children returning `io_result`, so plain tasks are wrapped. All three run concurrently. The result is `io_result`, a single `ec` plus the flattened payloads in input order. +cpp:when_all[] requires children returning cpp:io_result[], so plain tasks are wrapped. All three run concurrently. The result is `io_result`, a single `ec` plus the flattened payloads in input order. === Void io_tasks @@ -46,7 +42,7 @@ include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_dashboard,indent include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_void,indent=0] ---- -`io_task<>` children return `io_result<>` (just an error code, no payload). Check `r.ec` to detect failure. +cpp:io_task[io_task<>] children return cpp:io_result[io_result<>] (just an error code, no payload). Check `r.ec` to detect failure. === Error Propagation @@ -55,10 +51,11 @@ include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_void,indent=0] include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_errors,indent=0] ---- -I/O errors are reported via `ec` in the `io_result`. Thrown exceptions are captured separately — Upon error cancellation is requested and the first exception is rethrown after all tasks complete. +I/O errors are reported via `ec` in the cpp:io_result[io_result]. Thrown exceptions are captured separately — Upon error cancellation is requested and the first exception is rethrown after all tasks complete. == Output +[role=output] ---- === Fetching dashboard for: alice === Fetching user ID for: alice diff --git a/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc deleted file mode 100644 index 146aaae22..000000000 --- a/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc +++ /dev/null @@ -1,104 +0,0 @@ -= Echo Server with Corosio - -A complete echo server using Corosio for real network I/O. - -== What You Will Learn - -* Integrating Capy with Corosio networking -* Accepting TCP connections with `tcp_acceptor` -* Handling multiple clients concurrently - -== Prerequisites - -* Corosio library installed -* Understanding of TCP networking basics - -== Source Code - -[source,cpp] ----- -include::example$echo-server-corosio/echo_server.cpp[tag=full] ----- - -== Build - -[source,cmake] ----- -add_executable(echo_server echo_server.cpp) -target_link_libraries(echo_server PRIVATE Boost::capy Boost::corosio) ----- - -== Walkthrough - -=== TCP Acceptor - -[source,cpp] ----- -include::example$echo-server-corosio/echo_server.cpp[tag=acceptor,indent=0] ----- - -The `io_context` drives all asynchronous I/O. The `tcp_acceptor` listens on the specified port. Corosio uses a flat namespace -- types like `tcp_socket`, `tcp_acceptor`, and `endpoint` live directly in `boost::corosio`. - -=== Accept Loop - -[source,cpp] ----- -include::example$echo-server-corosio/echo_server.cpp[tag=accept,indent=0] ----- - -The accept loop runs forever, creating a new `tcp_socket` for each connection. `acc.accept(peer)` suspends the coroutine until a client connects. - -=== Echo Session - -[source,cpp] ----- -include::example$echo-server-corosio/echo_server.cpp[tag=session_io,indent=0] ----- - -Each session reads data with `read_some` and writes it back with `write`. When the client disconnects, `read_some` returns an error and the loop exits. - -=== Concurrent Clients - -[source,cpp] ----- -include::example$echo-server-corosio/echo_server.cpp[tag=spawn_session,indent=0] ----- - -Each accepted connection moves the socket into a new task via `run_async`. The coroutine owns the socket for the lifetime of the session. Multiple clients are handled concurrently on the same `io_context`. - -== Testing - -Start the server: - ----- -$ ./echo_server 8080 -Listening on port 8080 ----- - -Connect with netcat: - ----- -$ nc localhost 8080 -Hello -Hello -World -World -^C ----- - -Server output: - ----- -Listening on port 8080 -Connection from 127.0.0.1:54321 ----- - -== Exercises - -1. Add a connection limit with graceful rejection -2. Implement a simple command protocol (e.g., ECHO, QUIT, STATS) -3. Add TLS support using Corosio's TLS streams - -== Next Steps - -* xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] -- Erased streams behind a uniform interface diff --git a/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc b/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc index 8eac01203..bdaf84f0e 100644 --- a/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc +++ b/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc @@ -1,17 +1,13 @@ = Strand Serialization +:page-mode: how-to Protecting shared state with a strand instead of a mutex. -== What You Will Learn +== What This Example Shows -* Using a `strand` to serialize coroutine access to shared state +* Using a cpp:strand[] to serialize coroutine access to shared state * Lock-free shared state management -* Combining `when_all` with strand-based serialization - -== Prerequisites - -* Completed xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] (introduces `strand`) -* Understanding of `when_all` from xref:../4.coroutines/4f.composition.adoc[Composition] +* Combining cpp:when_all[] with strand-based serialization == Source Code @@ -22,7 +18,7 @@ include::example$strand-serialization/strand_serialization.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(strand_serialization strand_serialization.cpp) target_link_libraries(strand_serialization PRIVATE Boost::capy) @@ -37,7 +33,7 @@ target_link_libraries(strand_serialization PRIVATE Boost::capy) include::example$strand-serialization/strand_serialization.cpp[tag=strand,indent=0] ---- -A `strand` wraps an executor and guarantees that handlers dispatched through it never run concurrently. This replaces the need for a mutex when protecting shared state accessed by coroutines. +A cpp:strand[] wraps an executor and guarantees that handlers dispatched through it never run concurrently. This replaces the need for a mutex when protecting shared state accessed by coroutines. === Lock-Free Shared Access @@ -55,10 +51,11 @@ Multiple coroutines increment the same `counter` without any locks. The strand s include::example$strand-serialization/strand_serialization.cpp[tag=run_on_strand,indent=0] ---- -Passing the strand `s` to `run_async` ensures the entire coroutine tree executes through the strand. Even though the underlying `thread_pool` has 4 threads, the strand constrains execution to one coroutine at a time. +Passing the strand `s` to cpp:run_async[] ensures the entire coroutine tree executes through the strand. Even though the underlying cpp:thread_pool[] has 4 threads, the strand constrains execution to one coroutine at a time. == Output +[role=output] ---- Coroutine 0 finished, counter = 1000 Coroutine 1 finished, counter = 2000 diff --git a/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc index 660d6d383..4171946a1 100644 --- a/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc +++ b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc @@ -1,17 +1,14 @@ = Async Mutex +:page-mode: how-to -Fair FIFO coroutine locking with `async_mutex`. +Fair FIFO coroutine locking with cpp:async_mutex[]. -== What You Will Learn +== What This Example Shows -* Using `async_mutex` for mutual exclusion between coroutines +* Using cpp:async_mutex[] for mutual exclusion between coroutines * RAII lock guards with `scoped_lock` * FIFO fairness guarantees -* Comparing `async_mutex` to strand-based serialization - -== Prerequisites - -* Completed xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] +* Comparing cpp:async_mutex[] to strand-based serialization == Source Code @@ -22,7 +19,7 @@ include::example$async-mutex/async_mutex.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(async_mutex async_mutex.cpp) target_link_libraries(async_mutex PRIVATE Boost::capy) @@ -37,7 +34,7 @@ target_link_libraries(async_mutex PRIVATE Boost::capy) include::example$async-mutex/async_mutex.cpp[tag=mutex,indent=0] ---- -`async_mutex` is a coroutine-aware mutex. Unlike `std::mutex`, it suspends the calling coroutine instead of blocking the thread, allowing other coroutines to run while waiting for the lock. +cpp:async_mutex[] is a coroutine-aware mutex. Unlike `std::mutex`, it suspends the calling coroutine instead of blocking the thread, allowing other coroutines to run while waiting for the lock. === Scoped Lock @@ -46,18 +43,19 @@ include::example$async-mutex/async_mutex.cpp[tag=mutex,indent=0] include::example$async-mutex/async_mutex.cpp[tag=scoped_lock,indent=0] ---- -`scoped_lock()` returns an `io_result` with an error code and an RAII guard. The guard automatically releases the lock when it goes out of scope. If the operation is canceled (e.g., via a stop token), `ec` will be set. +`scoped_lock()` returns an cpp:io_result[] with an error code and an RAII guard. The guard automatically releases the lock when it goes out of scope. If the operation is canceled (e.g., via a stop token), `ec` is set. === FIFO Fairness -Workers acquire the lock in the order they request it. Unlike `std::mutex`, which has no fairness guarantees, `async_mutex` ensures FIFO ordering -- the first coroutine to call `scoped_lock()` is the first to acquire it. +Workers acquire the lock in the order they request it. Unlike `std::mutex`, which has no fairness guarantees, cpp:async_mutex[] ensures FIFO ordering -- the first coroutine to call `scoped_lock()` is the first to acquire it. === Strand vs Async Mutex -The strand serialization example showed how a strand can protect shared state by running all coroutines sequentially. `async_mutex` provides finer-grained control: coroutines run concurrently and only serialize when entering the critical section. +The strand serialization example showed how a strand can protect shared state by running all coroutines sequentially. cpp:async_mutex[] provides finer-grained control: coroutines run concurrently and only serialize when entering the critical section. == Output +[role=output] ---- Worker 0 waiting for lock Worker 0 acquired lock (sequence 0) @@ -85,7 +83,7 @@ Acquisition order: W0 -> W1 -> W2 -> W3 -> W4 -> W5 1. Add work outside the critical section (before and after `scoped_lock`) to observe concurrent execution 2. Use a stop token to cancel waiting workers after a timeout -3. Replace `async_mutex` with a strand and compare the two approaches +3. Replace cpp:async_mutex[] with a strand and compare the two approaches == Next Steps diff --git a/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc b/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc index a496bf320..e0ec65548 100644 --- a/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc @@ -1,17 +1,14 @@ = Parallel Tasks +:page-mode: how-to Distributing CPU-bound work across a thread pool and collecting results. -== What You Will Learn +== What This Example Shows -* Running tasks in parallel on a `thread_pool` -* Collecting results with `when_all` structured bindings +* Running tasks in parallel on a cpp:thread_pool[] +* Collecting results with cpp:when_all[] structured bindings * Observing thread IDs to verify parallel execution -== Prerequisites - -* Completed xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] (introduces `when_all`) - == Source Code [source,cpp] @@ -21,7 +18,7 @@ include::example$programs/8m_parallel_tasks_variadic.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(parallel_tasks parallel_tasks.cpp) target_link_libraries(parallel_tasks PRIVATE Boost::capy) @@ -45,7 +42,7 @@ The range `[0, 10000)` is divided into 4 equal chunks, one per task. Each task c include::example$programs/8m_parallel_tasks_variadic.cpp[tag=when_all_variadic,indent=0] ---- -`when_all` launches all four tasks concurrently on the thread pool. Each task may run on a different thread. The result is `io_result`, a single `ec` plus the four partial sums in input order. +cpp:when_all[] starts all four tasks concurrently on the thread pool. Each task may run on a different thread. The result is cpp:io_result[io_result]``, a single `ec` plus the four partial sums in input order. === Observing Thread IDs @@ -54,7 +51,7 @@ include::example$programs/8m_parallel_tasks_variadic.cpp[tag=when_all_variadic,i include::example$programs/8m_parallel_tasks_variadic.cpp[tag=thread_id,indent=0] ---- -Each task prints its thread ID. On a multi-core system you will see different thread IDs, confirming true parallel execution. The `ostringstream` ensures each line is printed atomically. +Each task prints its thread ID. On a multi-core system you see different thread IDs, confirming true parallel execution. The `ostringstream` ensures each line is printed atomically. === Verifying Results @@ -62,6 +59,7 @@ The sum of `[0, N)` is `N*(N-1)/2`. The example verifies that the sum of partial == Output +[role=output] ---- Dispatching 4 parallel tasks... range [0, 2500) on thread 140234567890432 diff --git a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc index 3f69d232a..5c9604975 100644 --- a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc +++ b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc @@ -1,17 +1,14 @@ = Custom Executor +:page-mode: how-to Implementing the Executor concept with a single-threaded run loop. -== What You Will Learn +== What This Example Shows -* Satisfying the `Executor` concept -* Implementing `execution_context`, `dispatch`, and `post` +* Satisfying the cpp:Executor[] concept +* Implementing cpp:execution_context[], cpp:Executor::dispatch[dispatch], and cpp:Executor::post[post] * Running Capy coroutines on a custom scheduling system -== Prerequisites - -* Understanding of executors from xref:../4.coroutines/4c.executors.adoc[Executors and Execution Contexts] - == Source Code [source,cpp] @@ -21,7 +18,7 @@ include::example$custom-executor/custom_executor.cpp[tag=full] == Build -[source,cmake] +[source,cmake,role=external] ---- add_executable(custom_executor custom_executor.cpp) target_link_libraries(custom_executor PRIVATE Boost::capy) @@ -36,17 +33,17 @@ target_link_libraries(custom_executor PRIVATE Boost::capy) include::example$custom-executor/custom_executor.cpp[tag=inherit] ---- -Custom execution contexts inherit from `execution_context` and pass `this` to the base constructor. The destructor must call `shutdown()` then `destroy()` to clean up coroutine state. +Custom execution contexts inherit from cpp:execution_context[] and pass `this` to the base constructor. The destructor must call `shutdown()` then `destroy()` to clean up coroutine state. === The Executor Concept The nested `executor_type` must provide: -* `context()` -- returns a reference to the owning `execution_context` -* `on_work_started()` / `on_work_finished()` -- work-tracking hooks -* `dispatch(c)` -- resume immediately if already on this context, otherwise enqueue. Takes a `continuation&` and returns `std::coroutine_handle<>`. +* cpp:Executor::context[context()] -- returns a reference to the owning cpp:execution_context[] +* cpp:Executor::on_work_started[on_work_started()] / cpp:Executor::on_work_finished[on_work_finished()] -- work-tracking hooks +* `dispatch(c)` -- resume immediately if already on this context, otherwise enqueue. Takes a cpp:continuation[]`&` and returns `std::coroutine_handle<>`. * `post(c)` -- always enqueue for later execution. Takes a `continuation&`. -* `operator==` -- compare two executors for identity +* cpp:Executor::operator==[+operator==+] -- compare two executors for identity [source,cpp] ---- @@ -73,15 +70,16 @@ include::example$custom-executor/custom_executor.cpp[tag=dispatch,indent=0] include::example$custom-executor/custom_executor.cpp[tag=drive,indent=0] ---- -`run_async` enqueues the initial coroutine. `loop.run()` drains the queue, resuming coroutines one by one until all work completes. This is analogous to a GUI event loop or game tick loop. +cpp:run_async[] enqueues the initial coroutine. `loop.run()` drains the queue, resuming coroutines one by one until all work completes. This is analogous to a GUI event loop or game tick loop. -Note that `run()` uses `capy::safe_resume(h)` instead of `h.resume()`. This saves and restores the thread-local frame allocator around each resumption, preventing coroutines from spoiling each other's allocator. All custom executor event loops must use `safe_resume` -- see xref:../4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation] for details. +`run()` uses `capy::safe_resume(h)` instead of `h.resume()`. This saves and restores the thread-local frame allocator around each resumption, preventing coroutines from spoiling each other's allocator. All custom executor event loops must use cpp:safe_resume[] -- see xref:../4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation] for details. == Output +[role=output] ---- Running event loop on main thread... -Launching 3 tasks with when_all... +Starting 3 tasks with when_all... computing 3 * 3 computing 7 * 7 computing 11 * 11 @@ -95,4 +93,4 @@ Event loop finished. 1. Add a `stop()` method that causes `run()` to exit early, even with work remaining 2. Make the run loop thread-safe so work can be posted from other threads -3. Integrate the run loop with a platform event system (e.g., `epoll`, `kqueue`, or a GUI framework) +3. Integrate the run loop with a platform event system (e.g., `epoll` or `kqueue`). For the GUI-framework case, xref:8.examples/8q.gui-integration.adoc[GUI Integration] works the example through, including where a coroutine resumes after awaiting work that finished on another thread diff --git a/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc b/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc index 71441e105..689a78bb3 100644 --- a/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc +++ b/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc @@ -1,33 +1,29 @@ = Bridging a P2300 Sender +:page-mode: how-to Awaiting a `std::execution` (P2300) sender from inside a Capy coroutine. -== What You Will Learn +== What This Example Shows * How to `co_await` a foreign awaitable that does not implement the xref:4.coroutines/4d.io-awaitable.adoc[IoAwaitable protocol] * How a bridge restores the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant] by posting the resumption through the caller's executor -* How a sender's value and error completion channels map onto `io_result` - -== Prerequisites - -* Completed xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] -* A `std::execution` implementation (this example uses https://github.com/bemanproject/execution[beman.execution]) +* How a sender's value and error completion channels map onto cpp:io_result[] [NOTE] ==== -This example is built only when `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` (it requires C++23 and fetches `beman.execution`). The full source is in `example/sender-bridge/`. +This example is built only when `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` (it requires C++23 and fetches https://github.com/bemanproject/execution[beman.execution]). The full source is in `example/sender-bridge/`. ==== == The Bridge -A P2300 sender is a foreign awaitable: it has no two-argument `await_suspend`, so a Capy `task` cannot `co_await` it directly. `await_sender` wraps it in an `IoAwaitable`. The key move is in the bridge's _receiver_: when the sender completes—on whatever thread its scheduler chose—it does not resume the coroutine inline. It stores the result and posts the continuation back through the caller's executor. From `example/sender-bridge/sender_awaitable.hpp`: +A P2300 sender is a foreign awaitable: it has no two-argument `await_suspend`, so a Capy cpp:task[] cannot `co_await` it directly. `await_sender` wraps it in an cpp:IoAwaitable[]. The key move is in the bridge's _receiver_: when the sender completes—on whatever thread its scheduler chose—it does not resume the coroutine inline. It stores the result and posts the continuation back through the caller's executor. From `example/sender-bridge/sender_awaitable.hpp`: [source,cpp] ---- include::example$sender-bridge/sender_awaitable.hpp[tag=set_value,indent=0] ---- -`await_sender` inspects the sender's completion signatures. If the sender can complete with `set_error(std::error_code)`, the bridge yields `io_result` so the error stays a value rather than an exception; otherwise it yields the value directly. +`await_sender` inspects the sender's completion signatures. If the sender can complete with `set_error(std::error_code)`, the bridge yields cpp:io_result[io_result] so the error stays a value rather than an exception. Otherwise the bridge yields the value directly. == Source Code @@ -38,8 +34,9 @@ include::example$sender-bridge/sender_bridge.cpp[tag=full] == Output -The sender runs on the `run_loop` thread, but the coroutine resumes on the `thread_pool` executor it was launched with—the invariant holds across the bridge: +The sender runs on the `run_loop` thread, but the coroutine resumes on the cpp:thread_pool[] executor it was started with—the invariant holds across the bridge: +[role=output] ---- main thread: 139667952822976 sender running on thread 139667946014400 diff --git a/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc index a612213a0..3dee054c2 100644 --- a/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc +++ b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc @@ -1,18 +1,14 @@ = Calling Asio from a Capy Coroutine +:page-mode: how-to Using Boost.Asio async operations directly inside a Capy coroutine through a `use_capy` completion token. -== What You Will Learn +== What This Example Shows * How a completion token adapts _any_ Asio async operation into an xref:4.coroutines/4d.io-awaitable.adoc[IoAwaitable] * How the token bridges `std::stop_token` to Asio's cancellation slot * How the bridge preserves the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant] -== Prerequisites - -* Completed xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] -* Boost.Asio available to the build - [NOTE] ==== This example is built when a `Boost::asio` target is available. The full source is in `example/asio/` (`use_capy_example.cpp` and the reusable token in `api/use_capy.hpp`). @@ -20,7 +16,7 @@ This example is built when a `Boost::asio` target is available. The full source == The Completion Token -Asio async operations accept a _completion token_ that decides what the call returns. `use_capy` is a token whose `async_result` returns an `IoAwaitable`. When co-awaited, its `await_suspend` starts the Asio operation and arranges the completion handler to post the resumption through the caller's executor—so the coroutine resumes on the executor it was launched with, never on whatever thread Asio completed on. From `example/asio/api/use_capy.hpp`: +Asio async operations accept a _completion token_ that decides what the call returns. `use_capy` is a token whose `async_result` returns an cpp:IoAwaitable[]. When co-awaited, its `await_suspend` starts the Asio operation and arranges the completion handler to post the resumption through the caller's executor. The coroutine therefore resumes on the executor it was started with, never on whatever thread Asio completed on. From `example/asio/api/use_capy.hpp`: [source,cpp] ---- @@ -29,17 +25,18 @@ include::example$asio/api/use_capy.hpp[tag=await_suspend,indent=0] == Using the Token -Pass `use_capy` where an Asio operation expects a completion token. The `co_await` yields an `io_result` carrying the error code and the operation's result: +Pass `use_capy` where an Asio operation expects a completion token. The `co_await` yields an cpp:io_result[] carrying the error code and the operation's result: [source,cpp] ---- include::example$asio/use_capy_example.cpp[tag=writer] ---- -The reader is symmetric, using `async_read_some`. `run_example` drives both concurrently over a connected socket pair with `when_all`, then reports completion. +The reader is symmetric, using `async_read_some`. `run_example` drives both concurrently over a connected socket pair with cpp:when_all[], then reports completion. == Output +[role=output] ---- writer: wrote 128 bytes (total 128) reader: read 128 bytes (total 128) diff --git a/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc b/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc new file mode 100644 index 000000000..622875ddb --- /dev/null +++ b/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc @@ -0,0 +1,186 @@ += GUI Integration +:page-mode: tutorial + +Running a Capy coroutine on a GUI framework's event loop, and resuming on the GUI thread to update widgets. + +== What This Example Shows + +* Wrapping a GUI toolkit's "run this on the main thread" primitive as an executor +* Moving slow work off the GUI thread with cpp:run[], and coming back +* Awaiting an operation the toolkit completes on a thread of its own +* Why widget access stays on the GUI thread with no hop written by hand + +== Source Code + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=full] +---- + +== Build + +[source,cmake,role=external] +---- +add_executable(gui_integration gui_integration.cpp) +target_link_libraries(gui_integration PRIVATE Boost::capy) +---- + +== Walkthrough + +=== A Toolkit Stand-In + +The program links no GUI library. It uses a stand-in instead. The binding to a real toolkit is a handful of lines that differ per toolkit, and the contract underneath is the same everywhere. That contract is what this page proves. + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=gui_app] +---- + +`gui_app` records the thread it is constructed on, holds a queue of coroutine handles, and exposes `post_to_gui_thread`. That last operation is the one primitive every toolkit already provides. Qt names it `QMetaObject::invokeMethod`, GTK `g_idle_add`, wxWidgets `wxEvtHandler::CallAfter`, Win32 `PostMessage`. + +=== The Event Loop + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=run,indent=0] +---- + +The loop blocks while the queue is empty. A GUI loop must do this: the background operation is still running, and an empty queue does not mean the program is finished. `quit()` sets the flag that lets the loop return once the queue drains. + +Resumption goes through cpp:safe_resume[] rather than `h.resume()`. This saves and restores the thread-local frame allocator around each resumption. See xref:4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation]. + +=== Wrapping the Primitive as an Executor + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=executor] +---- + +This class is the entire binding between Capy and the toolkit. `post` forwards to `post_to_gui_thread`. `dispatch` resumes inline when the caller is already on the GUI thread, and otherwise posts. + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=concept_check] +---- + +The `static_assert` confirms the class satisfies the concept before anything tries to run on it. + +=== A Widget That Checks Its Thread + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=label] +---- + +`label` stands in for `QLabel`, `GtkLabel`, or `wxStaticText`. Real widgets are not thread-safe. Touching one off the GUI thread is undefined behavior, and a real toolkit rarely tells you. This one tells you, on every access: + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=check] +---- + +The check is the program's reason to exist, so it must survive a release build. `assert` would compile out under `NDEBUG`. + +=== Work That Leaves the GUI Thread + +Slow work must not run on the GUI thread, or the interface stops repainting. Put it in a task of its own: + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=pool_task] +---- + +The check is the second half of the proof. The widget checks say the updates happen on the GUI thread; this one says the work does not. + +=== Where the Coroutine Resumes + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=coroutine] +---- + +This is the question a GUI developer needs answered. `count_rows` runs on the pool. The line after the first `co_await` touches a widget, so it must run on the GUI thread. It does, and the source contains no hop. The second `co_await` is the toolkit's dialog, covered next. + +cpp:run[] is what moves the work. It starts the inner task on the executor you name, and posts the awaiting coroutine back through the executor that coroutine was started with. The whole subtree under `count_rows` runs on the pool; only the boundary crosses back. + +Three contracts combine to put that boundary on the GUI thread: + +* cpp:run_async[] builds one cpp:io_env[] holding the executor it was given, and passes the task a pointer to it. +* cpp:task[] propagates that pointer into every `co_await` in its body. A task completes by symmetric transfer and never posts, so nothing in the chain substitutes a different executor. +* cpp:run[] records the caller's executor and posts the caller back through it when the inner task completes. That post runs on a pool thread and lands in the GUI queue. + +This is the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant] seen from a GUI application. Start the task on the GUI executor, and every line of the body runs on the GUI thread, whatever thread the awaited work ran on. + +[NOTE] +==== +Two conditions carry the invariant, and both are contracts rather than magic. + +The executor must resume handles on one thread only. A GUI loop does, by construction. + +Every awaitable in the chain must honor the cpp:IoAwaitable[] requirement to resume through cpp:io_env::executor[env->executor]. Capy's own types do: cpp:when_all[] and cpp:when_any[] return control to the caller through the caller's executor, and cpp:run[] does the same at an executor boundary. An awaitable that resumes the handle directly from a foreign thread breaks the invariant, and no other part of the library can repair it. <> shows one that honors it. +==== + +[#toolkit-completion] +=== A Completion From the Toolkit's Own Thread + +cpp:run[] covers work you hand to Capy. A toolkit also completes operations of its own, and it reports them on whichever thread it chose. A modal dialog is the common case: + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=dialog] +---- + +Awaiting that needs an cpp:IoAwaitable[]. xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] covers the protocol. One line of it decides thread affinity: + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=dialog_awaitable] +---- + +The toolkit's thread does not resume the coroutine. It posts the continuation through cpp:io_env::executor[env->executor], and the executor resumes it on the GUI thread. That is the second condition above, honored in one call. + +The widget update after this `co_await` carries the same thread check as every other access. Resume the handle directly instead of posting, and the check fires. + +`show_dialog` does not watch `env->stop_token`, so a stop request cannot interrupt a dialog already showing. Exercise 2 adds that. + +=== Starting and Stopping the Loop + +[source,cpp] +---- +include::example$gui-integration/gui_integration.cpp[tag=main] +---- + +The GUI thread is whichever thread constructs `gui_app`. Real toolkits impose the same rule on their application object. + +cpp:run_async[] is called on that thread, so `dispatch` resumes the task inline and the body reaches its `co_await` before `app.run()` is called. Nothing is lost if the pool finishes first: the continuation waits in the queue, and the loop picks it up when it starts. + +The completion handler runs on the GUI thread, because the task completed there. It calls `quit()`, the loop drains and returns, and `main` checks the final text. The program needs no sleep and no timeout to exit. + +=== Substituting a Real Toolkit + +The stand-in exists to prove the contract, not to model a toolkit. Five pieces change when the toolkit is real, and the rest does not: + +* `post_to_gui_thread` forwards to the toolkit's own post operation, and the receiving side calls cpp:safe_resume[] on the handle. +* `gui_app::run` disappears. The toolkit already owns a loop, such as `QApplication::exec`, `g_main_loop_run`, or `wxApp::OnRun`. +* `gui_app::quit` forwards to the toolkit's own quit, such as `QCoreApplication::quit`, `g_main_loop_quit`, or `wxAppConsole::ExitMainLoop`. +* `label` becomes a real widget. Its thread check is the part the toolkit does not do for you. +* `dialog` becomes the toolkit's own dialog, and its callback the toolkit's completion notification. The `gui_app&` it holds only for the thread check goes away. + +`executor_type`, `show_dialog`, `count_rows`, and `refresh` are unchanged. Those four are the pattern worth copying. + +== Output + +[role=output] +---- +[gui] label: Loading... +[gui] label: Loaded 42 rows +[gui] label: Confirmed: OK +[gui] event loop finished +---- + +== Exercises + +1. Break the invariant on purpose. Make `post` resume the handle inline instead of queueing it, and watch the widget check fire +2. Make `show_dialog` cancellable. Pass a `std::stop_token` to cpp:run_async[], request the stop before the dialog is awaited, and complete early in `await_suspend` when the token reports one +3. Give `dialog` a Cancel answer, and skip the update when the user chooses it +4. Add a second background step and a second widget, and check that both updates land on the GUI thread diff --git a/doc/modules/ROOT/pages/9.design/9.intro.adoc b/doc/modules/ROOT/pages/9.design/9.intro.adoc index a38010fcd..4e23e19d3 100644 --- a/doc/modules/ROOT/pages/9.design/9.intro.adoc +++ b/doc/modules/ROOT/pages/9.design/9.intro.adoc @@ -8,7 +8,21 @@ // = Design +:page-mode: explanation Capy's public interface--tasks, buffers, streams--is intentionally small. Behind that interface are design decisions that determine how concepts compose, where responsibility boundaries fall, and what guarantees the library can make. This section documents those decisions. -Each page examines one concept or facility in depth: its formal definition, the rationale behind its design, the alternatives that were considered, and the tradeoffs that were made. If you have ever wondered _why_ a particular concept requires a specific primitive, or why certain abstractions exist as separate concepts, the answers are here. These documents are reference material for library contributors and advanced users. They assume familiarity with the tutorial sections and focus on design reasoning rather than usage. +Each page examines one concept or facility in depth: the rationale behind its design, the alternatives that were considered, and the tradeoffs that were made. Formal definitions live in the header reference; these pages link to `cpp:` symbols rather than repeat them. If you have ever wondered _why_ a particular concept requires a specific primitive, or why certain abstractions exist as separate concepts, the answers are here. These documents are reference material for library contributors and advanced users. They assume familiarity with the tutorial sections and focus on design reasoning rather than usage. + +== What This Section Covers + +* xref:9.design/9a.CapyLayering.adoc[Layered Abstractions] -- Why Capy offers templates, virtual dispatch, and type erasure as separate layers instead of one abstraction level. +* xref:9.design/9b.Separation.adoc[Why Capy Is Separate] -- Why Capy and Corosio are two libraries instead of one. +* xref:9.design/9c.ReadStream.adoc[ReadStream] -- Why `read_some` is the fundamental partial-read primitive. +* xref:9.design/9f.WriteStream.adoc[WriteStream] -- Why `write_some` is the fundamental partial-write primitive, and when it outperforms `write_now`. +* xref:9.design/9i.TypeEraseAwaitable.adoc[Type-Erasing Awaitables] -- How the `any_*` wrappers achieve zero steady-state allocation. +* xref:9.design/9k.Executor.adoc[Executor] -- The `Executor` concept's relationship to Asio, and why `dispatch` returns a coroutine handle. +* xref:9.design/9l.RunApi.adoc[Run API] -- Why `run_async` and `run` use a two-phase `f(context)(task)` call syntax. +* xref:9.design/9m.WhyNotCobalt.adoc[Why Not Cobalt?] -- How Capy's foundation differs from Boost.Cobalt's Asio-based design. +* xref:9.design/9n.WhyNotCobaltConcepts.adoc[Why Not Cobalt Concepts?] -- A side-by-side look at type-erased write-stream algorithms in both libraries. +* xref:9.design/9o.WhyNotTMC.adoc[Why Not TooManyCooks?] -- Choosing between Capy and TooManyCooks by workload: network I/O versus compute. diff --git a/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc index 56023bdab..c4a95014d 100644 --- a/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc +++ b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc @@ -8,6 +8,7 @@ // = Layered Abstractions +:page-mode: explanation {cpp} async libraries have traditionally forced users into a single abstraction level, and every choice comes with baggage. You go with templates and you get zero overhead, full optimization, and unreadable error messages that scroll for pages. Compile times explode. You cannot hide implementation behind a compilation boundary, so you have no ABI stability. You go with virtual dispatch and you get readable code, stable ABIs, and a runtime cost that every call path pays whether it needs to or not. @@ -20,25 +21,20 @@ One abstraction level cannot serve all of these needs simultaneously. The insigh Capy offers three layers. They coexist. They interoperate. Users pick the one that matches their constraints. -The first layer is concepts. These are the template-based interfaces: `ReadStream`, `WriteStream`, `Stream`. Algorithms written against concepts get full optimization. The compiler sees through everything. There is no indirection, no vtable, no allocation overhead. This is what you use for hot inner loops, for protocol parsing, for any path where performance dominates: - -[source,cpp] ----- -include::example$snippets/9a_capy_layering.cpp[tag=write_signature,indent=0] ----- +The first layer is concepts. These are the template-based interfaces: cpp:ReadStream[], cpp:WriteStream[], cpp:Stream[]. Algorithms written against concepts get full optimization. The compiler sees through everything. There is no indirection, no vtable, no allocation overhead. This is what you use for hot inner loops, for protocol parsing, for any path where performance dominates. cpp:write[] is one example: a free function template constrained by cpp:WriteStream[] and cpp:ConstBufferSequence[]. The cost is that templates propagate. Every caller sees the full implementation. Compile times grow. You cannot hide this behind a `.cpp` file. -The second layer is type-erased wrappers. `any_stream`, `any_read_stream`, `any_write_stream`. These use a vtable internally, similar to `std::function` but specialized for I/O. You can write an algorithm against `any_stream&` and it compiles once, lives in a single translation unit, and works with any stream type: +The second layer is type-erased wrappers. cpp:any_stream[], cpp:any_read_stream[], cpp:any_write_stream[]. These use a vtable internally, similar to `std::function` but specialized for I/O. You can write an algorithm against `any_stream&` and it compiles once, lives in a single translation unit, and works with any stream type: [source,cpp] ---- include::example$snippets/9a_capy_layering.cpp[tag=any_stream_echo,indent=0] ---- -The cost is a virtual call per I/O operation. For operations dominated by syscalls and network latency, this cost is invisible. For tight loops over in-memory buffers, it matters. +The cost is up to five vtable calls per I/O operation, not one per continuation -- four when the read or write completes synchronously, since `await_suspend` is skipped whenever `await_ready` returns `true`. For operations dominated by syscalls and network latency, this cost is invisible. For tight loops over in-memory buffers, it matters. -The third layer is coroutine type erasure via `task<>`. This is the most powerful form of type erasure in the language. Inside a coroutine, when you write `co_await`, everything in the awaitable becomes type-erased from the perspective of the caller. The caller sees a `task<>`. The implementation is invisible. A pure virtual function returning `task<>` hides the stream type, the buffer strategy, the algorithm, the error handling - everything: +The third layer is coroutine type erasure via cpp:task[task<>]. This is the most powerful form of type erasure in the language. Inside a coroutine, when you write `co_await`, everything in the awaitable becomes type-erased from the perspective of the caller. The caller sees a `task<>`. The implementation is invisible. A pure virtual function returning `task<>` hides the stream type, the buffer strategy, the algorithm, the error handling - everything: [source,cpp] ---- @@ -69,7 +65,7 @@ The pump mechanism extends this by allowing multiple inline completions before r The trade-off is P99 latency. While the pump is running inline completions, queued work waits. For latency-sensitive workloads, you want to return to the queue more frequently so that every piece of work gets prompt attention. The pump is configurable. You can disable it entirely for HFT-style workloads that care about tail latency, or let it ramp up for servers that care about throughput. -The frame recycler is a per-thread cache of coroutine frames. Chain workloads that allocate and free frames in sequence benefit from this cache. Fan-out workloads that spawn many concurrent tasks can exhaust it. The `right_now` pattern addresses this for repeated invocations of the same operation: declare a stack object with a one-element frame cache, and repeated calls reuse that cache without touching the recycler at all. `when_all` could carry its own private frame cache sized to its arity, giving each child a frame from the parent's stash via a TLS hook. Every use case that you make better can make another use case worse. You have to pay attention to that which is not seen. +The frame recycler is a per-thread cache of coroutine frames. Chain workloads that allocate and free frames in sequence benefit from this cache. Fan-out workloads that start many concurrent tasks can exhaust it. The `write_now` pattern addresses this for repeated invocations of the same operation: declare a stack object with a one-element frame cache, and repeated calls reuse that cache without touching the recycler at all. cpp:when_all[] could carry its own private frame cache sized to its arity, giving each child a frame from the parent's stash via a TLS hook. Every use case that you make better can make another use case worse. You have to pay attention to that which is not seen. == The Type System as Architecture @@ -78,6 +74,7 @@ The derived class pattern is the practical application of everything described a Each derived class lives in its own translation unit. The linker only pulls in what is used. Users who need only TCP link only TCP code. Users who need SSL link the SSL translation unit. No variant that pulls in all transport code. No enum and switch that ties everything together. The type system enforces the separation: +.Design sketch [source,cpp,role=pseudocode] ---- // User who needs only plain TCP @@ -92,19 +89,19 @@ multi_connection conn(ctx, config); This extends naturally to testing. Derive a mock connection that uses Capy's test stream with a fuse for error injection, and a mock timer for deterministic time control. The base class algorithm runs against the mock exactly as it would against a real connection. No conditional compilation, no test-only code paths in production logic, no `#ifdef TESTING`. -A database library built this way can express protocol parsing with zero-copy buffer sinks for the hot path, implement connection logic against type-erased streams for maintainability, let users select TCP vs. SSL vs. Unix at the type level for linker efficiency, and test without linking OpenSSL or running a real server. The hot paths use concepts. The cold paths use virtual dispatch. The architectural boundaries use `task<>`. Every user finds the abstraction level they need. +A database library built this way can express protocol parsing with zero-copy buffer sinks for the hot path, implement connection logic against type-erased streams for maintainability, let users select TCP vs. SSL vs. Unix at the type level for linker efficiency, and test without linking OpenSSL or running a real server. The hot paths use concepts. The cold paths use virtual dispatch. The architectural boundaries use cpp:task[task<>]. Every user finds the abstraction level they need. == Choosing the Right Layer The question that matters is: can a library author look at their problem and immediately see which layer to use? If the answer is yes, the design is working. If they have to think about it, something is wrong. -**Protocol parsing:** use the `ReadStream` and `WriteStream` concepts as template parameters. Zero overhead. Call member functions whose awaitables do all the work, with no coroutine frame allocation. The compiler optimizes everything. +**Protocol parsing:** use the cpp:ReadStream[] and cpp:WriteStream[] concepts as template parameters. Zero overhead. Call member functions whose awaitables do all the work, with no coroutine frame allocation. The compiler optimizes everything. **Connection management:** use concrete types like `tcp_socket`. These give you `connect()` and `shutdown()` - the operations that are transport-specific. But the concrete type is derived from `io_stream`, a class that models `capy::Stream`, so you can pass `io_stream&` to a non-template function for the business logic that sits on top of the connection. -**Full transport abstraction across a library boundary:** use `any_stream`. Complete type erasure, but you lose connection management - there is no `connect()` on an `any_stream`. This means you have to carefully arrange your code so it genuinely requires a physical separation in the Lakos sense. The protocol logic and the connection logic live in separate components, and the type-erased boundary sits between them. +**Full transport abstraction across a library boundary:** use cpp:any_stream[]. Complete type erasure, but you lose connection management - there is no `connect()` on an `any_stream`. This means you have to carefully arrange your code so it genuinely requires a physical separation in the Lakos sense. The protocol logic and the connection logic live in separate components, and the type-erased boundary sits between them. -The layers compose. An algorithm written against a `ReadStream` concept can be called from inside a coroutine that is type-erased behind a `task<>`, which is dispatched through a virtual function on a base class that holds an `any_stream&`. Each layer handles its part. Nothing leaks through the boundaries unless you want it to. +The layers compose. An algorithm written against a cpp:ReadStream[] concept can be called from inside a coroutine that is type-erased behind a cpp:task[task<>], which is dispatched through a virtual function on a base class that holds an cpp:any_stream[]`&`. Each layer handles its part. Nothing leaks through the boundaries unless you want it to. This is what it means when we say the user chooses. Capy provides the tools. The user decides where the boundaries go based on what they know about their performance requirements, their compilation budget, and their architecture. The library does not impose a single answer because there is not one. diff --git a/doc/modules/ROOT/pages/9.design/9b.Separation.adoc b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc index 4f4d3846c..3487ef051 100644 --- a/doc/modules/ROOT/pages/9.design/9b.Separation.adoc +++ b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc @@ -8,6 +8,7 @@ // = Why Capy Is Separate +:page-mode: explanation "Why are Capy and Corosio two separate libraries? Why not just put everything in one place?" @@ -18,9 +19,9 @@ This document applies well-established physical design principles to show why th == What Lives Where -**Capy** provides the foundational abstractions for coroutine-based I/O. Tasks. Buffers. Stream concepts. Executors. The IoAwaitable protocol. Type-erased streams. Composition primitives like `when_all` and `when_any`. It is pure {cpp}20. It does not include a single line of platform-specific code. No sockets. No file descriptors. No `#ifdef _WIN32`. +**Capy** provides the foundational abstractions for coroutine-based I/O. Tasks. Buffers. Stream concepts. Executors. The IoAwaitable protocol. Type-erased streams. Composition primitives like cpp:when_all[] and cpp:when_any[]. It is pure {cpp}20. It has no platform-specific I/O code. No sockets. No file descriptors. No OS event loop. -**Corosio** provides platform networking. TCP sockets. TLS streams. DNS resolution. Delays and timeouts. Signal handling. It implements four platform-specific event loop backends: IOCP on Windows, epoll on Linux, kqueue on macOS/BSD, and POSIX select as a fallback. Corosio depends on Capy. Capy does not depend on Corosio. +**Corosio** provides platform networking. TCP sockets. TLS streams. DNS resolution. Delays and timeouts. Signal handling. It implements five platform-specific event loop backends: epoll and io_uring on Linux, kqueue on macOS/BSD, IOCP on Windows, and POSIX select as a fallback. Corosio depends on Capy. Capy does not depend on Corosio. The dependency arrow points in one direction. That is not an accident. @@ -60,10 +61,11 @@ Components at different levels belong in different packages. This is a structura == Cumulative Component Dependency -Lakos quantified the cost of getting levels wrong with Cumulative Component Dependency (CCD): the sum over all components in a subsystem of the number of components needed in order to test each component incrementally (see Figure 4-22, p. 191 of Lakos'96). +Lakos quantified the cost of getting levels wrong with Cumulative Component Dependency (CCD): the sum over all components in a subsystem of the number of components needed to test each component incrementally (see Figure 4-22, p. 191 of Lakos'96). CCD ranges from N for a perfectly horizontal (flat) design to N-squared for a vertical or cyclically dependent one. The metric is additive for independent subsystems. If two independent libraries each have CCD of 5, combining them without adding cross-dependencies gives CCD 10 - exactly the sum: +[role=figure] ---- -------- ---------- [3] [3] @@ -82,7 +84,7 @@ CCD = 5 CCD = 5 Each component should have a single purpose. Ideally all of the functionality within a component is primitive - if you can write a function in terms of a type rather than as a member of that type, write a free function (or today, a template function constrained by a concept). This keeps levels flat and CCD low. -Merging two libraries at different levels inflates CCD. Every component that only needs buffers and tasks now drags in sockets, TLS, and four platform backends. Testing cost, build cost, and cognitive cost all increase. +Merging two libraries at different levels inflates CCD. Every component that only needs buffers and tasks now drags in sockets, TLS, and five platform backends. Testing cost, build cost, and cognitive cost all increase. == Deep Modules @@ -95,9 +97,9 @@ The best modules are those that provide powerful functionality, but have a simple interface. ____ -Capy is a deep module. Its public surface is narrow: a handful of concepts (`ReadStream`, `WriteStream`, `Stream`), a task type, an executor model, and buffer utilities. Behind that surface lives a substantial implementation: coroutine frame allocation, forward propagation of executors and stop tokens, type-erased stream machinery, and composition primitives. +Capy is a deep module. Its public surface is narrow: a handful of concepts (cpp:ReadStream[], cpp:WriteStream[], cpp:Stream[]), a task type, an executor model, and buffer utilities. Behind that surface lives a substantial implementation: coroutine frame allocation, forward propagation of executors and stop tokens, type-erased stream machinery, and composition primitives. -Corosio is also a deep module, but a different one. It hides platform-specific event loop complexity (IOCP, epoll, kqueue, select) behind a uniform socket and timer interface. +Corosio is also a deep module, but a different one. It hides platform-specific event loop complexity (IOCP, epoll, io_uring, kqueue, select) behind a uniform socket and timer interface. These two modules hide different information. That is the practical reason they are separate. Lakos would say: do not collocate two independent systems, because doing so creates gratuitous physical dependencies. Ousterhout would say: modules that hide different information should remain different modules. @@ -106,7 +108,7 @@ Capy pulls the complexity of coroutine execution, buffer management, and context == Writing Against the Narrowest Interface -A `ReadStream` concept captures the essential operation: anything you can `read_some` from. TCP sockets, TLS streams, file handles, in-memory buffers - one generic algorithm works with all of them. That algorithm belongs in Capy, not Corosio, because it depends only on the concept, not on any particular implementation. +A cpp:ReadStream[] concept captures the essential operation: anything you can `read_some` from. TCP sockets, TLS streams, file handles, in-memory buffers - one generic algorithm works with all of them. That algorithm belongs in Capy, not Corosio, because it depends only on the concept, not on any particular implementation. Stepanov's principle applies here: algorithms should be abstracted away from particular implementations so that the minimum requirements the algorithm assumes are the only requirements the code uses. In practice, zero-overhead abstraction is an ideal rather than a guarantee - Chandler Carruth has argued persuasively that real compilers on real hardware rarely achieve it perfectly. But the principle of coding against minimal requirements remains sound, even when the abstraction has some cost. @@ -135,16 +137,17 @@ Merge them, and every test of a buffer copy routine must compile against platfor == Platform Isolation -Capy is portable {cpp}20. It compiles on any conforming compiler with no platform-specific code. It can be used on embedded systems, in WebAssembly, on platforms that do not have sockets, and in environments where the I/O backend has not been written yet. +Capy is portable {cpp}20. It compiles on any conforming compiler with no platform-specific I/O code. It can be used on embedded systems, in WebAssembly, on platforms that do not have sockets, and in environments where the I/O backend has not been written yet. -Corosio contains four platform backends, each a substantial body of platform-specific code: +Corosio contains five platform backends, each a substantial body of platform-specific code: * *IOCP* on Windows (sockets, overlapped I/O, NT timers) * *epoll* on Linux +* *io_uring* on Linux * *kqueue* on macOS and BSD * *select* as a POSIX fallback -Merging these into Capy would mean that a developer who wants a `task<>` type or a `buffer_slice` must compile against platform I/O headers. Keeping Capy separate ensures that none of the headers a consumer includes transitively pull in anything from the platform I/O layer. Consumers take only what they need. +Merging these into Capy would mean that a developer who wants a cpp:task[task<>] type or a cpp:buffer_slice[] must compile against platform I/O headers. Keeping Capy separate ensures that none of the headers a consumer includes transitively pull in anything from the platform I/O layer. Consumers take only what they need. == Conclusion diff --git a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc index 5f31a7d67..565676a73 100644 --- a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc @@ -1,8 +1,9 @@ = ReadStream Concept Design +:page-mode: explanation == Overview -This document describes the design of the `ReadStream` concept: the fundamental partial-read primitive in the concept hierarchy. It explains why `read_some` is the correct building block, how composed algorithms build on top of it, and the relationship to `ReadSource`. +This document describes the design of the cpp:ReadStream[] concept: the fundamental partial-read primitive among Capy's stream concepts. It explains why `read_some` is the correct building block and how composed algorithms build on top of it. == Definition @@ -11,71 +12,13 @@ This document describes the design of the `ReadStream` concept: the fundamental include::example$snippets/9c_read_stream.cpp[tag=concept_definition,indent=0] ---- -The `requires` clause checks `read_some` against a single representative buffer, `mutable_buffer_archetype`, because a {cpp} concept cannot quantify over "every buffer sequence." The contract is stronger than what the compiler verifies: a `ReadStream` must accept *any* `MutableBufferSequence`—a single buffer or a range of them—and the archetype stands in for that universally-quantified requirement. Read this as a textual requirement that the concept can only sample, not a claim that conformance is limited to the archetype type. +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. Read this as a textual requirement that the concept can only sample, not a claim that conformance is limited to the archetype type. -A `ReadStream` provides a single operation: +A cpp:ReadStream[] provides a single operation: === `read_some(buffers)` -- Partial Read -Attempts to read up to `buffer_size(buffers)` bytes from the stream into the buffer sequence. Returns `(error_code, std::size_t)` where `n` is the number of bytes read. - -==== Semantics - -If `buffer_size(buffers) > 0`: - -- If `!ec`, then `n >= 1 && n \<= buffer_size(buffers)`. `n` bytes were read into the buffer sequence. -- If `ec`, then `n >= 0 && n \< buffer_size(buffers)`. `n` is the number of bytes read before the I/O condition arose. - -Equivalently, `n == buffer_size(buffers)` implies `!ec`: a completion that fills the buffer sequence is a success, even when the underlying operation also signals a condition such as end-of-stream. That condition is reported on a subsequent read. - -If `buffer_empty(buffers)` is true, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. - -The caller must not assume the buffer is filled. `read_some` may return fewer bytes than the buffer can hold. This is the defining property of a partial-read primitive. - -Once `read_some` returns an error (including EOF), the caller must not call `read_some` again. The stream is done. Not all implementations can reproduce a prior error on subsequent calls, so the behavior after an error is undefined. - -Buffers in the sequence are filled in order. - -==== Error Reporting - -I/O conditions arising from the underlying I/O system (EOF, connection reset, broken pipe, etc.) are reported via the `error_code` component of the return value. Failures in the library wrapper itself (such as memory allocation failure) are reported via exceptions. - -*Throws:* `std::bad_alloc` if coroutine frame allocation fails. - -==== Buffer Lifetime - -The caller must ensure that the memory referenced by `buffers` remains valid until the `co_await` expression returns. - -==== Conforming Signatures - -[source,cpp] ----- -include::example$snippets/9c_read_stream.cpp[tag=read_some_signature,indent=0] ----- - -Buffer sequences should be accepted by value when the member function is a coroutine, to ensure the sequence lives in the coroutine frame across suspension points. - -== Concept Hierarchy - -`ReadStream` is the base of the read-side hierarchy: - ----- -ReadStream { read_some } - | - v -ReadSource { read_some, read } ----- - -`ReadSource` refines `ReadStream`. Every `ReadSource` is a `ReadStream`. Algorithms constrained on `ReadStream` accept both raw streams and sources. The `ReadSource` concept adds a complete-read primitive on top of the partial-read primitive. - -This mirrors the write side: - ----- -WriteStream { write_some } - | - v -WriteSink { write_some, write, write_eof(buffers), write_eof() } ----- +See cpp:ReadStream[] for the full syntactic and semantic contract: return-value semantics, behavior after an error, error reporting, throws, buffer lifetime, and conforming signatures. == Composed Algorithms @@ -112,19 +55,16 @@ include::example$snippets/9c_read_stream.cpp[tag=echo,indent=0] |=== | Read Side | Write Side -| `ReadStream::read_some` -| `WriteStream::write_some` +| cpp:ReadStream::read_some[read_some] +| cpp:WriteStream::write_some[WriteStream::write_some] -| `read` free function (composed) +| cpp:read[] free function (composed) | `write_now` (composed, eager) - -| `ReadSource::read` -| `WriteSink::write` |=== == Design Foundations: Why a Full Buffer Is Always Success -The `read_some` contract treats a completion that fills the buffer sequence as a success: `n == buffer_size(buffers)` implies `!ec`. An error is reported only when the transfer was incomplete, in which case `n \< buffer_size(buffers)`. A pending condition, such as end-of-stream, is never delivered alongside a full buffer; it is deferred to the next read. This is the most consequential design decision in the `ReadStream` concept, with implications for every consumer of `read_some` in the library. This section explains the design and its consequences. +The `read_some` contract treats a completion that fills the buffer sequence as a success: `n == buffer_size(buffers)` implies `!ec`. An error is reported only when the transfer was incomplete, in which case `n \< buffer_size(buffers)`. A pending condition, such as end-of-stream, is never delivered alongside a full buffer; it is deferred to the next read. This is the most consequential design decision in the cpp:ReadStream[] concept, with implications for every consumer of `read_some` in the library. This section explains the design and its consequences. === The Return Type's Purpose @@ -134,7 +74,7 @@ The `(error_code, size_t)` return type carries both a byte count and a condition A condition such as end-of-stream is a property of the stream, not of the bytes that were just delivered. If a read happens to fill the buffer exactly as the stream reaches its end, the bytes are still good and the read still succeeded. Reporting EOF on that same completion would force every caller to reconcile "I got all my bytes" with "but there was also an error," which is precisely the ambiguity the contract removes. Instead the condition surfaces on the next read, when `n` is necessarily less than the buffer size, and the caller observes it cleanly. -This is what lets generic composition algorithms such as `when_all` and `when_any` distinguish a completed transfer from a failure by inspecting `n` alone. A short read signals a condition; a full read does not. +This is what lets generic composition algorithms such as cpp:when_all[] and cpp:when_any[] distinguish a completed transfer from a failure by inspecting `n` alone. A short read signals a condition; a full read does not. === The Implementation Burden Is Internal @@ -150,9 +90,9 @@ This flexibility permits zero-length operations to serve as probes (fd validatio === Why EOF Is an Error -EOF is reported as an error code (`cond::eof`) rather than as a success with `n == 0`, for two reasons: +EOF is reported as an error code (cpp:cond::eof[cond::eof]) rather than as a success with `n == 0`, for two reasons: -*Composed operations need EOF-as-error to report early termination.* The composed `read(stream, buffer(buf, 100))` promises to fill exactly 100 bytes. If the stream ends after 50, the operation did not fulfill its contract. Reporting `{success, 50}` would be misleading. Reporting `{eof, 50}` tells the caller both what happened (50 bytes landed in the buffer) and why the operation stopped (the stream ended). +*Composed operations need EOF-as-error to report early termination.* The composed cpp:read[]`(stream, make_buffer(buf, 100))` promises to fill exactly 100 bytes. If the stream ends after 50, the operation did not fulfill its contract. Reporting `{success, 50}` would be misleading. Reporting `{eof, 50}` tells the caller both what happened (50 bytes landed in the buffer) and why the operation stopped (the stream ended). *EOF-as-error disambiguates the empty-buffer case from the end of a stream.* Without EOF-as-error, both `read_some(empty_buffer)` on a live stream and `read_some(non_empty_buffer)` on an exhausted stream could produce `{success, 0}`. The caller could not distinguish "I passed no buffer" from "the stream is done." @@ -165,23 +105,23 @@ Every composed read algorithm that accumulates progress follows the same pattern include::example$snippets/9c_read_stream.cpp[tag=canonical_loop,indent=0] ---- -The advance-then-check ordering is the only correct pattern. It is required for any operation that can report partial progress alongside an error -- `read` returning `(eof, 47)` being the canonical example. If the check precedes the advance, the 47 bytes are silently dropped. +The advance-then-check ordering is the only correct pattern. It is required for any operation that can report partial progress alongside an error -- cpp:read[] returning `(eof, 47)` being the canonical example. If the check precedes the advance, the 47 bytes are silently dropped. Because an error can accompany partial data, the advance must run before the check so the bytes that did arrive are counted; on a clean completion the same code advances by the full amount. Writing the check first would silently drop those bytes, so advance-then-check is the only correct order. === Implementer Freedom -When a stream produces some bytes and then observes a stopping condition before the buffer is full, it reports both at once: `(ec, k)` with `k \< buffer_size(buffers)`. There is no deferred state, no discarded data, and no internal replay buffer. A stream that decrypts or decompresses into the caller's buffer and then hits a terminal marker simply returns the bytes and the condition together. +When a stream produces some bytes and then observes a stopping condition before the buffer is full, it reports both at once: `(ec, k)` with `k \< buffer_size(buffers)`. There is no deferred state, no discarded data, and no internal replay buffer. A stream that decrypts or decompresses into the caller's buffer and then hits a terminal marker returns the bytes and the condition together. The one case that requires deferral is the exact-fill boundary, where the final bytes leave no free space. Since `(ec, buffer_size(buffers))` is not permitted, the stream reports `(!ec, buffer_size(buffers))` and carries the condition to the next call. This case is rare, and its bookkeeping is local to the stream. === Consistency from Primitives Through Composed Operations -`read_some` and the composed `read` report progress with the same shape: `(ec, n)`, where `n` counts the bytes transferred before the condition. The composed `read` returns `(eof, m)` with `m` short of the requested total when the stream ends early; the primitive `read_some` likewise returns `(ec, n)` with `n` short of the buffer size. Partial progress alongside an error code is the same pattern at every level. The single refinement at the primitive level, that an exactly full buffer is reported as success, keeps `n` a reliable proxy for completion at every layer. +`read_some` and the composed cpp:read[] report progress with the same shape: `(ec, n)`, where `n` counts the bytes transferred before the condition. The composed `read` returns `(eof, m)` with `m` short of the requested total when the stream ends early; the primitive `read_some` likewise returns `(ec, n)` with `n` short of the buffer size. Partial progress alongside an error code is the same pattern at every level. The single refinement at the primitive level, that an exactly full buffer is reported as success, keeps `n` a reliable proxy for completion at every layer. === Conforming Sources -Concrete `ReadStream` implementations are free to report `n == 0` or `n > 0` on error, whichever is natural: +Concrete cpp:ReadStream[] implementations are free to report `n == 0` or `n > 0` on error, whichever is natural: - **TCP sockets**: `read_some` maps to a single `recv()` or `WSARecv()` call. POSIX and Windows enforce binary outcomes, so these naturally produce `(ec, 0)` on error. - **TLS streams**: `read_some` decrypts application data. If a fatal alert arrives after decrypting a partial record, the implementation may report `(ec, n)` with the bytes that were decrypted. @@ -197,10 +137,9 @@ No source is forced into an unnatural pattern. Sources that naturally separate d == Summary -`ReadStream` provides `read_some` as the single partial-read primitive. This is deliberately minimal: +cpp:ReadStream[] provides `read_some` as the single partial-read primitive. This is deliberately minimal: -- Algorithms that need to fill a buffer completely use the `read` composed algorithm. +- Algorithms that need to fill a buffer completely use the cpp:read[] composed algorithm. - Algorithms that need to process data as it arrives use `read_some` directly. -- `ReadSource` refines `ReadStream` by adding `read` for complete-read semantics. The contract permits errors to accompany partial data, with one rule: a completely filled buffer is always reported as success, and any condition that coincides with it is deferred to the next call. This uses the `(error_code, size_t)` return type to its full potential, keeps a stream from deferring in the common partial case, and keeps `n` a reliable proxy for completion from `read_some` through composed operations. The canonical advance-then-check loop handles every case correctly with no additional call-site cost. diff --git a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc index 1a8050dd3..44ef5d170 100644 --- a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc @@ -1,8 +1,9 @@ = WriteStream Concept Design +:page-mode: explanation == Overview -This document describes the design of the `WriteStream` concept: the fundamental partial-write primitive in the concept hierarchy. It explains why `write_some` is the correct building block, how algorithms expressed directly in terms of `write_some` can outperform composed complete-write algorithms like `write_now`, and when each approach is appropriate. +This document describes the design of the cpp:WriteStream[] concept: the fundamental partial-write primitive among Capy's stream concepts. It explains why `write_some` is the correct building block, how algorithms expressed directly in terms of `write_some` can outperform composed complete-write algorithms like `write_now`, and when each approach is appropriate. == Definition @@ -11,58 +12,13 @@ This document describes the design of the `WriteStream` concept: the fundamental include::example$snippets/9f_write_stream.cpp[tag=write_stream_concept] ---- -The `requires` clause checks `write_some` against a single representative buffer, `const_buffer_archetype`, because a {cpp} concept cannot quantify over "every buffer sequence." The contract is stronger than what the compiler verifies: a `WriteStream` must accept *any* `ConstBufferSequence`—a single buffer or a range of them—and the archetype stands in for that universally-quantified requirement. Read this as a textual requirement that the concept can only sample, not a claim that conformance is limited to the archetype type. +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. Read this as a textual requirement that the concept can only sample, not a claim that conformance is limited to the archetype type. -A `WriteStream` provides a single operation: +A cpp:WriteStream[] provides a single operation: === `write_some(buffers)` -- Partial Write -Attempts to write up to `buffer_size(buffers)` bytes from the buffer sequence to the stream. Returns `(error_code, std::size_t)` where `n` is the number of bytes written. - -==== Semantics - -If `buffer_size(buffers) > 0`: - -- If `!ec`, then `n >= 1 && n \<= buffer_size(buffers)`. `n` bytes were written from the buffer sequence. -- If `ec`, then `n >= 0 && n \< buffer_size(buffers)`. `n` is the number of bytes written before the I/O condition arose. - -Equivalently, `n == buffer_size(buffers)` implies `!ec`: a completion that writes the entire buffer sequence is a success, even when the underlying operation also signals a condition. That condition is reported on a subsequent write. - -If `buffer_empty(buffers)` is true, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. - -The caller must not assume that all bytes are consumed. `write_some` may write fewer bytes than offered. This is the defining property of a partial-write primitive. - -==== Error Reporting - -I/O conditions arising from the underlying I/O system (connection reset, broken pipe, etc.) are reported via the `error_code` component of the return value. Failures in the library wrapper itself (such as memory allocation failure) are reported via exceptions. - -*Throws:* `std::bad_alloc` if coroutine frame allocation fails. - -==== Buffer Lifetime - -The caller must ensure that the memory referenced by `buffers` remains valid until the `co_await` expression returns. - -==== Conforming Signatures - -[source,cpp] ----- -include::example$snippets/9f_write_stream.cpp[tag=write_some_signature,indent=0] ----- - -Buffer sequences should be accepted by value when the member function is a coroutine, to ensure the sequence lives in the coroutine frame across suspension points. - -== Concept Hierarchy - -`WriteStream` is the base of the write-side hierarchy: - ----- -WriteStream { write_some } - | - v -WriteSink { write_some, write, write_eof(buffers), write_eof() } ----- - -Every `WriteSink` is a `WriteStream`. Algorithms constrained on `WriteStream` accept both raw streams and sinks. The `WriteSink` concept adds complete-write and EOF signaling on top of the partial-write primitive. See the WriteSink design document for details. +See cpp:WriteStream[] for the full syntactic and semantic contract: return-value semantics, error reporting, throws, buffer lifetime, and conforming signatures. == Composed Algorithms @@ -75,7 +31,7 @@ Two composed algorithms build complete-write behavior on top of `write_some`: include::example$snippets/9f_write_stream.cpp[tag=write_signature] ---- -Loops `write_some` until the entire buffer sequence is consumed. Always suspends (returns `task`). No frame caching. +Loops `write_some` until the entire buffer sequence is consumed. Always suspends (returns cpp:task[]). No frame caching. === `write_now` (class template) @@ -95,12 +51,13 @@ The critical design insight behind `write_some` as a primitive is that the calle A composed algorithm like `write_now` cannot do this. It receives a fixed buffer sequence and drains it to completion. When the kernel accepts only part of the data, `write_now` must send the remainder in a second call -- even though the remainder may be small. The caller has no opportunity to read more data from the source between iterations. -=== Diagram: Relaying 100KB from a ReadSource through a TCP Socket +=== Diagram: Relaying 100KB from a ReadStream through a TCP Socket -Consider relaying 100KB from a `ReadStream` to a TCP socket. The kernel's send buffer accepts at most 40KB per call. Compare two approaches: +Consider relaying 100KB from a cpp:ReadStream[] to a TCP socket. The kernel's send buffer accepts at most 40KB per call. Compare two approaches: ==== Approach A: `write_some` with Top-Up (3 syscalls) +[role=figure] ---- buffer contents syscall kernel accepts Step 1: [======== 64KB ========] write_some --> 40KB, read 40KB from source @@ -111,6 +68,7 @@ Step 3: [===== 44KB =====] write_some --> 44KB ==== Approach B: `write_now` Without Top-Up (4 syscalls) +[role=figure] ---- buffer contents syscall kernel accepts Step 1: [======== 64KB ========] write_some --> 40KB (write_now, read 64KB) @@ -124,7 +82,7 @@ Every time `write_now` partially drains a buffer, the remainder is a small paylo === Code: `write_now` Without Top-Up -This example reads from a `ReadSource` and writes to a `WriteStream` using `write_now`. Each chunk is drained to completion before the caller can read more from the source. +This example reads from a cpp:ReadStream[] and writes to a cpp:WriteStream[] using `write_now`. Each chunk is drained to completion before the caller can read more from the source. [source,cpp] ---- @@ -150,22 +108,15 @@ After the kernel accepts 40KB of a 64KB chunk, `write_now` must send the remaini or where the write is expected to complete in one call. | Cannot top up between iterations. Small remainders waste syscall payloads. - -| `WriteSink::write` -| Sink-oriented code where the concrete type implements complete-write - natively (buffered writer, file, compressor) and the caller does not - manage the loop. -| Requires `WriteSink`, not just `WriteStream`. |=== === Rule of Thumb - If the caller has a discrete, bounded payload and wants zero-fuss complete-write semantics, use `write_now`. -- If the destination is a `WriteSink`, use `write` directly. == Conforming Types -Examples of types that satisfy `WriteStream`: +Examples of types that satisfy cpp:WriteStream[]: - **TCP sockets**: `write_some` maps to a single `send()` or `WSASend()` call. Partial writes are normal under load. - **TLS streams**: `write_some` encrypts and sends one TLS record. @@ -173,34 +124,28 @@ Examples of types that satisfy `WriteStream`: - **QUIC streams**: `write_some` sends one or more QUIC frames. - **Test mock streams**: `write_some` records data and returns configurable results for testing. -All of these types also naturally extend to `WriteSink` by adding `write`, `write_eof(buffers)`, and `write_eof()`. - == Relationship to `ReadStream` -The read-side counterpart is `ReadStream`, which requires `read_some`. The same partial-transfer / composed-algorithm decomposition applies: +The read-side counterpart is cpp:ReadStream[], which requires `read_some`. The same partial-transfer / composed-algorithm decomposition applies: [cols="1,1"] |=== | Write Side | Read Side -| `WriteStream::write_some` -| `ReadStream::read_some` +| cpp:WriteStream::write_some[write_some] +| cpp:ReadStream::read_some[read_some] | `write_now` (composed) -| `read` free function (composed) - -| `WriteSink::write` -| `ReadSource::read` +| cpp:read[] free function (composed) |=== The asymmetry is that the read side does not have a `read_now` with eager completion, because reads depend on data arriving from the network -- the synchronous fast path is less reliably useful than for writes into a buffered stream. == Summary -`WriteStream` provides `write_some` as the single partial-write primitive. This is deliberately minimal: +cpp:WriteStream[] provides `write_some` as the single partial-write primitive. This is deliberately minimal: -- Algorithms that need complete-write semantics use `write_now` (for `WriteStream`) or `write` (for `WriteSink`). +- Algorithms that need complete-write semantics use `write_now` or cpp:write[], both built on `write_some`. - Algorithms that need maximum throughput use `write_some` directly with buffer top-up, achieving fewer syscalls than composed algorithms by keeping the buffer full between iterations. -- The concept is the base of the hierarchy. `WriteSink` refines it by adding `write`, `write_eof(buffers)`, and `write_eof()`. -The choice between `write_some`, `write_now`, and `WriteSink::write` is a throughput-versus-convenience trade-off. `write_some` gives the caller maximum control. `write_now` gives the caller maximum simplicity. `WriteSink::write` gives the concrete type maximum implementation freedom. +The choice between `write_some` and `write_now` is a throughput-versus-convenience trade-off. `write_some` gives the caller maximum control. `write_now` gives the caller maximum simplicity. diff --git a/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc b/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc index 2d1a25252..e975d58e6 100644 --- a/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc +++ b/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc @@ -1,4 +1,5 @@ = Type-Erasing Awaitables +:page-mode: explanation == Overview @@ -8,8 +9,9 @@ The vtable layout depends on how many async operations the wrapper exposes and w == Single-Operation: Flat Vtable -When a wrapper exposes exactly one async operation (e.g. `any_read_stream` with `read_some`, or `any_write_stream` with `write_some`), all function pointers live in a single flat vtable: +When a wrapper exposes exactly one async operation (e.g. cpp:any_read_stream[] with `read_some`, or cpp:any_write_stream[] with `write_some`), all function pointers live in a single flat vtable: +.Design sketch [source,cpp,role=pseudocode] ---- // Flat vtable -- 64 bytes, one cache line @@ -32,6 +34,7 @@ The inner awaitable can be constructed in either `await_ready` or `await_suspend When there is no outer short-circuit, constructing in `await_ready` lets immediate completions skip `await_suspend` entirely: +.Design sketch [source,cpp,role=pseudocode] ---- bool await_ready() { @@ -56,6 +59,7 @@ io_result await_resume() { When the outer awaitable has a short-circuit (empty buffers), construction is deferred to `await_suspend` so the inner awaitable is never created on the fast path: +.Design sketch [source,cpp,role=pseudocode] ---- bool await_ready() const noexcept { @@ -88,9 +92,12 @@ When a wrapper exposes more than one async operation, it cannot embed a single s Two sub-cases arise, distinguished by whether the operations share an await-return type: -* *Same await-return type* (`any_read_source`: `read_some` and `read` both await-return `io_result`). One ops struct (`awaitable_ops`) serves both operations; both `construct_*_awaitable` pointers return the same layout. -* *Different await-return types* (`any_buffer_source`: `pull` await-returns `io_result>` while its synthesized `read_some`/`read` await-return `io_result`; `any_buffer_sink` and `any_write_sink` similarly mix `io_result<>`, `io_result`). These need more than one ops struct (e.g. `awaitable_ops` plus `read_awaitable_ops`/`write_awaitable_ops`/`eof_awaitable_ops`), and each `construct` returns the ops matching its result type. +* *Same await-return type* (Boost.Http's `http::any_read_source`: `read_some` and `read` both await-return cpp:io_result[io_result]). One ops struct (`awaitable_ops`) serves both operations; both `construct_*_awaitable` pointers return the same layout. +* *Different await-return types* (Boost.Http's `http::any_buffer_source`: `pull` await-returns cpp:io_result[io_result>] while its synthesized `read_some`/`read` await-return `io_result`; `http::any_buffer_sink` and `http::any_write_sink` similarly mix `io_result<>`, `io_result`). These need more than one ops struct (e.g. `awaitable_ops` plus `read_awaitable_ops`/`write_awaitable_ops`/`eof_awaitable_ops`), and each `construct` returns the ops matching its result type. +Capy's own multi-operation wrapper avoids this problem entirely. cpp:any_stream[] exposes two operations, `read_some` and `write_some`, but does not build a shared per-construct ops table for them: it inherits from cpp:any_read_stream[] and cpp:any_write_stream[], composing two independent flat, single-operation vtables. Per-construct ops are needed only when multiple operations act on one shared inner source or sink object; `any_stream` sidesteps the question because its two operations already live in two separate single-operation wrappers. + +.Design sketch [source,cpp,role=pseudocode] ---- // Per-awaitable dispatch -- one struct per await-return type @@ -115,6 +122,7 @@ struct vtable The inner awaitable is constructed in `await_suspend`. Outer `await_ready` handles short-circuits (e.g. empty buffers) before the inner type is ever created: +.Design sketch [source,cpp,role=pseudocode] ---- bool await_ready() const noexcept { @@ -142,13 +150,20 @@ io_result await_resume() { Immediate completion path -- inner `await_ready` returns true: +[role=figure] ---- Flat (any_read_stream, any_write_stream): 2 cache lines LINE 1 object stream_, vt_, cached_awaitable_, ... LINE 2 vtable construct → await_ready → await_resume → destroy (contiguous, sequential access, prefetch-friendly) -Per-construct ops (any_read_source, any_buffer_source, +Composed flat (any_stream = any_read_stream + any_write_stream): 2 cache +lines per operation + read_some and write_some each hit their own base's 2-line flat vtable + independently -- no shared ops table, because the two operations + already live in two separate single-operation wrappers. + +Per-construct ops (Boost.Http's any_read_source, any_buffer_source, any_buffer_sink, any_write_sink): 3 cache lines LINE 1 object source_, vt_, cached_awaitable_, active_ops_, ... LINE 2 vtable construct_*_awaitable pointers @@ -165,18 +180,23 @@ The flat layout keeps all per-awaitable function pointers adjacent to `construct | Layout | Wrappers | Flat vtable (one operation, ops embedded in vtable) -| `any_read_stream` (`read_some`) + - `any_write_stream` (`write_some`) +| cpp:any_read_stream[] (`read_some`) + + cpp:any_write_stream[] (`write_some`) + +| Composed flat vtables (multiple operations via inheritance from single-operation wrappers) +| cpp:any_stream[] (`read_some` via cpp:any_read_stream[]; `write_some` via cpp:any_write_stream[] -- two independent flat vtables, no shared ops table) | Per-construct ops, single ops struct (multiple operations, all sharing one await-return type) -| `any_read_source` (`read_some`, `read` -- both `io_result`) +| Boost.Http's `http::any_read_source` (`read_some`, `read` -- both cpp:io_result[io_result]) | Per-construct ops, multiple ops structs (operations differ in await-return type) -| `any_buffer_source` (`pull` → `io_result`; synthesized `read_some`/`read` → `io_result`) + - `any_buffer_sink` (`commit`/`commit_eof` → `io_result<>`; `write_some`/`write` → `io_result`) + - `any_write_sink` (`write_some`/`write` → `io_result`; `write_eof()` → `io_result<>`) +| Boost.Http's `http::any_buffer_source` (`pull` → cpp:io_result[io_result]; synthesized `read_some`/`read` → `io_result`) + + `http::any_buffer_sink` (`commit`/`commit_eof` → `io_result<>`; `write_some`/`write` → `io_result`) + + `http::any_write_sink` (`write_some`/`write` → `io_result`; `write_eof()` → `io_result<>`) |=== == Why the Flat Layout Cannot Scale -With multiple operations, each `construct` call produces a different concrete awaitable type. The per-awaitable function pointers (`await_ready`, `await_suspend`, `await_resume`, `destroy`) must match the type that was constructed. Returning the correct ops pointer from each `construct` call solves this. Embedding the four function pointers directly in the vtable, as the flat layout does, would require one full set per operation -- workable for one operation, unwieldy for four. When the operations share an await-return type (`any_read_source`) a single ops struct suffices; when they differ (`any_buffer_source`, `any_buffer_sink`, `any_write_sink`) the wrapper carries one ops struct per result type. +With multiple operations against a shared inner object, each `construct` call produces a different concrete awaitable type. The per-awaitable function pointers (`await_ready`, `await_suspend`, `await_resume`, `destroy`) must match the type that was constructed. Returning the correct ops pointer from each `construct` call solves this. Embedding the four function pointers directly in the vtable, as the flat layout does, would require one full set per operation -- workable for one operation, unwieldy for four. When the operations share an await-return type (Boost.Http's `http::any_read_source`) a single ops struct suffices; when they differ (`http::any_buffer_source`, `http::any_buffer_sink`, `http::any_write_sink`) the wrapper carries one ops struct per result type. + +This scaling problem is specific to wrappers with several operations on one inner object. cpp:any_stream[] never hits it: its two operations belong to two separate inner-object-free bases, each already a flat single-operation wrapper, so composing them by inheritance keeps both vtables flat. diff --git a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc index f96a83670..033f9bcd7 100644 --- a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc +++ b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc @@ -1,10 +1,11 @@ = Executor Concept Design +:page-mode: explanation == Overview -This document describes the design of the `Executor` concept: the interface through which coroutines are scheduled for execution. It explains the relationship to Asio's executor model, why `dispatch` returns `std::coroutine_handle<>`, why `defer` was dropped, how `executor_ref` achieves zero-allocation type erasure, and the I/O completion pattern that motivates the entire design. +This document describes the design of the cpp:Executor[] concept: the interface through which coroutines are scheduled for execution. It explains the relationship to Asio's executor model, why cpp:Executor::dispatch[dispatch] returns `std::coroutine_handle<>`, why `defer` was dropped, how cpp:executor_ref[] achieves zero-allocation type erasure, and the I/O completion pattern that motivates the entire design. -The `Executor` concept exists to answer one question: when a coroutine is ready to run, _where_ does it run? The concept captures the rules for scheduling coroutine resumption, tracking outstanding work for graceful shutdown, and accessing the execution context that owns the executor. Every I/O awaitable in Corosio -- sockets, acceptors, timers, resolvers -- depends on this concept to dispatch completions back to the correct executor. +The cpp:Executor[] concept exists to answer one question: when a coroutine is ready to run, _where_ does it run? The concept captures the rules for scheduling coroutine resumption, tracking outstanding work for graceful shutdown, and accessing the execution context that owns the executor. Every I/O awaitable in Corosio -- sockets, acceptors, timers, resolvers -- depends on this concept to dispatch completions back to the correct executor. == Definition @@ -13,7 +14,7 @@ The `Executor` concept exists to answer one question: when a coroutine is ready include::example$snippets/9k_executor.cpp[tag=executor_concept] ---- -An `Executor` provides exactly two scheduling operations: +An cpp:Executor[] provides exactly two scheduling operations: === `dispatch(c)` -- Execute If Safe @@ -21,19 +22,19 @@ If the executor determines it is safe (e.g., the current thread is already assoc === `post(c)` -- Always Queue -Queues the continuation for later execution without ever executing it inline. Never blocks. The continuation is linked into the executor's internal queue via its `reserved` slot -- no per-post heap allocation. +Queues the continuation for later execution without ever executing it inline. Never blocks. The continuation is linked into the executor's internal queue via its cpp:continuation::reserved[reserved] slot -- no per-post heap allocation. -Both operations accept `continuation&` rather than `std::coroutine_handle<>`. A `continuation` wraps a coroutine handle with a pointer-sized `reserved` slot, which the executor commandeers as its queue link, enabling zero-allocation queuing. +Both operations accept cpp:continuation[]`&` rather than `std::coroutine_handle<>`. A cpp:continuation[] wraps a coroutine handle with a pointer-sized cpp:continuation::reserved[reserved] slot, which the executor commandeers as its queue link, enabling zero-allocation queuing. The remaining operations support context access, lifecycle management, and identity: === `context()` -- Access the Execution Context -Returns an lvalue reference to the `execution_context` that created this executor. The context provides service infrastructure, frame allocators, and shutdown coordination. +Returns an lvalue reference to the cpp:execution_context[] that created this executor. The context provides service infrastructure, frame allocators, and shutdown coordination. === `on_work_started()` / `on_work_finished()` -- Track Work -Paired calls that track outstanding work. When the count reaches zero, the context's event loop (`run()`) returns. These calls must be balanced: each `on_work_started` must have a matching `on_work_finished`. +Paired calls that track outstanding work. When the count reaches zero, the context's event loop (`run()`) returns. These calls must be balanced: each cpp:Executor::on_work_started[on_work_started] must have a matching cpp:Executor::on_work_finished[on_work_finished]. === `operator==` -- Equality Comparison @@ -51,23 +52,23 @@ ____ Capy retains the core elements of this model: -- **Work tracking.** `on_work_started` / `on_work_finished` for graceful shutdown. -- **`dispatch` / `post` duality.** Execute-if-safe versus always-queue. -- **`execution_context` base class.** Service infrastructure and context lifetime management. +- **Work tracking.** cpp:Executor::on_work_started[on_work_started] / cpp:Executor::on_work_finished[on_work_finished] for graceful shutdown. +- **cpp:Executor::dispatch[dispatch] / cpp:Executor::post[post] duality.** Execute-if-safe versus always-queue. +- **cpp:execution_context[] base class.** Service infrastructure and context lifetime management. - **Equality comparison.** Same-executor optimization. Capy removes or changes: - **`defer`.** Dropped entirely. See <>. -- **Function object submission.** Asio executors accept arbitrary callables. Capy executors accept `continuation&` -- a coroutine handle wrapped with an intrusive queue pointer. This removes the need for allocator-aware function erasure, eliminates per-post heap allocation, and enables a simpler, cheaper type-erased wrapper (`executor_ref`). -- **`dispatch` return type.** Asio's `dispatch` returns void. Capy's `dispatch` returns `std::coroutine_handle<>` for symmetric transfer. See <>. +- **Function object submission.** Asio executors accept arbitrary callables. Capy executors accept cpp:continuation[]`&` -- a coroutine handle wrapped with an intrusive queue pointer. This removes the need for allocator-aware function erasure, eliminates per-post heap allocation, and enables a simpler, cheaper type-erased wrapper (cpp:executor_ref[]). +- **cpp:Executor::dispatch[dispatch] return type.** Asio's `dispatch` returns void. Capy's cpp:Executor::dispatch[dispatch] returns `std::coroutine_handle<>` for symmetric transfer. See <>. The result is a concept that preserves Asio's proven execution model while removing the machinery that a coroutine-native library does not need. [[why-dispatch-returns-handle]] == Why `dispatch` Returns `std::coroutine_handle<>` -`dispatch` returns a `std::coroutine_handle<>` so that callers can use it for symmetric transfer from `await_suspend`. When the executor determines that inline resumption is safe, it returns `c.h` -- the caller returns this from `await_suspend` and the compiler performs a tail-call transfer to the target coroutine. When inline resumption is not safe, the executor queues the continuation and returns `std::noop_coroutine()`, which suspends the caller without resuming anything. +cpp:Executor::dispatch[dispatch] returns a `std::coroutine_handle<>` so that callers can use it for symmetric transfer from `await_suspend`. When the executor determines that inline resumption is safe, it returns `c.h` -- the caller returns this from `await_suspend` and the compiler performs a tail-call transfer to the target coroutine. When inline resumption is not safe, the executor queues the continuation and returns `std::noop_coroutine()`, which suspends the caller without resuming anything. A conforming implementation: @@ -82,12 +83,12 @@ This design enables the common fast path -- same-executor dispatch at `final_sus I/O awaitables return `std::noop_coroutine()` from `await_suspend` rather than a handle for symmetric transfer. The I/O operation is initiated during `await_suspend`, but completion comes from the reactor or proactor asynchronously. The awaitable cannot know which coroutine to transfer to at suspension time. -Symmetric transfer from `dispatch` is used at a different level: when a child coroutine completes and its `final_suspend` dispatches the parent's continuation through the executor. If the parent is on the same executor, `dispatch` returns the parent's handle for direct symmetric transfer. If not, it queues the continuation and returns `std::noop_coroutine()`. +Symmetric transfer from cpp:Executor::dispatch[dispatch] is used at a different level: when a child coroutine completes and its `final_suspend` dispatches the parent's continuation through the executor. If the parent is on the same executor, cpp:Executor::dispatch[dispatch] returns the parent's handle for direct symmetric transfer. If not, it queues the continuation and returns `std::noop_coroutine()`. [[why-not-defer]] == Why Two Operations, Not Three -Asio provides three submission methods: `dispatch`, `post`, and `defer`. Capy provides only `dispatch` and `post`. +Asio provides three submission methods: `dispatch`, `post`, and `defer`. Capy provides only cpp:Executor::dispatch[dispatch] and cpp:Executor::post[post]. === What `defer` Does @@ -129,11 +130,11 @@ ____ When `task::await_suspend` returns the parent's coroutine handle, the compiler performs a tail-call-like transfer directly to the parent. No queue, no executor submission, no `defer`. The optimization that `defer` provides through a runtime hint, symmetric transfer provides through a compile-time guarantee. -Corosio confirms this in practice: its entire I/O layer -- sockets, acceptors, timers, resolvers, signals -- across all three backends (epoll, IOCP, select) uses only `dispatch` and `post`. No code path requires `defer`. +Corosio confirms this in practice: its entire I/O layer -- sockets, acceptors, timers, resolvers, signals -- across all five backends (epoll, kqueue, io_uring, IOCP, select) uses only cpp:Executor::dispatch[dispatch] and cpp:Executor::post[post]. No code path requires `defer`. == Why `continuation`, Not Raw `coroutine_handle<>` -The executor accepts `continuation&` rather than `std::coroutine_handle<>`. A `continuation` wraps the handle with a pointer-sized `reserved` slot that the executor commandeers as its queue link, enabling zero-allocation queuing: +The executor accepts cpp:continuation[]`&` rather than `std::coroutine_handle<>`. A cpp:continuation[] wraps the handle with a pointer-sized cpp:continuation::reserved[reserved] slot that the executor commandeers as its queue link, enabling zero-allocation queuing: [source,cpp] ---- @@ -142,61 +143,61 @@ include::example$snippets/9k_executor.cpp[tag=continuation_struct] This design has three consequences: -- **Zero-allocation posting.** The thread pool links the `continuation` directly into its work queue via `reserved`. No `new work(h)` per post. The queue node is embedded in the thing being queued -- the awaitable, combinator state, or trampoline promise that owns the continuation. +- **Zero-allocation posting.** The thread pool links the cpp:continuation[] directly into its work queue via cpp:continuation::reserved[reserved]. No `new work(h)` per post. The queue node is embedded in the thing being queued -- the awaitable, combinator state, or trampoline promise that owns the continuation. -- **Type erasure remains possible.** `executor_ref` wraps any executor behind a uniform vtable. The vtable function pointers accept `continuation&`, which is a concrete type. No templates on promise type are needed. +- **Type erasure remains possible.** cpp:executor_ref[] wraps any executor behind a uniform vtable. The vtable function pointers accept cpp:continuation[]`&`, which is a concrete type. No templates on promise type are needed. -- **I/O operation structures stay simple.** Every I/O awaitable embeds a `continuation` for the caller's handle and an `executor_ref` for the executor. Both are non-templated, keeping I/O backend code non-generic and out of headers. +- **I/O operation structures stay simple.** Every I/O awaitable embeds a cpp:continuation[] for the caller's handle and an cpp:executor_ref[] for the executor. Both are non-templated, keeping I/O backend code non-generic and out of headers. -The handle within the `continuation` is still type-erased (`std::coroutine_handle<>`) for the same reasons that applied before: executor implementations are independent of coroutine internals, and the type-erased handle provides exactly the right interface (`resume()` and nothing else). +The handle within the cpp:continuation[] is still type-erased (`std::coroutine_handle<>`) for the same reasons that applied before: executor implementations are independent of coroutine internals, and the type-erased handle provides exactly the right interface (`resume()` and nothing else). == Why Nothrow Copy and Move The concept requires `std::is_nothrow_copy_constructible_v` and `std::is_nothrow_move_constructible_v`. -Executors propagate through coroutine machinery at points where exceptions cannot be handled: inside `await_suspend`, during promise construction, and through type-erased wrappers like `executor_ref`. An exception thrown from an executor copy at any of these points would leave the coroutine in an unrecoverable state -- suspended but with no executor to resume it through. +Executors propagate through coroutine machinery at points where exceptions cannot be handled: inside `await_suspend`, during promise construction, and through type-erased wrappers like cpp:executor_ref[]. An exception thrown from an executor copy at any of these points would leave the coroutine in an unrecoverable state -- suspended but with no executor to resume it through. The nothrow requirement eliminates this failure mode entirely. In practice, executors are lightweight handles -- a pointer to the execution context and perhaps a strand pointer or a priority value. Nothrow copy and move are natural for such types. The requirement does not impose a burden; it documents what is already true of every reasonable executor implementation. == Work Tracking, Shutdown, and Executor Validity -The `on_work_started` and `on_work_finished` operations serve three roles. +The cpp:Executor::on_work_started[on_work_started] and cpp:Executor::on_work_finished[on_work_finished] operations serve three roles. === Event Loop Lifetime Work tracking is the mechanism by which the event loop knows when to stop. When outstanding work reaches zero, `run()` returns. This is not bookkeeping -- it is the event loop's termination signal. -In Corosio, `on_work_finished` triggers `stop()` when the atomic work count reaches zero: +In Corosio, cpp:Executor::on_work_finished[on_work_finished] triggers `stop()` when the atomic work count reaches zero: [source,cpp] ---- include::example$snippets/9k_executor.cpp[tag=on_work_finished,indent=0] ---- -Every `run_async` call increments the count. When the launched task completes, the count decrements. When no tasks remain, `run()` returns. Without work tracking in the executor, the event loop would need a separate signaling mechanism or would spin indefinitely. +Every cpp:run_async[] call increments the count. When the task it starts completes, the count decrements. When no tasks remain, `run()` returns. Without work tracking in the executor, the event loop would need a separate signaling mechanism or would spin indefinitely. === Public Visibility -These operations are public, not private with friendship. The reason is extensibility: `work_guard` is the library's RAII wrapper for work tracking, but users may define their own guards with additional behaviors (logging, metrics, timeout detection). Making work tracking private would require the library to grant friendship to types it cannot anticipate. +These operations are public, not private with friendship. The reason is extensibility: cpp:work_guard[] is the library's RAII wrapper for work tracking, but users may define their own guards with additional behaviors (logging, metrics, timeout detection). Making work tracking private would require the library to grant friendship to types it cannot anticipate. === Executor Validity -An executor becomes invalid when its context's `shutdown()` returns. After shutdown: +An executor becomes invalid when its context's cpp:execution_context::shutdown[shutdown()] returns. After shutdown: -- `dispatch`, `post`, `on_work_started`, `on_work_finished`: undefined behavior. -- Copy, comparison, `context()`: valid until the context is destroyed. +- cpp:Executor::dispatch[dispatch], cpp:Executor::post[post], cpp:Executor::on_work_started[on_work_started], cpp:Executor::on_work_finished[on_work_finished]: undefined behavior. +- Copy, comparison, cpp:Executor::context[context()]: valid until the context is destroyed. This two-phase model exists because shutdown drains outstanding work. During the drain, executors must still be copyable (they are stored in pending operations) and comparable (for same-executor checks). Only the work-submission operations become invalid, because the context has stopped accepting new work. == Why `context()` Returns `execution_context&` -The `context()` operation returns a reference to the `execution_context` base class, not the concrete derived type. +The cpp:Executor::context[context()] operation returns a reference to the cpp:execution_context[] base class, not the concrete derived type. This serves two purposes: -- **Type erasure.** `executor_ref` can wrap any executor without knowing its context type. If `context()` returned a concrete type, the vtable would need a different return type per executor type. +- **Type erasure.** cpp:executor_ref[] can wrap any executor without knowing its context type. If cpp:Executor::context[context()] returned a concrete type, the vtable would need a different return type per executor type. -- **Service lookup.** The `execution_context` base class provides `use_service()` and `make_service()`, which is sufficient for all runtime service discovery. I/O objects do not need the concrete context type to find their services. +- **Service lookup.** The cpp:execution_context[] base class provides cpp:execution_context::use_service[use_service()] and cpp:execution_context::make_service[make_service()], which is sufficient for all runtime service discovery. I/O objects do not need the concrete context type to find their services. Corosio demonstrates this pattern throughout its public API. I/O objects accept any executor and extract the context via the base class reference: @@ -205,11 +206,11 @@ Corosio demonstrates this pattern throughout its public API. I/O objects accept include::example$snippets/9k_executor.cpp[tag=socket_ctor,indent=0] ---- -The socket constructor receives `execution_context&` and looks up the socket service. The concrete context type -- `epoll_context`, `iocp_context`, `select_context` -- is irrelevant to the socket. +The socket constructor receives cpp:execution_context[]`&` and looks up the socket service. The concrete context type -- `corosio::io_context` instantiated with a backend tag such as `epoll_t` or `iocp_t` -- is irrelevant to the socket. == The `executor_ref` Design -`executor_ref` is a non-owning, type-erased wrapper for any executor satisfying the `Executor` concept. It is the mechanism by which I/O operations store and use executors without templates. +cpp:executor_ref[] is a non-owning, type-erased wrapper for any executor satisfying the cpp:Executor[] concept. It is the mechanism by which I/O operations store and use executors without templates. === Two Pointers @@ -220,7 +221,7 @@ The entire object is two pointers: include::example$snippets/9k_executor.cpp[tag=executor_ref_layout] ---- -Two pointers fit in two registers. `executor_ref` can be passed by value as cheaply as passing a pointer. No heap allocation, no small-buffer optimization, no reference counting. +Two pointers fit in two registers. cpp:executor_ref[] can be passed by value as cheaply as passing a pointer. No heap allocation, no small-buffer optimization, no reference counting. === Why Not `std::function` or `std::any` @@ -232,17 +233,17 @@ Two pointers fit in two registers. `executor_ref` can be passed by value as chea - **Indirection.** SBO wrappers store either inline data or a heap pointer, adding a branch on every operation. -`executor_ref` avoids all three. The vtable pointer goes directly to a `static constexpr` structure in `.rodata`. One indirection, no branches, no allocation. +cpp:executor_ref[] avoids all three. The vtable pointer goes directly to a `static constexpr` structure in `.rodata`. One indirection, no branches, no allocation. === Why Not {cpp} Virtual Functions -{cpp} virtual dispatch places the vtable pointer inside each heap-allocated object. Every virtual call chases a pointer from the object to its vtable, which may reside at an unpredictable address in memory. When objects of different types are interleaved on the heap, their vtable pointers point to different locations in `.rodata`, defeating spatial prefetch and polluting the instruction cache. +{cpp} virtual dispatch places the vtable pointer inside each heap-allocated object. Every virtual call chases a pointer from the object to its vtable, which may reside at an unpredictable address in memory. When objects of different types are interleaved on the heap, their vtable pointers point to different locations in `.rodata`, defeating spatial prefetch and polluting the data cache. -`executor_ref` separates the vtable from the object. The vtable is a `static constexpr` structure -- one per executor type, shared by all instances of that type. Because most programs use only one or two executor types (a thread pool executor and perhaps a strand), the vtable stays hot in L1 cache. The executor pointer and the vtable pointer sit adjacent in the `executor_ref` object, so both are loaded in a single cache line. +cpp:executor_ref[] separates the vtable from the object. The vtable is a `static constexpr` structure -- one per executor type, shared by all instances of that type. Because most programs use only one or two executor types (a thread pool executor and perhaps a strand), the vtable stays hot in L1 cache. The executor pointer and the vtable pointer sit adjacent in the cpp:executor_ref[] object, so both are loaded in a single cache line. === Reference Semantics -`executor_ref` stores a pointer to the executor, not a copy. The executor must outlive the `executor_ref`. This matches how executors propagate through coroutine chains: the executor is owned by the execution context (which outlives all coroutines running on it), and `executor_ref` is a lightweight handle passed through `await_suspend` and stored in I/O operation structures. +cpp:executor_ref[] stores a pointer to the executor, not a copy. The executor must outlive the cpp:executor_ref[]. This matches how executors propagate through coroutine chains: the executor is owned by the execution context (which outlives all coroutines running on it), and cpp:executor_ref[] is a lightweight handle passed through `await_suspend` and stored in I/O operation structures. == The I/O Completion Pattern @@ -250,7 +251,7 @@ The executor concept is designed around a single use case: I/O completion dispat === Capture at Initiation -When a coroutine `co_await`s an I/O awaitable, the awaitable's `await_suspend` receives the caller's handle and executor. The awaitable embeds a `continuation` for the caller's handle: +When a coroutine `co_await`s an I/O awaitable, the awaitable's `await_suspend` receives the caller's handle and executor. The awaitable embeds a cpp:continuation[] for the caller's handle: [source,cpp] ---- @@ -266,43 +267,19 @@ When the I/O completes (from the reactor thread for epoll, the completion port f include::example$snippets/9k_executor.cpp[tag=dispatch_at_completion,indent=0] ---- -`post` links the continuation into the executor's work queue via `cont_.reserved`. No heap allocation occurs -- the continuation is embedded in the awaitable, which is alive for the duration of the suspension. A worker thread dequeues the continuation and calls `cont_.h.resume()`. +cpp:Executor::post[post] links the continuation into the executor's work queue via `cont_.reserved`. No heap allocation occurs -- the continuation is embedded in the awaitable, which is alive for the duration of the suspension. A worker thread dequeues the continuation and calls `cont_.h.resume()`. === Platform Independence -This pattern is identical across all three Corosio backends: epoll (Linux), IOCP (Windows), and select (POSIX fallback). The executor concept and `executor_ref` provide the abstraction that makes this possible. The backend-specific code deals with I/O readiness or completion notification. The executor-specific code deals with coroutine scheduling. The two concerns are cleanly separated. +This pattern is identical across all five Corosio backends: epoll and io_uring (Linux), kqueue (BSD/macOS), IOCP (Windows), and select (portable fallback). The executor concept and cpp:executor_ref[] provide the abstraction that makes this possible. The backend-specific code deals with I/O readiness or completion notification. The executor-specific code deals with coroutine scheduling. The two concerns are cleanly separated. -== Frame Allocator Preservation - -Capy propagates frame allocators via thread-local storage (see xref:../4.coroutines/4g.allocators.adoc#_thread_local_propagation[Thread-Local Propagation]). The TLS value is set in `await_resume` when a coroutine resumes and read in `operator new` when a child coroutine is created. Between these two points, the coroutine body executes arbitrary user code. - -If that user code resumes a coroutine from a different chain on the same thread -- by calling `.resume()` directly, pumping a dispatch queue, or running nested event loop work -- the other coroutine's `await_resume` overwrites TLS. The original coroutine's next child then allocates from the wrong resource. - -=== The Save/Restore Protocol - -The fix is to save and restore TLS around every `.resume()` call: - -[source,cpp] ----- -include::example$snippets/9k_executor.cpp[tag=safe_resume] ----- - -This makes TLS behave like a stack. Each nested resume pushes its own allocator; when the coroutine suspends and `.resume()` returns, the previous value is restored. The cost is two TLS accesses (one read, one write) per `.resume()` call -- negligible compared to the cost of resuming a coroutine. - -=== Where It Applies - -All executor event loops and strand dispatch loops must use `safe_resume` instead of calling `.resume()` directly. Capy's `thread_pool`, `blocking_context`, and `strand_queue` all use it internally. - -Two `.resume()` call sites intentionally do _not_ use `safe_resume`: - -* **`symmetric_transfer`** (MSVC workaround). The calling coroutine is about to suspend unconditionally. When it later resumes, `await_resume` restores TLS from the promise's stored environment. Save/restore would add overhead with no benefit. - -* **`run_async_wrapper::operator()`**. TLS is already saved in the wrapper's constructor and restored in its destructor, which bracket the entire task lifetime. +NOTE: For the TLS save/restore protocol required around `.resume()` calls (cpp:safe_resume[]), including which two call sites are deliberately exempt, see xref:../4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation]. == Why Not `std::execution` (P2300) https://wg21.link/P2300[P2300] defines a sender/receiver model where execution context flows _backward_ from receiver to sender via queries after `connect()`: +[source,cpp,role=external] ---- task async_work(); // Frame allocated NOW auto sndr = async_work(); @@ -312,7 +289,7 @@ start(op); // -- too late For coroutines, this ordering is fatal. Coroutine frame allocation happens _before_ the coroutine body executes. The compiler calls `operator new` first, then constructs the promise, then begins execution. Any mechanism that provides the allocator _after_ the coroutine call -- receiver queries, `await_transform`, explicit method calls -- arrives after the frame is already allocated with the wrong (or default) allocator. -Capy's model flows context _forward_ from launcher to task. The `run_async(ex, alloc)(my_task())` two-phase invocation sets the thread-local allocator _before_ the task expression is evaluated, so `operator new` reads it in time. This is described in detail in xref:9.design/9l.RunApi.adoc[Run API]. +Capy's model flows context _forward_ from launcher to task. The cpp:run_async[]`(ex, alloc)(my_task())` two-phase invocation sets the thread-local allocator _before_ the task expression is evaluated, so `operator new` reads it in time. This is described in detail in xref:9.design/9l.RunApi.adoc[Run API]. The same forward-flowing model applies to executors. The launcher binds the executor before the task runs. The task's promise stores the executor and propagates it to nested awaitables via `await_transform`. Context flows from caller to callee at every level, never backward. @@ -327,6 +304,6 @@ include::example$snippets/9k_executor.cpp[tag=minimal_executor] == Summary -The `Executor` concept provides `dispatch` and `post` for coroutine scheduling, work tracking for event loop lifetime, and `context()` for service access. The design descends from Asio's executor model but is adapted for coroutines: `defer` is replaced by symmetric transfer, function objects are replaced by `continuation&` for zero-allocation intrusive queuing, and `dispatch` returns `std::coroutine_handle<>` for symmetric transfer at `final_suspend`. +The cpp:Executor[] concept provides cpp:Executor::dispatch[dispatch] and cpp:Executor::post[post] for coroutine scheduling, work tracking for event loop lifetime, and cpp:Executor::context[context()] for service access. The design descends from Asio's executor model but is adapted for coroutines: `defer` is replaced by symmetric transfer, function objects are replaced by cpp:continuation[]`&` for zero-allocation intrusive queuing, and cpp:Executor::dispatch[dispatch] returns `std::coroutine_handle<>` for symmetric transfer at `final_suspend`. -`executor_ref` type-erases any executor into two pointers, enabling platform-independent I/O completion dispatch with zero allocation and predictable cache behavior. The capture-at-initiation / dispatch-at-completion pattern is the fundamental use case the concept serves. \ No newline at end of file +cpp:executor_ref[] type-erases any executor into two pointers, enabling platform-independent I/O completion dispatch with zero allocation and predictable cache behavior. The capture-at-initiation / dispatch-at-completion pattern is the fundamental use case the concept serves. \ No newline at end of file diff --git a/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc b/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc index ee7790cdc..0ce3dbc41 100644 --- a/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc +++ b/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc @@ -1,23 +1,24 @@ = Run API Design +:page-mode: explanation == Overview -This document explains the naming conventions and call syntax of the two launcher functions: `run_async` (fire-and-forget from non-coroutine code) and `run` (awaitable within a coroutine). Both accept any type satisfying _IoRunnable_ -- not just `task` -- and use a deliberate **two-phase invocation** pattern -- `f(context)(task)` -- that exists for a mechanical reason rooted in coroutine frame allocation timing. +This document explains the naming conventions and call syntax of the two launcher functions: cpp:run_async[] (fire-and-forget from non-coroutine code) and cpp:run[] (awaitable within a coroutine). Both accept any type satisfying _IoRunnable_ -- not just cpp:task[task] -- and use a deliberate **two-phase invocation** pattern -- `f(context)(task)` -- that exists for a mechanical reason rooted in coroutine frame allocation timing. == Usage -=== `run_async` -- Fire-and-Forget Launch +=== `run_async` -- Fire-and-Forget -`run_async` launches any _IoRunnable_ from non-coroutine code: `main()`, callback handlers, event loops. `task` is the most common conforming type, but any user-defined type satisfying the concept works. The function does not return a value to the caller. Handlers receive the task's result or exception after completion, as data; they should not throw. An exception that no handler consumes (none was supplied, or a handler let one escape) calls `std::terminate`; it is never silently discarded. To catch an error instead, `co_await` the work inside a coroutine. +cpp:run_async[] starts any _IoRunnable_ from non-coroutine code: `main()`, callback handlers, event loops. cpp:task[task] is the most common conforming type, but any user-defined type satisfying the concept works. The function does not return a value to the caller. Handlers receive the task's result or exception after completion, as data; they should not throw. An exception that no handler consumes (none was supplied, or a handler let one escape) calls `std::terminate`; it is never silently discarded. To catch an error instead, `co_await` the work inside a coroutine. [source,cpp] ---- include::example$snippets/9l_run_api.cpp[tag=run_async_usage,indent=0] ---- -=== `run` -- Awaitable Launch Within a Coroutine +=== `run` -- Awaitable Within a Coroutine -`run` is the coroutine-side counterpart. It binds any _IoRunnable_ to a (possibly different) executor and returns the result to the caller via `co_await`. It also supports overloads that customize stop token or allocator while inheriting the caller's executor. +cpp:run[] is the coroutine-side counterpart. It binds any _IoRunnable_ to a (possibly different) executor and returns the result to the caller via `co_await`. It also supports overloads that customize stop token or allocator while inheriting the caller's executor. [source,cpp] ---- @@ -26,7 +27,7 @@ include::example$snippets/9l_run_api.cpp[tag=run_usage] === `run_async` on a Strand -A common pattern for launching per-connection coroutines on a strand, ensuring serialized access to connection state: +A common pattern for starting per-connection coroutines on a strand, ensuring serialized access to connection state: [source,cpp] ---- @@ -44,23 +45,24 @@ Several alternative naming and syntax proposals were evaluated and discarded. Th | Rejected | Chosen | `capy::on(ex).spawn(t)` -| `run_async(ex)(t)` +| cpp:run_async[]`(ex)(t)` | `co_await capy::on(ex).call(t)` -| `co_await run(ex)(t)` +| `co_await` cpp:run[]`(ex)(t)` | `co_await capy::with(st).call(t)` -| `co_await run(st)(t)` +| `co_await` cpp:run[]`(st)(t)` | `co_await capy::with(alloc).call(t)` -| `co_await run(alloc)(t)` +| `co_await` cpp:run[]`(alloc)(t)` | `capy::on(ex).block(t)` -| `test::run_blocking(ex)(t)` +| cpp:test::run_blocking[]`(ex)(t)` |=== What this looks like in practice: +.Rejected design [source,cpp,role=pseudocode] ---- // Rejected: builder pattern @@ -78,16 +80,18 @@ The builder pattern reads well as English, but it creates problems in {cpp} prac === Single-Call with Named Method +.Rejected design [source,cpp,role=pseudocode] ---- // Rejected: single-call run_async(ex, my_task()); ---- -This fails the allocator timing constraint entirely. The task argument `my_task()` is evaluated _before_ `run_async` can set the thread-local allocator. The coroutine frame is allocated with the wrong (or no) allocator. This is not a style preference -- it is a correctness bug. +This fails the allocator timing constraint entirely. The task argument `my_task()` is evaluated _before_ cpp:run_async[] can set the thread-local allocator. The coroutine frame is allocated with the wrong (or no) allocator. This is not a style preference -- it is a correctness bug. === Named Method on Wrapper +.Rejected design [source,cpp,role=pseudocode] ---- // Rejected: named method instead of operator() @@ -101,13 +105,13 @@ This preserves the two-phase timing guarantee and avoids the namespace collision === Why `run` -The `run` prefix was chosen for several reasons: +The cpp:run[] prefix was chosen for several reasons: - **Greppability.** Searching for `run_async(` or `run(` in a codebase produces unambiguous results. Short, common English words like `on` or `with` collide with local variable names, parameter names, and other libraries. A `using namespace capy;` combined with a local variable named `on` produces silent shadowing bugs. -- **Verb clarity.** `run` tells you what happens: something executes. `run_async` tells you it executes without waiting. `run` inside a coroutine tells you control transfers and returns. Prepositions like `on` and `with` say nothing about the action -- they are sentence fragments waiting for a verb. +- **Verb clarity.** cpp:run[] tells you what happens: something executes. cpp:run_async[] tells you it executes without waiting. cpp:run[] inside a coroutine tells you control transfers and returns. Prepositions like `on` and `with` say nothing about the action -- they are sentence fragments waiting for a verb. -- **Discoverability.** The `run_*` family groups together in documentation, autocompletion, and alphabetical listings. Users searching for "how do I launch a task" find `run_async` and `run` as a coherent pair. +- **Discoverability.** The `run_*` family groups together in documentation, autocompletion, and alphabetical listings. Users searching for "how do I start a task" find cpp:run_async[] and cpp:run[] as a coherent pair. - **Consistency.** The naming follows the established pattern from `io_context::run()`, `std::jthread`, and other {cpp} APIs where `run` means "begin executing work." @@ -118,6 +122,7 @@ The `run` prefix was chosen for several reasons: An alternative proposal suggested replacing the two-call syntax with a builder-style API: +.Rejected design [source,cpp,role=pseudocode] ---- // Rejected builder pattern @@ -132,6 +137,7 @@ While the English readability of `on(ex).spawn(t)` is genuinely appealing, the a - **Namespace pollution.** `on` and `with` are among the most common English words in programming. In a Boost library used alongside dozens of other namespaces, these names invite collisions. Consider what happens with `using namespace capy;`: + +.Rejected design [source,cpp,role=pseudocode] ---- int on = 42; // local variable @@ -141,10 +147,11 @@ void handle(auto with) { // parameter name with(alloc).call(sub()); // won't compile } ---- -+ The names `run` and `run_async` do not have this problem. No one names their variables `run_async`. ++ The names cpp:run[] and cpp:run_async[] do not have this problem. No one names their variables `run_async`. -- **Semantic ambiguity.** `with(st)` versus `with(alloc)` -- with _what_, exactly? The current API uses `run(st)` and `run(alloc)` where overload resolution disambiguates naturally because the verb `run` provides context. A bare preposition provides none. +- **Semantic ambiguity.** `with(st)` versus `with(alloc)` -- with _what_, exactly? The current API uses cpp:run[]`(st)` and cpp:run[]`(alloc)` where overload resolution disambiguates naturally because the verb cpp:run[] provides context. A bare preposition provides none. + +.Rejected design [source,cpp,role=pseudocode] ---- // What does "with" mean here? Stop token or allocator? @@ -154,8 +161,9 @@ co_await capy::with(x).call(subtask()); co_await run(x)(subtask()); ---- -- **Builder illusion.** Dot-chaining suggests composability that does not exist. Users will naturally try: +- **Builder illusion.** Dot-chaining suggests composability that does not exist. Users naturally try: + +.Rejected design [source,cpp,role=pseudocode] ---- // These look reasonable but don't work @@ -164,11 +172,11 @@ capy::on(ex).with(st).with(alloc).spawn(my_task(), h1, h2); ---- + The current syntax makes the interface boundary explicit: the first call captures _all_ context, the second call accepts the task. There is no dot-chain to extend. -- **Erases the test boundary.** `run_blocking` lives in `capy::test` deliberately -- it is a test utility, not a production API. The proposed `on(ex).block(t)` places it alongside `.spawn()` and `.call()` as if it were a first-class production method. That is a promotion this API has not earned. +- **Erases the test boundary.** cpp:test::run_blocking[run_blocking] lives in `capy::test` deliberately -- it is a test utility, not a production API. The proposed `on(ex).block(t)` places it alongside `.spawn()` and `.call()` as if it were a first-class production method. That is a promotion this API has not earned. - **Hidden critical ordering.** The two-phase invocation exists for a mechanical reason (allocator timing, described below). With `on(ex).spawn(t)`, the critical sequencing guarantee is buried behind what looks like a casual method call. The `()()` syntax is pedagogically valuable -- it signals that something important happens in two distinct steps. -- **Overload count does not shrink.** `run_async` has 18 overloads for good reason (executor x stop_token x allocator x handlers). The builder pattern still needs all those combinations -- they just move from free function overloads to constructor or method overloads. The complexity does not vanish; it relocates. +- **Overload count does not shrink.** cpp:run_async[] has 18 overloads for good reason (executor x stop_token x allocator x handlers). The builder pattern still needs all those combinations -- they just move from free function overloads to constructor or method overloads. The complexity does not vanish; it relocates. == The Two-Phase Invocation @@ -204,11 +212,11 @@ The postfix-expression is sequenced before each expression in the expression-list and any default argument. -- [expr.call] ____ -In the expression `run_async(ex)(my_task())`: +In the expression cpp:run_async[]`(ex)(my_task())`: -1. `run_async(ex)` evaluates first. This returns a wrapper object (`run_async_wrapper`) whose constructor calls `set_current_frame_allocator()` -- storing a thread-local pointer to the memory resource. +1. cpp:run_async[]`(ex)` evaluates first. This returns a wrapper object (cpp:run_async_wrapper[]) whose constructor calls cpp:set_current_frame_allocator[]`()` -- storing a thread-local pointer to the memory resource. 2. `my_task()` evaluates second. The coroutine's `operator new` reads the thread-local pointer and allocates the frame from it. -3. `operator()` on the wrapper takes ownership of the task and dispatches it to the executor. +3. cpp:run_async_wrapper::operator()[operator()] on the wrapper takes ownership of the task and dispatches it to the executor. [source,cpp] ---- @@ -219,7 +227,7 @@ This sequencing is not an implementation detail -- it is the _only correct way_ === How It Works in the Code -The `run_async_wrapper` constructor sets the thread-local allocator: +The cpp:run_async_wrapper[] constructor sets the thread-local allocator: [source,cpp] ---- @@ -235,8 +243,9 @@ The task's `operator new` reads it: include::example$snippets/9l_run_api.cpp[tag=operator_new,indent=0] ---- -The wrapper is `[[nodiscard]]` and its `operator()` is rvalue-ref-qualified, preventing misuse: +The wrapper is `+[[nodiscard]]+` and its cpp:run_async_wrapper::operator()[operator()] is rvalue-ref-qualified, preventing misuse: +.Correct usage [source,cpp,role=pseudocode] ---- // Correct: wrapper is a temporary, used immediately @@ -249,7 +258,7 @@ w(my_task()); // Error: requires rvalue === The `run` Variant -The `run` function uses the same two-phase pattern inside coroutines. An additional subtlety arises: the wrapper is a temporary that dies before `co_await` suspends the caller. The wrapper's `frame_memory_resource` would be destroyed before the child task executes. +The cpp:run[] function uses the same two-phase pattern inside coroutines. An additional subtlety arises: the wrapper is a temporary that dies before `co_await` suspends the caller. The wrapper's `frame_memory_resource` would be destroyed before the child task executes. The solution is to store a _copy_ of the allocator in the awaitable returned by `operator()`. Since standard allocator copies are equivalent -- memory allocated with one copy can be deallocated with another -- this preserves correctness while keeping the allocator alive for the task's duration. @@ -257,6 +266,7 @@ The solution is to store a _copy_ of the allocator in the awaitable returned by In `std::execution` (P2300), context flows _backward_ from receiver to sender via queries _after_ `connect()`: +[source,cpp,role=external] ---- task async_work(); // Frame allocated NOW auto sndr = async_work(); @@ -266,6 +276,7 @@ start(op); In the _IoAwaitable_ model, context flows _forward_ from launcher to task: +[role=figure] ---- 1. Set TLS allocator --> 2. Call task() 3. operator new (uses TLS) @@ -278,8 +289,8 @@ The allocator is ready before the frame is created. No query machinery can retro [cols="1,2"] |=== -| `run_async(ctx)(task)` | Fire-and-forget launch from non-coroutine code -| `co_await run(ctx)(task)` | Awaitable launch within a coroutine +| cpp:run_async[]`(ctx)(task)` | Fire-and-forget from non-coroutine code +| `co_await` cpp:run[]`(ctx)(task)` | Awaitable within a coroutine |=== -The `run` name is greppable, unambiguous, and won't collide with local variables in a namespace-heavy Boost codebase. The `f(ctx)(task)` syntax exists because coroutine frame allocation requires the allocator to be set _before_ the task expression is evaluated, and {cpp}17 postfix sequencing guarantees exactly that ordering. The syntax is intentionally explicit about its two steps -- it tells the reader that something important happens between them. +The cpp:run[] name is greppable, unambiguous, and won't collide with local variables in a namespace-heavy Boost codebase. The `f(ctx)(task)` syntax exists because coroutine frame allocation requires the allocator to be set _before_ the task expression is evaluated, and {cpp}17 postfix sequencing guarantees exactly that ordering. The syntax is intentionally explicit about its two steps -- it tells the reader that something important happens between them. diff --git a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc index 1e0e406e7..49e1d5219 100644 --- a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc +++ b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc @@ -1,10 +1,11 @@ = Capy and Boost.Cobalt: A Comparison +:page-mode: explanation Both libraries use {cpp}20 coroutines for asynchronous programming. The differences begin with the foundation. Cobalt is a coroutine layer built on Boost.Asio. It adds coroutine syntax — `promise`, `task`, `generator` — on top of Asio's existing I/O infrastructure. Asio is not coroutines-only. It supports callbacks, futures, and coroutines equally. Cobalt inherits this foundation. It can add coroutine types on top, but it cannot change what lies beneath. -Capy is a coroutine-native I/O foundation designed from the ground up. The design started from the ideal use case and worked backward to the implementation. The concept hierarchy, the type-erased wrappers, the allocator model — these fell out naturally from use-case-first design, without compromise. +Capy is a coroutine-native I/O foundation designed from the ground up. The design started from the ideal use case and worked backward to the implementation. The flat concept set, the type-erased wrappers, the allocator model — these fell out naturally from use-case-first design, without compromise. == The Dimovian Ideal @@ -21,7 +22,7 @@ Capy achieves the Dimovian Ideal. The proof is in `example/asio/`. include::example$asio/api/capy_streams.hpp[tag=header] ---- -Asio appears only as a forward declaration. The context uses pimpl. The factory returns `capy::any_stream` — a type-erased stream that hides the concrete socket type entirely. +Asio appears only as a forward declaration. The context uses pimpl. The factory returns cpp:capy::any_stream[capy::any_stream] — a type-erased stream that hides the concrete socket type entirely. === The Translation Unit @@ -36,7 +37,7 @@ Asio appears only as a forward declaration. The context uses pimpl. The factory include::example$asio/any_stream.cpp[tag=writer_reader] ---- -`writer()` and `reader()` operate on `capy::any_stream&`. They don't know what I/O backend produced the stream. They never need to know. +`writer()` and `reader()` operate on cpp:capy::any_stream[capy::any_stream]`&`. They don't know what I/O backend produced the stream. They never need to know. === What Cobalt Does Instead @@ -77,54 +78,23 @@ Templates can achieve this by type-erasing every customization point. The cost m == Stream Concepts -Capy defines seven coroutine-only stream concepts. Cobalt inherits Asio's `AsyncReadStream` and `AsyncWriteStream`, which are hybrid concepts supporting callbacks, futures, and coroutines. Cobalt's `cobalt::io` wrappers simplify the API and Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes, a distinct approach from Capy's concept-based hierarchy. Cobalt's wrappers still include full Asio headers. See xref:9.design/9n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a detailed comparison of the two approaches. +Capy defines three coroutine-only stream concepts. Cobalt inherits Asio's `AsyncReadStream` and `AsyncWriteStream`, which are hybrid concepts supporting callbacks, futures, and coroutines. Cobalt's `cobalt::io` wrappers simplify the API and Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes, a distinct approach from Capy's flat set of concepts. Cobalt's wrappers still include full Asio headers. See xref:9.design/9n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a detailed comparison of the two approaches. -Capy's concepts form a refinement hierarchy that emerged naturally from use-case-first design: - -.... - ReadStream WriteStream - (partial reads) (partial writes) - | | - v v - ReadSource WriteSink - (complete reads) (complete writes + EOF) - - - BufferSource BufferSink - (zero-copy pull) (zero-copy prepare/commit) -.... - -`BufferSource` and `BufferSink` implement callee-owns-buffers I/O. The source provides buffers; the caller processes them in place. No copies. Memory-mapped files, hardware DMA buffers, and kernel-provided memory all work naturally through this pattern. +Capy's concepts form a small, flat set that emerged naturally from use-case-first design: cpp:ReadStream[] and cpp:WriteStream[] each define a single partial-transfer primitive (`read_some`, `write_some`); cpp:Stream[] requires both. There is no further refinement. [cols="1,1,1"] |=== | Concept | Capy | Cobalt -| `ReadStream` -| Yes -| - -| `WriteStream` -| Yes -| - -| `Stream` +| cpp:ReadStream[] | Yes | -| `ReadSource` +| cpp:WriteStream[] | Yes | -| `WriteSink` -| Yes -| - -| `BufferSource` -| Yes -| - -| `BufferSink` +| cpp:Stream[] | Yes | |=== @@ -133,22 +103,19 @@ Capy's concepts form a refinement hierarchy that emerged naturally from use-case Traditional approaches to type erasure in Asio focus on the lowest-level elements: the completion handler, the executor, the allocator. This is not the right layer. Type-erasing these individually adds overhead at every customization point while still leaving the stream type concrete and visible. -Capy type-erases the stream itself. This is possible because coroutines provide structural type erasure — the continuation is always a handle, not a template parameter. When the library is coroutines-only, one virtual call per I/O operation is the total cost. The completion handler, executor, and allocator do not need individual erasure because they are not part of the stream's operation signature. +Capy type-erases the stream itself. This is possible because coroutines provide structural type erasure — the continuation is always a handle, not a template parameter. When the library is coroutines-only, up to five vtable calls per I/O operation is the total cost — not one per continuation. A synchronously completed operation costs four; `await_suspend` is skipped whenever `await_ready` returns `true`. The completion handler, executor, and allocator do not need individual erasure because they are not part of the stream's operation signature. Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes in `cobalt/io/stream.hpp`, taking a different approach from Capy's concept + type-erased wrapper model. See xref:9.design/9n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a side-by-side analysis. -The wrappers compose. `any_buffer_source` also satisfies `ReadSource` — natively if the wrapped type supports both, synthesized otherwise. `any_buffer_sink` also satisfies `WriteSink`. You pick the abstraction level you need. +The wrappers compose. cpp:any_stream[] satisfies both cpp:any_read_stream[] and cpp:any_write_stream[] by inheriting from each, giving callers a single wrapper for bidirectional streams while keeping `read_some` and `write_some` on independent flat vtables. You pick the abstraction level you need. +[role=figure] .... Concept Type-Erased Wrapper --------------------+------------------------ ReadStream -----> any_read_stream WriteStream -----> any_write_stream Stream -----> any_stream - ReadSource -----> any_read_source - WriteSink -----> any_write_sink - BufferSource -----> any_buffer_source ----> also satisfies any_read_source - BufferSink -----> any_buffer_sink ----> also satisfies any_write_sink .... This is how the Dimovian Ideal is mechanically achieved. @@ -157,31 +124,15 @@ This is how the Dimovian Ideal is mechanically achieved. |=== | Type-Erased Wrapper | Capy | Cobalt -| `any_read_stream` +| cpp:any_read_stream[] | Yes | -| `any_write_stream` +| cpp:any_write_stream[] | Yes | -| `any_stream` -| Yes -| - -| `any_read_source` -| Yes -| - -| `any_write_sink` -| Yes -| - -| `any_buffer_source` -| Yes -| - -| `any_buffer_sink` +| cpp:any_stream[] | Yes | |=== @@ -192,12 +143,10 @@ When algorithms operate on type-erased interfaces, testing becomes deterministic Capy's mock types: -* `test::read_stream`, `test::write_stream` — partial I/O mocks -* `test::stream` — connected pair for bidirectional testing -* `test::read_source`, `test::write_sink` — complete I/O mocks -* `test::buffer_source`, `test::buffer_sink` — zero-copy mocks +* cpp:test::read_stream[test::read_stream], cpp:test::write_stream[test::write_stream] — partial I/O mocks +* cpp:test::stream[test::stream] — connected pair for bidirectional testing -`test::fuse` injects errors systematically at every I/O operation point. `test::run_blocking` executes coroutines synchronously for deterministic unit tests. `max_read_size` and `max_write_size` simulate chunked delivery. `expect()` validates written data. +cpp:test::fuse[test::fuse] injects errors systematically at every I/O operation point. cpp:test::run_blocking[test::run_blocking] executes coroutines synchronously for deterministic unit tests. `max_read_size` and `max_write_size` simulate chunked delivery. `expect()` validates written data. Tests run without sockets or network access, eliminating non-determinism. @@ -205,31 +154,15 @@ Tests run without sockets or network access, eliminating non-determinism. |=== | Testing Feature | Capy | Cobalt -| `test::read_stream` -| Yes -| - -| `test::write_stream` -| Yes -| - -| `test::stream` (connected pair) -| Yes -| - -| `test::read_source` +| cpp:test::read_stream[test::read_stream] | Yes | -| `test::write_sink` +| cpp:test::write_stream[test::write_stream] | Yes | -| `test::buffer_source` -| Yes -| - -| `test::buffer_sink` +| cpp:test::stream[test::stream] (connected pair) | Yes | @@ -254,18 +187,18 @@ Tests run without sockets or network access, eliminating non-determinism. Cobalt is single-threaded by design. One executor per thread. Channels are restricted to a single thread — Cobalt's own documentation states: "Channels can be used to exchange data between different coroutines on a single thread." Primitives cannot be shared between threads. -Capy supports multi-threaded execution. `thread_pool` distributes work across threads. `strand` serializes execution without blocking OS threads. The `Executor` concept is open — implement your own. +Capy supports multi-threaded execution. cpp:thread_pool[] distributes work across threads. cpp:strand[] serializes execution without blocking OS threads. The cpp:Executor[] concept is open — implement your own. [cols="1,1,1"] |=== | Threading | Capy | Cobalt | Multi-threaded execution -| `thread_pool` +| cpp:thread_pool[] | No | Serialized execution -| `strand` +| cpp:strand[] | Single-threaded only | Executor model @@ -273,8 +206,8 @@ Capy supports multi-threaded execution. `thread_pool` distributes work across th | Single-threaded (closed) | Cross-thread channels -| Yes -| No +| No channel primitive ships +| Single-thread only | Primitives shareable across threads | Yes @@ -292,7 +225,7 @@ Capy introduces the https://wg21.link/P4003[IoAwaitable protocol] and uses it fo include::example$snippets/9m_why_not_cobalt.cpp[tag=await_suspend_env,indent=0] ---- -No thread-local state. No ambient context. The executor and stop token flow forward through the call chain via the `io_env` parameter. +No thread-local state. No ambient context. The executor and stop token flow forward through the call chain via the cpp:io_env[] parameter. [cols="1,1,1"] |=== @@ -311,7 +244,7 @@ No thread-local state. No ambient context. The executor and stop token flow forw | No | Stop token delivery -| Structural (`io_env`) +| Structural (cpp:io_env[]) | `this_coro::cancellation_state` |=== @@ -319,7 +252,7 @@ No thread-local state. No ambient context. The executor and stop token flow forw Both libraries propagate cancellation automatically through coroutine chains. Both support OS-level cancellation of pending I/O operations (`CancelIoEx` on Windows, `IORING_OP_ASYNC_CANCEL` on Linux). -Capy uses `std::stop_token`, propagated via the IoAwaitable protocol's `io_env` parameter. The token flows forward structurally alongside the executor. +Capy uses `std::stop_token`, propagated via the IoAwaitable protocol's cpp:io_env[] parameter. The token flows forward structurally alongside the executor. Cobalt uses Asio's `cancellation_signal` and `cancellation_slot`. Propagation is wired automatically in `await_suspend` via `forward_cancellation`. `this_coro::cancellation_state` provides filtering control over which cancellation types pass through. @@ -332,7 +265,7 @@ Cobalt uses Asio's `cancellation_signal` and `cancellation_slot`. Propagation is | `asio::cancellation_signal` | Propagation -| Automatic (`io_env`) +| Automatic (cpp:io_env[]) | Automatic (slot/signal wiring) | Filtering @@ -346,7 +279,7 @@ Cobalt uses Asio's `cancellation_signal` and `cancellation_slot`. Propagation is == Buffer Sequences -Capy adopts Asio's buffer sequence model — `ConstBufferSequence`, `MutableBufferSequence` — because it works. Capy's buffer types are fully compatible with Asio's. You can pass Capy buffers to Asio operations and vice versa, seamlessly. Then Capy extends the model with additional types and algorithms, while still achieving the Dimovian Ideal — none of this requires exposing Asio headers to consumers. +Capy adopts Asio's buffer sequence model — cpp:ConstBufferSequence[], cpp:MutableBufferSequence[] — because it works. The optional `` header adapts between the two with `to_asio()`/`from_asio()` wrapper sequences — not an implicit conversion, but not a copy of the underlying memory either. Then Capy extends the model with additional types and algorithms, while still achieving the Dimovian Ideal — none of this requires exposing Asio headers to consumers. Cobalt does not provide buffer sequence types. Users who need these features use Asio's types directly. @@ -354,19 +287,19 @@ Cobalt does not provide buffer sequence types. Users who need these features use |=== | Buffer Feature | Capy | Cobalt -| `ConstBufferSequence` +| cpp:ConstBufferSequence[] | Yes | Via Asio -| `MutableBufferSequence` +| cpp:MutableBufferSequence[] | Yes | Via Asio -| `front` +| cpp:front[] | Yes | -| `buffer_slice` +| cpp:buffer_slice[] | Yes | @@ -391,9 +324,9 @@ struct awaitable : awaitable_base If the buffer is exhausted, allocations fall back to the upstream PMR resource or `operator new`. The buffer size is a compile-time constant. Changing it requires recompiling the library. -Capy leaves these decisions to the user. `run_async(executor, allocator)(my_task())` sets the allocator before the task is created. The task's `operator new` reads it from thread-local storage. This is a small, flexible customization point that permits usage patterns the authors did not anticipate: per-connection arenas, bounded pools, tracking allocators, per-tenant memory budgets. The allocation strategy is a deployment decision, not a library decision. +Capy leaves these decisions to the user. cpp:run_async[]`(executor, allocator)(my_task())` sets the allocator before the task is created. The task's `operator new` reads it from thread-local storage. This is a small, flexible customization point that permits usage patterns the authors did not anticipate: per-connection arenas, bounded pools, tracking allocators, per-tenant memory budgets. The allocation strategy is a deployment decision, not a library decision. -`recycling_memory_resource` provides zero-overhead recycling after warmup. Memory isolated per connection. Reclaimed instantly on disconnect. +cpp:recycling_memory_resource[] provides zero-overhead recycling after warmup. Memory isolated per connection. Reclaimed instantly on disconnect. [cols="1,1,1"] |=== @@ -412,11 +345,11 @@ Capy leaves these decisions to the user. `run_async(executor, allocator)(my_task | No | Recycling allocator -| `recycling_memory_resource` +| cpp:recycling_memory_resource[] | | Custom allocator support -| `run_async(ex, alloc)` +| cpp:run_async[]`(ex, alloc)` | Global setup only | Deterministic freeing @@ -428,7 +361,7 @@ Capy leaves these decisions to the user. `run_async(executor, allocator)(my_task Cobalt is coupled to Asio's `io_context`. The execution model and the platform abstractions are one thing. -Capy separates them. The execution model — executors, cancellation, allocation — lives in Capy. Platform abstractions live in Corosio, a companion library that provides native TCP sockets, acceptors, TLS streams, timers, DNS resolution, and signal handling — all built on Capy's IoAwaitable protocol with native IOCP and epoll backends. You can test Capy's execution model without a network stack. You can swap the I/O backend without changing your application code. +Capy separates them. The execution model — executors, cancellation, allocation — lives in Capy. Platform abstractions live in Corosio, a companion library that provides native TCP sockets, acceptors, TLS streams, timers, DNS resolution, and signal handling. It builds on Capy's IoAwaitable protocol with five native backends: epoll and io_uring (Linux), kqueue (BSD/macOS), IOCP (Windows), and select (portable fallback). You can test Capy's execution model without a network stack. You can swap the I/O backend without changing your application code. [cols="1,1,1"] |=== @@ -465,7 +398,7 @@ The input is `numbers.json` from the Boost.JSON benchmark suite. Results are bes | 317 us | 1.0x -| `capy::task` +| cpp:capy::task[capy::task] | 537 us | 1.69x @@ -482,7 +415,8 @@ Capy's coroutine-driven serializer runs at 1.69x the baseline. Cobalt's `promise The Capy implementation: -[source,cpp,role=external] +.Excerpt - not compiled here +[source,cpp,role=pseudocode] ---- namespace { @@ -575,19 +509,19 @@ Every `co_await ws.write(...)` call creates a coroutine frame, suspends, resumes | Backend types exposed | Stream concepts -| 7 coroutine-only (refinement hierarchy) +| 3 coroutine-only (flat) | Asio's (hybrid) | Type-erased streams -| 7 wrappers +| 3 wrappers | None | Mock streams -| 7 mock types + `fuse` +| 3 mock types + `fuse` | None | Threading -| Multi-threaded (`thread_pool`, `strand`) +| Multi-threaded (cpp:thread_pool[], cpp:strand[]) | Single-threaded | Context propagation @@ -599,7 +533,7 @@ Every `co_await ws.write(...)` call creates a coroutine frame, suspends, resumes | `cancellation_signal`, automatic, OS-level | Buffer sequences -| Extended (`buffer_slice`, `front`) +| Extended (cpp:buffer_slice[], cpp:front[]) | None (use Asio directly) | Allocator control diff --git a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc index e0dae2b69..b27c70d2a 100644 --- a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc +++ b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc @@ -1,4 +1,5 @@ = Write Stream Design: A Side-by-Side Analysis +:page-mode: explanation Both Capy and Cobalt allow you to write a non-template coroutine algorithm that operates on a type-erased write stream. The function signatures look similar: @@ -21,6 +22,7 @@ Each section below examines one design choice and its technical consequences. Capy formally defines what makes a task type conforming. Two {cpp}20 concepts form a refinement hierarchy: +[role=figure] .... IoAwaitable | @@ -28,21 +30,21 @@ IoAwaitable IoRunnable .... -`IoAwaitable` is the base. It requires a single syntactic property -- the `await_suspend` signature must accept an `io_env` parameter containing the execution environment: +cpp:IoAwaitable[] is the base. It requires a single syntactic property -- the `await_suspend` signature must accept an cpp:io_env[] parameter containing the execution environment: [source,cpp] ---- include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=io_awaitable_concept] ---- -`IoRunnable` refines `IoAwaitable` with operations needed to start a task from non-coroutine contexts: `handle()`, `release()`, `exception()`, `result()`, and the promise-level `set_continuation()` and `set_environment()`. +cpp:IoRunnable[] refines cpp:IoAwaitable[] with operations needed to start a task from non-coroutine contexts: cpp:IoRunnable::handle[handle()], cpp:IoRunnable::release[release()], cpp:IoRunnable::exception[exception()], cpp:IoRunnable::result[result()], and the promise-level cpp:IoRunnable::set_continuation[set_continuation()] and cpp:IoRunnable::set_environment[set_environment()]. [source,cpp] ---- include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=io_runnable_concept] ---- -Context injection (`set_environment`, `set_continuation`) consists of `noexcept` requirements that `IoRunnable` places on `T::promise_type`; launch functions invoke them through the typed handle returned by `handle()` before resuming the frame. +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. Each concept has documented syntactic requirements, semantic requirements, conforming signatures, and examples. A user who wants to create a custom task type can read the concept definition and know exactly what to provide. The compiler enforces the syntactic requirements at constraint-check time. @@ -91,7 +93,7 @@ The associators are optional member functions on the promise. Each awaitable pro | Aspect | Capy | Cobalt | Task requirements -| Named concepts (`IoAwaitable`, `IoRunnable`) +| Named concepts (cpp:IoAwaitable[], cpp:IoRunnable[]) | Implicit associators probed via `if constexpr` | Specification @@ -115,14 +117,14 @@ The associators are optional member functions on the promise. Each awaitable pro The task requirements directly determine how executor and cancellation context reach child operations. -Capy's `IoAwaitable` protocol passes the execution environment as an explicit parameter to `await_suspend`: +Capy's cpp:IoAwaitable[] protocol passes the execution environment as an explicit parameter to `await_suspend`: [source,cpp] ---- include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=await_suspend_env,indent=0] ---- -The executor and stop token flow forward structurally through the call chain via `io_env`. If a task's machinery does not provide them, the code does not compile. There is no fallback. +The executor and stop token flow forward structurally through the call chain via cpp:io_env[]. If a task's machinery does not provide them, the code does not compile. There is no fallback. Cobalt's approach probes the calling promise. The relevant code from `cobalt/detail/task.hpp`: @@ -150,7 +152,7 @@ Two things happen when a promise does not provide an associator: * If `get_cancellation_slot()` is absent, the cancellation wiring block is skipped. The child task operates without cancellation support. -Both behaviors are intentional. Cobalt treats associators as optional capabilities -- a task without `get_cancellation_slot()` simply does not propagate cancellation to its children. This is a design choice that favors flexibility: tasks can participate in the system without implementing every associator. The trade-off is that omitting an associator produces no compile-time diagnostic, so the behavior difference must be understood by the author of the custom task. +Both behaviors are intentional. Cobalt treats associators as optional capabilities -- a task without `get_cancellation_slot()` does not propagate cancellation to its children. This is a design choice that favors flexibility: tasks can participate in the system without implementing every associator. The trade-off is that omitting an associator produces no compile-time diagnostic, so the behavior difference must be understood by the author of the custom task. In Capy, the equivalent of "no cancellation" is passing `std::stop_token{}` (a never-stop token) explicitly. Both designs support uncancellable operations; they differ in whether that choice is expressed through presence of a parameter or absence of a member function. @@ -209,14 +211,14 @@ struct const_buffer_sequence This is a subset of Asio's full `ConstBufferSequence` concept. Buffer sequence types that do not convert to `const_buffer` or `span` cannot be passed through this interface. -Capy's `WriteStream` concept requires `write_some` to accept any `ConstBufferSequence`: +Capy's cpp:WriteStream[] concept requires `write_some` to accept any cpp:ConstBufferSequence[]: [source,cpp] ---- include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=write_stream_concept_short] ---- -The type-erased wrapper `any_write_stream` also models the `WriteStream` concept. Its `write_some` is a template that accepts any `ConstBufferSequence`: +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[]: [source,cpp] ---- @@ -230,11 +232,11 @@ Both the concept and the wrapper accept full buffer sequences. Type erasure does | Aspect | Capy | Cobalt | `write_some` parameter -| `template` +| cpp:ConstBufferSequence[template] | `const_buffer_sequence` (concrete type) | Accepted buffer types -| Any `ConstBufferSequence` +| Any cpp:ConstBufferSequence[] | `const_buffer`, `span` | Type-erased wrapper accepts full concept @@ -267,7 +269,7 @@ struct write_stream The documentation describes what a stream is but does not specify behavioral details for `write_some`: empty buffer handling, error reporting conventions, partial write guarantees, buffer consumption order, or buffer lifetime assumptions. These details are left to the implementor's judgment or inferred from Asio conventions. -Capy's `WriteStream` concept includes semantic requirements in the concept's documentation: +Capy's cpp:WriteStream[] concept includes semantic requirements in the concept's documentation: [source,cpp] ---- @@ -290,7 +292,7 @@ include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=lifetime_warning] | | Error reporting semantics -| Documented (`ec` + `n >= 0 && n \< buffer_size`) +| Documented (`ec` + `n >= 0 && n \<` cpp:buffer_size[]) | | Partial write guarantees @@ -349,14 +351,14 @@ ____ The documentation describes the mechanical role of each function pointer but does not specify what the `implementation` function must do with the buffer, what completion semantics to follow, how to report errors through the `completion_handler`, or under what conditions `try_implementation` should complete synchronously. Implementors can look to the existing I/O wrappers (e.g., `stream_socket`) as reference implementations. -In Capy, the implementation contract lives in the `WriteStream` concept definition. A type satisfies `WriteStream` by providing a `write_some` member function template that await-returns `(error_code, std::size_t)`. The semantic requirements are part of the concept. There is no separate operation type to construct and no function pointers to provide. +In Capy, the implementation contract lives in the cpp:WriteStream[] concept definition. A type satisfies cpp:WriteStream[] by providing a `write_some` member function template that await-returns `(error_code, std::size_t)`. The semantic requirements are part of the concept. There is no separate operation type to construct and no function pointers to provide. [cols="1,1,1"] |=== | Aspect | Capy | Cobalt | How to implement -| Satisfy the `WriteStream` concept +| Satisfy the cpp:WriteStream[] concept | Construct `write_op` with function pointers + `void*` | Contract location @@ -396,7 +398,7 @@ struct awaitable : awaitable_base `write_op` is `final`. The return type of `write_stream::write_some` is fixed. Subclasses of `write_stream` cannot change the return type, the allocation strategy, or the SBO buffer size. Every `co_await stream.write_some(buf)` places this 4096-byte awaitable in the coroutine frame regardless of whether the underlying implementation needs it. -Capy's `any_write_stream` takes a different approach. The constructor preallocates storage sized exactly to the wrapped stream's awaitable type: +Capy's cpp:any_write_stream[] takes a different approach. The constructor preallocates storage sized exactly to the wrapped stream's awaitable type: [source,cpp] ---- @@ -405,7 +407,7 @@ include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=ctor_prealloc] After construction, each `co_await stream.write_some(buf)` reuses this preallocated storage. There is no per-operation allocation and no fixed-size buffer in the coroutine frame. -If users prefer a different allocation strategy, they can write algorithms directly against the `WriteStream` concept and build their own wrapper. The concept and the type-erased wrapper are separate layers. Cobalt's abstract base class fuses the abstraction and the allocation strategy into one type. +If users prefer a different allocation strategy, they can write algorithms directly against the cpp:WriteStream[] concept and build their own wrapper. The concept and the type-erased wrapper are separate layers. Cobalt's abstract base class fuses the abstraction and the allocation strategy into one type. [cols="1,1,1"] |=== @@ -424,7 +426,7 @@ If users prefer a different allocation strategy, they can write algorithms direc | No (`write_op` is `final`) | Custom allocation strategy -| Write against `WriteStream` concept +| Write against cpp:WriteStream[] concept | Not possible (return type is fixed) |=== @@ -434,17 +436,17 @@ The preceding sections each examined a specific design choice. A common thread r Cobalt's `write_stream` is an abstract base class. The abstraction and the runtime wrapper are the same type. Writing against the abstraction means using virtual dispatch. The return type (`write_op`), the buffer parameter type (`const_buffer_sequence`), the allocation strategy (4096-byte SBO), and the context propagation mechanism (promise probing) are all fixed by the base class definition. -Capy separates the abstraction from the wrapper. `WriteStream` is a {cpp}20 concept: +Capy separates the abstraction from the wrapper. cpp:WriteStream[] is a {cpp}20 concept: [source,cpp] ---- include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=write_stream_concept] ---- -`any_write_stream` is a type-erased wrapper that satisfies this concept. It is one possible reification, not the only one. Users can: +cpp:any_write_stream[] is a type-erased wrapper that satisfies this concept. It is one possible reification, not the only one. Users can: -* Write generic algorithms constrained by `WriteStream` -- these work with any conforming stream, with no virtual dispatch overhead. -* Use `any_write_stream` when runtime polymorphism is needed -- it provides type erasure with preallocated awaitable storage. +* Write generic algorithms constrained by cpp:WriteStream[] -- these work with any conforming stream, with no virtual dispatch overhead. +* Use cpp:any_write_stream[] when runtime polymorphism is needed -- it provides type erasure with preallocated awaitable storage. * Build a custom wrapper with a different allocation or dispatch strategy -- the concept defines the contract independently of any particular wrapper. This separation is the architectural root of the differences examined in this document. It is what enables full buffer sequence support at the type-erased layer, formal semantic specification in a concept definition, user-selectable allocation strategies, and explicit context propagation through `await_suspend` parameters. @@ -454,11 +456,11 @@ This separation is the architectural root of the differences examined in this do | Aspect | Capy | Cobalt | Abstraction mechanism -| {cpp}20 concept (`WriteStream`) +| {cpp}20 concept (cpp:WriteStream[]) | Abstract base class (`write_stream`) | Runtime wrapper -| Separate (`any_write_stream`) +| Separate (cpp:any_write_stream[]) | Same type as abstraction | Generic algorithms (no type erasure) @@ -477,7 +479,7 @@ This separation is the architectural root of the differences examined in this do | Design Choice | Capy | Cobalt | Task requirements -| Named concept hierarchy (`IoAwaitable` -> `IoRunnable`) +| Named concept hierarchy (cpp:IoAwaitable[] -> cpp:IoRunnable[]) | Implicit associators (`get_executor`, `get_cancellation_slot`, `get_allocator`) | Context propagation @@ -485,7 +487,7 @@ This separation is the architectural root of the differences examined in this do | Promise probing via `if constexpr` with thread-local fallback | Buffer sequence support -| Full `ConstBufferSequence` (concept + wrapper) +| Full cpp:ConstBufferSequence[] (concept + wrapper) | `const_buffer_sequence` (concrete subset) | Semantic specification diff --git a/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc b/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc index b84474b33..893570f9b 100644 --- a/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc +++ b/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc @@ -1,4 +1,5 @@ = Capy and TooManyCooks: A Comparison +:page-mode: explanation You want to write async code in {cpp}. You've heard about coroutines. Two libraries exist: Capy and TooManyCooks (TMC). Both let you write `co_await`. Both run on multiple threads. @@ -33,7 +34,7 @@ When async code finishes waiting, it needs to resume somewhere. Where? * Context flows forward through `await_suspend(h, env)` where `env` is an `io_env` * Your coroutine started on executor X? It resumes on executor X. -* Child tasks can run on different executors via `run(other_ex)(child_task())` +* Child tasks can run on different executors via cpp:run[]`(other_ex)(child_task())` *TMC's answer:* Where you tell it, with flexibility to change mid-execution. @@ -65,7 +66,7 @@ tmc::task example() { This is powerful for compute workloads where work can migrate between thread pools. -*Capy's design choice:* Intentionally prevent mid-coroutine executor switching. A coroutine stays on its bound executor for its entire lifetime. Child tasks can run on different executors via `run(other_ex)(child_task())`, but the parent never moves. +*Capy's design choice:* Intentionally prevent mid-coroutine executor switching. A coroutine stays on its bound executor for its entire lifetime. Child tasks can run on different executors via cpp:run[]`(other_ex)(child_task())`, but the parent never moves. *Why Capy prevents this:* I/O objects often have invariants tied to their executor: @@ -137,7 +138,7 @@ HALO (Heap Allocation Lowering Optimization) lets compilers eliminate coroutine *Capy provides:* -* Custom allocator propagation via `run_async(ex, allocator)` and `run(allocator)` +* Custom allocator propagation via cpp:run_async[]`(ex, allocator)` and cpp:run[]`(allocator)` * Per-connection arena allocation * Memory isolation between connections * Instant reclamation on connection close @@ -179,9 +180,9 @@ The awaitable receives: * `h` — The handle (for resumption) * `env` — The execution environment containing: -** `env->executor` — The executor (where to resume) -** `env->stop_token` — A stop token (for cancellation) -** `env->frame_allocator` — An optional `std::pmr::memory_resource*` for coroutine frame allocation (null selects the default allocator) +** cpp:io_env::executor[env->executor] — The executor (where to resume) +** cpp:io_env::stop_token[env->stop_token] — A stop token (for cancellation) +** cpp:io_env::frame_allocator[env->frame_allocator] — An optional `std::pmr::memory_resource*` for coroutine frame allocation (null selects the default allocator) *TMC's approach:* @@ -297,7 +298,7 @@ TMC integrates with Asio via `aw_asio.hpp`/`ex_asio.hpp`. Corosio provides nativ | Type erasure | Asio handler + `ex_any` -| `executor_ref` only +| cpp:executor_ref[] only | Tuple packing | Yes (init args) @@ -322,9 +323,9 @@ The critical path difference is completion. TMC+Asio goes through `resume_contin *Capy:* -* `any_stream`, `any_read_stream`, `any_write_stream` +* cpp:any_stream[], cpp:any_read_stream[], cpp:any_write_stream[] * Write a function taking `any_stream&` - it compiles once -* One virtual call per I/O operation +* Up to five vtable calls per I/O operation (four when synchronous), not one per continuation * Clean ABI boundaries *TMC:* @@ -358,9 +359,9 @@ Neither is "more fundamental." If you're building a network server, Capy's const Capy is a foundation. Corosio builds real networking on it: * TCP sockets, acceptors -* TLS streams (WolfSSL) +* TLS streams (OpenSSL, WolfSSL) * Timers, DNS resolution, signal handling -* Native backends: IOCP (Windows), epoll (Linux), io_uring (planned) +* Native backends: epoll and io_uring (Linux), kqueue (BSD/macOS), IOCP (Windows), select (portable fallback) All built on Capy's IoAwaitable protocol. Coroutines only. No callbacks. @@ -399,7 +400,7 @@ TMC for compute scheduling, Capy/Corosio for I/O. They can coexist at different | Compute scheduling | Threading -| Multi-threaded (`thread_pool`) +| Multi-threaded (cpp:thread_pool[]) | Multi-threaded (work-stealing) | Executor mobility @@ -407,7 +408,7 @@ TMC for compute scheduling, Capy/Corosio for I/O. They can coexist at different | Mid-body switching (`resume_on`) | Serialization -| `strand` (ordering preserved across suspend) +| cpp:strand[] (ordering preserved across suspend) | `ex_braid` (lock released on suspend) | Context propagation @@ -431,11 +432,11 @@ TMC for compute scheduling, Capy/Corosio for I/O. They can coexist at different | No (use Asio) | Stream concepts -| Yes (`ReadStream`, `WriteStream`, etc.) +| Yes (cpp:ReadStream[], cpp:WriteStream[], etc.) | No | Type-erased streams -| Yes (`any_stream`) +| Yes (cpp:any_stream[]) | No | I/O support diff --git a/doc/modules/ROOT/pages/A.specification-methods/A.intro.adoc b/doc/modules/ROOT/pages/A.specification-methods/A.intro.adoc index 83184b6ef..d30d64ec5 100644 --- a/doc/modules/ROOT/pages/A.specification-methods/A.intro.adoc +++ b/doc/modules/ROOT/pages/A.specification-methods/A.intro.adoc @@ -8,9 +8,16 @@ // = Methods of API Description +:page-mode: explanation +This section describes the conventions used to specify the API of this library in the +following xref:reference:boost/capy.adoc[Reference] section. +== What This Section Covers -This section describes the conventions used to specify the API of this library in the following -xref:reference:boost/capy.adoc[Reference] section. +* xref:A.specification-methods/Ab.cancellation.adoc[Cancellation] -- What it means for a + function to support `IoAwaitable` cancellation via a propagated `std::stop_token`. +* xref:A.specification-methods/Ac.contingencies.adoc[Contingencies] -- What a + _contingency_ is, and how the reference describes operations that cannot complete in + full. diff --git a/doc/modules/ROOT/pages/A.specification-methods/Ab.cancellation.adoc b/doc/modules/ROOT/pages/A.specification-methods/Ab.cancellation.adoc index 145b7b1c4..7d52272e2 100644 --- a/doc/modules/ROOT/pages/A.specification-methods/Ab.cancellation.adoc +++ b/doc/modules/ROOT/pages/A.specification-methods/Ab.cancellation.adoc @@ -8,13 +8,14 @@ // = Cancellation +:page-mode: explanation A function is said to _support IoAwaitable cancellation_ when its return type -models concept `IoAwaitable` and this return object `a` controls a coroutine which +models concept cpp:IoAwaitable[] and this return object `a` controls a coroutine which can be prematurely stopped using the `std::stop_token` propagated through the `IoAwaitable` protocol. Additionally, if the result type of expression `co_await a` -in the context of a Capy-coroutine is a specialization of `io_result` +in the context of a Capy-coroutine is a specialization of cpp:io_result[] then the cancelling of an operation is -considered a contingency represented by condition `cond::canceled`. +considered a contingency represented by condition cpp:cond::canceled[cond::canceled]. diff --git a/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc b/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc index 54ccc9caa..6704a7930 100644 --- a/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc +++ b/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc @@ -8,24 +8,25 @@ // = Contingencies +:page-mode: explanation A _contingency_ is any situation occurring during an operation on a stream, caused by the stream's state, that prevents this operation from reading or writing the requested number of bytes. These situations do not violate the postconditions of the corresponding operations, -as their postconditions never say that the requested number of bytes will indeed be -processed. +as their postconditions never say that the operation indeed processes the requested +number of bytes. Each stream operation that may encounter a contingency await-returns -a type which is a specialization of `capy::io_result`. These objects can be _destructured_ +a type which is a specialization of cpp:capy::io_result[capy::io_result]. These objects can be _destructured_ using a structured binding. The first binding of such destructuring is of type `std::error_code`. This binding, call it `ec`, is used to signal if and which contingency occured: * If `ec == std::error_code{}`, no contingency occurred. - * Otherwise a contingency occurred. In order to determine which contingency occurred, - compare `ec` to error conditions, in particular to `capy::cond`. + * Otherwise a contingency occurred. To determine which contingency occurred, + compare `ec` to error conditions, in particular to cpp:capy::cond[capy::cond]. NOTE: Reaching the end of stream is also a contingency (which can be interpreted as preventing an infinite read @@ -36,7 +37,7 @@ NOTE: The stream operations can still throw exceptions to indicate conditions their postconditions, such as failures to grow a buffer, or failure to allocate a coroutine frame. -NOTE: Operations on streams often await-return `capy::io_result` +NOTE: Operations on streams often await-return cpp:capy::io_result[capy::io_result] destructuring to `[ec, n]`, where `n` represents the number of processed bytes. Upon a reported contingency, a non-zero `n` indicates the state of the partial or sometimes even a full read. When an inner operation reports a contingency, diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 3de9726b8..f382d3d8c 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -1,4 +1,5 @@ = Capy +:page-mode: explanation Capy abstracts away sockets, files, and asynchrony with type-erased streams and buffer sequences—code compiles fast because the implementation is hidden. It provides the framework for concurrent algorithms that transact in buffers of memory: networking, serial ports, console, timers, and any platform I/O. This is only possible because Capy is coroutine-only, enabling optimizations and ergonomics that hybrid approaches must sacrifice. @@ -6,32 +7,32 @@ Capy abstracts away sockets, files, and asynchrony with type-erased streams and Capy is two things at once: -* *A protocol.* `IoAwaitable` is a protocol for propagating a coroutine's _execution environment_—its executor, stop token, and allocator—forward through `co_await` chains. This is the vocabulary that lets awaitable-based coroutine libraries interoperate. +* *A protocol.* cpp:IoAwaitable[] is a protocol for propagating a coroutine's _execution environment_—its executor, stop token, and allocator—forward through `co_await` chains. This is the vocabulary that lets awaitable-based coroutine libraries interoperate. * *A reference implementation.* A concrete library—thread pool, task types, byte streams, buffer sequences, synchronization primitives—that proves the protocol works in practice. -The protocol is the smaller, more general library living inside Capy. Without a shared protocol, _N_ coroutine libraries need _N_×(_N_−1) adapters to interoperate; with one shared protocol for environment propagation, a single bridge covers everyone. This is the role `IoAwaitable` plays. +The protocol is the smaller, more general library living inside Capy. Without a shared protocol, _N_ coroutine libraries need _N_×(_N_−1) adapters to interoperate. With one shared protocol for environment propagation, a single bridge covers everyone. This is the role cpp:IoAwaitable[] plays. [IMPORTANT] ==== -*The core invariant: a coroutine always resumes on the executor it was launched with.* +*The core invariant: a coroutine always resumes on the executor it was started with.* -Launch a coroutine on a strand, and every resumption—after every `co_await`—happens on that strand. Shared state touched between suspension points is free of data races without a mutex. A _plain_ awaitable can resume a coroutine on any thread and would break this guarantee, so Capy rejects it at compile time and provides an explicit way to bridge such awaitables when you need one. +Start a coroutine on a strand, and every resumption—after every `co_await`—happens on that strand. Shared state touched between suspension points is free of data races without a mutex. A _plain_ awaitable can resume a coroutine on any thread and would break this guarantee. Capy therefore rejects it at compile time, and provides an explicit way to bridge such awaitables when you need one. See xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[the same-executor invariant] for the rationale and xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[bridging a foreign awaitable] for the escape hatch. ==== == What Capy Is Not -Capy is _not_ an all-purpose coroutine framework, and it is _not_ an implementation detail of Corosio. It is the execution model and byte-stream layer—usable standalone for logic that operates on streams without any platform I/O (HTTP parsing, protocol state machines, serialization), and usable as the foundation for Corosio's networking layer. CERN's traccc project uses Capy without Corosio for GPU reconstruction pipelines; the Boost.HTTP parser is built entirely on Capy's byte streams. +Capy is _not_ an all-purpose coroutine framework, and it is _not_ an implementation detail of Corosio. It is the execution model and byte-stream layer. It works standalone for logic that operates on streams without any platform I/O (HTTP parsing, protocol state machines, serialization). It also serves as the foundation for Corosio's networking layer. CERN's traccc project uses Capy without Corosio for GPU reconstruction pipelines; the Boost.HTTP parser is built entirely on Capy's byte streams. == What This Library Does -* *Lazy coroutine tasks* — `task` with forward-propagating stop tokens and automatic cancellation +* *Lazy coroutine tasks* — cpp:task[task] with forward-propagating stop tokens and automatic cancellation * *Buffer sequences* — taken straight from Asio and improved -* *Stream concepts* — three coroutine stream concepts: `ReadStream`, `WriteStream`, `Stream` -* *Type-erased streams* — `any_stream`, `any_read_stream`, `any_write_stream` for fast compilation -* *Concurrency facilities* — executors, strands, thread pools, `when_all`, `when_any` -* *Test utilities* — mock streams, mock sources/sinks, error injection +* *Stream concepts* — three coroutine stream concepts: cpp:ReadStream[], cpp:WriteStream[], cpp:Stream[] +* *Type-erased streams* — cpp:any_stream[], cpp:any_read_stream[], cpp:any_write_stream[] for fast compilation +* *Concurrency facilities* — executors, strands, thread pools, cpp:when_all[], cpp:when_any[] +* *Test utilities* — mock streams, error injection == What This Library Does Not Do @@ -89,7 +90,7 @@ None. Capy is self-contained and does not require Boost. === Linking -Capy is a compiled library. Link against `capy`. +Capy is a compiled library. Link against `Boost::capy`. == Code Convention @@ -114,13 +115,13 @@ include::example$programs/index_page_echo.cpp[tag=full] The `echo` function accepts an `any_stream&`—a type-erased wrapper that works with any concrete stream implementation. The function reads data into a buffer, then writes it back. Both operations use `co_await` to suspend until the I/O completes. -The `task<>` return type (equivalent to `task`) creates a lazy coroutine that does not start executing until awaited or launched with `run_async`. +The cpp:task[task<>] return type (equivalent to `task`) creates a lazy coroutine that does not start executing until awaited or started with cpp:run_async[]. == Next Steps * xref:quick-start.adoc[Quick Start] — Set up your first Capy project * xref:2.cpp20-coroutines/2a.foundations.adoc[{cpp}20 Coroutines Tutorial] — Learn coroutines from the ground up * xref:3.concurrency/3a.foundations.adoc[Concurrency Tutorial] — Understand threads, mutexes, and synchronization -* xref:4.coroutines/4a.tasks.adoc[Coroutines in Capy] — Deep dive into `task` and the IoAwaitable protocol -* xref:5.buffers/5a.overview.adoc[Buffer Sequences] — Master the concept-driven buffer model -* xref:6.streams/6a.overview.adoc[Stream Concepts] — Understand the seven stream concepts +* xref:4.coroutines/4a.tasks.adoc[Coroutines in Capy] — Deep dive into cpp:task[task] and the IoAwaitable protocol +* xref:5.buffers/5.intro.adoc[Buffer Sequences] — Buffer types, sequences, system I/O, and the algorithms over them +* xref:6.streams/6a.overview.adoc[Stream Concepts] — Understand the three stream concepts diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index 456974de7..048591db4 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -8,6 +8,7 @@ // = Quick Start +:page-mode: tutorial This page gets you from zero to a working coroutine program in five minutes. @@ -24,26 +25,41 @@ include::example$programs/quick_start_hello.cpp[tag=full] == Build and Run -[source,bash] +Capy is not installed as a system package, so build it first: + +[source,bash,role=external] +---- +cmake -B build && cmake --build build +---- + +Then point the compiler at your own checkout's include directory and built +library. Replace both paths below with where you built Capy: + +[source,bash,role=external] +---- +g++ -std=c++20 -I/path/to/capy/include -o hello_coro hello_coro.cpp \ + /path/to/capy/build/libboost_capy.a -pthread ---- -# With GCC -g++ -std=c++20 -o hello_coro hello_coro.cpp -lcapy -pthread -# Run +Then run it: + +[source,bash,role=external] +---- ./hello_coro ---- Expected output: +[role=output] ---- The answer is 42 ---- == What Just Happened? -1. `answer()` creates a suspended coroutine that will return 42 -2. `greet()` creates a suspended coroutine that will await `answer()` -3. `run_async(executor)(greet())` starts `greet()` on the pool's executor +1. `answer()` creates a suspended coroutine that returns 42 +2. `greet()` creates a suspended coroutine that awaits `answer()` +3. cpp:run_async[]`(executor)(greet())` starts `greet()` on the pool's executor 4. `greet()` runs until it hits `co_await answer()` 5. `answer()` runs and returns 42 6. `greet()` resumes with the result and prints it @@ -75,5 +91,5 @@ include::example$snippets/quick_start.cpp[tag=errors,indent=0] Now that you have a working program: * xref:4.coroutines/4a.tasks.adoc[Tasks] — Learn how lazy tasks work -* xref:4.coroutines/4b.launching.adoc[Launching Tasks] — Understand `run_async` in detail +* xref:4.coroutines/4b.launching.adoc[Starting Tasks] — Understand cpp:run_async[] in detail * xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] — Control where coroutines execute diff --git a/doc/modules/ROOT/pages/why-capy.adoc b/doc/modules/ROOT/pages/why-capy.adoc index 48e013c98..4a19ae951 100644 --- a/doc/modules/ROOT/pages/why-capy.adoc +++ b/doc/modules/ROOT/pages/why-capy.adoc @@ -1,10 +1,11 @@ = Why Capy? +:page-mode: explanation Boost.Asio is currently the world leader in portable asynchronous I/O. The standard is silent here. The global ecosystem offers nothing comparable. *Capy advances beyond Boost.Asio in several specific domains* -The sections that follow will demonstrate this claim. Each section examines a domain where Capy innovates—not by reinventing what works, but by solving problems that have remained unsolved. +The sections that follow demonstrate this claim. Each section examines a domain where Capy innovates—not by reinventing what works, but by solving problems that have remained unsolved. == Coroutine-Only Stream Concepts @@ -18,7 +19,7 @@ Asio's stream concepts are hybrid by design. Capy's are coroutine-only, which is === What Capy Offers -* `ReadStream`, `WriteStream`, `Stream` — partial I/O (returns what's available) +* cpp:ReadStream[], cpp:WriteStream[], cpp:Stream[] — partial I/O (returns what's available) === Comparison @@ -26,13 +27,13 @@ Asio's stream concepts are hybrid by design. Capy's are coroutine-only, which is |=== | Capy | Asio -| `ReadStream` +| cpp:ReadStream[] | `AsyncReadStream`* -| `WriteStream` +| cpp:WriteStream[] | `AsyncWriteStream`* -| `Stream` +| cpp:Stream[] ^| - |=== @@ -47,14 +48,14 @@ Asio does offer type-erasure—but at the wrong level. `any_executor` erases the Why hasn't anyone type-erased the stream? Because with callbacks and futures, it's expensive. The completion handler type is part of the stream's operation signature. Erasing it means virtual calls on the hot path—for every continuation, not just every I/O operation. -Coroutines change this equation. A coroutine's continuation is always the same thing: a handle to resume. The caller doesn't need to know what type will resume it. This is structural type-erasure—built into the language. Capy exploits this. Type-erasing a stream costs one virtual call per I/O operation. That's it. No per-callback overhead. No template instantiation cascades. +Coroutines change this equation. A coroutine's continuation is always the same thing: a handle to resume. The caller doesn't need to know what type resumes it. This is structural type-erasure—built into the language. Capy exploits this. Type-erasing a stream costs up to five vtable calls per I/O operation, not one per continuation. A synchronously completed read costs four; `await_suspend` is skipped when `await_ready` returns `true`. No per-callback overhead. No template instantiation cascades. -Write `any_stream&` and accept any stream. Your function compiles once. It links anywhere. Your build times drop. Your binaries shrink. Your error messages become readable. And because coroutines are ordinary functions (not templates), you get natural ABI stability. Link against a new stream implementation without recompiling your code. +Write cpp:any_stream[]`&` and accept any stream. Your function compiles once. It links anywhere. Your build times drop. Your binaries shrink. Your error messages become readable. And because coroutines are ordinary functions (not templates), you get natural ABI stability. Link against a new stream implementation without recompiling your code. === What Capy Offers -* `any_read_stream`, `any_write_stream`, `any_stream` — type-erased partial I/O -* `read`, `write` — algorithms that work with erased or concrete streams +* cpp:any_read_stream[], cpp:any_write_stream[], cpp:any_stream[] — type-erased partial I/O +* cpp:read[], cpp:write[] — algorithms that work with erased or concrete streams === Comparison @@ -62,19 +63,19 @@ Write `any_stream&` and accept any stream. Your function compiles once. It links |=== | Capy | Asio -| `any_read_stream` +| cpp:any_read_stream[] ^| - -| `any_write_stream` +| cpp:any_write_stream[] ^| - -| `any_stream` +| cpp:any_stream[] ^| - -| `read` +| cpp:read[] | `async_read`* -| `write` +| cpp:write[] | `async_write`* |=== @@ -87,14 +88,14 @@ Asio got buffer sequences right. The concept-driven approach—`ConstBufferSeque Capy doesn't reinvent this. We adopt Asio's buffer sequence model because it works. -But we improve on it. Asio provides the basics; Capy extends them. Need to trim bytes from the front of a buffer sequence? Asio makes you work for it. Capy provides `buffer_slice` and `front`—byte-range slicing primitives for efficient byte-level manipulation. Need to compose two buffers without copying? Use `std::array` (or any range of buffers) directly — Capy's buffer-sequence concepts accept arbitrary ranges. +But we improve on it. Asio provides the basics; Capy extends them. Need to trim bytes from the front of a buffer sequence? Asio makes you work for it. Capy provides cpp:buffer_slice[] and cpp:front[]—byte-range slicing primitives for efficient byte-level manipulation. Need to compose two buffers without copying? Use `std::array` (or any range of buffers) directly — Capy's buffer-sequence concepts accept arbitrary ranges. One more thing: `std::ranges` cannot help here. `ranges::size` returns the number of buffers, not the total bytes. Range views can drop entire elements, but buffer sequences need byte-level trimming. The abstractions don't match. Buffer sequences need their own concepts. === What Capy Offers -* `ConstBufferSequence`, `MutableBufferSequence` — core concepts (Asio-compatible) -* `buffer_slice`, `front` — byte-level manipulation utilities +* cpp:ConstBufferSequence[], cpp:MutableBufferSequence[] — core concepts (Asio-compatible) +* cpp:buffer_slice[], cpp:front[] — byte-level manipulation utilities === Comparison @@ -102,31 +103,25 @@ One more thing: `std::ranges` cannot help here. `ranges::size` returns the numbe |=== | Capy | Asio -| `ConstBufferSequence` +| cpp:ConstBufferSequence[] | `ConstBufferSequence` -| `MutableBufferSequence` +| cpp:MutableBufferSequence[] | `MutableBufferSequence` -| `const_buffer` +| cpp:const_buffer[] | `const_buffer` +| cpp:mutable_buffer[] | `mutable_buffer` -| `mutable_buffer` - -| `buffer_slice` -^| - -| `Slice` +| cpp:buffer_slice[] ^| - -| `MutableSlice` +| cpp:front[] ^| - -| `front` -^| - - -| `buffer_copy` +| cpp:buffer_copy[] | `buffer_copy` | Byte-level trimming @@ -155,21 +150,21 @@ The *IoAwaitable protocol* solves context propagation. When you `co_await`, the *Stop tokens propagate automatically.* Cancel at the top of your coroutine tree, and every nested operation receives the signal. Capy integrates with OS-level cancellation—`CancelIoEx` on Windows, `IORING_OP_ASYNC_CANCEL` on Linux. Pending I/O operations cancel immediately. -*Frame allocation uses forward flow.* The two-call syntax of `run_async(executor)(my_task())` sets a thread-local allocator before the task is evaluated. The task's `operator new` reads it. No late binding. No backward flow. Ergonomic control over where every frame is allocated. +*Frame allocation uses forward flow.* The two-call syntax of cpp:run_async[]`(executor)(my_task())` sets a thread-local allocator before the task is evaluated. The task's `operator new` reads it. No late binding. No backward flow. Ergonomic control over where every frame is allocated. And Capy *separates execution from platform*. The execution model—executors, cancellation, allocation—lives in Capy. Platform abstractions—sockets, `io_uring`, IOCP—live in Corosio. Clean boundaries. Testable components. You can use Capy's execution model with a different I/O backend if you choose. -Most importantly, Capy defines a *taxonomy of awaitables*. `IoAwaitable` is the base protocol for any type that participates in context propagation. `IoRunnable` refines it with the launch interface needed by `run_async` and `run`. This hierarchy means you can write your own task types that integrate with Capy's execution model. Asio's `awaitable` is a concrete type, not a concept. You use it or you don't. Capy gives you building blocks. +Most importantly, Capy defines a *taxonomy of awaitables*. cpp:IoAwaitable[] is the base protocol for any type that participates in context propagation. cpp:IoRunnable[] refines it with the interface that cpp:run_async[] and cpp:run[] need to start a task. This hierarchy means you can write your own task types that integrate with Capy's execution model. Asio's `awaitable` is a concrete type, not a concept. You use it or you don't. Capy gives you building blocks. Neither Asio nor `std::execution` offers this combination of forward-flow allocator control, automatic stop-token propagation, and execution/platform separation. === What Capy Offers -* `IoAwaitable`, `IoRunnable` — taxonomy of awaitable concepts -* `task` — concrete task type implementing the protocol (user-defined tasks also supported) -* `run`, `run_async` — launch functions with forward-flow allocator control -* `strand`, `thread_pool`, `async_mutex`, `async_event`, `async_waker`: concurrency primitives -* `frame_allocator`, `recycling_memory_resource` — coroutine-optimized allocation +* cpp:IoAwaitable[], cpp:IoRunnable[] — taxonomy of awaitable concepts +* cpp:task[task] — concrete task type implementing the protocol (user-defined tasks also supported) +* cpp:run[], cpp:run_async[] — launcher functions with forward-flow allocator control +* cpp:strand[], cpp:thread_pool[], cpp:async_mutex[], cpp:async_event[], cpp:async_waker[]: concurrency primitives +* `frame_allocator`, cpp:recycling_memory_resource[] — coroutine-optimized allocation === Comparison @@ -177,43 +172,43 @@ Neither Asio nor `std::execution` offers this combination of forward-flow alloca |=== | Capy | Asio | std -| `IoAwaitable` +| cpp:IoAwaitable[] ^| - ^| - -| `IoRunnable` +| cpp:IoRunnable[] ^| - ^| - -| `io_awaitable_promise_base` +| cpp:io_awaitable_promise_base[] ^| - ^| - -| `task` +| cpp:task[task] | `awaitable`* | P3552R3** -| `run` +| cpp:run[] ^| - ^| - -| `run_async` +| cpp:run_async[] | `co_spawn`* ^| - -| `strand` +| cpp:strand[] | `strand` ^| - -| `executor_ref` +| cpp:executor_ref[] | `any_executor` ^| - -| `thread_pool` +| cpp:thread_pool[] | `thread_pool` ^| - -| `execution_context` +| cpp:execution_context[] | `execution_context` ^| - @@ -221,19 +216,19 @@ Neither Asio nor `std::execution` offers this combination of forward-flow alloca ^| - ^| - -| `recycling_memory_resource` +| cpp:recycling_memory_resource[] ^| - ^| - -| `async_mutex` +| cpp:async_mutex[] ^| - ^| - -| `async_event` +| cpp:async_event[] ^| - ^| - -| `async_waker` +| cpp:async_waker[] ^| - ^| - @@ -268,10 +263,10 @@ Capy builds on Asio's foundation—the buffer sequences, the executor model, the The result is something new. Stream concepts designed for coroutines alone. Type-erasure at the level where it matters most. A simple execution model discovered through use-case-first design. Clean separation between execution and platform. A taxonomy of awaitables that invites extension rather than mandating a single concrete type. -Meanwhile, the {cpp} standards committee has produced `std::execution`—a sender/receiver model of considerable theoretical elegance. It is general. It is powerful. It is also complex, and its relationship to the I/O problems that most {cpp} developers face daily remains unclear. The community watches, waits, and wonders when the abstractions will connect to the work they need to accomplish. +Meanwhile, the {cpp} standards committee has produced `std::execution`—a sender/receiver model of considerable theoretical elegance. It is general. It is powerful. It is also complex, and its relationship to the I/O problems that most {cpp} developers face daily remains unclear. The community watches, waits, and wonders when the abstractions connect to the work they need to accomplish. Boost has always been where the practical meets the principled. Where real-world feedback shapes design. Where code ships before papers standardize. Capy continues this tradition. -If you are reading this as a Boost contributor, know what you are part of. This library advances beyond Asio in the domains where they overlap. Not by abandoning what works, but by building on it. Not by chasing theoretical purity, but by solving the problems that have frustrated {cpp} developers for years: template explosion, compile-time costs, error message novels, ergonomic concurrency, and more. +If you are reading this as a Boost contributor, know what you are part of. This library advances beyond Asio in the domains where they overlap. Not by abandoning what works, but by building on it. Not by chasing theoretical purity, but by solving the problems that have frustrated {cpp} developers for years. Those problems are template explosion, compile-time costs, error message novels, ergonomic concurrency, and more. The coroutine era has arrived. And Boost, as it has so many times before, is leading the way. diff --git a/doc/mrdocs.yml b/doc/mrdocs.yml index 2560be736..cc3020896 100644 --- a/doc/mrdocs.yml +++ b/doc/mrdocs.yml @@ -10,6 +10,28 @@ file-patterns: # Filters include-symbols: - 'boost::capy::**' +# Internal-only test helper types: implementation machinery of the public +# run_blocking / bufgrind toolkit, not user-facing API. Omitted from the +# reference (Doxygen @cond does not suppress warnings in this MrDocs build). +exclude-symbols: + - 'boost::capy::test::blocking_handler_wrapper' + - 'boost::capy::test::run_blocking_wrapper' + - 'boost::capy::test::bufgrind::next_awaitable' +# Private members of buffer_param's anonymous union. `inaccessible-members: +# never` does not reach variant members of an anonymous union in this MrDocs +# build, so these two private fields leak into the reference as "[variant +# member]" rows. They are storage internals, not API. The `**` spans the +# unnamed union's own scope, which sits between the class and the members. + - 'boost::capy::buffer_param::**::dummy_' + - 'boost::capy::buffer_param::**::arr_' +# Same anonymous-union leak in the task and quitter promises: `ep_` is the +# exception_ptr held in a union so it is not constructed or destroyed +# automatically, and it sits in a `private:` section that `inaccessible-members: +# never` would otherwise filter. Its sibling `has_ep_`/`state_` -- private but +# not in a union -- is correctly filtered, which is what identifies the union as +# the cause. + - 'boost::capy::task::**::ep_' + - 'boost::capy::quitter::**::ep_' implementation-defined: - 'boost::capy::detail' - 'boost::capy::*::detail' @@ -26,6 +48,24 @@ multipage: true # use-system-libc: true # use-system-stdlib: true +# Warnings (Style Guide Part F.0 "MrDocs-no-warnings" gate; doc/lint/mrdocs-warnings.mjs +# scans the build log for these). Deliberately not warn-as-error: the gate stays +# non-blocking until Phase 2 exit promotes it (see task-2 brief). +warnings: true +warn-if-undocumented: true +warn-no-paramdoc: true +warn-unnamed-param: true +warn-broken-ref: true +warn-if-doc-error: true + +# Two further MrDocs 0.8.0 limitations stay grandfathered in +# doc/lint/baseline.json instead of being fixed here. The reasoning lives next +# to the affected code so it is findable from either end: +# include/boost/capy/buffers/front.hpp -- the six buffer CPO +# "variable is undocumented" findings +# include/boost/capy/ex/run.hpp -- "run: Documented parameter 'alloc' +# does not exist" + # Automation auto-function-metadata: false diff --git a/doc/package-lock.json b/doc/package-lock.json index 8aae7fdb6..5408cea7c 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -18,7 +18,9 @@ "devDependencies": { "@antora/cli": "3.1.14", "@antora/site-generator": "3.1.14", - "antora": "3.1.14" + "antora": "3.1.14", + "asciidoctor": "^4.0.5", + "pa11y-ci": "^4.1.1" } }, "node_modules/@antora/asciidoc-loader": { @@ -365,6 +367,31 @@ "node": ">=16.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cppalliance/antora-cpp-reference-extension": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@cppalliance/antora-cpp-reference-extension/-/antora-cpp-reference-extension-0.1.1.tgz", @@ -551,12 +578,73 @@ "node": ">= 8" } }, + "node_modules/@pa11y/html_codesniffer": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@pa11y/html_codesniffer/-/html_codesniffer-2.6.0.tgz", + "integrity": "sha512-BKA7qG8NyaIBdCBDep0hYuYoF/bEyWJprE6EEVJOPiwj80sSiIKDT8LUVd19qKhVqNZZD3QvJIdFZ35p+vAFPg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sntke/antora-mermaid-extension": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@sntke/antora-mermaid-extension/-/antora-mermaid-extension-0.0.8.tgz", "integrity": "sha512-tTGNECQJcJaz2m/W2izgVNLO78LBq1OyNxIpTYU/IslkRjN62ghZfK25sZTfpvJQjKeNTOnx+SmFcCpq/Sn3FQ==", "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -581,6 +669,32 @@ "node": ">= 6.0.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/antora": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/antora/-/antora-3.1.14.tgz", @@ -604,6 +718,46 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asciidoctor": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-4.0.5.tgz", + "integrity": "sha512-udhbF7+zNkc39nHIoNgNn2+t9fIGO5gCQ6HvMeV60uYV9fA2eh228bAQFSeANl6EeFXvFYpfsn2dLQL08nTZ0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asciidoctor/core": "4.0.5" + }, + "bin": { + "asciidoctor": "bin/asciidoctor", + "asciidoctorjs": "bin/asciidoctor" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/asciidoctor-opal-runtime": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.3.tgz", @@ -618,6 +772,36 @@ "node": ">=8.11" } }, + "node_modules/asciidoctor/node_modules/@asciidoctor/core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-4.0.5.tgz", + "integrity": "sha512-Rg6w6YHU1FqACkrE62f5oqI8LHhM7Dp9eWZFQfXl/0fCYYLgPSyNbGqBTpWbGYlSaNp3yfB0naeQhlWH1ZibPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-lock": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", @@ -655,6 +839,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", @@ -668,9 +862,9 @@ } }, "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -704,6 +898,76 @@ } } }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz", + "integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -724,6 +988,38 @@ ], "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bfj": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-9.1.3.tgz", + "integrity": "sha512-1ythbcNNAd2UjTYW6M+MAHd9KM/m3g4mQ+3a4Vom16WgmUa4GsisdmXAYfpAjkObY5zdpgzaBh1ctZOEcJipuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "check-types": "^11.2.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -840,12 +1136,102 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/check-types": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/clean-git-ref": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", "license": "Apache-2.0" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -856,6 +1242,26 @@ "node": ">=0.8" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -906,6 +1312,33 @@ "node": ">=6" } }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -918,20 +1351,60 @@ "node": ">=0.8" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, - "license": "MIT", - "engines": { - "node": "*" + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -977,6 +1450,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -986,6 +1474,14 @@ "node": ">=0.4.0" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/diff3": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", @@ -1061,6 +1557,27 @@ "node": ">= 0.4" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -1083,6 +1600,39 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1128,6 +1678,72 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -1156,6 +1772,38 @@ "bare-events": "^2.7.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -1230,6 +1878,26 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", + "integrity": "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1309,6 +1977,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1346,6 +2024,37 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -1377,6 +2086,33 @@ "node": ">= 6" } }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1478,6 +2214,16 @@ "dev": true, "license": "MIT" }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/hpagent": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", @@ -1507,6 +2253,30 @@ "entities": "^4.5.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1520,6 +2290,19 @@ "node": ">= 6" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1549,6 +2332,23 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1567,6 +2367,33 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/is/-/is-3.3.2.tgz", + "integrity": "sha512-a2xr4E3s1PjDS8ORcGgXpWx6V+liNs+O3JRD2mb9aeugD7rtkkZ0zgLdYgw0tWsKhsdiezGYptSiMlVazCBTuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -1588,6 +2415,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1666,6 +2503,13 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1678,6 +2522,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -1691,6 +2542,30 @@ "node": ">=6" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -1698,6 +2573,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -1817,6 +2702,13 @@ "minimist": "^1.2.5" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1833,6 +2725,16 @@ "progress": "^2.0.0" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -1840,6 +2742,74 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node.extend": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.3.tgz", + "integrity": "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw==", + "dev": true, + "license": "(MIT OR GPL-2.0)", + "dependencies": { + "hasown": "^2.0.0", + "is": "^3.3.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -1859,12 +2829,239 @@ "wrappy": "1" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, + "node_modules/pa11y": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/pa11y/-/pa11y-9.1.1.tgz", + "integrity": "sha512-kHuEgMcoH7YZjcf/G/GEFi2XELsLOv5R+ctaus4EDlLaTU+Cd9GjPbHc/wsKpl87Rmk3lHL2eJA+mZ0XXd0Eew==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@pa11y/html_codesniffer": "^2.6.0", + "axe-core": "~4.11.1", + "bfj": "~9.1.3", + "commander": "~14.0.3", + "envinfo": "~7.21.0", + "kleur": "~4.1.5", + "mustache": "~4.2.0", + "node.extend": "~2.0.3", + "puppeteer": "^24.37.5", + "semver": "~7.7.4" + }, + "bin": { + "pa11y": "bin/pa11y.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pa11y-ci": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/pa11y-ci/-/pa11y-ci-4.1.1.tgz", + "integrity": "sha512-urSrJTTDtXypYxpjhYSzVsbHlFblmbPgMfCxJI6WyTF3oSxNHfQ77mFIv5PKGiN/k46Qc/tVc8+4Ny+y13pGow==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "async": "~3.2.6", + "cheerio": "~1.0.0", + "commander": "~14.0.3", + "globby": "~6.1.0", + "kleur": "~4.1.5", + "lodash": "~4.18.1", + "node-fetch": "~2.7.0", + "pa11y": "^9.1.1", + "protocolify": "~3.0.0", + "puppeteer": "^24.37.5", + "wordwrap": "~1.0.0" + }, + "bin": { + "pa11y-ci": "bin/pa11y-ci.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pa11y-ci/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/pa11y/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/pa11y/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1882,6 +3079,13 @@ "dev": true, "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -1904,6 +3108,29 @@ "node": ">=6" } }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pino": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", @@ -2014,6 +3241,16 @@ "node": ">= 0.4" } }, + "node_modules/prepend-http": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-3.0.1.tgz", + "integrity": "sha512-BLxfZh+m6UiAiCPZFJ4+vYoL7NrRs5XgCTRrjseATAggXhdZKKxn+JUNmuVYWY23bDHgaEHodxw8mnmtVEDtHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2036,10 +3273,76 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.4.0" } }, + "node_modules/protocolify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/protocolify/-/protocolify-3.0.0.tgz", + "integrity": "sha512-PuvDJOkKJMVQx8jSNf8E5g0bJw/UTKm30mTjFHg4N30c8sefgA5Qr/f8INKqYBKfvP/MUSJrj+z1Smjbq4/3rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-url": "^3.0.0", + "prepend-http": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -2060,6 +3363,47 @@ "once": "^1.3.1" } }, + "node_modules/puppeteer": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", + "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.1", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2129,6 +3473,16 @@ "node": ">= 10" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2139,6 +3493,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2202,6 +3566,13 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -2310,6 +3681,57 @@ "simple-concat": "^1.0.0" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", @@ -2341,9 +3763,9 @@ } }, "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "dev": true, "license": "MIT", "dependencies": { @@ -2361,6 +3783,34 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2386,6 +3836,34 @@ ], "license": "MIT" }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -2442,6 +3920,27 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -2456,6 +3955,13 @@ "node": ">= 0.4" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -2470,6 +3976,24 @@ "node": ">=0.8.0" } }, + "node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/unxhr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.0.1.tgz", @@ -2503,6 +4027,55 @@ "node": ">=10.13.0" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -2531,12 +4104,52 @@ "dev": true, "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", @@ -2546,6 +4159,35 @@ "node": ">=4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", @@ -2556,6 +4198,16 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yauzl": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", @@ -2579,6 +4231,16 @@ "dependencies": { "buffer-crc32": "~0.2.3" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/doc/package.json b/doc/package.json index 0fea5e859..90fe2e872 100644 --- a/doc/package.json +++ b/doc/package.json @@ -2,7 +2,9 @@ "devDependencies": { "@antora/cli": "3.1.14", "@antora/site-generator": "3.1.14", - "antora": "3.1.14" + "antora": "3.1.14", + "asciidoctor": "^4.0.5", + "pa11y-ci": "^4.1.1" }, "dependencies": { "@antora/collector-extension": "^1.0.3", diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index ad09af8bd..a767db1e1 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(async-mutex) add_subdirectory(buffer-composition) add_subdirectory(custom-executor) +add_subdirectory(gui-integration) add_subdirectory(hello-task) add_subdirectory(mock-stream-testing) add_subdirectory(parallel-fetch) diff --git a/example/custom-executor/custom_executor.cpp b/example/custom-executor/custom_executor.cpp index 07d55e57a..bdbb88701 100644 --- a/example/custom-executor/custom_executor.cpp +++ b/example/custom-executor/custom_executor.cpp @@ -143,7 +143,7 @@ capy::io_task compute(int x) capy::task<> run_tasks() { - std::cout << "Launching 3 tasks with when_all...\n"; + std::cout << "Starting 3 tasks with when_all...\n"; auto [ec, r1, r2, r3] = co_await capy::when_all( compute(3), compute(7), compute(11)); @@ -158,7 +158,7 @@ int main() { run_loop loop; - // Launch using run_async, just like with thread_pool + // Start using run_async, just like with thread_pool // tag::drive[] capy::run_async(loop.get_executor())(run_tasks()); // end::drive[] diff --git a/example/gui-integration/CMakeLists.txt b/example/gui-integration/CMakeLists.txt new file mode 100644 index 000000000..2e7d1fd23 --- /dev/null +++ b/example/gui-integration/CMakeLists.txt @@ -0,0 +1,22 @@ +# +# 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 +# + +file(GLOB_RECURSE PFILES CONFIGURE_DEPENDS *.cpp *.hpp + CMakeLists.txt + Jamfile) + +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) + +add_executable(capy_example_gui_integration ${PFILES}) + +set_property(TARGET capy_example_gui_integration + PROPERTY FOLDER "examples") + +target_link_libraries(capy_example_gui_integration + Boost::capy) diff --git a/example/gui-integration/gui_integration.cpp b/example/gui-integration/gui_integration.cpp new file mode 100644 index 000000000..e33c2d328 --- /dev/null +++ b/example/gui-integration/gui_integration.cpp @@ -0,0 +1,386 @@ +// +// 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 +// + +// +// GUI Integration Example +// +// Runs a Capy coroutine on a GUI framework's event loop. The +// framework here is a stand-in: it owns the thread it is created on, +// pumps an event loop on it, and accepts work from any thread. That +// is the only primitive a real toolkit has to provide. +// +// The interesting part is thread affinity. The coroutine awaits work +// on a thread pool, then a dialog the toolkit answers on its own +// thread, and updates a widget after each. Every widget access checks +// which thread it is on, so the program proves where the coroutine +// resumes instead of assuming it. +// + +// tag::full[] +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace capy = boost::capy; + +// tag::check[] +// The thread checks are the point of this example, so they have to +// survive a release build. assert() would compile out under NDEBUG. +void check(bool ok, char const* what) +{ + if(ok) + return; + std::cerr << "FAILED: " << what << std::endl; + // abort() rather than exit(): a failing check can fire on a pool + // thread, and exit() would run static destructors alongside the + // still-running threads. + std::abort(); +} +// end::check[] + +//---------------------------------------------------------- +// The stand-in toolkit +//---------------------------------------------------------- + +// tag::gui_app[] +// Stands in for QApplication, GtkApplication, or wxApp. It owns the +// thread it is constructed on, runs the event loop on that thread, and +// takes work from any thread. Every real toolkit offers that last +// operation: QMetaObject::invokeMethod, g_idle_add, +// wxEvtHandler::CallAfter, PostMessage. +class gui_app : public capy::execution_context +{ + std::thread::id const gui_thread_ = std::this_thread::get_id(); + std::mutex m_; + std::condition_variable cv_; + std::deque> queue_; + bool quit_ = false; + +public: + class executor_type; + + gui_app() + : execution_context(this) + { + } + + ~gui_app() + { + shutdown(); + destroy(); + } + + gui_app(gui_app const&) = delete; + gui_app& operator=(gui_app const&) = delete; + + // Run a coroutine on the GUI thread. Callable from any thread and + // never blocks the caller. + void post_to_gui_thread(std::coroutine_handle<> h) + { + { + std::lock_guard lock(m_); + queue_.push_back(h); + } + cv_.notify_one(); + } + + // Ask the loop to return once it has drained its queue. + void quit() + { + { + std::lock_guard lock(m_); + quit_ = true; + } + cv_.notify_one(); + } + + // tag::run[] + // The event loop. It blocks while the queue is empty, so an + // operation still running on another thread cannot end the loop. + void run() + { + check(on_gui_thread(), "run() called off the GUI thread"); + for(;;) + { + std::coroutine_handle<> h; + { + std::unique_lock lock(m_); + cv_.wait(lock, + [this]{ return !queue_.empty() || quit_; }); + if(queue_.empty()) + return; // quit requested, nothing left to run + h = queue_.front(); + queue_.pop_front(); + } + capy::safe_resume(h); + } + } + // end::run[] + + bool on_gui_thread() const noexcept + { + return std::this_thread::get_id() == gui_thread_; + } + + executor_type get_executor() noexcept; +}; +// end::gui_app[] + +// tag::executor[] +// Wraps post_to_gui_thread as an Executor. This is the whole binding +// between Capy and the toolkit. +class gui_app::executor_type +{ + friend class gui_app; + gui_app* app_ = nullptr; + + explicit executor_type(gui_app& app) noexcept + : app_(&app) + { + } + +public: + executor_type() = default; + + capy::execution_context& context() const noexcept + { + return *app_; + } + + void on_work_started() const noexcept {} + void on_work_finished() const noexcept {} + + std::coroutine_handle<> dispatch(capy::continuation& c) const + { + if(app_->on_gui_thread()) + return c.h; // resume inline by symmetric transfer + app_->post_to_gui_thread(c.h); + return std::noop_coroutine(); + } + + void post(capy::continuation& c) const + { + app_->post_to_gui_thread(c.h); + } + + bool operator==(executor_type const& other) const noexcept + { + return app_ == other.app_; + } +}; +// end::executor[] + +inline +gui_app::executor_type +gui_app::get_executor() noexcept +{ + return executor_type{*this}; +} + +// tag::concept_check[] +static_assert(capy::Executor); +// end::concept_check[] + +// tag::label[] +// Stands in for QLabel, GtkLabel, or wxStaticText. Real widgets are +// not thread-safe, and touching one off the GUI thread is undefined +// behavior that a real toolkit usually fails to diagnose. This one +// diagnoses it. +class label +{ + gui_app& app_; + std::string text_; + +public: + explicit label(gui_app& app) noexcept + : app_(app) + { + } + + void set_text(std::string text) + { + check(app_.on_gui_thread(), "set_text off the GUI thread"); + text_ = std::move(text); + std::cout << "[gui] label: " << text_ << "\n"; + } + + std::string const& text() const + { + check(app_.on_gui_thread(), "text() off the GUI thread"); + return text_; + } +}; +// end::label[] + +//---------------------------------------------------------- +// Work that leaves the GUI thread +//---------------------------------------------------------- + +// tag::pool_task[] +// A task meant to run somewhere other than the GUI thread. It fails +// the program if it finds itself on the GUI thread, which would mean +// the work never left it. +capy::task +count_rows(gui_app& app) +{ + check(!app.on_gui_thread(), "count_rows ran on the GUI thread"); + co_return "42"; +} +// end::pool_task[] + +//---------------------------------------------------------- +// A completion from the toolkit's own thread +//---------------------------------------------------------- + +// tag::dialog[] +// Stands in for QMessageBox, GtkDialog, or wxMessageDialog. A toolkit +// reports the user's answer on whichever thread it chooses, so a plain +// thread is the honest stand-in. It is not the GUI thread, and it is +// not a thread Capy scheduled. +class dialog +{ + gui_app& app_; + std::thread thread_; + +public: + explicit dialog(gui_app& app) noexcept + : app_(app) + { + } + + // Joins the toolkit's thread, so no answer outlives main. + ~dialog() + { + if(thread_.joinable()) + thread_.join(); + } + + // Show the dialog. Returns at once; the answer arrives later, on + // the toolkit's thread. + void show(std::function on_closed) + { + // The stand-in delivers one answer at a time. A previous + // thread has already posted its answer by the time the + // coroutine can ask again, so this join does not block. + if(thread_.joinable()) + thread_.join(); + thread_ = std::thread( + [this, cb = std::move(on_closed)] + { + check(!app_.on_gui_thread(), + "dialog answered on the GUI thread"); + cb("OK"); // the user chose OK + }); + } +}; +// end::dialog[] + +// tag::dialog_awaitable[] +// An IoAwaitable for an operation the toolkit completes. The protocol +// is the subject of the IoAwaitable page; one line of it matters here. +struct show_dialog +{ + dialog& dialog_; + capy::io_env const* env_ = nullptr; + capy::continuation cont_ = {}; + std::string answer_ = {}; + + bool await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, capy::io_env const* env) + { + env_ = env; + cont_.h = h; + dialog_.show([this](std::string answer) + { + answer_ = std::move(answer); + // The toolkit's thread must not resume the coroutine + // itself. Handing the continuation to the executor is what + // puts the resumption back on the GUI thread. This is also + // the last read of *this: the executor may resume the + // coroutine -- and then destroy this awaitable -- before + // this call returns. + env_->executor.post(cont_); + }); + return std::noop_coroutine(); + } + + std::string await_resume() + { + return std::move(answer_); + } +}; +// end::dialog_awaitable[] + +// tag::coroutine[] +capy::task<> +refresh(gui_app& app, label& status, + capy::thread_pool& pool, dialog& confirm) +{ + // Started on the GUI executor, so the body runs on the GUI thread + // and touching the widget is safe. + status.set_text("Loading..."); + + // run() starts count_rows on the pool and posts this coroutine + // back through the executor it was started with. + auto rows = co_await capy::run(pool.get_executor())(count_rows(app)); + + // Back on the GUI thread, without a hop written by hand. + status.set_text("Loaded " + rows + " rows"); + + // The toolkit answers on its own thread. show_dialog posts the + // continuation through this coroutine's executor, so the resumption + // lands on the GUI thread again. + auto answer = co_await show_dialog{confirm}; + + status.set_text("Confirmed: " + answer); +} +// end::coroutine[] + +// tag::main[] +int main() +{ + // The GUI thread is whichever thread constructs the app. + gui_app app; + label status(app); + capy::thread_pool pool(1); + dialog confirm(app); + + capy::run_async(app.get_executor(), [&app] + { + // The task completed on the GUI thread, so this handler runs + // there too. + check(app.on_gui_thread(), "handler off the GUI thread"); + app.quit(); + })(refresh(app, status, pool, confirm)); + + app.run(); + pool.join(); + + // The updates are sequential, so the last one proves all of them. + check(status.text() == "Confirmed: OK", "wrong final text"); + std::cout << "[gui] event loop finished\n"; + return 0; +} +// end::main[] +// end::full[] diff --git a/example/mock-stream-testing/mock_stream_testing.cpp b/example/mock-stream-testing/mock_stream_testing.cpp index 98cbc34b7..547b911c6 100644 --- a/example/mock-stream-testing/mock_stream_testing.cpp +++ b/example/mock-stream-testing/mock_stream_testing.cpp @@ -123,7 +123,7 @@ void test_with_error_injection() capy::any_stream stream{&a}; // any_stream - // Run the protocol - fuse will inject errors at each step + // Run the protocol - fuse injects errors at each step bool result = co_await echo_line_uppercase(stream); // bool // Either succeeds with correct output, or fails cleanly diff --git a/example/timeout-cancellation/timeout_cancellation.cpp b/example/timeout-cancellation/timeout_cancellation.cpp index 99cd96f49..c4a08e44d 100644 --- a/example/timeout-cancellation/timeout_cancellation.cpp +++ b/example/timeout-cancellation/timeout_cancellation.cpp @@ -197,7 +197,7 @@ void demo_cancellation() std::stop_source source; std::latch done(1); // std::latch - wait for 1 task - // Launch the task + // Start the task capy::run_async(pool.get_executor(), source.get_token(), [&done](std::optional result) { if (result) diff --git a/include/boost/capy/buffers.hpp b/include/boost/capy/buffers.hpp index 04d676a40..36f999734 100644 --- a/include/boost/capy/buffers.hpp +++ b/include/boost/capy/buffers.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) @@ -50,15 +51,33 @@ class mutable_buffer /// Construct an empty buffer. mutable_buffer() = default; - /// Construct a copy. + /** Construct a copy. + + @param other The buffer to copy. + */ mutable_buffer( - mutable_buffer const&) = default; + mutable_buffer const& other) = default; + + /** Assign by copying. - /// Assign by copying. + @param other The buffer to copy. + + @return A reference to `*this`. + */ mutable_buffer& operator=( - mutable_buffer const&) = default; + mutable_buffer const& other) = default; - /// Construct from pointer and size. + /** Construct from a pointer and size. + + Takes `void*` so a pointer to any object type binds without a + cast, since the buffer represents a raw, untyped writable + region. Stored internally as `unsigned char*` for byte-wise + pointer arithmetic (see `operator+=`). + + @param data A pointer to the first byte of the region. + + @param size The size of the region, in bytes. + */ constexpr mutable_buffer( void* data, std::size_t size) noexcept : p_(static_cast(data)) @@ -66,13 +85,22 @@ class mutable_buffer { } - /// Return a pointer to the memory region. + /** Return a pointer to the memory region. + + Returns `void*`, symmetric with the constructor, so the + caller can reinterpret the raw region as whatever type it needs. + + @return A pointer to the first byte of the region. + */ constexpr void* data() const noexcept { return p_; } - /// Return the size in bytes. + /** Return the size in bytes. + + @return The size of the region, in bytes. + */ constexpr std::size_t size() const noexcept { return n_; @@ -81,6 +109,8 @@ class mutable_buffer /** Advance the buffer start, shrinking the region. @param n Bytes to skip. Clamped to `size()`. + + @return A reference to `*this`. */ mutable_buffer& operator+=(std::size_t n) noexcept @@ -111,14 +141,32 @@ class const_buffer /// Construct an empty buffer. const_buffer() = default; - /// Construct a copy. - const_buffer(const_buffer const&) = default; + /** Construct a copy. + + @param other The buffer to copy. + */ + const_buffer(const_buffer const& other) = default; + + /** Assign by copying. - /// Assign by copying. + @param other The buffer to copy. + + @return A reference to `*this`. + */ const_buffer& operator=( const_buffer const& other) = default; - /// Construct from pointer and size. + /** Construct from a pointer and size. + + Takes `void const*` so a pointer to any object type binds + without a cast, since the buffer represents a raw, untyped + read-only region. Stored internally as `unsigned char const*` + for byte-wise pointer arithmetic (see `operator+=`). + + @param data A pointer to the first byte of the region. + + @param size The size of the region, in bytes. + */ constexpr const_buffer( void const* data, std::size_t size) noexcept : p_(static_cast(data)) @@ -126,7 +174,10 @@ class const_buffer { } - /// Construct from mutable_buffer. + /** Construct from mutable_buffer. + + @param b The writable buffer whose region is referenced. + */ constexpr const_buffer( mutable_buffer const& b) noexcept : p_(static_cast(b.data())) @@ -134,13 +185,22 @@ class const_buffer { } - /// Return a pointer to the memory region. + /** Return a pointer to the memory region. + + Returns `void const*`, symmetric with the constructor, so the + caller can reinterpret the raw region as whatever type it needs. + + @return A pointer to the first byte of the region. + */ constexpr void const* data() const noexcept { return p_; } - /// Return the size in bytes. + /** Return the size in bytes. + + @return The size of the region, in bytes. + */ constexpr std::size_t size() const noexcept { return n_; @@ -149,6 +209,8 @@ class const_buffer /** Advance the buffer start, shrinking the region. @param n Bytes to skip. Clamped to `size()`. + + @return A reference to `*this`. */ const_buffer& operator+=(std::size_t n) noexcept @@ -161,7 +223,7 @@ class const_buffer } }; -/** Concept for sequences of read-only buffer regions. +/** Requires a type to convert to `const_buffer`, or be a range of such buffers. A type satisfies `ConstBufferSequence` if it represents one or more contiguous memory regions that can be read. This includes single @@ -179,12 +241,16 @@ concept ConstBufferSequence = std::ranges::bidirectional_range && std::is_convertible_v, const_buffer>); -/** Concept for sequences of writable buffer regions. +/** Requires a type to convert to `mutable_buffer`, or be a range of such buffers. A type satisfies `MutableBufferSequence` if it represents one or more contiguous memory regions that can be written. This includes single buffers (convertible to `mutable_buffer`) and ranges of buffers. - Every `MutableBufferSequence` also satisfies `ConstBufferSequence`. + + This does not imply `ConstBufferSequence`. A type reaching + `mutable_buffer` through its own conversion operator would need a + second conversion, to `const_buffer`. An implicit conversion + sequence allows only one user-defined step. @par Syntactic Requirements @li Convertible to `mutable_buffer`, OR @@ -402,6 +468,10 @@ length_impl(It first, It last, long) For a single buffer, returns 1. For a range, returns the distance from `begin` to `end`. + @param bs The buffer sequence. + + @return The number of buffers in `bs`. + @see buffer_size */ template @@ -412,7 +482,7 @@ buffer_length(CB const& bs) begin(bs), end(bs), 0); } -/// Alias for `mutable_buffer` or `const_buffer` based on sequence type. +/// Names `mutable_buffer` for a mutable sequence, `const_buffer` otherwise. template using buffer_type = std::conditional_t< MutableBufferSequence, diff --git a/include/boost/capy/buffers/buffer_param.hpp b/include/boost/capy/buffers/buffer_param.hpp index 9b53e50a2..ebe3ae812 100644 --- a/include/boost/capy/buffers/buffer_param.hpp +++ b/include/boost/capy/buffers/buffer_param.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) @@ -65,7 +66,7 @@ namespace capy { more efficient to process buffers in batches rather than one at a time. This class maintains a window of up to a fixed, implementation-defined number of buffer descriptors - (currently 16), automatically refilling from the underlying + (currently 16). It refills the window from the underlying sequence as buffers are consumed. @par Example @@ -95,15 +96,20 @@ namespace capy { This class enables passing arbitrary buffer sequences through a virtual function boundary. The template function captures the buffer sequence by value and drives the iteration, while - the virtual function receives a simple span: + the virtual function receives a simple span. Plain CTAD + (`buffer_param bp(buffers)`) deduces `BS`'s own buffer type, so a + mutable sequence yields `span`. That does not match + `write_impl`'s `span` parameter. Use @ref const_buffer_param + to force `const_buffer` storage regardless of what `BS` is: @code class base { public: - task<> write(ConstBufferSequence auto buffers) + template + task<> write(BS buffers) { - buffer_param bp(buffers); + const_buffer_param bp(buffers); while(true) { auto bufs = bp.data(); @@ -132,7 +138,7 @@ template class buffer_param { public: - /// The buffer type (const_buffer or mutable_buffer) + /// Names `const_buffer` when `MakeConst`, else `BS`'s own buffer type. using buffer_type = std::conditional_t< MakeConst, const_buffer, @@ -243,11 +249,14 @@ class buffer_param } }; -// CTAD deduction guide +/** Deduce the sequence type from the constructor argument. + + @tparam BS The buffer sequence type. +*/ template buffer_param(BS const&) -> buffer_param; -/// Alias for buffer_param that always uses const_buffer storage. +/// Forces `buffer_param` to store windows as `const_buffer`, regardless of `BS`. template using const_buffer_param = buffer_param; diff --git a/include/boost/capy/buffers/buffer_slice.hpp b/include/boost/capy/buffers/buffer_slice.hpp index b509dce3f..71e9f6d46 100644 --- a/include/boost/capy/buffers/buffer_slice.hpp +++ b/include/boost/capy/buffers/buffer_slice.hpp @@ -22,7 +22,7 @@ namespace boost { namespace capy { -/** The type produced by `buffer_slice` for a sequence `BS`. +/** Names whichever buffer type `buffer_slice` returns for a sequence `BS`. A single buffer is closed under sub-ranging, so slicing it yields a buffer of the same kind. Any other sequence yields the generic @@ -37,8 +37,8 @@ using slice_type = std::conditional_t< /** Return a byte sub-range of a buffer sequence, as a value. - The result is itself a buffer sequence (`slice_type`): pass it - directly to any operation expecting a buffer sequence — there is no + The result is itself a buffer sequence (`slice_type`), so pass it + directly to any operation expecting a buffer sequence. There is no `.data()` and no separate concept to bind. For a single buffer the result is an adjusted buffer; for any other sequence it is a borrowed `slice_of` view. @@ -92,7 +92,7 @@ buffer_slice( } } -/** Deleted rvalue overload. +/** Rejects a temporary sequence at compile time, since the result would dangle. Slicing a temporary would yield an immediately dangling view (the result borrows the sequence). Hoist the sequence into a named variable diff --git a/include/boost/capy/buffers/consuming_buffers.hpp b/include/boost/capy/buffers/consuming_buffers.hpp index 4d3786ea7..509bf85ca 100644 --- a/include/boost/capy/buffers/consuming_buffers.hpp +++ b/include/boost/capy/buffers/consuming_buffers.hpp @@ -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) @@ -23,14 +24,14 @@ namespace capy { /** A cursor that drives consumption of a buffer sequence. `consuming_buffers` is the dedicated driver for `read_some`/`write_some` - loops: it presents the not-yet-consumed bytes of a buffer sequence via + loops. It presents the not-yet-consumed bytes of a buffer sequence via `data()`, and `consume(n)` advances past `n` transferred bytes **in place**. It is deliberately **not** itself a buffer sequence — it hands out the remaining bytes through `data()` (returning a `slice_of` view). It - **borrows** the underlying sequence (iterators + a consumed-byte offset); - the sequence must outlive the cursor, which is the natural case when the + **borrows** the underlying sequence (iterators + a consumed-byte offset). + The sequence must outlive the cursor. That is the natural case when the cursor is a local of a composed operation that took its buffers by value. @par Example @@ -53,7 +54,7 @@ template class consuming_buffers { public: - /// The buffer type of the underlying sequence. + /// Names the buffer type the underlying sequence `Seq` yields. using buffer_type = capy::buffer_type; private: @@ -75,10 +76,16 @@ class consuming_buffers { } - /// Reject construction from a temporary (the view would dangle). - consuming_buffers(Seq const&&) = delete; + /** Reject construction from a temporary (the view would dangle). - /// Return the remaining (unconsumed) bytes as a buffer sequence. + @param s The sequence that would be consumed. + */ + consuming_buffers(Seq const&& s) = delete; + + /** Return the remaining (unconsumed) bytes as a buffer sequence. + + @return The bytes not yet consumed, as a buffer sequence. + */ detail::slice_of data() const noexcept { @@ -110,7 +117,10 @@ class consuming_buffers } }; -// CTAD: deduce the sequence type from the constructor argument. +/** Deduce the sequence type from the constructor argument. + + @tparam Seq The buffer sequence type. +*/ template consuming_buffers(Seq const&) -> consuming_buffers; diff --git a/include/boost/capy/buffers/front.hpp b/include/boost/capy/buffers/front.hpp index 11ca0adfa..2d04b0dd6 100644 --- a/include/boost/capy/buffers/front.hpp +++ b/include/boost/capy/buffers/front.hpp @@ -16,6 +16,13 @@ namespace boost { namespace capy { +// MrDocs 0.8.0 flags this object as `front: variable is undocumented`, as it +// does the other five buffer CPOs (begin, end, buffer_size, buffer_empty, +// buffer_copy). The finding is grandfathered in doc/lint/baseline.json. +// Silencing it buys nothing: on the synthesised overload page MrDocs renders +// neither the record-level docstring below nor one attached to the variable, +// so any docstring that clears the warning is invisible to readers -- a +// second, dead copy of this prose. Verified twice against the built site. /** Return the first buffer in a sequence. @functionobject diff --git a/include/boost/capy/buffers/make_buffer.hpp b/include/boost/capy/buffers/make_buffer.hpp index 1ce53aa2c..6d752a2ce 100644 --- a/include/boost/capy/buffers/make_buffer.hpp +++ b/include/boost/capy/buffers/make_buffer.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2023 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) @@ -234,11 +235,17 @@ concept const_contiguous_range = /** Return a buffer from a mutable contiguous range. Accepts any sized, contiguous range of trivially-copyable, - non-const elements, including `std::vector`, `std::array`, - `std::string`, `std::span`, `boost::span`, and built-in arrays, - whether passed as an lvalue or a temporary. The returned buffer + non-const elements, whether passed as an lvalue or a temporary. + That includes `std::vector`, `std::array`, `std::string`, + `std::span`, `boost::span`, and built-in arrays. The returned buffer refers to the range's storage, which must outlive the buffer. Its size, in bytes, is `size() * sizeof(element)`. + + @param data The range whose storage is referenced. It must + outlive the returned buffer. + + @return A buffer of size `size() * sizeof(element)` referring to + the range's storage. */ template [[nodiscard]] @@ -282,6 +289,12 @@ make_buffer( string literals. The returned buffer refers to the range's storage, which must outlive the buffer. Its size, in bytes, is `size() * sizeof(element)`. + + @param data The range whose storage is referenced. It must + outlive the returned buffer. + + @return A buffer of size `size() * sizeof(element)` referring to + the range's storage. */ template [[nodiscard]] diff --git a/include/boost/capy/concept/buffer_archetype.hpp b/include/boost/capy/concept/buffer_archetype.hpp index b9f0e031e..7517f0426 100644 --- a/include/boost/capy/concept/buffer_archetype.hpp +++ b/include/boost/capy/concept/buffer_archetype.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) @@ -16,12 +17,12 @@ namespace boost { namespace capy { -/** Archetype for ConstBufferSequence concept checking. +/** Satisfies `ConstBufferSequence` without being default-constructible. This type satisfies @ref ConstBufferSequence but cannot be - default-constructed; it is intended only as an unevaluated - parameter type in `requires`-clauses, to verify that a function - template accepts any ConstBufferSequence. + default-constructed. Use it only as an unevaluated parameter + type in `requires`-clauses, to verify that a function template + accepts any ConstBufferSequence. @par Example @code @@ -35,29 +36,58 @@ namespace capy { */ struct const_buffer_archetype_ { + /// Default construction is not permitted. const_buffer_archetype_() = delete; - const_buffer_archetype_(const_buffer_archetype_ const&) = default; - const_buffer_archetype_(const_buffer_archetype_&&) = default; - const_buffer_archetype_& operator=(const_buffer_archetype_ const&) = default; - const_buffer_archetype_& operator=(const_buffer_archetype_&&) = default; - /// Convert to const_buffer. + /** Construct a copy. + + @param other The archetype to copy. + */ + const_buffer_archetype_(const_buffer_archetype_ const& other) = default; + + /** Construct by moving. + + @param other The archetype to move from. + */ + const_buffer_archetype_(const_buffer_archetype_&& other) = default; + + /** Assign by copying. + + @param other The archetype to copy. + + @return A reference to `*this`. + */ + const_buffer_archetype_& operator=(const_buffer_archetype_ const& other) = default; + + /** Assign by moving. + + @param other The archetype to move from. + + @return A reference to `*this`. + */ + const_buffer_archetype_& operator=(const_buffer_archetype_&& other) = default; + + /** Convert to const_buffer. + + @return An empty `const_buffer`. + */ operator const_buffer() const noexcept { return {}; } }; #ifdef __clang__ +/// Falls back to `const_buffer` itself: `const_buffer_archetype_` crashes clang. using const_buffer_archetype = const_buffer; #else -/// Alias for the const buffer archetype type. +/// Picks `const_buffer_archetype_` to keep default construction rejected. using const_buffer_archetype = const_buffer_archetype_; #endif -/** Archetype for MutableBufferSequence concept checking. +/** Satisfies `MutableBufferSequence` without being default-constructible. This type satisfies @ref MutableBufferSequence but cannot be - default-constructed; it is intended only as an unevaluated - parameter type in `requires`-clauses, to verify that a function - template accepts any MutableBufferSequence. + default-constructed. Use it only as an unevaluated parameter + type in `requires`-clauses, to verify that a function template + accepts any MutableBufferSequence. @par Example @code @@ -71,23 +101,55 @@ using const_buffer_archetype = const_buffer_archetype_; */ struct mutable_buffer_archetype_ { + /// Default construction is not permitted. mutable_buffer_archetype_() = delete; - mutable_buffer_archetype_(mutable_buffer_archetype_ const&) = default; - mutable_buffer_archetype_(mutable_buffer_archetype_&&) = default; - mutable_buffer_archetype_& operator=(mutable_buffer_archetype_ const&) = default; - mutable_buffer_archetype_& operator=(mutable_buffer_archetype_&&) = default; - /// Convert to mutable_buffer. + /** Construct a copy. + + @param other The archetype to copy. + */ + mutable_buffer_archetype_(mutable_buffer_archetype_ const& other) = default; + + /** Construct by moving. + + @param other The archetype to move from. + */ + mutable_buffer_archetype_(mutable_buffer_archetype_&& other) = default; + + /** Assign by copying. + + @param other The archetype to copy. + + @return A reference to `*this`. + */ + mutable_buffer_archetype_& operator=(mutable_buffer_archetype_ const& other) = default; + + /** Assign by moving. + + @param other The archetype to move from. + + @return A reference to `*this`. + */ + mutable_buffer_archetype_& operator=(mutable_buffer_archetype_&& other) = default; + + /** Convert to mutable_buffer. + + @return An empty `mutable_buffer`. + */ operator mutable_buffer() const noexcept { return {}; } - /// Convert to const_buffer. + /** Convert to const_buffer. + + @return An empty `const_buffer`. + */ operator const_buffer() const noexcept { return {}; } }; #ifdef __clang__ +/// Falls back to `mutable_buffer` itself: `mutable_buffer_archetype_` crashes clang. using mutable_buffer_archetype = mutable_buffer; #else -/// Alias for the mutable buffer archetype type. +/// Picks `mutable_buffer_archetype_` to keep default construction rejected. using mutable_buffer_archetype = mutable_buffer_archetype_; #endif diff --git a/include/boost/capy/concept/decomposes_to.hpp b/include/boost/capy/concept/decomposes_to.hpp index 76e5556b8..b1e86398c 100644 --- a/include/boost/capy/concept/decomposes_to.hpp +++ b/include/boost/capy/concept/decomposes_to.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) @@ -123,7 +124,7 @@ using awaitable_return_t = decltype( } // namespace detail -/** Concept for types that decompose to a specific typelist. +/** Requires a type to destructure via structured bindings into the given types. A type satisfies `decomposes_to` if it can be decomposed via structured bindings into the specified types. This includes @@ -146,7 +147,7 @@ concept decomposes_to = requires(T&& t) { { detail::decomposed_types(std::forward(t)) } -> std::same_as>; }; -/** Concept for awaitables whose return type decomposes to a specific typelist. +/** Requires an awaitable's result to destructure into the given types. A type satisfies `awaitable_decomposes_to` if it is an awaitable (has `await_resume`) and its return type decomposes to the diff --git a/include/boost/capy/concept/execution_context.hpp b/include/boost/capy/concept/execution_context.hpp index d6e0566ec..4fe784cac 100644 --- a/include/boost/capy/concept/execution_context.hpp +++ b/include/boost/capy/concept/execution_context.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) @@ -19,10 +20,10 @@ namespace boost { namespace capy { -/** Concept for types that provide a place where work is executed. +/** Requires a type to expose a `noexcept get_executor()` bound to its own resources. An execution context owns the resources (threads, event loops, - completion ports) needed to execute function objects. It serves + completion ports) needed to run coroutine continuations. It serves as the factory for executors, which are lightweight handles used to submit work. Multiple executors may reference the same context. @@ -30,20 +31,20 @@ namespace capy { @par Syntactic Requirements - @li `X` must be publicly derived from `execution_context` - @li `X::executor_type` must be a type satisfying @ref Executor - @li `x.get_executor()` must return `X::executor_type` and be `noexcept` + @li `X` must be publicly derived from `execution_context`. + @li `X::executor_type` must be a type satisfying @ref Executor. + @li `x.get_executor()` must return `X::executor_type` and be `noexcept`. @par Semantic Requirements The execution context owns the execution environment: @li Work submitted via any executor from this context runs on - resources owned by the context + resources owned by the context. @li The context remains valid while any executor referencing it - exists and may be used - @li Destroying the context destroys all unexecuted work submitted - via associated executors + exists and may be used. + @li Destroying the context abandons work submitted via associated + executors that has not started running. @par Conforming Signatures @@ -58,12 +59,15 @@ namespace capy { @par Example + `post` takes a `continuation&`, which no closure converts to; ordinary + callers reach it indirectly through `run_async` or similar combinators: + @code template - void spawn_work( Ctx& ctx ) + void spawn_work( Ctx& ctx, task<> work ) { auto ex = ctx.get_executor(); - ex.post( []{ } ); // work runs on ctx + run_async(ex)(std::move(work)); // schedules work; runs on ctx } @endcode diff --git a/include/boost/capy/concept/executor.hpp b/include/boost/capy/concept/executor.hpp index efec3d058..6cae87ea3 100644 --- a/include/boost/capy/concept/executor.hpp +++ b/include/boost/capy/concept/executor.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) @@ -22,7 +23,7 @@ namespace capy { class execution_context; -/** Concept for types that schedule coroutine execution. +/** Requires a copyable type to dispatch or post continuations to its context. An executor embodies a set of rules for determining how and where coroutines are executed. It provides operations to submit work @@ -31,7 +32,7 @@ class execution_context; Ordinary users writing coroutine tasks do not interact with `dispatch` and `post` directly. These operations are used by authors of coroutine machinery -- `promise_type` implementations, - awaitables, `await_transform` -- to implement asynchronous + awaitables, `await_transform`. That code implements asynchronous algorithms such as `when_all`, `when_any`, `async_mutex`, channels, and similar primitives. @@ -39,51 +40,50 @@ class execution_context; @par Syntactic Requirements - @li `E` must be nothrow copy and move constructible - @li `ce == ce2` must return a type convertible to `bool`, `noexcept` + @li `E` must be nothrow copy and move constructible. + @li `ce == ce2` must return a type convertible to `bool`, `noexcept`. @li `ce.context()` must return an lvalue reference to a type derived - from `execution_context`, `noexcept` - @li `ce.on_work_started()` must be valid and `noexcept` - @li `ce.on_work_finished()` must be valid and `noexcept` - @li `ce.dispatch(c)` must return `std::coroutine_handle<>` - @li `ce.post(c)` must be valid + from `execution_context`, `noexcept`. + @li `ce.on_work_started()` must be valid and `noexcept`. + @li `ce.on_work_finished()` must be valid and `noexcept`. + @li `ce.dispatch(c)` must return `std::coroutine_handle<>`. + @li `ce.post(c)` must be valid. @par Semantic Requirements The `context` operation returns the owning context: @li Returns a reference to the execution context that created - this executor - @li The context outlives all executors created from it + this executor. + @li The context outlives all executors created from it. The `on_work_started` and `on_work_finished` operations track work: @li Calls must be paired; each `on_work_started` must have a - matching `on_work_finished` + matching `on_work_finished`. @li The context uses this count to determine when shutdown - is complete + is complete. @li These are not intended for direct use by callers. They are public so that work guards can invoke them. This enables user-defined guards with additional tracking behaviors, without the library needing to grant friendship - to types it cannot anticipate + to types it cannot anticipate. The `dispatch` operation returns a handle for symmetric transfer: - Every coroutine resumption must go through either symmetric - transfer or the scheduler queue -- never through an inline + Every coroutine resumption must go through symmetric transfer + or the executor's queue. It must never go through an inline `resume()` or `dispatch()` that creates a frame below the resumed coroutine. - @li If the executor determines it is safe to resume inline - (e.g., already on the correct thread), returns `c.h` for - the caller to use in symmetric transfer + @li If the executor determines it is safe to resume inline, + returns `c.h` for the caller to use in symmetric transfer. + One such case is an executor already on the correct thread. @li Otherwise, posts the continuation for later execution and - returns `std::noop_coroutine()` - @li The caller is responsible for using the returned handle - appropriately: returning it from `await_suspend` for - symmetric transfer, or calling `.resume()` if at the - event loop pump level + returns `std::noop_coroutine()`. + @li The caller must use the returned handle appropriately. + Return it from `await_suspend` for symmetric transfer, or + call `.resume()` if at the event loop pump level. A conforming implementation might look like: @@ -100,8 +100,8 @@ class execution_context; The `post` operation queues for later execution: - @li Never blocks the caller - @li The coroutine executes on the executor's associated context + @li Never blocks the caller. + @li The coroutine executes on the executor's associated context. @par Continuation Lifetime diff --git a/include/boost/capy/concept/io_awaitable.hpp b/include/boost/capy/concept/io_awaitable.hpp index a1942e372..835c59480 100644 --- a/include/boost/capy/concept/io_awaitable.hpp +++ b/include/boost/capy/concept/io_awaitable.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) @@ -18,48 +19,65 @@ namespace boost { namespace capy { -/** Concept for awaitables that participate in the I/O protocol. +/** Requires `await_suspend` to accept a coroutine handle and an `io_env` pointer. An awaitable satisfies `IoAwaitable` if its `await_suspend` accepts an `io_env`, enabling scheduler affinity, cancellation, and allocator propagation. This extended signature distinguishes I/O awaitables from standard C++ awaitables that only take a coroutine handle. + `IoAwaitable` constrains only this one member function, + `await_suspend(std::coroutine_handle<>, io_env const*)`. It is the + single customization point that receives the `io_env`. It is + therefore the only member that needs the executor, stop token, and + frame allocator used to start, schedule, and cancel the operation. + `await_ready` and `await_resume` operate on state local to the + awaitable and take no `io_env` parameter, so this concept does not + check them. + @tparam A The awaitable type. @par Syntactic Requirements @li `a.await_suspend(h, env)` must be a valid expression where: - - `h` is a `std::coroutine_handle<>` (coroutine handle) - - `env` is an `io_env const*` + - `h` is a `std::coroutine_handle<>` (coroutine handle). + - `env` is an `io_env const*`. @par Semantic Requirements When `await_suspend` is called: @li The awaitable uses `env->executor` to schedule - resumption of the coroutine when the operation completes + resumption of the coroutine when the operation completes. @li The awaitable should monitor `env->stop_token` and complete early with a cancellation error if stop is - requested + requested. @li The awaitable may use `env->frame_allocator` for internal - allocations + allocations. @li The awaitable must propagate `env->frame_allocator` faithfully - to any child coroutines it creates + to any child coroutines it creates. @li The awaitable may return `std::noop_coroutine()` to - indicate the operation was started asynchronously + indicate the operation was started asynchronously. @par Lifetime - The `io_env` passed to `await_suspend` is guaranteed by launch - functions such as @ref run or @ref run_async to remain valid for the - lifetime of the awaitable's async operation. Awaitables that need to - retain access to the environment should store it as `io_env const*`, - never as a copy. Copying is unnecessary and wasteful because the - referent is guaranteed to outlive the operation. + The `io_env` passed to `await_suspend` remains valid for the + lifetime of the awaitable's async operation. @ref run, + @ref run_async and the other functions that start a task + guarantee this. + Awaitables that need to retain access to the environment should + store it as `io_env const*`, never as a copy. Copying is + unnecessary and wasteful because the referent is guaranteed to + outlive the operation. @par Conforming Signatures + Only the `await_suspend` overload shown below is checked by + `IoAwaitable`. `await_ready` and `await_resume` are shown for + context. The C++ awaitable protocol (`co_await`) requires the + compiler to find them on the awaiter type. This concept does not + require them. + @code struct A { @@ -116,7 +134,7 @@ concept IoAwaitable = a.await_suspend(h, env); }; -/** The return type of `co_await a` for awaitable type A. +/** Names what `co_await a` yields for awaitable type A. Given an awaitable A, yields the type returned by A::await_resume(). @@ -125,7 +143,7 @@ concept IoAwaitable = template using awaitable_result_t = decltype(std::declval&>().await_resume()); -/** Concept for ranges of I/O awaitables. +/** Requires a sized input range whose value type satisfies `IoAwaitable`. A range satisfies `IoAwaitableRange` if it is a sized input range whose value type satisfies @ref IoAwaitable. diff --git a/include/boost/capy/concept/io_runnable.hpp b/include/boost/capy/concept/io_runnable.hpp index 202b69e02..cb56b51b5 100644 --- a/include/boost/capy/concept/io_runnable.hpp +++ b/include/boost/capy/concept/io_runnable.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) @@ -21,60 +22,62 @@ namespace boost { namespace capy { -/** Concept for task types that can be launched from non-coroutine contexts. +/** Requires an `IoAwaitable` that exposes `handle` and `release`, and whose promise takes a continuation and an environment from outside a coroutine. - Extends @ref IoAwaitable with operations needed by launch utilities - (@ref run, @ref run_async) to start a task, transfer ownership of the - coroutine frame, and retrieve results or exceptions after completion. + Extends @ref IoAwaitable with the operations that @ref run and + @ref run_async need. Those operations start a task, transfer ownership + of the coroutine frame, and retrieve results or exceptions after + completion. @tparam T The task type. @par Syntactic Requirements - @li `T` must satisfy @ref IoAwaitable - @li `T::promise_type` must be a valid type + @li `T` must satisfy @ref IoAwaitable. + @li `T::promise_type` must be a valid type. @li `ct.handle()` (callable on a `const` task) returns - `std::coroutine_handle`, must be `noexcept` - @li `t.release()` releases ownership, must be `noexcept` - @li `p.exception()` returns `std::exception_ptr`, must be `noexcept` - @li `p.result()` returns the task result (required for non-void tasks) - @li `p.set_continuation(h)` sets the continuation handle, must be `noexcept` - @li `p.set_environment(env)` sets the execution environment, must be `noexcept` + `std::coroutine_handle`, must be `noexcept`. + @li `t.release()` releases ownership, must be `noexcept`. + @li `p.exception()` returns `std::exception_ptr`, must be `noexcept`. + @li `p.result()` returns the task result (required for non-void tasks). + @li `p.set_continuation(h)` sets the continuation handle, must be `noexcept`. + @li `p.set_environment(env)` sets the execution environment, must be `noexcept`. @par Semantic Requirements The `handle` operation provides access to the coroutine: - @li Returns the typed coroutine handle for the task's frame - @li The task retains ownership; destroying the task destroys the frame + @li Returns the typed coroutine handle for the task's frame. + @li The task retains ownership; destroying the task destroys the frame. The `release` operation transfers ownership: - @li After `release()`, destroying the task does not destroy the frame - @li The caller becomes responsible for resuming and destroying the frame + @li After `release()`, destroying the task does not destroy the frame. + @li The caller becomes responsible for resuming and destroying the frame. The `exception` operation retrieves failure state: @li Returns the exception stored by the promise if the coroutine - completed with an unhandled exception - @li Returns `nullptr` if no exception was thrown + completed with an unhandled exception. + @li Returns `nullptr` if no exception was thrown. The `result` operation retrieves success state (non-void tasks): - @li Returns the value passed to `co_return` - @li Behavior is undefined if called when `exception()` is non-null + @li Returns the value passed to `co_return`. + @li Behavior is undefined if called when `exception()` is non-null. The `set_continuation` operation establishes the continuation: @li Sets the coroutine handle to resume when this task reaches - `final_suspend` - @li Used by launch functions to wire the task back to the trampoline + `final_suspend`. + @li The functions that start a task use it to wire the task back + to the trampoline. The `set_environment` operation establishes the execution environment: @li Sets the `io_env` pointer that propagates executor, stop token, - and allocator through the coroutine chain - @li The pointed-to `io_env` must outlive the coroutine + and allocator through the coroutine chain. + @li The pointed-to `io_env` must outlive the coroutine. @par Conforming Signatures diff --git a/include/boost/capy/concept/read_stream.hpp b/include/boost/capy/concept/read_stream.hpp index 2e138da2b..2cdf5267e 100644 --- a/include/boost/capy/concept/read_stream.hpp +++ b/include/boost/capy/concept/read_stream.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) @@ -22,7 +23,7 @@ namespace boost { namespace capy { -/** Concept for types providing awaitable read operations. +/** Requires `read_some` to return an `IoAwaitable` decomposing to an error code and a size. A type satisfies `ReadStream` if it provides a `read_some` member function template that accepts any @ref MutableBufferSequence @@ -30,10 +31,10 @@ namespace capy { @par Syntactic Requirements @li `T` must provide a `read_some` member function template - accepting any @ref MutableBufferSequence - @li The return type of `read_some` must satisfy @ref IoAwaitable + accepting any @ref MutableBufferSequence. + @li The return type of `read_some` must satisfy @ref IoAwaitable. @li The awaitable's result must decompose to - `(error_code,std::size_t)` via structured bindings + `(error_code,std::size_t)` via structured bindings. @par Semantic Requirements Attempts to read up to `buffer_size( buffers )` bytes from @@ -45,12 +46,12 @@ namespace capy { `n` bytes were read into the buffer sequence. @li If `ec`, then `n >= 0 && n < buffer_size( buffers )`. `n` is the number of bytes read before the I/O - condition arose. + contingency arose. - Equivalently, `n == buffer_size( buffers )` implies `!ec`: a + Equivalently, `n == buffer_size( buffers )` implies `!ec`. A completion that fills the buffer sequence is a success, even when - the underlying operation also signals a condition such as - end-of-stream. That condition is reported on a subsequent read. + the underlying operation also signals a contingency such as + end-of-stream. That contingency is reported on a subsequent read. This lets generic composition algorithms such as `when_all` and `when_any` distinguish a completed transfer from a failure. @@ -60,12 +61,17 @@ namespace capy { Buffers in the sequence are filled in order. + @par After an Error + A subsequent `read_some` call is permitted. A conforming stream + may report the same contingency, report a different one, or + resume delivering data. + @par Error Reporting - I/O conditions arising from the underlying I/O system (EOF, - connection reset, broken pipe, etc.) are reported via the - `error_code` component of the return value. Failures in the - library wrapper itself (such as memory allocation failure) - are reported via exceptions. + I/O contingencies arising from the underlying I/O system are + reported via the `error_code` component of the return value. + Examples are EOF, connection reset, and broken pipe. Failures + in the library wrapper itself (such as memory allocation + failure) are reported via exceptions. @throws std::bad_alloc If coroutine frame allocation fails. @@ -80,12 +86,12 @@ namespace capy { @endcode @warning **Pass buffer sequences by value.** A by-value parameter - is copied into the coroutine frame (or the awaitable's state), - so the returned awaitable is self-contained and may be stored, - moved across threads, or wrapped into a sender without lifetime - concerns. A by-const-reference parameter binds to caller storage - and is only safe when the awaitable is consumed immediately by - `co_await` in the same scope; storing such an awaitable produces + is copied into the coroutine frame, or into the awaitable's state. + The returned awaitable is therefore self-contained. A caller may + store it, move it across threads, or wrap it into a sender without + lifetime concerns. A by-const-reference parameter binds to caller + storage. It is safe only when `co_await` consumes the awaitable + immediately, in the same scope. Storing such an awaitable produces a dangling reference. @note Callers who want to avoid copying an expensive buffer diff --git a/include/boost/capy/concept/stream.hpp b/include/boost/capy/concept/stream.hpp index 944f0d451..571f42d8d 100644 --- a/include/boost/capy/concept/stream.hpp +++ b/include/boost/capy/concept/stream.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) @@ -17,7 +18,7 @@ namespace boost { namespace capy { -/** Concept for types providing both read and write operations. +/** Requires a type to satisfy both `ReadStream` and `WriteStream`. A type satisfies `Stream` if it satisfies both @ref ReadStream and @ref WriteStream. @@ -40,7 +41,19 @@ namespace capy { auto [ec, n] = co_await stream.read_some(make_buffer(buf)); if(ec) co_return; - co_await stream.write_some(const_buffer(buf, n)); + + // write_some may transfer fewer than n bytes (the partial-write + // contract it inherits from WriteStream), so loop until every + // byte read is written, or an error stops the loop early. + std::size_t total = 0; + while(total < n) + { + auto [ec2, n2] = co_await stream.write_some( + const_buffer(buf + total, n - total)); + total += n2; + if(ec2) + co_return; + } } @endcode diff --git a/include/boost/capy/concept/write_stream.hpp b/include/boost/capy/concept/write_stream.hpp index d55a4218c..7d326f04f 100644 --- a/include/boost/capy/concept/write_stream.hpp +++ b/include/boost/capy/concept/write_stream.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) @@ -22,7 +23,7 @@ namespace boost { namespace capy { -/** Concept for types that provide awaitable write operations. +/** Requires `write_some` to return an `IoAwaitable` decomposing to an error code and a size. A type satisfies `WriteStream` if it provides a `write_some` member function template that accepts any @ref ConstBufferSequence @@ -33,10 +34,10 @@ namespace capy { @par Syntactic Requirements @li `T` must provide a `write_some` member function template - accepting any @ref ConstBufferSequence - @li The return type of `write_some` must satisfy @ref IoAwaitable + accepting any @ref ConstBufferSequence. + @li The return type of `write_some` must satisfy @ref IoAwaitable. @li The awaitable's result must decompose to - `(error_code,std::size_t)` via structured bindings + `(error_code,std::size_t)` via structured bindings. @par Semantic Requirements @@ -49,12 +50,12 @@ namespace capy { `n` bytes were written from the buffer sequence. @li If `ec`, then `n >= 0 && n < buffer_size( buffers )`. `n` is the number of bytes written before the I/O - condition arose. + contingency arose. - Equivalently, `n == buffer_size( buffers )` implies `!ec`: a + Equivalently, `n == buffer_size( buffers )` implies `!ec`. A completion that writes the entire buffer sequence is a success, even - when the underlying operation also signals a condition. That - condition is reported on a subsequent write. This lets generic + when the underlying operation also signals a contingency. That + contingency is reported on a subsequent write. This lets generic composition algorithms such as `when_all` and `when_any` distinguish a completed transfer from a failure. @@ -64,13 +65,19 @@ namespace capy { Buffers in the sequence are consumed in order. + @par After an Error + + A subsequent `write_some` call is permitted. A conforming stream + may report the same contingency, report a different one, or + resume delivering data. + @par Error Reporting - I/O conditions arising from the underlying I/O system (EOF, - connection reset, broken pipe, etc.) are reported via the - `error_code` component of the return value. Failures in the - library wrapper itself (such as memory allocation failure) - are reported via exceptions. + I/O contingencies arising from the underlying I/O system are + reported via the `error_code` component of the return value. + Examples are EOF, connection reset, and broken pipe. Failures + in the library wrapper itself (such as memory allocation + failure) are reported via exceptions. @throws std::bad_alloc If coroutine frame allocation fails. @@ -87,12 +94,12 @@ namespace capy { @endcode @warning **Pass buffer sequences by value.** A by-value parameter - is copied into the coroutine frame (or the awaitable's state), - so the returned awaitable is self-contained and may be stored, - moved across threads, or wrapped into a sender without lifetime - concerns. A by-const-reference parameter binds to caller storage - and is only safe when the awaitable is consumed immediately by - `co_await` in the same scope; storing such an awaitable produces + is copied into the coroutine frame, or into the awaitable's state. + The returned awaitable is therefore self-contained. A caller may + store it, move it across threads, or wrap it into a sender without + lifetime concerns. A by-const-reference parameter binds to caller + storage. It is safe only when `co_await` consumes the awaitable + immediately, in the same scope. Storing such an awaitable produces a dangling reference. @note Callers who want to avoid copying an expensive buffer diff --git a/include/boost/capy/cond.hpp b/include/boost/capy/cond.hpp index 430a35740..d542f0c04 100644 --- a/include/boost/capy/cond.hpp +++ b/include/boost/capy/cond.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) @@ -28,11 +29,17 @@ namespace capy { @code auto [ec, n] = co_await stream.read_some( bufs ); if( ec == cond::canceled ) + { // handle cancellation + } else if( ec == cond::eof ) + { // handle end of stream + } else if( ec ) + { // handle other errors + } @endcode @see error @@ -42,8 +49,8 @@ enum class cond /** End-of-stream condition. An `error_code` compares equal to `eof` when the stream - reached its natural end, such as when a peer sends TCP FIN - or a file reaches EOF. + reached its natural end. Examples are a peer sending TCP + FIN, or a file reaching EOF. */ eof = 1, @@ -105,7 +112,13 @@ BOOST_CAPY_DECL extern cond_cat_type cond_cat; } // detail -/// Create an error_condition from a cond value. +/** Create an error_condition from a cond value. + + @param ev The library condition value. + + @return An `std::error_condition` holding `ev` in the library's + condition category. +*/ inline std::error_condition make_error_condition( diff --git a/include/boost/capy/continuation.hpp b/include/boost/capy/continuation.hpp index e0a581ad4..859fe53fd 100644 --- a/include/boost/capy/continuation.hpp +++ b/include/boost/capy/continuation.hpp @@ -17,7 +17,7 @@ namespace boost { namespace capy { -/** Executor-facing schedulable unit. +/** Pairs a coroutine handle with a scratch slot so executors can queue it without heap allocation. Wraps a `std::coroutine_handle<>` with a single pointer-sized scratch slot so executors can queue @@ -33,8 +33,9 @@ namespace capy { @li `reserved` — a pointer-sized scratch slot. Ordinary users must not touch it. Authors of awaitable algorithms (e.g. `async_mutex`, `async_semaphore`) may commandeer - it for their own node-based data structure, but **only - before** the continuation is submitted to an executor. + it for their own node-based data structure. They may do + so **only before** the continuation is submitted to an + executor. On submission the executor **clobbers** `reserved` to link the continuation into its internal queue; the value carries **no meaning** afterward. Once submitted, the @@ -52,11 +53,11 @@ namespace capy { destroyed, or enqueued in more than one queue concurrently. An author who needs a doubly-linked (or otherwise richer) - structure should hold a `continuation` as a member — or - derive from it, since it is an aggregate — and manage their - own links: `reserved` is only a single pre-submission scratch - slot, and it is no longer available once the continuation is - submitted. + structure should hold a `continuation` as a member. Deriving + from it also works, because `continuation` is an aggregate. + Such an author should manage their own links, because + `reserved` is only a single pre-submission scratch slot. It + is no longer available once the continuation is submitted. @par Copy and Move @@ -79,7 +80,21 @@ namespace capy { */ struct continuation { + /** The coroutine handle to resume. + + Set by the code that creates or reuses the continuation, and read + by the executor when it dequeues it. + */ std::coroutine_handle<> h; + + /** Pointer-sized scratch slot, available only before submission. + + Authors of awaitable algorithms may commandeer it for their own + node links until the continuation is submitted to an executor. On + submission the executor clobbers it to link the continuation into + its own queue, after which the value carries no meaning. See the + class description for the full contract. + */ void* reserved = nullptr; }; diff --git a/include/boost/capy/detail/slice_of.hpp b/include/boost/capy/detail/slice_of.hpp index 740727dc7..8df1470a3 100644 --- a/include/boost/capy/detail/slice_of.hpp +++ b/include/boost/capy/detail/slice_of.hpp @@ -26,19 +26,20 @@ namespace detail { `slice_of` is the generic result of `buffer_slice` for a sequence that is not closed under sub-ranging (everything except a single - buffer). It models the same buffer-sequence concept as `BS` - (`MutableBufferSequence` if `BS` is mutable, otherwise - `ConstBufferSequence`), so it can be passed anywhere a buffer sequence - is expected. + buffer). It models the same buffer-sequence concept as `BS`: + `MutableBufferSequence` if `BS` is mutable, otherwise + `ConstBufferSequence`. It can therefore be passed anywhere a buffer + sequence is expected. It stores iterators into the underlying sequence plus front/back byte offsets; it neither owns nor copies the descriptors. The underlying sequence must outlive the view. @par Complexity - Construction is a single forward pass to the cut points: O(buffers up - to `offset`) for a to-end slice, O(buffers up to `offset + length`) - for a bounded slice. It never sums the whole sequence. + Construction is a single forward pass to the cut points. It is + O(buffers up to `offset`) for a to-end slice, and O(buffers up to + `offset + length`) for a bounded slice. It never sums the whole + sequence. */ template requires MutableBufferSequence || ConstBufferSequence diff --git a/include/boost/capy/error.hpp b/include/boost/capy/error.hpp index 41f8c20e1..4aae7d3bd 100644 --- a/include/boost/capy/error.hpp +++ b/include/boost/capy/error.hpp @@ -79,7 +79,13 @@ BOOST_CAPY_DECL extern error_cat_type error_cat; } // detail -/// Create an error_code from an error value. +/** Create an error_code from an error value. + + @param ev The library error value. + + @return An `std::error_code` holding `ev` in the library's + error category. +*/ inline std::error_code make_error_code( diff --git a/include/boost/capy/ex/any_executor.hpp b/include/boost/capy/ex/any_executor.hpp index fa8cad49f..bde7539f5 100644 --- a/include/boost/capy/ex/any_executor.hpp +++ b/include/boost/capy/ex/any_executor.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) @@ -34,7 +35,7 @@ struct is_strand_type> : std::true_type {}; } // detail -/** A type-erased wrapper for executor objects. +/** Forwards `dispatch`/`post`/`context` calls through a shared, type-erased executor pointer. This class provides type erasure for any executor type, enabling runtime polymorphism with automatic memory management via shared @@ -45,16 +46,18 @@ struct is_strand_type> : std::true_type {}; @par Value Semantics This class has value semantics with shared ownership. Copy and - move operations are cheap, simply copying the internal shared + move operations are cheap, copying the internal shared pointer. Multiple `any_executor` instances may share the same underlying executor. Move operations do not invalidate the source; there is no moved-from state. @par Default State - A default-constructed `any_executor` holds no executor. Calling - executor operations on a default-constructed instance results - in undefined behavior. Use `operator bool()` to check validity. + A default-constructed `any_executor` holds no executor. + `operator bool()`, `operator==`, and `target_type()` report the + empty state. `context()`, `on_work_started()`, `on_work_finished()`, + `dispatch()`, and `post()` are undefined behavior until an + executor is assigned. @par Thread Safety @@ -150,9 +153,10 @@ class any_executor public: /** Construct a default instance. - Constructs an empty `any_executor`. Calling any executor - operations on a default-constructed instance results in - undefined behavior. + Constructs an empty `any_executor`. `operator bool()` reports + the empty state; `context()`, `on_work_started()`, + `on_work_finished()`, `dispatch()`, and `post()` are undefined + behavior until an executor is assigned. @par Postconditions @li `!*this` @@ -164,19 +168,25 @@ class any_executor Creates a new `any_executor` sharing ownership of the underlying executor with `other`. + @param other The executor to copy. + @par Postconditions @li `*this == other` */ - any_executor(any_executor const&) = default; + any_executor(any_executor const& other) = default; /** Copy assignment operator. Shares ownership of the underlying executor with `other`. + @param other The executor to copy. + + @return A reference to `*this`. + @par Postconditions @li `*this == other` */ - any_executor& operator=(any_executor const&) = default; + any_executor& operator=(any_executor const& other) = default; /** Constructs from any executor type. diff --git a/include/boost/capy/ex/async_event.hpp b/include/boost/capy/ex/async_event.hpp index ed254b2be..13c950554 100644 --- a/include/boost/capy/ex/async_event.hpp +++ b/include/boost/capy/ex/async_event.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) @@ -46,7 +47,7 @@ namespace boost { namespace capy { -/** An asynchronous event for coroutines. +/** Queues coroutines in `wait()` and resumes all of them when `set()` is called. This event provides a way to notify multiple coroutines that some condition has occurred. When a coroutine awaits an unset event, it @@ -106,7 +107,7 @@ class async_event detail::intrusive_list waiters_; public: - /** Awaiter returned by wait(). + /** Suspends the caller until `set()` runs, or resumes it with `error::canceled` on a stop request. */ class wait_awaiter : public detail::intrusive_list::node @@ -159,6 +160,14 @@ class async_event } public: + /** Destroy the awaiter, leaving the event unable to reach it. + + Destroys the stop callback if one is registered, and unlinks + the awaiter from the event's wait queue if it is still linked. + Both are necessary when the coroutine frame is torn down while + suspended, so that neither `set()` nor the stop callback can + reach a destroyed awaiter. + */ ~wait_awaiter() { if(active_) @@ -167,11 +176,23 @@ class async_event e_->waiters_.remove(this); } + /** Construct an awaiter for the given event. + + @param e The event to wait on. It must outlive the awaiter. + */ explicit wait_awaiter(async_event* e) noexcept : e_(e) { } + /** Construct by moving. + + The moved-from awaiter is left inert: its destructor no longer + destroys the stop callback and no longer unlinks from the + event's wait queue. + + @param o The awaiter to move from. + */ wait_awaiter(wait_awaiter&& o) noexcept : e_(o.e_) , cont_(o.cont_) @@ -184,16 +205,69 @@ class async_event { } - wait_awaiter(wait_awaiter const&) = delete; - wait_awaiter& operator=(wait_awaiter const&) = delete; - wait_awaiter& operator=(wait_awaiter&&) = delete; + /** Copy construction is disabled; a waiter is linked into the + event's wait queue by address. + + @param other The awaiter that would be copied. + */ + wait_awaiter(wait_awaiter const& other) = delete; + + /** Copy assignment is disabled; a waiter is linked into the + event's wait queue by address. + + @param other The awaiter that would be assigned from. + + @return A reference to `*this`. + */ + wait_awaiter& operator=(wait_awaiter const& other) = delete; + + /** Move assignment is disabled; a waiter is linked into the + event's wait queue by address. + @param other The awaiter that would be moved from. + + @return A reference to `*this`. + */ + wait_awaiter& operator=(wait_awaiter&& other) = delete; + + /** Report whether the event is already set. + + @return `true` if the event is set, in which case the awaiting + coroutine does not suspend; otherwise `false`. + */ bool await_ready() const noexcept { return e_->set_; } - /** IoAwaitable protocol overload. */ + /** Enqueue the awaiting coroutine until the event is set. + + This is the @ref IoAwaitable overload of `await_suspend`. + + If a stop request is already pending on `env->stop_token`, the + awaiter records the cancellation and does not enqueue. + + Otherwise it stores `h` and `env->executor`, links itself into + the event's wait queue, and registers a stop callback on + `env->stop_token`. Exactly one of `set()` and that callback posts + `h` through the stored executor, whichever claims the waiter + first. Only the post is subject to that race. A losing stop + callback does nothing at all, but `set()` unlinks every waiter it + pops whether it claims it or not. That is why @ref await_resume + unlinks a canceled waiter only when it is still linked. + + @param h The awaiting coroutine, resumed when the event is set + or the wait is canceled. + + @param env The execution environment. Its executor posts the + resumption and its stop token is watched for the duration of + the wait. It must outlive the wait. + + @return `h` if a stop request was already pending, which + resumes the awaiting coroutine immediately without enqueuing + it. Otherwise `std::noop_coroutine()`, which leaves the + coroutine suspended and returns control to the resumer. + */ std::coroutine_handle<> await_suspend( std::coroutine_handle<> h, @@ -214,6 +288,16 @@ class async_event return std::noop_coroutine(); } + /** Complete the wait and report the outcome. + + Destroys the stop callback if one is registered. If the wait + was canceled while still linked into the event's wait queue, + unlinks it. `set()` pops every waiter, so a canceled waiter may + or may not still be linked when it resumes. + + @return An empty `io_result<>` if the event was set, or one + holding `error::canceled` if the stop token fired first. + */ io_result<> await_resume() noexcept { if(active_) @@ -238,17 +322,37 @@ class async_event /// Construct an unset event. async_event() = default; - /// Copy constructor (deleted). - async_event(async_event const&) = delete; + /** Copy construction is disabled; suspended waiters point into the + event's wait queue. - /// Copy assignment (deleted). - async_event& operator=(async_event const&) = delete; + @param other The event that would be copied. + */ + async_event(async_event const& other) = delete; + + /** Copy assignment is disabled; suspended waiters point into the + event's wait queue. - /// Move constructor (deleted). - async_event(async_event&&) = delete; + @param other The event that would be assigned from. + + @return A reference to `*this`. + */ + async_event& operator=(async_event const& other) = delete; - /// Move assignment (deleted). - async_event& operator=(async_event&&) = delete; + /** Move construction is disabled; suspended waiters point into the + event's wait queue. + + @param other The event that would be moved from. + */ + async_event(async_event&& other) = delete; + + /** Move assignment is disabled; suspended waiters point into the + event's wait queue. + + @param other The event that would be moved from. + + @return A reference to `*this`. + */ + async_event& operator=(async_event&& other) = delete; /** Returns an awaiter that waits until the event is set. @@ -261,7 +365,7 @@ class async_event return wait_awaiter{this}; } - /** Sets the event. + /** Resumes every waiting coroutine and marks the event set for later `wait()` calls. All waiting coroutines are resumed. Canceled waiters are skipped. Subsequent calls to wait() complete @@ -286,7 +390,7 @@ class async_event /** Clears the event. - Subsequent calls to wait() will suspend until + Subsequent calls to wait() suspend until set() is called again. */ void clear() noexcept @@ -295,6 +399,8 @@ class async_event } /** Returns true if the event is currently set. + + @return `true` if the event is set; otherwise `false`. */ bool is_set() const noexcept { diff --git a/include/boost/capy/ex/async_mutex.hpp b/include/boost/capy/ex/async_mutex.hpp index 90c9a4a13..aa5525bfe 100644 --- a/include/boost/capy/ex/async_mutex.hpp +++ b/include/boost/capy/ex/async_mutex.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) @@ -90,7 +91,7 @@ namespace boost { namespace capy { -/** An asynchronous mutex for coroutines. +/** Queues coroutines in `lock()` and resumes exactly one when the mutex is free. This mutex provides mutual exclusion for coroutines without blocking. When a coroutine attempts to acquire a locked mutex, it suspends and @@ -136,7 +137,7 @@ namespace capy { } // Or with RAII: - task<> protected_operation() { + task<> protected_operation_raii() { auto [ec, guard] = co_await cm.scoped_lock(); if(ec) co_return; @@ -157,7 +158,7 @@ class async_mutex detail::intrusive_list waiters_; public: - /** Awaiter returned by lock(). + /** Suspends the caller until the mutex is free, or resumes it with `error::canceled` on a stop request. */ class lock_awaiter : public detail::intrusive_list::node @@ -208,6 +209,18 @@ class async_mutex } public: + /** Destroy the awaiter, leaving the mutex unable to reach it. + + If the awaiter is suspended in the wait queue, destroys the + stop callback and unlinks the awaiter. Neither `unlock()` nor + the stop callback can then reach a destroyed awaiter when the + coroutine frame is torn down while suspended. + + @par Preconditions + Called on the executor thread. The stop callback may fire from + any thread, so destroying a still-suspended awaiter from + another thread is undefined. + */ ~lock_awaiter() { if(active_) @@ -217,11 +230,23 @@ class async_mutex } } + /** Construct an awaiter for the given mutex. + + @param m The mutex to acquire. It must outlive the awaiter. + */ explicit lock_awaiter(async_mutex* m) noexcept : m_(m) { } + /** Construct by moving. + + The moved-from awaiter is left inert: its destructor no longer + destroys the stop callback and no longer unlinks from the + mutex's wait queue. + + @param o The awaiter to move from. + */ lock_awaiter(lock_awaiter&& o) noexcept : m_(o.m_) , cont_(o.cont_) @@ -233,10 +258,43 @@ class async_mutex { } - lock_awaiter(lock_awaiter const&) = delete; - lock_awaiter& operator=(lock_awaiter const&) = delete; - lock_awaiter& operator=(lock_awaiter&&) = delete; + /** Copy construction is disabled; a waiter is linked into the + mutex's wait queue by address. + + @param other The awaiter that would be copied. + */ + lock_awaiter(lock_awaiter const& other) = delete; + + /** Copy assignment is disabled; a waiter is linked into the + mutex's wait queue by address. + + @param other The awaiter that would be assigned from. + @return A reference to `*this`. + */ + lock_awaiter& operator=(lock_awaiter const& other) = delete; + + /** Move assignment is disabled; a waiter is linked into the + mutex's wait queue by address. + + @param other The awaiter that would be moved from. + + @return A reference to `*this`. + */ + lock_awaiter& operator=(lock_awaiter&& other) = delete; + + /** Acquire the mutex if it is free, reporting whether to suspend. + + This is not a pure query: on the fast path it takes the lock. + When the mutex is unlocked, it marks the mutex locked and + reports that no suspension is needed. The stop token is not + consulted, so an uncontended `lock()` succeeds even when stop + has already been requested. + + @return `true` if the mutex was free and is now held by the + awaiting coroutine. `false` if the mutex is held elsewhere, in + which case the coroutine suspends. + */ bool await_ready() const noexcept { if(!m_->locked_) @@ -247,7 +305,32 @@ class async_mutex return false; } - /** IoAwaitable protocol overload. */ + /** Enqueue the awaiting coroutine until the mutex is released. + + This is the @ref IoAwaitable overload of `await_suspend`. + + If a stop request is already pending on `env->stop_token`, the + awaiter records the cancellation and does not enqueue. The + mutex is not acquired. + + Otherwise it stores `h` and `env->executor`, links itself into + the back of the mutex's wait queue, and registers a stop + callback on `env->stop_token`. Whichever of `unlock()` and that + callback claims the awaiter first posts `h` through the stored + executor; the other skips it. + + @param h The awaiting coroutine, resumed when the mutex is + acquired or the wait is canceled. + + @param env The execution environment. Its executor posts the + resumption and its stop token is watched for the duration of + the wait. It must outlive the wait. + + @return `h` if a stop request was already pending, which + resumes the awaiting coroutine immediately without enqueuing + it. Otherwise `std::noop_coroutine()`, which leaves the + coroutine suspended and returns control to the resumer. + */ std::coroutine_handle<> await_suspend( std::coroutine_handle<> h, @@ -267,6 +350,16 @@ class async_mutex return std::noop_coroutine(); } + /** Complete the acquisition and report the outcome. + + Destroys the stop callback if one is registered, and unlinks a + canceled awaiter from the wait queue. + + @return An empty `io_result<>` if the mutex is now held by the + awaiting coroutine. Otherwise one holding `error::canceled`, + which means the stop token won the race and the mutex is not + held. + */ io_result<> await_resume() noexcept { if(active_) @@ -288,36 +381,62 @@ class async_mutex } }; - /** RAII lock guard for async_mutex. - - Automatically unlocks the mutex when destroyed. + /** Unlocks the mutex automatically when destroyed. */ class [[nodiscard]] lock_guard { async_mutex* m_; public: + /// Unlock the mutex, if this guard holds one. ~lock_guard() { if(m_) m_->unlock(); } + /// Construct a guard that holds no mutex. lock_guard() noexcept : m_(nullptr) { } + /** Construct a guard that releases the given mutex on destruction. + + Adopts an already-held lock; it does not acquire one. + + @param m The mutex to unlock on destruction. It must outlive + the guard. + */ explicit lock_guard(async_mutex* m) noexcept : m_(m) { } + /** Construct by moving, transferring the lock. + + @par Postconditions + `o` holds no mutex, and its destructor unlocks nothing. + + @param o The guard to move from. + */ lock_guard(lock_guard&& o) noexcept : m_(std::exchange(o.m_, nullptr)) { } + /** Assign by moving, transferring the lock. + + If this guard already holds a mutex, that mutex is unlocked + first. Self-assignment is a no-op. + + @par Postconditions + `o` holds no mutex, and its destructor unlocks nothing. + + @param o The guard to move from. + + @return A reference to `*this`. + */ lock_guard& operator=(lock_guard&& o) noexcept { if(this != &o) @@ -329,11 +448,22 @@ class async_mutex return *this; } - lock_guard(lock_guard const&) = delete; - lock_guard& operator=(lock_guard const&) = delete; + /** Copy construction is disabled; a guard uniquely owns the lock. + + @param other The guard that would be copied. + */ + lock_guard(lock_guard const& other) = delete; + + /** Copy assignment is disabled; a guard uniquely owns the lock. + + @param other The guard that would be assigned from. + + @return A reference to `*this`. + */ + lock_guard& operator=(lock_guard const& other) = delete; }; - /** Awaiter returned by scoped_lock() that returns a lock_guard on resume. + /** Acquires the mutex like `lock_awaiter`, then resumes with a `lock_guard` that unlocks it. */ class lock_guard_awaiter { @@ -341,18 +471,48 @@ class async_mutex lock_awaiter inner_; public: + /** Construct an awaiter for the given mutex. + + @param m The mutex to acquire. It must outlive the awaiter. + */ explicit lock_guard_awaiter(async_mutex* m) noexcept : m_(m) , inner_(m) { } + /** Acquire the mutex if it is free, reporting whether to suspend. + + Delegates to @ref lock_awaiter::await_ready, so as there this is + not a pure query: on the fast path it takes the lock. + + @return `true` if the mutex was free and is now held by the + awaiting coroutine. `false` if the mutex is held elsewhere, in + which case the coroutine suspends. + */ bool await_ready() const noexcept { return inner_.await_ready(); } - /** IoAwaitable protocol overload. */ + /** Enqueue the awaiting coroutine until the mutex is released. + + This is the @ref IoAwaitable overload of `await_suspend`. It + delegates to @ref lock_awaiter::await_suspend on the wrapped + awaiter, so it has that function's contract. + + @param h The awaiting coroutine, resumed when the mutex is + acquired or the wait is canceled. + + @param env The execution environment. Its executor posts the + resumption and its stop token is watched for the duration of + the wait. It must outlive the wait. + + @return `h` if a stop request was already pending, which + resumes the awaiting coroutine immediately without enqueuing + it. Otherwise `std::noop_coroutine()`, which leaves the + coroutine suspended and returns control to the resumer. + */ std::coroutine_handle<> await_suspend( std::coroutine_handle<> h, @@ -361,6 +521,13 @@ class async_mutex return inner_.await_suspend(h, env); } + /** Complete the acquisition and report the outcome. + + @return An `io_result` destructuring as + `[ec, guard]`. On success `ec` is empty and `guard` holds the + mutex, releasing it when destroyed. If the wait was canceled, + `ec` is `error::canceled` and `guard` holds no mutex. + */ io_result await_resume() noexcept { auto r = inner_.await_resume(); @@ -373,17 +540,37 @@ class async_mutex /// Construct an unlocked mutex. async_mutex() = default; - /// Copy constructor (deleted). - async_mutex(async_mutex const&) = delete; + /** Copy construction is disabled; suspended waiters point into the + mutex's wait queue. - /// Copy assignment (deleted). - async_mutex& operator=(async_mutex const&) = delete; + @param other The mutex that would be copied. + */ + async_mutex(async_mutex const& other) = delete; - /// Move constructor (deleted). - async_mutex(async_mutex&&) = delete; + /** Copy assignment is disabled; suspended waiters point into the + mutex's wait queue. - /// Move assignment (deleted). - async_mutex& operator=(async_mutex&&) = delete; + @param other The mutex that would be assigned from. + + @return A reference to `*this`. + */ + async_mutex& operator=(async_mutex const& other) = delete; + + /** Move construction is disabled; suspended waiters point into the + mutex's wait queue. + + @param other The mutex that would be moved from. + */ + async_mutex(async_mutex&& other) = delete; + + /** Move assignment is disabled; suspended waiters point into the + mutex's wait queue. + + @param other The mutex that would be moved from. + + @return A reference to `*this`. + */ + async_mutex& operator=(async_mutex&& other) = delete; /** Returns an awaiter that acquires the mutex. @@ -430,6 +617,8 @@ class async_mutex } /** Returns true if the mutex is currently locked. + + @return `true` if the mutex is held; otherwise `false`. */ bool is_locked() const noexcept { diff --git a/include/boost/capy/ex/async_waker.hpp b/include/boost/capy/ex/async_waker.hpp index 3f47af66a..ba940ab69 100644 --- a/include/boost/capy/ex/async_waker.hpp +++ b/include/boost/capy/ex/async_waker.hpp @@ -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) @@ -88,11 +89,11 @@ namespace capy { Distinct objects: Safe.@n Shared objects: `wake()` may be called from any thread. - `wait()` must only be awaited by one coroutine at a time, and - only on an executor that never runs the coroutine's - continuations concurrently: a single-threaded executor or a - strand over a multi-threaded one (the same threading model as - `async_event` and `async_mutex`). Awaiting `wait()` directly + `wait()` must only be awaited by one coroutine at a time. The + executor must never run the coroutine's continuations + concurrently: use a single-threaded executor, or a strand over + a multi-threaded one. That is the same threading model as + `async_event` and `async_mutex`. Awaiting `wait()` directly on a multi-threaded executor is undefined. This type is non-copyable and non-movable because a suspended @@ -129,7 +130,7 @@ class async_waker wait_awaiter* waiter_ = nullptr; public: - /** Awaiter returned by wait(). + /** Suspends the caller until `wake()` runs, or resumes it with `error::canceled` on a stop request. */ class wait_awaiter { @@ -181,6 +182,15 @@ class async_waker } public: + /** Destroy the awaiter, leaving the waker unable to reach it. + + Destroys the stop callback if one is registered. If the awaiter + is still armed, it also returns the waker's slot to the empty + state, so a later `wake()` cannot dereference a destroyed + awaiter. That case means the frame is being torn down without + ever being resumed; a wake arriving afterward latches a token + instead. + */ ~wait_awaiter() { if(active_) @@ -198,11 +208,23 @@ class async_waker } } + /** Construct an awaiter for the given waker. + + @param waker The waker to wait on. It must outlive the awaiter. + */ explicit wait_awaiter(async_waker* waker) noexcept : waker_(waker) { } + /** Construct by moving. + + The moved-from awaiter is left inert: its destructor no longer + destroys the stop callback and no longer deregisters from the + waker. + + @param o The awaiter to move from. + */ wait_awaiter(wait_awaiter&& o) noexcept : waker_(o.waker_) , cont_(o.cont_) @@ -213,11 +235,42 @@ class async_waker { } - wait_awaiter(wait_awaiter const&) = delete; - wait_awaiter& operator=(wait_awaiter const&) = delete; - wait_awaiter& operator=(wait_awaiter&&) = delete; + /** Copy construction is disabled; an armed waiter is registered + with the waker by address. + + @param other The awaiter that would be copied. + */ + wait_awaiter(wait_awaiter const& other) = delete; + + /** Copy assignment is disabled; an armed waiter is registered + with the waker by address. + + @param other The awaiter that would be assigned from. + + @return A reference to `*this`. + */ + wait_awaiter& operator=(wait_awaiter const& other) = delete; + + /** Move assignment is disabled; an armed waiter is registered + with the waker by address. - /// Consume a latched token, completing synchronously. + @param other The awaiter that would be moved from. + + @return A reference to `*this`. + */ + wait_awaiter& operator=(wait_awaiter&& other) = delete; + + /** Consume a latched token, completing synchronously. + + This is not a pure query: the check is a compare-exchange that + takes the token. Calling it twice is not idempotent: the second + call reports `false`, because the first already consumed the + wakeup. + + @return `true` if a pending wakeup token was latched and has now + been consumed, in which case the awaiting coroutine does not + suspend; otherwise `false`. + */ bool await_ready() noexcept { int expected = state_token; @@ -227,7 +280,40 @@ class async_waker std::memory_order_acquire); } - /** IoAwaitable protocol overload. */ + /** Arm the waker with the awaiting coroutine. + + This is the @ref IoAwaitable overload of `await_suspend`. + Unlike `async_event` and `async_mutex`, it has three outcomes, + because a `wake()` from another thread can land in the window + between `await_ready` and this call. + + @li A stop request is already pending on `env->stop_token`: the + awaiter records the cancellation and does not arm. + + @li The waker's slot is no longer empty. Under the single-waiter + precondition that means a wakeup was latched after + `await_ready` looked, so the token is consumed here instead + and the wait succeeds. + + @li Otherwise the slot moves to the armed state, publishing this + awaiter to the waker, and a stop callback is registered on + `env->stop_token`. Whichever of `wake()` and that callback + wins the armed-to-empty transition posts `h` through + `env->executor`. The loser does nothing, and a losing + `wake()` re-latches its token for the next `wait()`. + + @param h The awaiting coroutine, resumed when the waker fires + or the wait is canceled. + + @param env The execution environment. Its executor posts the + resumption and its stop token is watched for the duration of + the wait. It must outlive the wait. + + @return `h` in the first two cases, which resumes the awaiting + coroutine immediately; otherwise `std::noop_coroutine()`, which + leaves the coroutine suspended and returns control to the + resumer. + */ std::coroutine_handle<> await_suspend( std::coroutine_handle<> h, @@ -266,6 +352,16 @@ class async_waker return std::noop_coroutine(); } + /** Complete the wait and report the outcome. + + Destroys the stop callback if one is registered and clears the + armed bookkeeping, so the destructor does not deregister a slot + the resumption already consumed. + + @return An empty `io_result<>` if the wait was woken, whether by + `wake()` or by a token consumed inline. Otherwise one holding + `error::canceled`, which means the stop token won the race. + */ io_result<> await_resume() noexcept { if(active_) @@ -283,17 +379,35 @@ class async_waker /// Construct with no token latched. async_waker() = default; - /// Copy constructor (deleted). - async_waker(async_waker const&) = delete; + /** Copy construction is disabled; an armed waiter points into the + waker. + + @param other The waker that would be copied. + */ + async_waker(async_waker const& other) = delete; - /// Copy assignment (deleted). - async_waker& operator=(async_waker const&) = delete; + /** Copy assignment is disabled; an armed waiter points into the waker. - /// Move constructor (deleted). - async_waker(async_waker&&) = delete; + @param other The waker that would be assigned from. - /// Move assignment (deleted). - async_waker& operator=(async_waker&&) = delete; + @return A reference to `*this`. + */ + async_waker& operator=(async_waker const& other) = delete; + + /** Move construction is disabled; an armed waiter points into the + waker. + + @param other The waker that would be moved from. + */ + async_waker(async_waker&& other) = delete; + + /** Move assignment is disabled; an armed waiter points into the waker. + + @param other The waker that would be moved from. + + @return A reference to `*this`. + */ + async_waker& operator=(async_waker&& other) = delete; /** Asynchronously wait until woken. diff --git a/include/boost/capy/ex/execution_context.hpp b/include/boost/capy/ex/execution_context.hpp index 755b1aae8..c6e956309 100644 --- a/include/boost/capy/ex/execution_context.hpp +++ b/include/boost/capy/ex/execution_context.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) @@ -25,7 +26,7 @@ namespace boost { namespace capy { -/** Base class for I/O object containers providing service management. +/** Registers, looks up, and shuts down `service` objects owned by a derived context. An execution context represents a place where function objects are executed. It provides a service registry where polymorphic services @@ -109,14 +110,17 @@ class BOOST_CAPY_DECL `target()` to return `nullptr`. @tparam Derived The most-derived context type. + + @param self `this`, typed as the most-derived context type. + Only its type is recorded; the pointer is not stored. */ template< typename Derived > - explicit execution_context( Derived* ) noexcept; + explicit execution_context( Derived* self ) noexcept; public: //------------------------------------------------ - /** Abstract base class for services owned by an execution context. + /** Gives a derived service a `shutdown()` hook, run when its owning `execution_context` is destroyed. Services provide extensible functionality to an execution context. Each service type can be registered at most once. Services are @@ -153,9 +157,11 @@ class BOOST_CAPY_DECL service { public: + /// Destructor. virtual ~service() = default; protected: + /// Construct a service. Only derived classes may do so. service() = default; /** Called when the owning execution context shuts down. @@ -185,9 +191,19 @@ class BOOST_CAPY_DECL //------------------------------------------------ - execution_context(execution_context const&) = delete; + /** Copy construction is disabled; a context owns its services. + + @param other The context that would be copied. + */ + execution_context(execution_context const& other) = delete; + + /** Copy assignment is disabled; a context owns its services. + + @param other The context that would be assigned from. - execution_context& operator=(execution_context const&) = delete; + @return A reference to `*this`. + */ + execution_context& operator=(execution_context const& other) = delete; /** Destructor. @@ -376,7 +392,7 @@ class BOOST_CAPY_DECL /** Set the memory resource used for coroutine frame allocation. The caller is responsible for ensuring the memory resource - remains valid for the lifetime of all coroutines launched + remains valid for the lifetime of all coroutines started using this context's executor. @par Thread Safety @@ -491,7 +507,7 @@ class BOOST_CAPY_DECL This function is idempotent; subsequent calls have no effect. @par Preconditions - @li `shutdown()` has been called. + @li `shutdown()` was called. @par Effects All services are deleted and removed from the container. diff --git a/include/boost/capy/ex/executor_ref.hpp b/include/boost/capy/ex/executor_ref.hpp index 3eb293a1e..8339dfcba 100644 --- a/include/boost/capy/ex/executor_ref.hpp +++ b/include/boost/capy/ex/executor_ref.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) @@ -70,17 +71,17 @@ inline constexpr executor_vtable vtable_for = { } // detail -/** A type-erased reference wrapper for executor objects. +/** Forwards `dispatch`/`post`/`context` calls through a non-owning, type-erased executor pointer. This class provides type erasure for any executor type, enabling runtime polymorphism without virtual functions or allocation. It stores a pointer to the original executor and a pointer to a - static vtable, allowing executors of different types to be stored - uniformly while satisfying the full `Executor` concept. + static vtable. Executors of different types are therefore stored + uniformly, while satisfying the full `Executor` concept. @par Reference Semantics This class has reference semantics: it does not allocate or own - the wrapped executor. Copy operations simply copy the internal + the wrapped executor. Copy operations copy the internal pointers. The caller must ensure the referenced executor outlives all `executor_ref` instances that wrap it. @@ -115,9 +116,11 @@ class executor_ref public: /** Construct a default instance. - Constructs an empty `executor_ref`. Calling any executor - operations on a default-constructed instance results in - undefined behavior. + Constructs an empty `executor_ref`. `operator bool()` and + `operator==()` report the empty state; `context()`, + `on_work_started()`, `on_work_finished()`, `dispatch()`, + `post()`, and `target()` are undefined behavior until an + executor is assigned. */ executor_ref() = default; @@ -126,11 +129,18 @@ class executor_ref Copies the internal pointers, preserving identity. This enables the same-executor optimization when passing executor_ref through coroutine chains. + + @param other The reference to copy. */ - executor_ref(executor_ref const&) = default; + executor_ref(executor_ref const& other) = default; + + /** Copy assignment operator. - /** Copy assignment operator. */ - executor_ref& operator=(executor_ref const&) = default; + @param other The reference to copy. + + @return A reference to `*this`. + */ + executor_ref& operator=(executor_ref const& other) = default; /** Constructs from any executor type. diff --git a/include/boost/capy/ex/frame_alloc_mixin.hpp b/include/boost/capy/ex/frame_alloc_mixin.hpp index 11d54f692..1823d81ee 100644 --- a/include/boost/capy/ex/frame_alloc_mixin.hpp +++ b/include/boost/capy/ex/frame_alloc_mixin.hpp @@ -76,9 +76,10 @@ struct frame_alloc_mixin @return A pointer to storage for the frame. - @throws Propagates any exception thrown by the underlying - memory resource's `allocate` (for example `std::bad_alloc` - from `::operator new`). + @par Exception Safety + Propagates any exception thrown by the underlying memory + resource's `allocate`, for example `std::bad_alloc` from + `::operator new`. */ static void* operator new(std::size_t size) { @@ -104,6 +105,12 @@ struct frame_alloc_mixin Reads the allocator pointer stored at the end of the frame to ensure correct deallocation regardless of current TLS. Bypasses virtual dispatch for the recycling allocator. + + @param ptr The frame storage returned by `operator new`. + + @param size The size, in bytes, that was passed to `operator new`. + The allocator pointer is read from `ptr + size`, which is where + `operator new` wrote it, so this value must match. */ static void operator delete(void* ptr, std::size_t size) noexcept { diff --git a/include/boost/capy/ex/frame_allocator.hpp b/include/boost/capy/ex/frame_allocator.hpp index 8dbdb3b2e..b3837515c 100644 --- a/include/boost/capy/ex/frame_allocator.hpp +++ b/include/boost/capy/ex/frame_allocator.hpp @@ -49,9 +49,9 @@ current_frame_allocator_ref() noexcept These accessors exist to implement the allocator propagation portion of the @ref IoAwaitable protocol. - Launch functions (`run_async`, `run`) set the - thread-local value before invoking a child coroutine; - the child's `promise_type::operator new` reads it to + Launcher functions (`run_async`, `run`) set the + thread-local value before invoking a child coroutine. + The child's `promise_type::operator new` reads it to allocate the coroutine frame from the correct resource. The value is only valid during a narrow execution @@ -63,7 +63,7 @@ current_frame_allocator_ref() noexcept @ref IoAwaitable should call these functions. A return value of `nullptr` means "not specified" - - no allocator has been established for this chain. + no allocator is established for this chain. The awaitable is free to use whatever allocation strategy makes best sense (e.g. `std::pmr::new_delete_resource()`). @@ -75,7 +75,7 @@ current_frame_allocator_ref() noexcept so that downstream coroutines can use it. @return The thread-local memory_resource pointer, - or `nullptr` if none has been set. + or `nullptr` if none is set. @see set_current_frame_allocator, IoAwaitable */ @@ -88,9 +88,9 @@ get_current_frame_allocator() noexcept /** Set the current frame allocator for this thread. - Installs @p mr as the frame allocator that will be - read by the next coroutine's `promise_type::operator - new` on this thread. Only launch functions and + Installs @p mr as the frame allocator read by the + next coroutine's `promise_type::operator new` on + this thread. Only launcher functions and @ref IoAwaitable machinery should call this; see @ref get_current_frame_allocator for the full protocol description. diff --git a/include/boost/capy/ex/immediate.hpp b/include/boost/capy/ex/immediate.hpp index 7f315a96a..0402c90a2 100644 --- a/include/boost/capy/ex/immediate.hpp +++ b/include/boost/capy/ex/immediate.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) @@ -75,7 +76,11 @@ struct immediate /** The wrapped value. */ T value_; - /** Always returns true - this awaitable never suspends. */ + /** Always returns true - this awaitable never suspends. + + @return Always `true`, so the awaiting coroutine does not suspend + and `await_suspend` is never called. + */ constexpr bool await_ready() const noexcept { @@ -113,7 +118,11 @@ struct immediate return std::move(value_); } - /** Returns the wrapped value (const overload). */ + /** Returns the wrapped value (const overload). + + @return A reference to the stored value. Nothing is moved, so the + reference is valid only while the `immediate` is alive. + */ constexpr T const& await_resume() const noexcept { diff --git a/include/boost/capy/ex/io_awaitable_promise_base.hpp b/include/boost/capy/ex/io_awaitable_promise_base.hpp index 6ef44dc0a..abfafa738 100644 --- a/include/boost/capy/ex/io_awaitable_promise_base.hpp +++ b/include/boost/capy/ex/io_awaitable_promise_base.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,19 @@ class io_awaitable_promise_base mutable std::coroutine_handle<> cont_{std::noop_coroutine()}; public: + /** Destroy the promise, destroying an orphaned continuation. + + A continuation is still stored only when the coroutine never + reached `final_suspend`, because @ref continuation consumes the + stored handle. Destroying it here is what keeps an abandoned + coroutine from leaking the trampoline frame that was waiting on it. + + @par Preconditions + No parent coroutine is awaiting this one. A parent's `await_suspend` + installs its own handle as the continuation, so destroying such a + coroutine directly would destroy the parent from here as well. See + @ref task::handle and @ref quitter::handle for the contract. + */ ~io_awaitable_promise_base() { // Abnormal teardown: destroy an orphaned continuation, e.g. @@ -168,7 +182,7 @@ class io_awaitable_promise_base /** Return and consume the stored continuation handle. Resets the stored handle to `noop_coroutine()` so the - destructor will not double-destroy it. + destructor does not double-destroy it. @return The continuation for symmetric transfer. */ diff --git a/include/boost/capy/ex/io_env.hpp b/include/boost/capy/ex/io_env.hpp index fdf49ae01..b3d9d8b00 100644 --- a/include/boost/capy/ex/io_env.hpp +++ b/include/boost/capy/ex/io_env.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) @@ -20,7 +21,7 @@ namespace boost { namespace capy { -/** Execution environment for IoAwaitables. +/** Carries the executor, stop token, and frame allocator through a coroutine chain. This struct bundles the execution context passed through coroutine chains via the IoAwaitable protocol. It contains @@ -29,8 +30,8 @@ namespace capy { @par Lifetime - Launch functions (@ref run_async, @ref run) own the `io_env` and - guarantee it outlives all tasks and awaitables in the launched + Launcher functions (@ref run_async, @ref run) own the `io_env` and + guarantee it outlives all tasks and awaitables in the started chain. Awaitables receive `io_env const*` in `await_suspend` and should store it directly, never copy the pointed-to object. diff --git a/include/boost/capy/ex/recycling_memory_resource.hpp b/include/boost/capy/ex/recycling_memory_resource.hpp index ca1ef449d..54ac89fd8 100644 --- a/include/boost/capy/ex/recycling_memory_resource.hpp +++ b/include/boost/capy/ex/recycling_memory_resource.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) @@ -20,7 +21,7 @@ namespace boost { namespace capy { -/** Recycling memory resource with size-class buckets. +/** Recycles freed blocks through per-thread pools, with a shared pool for cross-thread reuse. This memory resource recycles memory blocks using power-of-two size classes for O(1) allocation lookup. It maintains a thread-local @@ -132,6 +133,12 @@ class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resour static void arm_thread_cleanup() noexcept; public: + /** Destroy the resource. + + No cached block is released here. Every pool is static, so an + instance holds no state of its own. The thread-local pool is + drained at thread exit, and the global pool at process exit. + */ ~recycling_memory_resource(); /** Allocate without virtual dispatch. @@ -139,6 +146,21 @@ class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resour Handles the fast path inline (thread-local bucket pop) and falls through to the slow path for global pool or heap allocation. + + A request larger than the largest size class (2048 bytes) + bypasses the pools and goes straight to `::operator new`. + + The second parameter is the requested alignment, and it is ignored. + Every block comes from `::operator new`, so blocks carry the + implementation's default new alignment and no more. + + @param bytes The number of bytes to allocate. + + @return A pointer to a block of at least `bytes` bytes. A pooled + block is rounded up to its size class, so it may be larger than + requested. + + @throws std::bad_alloc If the underlying `::operator new` fails. */ void* allocate_fast(std::size_t bytes, std::size_t) @@ -158,6 +180,19 @@ class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resour Handles the fast path inline (thread-local bucket push) and falls through to the slow path for global pool or heap deallocation. + + The block is cached in the pool of the thread that frees it, not + the thread that allocated it. + + The third parameter is the alignment the block was allocated with, + and it is ignored, as it is on allocation. + + @param p The block to return. It must have come from + @ref allocate_fast or @ref do_allocate on this resource. + + @param bytes The size the block was allocated with. The size class + is recomputed from it, so passing a different value puts the block + in the wrong bucket. */ void deallocate_fast(void* p, std::size_t bytes, std::size_t) @@ -185,12 +220,48 @@ class BOOST_CAPY_DECL recycling_memory_resource : public std::pmr::memory_resour } protected: + /** Allocate through the `std::pmr::memory_resource` interface. + + Forwards to @ref allocate_fast, so it has that function's contract. + Call `allocate_fast` directly to skip the virtual dispatch. + + @param bytes The number of bytes to allocate. + + @param alignment The requested alignment. It is ignored. + + @return A pointer to a block of at least `bytes` bytes. + + @throws std::bad_alloc If the underlying `::operator new` fails. + */ void* - do_allocate(std::size_t bytes, std::size_t) override; + do_allocate(std::size_t bytes, std::size_t alignment) override; + + /** Deallocate through the `std::pmr::memory_resource` interface. + + Forwards to @ref deallocate_fast, so it has that function's + contract. + + @param p The block to return, as obtained from this resource. + @param bytes The size the block was allocated with. + + @param alignment The alignment the block was allocated with. It is + ignored. + */ void - do_deallocate(void* p, std::size_t bytes, std::size_t) override; + do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override; + + /** Compare this resource with another for equality. + Equality is object identity: two distinct + `recycling_memory_resource` objects compare unequal, even though the + pools they draw from are static and therefore shared. + + @param other The resource to compare against. + + @return `true` if `other` is the same object as `*this`; otherwise + `false`. + */ bool do_is_equal(const memory_resource& other) const noexcept override { diff --git a/include/boost/capy/ex/run.hpp b/include/boost/capy/ex/run.hpp index 382a53d9f..9f21bd6b6 100644 --- a/include/boost/capy/ex/run.hpp +++ b/include/boost/capy/ex/run.hpp @@ -620,7 +620,7 @@ namespace boost::capy { @return A wrapper that accepts a task for execution. @see task - @see executor + @see Executor */ template [[nodiscard]] auto @@ -742,6 +742,14 @@ run(std::pmr::memory_resource* mr) return detail::run_wrapper{mr}; } +// MrDocs 0.8.0 reports `run: Documented parameter 'alloc' does not exist` and +// pins it to the run(Ex) overload near the top of this namespace: +// `detail::Allocator` is implementation-defined, so the constraint is erased +// and the two single-parameter `run` overloads collapse onto one page. The +// finding is grandfathered in doc/lint/baseline.json. Deleting the `@param +// alloc` below clears it and produces no replacement finding, because MrDocs +// has no model of this overload at all -- so it would silently drop real +// documentation. Keep the `@param`. /** Run a task with a custom standard allocator. The task inherits the caller's executor. The allocator is used diff --git a/include/boost/capy/ex/run_async.hpp b/include/boost/capy/ex/run_async.hpp index 3951e552a..3233ed064 100644 --- a/include/boost/capy/ex/run_async.hpp +++ b/include/boost/capy/ex/run_async.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) @@ -36,10 +37,10 @@ namespace detail { /** Match types usable as `run_async` completion handlers. - Excludes the types meaningful to the other `run_async` parameters, - so a stop token, memory resource pointer, or allocator argument - selects its dedicated overload by conversion instead of deducing - as an exact-match handler. + Excludes the types meaningful to the other `run_async` parameters. + A stop token, memory resource pointer, or allocator argument + therefore selects its dedicated overload by conversion. It does not + deduce as an exact-match handler. */ template concept RunAsyncHandler = @@ -322,7 +323,7 @@ make_trampoline(Ex, Handlers, Alloc) } // namespace detail -/** Wrapper returned by run_async that accepts a task for execution. +/** Installs the frame allocator, then starts the task on the executor when called once. This wrapper holds the run_async_trampoline coroutine, executor, stop token, and handlers. The run_async_trampoline is allocated when the wrapper is constructed @@ -339,14 +340,51 @@ make_trampoline(Ex, Handlers, Alloc) The wrapper itself should only be used from one thread. The handlers may be invoked from any thread where the executor schedules work. + @warning **Always construct the task as the direct argument of the + two-call expression `run_async(ex)(task)`.** The wrapper's constructor + installs the frame allocator in thread-local storage. The task's + `operator new` reads that thread-local state. Splitting the two calls + apart in any of the following ways allocates the task's coroutine + frame under the wrong allocator. Each does so silently, with no + compile error. + @li *Stored wrapper.* Storing the wrapper itself + (`auto w = run_async(ex);`) compiles fine. C++17 guaranteed copy + elision constructs `w` directly from the prvalue. The deleted + copy/move constructors are never considered. What the rvalue + ref-qualifier on `operator()` rejects is calling through that + stored lvalue: `w(my_task())` does not compile, and + `std::move(w)(my_task())` is required instead. The silent + variant is storing the *task* + (`auto t = my_task(); run_async(ex)(std::move(t));`): `t`'s frame + is allocated before `run_async(ex)` ever runs. + @li *Preconstructed task.* Passing an already-constructed task object + has the same effect as the stored-wrapper case. So does passing a + moved-from local, or a task returned from an earlier statement. + The frame exists before the allocator is installed. + @li *Wrapper function.* Forwarding the task through a helper that + itself performs the two-call pattern constructs the task as an + argument to the helper. It is therefore constructed before the + helper's body runs, and so before `run_async` runs. An example is + `submit(ex, my_task())`, where `submit` calls + `run_async(ex)(std::forward(t))` internally. + + See the Frame Allocators guide + (`doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc`) for the full + C++17-evaluation-order rationale behind this constraint. + @par Example @code - // Correct usage - wrapper is temporary + // Correct usage - wrapper is temporary, task is the direct argument run_async(ex)(my_task()); - // Compile error - cannot call operator() on lvalue + // Compiles - copy elision constructs w directly from the prvalue auto w = run_async(ex); - w(my_task()); // Error: operator() requires rvalue + w(my_task()); // Compile error: operator() requires rvalue + std::move(w)(my_task()); // Compiles: w is now an rvalue + + // Compiles, but WRONG - task frame allocated before run_async runs + auto t = my_task(); + run_async(ex)(std::move(t)); @endcode @see run_async @@ -361,10 +399,10 @@ class [[nodiscard]] run_async_wrapper public: /** Construct the wrapper and install the frame allocator. - Builds the trampoline, saves the current thread-local frame - allocator, and installs the trampoline's resource as the new - thread-local allocator so that the task frame (evaluated as the - argument to @ref operator()) is allocated from it. + Builds the trampoline and saves the current thread-local frame + allocator. Then installs the trampoline's resource as the new + thread-local allocator. The task frame, evaluated as the argument + to @ref operator(), is therefore allocated from that resource. @param ex The executor on which the task runs. @param st The stop token for cooperative cancellation. @@ -398,8 +436,8 @@ class [[nodiscard]] run_async_wrapper /** Restore the previously installed frame allocator. Resets the thread-local frame allocator to the value saved at - construction, so a stale pointer to the trampoline's resource does - not outlive the execution context that owns it. + construction. A stale pointer to the trampoline's resource + therefore does not outlive the execution context that owns it. */ ~run_async_wrapper() { @@ -407,14 +445,38 @@ class [[nodiscard]] run_async_wrapper } // Non-copyable, non-movable (must be used immediately) - run_async_wrapper(run_async_wrapper const&) = delete; - run_async_wrapper(run_async_wrapper&&) = delete; - run_async_wrapper& operator=(run_async_wrapper const&) = delete; - run_async_wrapper& operator=(run_async_wrapper&&) = delete; - /** Launch the task for execution. + /** Copy construction is disabled; the wrapper must be used immediately. + + @param other The wrapper that would be copied. + */ + run_async_wrapper(run_async_wrapper const& other) = delete; + + /** Move construction is disabled; the wrapper must be used immediately. + + @param other The wrapper that would be moved from. + */ + run_async_wrapper(run_async_wrapper&& other) = delete; + + /** Copy assignment is disabled; the wrapper must be used immediately. + + @param other The wrapper that would be assigned from. + + @return A reference to `*this`. + */ + run_async_wrapper& operator=(run_async_wrapper const& other) = delete; + + /** Move assignment is disabled; the wrapper must be used immediately. - This operator accepts a task and launches it on the executor. + @param other The wrapper that would be moved from. + + @return A reference to `*this`. + */ + run_async_wrapper& operator=(run_async_wrapper&& other) = delete; + + /** Start the task for execution. + + This operator accepts a task and starts it on the executor. The rvalue ref-qualifier ensures the wrapper is consumed, enforcing correct LIFO destruction order. @@ -426,7 +488,7 @@ class [[nodiscard]] run_async_wrapper @tparam Task The IoRunnable type. @param t The task to execute. Ownership is transferred to the - run_async_trampoline which will destroy it after completion. + run_async_trampoline which destroys it after completion. */ template void operator()(Task t) && @@ -457,7 +519,7 @@ class [[nodiscard]] run_async_wrapper // Executor only (uses default recycling allocator) -/** Asynchronously launch a lazy task on the given executor. +/** Bind an executor to produce a launcher. Invoke the launcher with a task to start it. Use this to start execution of a `task` that was created lazily. The returned wrapper must be immediately invoked with the task; @@ -465,13 +527,15 @@ class [[nodiscard]] run_async_wrapper Uses the default recycling frame allocator for coroutine frames. With no handlers, the result is discarded. An unhandled exception - thrown by the task calls `std::terminate`; pass an error handler to - receive it as an `exception_ptr`, or `co_await` the work inside a - coroutine if you want to catch it. + thrown by the task calls `std::terminate`. To catch it instead, pass + an error handler that receives it as an `exception_ptr`, or `co_await` + the work inside a coroutine. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. @par Thread Safety - The wrapper and handlers may be called from any thread where the - executor schedules work. + The wrapper itself should only be used from one thread. @par Example @code @@ -483,7 +547,8 @@ class [[nodiscard]] run_async_wrapper @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -497,15 +562,18 @@ run_async(Ex ex) mr); } -/** Asynchronously launch a lazy task with a result handler. +/** Bind an executor and a result handler to produce a launcher. Invoke the launcher with a task to start it. The handler `h1` is called with the task's result on success. If `h1` is also invocable with `std::exception_ptr`, it handles exceptions too. Otherwise, an unhandled exception calls `std::terminate`. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + @par Thread Safety - The handler may be called from any thread where the executor - schedules work. + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. @par Example @code @@ -527,7 +595,8 @@ run_async(Ex ex) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template requires detail::RunAsyncHandler

    @@ -542,14 +611,17 @@ run_async(Ex ex, H1 h1) mr); } -/** Asynchronously launch a lazy task with separate result and error handlers. +/** Bind an executor and separate result and error handlers to produce a launcher. Invoke the launcher with a task to start it. The handler `h1` is called with the task's result on success. The handler `h2` is called with the exception_ptr on failure. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + @par Thread Safety - The handlers may be called from any thread where the executor - schedules work. + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. @par Example @code @@ -571,7 +643,8 @@ run_async(Ex ex, H1 h1) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template requires (detail::RunAsyncHandler

    && detail::RunAsyncHandler

    ) @@ -588,15 +661,17 @@ run_async(Ex ex, H1 h1, H2 h2) // Ex + stop_token -/** Asynchronously launch a lazy task with stop token support. +/** Bind an executor and a stop token to produce a launcher. Invoke the launcher with a task to start it. The stop token is propagated to the task, enabling cooperative cancellation. With no handlers, the result is discarded and an unhandled exception calls `std::terminate`. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + @par Thread Safety - The wrapper may be called from any thread where the executor - schedules work. + The wrapper itself should only be used from one thread. @par Example @code @@ -611,7 +686,8 @@ run_async(Ex ex, H1 h1, H2 h2) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -625,12 +701,19 @@ run_async(Ex ex, std::stop_token st) mr); } -/** Asynchronously launch a lazy task with stop token and result handler. +/** Bind an executor, a stop token, and a result handler to produce a launcher. Invoke the launcher with a task to start it. The stop token is propagated to the task for cooperative cancellation. The handler `h1` is called with the result on success, and optionally with exception_ptr if it accepts that type. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. + @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @param h1 The handler to invoke with the result (and optionally exception). @@ -638,7 +721,8 @@ run_async(Ex ex, std::stop_token st) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template requires detail::RunAsyncHandler

    @@ -653,11 +737,18 @@ run_async(Ex ex, std::stop_token st, H1 h1) mr); } -/** Asynchronously launch a lazy task with stop token and separate handlers. +/** Bind an executor, a stop token, and separate result and error handlers to produce a launcher. Invoke the launcher with a task to start it. The stop token is propagated to the task for cooperative cancellation. The handler `h1` is called on success, `h2` on failure. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. + @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @param h1 The handler to invoke with the result on success. @@ -666,7 +757,8 @@ run_async(Ex ex, std::stop_token st, H1 h1) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template requires (detail::RunAsyncHandler

    && detail::RunAsyncHandler

    ) @@ -683,10 +775,17 @@ run_async(Ex ex, std::stop_token st, H1 h1, H2 h2) // Ex + memory_resource* -/** Asynchronously launch a lazy task with custom memory resource. +/** Bind an executor and a memory resource to produce a launcher. Invoke the launcher with a task to start it. + + The memory resource is used for coroutine frame allocation. - The memory resource is used for coroutine frame allocation. The caller - is responsible for ensuring the memory resource outlives all tasks. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. + + @pre `mr` outlives every task started through the returned wrapper. @param ex The executor to execute the task on. @param mr The memory resource for frame allocation. @@ -694,7 +793,8 @@ run_async(Ex ex, std::stop_token st, H1 h1, H2 h2) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -707,7 +807,16 @@ run_async(Ex ex, std::pmr::memory_resource* mr) mr); } -/** Asynchronously launch a lazy task with memory resource and handler. +/** Bind an executor, a memory resource, and a result handler to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. + + @pre `mr` outlives every task started through the returned wrapper. @param ex The executor to execute the task on. @param mr The memory resource for frame allocation. @@ -716,7 +825,8 @@ run_async(Ex ex, std::pmr::memory_resource* mr) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -729,7 +839,16 @@ run_async(Ex ex, std::pmr::memory_resource* mr, H1 h1) mr); } -/** Asynchronously launch a lazy task with memory resource and handlers. +/** Bind an executor, a memory resource, and separate result and error handlers to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. + + @pre `mr` outlives every task started through the returned wrapper. @param ex The executor to execute the task on. @param mr The memory resource for frame allocation. @@ -739,7 +858,8 @@ run_async(Ex ex, std::pmr::memory_resource* mr, H1 h1) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -754,7 +874,15 @@ run_async(Ex ex, std::pmr::memory_resource* mr, H1 h1, H2 h2) // Ex + stop_token + memory_resource* -/** Asynchronously launch a lazy task with stop token and memory resource. +/** Bind an executor, a stop token, and a memory resource to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. + + @pre `mr` outlives every task started through the returned wrapper. @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @@ -763,7 +891,8 @@ run_async(Ex ex, std::pmr::memory_resource* mr, H1 h1, H2 h2) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -776,7 +905,16 @@ run_async(Ex ex, std::stop_token st, std::pmr::memory_resource* mr) mr); } -/** Asynchronously launch a lazy task with stop token, memory resource, and handler. +/** Bind an executor, a stop token, a memory resource, and a result handler to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. + + @pre `mr` outlives every task started through the returned wrapper. @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @@ -786,7 +924,8 @@ run_async(Ex ex, std::stop_token st, std::pmr::memory_resource* mr) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -799,7 +938,16 @@ run_async(Ex ex, std::stop_token st, std::pmr::memory_resource* mr, H1 h1) mr); } -/** Asynchronously launch a lazy task with stop token, memory resource, and handlers. +/** Bind an executor, a stop token, a memory resource, and separate result and error handlers to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. + + @pre `mr` outlives every task started through the returned wrapper. @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @@ -810,7 +958,8 @@ run_async(Ex ex, std::stop_token st, std::pmr::memory_resource* mr, H1 h1) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -825,18 +974,25 @@ run_async(Ex ex, std::stop_token st, std::pmr::memory_resource* mr, H1 h1, H2 h2 // Ex + standard Allocator (value type) -/** Asynchronously launch a lazy task with custom allocator. +/** Bind an executor and an allocator to produce a launcher. Invoke the launcher with a task to start it. The allocator is wrapped in a frame_memory_resource and stored in the run_async_trampoline, ensuring it outlives all coroutine frames. + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. + @param ex The executor to execute the task on. @param alloc The allocator for frame allocation (copied and stored). @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -849,7 +1005,14 @@ run_async(Ex ex, Alloc alloc) std::move(alloc)); } -/** Asynchronously launch a lazy task with allocator and handler. +/** Bind an executor, an allocator, and a result handler to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. @param ex The executor to execute the task on. @param alloc The allocator for frame allocation (copied and stored). @@ -858,7 +1021,8 @@ run_async(Ex ex, Alloc alloc) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -871,7 +1035,14 @@ run_async(Ex ex, Alloc alloc, H1 h1) std::move(alloc)); } -/** Asynchronously launch a lazy task with allocator and handlers. +/** Bind an executor, an allocator, and separate result and error handlers to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. @param ex The executor to execute the task on. @param alloc The allocator for frame allocation (copied and stored). @@ -881,7 +1052,8 @@ run_async(Ex ex, Alloc alloc, H1 h1) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -896,7 +1068,13 @@ run_async(Ex ex, Alloc alloc, H1 h1, H2 h2) // Ex + stop_token + standard Allocator -/** Asynchronously launch a lazy task with stop token and allocator. +/** Bind an executor, a stop token, and an allocator to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @@ -905,7 +1083,8 @@ run_async(Ex ex, Alloc alloc, H1 h1, H2 h2) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -918,7 +1097,14 @@ run_async(Ex ex, std::stop_token st, Alloc alloc) std::move(alloc)); } -/** Asynchronously launch a lazy task with stop token, allocator, and handler. +/** Bind an executor, a stop token, an allocator, and a result handler to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @@ -928,7 +1114,8 @@ run_async(Ex ex, std::stop_token st, Alloc alloc) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto @@ -941,7 +1128,14 @@ run_async(Ex ex, std::stop_token st, Alloc alloc, H1 h1) std::move(alloc)); } -/** Asynchronously launch a lazy task with stop token, allocator, and handlers. +/** Bind an executor, a stop token, an allocator, and separate result and error handlers to produce a launcher. Invoke the launcher with a task to start it. + + Construct the task as the direct argument of the two-call expression + `run_async(ex)(task)`. + + @par Thread Safety + The wrapper itself should only be used from one thread. The handlers + may be invoked from any thread where the executor schedules work. @param ex The executor to execute the task on. @param st The stop token for cooperative cancellation. @@ -952,7 +1146,8 @@ run_async(Ex ex, std::stop_token st, Alloc alloc, H1 h1) @return A wrapper that accepts a `task` for immediate execution. @see task - @see executor + @see Executor + @see run_async_wrapper */ template [[nodiscard]] auto diff --git a/include/boost/capy/ex/strand.hpp b/include/boost/capy/ex/strand.hpp index a84a40857..44523381d 100644 --- a/include/boost/capy/ex/strand.hpp +++ b/include/boost/capy/ex/strand.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) @@ -38,7 +39,7 @@ namespace capy { @par Implementation Each strand allocates a private serialization state. Strands constructed from the same execution context share a small pool - of mutexes (193 entries) selected by hash; mutex sharing causes + of mutexes (193 entries) selected by hash. Mutex sharing causes only brief contention on the push/pop critical section, never cross-strand state sharing. Construction cost: one `std::make_shared` per strand. @@ -56,11 +57,11 @@ namespace capy { outlive every post() and dispatch() call; posting or dispatching concurrently with, or after, the context's destruction is undefined behavior. To guarantee this, submit work through @ref run_async or - @ref run — whose operations are work-tracked, so the context's - `join()` waits for them — and call `join()` on the context before + @ref run. Their operations are work-tracked, so the context's + `join()` waits for them. Call `join()` on the context before destroying it, rather than posting to a strand from an external thread the context does not track. Destroying the strand handle - itself is always safe, including after the context has been + itself is always safe, including after the context is destroyed. @par Thread Safety @@ -95,7 +96,7 @@ class strand friend struct strand_test; public: - /** The type of the underlying executor. + /** Names the executor type this `strand` wraps. */ using inner_executor_type = Ex; @@ -104,8 +105,8 @@ class strand Allocates a fresh strand implementation from the service associated with the executor's context. - @param ex The inner executor to wrap. Coroutines will - ultimately be dispatched through this executor. + @param ex The inner executor to wrap. Coroutines are + ultimately dispatched through this executor. @note This constructor is disabled if the argument is a strand type, to prevent strand-of-strand wrapping. @@ -127,27 +128,42 @@ class strand Creates a strand that shares serialization state with the original. Coroutines dispatched through either strand - will be serialized with respect to each other. + are serialized with respect to each other. + + @param other The strand to copy. */ - strand(strand const&) = default; + strand(strand const& other) = default; /** Construct by moving. + @param other The strand to move from. + @note A moved-from strand is only safe to destroy or reassign. */ - strand(strand&&) = default; + strand(strand&& other) = default; /** Assign by copying. + + Shares serialization state with `other`, as the copy + constructor does. + + @param other The strand to copy. + + @return A reference to `*this`. */ - strand& operator=(strand const&) = default; + strand& operator=(strand const& other) = default; /** Assign by moving. + @param other The strand to move from. + + @return A reference to `*this`. + @note A moved-from strand is only safe to destroy or reassign. */ - strand& operator=(strand&&) = default; + strand& operator=(strand&& other) = default; /** Return the underlying executor. @@ -172,8 +188,9 @@ class strand /** Notify that work has started. - Delegates to the inner executor's `on_work_started()`. - This is a no-op for most executor types. + Delegates to the inner executor's `on_work_started()`. For a + `thread_pool` inner executor, this increments the count that + `join()` blocks on. */ void on_work_started() const noexcept @@ -183,8 +200,9 @@ class strand /** Notify that work has finished. - Delegates to the inner executor's `on_work_finished()`. - This is a no-op for most executor types. + Delegates to the inner executor's `on_work_finished()`. For a + `thread_pool` inner executor, this decrements the count that + `join()` blocks on. */ void on_work_finished() const noexcept @@ -272,7 +290,10 @@ class strand } }; -// Deduction guide +/** Deduce the executor type from the constructor argument. + + @tparam Ex The wrapped executor type. +*/ template strand(Ex) -> strand; diff --git a/include/boost/capy/ex/this_coro.hpp b/include/boost/capy/ex/this_coro.hpp index 897e27c47..31bb306ad 100644 --- a/include/boost/capy/ex/this_coro.hpp +++ b/include/boost/capy/ex/this_coro.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) @@ -37,7 +38,7 @@ namespace capy { */ namespace this_coro { -/** Tag type for coroutine environment retrieval. +/** Selects `co_await this_coro::environment` to fetch the running coroutine's environment. This tag is intercepted by a promise type's `await_transform` to yield the coroutine's current execution environment. The tag itself @@ -48,7 +49,7 @@ namespace this_coro { */ struct environment_tag {}; -/** Tag type for coroutine executor retrieval. +/** Selects `co_await this_coro::executor` to fetch the running coroutine's executor. This tag is intercepted by a promise type's `await_transform` to yield the coroutine's current executor. The tag itself carries no @@ -59,7 +60,7 @@ struct environment_tag {}; */ struct executor_tag {}; -/** Tag type for coroutine stop token retrieval. +/** Selects `co_await this_coro::stop_token` to fetch the running coroutine's stop token. This tag is intercepted by a promise type's `await_transform` to yield the coroutine's current stop token. The tag itself carries @@ -70,7 +71,7 @@ struct executor_tag {}; */ struct stop_token_tag {}; -/** Tag type for coroutine frame allocator retrieval. +/** Selects `co_await this_coro::frame_allocator` to fetch the running coroutine's frame allocator. This tag is intercepted by a promise type's `await_transform` to yield the coroutine's current frame allocator. The tag itself carries @@ -101,7 +102,7 @@ struct frame_allocator_tag {}; @par Preconditions An `io_env` must have been installed for this coroutine before the tag - is awaited. Launching the coroutine via @ref run or `run_async` installs + is awaited. Starting the coroutine via @ref run or `run_async` installs one; awaiting the tag without an installed environment is undefined behavior (an assertion fires in debug builds). @@ -136,7 +137,7 @@ inline constexpr environment_tag environment{}; environment is undefined behavior (an assertion fires in debug builds). @par Behavior - @li Returns the installed environment's `executor` field. If the launched + @li Returns the installed environment's `executor` field. If the started chain installed an `io_env` whose `executor` was left default, the result is a default-constructed `executor_ref` (where `operator bool()` returns `false`). @@ -170,7 +171,7 @@ inline constexpr executor_tag executor{}; environment is undefined behavior (an assertion fires in debug builds). @par Behavior - @li Returns the installed environment's `stop_token` field. If the launched + @li Returns the installed environment's `stop_token` field. If the started chain installed an `io_env` whose `stop_token` was left default, the result is a default-constructed `std::stop_token` (where `stop_possible()` returns `false`). diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp index ef8586d3b..866791ae6 100644 --- a/include/boost/capy/ex/thread_pool.hpp +++ b/include/boost/capy/ex/thread_pool.hpp @@ -21,7 +21,7 @@ namespace boost { namespace capy { -/** A pool of threads for executing work concurrently. +/** Distributes posted work across a fixed group of worker threads via a shared queue. Use this when you need to run coroutines on multiple threads without the overhead of creating and destroying threads for @@ -30,13 +30,14 @@ namespace capy { @par Thread Safety Distinct objects: Safe. - Shared objects: Unsafe. + Shared objects: Safe for @ref get_executor, @ref join, and + @ref stop. Unsafe for construction and destruction. @par Example @code thread_pool pool(4); // 4 worker threads auto ex = pool.get_executor(); - run_async(ex)(some_task()); // launch work; tracked so join() waits for it + run_async(ex)(some_task()); // start work; tracked so join() waits for it pool.join(); // wait for outstanding work to complete // pool destructor stops the pool, discarding any pending work @endcode @@ -44,7 +45,7 @@ namespace capy { @note `join()` waits only for work that holds outstanding-work counting, which `run_async` (and `make_work_guard`) provide. A bare `executor_type::post()` does not register outstanding work, so - `join()` will not wait for it. + `join()` does not wait for it. */ class BOOST_CAPY_DECL thread_pool @@ -61,10 +62,9 @@ class BOOST_CAPY_DECL Signals all worker threads to stop, waits for them to finish, and destroys any pending work items. - @par Preconditions - No thread outside this pool may post or dispatch work to it + @pre No thread outside this pool may post or dispatch work to it (or to a strand built on it) concurrently with, or after, - destruction; doing so is undefined behavior. Submit such work + destruction. Doing so is undefined behavior. Submit such work through @ref run_async or @ref run and call @ref join before the pool is destroyed, so it has completed first. */ @@ -72,7 +72,8 @@ class BOOST_CAPY_DECL /** Construct a thread pool. - Creates a pool with the specified number of worker threads. + Records the requested worker count; no threads are created + yet. Threads start lazily on the executor's first `post()`. If `num_threads` is zero, the number of threads is set to the hardware concurrency, or one if that cannot be determined. @@ -89,8 +90,19 @@ class BOOST_CAPY_DECL std::size_t num_threads = 0, std::string_view thread_name_prefix = "capy-pool-"); - thread_pool(thread_pool const&) = delete; - thread_pool& operator=(thread_pool const&) = delete; + /** Copy construction is disabled; a pool owns its worker threads. + + @param other The pool that would be copied. + */ + thread_pool(thread_pool const& other) = delete; + + /** Copy assignment is disabled; a pool owns its worker threads. + + @param other The pool that would be assigned from. + + @return A reference to `*this`. + */ + thread_pool& operator=(thread_pool const& other) = delete; /** Wait for all outstanding work to complete. @@ -109,8 +121,7 @@ class BOOST_CAPY_DECL This function is idempotent. The first call performs the join; subsequent calls return immediately. - @par Preconditions - Must not be called from a thread in this pool (undefined + @pre Must not be called from a thread in this pool (undefined behavior). @par Postconditions @@ -133,6 +144,10 @@ class BOOST_CAPY_DECL `stop()` causes it to stop waiting for outstanding work. The `join()` call still waits for worker threads to finish their current item and exit before returning. + + @par Thread Safety + May be called concurrently from any thread, including a + thread in this pool. */ void stop() noexcept; @@ -175,7 +190,11 @@ class thread_pool::executor_type */ executor_type() = default; - /// Return the underlying thread pool. + /** Return the underlying thread pool. + + @return A reference to the associated pool. The behavior is + undefined if the executor is not associated with a pool. + */ thread_pool& context() const noexcept { @@ -196,7 +215,7 @@ class thread_pool::executor_type /** Notify that work has finished. Decrements the outstanding work count. When the count - reaches zero after @ref thread_pool::join has been called, + reaches zero after @ref thread_pool::join is called, the pool's worker threads are signaled to stop. @pre A preceding call to @ref on_work_started was made. @@ -228,7 +247,7 @@ class thread_pool::executor_type /** Post a continuation to the thread pool. - The continuation will be resumed on one of the pool's + The continuation is resumed on one of the pool's worker threads. The continuation must remain at a stable address until it is dequeued and resumed. @@ -238,7 +257,12 @@ class thread_pool::executor_type void post(continuation& c) const; - /// Return true if two executors refer to the same thread pool. + /** Return true if two executors refer to the same thread pool. + + @param other The executor to compare against. + + @return `true` if both executors refer to the same pool. + */ bool operator==(executor_type const& other) const noexcept { diff --git a/include/boost/capy/ex/work_guard.hpp b/include/boost/capy/ex/work_guard.hpp index 5a8b50939..decd5a77f 100644 --- a/include/boost/capy/ex/work_guard.hpp +++ b/include/boost/capy/ex/work_guard.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) @@ -27,7 +28,7 @@ namespace capy { `on_work_finished()` on destruction, ensuring proper work tracking. The guard is useful when you need to keep an execution context - running while waiting for external events or when work will be + running while waiting for external events or when work is posted later. @par RAII Semantics @@ -45,19 +46,17 @@ namespace capy { @par Example @code - io_context ctx; + thread_pool pool(1); - // Keep context running while we set things up - auto guard = make_work_guard(ctx); + // Keep the pool from completing while we set things up + auto guard = make_work_guard(pool.get_executor()); - std::thread t([&ctx]{ ctx.run(); }); + // ... post work to pool ... - // ... post work to ctx ... - - // Allow context to complete when work is done + // Allow the pool to complete when work is done guard.reset(); - t.join(); + pool.join(); @endcode @note The executor is returned by reference, allowing callers to @@ -76,7 +75,7 @@ class work_guard bool owns_; public: - /** The underlying executor type. */ + /** Names the executor type this `work_guard` guards. */ using executor_type = Ex; /** Construct a work guard. @@ -158,7 +157,17 @@ class work_guard ex_.on_work_finished(); } - work_guard& operator=(work_guard const&) = delete; + /** Copy assignment is disabled. + + A guard takes its work reference at construction and releases it at + destruction or through @ref reset. No operation rebinds an existing + guard to a different executor. + + @param other The work guard that would be assigned from. + + @return A reference to `*this`. + */ + work_guard& operator=(work_guard const& other) = delete; /** Return the underlying executor by reference. @@ -181,7 +190,7 @@ class work_guard @par Exception Safety No-throw guarantee. - @return `true` if this guard will call `on_work_finished()` + @return `true` if this guard calls `on_work_finished()` on destruction, `false` otherwise. */ bool diff --git a/include/boost/capy/io/any_read_stream.hpp b/include/boost/capy/io/any_read_stream.hpp index 83f8a6e92..fa4b7dd44 100644 --- a/include/boost/capy/io/any_read_stream.hpp +++ b/include/boost/capy/io/any_read_stream.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) @@ -32,7 +33,7 @@ namespace boost { namespace capy { -/** Type-erased wrapper for any ReadStream. +/** Dispatches `read_some` through a type-erased vtable, using preallocated awaitable storage. This class provides type erasure for any type satisfying the @ref ReadStream concept, enabling runtime polymorphism for @@ -63,14 +64,15 @@ namespace capy { @par Example @code // Owning - takes ownership of the stream - any_read_stream stream(socket{ioc}); + any_read_stream owning_stream(socket{ioc}); // Reference - wraps without ownership socket sock(ioc); - any_read_stream stream(&sock); + any_read_stream ref_stream(&sock); - mutable_buffer buf(data, size); - auto [ec, n] = co_await stream.read_some(buf); + char data[1024]; + mutable_buffer buf(data, sizeof(data)); + auto [ec, n] = co_await owning_stream.read_some(buf); @endcode @see any_write_stream, any_stream, ReadStream @@ -99,17 +101,29 @@ class any_read_stream /** Construct a default instance. - Constructs an empty wrapper. Operations on a default-constructed - wrapper result in undefined behavior. + Constructs an empty wrapper. @ref has_value and `operator bool` + report the empty state; calling @ref read_some before the + wrapper holds a stream is undefined behavior. */ any_read_stream() = default; /** Non-copyable. The awaitable cache is per-instance and cannot be shared. + + @param other The wrapper that would be copied. + */ + any_read_stream(any_read_stream const& other) = delete; + + /** Copy assignment is disabled. + + The awaitable cache is per-instance and cannot be shared. + + @param other The wrapper that would be assigned from. + + @return A reference to `*this`. */ - any_read_stream(any_read_stream const&) = delete; - any_read_stream& operator=(any_read_stream const&) = delete; + any_read_stream& operator=(any_read_stream const& other) = delete; /** Construct by moving. @@ -142,7 +156,7 @@ class any_read_stream /** Construct by taking ownership of a ReadStream. Allocates storage and moves the stream into this wrapper. - The wrapper owns the stream and will destroy it. + The wrapper owns the stream and destroys it. @param s The stream to take ownership of. */ @@ -185,7 +199,7 @@ class any_read_stream /** Initiate an asynchronous read operation. Reads data into the provided buffer sequence. The operation - completes when at least one byte has been read, or an error + completes when at least one byte is read, or an error occurs. @param buffers The buffer sequence to read into. Passed by @@ -205,8 +219,11 @@ class any_read_stream @par Preconditions The wrapper must contain a valid stream (`has_value() == true`). - The caller must not call this function again after a prior - call returned an error (including EOF). + + @par After an Error + A subsequent call is permitted. The wrapper forwards directly + to the underlying stream, imposing no stricter rule than + @ref ReadStream. */ template auto diff --git a/include/boost/capy/io/any_stream.hpp b/include/boost/capy/io/any_stream.hpp index 9d86e2d6d..b3ff43183 100644 --- a/include/boost/capy/io/any_stream.hpp +++ b/include/boost/capy/io/any_stream.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) @@ -21,7 +22,7 @@ namespace boost { namespace capy { -/** Type-erased wrapper for bidirectional streams. +/** Dispatches `read_some` and `write_some` through independent type-erased vtables. This class provides type erasure for any type satisfying both the @ref ReadStream and @ref WriteStream concepts, enabling @@ -52,26 +53,29 @@ namespace capy { @par Example @code + void reader(any_read_stream&); + void writer(any_write_stream&); + // Owning - takes ownership of the stream - any_stream stream(socket{ioc}); + any_stream owning_stream(socket{ioc}); // Reference - wraps without ownership socket sock(ioc); - any_stream stream(&sock); + any_stream ref_stream(&sock); - // Use read_some from any_read_stream base - mutable_buffer rbuf(rdata, rsize); - auto [ec1, n1] = co_await stream.read_some(std::span(&rbuf, 1)); + // Use read_some from the any_read_stream base + char rdata[1024]; + mutable_buffer rbuf(rdata, sizeof(rdata)); + auto [ec1, n1] = co_await owning_stream.read_some(std::span(&rbuf, 1)); - // Use write_some from any_write_stream base - const_buffer wbuf(wdata, wsize); - auto [ec2, n2] = co_await stream.write_some(std::span(&wbuf, 1)); + // Use write_some from the any_write_stream base + char wdata[] = "hello"; + const_buffer wbuf(wdata, sizeof(wdata)); + auto [ec2, n2] = co_await owning_stream.write_some(std::span(&wbuf, 1)); // Pass to functions expecting one capability - void reader(any_read_stream&); - void writer(any_write_stream&); - reader(stream); // Implicit upcast - writer(stream); // Implicit upcast + reader(owning_stream); // Implicit upcast + writer(owning_stream); // Implicit upcast @endcode @see any_read_stream, any_write_stream, ReadStream, WriteStream @@ -101,17 +105,29 @@ class any_stream /** Construct a default instance. - Constructs an empty wrapper. Operations on a default-constructed - wrapper result in undefined behavior. + Constructs an empty wrapper. @ref has_value and `operator bool` + report the empty state; calling `read_some` or `write_some` + before the wrapper holds a stream is undefined behavior. */ any_stream() = default; /** Non-copyable. The awaitable caches are per-instance and cannot be shared. + + @param other The wrapper that would be copied. + */ + any_stream(any_stream const& other) = delete; + + /** Copy assignment is disabled. + + The awaitable caches are per-instance and cannot be shared. + + @param other The wrapper that would be assigned from. + + @return A reference to `*this`. */ - any_stream(any_stream const&) = delete; - any_stream& operator=(any_stream const&) = delete; + any_stream& operator=(any_stream const& other) = delete; /** Construct by moving. @@ -160,7 +176,7 @@ class any_stream /** Construct by taking ownership of a bidirectional stream. Allocates storage and moves the stream into this wrapper. - The wrapper owns the stream and will destroy it. + The wrapper owns the stream and destroys it. @param s The stream to take ownership of. Must satisfy both ReadStream and WriteStream concepts. diff --git a/include/boost/capy/io/any_write_stream.hpp b/include/boost/capy/io/any_write_stream.hpp index a300c2d66..ca12bfef5 100644 --- a/include/boost/capy/io/any_write_stream.hpp +++ b/include/boost/capy/io/any_write_stream.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) @@ -33,7 +34,7 @@ namespace boost { namespace capy { -/** Type-erased wrapper for any WriteStream. +/** Dispatches `write_some` through a type-erased vtable, using preallocated awaitable storage. This class provides type erasure for any type satisfying the @ref WriteStream concept, enabling runtime polymorphism for @@ -64,14 +65,15 @@ namespace capy { @par Example @code // Owning - takes ownership of the stream - any_write_stream stream(socket{ioc}); + any_write_stream owning_stream(socket{ioc}); // Reference - wraps without ownership socket sock(ioc); - any_write_stream stream(&sock); + any_write_stream ref_stream(&sock); - const_buffer buf(data, size); - auto [ec, n] = co_await stream.write_some(std::span(&buf, 1)); + char data[] = "hello"; + const_buffer buf(data, sizeof(data)); + auto [ec, n] = co_await owning_stream.write_some(std::span(&buf, 1)); @endcode @see any_read_stream, any_stream, WriteStream @@ -100,17 +102,29 @@ class any_write_stream /** Construct a default instance. - Constructs an empty wrapper. Operations on a default-constructed - wrapper result in undefined behavior. + Constructs an empty wrapper. @ref has_value and `operator bool` + report the empty state; calling @ref write_some before the + wrapper holds a stream is undefined behavior. */ any_write_stream() = default; /** Non-copyable. The awaitable cache is per-instance and cannot be shared. + + @param other The wrapper that would be copied. + */ + any_write_stream(any_write_stream const& other) = delete; + + /** Copy assignment is disabled. + + The awaitable cache is per-instance and cannot be shared. + + @param other The wrapper that would be assigned from. + + @return A reference to `*this`. */ - any_write_stream(any_write_stream const&) = delete; - any_write_stream& operator=(any_write_stream const&) = delete; + any_write_stream& operator=(any_write_stream const& other) = delete; /** Construct by moving. @@ -143,7 +157,7 @@ class any_write_stream /** Construct by taking ownership of a WriteStream. Allocates storage and moves the stream into this wrapper. - The wrapper owns the stream and will destroy it. + The wrapper owns the stream and destroys it. @param s The stream to take ownership of. */ @@ -186,7 +200,7 @@ class any_write_stream /** Initiate an asynchronous write operation. Writes data from the provided buffer sequence. The operation - completes when at least one byte has been written, or an error + completes when at least one byte is written, or an error occurs. @param buffers The buffer sequence containing data to write. @@ -208,6 +222,11 @@ class any_write_stream @par Preconditions The wrapper must contain a valid stream (`has_value() == true`). + + @par After an Error + A subsequent call is permitted. The wrapper forwards directly + to the underlying stream, imposing no stricter rule than + @ref WriteStream. */ template auto diff --git a/include/boost/capy/io/write_now.hpp b/include/boost/capy/io/write_now.hpp index 1c1a585e5..dfd5c87ed 100644 --- a/include/boost/capy/io/write_now.hpp +++ b/include/boost/capy/io/write_now.hpp @@ -59,7 +59,7 @@ namespace capy { @par Preconditions Only one operation may be outstanding at a time. A new call to `operator()` must not be made until the previous operation has - completed (i.e., the returned awaitable has been fully consumed). + completed (i.e., the returned awaitable is fully consumed). @par Example diff --git a/include/boost/capy/io_result.hpp b/include/boost/capy/io_result.hpp index 8a058ae22..7098a9d96 100644 --- a/include/boost/capy/io_result.hpp +++ b/include/boost/capy/io_result.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) @@ -21,7 +22,7 @@ namespace boost { namespace capy { -/** Result type for asynchronous I/O operations. +/** Bundles an error code with optional payload values, exposed via the tuple protocol. This template provides a unified result type for async operations, always containing a `std::error_code` plus optional additional @@ -37,7 +38,7 @@ namespace capy { defined by the operation that produced the result. Many I/O operations report a meaningful partial result alongside `ec` (for example, the number of bytes transferred before the - condition, as with EOF); others leave it unspecified. + condition, as with EOF). Others leave it unspecified. @tparam Ts Ordered payload types following the leading `std::error_code`. @@ -55,14 +56,28 @@ struct [[nodiscard]] io_result /// Construct a default io_result. io_result() = default; - /// Construct from an error code and payload values. + /** Construct from an error code and payload values. + + @param ec_ The error code for the operation. + + @param ts The payload values, in declaration order. + */ io_result(std::error_code ec_, Ts... ts) : ec(ec_) , values(std::move(ts)...) { } - /// @cond + /** Return the `I`-th element of the tuple protocol. + + Index 0 is @ref ec; index `I` for `I > 0` is the `I - 1`-th + payload value. This is the accessor structured bindings use. + + @tparam I The element index. Must be less than + `1 + sizeof...(Ts)`. + + @return A reference to the element. + */ template decltype(auto) get() & noexcept { @@ -71,6 +86,16 @@ struct [[nodiscard]] io_result else return std::get(values); } + /** Return the `I`-th element of the tuple protocol. + + Index 0 is @ref ec; index `I` for `I > 0` is the `I - 1`-th + payload value. This is the accessor structured bindings use. + + @tparam I The element index. Must be less than + `1 + sizeof...(Ts)`. + + @return A const reference to the element. + */ template decltype(auto) get() const& noexcept { @@ -79,6 +104,17 @@ struct [[nodiscard]] io_result else return std::get(values); } + /** Return the `I`-th element of the tuple protocol, moved. + + Index 0 is @ref ec; index `I` for `I > 0` is the `I - 1`-th + payload value. + + @tparam I The element index. Must be less than + `1 + sizeof...(Ts)`. + + @return An rvalue reference to the element, suitable for moving + out of the result. + */ template decltype(auto) get() && noexcept { @@ -86,28 +122,53 @@ struct [[nodiscard]] io_result if constexpr (I == 0) return std::move(ec); else return std::get(std::move(values)); } - /// @endcond }; -/// @cond +/** Return the `I`-th element of the tuple protocol. + + @tparam I The element index. Must be less than + `1 + sizeof...(Ts)`. + + @param r The result to access. + + @return A reference to the element. +*/ template decltype(auto) get(io_result& r) noexcept { return r.template get(); } +/** Return the `I`-th element of the tuple protocol. + + @tparam I The element index. Must be less than + `1 + sizeof...(Ts)`. + + @param r The result to access. + + @return A const reference to the element. +*/ template decltype(auto) get(io_result const& r) noexcept { return r.template get(); } +/** Return the `I`-th element of the tuple protocol, moved. + + @tparam I The element index. Must be less than + `1 + sizeof...(Ts)`. + + @param r The result to access. + + @return An rvalue reference to the element, suitable for moving out + of `r`. +*/ template decltype(auto) get(io_result&& r) noexcept { return std::move(r).template get(); } -/// @endcond } // namespace capy } // namespace boost diff --git a/include/boost/capy/io_task.hpp b/include/boost/capy/io_task.hpp index b3067c2c2..6ac443772 100644 --- a/include/boost/capy/io_task.hpp +++ b/include/boost/capy/io_task.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) @@ -10,17 +11,18 @@ #ifndef BOOST_CAPY_IO_TASK_HPP #define BOOST_CAPY_IO_TASK_HPP +#include #include #include namespace boost { namespace capy { -/** A task type for I/O operations yielding io_result. +/** Names `task>`, whose `co_return` can convert an error code directly. This is a convenience alias for `task>`. The converting constructor on `io_result<>` allows direct - `co_return` of error codes: + `co_return` of a `std::error_code`: @code io_task<> connect_to_server(socket& s, endpoint ep) @@ -28,9 +30,11 @@ namespace capy { co_return co_await s.connect(ep); // returns io_result<> } - io_task<> handler(route_params& rp) + io_task<> require_ready(bool ready) { - co_return route::next; // error_code converts to io_result<> + if(!ready) + co_return make_error_code(error::eof); // error_code converts to io_result<> + co_return {}; } @endcode diff --git a/include/boost/capy/quitter.hpp b/include/boost/capy/quitter.hpp index 864c18af8..ecbd46f73 100644 --- a/include/boost/capy/quitter.hpp +++ b/include/boost/capy/quitter.hpp @@ -68,7 +68,7 @@ struct quitter_return_base } // namespace detail -/** Stop-aware lazy coroutine task satisfying @ref IoRunnable. +/** Defers a coroutine body until awaited, then unwinds it early on a stop request. When the stop token is triggered, the next `co_await` inside the coroutine short-circuits: the body never sees the result and RAII @@ -78,6 +78,52 @@ struct quitter_return_base Everything else — frame allocation, environment propagation, symmetric transfer, move semantics — is identical to @ref task. + @par Await-effects + + Let `q` be a `quitter`. `co_await q` always suspends the awaiting + coroutine, then transfers control directly into the quitter's + coroutine body on the current thread; no executor operation is + posted. The quitter records the caller's environment (executor, stop + token, and frame allocator) by pointer rather than copying it. It + propagates that environment to every `co_await` inside the body. + + Unlike @ref task, the stop token is checked at every point where the + body would resume. Those points are before the body's first + statement, and again each time an awaited operation resumes it. If a + stop request is pending, the body is not resumed. An internal + sentinel exception unwinds it instead, so RAII destructors run, and + the coroutine completes as stopped. + + The body runs until it returns, exits via an exception, or is unwound + by a stop request. Control then transfers directly back to the + awaiting coroutine, again without an executor operation. + + @par Await-returns + The value the body passed to `co_return`, moved out of the quitter, + or nothing when `T` is `void`. + + If the body exits via an unhandled exception, that exception is + rethrown instead. + + If the coroutine completed as stopped, the internal sentinel + exception is thrown instead of await-returning. Awaiting a stopped + `quitter` from another `quitter` therefore stops that one too. A + @ref task awaiting it sees the sentinel as an unhandled exception in + its own body. When a quitter is started by `run_async`, a stopped + completion reaches the error handler as the sentinel + `std::exception_ptr`, not the value handler. + + @par Await-postcondition + The quitter's coroutine has run to completion and is suspended at its + final suspend point; the body's RAII destructors have run. Exactly + one of the following holds: the body returned a value; the body + exited via an exception; or `handle().promise().stopped()` returns + `true`. When the body returned a value, the await moved it out, so a + quitter must not be awaited twice. + + @par Remarks + Supports _IoAwaitable cancellation_. + @tparam T The result type. Use `quitter<>` for `quitter`. @see task, IoRunnable, IoAwaitable @@ -86,17 +132,17 @@ template struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE quitter { - /** The coroutine promise type for `quitter`. + /** Stores `quitter`'s result and unwinds the body when the stop token fires. This is the promise object the compiler associates with a `quitter` coroutine. It satisfies the coroutine promise requirements and participates in the I/O awaitable protocol via @ref io_awaitable_promise_base. Unlike @ref task::promise_type, its `transform_awaitable` checks the stop token before each - awaited result reaches the body, throwing an internal sentinel - exception that unwinds to a "stopped" completion. It is part of - the coroutine machinery and is not intended to be used directly - by callers. + awaited result reaches the body. A pending stop request throws an + internal sentinel exception that unwinds to a "stopped" + completion. It is part of the coroutine machinery and is not + intended to be used directly by callers. Result storage and `return_value`/`return_void` are provided by `detail::quitter_return_base`. @@ -130,11 +176,18 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE ep_.~exception_ptr(); } - /// Return a non-null exception_ptr when the coroutine threw - /// or was stopped. Stopped quitters report the sentinel - /// stop_requested_exception so that run_async routes to - /// the error handler instead of accessing a non-existent - /// result. + /** Return a non-null exception_ptr when the coroutine threw + or was stopped. + + Stopped quitters report the sentinel + stop_requested_exception so that run_async routes to + the error handler instead of accessing a non-existent + result. + + @return The stored exception if the coroutine exited via an + exception or was stopped, otherwise a null + `std::exception_ptr`. + */ std::exception_ptr exception() const noexcept { if(state_ == completion::exception || @@ -143,7 +196,12 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return {}; } - /// True when the coroutine was stopped via the stop token. + /** True when the coroutine was stopped via the stop token. + + @return `true` if the body was unwound by a stop request; + `false` if it returned a value or exited via any other + exception. + */ bool stopped() const noexcept { return state_ == completion::stopped; @@ -167,8 +225,8 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE The coroutine always suspends at the initial suspend point, so the body does not start until the quitter is awaited. When the body is resumed, the awaiter restores the thread-local - frame allocator and, if stop has already been requested, - throws the internal sentinel exception so the body never + frame allocator. It then throws the internal sentinel + exception if stop is already requested, so the body never runs and the coroutine completes as stopped. @return An awaiter that suspends unconditionally. @@ -272,23 +330,55 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE Forwards the environment to the inner awaitable's environment-taking `await_suspend` and restores the thread-local frame allocator before the body resumes. Unlike - `task`'s, it also checks the stop token on resumption, throwing - the internal sentinel so a stop request unwinds the body before - it observes the I/O result. + `task`'s, it also checks the stop token on resumption. A + pending stop request throws the internal sentinel, so the body + unwinds before it observes the I/O result. @tparam Awaitable The awaitable being transformed. */ template struct transform_awaiter { + /// The wrapped awaitable, decayed and stored by value. std::decay_t a_; + + /// The promise of the coroutine performing the `co_await`. promise_type* p_; + /** Report whether the wrapped awaitable is already complete. + + The stop token is not checked here. A stop request that + arrives before an already-complete operation is observed by + @ref await_resume, which runs in either case. + + @return The wrapped awaitable's own `await_ready` result: + `true` if no suspension is needed. + */ bool await_ready() noexcept { return a_.await_ready(); } + /** Restore the frame allocator, check for stop, then resume the + wrapped awaitable. + + Reinstalls the thread-local frame allocator from the stored + environment, then reads the environment's stop token. If a + stop request is pending, the internal sentinel exception is + thrown from here. The body therefore never observes the + operation's result. It unwinds through its RAII destructors + to a stopped completion. This is the one place `quitter` + differs from @ref task::promise_type::transform_awaiter. + + @return The wrapped awaitable's await-result, forwarded + unchanged, when no stop request is pending. + + @par Exception Safety + Throws the library's internal stop sentinel if the + environment's stop token has a stop request pending. The + wrapped awaitable's `await_resume` is not called in that + case. + */ // Check the stop token BEFORE the coroutine body // sees the result of the I/O operation. decltype(auto) await_resume() @@ -300,6 +390,29 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return a_.await_resume(); } + /** Suspend by calling the wrapped awaitable with the + environment. + + This is the plain `await_suspend` the compiler calls for the + nested `co_await`. It forwards to the wrapped awaitable's + @ref IoAwaitable overload, supplying the promise's stored + environment as the second argument. It then hands back + that call's result unchanged, so the wrapped awaitable's + suspension decision, whatever form it takes, is preserved. + The stop token is not checked here; @ref await_resume checks + it on the way back out. + + @param h The coroutine performing the `co_await`. + + @return Whatever the wrapped awaitable's `await_suspend` + returns. When that is a `std::coroutine_handle<>`, the + handle is routed through `detail::symmetric_transfer`. + On MSVC that helper resumes the handle on the current + stack, and this function returns `void`, so the awaiting + coroutine suspends unconditionally. On every other + compiler the handle is returned unchanged for symmetric + transfer. + */ template auto await_suspend( std::coroutine_handle h) noexcept @@ -359,7 +472,13 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE h_.destroy(); } - /// Return false; quitters are never immediately ready. + /** Return false; quitters are never immediately ready. + + A quitter is lazy and has not started when it is awaited, so the + awaiting coroutine always suspends. + + @return `false`. + */ bool await_ready() const noexcept { return false; @@ -368,8 +487,16 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE /** Return the result, rethrow exception, or propagate stop. When stopped, throws stop_requested_exception so that a - parent quitter also stops. A parent task will see this + parent quitter also stops. A parent task sees this as an unhandled exception — by design. + + @return The result value for non-void `T`, moved out of the + quitter; otherwise `void`. + + @par Exception Safety + If the coroutine was stopped, the library's internal stop sentinel + is thrown. If the body exited via any other exception, that + exception is rethrown. */ auto await_resume() { @@ -383,7 +510,21 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return; } - /// Start execution with the caller's context. + /** Start execution with the caller's context. + + Stores `cont` as the continuation to resume on completion. + Stores `env` as the execution environment propagated to nested + `co_await` expressions. Then transfers control into the quitter's + coroutine body via the returned handle. + + @param cont The awaiting coroutine to resume when the quitter + completes. + + @param env The execution environment (executor, stop token, and + frame allocator). It must outlive the quitter. + + @return The quitter's coroutine handle, for symmetric transfer. + */ std::coroutine_handle<> await_suspend( std::coroutine_handle<> cont, io_env const* env) @@ -398,7 +539,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE @note Do not call `destroy()` on the returned handle while the quitter is being awaited. The quitter's lifetime is normally managed by `run_async`, `run`, or the awaiting - parent; manually destroying a suspended quitter that another + parent. Manually destroying a suspended quitter that another coroutine is awaiting produces undefined behavior. For cooperative cancellation, use `std::stop_token`. @@ -411,26 +552,59 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE /** Release ownership of the coroutine frame. - @note If the caller intends to call `destroy()` on the - released handle, it must do so only when the quitter has not - started or has fully completed. Destroying a suspended - quitter that is being awaited produces undefined behavior. + @note The caller may call `destroy()` on the released handle + only when the quitter has not started or has fully completed. + Destroying a suspended quitter that is being awaited produces + undefined behavior. + + @par Postconditions + `handle()` returns a null handle. Callers needing the + original handle must save it, via @ref handle, before + calling this. */ void release() noexcept { h_ = nullptr; } - quitter(quitter const&) = delete; - quitter& operator=(quitter const&) = delete; + /** Copy construction is disabled; a quitter uniquely owns its frame. + + @param other The quitter that would be copied. + */ + quitter(quitter const& other) = delete; + + /** Copy assignment is disabled; a quitter uniquely owns its frame. + + @param other The quitter that would be assigned from. + + @return A reference to `*this`. + */ + quitter& operator=(quitter const& other) = delete; - /// Construct by moving, transferring ownership. + /** Construct by moving, transferring ownership. + + @par Postconditions + `other` is empty and must not be awaited. + + @param other The quitter to move from. + */ quitter(quitter&& other) noexcept : h_(std::exchange(other.h_, nullptr)) { } - /// Assign by moving, transferring ownership. + /** Assign by moving, transferring ownership. + + If this quitter already owns a coroutine frame, that frame is + destroyed first. Self-assignment is a no-op. + + @par Postconditions + `other` is empty and must not be awaited. + + @param other The quitter to move from. + + @return A reference to `*this`. + */ quitter& operator=(quitter&& other) noexcept { if(this != &other) diff --git a/include/boost/capy/read.hpp b/include/boost/capy/read.hpp index 01839f229..198385794 100644 --- a/include/boost/capy/read.hpp +++ b/include/boost/capy/read.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) @@ -64,6 +65,8 @@ namespace capy { @param buffers The buffer sequence to fill. If the lifetime of the buffer sequence represented by `buffers` ends before the coroutine finishes, the behavior is undefined. + @return A task yielding `io_result` whose second element + is the number of bytes read. @par Remarks Supports _IoAwaitable cancellation_. diff --git a/include/boost/capy/read_at_least.hpp b/include/boost/capy/read_at_least.hpp index 5362df682..41338f5da 100644 --- a/include/boost/capy/read_at_least.hpp +++ b/include/boost/capy/read_at_least.hpp @@ -27,15 +27,15 @@ namespace capy { This is a straightforward extension of @ref read. While @ref read transfers exactly `buffer_size(buffers)` bytes, `read_at_least` - transfers at least `n` bytes: the loop stops as soon as `n` bytes + transfers at least `n` bytes. The loop stops as soon as `n` bytes have been read, even if `buffers` is not yet full. Any bytes beyond - `n` that a single `stream.read_some` happens to deliver (up to the - capacity of `buffers`) are kept, but no further awaiting is performed - to fill the remainder. + `n` that a single `stream.read_some` happens to deliver are kept, up + to the capacity of `buffers`. No further awaiting is performed to + fill the remainder. This is useful when a caller has a required amount of data `n` that - must be met or exceeded, while the subsequent capacity of `buffers` - is optional and should not block. + must be met or exceeded. The subsequent capacity of `buffers` is then + optional, and filling it should not block. @par Await-effects @@ -73,8 +73,8 @@ namespace capy { @par Await-postcondition On success the returned count is greater than or equal to `n` and - less than or equal to `buffer_size(buffers)`, and `ec` is success; - otherwise `ec` is set. + less than or equal to `buffer_size(buffers)`, and `ec` is success. + Otherwise `ec` is set. @param stream The stream to read from. If the lifetime of `stream` ends before the coroutine finishes, the behavior is undefined. @@ -86,6 +86,9 @@ namespace capy { @param n The minimum number of bytes to read. Must not exceed `buffer_size(buffers)`. + @return A task yielding `io_result` whose second element + is the number of bytes read. + @par Remarks Supports _IoAwaitable cancellation_. diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index 119ba442f..21442d454 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.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) @@ -56,7 +57,7 @@ struct task_return_base } // namespace detail -/** Lazy coroutine task satisfying @ref IoRunnable. +/** Defers a coroutine body until awaited, then runs it inline on the caller's thread. Use `task` as the return type for coroutines that perform I/O and return a value of type `T`. The coroutine body does not start @@ -68,6 +69,36 @@ struct task_return_base to nested `co_await` expressions. This enables cancellation and proper completion dispatch across executor boundaries. + @par Await-effects + + Let `t` be a `task`. `co_await t` always suspends the awaiting + coroutine, then transfers control directly into the task's coroutine + body on the current thread; no executor operation is posted. The task + records the caller's environment (executor, stop token, and frame + allocator) by pointer rather than copying it. It propagates that + environment to every `co_await` inside the body. + + The body runs until it returns or exits via an exception. Control + then transfers directly back to the awaiting coroutine, again + without an executor operation. + + `task` never inspects the stop token; it only propagates it. A task + body observes a stop request through the results of the operations it + awaits, or by reading the token itself. See @ref quitter for a task + that stops its own body. + + @par Await-returns + The value the body passed to `co_return`, moved out of the task, or + nothing when `T` is `void`. + + If the body exits via an unhandled exception, that exception is + rethrown instead. + + @par Await-postcondition + The task's coroutine has run to completion and is suspended at its + final suspend point. The task still owns the frame, but not the + result: the await moves it out, so a task must not be awaited twice. + @par Thread Safety Distinct objects: Safe. Shared objects: Unsafe. @@ -98,7 +129,7 @@ template struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE task { - /** The coroutine promise type for `task`. + /** Stores `task`'s result and joins the I/O awaitable protocol via `io_awaitable_promise_base`. This is the promise object the compiler associates with a `task` coroutine. It satisfies the coroutine promise @@ -245,14 +276,33 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE template struct transform_awaiter { + /// The wrapped awaitable, decayed and stored by value. std::decay_t a_; + + /// The promise of the coroutine performing the `co_await`. promise_type* p_; + /** Report whether the wrapped awaitable is already complete. + + @return The wrapped awaitable's own `await_ready` result: + `true` if no suspension is needed. + */ bool await_ready() noexcept { return a_.await_ready(); } + /** Restore the frame allocator, then resume the wrapped + awaitable. + + Reinstalls the thread-local frame allocator from the stored + environment before the body continues. This is needed + because the resumption may arrive on a different thread + than the one that suspended. + + @return The wrapped awaitable's await-result, forwarded + unchanged. + */ decltype(auto) await_resume() { // Restore TLS before body resumes @@ -260,6 +310,27 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return a_.await_resume(); } + /** Suspend by calling the wrapped awaitable with the + environment. + + This is the plain `await_suspend` the compiler calls for the + nested `co_await`. It forwards to the wrapped awaitable's + @ref IoAwaitable overload, supplying the promise's stored + environment as the second argument. It then hands back + that call's result unchanged, so the wrapped awaitable's + suspension decision, whatever form it takes, is preserved. + + @param h The coroutine performing the `co_await`. + + @return Whatever the wrapped awaitable's `await_suspend` + returns. When that is a `std::coroutine_handle<>`, the + handle is routed through `detail::symmetric_transfer`. + On MSVC that helper resumes the handle on the current + stack, and this function returns `void`, so the awaiting + coroutine suspends unconditionally. On every other + compiler the handle is returned unchanged for symmetric + transfer. + */ template auto await_suspend(std::coroutine_handle h) noexcept { @@ -332,7 +403,9 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE @return The result value for non-void `T`; otherwise `void`. - @throws The exception captured by the coroutine body, if any. + @par Exception Safety + If the coroutine body captured an exception, that exception is + rethrown here. */ auto await_resume() { @@ -346,9 +419,9 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE /** Start the task with the awaiting coroutine's context. - Stores `cont` as the continuation to resume on completion and - `env` as the execution environment propagated to nested - `co_await` expressions, then transfers control into the task's + Stores `cont` as the continuation to resume on completion. + Stores `env` as the execution environment propagated to nested + `co_await` expressions. Then transfers control into the task's coroutine body via the returned handle. @param cont The awaiting coroutine to resume when the task @@ -370,7 +443,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE @note Do not call `destroy()` on the returned handle while the task is being awaited. The task's lifetime is normally managed - by `run_async`, `run`, or the awaiting parent; manually + by `run_async`, `run`, or the awaiting parent. Manually destroying a suspended task that another coroutine is awaiting produces undefined behavior. For cooperative cancellation, use `std::stop_token`. @@ -388,22 +461,34 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE coroutine frame. The caller becomes responsible for the frame's lifetime. - @note If the caller intends to call `destroy()` on the - released handle, it must do so only when the task has not - started or has fully completed. Destroying a suspended task - that is being awaited produces undefined behavior. + @note The caller may call `destroy()` on the released handle + only when the task has not started or has fully completed. + Destroying a suspended task that is being awaited produces + undefined behavior. @par Postconditions - `handle()` returns the original handle, but the task no longer - owns it. + `handle()` returns a null handle. Callers needing the + original handle must save it, via @ref handle, before + calling this. */ void release() noexcept { h_ = nullptr; } - task(task const&) = delete; - task& operator=(task const&) = delete; + /** Copy construction is disabled; a task uniquely owns its frame. + + @param other The task that would be copied. + */ + task(task const& other) = delete; + + /** Copy assignment is disabled; a task uniquely owns its frame. + + @param other The task that would be assigned from. + + @return A reference to `*this`. + */ + task& operator=(task const& other) = delete; /** Construct by moving, transferring ownership of the frame. @@ -427,7 +512,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE @param other The task to move from. - @return `*this`. + @return A reference to `*this`. */ task& operator=(task&& other) noexcept { diff --git a/include/boost/capy/test.hpp b/include/boost/capy/test.hpp new file mode 100644 index 000000000..faa919854 --- /dev/null +++ b/include/boost/capy/test.hpp @@ -0,0 +1,33 @@ +// +// 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 +// + +#ifndef BOOST_CAPY_TEST_HPP +#define BOOST_CAPY_TEST_HPP + +/** @file + @brief Single-include header for the public capy testing toolkit. + + Including this header provides access to the complete + @ref boost::capy::test toolkit: mock streams, the fuse fail-point + machinery, blocking task runners, buffer inspection helpers, and + thread naming. It is a convenience for test code, and the main + umbrella does not pull it in. Normal consumers are + therefore never forced to depend on the testing utilities. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/include/boost/capy/test/buffer_to_string.hpp b/include/boost/capy/test/buffer_to_string.hpp index 9d66d7327..89c026570 100644 --- a/include/boost/capy/test/buffer_to_string.hpp +++ b/include/boost/capy/test/buffer_to_string.hpp @@ -37,11 +37,12 @@ namespace test { const_buffer b2( " world", 6 ); std::string s = buffer_to_string( b1, b2 ); // "hello world" - // With bufgrind splits + // With bufgrind splits: each half is itself a buffer sequence, + // so pass it directly -- there is no .data() to unwrap. bufgrind bg( cb ); while( bg ) { auto [b1, b2] = co_await bg.next(); - BOOST_TEST_EQ( buffer_to_string( b1.data(), b2.data() ), "hello" ); + BOOST_TEST_EQ( buffer_to_string( b1, b2 ), "hello" ); } @endcode diff --git a/include/boost/capy/test/bufgrind.hpp b/include/boost/capy/test/bufgrind.hpp index c5fb89d67..3a92c8176 100644 --- a/include/boost/capy/test/bufgrind.hpp +++ b/include/boost/capy/test/bufgrind.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) @@ -25,7 +26,7 @@ namespace boost { namespace capy { namespace test { -/** A test utility for iterating buffer sequence split points. +/** Iterates split points of a buffer sequence into two adjacent halves. This class iterates through all possible ways to split a buffer sequence into two parts (b1, b2) where concatenating them yields @@ -96,12 +97,11 @@ class bufgrind std::size_t pos_ = 0; public: - /// The buffer-sequence type produced for each half of a split. + /// Names the buffer-sequence type `buffer_slice` yields for each half. using slice_type = std::decay_t< decltype(buffer_slice(std::declval()))>; - /// The type returned by @ref next. Each half is itself a buffer - /// sequence (the value returned by `buffer_slice`). + /// Pairs the two `slice_type` halves that @ref next yields together. using split_type = std::pair; /** Construct a buffer grinder. @@ -132,15 +132,31 @@ class bufgrind return pos_ <= size_; } - /** Awaitable returned by @ref next. + /** Computes the current split synchronously, so awaiting it never suspends the caller. */ struct next_awaitable { + /// The grinder that produced this awaitable. bufgrind* self_; + /** Report whether the awaitable is ready. + + @return `true` always; the split is available without suspending. + */ bool await_ready() const noexcept { return true; } + + /** Resume the caller inline without suspending. + + @param h The awaiting coroutine handle. + + @return @p h, so the caller resumes immediately. + */ std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const*) const noexcept { return h; } + /** Return the current split and advance to the next. + + @return The `(b1, b2)` split at the current position. + */ split_type await_resume() { diff --git a/include/boost/capy/test/fuse.hpp b/include/boost/capy/test/fuse.hpp index 763a06149..4291b7307 100644 --- a/include/boost/capy/test/fuse.hpp +++ b/include/boost/capy/test/fuse.hpp @@ -52,7 +52,7 @@ namespace boost { namespace capy { namespace test { -/** A test utility for systematic error injection. +/** Reruns a code path, injecting a failure at one later point on each pass. This class enables exhaustive testing of error handling paths by injecting failures at successive points in code. @@ -240,7 +240,7 @@ class fuse } public: - /** Result of a fuse operation. + /** Converts to `bool`, reporting success, and carries the failure point on failure. Contains the outcome of @ref armed or @ref inert and, on failure, the source location of the failing @@ -276,7 +276,10 @@ class fuse /// True if the test completed without a failure. bool success = true; - /// Return @ref success. + /** Return whether the test completed without a failure. + + @return @ref success. + */ constexpr explicit operator bool() const noexcept { return success; @@ -412,7 +415,7 @@ class fuse /** Signal a test failure and stop execution. Call this from the test function to indicate a failure - condition. Both @ref armed and @ref inert will return + condition. Both @ref armed and @ref inert return a failed @ref result immediately. @par Example @@ -456,7 +459,7 @@ class fuse Call this from the test function to indicate a failure condition with an associated exception. Both @ref armed - and @ref inert will return a failed @ref result with + and @ref inert return a failed @ref result with the captured exception pointer. @par Example @@ -760,22 +763,22 @@ class fuse Behaves like the @ref IoRunnable overload of @ref armed, but instead of driving each iteration through @ref run_blocking, it hands the coroutine to `run_one`. This lets a caller run each - iteration on any execution context it chooses — in particular an - `io_context`, which operations built on `corosio::timeout` or - `corosio::delay` require, since those abort on a - non-`io_context` executor. `fuse` never learns about the context; + iteration on any execution context it chooses. Operations built + on `corosio::timeout` or `corosio::delay` in particular require + an `io_context`, because they abort on a non-`io_context` + executor. `fuse` never learns about the context; the caller owns the drive loop. @par Runner contract `run_one` is invoked once per round with the @ref IoRunnable produced by `fn`. It must run that task to completion synchronously and *return* any exception the task raised as a - `std::exception_ptr` (null on success). It must not rethrow: + `std::exception_ptr` (null on success). It must not rethrow. `armed` rethrows the returned pointer from its own synchronous - code so the exception phase observes injected failures, whereas - an exception escaping a `run_async` completion handler would call - `std::terminate`. Capture the exception in the error handler and - return it once the run loop is done. + code, so the exception phase observes injected failures. An + exception escaping a `run_async` completion handler would + instead call `std::terminate`. Capture the exception in the error + handler and return it once the run loop is done. @par Example @code @@ -841,6 +844,10 @@ class fuse }); @endcode + @param fn The test function to run under failure injection. + + @return The @ref result of the armed run. + @see armed */ template @@ -852,6 +859,10 @@ class fuse /** Alias for @ref armed (coroutine overload). + @param fn The test coroutine factory to run under failure injection. + + @return The @ref result of the armed run. + @see armed */ template @@ -899,7 +910,7 @@ class fuse @param fn The test function to invoke. It receives a reference to the fuse. Calls to @ref maybe_fail - will always succeed. + always succeed. @return A @ref result indicating success or failure. On failure, `result::loc` contains the source location @@ -964,7 +975,7 @@ class fuse @param fn The coroutine test function to invoke. It receives a reference to the fuse. Calls to @ref maybe_fail - will always succeed. + always succeed. @return A @ref result indicating success or failure. On failure, `result::loc` contains the source location diff --git a/include/boost/capy/test/read_stream.hpp b/include/boost/capy/test/read_stream.hpp index 919439761..9f986bd2e 100644 --- a/include/boost/capy/test/read_stream.hpp +++ b/include/boost/capy/test/read_stream.hpp @@ -28,7 +28,7 @@ namespace boost { namespace capy { namespace test { -/** A mock stream for testing read operations. +/** Buffers data supplied via `provide`, then hands it out through `read_some`. Use this to verify code that performs reads without needing real I/O. Call @ref provide to supply data, then @ref read_some @@ -103,7 +103,10 @@ class read_stream pos_ = 0; } - /// Return the number of bytes available for reading. + /** Return the number of bytes available for reading. + + @return The number of provided bytes not yet consumed. + */ std::size_t available() const noexcept { @@ -130,7 +133,7 @@ class read_stream failure point; no-throw otherwise. @par Cancellation - If the environment's stop token has been requested, the read + If the environment's stop token is requested, the read completes immediately with `error::canceled` and transfers no data. This lets code under test exercise its cancellation paths. An empty buffer sequence is a no-op that completes successfully diff --git a/include/boost/capy/test/run_blocking.hpp b/include/boost/capy/test/run_blocking.hpp index 14667c87d..7e1cec2cf 100644 --- a/include/boost/capy/test/run_blocking.hpp +++ b/include/boost/capy/test/run_blocking.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) @@ -27,7 +28,7 @@ namespace test { class blocking_context; -/** Single-threaded executor for blocking synchronous tests. +/** Dispatches work inline for symmetric transfer, or enqueues it into the owning `blocking_context`. This executor is used internally by @ref run_blocking to execute coroutine tasks on the calling thread. Work submitted @@ -45,7 +46,10 @@ class blocking_context; */ struct BOOST_CAPY_DECL blocking_executor { - /// Construct from a context pointer. + /** Construct from a context pointer. + + @param ctx The owning execution context. + */ explicit blocking_executor( blocking_context* ctx) noexcept : ctx_(ctx) @@ -55,6 +59,10 @@ struct BOOST_CAPY_DECL blocking_executor /** Compare two blocking executors for equality. Two executors are equal if they share the same context. + + @param other The executor to compare against. + + @return `true` if both executors share the same context. */ bool operator==(blocking_executor const& other) const noexcept; @@ -99,7 +107,7 @@ struct BOOST_CAPY_DECL blocking_executor blocking_context* ctx_; }; -/** Single-threaded execution context for blocking tests. +/** Runs a work queue and event loop on the calling thread until the task completes. Provides a work queue and event loop that runs on the calling thread. Coroutines dispatched through the @@ -124,6 +132,7 @@ class BOOST_CAPY_DECL blocking_context impl* impl_; public: + /// Names `blocking_executor` as the type `get_executor()` returns. using executor_type = blocking_executor; /** Construct a blocking context. @@ -201,10 +210,16 @@ class BOOST_CAPY_DECL blocking_context template struct blocking_handler_wrapper { + /// The context signalled once the handler returns. blocking_context* ctx_; + + /// The success and error handlers to forward to. detail::handler_pair handlers_; - /** Invoke the handler with a non-void result. */ + /** Invoke the handler with a non-void result. + + @param v The result value to forward to the handler. + */ template void operator()(T&& v) { @@ -235,7 +250,10 @@ struct blocking_handler_wrapper ctx_->signal_done(); } - /** Invoke the handler with an exception. */ + /** Invoke the handler with an exception. + + @param ep The exception to forward to the error handler. + */ void operator()(std::exception_ptr ep) { try @@ -251,15 +269,22 @@ struct blocking_handler_wrapper } }; -/** Wrapper returned by run_blocking that accepts a task. +/** Starts a `blocking_context`, runs the task on it, and pumps the event loop until it completes. Holds the handlers and optional stop token. When invoked - with a task, creates a @ref blocking_context, launches + with a task, creates a @ref blocking_context, starts the task via `run_async`, and pumps the event loop until the task completes. - The rvalue ref-qualifier on `operator()` ensures the - wrapper can only be used as a temporary. + The rvalue ref-qualifier on `operator()` restricts invocation + to rvalues, so `run_blocking(h)(task)` is the supported spelling. + `operator()` moves `h1_` out of the wrapper, and `h2_` too unless + `H2` is `default_handler`. The stop token is copied, not moved. + The wrapper is single-use regardless. A stored wrapper needs an + explicit `std::move` to invoke: + `auto w = run_blocking(h); std::move(w)(task);`. That explicit + `std::move` surfaces the single-use hazard that a bare `w(task)` + on an lvalue would otherwise hide. @tparam H1 The success handler type. @tparam H2 The error handler type. @@ -300,15 +325,38 @@ class [[nodiscard]] run_blocking_wrapper { } - run_blocking_wrapper(run_blocking_wrapper const&) = delete; - run_blocking_wrapper(run_blocking_wrapper&&) = delete; - run_blocking_wrapper& operator=(run_blocking_wrapper const&) = delete; - run_blocking_wrapper& operator=(run_blocking_wrapper&&) = delete; + /** Copy construction is disabled; the wrapper is single-use. + + @param other The wrapper that would be copied. + */ + run_blocking_wrapper(run_blocking_wrapper const& other) = delete; + + /** Move construction is disabled; the wrapper is single-use. + + @param other The wrapper that would be moved from. + */ + run_blocking_wrapper(run_blocking_wrapper&& other) = delete; + + /** Copy assignment is disabled; the wrapper is single-use. + + @param other The wrapper that would be assigned from. + + @return A reference to `*this`. + */ + run_blocking_wrapper& operator=(run_blocking_wrapper const& other) = delete; + + /** Move assignment is disabled; the wrapper is single-use. + + @param other The wrapper that would be moved from. + + @return A reference to `*this`. + */ + run_blocking_wrapper& operator=(run_blocking_wrapper&& other) = delete; - /** Launch the task and block until completion. + /** Start the task and block until completion. Creates a blocking_context with a single-threaded - event loop, launches the task via `run_async`, then + event loop, starts the task via `run_async`, then pumps the loop until the task completes or throws. @tparam Task The IoRunnable type. diff --git a/include/boost/capy/test/stream.hpp b/include/boost/capy/test/stream.hpp index c1a6d2c52..7420acc08 100644 --- a/include/boost/capy/test/stream.hpp +++ b/include/boost/capy/test/stream.hpp @@ -37,7 +37,7 @@ namespace boost { namespace capy { namespace test { -/** A connected stream for testing bidirectional I/O. +/** Suspends a reader until its paired end writes, or the shared fuse injects an error. Streams are created in pairs via @ref make_stream_pair. Data written to one end becomes available for reading on @@ -47,11 +47,12 @@ namespace test { injection at controlled points in both directions. When the fuse injects an error or throws on one end, the - other end is automatically closed: any suspended reader is - resumed with `error::eof`, and subsequent operations on - both ends return `error::eof`. Calling @ref close on one - end signals eof to the peer's reads after draining any - buffered data, while the peer may still write. + pair is automatically closed. Any suspended reader on + either end is resumed with `error::eof`, and subsequent + operations on both ends return `error::eof`. Calling + @ref close on one end signals eof to the peer's reads + after draining any buffered data, while the peer may + still write. @par Thread Safety Single-threaded only. Both ends of the pair must be @@ -61,9 +62,14 @@ namespace test { @par Example @code fuse f; - auto [a, b] = make_stream_pair( f ); auto r = f.armed( [&]( fuse& ) -> task<> { + // Constructed inside the lambda: armed() re-invokes this + // function once per injected failure point, and a stream + // pair constructed outside would carry buffered state + // across those rounds. + auto [a, b] = make_stream_pair( f ); + auto [ec, n] = co_await a.write_some( const_buffer( "hello", 5 ) ); if( ec ) @@ -165,10 +171,33 @@ class stream make_stream_pair(fuse); public: - stream(stream const&) = delete; - stream& operator=(stream const&) = delete; - stream(stream&&) = default; - stream& operator=(stream&&) = default; + /** Copy construction is disabled; a stream end is move-only. + + @param other The stream end that would be copied. + */ + stream(stream const& other) = delete; + + /** Copy assignment is disabled; a stream end is move-only. + + @param other The stream end that would be assigned from. + + @return A reference to `*this`. + */ + stream& operator=(stream const& other) = delete; + + /** Move constructor. + + @param other The stream end to move from. + */ + stream(stream&& other) = default; + + /** Move assignment. + + @param other The stream end to move from. + + @return A reference to `*this`. + */ + stream& operator=(stream&& other) = default; /** Signal end-of-stream to the peer. @@ -208,7 +237,7 @@ class stream the calling coroutine suspends until the peer calls @ref write_some. Before every read, the attached @ref fuse is consulted to possibly inject an error. - If the fuse fires, the peer is automatically closed. + If the fuse fires, the pair is automatically closed. If the stream is closed, returns `error::eof`. The returned `std::size_t` is the number of bytes transferred. @@ -218,9 +247,9 @@ class stream @return An awaitable that await-returns `(error_code,std::size_t)`. @par Cancellation - Cancellation applies only to a read that would otherwise suspend: - if no data is available and the environment's stop token is - requested (before or during the wait), the read resumes with + Cancellation applies only to a read that would otherwise suspend. + If no data is available and the environment's stop token is + requested, before or during the wait, the read resumes with `error::canceled`. A read that can complete immediately from buffered data is unaffected by the stop token. @@ -414,7 +443,7 @@ class stream peer's incoming buffer. If the peer is suspended in @ref read_some, it is resumed. Before every write, the attached @ref fuse is consulted to possibly inject - an error. If the fuse fires, the peer is automatically + an error. If the fuse fires, the pair is automatically closed. If the stream is closed, returns `error::eof`. The returned `std::size_t` is the number of bytes transferred. @@ -425,7 +454,7 @@ class stream @return An awaitable that await-returns `(error_code,std::size_t)`. @par Cancellation - If the environment's stop token has been requested, the write + If the environment's stop token is requested, the write completes immediately with `error::canceled` and transfers no data. An empty buffer sequence is a no-op that completes successfully regardless of the stop token. @@ -578,7 +607,7 @@ class stream is available, it suspends until the peer writes. Before every read or write, the @ref fuse is consulted to possibly inject an error for testing fault scenarios. - When the fuse fires, the peer is automatically closed. + When the fuse fires, the pair is automatically closed. @param f The fuse used to inject errors during operations. diff --git a/include/boost/capy/test/write_stream.hpp b/include/boost/capy/test/write_stream.hpp index f5c9ccc8e..103c7da81 100644 --- a/include/boost/capy/test/write_stream.hpp +++ b/include/boost/capy/test/write_stream.hpp @@ -29,7 +29,7 @@ namespace boost { namespace capy { namespace test { -/** A mock stream for testing write operations. +/** Captures bytes passed to `write_some`, retrievable afterward through `data`. Use this to verify code that performs writes without needing real I/O. Call @ref write_some to write data, then @ref data @@ -46,9 +46,14 @@ namespace test { @par Example @code fuse f; - write_stream ws( f ); auto r = f.armed( [&]( fuse& ) -> task { + // Constructed inside the lambda: armed() re-invokes this + // function once per injected failure point, and a write_stream + // constructed outside would carry accumulated data across + // those rounds. + write_stream ws( f ); + auto [ec, n] = co_await ws.write_some( const_buffer( "Hello", 5 ) ); if( ec ) @@ -96,7 +101,10 @@ class write_stream { } - /// Return the written data as a string view. + /** Return the written data as a string view. + + @return A view of bytes written but not yet matched by @ref expect. + */ std::string_view data() const noexcept { @@ -120,7 +128,10 @@ class write_stream return consume_match_(); } - /// Return the number of bytes written. + /** Return the number of bytes written. + + @return The number of bytes written but not yet matched by @ref expect. + */ std::size_t size() const noexcept { @@ -147,7 +158,7 @@ class write_stream failure point; no-throw otherwise. @par Cancellation - If the environment's stop token has been requested, the write + If the environment's stop token is requested, the write completes immediately with `error::canceled` and transfers no data. An empty buffer sequence is a no-op that completes successfully regardless of the stop token. diff --git a/include/boost/capy/when_all.hpp b/include/boost/capy/when_all.hpp index 807c404be..e372be5e5 100644 --- a/include/boost/capy/when_all.hpp +++ b/include/boost/capy/when_all.hpp @@ -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) @@ -456,7 +457,7 @@ auto extract_results(when_all_state& state) }(std::index_sequence_for{}); } -/** Launches all homogeneous runners concurrently. +/** Starts all homogeneous runners concurrently. Two-phase approach: create all runners first, then post all. This avoids lifetime issues if a task completes synchronously. @@ -536,16 +537,63 @@ class when_all_homogeneous_launcher /** Execute a range of io_result-returning awaitables concurrently. - Launches all awaitables simultaneously and waits for all to complete. + Starts all awaitables simultaneously and waits for all to complete. On success, extracted payloads are collected in a vector preserving - input order. The first error_code cancels siblings and is propagated - in the outer io_result. Exceptions always beat error codes. + input order. The first error_code makes a stop request that every + sibling observes, and is propagated in the outer io_result. + Exceptions always beat error codes. + + @li All child awaitables run concurrently on the caller's executor. + @li Payloads are returned as a vector in input order. + @li First error_code wins and makes a stop request that siblings observe. + @li Exception always beats error_code. + @li Completes only after all children have finished. + + @par Await-effects + + Takes ownership of the range, creates one wrapper coroutine per + element, then posts every wrapper to the caller's executor. All + children therefore run concurrently, each awaited with the caller's + executor and frame allocator and with a stop token owned by this + operation. + + Awaiting an empty range throws `std::invalid_argument` before any + child is started. + + A stop request is made on the operation's own stop token when: + + @li a child await-returns a non-zero `ec`, or + @li a child exits via an exception, or + @li the caller's stop token is triggered. + + Every sibling observes that request through the stop token it was + awaited with. The request does not end the operation: the await + completes only after every child has finished. + + @par Await-returns + An object of type `io_result>` destructuring as + `[ec, values]`, where `PayloadT` is the payload of one child's + `io_result`. + + `ec` is the first non-zero `ec` await-returned by a child, in + completion order rather than input order. The `ec` of every other + child is discarded. + + On success, `values` holds one payload per element of the input + range, in input order. If `ec` is set, `values` is empty: the + payloads of the children that did succeed are discarded. + + If any child exits via an exception, the first such exception is + rethrown instead of await-returning, even when a child also reported + an `ec`. + + @par Await-postcondition + Every child has finished. `ec` is success only if every child + await-returned success. If `ec` is success, `values` holds one + payload per input awaitable; otherwise `values` is empty. - @li All child awaitables run concurrently on the caller's executor - @li Payloads are returned as a vector in input order - @li First error_code wins and cancels siblings - @li Exception always beats error_code - @li Completes only after all children have finished + @par Remarks + Supports _IoAwaitable cancellation_. @par Thread Safety The returned task must be awaited from a single execution context. @@ -560,8 +608,10 @@ class when_all_homogeneous_launcher @throws std::invalid_argument if range is empty (thrown before coroutine suspends). - @throws Rethrows the first child exception after all children - complete (exception beats error_code). + + @par Exception Safety + If a child throws, the first child exception is rethrown after + all children complete (exception beats error_code). @par Example @code @@ -623,10 +673,56 @@ template /** Execute a range of void io_result-returning awaitables concurrently. - Launches all awaitables simultaneously and waits for all to complete. + Starts all awaitables simultaneously and waits for all to complete. Since all awaitables return io_result<>, no payload values are - collected. The first error_code cancels siblings and is propagated. - Exceptions always beat error codes. + collected. The first error_code makes a stop request that every + sibling observes, and is propagated. Exceptions always beat error + codes. + + @par Await-effects + + Takes ownership of the range, creates one wrapper coroutine per + element, then posts every wrapper to the caller's executor. All + children therefore run concurrently, each awaited with the caller's + executor and frame allocator and with a stop token owned by this + operation. + + Awaiting an empty range throws `std::invalid_argument` before any + child is started. + + A stop request is made on the operation's own stop token when: + + @li a child await-returns a non-zero `ec`, or + @li a child exits via an exception, or + @li the caller's stop token is triggered. + + Every sibling observes that request through the stop token it was + awaited with. The request does not end the operation: the await + completes only after every child has finished. + + @par Await-returns + An object of type `io_result<>` destructuring as `[ec]`. The children + have no payloads, so nothing else is reported. + + `ec` is the first non-zero `ec` await-returned by a child, in + completion order rather than input order. The `ec` of every other + child is discarded. + + If any child exits via an exception, the first such exception is + rethrown instead of await-returning, even when a child also reported + an `ec`. + + @par Await-postcondition + Every child has finished. `ec` is success only if every child + await-returned success. + + @par Remarks + Supports _IoAwaitable cancellation_. + + @par Thread Safety + The returned task must be awaited from a single execution context. + Child awaitables execute concurrently but complete through the caller's + executor. @param awaitables Range of io_result<>-returning awaitables to execute concurrently (must not be empty). @@ -635,8 +731,10 @@ template error, or default-constructed on success. @throws std::invalid_argument if range is empty. - @throws Rethrows the first child exception after all children - complete (exception beats error_code). + + @par Exception Safety + If a child throws, the first child exception is rethrown after + all children complete (exception beats error_code). @par Example @code @@ -690,18 +788,64 @@ template io_result. On success all values are returned; on failure the first error_code wins. + @par Await-effects + + Creates and posts one wrapper coroutine per argument to the caller's + executor, in argument order. All children therefore run concurrently, + each awaited with the caller's executor and frame allocator and with + a stop token owned by this operation. The overload requires at least + one awaitable, so there is no empty case. + + A stop request is made on the operation's own stop token when: + + @li a child await-returns a non-zero `ec`, or + @li a child exits via an exception, or + @li the caller's stop token is triggered. + + Every sibling observes that request through the stop token it was + awaited with. The request does not end the operation: the await + completes only after every child has finished. + + @par Await-returns + An object of type `io_result` destructuring as + `[ec, v1, ..., vn]`, where `Pi` is the payload of the i-th child's + `io_result`. + + `ec` is the first non-zero `ec` await-returned by a child, in + completion order rather than argument order. The `ec` of every other + child is discarded. + + Each `vi` is the payload the i-th child itself await-returned, even + when that child or a sibling reported an `ec`. A failed child + therefore still contributes whatever payload it produced. This + differs from the range overloads, which discard all payloads once any + child fails. + + If any child exits via an exception, the first such exception is + rethrown instead of await-returning, even when a child also reported + an `ec`. + + @par Await-postcondition + Every child has finished. Each `vi` holds the i-th child's payload, + and `ec` is success only if every child await-returned success. + + @par Remarks + Supports _IoAwaitable cancellation_. + + @par Thread Safety + The returned task must be awaited from a single execution context. + Child awaitables execute concurrently but complete through the caller's + executor. + @par Exception Safety - Exception always beats error_code. If any child throws, the - exception is rethrown regardless of error_code results. + If a child throws, the first child exception is rethrown after + all children complete (exception beats error_code). @param awaitables One or more awaitables each returning io_result. @return A task yielding io_result where each Ri follows the payload flattening rules. - - @throws Rethrows the first child exception after all children - complete (exception beats error_code). */ template requires (sizeof...(As) > 0) diff --git a/include/boost/capy/when_any.hpp b/include/boost/capy/when_any.hpp index 0db0e7f59..014043c2d 100644 --- a/include/boost/capy/when_any.hpp +++ b/include/boost/capy/when_any.hpp @@ -542,7 +542,7 @@ make_when_any_io_homogeneous_runner( } } -/** Launches all io_result-aware homogeneous runners concurrently. */ +/** Starts all io_result-aware homogeneous runners concurrently. */ template class when_any_io_homogeneous_launcher { @@ -622,6 +622,62 @@ class when_any_io_homogeneous_launcher the failures is reported — either an error_code at variant index 0, or a child's exception rethrown. + @par Await-effects + + Takes ownership of the range, creates one wrapper coroutine per + element, then posts every wrapper to the caller's executor. All + children therefore run concurrently, each awaited with the caller's + executor and frame allocator and with a stop token owned by this + operation. + + Awaiting an empty range throws `std::invalid_argument` before any + child is started. + + The first child to await-return a zero `ec` claims the win. Claiming + the win requests stop on the operation's own stop token, which every + sibling observes through the stop token it was awaited with. A child + that await-returns a non-zero `ec`, or that exits via an exception, + does not claim the win and does not request stop. The operation keeps + waiting for a success. A stop request on the caller's stop token is + also forwarded to every child. + + The await completes only after every child has finished, regardless of + whether a win was claimed. + + @par Await-returns + An object of type + `std::variant>`, + where `PayloadT` is the payload of one child's `io_result`. + + @li Index 1 holds the winner's position in the input range paired + with its payload. + @li Index 0 holds a non-zero `error_code` when no child won, that is, + when every child failed. It is the `ec` of one of the failed + children; which one is unspecified. + + A child that succeeds after the win has already been claimed + contributes nothing: its payload is discarded. + + If no child won and the failure selected for reporting is an + exception rather than an `ec`, that exception is rethrown instead of + await-returning. The choice of child is unspecified. + + @par Await-postcondition + Every child has finished. If at least one child await-returned a zero + `ec`, the result holds index 1, unless producing the winner's payload + threw, in which case that exception is rethrown. Otherwise the result + holds index 0, or a failed child's exception is rethrown. + + @par Remarks + Supports _IoAwaitable cancellation_. A canceled child await-returns a + non-zero `ec` and so cannot win; if no child has already succeeded, + the result settles at index 0. + + @par Thread Safety + The returned task must be awaited from a single execution context. + Child awaitables execute concurrently but complete through the caller's + executor. + @param awaitables Range of io_result-returning awaitables (must not be empty). @@ -630,11 +686,13 @@ class when_any_io_homogeneous_launcher index and payload. @throws std::invalid_argument if range is empty. - @throws Rethrows the winner's exception if extracting or - move-constructing the winning payload throws (a winner was - found but its result could not be produced). - @throws Rethrows a child's exception when all children fail and the - reported failure is an exception (which child is unspecified). + + @par Exception Safety + The winner's exception is rethrown if extracting or + move-constructing the winning payload throws. In that case a winner + was found, but its result could not be produced. If all children + fail and the reported failure is an exception, that child's + exception is rethrown (which child is unspecified). @par Example @code @@ -705,6 +763,56 @@ template Only a child returning !ec can win. Returns the winner's index at variant index 1, or error_code at index 0 on all-fail. + @par Await-effects + + Takes ownership of the range, creates one wrapper coroutine per + element, then posts every wrapper to the caller's executor. All + children therefore run concurrently, each awaited with the caller's + executor and frame allocator and with a stop token owned by this + operation. + + Awaiting an empty range throws `std::invalid_argument` before any + child is started. + + The first child to await-return a zero `ec` claims the win. Claiming + the win requests stop on the operation's own stop token, which every + sibling observes through the stop token it was awaited with. A child + that await-returns a non-zero `ec`, or that exits via an exception, + does not claim the win and does not request stop. The operation keeps + waiting for a success. A stop request on the caller's stop token is + also forwarded to every child. + + The await completes only after every child has finished, regardless of + whether a win was claimed. + + @par Await-returns + An object of type `std::variant`. + + @li Index 1 holds the winner's position in the input range. The + children have no payloads, so nothing else is reported. + @li Index 0 holds a non-zero `error_code` when no child won, that is, + when every child failed. It is the `ec` of one of the failed + children; which one is unspecified. + + If no child won and the failure selected for reporting is an + exception rather than an `ec`, that exception is rethrown instead of + await-returning. The choice of child is unspecified. + + @par Await-postcondition + Every child has finished. The result holds index 1 if at least one + child await-returned a zero `ec`. Otherwise the result holds index 0, + or a failed child's exception is rethrown. + + @par Remarks + Supports _IoAwaitable cancellation_. A canceled child await-returns a + non-zero `ec` and so cannot win; if no child has already succeeded, + the result settles at index 0. + + @par Thread Safety + The returned task must be awaited from a single execution context. + Child awaitables execute concurrently but complete through the caller's + executor. + @param awaitables Range of io_result<>-returning awaitables (must not be empty). @@ -712,8 +820,10 @@ template is failure and index 1 carries the winner's index. @throws std::invalid_argument if range is empty. - @throws Rethrows a child's exception when all children fail and the - reported failure is an exception (which child is unspecified). + + @par Exception Safety + If all children fail and the reported failure is an exception, + that child's exception is rethrown (which child is unspecified). @par Example @code @@ -778,6 +888,59 @@ template Only a child returning !ec can win. Errors and exceptions do not claim winner status. + @par Await-effects + + Creates and posts one wrapper coroutine per argument to the caller's + executor, in argument order. All children therefore run concurrently, + each awaited with the caller's executor and frame allocator and with + a stop token owned by this operation. The overload requires at least + one awaitable, so there is no empty case. + + The first child to await-return a zero `ec` claims the win. Claiming + the win requests stop on the operation's own stop token, which every + sibling observes through the stop token it was awaited with. A child + that await-returns a non-zero `ec`, or that exits via an exception, + does not claim the win and does not request stop. The operation keeps + waiting for a success. A stop request on the caller's stop token is + also forwarded to every child. + + The await completes only after every child has finished, regardless of + whether a win was claimed. + + @par Await-returns + An object of type `std::variant`, where + `Pi` is the payload of the i-th child's `io_result`. + + @li Index i+1 identifies the i-th argument as the winner and holds + its payload. + @li Index 0 holds a non-zero `error_code` when no child won, that is, + when every child failed. It is the `ec` of one of the failed + children; which one is unspecified. + + A child that succeeds after the win has already been claimed + contributes nothing: its payload is discarded. + + If no child won and the failure selected for reporting is an + exception rather than an `ec`, that exception is rethrown instead of + await-returning. The choice of child is unspecified. + + @par Await-postcondition + Every child has finished. If at least one child await-returned a zero + `ec`, the result holds the index of the winning child, unless producing + the winner's payload threw, in which case that exception is rethrown. + Otherwise the result holds index 0, or a failed child's exception is + rethrown. + + @par Remarks + Supports _IoAwaitable cancellation_. A canceled child await-returns a + non-zero `ec` and so cannot win; if no child has already succeeded, + the result settles at index 0. + + @par Thread Safety + The returned task must be awaited from a single execution context. + Child awaitables execute concurrently but complete through the caller's + executor. + @param as The awaitables to race. Each must satisfy @ref IoAwaitable and is consumed (moved-from) when `when_any` is awaited. @@ -788,11 +951,12 @@ template an error_code from one of the failed children (unspecified which; no priority between errors and exceptions). - @throws Rethrows the winner's exception if extracting or - constructing the winning payload throws (a winner was found - but its result could not be produced). - @throws Rethrows a child's exception when all children fail and the - reported failure is an exception (which child is unspecified). + @par Exception Safety + The winner's exception is rethrown if extracting or constructing + the winning payload throws. In that case a winner was found, but + its result could not be produced. If all children fail and the + reported failure is an exception, that child's exception is + rethrown (which child is unspecified). @note A failing child does not cancel its siblings; `when_any` waits for a success or for every child to finish. To make a diff --git a/include/boost/capy/write.hpp b/include/boost/capy/write.hpp index 354123ab4..b97d9711d 100644 --- a/include/boost/capy/write.hpp +++ b/include/boost/capy/write.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) @@ -72,6 +73,9 @@ namespace capy { sequence represented by `buffers` ends before the coroutine finishes, the behavior is undefined. + @return A task yielding `io_result` whose second element + is the number of bytes written. + @par Remarks Supports _IoAwaitable cancellation_. diff --git a/include/boost/capy/write_at_least.hpp b/include/boost/capy/write_at_least.hpp index 1a910a3b7..5fe7c3577 100644 --- a/include/boost/capy/write_at_least.hpp +++ b/include/boost/capy/write_at_least.hpp @@ -26,11 +26,11 @@ namespace capy { This is a straightforward extension of @ref write. While @ref write transfers exactly `buffer_size(buffers)` bytes, `write_at_least` - transfers at least `n` bytes: the loop stops as soon as `n` bytes + transfers at least `n` bytes. The loop stops as soon as `n` bytes have been written, even if `buffers` has not been fully consumed. Any bytes beyond `n` that a single `stream.write_some` happens to - transfer are counted, but no further awaiting is performed to write - the remainder. + transfer are counted. No further awaiting is performed to write the + remainder. Provided for symmetry with @ref read_at_least. @@ -71,8 +71,8 @@ namespace capy { @par Await-postcondition On success the returned count is greater than or equal to `n` and - less than or equal to `buffer_size(buffers)`, and `ec` is success; - otherwise `ec` is set. + less than or equal to `buffer_size(buffers)`, and `ec` is success. + Otherwise `ec` is set. @param stream The stream to write to. If the lifetime of `stream` ends before the coroutine finishes, the behavior is undefined. @@ -84,6 +84,9 @@ namespace capy { @param n The minimum number of bytes to write. Must not exceed `buffer_size(buffers)`. + @return A task yielding `io_result` whose second element + is the number of bytes written. + @par Remarks Supports _IoAwaitable cancellation_. diff --git a/test/doc/CMakeLists.txt b/test/doc/CMakeLists.txt index 7f7788795..0f9d68f4f 100644 --- a/test/doc/CMakeLists.txt +++ b/test/doc/CMakeLists.txt @@ -11,11 +11,26 @@ # files by tag through the Antora collector, so a fragment that fails to # build here is a fragment that would render broken on the site. +# Jamfile sets extra and on for this +# directory, and the b2 leg is a hard CI gate. Mirror that posture on the +# doc-test targets so a local CMake build fails on the same warnings; a +# CMake build that cannot fail on warnings is not a check of these files. +# Scoped to this directory: the library and the main test suite keep their +# own settings. +function(boost_capy_doc_warnings_as_errors target) + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /WX) + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Werror) + endif() +endfunction() + file(GLOB SNIPPETS CONFIGURE_DEPENDS snippets/*.cpp) set(PFILES ${SNIPPETS} CMakeLists.txt Jamfile) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) add_executable(boost_capy_doc_tests ${PFILES}) +boost_capy_doc_warnings_as_errors(boost_capy_doc_tests) target_link_libraries( boost_capy_doc_tests PRIVATE Boost::capy_test_suite_main @@ -39,6 +54,7 @@ file(GLOB DOC_PROGRAMS CONFIGURE_DEPENDS programs/*.cpp) foreach(src ${DOC_PROGRAMS}) get_filename_component(name ${src} NAME_WE) add_executable(boost_capy_doc_${name} ${src}) + boost_capy_doc_warnings_as_errors(boost_capy_doc_${name}) target_link_libraries(boost_capy_doc_${name} PRIVATE Boost::capy) add_dependencies(tests boost_capy_doc_${name}) if(name IN_LIST COMPILE_ONLY_PROGRAMS) diff --git a/test/doc/programs/4c_executors_thread_pool.cpp b/test/doc/programs/4c_executors_thread_pool.cpp index d36263775..51b36c5eb 100644 --- a/test/doc/programs/4c_executors_thread_pool.cpp +++ b/test/doc/programs/4c_executors_thread_pool.cpp @@ -32,7 +32,7 @@ int main() // Get an executor for this pool auto ex = pool.get_executor(); - // Launch work on the pool + // Start work on the pool run_async(ex)(my_task()); pool.join(); // wait for outstanding work to complete diff --git a/test/doc/programs/index_page_echo.cpp b/test/doc/programs/index_page_echo.cpp index 29f37a65f..bae6b95d2 100644 --- a/test/doc/programs/index_page_echo.cpp +++ b/test/doc/programs/index_page_echo.cpp @@ -34,7 +34,7 @@ task<> echo(any_stream& stream) int main() { // In a real application, you would obtain a stream from Corosio, - // then launch the coroutine on its io_context and run it: + // then start the coroutine on its io_context and run it: // // corosio::io_context ioc; // corosio::tcp_socket stream = /* from an acceptor or connect */; diff --git a/test/doc/programs/quick_start_hello.cpp b/test/doc/programs/quick_start_hello.cpp index 68c07457d..0d5e943a4 100644 --- a/test/doc/programs/quick_start_hello.cpp +++ b/test/doc/programs/quick_start_hello.cpp @@ -34,7 +34,7 @@ int main() { capy::thread_pool pool(1); - // Launch the coroutine on the pool's executor + // Start the coroutine on the pool's executor capy::run_async(pool.get_executor())(greet()); // join() waits for outstanding work to complete; the pool diff --git a/test/doc/snippets/4a_tasks.cpp b/test/doc/snippets/4a_tasks.cpp index 86e96f9d7..d9c4b5d94 100644 --- a/test/doc/snippets/4a_tasks.cpp +++ b/test/doc/snippets/4a_tasks.cpp @@ -106,6 +106,39 @@ task<> log_message(std::string msg) } // namespace returning +namespace io_results { + +// tag::io_task[] +// io_result holds an error code `ec` plus zero or more payload +// values. io_task is just an alias for task>. + +io_task<> ensure_ready(bool ready) +{ + if(! ready) + co_return make_error_code(std::errc::not_connected); // ec converts + co_return {}; // success +} + +io_task count_ready(bool ready) +{ + using result = io_result; + if(! ready) + co_return result{make_error_code(std::errc::not_connected), 0}; + co_return result{{}, 42}; // success, carrying a value +} + +task<> use_them() +{ + // io_result 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::io_task[] + +} // namespace io_results + namespace awaiting { // tag::awaiting[] @@ -255,6 +288,26 @@ struct tasks_test BOOST_TEST(sum == 5); } + void + testRunning() + { + using returning::add; + // tag::run[] + // You have a task; run it on an executor and observe its result. + thread_pool pool(1); + auto ex = pool.get_executor(); + + int total = 0; + run_async(ex, [&](int result) { + std::cout << "Result: " << result << "\n"; // prints 5 + total = result; + })(add(2, 3)); + + pool.join(); // wait for the pooled task to finish + // end::run[] + BOOST_TEST(total == 5); + } + void testAwaiting() { @@ -303,6 +356,7 @@ struct tasks_test { testDeclaring(); testReturning(); + testRunning(); testAwaiting(); testLazy(); testChain(); diff --git a/test/doc/snippets/4c_executors.cpp b/test/doc/snippets/4c_executors.cpp index e3dd07b4d..12ac0f24f 100644 --- a/test/doc/snippets/4c_executors.cpp +++ b/test/doc/snippets/4c_executors.cpp @@ -47,10 +47,6 @@ #include #include -#include -#include -#include -#include #include #include "test_suite.hpp" @@ -82,55 +78,6 @@ task handle_client(connection& conn) } // end::handle_client[] -// The page shows the requires-expression; the real concept in -// adds nothrow copy/move -// constructibility, which the prose states separately. -namespace executor_concept_sketch { - -template -// tag::executor_concept[] -concept Executor = requires(E const& ce, E const& ce2, continuation& c) { - // Equality comparable - { ce == ce2 } noexcept -> std::convertible_to; - - // Owning context, returned as an lvalue reference to a type - // derived from execution_context - { ce.context() } noexcept; - - // Work tracking - { ce.on_work_started() } noexcept; - { ce.on_work_finished() } noexcept; - - // Scheduling - { ce.dispatch(c) } -> std::same_as>; - { ce.post(c) }; -}; -// end::executor_concept[] - -} // namespace executor_concept_sketch - -static_assert(executor_concept_sketch::Executor< - thread_pool::executor_type>); -static_assert(executor_concept_sketch::Executor< - strand>); - -// Signature sketch; the real declaration is in -// . -namespace api_sketch { - -class thread_pool -{ -public: - // tag::thread_pool_ctor[] - thread_pool( - std::size_t num_threads = 0, - std::string_view thread_name_prefix = "capy-pool-" - ); - // end::thread_pool_ctor[] -}; - -} // namespace api_sketch - struct my_executor; // tag::my_context[] @@ -227,7 +174,7 @@ struct executors_test thread_pool pool(4); auto ex = pool.get_executor(); - // Launch independent tasks directly on the pool + // Start independent tasks directly on the pool std::vector> tasks; for (int i = 0; i < 100; ++i) run_async(ex)(independent_task(i)); diff --git a/test/doc/snippets/4d_io_awaitable.cpp b/test/doc/snippets/4d_io_awaitable.cpp index bf440d1c3..5dc62731e 100644 --- a/test/doc/snippets/4d_io_awaitable.cpp +++ b/test/doc/snippets/4d_io_awaitable.cpp @@ -201,6 +201,41 @@ struct my_awaitable }; // end::my_awaitable[] +// tag::runnable_awaitable[] +// A complete IoAwaitable following the pattern above. It produces a +// value, then resumes the caller on the caller's own executor by posting +// its continuation. A real one would do this from an async completion +// callback; here the "operation" finishes immediately. +struct add_awaitable +{ + int a_; + int b_; + io_env const* env_ = nullptr; + // Defaulted, so the call site supplies only a_ and b_. + continuation cont_{}; + result_type result_{}; + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env) + { + env_ = env; + cont_.h = h; + result_ = a_ + b_; // the operation produced a value + env_->executor.post(cont_); // resume the caller on its executor + return std::noop_coroutine(); + } + + result_type await_resume() { return result_; } +}; + +// A task awaits the custom IoAwaitable and returns what it delivered. +task add_via_awaitable() +{ + co_return co_await add_awaitable{2, 3}; +} +// end::runnable_awaitable[] + // tag::stoppable_awaitable[] struct stoppable_awaitable { @@ -324,8 +359,24 @@ struct io_awaitable_test BOOST_TEST(done); } + void testRunning() + { + // tag::run_awaitable[] + // Run the task on a thread pool and observe the value the + // custom IoAwaitable delivered to the completion handler. + thread_pool pool(1); + int result = 0; + run_async(pool.get_executor(), [&result](int value) { + result = value; // the awaitable delivered 5 + })(add_via_awaitable()); + pool.join(); + // end::run_awaitable[] + BOOST_TEST(result == 5); + } + void run() { + testRunning(); testForeignBridge(); } }; diff --git a/test/doc/snippets/4f_composition.cpp b/test/doc/snippets/4f_composition.cpp index 5885ca9a1..500f03aab 100644 --- a/test/doc/snippets/4f_composition.cpp +++ b/test/doc/snippets/4f_composition.cpp @@ -337,7 +337,7 @@ struct item }; // tag::fan_out[] -io_task process_item(item const& i); +io_task process_item(item i); task process_all(std::vector const& items) { @@ -356,7 +356,7 @@ task process_all(std::vector const& items) } // end::fan_out[] -io_task process_item(item const& i) +io_task process_item(item i) { co_return io_result{{}, i.value}; } diff --git a/test/doc/snippets/4g_allocators.cpp b/test/doc/snippets/4g_allocators.cpp index 810711e2b..ff84c5065 100644 --- a/test/doc/snippets/4g_allocators.cpp +++ b/test/doc/snippets/4g_allocators.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include @@ -169,6 +170,55 @@ void process_batch(std::vector const& items) } // end::batch_allocator[] +// tag::recycling_observe_resource[] +// A memory resource that pools freed blocks by size. When a coroutine +// frame is freed, its block is kept; the next frame of the same size +// reuses it instead of allocating again -- the strategy that makes +// recycling_memory_resource fast. The two counters let the test observe +// upstream allocations versus reuse. +struct pooling_resource : std::pmr::memory_resource +{ + std::size_t upstream = 0; // blocks taken from the heap + std::size_t reused = 0; // blocks served from the freelist + + void* + do_allocate(std::size_t bytes, std::size_t) override + { + auto& blocks = pool_[bytes]; + if(! blocks.empty()) + { + ++reused; + void* p = blocks.back(); + blocks.pop_back(); + return p; + } + ++upstream; + return ::operator new(bytes); + } + + void + do_deallocate(void* p, std::size_t bytes, std::size_t) override + { + pool_[bytes].push_back(p); // keep the block for the next frame + } + + bool + do_is_equal(memory_resource const& other) const noexcept override + { + return this == &other; + } + + ~pooling_resource() override + { + for(auto& [bytes, blocks] : pool_) + for(void* p : blocks) + ::operator delete(p); + } + + std::unordered_map> pool_; +}; +// end::recycling_observe_resource[] + struct io_step { std::error_code ec; @@ -202,14 +252,13 @@ void prepare(char*, char const*, std::size_t) {} void prepare(char*, std::size_t) {} -char reply[] = "reply"; - namespace scope_bad { // tag::frame_scope_bad[] // BAD: buf lives in frame across all subsequent co_awaits task<> process(stream& s) { + char reply[] = "ok"; char buf[4096]; auto [ec, n] = co_await s.read_some(buf); co_await do_work(buf, n); @@ -225,11 +274,10 @@ namespace scope_good { // GOOD: braces end buf's lifetime before next suspend task<> process(stream& s) { - std::size_t n; + char reply[] = "ok"; { char buf[4096]; - auto [ec, n_] = co_await s.read_some(buf); - n = n_; + auto [ec, n] = co_await s.read_some(buf); co_await do_work(buf, n); } co_await s.write_some(reply); // 4K saved @@ -344,6 +392,38 @@ struct allocators_test BOOST_TEST(tasks_completed.load() == before + 1); } + void + testRecyclingObserved() + { + // tag::recycling_observe[] + // Run the same task repeatedly through one pooling resource. The + // first run has an empty pool, so its frames come from upstream. + // Once the task completes, its frames go back into the pool, so + // every later run reuses a freed block of the right size. + pooling_resource pooling; + + auto run_once = [&] + { + thread_pool pool(1); + run_async(pool.get_executor(), &pooling)(my_task()); + pool.join(); // task done: its frames are back in the pool + }; + + run_once(); // cold: fills pool + std::size_t const upstream_when_warm = pooling.upstream; + + for(int i = 0; i < 7; ++i) + run_once(); // warm: reuses pool + // end::recycling_observe[] + + // The frames really were allocated through our resource... + BOOST_TEST(pooling.upstream > 0); + // ...and the seven warm runs added no upstream allocations: every + // frame came from a recycled block. + BOOST_TEST(pooling.upstream == upstream_when_warm); + BOOST_TEST(pooling.reused > 0); + } + void testHaloPatterns() { @@ -395,6 +475,7 @@ struct allocators_test testSafeResume(); testRunAsyncPmrAllocator(); testRunAsyncMemoryResource(); + testRecyclingObserved(); testHaloPatterns(); testBatchAllocator(); testFrameScope(); diff --git a/test/doc/snippets/4h_lambda_captures.cpp b/test/doc/snippets/4h_lambda_captures.cpp index f83068972..c518b94cd 100644 --- a/test/doc/snippets/4h_lambda_captures.cpp +++ b/test/doc/snippets/4h_lambda_captures.cpp @@ -101,13 +101,15 @@ namespace capy = boost::capy; void process(socket& sock) { - auto task = [&sock]() -> capy::task<> + // The lambda is created and called on the spot. `started` holds the + // task the call returned -- not the lambda itself. + capy::task<> started = [&sock]() -> capy::task<> { char buf[1024]; auto [ec, n] = co_await sock.read_some(make_buffer(buf)); - }(); + }(); // <-- called here, so the lambda is a temporary and dies now - run_async(executor)(std::move(task)); + run_async(executor)(std::move(started)); } // end::dangling_capture[] diff --git a/test/doc/snippets/5b_types.cpp b/test/doc/snippets/5b_types.cpp index 881cf812b..3f32d5fbc 100644 --- a/test/doc/snippets/5b_types.cpp +++ b/test/doc/snippets/5b_types.cpp @@ -54,45 +54,6 @@ namespace { using namespace boost::capy; -// Interface sketches shown on the page; the sketch namespaces keep them -// from clashing with the real types. Compiling them is the test. -namespace const_buffer_sketch { - -// tag::const_buffer_interface[] -class const_buffer -{ -public: - const_buffer() = default; - const_buffer(void const* data, std::size_t size) noexcept; - const_buffer(mutable_buffer const& b) noexcept; // Implicit conversion - - void const* data() const noexcept; - std::size_t size() const noexcept; - - const_buffer& operator+=(std::size_t n) noexcept; // Remove prefix -}; -// end::const_buffer_interface[] - -} // namespace const_buffer_sketch - -namespace mutable_buffer_sketch { - -// tag::mutable_buffer_interface[] -class mutable_buffer -{ -public: - mutable_buffer() = default; - mutable_buffer(void* data, std::size_t size) noexcept; - - void* data() const noexcept; - std::size_t size() const noexcept; - - mutable_buffer& operator+=(std::size_t n) noexcept; -}; -// end::mutable_buffer_interface[] - -} // namespace mutable_buffer_sketch - // Records the size seen so the conversion fragment is observable. std::size_t processed_size = 0; diff --git a/test/doc/snippets/5e_algorithms.cpp b/test/doc/snippets/5e_algorithms.cpp index 16d094907..577181b3e 100644 --- a/test/doc/snippets/5e_algorithms.cpp +++ b/test/doc/snippets/5e_algorithms.cpp @@ -64,34 +64,6 @@ namespace { using namespace boost::capy; using namespace std::string_view_literals; -// The pages show the algorithms with function-style synopses; the real -// entities are function objects with identical call signatures. The -// declarations only need to compile. -namespace synopsis { - -// tag::buffer_size_signature[] -template -std::size_t buffer_size(CB const& buffers); -// end::buffer_size_signature[] - -// tag::buffer_empty_signature[] -template -bool buffer_empty(CB const& buffers); -// end::buffer_empty_signature[] - -// tag::buffer_length_signature[] -template -std::size_t buffer_length(CB const& buffers); -// end::buffer_length_signature[] - -// tag::buffer_copy_signature[] -template -std::size_t buffer_copy(Target const& target, Source const& source, - std::size_t at_most = std::size_t(-1)); -// end::buffer_copy_signature[] - -} // namespace synopsis - // tag::read_loop[] template task read_full(Stream& stream, Buffers buffers) diff --git a/test/doc/snippets/6b_streams.cpp b/test/doc/snippets/6b_streams.cpp index 75e54734b..75df9c9f3 100644 --- a/test/doc/snippets/6b_streams.cpp +++ b/test/doc/snippets/6b_streams.cpp @@ -106,23 +106,6 @@ static_assert(capy::WriteStream); } // namespace definition -// Scaffolds owning the conforming-signature declarations. -struct conforming_read_stream -{ - // tag::read_some_signature[] - template - IoAwaitable auto read_some(Buffers buffers); - // end::read_some_signature[] -}; - -struct conforming_write_stream -{ - // tag::write_some_signature[] - template - IoAwaitable auto write_some(Buffers buffers); - // end::write_some_signature[] -}; - task<> partial_read(test::stream& stream) { // tag::read_partial[] diff --git a/test/doc/snippets/9a_capy_layering.cpp b/test/doc/snippets/9a_capy_layering.cpp index 3f444db85..c20299fb7 100644 --- a/test/doc/snippets/9a_capy_layering.cpp +++ b/test/doc/snippets/9a_capy_layering.cpp @@ -40,17 +40,12 @@ #include #include #include -#include #include #include #include #include #include -#include -#include -#include - #include "test_suite.hpp" namespace capy = boost::capy; @@ -59,16 +54,6 @@ namespace { using namespace boost::capy; -namespace concept_layer { - -// tag::write_signature[] -template -io_task -write(S& stream, CB buffers); -// end::write_signature[] - -} // namespace concept_layer - // tag::any_stream_echo[] task<> echo(any_stream& stream) { diff --git a/test/doc/snippets/9c_read_stream.cpp b/test/doc/snippets/9c_read_stream.cpp index 19a472147..1da770a53 100644 --- a/test/doc/snippets/9c_read_stream.cpp +++ b/test/doc/snippets/9c_read_stream.cpp @@ -82,15 +82,6 @@ static_assert(capy::ReadStream); } // namespace definition -// Scaffold owning the conforming-signature declaration. -struct conforming_stream -{ - // tag::read_some_signature[] - template - IoAwaitable auto read_some(Buffers buffers); - // end::read_some_signature[] -}; - namespace composed { // tag::read_signature[] diff --git a/test/doc/snippets/9f_write_stream.cpp b/test/doc/snippets/9f_write_stream.cpp index c605a5dc6..9b301bdfa 100644 --- a/test/doc/snippets/9f_write_stream.cpp +++ b/test/doc/snippets/9f_write_stream.cpp @@ -84,15 +84,6 @@ static_assert(capy::WriteStream); static_assert(!concept_sketch::WriteStream); static_assert(!capy::WriteStream); -// The page shows the conforming member signature in isolation. -struct member_signatures -{ - // tag::write_some_signature[] - template - IoAwaitable auto write_some(Buffers buffers); - // end::write_some_signature[] -}; - // The real algorithms live in and // ; these sketches mirror the interface // the page presents and are checked against the real API below.