docs(adr): ADR 005 folds in the user's answer to question 16, K = 32 (improvise-l2b8)
Revision 7. Q16 decided (a), K = 32, over the K = 8 recommendation; figures re-derived from the appendix, none new. Adds Relations to other ADRs (007, 008, 009) with flagged conflicts. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
b232c5a9fa
commit
5f0bbb837b
@@ -1,7 +1,7 @@
|
||||
# ADR 005 — Exact decimal arithmetic: a three-level numeric tower for formulas
|
||||
|
||||
**Status:** proposed. Only the user accepts an ADR. The numbered questions at the end
|
||||
are theirs to answer on the PR. ·
|
||||
**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". ·
|
||||
**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,6 +72,16 @@ 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
|
||||
**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
|
||||
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.
|
||||
|
||||
## What the user decided
|
||||
|
||||
The user answered inline on `e4acf643`, one comment per question. Each answer is quoted
|
||||
@@ -167,7 +177,8 @@ and a read that combines at most K groups in a bigint rational and then applies
|
||||
stored bound. One figure changed after the answer, and it is flagged here: the exact
|
||||
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 is split out as question 16 rather than reopening this one.
|
||||
or wire fact, so its value was split out as question 16 rather than reopening this one.
|
||||
The user then decided K = 32 there.
|
||||
|
||||
**11. The `MIN`/`MAX` tie-break.**
|
||||
> "Yeah, exact."
|
||||
@@ -202,6 +213,19 @@ 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.**
|
||||
> "let's do k = 32"
|
||||
|
||||
(Left on `b232c5a9`, on question 16's options, 2026-10-09T03:37:34Z; comment
|
||||
4226381911.)
|
||||
|
||||
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. 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.
|
||||
|
||||
## The decision this ADR writes down
|
||||
|
||||
1. **Three levels** (decided, question 1). A number is an **exact decimal**, an **exact
|
||||
@@ -1349,7 +1373,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.** Otherwise read the f64 accumulator, and the result is inexact. The count is
|
||||
K = 32** (decided, question 16). 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.
|
||||
|
||||
@@ -1381,7 +1405,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
|
||||
question 16.
|
||||
32, 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
|
||||
@@ -1393,8 +1417,12 @@ This keeps (d)'s wins, because `SUM(v / 3)`, averages and percentages have one o
|
||||
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, and the read's
|
||||
measured cost above is why question 16 asks for a smaller one.
|
||||
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
|
||||
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).
|
||||
|
||||
### The file and the wire
|
||||
|
||||
@@ -1604,9 +1632,9 @@ In order. Each step lands on its own, with the suite green.
|
||||
## Open questions
|
||||
|
||||
The user answered questions 1–9, 11 and 12 inline on `e4acf643`, and 10, 13, 14 and
|
||||
15 inline on `33833f28`. Each is marked **decided** below, and quoted in full under
|
||||
"What the user decided". Question 16 is new in revision 6, and it is the only open one.
|
||||
Nothing is renumbered.
|
||||
15 inline on `33833f28`, and question 16 inline on `b232c5a9`. Each is marked
|
||||
**decided** below, and quoted in full under "What the user decided". No question is
|
||||
open. Nothing is renumbered.
|
||||
|
||||
1. **The tower's shape.** **Decided (the user): three levels**: exact decimal, exact
|
||||
bounded fraction, inexact f64 (clause 1). The original recommendation, two levels,
|
||||
@@ -1667,13 +1695,14 @@ 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.
|
||||
*Recommendation:* (a), with K = 32. The user confirmed the value in question 16.
|
||||
- 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. See question 16.)
|
||||
0.67–1.9 ms at K = 32 in the worst case, and the user kept K = 32. See
|
||||
question 16.)
|
||||
- (c) breaks order-independence (revision 2).
|
||||
|
||||
11. **The `MIN`/`MAX` tie-break.** **Decided (the user): prefer the exact value**
|
||||
@@ -1750,7 +1779,8 @@ 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** (open; added in revision 6). K is the most non-decimal groups a
|
||||
16. **The value of K.** **Decided (the user): (a), K = 32** ("let's do k = 32"). 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
|
||||
the measured worst case is **0.67–1.9 ms** (option (d), "What that costs"). Under
|
||||
@@ -1761,8 +1791,8 @@ Nothing is renumbered.
|
||||
denominators is inexact.
|
||||
- **(c)** K = 16: worst case 174–448 µs per read.
|
||||
|
||||
*Recommendation:* **(b), K = 8**, with the read combining over the lcm and reducing
|
||||
once (the lower figure in each range).
|
||||
*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).
|
||||
- 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.
|
||||
@@ -1771,6 +1801,77 @@ 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
|
||||
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
|
||||
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
|
||||
page of such cells is a question for the evaluator benchmark, not a figure here.
|
||||
|
||||
## Relations to other ADRs
|
||||
|
||||
Added in revision 7. This lists what the three ADRs written since ask of this one, as
|
||||
read at their heads, and **changes none of the user's decisions** here. Each ADR is
|
||||
cited at the head named, and none was built or run.
|
||||
|
||||
**ADR 007 (stable identities, accepted; merged, #326).**
|
||||
- **The version literal.** ADR 007 clause 10 adds a second `.improv` literal and says
|
||||
its exact spelling is left to the implementation. This ADR shares it (clause 10,
|
||||
question 5), so `#i` and `n/d` ride on the same bump. ADR 007 says nothing about
|
||||
numbers, so nothing in its text conflicts.
|
||||
- **Flag, ordering.** If ADR 007's migration ships the new literal **before** this ADR's
|
||||
grammar arms, a reader of that literal would not know `#i` or `n/d`, and "shared" would
|
||||
mean one literal with two grammars. The two should either land the literal together or
|
||||
this ADR's arms should be added under the literal ADR 007 has already shipped, with the
|
||||
old readers' behaviour on `#i` stated. That is a merge-order matter for the
|
||||
implementer, not a decision the user has made.
|
||||
- **Ids and display.** This ADR's name-quoting and the formula-target handling are about
|
||||
names, and ADR 007 moves keys from names to ids. Numbers are not names, so clause 10's
|
||||
number spellings are unaffected; any clause here that quotes a category or item by
|
||||
name should be read as "by display name" once ADR 007 lands. Not re-read clause by
|
||||
clause.
|
||||
|
||||
**ADR 008 (virtualized grid, PR #308, proposed).** Its Q28 answer, "bigint is fine, we
|
||||
don't need to reinvent integers > safe size", makes a grid position a bigint above 2^53
|
||||
(its Q36 spells it as a decimal string in JSON).
|
||||
- **Dependency.** It adds `num-bigint` to `improvise-protocol`, a wasm-bundle crate. This
|
||||
ADR already accepts `num-bigint` in `numtower` (+20.8 KB gzipped for `num-rational`
|
||||
plus `num-bigint`, probed; question 8). If `improvise-core` depends on `numtower` (clause
|
||||
2), `improvise-protocol` already reaches `num-bigint` through it, so the cost is paid
|
||||
once, not twice. ADR 008 says its `cargo tree --edges normal` check has not been run;
|
||||
neither has one here for the bundle.
|
||||
- **Flag, a new consumer of `numtower`.** Positions are integers, not numbers in the
|
||||
tower's sense. Whether they use `numtower`'s type or `num-bigint` directly is open in
|
||||
ADR 008. The tower's `n/d` and `#i` spellings must not leak into a position's wire
|
||||
spelling.
|
||||
|
||||
**ADR 009 (incremental evaluation, PR #309, proposed).**
|
||||
- **`ExactSum` lives in `numtower`** (question 15, (a)), which ADR 009 cites. `numtower`
|
||||
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
|
||||
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.** ADR 009 reads a maintained `SUM` after every edit that touches
|
||||
it. With K = 32 (question 16) that is up to 0.67–1.9 ms per edit for a cell holding
|
||||
many large distinct denominators (see "Re-derived under K = 32"). 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
|
||||
`rust_decimal` alone, "not a bignum, +15.4 KB", with 40 bytes per summed group. Under
|
||||
option (d) the crate also carries `num-rational` and `num-bigint` (up to about +36 KB
|
||||
together, unprobed together) and a per-denominator group map. It also gives `ExactSum`
|
||||
568 bytes per group where this ADR gives the decimal partition 40 bytes; those are two
|
||||
different accumulators and are not a conflict.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Typed values and their `+ - *` are exact. `0.1 + 0.2 = 0.3`, `1234.56 - 1234.55 =
|
||||
@@ -1821,7 +1922,7 @@ Nothing is renumbered.
|
||||
- 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), and past K it is inexact.
|
||||
cost that grows about as K² (question 16: K = 32, 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
|
||||
|
||||
Reference in New Issue
Block a user