diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6765f7f..1089a14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ jobs: - { os: ubuntu-24.04, cc: clang, ver: 18, build: Release } - { os: ubuntu-22.04, cc: gcc, ver: 12, build: Debug, extra: "-DBOUND_CXX20=ON", note: " (C++20)" } - { os: ubuntu-24.04, cc: gcc, ver: 14, build: Release, extra: "-DBOUND_MATH_FIXED=ON", note: " (CORDIC)", heavy: true } + - { os: ubuntu-24.04, cc: gcc, ver: 14, build: Release, extra: "-DBOUND_MATH_FLOAT=ON", note: " (float)" } # Native ARM64 (free for public repos): exercises a different ABI/codegen — # the Release cell runs the fuzz harness as a cross-arch bit-exactness check. - { os: ubuntu-24.04-arm, cc: gcc, ver: 14, build: Debug, note: " (arm64)" } diff --git a/CMakeLists.txt b/CMakeLists.txt index e55d8c0..0603276 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,15 @@ if(BOUND_MATH_FIXED) target_compile_definitions(bound_bound INTERFACE BND_MATH_FIXED) endif() +# -DBOUND_MATH_FLOAT=ON makes the UNQUALIFIED bnd::math::fn use the float +# (binary32) engine — for single-precision-only FPUs. All engines stay reachable +# by namespace (cordic::/dbl::/flt::) regardless; this only picks the default. +# Ignored under BOUND_MATH_FIXED (the integer engine wins / no FP). +option(BOUND_MATH_FLOAT "Default bnd::math to the float (binary32) engine instead of double" OFF) +if(BOUND_MATH_FLOAT) + target_compile_definitions(bound_bound INTERFACE BND_MATH_FLOAT) +endif() + # By default an incompatible assignment/conversion (`b = x;`, `bound{x}`, a by-value # bound argument) reports the *reason* at compile time via named static_asserts. That # makes `bound` advertise itself is_constructible/convertible from incompatible types diff --git a/docs/math.md b/docs/math.md index 87dd032..12aebfa 100644 --- a/docs/math.md +++ b/docs/math.md @@ -225,7 +225,12 @@ are also reachable by name, **callable side-by-side in the same binary**: | `bnd::math::cordic::fn` | integer / CORDIC | **always** (constexpr, FPU-free) | | `bnd::math::dbl::fn` | `double` (binary64) | unless `BND_MATH_NO_FP` | | `bnd::math::flt::fn` | `float` (binary32) | unless `BND_MATH_NO_FP` | -| `bnd::math::fn` | the default | `cordic` under `BND_MATH_FIXED`/`BND_MATH_NO_FP`, else `dbl` | +| `bnd::math::fn` | the default | `cordic` under `BND_MATH_FIXED`/`BND_MATH_NO_FP`; `flt` under `BND_MATH_FLOAT`; else `dbl` | + +Select the unqualified default at build time: `-DBOUND_MATH_FIXED=ON` (integer), +`-DBOUND_MATH_FLOAT=ON` (binary32), or neither (binary64). The macro only changes +what the bare `bnd::math::fn` name means — `cordic::`/`dbl::`/`flt::` stay +individually reachable regardless. The qualified entry points have the **same signatures, domains, auto-deduced output grids, and domain `static_assert`s** as the unqualified one — only the diff --git a/include/bound/cmath.hpp b/include/bound/cmath.hpp index 8d1b2d0..c1873e8 100644 --- a/include/bound/cmath.hpp +++ b/include/bound/cmath.hpp @@ -41,11 +41,18 @@ // platform built without `-ffast-math`. Fast (~ns); needs an FPU; runtime. // * `BND_MATH_FIXED` — integer/CORDIC engine (this file): FPU-free, constexpr, // UNCONDITIONALLY bit-identical (any platform/flags). For embedded/portability. +// * `BND_MATH_FLOAT` — float (binary32) engine (`cmath_float.hpp`): like the +// double engine but single precision, for single-precision-only FPUs. +// +// The macro picks only which engine the UNQUALIFIED `bnd::math::fn` uses; all +// engines are always reachable by namespace (`cordic::`/`dbl::`/`flt::`). Default +// selection (the dispatch below): `BND_MATH_NO_FP`→cordic, else +// `BND_MATH_FLOAT`→flt, else dbl. // // `BND_MATH_NO_FP` (implied by `BND_MATH_FIXED`, auto-enabled when -// `__STDC_HOSTED__ == 0`) compiles the double engine and its `` out -// entirely, leaving the integer engine — so the library, including the single -// header, builds with no hardware floating point. The dispatch below keys on it. +// `__STDC_HOSTED__ == 0`) compiles the double AND float engines and their +// `` out entirely, leaving the integer engine — so the library, including +// the single header, builds with no hardware floating point. // // ===================================================================== // INTEGER (CORDIC) ENGINE — BIT-EXACT REPRODUCIBILITY CONTRACT @@ -1187,8 +1194,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sqrt_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::sqrt_core>(x); #else return dbl::sqrt_core>(x); #endif @@ -1203,8 +1212,13 @@ namespace bnd::math { static_assert(detail::require_snap()); using Out = detail::sqrt_signed_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sqrt_signed_impl(x); +#elif defined(BND_MATH_FLOAT) + float v = static_cast(static_cast(x)); + if (v < 0.0f) + return slim::expected{slim::unexpected(errc::domain_error)}; + return slim::expected{flt::store(flt::detail::d_sqrt(v))}; #else double v = static_cast(x); if (v < 0.0) @@ -1217,8 +1231,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto exp2(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return exp2_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::exp2_core>(x); #else return dbl::exp2_core>(x); #endif @@ -1229,11 +1245,13 @@ namespace bnd::math { 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, + // engine's *_impl: the FP engines' log_core has no singularity check, // so log2(x<=0) would silently store finite garbage (e.g. log2(0) ≈ -7). static_assert(Lower > 0, "bnd::math::log2: input must be strictly positive"); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return log2_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::log2_core>(x); #else return dbl::log2_core>(x); #endif @@ -1243,8 +1261,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto exp(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return exp_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::exp_core>(x); #else return dbl::exp_core>(x); #endif @@ -1255,8 +1275,10 @@ namespace bnd::math { static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log: input must be strictly positive"); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return log_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::log_core>(x); #else return dbl::log_core>(x); #endif @@ -1267,8 +1289,10 @@ namespace bnd::math { static_assert(detail::require_snap()); using Out = detail::pow_base_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return pow_base_impl(x); +#elif defined(BND_MATH_FLOAT) + return flt::store(flt::detail::d_pow(static_cast(Base), static_cast(static_cast(x)))); #else return dbl::store(dbl::detail::d_pow(static_cast(Base), static_cast(x))); #endif @@ -1318,8 +1342,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto sin(In angle) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sin_impl>(angle); +#elif defined(BND_MATH_FLOAT) + return flt::sin_core>(angle); #else return dbl::sin_core>(angle); #endif @@ -1329,8 +1355,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto cos(In angle) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return cos_impl>(angle); +#elif defined(BND_MATH_FLOAT) + return flt::cos_core>(angle); #else return dbl::cos_core>(angle); #endif @@ -1340,8 +1368,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto atan2(In y, In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return atan2_impl>(y, x); +#elif defined(BND_MATH_FLOAT) + return flt::atan2_core>(y, x); #else return dbl::atan2_core>(y, x); #endif @@ -1352,8 +1382,18 @@ namespace bnd::math { static_assert(detail::require_snap()); using Out = detail::tan_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return tan_impl(angle); +#elif defined(BND_MATH_FLOAT) + float x = static_cast(static_cast(angle)); + float c = flt::detail::d_cos(x); + if (c == 0.0f) + return slim::expected{slim::unexpected(errc::division_by_zero)}; + float t = flt::detail::d_sin(x) / c; + if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below + if (t < static_cast(static_cast(Lower)) || t > static_cast(static_cast(Upper))) + return slim::expected{slim::unexpected(errc::overflow)}; + return slim::expected{flt::store(t)}; #else double x = static_cast(angle); double c = dbl::detail::d_cos(x); @@ -1463,10 +1503,12 @@ namespace bnd::math BND_MATH_FN void sin(DEG angle, AMP& out) noexcept { static_assert(detail::valid_circle()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) constexpr imax M = detail::circle_slots; constexpr int W = detail::working_bits(); out = detail::sin_slot(bnd::detail::raw_imax(angle)); +#elif defined(BND_MATH_FLOAT) + out = flt::detail::d_sin(static_cast(static_cast(angle)) * (flt::detail::kPi / 180.0f)); #else out = dbl::detail::d_sin(static_cast(angle) * (dbl::detail::kPi / 180.0)); #endif @@ -1477,10 +1519,12 @@ namespace bnd::math BND_MATH_FN void cos(DEG angle, AMP& out) noexcept { static_assert(detail::valid_circle()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) constexpr imax M = detail::circle_slots; constexpr int W = detail::working_bits(); out = detail::sin_slot(bnd::detail::raw_imax(angle) + M / 4); +#elif defined(BND_MATH_FLOAT) + out = flt::detail::d_cos(static_cast(static_cast(angle)) * (flt::detail::kPi / 180.0f)); #else out = dbl::detail::d_cos(static_cast(angle) * (dbl::detail::kPi / 180.0)); #endif @@ -1493,7 +1537,7 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN bool tan(DEG angle, AMP& out) noexcept { static_assert(detail::valid_circle()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) constexpr imax M = detail::circle_slots; constexpr int W = detail::working_bits(); imax i = bnd::detail::raw_imax(angle); @@ -1501,6 +1545,12 @@ namespace bnd::math if (c == 0) return false; // pole out = (detail::sin_slot(i) / c).value(); // sin / cos return true; +#elif defined(BND_MATH_FLOAT) + float rad = static_cast(static_cast(angle)) * (flt::detail::kPi / 180.0f); + float c = flt::detail::d_cos(rad); + if (c == 0.0f) return false; // pole + out = flt::detail::d_sin(rad) / c; + return true; #else double rad = static_cast(angle) * (dbl::detail::kPi / 180.0); double c = dbl::detail::d_cos(rad); @@ -1852,8 +1902,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto atan(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return atan_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::atan_core>(x); #else return dbl::atan_core>(x); #endif @@ -1863,8 +1915,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto asin(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return asin_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::asin_core>(x); #else return dbl::asin_core>(x); #endif @@ -1874,8 +1928,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto acos(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return acos_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::acos_core>(x); #else return dbl::acos_core>(x); #endif @@ -1885,8 +1941,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto sinh(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sinh_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::sinh_core>(x); #else return dbl::sinh_core>(x); #endif @@ -1896,8 +1954,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto cosh(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return cosh_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::cosh_core>(x); #else return dbl::cosh_core>(x); #endif @@ -1907,8 +1967,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto tanh(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return tanh_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::tanh_core>(x); #else return dbl::tanh_core>(x); #endif @@ -1919,8 +1981,10 @@ namespace bnd::math { static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log10: input must be strictly positive"); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return log10_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::log10_core>(x); #else return dbl::log10_core>(x); #endif @@ -1930,8 +1994,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto cbrt(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return cbrt_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::cbrt_core>(x); #else return dbl::cbrt_core>(x); #endif @@ -1941,8 +2007,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto hypot(InX x, InY y) noexcept { static_assert(detail::require_snap() && detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return hypot_impl>(x, y); +#elif defined(BND_MATH_FLOAT) + return flt::hypot_core>(x, y); #else return dbl::hypot_core>(x, y); #endif @@ -1954,8 +2022,17 @@ namespace bnd::math { static_assert(detail::require_snap() && detail::require_snap()); using Out = detail::pow_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return pow_impl(base, exp); +#elif defined(BND_MATH_FLOAT) + float b = static_cast(static_cast(base)); + if (b <= 0.0f) + return slim::expected{slim::unexpected(errc::domain_error)}; + float r = flt::detail::d_pow(b, static_cast(static_cast(exp))); + if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below + if (r < static_cast(static_cast(Lower)) || r > static_cast(static_cast(Upper))) + return slim::expected{slim::unexpected(errc::overflow)}; + return slim::expected{flt::store(r)}; #else double b = static_cast(base); if (b <= 0.0) @@ -2250,7 +2327,7 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap()); using Out = bnd::math::detail::sqrt_signed_auto_t; - float v = static_cast(x); + float v = static_cast(static_cast(x)); if (v < 0.0f) return slim::expected{slim::unexpected(errc::domain_error)}; return slim::expected{store(detail::d_sqrt(v))}; @@ -2285,7 +2362,7 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap()); using Out = bnd::math::detail::pow_base_auto_t; - return store(detail::d_pow(static_cast(Base), static_cast(x))); + return store(detail::d_pow(static_cast(Base), static_cast(static_cast(x)))); } template @@ -2301,13 +2378,13 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap()); using Out = bnd::math::detail::tan_auto_t; - float x = static_cast(angle); + float x = static_cast(static_cast(angle)); float c = detail::d_cos(x); if (c == 0.0f) return slim::expected{slim::unexpected(errc::division_by_zero)}; float t = detail::d_sin(x) / c; if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below - if (t < static_cast(Lower) || t > static_cast(Upper)) + if (t < static_cast(static_cast(Lower)) || t > static_cast(static_cast(Upper))) return slim::expected{slim::unexpected(errc::overflow)}; return slim::expected{store(t)}; } @@ -2365,12 +2442,12 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap() && bnd::math::detail::require_snap()); using Out = bnd::math::detail::pow_auto_t; - float b = static_cast(base); + float b = static_cast(static_cast(base)); if (b <= 0.0f) return slim::expected{slim::unexpected(errc::domain_error)}; - float r = detail::d_pow(b, static_cast(exp)); + float r = detail::d_pow(b, static_cast(static_cast(exp))); if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below - if (r < static_cast(Lower) || r > static_cast(Upper)) + if (r < static_cast(static_cast(Lower)) || r > static_cast(static_cast(Upper))) return slim::expected{slim::unexpected(errc::overflow)}; return slim::expected{store(r)}; } diff --git a/include/bound/cmath_float.hpp b/include/bound/cmath_float.hpp index abb77e5..231481f 100644 --- a/include/bound/cmath_float.hpp +++ b/include/bound/cmath_float.hpp @@ -235,41 +235,41 @@ namespace bnd::math::flt } template - [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return store(detail::d_sin(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return store(detail::d_sin(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return store(detail::d_cos(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return store(detail::d_cos(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return store(detail::d_exp(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return store(detail::d_exp(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return store(detail::d_sqrt(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return store(detail::d_sqrt(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out log_core(In x) { return store(detail::d_log(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out log_core(In x) { return store(detail::d_log(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return store(detail::d_exp2(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return store(detail::d_exp2(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return store(detail::d_log2(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return store(detail::d_log2(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return store(detail::d_log10(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return store(detail::d_log10(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return store(detail::d_cbrt(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return store(detail::d_cbrt(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return store(detail::d_sinh(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return store(detail::d_sinh(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return store(detail::d_cosh(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return store(detail::d_cosh(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return store(detail::d_tanh(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return store(detail::d_tanh(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return store(detail::d_atan(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return store(detail::d_atan(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return store(detail::d_asin(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return store(detail::d_asin(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return store(detail::d_acos(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return store(detail::d_acos(static_cast(static_cast(x)))); } template [[nodiscard]] BND_DBL_FN Out atan2_core(In y, In x) - { return store(detail::d_atan2(static_cast(y), static_cast(x))); } + { return store(detail::d_atan2(static_cast(static_cast(y)), static_cast(static_cast(x)))); } template [[nodiscard]] BND_DBL_FN Out hypot_core(InX x, InY y) - { return store(detail::d_hypot(static_cast(x), static_cast(y))); } + { return store(detail::d_hypot(static_cast(static_cast(x)), static_cast(static_cast(y)))); } } // namespace bnd::math::flt #endif // !BND_MATH_NO_FP diff --git a/single_include/bound/bound.hpp b/single_include/bound/bound.hpp index aa4c8f3..dc95468 100644 --- a/single_include/bound/bound.hpp +++ b/single_include/bound/bound.hpp @@ -8201,41 +8201,41 @@ namespace bnd::math::flt } template - [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return store(detail::d_sin(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out sin_core(In x) { return store(detail::d_sin(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return store(detail::d_cos(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out cos_core(In x) { return store(detail::d_cos(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return store(detail::d_exp(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out exp_core(In x) { return store(detail::d_exp(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return store(detail::d_sqrt(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out sqrt_core(In x) { return store(detail::d_sqrt(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out log_core(In x) { return store(detail::d_log(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out log_core(In x) { return store(detail::d_log(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return store(detail::d_exp2(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out exp2_core(In x) { return store(detail::d_exp2(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return store(detail::d_log2(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out log2_core(In x) { return store(detail::d_log2(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return store(detail::d_log10(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out log10_core(In x){ return store(detail::d_log10(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return store(detail::d_cbrt(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out cbrt_core(In x) { return store(detail::d_cbrt(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return store(detail::d_sinh(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out sinh_core(In x) { return store(detail::d_sinh(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return store(detail::d_cosh(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out cosh_core(In x) { return store(detail::d_cosh(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return store(detail::d_tanh(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out tanh_core(In x) { return store(detail::d_tanh(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return store(detail::d_atan(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out atan_core(In x) { return store(detail::d_atan(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return store(detail::d_asin(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out asin_core(In x) { return store(detail::d_asin(static_cast(static_cast(x)))); } template - [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return store(detail::d_acos(static_cast(x))); } + [[nodiscard]] BND_DBL_FN Out acos_core(In x) { return store(detail::d_acos(static_cast(static_cast(x)))); } template [[nodiscard]] BND_DBL_FN Out atan2_core(In y, In x) - { return store(detail::d_atan2(static_cast(y), static_cast(x))); } + { return store(detail::d_atan2(static_cast(static_cast(y)), static_cast(static_cast(x)))); } template [[nodiscard]] BND_DBL_FN Out hypot_core(InX x, InY y) - { return store(detail::d_hypot(static_cast(x), static_cast(y))); } + { return store(detail::d_hypot(static_cast(static_cast(x)), static_cast(static_cast(y)))); } } // namespace bnd::math::flt #endif // !BND_MATH_NO_FP @@ -8271,11 +8271,18 @@ namespace bnd::math::flt // platform built without `-ffast-math`. Fast (~ns); needs an FPU; runtime. // * `BND_MATH_FIXED` — integer/CORDIC engine (this file): FPU-free, constexpr, // UNCONDITIONALLY bit-identical (any platform/flags). For embedded/portability. +// * `BND_MATH_FLOAT` — float (binary32) engine (`cmath_float.hpp`): like the +// double engine but single precision, for single-precision-only FPUs. +// +// The macro picks only which engine the UNQUALIFIED `bnd::math::fn` uses; all +// engines are always reachable by namespace (`cordic::`/`dbl::`/`flt::`). Default +// selection (the dispatch below): `BND_MATH_NO_FP`→cordic, else +// `BND_MATH_FLOAT`→flt, else dbl. // // `BND_MATH_NO_FP` (implied by `BND_MATH_FIXED`, auto-enabled when -// `__STDC_HOSTED__ == 0`) compiles the double engine and its `` out -// entirely, leaving the integer engine — so the library, including the single -// header, builds with no hardware floating point. The dispatch below keys on it. +// `__STDC_HOSTED__ == 0`) compiles the double AND float engines and their +// `` out entirely, leaving the integer engine — so the library, including +// the single header, builds with no hardware floating point. // // ===================================================================== // INTEGER (CORDIC) ENGINE — BIT-EXACT REPRODUCIBILITY CONTRACT @@ -9417,8 +9424,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sqrt_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::sqrt_core>(x); #else return dbl::sqrt_core>(x); #endif @@ -9433,8 +9442,13 @@ namespace bnd::math { static_assert(detail::require_snap()); using Out = detail::sqrt_signed_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sqrt_signed_impl(x); +#elif defined(BND_MATH_FLOAT) + float v = static_cast(static_cast(x)); + if (v < 0.0f) + return slim::expected{slim::unexpected(errc::domain_error)}; + return slim::expected{flt::store(flt::detail::d_sqrt(v))}; #else double v = static_cast(x); if (v < 0.0) @@ -9447,8 +9461,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto exp2(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return exp2_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::exp2_core>(x); #else return dbl::exp2_core>(x); #endif @@ -9459,11 +9475,13 @@ namespace bnd::math { 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, + // engine's *_impl: the FP engines' log_core has no singularity check, // so log2(x<=0) would silently store finite garbage (e.g. log2(0) ≈ -7). static_assert(Lower > 0, "bnd::math::log2: input must be strictly positive"); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return log2_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::log2_core>(x); #else return dbl::log2_core>(x); #endif @@ -9473,8 +9491,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto exp(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return exp_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::exp_core>(x); #else return dbl::exp_core>(x); #endif @@ -9485,8 +9505,10 @@ namespace bnd::math { static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log: input must be strictly positive"); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return log_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::log_core>(x); #else return dbl::log_core>(x); #endif @@ -9497,8 +9519,10 @@ namespace bnd::math { static_assert(detail::require_snap()); using Out = detail::pow_base_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return pow_base_impl(x); +#elif defined(BND_MATH_FLOAT) + return flt::store(flt::detail::d_pow(static_cast(Base), static_cast(static_cast(x)))); #else return dbl::store(dbl::detail::d_pow(static_cast(Base), static_cast(x))); #endif @@ -9548,8 +9572,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto sin(In angle) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sin_impl>(angle); +#elif defined(BND_MATH_FLOAT) + return flt::sin_core>(angle); #else return dbl::sin_core>(angle); #endif @@ -9559,8 +9585,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto cos(In angle) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return cos_impl>(angle); +#elif defined(BND_MATH_FLOAT) + return flt::cos_core>(angle); #else return dbl::cos_core>(angle); #endif @@ -9570,8 +9598,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto atan2(In y, In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return atan2_impl>(y, x); +#elif defined(BND_MATH_FLOAT) + return flt::atan2_core>(y, x); #else return dbl::atan2_core>(y, x); #endif @@ -9582,8 +9612,18 @@ namespace bnd::math { static_assert(detail::require_snap()); using Out = detail::tan_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return tan_impl(angle); +#elif defined(BND_MATH_FLOAT) + float x = static_cast(static_cast(angle)); + float c = flt::detail::d_cos(x); + if (c == 0.0f) + return slim::expected{slim::unexpected(errc::division_by_zero)}; + float t = flt::detail::d_sin(x) / c; + if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below + if (t < static_cast(static_cast(Lower)) || t > static_cast(static_cast(Upper))) + return slim::expected{slim::unexpected(errc::overflow)}; + return slim::expected{flt::store(t)}; #else double x = static_cast(angle); double c = dbl::detail::d_cos(x); @@ -9693,10 +9733,12 @@ namespace bnd::math BND_MATH_FN void sin(DEG angle, AMP& out) noexcept { static_assert(detail::valid_circle()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) constexpr imax M = detail::circle_slots; constexpr int W = detail::working_bits(); out = detail::sin_slot(bnd::detail::raw_imax(angle)); +#elif defined(BND_MATH_FLOAT) + out = flt::detail::d_sin(static_cast(static_cast(angle)) * (flt::detail::kPi / 180.0f)); #else out = dbl::detail::d_sin(static_cast(angle) * (dbl::detail::kPi / 180.0)); #endif @@ -9707,10 +9749,12 @@ namespace bnd::math BND_MATH_FN void cos(DEG angle, AMP& out) noexcept { static_assert(detail::valid_circle()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) constexpr imax M = detail::circle_slots; constexpr int W = detail::working_bits(); out = detail::sin_slot(bnd::detail::raw_imax(angle) + M / 4); +#elif defined(BND_MATH_FLOAT) + out = flt::detail::d_cos(static_cast(static_cast(angle)) * (flt::detail::kPi / 180.0f)); #else out = dbl::detail::d_cos(static_cast(angle) * (dbl::detail::kPi / 180.0)); #endif @@ -9723,7 +9767,7 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN bool tan(DEG angle, AMP& out) noexcept { static_assert(detail::valid_circle()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) constexpr imax M = detail::circle_slots; constexpr int W = detail::working_bits(); imax i = bnd::detail::raw_imax(angle); @@ -9731,6 +9775,12 @@ namespace bnd::math if (c == 0) return false; // pole out = (detail::sin_slot(i) / c).value(); // sin / cos return true; +#elif defined(BND_MATH_FLOAT) + float rad = static_cast(static_cast(angle)) * (flt::detail::kPi / 180.0f); + float c = flt::detail::d_cos(rad); + if (c == 0.0f) return false; // pole + out = flt::detail::d_sin(rad) / c; + return true; #else double rad = static_cast(angle) * (dbl::detail::kPi / 180.0); double c = dbl::detail::d_cos(rad); @@ -10082,8 +10132,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto atan(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return atan_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::atan_core>(x); #else return dbl::atan_core>(x); #endif @@ -10093,8 +10145,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto asin(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return asin_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::asin_core>(x); #else return dbl::asin_core>(x); #endif @@ -10104,8 +10158,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto acos(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return acos_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::acos_core>(x); #else return dbl::acos_core>(x); #endif @@ -10115,8 +10171,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto sinh(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return sinh_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::sinh_core>(x); #else return dbl::sinh_core>(x); #endif @@ -10126,8 +10184,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto cosh(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return cosh_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::cosh_core>(x); #else return dbl::cosh_core>(x); #endif @@ -10137,8 +10197,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto tanh(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return tanh_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::tanh_core>(x); #else return dbl::tanh_core>(x); #endif @@ -10149,8 +10211,10 @@ namespace bnd::math { static_assert(detail::require_snap()); static_assert(Lower > 0, "bnd::math::log10: input must be strictly positive"); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return log10_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::log10_core>(x); #else return dbl::log10_core>(x); #endif @@ -10160,8 +10224,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto cbrt(In x) noexcept { static_assert(detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return cbrt_impl>(x); +#elif defined(BND_MATH_FLOAT) + return flt::cbrt_core>(x); #else return dbl::cbrt_core>(x); #endif @@ -10171,8 +10237,10 @@ namespace bnd::math [[nodiscard]] BND_MATH_FN auto hypot(InX x, InY y) noexcept { static_assert(detail::require_snap() && detail::require_snap()); -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return hypot_impl>(x, y); +#elif defined(BND_MATH_FLOAT) + return flt::hypot_core>(x, y); #else return dbl::hypot_core>(x, y); #endif @@ -10184,8 +10252,17 @@ namespace bnd::math { static_assert(detail::require_snap() && detail::require_snap()); using Out = detail::pow_auto_t; -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) return pow_impl(base, exp); +#elif defined(BND_MATH_FLOAT) + float b = static_cast(static_cast(base)); + if (b <= 0.0f) + return slim::expected{slim::unexpected(errc::domain_error)}; + float r = flt::detail::d_pow(b, static_cast(static_cast(exp))); + if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below + if (r < static_cast(static_cast(Lower)) || r > static_cast(static_cast(Upper))) + return slim::expected{slim::unexpected(errc::overflow)}; + return slim::expected{flt::store(r)}; #else double b = static_cast(base); if (b <= 0.0) @@ -10480,7 +10557,7 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap()); using Out = bnd::math::detail::sqrt_signed_auto_t; - float v = static_cast(x); + float v = static_cast(static_cast(x)); if (v < 0.0f) return slim::expected{slim::unexpected(errc::domain_error)}; return slim::expected{store(detail::d_sqrt(v))}; @@ -10515,7 +10592,7 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap()); using Out = bnd::math::detail::pow_base_auto_t; - return store(detail::d_pow(static_cast(Base), static_cast(x))); + return store(detail::d_pow(static_cast(Base), static_cast(static_cast(x)))); } template @@ -10531,13 +10608,13 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap()); using Out = bnd::math::detail::tan_auto_t; - float x = static_cast(angle); + float x = static_cast(static_cast(angle)); float c = detail::d_cos(x); if (c == 0.0f) return slim::expected{slim::unexpected(errc::division_by_zero)}; float t = detail::d_sin(x) / c; if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below - if (t < static_cast(Lower) || t > static_cast(Upper)) + if (t < static_cast(static_cast(Lower)) || t > static_cast(static_cast(Upper))) return slim::expected{slim::unexpected(errc::overflow)}; return slim::expected{store(t)}; } @@ -10595,12 +10672,12 @@ namespace bnd::math { static_assert(bnd::math::detail::require_snap() && bnd::math::detail::require_snap()); using Out = bnd::math::detail::pow_auto_t; - float b = static_cast(base); + float b = static_cast(static_cast(base)); if (b <= 0.0f) return slim::expected{slim::unexpected(errc::domain_error)}; - float r = detail::d_pow(b, static_cast(exp)); + float r = detail::d_pow(b, static_cast(static_cast(exp))); if constexpr (!has_flag(BoundPolicy, clamp)) // clamp Out: saturate below - if (r < static_cast(Lower) || r > static_cast(Upper)) + if (r < static_cast(static_cast(Lower)) || r > static_cast(static_cast(Upper))) return slim::expected{slim::unexpected(errc::overflow)}; return slim::expected{store(r)}; } diff --git a/tests/test_determinism.cpp b/tests/test_determinism.cpp index b73e32b..1dddbad 100644 --- a/tests/test_determinism.cpp +++ b/tests/test_determinism.cpp @@ -26,6 +26,12 @@ using namespace bnd::detail; #define EXACT_ERR(expr) do { auto _r = (expr); REQUIRE_FALSE(_r.has_value()); \ REQUIRE(_r.error() == errc::domain_error); } while (0) +// These pins are the DEFAULT engine's values (double / CORDIC, which agree here +// except the one #ifdef'd case). The float engine is a third value set, so under +// BND_MATH_FLOAT the unqualified math::fn produces different snapped values — its +// determinism is pinned separately in test_math_engines.cpp. +#ifndef BND_MATH_FLOAT + TEST_CASE("determinism: asin / acos across grids and domain corners", "[determinism][cmath]") { using A1 = bound<{{-1, 1}, notch<1, 65536>}, round_nearest | real>; @@ -199,3 +205,5 @@ TEST_CASE("determinism: sin / cos / tan radian corners", "[determinism][cmath]") EXACT_OK(math::tan(RAD{0}), 0, 1); EXACT_OK(math::tan(RAD{1}), 25517, 16384); // ~1.5574 } + +#endif // !BND_MATH_FLOAT diff --git a/tests/test_math_engines.cpp b/tests/test_math_engines.cpp index 6fec970..f88f5ca 100644 --- a/tests/test_math_engines.cpp +++ b/tests/test_math_engines.cpp @@ -141,8 +141,10 @@ TEST_CASE("unqualified name aliases the build's default engine", "[cmath][engines][default]") { // bnd::math::sin must equal the selected engine bit-for-bit. -#ifdef BND_MATH_NO_FP +#if defined(BND_MATH_NO_FP) REQUIRE(rational{math::sin(Ang{1})} == rational{math::cordic::sin(Ang{1})}); +#elif defined(BND_MATH_FLOAT) + REQUIRE(rational{math::sin(Ang{1})} == rational{math::flt::sin(Ang{1})}); #else REQUIRE(rational{math::sin(Ang{1})} == rational{math::dbl::sin(Ang{1})}); #endif