Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ff148f1
docs(spec): a unit on every computed trace value, and a Rational over…
Oct 3, 2026
71b9fe5
docs(plan): a unit on every computed trace value, and a Rational over…
Oct 3, 2026
cf0ff51
feat(trace): write the coherent unit after every computed value that …
Oct 3, 2026
019a661
fix(trace): show conformity rows and derivation headers in the unit t…
Oct 3, 2026
3b99520
feat: add formula::Int128, a 128-bit integer that is native where the…
Oct 3, 2026
1ffbe4e
feat(trace): show a computed value in the unit its operands are shown…
Oct 3, 2026
56cd44e
test(trace): pin each guard of the borrowed-unit rule with a case its…
Oct 3, 2026
6b4f234
test(int128): pin every boundary the design lists, and state the over…
Oct 3, 2026
7490010
fix(trace): write a table's bounds in the unit their value is shown i…
Oct 3, 2026
c66cd96
test(int128): reach the cross-term overflow exit on every compiler, a…
Oct 3, 2026
4861d4f
docs(trace): state the shown-unit rule, and prove every value is in t…
Oct 3, 2026
3fea3da
docs(int128): say the shifts are outside the overflow contract, and s…
Oct 3, 2026
a558625
refactor: read Rational's integers through helpers that hold 128 bits
Oct 3, 2026
3604fad
fix(trace): tell two table rows apart by their values, not by their s…
Oct 3, 2026
ee9cbe8
docs(display): show a value in the coherent unit left unpadded
Oct 3, 2026
4fad8ab
test(trace): reach two unshowable table bounds without relying on the…
Oct 3, 2026
3c94fd0
refactor: state the longest number text exactly, and tidy the 128-bit…
Oct 3, 2026
3ff3081
feat: store Rational's numerator and denominator in 128 bits
Oct 3, 2026
22aa6be
test: check the rounded line where the exact one overflows, and state…
Oct 3, 2026
5a56f1b
test: show where the one-pass variance overflows at 128 bits, and pin…
Oct 3, 2026
113895f
docs: describe Rational's 128-bit range, and the headroom it measures
Oct 3, 2026
14d2aa0
test: pin where rational_from_double stops, a Rational's size, and ex…
Oct 3, 2026
6abdb9e
docs: state the 128-bit limits that hold, not the 64-bit ones
Oct 3, 2026
8d741ea
Merge a 128-bit Rational: formula::Int128, readers widened, and the c…
Oct 3, 2026
ed3ebcf
docs: regenerate the census page for both changes
Oct 3, 2026
055bd54
test: re-pin two tests whose overflow no longer happens at 128 bits
Oct 3, 2026
0a450b4
docs(trace): state when a declared bound fails to show
Oct 3, 2026
8f40456
test: pin log10 of wide arguments and rational_from_double at 2^53 - 1
Oct 3, 2026
9014a8f
docs: name the wide arguments that never reach the kernel, and reflow
Oct 3, 2026
f8b8d11
test: pin a step's size with a 128-bit Rational, measured on every co…
Oct 4, 2026
4de0f8d
fix: keep a wide integer's low limbs off a consumer's global name
Oct 4, 2026
37bf7e5
docs: correct stale statements about headers, integers and headroom
Oct 4, 2026
674dd58
fix(trace): name a critical value's whole count, and say when a bound…
Oct 4, 2026
c4d593d
docs: name the compilers the census and the fit's figures hold on
Oct 4, 2026
efa9600
docs: claim only the fit overflow figures a test pins
Oct 4, 2026
07b5ef4
test: pin an optional Rational's 40 bytes beside a step's size
Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@ change is recorded here.

## [Unreleased]

### Added

- **`formula::Int128`** (`int128.hpp`), a signed 128-bit integer with one API on every compiler: the compiler's own
128-bit integer computes where it has one (GCC, Clang), portable `constexpr` code everywhere else (cl, clang-cl).
It converts to no built-in integer implicitly or explicitly; `to_int64()` and `to_uint64()` say when a value does
not fit. `std::format` writes it in decimal, and `std::numeric_limits<formula::Int128>` states its limits as a
built-in signed integer's are.

