From 7854d5e6ba4dd508caa603b9ab86d828983dc29d Mon Sep 17 00:00:00 2001 From: Peter Neiss Date: Mon, 22 Jun 2026 21:14:08 +0200 Subject: [PATCH 1/2] math: rename internal real_raw->f64_raw, store_real->store_f64 (no behavior change) Prep for the f32 storage sibling: the double-backed raw predicate/helper read 'real' but the flag is now f64, and a float-backed f32_raw is coming. Pure mechanical rename across include/ + tests; behavior-identical. Co-Authored-By: Claude Opus 4.8 --- include/bound/cmath.hpp | 8 +-- include/bound/core.hpp | 22 ++++---- include/bound/detail/addition.hpp | 2 +- include/bound/detail/assignment.hpp | 18 +++---- include/bound/detail/division.hpp | 4 +- include/bound/detail/multiplication.hpp | 4 +- include/bound/generic.hpp | 8 +-- include/bound/io.hpp | 4 +- single_include/bound/bound.hpp | 70 ++++++++++++------------- tests/fuzz.cpp | 2 +- tests/test_real_exact.cpp | 2 +- tests/test_storage_flags.cpp | 6 +-- 12 files changed, 75 insertions(+), 75 deletions(-) diff --git a/include/bound/cmath.hpp b/include/bound/cmath.hpp index c1873e8..e0dcb8b 100644 --- a/include/bound/cmath.hpp +++ b/include/bound/cmath.hpp @@ -256,7 +256,7 @@ namespace bnd::math && !rational_raw // `real` storage holds the VALUE, not an offset index, so route it // through the rational fallback `Out{r}` (same guard as fmod_int_fast). - && !real_raw + && !f64_raw && has_flag(BoundPolicy, round_nearest) && (std::signed_integral> || NotchCount @@ -907,9 +907,9 @@ namespace bnd::math // * all unit counts fit comfortably in imax (headroom 4). template inline constexpr bool fmod_int_fast = []{ - if (rational_raw || real_raw - || rational_raw || real_raw - || rational_raw || real_raw) + if (rational_raw || f64_raw + || rational_raw || f64_raw + || rational_raw || f64_raw) return false; if (Notch == 0 || Notch == 0 || Notch == 0) return false; diff --git a/include/bound/core.hpp b/include/bound/core.hpp index 9aec2a3..b5c0439 100644 --- a/include/bound/core.hpp +++ b/include/bound/core.hpp @@ -123,7 +123,7 @@ namespace bnd // Snap a value onto `real` storage: lossless on the dyadic grid. Out-of-range // values run the same policy cascade as the fractional path (clamp → wrap → // sentinel/checked-report → store as-is); all arithmetic stays in double. - constexpr void store_real(double v) + constexpr void store_f64(double v) { // NaN/±inf would reach snap_double's integer cast (UB); reject like the // non-real path. `v - v` is 0 for every finite v, NaN otherwise. @@ -165,14 +165,14 @@ namespace bnd template constexpr void store_value(A const& value) { - if constexpr (detail::real_raw) - store_real(to_double(value)); + if constexpr (detail::f64_raw) + store_f64(to_double(value)); else if constexpr (is_bound_v) { // A `real` SOURCE holds its value as a double raw; the assignment engine's // integer offset formula (Lower + raw·Notch) would misread it. Extract as // a double and route through the arithmetic-source path. - if constexpr (detail::real_raw) + if constexpr (detail::f64_raw) detail::assignment::assign(*this, detail::as_double(value), make_policy

()); else detail::assignment::assign(*this, value, make_policy

()); @@ -193,8 +193,8 @@ namespace bnd // The one-shot `pol` widens the assignable check (a clamp/round passed here // relaxes the notch/interval clause), so a notch-incompatible boundable source // is accepted — e.g. clamp_round(some_bound). Body honours `pol` as before. - if constexpr (detail::real_raw) - store_real(to_double(value)); + if constexpr (detail::f64_raw) + store_f64(to_double(value)); else detail::assignment::assign(*this, value, pol); } @@ -207,8 +207,8 @@ namespace bnd requires bound_assignable constexpr bound(A value, errc& ec) { - if constexpr (detail::real_raw) - store_real(to_double(value)); + if constexpr (detail::f64_raw) + store_f64(to_double(value)); else detail::assignment::assign(*this, value, make_policy

