fix(numtower): inexact presence per value, POW sign from exact parity, i128::MIN numerators (improvise-59nw.1)

Answers ChatGPT Codex's P1 and Copilot's two findings on PR #337; each has a regression test that failed at edbb50c4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (numtower)
2026-10-08 22:47:04 -07:00
committed by Edward Langley
parent edbb50c426
commit 568f52165d
7 changed files with 156 additions and 21 deletions
+19 -4
View File
@@ -34,7 +34,10 @@ bytes, as the ADR measured, and `a_num_is_24_bytes` pins it.
**The normal form.** An exact zero has no sign. A fraction is reduced and has a positive
denominator. A value whose reduced denominator is `2^p·5^q`, with `k = max(p, q) <= 28`
and a mantissa that fits 96 bits, is always the `Decimal`, never a `Fraction`. So
`Fraction(n/1)` exists only when `n` does not fit 96 bits, as with `10^35/1`.
`Fraction(n/1)` exists only when `n` does not fit 96 bits, as with `10^35/1`. Every `i128`
is a valid numerator, `i128::MIN` included, because the arm is `Ratio<i128>`. That one
value cannot be negated in `i128`, so `Operand::parts` keeps it out of the `i128` fast
path and its arithmetic takes the bigint path (Copilot on PR #337).
## Arithmetic
@@ -116,6 +119,10 @@ rule, because the ADR decided it and because it needs no second division.
negative power is a division and has its minimal scale.
- **The edge values.** `0^0` is `1`. `0^-n` is `div/0`. `±1^n` is `±1` for any `n`,
including one too large to compute.
- **A demoted power takes its sign from the exact exponent.** `powf` gets the base's
magnitude, and the sign is set from `base < 0 && n odd`. An exponent above 2^53 rounds
to an even f64, so handing `powf` the signed base made `(-2)^(2^53 + 1)` read `+inf`
(Copilot on PR #337).
### The ceiling
@@ -192,16 +199,24 @@ of the multiset of terms and not of their order.
| the decimal partition | every decimal, and every fraction whose denominator is `2^p·5^q` with `k <= 28`, as an integer at scale 28 | 320 bits |
| the groups | per other reduced denominator, the summed numerator | 256 bits each, in a `BTreeMap` |
| `group_floats` | an `ExactSum` of each group's correctly rounded value | ADR 009's |
| `inexact` | an `ExactSum` of the f64 terms, plus their net weight | ADR 009's |
| `inexact` | an `ExactSum` of the f64 terms | ADR 009's |
| `inexact_terms` | the net weight per f64 bit pattern, entries at zero dropped | a `BTreeMap` |
**The read.** It is exact when the net inexact weight is zero and at most K = 8 groups
**The read.** It is exact when no inexact term is present and at most K = 8 groups
remain. The groups and the decimal partition are then combined **over the lcm of their
denominators, and reduced once**, and the stored bound applies to the result. Past K the
read is the f64 accumulators: `group_floats`, the decimal partition converted once, and
the inexact terms. That reading rounds each group, then the total. With inexact terms and
at most K groups, the exact total is converted once and added to the inexact terms. A
group whose numerator returns to zero is dropped, so "how many groups" is a function of
state. The K cutoff overrides representability: nine denominators whose exact total fits
state.
**"Is an inexact term present" is asked per value, never of the net weight.** The first
version kept one net count. Inserting `1.0` and retracting `2.0` leaves the count at zero
while the accumulator holds −1.0, so the read took the exact branch and dropped it
(ChatGPT Codex, P1 on PR #337). Presence is now a map from f64 bit pattern to net weight,
dropped at zero, the same rule the groups follow. It costs O(distinct inexact values),
the same order as the groups. The K cutoff overrides representability: nine denominators whose exact total fits
still read inexact (`more_than_k_denominators_read_inexact_even_when_the_total_fits`).
That is question 13's second exception, and the ADR's flagged reading.