docs(adr): ADR 005 sets K = 8 on the user's second answer to question 16 (improvise-l2b8)

Revision 8. "Ok, let's drop K back to 8" supersedes "let's do k = 32". Figures re-derived from the appendix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (adr5-steward)
2026-10-08 21:06:24 -07:00
committed by Edward Langley
parent 76fc5a19ff
commit e258599d1c
+87 -65
View File
@@ -1,7 +1,7 @@
# ADR 005 — Exact decimal arithmetic: a three-level numeric tower for formulas
**Status:** proposed. Only the user accepts an ADR. All sixteen numbered questions are
answered (question 16 in revision 7); the ADR waits for the user's "Approved". ·
answered (question 16 last, in revision 8, K = 8); the ADR waits for the user's "Approved". ·
**Ticket:** improvise-l2b8 (parent improvise-felo) ·
**Direction from:** the user, 2026-09-22 (below) ·
**Relates to:** ADR 009 (incremental evaluation, improvise-felo.4, PR #309), which now
@@ -72,16 +72,26 @@ for question 15. It answers the review of `33833f28` (issuecomment-5836990563):
It also corrects when a last-bit miss is visible to `=`. The bound is on the **terms**,
not the total, as ADR 009 now says at `4973845f` (Codex on #309).
**Revision 7** folds in **the user's answer to question 16**, left inline on `b232c5a9`
at 03:37 UTC on 2026-10-09: "let's do k = 32". K is **32**, which is option (a) and not
the recommendation (b), K = 8. Every question 1–16 is now answered, and the ADR stays
**Revision 7** folded in **the user's first answer to question 16**, left inline on
`b232c5a9` at 03:37 UTC on 2026-10-09: "let's do k = 32". It made K **32**, option (a).
**Revision 8 supersedes that answer** (below). Every question 1–16 was then answered, and the ADR stayed
**proposed** until the user says "Approved". The change is confined to the value of K
and the text that waited on it; the figures that depend on K were re-derived from the
appendix's own measurements and none is new (question 16's "Re-derived under K = 32"
note lists them). The revision also adds "Relations to other ADRs", a note on what ADRs
appendix's own measurements and none is new (question 16's "Re-derived" notes list them, now for K = 8). The revision also adds "Relations to other ADRs", a note on what ADRs
007, 008 and 009 ask of this one since it was written. It changes none of the user's
decisions, and it lists the places where the other ADRs' text and this one's disagree.
**Revision 8** folds in **the user's second answer to question 16**, a PR-conversation
comment at 04:04 UTC on 2026-10-09: "Ok, let's drop K back to 8". It **supersedes** "let's
do k = 32": K is **8**, option (b), which was the recommendation of question 16. The
earlier answer stays on the record, quoted in "What the user decided". The comment came
about a minute after revision 7's relations note put the `Eager` exposure of K = 32 on
the PR (`76fc5a19`, committed 04:03:29Z); the comment gives no reason, and the ADR does
not supply one. Every dependent figure was re-derived from the appendix's own
measurements (question 16 lists them), and none is new. The K = 8 cutoff is still the
second exception to "exact iff it fits" (question 13), and still overrides
representability.
## What the user decided
The user answered inline on `e4acf643`, one comment per question. Each answer is quoted
@@ -178,7 +188,7 @@ stored bound. One figure changed after the answer, and it is flagged here: the e
read was an estimate of 16–34 µs at K = 32, and it measures **0.67–1.9 ms** in the worst
case (option (d), "What that costs"). K is a constant inside `numtower`, not a format
or wire fact, so its value was split out as question 16 rather than reopening this one.
The user then decided K = 32 there.
The user decided K = 8 there (first K = 32, then K = 8; the later answer governs).
**11. The `MIN`/`MAX` tie-break.**
> "Yeah, exact."
@@ -200,8 +210,8 @@ Clause 12 defines the one number language this uses.
Decided: **(b)**. The bound applies to the **stored** result, and every intermediate
inside one formula evaluation is unbounded up to the ceiling. The ceiling is the first
of two exceptions to "exact iff it fits", sized to the grouped read (question 13). The
second, added by revision 7, is the K = 32 cutoff on a `SUM` (question 13, "The K
cutoff").
second, added by revision 7, is the K cutoff on a `SUM`, K = 8 since revision 8
(question 13, "The K cutoff").
**14. The numeric crate's name.**
> "this is fine"
@@ -215,19 +225,24 @@ Decided: **(a)**, in `numtower`, beside the exact decimal accumulator. ADR 009's
crate adapts it to `zset`'s group trait through a newtype, and `numtower` can land
holding `ExactSum` alone, ahead of this ADR (clause 14).
**16. The value of K.**
**16. The value of K.** The user answered twice. Both are quoted verbatim.
First, inline on `b232c5a9`, on question 16's options (2026-10-09T03:37:34Z, comment
4226381911):
> "let's do k = 32"
(Left on `b232c5a9`, on question 16's options, 2026-10-09T03:37:34Z; comment
4226381911.)
Then, in a PR-conversation comment (2026-10-09T04:04:36Z, comment 6074029223):
> "Ok, let's drop K back to 8"
Decided: **(a), K = 32.** This is the value revision 5 proposed and question 10 was
answered with, and it is **not** the recommendation of question 16, which was K = 8.
The worst-case exact read stays at **0.67–1.9 ms**, the measured figure the question put
to the user (0.67 ms is the lcm method, the one `numtower` uses; 1.9 ms reduces after
every add). The ceiling stays at 8,192 bits, which already covers K = 32 (4,320 bits
needed). K remains a constant inside `numtower`, so it can still be tuned later against
the evaluator benchmark without a new ADR.
Decided: **(b), K = 8.** The later answer governs, and the first is superseded, not
deleted. K = 8 is the recommendation of question 16, and not the value revision 5
proposed and question 10 was answered with. The worst-case exact read is **52–127 µs**
(52 µs is the lcm method, the one `numtower` uses; 127 µs reduces after every add). The
combined numerator is at most K·127 + 256 = 1,272 bits, which the 8,192-bit ceiling
clears by 6,920. A `SUM` over more than 8 distinct non-decimal denominators is
inexact, and the cutoff overrides representability (question 13). K remains a constant
inside `numtower`, so it can still be tuned later against the evaluator benchmark
without a new ADR.
## The decision this ADR writes down
@@ -841,7 +856,7 @@ further to `inf` only at the inexact level.
- **The result is exact if every value was exact** and the exact total fits the stored
bound: a `Decimal`, or, under option (d), an `i128` fraction (a sum of fractions can
fit the fraction arm without fitting a `Decimal`). **A second condition applies to
fractions:** if the sum holds more than K = 32 non-decimal denominator groups it reads
fractions:** if the sum holds more than K = 8 non-decimal denominator groups it reads
the inexact f64 accumulator, so it is inexact **even when its exact total would have
fitted** (clause 8's recommendation below, question 16). Otherwise it is inexact. **It must not be a fold with contagion part-way through.**
Such a fold switches to f64 at the first inexact value in store order, so the answer
@@ -908,9 +923,10 @@ each group is an integer:
- a group's numerator is at most `i128` × an `i64` weight × 2⁶⁴ terms, so **256 bits**
per group;
- the only unbounded step is the **read**, which combines the groups over their lcm;
- with at most K = 32 groups that lcm is at most about 4,100 bits, or 2,030 at K = 16,
and the combined numerator at most K·127 + 256 ≈ 4,320 bits (measured: 3,924 and
4,053 bits for 32 random 126-bit denominators and 250-bit numerators).
- with at most K = 8 groups that lcm is at most about 1,016 bits (K·127), or 2,030 at
K = 16 and 4,100 at K = 32, and the combined numerator at most K·127 + 256 ≈ 1,272
bits at K = 8 (measured lcm: 988 bits for 8 random 126-bit denominators; 1,957 at 16
and 3,924 at 32; the combined numerator was measured only at K = 32, 4,053 bits).
Under question 13 (b) that read runs in a bigint rational (`num-bigint`), so it needs no
fixed register at all. Its result is then held to the stored bound: it is exact if it
@@ -1395,7 +1411,7 @@ integer and reads in O(1). The review proposed a mitigation, and it is adopted:
accumulator alone, and is converted to f64 **once, at an f64 read** (only when there
are more than K non-decimal groups). So a pure-decimal `SUM` pays nothing for (d).
- **Read exactly only when the current number of non-decimal groups is at most
K = 32** (decided, question 16). Otherwise read the f64 accumulator, and the result is inexact. The count is
K = 8** (decided, question 16; revision 8). Otherwise read the f64 accumulator, and the result is inexact. The count is
a function of the current state, not of history, so the choice is independent of
order.
@@ -1427,7 +1443,7 @@ integer and reads in O(1). The review proposed a mitigation, and it is adopted:
should do that.
- Denominators a person produces (`/ 3`, `/ 12`, a percentage) are small, and the read
is then far cheaper. The table is the bound, not the common case. The value of K is
32, decided in question 16.
8, decided in question 16.
- **f64 read:** ADR 009's 0.6–1.1 µs.
- **Memory is still O(distinct denominators)**, because the map must survive
retraction to know when the count falls back under K. For a 90k-record margin sum
@@ -1438,11 +1454,10 @@ integer and reads in O(1). The review proposed a mitigation, and it is adopted:
This keeps (d)'s wins, because `SUM(v / 3)`, averages and percentages have one or a
handful of denominators, and it bounds the rest. The sum of margins reads the f64
accumulator and is inexact, as under (a). Its exact combination would overflow by the
6th term anyway (below). The 32 was a judgement: large enough for a `/ N` over a few
different Ns, and small enough to bound the read. It was not tuned. The read's measured
cost above is why question 16 recommended a smaller one (K = 8), and the user chose 32
anyway, so 0.67–1.9 ms is the accepted worst case. It is paid per read, and under ADR 009
per edit, by a maintained `SUM` that holds between 2 and 32 distinct non-decimal
6th term anyway (below). The 32 of revisions 5 to 7 was a judgement and was not tuned. The read's measured cost
above is why question 16 recommended a smaller one (K = 8). The user first chose 32
and then 8, so **52–127 µs is the accepted worst case**. It is paid per read, and under
ADR 009 per edit, by a maintained `SUM` that holds between 2 and 8 distinct non-decimal
denominators near `i128` size; a sum with more groups reads the f64 accumulator, and one
with few or small denominators is far cheaper (the table is the bound).
@@ -1717,14 +1732,14 @@ open. Nothing is renumbered.
denominators, with no bigint on the read;
- **(c)** `Decimal`'s own `+`, accepting demotion that depends on order.
*Recommendation:* (a), with K = 32. The user confirmed the value in question 16.
*Recommendation:* (a), with K = 32 as proposed here. Question 16 later set K = 8.
- A fixed register cannot be generic for fractions at any width.
- 2,048 bits pays 256 bytes per summed group for a combination that rarely fits the
stored bound anyway.
- The bigint read is paid only when a sum holds 2 to K distinct fraction
denominators: about 16–34 µs at K = 32, an estimate. (Revision 6 measured it:
0.67–1.9 ms at K = 32 in the worst case, and the user kept K = 32. See
question 16.)
0.67–1.9 ms at K = 32 in the worst case, 52–127 µs at K = 8, and the user chose
K = 8. See question 16.)
- (c) breaks order-independence (revision 2).
11. **The `MIN`/`MAX` tie-break.** **Decided (the user): prefer the exact value**
@@ -1752,7 +1767,7 @@ open. Nothing is renumbered.
can overflow in one spelling and not the other, so one is exact and the other a
demoted f64. Under (b) the rule is one sentence: **a stored value is exact if and
only if it fits.** Algebraically equal formulas give equal values. (Two stated
exceptions: the ceiling below, and a `SUM` of more than K = 32 non-decimal
exceptions: the ceiling below, and a `SUM` of more than K = 8 non-decimal
denominator groups, which reads inexact by question 16 whatever its value.)
**The rule has two exceptions, and they are stated rather than hedged** (the ceiling
@@ -1760,23 +1775,24 @@ open. Nothing is renumbered.
`be9de2ee`):
- **The ceiling.** Widening carries a size ceiling so that a pathological
expression cannot grow a bigint without limit. It must be at least
K·127 + 256 bits, so that the grouped read (below) never reaches it: 4,320 bits
at K = 32. Revision 5's example of 4,096 bits was below that, and a worst-case
K = 32 read would have hit it (the review of `33833f28`). Proposed: **8,192
bits**, which covers K = 32 with room, and costs nothing until a value is that
large. Past it, the
K·127 + 256 bits, so that the grouped read (below) never reaches it: 1,272 bits
at K = 8 (it was 4,320 at K = 32). Revision 5's example of 4,096 bits was below the
K = 32 figure, and a worst-case K = 32 read would have hit it (the review of
`33833f28`). Proposed: **8,192 bits**, which covers K = 8 by 6,920 bits and
K = 32 as well, and costs nothing until a value is that large. A smaller ceiling
would now suffice; 8,192 is kept because question 13 was decided against it. Past it, the
intermediate demotes, and by contagion the stored result is inexact **even when
the exact result would fit**. That is spelling-dependent, so distributivity fails
past the ceiling. It is the first exception to "exact iff it fits", and no ledger
reaches it. `POW` checks its result size **before** computing (clause 5), so the
ceiling is never reached by allocating first.
- **The K cutoff, which does override representability.** A `SUM` holding more than
K = 32 non-decimal denominator groups reads the inexact f64 accumulator, so its
K = 8 non-decimal denominator groups reads the inexact f64 accumulator, so its
stored result is inexact **even when the exact total would fit** the bound. That
is the second exception. It is a policy about the read's cost (question 16), not
about whether the value is representable, and it is the consequence of the user's
K = 32. **Flagged reading (revision 7):** the user decided K = 32 on question 16's
text, which says "Above it the read is the inexact f64 accumulator", and answered
K = 8. **Flagged reading (revision 7, restated in revision 8):** the user decided K
on question 16's text, which says "Above it the read is the inexact f64 accumulator", and answered
question 10 (a) with "at most K groups". No "What the user decided" item says in
terms that the cutoff overrides representability. That sentence is the ADR's
reading of those two answers, so approval confirms it.
@@ -1814,7 +1830,9 @@ open. Nothing is renumbered.
`ExactSum` alone, ahead of this ADR, so ADR 009 does not wait for it (clause 14).
This question is shared with ADR 009 (PR #309).
16. **The value of K.** **Decided (the user): (a), K = 32** ("let's do k = 32"). Added
16. **The value of K.** **Decided (the user): (b), K = 8** ("Ok, let's drop K back to 8",
superseding an earlier "let's do k = 32"; both quoted under "What the user
decided"). Added
in revision 6, and put as follows. K is the most non-decimal groups a
`SUM` reads exactly (question 10). Above it the read is the inexact f64
accumulator. Revision 5 proposed K = 32 on an estimate of 16–34 µs per read, and
@@ -1822,12 +1840,13 @@ open. Nothing is renumbered.
ADR 009 a maintained `SUM` reads after every edit that touches it, so that is a
per-edit cost, paid by each such cell.
- **(a)** K = 32, as decided with question 10: worst case 0.67–1.9 ms per read.
Chosen first, then **superseded**.
- **(b)** K = 8: worst case 52–127 µs per read. A `SUM` over more than 8 distinct
denominators is inexact.
denominators is inexact. **Chosen (the later answer).**
- **(c)** K = 16: worst case 174–448 µs per read.
*Recommendation (not taken):* **(b), K = 8**, with the read combining over the lcm
and reducing once (the lower figure in each range). The user chose (a).
*Recommendation (taken in the end):* **(b), K = 8**, with the read combining over the
lcm and reducing once (the lower figure in each range). The user chose (a), then (b).
- The ceiling (question 13) stays at 8,192 bits under any of the three.
- The common sums keep exactness under K = 8: `SUM(v / 3)` has one denominator, and
an average or a percentage has a handful.
@@ -1836,18 +1855,19 @@ open. Nothing is renumbered.
- K is a constant inside `numtower`, not a format or wire fact, so it can be tuned
later against the evaluator benchmark (migration step 1) without a new ADR.
**Re-derived under K = 32** (revision 7), from this ADR's own measurements, with
nothing new measured:
- **Read cost:** the K = 32 row of the table, **0.67 ms** (lcm, then one reduce) to
**1.9 ms** (reduce after every add). `numtower` reads by the first method, so the
**Re-derived under K = 8** (revision 8), from this ADR's own measurements, with
nothing new measured (the K = 32 figures of revision 7 are superseded):
- **Read cost:** the K = 8 row of the table, **52 µs** (lcm, then one reduce) to
**127 µs** (reduce after every add). `numtower` reads by the first method, so the
lower figure is the working number. The 16–34 µs of revision 5 stays withdrawn.
- **Combined width:** lcm up to about 4,100 bits (measured 3,924), numerator up to
K·127 + 256 = **4,320 bits** (measured 4,053).
- **Ceiling:** 8,192 bits, which clears 4,320 by 3,872. Unchanged, so question 13
- **Combined width:** lcm up to K·127 = 1,016 bits (measured 988), numerator up to
K·127 + 256 = **1,272 bits**. The combined numerator was measured only at K = 32
(4,053 bits), so 1,272 is a bound and not a measurement.
- **Ceiling:** 8,192 bits, which clears 1,272 by 6,920. Unchanged, so question 13
is untouched.
- **Memory:** O(distinct denominators), which K does not change (clause 8).
- **Not re-derived:** an end-to-end per-edit latency for a maintained `SUM`. The
benchmark times the read alone. Whether 0.67–1.9 ms per edit is acceptable on a
benchmark times the read alone. Whether 52–127 µs per edit is acceptable on a
page of such cells is a question for the evaluator benchmark, not a figure here.
## Relations to other ADRs
@@ -1901,22 +1921,24 @@ don't need to reinvent integers > safe size", makes a grid position a bigint abo
can land holding it alone, so ADR 009 does not wait for this ADR.
- **`Sum` is exact only under ADR 009's question 3 (c) or under this ADR's wide
accumulator.** This ADR's accumulator is exact for decimals (320 bits) and for up to
K = 32 fraction denominators (clause 8). For **f64** terms the exact accumulator is
K = 8 fraction denominators (clause 8). For **f64** terms the exact accumulator is
ADR 009's `ExactSum`. A mixed sum is read from both (clause 8), so "exact under this
ADR" holds for decimal and bounded-fraction data, not for f64 data.
- **The per-edit cost is a lower bound, and the larger exposure is `Eager`.** ADR 009
reads a maintained `SUM` after every edit that touches it. With K = 32 (question 16)
the read alone is up to 0.67–1.9 ms for a cell holding many large distinct
denominators (see "Re-derived under K = 32"), and the update cost comes on top. ADR
reads a maintained `SUM` after every edit that touches it. With K = 8 (question 16)
the read alone is up to 52–127 µs for a cell holding several large distinct
denominators (see "Re-derived under K = 8"), and the update cost comes on top. ADR
009 says `Eager` "pays the read on every aggregate evaluation", and today's TUI
projects the whole grid every frame, so it pays one read per summed cell. Against ADR
009's budget (8 ms per operation, a 2 ms slice for a job; ADR 008 clause 11), as
arithmetic from these figures and not a finding: one worst-case read at 1.9 ms nearly
fills a slice; **five** such cells (5 × 1.9 = 9.5 ms) or **twelve** at 0.67 ms
(12 × 0.67 = 8.04 ms) exceed 8 ms. This is not a reason to revisit K = 32, which the
user chose knowing the read's cost. It is a question for ADR 009's `Eager` and cap
design. The worst case needs 2 to 32 large distinct denominators in one sum; a sum of
small denominators is far cheaper. ADR 009's own
arithmetic from these figures and not a finding: **154** such cells at 52 µs
(154 × 52 = 8.0 ms; 153 stay under) or **63** at 127 µs (63 × 127 = 8.0 ms) exceed
8 ms, and **39** at 52 µs or **16** at 127 µs exceed a 2 ms slice. At the earlier
K = 32 (superseded) it was five and twelve cells per 8 ms, and one read nearly filled a
slice. K = 8 is 13 to 15 times cheaper per read than 32 (0.67/0.052, 1.9/0.127). It remains a question for ADR
009's `Eager` and cap design, but a far smaller one. The worst case needs 2 to 8 large
distinct denominators in one sum; a sum of small denominators is far cheaper. ADR
009's own
`ExactSum` figures (2.0–3.6 ns per update, 0.6–1.1 µs per read) are for the f64
accumulator and are not comparable.
- **Flag, stale text in ADR 009.** At its head it describes ADR 005's cost as
@@ -1976,7 +1998,7 @@ don't need to reinvent integers > safe size", makes a grid position a bigint abo
- A value typed past `Decimal`'s range is an exact fraction when it fits `i128`
(`1e-30`, a 30-digit integer), as the same value computed already is (clause 3).
- A `SUM` reads exactly over at most K distinct fraction denominators, at a worst-case
cost that grows about as K² (question 16: K = 32, decided), and past K it is inexact.
cost that grows about as K² (question 16: K = 8, decided), and past K it is inexact.
- Formulas gain `EQUAL(a, b, epsilon)`, and `EQUAL` becomes a keyword the writer quotes.
- **Costs:** `Num` is 24 bytes where an `f64` is 8, but **`CellValue` stays 32 bytes**
under both (a) and (d), because its `String` arm already sets the size (the review