### Changed

- **Every computed value in a trace shows a unit.** A value scaled by a pure number, and a sum or difference of
values shown in one unit, read in that unit: the outlier-rejection limit `#2 * #3 = 1239/500000` is now
`#2 * #3 = 1239/500 g`. A negation and an absolute value read in their operand's unit, and a conditional and a
precision limit in the unit of the step they restate. An offset unit is never borrowed for a sum, difference,
scaling or negation: the difference of two Celsius readings reads in `K`. Any other dimensioned value is shown in
the coherent unit, followed by its spelling from the base units (`427/125000000 kg^2`, `60000000 kg/(m s^2)`), and
so is a value in a unit that has no symbol. A dimensionless value is still a bare number. A trace text pinned in
a test changes wherever it showed a dimensioned value bare. `Step::unit` of a scaled, summed, negated,
absolute-value, conditional or precision-limit step now holds the unit it borrowed, so code that reads steps sees the unit the
trace text names.
- A snap's permitted values, a binning's classes, a lookup's bands and rows and a curve's rows, declared in a unit
that has no symbol, are written in the coherent unit with its spelling, as the value beside them is, rather than
as numbers in a scale the line does not name.
- **`Rational` stores its numerator and denominator in `formula::Int128`**, so `Rational::Int` is `Int128` and a
`Rational` is 32 bytes. Realistic laboratory statistics that overflowed 64 bits now answer: the sample variance of
masses read to 6 decimal places of a gram, rejection by standard deviations at that resolution, and a cylinder's
strength at every diameter measured (`docs/numeric-headroom.md`). Code that stored `numerator()` or `denominator()`
in a built-in integer must narrow with `to_int64()`. The `_r` literal, `Rational::from_decimal`'s exponents and
rounding's decimal places keep their limits of 18.
- `rounded_sqrt` computes in 128 bits, and answers at more places before it reports `Overflow`.
- `NumberTextCapacity` is 128, so that a 39-digit numerator over a 39-digit denominator fits a `NumberText`.
- `band(Rational, Rational)` and `breakpoint(Rational)` refuse a bound or key that does not fit their 64-bit fields:
in a constant expression it fails to compile, naming `formula_band_bound_out_of_range` or
`formula_breakpoint_key_out_of_range`; reached at run time, it ends the program, because a `Band` or a
`Breakpoint` is a template argument, built at compile time, and has no way to carry a failure.
- `Step` gains `lookupKeyHigh`, bits 64 to 127 of the count a sample-size lookup selected with, so that a miss on a
count above 2^64 - 1 names the whole count. Code that reads `lookupKey` for such a step reads both.
- `Rational`'s converting constructor takes every built-in integer type of at most 64 bits except `bool`, exactly,
`std::uint64_t` now among them; a wider built-in integer is refused. A constructor from `Int128` is added.

## [0.3.0] - 2026-10-01

