Skip to content

Shorter spellings for the public API, and examples that use them - #9

Merged
christianparpart merged 59 commits into
masterfrom
feature/concise-spellings
Sep 30, 2026
Merged

christianparpart merged 59 commits into
masterfrom
feature/concise-spellings

Conversation

@christianparpart

@christianparpart christianparpart commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Working with formula-cpp read as verbose. The examples show where that came from: a few shapes repeated hundreds of times.

  • Exact numbers: formula::Rational { 273, 10 }, and five example files that define their own rat() helper.
  • Inputs: Measured<Q> { Rational { n } }.
  • Reading a result: r.has_value() && r->is_value() && r->measurement().value().
  • Tracing: a trace built by hand, with Trace<> and RecordingSink, then the same formula evaluated again for its value.
  • Printing: printf with %.*s, .c_str(), static_cast<long long> and .to_double().
  • Enumerations: turned into words by hand.
  • Rounding: the same rounding restated at every use.
  • The result quantity: named again at every call.

This branch adds a short spelling for each. Every existing spelling keeps compiling.

  • 27.3_r, an exact decimal literal. It is exact or it does not compile: 0x1F_r, 017_r and a value Rational cannot hold are refused.
  • measured_series<Q>(127, 10.3_r, not_measured), and band(83.7_r, 97.3_r) / breakpoint(12.7_r).
  • number_of(x): the number a result holds, or nothing.
  • describe() and std::format for outcomes, units, dimensions and every described enumeration.
  • symbol_of<Q>(), and render(x, options) without a placeholder vocabulary.
  • traced(…), and an explain_* twin for every evaluation verb.
  • trace_of<Q>(e, env), trace_of(bound, env) and trace_of_si(e, env): the trace alone, whether the evaluation succeeds or fails.
  • DecimalRounding, SignificantRounding and declared_rounding, which name a rounding once.
  • Throwing twins for the Measured operations.
  • yields<Q>(expr): a formula bound to its result quantity, named once where the formula is written. Every verb that takes a result quantity takes it.

Misusing one of these draws one formula: message, not an overload list:

  • a bound series, rejection or retry handed to a single-value verb;
  • a bound formula used as an operand or compared;
  • a relabelled or doubly bound formula;
  • a floating-point band or breakpoint argument, which used to be truncated silently;
  • a conversion between dimensions that differ, which used to fail only at run time.

Every example, and every guide checked against the examples, is rewritten with these spellings. They print with std::println, and they handle errors explicitly: every checked_ result is checked before use, either at run time or, for a constant, with static_assert. Measured over the 18 example programs, master against this branch:

master this branch
lines 4705 4402
uses of a private rat() helper 155 0
Rational { … } 190 25
printf 361 0 (std::println: 371)
measurement().value() 60 0
RecordingSink (hand-built traces) 23 0
.c_str() / %.*s / .to_double() 217 / 47 / 18 0 / 0 / 0

None of the library's rules change:

  • a number is exact, or refused;
  • an absent value is not zero;
  • the author names the result;
  • no unit is guessed.

Breaking changes. Two Changed entries in the CHANGELOG need attention on upgrade:

  • Argument-dependent lookup now reaches the new names. A consumer's own template of the same name and shape (convert_to, within_bounds, …) is displaced silently, and a helper of another shape becomes ambiguous.
  • checked_convert_to across dimensions no longer compiles.

GCC 14 is now the oldest supported GCC.

Verified on every compiler: cl and clang-cl in debug and release, g++-14, and clang-20 in debug, release and UBSan, 2087 tests each, refusal tests included. Doxygen 1.9.8 and mkdocs build --strict also pass, and so do all CI legs, including macOS AppleClang and install-and-consume.

The design is in docs/superpowers/specs/2026-09-30-concise-spellings-design.md.

Christian Parpart added 30 commits September 30, 2026 17:09
The examples repeat a handful of shapes that make the library read as
verbose: exact numbers spelled as Rational { n, d }, a value dug out of a
result through three accessors, a trace built by hand, printf noise, and a
rounding restated at every use. The design and the plan add short
spellings for each, keeping every existing one.

