Skip to content

Generate [expected] wording from the annotated headers via specgen - #94

Draft
steve-downey wants to merge 8 commits into
bemanproject:mainfrom
steve-downey:wording-from-headers
Draft

steve-downey wants to merge 8 commits into
bemanproject:mainfrom
steve-downey:wording-from-headers

Conversation

@steve-downey

Copy link
Copy Markdown
Member

Summary

  • Annotate unexpected.hpp, bad_expected_access.hpp, and expected.hpp with //! specgen docblocks (Effects, Constraints, Mandates, Returns, Throws, Remarks) for every declaration, sourced from the real standard text for 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.
  • Add papers/wording/generate.sh (wired up as make wording) to turn those docblocks into wording, landed two ways:
    • papers/wording/fragments/*.tex — one file per top-level clause, for \input into 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 \input directives — the basis for a patch to source/utilities.tex in 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 --validate passes on all three headers
  • cmake --build --preset gcc-debug
  • ctest --preset gcc-debug — 1178/1178 tests pass
  • Reviewer spot-check: does papers/wording/expected.tex read correctly against the current [expected] clause and papers/expected-new.tex?

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.
Comment thread papers/wording/expected.tex Outdated
\returns
\tcode{!x.has_value() && static_cast<bool>(x.error() == e.error())}.
\end{itemdescr}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[pre-commit] reported by reviewdog 🐶

Suggested change

@github-advanced-security

Copy link
Copy Markdown
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:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

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
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants