Repository navigation
Generate [expected] wording from the annotated headers via specgen - #94
Draft
steve-downey wants to merge 17 commits into
Draft
steve-downey wants to merge 17 commits into
steve-downey wants to merge 17 commits into
Conversation
catch_discover_tests defaults to DISCOVERY_MODE POST_BUILD, which runs the freshly-linked (ASan-instrumented) test binaries during the ninja build itself to enumerate their test cases. Under the CodeQL Advanced workflow, CodeQL's build tracer injects its own LD_PRELOAD ahead of the ASan runtime, and ASan aborts immediately with "ASan runtime does not come first in initial library list", failing the build before analysis can run. Switching to DISCOVERY_MODE PRE_TEST moves the enumeration step to ctest invocation time instead of build time, so no instrumented binary runs while CodeQL is tracing the build. Verified locally: gcc-debug preset builds cleanly and `ctest` still discovers and passes all 1178 tests.
fix: defer Catch2 test discovery to ctest time to unblock CodeQL build
Add //! docblocks (Effects, Constraints, Mandates, Returns, Throws, Remarks) to every declaration in unexpected.hpp, bad_expected_access.hpp, and expected.hpp, sourced from the real standard text for the already-standardized members and from papers/expected-new.tex for the expected-over-references additions (unexpected<E&>, expected<T&,E>). specgen generate --validate passes cleanly on all three headers, so the headers are now a source of truth specgen can turn into wording directly.
Add papers/wording/generate.sh (wired up as `make wording`), which runs
specgen against the three headers and assembles the result two ways:
- papers/wording/fragments/*.tex: one file per top-level clause, for
\input into a standalone paper (specgen numbers a fragment's \rSec
markers one level deeper than written, so a paper's own
\rSec1[expected]{Expected objects} supplies the level these assume).
- papers/wording/expected.tex: the same content concatenated in real
standard clause order, at the draft's own absolute numbering, with no
\rSec2[expected] wrapper and no \input directives -- the basis for a
patch to source/utilities.tex in the actual C++ working draft
(github.com/cplusplus/draft), where [expected.general] and
[expected.syn] are untouched and only the subclauses from
[expected.unexpected] on are replaced/extended.
[expected.general] and [expected.syn] are prose, not generated from any
one declaration; they stay hand-authored in papers/expected-new.tex.
| \returns | ||
| \tcode{!x.has_value() && static_cast<bool>(x.error() == e.error())}. | ||
| \end{itemdescr} | ||
|
|
There was a problem hiding this comment.
[pre-commit] reported by reviewdog 🐶
Suggested change
Contributor
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
…s document A newer specgen enforces that a name used in one generate invocation's wording must be documented in that same run, which broke generating expected.hpp on its own: it uses unexpected, unexpect, unexpect_t, reference_constructs_from_temporary_v, and bad_expected_access, all declared in the other two headers. Wrap expected.hpp's existing #includes of unexpected.hpp and bad_expected_access.hpp in a gathered \rSec2[expected.syn] region so specgen treats all three headers as one document, and add a throwaway \rSec2[expected.detail] marker so the exposition-only helper templates above [expected.expected] don't bleed into [expected.bad]'s fragment once nothing else is there to close the section. generate.sh now runs specgen once on expected.hpp instead of three times, mapping the same six clause fragments as before plus the two new non-clause fragments, excluded exactly like the previous per-header root fragments were. unexpected.tex/bad.tex/bad-void.tex regenerate byte-identical to the prior per-header runs; object.tex/void.tex/ref.tex pick up only this specgen version's own docblock-element reordering, confirmed by equal itemdecl counts before and after.
…ected.tex The per-fragment loop echoed a separator after every fragment, including the last, leaving a second trailing newline that pre-commit's end-of-file-fixer then had to strip on every regeneration. Insert the separator between fragments instead.
steve-downey
marked this pull request as draft
September 10, 2026 11:54
generate.sh relied on specgen's hard-coded latex base depth of 3, and the banner and README rationalized the result as "one level deeper than written, matching the real standard's absolute numbering". It does not match: in source/utilities.tex [expected] is \rSec1 and [expected.unexpected] is \rSec2, so emitting [expected.unexpected] as \rSec3 and its subclauses as \rSec4 numbered them one level too deep for both consumers -- a patch against the draft, and an \input into a paper whose own \rSec1[expected] makes its sibling clauses \rSec2. specgen now reaches that depth from the command line (specgen#97), so pass --base-section-depth 2 and drop the compensation from the prose. Every \rSec marker a clause writes in a header now means what it says. Regenerating changes nothing but the heading levels: the stable names and their nesting are unchanged, and every clause the draft already has now sits at exactly the draft's own level. The other two specgen workarounds stay, both still load-bearing: - the gathered \rSec2[expected.syn] region over expected.hpp's #includes. specgen#109 (validate across the union of a paper's documents) removes the unexpected/bad_expected_access findings, but a split run still reports unexpect and unexpect_t (\omit'd here, since they belong to the hand-authored [expected.syn]) and reference_constructs_from_temporary_v (a using-declaration, which contributes no documented name) as foreign. - the throwaway \rSec2[expected.detail] marker. Without it the five exposition-only helpers above [expected.expected] still bleed into [expected.bad]'s fragment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 15, 2026
2b737a4 left two workarounds in place and spelled out why each was still load-bearing. specgen has since closed both underlying issues, so both come out. specgen#113 (PR #116) adds a sibling document's \elsewhere and exposition-only using-declared names to the paper-wide documented set, so a split run no longer reports unexpect, unexpect_t, or reference_constructs_from_temporary_v as foreign. The gathered \rSec2[expected.syn] region over expected.hpp's #includes is gone: generate.sh now emits one IR per header and renders the three together with --from-ir, so validation still sees the union of documented names. specgen#114 (PR #115) stops a declaration between two clauses landing in the preceding one, so the throwaway \rSec2[expected.detail] marker is gone too. The exposition-only helpers land in the root fragments, which generate.sh already discards: expected.root.tex holds converts-from-any-cvref, is-expected-specialization and unexpect-dangles-v, expected.unexpected.root.tex holds is-unexpected-specialization. No helper leaks into a kept fragment. Two marker corrections fall out of #116 actually acting on these declarations: - unexpect_t and unexpect move from \omit to \elsewhere. They belong to the hand-authored [expected.syn], and \elsewhere is what promises that to a sibling document rather than hiding them outright. - reference_constructs_from_temporary_v moves from \expos to \elsewhere on both arms of its #ifdef. \expos on a using-declaration used to be a no-op; now that it contributes a name, every use started rendering as an exposition-only *reference-constructs-from-temporary-v*. It is the real std:: trait from [meta.rel], not a library invention, so the wording has to keep the plain spelling. With \elsewhere, fragments/unexpected.tex regenerates byte-identical to before. Also reattach [expected.ref.assign]'s trivial copy assignment docblock. A blank line had crept in between it and the declaration, silently dropping the whole itemdescr -- five paragraphs, no diagnostic. Moving the "// Copy assignment (trivial path)" comment above the docblock, as the non-trivial path a few lines down already does, restores them, and also stops that comment being swallowed into the end of the Remarks prose, which is how it read before. Regenerating leaves the section set, the itemdecl count (167) and the itemdescr count (171) unchanged. A sorted-line diff against the previous wording is exactly: the two [expected.ref.assign] \remarks merged into one, the swallowed-comment paragraph gone, and "// Copy assignment (trivial path)" added to the synopsis. The rest of the diff is declaration reordering from the #114 fix. make papers still builds D4280R0.pdf, 116 pages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
papers/wording/generate.sh existed because specgen could not express a three-header paper in one command: it ran three `generate --emit-ir` invocations into a temporary directory, one `render --split`, renamed the fragments, dropped the ones it did not want, and concatenated the rest. And nothing depended on any of it -- `wording` was .PHONY with no prerequisites and `papers` did not depend on it at all, so the PDF could be built from wording three commits stale and look exactly like a PDF built from current wording. specgen now takes the whole paper in one invocation and reports what it read, so the script becomes a makefile rule with real prerequisites: - one `specgen generate` over all three headers, so `--validate` sees the paper-wide union of documented names in a single parse (~2s); - `--depfile` writes what it read to papers/.deps/wording.d, which the makefile `-include`s. That list has include/beman/expected/config.hpp in it -- a header none of the three names on the command line, which the old script had no way to know about and which a hand-written prerequisite list would forget; - `make papers` now depends on `wording`, closing the last open edge: headers -> wording -> PDF. The fragments keep their stable names (expected.bad.void.tex rather than bad-void.tex), which deletes the rename step; nothing `\input`s them, so this costs nothing. The regenerated fragments are byte-identical to the ones they replace, and expected.tex differs only in the comment at its top, which now lives in papers/wording/preamble.tex. What did not move into specgen is the clause order. bad_expected_access<void> is the base class of bad_expected_access<E>, so the header has to define the specialization first, while the draft states the primary template first -- the paper's order is not the headers' and cannot be derived from them. $(WORDING_CLAUSES) is where that one divergence is now written down, instead of being implicit in the order a script happened to cat six files. Requires specgen with multi-header `generate` and `--depfile`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`-include papers/.deps/*.d` is a bare glob, unlike the three other depfile includes in this makefile, which all wrap theirs in $(wildcard). Two things follow from that. When the glob matches nothing, make keeps it as a literal target name and tries to remake it -- through `.DEFAULT`, which hands it to cmake. The depfile is only written by an actual regeneration, so on a fresh clone, where the committed fragments are already up to date, it is never written: every `make` invocation, `make help` included, first runs `uv run cmake --build ... --target 'papers/.deps/*.d'` and prints an error about it. `-include` swallows the failure, so nothing breaks; it is just noise that never goes away on its own. And papers/.deps/ is not ours alone -- papers/Makefile already points latexmk's -deps-out at it. Once anyone builds the PDF, the glob pulls D4280R0.pdf.d into the top-level makefile, where its papers/-relative paths (../include/..., ./wg21.bib) resolve against the repository root instead: 293 phantom targets at paths that do not exist, and `make D4280R0.pdf` at the root answering "nothing to be done" rather than falling through to cmake. $(wildcard) fixes the first, naming wording.d fixes the second. Also record the GNU Make floor in papers/wording/README.md. The one-invocation rule is a grouped target, which 4.3 introduced; 4.2 parses `&:` as something else and would run specgen once per fragment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
specgen's collect_inclass_items previously earned a defaulted/deleted member a standalone itemdecl whenever its derived elements were non-empty, but a template-head requires-clause now derives a Constraints element even when nothing was authored — so a deleted overload with only that derivation got a standalone item that just restated its own declaration. Fixed upstream in specgen (issue #121); this regenerates papers/wording/ against the corrected binary. The requires-clause is also now stripped from each item declaration in favor of the derived Constraints paragraph, matching the draft's own style, which accounts for most of the diff.
…nce-E converting copy/move constructors expected(const expected<U,G>&) and expected(expected<U,G>&&), for the overload set enabled when E is a reference type, require is_constructible_v<T, const U&> / is_constructible_v<T, U&&> in code (cvt-copy-ctor-ref's requires-clause) but the authored Constraints element never said so — only is_reference_v<G> and is_convertible_v<G, E> were documented. Nothing else in the docblock accounts for T's constructibility from U, unlike is_reference_v<E>, which the existing Remarks paragraph already covers in prose. Add the missing conjunct, worded the way the sibling value-E overload group above states the same kind of constraint.
…erenced type unexpected<E&>'s class-level Mandates only named an array type or a specialization of unexpected as ill-formed instantiations, but the class also static_asserts is_object_v<E> — banning a function-type referent (e.g. unexpected<F&> for a function type F) — which the prose never mentioned. The primary unexpected<E> template's Mandates already lists the equivalent non-object-type case; the E& partial specialization's didn't carry it over.
…ates expected<T&,E>'s class-level Mandates said only "T shall be an object type that is not an array type," but the class also static_asserts that T is not in_place_t, not unexpect_t, and not a specialization of unexpected -- three real restrictions the prose never mentioned. All three are object types and non-array, so the existing wording wouldn't have excluded them. The primary expected<T,E> template's Mandates already lists the equivalent exclusions for its value type; this specialization's didn't carry them over.
…ference-error unexpected constructor/assignment ref-cvt-unexpected-ctor-ref and ref-cvt-unexpected-assign-ref both documented is_convertible_v<G, E> as the Constraints, but their requires-clauses actually gate on is_constructible_v<E, G>; is_convertible_v<G, E> only governs whether the same declarations are explicit (the explicit(...) specifier just above each). Since is_convertible implies is_constructible but not the reverse, the wording as written over-restricted participation: a G for which E has only an explicit converting constructor would read as excluded even though the code accepts it. The identically-shaped pairs in the primary template (cvt-unexpected-ctor-ref, cvt-unexpected-assign-ref) and the void specialization already state the correct predicate; only the T& specialization had it wrong, in both places.
…uctor overload split ref-cvt-copy-ctor-ref documented is_reference_v<E> nowhere, unlike every other reference-E-gated overload in this specialization and its sibling in the primary template's cvt-copy-ctor-ref, both of which carry a Remarks paragraph saying the overload participates only when E and G are both reference types. Not a wording error -- the Constraints element is accurate either way -- but the omission was an inconsistency against the paper's own established convention of explaining the split instead of restating is_reference_v<E> in every Constraints. Add the matching Remarks.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
unexpected.hpp,bad_expected_access.hpp, andexpected.hppwith//!specgen docblocks (Effects, Constraints, Mandates, Returns, Throws, Remarks) for every declaration, sourced from the real standard text for already-standardized members and frompapers/expected-new.texfor the expected-over-references additions (unexpected<E&>,expected<T&,E>).specgen generate --validatepasses cleanly on all three headers.papers/wording/generate.sh(wired up asmake wording) to turn those docblocks into wording, landed two ways:papers/wording/fragments/*.tex— one file per top-level clause, for\inputinto a standalone paper.papers/wording/expected.tex— the same content concatenated in real standard clause order at the draft's own absolute numbering, with no\rSec2[expected]wrapper and no\inputdirectives — the basis for a patch tosource/utilities.texin the actual C++ working draft (cplusplus/draft), where[expected.general]and[expected.syn]stay untouched (hand-authored prose, not generated from any one declaration) and only the subclauses from[expected.unexpected]on are replaced/extended.The headers are now a source of truth specgen can turn into wording directly, instead of the wording and the implementation being able to drift apart.
Test plan
specgen generate --validatepasses on all three headerscmake --build --preset gcc-debugctest --preset gcc-debug— 1178/1178 tests passpapers/wording/expected.texread correctly against the current[expected]clause andpapers/expected-new.tex?