(ec)); } @@ -430,7 +430,7 @@ namespace bnd [[nodiscard]] constexpr negative operator-() const { negative neg; - if constexpr (detail::real_raw) + if constexpr (detail::f64_raw) neg = negative::from_raw(-Raw); else if constexpr (detail::rational_raw) neg = negative::from_raw(-(Raw)); @@ -703,7 +703,7 @@ namespace bnd if constexpr (Grid == Grid) return lhs.raw() <=> rhs.raw(); // double-backed (`real`) operand: compare in double (raw_imax would truncate) - else if constexpr (detail::real_raw || detail::real_raw) + else if constexpr (detail::f64_raw || detail::f64_raw) return detail::as_double(lhs) <=> detail::as_double(rhs); // both integer-direct (notch=1, Raw==value): compare as integers else if constexpr (!detail::rational_raw && !detail::rational_raw @@ -718,7 +718,7 @@ namespace bnd { if constexpr (Grid == Grid) return lhs.raw() == rhs.raw(); - else if constexpr (detail::real_raw || detail::real_raw) + else if constexpr (detail::f64_raw || detail::f64_raw) return detail::as_double(lhs) == detail::as_double(rhs); else if constexpr (!detail::rational_raw && !detail::rational_raw && !detail::index_raw && !detail::index_raw) diff --git a/include/bound/detail/addition.hpp b/include/bound/detail/addition.hpp index 0e24b8a..d660150 100644 --- a/include/bound/detail/addition.hpp +++ b/include/bound/detail/addition.hpp @@ -68,7 +68,7 @@ namespace bnd::detail static constexpr auto add(L lhs, R rhs, policy policy = {}, A&& action = {}) -> add_return_t { result res; - if constexpr (real_raw) + if constexpr (f64_raw) { res = result::from_raw(Grid.snap_double(as_double(lhs) + as_double(rhs))); } diff --git a/include/bound/detail/assignment.hpp b/include/bound/detail/assignment.hpp index 553e025..869bfb9 100644 --- a/include/bound/detail/assignment.hpp +++ b/include/bound/detail/assignment.hpp @@ -217,7 +217,7 @@ namespace bnd::detail // or NotchCount, no rounding. real takes the endpoint as a double, rational // the exact constant (a double round-trip would lose non-dyadic endpoints); // raw_from_offset adds Lower back for direct-encoded storage. - if constexpr (real_raw) + if constexpr (f64_raw) lhs = L::from_raw((rhs < Lower) ? static_cast(Lower) : static_cast(Upper)); else if constexpr (rational_raw) @@ -263,10 +263,10 @@ namespace bnd::detail { if constexpr (rational_raw && Notch == 0) { lhs = L::from_raw(rhs); return true; } // continuous: store verbatim - else if constexpr (real_raw) + else if constexpr (f64_raw) { // real target: raw IS the value — snap to the dyadic grid (range handling - // already ran in the assign cascade; finite guard mirrors store_real's). + // already ran in the assign cascade; finite guard mirrors store_f64's). const double v = static_cast(rhs); if (!(v - v == 0)) detail::raise(errc::not_finite, "non-finite double"); @@ -306,7 +306,7 @@ namespace bnd::detail // instead of two rational ops. round_quotient is invariant under reduction, // so the slot is bit-identical to the rational path. Oversized denominators // fall through (the kMaxDen guard keeps every product inside imax). - if constexpr (HasQFormatFastPath && !real_raw && Notch != 0) + if constexpr (HasQFormatFastPath && !f64_raw && Notch != 0) { constexpr imax K = abs_den(Notch.Denominator); constexpr imax Lo = LowerImax; @@ -399,7 +399,7 @@ namespace bnd::detail if constexpr (rational_raw) return Lower; else if constexpr (Notch == 0) - // Continuous real_raw L: no grid to land on, mapping unused (store + // Continuous f64_raw L: no grid to land on, mapping unused (store // routes through snap_double). 0 avoids the /Notch divide-by-zero. return rational{0}; else if constexpr (rational_raw) @@ -413,7 +413,7 @@ namespace bnd::detail if constexpr (rational_raw) return Notch; else if constexpr (Notch == 0) - // Continuous real_raw L (see calcOffset). A denominator-1 Factor also + // Continuous f64_raw L (see calcOffset). A denominator-1 Factor also // makes assign_notch_ok vacuously true (any value representable). return rational{0}; else if constexpr (rational_raw) @@ -430,7 +430,7 @@ namespace bnd::detail // sides (not rational, not real). static constexpr bool is_integer_mapping = !rational_raw && !rational_raw - && !real_raw && !real_raw + && !f64_raw && !f64_raw && abs_den(Factor.Denominator) == 1 && abs_den(Offset.Denominator) == 1; // Map rhs.Raw into L's raw space (requires is_integer_mapping). The @@ -469,7 +469,7 @@ namespace bnd::detail { // RawLo/RawHi are already the correct Raw (no raw_from_offset). Real storage // takes the endpoint as a double (RawLo/Hi truncate fractional dyadic endpoints). - if constexpr (real_raw) + if constexpr (f64_raw) lhs = L::from_raw((as_rational(rhs) < Lower) ? static_cast(Lower) : static_cast(Upper)); else @@ -538,7 +538,7 @@ namespace bnd::detail template static constexpr void store(L& lhs, R const& rhs, P&&) { - if constexpr (real_raw) + if constexpr (f64_raw) // real target: raw IS the value — decode the source and snap to the dyadic // grid (the offset machinery below mis-encodes a double raw). lhs = L::from_raw(Grid.snap_double(as_double(rhs))); diff --git a/include/bound/detail/division.hpp b/include/bound/detail/division.hpp index 99e72e6..9c964c9 100644 --- a/include/bound/detail/division.hpp +++ b/include/bound/detail/division.hpp @@ -175,7 +175,7 @@ namespace bnd::detail // (overflow). So when the divisor excludes zero AND this is false, `div` // returns a plain `result` rather than optional. static constexpr bool may_overflow_nonzero = - !native_div && !real_raw && (needs_overflow_check != 0); + !native_div && !f64_raw && (needs_overflow_check != 0); // Real division can still fail on a zero divisor, so it uses the same // return-type rule as the rest: plain `result` when the op cannot fail @@ -218,7 +218,7 @@ namespace bnd::detail [[maybe_unused]] constexpr bool zero_unchecked = DivisorExcludesZero || (((G | F | BoundPolicy | BoundPolicy) & ignore_zero) != 0); - if constexpr (real_raw) + if constexpr (f64_raw) { // Real division reports zero like every other path (throw / report / // action / nullopt). Finite operands keep the quotient finite, so no diff --git a/include/bound/detail/multiplication.hpp b/include/bound/detail/multiplication.hpp index 4d9cdb0..c4f0d61 100644 --- a/include/bound/detail/multiplication.hpp +++ b/include/bound/detail/multiplication.hpp @@ -60,7 +60,7 @@ namespace bnd::detail template static constexpr auto mul(L lhs, R rhs, P&& policy, A&& action = {}) -> mul_return_t { - if constexpr (real_raw) + if constexpr (f64_raw) { return result::from_raw(Grid.snap_double(as_double(lhs) * as_double(rhs))); } @@ -84,7 +84,7 @@ namespace bnd::detail from_value(res, to_value(lhs) * to_value(rhs)); return res; } - else if constexpr (real_raw || real_raw || rational_raw || rational_raw) + else if constexpr (f64_raw || f64_raw || rational_raw || rational_raw) { // An operand whose raw is a double/rational can't feed the integer // four-quadrant formula below (it reads the raw as an integer offset). diff --git a/include/bound/generic.hpp b/include/bound/generic.hpp index 660bb49..0b36b48 100644 --- a/include/bound/generic.hpp +++ b/include/bound/generic.hpp @@ -113,18 +113,18 @@ namespace bnd // How a bound's value lives in its raw storage — four disjoint encodings // (selected by policy flags or deduced; see grid.hpp storage_pick): // rational_raw — raw IS the value, as a rational. - // real_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). + // f64_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). // value_raw — raw IS the value, as a plain integer. // index_raw — raw is a 0-based notch index; value = Lower + raw*Notch. template - inline constexpr bool real_raw = std::is_same_v, double>; + inline constexpr bool f64_raw = std::is_same_v, double>; template inline constexpr bool rational_raw = std::is_same_v, rational>; template inline constexpr bool value_raw = - !real_raw && !rational_raw + !f64_raw && !rational_raw && ((BoundPolicy & bnd::direct) == bnd::direct || ((BoundPolicy & bnd::indexed) != bnd::indexed && Notch == 1 @@ -132,7 +132,7 @@ namespace bnd template inline constexpr bool index_raw = - !real_raw && !rational_raw && !value_raw; + !f64_raw && !rational_raw && !value_raw; // Ungated double view of any bound, for the `real` arithmetic arms (the // public operator double() is gated on a rounding flag; this is always diff --git a/include/bound/io.hpp b/include/bound/io.hpp index 5c8e57b..b6efa86 100644 --- a/include/bound/io.hpp +++ b/include/bound/io.hpp @@ -133,10 +133,10 @@ namespace bnd // rational-raw bound has no std::to_string at all.) A continuous (Notch == 0) // real bound prints the double. template - requires (detail::real_raw || detail::rational_raw) + requires (detail::f64_raw || detail::rational_raw) inline std::string to_string(B b) { - if constexpr (detail::real_raw && Notch == bnd::detail::rational{0}) + if constexpr (detail::f64_raw && Notch == bnd::detail::rational{0}) return std::to_string(detail::as_double(b)); else return to_string(bnd::detail::as_rational(b)); diff --git a/single_include/bound/bound.hpp b/single_include/bound/bound.hpp index 8791d5b..af3116e 100644 --- a/single_include/bound/bound.hpp +++ b/single_include/bound/bound.hpp @@ -4043,18 +4043,18 @@ namespace bnd // How a bound's value lives in its raw storage — four disjoint encodings // (selected by policy flags or deduced; see grid.hpp storage_pick): // rational_raw — raw IS the value, as a rational. - // real_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). + // f64_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). // value_raw — raw IS the value, as a plain integer. // index_raw — raw is a 0-based notch index; value = Lower + raw*Notch. template - inline constexpr bool real_raw = std::is_same_v, double>; + inline constexpr bool f64_raw = std::is_same_v, double>; template inline constexpr bool rational_raw = std::is_same_v, rational>; template inline constexpr bool value_raw = - !real_raw && !rational_raw + !f64_raw && !rational_raw && ((BoundPolicy & bnd::direct) == bnd::direct || ((BoundPolicy & bnd::indexed) != bnd::indexed && Notch == 1 @@ -4062,7 +4062,7 @@ namespace bnd template inline constexpr bool index_raw = - !real_raw && !rational_raw && !value_raw; + !f64_raw && !rational_raw && !value_raw; // Ungated double view of any bound, for the `real` arithmetic arms (the // public operator double() is gated on a rounding flag; this is always @@ -4729,7 +4729,7 @@ namespace bnd::detail // or NotchCount, no rounding. real takes the endpoint as a double, rational // the exact constant (a double round-trip would lose non-dyadic endpoints); // raw_from_offset adds Lower back for direct-encoded storage. - if constexpr (real_raw) + if constexpr (f64_raw) lhs = L::from_raw((rhs < Lower) ? static_cast(Lower) : static_cast(Upper)); else if constexpr (rational_raw) @@ -4775,10 +4775,10 @@ namespace bnd::detail { if constexpr (rational_raw && Notch == 0) { lhs = L::from_raw(rhs); return true; } // continuous: store verbatim - else if constexpr (real_raw) + else if constexpr (f64_raw) { // real target: raw IS the value — snap to the dyadic grid (range handling - // already ran in the assign cascade; finite guard mirrors store_real's). + // already ran in the assign cascade; finite guard mirrors store_f64's). const double v = static_cast(rhs); if (!(v - v == 0)) detail::raise(errc::not_finite, "non-finite double"); @@ -4818,7 +4818,7 @@ namespace bnd::detail // instead of two rational ops. round_quotient is invariant under reduction, // so the slot is bit-identical to the rational path. Oversized denominators // fall through (the kMaxDen guard keeps every product inside imax). - if constexpr (HasQFormatFastPath && !real_raw && Notch != 0) + if constexpr (HasQFormatFastPath && !f64_raw && Notch != 0) { constexpr imax K = abs_den(Notch.Denominator); constexpr imax Lo = LowerImax; @@ -4911,7 +4911,7 @@ namespace bnd::detail if constexpr (rational_raw) return Lower; else if constexpr (Notch == 0) - // Continuous real_raw L: no grid to land on, mapping unused (store + // Continuous f64_raw L: no grid to land on, mapping unused (store // routes through snap_double). 0 avoids the /Notch divide-by-zero. return rational{0}; else if constexpr (rational_raw) @@ -4925,7 +4925,7 @@ namespace bnd::detail if constexpr (rational_raw) return Notch; else if constexpr (Notch == 0) - // Continuous real_raw L (see calcOffset). A denominator-1 Factor also + // Continuous f64_raw L (see calcOffset). A denominator-1 Factor also // makes assign_notch_ok vacuously true (any value representable). return rational{0}; else if constexpr (rational_raw) @@ -4942,7 +4942,7 @@ namespace bnd::detail // sides (not rational, not real). static constexpr bool is_integer_mapping = !rational_raw && !rational_raw - && !real_raw && !real_raw + && !f64_raw && !f64_raw && abs_den(Factor.Denominator) == 1 && abs_den(Offset.Denominator) == 1; // Map rhs.Raw into L's raw space (requires is_integer_mapping). The @@ -4981,7 +4981,7 @@ namespace bnd::detail { // RawLo/RawHi are already the correct Raw (no raw_from_offset). Real storage // takes the endpoint as a double (RawLo/Hi truncate fractional dyadic endpoints). - if constexpr (real_raw) + if constexpr (f64_raw) lhs = L::from_raw((as_rational(rhs) < Lower) ? static_cast(Lower) : static_cast(Upper)); else @@ -5050,7 +5050,7 @@ namespace bnd::detail template static constexpr void store(L& lhs, R const& rhs, P&&) { - if constexpr (real_raw) + if constexpr (f64_raw) // real target: raw IS the value — decode the source and snap to the dyadic // grid (the offset machinery below mis-encodes a double raw). lhs = L::from_raw(Grid.snap_double(as_double(rhs))); @@ -5558,7 +5558,7 @@ namespace bnd::detail static constexpr auto add(L lhs, R rhs, policy policy = {}, A&& action = {}) -> add_return_t { result res; - if constexpr (real_raw) + if constexpr (f64_raw) { res = result::from_raw(Grid.snap_double(as_double(lhs) + as_double(rhs))); } @@ -5664,7 +5664,7 @@ namespace bnd::detail template static constexpr auto mul(L lhs, R rhs, P&& policy, A&& action = {}) -> mul_return_t { - if constexpr (real_raw) + if constexpr (f64_raw) { return result::from_raw(Grid.snap_double(as_double(lhs) * as_double(rhs))); } @@ -5688,7 +5688,7 @@ namespace bnd::detail from_value(res, to_value(lhs) * to_value(rhs)); return res; } - else if constexpr (real_raw || real_raw || rational_raw || rational_raw) + else if constexpr (f64_raw || f64_raw || rational_raw || rational_raw) { // An operand whose raw is a double/rational can't feed the integer // four-quadrant formula below (it reads the raw as an integer offset). @@ -5931,7 +5931,7 @@ namespace bnd::detail // (overflow). So when the divisor excludes zero AND this is false, `div` // returns a plain `result` rather than optional. static constexpr bool may_overflow_nonzero = - !native_div && !real_raw && (needs_overflow_check != 0); + !native_div && !f64_raw && (needs_overflow_check != 0); // Real division can still fail on a zero divisor, so it uses the same // return-type rule as the rest: plain `result` when the op cannot fail @@ -5974,7 +5974,7 @@ namespace bnd::detail [[maybe_unused]] constexpr bool zero_unchecked = DivisorExcludesZero || (((G | F | BoundPolicy | BoundPolicy) & ignore_zero) != 0); - if constexpr (real_raw) + if constexpr (f64_raw) { // Real division reports zero like every other path (throw / report / // action / nullopt). Finite operands keep the quotient finite, so no @@ -6231,7 +6231,7 @@ namespace bnd // Snap a value onto `real` storage: lossless on the dyadic grid. Out-of-range // values run the same policy cascade as the fractional path (clamp → wrap → // sentinel/checked-report → store as-is); all arithmetic stays in double. - constexpr void store_real(double v) + constexpr void store_f64(double v) { // NaN/±inf would reach snap_double's integer cast (UB); reject like the // non-real path. `v - v` is 0 for every finite v, NaN otherwise. @@ -6273,14 +6273,14 @@ namespace bnd template constexpr void store_value(A const& value) { - if constexpr (detail::real_raw) - store_real(to_double(value)); + if constexpr (detail::f64_raw) + store_f64(to_double(value)); else if constexpr (is_bound_v) { // A `real` SOURCE holds its value as a double raw; the assignment engine's // integer offset formula (Lower + raw·Notch) would misread it. Extract as // a double and route through the arithmetic-source path. - if constexpr (detail::real_raw) + if constexpr (detail::f64_raw) detail::assignment::assign(*this, detail::as_double(value), make_policy

()); else detail::assignment::assign(*this, value, make_policy

()); @@ -6301,8 +6301,8 @@ namespace bnd // The one-shot `pol` widens the assignable check (a clamp/round passed here // relaxes the notch/interval clause), so a notch-incompatible boundable source // is accepted — e.g. clamp_round(some_bound). Body honours `pol` as before. - if constexpr (detail::real_raw) - store_real(to_double(value)); + if constexpr (detail::f64_raw) + store_f64(to_double(value)); else detail::assignment::assign(*this, value, pol); } @@ -6315,8 +6315,8 @@ namespace bnd requires bound_assignable constexpr bound(A value, errc& ec) { - if constexpr (detail::real_raw) - store_real(to_double(value)); + if constexpr (detail::f64_raw) + store_f64(to_double(value)); else detail::assignment::assign(*this, value, make_policy