examples/simple.cpp now prints with std::println, the spelling every
example moves to.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A decimal such as 27.3 written as a double is the binary fraction nearest
it, not 273/10, and Rational deliberately refuses a double for that reason.
That left Rational::from_decimal(273, -1) as the only exact way to write a
decimal, which is hard to read in a formula. `27.3_r` reads the spelling
itself, so nothing is rounded on the way in.

The literal is consteval and takes the raw spelling: integers, a fraction
with a leading or trailing point, an exponent that scales exactly, digit
separators, and trailing fractional zeros that cost nothing. A spelling
Rational cannot hold, or one that is not a decimal (hexadecimal, binary,
and a leading zero, which C++ reads as octal), fails to compile and the
diagnostic names the guard that was reached.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…series

A series of measurements had to spell every element as
Measured<Q> { Rational { ... } }, and a point that was not measured as
Measured<Q>::absent(), although the quantity is already stated once in
measured_series<Q>. Each element may now be a Measured<Q>, anything a
Rational is built from (127, 10.3_r, a Rational), or not_measured.

A wrong element draws one message: a Measured of another quantity, a
string or a bool is refused by measured_series in its own words, and a
double or a wide unsigned integer by Rational's, with the series' own
check silent.

band(low, high) and breakpoint(key) likewise take exact numbers, so
band(83.7_r, 97.3_r) and breakpoint(12.7_r) read as the numbers they are.
Every earlier spelling still works. The header comments that called a
rat() helper which does not exist now show the real spellings, and the
line numbers the documentation quotes from the headers follow the move.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A hexadecimal spelling with an E in it, such as 0x1E, looks like it has an
exponent, so the leading-zero check is skipped and only the digit check in
the loop refuses the x. Nothing pinned that check: without it the x would
be read as a digit and the literal would yield a wrong number silently.
It now has a negative case and a comment saying why it must stay.

Also pin the spellings a reader is most likely to get wrong: 00.5 and 0e3
are exact, 1e-18 is the largest exact denominator, and 1e19 is refused by
name however it is spelled.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A bool is not an exact number: Rational is deliberately not built from
one, so `measured_series<Q>(127, true)` must be refused, and by one
message rather than an overload list or a second refusal from the
conversion that would follow. The sibling cases pin a Measured of
another quantity, a double and a wide unsigned integer; a bool was the
one wrong element left unpinned.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Reading a number out of a result meant choosing between is_value(),
measurement().value() and the std::expected around it, and forgetting one
check turned an absent number, a verdict or an error into a throw or a
zero. number_of returns a std::optional<Rational> for a Measured, an
Outcome, a checked_evaluate result, an Evaluated<Rational>, a RetryOutcome
and a RejectionOutcome: the number when there is one, nothing otherwise.

Because comparing an empty optional with a number is false,
number_of(checked_evaluate<Q>(...)) == 0.5_r is a complete check. Zero is
not used for "no number", since zero is a measurement.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…e kinds and failure sites

Five public enums had no words of their own, so every caller that wanted to
print one wrote a switch: the two examples each carried a local
describe(ConstraintOutcomeKind). The library now gives each a lowercase
phrase with no trailing punctuation, as describe(ArithmeticError) does.

The two local helpers are removed, since an unqualified call would now be
ambiguous with the library's by argument-dependent lookup. Their words equal
the library's, so the examples print exactly what they did. The CHANGELOG
records the rule for a consumer's own describe of these enums.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…fault symbol_of's vocabulary

The examples and a consumer's own output print results, units, dimensions
and the words of an enumeration, and each of those needed a hand-written
helper. std::format now writes an Outcome<Q> (a value as a Measured, an
empty one as "(not measured)", a verdict or invalid one as its label), a
Unit (its symbol), a Dimension (L^2 M^-3, L^(1/2), (dimensionless)) and
every enumeration with a describe(), which lists itself beside that
describe() so format.hpp includes no further header.

