Skip to content

Unnamed and inverse units in traces and rendered formulas, 128-bit logarithm arguments, and the series refusal for explain - #28

Merged
christianparpart merged 36 commits into
masterfrom
feature/open-issues
Oct 5, 2026
Merged

christianparpart merged 36 commits into
masterfrom
feature/open-issues

Conversation

@christianparpart

Copy link
Copy Markdown
Member

Traces, rendered formulas and number text

  • A scaled dimensionless unit must have a symbol (breaking). A dimensionless quantity in a unit with a scale or
    an offset and no symbol traced as a bare number in a scale nothing named: one half in hundredths read 50. Such a
    unit is now refused at compile time wherever it is used: a quantity, a constant, a rounding, or a table's key or
    result. The refusal reads formula: a dimensionless unit with a scale must have a symbol. Give the unit a symbol
    (%, ppm, …) or declare the quantity in scale 1.
  • A unit with no symbol is named by its size.
    • A rounding to places of such a unit reads round(#1, to 2 dp of 1/1000 kg), where it read round(#1, to 2 dp)
      next to a value in kilograms. An offset unit is named by its size and its zero: to 1 dp of 1 K from 5463/20 K.
    • numeric(x, in <unit>) names the unit the same way.
    • render() writes every number a formula declares in such a unit in the coherent unit, as its trace does:
      constants (3/1000 kg, not 3), per-element constants, lookup bands and rows, binning classes, snap values,
      domain points and conformity limits.
    • number_text and std::format write a Measured value in such a unit the same way, with the same spelling
      code as the trace.
    • In LaTeX a size is grouped, \operatorname{round}_{2\,(1/1000\,\mathrm{kg})}, and coherent powers are
      superscripts: \mathrm{kg}^{-1}.
  • An inverse unit cannot be misread as part of a fraction. A coherent unit with nothing above the slash is
    written with negative exponents: 20000/413 kg^-1, not 20000/413 1/kg. Units with a numerator keep the slash
    (m/s, EUR/JPY).
  • A precision limit's first pass reads in its level's unit. A level constant declared in grams reads 40 g on
    both lines, where the first pass read 1/25 kg.
  • A snap's "on a permitted value" test now explains why it compares the stored pairs, and a missed lookup spells
    its low bound only where it writes it.

Numerics

  • rounded_ln, rounded_log10 and rounded_exp take every argument a Rational holds. The kernel narrowed
    its argument to 64 bits and answered Overflow beyond, so ln 2^70 was refused; it now answers 48.5203 at
    4 places.
    • rounded_exp's cap moves from 44 to 887/10, past which no result fits a Rational. Below the cap every
      result that fits the declared places is answered: e^45 to 18 places, e^88 to whole units.
    • The exponential computes with 192 fraction bits, so a result as wide as a Rational is still decided.
      The 38 reference comparisons that were undecided are now 0.
    • Every new expected value was checked against an independent computation with Python's decimal.
  • The bit widths two guides quote are measured.
    • The headroom page's least-squares table has a generated "widest fit intermediate (of 256 bits)" column, from
      a census hook in the exact fit.
    • The opaque-operation guide's coefficient widths are pinned by a test that shares the example's fifty
      readings (examples/fifty_readings.hpp).

Diagnostics and documentation

  • explain and checked_explain handed a series refuse it in the library's words, pointing at explain_series,
    instead of failing with "no matching function".
  • Comments, test names, section banners and the changelog's release headings say what they rely on, not which
    stage of development produced them.
  • The README links the tracing guide once, and the 128-bit design document describes the checked add, subtract
    and multiply as built.

Closes #11
Closes #13
Closes #14
Closes #15
Closes #16
Closes #17
Closes #18
Closes #19
Closes #20
Closes #21

Christian Parpart added 30 commits October 4, 2026 22:48
Covers #11 and #13 to #21: trace text (a scaled dimensionless unit must
have a symbol, an unnamed unit named by its size in a rounding clause,
negative exponents for an inverse-only unit, a precision limit's first
pass borrowing its unit, the snap's on-a-value compare), 128-bit
arguments for the logarithm and exponential kernel, pinned bit widths in
two guides, the library's refusal for explain of a series, two
documentation corrections, and self-describing comments in place of
development labels.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Ten tasks: four on trace text, two on numerics, then a merge, the
series refusal with two documentation corrections, the replacement of
development labels, and the finish.

Planning corrected the design in two places. The exponential computes
with 192 fraction bits, because at 128 a result near 2^127 could never
be rounded with certainty and every such answer would be Overflow. The
least-squares rows of the headroom page come from the 256-bit exact fit,
so the "256-bit" figure there was right, and the new width column is
generated from that fit's own constant.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The kernel narrowed an argument's numerator and denominator to 64 bits
and refused wider ones with Overflow, though a Rational holds 128. It
now reads both as 128-bit magnitudes: the scaled quotient is a long
division over 128-bit words, a logarithm's reduction reaches 2^126, and
the exponential's reaches 2^127 under a cap of 88.7, past which no value
fits a Rational. The exponential computes with 192 fraction bits, so
that a result as wide as a Rational is still enclosed within a fraction
of its last kept unit: e^45 at 18 places and e^88 whole now answer.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A number in such a unit is in a scale nothing names: a trace showed one
half in hundredths as 50, and no spelling of the unit could say so,
since the coherent dimensionless unit is written as nothing. Every class
that holds a unit as a template argument now refuses one, beside the
checks it already makes: a quantity's description and the three nodes
that read a quantity, a constant, every rounding, every table's key and
result, a numeric value, a conformity check, a rounding rule and its
override. A dimensioned unit with no symbol is still accepted and shown
in the coherent unit.

This breaks code that declares such a unit: give it a symbol, or declare
the quantity in scale 1.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The series' product of a term and the reduced argument is below 2^384,
the kernel word's width, and is what limits the exponential to 192
fraction bits; the comments now say so. Also pin the exponential of a
negative argument with a 127-bit denominator, which rounds to 1, or one
unit below it under Floor.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Reflow the exponential bullet's paragraph, name log10's widest
product as log10's own, and wrap two rows of the reference table
within the column limit.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A measurement names its quantity's description through RequireDescribed
alone, with no variable node in sight, so that check is pinned by a case
of its own. RequireDescribed's comments now name the three refusals it
makes, the changelog entry leads with the break, and the new test files
end in a newline.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A coherent unit with nothing above the slash was written 1/kg, so after
a fraction a line read 20000/413 1/kg: one fraction divided again, at a
glance. It is now kg^-1, m^-1 s^-1, JPY^-1, kg^(-1/2). A unit with a
numerator keeps its slash: m/s, EUR s^2/(m^2 kg).

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The overflow census is told the bits every wide integer of
LinearLeastSquares::compute_exact used, and the headroom page's
least-squares table gains a generated column with the widest at the
sizes that answer: 68 of 256 bits on readings at 3 decimal places, 249
on a different denominator for every point. A test pins the widths of
the exact outputs of the fifty-readings fit the opaque guide quotes,
and the guide cites it.

The fifty readings now live once, in examples/fifty_readings.hpp, which
the example and the least-squares tests both include.

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

A named base leads the SI units on its side of the slash, or among the
negated factors when nothing stands above it, as in EUR^-1 s^-1. The
tracing guide's note on negative exponents becomes short sentences.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The test of a named base dimension's place, and the paragraph on it in
the dimensions guide, still spoke of the denominator for EUR^-1 s^-1,
which has none. They now say that the name leads the negated factors
when nothing stands above the slash, and the guide's list of examples
reads as a sentence.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The SPDX and NOLINT hygiene checks globbed only the examples' sources and
build files, so a header beside the examples, such as fifty_readings.hpp,
escaped both. They now scan examples/*.hpp as well.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A rounding to places of a unit with no symbol wrote no unit clause,
while the value after it read in the coherent unit: round(#1, to 2 dp) =
3/1000 kg let "2 dp" read as places of a kilogram. The clause now names
the unit by its size in the coherent unit, round(#1, to 2 dp of 1/1000
kg), and an offset unit by its size and its zero, to 1 dp of 1 K from
5463/20 K, in render() and in a trace alike. numeric(x, in <unit>)
names such a unit the same way, and render() writes a constant in it in
the coherent unit, 3/1000 kg, where it wrote a bare 3. The coherent unit's
spelling moves into render.hpp so that both can write it.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
In LaTeX a rounding's size clause read as a mixed number,
\operatorname{round}_{2\,\mathrm{1/1000\ kg}} as 2 1/1000 kg, and a
coherent unit's powers came out as an escaped caret. The coherent
spelling now takes a notation: plain text and a trace keep kg^-1, and
LaTeX sets each symbol upright with its power raised,
2000\,\mathrm{kg}^{-1}. A size clause is grouped in LaTeX,
round_{2\,(1/1000\,\mathrm{kg})}, and a numeric value's quotient too.
A constant that cannot be moved into the coherent unit now reads only
"(not shown: ...)", with no unit after it, as in a trace.

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

`verbatim_text` now says LaTeX escapes each factor as `LatexUnitNotation`
sets it, and an element-wise rounding's comment names the clause's helper,
`rounding_unit_in`, and the LaTeX grouping of a unit named by its size.
`rounding_call_text`'s parameter is `unitText`, since it receives a size
clause as well as a symbol. A test pins the LaTeX of a significant-digits
rounding in a unit with no symbol.

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

A number a formula declares in a dimensioned unit with no symbol is now
moved exactly into the coherent unit and followed by that unit's
spelling, in render() as in a trace, so that no number is shown in a
scale the text after it does not name. A move that fails writes
`(not shown: ...)`, never a number in the wrong scale. A unit with a
symbol, and a dimensionless unit with no symbol, which is at scale 1,
are never converted, and render as before.

The rule now covers a constant, a per-element constant's values
(`values(3/1000 kg, 1/200 kg)`), the bands, rows and values of a banded,
an exact and an interpolating lookup, a binning's classes, a snap's
permitted values, a domain's points and an envelope's limits
(`from 1/4 to 1/2 kg`).

One helper states the rule, `shown_number`, and `render()` and the trace
share it: the bound helper `shown_bound_text`, the unit's spelling
`shown_unit_spelling` and the envelope row `shown_limit_row` move from
the trace into render.hpp, with the author-text spelling as a parameter.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A table's row is one run of upright words in LaTeX, escaped as a whole,
so a coherent unit with a power, such as kg^-1, was set as escaped
characters. A coherent unit is now set outside the row's words, after a
thin space, as a constant's unit is: `\mathrm{1000\ to\ under\
2000}\,\mathrm{kg}^{-1}`. A row in a unit with a symbol keeps its unit
among the words, byte for byte as before.

An envelope's row has one spelling, `limit_row_text`, for render() and
for a trace, with the author-text spelling as a parameter. A derivation's
header moves its value into the coherent unit through `shown_number`, the
one rule every other declared number follows.

Tests pin the LaTeX of a table keyed in a per-gram unit with no symbol,
and render()'s text for a point, a permitted value and a limit that the
coherent unit cannot hold.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A row that holds a coherent unit is set as words, a thin space and the
unit's own factors and powers, not as one atom. The separator's comment
said each row is one atom; it now says a row may be several, none of
which is a break point in math, so its conclusion stands.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Pass 1 restates the level's value but took its unit from the types
alone: the limit's quantity, else the level expression's, else the
coherent unit. A level constant in grams read 40 g on its own line and
1/25 kg on the pass-1 line. Pass 1 now borrows the unit of the step it
restates, under the rule pass 2 and a conditional use; the unit the
types give stays the fallback.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A snap decides it sat on a permitted value by comparing two raw pairs,
where a lookup's rows compare values. That is exact: an exact hit
records one row twice, and the permitted set is strictly ascending by
value. A comment now says so. A missed curve lookup also spells its low
bound only on the path that writes it.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The check said the first pass reads in degrees Celsius as the constant's
own line does, but pinned only the first pass. It now pins both lines,
as the gram check does. The snap comment no longer says a lookup's rows
compare by value because they can be typed as different pairs: they too
are strictly ascending, and comparing by value simply does not depend on
how a row was typed.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The logarithm and exponential kernel takes 128-bit arguments, and the
bit widths two guides quote are measured by the census and by a test.

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

A series handed to explain or checked_explain failed with the compiler's
"no matching function": each had an overload for a single-value
expression and one for a bound formula, and a series is neither. Each now
has a series overload that refuses it as evaluate refuses one, pointing
at explain_series, the verb that gives a series' derivation. trace_of
and the bound forms refuse a series as before.

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

The README's tracing paragraph linked the tracing guide twice; it now
links it once, at the end. The 128-bit design document said the checked
add and subtract use the compiler's overflow builtins. They compute on
the words' bit patterns with the portable routines on every compiler, and
detect overflow from the signs; only the multiply uses the native checked
builtin.

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

The 128-bit design document said the checked multiply uses
__builtin_mul_overflow wherever the compiler has __int128. The native
path is taken on GCC and Clang outside MSVC's ABI only: clang-cl has
__int128 but multiplies in software, as cl does.

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

Comments, test names and section banners referred to stages of the
project's development and to one-off experiments by labels a reader
cannot look up. Each now says what it relies on: a measurement keeps its
compilers and versions or points at the test that pins it, a banner
names its feature, and a reference that added nothing is gone. Two census
test cases are renamed after what they cover.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Thirteen comments cited a "defect class" by number, a classification
recorded nowhere a reader can look up. Each now says what it relies on:
a missing `{}` initialiser points at `Corrections` (lookup.hpp), which
gives the measurement, and a comment that already stated the property
simply drops the label. A negative test that leaned on "the other defect
classes here" names the three files it means.

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

Comments in the lookup header and the tests still referred to tasks,
task reports and reviewers, and to work owed by "a later task" that has
since been done: a lookup's step now records its key, the interval its
table covers and why it failed, and the lookup comments say so. A
reference to a report or a reviewer is dropped.

Two claims were untrue. describe(ArithmeticError) is what std::format
and the trace's lines write, not an evaluation result's invalid arm, and
snapping does not call checked_round_to_multiple. The fixture names in
the record statistics tests now name the files that define them.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The released 0.1.0 entries were grouped under "Phase N:" headings that
named development stages. Each heading now names only the feature it
groups; every entry and the release structure are unchanged.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Christian Parpart added 6 commits October 5, 2026 02:44
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Only a dimensionless unit at scale 1 drops the unit clause; a dimensioned
unit with no symbol is written in its coherent unit instead. Both cases
test `unit::One` alone, so their names now say so.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Wraps the test lines over 125 columns that came in with the unnamed-unit
display work, following each file's own continuation style, and sets the
note on a dimensionless unit with no symbol in `spells_coherent_unit`'s
comment apart as its own paragraph. The header keeps its line count.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
`number_text` and `std::format` of a `Measured` value whose unit has a
dimension but no symbol wrote the number on that unit's scale with nothing
after it: 3 of a unit of 1/1000 kg read `3`. They now move it exactly into
the coherent unit and write that unit's spelling after it, `3/1000 kg` or
`0.003 kg`, as `render()` and a trace already do. A unit with a symbol, and a
dimensionless unit with no symbol, are written as before.

The coherent unit's order and shape are now written once, by a constexpr
walker in `number_text.hpp` that both `coherent_unit_spelling` and a
`NumberText` use, along with the plain spelling of a power;
`spells_coherent_unit`, `shown_unit_of` and `shown_number` move there with
it, and `coherent()` moves from `evaluate.hpp` to `unit.hpp`. A move into the
coherent unit that fails returns its error, and a spelling too long for a
`NumberText` returns `Overflow`. The gallery writes its worked value through
`number_text`, and names a dimensioned unit with no symbol by its size
instead of calling it dimensionless.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
`number_text` and `std::format` wrote a `Measured` value they had moved into
the coherent unit at that unit's default 3 places, so a decimal style padded
it and an approximating one could round a small value to `≈0 kg`: 1/3 of a
unit of 1/1000 kg read `≈0 kg`. They now spell it through the trace's own
`checked_shown_text`, which never pads those places and extends them to the
value's first significant digit: `≈0.0003 kg`, as its trace line reads.
`~.N` and `.N` still round at the places they name, and a value in any
other unit keeps the decimals its unit declares.

`checked_shown_text` and its helpers move from `render.hpp` to
`number_text.hpp`, so the three surfaces share one definition. The gallery
says why its unit cell uses the library's one spelling of a unit by its
size, which has no public form.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A `Measured` value in a dimensioned unit with no symbol is shown in the
coherent unit, at that unit's places, so the refusal for declared decimals
outside -18 to 18 applies only to a `Rational` in the unit passed and to a
`Measured` whose unit has a symbol or is dimensionless. Also wraps a test
line that was over 125 columns.

Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
@christianparpart
christianparpart marked this pull request as ready for review October 5, 2026 02:59
@christianparpart
christianparpart merged commit 9f68dbe into master Oct 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment