A unit on every computed trace value, and a 128-bit Rational - #12
Merged
Merged
Conversation
added 30 commits
October 3, 2026 17:49
… a 128-bit integer Two designs that land together. A trace step computed by arithmetic shows a unit read off its operand steps where that is safe, and otherwise its coherent symbol, so no computed value prints bare. Rational moves to a new formula::Int128, native where the compiler has a 128-bit integer and constexpr software where it does not, so realistic laboratory statistics no longer overflow. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… a 128-bit integer Eight tasks in two independent lanes that merge at the end: the trace recorder and renderer first, then formula::Int128, the readers of Rational made width-agnostic, and the switch with the overflow census re-measured. The Int128 design is refined to convert to no built-in integer type, so that no 64-bit narrowing can survive the switch silently, and to keep one storage layout on every compiler. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…has no unit of its own A unit with no symbol cannot say what scale its number is on, so a trace step whose unit has none now writes the coherent unit's spelling after its number (`1239/500000 kg`, `36 kg^2`, `60000000 kg/(m s^2)`). A dimensionless value is still a bare number. A value recorded in a unit that has no symbol but is not the coherent one is shown converted into the coherent unit, with that unit's spelling, rather than as a bare number in a scale nothing on the line names. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…heir value is shown in A conformity check whose unit has no symbol showed each element's value in the coherent unit but its row as bare numbers in the check's own scale, and a derivation's header showed a value in such a unit as a bare number. Both now follow the rule every trace value does: a dimensioned value whose unit has no symbol is converted into the coherent unit and followed by that unit's spelling. The decision is made in one place, `detail::shown_unit_of` and `detail::shown_unit_text`, which a step's value, a squared deviation, a conformity row and a derivation's header all use. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… compiler has one and constexpr everywhere Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… in, where that is safe A product with exactly one pure number, or a quotient by one, is shown in the other operand's unit, so 3/50 of a mean in grams reads in grams. A sum or a difference of two values shown on one scale under one symbol is shown in that unit, at the finer of the two declared precisions. A negation and an absolute value are shown in their operand's unit. A conditional and a precision limit, whose value restates another step's, are shown in that step's unit, offset or not, when the step holds exactly that value. A unit with an offset is never borrowed for a sum, a difference, a scaling or a negation: the difference of two Celsius readings is an interval, and reads in kelvin. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… mutation fails A Celsius reading scaled by a pure number, and its absolute value, read in kelvin; a product of two dimensionless values borrows no unit on either side; a conditional over a branch that records no step of its own, and a product over such a node, read in the coherent unit. Which side of a binary node is scaled by a pure number is now written once, for a single value's node and an elementwise one alike, and comments the borrowed units made false are corrected. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…flow contract once
Int128's arithmetic is unchanged. The tests now reach the boundaries:
- 0, +-1, +-2^63, 2^64 and -(2^127 - 1) as operands;
- -2^127 as a divisor, and its remainder by -1;
- a shift right by zero of a negative value;
- the checked product's refusal exactly past 128 bits, on both routes;
- every standard integer type converted in;
- to_double with no bits dropped;
- u128_pow10(0) and signed_from_magnitude.
The header states overflow once, as a built-in signed integer's: a
result that does not fit, -2^127 / -1 and division by zero are
precondition violations. An Int128 format spec it does not take is
refused by a guard of its own, which says that {} is the only one.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…n, and a header's value without the coherent detour A value declared in a unit with no symbol is shown in the coherent unit, followed by that unit's spelling. The bounds stated beside it were not: a snap's permitted values, a binning's classes, a banded lookup's bands, an interpolating lookup's or a curve's rows were written as raw numbers in the unnamed scale, a thousand times the kilograms written after the value on the same line. Each is now spelled in the unit the value is shown in, so every number on a line is in the unit written after it. A derivation's header converted its value from the declared unit to the declared unit through the coherent one. For a unit with a symbol that conversion does nothing but can overflow: 10^13 kWh, a good number of kilowatt-hours, read "(not shown: overflow in exact arithmetic)". The value is now shown as it is held. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…nd keep the minimum's negation inside the contract The portable checked product's cross-term exit is now pinned by 2^65 * 2^63, which overflows only there. That check runs on the compilers without a 128-bit integer of their own too. Division and signed_from_magnitude negate -2^127's magnitude through the bit pattern instead of through unary minus. The result is the same bits, and no call inside the contract now negates the minimum. The file comment names the arithmetic operators that carry the fits precondition. It says that << loses the bits shifted out, as a built-in << does since C++20, and that >> fills with the sign. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…he unit written after it A trace step's number is now always followed by the unit it is in: one the step declares, one borrowed from the steps it read where that is safe, or the coherent unit spelt from the base units. A new test walks whole traces -- the outlier rejection, an electricity bill in euros, the statistics of a series, a precision limit, an opaque call, conditionals and synthetic formulas -- and for every step that holds a value reads the number back from the unit written after it and checks it is the value recorded. Step::unit's comment, the renderer's comments and every guide that stated the old rule -- a computed value shown bare, in the coherent unit -- now state the new one, and each hand-written trace block reads as the library prints it. The display guide's section on a value in a unit nobody declared shows one again: a creep rate, a length over a time, in m/s, never padded and rounded at its first significant digit. The gallery and the numeric-headroom page are regenerated. Two more cases are pinned: an opaque output that cannot be shown reads "(not shown: ...)" with no unit after it, and a product whose right operand is a consumer's node that forwards the sink reads in the coherent unit. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…pell the shift example on Int128
The file comment now says the shifts are outside the overflow contract,
rather than "not arithmetic", which read against operator>>'s comment
calling >> arithmetic. The operator<< example is written as
Int128 { 1 } << 127, so it cannot read as a built-in int expression.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
No behaviour change. Every reader of a numerator or a denominator now goes through helpers that work on 128-bit magnitudes: number spelling, decimal exponents, wide rounding, the least-squares common denominator, sample-size lookups and the transcendental kernels. The overflow census tally and the wide integers carry 128-bit magnitudes too, ready for a wider Rational::Int. Int128 gains checked addition, subtraction and multiplication, floored division, a digit count and a power-of-ten product, beside their 64-bit forms. The places that store a Rational in a 64-bit structural type -- band, breakpoint, a trace's curve points and quotient units, and a rounded root's unit scale -- narrow on purpose and refuse a value that does not fit: band() and breakpoint() through guards that fail to compile in a constant expression. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…pelled text A curve's single row, and an interpolation that sat on a row, were recognised by their two bounds reading the same. A bound in a unit with no symbol is moved into the coherent unit to be written, and two different bounds whose coherent forms both overflow both read "(not shown: ...)": the line then claimed one row where the table has two. The bounds are now compared as the numbers they are. The walker's comment also says which numbers it does not read: those a line builds from side tables are pinned by their own tests. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The creep rate in the display example rounds to more places than the coherent unit's three, so padding could not show on its line. A bearing plate's area, from two whole-millimetre edges, now stands beside it: padded, its edges read 100.0 mm and 200.0 mm while the area reads 0.02 m^2, and the example checks it. The guide shows both lines from the example's output. Test comments and guide sentences that still described a computed step as always in the coherent unit now say what each line does, and a few over-long lines are re-wrapped. The numeric-headroom page is regenerated. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… integer width The test that two different rows which cannot be shown are not read as one reached them through rows whose coherent forms overflow 64 bits. A wider integer would show both, and the test would no longer test anything. The rows are now declared over a zero denominator, set by hand on a recorded lookup step: they name no number at any width, both read "(not shown: division by zero)", and the line must still say there are two. The README's paragraph on the units a trace shows is re-wrapped to the file's width. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… readers No behaviour change. LongestNumberText is now the longest text the header really writes: a fraction of a sign, two 39-digit integers, a slash, a space and a unit symbol, which is never marked approximate. The 64-bit put_whole, left with no caller, is gone; common_denominator states the four limbs from_u128 needs; the sample-size static_assert says how a size is compared; band() and breakpoint() say what their guards do at run time. The quoted band-gap diagnostic is recaptured at its new line. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Rational::Int is now formula::Int128. Computations that overflowed 64 bits on realistic laboratory data now answer exactly: - the sample variance of six masses read to the microgram: 0 of 1000 samples overflow at 6 dp (423 did), the least leaving 62 bits of headroom of 127; - the rejection by 7/4 standard deviations at 6 dp: 0 of 1000 overflow (897 did), with 58 bits left; - a cylinder's strength, 4F / (pi * d^2) at 89.3 kN in MPa: no diameter from 101 to 163 mm overflows (17 did), with 63 bits left, and the area 79. The overflow census measures against 127 bits, and the page's tables are regenerated from it. The _r literal, from_decimal and rounding keep their limits: a literal's mantissa is still 64 bits, and rounding still takes 0 to 18 places. rounded_sqrt computes in 128 bits, so an integer radicand of about 10^6 now rounds to 16 places. Narrowing into the 64-bit structural types -- a Band's bounds, a Breakpoint's key -- is refused, naming its guard, and never cut; two negative tests pin that. A 64-bit unsigned value converts to a Rational exactly, so the two negative tests that pinned its refusal are gone. A critical value's miss on a count past 64 bits spells the whole count (Step::lookupKeyHigh holds its high word) rather than "n = 0". The longest number text, a 39-digit fraction in a 16-byte unit, is pinned at exactly the buffer's size. Tests that pinned an Overflow at the 64-bit bound move it to the 128-bit bound and pin what the 64-bit case now answers. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… the variance's 128-bit figures The fifty-readings test now runs its three rounded checks on the eight-decimal readings, where the exact line overflows, and keeps the four-decimal ones as the case where both answer. The variance's and the least-squares fit's doc comments quote the census's 128-bit figures. The edge companion of the overflowing ratio runs at 1.3e19, below the square root of 2^127. Rational's integer constructor takes only types of up to 64 bits, as Int128's does. Stale comments and wrapping fixed. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… the bounds the docs state The test that the two-pass sample variance holds past the textbook one-pass form now computes the one-pass form itself: on fixture A scaled by 2^57 it overflows, where the square of the masses' sum outgrows 127 bits, while the two-pass form answers exactly; at 2^56 both agree. The overflow test pins the two-pass form's last scale, 2^62, at 427 * 2^118 / 1953125 kg^2, and the declared g^2 conversion's first overflow at 2^60. The decimal logarithm is pinned at 10^38 and 10^-38, the largest powers of ten a Rational holds. The rounded logarithm and exponential are pinned to refuse an argument whose numerator or denominator needs more than 64 bits with Overflow, and to answer just inside that bound. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The headroom page answers its question for a 128-bit Rational: every realistic case measured answers with far more than 8 bits to spare, and only the stress control on a different denominator for every point still overflows. It states what was chosen and why: 128-bit intermediates alone could not have helped, because checked_mul reduces before it multiplies, and the variances and strengths are stored in SI, where they need 64 bits or more; so the stored integer was widened. Headroom is now 127 minus the bits used, and the stress controls read 2^126 and 2^127. The guides and doc comments that stated Rational's range as 64 bits now state 128: the numbers guide's limits, the statistics, display, dimensions and expressions guides, the powers of ten a Rational holds, the checked integer primitives, rational_from_double's limits, and the rounded exponential's range, which keeps the 64-bit argument bound of its kernel. Int128's numeric_limits members are documented, so Doxygen builds clean. The CHANGELOG records Int128 and the 128-bit Rational. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…p of a wide negative argument A double below 2^53 rounds at 18 places, while 1e21 at 18 places, 1e38 at 1 and 2^-100 at -18 are Overflow. A Rational is 32 bytes, takes an unsigned 64-bit integer and refuses bool. The rounded exponential of -2^70 is 0: the rule below -43 comes before the kernel's argument bound. The 2^63 variance fixture is built with the file's own helper. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
rational_from_double rounds at every place only below 2^53 in magnitude. The lookup's conversion and interpolation-order examples, and the minimum's root, are restated at 128-bit scale. The rounded logarithms name the kernel's 64-bit argument bound, and the rounded exponential the order its rules apply in. Smaller wording fixes on the headroom page, the numbers guide and several comments. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ensus re-measured Brings formula::Int128 (native where the compiler has a 128-bit integer, portable constexpr elsewhere), every reader of a Rational's numerator and denominator moved onto 128-bit helpers, Rational::Int switched to Int128, the overflow census measured against 127 bits, and the documentation of the new range. CHANGELOG.md keeps both changes' entries. docs/numeric-headroom.md takes the 128-bit side and is regenerated after this merge. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The display example now shows an area and a creep rate in the coherent unit, so its row on the census page moves from 15/17/17 bits to 20/21/21, with 106 of 127 bits to spare. The gallery regenerates unchanged. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A derivation's header that shows a kilowatt-hour value as held was pinned with 10^13 kWh, whose joules (3.6 * 10^19) overflowed 64 bits but fit 128, so the test passed without the behaviour it guards. It now uses 10^35 kWh, which fits while its 3.6 * 10^41 J does not, and fails if the header goes through the coherent unit again. The changelog entry names the new value. The snap test of distances taken in the key unit relied on a permitted value whose SI denominator overflowed 64 bits. It now snaps an opening whose distance to a neighbour in metres needs a denominator past 2^127, pins that overflow, and snaps exactly in millimetres. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A 64-bit bound times a unit's 64-bit magnitude always fits a Rational, so a bound moved into the coherent unit no longer fails by overflowing its scale. It fails for a pair with a zero denominator, a unit of zero magnitude, or in principle an offset; the comparison of two bounds now takes two zero denominators as its example. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
log10 of 2^70 is Overflow, beyond the kernel's 64-bit words, while log10 of 10^30, a power of ten answered before the kernel is asked, is 30. And 2^53 - 1 converts exactly at 18 decimal places. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
rounded_log10 and the two guides said every argument wider than 64 bits is Overflow; a power of ten up to 10^38 is answered exactly, and an exponential below -43 is 0 whatever its width. The list of narrow_to_int64's users now includes a Unit's magnitude, the interpolation note no longer calls its reasoned cases measured, and four over-long lines are reflowed. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…mpiler A Step<Rational> is 1320 bytes on cl, clang-cl, g++ 14 and clang++ 20, all 64-bit and unchecked, up from 1296: the optional value it holds inline grew from 24 bytes to 40 with the 32-byte Rational, and lookupKeyHigh adds 8. The test pinned the old size, so every libstdc++ build failed its static assertion. That build also showed g++ 14 at -O3 failing -Warray-bounds in WideUnsigned::to_u128. Its early return left a tail reading only the low four limbs, which g++ split out of each width and merged across widths; the merged copy, typed for 19 limbs, was reported as reading past a 12-limb value. The code it emitted was correct. to_u128 now reads every limb before deciding, so there is no such tail, and the warning stays on. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
added 6 commits
October 4, 2026 02:16
WideUnsigned::to_u128 named its array of the low four limbs `low`, which a consumer may declare as a global: cl's C4459 turned the shadowing into an error in every build that includes least_squares.hpp. It is now `lowLimbs`. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The changelog no longer says a derivation's header used to read `(not shown: ...)` for 10^35 kWh: no release did. It now lists std::numeric_limits<formula::Int128>, says that Step::unit holds the unit a scaled, summed, negated, conditional or precision-limit step borrowed, and states the band and breakpoint refusal in one entry. The quantities guide says every built-in integer of up to 64 bits converts exactly, std::uint64_t among them, and a wider one is refused. The headroom guide says what a sum scaled by the product of the denominators does now: the twenty masses' headroom pin fails. The expressions guide names Rational::Int's minimum rather than IntMin, which is still the 64-bit one. The lookup-tables guide says that band and breakpoint refuse a bound or key past 64 bits, and how. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… fails to move A sample-size lookup's hit spelled only the low word of the count, so a hand-built Step whose count is past 2^64 read the wrong number. It now spells both words, as the miss already did; a test pins 2^64. The comments on shown_bound_text and block_value_text now say that the move into the coherent unit also fails for a malformed unit, one whose magnitude is zero (DomainError) or whose magnitude or offset has a zero denominator (DivisionByZero). A test pins the offset case: a bound of 1/(2^63 - 1) in a unit of magnitude 1/(2^63 - 25) and offset 1/(2^63 - 165) reads `(not shown: overflow in exact arithmetic)`. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The headroom guide said the census printed the same on cl 19.51 and gcc 13.3, a GCC no longer supported. Every preset regenerates the page and compares it, and the census tables passed on cl 19.51, clang-cl 22.1, g++ 14.2 and clang++ 20.1; the guide names those. The least-squares section of the opaque-operations guide says its overflow figures hold on the same four compilers, whose tests pin them. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The fit guide named integers and thirds mixed with sevenths among the data that never overflows on every compiler, but no test measures either shape. It now names only readings at one and three decimal places, a different denominator on every point, and the example's own data, which overflows at 27 points. The changelog lists the absolute value among the steps whose Step::unit borrows its operand's unit, and two comments are made exact. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The size pin's comment states that a step's inline optional value grew to 40 bytes; that figure is now a static_assert on every build. The census comment on the twenty masses says the variance failed to evaluate at 64 bits, and that at 128 bits the headroom check is what fails. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
christianparpart
marked this pull request as ready for review
October 4, 2026 02:04
This was referenced Oct 4, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A trace is meant to show how a number was reached, but arithmetic on single values printed bare numbers in coherent SI with no unit: the outlier-rejection limit read
#2 * #3 = 1239/500000, a mass in kilograms with nothing to say so. Separately, a 64-bitRationaloverflowed on realistic laboratory statistics: the sample variance of masses read to 6 decimal places of a gram overflowed on 423 of 1000 samples, rejection by standard deviations at that resolution on 897, and a cylinder's compressive strength on 17 of 63 diameters. This PR fixes both: every computed value in a trace now shows a unit, andRationalstores its numerator and denominator in a new 128-bit integer, so those cases answer.Changes
A unit on every computed trace value
kg^2,m/s,kg/(m s^2)); a dimensionless value is still a bare number.K. The rejection limit now reads#2 * #3 = 1239/500 g.Rationalover a 128-bit integerformula::Int128(int128.hpp): one two-word layout on every compiler, the compiler's own 128-bit integer computing where it has one (GCC, Clang) and portableconstexprcode elsewhere (cl, clang-cl), with 64-bit fast paths. It converts to no built-in integer;to_int64()andto_uint64()say when a value does not fit.std::numeric_limitsandstd::formatsupport it.Rational::IntisInt128, and aRationalis 32 bytes. Every reader ofnumerator()anddenominator()goes through width-agnostic helpers; fields that stay 64 bits (bands, breakpoints, a unit's scale, the transcendental kernel's arguments) refuse a value that does not fit rather than cut it.docs/numeric-headroom.mdis regenerated and rewritten: the 6 dp variance and the rejection overflow on none of 1000 samples, with at least 62 and 58 bits to spare, and the cylinder answers at every diameter with at least 63.StepgainslookupKeyHigh, so a sample-size lookup names a count above 2^64 − 1 in full.Rational's converting constructor takes every built-in integer of up to 64 bits exactly,std::uint64_tincluded.band(Rational, Rational)andbreakpoint(Rational)refuse a bound or key that does not fit their 64-bit fields.NumberTextCapacityis 128.Closes #1
Closes #2