symbol_of<Q>() without a vocabulary is the declared symbol, and render()
and document() take RenderOptions without a vocabulary that renames
nothing. Two hygiene checks learn the new spellings: the forwarding
overloads that pass RenderOptions on, and format.hpp's use of <string>.
The documented diagnostic quoted from format.hpp follows its line.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ensions where it is written

Callers who would only rethrow the error of checked_convert_to,
checked_round_to_declared or checked_within_bounds on a Measured had no
short spelling: convert_to, round_to_declared and within_bounds now throw
ArithmeticException where the checked form returns an error.

checked_convert_to also knew both dimensions where the call is written but
only refused a mismatch at run time, as DomainError. It now refuses a
conversion between quantities of different dimensions, with or without a
value present, when the call is compiled, with one message. The tests that
pinned the run-time error become negative compile tests.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… one

format.hpp's overview still described two formatters, so the rule to include
it wherever a type is formatted, and the warning that a consumer's own
std::formatter defines the same entity twice, did not reach Outcome, Unit,
Dimension or the described enumerations. It now names all of them, the
Unit, Dimension and enumeration formatters carry the ownership sentence, and
the changelog records that a consumer's own formatter for these types
collides with the library's.

The vocabulary tripwire could not see the two spellings that resolve the
default vocabulary without naming it, symbol_of<Q>() and a two-argument
render or document call, so it refuses both. The enumeration formatter reads
describe() through a helper inside formula::detail, so a consumer's global
named describe cannot hide it.

Tests pin all twelve enumeration rows and the absence of one for an
enumeration with no describe(), the padding and default alignment of an
outcome's label, a named base in a dimension, and the refusal of a bad spec
for an outcome. The probe now covers document with options. The five headers
that specialise formats_by_describe include error.hpp, and the new rows share
the existing detail blocks in series.hpp and snap.hpp.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… explain twin

Only formulas, series and retries could be traced in one call; a method, a
curve, a rejection, a constraint or a conformity check needed a hand-built
Trace and RecordingSink around the verb, which every caller repeated.

traced runs any evaluation that takes a sink with a RecordingSink and returns
what it returned, failure included, beside the steps recorded. Seven twins
(explain_method, explain_check_method, explain_curve, explain_rejection,
explain_check, explain_check_all, explain_conformity) give each remaining
verb the same one-call form, and explain_series and explain_retry are now
written through traced with the same result types as before.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A method that rounds the same way in several places repeated the same
three arguments -- a unit, a number of places, a mode -- at every use.
DecimalRounding and SignificantRounding name them once, and
declared_rounding takes the places a unit already declares.

Every public factory that took the three arguments separately gains an
overload taking the named value and building the very same type:
rounded, rounded_to_digits, rounding_rule, with_rounding (with and
without a citation, the latter still refused), rounded_output,
rounded_sqrt and rounded_elementwise. For rounded_elementwise the named
places apply to every element of the series. Every earlier spelling
stays, and a unit of the wrong dimension still draws the rounding
node's one message.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ary it is given

Only explain_method was tested with a renamed quantity, so a twin that
dropped the vocabulary from its traced call would have written the default
symbols and passed. Each remaining twin now has a case comparing its
rendered trace with a hand-built RecordingSink over the same vocabulary, and
asserting that the renamed symbol appears there and not in the default one.

Also require a verdict to be present before explain_check_all's test reads
its label.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The consumer-globals probe instantiated only rounded<R> and
declared_rounding, so a parameter name in any other new overload could
shadow a consumer's global unseen. It now instantiates each of them, and
doing so found one: filling a value-initialised PlacesTable made cl
instantiate a helper that warned about a hidden global `i`. The table for
rounded_elementwise<R> is now built from an index pack.