(ec)); } @@ -6538,7 +6538,7 @@ namespace bnd [[nodiscard]] constexpr negative operator-() const { negative neg; - if constexpr (detail::real_raw) + if constexpr (detail::f64_raw) neg = negative::from_raw(-Raw); else if constexpr (detail::rational_raw) neg = negative::from_raw(-(Raw)); @@ -6811,7 +6811,7 @@ namespace bnd if constexpr (Grid == Grid) return lhs.raw() <=> rhs.raw(); // double-backed (`real`) operand: compare in double (raw_imax would truncate) - else if constexpr (detail::real_raw || detail::real_raw) + else if constexpr (detail::f64_raw || detail::f64_raw) return detail::as_double(lhs) <=> detail::as_double(rhs); // both integer-direct (notch=1, Raw==value): compare as integers else if constexpr (!detail::rational_raw && !detail::rational_raw @@ -6826,7 +6826,7 @@ namespace bnd { if constexpr (Grid == Grid) return lhs.raw() == rhs.raw(); - else if constexpr (detail::real_raw || detail::real_raw) + else if constexpr (detail::f64_raw || detail::f64_raw) return detail::as_double(lhs) == detail::as_double(rhs); else if constexpr (!detail::rational_raw && !detail::rational_raw && !detail::index_raw && !detail::index_raw) @@ -8492,7 +8492,7 @@ namespace bnd::math && !rational_raw // `real` storage holds the VALUE, not an offset index, so route it // through the rational fallback `Out{r}` (same guard as fmod_int_fast). - && !real_raw + && !f64_raw && has_flag(BoundPolicy, round_nearest) && (std::signed_integral> || NotchCount @@ -9143,9 +9143,9 @@ namespace bnd::math // * all unit counts fit comfortably in imax (headroom 4). template inline constexpr bool fmod_int_fast = []{ - if (rational_raw || real_raw - || rational_raw || real_raw - || rational_raw || real_raw) + if (rational_raw || f64_raw + || rational_raw || f64_raw + || rational_raw || f64_raw) return false; if (Notch == 0 || Notch == 0 || Notch == 0) return false; @@ -10897,10 +10897,10 @@ namespace bnd // rational-raw bound has no std::to_string at all.) A continuous (Notch == 0) // real bound prints the double. template - requires (detail::real_raw || detail::rational_raw) + requires (detail::f64_raw || detail::rational_raw) inline std::string to_string(B b) { - if constexpr (detail::real_raw && Notch == bnd::detail::rational{0}) + if constexpr (detail::f64_raw && Notch == bnd::detail::rational{0}) return std::to_string(detail::as_double(b)); else return to_string(bnd::detail::as_rational(b)); diff --git a/tests/fuzz.cpp b/tests/fuzz.cpp index 54e7e7d..8dd36af 100644 --- a/tests/fuzz.cpp +++ b/tests/fuzz.cpp @@ -77,7 +77,7 @@ template typename B::raw_type random_in_range_raw(std::mt19937_64& rng) { using raw = typename B::raw_type; - if constexpr (real_raw) + if constexpr (f64_raw) { // `real` (double-backed) bounds hold a grid point as a double — generate a // random in-range grid point Lower + k·Notch (the integer-cast branch below diff --git a/tests/test_real_exact.cpp b/tests/test_real_exact.cpp index ce1b4c5..59f393b 100644 --- a/tests/test_real_exact.cpp +++ b/tests/test_real_exact.cpp @@ -36,7 +36,7 @@ namespace template void check_bits(const R& r, const char* op) { - if constexpr (real_raw) + if constexpr (f64_raw) { const double v = r.raw(); INFO(op << " raw=" << v); diff --git a/tests/test_storage_flags.cpp b/tests/test_storage_flags.cpp index b3bfc5f..06038ac 100644 --- a/tests/test_storage_flags.cpp +++ b/tests/test_storage_flags.cpp @@ -124,7 +124,7 @@ TEST_CASE("representation flags resolve widest-wins", "[storage][policy]") // BND_MATH_FIXED the real arm is elided and direct wins). using RD = bound<{0, 4}, real | direct>; #ifndef BND_MATH_FIXED - STATIC_REQUIRE(detail::real_raw); + STATIC_REQUIRE(detail::f64_raw); #else STATIC_REQUIRE(detail::value_raw); #endif @@ -149,7 +149,7 @@ TEST_CASE("f64 is the canonical double-backed flag; real is its alias", using R = bound<{{0, 4}, notch<1, 256>}, round_nearest | real>; STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(detail::real_raw); + STATIC_REQUIRE(detail::f64_raw); #endif } @@ -256,7 +256,7 @@ TEST_CASE("atan / atan2 accept magnitudes beyond 1", "[cmath][atan][domain]") } // Regression: per-operation policy overrides (`with_*`, `on_*`, `policy(ec)`) -// route through the assignment engine, which previously had no real_raw arm +// route through the assignment engine, which previously had no f64_raw arm // and wrote integer offsets into the double raw (e.g. with_clamp stored the // notch COUNT instead of the endpoint). TEST_CASE("per-operation policies work on real-backed bounds", From b9a99c07d8a10e35b4bdae67c7887c1e02bc666a Mon Sep 17 00:00:00 2001 From: Peter Neiss Date: Mon, 22 Jun 2026 21:27:40 +0200 Subject: [PATCH 2/2] =?UTF-8?q?math:=20add=20f32=20(binary32)=20storage=20?= =?UTF-8?q?=E2=80=94=20the=20float=20sibling=20of=20f64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bound carrying `f32` holds its value in a binary32 raw (float), the single-precision sibling of `f64`, for single-precision FPUs and the `flt` engine. The natural pairing `flt` compute + `f32` storage keeps the whole path in hardware float — no double round-trip at the I/O boundary. Implemented via an `fp_raw` (= f64_raw || f32_raw) unification rather than a parallel float path: read/store/compare/arithmetic compute in double and narrow to the raw type on store, which is lossless because the grid is fp-exact. - policy_flag: new `f32` flag (bundles round_nearest), widest-wins order exact > f64 > f32 > direct > indexed. - grid.hpp: `float_exact` (24-bit significand, exponent ≥ -126), the binary32 analogue of double_exact; storage_pick returns `float{}` for an f32 dyadic float-exact grid (static_asserts on direct misuse, like f64). - generic.hpp: `f32_raw`/`fp_raw` predicates; value_raw/index_raw exclude fp_raw; as_double / operator rational / sentinel_raw already fp-generic. - core.hpp: `store_f64`→`store_fp` (narrows the double snap to the raw type); value-path branches keyed on fp_raw; operator double implicit for f32 too; the f32 dyadic guard. - addition/multiplication/division: rep propagation gains f32 with **demotion** — f32 stays only when both operands are f32-only and the result fits float, else widens to f64, else drops to exact; bodies snap-and-narrow via fp_raw. - cmath: dbl::store / flt::store store straight into any fp-backed Out (f32 result no longer detours through the rational path); fast-path guards exclude fp_raw. - io.hpp / assignment.hpp: f64_raw→fp_raw so f32 prints and assigns like f64. Tests: f32 storage selection, lossless construct/read, arithmetic stays f32 and demotes f32→f64 when a result grid outgrows float, widest-wins, and math output landing in f32 (test_storage_flags.cpp). Docs: policies.md + math.md. Verified: default 407/407, CORDIC 443/443 (f32 elided → integer), FLOAT 398/398, all single-header smokes build. Co-Authored-By: Claude Opus 4.8 --- docs/math.md | 8 + docs/policies.md | 5 +- include/bound/cmath.hpp | 8 +- include/bound/cmath_double.hpp | 7 +- include/bound/cmath_float.hpp | 10 +- include/bound/core.hpp | 45 +++-- include/bound/detail/addition.hpp | 22 ++- include/bound/detail/assignment.hpp | 16 +- include/bound/detail/division.hpp | 24 ++- include/bound/detail/multiplication.hpp | 30 +-- include/bound/generic.hpp | 16 +- include/bound/grid.hpp | 41 +++- include/bound/io.hpp | 4 +- include/bound/policy_flag.hpp | 13 +- single_include/bound/bound.hpp | 236 ++++++++++++++++-------- tests/test_storage_flags.cpp | 47 +++++ 16 files changed, 370 insertions(+), 162 deletions(-) diff --git a/docs/math.md b/docs/math.md index f647d56..703c871 100644 --- a/docs/math.md +++ b/docs/math.md @@ -277,6 +277,14 @@ size/speed where double-grade precision isn't needed. quantized onto the output grid. Ships its own golden pins (`tests/test_math_engines.cpp`). +**Pair `flt` with `f32` storage.** An `f32`-backed operand holds a binary32 raw, +so `flt` reads it, computes, and stores the result straight in `float` — no +`double` round-trip. On a single-precision-only FPU that keeps the whole path in +hardware float; with `f64`/rational storage the boundary marshalling goes through +`double` (soft-float on such targets). Because binary32 has only a 24-bit +significand, an `f32` result grid that a function would overflow (e.g. `exp` of a +large argument on a fine grid) is a compile error — widen the grid or use `f64`. + ## Compiling without floating point (`BND_MATH_NO_FP`) On a target with no hardware FPU and no ``, define **`BND_MATH_NO_FP`** diff --git a/docs/policies.md b/docs/policies.md index 405ec95..f2f3802 100644 --- a/docs/policies.md +++ b/docs/policies.md @@ -49,7 +49,7 @@ is proven elsewhere. `clamp`, `wrap`, and `sentinel` are mutually exclusive | `round_half_even` | banker's rounding — half to even (implies `snap`) | | `ignore_zero` | skip the divide-by-zero check — `a / 0` / `a % 0` is UB (binary `div`/`mod`); compound `/= 0` / `%= 0` no-op | | `ignore_domain` | suppress the runtime domain check | -| `f64` / `exact` / `direct` / `indexed` | **representation flags** — select how the raw value is stored; see the next section (`real` is a deprecated alias of `f64`) | +| `f64` / `f32` / `exact` / `direct` / `indexed` | **representation flags** — select how the raw value is stored; see the next section (`real` is a deprecated alias of `f64`) | ## Representation flags @@ -60,6 +60,7 @@ Besides the *behavior* flags above, four flags select the **representation** | Flag | Forces | Grid requirement | Notes | |---|---|---|---| | `f64` | IEEE-754 `double` raw (the value itself, snapped to the grid) | dyadic **and** double-exact (every value fits `double`'s 53-bit significand) | bundles `round_nearest`; the fast math-storage flag. Arithmetic drops `f64` to an exact representation when a result grid is too fine for `double`. Under `BND_MATH_FIXED` it falls back to integer storage. **`real` is a deprecated alias of `f64`.** | +| `f32` | IEEE-754 `float` raw (the value itself, snapped to the grid) | dyadic **and** float-exact (every value fits `float`'s 24-bit significand) | the binary32 sibling of `f64`, for single-precision FPUs and the `flt` engine. Arithmetic **demotes `f32`→`f64`** when a result grid outgrows `float` (then drops to exact when it outgrows `double`). Under `BND_MATH_FIXED` it falls back to integer storage. | | `exact` | exact-fraction raw on **any** grid | none | no notch-count limit, no `double` anywhere; arithmetic is exact — on notched grids overflow is usually provably impossible and `+ − ×` return plain bounds (no `optional`) | | `direct` | raw == value as a plain integer | `Notch == 1` | e.g. `bound<{5, 100}, direct>` stores 5..100, not index 0..95 — the raw equals the wire/debugger value | | `indexed` | raw == 0-based notch index | `Notch != 0` | e.g. `bound<{-5, 5}, indexed>` stores 0..10 unsigned — dense layout for serialization | @@ -73,7 +74,7 @@ using slot = bound<{-5, 5}, indexed>; // raw() == 0..10, dense unsigned Binary arithmetic ORs the policies of both operands, so a result can carry several representation flags; storage selection resolves them -**widest-wins**: `exact > f64 > direct > indexed > deduced`. An +**widest-wins**: `exact > f64 > f32 > direct > indexed > deduced`. An `exact + f64` sum is therefore exact, and a `f64` math chain stays double-backed end to end — no errors at mixed call sites. diff --git a/include/bound/cmath.hpp b/include/bound/cmath.hpp index e0dcb8b..e4dc508 100644 --- a/include/bound/cmath.hpp +++ b/include/bound/cmath.hpp @@ -256,7 +256,7 @@ namespace bnd::math && !rational_raw // `real` storage holds the VALUE, not an offset index, so route it // through the rational fallback `Out{r}` (same guard as fmod_int_fast). - && !f64_raw + && !fp_raw && has_flag(BoundPolicy, round_nearest) && (std::signed_integral> || NotchCount @@ -907,9 +907,9 @@ namespace bnd::math // * all unit counts fit comfortably in imax (headroom 4). template inline constexpr bool fmod_int_fast = []{ - if (rational_raw || f64_raw - || rational_raw || f64_raw - || rational_raw || f64_raw) + if (rational_raw || fp_raw + || rational_raw || fp_raw + || rational_raw || fp_raw) return false; if (Notch == 0 || Notch == 0 || Notch == 0) return false; diff --git a/include/bound/cmath_double.hpp b/include/bound/cmath_double.hpp index ab4b6f0..51eaec9 100644 --- a/include/bound/cmath_double.hpp +++ b/include/bound/cmath_double.hpp @@ -226,9 +226,10 @@ namespace bnd::math::dbl template [[nodiscard]] BND_DBL_FN Out store(double d) { - // `real` (double-backed) Out stores the double directly; a non-`real` snap grid - // assigns through the rational path, snapping via Out's round policy. - if constexpr (has_flag(BoundPolicy, real)) return Out{d}; + // An fp-backed Out (f64 or f32) stores the value directly via its raw (an f32 + // Out narrows double→float, lossless on its float-exact grid); a non-fp snap + // grid assigns through the rational path, snapping via Out's round policy. + if constexpr (bnd::detail::fp_raw) return Out{d}; else { Out o{}; o = bnd::detail::rational{d}; return o; } } diff --git a/include/bound/cmath_float.hpp b/include/bound/cmath_float.hpp index 231481f..a881db0 100644 --- a/include/bound/cmath_float.hpp +++ b/include/bound/cmath_float.hpp @@ -223,14 +223,14 @@ namespace bnd::math::flt::detail namespace bnd::math::flt { // Engine cores: bound in → `float` math → bound out. Storing the float result: - // a `real` (double-backed) bound takes Out{double(f)} (widening is exact); a - // non-`real` snap grid assigns through the rational path, snapping via Out's - // round policy. (An f32-backed bound — Phase 4b — will store the float raw - // directly; until then it routes through the same paths.) + // an fp-backed Out (f32 OR f64) stores the value directly via its float/double + // raw (the natural pairing for `flt` is `f32` — no rational, no double round- + // trip on the result); any other snap grid assigns through the rational path, + // snapping via Out's round policy. template [[nodiscard]] BND_DBL_FN Out store(float f) { - if constexpr (has_flag(BoundPolicy, real)) return Out{static_cast(f)}; + if constexpr (bnd::detail::fp_raw) return Out{static_cast(f)}; else { Out o{}; o = bnd::detail::rational{static_cast(f)}; return o; } } diff --git a/include/bound/core.hpp b/include/bound/core.hpp index b5c0439..021063e 100644 --- a/include/bound/core.hpp +++ b/include/bound/core.hpp @@ -74,8 +74,12 @@ namespace bnd // no grid to snap to. Anything else is rejected here rather than silently // demoted to integer storage. static_assert(!has_flag(P, real) || detail::dyadic_grid || G.Notch == 0, - "bnd: the `real` policy requires a dyadic grid (power-of-two " + "bnd: the `real`/`f64` policy requires a dyadic grid (power-of-two " "notch and Lower, so values are exactly representable in double)"); + static_assert(!has_flag(P, f32) || detail::dyadic_grid || G.Notch == 0, + "bnd: the `f32` policy requires a dyadic grid (power-of-two notch " + "and Lower); values must also fit float's 24-bit significand " + "(checked at storage selection — see `float_exact`)"); #endif // Representation flags vs grid shape (exact has no requirement; a result // policy may carry several flags — storage selection resolves widest-wins, @@ -110,8 +114,9 @@ namespace bnd // Value-init `bound{}` still zero-fills where a zero raw is genuinely wanted.) constexpr bound() = default; - // `real` storage holds the value as a double directly. An arithmetic rhs - // casts straight to double; a bound rhs goes through its exact rational view. + // fp storage (f64/f32) holds the value as a floating raw directly. An + // arithmetic rhs casts straight to double; a bound rhs goes through its exact + // rational view. private: template constexpr double to_double(A const& value) @@ -120,10 +125,12 @@ namespace bnd else return static_cast(detail::as_rational(value)); } public: - // Snap a value onto `real` storage: lossless on the dyadic grid. Out-of-range - // values run the same policy cascade as the fractional path (clamp → wrap → - // sentinel/checked-report → store as-is); all arithmetic stays in double. - constexpr void store_f64(double v) + // Snap a value onto fp storage: lossless on the (fp-exact) dyadic grid — the + // snap is computed in double and narrowed to the raw type (double or float), + // which is exact because every grid point fits the raw's significand. Out-of- + // range values run the same policy cascade as the fractional path (clamp → + // wrap → sentinel/checked-report → store as-is). + constexpr void store_fp(double v) { // NaN/±inf would reach snap_double's integer cast (UB); reject like the // non-real path. `v - v` is 0 for every finite v, NaN otherwise. @@ -159,20 +166,20 @@ namespace bnd return; // sentinel stored / reported (error_code mode) // no handler (unchecked policy): fall through and store snapped as-is } - Raw = G.snap_double(v); + Raw = static_cast(G.snap_double(v)); // narrow to float for f32 (lossless) } template constexpr void store_value(A const& value) { - if constexpr (detail::f64_raw) - store_f64(to_double(value)); + if constexpr (detail::fp_raw) + store_fp(to_double(value)); else if constexpr (is_bound_v) { // A `real` SOURCE holds its value as a double raw; the assignment engine's // integer offset formula (Lower + raw·Notch) would misread it. Extract as // a double and route through the arithmetic-source path. - if constexpr (detail::f64_raw) + if constexpr (detail::fp_raw) detail::assignment::assign(*this, detail::as_double(value), make_policy

()); else detail::assignment::assign(*this, value, make_policy

()); @@ -193,8 +200,8 @@ namespace bnd // The one-shot `pol` widens the assignable check (a clamp/round passed here // relaxes the notch/interval clause), so a notch-incompatible boundable source // is accepted — e.g. clamp_round(some_bound). Body honours `pol` as before. - if constexpr (detail::f64_raw) - store_f64(to_double(value)); + if constexpr (detail::fp_raw) + store_fp(to_double(value)); else detail::assignment::assign(*this, value, pol); } @@ -207,8 +214,8 @@ namespace bnd requires bound_assignable constexpr bound(A value, errc& ec) { - if constexpr (detail::f64_raw) - store_f64(to_double(value)); + if constexpr (detail::fp_raw) + store_fp(to_double(value)); else detail::assignment::assign(*this, value, make_policy

