From 36c81ee13b0875aba628c8cfdada879da7bce823 Mon Sep 17 00:00:00 2001 From: Peter Neiss Date: Mon, 22 Jun 2026 21:44:48 +0200 Subject: [PATCH] =?UTF-8?q?math:=20auto-widen=20f32=E2=86=92f64=20storage?= =?UTF-8?q?=20when=20a=20grid=20overflows=20binary32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A deduced cmath output inherits the operand's storage flag. With f32 that could land on a result grid too fine for binary32 (e.g. exp of a large argument: e^20 ≈ 4.85e8 > 2^24), which hard-errored in storage_pick. Now storage_pick WIDENS such a grid to binary64: an `f32` grid that float can't represent exactly but double can stores its raw as `double`. The value stays exact; the f32 policy bit is harmless (storage is raw-driven via fp_raw). Only a grid too fine for double as well is a hard error (`exact` is the escape hatch). This covers the deduced-output case the request was about (a too-fine f32 cmath result widens instead of failing to compile) and, uniformly, a direct f32 grid that needs the wider type — "give me float, or the next best thing it fits in." Done in storage_pick (the one gcc-proven place that already had the f32 clauses) rather than at the type level: an earlier attempt computed a demoted policy via a constexpr function over the grid NTTP in a using-alias, which ICEs GCC 12/13 (cxx_eval_bare_aggregate). Reproduced with g++-13 on test_cmath_double and confirmed the storage_pick approach compiles clean on g++-13/14 and C++20. Test: f32 exp with an over-float output grid deduces f64 storage (no error) and computes correctly; small-output f32 stays f32 (test_storage_flags.cpp). Docs updated. Verified: default 407/407, CORDIC 443/443, FLOAT 398/398; no ICE on gcc-13/14. Co-Authored-By: Claude Opus 4.8 --- docs/math.md | 7 +++++-- include/bound/grid.hpp | 24 +++++++++++++++++++----- single_include/bound/bound.hpp | 24 +++++++++++++++++++----- tests/test_storage_flags.cpp | 8 ++++++++ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/docs/math.md b/docs/math.md index 703c871..c109431 100644 --- a/docs/math.md +++ b/docs/math.md @@ -282,8 +282,11 @@ 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`. +significand, an `f32` grid too fine for `float` but representable in `double` +**auto-widens its storage to `f64`** (the value stays exact) — so a deduced `f32` +output whose grid overflows binary32 (e.g. `exp` of a large argument on a fine +grid) stores its result in `double` instead of failing to compile. Only a grid +too fine for `double` as well is a hard error (`exact` is the escape hatch). ## Compiling without floating point (`BND_MATH_NO_FP`) diff --git a/include/bound/grid.hpp b/include/bound/grid.hpp index c2bc5f3..903aa06 100644 --- a/include/bound/grid.hpp +++ b/include/bound/grid.hpp @@ -255,6 +255,14 @@ namespace bnd template inline constexpr bool float_exact = compute_float_exact(); + // Demote an fp STORAGE flag a result grid can't represent — for DEDUCED policies + // (cmath auto-outputs, which inherit the operand's storage flag), so a deduced + // f32 output whose grid overflows binary32 silently widens instead of hard- + // erroring. (A grid a user spells `f32` on directly still static_asserts in + // storage_pick — that's deliberate misuse, not deduction.) f32 needs float_exact, + // f64 needs double_exact (Notch == 0 continuous fits either). When the flag + // doesn't fit: widen f32→f64 if double holds the grid, else drop the fp flag so + // storage is deduced. The snap/round bits are preserved. // Storage for a bound: representation flags pick the raw type, widest-wins // (exact > real > direct > indexed > deduced). // exact → rational raw on any grid. @@ -285,13 +293,19 @@ namespace bnd else if constexpr (((P & bnd::f32) == bnd::f32) && (float_exact || G.Notch == 0)) return float{}; + else if constexpr (((P & bnd::f32) == bnd::f32) && double_exact) + // `f32` requested on a grid too fine for float but representable in double: + // WIDEN the storage to binary64. This makes a deduced f32 output (a cmath + // result inheriting the operand's flag) whose grid overflows float store its + // value in double rather than hard-erroring — the value stays exact. The f32 + // POLICY bit remains (harmless; storage is raw-driven via fp_raw). + return double{}; 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`"); + // Too fine for double too → genuinely unrepresentable as fp storage. + static_assert(double_exact, + "f32 storage: grid exceeds double's 53-bit significand — coarsen the " + "notch/range or use `exact`"); return float{}; // unreachable; fixes the deduced return type } #endif diff --git a/single_include/bound/bound.hpp b/single_include/bound/bound.hpp index ad6d417..6c2ec7f 100644 --- a/single_include/bound/bound.hpp +++ b/single_include/bound/bound.hpp @@ -3823,6 +3823,14 @@ namespace bnd template inline constexpr bool float_exact = compute_float_exact(); + // Demote an fp STORAGE flag a result grid can't represent — for DEDUCED policies + // (cmath auto-outputs, which inherit the operand's storage flag), so a deduced + // f32 output whose grid overflows binary32 silently widens instead of hard- + // erroring. (A grid a user spells `f32` on directly still static_asserts in + // storage_pick — that's deliberate misuse, not deduction.) f32 needs float_exact, + // f64 needs double_exact (Notch == 0 continuous fits either). When the flag + // doesn't fit: widen f32→f64 if double holds the grid, else drop the fp flag so + // storage is deduced. The snap/round bits are preserved. // Storage for a bound: representation flags pick the raw type, widest-wins // (exact > real > direct > indexed > deduced). // exact → rational raw on any grid. @@ -3853,13 +3861,19 @@ namespace bnd else if constexpr (((P & bnd::f32) == bnd::f32) && (float_exact || G.Notch == 0)) return float{}; + else if constexpr (((P & bnd::f32) == bnd::f32) && double_exact) + // `f32` requested on a grid too fine for float but representable in double: + // WIDEN the storage to binary64. This makes a deduced f32 output (a cmath + // result inheriting the operand's flag) whose grid overflows float store its + // value in double rather than hard-erroring — the value stays exact. The f32 + // POLICY bit remains (harmless; storage is raw-driven via fp_raw). + return double{}; 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`"); + // Too fine for double too → genuinely unrepresentable as fp storage. + static_assert(double_exact, + "f32 storage: grid exceeds double's 53-bit significand — coarsen the " + "notch/range or use `exact`"); return float{}; // unreachable; fixes the deduced return type } #endif diff --git a/tests/test_storage_flags.cpp b/tests/test_storage_flags.cpp index a6b4830..c24b18d 100644 --- a/tests/test_storage_flags.cpp +++ b/tests/test_storage_flags.cpp @@ -179,6 +179,14 @@ TEST_CASE("math output lands in f32 storage (flt engine pairs with f32)", STATIC_REQUIRE(detail::f32_raw); REQUIRE(rational{s} == 0); REQUIRE(rational{r} == 2); + + // Auto-demote: an f32 input whose result grid overflows binary32 (exp's range + // e^20 ≈ 4.85e8 > 2^24, still < 2^53) widens its OUTPUT to f64 storage rather + // than hard-erroring — the deduced output never static_asserts on f32 overflow. + using Big = bound<{{0, 20}, notch<1, 256>}, round_nearest | f32>; + auto e = math::flt::exp(Big{2}); + STATIC_REQUIRE(detail::f64_raw); // demoted f32 → f64 + REQUIRE(rational{e} > rational{7}); // ≈ 7.39 } #endif // !BND_MATH_FIXED