The single-value refusal of rounded_elementwise<R> gets its own negative
case, so deleting it or splitting its message is noticed. with_rounding<R>()
forwards to the three-argument refusal instead of repeating its text,
declared_rounding reuses declared_decimals and says what a unit declaring
decimals outside -18 to 18 does, and the tests for rounded_sqrt,
rounded_output and with_rounding also check a second, different rounding
so an overload that ignored its value would fail.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Every verb that evaluates a formula is told its result quantity at every
call, because the library never deduces one from an expression: a
dimension does not name a quantity. So the quantity was repeated at every
call site, where a call could name another quantity of the same dimension
and nothing would notice.

yields<Q>(expression) names the quantity once, where the formula is
written. evaluate, checked_evaluate, checked_evaluate_series,
checked_evaluate_rejection, explain, checked_explain, explain_series,
explain_rejection and define take the bound formula and take Q from it;
render and document write the formula it holds. Nothing is deduced from
the expression. Q is still the author's, checked against the dimension
the expression computes where it is bound, with checked_evaluate's own
message, and a quantity named again at a call is accepted only when it is
Q.

A verb handed a refused bound formula asks nothing further, so one
mistake draws one message. Where a verb's own check would add a second,
differently worded one -- define and the rejection's evaluation and
trace -- a negative case pins the gate.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The project prints with std::print and std::println everywhere outside
the CRT-failure handler, so the gallery generator, the census report and
the census tests move off printf. Their output stays byte-identical.

support/fail_without_dialogs.cpp keeps std::fputs, now with a comment
saying why: its invalid-parameter handler is noexcept and must neither
allocate nor throw, which std::print may do.

The package consumer under test/package keeps printf: the Package
workflow builds it on ubuntu-24.04 with the default g++ 13, whose
standard library has no <print>.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The gallery generator names the stderr macro, which <cstdio> defines, so
it includes that header itself instead of relying on <print> to pull it
in.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
traced() calls std::invoke and constrains on std::invocable, but trace.hpp
included neither <functional> nor <concepts>. MSVC reached them through
other headers; libstdc++ and libc++ do not, so g++ 14 and clang 22 failed
with "invoke is not a member of std". std::invoke_result_t,
std::remove_cvref_t and std::move were already covered by <type_traits> and
<utility>.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… package test

The library supports only the newest GCC, the one the main build pins
(g++ 14), so there is no reason to keep older GCC working. The Package
workflow's Linux leg now installs g++-14 and builds both the library and
the consumer with it, and the README and changelog say that older GCC is
not supported.

With that, the package consumer prints with std::println like every other
program; its output is unchanged.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A bound formula may be evaluated for the quantity it names and no other,
and nine verbs each enforce that on their own. Only one of them was
compiled by a negative case, so eight refusals could have been deleted
without any test noticing. One case now asks each verb for a different
wrong quantity, and counts one message per verb.

Compiling those refused calls found a second message. Each verb gated on
the value of the check that had just failed, and clang-cl then compiled
both branches of define's gate, whose two returns deduce different types.
Each verb now states the refusal, and gates on a plain predicate that no
failed check can spoil.

A formula bound twice was not refused, and the verbs forwarded it to the
inner binding, whose answer is for another quantity: one mistake gave a
cascade of conversion errors. It is now refused where it is written, even
for the same quantity, since a bound formula is the top of a formula.

Tests also observe that the sink and the vocabulary handed to a bound
formula's verb reach the formula, and that define of a bound series draws
the series refusal. The consumer-globals probe now calls every bound
overload, and yields.hpp includes what it uses directly.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A bound formula around a bound formula is refused where it is written,
and every verb given it is meant to say nothing more. The series and
rejection verbs only take a bound series or a bound rejection, so a
nested one reached none of their overloads: the refusal was followed by
"no matching function", two messages for one mistake.

Each of the four -- checked_evaluate_series, explain_series,
checked_evaluate_rejection and explain_rejection -- now takes the nested
formula too, and answers it with a placeholder that is never seen, so the
refusal is the only message. Two negative cases, one for a series and one
for a rejection, hand the nesting to those verbs and count one message.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…spellings