(ec)); } @@ -315,7 +322,7 @@ namespace bnd && G.Interval.Upper <= bnd::detail::rational{std::numeric_limits::max()}) { return detail::to_value(*this); } - constexpr explicit(!has_flag(P, real)) operator double() const + constexpr explicit(!has_flag(P, real) && !has_flag(P, f32)) operator double() const requires ((P & (round_floor | round_ceil | round_nearest | round_half_even | snap)) != 0) { return detail::as_double(*this); } @@ -430,7 +437,7 @@ namespace bnd [[nodiscard]] constexpr negative operator-() const { negative neg; - if constexpr (detail::f64_raw) + if constexpr (detail::fp_raw) neg = negative::from_raw(-Raw); else if constexpr (detail::rational_raw) neg = negative::from_raw(-(Raw)); @@ -703,7 +710,7 @@ namespace bnd if constexpr (Grid == Grid) return lhs.raw() <=> rhs.raw(); // double-backed (`real`) operand: compare in double (raw_imax would truncate) - else if constexpr (detail::f64_raw || detail::f64_raw) + else if constexpr (detail::fp_raw || detail::fp_raw) return detail::as_double(lhs) <=> detail::as_double(rhs); // both integer-direct (notch=1, Raw==value): compare as integers else if constexpr (!detail::rational_raw && !detail::rational_raw @@ -718,7 +725,7 @@ namespace bnd { if constexpr (Grid == Grid) return lhs.raw() == rhs.raw(); - else if constexpr (detail::f64_raw || detail::f64_raw) + else if constexpr (detail::fp_raw || detail::fp_raw) return detail::as_double(lhs) == detail::as_double(rhs); else if constexpr (!detail::rational_raw && !detail::rational_raw && !detail::index_raw && !detail::index_raw) diff --git a/include/bound/detail/addition.hpp b/include/bound/detail/addition.hpp index d660150..82e51e7 100644 --- a/include/bound/detail/addition.hpp +++ b/include/bound/detail/addition.hpp @@ -24,16 +24,22 @@ namespace bnd::detail "addition: result grid's notch/interval exceeds the representable rational " "range — coarsen the operand grids"); static constexpr grid result_grid = (Grid + Grid).value(); - // Propagate `real` only when the result grid stays exactly representable in - // double; otherwise drop it so storage_pick deduces an exact representation - // (the double sum would diverge from the exact sum — see grid::double_exact). - static constexpr bool any_real = + // Propagate fp storage only when the result grid stays exactly representable + // in the chosen width; otherwise demote (f32→f64) or drop it so storage_pick + // deduces an exact representation (the fp sum would diverge from the exact sum + // — see grid::double_exact / float_exact). Widest-wins: prefer f32 only when + // both operands are f32-only and the result fits float; an f64 operand or a + // too-fine-for-float result widens to f64; too fine for double → exact. + static constexpr bool any_f64 = (BoundPolicy & bnd::real) == bnd::real || (BoundPolicy & bnd::real) == bnd::real; - static constexpr bool keep_real = any_real && double_exact; + static constexpr bool any_f32 = + (BoundPolicy & bnd::f32) == bnd::f32 || (BoundPolicy & bnd::f32) == bnd::f32; + static constexpr bool keep_f32 = any_f32 && !any_f64 && float_exact; + static constexpr bool keep_f64 = !keep_f32 && (any_f64 || any_f32) && double_exact; // Carry both operands' representation flags (widest-wins at storage selection). static constexpr policy_flag rep = ((BoundPolicy | BoundPolicy) & (bnd::exact | bnd::direct | bnd::indexed)) - | (keep_real ? bnd::real : none); + | (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none); using result = bound; template @@ -68,9 +74,9 @@ namespace bnd::detail static constexpr auto add(L lhs, R rhs, policy policy = {}, A&& action = {}) -> add_return_t { result res; - if constexpr (f64_raw) + if constexpr (fp_raw) { - res = result::from_raw(Grid.snap_double(as_double(lhs) + as_double(rhs))); + res = result::from_raw(raw_cast(Grid.snap_double(as_double(lhs) + as_double(rhs)))); } else if constexpr (rational_raw) { diff --git a/include/bound/detail/assignment.hpp b/include/bound/detail/assignment.hpp index 869bfb9..bbd6fae 100644 --- a/include/bound/detail/assignment.hpp +++ b/include/bound/detail/assignment.hpp @@ -217,7 +217,7 @@ namespace bnd::detail // or NotchCount, no rounding. real takes the endpoint as a double, rational // the exact constant (a double round-trip would lose non-dyadic endpoints); // raw_from_offset adds Lower back for direct-encoded storage. - if constexpr (f64_raw) + if constexpr (fp_raw) lhs = L::from_raw((rhs < Lower) ? static_cast(Lower) : static_cast(Upper)); else if constexpr (rational_raw) @@ -263,7 +263,7 @@ namespace bnd::detail { if constexpr (rational_raw && Notch == 0) { lhs = L::from_raw(rhs); return true; } // continuous: store verbatim - else if constexpr (f64_raw) + else if constexpr (fp_raw) { // real target: raw IS the value — snap to the dyadic grid (range handling // already ran in the assign cascade; finite guard mirrors store_f64's). @@ -306,7 +306,7 @@ namespace bnd::detail // instead of two rational ops. round_quotient is invariant under reduction, // so the slot is bit-identical to the rational path. Oversized denominators // fall through (the kMaxDen guard keeps every product inside imax). - if constexpr (HasQFormatFastPath && !f64_raw && Notch != 0) + if constexpr (HasQFormatFastPath && !fp_raw && Notch != 0) { constexpr imax K = abs_den(Notch.Denominator); constexpr imax Lo = LowerImax; @@ -399,7 +399,7 @@ namespace bnd::detail if constexpr (rational_raw) return Lower; else if constexpr (Notch == 0) - // Continuous f64_raw L: no grid to land on, mapping unused (store + // Continuous fp_raw L: no grid to land on, mapping unused (store // routes through snap_double). 0 avoids the /Notch divide-by-zero. return rational{0}; else if constexpr (rational_raw) @@ -413,7 +413,7 @@ namespace bnd::detail if constexpr (rational_raw) return Notch; else if constexpr (Notch == 0) - // Continuous f64_raw L (see calcOffset). A denominator-1 Factor also + // Continuous fp_raw L (see calcOffset). A denominator-1 Factor also // makes assign_notch_ok vacuously true (any value representable). return rational{0}; else if constexpr (rational_raw) @@ -430,7 +430,7 @@ namespace bnd::detail // sides (not rational, not real). static constexpr bool is_integer_mapping = !rational_raw && !rational_raw - && !f64_raw && !f64_raw + && !fp_raw && !fp_raw && abs_den(Factor.Denominator) == 1 && abs_den(Offset.Denominator) == 1; // Map rhs.Raw into L's raw space (requires is_integer_mapping). The @@ -469,7 +469,7 @@ namespace bnd::detail { // RawLo/RawHi are already the correct Raw (no raw_from_offset). Real storage // takes the endpoint as a double (RawLo/Hi truncate fractional dyadic endpoints). - if constexpr (f64_raw) + if constexpr (fp_raw) lhs = L::from_raw((as_rational(rhs) < Lower) ? static_cast(Lower) : static_cast(Upper)); else @@ -538,7 +538,7 @@ namespace bnd::detail template static constexpr void store(L& lhs, R const& rhs, P&&) { - if constexpr (f64_raw) + if constexpr (fp_raw) // real target: raw IS the value — decode the source and snap to the dyadic // grid (the offset machinery below mis-encodes a double raw). lhs = L::from_raw(Grid.snap_double(as_double(rhs))); diff --git a/include/bound/detail/division.hpp b/include/bound/detail/division.hpp index 9c964c9..64f3571 100644 --- a/include/bound/detail/division.hpp +++ b/include/bound/detail/division.hpp @@ -154,17 +154,21 @@ namespace bnd::detail ? grid{interval{rational{0}, (Upper / Notch).value()}, Notch} : *(Grid / Grid); - static constexpr bool any_real = + static constexpr bool any_f64 = (BoundPolicy & bnd::real) == bnd::real || (BoundPolicy & bnd::real) == bnd::real; - // Keep `real` for a continuous result (Notch 0: double stores the quotient - // verbatim) or a double-exact dyadic result; otherwise drop it (the double - // quotient would not land on the result grid). See grid::double_exact. - static constexpr bool keep_real = - any_real && (result_grid.Notch == 0 || double_exact); + static constexpr bool any_f32 = + (BoundPolicy & bnd::f32) == bnd::f32 || (BoundPolicy & bnd::f32) == bnd::f32; + // Keep fp for a continuous result (Notch 0: the raw stores the quotient + // verbatim) or an fp-exact dyadic result; otherwise demote f32→f64 / drop f64 + // (the fp quotient would not land on the result grid). Widest-wins as in mul. + static constexpr bool keep_f32 = + any_f32 && !any_f64 && (result_grid.Notch == 0 || float_exact); + static constexpr bool keep_f64 = + !keep_f32 && (any_f64 || any_f32) && (result_grid.Notch == 0 || double_exact); // Carry both operands' representation flags (widest-wins at storage selection). static constexpr policy_flag rep = ((BoundPolicy | BoundPolicy) & (bnd::exact | bnd::direct | bnd::indexed)) - | (keep_real ? bnd::real : none); + | (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none); using result = bound; template @@ -175,7 +179,7 @@ namespace bnd::detail // (overflow). So when the divisor excludes zero AND this is false, `div` // returns a plain `result` rather than optional. static constexpr bool may_overflow_nonzero = - !native_div && !f64_raw && (needs_overflow_check != 0); + !native_div && !fp_raw && (needs_overflow_check != 0); // Real division can still fail on a zero divisor, so it uses the same // return-type rule as the rest: plain `result` when the op cannot fail @@ -218,14 +222,14 @@ namespace bnd::detail [[maybe_unused]] constexpr bool zero_unchecked = DivisorExcludesZero || (((G | F | BoundPolicy | BoundPolicy) & ignore_zero) != 0); - if constexpr (f64_raw) + if constexpr (fp_raw) { // Real division reports zero like every other path (throw / report / // action / nullopt). Finite operands keep the quotient finite, so no // non-finite ever reaches storage. if constexpr (!zero_unchecked) if (as_double(rhs) == 0.0) return fail(errc::division_by_zero, "division by zero in div"); - return result::from_raw(Grid.snap_double(as_double(lhs) / as_double(rhs))); + return result::from_raw(raw_cast(Grid.snap_double(as_double(lhs) / as_double(rhs)))); } else if constexpr (native_div_qformat) { diff --git a/include/bound/detail/multiplication.hpp b/include/bound/detail/multiplication.hpp index c4f0d61..d0a2a9c 100644 --- a/include/bound/detail/multiplication.hpp +++ b/include/bound/detail/multiplication.hpp @@ -24,25 +24,31 @@ namespace bnd::detail "multiplication: result grid's notch/interval exceeds the representable " "rational range — coarsen the operand grids"); static constexpr grid result_grid = (Grid * Grid).value(); - static constexpr bool any_real = + // fp storage propagation (see addition.hpp): the product grid (notch = N_L·N_R) + // is finer, so demote f32→f64 / drop f64 when it outgrows the width — the fp + // product would round below the result notch. Widest-wins: f32 only if both + // operands f32-only and product fits float; else widen to f64; else exact. + static constexpr bool any_f64 = (BoundPolicy & bnd::real) == bnd::real || (BoundPolicy & bnd::real) == bnd::real; - // Drop `real` when the product grid (notch = N_L·N_R) outgrows double's - // 53-bit significand — the double product would round below the result notch. - static constexpr bool keep_real = any_real && double_exact; + static constexpr bool any_f32 = + (BoundPolicy & bnd::f32) == bnd::f32 || (BoundPolicy & bnd::f32) == bnd::f32; + static constexpr bool keep_f32 = any_f32 && !any_f64 && float_exact; + static constexpr bool keep_f64 = !keep_f32 && (any_f64 || any_f32) && double_exact; + static constexpr bool dropped_fp = (any_f64 || any_f32) && !keep_f64 && !keep_f32; // Carry both operands' representation flags (widest-wins at storage selection). static constexpr policy_flag rep = ((BoundPolicy | BoundPolicy) & (bnd::exact | bnd::direct | bnd::indexed)) - | (keep_real ? bnd::real : none); + | (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none); using result = bound; - // The dropped-`real` case (any_real && !keep_real) lands on a rational result - // when the product grid outgrows uint index space; its product numerator can - // exceed `umax`, so check it (the result carries `checked`) rather than wrap. + // The dropped-fp case lands on a rational result when the product grid outgrows + // uint index space; its product numerator can exceed `umax`, so check it (the + // result carries `checked`) rather than wrap. template static constexpr bool needs_overflow_check = rational_raw && (((BoundPolicy | BoundPolicy) & checked) || plain

::test(checked) - || (any_real && !keep_real)) + || dropped_fp) && !rational_mul_is_safe(Grid, Grid); template @@ -60,9 +66,9 @@ namespace bnd::detail template static constexpr auto mul(L lhs, R rhs, P&& policy, A&& action = {}) -> mul_return_t { - if constexpr (f64_raw) + if constexpr (fp_raw) { - return result::from_raw(Grid.snap_double(as_double(lhs) * as_double(rhs))); + return result::from_raw(raw_cast(Grid.snap_double(as_double(lhs) * as_double(rhs)))); } else if constexpr (rational_raw) { @@ -84,7 +90,7 @@ namespace bnd::detail from_value(res, to_value(lhs) * to_value(rhs)); return res; } - else if constexpr (f64_raw || f64_raw || rational_raw || rational_raw) + else if constexpr (fp_raw || fp_raw || rational_raw || rational_raw) { // An operand whose raw is a double/rational can't feed the integer // four-quadrant formula below (it reads the raw as an integer offset). diff --git a/include/bound/generic.hpp b/include/bound/generic.hpp index 0b36b48..7a98cca 100644 --- a/include/bound/generic.hpp +++ b/include/bound/generic.hpp @@ -113,18 +113,28 @@ namespace bnd // How a bound's value lives in its raw storage — four disjoint encodings // (selected by policy flags or deduced; see grid.hpp storage_pick): // rational_raw — raw IS the value, as a rational. - // f64_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). + // f64_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). + // f32_raw — raw IS the value, as an IEEE-754 float (dyadic grids only). // value_raw — raw IS the value, as a plain integer. // index_raw — raw is a 0-based notch index; value = Lower + raw*Notch. template inline constexpr bool f64_raw = std::is_same_v, double>; + template + inline constexpr bool f32_raw = std::is_same_v, float>; + + // fp_raw — value held directly in a floating-point raw (f64 or f32). These + // share every value-path branch: read/store/compare/arithmetic compute in + // double, narrowing to the raw type on store (lossless on an fp-exact grid). + template + inline constexpr bool fp_raw = f64_raw || f32_raw; + template inline constexpr bool rational_raw = std::is_same_v, rational>; template inline constexpr bool value_raw = - !f64_raw && !rational_raw + !fp_raw && !rational_raw && ((BoundPolicy & bnd::direct) == bnd::direct || ((BoundPolicy & bnd::indexed) != bnd::indexed && Notch == 1 @@ -132,7 +142,7 @@ namespace bnd template inline constexpr bool index_raw = - !f64_raw && !rational_raw && !value_raw; + !fp_raw && !rational_raw && !value_raw; // Ungated double view of any bound, for the `real` arithmetic arms (the // public operator double() is gated on a rounding flag; this is always diff --git a/include/bound/grid.hpp b/include/bound/grid.hpp index 8446334..c2bc5f3 100644 --- a/include/bound/grid.hpp +++ b/include/bound/grid.hpp @@ -232,6 +232,29 @@ namespace bnd template inline constexpr bool double_exact = compute_double_exact(); + // `float`-exactness: the binary32 analogue of double_exact. Every on-grid value + // v = N·2^(−f) must fit float's 24-bit significand (|N| < 2^24) with f ≤ 126 + // (notch ≥ float's smallest normal, so no on-grid value is subnormal). + // Necessary precondition for `f32` (binary32-backed) storage. + template + constexpr bool compute_float_exact() noexcept + { + if constexpr (!dyadic_grid) return false; + else + { + constexpr int f = log2_pow2_mag(abs_den(G.Notch.Denominator)); + if (f > 126) return false; + umax nlo = 0, nhi = 0; + if (!scaled_numerator(G.Interval.Lower, f, nlo)) return false; + if (!scaled_numerator(G.Interval.Upper, f, nhi)) return false; + constexpr umax lim = umax{1} << 24; + return nlo < lim && nhi < lim; + } + } + + template + inline constexpr bool float_exact = compute_float_exact(); + // Storage for a bound: representation flags pick the raw type, widest-wins // (exact > real > direct > indexed > deduced). // exact → rational raw on any grid. @@ -251,14 +274,26 @@ namespace bnd return double{}; else if constexpr (((P & bnd::real) == bnd::real) && dyadic_grid) { - // `real` explicitly requested on a dyadic grid double can't represent + // `real`/`f64` explicitly requested on a dyadic grid double can't represent // exactly (max |value·2^f| ≥ 2^53, or notch below the smallest normal). - // Arithmetic drops `real` before reaching here, so this is direct misuse. + // Arithmetic drops the flag before reaching here, so this is direct misuse. static_assert(double_exact, - "real storage: grid exceeds double's 53-bit significand — coarsen the " + "f64 storage: grid exceeds double's 53-bit significand — coarsen the " "notch/range or use `exact`"); return double{}; // unreachable; fixes the deduced return type } + else if constexpr (((P & bnd::f32) == bnd::f32) + && (float_exact || G.Notch == 0)) + return float{}; + else if constexpr (((P & bnd::f32) == bnd::f32) && dyadic_grid) + { + // `f32` requested on a dyadic grid float can't represent exactly. Arithmetic + // demotes f32→f64 (or drops it) before reaching here, so this is direct misuse. + static_assert(float_exact, + "f32 storage: grid exceeds float's 24-bit significand — use `f64` for the " + "wider range, or `exact`"); + return float{}; // unreachable; fixes the deduced return type + } #endif else if constexpr ((P & bnd::direct) == bnd::direct && G.Notch == 1) return std::conditional_t<(G.Interval.Lower < 0), diff --git a/include/bound/io.hpp b/include/bound/io.hpp index b6efa86..f1fa959 100644 --- a/include/bound/io.hpp +++ b/include/bound/io.hpp @@ -133,10 +133,10 @@ namespace bnd // rational-raw bound has no std::to_string at all.) A continuous (Notch == 0) // real bound prints the double. template - requires (detail::f64_raw || detail::rational_raw) + requires (detail::fp_raw || detail::rational_raw) inline std::string to_string(B b) { - if constexpr (detail::f64_raw && Notch == bnd::detail::rational{0}) + if constexpr (detail::fp_raw && Notch == bnd::detail::rational{0}) return std::to_string(detail::as_double(b)); else return to_string(bnd::detail::as_rational(b)); diff --git a/include/bound/policy_flag.hpp b/include/bound/policy_flag.hpp index 0ae1301..9e26f96 100644 --- a/include/bound/policy_flag.hpp +++ b/include/bound/policy_flag.hpp @@ -53,10 +53,17 @@ namespace bnd // on-grid values are exact in double (see `double_exact`). inline static constexpr policy_flag f64{(1ull << 37) | round_nearest}; + // `f32` — binary32-backed storage (raw held as IEEE-754 float, notch nominal); + // the single-precision sibling of `f64`, for float-only FPUs (Cortex-M4F) and + // the `flt` engine. Power-of-2 notch + dyadic Lower required AND every on-grid + // value must fit float's 24-bit significand (see `float_exact`). Like `f64` it + // is an ordinary round_nearest integer bound under BND_MATH_FIXED. Widest-wins + // storage order: exact > f64 > f32 > direct > indexed > deduced. + inline static constexpr policy_flag f32{(1ull << 41) | round_nearest}; + // `real` — deprecated spelling of `f64`, kept as an alias for one release. New - // code should use `f64` (binary64 storage); a future `f32` brings binary32 - // storage. The flag is purely a storage choice — transcendentals gate on - // `snap`, not on this (see cmath.hpp). + // code should use `f64` (binary64 storage) or `f32` (binary32). The flag is + // purely a storage choice — transcendentals gate on `snap`, not on this. inline static constexpr policy_flag real = f64; // `exact` — force rational raw storage on any grid. Values still obey the grid; diff --git a/single_include/bound/bound.hpp b/single_include/bound/bound.hpp index af3116e..ad6d417 100644 --- a/single_include/bound/bound.hpp +++ b/single_include/bound/bound.hpp @@ -3413,10 +3413,17 @@ namespace bnd // on-grid values are exact in double (see `double_exact`). inline static constexpr policy_flag f64{(1ull << 37) | round_nearest}; + // `f32` — binary32-backed storage (raw held as IEEE-754 float, notch nominal); + // the single-precision sibling of `f64`, for float-only FPUs (Cortex-M4F) and + // the `flt` engine. Power-of-2 notch + dyadic Lower required AND every on-grid + // value must fit float's 24-bit significand (see `float_exact`). Like `f64` it + // is an ordinary round_nearest integer bound under BND_MATH_FIXED. Widest-wins + // storage order: exact > f64 > f32 > direct > indexed > deduced. + inline static constexpr policy_flag f32{(1ull << 41) | round_nearest}; + // `real` — deprecated spelling of `f64`, kept as an alias for one release. New - // code should use `f64` (binary64 storage); a future `f32` brings binary32 - // storage. The flag is purely a storage choice — transcendentals gate on - // `snap`, not on this (see cmath.hpp). + // code should use `f64` (binary64 storage) or `f32` (binary32). The flag is + // purely a storage choice — transcendentals gate on `snap`, not on this. inline static constexpr policy_flag real = f64; // `exact` — force rational raw storage on any grid. Values still obey the grid; @@ -3793,6 +3800,29 @@ namespace bnd template inline constexpr bool double_exact = compute_double_exact(); + // `float`-exactness: the binary32 analogue of double_exact. Every on-grid value + // v = N·2^(−f) must fit float's 24-bit significand (|N| < 2^24) with f ≤ 126 + // (notch ≥ float's smallest normal, so no on-grid value is subnormal). + // Necessary precondition for `f32` (binary32-backed) storage. + template + constexpr bool compute_float_exact() noexcept + { + if constexpr (!dyadic_grid) return false; + else + { + constexpr int f = log2_pow2_mag(abs_den(G.Notch.Denominator)); + if (f > 126) return false; + umax nlo = 0, nhi = 0; + if (!scaled_numerator(G.Interval.Lower, f, nlo)) return false; + if (!scaled_numerator(G.Interval.Upper, f, nhi)) return false; + constexpr umax lim = umax{1} << 24; + return nlo < lim && nhi < lim; + } + } + + template + inline constexpr bool float_exact = compute_float_exact(); + // Storage for a bound: representation flags pick the raw type, widest-wins // (exact > real > direct > indexed > deduced). // exact → rational raw on any grid. @@ -3812,14 +3842,26 @@ namespace bnd return double{}; else if constexpr (((P & bnd::real) == bnd::real) && dyadic_grid) { - // `real` explicitly requested on a dyadic grid double can't represent + // `real`/`f64` explicitly requested on a dyadic grid double can't represent // exactly (max |value·2^f| ≥ 2^53, or notch below the smallest normal). - // Arithmetic drops `real` before reaching here, so this is direct misuse. + // Arithmetic drops the flag before reaching here, so this is direct misuse. static_assert(double_exact, - "real storage: grid exceeds double's 53-bit significand — coarsen the " + "f64 storage: grid exceeds double's 53-bit significand — coarsen the " "notch/range or use `exact`"); return double{}; // unreachable; fixes the deduced return type } + else if constexpr (((P & bnd::f32) == bnd::f32) + && (float_exact || G.Notch == 0)) + return float{}; + else if constexpr (((P & bnd::f32) == bnd::f32) && dyadic_grid) + { + // `f32` requested on a dyadic grid float can't represent exactly. Arithmetic + // demotes f32→f64 (or drops it) before reaching here, so this is direct misuse. + static_assert(float_exact, + "f32 storage: grid exceeds float's 24-bit significand — use `f64` for the " + "wider range, or `exact`"); + return float{}; // unreachable; fixes the deduced return type + } #endif else if constexpr ((P & bnd::direct) == bnd::direct && G.Notch == 1) return std::conditional_t<(G.Interval.Lower < 0), @@ -4043,18 +4085,28 @@ namespace bnd // How a bound's value lives in its raw storage — four disjoint encodings // (selected by policy flags or deduced; see grid.hpp storage_pick): // rational_raw — raw IS the value, as a rational. - // f64_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). + // f64_raw — raw IS the value, as an IEEE-754 double (dyadic grids only). + // f32_raw — raw IS the value, as an IEEE-754 float (dyadic grids only). // value_raw — raw IS the value, as a plain integer. // index_raw — raw is a 0-based notch index; value = Lower + raw*Notch. template inline constexpr bool f64_raw = std::is_same_v, double>; + template + inline constexpr bool f32_raw = std::is_same_v, float>; + + // fp_raw — value held directly in a floating-point raw (f64 or f32). These + // share every value-path branch: read/store/compare/arithmetic compute in + // double, narrowing to the raw type on store (lossless on an fp-exact grid). + template + inline constexpr bool fp_raw = f64_raw || f32_raw; + template inline constexpr bool rational_raw = std::is_same_v, rational>; template inline constexpr bool value_raw = - !f64_raw && !rational_raw + !fp_raw && !rational_raw && ((BoundPolicy & bnd::direct) == bnd::direct || ((BoundPolicy & bnd::indexed) != bnd::indexed && Notch == 1 @@ -4062,7 +4114,7 @@ namespace bnd template inline constexpr bool index_raw = - !f64_raw && !rational_raw && !value_raw; + !fp_raw && !rational_raw && !value_raw; // Ungated double view of any bound, for the `real` arithmetic arms (the // public operator double() is gated on a rounding flag; this is always @@ -4729,7 +4781,7 @@ namespace bnd::detail // or NotchCount, no rounding. real takes the endpoint as a double, rational // the exact constant (a double round-trip would lose non-dyadic endpoints); // raw_from_offset adds Lower back for direct-encoded storage. - if constexpr (f64_raw) + if constexpr (fp_raw) lhs = L::from_raw((rhs < Lower) ? static_cast(Lower) : static_cast(Upper)); else if constexpr (rational_raw) @@ -4775,7 +4827,7 @@ namespace bnd::detail { if constexpr (rational_raw && Notch == 0) { lhs = L::from_raw(rhs); return true; } // continuous: store verbatim - else if constexpr (f64_raw) + else if constexpr (fp_raw) { // real target: raw IS the value — snap to the dyadic grid (range handling // already ran in the assign cascade; finite guard mirrors store_f64's). @@ -4818,7 +4870,7 @@ namespace bnd::detail // instead of two rational ops. round_quotient is invariant under reduction, // so the slot is bit-identical to the rational path. Oversized denominators // fall through (the kMaxDen guard keeps every product inside imax). - if constexpr (HasQFormatFastPath && !f64_raw && Notch != 0) + if constexpr (HasQFormatFastPath && !fp_raw && Notch != 0) { constexpr imax K = abs_den(Notch.Denominator); constexpr imax Lo = LowerImax; @@ -4911,7 +4963,7 @@ namespace bnd::detail if constexpr (rational_raw) return Lower; else if constexpr (Notch == 0) - // Continuous f64_raw L: no grid to land on, mapping unused (store + // Continuous fp_raw L: no grid to land on, mapping unused (store // routes through snap_double). 0 avoids the /Notch divide-by-zero. return rational{0}; else if constexpr (rational_raw) @@ -4925,7 +4977,7 @@ namespace bnd::detail if constexpr (rational_raw) return Notch; else if constexpr (Notch == 0) - // Continuous f64_raw L (see calcOffset). A denominator-1 Factor also + // Continuous fp_raw L (see calcOffset). A denominator-1 Factor also // makes assign_notch_ok vacuously true (any value representable). return rational{0}; else if constexpr (rational_raw) @@ -4942,7 +4994,7 @@ namespace bnd::detail // sides (not rational, not real). static constexpr bool is_integer_mapping = !rational_raw && !rational_raw - && !f64_raw && !f64_raw + && !fp_raw && !fp_raw && abs_den(Factor.Denominator) == 1 && abs_den(Offset.Denominator) == 1; // Map rhs.Raw into L's raw space (requires is_integer_mapping). The @@ -4981,7 +5033,7 @@ namespace bnd::detail { // RawLo/RawHi are already the correct Raw (no raw_from_offset). Real storage // takes the endpoint as a double (RawLo/Hi truncate fractional dyadic endpoints). - if constexpr (f64_raw) + if constexpr (fp_raw) lhs = L::from_raw((as_rational(rhs) < Lower) ? static_cast(Lower) : static_cast(Upper)); else @@ -5050,7 +5102,7 @@ namespace bnd::detail template static constexpr void store(L& lhs, R const& rhs, P&&) { - if constexpr (f64_raw) + if constexpr (fp_raw) // real target: raw IS the value — decode the source and snap to the dyadic // grid (the offset machinery below mis-encodes a double raw). lhs = L::from_raw(Grid.snap_double(as_double(rhs))); @@ -5514,16 +5566,22 @@ namespace bnd::detail "addition: result grid's notch/interval exceeds the representable rational " "range — coarsen the operand grids"); static constexpr grid result_grid = (Grid + Grid).value(); - // Propagate `real` only when the result grid stays exactly representable in - // double; otherwise drop it so storage_pick deduces an exact representation - // (the double sum would diverge from the exact sum — see grid::double_exact). - static constexpr bool any_real = + // Propagate fp storage only when the result grid stays exactly representable + // in the chosen width; otherwise demote (f32→f64) or drop it so storage_pick + // deduces an exact representation (the fp sum would diverge from the exact sum + // — see grid::double_exact / float_exact). Widest-wins: prefer f32 only when + // both operands are f32-only and the result fits float; an f64 operand or a + // too-fine-for-float result widens to f64; too fine for double → exact. + static constexpr bool any_f64 = (BoundPolicy & bnd::real) == bnd::real || (BoundPolicy & bnd::real) == bnd::real; - static constexpr bool keep_real = any_real && double_exact; + static constexpr bool any_f32 = + (BoundPolicy & bnd::f32) == bnd::f32 || (BoundPolicy & bnd::f32) == bnd::f32; + static constexpr bool keep_f32 = any_f32 && !any_f64 && float_exact; + static constexpr bool keep_f64 = !keep_f32 && (any_f64 || any_f32) && double_exact; // Carry both operands' representation flags (widest-wins at storage selection). static constexpr policy_flag rep = ((BoundPolicy | BoundPolicy) & (bnd::exact | bnd::direct | bnd::indexed)) - | (keep_real ? bnd::real : none); + | (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none); using result = bound; template @@ -5558,9 +5616,9 @@ namespace bnd::detail static constexpr auto add(L lhs, R rhs, policy policy = {}, A&& action = {}) -> add_return_t { result res; - if constexpr (f64_raw) + if constexpr (fp_raw) { - res = result::from_raw(Grid.snap_double(as_double(lhs) + as_double(rhs))); + res = result::from_raw(raw_cast(Grid.snap_double(as_double(lhs) + as_double(rhs)))); } else if constexpr (rational_raw) { @@ -5628,25 +5686,31 @@ namespace bnd::detail "multiplication: result grid's notch/interval exceeds the representable " "rational range — coarsen the operand grids"); static constexpr grid result_grid = (Grid * Grid).value(); - static constexpr bool any_real = + // fp storage propagation (see addition.hpp): the product grid (notch = N_L·N_R) + // is finer, so demote f32→f64 / drop f64 when it outgrows the width — the fp + // product would round below the result notch. Widest-wins: f32 only if both + // operands f32-only and product fits float; else widen to f64; else exact. + static constexpr bool any_f64 = (BoundPolicy & bnd::real) == bnd::real || (BoundPolicy & bnd::real) == bnd::real; - // Drop `real` when the product grid (notch = N_L·N_R) outgrows double's - // 53-bit significand — the double product would round below the result notch. - static constexpr bool keep_real = any_real && double_exact; + static constexpr bool any_f32 = + (BoundPolicy & bnd::f32) == bnd::f32 || (BoundPolicy & bnd::f32) == bnd::f32; + static constexpr bool keep_f32 = any_f32 && !any_f64 && float_exact; + static constexpr bool keep_f64 = !keep_f32 && (any_f64 || any_f32) && double_exact; + static constexpr bool dropped_fp = (any_f64 || any_f32) && !keep_f64 && !keep_f32; // Carry both operands' representation flags (widest-wins at storage selection). static constexpr policy_flag rep = ((BoundPolicy | BoundPolicy) & (bnd::exact | bnd::direct | bnd::indexed)) - | (keep_real ? bnd::real : none); + | (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none); using result = bound; - // The dropped-`real` case (any_real && !keep_real) lands on a rational result - // when the product grid outgrows uint index space; its product numerator can - // exceed `umax`, so check it (the result carries `checked`) rather than wrap. + // The dropped-fp case lands on a rational result when the product grid outgrows + // uint index space; its product numerator can exceed `umax`, so check it (the + // result carries `checked`) rather than wrap. template static constexpr bool needs_overflow_check = rational_raw && (((BoundPolicy | BoundPolicy) & checked) || plain

::test(checked) - || (any_real && !keep_real)) + || dropped_fp) && !rational_mul_is_safe(Grid, Grid); template @@ -5664,9 +5728,9 @@ namespace bnd::detail template static constexpr auto mul(L lhs, R rhs, P&& policy, A&& action = {}) -> mul_return_t { - if constexpr (f64_raw) + if constexpr (fp_raw) { - return result::from_raw(Grid.snap_double(as_double(lhs) * as_double(rhs))); + return result::from_raw(raw_cast(Grid.snap_double(as_double(lhs) * as_double(rhs)))); } else if constexpr (rational_raw) { @@ -5688,7 +5752,7 @@ namespace bnd::detail from_value(res, to_value(lhs) * to_value(rhs)); return res; } - else if constexpr (f64_raw || f64_raw || rational_raw || rational_raw) + else if constexpr (fp_raw || fp_raw || rational_raw || rational_raw) { // An operand whose raw is a double/rational can't feed the integer // four-quadrant formula below (it reads the raw as an integer offset). @@ -5910,17 +5974,21 @@ namespace bnd::detail ? grid{interval{rational{0}, (Upper / Notch).value()}, Notch} : *(Grid / Grid); - static constexpr bool any_real = + static constexpr bool any_f64 = (BoundPolicy & bnd::real) == bnd::real || (BoundPolicy & bnd::real) == bnd::real; - // Keep `real` for a continuous result (Notch 0: double stores the quotient - // verbatim) or a double-exact dyadic result; otherwise drop it (the double - // quotient would not land on the result grid). See grid::double_exact. - static constexpr bool keep_real = - any_real && (result_grid.Notch == 0 || double_exact); + static constexpr bool any_f32 = + (BoundPolicy & bnd::f32) == bnd::f32 || (BoundPolicy & bnd::f32) == bnd::f32; + // Keep fp for a continuous result (Notch 0: the raw stores the quotient + // verbatim) or an fp-exact dyadic result; otherwise demote f32→f64 / drop f64 + // (the fp quotient would not land on the result grid). Widest-wins as in mul. + static constexpr bool keep_f32 = + any_f32 && !any_f64 && (result_grid.Notch == 0 || float_exact); + static constexpr bool keep_f64 = + !keep_f32 && (any_f64 || any_f32) && (result_grid.Notch == 0 || double_exact); // Carry both operands' representation flags (widest-wins at storage selection). static constexpr policy_flag rep = ((BoundPolicy | BoundPolicy) & (bnd::exact | bnd::direct | bnd::indexed)) - | (keep_real ? bnd::real : none); + | (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none); using result = bound; template @@ -5931,7 +5999,7 @@ namespace bnd::detail // (overflow). So when the divisor excludes zero AND this is false, `div` // returns a plain `result` rather than optional. static constexpr bool may_overflow_nonzero = - !native_div && !f64_raw && (needs_overflow_check != 0); + !native_div && !fp_raw && (needs_overflow_check != 0); // Real division can still fail on a zero divisor, so it uses the same // return-type rule as the rest: plain `result` when the op cannot fail @@ -5974,14 +6042,14 @@ namespace bnd::detail [[maybe_unused]] constexpr bool zero_unchecked = DivisorExcludesZero || (((G | F | BoundPolicy | BoundPolicy) & ignore_zero) != 0); - if constexpr (f64_raw) + if constexpr (fp_raw) { // Real division reports zero like every other path (throw / report / // action / nullopt). Finite operands keep the quotient finite, so no // non-finite ever reaches storage. if constexpr (!zero_unchecked) if (as_double(rhs) == 0.0) return fail(errc::division_by_zero, "division by zero in div"); - return result::from_raw(Grid.snap_double(as_double(lhs) / as_double(rhs))); + return result::from_raw(raw_cast(Grid.snap_double(as_double(lhs) / as_double(rhs)))); } else if constexpr (native_div_qformat) { @@ -6182,8 +6250,12 @@ namespace bnd // no grid to snap to. Anything else is rejected here rather than silently // demoted to integer storage. static_assert(!has_flag(P, real) || detail::dyadic_grid || G.Notch == 0, - "bnd: the `real` policy requires a dyadic grid (power-of-two " + "bnd: the `real`/`f64` policy requires a dyadic grid (power-of-two " "notch and Lower, so values are exactly representable in double)"); + static_assert(!has_flag(P, f32) || detail::dyadic_grid || G.Notch == 0, + "bnd: the `f32` policy requires a dyadic grid (power-of-two notch " + "and Lower); values must also fit float's 24-bit significand " + "(checked at storage selection — see `float_exact`)"); #endif // Representation flags vs grid shape (exact has no requirement; a result // policy may carry several flags — storage selection resolves widest-wins, @@ -6218,8 +6290,9 @@ namespace bnd // Value-init `bound{}` still zero-fills where a zero raw is genuinely wanted.) constexpr bound() = default; - // `real` storage holds the value as a double directly. An arithmetic rhs - // casts straight to double; a bound rhs goes through its exact rational view. + // fp storage (f64/f32) holds the value as a floating raw directly. An + // arithmetic rhs casts straight to double; a bound rhs goes through its exact + // rational view. private: template constexpr double to_double(A const& value) @@ -6228,10 +6301,12 @@ namespace bnd else return static_cast(detail::as_rational(value)); } public: - // Snap a value onto `real` storage: lossless on the dyadic grid. Out-of-range - // values run the same policy cascade as the fractional path (clamp → wrap → - // sentinel/checked-report → store as-is); all arithmetic stays in double. - constexpr void store_f64(double v) + // Snap a value onto fp storage: lossless on the (fp-exact) dyadic grid — the + // snap is computed in double and narrowed to the raw type (double or float), + // which is exact because every grid point fits the raw's significand. Out-of- + // range values run the same policy cascade as the fractional path (clamp → + // wrap → sentinel/checked-report → store as-is). + constexpr void store_fp(double v) { // NaN/±inf would reach snap_double's integer cast (UB); reject like the // non-real path. `v - v` is 0 for every finite v, NaN otherwise. @@ -6267,20 +6342,20 @@ namespace bnd return; // sentinel stored / reported (error_code mode) // no handler (unchecked policy): fall through and store snapped as-is } - Raw = G.snap_double(v); + Raw = static_cast(G.snap_double(v)); // narrow to float for f32 (lossless) } template constexpr void store_value(A const& value) { - if constexpr (detail::f64_raw) - store_f64(to_double(value)); + if constexpr (detail::fp_raw) + store_fp(to_double(value)); else if constexpr (is_bound_v) { // A `real` SOURCE holds its value as a double raw; the assignment engine's // integer offset formula (Lower + raw·Notch) would misread it. Extract as // a double and route through the arithmetic-source path. - if constexpr (detail::f64_raw) + if constexpr (detail::fp_raw) detail::assignment::assign(*this, detail::as_double(value), make_policy

()); else detail::assignment::assign(*this, value, make_policy

()); @@ -6301,8 +6376,8 @@ namespace bnd // The one-shot `pol` widens the assignable check (a clamp/round passed here // relaxes the notch/interval clause), so a notch-incompatible boundable source // is accepted — e.g. clamp_round(some_bound). Body honours `pol` as before. - if constexpr (detail::f64_raw) - store_f64(to_double(value)); + if constexpr (detail::fp_raw) + store_fp(to_double(value)); else detail::assignment::assign(*this, value, pol); } @@ -6315,8 +6390,8 @@ namespace bnd requires bound_assignable constexpr bound(A value, errc& ec) { - if constexpr (detail::f64_raw) - store_f64(to_double(value)); + if constexpr (detail::fp_raw) + store_fp(to_double(value)); else detail::assignment::assign(*this, value, make_policy

(ec)); } @@ -6423,7 +6498,7 @@ namespace bnd && G.Interval.Upper <= bnd::detail::rational{std::numeric_limits::max()}) { return detail::to_value(*this); } - constexpr explicit(!has_flag(P, real)) operator double() const + constexpr explicit(!has_flag(P, real) && !has_flag(P, f32)) operator double() const requires ((P & (round_floor | round_ceil | round_nearest | round_half_even | snap)) != 0) { return detail::as_double(*this); } @@ -6538,7 +6613,7 @@ namespace bnd [[nodiscard]] constexpr negative operator-() const { negative neg; - if constexpr (detail::f64_raw) + if constexpr (detail::fp_raw) neg = negative::from_raw(-Raw); else if constexpr (detail::rational_raw) neg = negative::from_raw(-(Raw)); @@ -6811,7 +6886,7 @@ namespace bnd if constexpr (Grid == Grid) return lhs.raw() <=> rhs.raw(); // double-backed (`real`) operand: compare in double (raw_imax would truncate) - else if constexpr (detail::f64_raw || detail::f64_raw) + else if constexpr (detail::fp_raw || detail::fp_raw) return detail::as_double(lhs) <=> detail::as_double(rhs); // both integer-direct (notch=1, Raw==value): compare as integers else if constexpr (!detail::rational_raw && !detail::rational_raw @@ -6826,7 +6901,7 @@ namespace bnd { if constexpr (Grid == Grid) return lhs.raw() == rhs.raw(); - else if constexpr (detail::f64_raw || detail::f64_raw) + else if constexpr (detail::fp_raw || detail::fp_raw) return detail::as_double(lhs) == detail::as_double(rhs); else if constexpr (!detail::rational_raw && !detail::rational_raw && !detail::index_raw && !detail::index_raw) @@ -7924,9 +7999,10 @@ namespace bnd::math::dbl template [[nodiscard]] BND_DBL_FN Out store(double d) { - // `real` (double-backed) Out stores the double directly; a non-`real` snap grid - // assigns through the rational path, snapping via Out's round policy. - if constexpr (has_flag(BoundPolicy, real)) return Out{d}; + // An fp-backed Out (f64 or f32) stores the value directly via its raw (an f32 + // Out narrows double→float, lossless on its float-exact grid); a non-fp snap + // grid assigns through the rational path, snapping via Out's round policy. + if constexpr (bnd::detail::fp_raw) return Out{d}; else { Out o{}; o = bnd::detail::rational{d}; return o; } } @@ -8195,14 +8271,14 @@ namespace bnd::math::flt::detail namespace bnd::math::flt { // Engine cores: bound in → `float` math → bound out. Storing the float result: - // a `real` (double-backed) bound takes Out{double(f)} (widening is exact); a - // non-`real` snap grid assigns through the rational path, snapping via Out's - // round policy. (An f32-backed bound — Phase 4b — will store the float raw - // directly; until then it routes through the same paths.) + // an fp-backed Out (f32 OR f64) stores the value directly via its float/double + // raw (the natural pairing for `flt` is `f32` — no rational, no double round- + // trip on the result); any other snap grid assigns through the rational path, + // snapping via Out's round policy. template [[nodiscard]] BND_DBL_FN Out store(float f) { - if constexpr (has_flag(BoundPolicy, real)) return Out{static_cast(f)}; + if constexpr (bnd::detail::fp_raw) return Out{static_cast(f)}; else { Out o{}; o = bnd::detail::rational{static_cast(f)}; return o; } } @@ -8492,7 +8568,7 @@ namespace bnd::math && !rational_raw // `real` storage holds the VALUE, not an offset index, so route it // through the rational fallback `Out{r}` (same guard as fmod_int_fast). - && !f64_raw + && !fp_raw && has_flag(BoundPolicy, round_nearest) && (std::signed_integral> || NotchCount @@ -9143,9 +9219,9 @@ namespace bnd::math // * all unit counts fit comfortably in imax (headroom 4). template inline constexpr bool fmod_int_fast = []{ - if (rational_raw || f64_raw - || rational_raw || f64_raw - || rational_raw || f64_raw) + if (rational_raw || fp_raw + || rational_raw || fp_raw + || rational_raw || fp_raw) return false; if (Notch == 0 || Notch == 0 || Notch == 0) return false; @@ -10897,10 +10973,10 @@ namespace bnd // rational-raw bound has no std::to_string at all.) A continuous (Notch == 0) // real bound prints the double. template - requires (detail::f64_raw || detail::rational_raw) + requires (detail::fp_raw || detail::rational_raw) inline std::string to_string(B b) { - if constexpr (detail::f64_raw && Notch == bnd::detail::rational{0}) + if constexpr (detail::fp_raw && Notch == bnd::detail::rational{0}) return std::to_string(detail::as_double(b)); else return to_string(bnd::detail::as_rational(b)); diff --git a/tests/test_storage_flags.cpp b/tests/test_storage_flags.cpp index 06038ac..a6b4830 100644 --- a/tests/test_storage_flags.cpp +++ b/tests/test_storage_flags.cpp @@ -135,6 +135,53 @@ TEST_CASE("representation flags resolve widest-wins", "[storage][policy]") REQUIRE(DI{42}.raw() == 42); } +#ifndef BND_MATH_FIXED +TEST_CASE("f32 selects binary32-backed storage; arithmetic demotes when too fine", + "[storage][f32]") +{ + using F = bound<{{-8, 8}, notch<1, 256>}, round_nearest | f32>; + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(detail::f32_raw); + STATIC_REQUIRE(detail::fp_raw && !detail::f64_raw); + + // Construct/read are lossless on the float-exact grid. + F a{rational{3, 2}}; + REQUIRE(static_cast(a) == 1.5); + REQUIRE(rational{a + F{rational{1, 4}}} == rational{7, 4}); // 1.75 + REQUIRE(rational{a * F{2}} == 3); + + // f32 ⊕ f32 stays f32 while the result grid still fits float... + using Sum = decltype(F{} + F{}); // notch 1/256, |v|≤16 + STATIC_REQUIRE(detail::f32_raw); + + // ...but a product whose grid outgrows float's 24-bit significand demotes to f64 + // (notch 1/65536, |v|≤2^16 → 2^32 > 2^24, but < 2^53). + using Wide = bound<{{-256, 256}, notch<1, 256>}, round_nearest | f32>; + using WideProd = decltype(Wide{} * Wide{}); + STATIC_REQUIRE(detail::f64_raw); + + // Mixing f32 with f64 widens to f64; exact still beats both. + using D = bound<{{-8, 8}, notch<1, 256>}, round_nearest | f64>; + STATIC_REQUIRE(detail::f64_raw); + using E = bound<{{-8, 8}, notch<1, 256>}, exact>; + STATIC_REQUIRE(detail::rational_raw); +} + +TEST_CASE("math output lands in f32 storage (flt engine pairs with f32)", + "[storage][f32][cmath]") +{ + using Ang = bound<{{-8, 8}, notch<1, 256>}, round_nearest | f32>; + using Sq = bound<{{0, 16}, notch<1, 256>}, round_nearest | f32>; + // The float engine stores its result straight into the f32 raw (no rational). + auto s = math::flt::sin(Ang{0}); + auto r = math::flt::sqrt(Sq{4}); + STATIC_REQUIRE(detail::f32_raw); + STATIC_REQUIRE(detail::f32_raw); + REQUIRE(rational{s} == 0); + REQUIRE(rational{r} == 2); +} +#endif // !BND_MATH_FIXED + TEST_CASE("f64 is the canonical double-backed flag; real is its alias", "[storage][f64]") {