Methods and jurisdiction overlays
A test method often reports one quantity by more than one formula: the
compressive strength of a cube, a cylinder and a prism are three different
expressions for the same thing. Which formula applies is a property of the
specimen, not of a number in the environment, and it has to be declared
rather than hidden in an if the library never sees. An if on specimen shape
is control flow no trace can report, so an inspector asking "why the cylinder
formula?" would get no answer.
The same method then varies by jurisdiction. The core algebra stays the same everywhere, but one national annex fixes a constant the base standard leaves open, another computes it, a third replaces a formula outright or drops a variant it does not use. formula-cpp models each jurisdiction as an overlay: a declared list of changes, applied to a method, that yields a method.
The worked example is examples/methods_and_overlays.cpp. The page holds two
kinds of quoted block. Program output is copied verbatim from that
program's actual output, and docs.methods-and-overlays-output fails unless
each of these blocks is a run of consecutive lines the program prints, exactly
as quoted (cmake/CheckGuideOutput.cmake). Compiler diagnostics -- the blocks
opening static assertion failed -- are the library's refusals as g++ 13.3
printed them, captured from this repository's negative tests.
Code is copied from the example's source, and
docs.methods-and-overlays-snippets fails unless each code block appears there
as a run of consecutive lines, compared without their indentation
(cmake/CheckGuideSnippets.cmake). A code block that is deliberately not
from the example -- a misuse shown in order to say what happens -- would carry
a <!-- snippet: not from the example --> comment directly above it, which
the check skips. No code block on this page carries one.
A method: variants, tags, and one rounding rule
A method is built from three parts, always in this order: the variants, the
rounding rule, and the constraints. The rounding rule is named once, as a
DecimalRounding: a unit, how many decimal places of it to keep, and which way
to break ties.
inline constexpr formula::DecimalRounding tenthMpa { unit::Megapascal,
formula::DecimalPlaces { 1 },
formula::RoundingMode::HalfAwayFromZero };
inline constexpr auto compressiveStrength = formula::method(
formula::variants(formula::variant<Prism>(var<Force> / (var<EdgeA> * var<EdgeA>)),
formula::variant<Cube>(var<ShapeFactor> * var<Force> / (var<EdgeA> * var<EdgeB>)),
formula::variant<Cylinder>(formula::constant<unit::One>(4) * var<Force>
/ (formula::pi * formula::pow<2>(var<Diameter>)))),
formula::rounding_rule<tenthMpa>(),
formula::constraints(formula::constraint(var<Force> >= formula::constant<unit::Kilonewton>(47.3_r),
formula::Verdict { "the load at failure is below 47.3 kN" })));
- A tag such as
Cubeis an empty struct, never instantiated, and it need not even be complete. What a variant applies to is a type, so selecting one is a compile-time fact rather than a string nobody checks. - The variants are expressions of different types. They are held in a
std::tuplerather than an array for that reason. What they must share is the dimension they report. - The rounding rule is part of the method, not something applied afterwards. It rounds whichever variant is selected.
- The constraints are the checks a result must pass to be accepted, as
Constraints and verdicts describes them.
evaluate_methoddoes not check them;check_methoddoes, and says whose they are -- see Whose acceptance logic.
evaluate_method<Tag> selects a variant by tag. The tag is always stated,
never deduced: which variant applies is a property of the specimen, and the
caller says what the specimen is.
The example calls explain_method<Tag>, which is evaluate_method<Tag> with a
recording sink. It returns what evaluate_method returned in outcome, and
the derivation in trace, from one evaluation:
auto const cube = formula::explain_method<Cube>(compressiveStrength, specimen);
cube: 5500000 Pa
A method answers in the coherent unit, here pascals, like every Evaluated
value in this library. The rule rounded the value in megapascals, to one
decimal (5.5477... MPa became 5.5 MPa), and the answer is that rounded value
expressed in pascals. A method has no typed result. It knows only its variants'
dimension, so its answer is a number in the coherent unit of that dimension.
Traced, the selection is a step of its own, the root of the derivation, with the rounded variant beneath it. The rounding step says whose rule it was:
1. k_s = 1043/1000
2. F = 89300 N
3. #1 * #2 = 931399/10 N
4. a = 163 mm
5. b = 103 mm
6. #4 * #5 = 16789/1000000 m^2
7. #3 / #6 = 93139900000/16789 kg/(m s^2)
8. round(#7, in MPa) = 11/2 MPa [rounded to 1 dp (method default); nearest, ties away from zero]
9. #8 = 11/2 MPa [variant Cube (2nd of 3), selected by tag]
Each computed step reads in a unit written after it. The force scaled by the
shape factor, a pure number, keeps the force's newtons. The area and the
stress borrow neither operand's unit, so each reads in the coherent unit of its
dimension, spelt from the base units: m^2, and kg/(m s^2) for the pascal.
Naming a variant as the published method does
A tag is shown under its own name, Cube. Where the published method words a
variant differently, specialise formula::TagName once for that tag. The shape
is EnumeratorName's, without the argument:
template <>
struct formula::TagName<Cylinder>
{
static constexpr std::string_view of() noexcept
{
return "cylinder 135 x 271 mm";
}
};
10. #9 = 31/5 MPa [variant cylinder 135 x 271 mm (3rd of 3), selected by tag]
A spelling holding a square bracket or a control character is refused, since a trace line ends in a bracketed clause saying where a value came from, and such a spelling could imitate one -- see the provenance section. So are two variants of one method spelt the same, since the line naming the variant that ran could not say which it was.
(3rd of 3) is the variant's position in the method as published. It stays
that way after an overlay prunes or pins, so the number refers to the list a
reader can find in the standard -- unless an author copies another pack's
layout into the method's, which the section on provenance below describes.
Why variant<Tag>, not when<Tag>
The design specification sketches the selector as formula::when<Cube>(expr).
This library spells it variant<Cube>(expr) because of measured evidence, not
preference. conditional.hpp already ships when(predicate, then, else): a
runtime-predicate ternary, a different question with a different arity. When
both were declared in namespace formula, a measurement made while this
selector was being designed found the following on cl 19.51, clang-cl 22.1.3,
clang++ 20.1.8, g++ 13.3 and g++-14 14.2.0 (method.hpp's file comment records
the measurement):
when<P>(pred, then, else)bindsPto the ternary's first template parameter and compiles silently on all five. An author who believes they are naming a tag there gets the ternary instead.- Every misspelling produces one uninformative diagnostic on all five: clang and g++ print "no matching function for call to 'when'", and cl prints "C2672: 'formula::when': no matching overloaded function found". It names neither concept, because both are in the overload set and both failed.
The spec's own one-argument spelling, when<Cube>(expr), would have resolved
correctly. The departure rests on the silent mis-binding next to it, and on no
diagnostic being able to tell the two apart.
What is checked, and when
A mistake in a method is caught at compile time wherever the mistake is a fact about types. Where it is a fact about values, it is not caught, and this page says so rather than implying otherwise.
When the method is declared, a variants pack is refused unless:
- every argument is a variant;
- there is at least one;
- all variants report the same dimension;
- no two declare the same tag;
- no two are spelt the same in a trace (a
TagNamecan make two tags read alike); - every tag is a plain class type.
Two of those refusals, as g++ 13.3 prints them:
static assertion failed: formula: two variants of this method measure different dimensions; every variant must report the same quantity, because a method reports one -- the two offending variants appear in this diagnostic as the template arguments of RequireVariantsAgree
static assertion failed: formula: this method declares two variants for the same tag; a method with two variants for one tag has no answer to which of them applies -- the tag appears in this diagnostic as the template argument Tag of RequireTagDeclaredOnce, and First and Second are the ZERO-BASED positions of the two variants that declare it, so 0 is the first variant
Two tags spelt alike:
static assertion failed: formula: two variants of this method are spelt the same in a trace, so a line naming the variant that ran could not say which of them it was -- the tags appear in this diagnostic as the template arguments FirstTag and SecondTag of RequireTagNamesDistinct, and First and Second are the ZERO-BASED positions of their variants; spell them apart with formula::TagName
A rounding rule whose unit does not measure the variants' dimension is refused too:
static assertion failed: formula: this method's rounding rule rounds in a unit that does not measure the dimension its variants report; the rule rounds whichever variant is selected, so its unit must measure what every variant measures -- the variants and the rounding rule appear in this diagnostic as the template arguments Vs and Rounding of RequireRoundingRuleMeasuresVariants
When a variant is selected, a tag no variant declares is refused. A method that matches nothing has no result. There is no fallback variant and no "first match wins":
static assertion failed: formula: this method declares no variant for that tag; a method that matches nothing has no result, so add a variant for it or select a tag it declares
What is not checked:
- Completeness. Whether the method covers every specimen shape is only
decidable against a list of shapes that someone wrote down, and the library
has no such list. It checks every call: each
evaluate_method<Tag>names a tag, and a tag with no variant does not compile. - Overlap of runtime conditions. Two tags are two types, so a repeated tag
is always caught. If you choose between formulas with
when()on a measured value instead, nothing checks whether two conditions overlap or leave a gap. The number a condition compares against is a runtime value, not part of the type. Selecting by tag is what buys the compile-time checks.
An overlay yields a method
Seven operations, and every one of them requires a citation and is said in the trace with what the jurisdiction cited: a fixed constant, a derived quantity and a replaced formula each on a step of its own, a jurisdiction's rounding rule on the rounding step, a pin or a prune on the step that names the selected variant, and a jurisdiction's constraints beside each verdict (see Whose acceptance logic).
document() marks less than the trace says. It documents a formula, and
marks on the page only what an overlay put inside one: a fixed, a derived and
a replaced part. A pin, a prune, a rounding rule and a set of constraints are
parts of a method, not of a formula, and no page shows them: there is no
document() for a whole method yet. A document(method) is future work.
| Operation | What it does |
|---|---|
with_constant<Q>(value, citation) |
fixes Q at value (in Q's declared unit) wherever the method uses it |
add_derived<Q>(expression, citation) |
defines Q by an expression over other inputs wherever the method uses it |
replace_variant<Tag>(expression, citation) |
replaces one variant's formula wholesale |
pin_variant<Tag>(citation) |
keeps only that variant, making it mandatory |
prune_variant<Tag>(citation) |
deletes that variant |
with_rounding<U, Places, Mode>(citation) |
replaces the method's rounding rule |
with_constraints(constraints(...), citation) |
replaces the method's constraints wholesale |
apply(overlay, method) returns a new method, a new type built at compile
time. The base method is unchanged, so one base method can carry every
jurisdiction's overlay side by side. Operations apply in the order the
overlay lists them, each to the method the previous one produced.
An operation given no citation is refused, in words naming the operation:
static assertion failed: formula: pin_variant<Tag>() was given no citation; which variant is mandatory is a jurisdiction's decision, and a trace must say whose -- pass the Citation of the clause that makes it, pin_variant<Tag>(citation)
Fixing a constant, and the unit a jurisdiction reports in
inline constexpr auto north =
formula::overlay(formula::with_constant<ShapeFactor>(0.863_r, northConstant),
formula::with_rounding<unit::NewtonPerSquareMillimetre,
formula::DecimalPlaces { 2 },
formula::RoundingMode::HalfAwayFromZero>(northRounding));
north cube: 4590000 Pa
1. k_s = 863/1000 [fixed by jurisdiction overlay: Shape factor, Example Standard 12:2021 NA, NA.2.1]
2. F = 89300 N
3. #1 * #2 = 770659/10 N
4. a = 163 mm
5. b = 103 mm
6. #4 * #5 = 16789/1000000 m^2
7. #3 / #6 = 77065900000/16789 kg/(m s^2)
8. round(#7, in N/mm2) = 459/100 N/mm2 [rounded to 2 dp (jurisdiction overlay: Example Standard 12:2021 NA, NA.4); nearest, ties away from zero]
9. #8 = 459/100 N/mm2 [variant Cube (2nd of 3), selected by tag]
What "changing the declared unit" means for a method. The design specification asks that a jurisdiction can change the unit a result is declared in. A method's answer is always in the coherent unit, so what a jurisdiction changes is the unit it reports in, which is the unit of its rounding rule. The north rounds in N/mm² to two decimals where the base method rounds in MPa to one, and the trace says whose rule that was.
The reporting unit can change only within the dimension. A rule whose unit measures anything else is refused, and so is a replacement formula of another dimension:
static assertion failed: formula: this overlay replaces a variant with a formula of a different dimension from the method's; a replacement changes one variant, never what the method reports -- the tag, the method's dimension and the replacement's appear in this diagnostic as the template arguments Tag, Reported and Replacement of RequireReplacementKeepsDimension
No unit field exists anywhere to change instead. A Measured<Q> is a value in
Q's declared unit and carries no unit of its own (Quantities and
measurements), so a number can never disagree with its label.
Deriving a quantity, replacing a formula, dropping a variant
inline constexpr auto south = formula::overlay(
formula::replace_variant<Cylinder>(
var<Force> / (formula::constant<unit::One>(1.127_r) * formula::pow<2>(var<Diameter>)), southReplacement),
formula::add_derived<ShapeFactor>(var<EdgeB> / var<EdgeA>, southDefinition),
formula::prune_variant<Prism>(southScope));
A derived quantity is traced as the quantity equal to its definition's step:
south cube: 3400000 Pa
1. b = 103 mm
2. a = 163 mm
3. #1 / #2 = 103/163
4. k_s = #3 = 103/163 [derived by jurisdiction overlay: Example Standard 7:2019 A, A.3]
5. F = 89300 N
6. #4 * #5 = 9197900/163 N
7. a = 163 mm
8. b = 103 mm
9. #7 * #8 = 16789/1000000 m^2
10. #6 / #9 = 89300000000/26569 kg/(m s^2)
11. round(#10, in MPa) = 17/5 MPa [rounded to 1 dp (method default); nearest, ties away from zero]
12. #11 = 17/5 MPa [variant Cube (2nd of 3), selected by tag; 1 of 3 pruned by jurisdiction overlay: Example Standard 7:2019 A, A.1]
The last line says the prune: one of the method's three published variants is gone, and by whose clause.
A replaced formula is marked as the jurisdiction's. The selection still counts the Cylinder as the 3rd of 3, although the Prism published before it is gone -- the position is the published one, not the Cylinder's place in what is left:
1. F = 89300 N
2. 1127/1000
3. d = 135 mm
4. #3^2 = 729/40000 m^2
5. #2 * #4 = 821583/40000000 m^2
6. #1 / #5 = 3572000000000/821583 kg/(m s^2)
7. #6 = 3572000000000/821583 kg/(m s^2) [replaced by jurisdiction overlay: Example Standard 7:2019 A, A.5]
8. round(#7, in MPa) = 43/10 MPa [rounded to 1 dp (method default); nearest, ties away from zero]
9. #8 = 43/10 MPa [variant cylinder 135 x 271 mm (3rd of 3), selected by tag; 1 of 3 pruned by jurisdiction overlay: Example Standard 7:2019 A, A.1]
document() marks the same things on the page. A fixed quantity's row carries
fixedValue and fixedBy, and a derived one carries derivedAs (the
definition, rendered in the page's dialect) and derivedBy. Every replacement,
cited or not, is listed in Documentation::replacedBy. The south's cube, as
the example prints its page:
documentation of the south's cube:
k_s * F / (a * b)
k_s: shape factor -- derived as b / a
b: second loaded edge
a: first loaded edge
F: maximum load at failure
A pin keeps one variant and makes it mandatory:
/// The east tests cubes only, so the cube variant is mandatory there.
inline constexpr auto east = formula::overlay(formula::pin_variant<Cube>(eastScope));
The east's cube is still (2nd of 3), and the selection says the pin:
9. #8 = 11/2 MPa [variant Cube (2nd of 3), selected by tag; pinned by jurisdiction overlay: Example Standard 3:2023 E, E.1]
east: 1 variant(s) left after the pin
One overlay cannot both pin and prune, but one jurisdiction may prune what a later one pins; the trace then says both, the prune first.
The provenance is the library's to state
A trace that says "fixed by jurisdiction overlay", "derived", "replaced" or "(jurisdiction overlay)" is the audit trail, so none of those can be claimed by hand:
- only an overlay builds the nodes a trace reads "fixed", "derived" or "replaced" from;
- only
with_roundingcreates a rule claiming a jurisdiction; - only
evaluate_methodbuilds the rounding node a method applies; - only
variants(...)states a variant's published position and count, and onlyapply, through a pin or a prune, carries them on with what the overlay cited.
Building one of those nodes directly is refused:
static assertion failed: formula: only an overlay builds this node; it makes a trace say a jurisdiction fixed a value, defined a quantity or replaced a formula, so one built by hand would say so of something no overlay did -- use with_constant, add_derived or replace_variant in an overlay(...) given to apply; the node appears in this diagnostic as the template argument OverlayNode of RequireOverlayMadeNode
A published layout written by hand -- { { 5, 7 }, 9 }, which would make a
trace call a variant the 5th of 9 -- is refused, whether as an initialiser or
assigned later:
static assertion failed: formula: a variant's published position is stated only by the library -- by variants(...), and carried by apply() through a pin or a prune -- since a layout written by hand could make a trace report a position and a count no method has; build the pack with variants(...)
What is authoritative is the Step, not the rendered line. A trace step
records its provenance in fields only the library sets -- kind,
roundingProvenance, constraintProvenance, variantPinned,
variantPrunedCount and the citations beside them -- and code that must decide
whose a value was reads those.
The rendered line is escaped so that author text cannot break its
structure. Some of the words in a trace line are the author's: a symbol, a
citation, a verdict, a unit's symbol, a variant's tag, a lookup key's name.
render_trace escapes every one of them -- \ as \\, [ as \[, ] as
\], ; as \;, a newline as \n and any other control character as \x
and two hex digits -- and writes its own clauses as they are, so author text
cannot open or close a clause or end a line. It may still contain any
words: a citation titled like a library clause renders like one, and only
Step::kind tells them apart (docs/tracing.md gives the example). A TagName
or EnumeratorName spelling goes further: holding a square bracket or a
control character, it is refused at compile time:
static assertion failed: formula: this TagName spelling holds a square bracket or a control character (a newline, a tab, any byte below 0x20, or 0x7f); a trace line ends in a bracketed clause saying where a value came from, and a line ends at a newline, so such a spelling could make a trace claim an overlay replaced or fixed something no overlay touched, or add a line that is no step -- the tag appears in this diagnostic as template argument Tag of RequireTagNameSpelling -- spell the tag without them
What the guard governs is how a rule, a node or a layout is created, not where a copy travels. A method holding a copy of an overlay's rule is traced as that overlay's rule, which is true of it. Two relabellings on public members are documented, and nothing refuses them:
- assigning
Method::roundinga method's own rule, which makes a trace say(method default)of what was a jurisdiction's rule; - copying another pack's layout into
Variants::published, or resetting it to declaration order with{}, which gives the pack a layout the library made for another -- positions, count and any pin or prune with its citation. A pruned pack reset this way reports its variants as the 1st and 2nd of 2, and says nothing of the prune.
Both are explicit acts on public members (docs/tracing.md lists every such
route). What no author can do is create a rule, node or layout that states a
provenance the library did not give it.
An override that would do nothing is refused
An overlay is written once per jurisdiction and read by nobody until an inspector asks why a number came out as it did. An operation that names the wrong quantity or the wrong variant would change nothing, produce no error, and leave the base method under a jurisdiction's name. So these are build errors, judged against the method the overlay produces, whatever order its operations are listed in:
static assertion failed: formula: this overlay overrides a quantity that no variant or constraint of the method uses; an override nobody reads would silently do nothing, most likely because it names the wrong quantity -- the quantity appears in this diagnostic as the template argument Q of RequireConstantUsed
static assertion failed: formula: this overlay derives a quantity that no variant or constraint of the method uses; a definition nobody reads would silently do nothing, most likely because it names the wrong quantity -- the quantity appears in this diagnostic as the template argument Q of RequireDerivationUsed
static assertion failed: formula: this overlay both pins a variant and prunes one; a pin already keeps exactly one variant, so a prune beside it either removes a variant the pin drops anyway or removes the one it pins -- list the pin alone, or the prunes alone
static assertion failed: formula: this overlay prunes every variant of the method; a method left with nothing to choose between can never produce a result, so an overlay that removes its last variant is a mistake rather than a jurisdiction -- the last variant's tag appears in this diagnostic as the template argument Tag of RequirePruneLeavesAVariant
The two an author meets first are a name that matches nothing, and the same change listed twice:
static assertion failed: formula: this overlay pins or prunes a variant the method does not declare; an overlay that names a variant by mistake would silently do nothing -- the tag appears in this diagnostic as the template argument Tag of RequireOverlayNamesDeclaredVariant
static assertion failed: formula: this overlay replaces a variant the method does not declare; a replacement that names a variant by mistake would silently do nothing -- the tag appears in this diagnostic as the template argument Tag of RequireReplacementNamesDeclaredVariant
static assertion failed: formula: this overlay lists the same operation twice; two constants or definitions of one quantity, two pins, prunes or replacements of one variant, two rounding overrides, or two replacements of the constraints, leave the first silently doing nothing -- the first of the two appears in this diagnostic as the template argument Operation of RequireOperationListedOnce, and First and Second are the ZERO-BASED positions of the two arguments that list it, so 0 is the first argument
A produced method that reads a quantity both where an overlay fixed or derived it and, elsewhere, straight from the environment is refused as well. A later operation, a later overlay, or two definitions that read each other can put such a plain read back:
static assertion failed: formula: this method reads a quantity both where an overlay fixed or derived it and, elsewhere, unsubstituted from the environment; one formula would evaluate one quantity at two values -- an operation listed after the substitution, a later overlay, or definitions that read each other put the plain use back; the quantity appears in this diagnostic as the template argument Q of RequireSubstitutionEverywhere
A constant or definition that a later operation left with nothing to fix is refused too, in words that say whether it was bypassed or listed too early -- see An overlay's acceptance logic among its other operations.
The full list of refusals, with the reason for each, is overlay.hpp's file
comment. The messages above are g++ 13.3's, copied from the build of this
repository's negative tests. cl and clang print the same library text in their
own frame.
Whose acceptance logic
A method's third part is its constraints: the checks a result must pass before
it is accepted. The base method holds one of its own, F >= 47.3 kN (declared
above), and the west
replaces it with two checks of its own:
inline constexpr auto west = formula::overlay(formula::with_constraints(
formula::constraints(formula::constraint(var<Force> >= formula::constant<unit::Kilonewton>(97.3_r),
formula::Verdict { "the load at failure is below 97.3 kN" }),
formula::constraint(var<EdgeA> <= formula::number(1.73_r) * var<EdgeB>,
formula::Verdict { "the loaded face is more than 1.73 times as long as wide" })),
westAcceptance));
check_method(m, environment) checks every constraint the method holds and
answers one ConstraintOutcome per constraint, at the index the method holds
it. It never stops at the first failure, for the reason Constraints and
verdicts gives:
auto const baseCheck = formula::explain_check_method(compressiveStrength, specimen);
auto const westCheck = formula::explain_check_method(western, specimen);
explain_check_method is check_method with a recording sink: it returns the
outcomes in outcome and the derivation in trace, from one run. Here are the
outcomes:
base: 1 constraint(s)
[0] satisfied
west: 2 constraint(s)
[0] violated: the load at failure is below 97.3 kN
[1] satisfied
with_constraints replaces the constraints, it does not add to them. The
west's method does not check the base method's 47.3 kN. A jurisdiction that
keeps a base check restates it in its own set, with its own citation. An
overlay can also leave fewer constraints than the method had, or none:
with_constraints(formula::constraints(), citation) is accepted, because a
jurisdiction that checks nothing is a position it can state and cite.
Whose each verdict is
constraint_origin(m) answers whose constraints a method holds, and what the
overlay that supplied them cited:
formula::ConstraintOrigin const origin = formula::constraint_origin(m);
if (origin.provenance() == formula::ConstraintProvenance::MethodOwn)
return "the method's own";
base constraints: the method's own
west constraints: jurisdiction overlay, Example Standard 9:2022 B, B.2
The trace says the same beside every verdict. check_method records the
verdicts under an acceptance step of their own, in the order it returns
them:
1. F = 89300 N
2. 473/10 kN
3. require #1 >= #2 [satisfied; the method's own constraint]
4. acceptance(#3) [the method's own constraints]
1. F = 89300 N
2. 973/10 kN
3. require #1 >= #2 [the load at failure is below 97.3 kN; jurisdiction overlay: Acceptance, Example Standard 9:2022 B, B.2]
4. a = 163 mm
5. 173/100
6. b = 103 mm
7. #5 * #6 = 17819/100 mm
8. require #4 <= #7 [satisfied; jurisdiction overlay: Acceptance, Example Standard 9:2022 B, B.2]
9. acceptance(#3, #8) [jurisdiction overlay: Acceptance, Example Standard 9:2022 B, B.2]
A method with no constraints still gets its acceptance line --
acceptance(none), with whose it is -- so a jurisdiction that removed every
check is never silent about it. Step 7 is 1.73 x 103 mm, a length scaled by a
pure number, so it reads in the length's millimetres, 17819/100 mm, as the
force scaled by the shape factor in the cube's trace above reads in newtons.
The constraints carry whose they are with them: with_constraints puts an
OverlaidConstraints in the method -- the jurisdiction's set together with
its citation -- and check_method and constraint_origin read it from there.
So a method built from an overlaid method's parts,
formula::method(o.variantSet, o.rounding, o.constraintSet), still checks the
jurisdiction's constraints and still says so. check_all takes a plain
ConstraintSet, which an overlaid method's constraintSet is not; call
check_method to check a method's constraints.
Building an OverlaidConstraints by hand is refused:
static assertion failed: formula: whose a method's constraints are is the library's to state, not an author's; method(..., constraints(...)) declares the method's own, and with_constraints(...) applied by an overlay a jurisdiction's
Two things are not refused, and are relabellings an author makes on purpose:
- building a method from the plain set:
formula::method(o.variantSet, o.rounding, o.constraintSet.constraintSet())checks the jurisdiction's constraints as the new method's own; - explicitly specialising
OverlaidConstraintsfor a predicate over a quantity of one's own, which can then claim any citation.
Both compile without a diagnostic on cl 19.51 at /W4, and on g++ 13.3 and
clang++ 20.1.8 at -Wall -Wextra (measured with a probe written for this
page, not a test of this repository).
An overlay's acceptance logic among its other operations
with_constraints is an operation like the others, applied in the order the
overlay lists it. An operation listed after it reaches inside the new
constraints: a with_constant<Q> listed after it fixes Q in the
jurisdiction's constraints as well as in the variants
(test/overlay_tests.cpp, "a constant listed after an overlay's constraints
reaches inside them"). A constant listed before it does nothing for the
new constraints, and an overlay whose new constraints then read Q straight
from the environment is refused. Which refusal depends on what the constant
met where it is listed. If it fixed Q in the method's own constraints,
which with_constraints then discarded, it was bypassed:
static assertion failed: formula: this overlay fixes a quantity, and an operation listed after the constant removed every use it fixed and put back one that reads the quantity from the environment; the method it produces reads the quantity only unsubstituted, so the constant does nothing -- list the constant after that operation; the quantity appears in this diagnostic as the template argument Q of RequireConstantNotBypassed
If nothing read Q where the constant is listed, it came too early:
static assertion failed: formula: this overlay fixes a quantity that nothing read where the constant is listed, and an operation listed after it reads the quantity from the environment; the method it produces reads the quantity only unfixed, so the constant does nothing -- list the constant after that operation; the quantity appears in this diagnostic as the template argument Q of RequireConstantPrecedesItsUse
If the new constraints do not read Q either, and nothing else in the method
does, the constant is refused as an override nobody reads. add_derived has
the same three refusals, in its own words. Either way the fix is to list the
constant after the constraints it is meant to reach. The messages are g++
13.3's, from overlay_constant_before_constraints_read_plainly and
overlay_constant_never_read_then_constraints_read_it in test/negative/.
What constraints cannot yet say
Two limits, stated here so that nobody mistakes them for supported cases:
- A constraint cannot yet judge the specimen's own category. A category
code is written into a constraint through
exact_lookup, which takes its key as a value, not from the environment. So a jurisdiction that accepts, say, square specimens only can write a constraint that judges one fixed category, the same for every specimen, but not one that looks up the category of the specimen in front of it. The verdict does name the category by its enumerator (test/overlay_tests.cpp, "an overlay's constraint judges a category code, and its verdict names the category"). - Stacked overlays: the later overlay's constraints hold. Applying a
second overlay to an overlaid method replaces the constraints again,
wholesale. The earlier overlay's constraints are gone, not merged, and
constraint_originnames only the later citation:
/// A later revision of the west's annex, applied on top of the west's method:
/// its one constraint is all the stacked method checks.
inline constexpr auto westRevised = formula::overlay(formula::with_constraints(
formula::constraints(formula::constraint(var<Force> >= formula::constant<unit::Kilonewton>(83.1_r),
formula::Verdict { "the load at failure is below 83.1 kN" })),
westRevision));
inline constexpr auto western = formula::apply(west, compressiveStrength);
inline constexpr auto westernRevised = formula::apply(westRevised, western);
west, revised on top: 1 constraint(s)
[0] satisfied
revised constraints: jurisdiction overlay, Example Standard 9:2025 B, B.2
A later overlay's with_constraints may also discard every place an earlier
overlay's constant or definition had fixed a quantity. That is accepted as
"the later overlay holds", although the same two operations inside one
overlay are refused, as described above.
The jurisdiction set is compiled in; which one applies is data
Every overlaid method is a different type. Every one answers the same
Evaluated<Rational>, though, so choosing among them for a sample is an
ordinary switch on a runtime value:
[[nodiscard]] formula::Evaluated<formula::Rational> cubeStrengthIn(Jurisdiction jurisdiction)
{
switch (jurisdiction)
{
case Jurisdiction::North:
return formula::evaluate_method<Cube>(northern, specimen);
case Jurisdiction::South:
return formula::evaluate_method<Cube>(southern, specimen);
case Jurisdiction::Base:
break;
}
return formula::evaluate_method<Cube>(compressiveStrength, specimen);
}
jurisdiction 0: 5500000 Pa
jurisdiction 1: 4590000 Pa
jurisdiction 2: 3400000 Pa
Why not an overlay chosen at run time, from a registry? A fixed constant,
a rounding rule, a pin and a prune are data, and could be chosen at run time.
A new formula could not: a replaced variant's formula and a derived
quantity's definition are both expressions, each of its own type. Choosing
either at run time means holding formulas of different types behind one
interface: type erasure, a virtual call per formula. The trace would not survive that for free.
RecordingSink's entered and produced are member templates on the node
type, and a member template cannot be virtual. So an erased formula would have
to name one concrete sink type, one Rep and one vocabulary in its virtual
signature, permanently, for every consumer. An experiment made while overlays
were being designed built that shape for replacement and recorded its cost;
add_derived, added later, meets the same obstacle for the same reason. The rule here follows: the set of
jurisdictions is closed and lives in the type, and which one applies to a
sample is a runtime index. That fits a registry "per customer, per region, per
contract" for everything except a formula nobody compiled.
The same formula in two jurisdictions' words
The same symbol can name different quantities in different jurisdictions. In the example the two jurisdictions write the specimen's two loaded edges with each other's letters, so a page in the wrong words states the wrong formula. A vocabulary (Citations and rendering) resolves every symbol:
inline constexpr auto northernWords = formula::vocabulary(formula::renames<EdgeA>("a"), formula::renames<EdgeB>("b"));
inline constexpr auto southernWords = formula::vocabulary(formula::renames<EdgeA>("b"), formula::renames<EdgeB>("a"));
north: k_s * F / (a * b)
south: k_s * F / (b * a)
the southern page's symbol table:
k_s: shape factor
F: maximum load at failure
b: first loaded edge
a: second loaded edge
The symbol moves and the meaning stays: in the south, b is the first loaded
edge. A trace records symbols when the method is evaluated, so the trace
must be recorded with the same vocabulary as the page:
auto const southernRun = formula::explain_method<Cube>(compressiveStrength, specimen, southernWords);
4. b = 163 mm
5. a = 103 mm
A vocabulary renames quantities only. A variant's tag (TagName) and a lookup
key's enumerator (EnumeratorName) have their own customisation traits and are
not renamed by it. The fixed, derived and replaced parts of an overlaid method
follow the vocabulary too: a derived quantity's derivedAs is rendered in the
page's words.
Every citation here is invented
Every citation on this page and in examples/methods_and_overlays.cpp names a
fictional Example Standard, never a real one. See the home page.