The seven small examples now read the way the library is meant to be
written. A value is supplied as `Measured<Q> { 180 }` or `0.5_r`, a formula
evaluated more than once names its result once with `yields`, a result is
checked with `number_of`, and a conversion that cannot fail uses the
throwing twin. Every line prints with std::println, formatting the
library's own values: a Rational, a Measured with its unit, an Outcome, a
Unit and an enumeration's words.

Printing the values themselves changes some lines, each on purpose. The
water/cement ratio prints as 0.6 rather than 0.600000 and says whether it
was derived or manually entered, the circular area is marked as rounded, a
converted volume carries its unit, and an absent measurement reads
"(not measured)". exact_numbers gains the self-check it lacked, and its
test now pins its closing "all checks passed: yes" beside
"ten tenths == one: yes".

Every README snippet is now consecutive lines of an example, beside that
example's real output, which also ends the README calling evaluate<> where
citations.cpp calls checked_evaluate. The documentation site's front page
opens with the same two blocks and output. The guides quote the new lines,
name what their snippets use, and show the short spellings where they
documented the long ones. docs/numeric-headroom.md's census table is
regenerated from the rewritten examples.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…t spellings

dimensions_and_units now prints every dimension, number and bounds verdict
with std::println. The formatters in format.hpp for a Dimension, a Rational
and an enumeration replace its own print_dimension and its printf calls.
Its inputs are _r decimals. It rounds to the kilogram's declared precision
with the throwing round_to_declared, because nothing in it can fail. Every
line of its output is unchanged.

display now writes its values as 25.5_r and Measured<Q> { 144 }. It supplies
the dish's weighings through measured_series. It traces with explain and
explain_conformity, instead of assembling a Trace and a RecordingSink. It
renders and documents with RenderOptions alone, and prints with std::println.
A new sixth section formats an Outcome, a Unit, a Dimension and an
enumeration. The Outcome rows cover a value, an empty outcome and a verdict,
whose label is right-aligned. Each row is printed beside its call and
checked. The first five sections print exactly what they printed before.

docs/display.md quotes the new lines. Its new section, "Formatting outcomes,
units, dimensions and enumerations", says that format.hpp must be included
wherever these are formatted. Its table and output block are checked against
the program, like the rest of the page. docs/dimensions.md quotes the new
rounding call and says what formats a Dimension.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…truncating it

`breakpoint(1.5)` compiled and meant `breakpoint(1)`: a conversion from
double to std::int64_t beats the user-defined conversion to Rational, so
the integer overload took the call and dropped the fraction without a
word. `breakpoint(1.5, 2)` and the four-argument `band(12.7, 1, 17.3, 1)`
did the same. A key or bound that reads as an exact decimal and silently
becomes a different number is the one thing this library must not do.

Each form now has an exact-match overload for floating-point arguments
that refuses in the library's own words and names the spellings that work
(`12.7_r`, `Rational { 127, 10 }`, `breakpoint(127, 10)`). Integer calls
do not satisfy the constraint and still take the old overloads. The line
numbers the documentation quotes from band.hpp follow the move.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…amples

A std::expected that holds an error must not be dereferenced, and an
example is where a reader learns how to read one. The expressions example
now checks each checked_evaluate before it reads the outcome: its three
compile-time results with a static_assert, so an error stops the build, and
the water/cement ratio with an if that prints the error's words and fails.
The quantities example goes back to checked_convert_to,
checked_round_to_declared and checked_within_bounds, and checks each the
same way, rather than trading the check for the throwing spellings to be
shorter. Nothing they print changes.

The README and the guides quote the guarded lines, and the numbers guide
says that std::format writes a Rational's exact decimal where it has one,
and gives each block that uses _r its using-directive. The tracing guide
names the fixtures its vocabulary snippet uses, and the census test says
how the expressions example now writes the circular area it copies.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ument

The refusal of a floating-point key or bound was pinned only for the
first argument of each form. The other positions are separate arms of the
constraint, so dropping one of them would have let a double be truncated
again without any test noticing. One case per remaining position is added,
plus one with two floating-point arguments of different types, which must
still draw a single message; a guard instantiated once per argument would
draw two.

