From 8371077132efed14bdc4a92dce85fa8e12a011ba Mon Sep 17 00:00:00 2001 From: Peter Neiss Date: Sun, 21 Jun 2026 19:27:48 +0200 Subject: [PATCH] math: gate transcendentals on `snap`, not `real` (allow non-real grids) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A transcendental result is rounded onto the operand's grid, so the real requirement is `snap` (rounding permission), not `real` (double storage). - cmath.hpp: require_real → require_snap (has_flag(.., snap)) at all 22 sites. Backward compatible: `real` ⊃ round_nearest ⊃ snap, so existing code is unchanged. - New capability: transcendentals now work on any snap-capable grid — integer grids and non-dyadic grids (e.g. notch<1,100>) — not just `real` ones. - cmath_double.hpp: the double engine assumed `real` I/O (implicit bound→double, which is explicit for non-real). Add `dbl::store(double)` (direct store for `real`, rational-snap for non-real) and route every *_core + the inline sqrt/pow/pow_base/tan #else sites through it with explicit `static_cast` inputs. Make cmath_double.hpp self-contained (include bound.hpp). - Tests: test_math_snap_grids.cpp pins exact special values on non-real integer and 1/100 grids (engine-agnostic). docs/math.md updated. Single header regen. Local: double 362/362, CORDIC 405/405. Co-Authored-By: Claude Opus 4.8 --- docs/math.md | 37 ++++++----- include/bound/cmath.hpp | 67 ++++++++++---------- include/bound/cmath_double.hpp | 44 ++++++++----- single_include/bound/bound.hpp | 110 ++++++++++++++++++--------------- tests/test_math_snap_grids.cpp | 51 +++++++++++++++ 5 files changed, 195 insertions(+), 114 deletions(-) create mode 100644 tests/test_math_snap_grids.cpp diff --git a/docs/math.md b/docs/math.md index 329cde3..c4e68c5 100644 --- a/docs/math.md +++ b/docs/math.md @@ -44,20 +44,29 @@ auto s = math::sin(angle{1}); // amplitude bound in [-1, 1] auto h = math::hypot(s, s); // √(s²+s²), output grid auto-deduced ``` -## The `real` policy requirement - -Every transcendental operand must carry the **`real` representation flag** -(`bound`); omitting it is a compile error. `real` -marks a bound as a math operand: - -- Under the default engine it selects **double-backed storage** on the - bound's grid — the raw *is* the value, so input marshalling into the - engine is free (this is where the large speedup over integer-index I/O - comes from). Values still obey the grid: they snap to the notch on store. - Out-of-range stores run the same policy cascade as every other bound - (clamp / wrap / sentinel / checked report). -- Under `BND_MATH_FIXED` the same flag is an ordinary `round_nearest` - integer-backed bound — the source compiles unchanged. +## The `snap` requirement (and `real` as a fast storage option) + +A transcendental result is irrational and must be **rounded onto the operand's +grid**, so every transcendental operand must carry a policy that **permits +rounding** — i.e. the **`snap`** bit (`snap`, any `round_*` mode, or `real`, +which implies `round_nearest`). Omitting it is a compile error. This is the only +hard requirement: `math::sin` etc. work on **any snap-capable grid**, including +plain integer grids and non-dyadic ones (e.g. a `notch<1,100>` money grid) — +the value is computed by the engine and snapped to the grid via exact rational +rounding. + +`real` is **not** required — it is an optional **storage** flag that buys speed: + +- Under the default engine `real` selects **double-backed storage** on the + bound's grid — the raw *is* the value, so input marshalling into the engine is + free (the large speedup over integer-index I/O). Values still obey the grid: + they snap to the notch on store. Out-of-range stores run the usual policy + cascade (clamp / wrap / sentinel / checked report). +- Without `real`, a snap-capable grid still works — the engine's `double`/integer + result is snapped to the grid through the assignment path (a touch slower; no + double fast path). Use `real` when the grid is dyadic and you want the speed. +- Under `BND_MATH_FIXED` `real` is an ordinary `round_nearest` integer-backed + bound — the source compiles unchanged. - `real` requires a grid that is **exactly representable in `double`**: dyadic (power-of-two notch and Lower) **and** within the 53-bit significand — writing a value as `N·2^(−f)` with `f = log2(notch denominator)`, every on-grid value diff --git a/include/bound/cmath.hpp b/include/bound/cmath.hpp index b40ea15..5ac9f25 100644 --- a/include/bound/cmath.hpp +++ b/include/bound/cmath.hpp @@ -74,11 +74,12 @@ namespace bnd::math // and avoids the slow integer-I/O path. Pure grid ops (abs/floor/ceil/round/ // trunc/fmod) have no engine and don't require it. template - consteval bool require_real() noexcept + consteval bool require_snap() noexcept { - static_assert(has_flag(BoundPolicy, real), - "bnd::math: transcendental operand must carry the `real` policy — " - "declare it as bound (or add `| real` to its policy)."); + static_assert(has_flag(BoundPolicy, snap), + "bnd::math: a transcendental result is rounded onto the grid — its " + "operand must permit rounding. Declare it with `round_nearest` (or " + "`snap` / a `round_*` mode / `real`)."); return true; } } @@ -1175,7 +1176,7 @@ namespace bnd::math requires (Lower == bnd::detail::rational{0}) [[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return sqrt_impl>(x); #else @@ -1190,22 +1191,22 @@ namespace bnd::math requires (Lower < bnd::detail::rational{0}) [[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); using Out = detail::sqrt_signed_auto_t; #ifdef BND_MATH_FIXED return sqrt_signed_impl(x); #else - double v = x; + double v = static_cast(x); if (v < 0.0) return slim::expected{slim::unexpected(errc::domain_error)}; - return slim::expected{Out{dbl::detail::d_sqrt(v)}}; + return slim::expected{dbl::store(dbl::detail::d_sqrt(v))}; #endif } template [[nodiscard]] BND_MATH_FN auto exp2(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return exp2_impl>(x); #else @@ -1216,7 +1217,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto log2(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); // Domain guard belongs on the shared entry point, not just the fixed // engine's *_impl: the double engine's log_core has no singularity check, // so log2(x<=0) would silently store finite garbage (e.g. log2(0) ≈ -7). @@ -1231,7 +1232,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto exp(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return exp_impl>(x); #else @@ -1242,7 +1243,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto log(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log: input must be strictly positive"); #ifdef BND_MATH_FIXED return log_impl>(x); @@ -1254,12 +1255,12 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto pow_base(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); using Out = detail::pow_base_auto_t; #ifdef BND_MATH_FIXED return pow_base_impl(x); #else - return Out{dbl::detail::d_pow(static_cast(Base), x)}; + return dbl::store(dbl::detail::d_pow(static_cast(Base), static_cast(x))); #endif } @@ -1306,7 +1307,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto sin(In angle) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return sin_impl>(angle); #else @@ -1317,7 +1318,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto cos(In angle) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return cos_impl>(angle); #else @@ -1328,7 +1329,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto atan2(In y, In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return atan2_impl>(y, x); #else @@ -1339,12 +1340,12 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto tan(In angle) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); using Out = detail::tan_auto_t; #ifdef BND_MATH_FIXED return tan_impl(angle); #else - double x = angle; + double x = static_cast(angle); double c = dbl::detail::d_cos(x); if (c == 0.0) return slim::expected{slim::unexpected(errc::division_by_zero)}; @@ -1352,7 +1353,7 @@ namespace bnd::math if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below if (t < static_cast(Lower) || t > static_cast(Upper)) return slim::expected{slim::unexpected(errc::overflow)}; - return slim::expected{Out{t}}; + return slim::expected{dbl::store(t)}; #endif } @@ -1840,7 +1841,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto atan(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return atan_impl>(x); #else @@ -1851,7 +1852,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto asin(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return asin_impl>(x); #else @@ -1862,7 +1863,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto acos(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return acos_impl>(x); #else @@ -1873,7 +1874,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto sinh(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return sinh_impl>(x); #else @@ -1884,7 +1885,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto cosh(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return cosh_impl>(x); #else @@ -1895,7 +1896,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto tanh(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return tanh_impl>(x); #else @@ -1906,7 +1907,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto log10(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log10: input must be strictly positive"); #ifdef BND_MATH_FIXED return log10_impl>(x); @@ -1918,7 +1919,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto cbrt(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return cbrt_impl>(x); #else @@ -1929,7 +1930,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto hypot(InX x, InY y) noexcept { - static_assert(detail::require_real() && detail::require_real()); + static_assert(detail::require_snap() && detail::require_snap()); #ifdef BND_MATH_FIXED return hypot_impl>(x, y); #else @@ -1941,19 +1942,19 @@ namespace bnd::math requires (Lower > bnd::detail::rational{0}) [[nodiscard]] BND_MATH_FN auto pow(InB base, InE exp) noexcept { - static_assert(detail::require_real() && detail::require_real()); + static_assert(detail::require_snap() && detail::require_snap()); using Out = detail::pow_auto_t; #ifdef BND_MATH_FIXED return pow_impl(base, exp); #else - double b = base; + double b = static_cast(base); if (b <= 0.0) return slim::expected{slim::unexpected(errc::domain_error)}; - double r = dbl::detail::d_pow(b, exp); + double r = dbl::detail::d_pow(b, static_cast(exp)); if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below if (r < static_cast(Lower) || r > static_cast(Upper)) return slim::expected{slim::unexpected(errc::overflow)}; - return slim::expected{Out{r}}; + return slim::expected{dbl::store(r)}; #endif } } diff --git a/include/bound/cmath_double.hpp b/include/bound/cmath_double.hpp index 22d6a0c..165c228 100644 --- a/include/bound/cmath_double.hpp +++ b/include/bound/cmath_double.hpp @@ -13,6 +13,7 @@ #define BNDcmathdoubleHPP #include "bound/math.hpp" // bnd::detail::ldexp (constexpr, reproducible) +#include "bound/bound.hpp" // complete bound/rational + has_flag/BoundPolicy/real (store<>) #include // std::fma, std::sqrt, std::nearbyint ONLY @@ -206,42 +207,51 @@ namespace bnd::math::dbl // The bound I/O is a plain double read/store (operator double / Out{double}), // so the cost is the polynomial itself. These plug into the shared public // surface as `fn_core` under the default build. + 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}; + else { Out o{}; o = bnd::detail::rational{d}; return o; } + } + template - [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return Out{detail::d_sin (x)}; } + [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return store(detail::d_sin(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return Out{detail::d_cos (x)}; } + [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return store(detail::d_cos(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return Out{detail::d_exp (x)}; } + [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return store(detail::d_exp(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return Out{detail::d_sqrt(x)}; } + [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return store(detail::d_sqrt(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out log_core(In x) { return Out{detail::d_log (x)}; } + [[nodiscard]] BND_DBL_FN Out log_core(In x) { return store(detail::d_log(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return Out{detail::d_exp2(x)}; } + [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return store(detail::d_exp2(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return Out{detail::d_log2(x)}; } + [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return store(detail::d_log2(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return Out{detail::d_log10(x)}; } + [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return store(detail::d_log10(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return Out{detail::d_cbrt(x)}; } + [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return store(detail::d_cbrt(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return Out{detail::d_sinh(x)}; } + [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return store(detail::d_sinh(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return Out{detail::d_cosh(x)}; } + [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return store(detail::d_cosh(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return Out{detail::d_tanh(x)}; } + [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return store(detail::d_tanh(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return Out{detail::d_atan(x)}; } + [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return store(detail::d_atan(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return Out{detail::d_asin(x)}; } + [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return store(detail::d_asin(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return Out{detail::d_acos(x)}; } + [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return store(detail::d_acos(static_cast(x))); } template [[nodiscard]] BND_DBL_FN Out atan2_core(In y, In x) - { return Out{detail::d_atan2(y, x)}; } + { return store(detail::d_atan2(static_cast(y), static_cast(x))); } template [[nodiscard]] BND_DBL_FN Out hypot_core(InX x, InY y) - { return Out{detail::d_hypot(x, y)}; } + { return store(detail::d_hypot(static_cast(x), static_cast(y))); } } // namespace bnd::math::dbl #endif // BNDcmathdoubleHPP diff --git a/single_include/bound/bound.hpp b/single_include/bound/bound.hpp index 8f0e2c1..374ceaa 100644 --- a/single_include/bound/bound.hpp +++ b/single_include/bound/bound.hpp @@ -7899,42 +7899,51 @@ namespace bnd::math::dbl // The bound I/O is a plain double read/store (operator double / Out{double}), // so the cost is the polynomial itself. These plug into the shared public // surface as `fn_core` under the default build. + 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}; + else { Out o{}; o = bnd::detail::rational{d}; return o; } + } + template - [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return Out{detail::d_sin (x)}; } + [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return store(detail::d_sin(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return Out{detail::d_cos (x)}; } + [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return store(detail::d_cos(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return Out{detail::d_exp (x)}; } + [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return store(detail::d_exp(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return Out{detail::d_sqrt(x)}; } + [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return store(detail::d_sqrt(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out log_core(In x) { return Out{detail::d_log (x)}; } + [[nodiscard]] BND_DBL_FN Out log_core(In x) { return store(detail::d_log(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return Out{detail::d_exp2(x)}; } + [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return store(detail::d_exp2(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return Out{detail::d_log2(x)}; } + [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return store(detail::d_log2(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return Out{detail::d_log10(x)}; } + [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return store(detail::d_log10(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return Out{detail::d_cbrt(x)}; } + [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return store(detail::d_cbrt(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return Out{detail::d_sinh(x)}; } + [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return store(detail::d_sinh(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return Out{detail::d_cosh(x)}; } + [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return store(detail::d_cosh(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return Out{detail::d_tanh(x)}; } + [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return store(detail::d_tanh(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return Out{detail::d_atan(x)}; } + [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return store(detail::d_atan(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return Out{detail::d_asin(x)}; } + [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return store(detail::d_asin(static_cast(x))); } template - [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return Out{detail::d_acos(x)}; } + [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return store(detail::d_acos(static_cast(x))); } template [[nodiscard]] BND_DBL_FN Out atan2_core(In y, In x) - { return Out{detail::d_atan2(y, x)}; } + { return store(detail::d_atan2(static_cast(y), static_cast(x))); } template [[nodiscard]] BND_DBL_FN Out hypot_core(InX x, InY y) - { return Out{detail::d_hypot(x, y)}; } + { return store(detail::d_hypot(static_cast(x), static_cast(y))); } } // namespace bnd::math::dbl @@ -8002,11 +8011,12 @@ namespace bnd::math // and avoids the slow integer-I/O path. Pure grid ops (abs/floor/ceil/round/ // trunc/fmod) have no engine and don't require it. template - consteval bool require_real() noexcept + consteval bool require_snap() noexcept { - static_assert(has_flag(BoundPolicy, real), - "bnd::math: transcendental operand must carry the `real` policy — " - "declare it as bound (or add `| real` to its policy)."); + static_assert(has_flag(BoundPolicy, snap), + "bnd::math: a transcendental result is rounded onto the grid — its " + "operand must permit rounding. Declare it with `round_nearest` (or " + "`snap` / a `round_*` mode / `real`)."); return true; } } @@ -9103,7 +9113,7 @@ namespace bnd::math requires (Lower == bnd::detail::rational{0}) [[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return sqrt_impl>(x); #else @@ -9118,22 +9128,22 @@ namespace bnd::math requires (Lower < bnd::detail::rational{0}) [[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); using Out = detail::sqrt_signed_auto_t; #ifdef BND_MATH_FIXED return sqrt_signed_impl(x); #else - double v = x; + double v = static_cast(x); if (v < 0.0) return slim::expected{slim::unexpected(errc::domain_error)}; - return slim::expected{Out{dbl::detail::d_sqrt(v)}}; + return slim::expected{dbl::store(dbl::detail::d_sqrt(v))}; #endif } template [[nodiscard]] BND_MATH_FN auto exp2(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return exp2_impl>(x); #else @@ -9144,7 +9154,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto log2(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); // Domain guard belongs on the shared entry point, not just the fixed // engine's *_impl: the double engine's log_core has no singularity check, // so log2(x<=0) would silently store finite garbage (e.g. log2(0) ≈ -7). @@ -9159,7 +9169,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto exp(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return exp_impl>(x); #else @@ -9170,7 +9180,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto log(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log: input must be strictly positive"); #ifdef BND_MATH_FIXED return log_impl>(x); @@ -9182,12 +9192,12 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto pow_base(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); using Out = detail::pow_base_auto_t; #ifdef BND_MATH_FIXED return pow_base_impl(x); #else - return Out{dbl::detail::d_pow(static_cast(Base), x)}; + return dbl::store(dbl::detail::d_pow(static_cast(Base), static_cast(x))); #endif } @@ -9234,7 +9244,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto sin(In angle) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return sin_impl>(angle); #else @@ -9245,7 +9255,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto cos(In angle) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return cos_impl>(angle); #else @@ -9256,7 +9266,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto atan2(In y, In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return atan2_impl>(y, x); #else @@ -9267,12 +9277,12 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto tan(In angle) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); using Out = detail::tan_auto_t; #ifdef BND_MATH_FIXED return tan_impl(angle); #else - double x = angle; + double x = static_cast(angle); double c = dbl::detail::d_cos(x); if (c == 0.0) return slim::expected{slim::unexpected(errc::division_by_zero)}; @@ -9280,7 +9290,7 @@ namespace bnd::math if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below if (t < static_cast(Lower) || t > static_cast(Upper)) return slim::expected{slim::unexpected(errc::overflow)}; - return slim::expected{Out{t}}; + return slim::expected{dbl::store(t)}; #endif } @@ -9768,7 +9778,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto atan(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return atan_impl>(x); #else @@ -9779,7 +9789,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto asin(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return asin_impl>(x); #else @@ -9790,7 +9800,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto acos(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return acos_impl>(x); #else @@ -9801,7 +9811,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto sinh(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return sinh_impl>(x); #else @@ -9812,7 +9822,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto cosh(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return cosh_impl>(x); #else @@ -9823,7 +9833,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto tanh(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return tanh_impl>(x); #else @@ -9834,7 +9844,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto log10(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log10: input must be strictly positive"); #ifdef BND_MATH_FIXED return log10_impl>(x); @@ -9846,7 +9856,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto cbrt(In x) noexcept { - static_assert(detail::require_real()); + static_assert(detail::require_snap()); #ifdef BND_MATH_FIXED return cbrt_impl>(x); #else @@ -9857,7 +9867,7 @@ namespace bnd::math template [[nodiscard]] BND_MATH_FN auto hypot(InX x, InY y) noexcept { - static_assert(detail::require_real() && detail::require_real()); + static_assert(detail::require_snap() && detail::require_snap()); #ifdef BND_MATH_FIXED return hypot_impl>(x, y); #else @@ -9869,19 +9879,19 @@ namespace bnd::math requires (Lower > bnd::detail::rational{0}) [[nodiscard]] BND_MATH_FN auto pow(InB base, InE exp) noexcept { - static_assert(detail::require_real() && detail::require_real()); + static_assert(detail::require_snap() && detail::require_snap()); using Out = detail::pow_auto_t; #ifdef BND_MATH_FIXED return pow_impl(base, exp); #else - double b = base; + double b = static_cast(base); if (b <= 0.0) return slim::expected{slim::unexpected(errc::domain_error)}; - double r = dbl::detail::d_pow(b, exp); + double r = dbl::detail::d_pow(b, static_cast(exp)); if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below if (r < static_cast(Lower) || r > static_cast(Upper)) return slim::expected{slim::unexpected(errc::overflow)}; - return slim::expected{Out{r}}; + return slim::expected{dbl::store(r)}; #endif } } diff --git a/tests/test_math_snap_grids.cpp b/tests/test_math_snap_grids.cpp new file mode 100644 index 0000000..adf51b4 --- /dev/null +++ b/tests/test_math_snap_grids.cpp @@ -0,0 +1,51 @@ +// Phase-1: transcendentals are gated on `snap` (rounding permission), not `real` +// (double storage). This exercises the new capability — `bnd::math` on NON-`real` +// snap grids: integer-index storage and non-dyadic (1/100) grids — using exact +// special values that are bit-exact on both engines and any grid containing them. + +#include "bound/bound.hpp" +#include "bound/cmath.hpp" + +#include + +using namespace bnd; +using namespace bnd::detail; + +TEST_CASE("snap-gated transcendentals on non-real grids (integer & 1/100)", + "[cmath][snap][nonreal]") +{ + // round_nearest implies snap but NOT real → these are integer/index-stored, + // not double-backed. Pre-Phase-1 these were a hard `require_real` compile error. + using Ang = bound<{{-8, 8}, notch<1, 16384>}, round_nearest>; // integer-index storage + REQUIRE(rational{math::sin(Ang{0})} == 0); + REQUIRE(rational{math::cos(Ang{0})} == 1); + REQUIRE(rational{math::atan(Ang{0})} == 0); + + using Sq = bound<{{0, 16}, notch<1, 100>}, round_nearest>; // non-dyadic 1/100 grid + REQUIRE(rational{math::sqrt(Sq{0})} == 0); + REQUIRE(rational{math::sqrt(Sq{4})} == 2); + + using Lg = bound<{{1, 1000}, notch<1, 100>}, round_nearest>; + REQUIRE(rational{math::log10(Lg{1})} == 0); + REQUIRE(rational{math::log10(Lg{100})} == 2); + + using Cb = bound<{{-8, 8}, notch<1, 100>}, round_nearest>; + REQUIRE(rational{math::cbrt(Cb{0})} == 0); + REQUIRE(rational{math::cbrt(Cb{8})} == 2); + REQUIRE(rational{math::cbrt(Cb{-8})} == -2); + + using Hy = bound<{{-10, 10}, notch<1, 100>}, round_nearest>; + REQUIRE(rational{math::sinh(Hy{0})} == 0); + REQUIRE(rational{math::cosh(Hy{0})} == 1); + REQUIRE(rational{math::tanh(Hy{0})} == 0); + + using Ex = bound<{{-4, 4}, notch<1, 100>}, round_nearest>; + REQUIRE(rational{math::exp(Ex{0})} == 1); + + // pow returns expected; 2^4 snaps exactly onto the 1/100 grid. + using B = bound<{{1, 16}, notch<1, 100>}, round_nearest>; + using E = bound<{{-4, 8}, notch<1, 100>}, round_nearest>; + auto p = math::pow(B{2}, E{4}); + REQUIRE(p.has_value()); + REQUIRE(rational{*p} == 16); +}