The third release. It gives a shorter spelling to everything the examples repeated, and takes no
Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ target_sources(formula-cpp INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/format.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/formula.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/function.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/int128.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/least_squares.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/lineage.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/include/formula-cpp/lookup.hpp"
Expand Down Expand Up @@ -210,7 +211,7 @@ if(FORMULA_BUILD_TESTS AND FORMULA_BUILD_EXAMPLES AND FORMULA_TOOLS)
add_test(NAME census.exact-sizes
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tools/census/exact_sizes.py")
set_tests_properties(census.exact-sizes PROPERTIES PASS_REGULAR_EXPRESSION
"at 6 dp: the exact variance does not fit 64 bits in 374 of 1000 in kg2 \\(SI\\), in 0 of 1000 in g2 \\(declared. widest 45 bits\\)[\r\n]+[^\r\n]*at d = 101 \\(64 bits\\), 103 \\(64 bits\\), 107 \\(64 bits\\), 109 \\(64 bits\\), 113 \\(64 bits\\), 119 \\(64 bits\\), 121 \\(64 bits\\), 127 \\(64 bits\\), 131 \\(64 bits\\), 137 \\(64 bits\\), 139 \\(64 bits\\), 143 \\(64 bits\\), 149 \\(64 bits\\), 151 \\(64 bits\\), 157 \\(64 bits\\), 161 \\(64 bits\\), 163 \\(64 bits\\)[\r\n]+[^\r\n]*in MPa \\(declared\\) it does not fit at 0 of 63 \\(widest 44 bits\\)")
"at 6 dp: the exact variance does not fit 128 bits in 0 of 1000 in kg2 \\(SI. widest 65 bits\\), in 0 of 1000 in g2 \\(declared. widest 45 bits\\)[\r\n]+[^\r\n]*in Pa \\(SI\\) the exact strength does not fit 128 bits at 0 of 63 \\(widest 64 bits\\)[\r\n]+[^\r\n]*in MPa \\(declared\\) it does not fit at 0 of 63 \\(widest 44 bits\\)")
endif()
add_test(NAME docs.numeric-headroom
COMMAND "${CMAKE_COMMAND}" ${censusPageArguments} -P "${PROJECT_SOURCE_DIR}/cmake/CheckCensusPage.cmake")
Expand Down
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,18 +234,21 @@ std::print("{}", formula::render_trace(explained.trace, { .maxSteps = 10 }));
4. #3 = 3/5 [Water/cement ratio, Example Standard 1:2020, 5.4.2, (3)]
```

Every value is shown in the unit it was declared in, not the coherent unit
the arithmetic actually ran on — that is `9/50` cubic metres above, and nobody
typed cubic metres. When the environment overrides the result instead of
letting the formula derive it, `explained.trace` comes back empty — nothing
ran, so nothing was recorded — and `explained.outcome.is_overridden()` says
so instead: an overridden number shows *that a person entered it*, a
different fact from how it was reached and arguably a more important one.
See [the tracing guide](docs/tracing.md) for the detail. Tracing costs
nothing when nobody asks for it: a sink is
passed by value, and the untraced path — `evaluate()`, `checked_evaluate()` —
defaults to one that does nothing, adding no instruction the evaluator would
not already emit once the call inlines, measured on all four compilers this
Every value is shown in the unit written after it: an input in the unit it was
declared in, not the coherent unit the arithmetic actually ran on — that is
`9/50` cubic metres above, and nobody typed cubic metres. A computed value
borrows the unit of the values it was computed from where that is safe, and is
otherwise shown in the coherent unit, spelt from the base units (`kg/m^3`);
only a dimensionless value, like the ratio above, is a bare number. When the
environment overrides the result instead of letting the formula derive it,
`explained.trace` comes back empty — nothing ran, so nothing was recorded —
and `explained.outcome.is_overridden()` says so instead: an overridden number
shows *that a person entered it*, a different fact from how it was reached and
arguably a more important one. See [the tracing guide](docs/tracing.md) for
the detail. Tracing costs nothing when nobody asks for it: a sink is passed by
value, and the untraced path — `evaluate()`, `checked_evaluate()` — defaults
to one that does nothing, adding no instruction the evaluator would not
already emit once the call inlines, measured on all four compilers this
library targets. See [the tracing guide](docs/tracing.md).

### A published table that a value falls outside of gives no number at all
Expand Down
2 changes: 1 addition & 1 deletion cmake/CheckCensusPage.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ census_block(regression regressionTable)

# ---- The census twins ---------------------------------------------------------------
set(examplesTable "| program | numerator bits | denominator bits | intermediate bits | headroom |\n|---|---|---|---|---|\n")
set(censusLine "overflow census: numerator ([0-9]+) bits, denominator ([0-9]+) bits, intermediate ([0-9]+) bits, unsigned ([0-9]+) bits; headroom ([0-9]+) of 63")
set(censusLine "overflow census: numerator ([0-9]+) bits, denominator ([0-9]+) bits, intermediate ([0-9]+) bits, unsigned ([0-9]+) bits; headroom ([0-9]+) of 127")

function(twin_row label program outVariable)
execute_process(
Expand Down
16 changes: 9 additions & 7 deletions docs/calculations.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,9 @@ In it, a calculated value it reads is one step, marked `calculated`:
daily_load = fridge_kwh + oven_kwh + heater_kwh = 13.3 kWh
1. fridge_kwh = 4.8 kWh, calculated
2. oven_kwh = 2.5 kWh, calculated
3. #1 + #2 = 26280000
3. #1 + #2 = 7.3 kWh
4. heater_kwh = 6 kWh, calculated
5. #3 + #4 = 47880000
5. #3 + #4 = 13.3 kWh
```

The blocks of the calculated values it reads follow it, the last calculated
Expand All @@ -559,7 +559,7 @@ the fridge's energy reads. The inputs read come last, one line each:
fridge_kwh = fridge_kw * fridge_h = 4.8 kWh
1. fridge_kw = 0.4 kW, calculated
2. fridge_h = 12 h
3. #1 * #2 = 17280000
3. #1 * #2 = 17280000 m^2 kg/s^2
fridge_kw = fridge_w = 0.4 kW
1. fridge_w = 400 W
inputs
Expand Down Expand Up @@ -594,9 +594,11 @@ Asking for `Q` brings it up to date, as `checked_calculate` does, and counts as
it does; recording the blocks calculates nothing again. Here nothing was out
of date, and the example checks that neither counter moved.

A computed step states its value in the coherent unit of its dimension, as
every trace does: the fridge's 4.8 kWh reads `17280000` there, in joules,
under a header in kilowatt-hours.
A computed step states its value in the unit its trace step is shown in, as
every trace does: a unit borrowed from the steps it read where that is safe,
and otherwise the coherent unit of its dimension, spelt from its base units. A
power times a time borrows neither one's unit, so the fridge's 4.8 kWh reads
`17280000 m^2 kg/s^2` there, in joules, under a header in kilowatt-hours.

## A value typed in by hand

Expand Down Expand Up @@ -629,7 +631,7 @@ std::string const gridText = formula::render_derivation(gridCost, { .maxSteps =
grid_cost = net_draw * price = 62.5 EUR
1. net_draw = 250 kWh, entered by hand
2. price = 0.25 EUR/kWh
3. #1 * #2 = 62.5
3. #1 * #2 = 62.5 EUR
net_draw = 250 kWh, entered by hand in place of monthly_load - self_used
... 1 further step not shown
```
Expand Down
19 changes: 10 additions & 9 deletions docs/dimensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,14 @@ which is why a dimension should only ever be built with `base_dimension` and
the operators.

**In a coherent unit, the name is the symbol.** A computed step in a trace
carries no unit symbol of its own (see
[Tracing and audit trails](tracing.md#reading-a-derivation)), but where the
trace does spell a coherent unit out -- for an opaque operation's output that
no input's unit fits ([Opaque operations and bounded retry](opaque-and-retry.md))
-- a named base is written by its name, ahead of the SI units on its side of
the slash: `EUR s^2/(m^2 kg)` for euros per joule, then `1/JPY`, `EUR/JPY`,
`EUR^(1/2)`. The money comes first because a tariff is read as money per
energy.
that has no unit to borrow from the steps it read is shown in the coherent
unit, and the trace spells that unit out after its number (see
[Tracing and audit trails](tracing.md#reading-a-derivation)) -- as it does for
an opaque operation's output that no input's unit fits
([Opaque operations and bounded retry](opaque-and-retry.md)). A named base is
written by its name, ahead of the SI units on its side of the slash:
`EUR s^2/(m^2 kg)` for euros per joule, then `1/JPY`, `EUR/JPY`, `EUR^(1/2)`.
The money comes first because a tariff is read as money per energy.

## Limits

Expand Down Expand Up @@ -498,7 +498,8 @@ filled by hand is checked by none of them.

`Unit`'s `magnitudeNumerator`, `magnitudeDenominator`,
`offsetNumerator`, `offsetDenominator` and the four fields of `Bounds` are all
`std::int64_t`, the same width as `Rational`'s own numerator and denominator.
`std::int64_t`. `Rational`'s own numerator and denominator are 128-bit, so every
value these fields state converts to one exactly.

Conversion is built on `formula::Rational` and the `checked_` arithmetic
functions, so it inherits their overflow behaviour and rounding limits
Expand Down
Loading
Loading