The changelog entry now says the message names the exact spelling for
the call that was made, which is true of both messages, and each refusing
overload's comment says that only a floating-point type is refused. The
line numbers the documentation quotes from band.hpp follow the comments.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ns and display examples

The examples handle errors the way the library means them to be handled. A
std::expected is checked before it is read, and nothing swaps in a throwing
spelling to be shorter. The display example now traces with checked_explain,
and checks each of its three evaluations before it reads the outcome or the
trace. On an error it prints the error's words and fails. The dimensions
example rounds with checked_round_to_declared and bounds-checks with
checked_within_bounds, and guards each result the same way. It no longer
unwraps the bounds checks unchecked.

The display example's outcome rows now name the checked result as
moisture->outcome. The program prints each call as its source spells it, so
those three reference lines change with it. Nothing else either program
prints changes.

The guides quote the guarded lines and say what the std::expected holds. The
display guide says that its verdict outcome is built directly, as a rejection
would yield it, and why some outputs are quoted. The dimensions example
writes its million litres as 1'000'000_r. The README names the
using-directive that its 0.5_r needs.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ples in the short spellings

The three examples and the guides that quote them now read the way the
library is meant to be used: exact decimals with _r, inputs as
Measured<Q> { n }, bands as band(low, high), a rounding named once as a
DecimalRounding or SignificantRounding, a formula evaluated repeatedly
bound with yields<Q>, and values checked with number_of. The hand-built
trace in the constraints example is explain_check, and the lookup
example's trace keeps a miss's derivation through checked_explain. Every
checked result is tested before it is read, and everything prints with
std::println.

Values that used to print through to_double() or as a fraction now print
as the exact decimal the library formats (26 mm, 0.448, 1.051, 36.28052
MPa); the guides quote the new lines. The rounding guide gains a
SignificantRounding example and one declaration style per snippet, and the
README and lookup-table guide show the two-argument band().

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… short spellings

The three examples and the guides that quote them now read the way the
library is meant to be used: exact decimals with _r, inputs as
Measured<Q> { n }, series as measured_series<Q>(130, 210, 95), bands as
band(low, high), the method's rounding named once as a DecimalRounding,
and a formula evaluated repeatedly bound with yields<Q>. The hand-built
trace blocks are explain_method, explain_check_method, explain_series,
explain_curve and explain_conformity, or traced around a verb that has
no twin, and each evaluation is run once and read for both its value and
its derivation. The local rat, m<Q>, exact, trace and outcome_word
helpers are gone; constraint outcomes print with the library's own
formatting.

Every checked result is tested before it is read, and everything prints
with std::println. The programs print exactly what they printed before,
so the guides' quoted output is unchanged; only their quoted code moved.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ings

The constraints example's header comment lost its last line, and the
rounding example carried its include block twice. The top-row band is now
band(173_r, 211.1_r), like every other decimal bound, and the lookup guide
says why its gap test spells the bounds as integer pairs.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Christian Parpart added 27 commits September 30, 2026 21:50
…ords and methods examples

Sites that only read a trace now call trace_of, trace_of<Q> or
trace_of_si instead of wrapping a checked evaluation in traced. The
gated reads in the records example use checked_explain<Strength>, which
gives the same trace and keeps the outcome for the no-answer check. The
series example runs its passing series once and renders it at both step
budgets, tests presence with contains, and writes its one fraction as
27708_r / 425. The methods example says why it runs the north a second
time.

The three programs print exactly what they printed before.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…at compile time

Where an example shows only a trace, it now asks for the trace alone. The
statistics of a constant sample are checked with static_assert instead of a
runtime guard, and a trace rendered twice is rendered once.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
deviation_in_stddevs takes a Node, so the bare 1.75_r in its
documentation did not compile; wrap it in number().

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…n integers in the statistics example

