fix(core): Min/Max, NaN and signed zero follow the math, not the old fold (improvise-bw4p.20)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (dbsp-backend)
2026-10-10 16:37:00 -07:00
committed by Edward Langley
parent 5645f8d24a
commit 9f035f918e
9 changed files with 222 additions and 94 deletions
+25 -6
View File
@@ -314,8 +314,8 @@ because they change nothing observable (below); every other is named by at least
`group::tests::an_emptied_group_is_absent_and_keeps_no_entries`,
`incremental_matches_eager_on_the_shipped_documents`, `incremental_equals_eager_over_random_edit_sequences`;
- a group never counts its numbers: `a_group_read_comes_from_a_maintained_group`,
`group::tests::min_and_max_skip_nan_and_are_infinite_over_nothing_else` and eight differentials;
- `Max` takes the first key: `group::tests::min_and_max_skip_nan_and_are_infinite_over_nothing_else`,
`group::tests::min_and_max_skip_nan_and_are_nan_over_nothing_else` and eight differentials;
- `Max` takes the first key: `group::tests::min_and_max_skip_nan_and_are_nan_over_nothing_else`,
`an_overwrite_costs_two_updates_per_shape_whatever_the_table_size` and five differentials;
- `Count` counts one: `a_collapsed_group_reads_one_group_per_member_it_covers`, `group::tests::*`;
- unanimity ignores disagreement: `group::tests::text_is_unanimous_or_mixed_across_groups`,
@@ -437,7 +437,7 @@ A multiset that ignores a negative weight keeps the minimum it should have lost.
After the last retraction the group reads as absent and its map holds no zero-weight entry.
### group::tests::min_and_max_skip_nan_and_are_infinite_over_nothing_else
### group::tests::min_and_max_skip_nan_and_are_nan_over_nothing_else
NaN is skipped by `Min` and `Max`, a group of only NaN gives `+inf` and `-inf` and still counts.
@@ -472,7 +472,7 @@ What each fault was and the test that names it:
`a_formula_that_keeps_moving_for_sixteen_passes_is_circular`,
`dependency_order_and_the_plain_passes_leave_the_same_cache`;
- dependencies after dependents: `dependency_order_and_the_plain_passes_leave_the_same_cache` and five more;
- NaN is not marked circular: `a_formula_cell_holding_nan_is_circular_and_the_cells_that_read_it_keep_the_nan_they_read`;
- NaN is not marked circular: `a_formula_cell_holding_nan_is_nan_and_not_circular`;
- fifteen passes / seventeen passes: `a_chain_added_last_link_first_is_circular_exactly_where_eager_says`;
- the passes end without marking the cells still moving, or begin from the old cache:
`a_chain_added_last_link_first_is_circular_exactly_where_eager_says`,
@@ -557,7 +557,7 @@ the whole recompute.
`IF(Revenue > 1000, B, Revenue)`: at the stem of a big product it reads the cache of the stem of B,
which is evaluated after it. Fails when such a formula set is not wide.
### a_formula_cell_holding_nan_is_circular_and_the_cells_that_read_it_keep_the_nan_they_read
### a_formula_cell_holding_nan_is_nan_and_not_circular
NaN is not equal to itself, so eager's passes never see it settle. Fails when the ordered path leaves it.
@@ -643,7 +643,7 @@ runs because its two heaviest tests shared it. The tests are unchanged.
East plus the maximum of West), the first version kept 14. Fails when a new aggregation dirties only
the stems of formulas that name the measure, and when `Stems::carrying` ignores the measure.
### a_new_aggregation_reaches_a_product_targeted_filtered_sum
### a_new_aggregation_reaches_a_filtered_sum_on_the_fallback_path
The reviewer's second repro, a Product-targeted `SUM(F0 WHERE Region = East)`. It already agreed on this
fixture and stays as the second shape of the same hazard.
@@ -766,3 +766,22 @@ average of a rounded sum.
Infinities and NaN read as `ExactSum::value` does, and a zero keeps its sign.
## Eager's math, fixed (follow-up to steps 4 to 6)
Ruling (user, 2026-10-10): do not reproduce math bugs found in Eager; fix Eager. Eight faults were
injected (six in the table below, two more in the groups' NaN handling); `nan-never-settles` survived the
first pass because NaN produced by one evaluation always has the same bits, and is closed by a unit test of
`settled`. Each fix has a regression test named after the bug:
- `model::types::eager_math_tests::min_and_max_of_both_zeros_do_not_depend_on_their_order` and
`with_both_zeros_min_is_negative_and_max_is_positive_whatever_the_order`: a naive `f64::min` fold
passes on ARM and fails on other platforms, so the rival is a fold that keeps the first of two zeros;
- `model::types::eager_math_tests::min_and_max_over_only_nan_are_nan_not_infinite`,
`min_and_max_over_only_nan_are_nan_in_both_backends`, `group::tests::min_and_max_skip_nan_and_are_nan_over_nothing_else`;
- `model::types::eager_math_tests::a_formula_that_settles_on_nan_is_nan_and_not_circular`,
`a_formula_cell_holding_nan_is_nan_and_not_circular`, `settled_treats_every_nan_as_one_value_and_the_zeros_as_two`;
- `model::types::eager_math_tests::a_zero_that_changes_sign_between_passes_settles_on_the_new_sign` and
`ordered_evaluation_keeps_the_zero_eager_settles_on` (now with the sign settled by bits, no fallback);
- `model::types::eager_math_tests::a_formula_that_never_settles_is_still_circular`: the guard that a real
oscillation is still `circular`.
The random and differential suites compare numbers by bits and their pools hold both zeros again.