Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
15 KiB
Test evidence: numtower
What each test guards, which rival it was run against, and where that rival failed. The
entries are keyed by exact test name, and a rename orphans its entry, which
scripts/check-comment-runs.py --check-evidence catches. The reasoning these tests defend
is in context/code/numtower.md. The decisions are ADR 005's.
How the rivals are kept. Every property in crates/numtower/tests/properties.rs is a
checker that takes the implementation as a parameter. The property's own test runs it on
the real code. A companion test, named <property>_catches_<rival>, runs the same checker
on the rival, with a deterministic seed, and asserts two things:
- the run fails;
- the failure message carries the property's own assertion text, so a rival that
fails in setup, panics, or fails some other assertion does not count (
assert_caughtintests/oracle/mod.rs).
So the rival is watched failing on every run of the suite, not once by hand. Two rivals were also injected into the crate's source, and the run was recorded. Those runs are noted under their entries.
The oracle is num-rational's BigRational, a dev-dependency feature. The crate
itself never uses BigRational, so the oracle shares no rational type with the code under
test. "Correctly rounded" is checked in the review's interval form (is_nearest). The
exact value must lie inside the round-to-nearest-even interval of the returned f64, with
the endpoints included only when the f64's mantissa is even. Comparing against
BigRational::to_f64 would have been circular in the way ADR 005's option (d) section
describes.
crates/numtower/tests/properties.rs
an_exact_result_is_never_a_rounded_one
Every exact pairing of arms and every operation (+ − × ÷) is checked against the oracle.
When the result claims to be exact, its value must be the exact value. The generator
spans 96-bit mantissas, scales 0–28 and i128 fractions. A fixed case pins ADR 005's own
example, 1000 + 0.3333333333333333333333333333. rust_decimal returns that at scale
25, rounded, and the crate must not.
Injected in the source. In arith.rs::decimal_sum, the scale filter was deleted, so
checked_add's result was trusted. Three tests failed: this one,
contagion_is_total, and a_recomputed_result_keeps_the_stored_scale_clause_4_names.
The revert was followed by a full rerun.
an_exact_result_is_never_a_rounded_one_catches_a_silently_rounding_add
The rival is rust_decimal's own checked_add, trusted as exact. That is the one
outcome ADR 005 clause 4 exists to prevent.
contagion_is_total
Every pairing of the three arms, NaN and the infinities included, through + − × ÷:
- an inexact operand makes the result inexact;
- an exact pair gives an exact result iff the oracle value fits a stored arm, and otherwise the correctly rounded f64 of that value;
- division refuses iff the divisor is exactly zero, at either level.
"Total" means no pairing is missing. A pairing the code never handled would panic, and the runner reports a panic as a failure.
contagion_is_total_catches_an_integral_inexact_kept_exact
The rival is a "harmless" promotion: an integral f64 is treated as exact, so 2 + 3.0 is
exact. It is caught on the seed pair (2, Inexact(3.0)). That pair is in the generator on
purpose. Integral f64s are rare among random floats, so without the seed the rival was
caught only by luck.
pow_is_exact_for_an_exact_base_and_integer_exponent_when_it_fits
Bases from every arm, exponents −6 to 6. An exact base gives an exact result iff the
oracle's power fits, and otherwise the correctly rounded value. 0^-n refuses.
pow_decides_its_size_before_computing_and_honours_the_ceiling
POW(1/3, 10^9)returns at once, inexact. Its lower bound is past the ceiling, so nothing is allocated. Without the size check the test does not finish.(1/3)^6000is computed, but is 9,510 bits and demotes. That is the ceiling's exception.1.05^28is exact and1.05^29is not, the option (d) table's k = 29.- Also pinned:
(-1.0)^(10^20),0^0, the scale of1.50^2, and that a non-integer exponent ispowf.
the_f64_conversion_agrees_with_parsing_the_decimal_text
The generator spans 96-bit mantissas and scales 0–28, as ADR 005 requires. The oracle
text is built digit by digit, not by rust_decimal's Display, which is what the crate
itself calls. So the test does not compare the crate's path with itself. The review's
case 63294.607176721429540940386554 is pinned.
Injected in the source. num.rs::decimal_to_f64 was replaced with rust_decimal's
ToPrimitive::to_f64, and three tests failed:
- this one, minimal case
-0.10806611601966961at scale 17; the_number_languages_gate_and_parse_agree, on92240.01133710762224;exact_mixed_comparison_is_transitive.
The third was not expected. The comparison's fast path decides by the rounded value, and that is sound only if the rounding is correct.
the_f64_conversion_agrees_with_parsing_the_decimal_text_catches_rust_decimal_to_f64
The rival is rust_decimal's to_f64, which is not correctly rounded (ADR 005, B2). The
test also asserts that the rival misses on the review's case, so the pinned case is known
to discriminate.
rust_decimal_to_f64_agrees_on_fifteen_digit_mantissas_so_a_narrow_generator_is_vacuous
This test passes the rival on purpose. With mantissas of at most 15 digits, 4,096
cases, rust_decimal's to_f64 agrees with the text every time, as ADR 005 says (200,000
of 200,000). So a test whose generator stays in that range proves nothing about the
conversion. This test is why the generator above spans 96 bits, and it is what fails, and
should be re-read, if rust_decimal ever fixes its conversion.
a_fractions_conversion_lies_inside_the_round_to_nearest_even_interval
Reduced fractions with numerator and denominator widths from 20 to 126 bits, both signs:
the review's independent form. It also checks the product of two such fractions, which
reaches the bigint path and is stored. That covers ratio_to_f64 on values wider than
i128.
a_fractions_conversion_lies_inside_the_round_to_nearest_even_interval_catches_an_f64_division
The rival is n as f64 / d as f64, which rounds three times.
exact_mixed_comparison_is_transitive
The generator is a crowd: values crowded around one f64 x.
xitself, and its two neighbours;- the shortest decimal that spells
x, and that decimal ±k·10^-25; - a third of the shortest decimal.
Every triple is checked for transitivity, and every pair for antisymmetry. Random values would almost never put two exacts and an inexact on one f64, which is exactly where the rival breaks.
exact_mixed_comparison_is_transitive_catches_comparing_through_f64
The rival is revision 1's rule: convert the exact operand to f64, then compare. It fails
as ADR 005 clause 6 says it does. Exact(s) = Inexact(x) and Inexact(x) = Exact(s + 10^-25), while the two exacts differ. The marker is comparison: not transitive, so it
is the transitivity assertion that fails, not antisymmetry.
comparison_agrees_with_the_oracle_at_every_pairing_of_arms
The differential half of comparison: the crowd, plus mixed random pools of decimals,
fractions and finite f64s, against BigRational's order. NaN is unordered, and an
infinity orders past every exact value.
min_and_max_prefer_the_exact_value_and_do_not_depend_on_order
Question 11's tie-break, plus the two the crate adds: the larger scale between equal
decimals, and IEEE's -0.0 for MIN and 0.0 for MAX. Each pair is asserted in both
argument orders, the order-independence property the tie-breaks exist for.
the_accumulator_is_independent_of_order
Random term lists, from every arm, are summed in one order and in a shuffled order. The
results must agree in arm, value and scale. The terms include fractions over a small
pool of denominators, so the exact grouped read is reached, and arbitrary fractions, so
the K cutoff is too. ADR 005 clause 8's seed case, [0.30000000000000004, 1e12, −1e12],
is one of the generator's arms and is also pinned exactly.
the_accumulator_is_independent_of_order_catches_decimal_addition
The rival is clause 8's own rival, a fold with Decimal's +, demoting to the tower
where it overflows. On the seed case, one order rounds 0.30000000000000004 + 1e12 to
scale 16 and the other does not.
the_accumulator_retracts_exactly
Inserting xs and ys, then retracting ys in reverse order, must equal a fresh sum of
xs in arm, value and scale. A group whose numerator returns to zero must be dropped, or
a retracted denominator would still count toward K.
the_accumulator_reads_the_exact_sum_or_its_stated_roundings
This is the differential test of the read against the oracle. It is the only test that states every branch.
- No inexact term, at most K live groups. The read is exact iff the exact total fits, and is then that total. Otherwise it is the total's correctly rounded f64.
- Inexact terms present. The exact total is rounded once, then the f64s are added and the result is rounded once more.
- Past K groups. Inexact.
The oracle counts live groups, those whose summed numerator is nonzero. The first version counted denominators ever seen, and that disagrees with the state-based count whenever terms cancel.
more_than_k_denominators_read_inexact_even_when_the_total_fits
1/3 + 1/7 + … + 1/31 is nine distinct denominators. The exact total fits an i128
fraction, yet the read is inexact. That is question 13's second exception, the K cutoff
overriding representability. Eight of them read as an exact Fraction. The read past K is
asserted to be each group rounded, then the total rounded once. The first version
asserted one rounding of the exact total, and it failed: clause 8's maintained f64
accumulator of group values rounds twice by design.
exact_sum_is_the_exact_weighted_total_rounded_once
ADR 009's ExactSum against the oracle. Weights are drawn from small values, from the
whole i64 range and up to 2^40. A whole-range weight exercises all three base-2^30
digits, which ADR 009's chunk loop could not reach in reasonable time. Values include
subnormals.
division_decides_fit_directly
ADR 005 clause 5's three seed cases, as both property inputs and pinned assertions:
1 / 2^40and1 / 5^30are fractions;1e-28 / 0.5is the exact decimal2e-28.
Random decimal pairs must also be an exact decimal iff the quotient fits one, an exact
fraction iff it fits only that, and otherwise inexact. The test also pins AVG's example
(70/3), 1/3 × 3 = 1, and division by an inexact -0.0.
division_decides_fit_directly_catches_multiplying_back
The rival is multiplying back with revision 6's scale test (demote on the hint). It
fails on 1e-28 / 0.5, as ADR 005 says. An experiment, not committed, ran multiplying
back with Num's own *, which carries revision 7's recompute. It agreed with the direct
test on all 5,120 random cases and the seeds. The ADR's argument that multiplying back is
incomplete holds only under the earlier rule (context/code/numtower.md, "Division").
the_number_languages_gate_and_parse_agree
The generated lexemes have four sources:
- structured lexemes: an optional sign, up to 40 digits, an optional fraction, and an
optional exponent of up to 4 digits, so the empty parts give
1.,.5and.; - junk over the alphabet of digits,
eE+-._/aifnINand space; n/dlexemes;- the named spellings:
inf,NaN,Infinity,1_000,+.5,e5.
For each lexeme, the parse succeeds iff f64::from_str reads a finite value. The exact
value parsed must round to the f64 f64::from_str reads. is_number and parse_number
must agree in both typings.
the_number_languages_gate_and_parse_agree_catches_from_str_exact
The rival is rust_decimal's from_str_exact. ADR 005 clause 3 names it as the parse
whose language differs from the gate's: it rejects 1.5e3 and accepts 1_000.
the_number_language_seed_cases
Every seed case ADR 005's migration step 1 lists:
1.5e3is the exact1500;1_000is text;-0.0is the exact0.0, with no sign;3/4is text when the type is inferred, and0.75when it is forced;1e999,-1e999,nan,infandInfinityare text;1e-999is an inexact zero;1e-30is the exact1/10^30;0.12345678901234567890123456789is an exact fraction, not a rounded decimal;2^120is an exactn/1;1e-40is inexact.
a_typed_value_and_the_same_value_computed_are_equally_exact
For random decimal pairs (and the seed 1e-15 × 1e-15), the exact product is spelled as
<integer>e-<k> and parsed. The parse must have the arm and the value of the computed
product. An earlier draft built the text only when the reduced denominator was a power of
ten, and rejected most cases. A reduced decimal product's denominator is 2^a·5^b, so the
text now scales by the smallest power of ten that makes the product an integer.
a_typed_value_and_the_same_value_computed_are_equally_exact_catches_demoting_on_the_hint
The rival is revision 6's multiplication, which demotes whenever the returned scale drops.
1e-15 × 1e-15 then becomes an f64, while the typed 1e-30 is the exact fraction. That
is the asymmetry revision 6's N2 was about.
a_recomputed_result_keeps_the_stored_scale_clause_4_names
ADR 005 clause 4's example: 1.2500000000000000 × 4.0000000000000000 is 5 at scale 28,
not scale 0 and not 32. The test also pins that a common-path sum keeps its returned scale
(12.50 + 1.5 = 14.00), that 1000 + 0.333… goes to the fraction arm, and that an exact
zero result has no sign.
an_intermediate_beyond_i128_comes_back_exact
The point of question 13 (b). a = 10^30/7; a × a does not fit i128, yet (a × a) / a through Interim is a, exactly. The same product stored through Num is inexact
and correctly rounded.
the_canonical_text_reads_back_as_the_same_number
Every arm's Display text reads back through from_canonical as the same arm, value and
scale. The test pins the four spellings ADR 005 clause 11 tabulates, and that canonical
exact text never reads back as an inexact value.
crates/numtower/src/sum.rs
sum::tests::the_grouped_read_combines_over_the_lcm_and_reduces_once
The combine must return the lcm as its denominator, and the reduced result must equal
the BigRational sum. Group numerators are up to i128 × i64, and the denominators share
factors (3, 77, 2^20, 3^40), so the lcm and the product differ.
sum::tests::the_grouped_read_combines_over_the_lcm_and_reduces_once_catches_the_product
The rival combines over the product of the denominators. Its value is right, so only the lcm assertion can see it. That is why the property asserts the denominator before the value.