The tracing guide quoted an older body of explain; point at trace.hpp
instead. The double-trace sentence now says it is explain that records
in Rational. sixMasses spells 44 and 40 as integers like its siblings.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…d and breakpoint negatives

Add negatives for 1e1001_r and 0b101_r, a BelowMinimum check for
within_bounds, and widen the vocabulary-reach tripwire so it also sees
symbol_of<decltype(x)>(). The band and breakpoint negatives now return 0
and say plainly why their floating-point types differ. Explain in
rational.hpp why hasPoint is also true for an exponent, and rewrap a long
comment in fail_without_dialogs.cpp.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…at it should

The documentation implied trace_of_si differs from trace_of<Q> by units, which
it does not; the real difference is that trace_of<Q> is empty where a value was
typed in for the result and trace_of_si traces the derivation anyway. Both
docstrings and the guide now say so, narrow "a failure is the last step" to
failures while the expression is evaluated (a failure converting into the
result's unit comes after the trace), and use names the guide already has.

trace_of now reaches checked_evaluate's series refusal instead of an overload
list, and trace_of_si goes through the same dispatch as checked_evaluate so a
consumer's own node at the root still traces. The nested-Yields gate, the
verb lists that say "every" and the dimension negative's second-message guard
are now pinned, each with a deletion check.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…cap itself, mark the refused lookup snippet

The widened symbol_of pattern dropped angle brackets, so
symbol_of<Wrapper<Q>>() slipped through; it now accepts them and one
level of parentheses, and stops at a statement end so it cannot run on
into later code. The cap negative now uses an exponent too large for an
int, so it fails when the cap is deleted (the old 1e1001 case was also
refused by a later guard); 1e1001_r stays as its own case. The lookup
comment says its snippet is now refused.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… a single-value verb

A bound formula is meant to be refused in the library's words when it is
misused, with one message. But the verbs that answer with one value --
evaluate, checked_evaluate, explain, checked_explain, trace_of and define
-- forwarded whatever the Yields held to an overload that takes a single
expression. A bound rejection reached none of them, nor did a bound
series, retry or whole opaque call handed to explain, checked_explain,
trace_of or define: the mistake was reported as "no matching function"
inside the library, which reads as a library defect.

Each of those verbs now refuses a bound formula that is not an expression
of one value, and forwards only one that is, so the refusal is the only
message. A series is refused as checked_evaluate already refuses one, a
retry and an opaque call as checked_evaluate refuses them, and a rejection
in new words that name checked_evaluate_rejection and explain_rejection.
define keeps its own words for a series. trace_of_si refuses a series as
trace_of does.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A bound formula is the top of a formula, not a part of one; the formula
it holds, .expression, is the operand. Written as an operand anyway --
var<WaterVolume> * ratio -- it matched no operator, and the mistake came
back as a list of twenty to thirty candidate operators.

Arithmetic with a bound formula on either side, and its negation, is now
refused in one sentence that names .expression. The operators take part
only when an operand is a bound formula, so arithmetic over formulas
resolves exactly as before. As for a retry, each gives a node refused
already, so an evaluation of the result asks nothing more, and names its
return type, so a concept asking whether a bound formula can be added is
answered without the refusal.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The refusal of a rounding rule with no citation is shared by both
spellings of with_rounding, yet named only the three-argument one; it now
names with_rounding<...>(). The unit conversion's note no longer cites the
compilers it was measured with, the every-verb negative counts ten verbs
and says which REJECT is g++'s, a rewrapped paragraph in lookup.hpp is
reflowed, and the vocabulary check says it misses a template argument
split across lines.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… root

The guide's trace_of example now uses the names of the explain example it
points back to. trace_of_si consults no entered value for a result, while
an input typed in is read as any other, and a failure is the trace's last
step only at a node that reports to its sink, as each of this library's
does. A test traces a consumer's two-parameter node at the root of
trace_of_si, which compiles only through detail::dispatch.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The probe now instantiates explain_check, explain_check_all,
explain_check_method, explain_curve and the unbound explain_rejection,
so a consumer's global hidden by one of their locals breaks its build,
and checks each against the outcome of its untraced verb.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The entry named only describe, whose clash is loud. An unqualified call
with library arguments now also finds number_of, convert_to,
round_to_declared, within_bounds, traced, trace_of, trace_of_si and the
explain twins; a consumer's template of the same shape is displaced
silently, and a non-template or differently typed one is ambiguous.
using namespace formula also brings _r into scope.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The plan named a local status file, its board and the configuration that
remembers the board's address, none of which is in the repository or
means anything to its readers. Where a sentence carried a rule, it now
says that the controller keeps local progress notes.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ires

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ails

citations.cpp now says which error stopped the evaluation, and
composition.cpp guards the evaluation outcome itself, printing its error,
before reading the number from it.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…s read

composition.cpp, rounding_and_conditionals.cpp and display.cpp no longer
explain or checked_explain a formula just to take the trace out of it.
composition.cpp also stops calling the throwing explain.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…verts

_r stays where only a Rational is accepted, such as the first argument of
MeasuredObservations and the operands that must stay Rational. The guides
quote the same lines and follow them.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
An Outcome formats as the Measured it holds, with the same specs, so the
.measurement() call in front of each print only spelled the same text
longer. The output is unchanged, and docs/calculations.md quotes the
same lines.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… or the absence

The four results in methods_and_overlays.cpp were each checked twice, for
an error and then for no value. One guard on number_of now prints the
error's words when there is an error and "no value" otherwise.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… of a runtime guard

records.cpp, quantities.cpp, statistics.cpp, dimensions_and_units.cpp and
series.cpp evaluate, convert or bounds-check constants, so the std::expected
is constexpr and static_assert(r.has_value()) checks it: an error stops the
build. docs/quantities.md quotes the changed lines and follows them, and the
census page's operation counts for the quantities example follow its source.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…rary's

The entry said any non-template of the same name made the call
ambiguous. That holds beside a non-template such as describe; beside one
of the library's templates, a non-template taking exactly its parameter
types is preferred and keeps working. The list of names now says it
gives some of them, adds yields and declared_rounding, and says the
example's describe was removed rather than renamed.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The refusal of a rounding rule with no citation shipped in 0.2.0 opening
"formula: with_rounding<U, Places, Mode>()", and a consumer's own
negative test may match that opening. It now names with_rounding<...>()
for either spelling, which is a change to tested text.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A bound formula or a retry used as an operand is refused once, and the
refusal gives a stand-in so that nothing over it asks again. render and
document had no case for either stand-in, so each added a compiler's
"no matching function" after the refusal. Each now renders as
"(refused)" and documents nothing, as the other refused stand-ins do,
and both operand negatives render and document the refused value and
expect one error.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A bound formula compared with a limit, as a constraint compares, drew the
compiler's list of comparison operators none of which takes a Yields.
It is now refused as a bound operand is, with the same message, and
gives a comparison of refused values, so that a constraint, check,
render or document over it adds nothing. The operators are constrained
to a Yields operand, so comparisons of formulas are untouched, and a
bound formula stays not equality-comparable.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…he whole-number spellings

The census page named three examples that evaluate formulas at compile
time; nine do now, and a row reading 0 | 0 | 0 is explained. The expressions
guide says a comparison over a bound formula is refused in the same words
as an operand. Two more whole numbers are written plainly in opaque_and_retry.cpp
and dimensions_and_units.cpp, with the guide quote that follows the first.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…uantities does at run time

The guide said a comparison over a bound formula "is refused in the same
words" without quoting any refusal nearby, so a reader looking for the words
found the one for a bound formula inside another, which is a different
message. It now quotes the refusal both uses get.

The census page said every check the quantities example makes is a
compile-time one. One is not: it combines an absent input at run time. That
computes no integer, which is why its row reads zero.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
@christianparpart
christianparpart marked this pull request as ready for review September 30, 2026 22:35
@christianparpart
christianparpart merged commit ed6b32a into master Sep 30, 2026
12 checks passed
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.

1 participant