diff --git a/docs/math.md b/docs/math.md index 06fdaba..39e6b1b 100644 --- a/docs/math.md +++ b/docs/math.md @@ -215,6 +215,35 @@ FPU-free build. The default double engine is the right choice everywhere else: it carries the same grid guarantees and is reproducible across IEEE-754 platforms compiled without `-ffast-math`. +## Choosing an engine per call (`cordic::` / `dbl::`) + +The unqualified `bnd::math::fn` uses the build's default engine. Both engines are +also reachable by name, **callable side-by-side in the same binary**: + +| Namespace | Engine | Availability | +|---|---|---| +| `bnd::math::cordic::fn` | integer / CORDIC | **always** (constexpr, FPU-free) | +| `bnd::math::dbl::fn` | `double` | unless `BND_MATH_NO_FP` | +| `bnd::math::fn` | the default | alias of `cordic` under `BND_MATH_FIXED`/`BND_MATH_NO_FP`, else `dbl` | + +The qualified entry points have the **same signatures, domains, auto-deduced +output grids, and domain `static_assert`s** as the unqualified one — only the +compute backend differs. This lets one program pick per call site: + +```cpp +using A = bound<{{-8, 8}, notch<1, 16384>}, round_nearest | real>; + +auto a = math::cordic::sin(A{1}); // bit-exact across every target — replay/sim +auto b = math::dbl::sin(A{1}); // ~2× faster — hot, accuracy-insensitive path +auto c = math::sin(A{1}); // whichever the build selected +``` + +Because the engines are independent approximations, `cordic::fn` and `dbl::fn` +can disagree by up to one notch on rounding ties (the table-maker's dilemma — see +[determinism.md](determinism.md)); algebraically-exact inputs (e.g. `sqrt(4)`, +`pow(2,4)`) land identically. Under `BND_MATH_NO_FP` the `dbl::` namespace is not +defined, so a `dbl::` call there is a compile error; `cordic::` always works. + ## 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/include/bound/cmath.hpp b/include/bound/cmath.hpp index 77aa8cb..20f3dce 100644 --- a/include/bound/cmath.hpp +++ b/include/bound/cmath.hpp @@ -1966,6 +1966,273 @@ namespace bnd::math return slim::expected{dbl::store(r)}; #endif } + + //=========================================================================== + // Explicit engine namespaces — call a chosen engine regardless of the build + // default. `cordic::fn` (integer/CORDIC, ALWAYS present) and `dbl::fn` (the + // double engine, present unless BND_MATH_NO_FP) expose the SAME public-shaped + // API as the top-level `bnd::math::fn` — same signatures, domains, auto-deduced + // output grids, and domain static_asserts. The unqualified `bnd::math::fn` is + // an alias for whichever engine the build selects (see the #ifdef dispatch + // above); these let a single binary mix both — e.g. `cordic::sin` on a + // determinism-critical path and `dbl::sin` on a hot one. + // + // The engines are independent approximations: a grid-snapped value can differ + // between them by up to one notch on rounding ties (table-maker's dilemma). + // Don't compare outputs across engines — see determinism.md. + //=========================================================================== + namespace cordic + { + template + requires (Lower == bnd::detail::rational{0}) + [[nodiscard]] constexpr auto sqrt(In x) noexcept + { static_assert(detail::require_snap()); return sqrt_impl>(x); } + + template + requires (Lower < bnd::detail::rational{0}) + [[nodiscard]] constexpr auto sqrt(In x) noexcept + { static_assert(detail::require_snap()); return sqrt_signed_impl>(x); } + + template + [[nodiscard]] constexpr auto exp2(In x) noexcept + { static_assert(detail::require_snap()); return exp2_impl>(x); } + + template + [[nodiscard]] constexpr auto log2(In x) noexcept + { + static_assert(detail::require_snap()); + static_assert(Lower > 0, "bnd::math::cordic::log2: input must be strictly positive"); + return log2_impl>(x); + } + + template + [[nodiscard]] constexpr auto exp(In x) noexcept + { static_assert(detail::require_snap()); return exp_impl>(x); } + + template + [[nodiscard]] constexpr auto log(In x) noexcept + { + static_assert(detail::require_snap()); + static_assert(Lower > 0, "bnd::math::cordic::log: input must be strictly positive"); + return log_impl>(x); + } + + template + [[nodiscard]] constexpr auto pow_base(In x) noexcept + { static_assert(detail::require_snap()); return pow_base_impl>(x); } + + template + [[nodiscard]] constexpr auto sin(In angle) noexcept + { static_assert(detail::require_snap()); return sin_impl>(angle); } + + template + [[nodiscard]] constexpr auto cos(In angle) noexcept + { static_assert(detail::require_snap()); return cos_impl>(angle); } + + template + [[nodiscard]] constexpr auto tan(In angle) noexcept + { static_assert(detail::require_snap()); return tan_impl>(angle); } + + template + [[nodiscard]] constexpr auto atan2(In y, In x) noexcept + { static_assert(detail::require_snap()); return atan2_impl>(y, x); } + + template + [[nodiscard]] constexpr auto atan(In x) noexcept + { static_assert(detail::require_snap()); return atan_impl>(x); } + + template + [[nodiscard]] constexpr auto asin(In x) noexcept + { static_assert(detail::require_snap()); return asin_impl>(x); } + + template + [[nodiscard]] constexpr auto acos(In x) noexcept + { static_assert(detail::require_snap()); return acos_impl>(x); } + + template + [[nodiscard]] constexpr auto sinh(In x) noexcept + { static_assert(detail::require_snap()); return sinh_impl>(x); } + + template + [[nodiscard]] constexpr auto cosh(In x) noexcept + { static_assert(detail::require_snap()); return cosh_impl>(x); } + + template + [[nodiscard]] constexpr auto tanh(In x) noexcept + { static_assert(detail::require_snap()); return tanh_impl>(x); } + + template + [[nodiscard]] constexpr auto log10(In x) noexcept + { + static_assert(detail::require_snap()); + static_assert(Lower > 0, "bnd::math::cordic::log10: input must be strictly positive"); + return log10_impl>(x); + } + + template + [[nodiscard]] constexpr auto cbrt(In x) noexcept + { static_assert(detail::require_snap()); return cbrt_impl>(x); } + + template + [[nodiscard]] constexpr auto hypot(InX x, InY y) noexcept + { + static_assert(detail::require_snap() && detail::require_snap()); + return hypot_impl>(x, y); + } + + template + requires (Lower > bnd::detail::rational{0}) + [[nodiscard]] constexpr auto pow(InB base, InE exp) noexcept + { + static_assert(detail::require_snap() && detail::require_snap()); + return pow_impl>(base, exp); + } + } // namespace cordic + +#ifndef BND_MATH_NO_FP + namespace dbl + { + // Public-shaped double-engine entry points. `detail::` here would resolve to + // bnd::math::dbl::detail (the engine cores), so the shared deduction/helpers + // are qualified as `bnd::math::detail::`; the `*_core`/`store`/`d_*` names are + // this namespace's own. Absent under BND_MATH_NO_FP (no FP, no ). + template + requires (Lower == bnd::detail::rational{0}) + [[nodiscard]] BND_DBL_FN auto sqrt(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return sqrt_core>(x); } + + template + requires (Lower < bnd::detail::rational{0}) + [[nodiscard]] BND_DBL_FN auto sqrt(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + using Out = bnd::math::detail::sqrt_signed_auto_t; + double v = static_cast(x); + if (v < 0.0) + return slim::expected{slim::unexpected(errc::domain_error)}; + return slim::expected{store(detail::d_sqrt(v))}; + } + + template + [[nodiscard]] BND_DBL_FN auto exp2(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return exp2_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto log2(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + static_assert(Lower > 0, "bnd::math::dbl::log2: input must be strictly positive"); + return log2_core>(x); + } + + template + [[nodiscard]] BND_DBL_FN auto exp(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return exp_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto log(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + static_assert(Lower > 0, "bnd::math::dbl::log: input must be strictly positive"); + return log_core>(x); + } + + template + [[nodiscard]] BND_DBL_FN auto pow_base(In x) noexcept + { + 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))); + } + + template + [[nodiscard]] BND_DBL_FN auto sin(In angle) noexcept + { static_assert(bnd::math::detail::require_snap()); return sin_core>(angle); } + + template + [[nodiscard]] BND_DBL_FN auto cos(In angle) noexcept + { static_assert(bnd::math::detail::require_snap()); return cos_core>(angle); } + + template + [[nodiscard]] BND_DBL_FN auto tan(In angle) noexcept + { + static_assert(bnd::math::detail::require_snap()); + using Out = bnd::math::detail::tan_auto_t; + double x = static_cast(angle); + double c = detail::d_cos(x); + if (c == 0.0) + return slim::expected{slim::unexpected(errc::division_by_zero)}; + double 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)) + return slim::expected{slim::unexpected(errc::overflow)}; + return slim::expected{store(t)}; + } + + template + [[nodiscard]] BND_DBL_FN auto atan2(In y, In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return atan2_core>(y, x); } + + template + [[nodiscard]] BND_DBL_FN auto atan(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return atan_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto asin(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return asin_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto acos(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return acos_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto sinh(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return sinh_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto cosh(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return cosh_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto tanh(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return tanh_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto log10(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + static_assert(Lower > 0, "bnd::math::dbl::log10: input must be strictly positive"); + return log10_core>(x); + } + + template + [[nodiscard]] BND_DBL_FN auto cbrt(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return cbrt_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto hypot(InX x, InY y) noexcept + { + static_assert(bnd::math::detail::require_snap() && bnd::math::detail::require_snap()); + return hypot_core>(x, y); + } + + template + requires (Lower > bnd::detail::rational{0}) + [[nodiscard]] BND_DBL_FN auto pow(InB base, InE exp) noexcept + { + static_assert(bnd::math::detail::require_snap() && bnd::math::detail::require_snap()); + using Out = bnd::math::detail::pow_auto_t; + double b = static_cast(base); + if (b <= 0.0) + return slim::expected{slim::unexpected(errc::domain_error)}; + double r = 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{store(r)}; + } + } // namespace dbl +#endif // !BND_MATH_NO_FP } #endif diff --git a/single_include/bound/bound.hpp b/single_include/bound/bound.hpp index 1d2535b..e0f392d 100644 --- a/single_include/bound/bound.hpp +++ b/single_include/bound/bound.hpp @@ -9921,6 +9921,273 @@ namespace bnd::math return slim::expected{dbl::store(r)}; #endif } + + //=========================================================================== + // Explicit engine namespaces — call a chosen engine regardless of the build + // default. `cordic::fn` (integer/CORDIC, ALWAYS present) and `dbl::fn` (the + // double engine, present unless BND_MATH_NO_FP) expose the SAME public-shaped + // API as the top-level `bnd::math::fn` — same signatures, domains, auto-deduced + // output grids, and domain static_asserts. The unqualified `bnd::math::fn` is + // an alias for whichever engine the build selects (see the #ifdef dispatch + // above); these let a single binary mix both — e.g. `cordic::sin` on a + // determinism-critical path and `dbl::sin` on a hot one. + // + // The engines are independent approximations: a grid-snapped value can differ + // between them by up to one notch on rounding ties (table-maker's dilemma). + // Don't compare outputs across engines — see determinism.md. + //=========================================================================== + namespace cordic + { + template + requires (Lower == bnd::detail::rational{0}) + [[nodiscard]] constexpr auto sqrt(In x) noexcept + { static_assert(detail::require_snap()); return sqrt_impl>(x); } + + template + requires (Lower < bnd::detail::rational{0}) + [[nodiscard]] constexpr auto sqrt(In x) noexcept + { static_assert(detail::require_snap()); return sqrt_signed_impl>(x); } + + template + [[nodiscard]] constexpr auto exp2(In x) noexcept + { static_assert(detail::require_snap()); return exp2_impl>(x); } + + template + [[nodiscard]] constexpr auto log2(In x) noexcept + { + static_assert(detail::require_snap()); + static_assert(Lower > 0, "bnd::math::cordic::log2: input must be strictly positive"); + return log2_impl>(x); + } + + template + [[nodiscard]] constexpr auto exp(In x) noexcept + { static_assert(detail::require_snap()); return exp_impl>(x); } + + template + [[nodiscard]] constexpr auto log(In x) noexcept + { + static_assert(detail::require_snap()); + static_assert(Lower > 0, "bnd::math::cordic::log: input must be strictly positive"); + return log_impl>(x); + } + + template + [[nodiscard]] constexpr auto pow_base(In x) noexcept + { static_assert(detail::require_snap()); return pow_base_impl>(x); } + + template + [[nodiscard]] constexpr auto sin(In angle) noexcept + { static_assert(detail::require_snap()); return sin_impl>(angle); } + + template + [[nodiscard]] constexpr auto cos(In angle) noexcept + { static_assert(detail::require_snap()); return cos_impl>(angle); } + + template + [[nodiscard]] constexpr auto tan(In angle) noexcept + { static_assert(detail::require_snap()); return tan_impl>(angle); } + + template + [[nodiscard]] constexpr auto atan2(In y, In x) noexcept + { static_assert(detail::require_snap()); return atan2_impl>(y, x); } + + template + [[nodiscard]] constexpr auto atan(In x) noexcept + { static_assert(detail::require_snap()); return atan_impl>(x); } + + template + [[nodiscard]] constexpr auto asin(In x) noexcept + { static_assert(detail::require_snap()); return asin_impl>(x); } + + template + [[nodiscard]] constexpr auto acos(In x) noexcept + { static_assert(detail::require_snap()); return acos_impl>(x); } + + template + [[nodiscard]] constexpr auto sinh(In x) noexcept + { static_assert(detail::require_snap()); return sinh_impl>(x); } + + template + [[nodiscard]] constexpr auto cosh(In x) noexcept + { static_assert(detail::require_snap()); return cosh_impl>(x); } + + template + [[nodiscard]] constexpr auto tanh(In x) noexcept + { static_assert(detail::require_snap()); return tanh_impl>(x); } + + template + [[nodiscard]] constexpr auto log10(In x) noexcept + { + static_assert(detail::require_snap()); + static_assert(Lower > 0, "bnd::math::cordic::log10: input must be strictly positive"); + return log10_impl>(x); + } + + template + [[nodiscard]] constexpr auto cbrt(In x) noexcept + { static_assert(detail::require_snap()); return cbrt_impl>(x); } + + template + [[nodiscard]] constexpr auto hypot(InX x, InY y) noexcept + { + static_assert(detail::require_snap() && detail::require_snap()); + return hypot_impl>(x, y); + } + + template + requires (Lower > bnd::detail::rational{0}) + [[nodiscard]] constexpr auto pow(InB base, InE exp) noexcept + { + static_assert(detail::require_snap() && detail::require_snap()); + return pow_impl>(base, exp); + } + } // namespace cordic + +#ifndef BND_MATH_NO_FP + namespace dbl + { + // Public-shaped double-engine entry points. `detail::` here would resolve to + // bnd::math::dbl::detail (the engine cores), so the shared deduction/helpers + // are qualified as `bnd::math::detail::`; the `*_core`/`store`/`d_*` names are + // this namespace's own. Absent under BND_MATH_NO_FP (no FP, no ). + template + requires (Lower == bnd::detail::rational{0}) + [[nodiscard]] BND_DBL_FN auto sqrt(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return sqrt_core>(x); } + + template + requires (Lower < bnd::detail::rational{0}) + [[nodiscard]] BND_DBL_FN auto sqrt(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + using Out = bnd::math::detail::sqrt_signed_auto_t; + double v = static_cast(x); + if (v < 0.0) + return slim::expected{slim::unexpected(errc::domain_error)}; + return slim::expected{store(detail::d_sqrt(v))}; + } + + template + [[nodiscard]] BND_DBL_FN auto exp2(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return exp2_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto log2(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + static_assert(Lower > 0, "bnd::math::dbl::log2: input must be strictly positive"); + return log2_core>(x); + } + + template + [[nodiscard]] BND_DBL_FN auto exp(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return exp_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto log(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + static_assert(Lower > 0, "bnd::math::dbl::log: input must be strictly positive"); + return log_core>(x); + } + + template + [[nodiscard]] BND_DBL_FN auto pow_base(In x) noexcept + { + 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))); + } + + template + [[nodiscard]] BND_DBL_FN auto sin(In angle) noexcept + { static_assert(bnd::math::detail::require_snap()); return sin_core>(angle); } + + template + [[nodiscard]] BND_DBL_FN auto cos(In angle) noexcept + { static_assert(bnd::math::detail::require_snap()); return cos_core>(angle); } + + template + [[nodiscard]] BND_DBL_FN auto tan(In angle) noexcept + { + static_assert(bnd::math::detail::require_snap()); + using Out = bnd::math::detail::tan_auto_t; + double x = static_cast(angle); + double c = detail::d_cos(x); + if (c == 0.0) + return slim::expected{slim::unexpected(errc::division_by_zero)}; + double 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)) + return slim::expected{slim::unexpected(errc::overflow)}; + return slim::expected{store(t)}; + } + + template + [[nodiscard]] BND_DBL_FN auto atan2(In y, In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return atan2_core>(y, x); } + + template + [[nodiscard]] BND_DBL_FN auto atan(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return atan_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto asin(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return asin_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto acos(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return acos_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto sinh(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return sinh_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto cosh(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return cosh_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto tanh(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return tanh_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto log10(In x) noexcept + { + static_assert(bnd::math::detail::require_snap()); + static_assert(Lower > 0, "bnd::math::dbl::log10: input must be strictly positive"); + return log10_core>(x); + } + + template + [[nodiscard]] BND_DBL_FN auto cbrt(In x) noexcept + { static_assert(bnd::math::detail::require_snap()); return cbrt_core>(x); } + + template + [[nodiscard]] BND_DBL_FN auto hypot(InX x, InY y) noexcept + { + static_assert(bnd::math::detail::require_snap() && bnd::math::detail::require_snap()); + return hypot_core>(x, y); + } + + template + requires (Lower > bnd::detail::rational{0}) + [[nodiscard]] BND_DBL_FN auto pow(InB base, InE exp) noexcept + { + static_assert(bnd::math::detail::require_snap() && bnd::math::detail::require_snap()); + using Out = bnd::math::detail::pow_auto_t; + double b = static_cast(base); + if (b <= 0.0) + return slim::expected{slim::unexpected(errc::domain_error)}; + double r = 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{store(r)}; + } + } // namespace dbl +#endif // !BND_MATH_NO_FP } diff --git a/tests/test_math_engines.cpp b/tests/test_math_engines.cpp new file mode 100644 index 0000000..dbedb50 --- /dev/null +++ b/tests/test_math_engines.cpp @@ -0,0 +1,96 @@ +// Phase-3: explicit engine namespaces. `bnd::math::cordic::fn` (integer/CORDIC, +// always present) and `bnd::math::dbl::fn` (double engine, present unless +// BND_MATH_NO_FP) expose the same public-shaped API as `bnd::math::fn` and are +// callable SIDE-BY-SIDE in one binary. The unqualified name aliases the build's +// default engine. This TU instantiates both so the wrappers actually compile. + +#include "bound/bound.hpp" +#include "bound/cmath.hpp" + +#include + +using namespace bnd; +using namespace bnd::detail; + +namespace +{ + // A double-backed real grid (works under both engines: `real` ⊃ snap; under + // BND_MATH_FIXED it is an ordinary round_nearest integer-backed bound). + using Ang = bound<{{-8, 8}, notch<1, 16384>}, round_nearest | real>; + using Pos = bound<{{1, 1000}, notch<1, 16384>}, round_nearest | real>; + using Sq = bound<{{0, 16}, notch<1, 16384>}, round_nearest | real>; // sqrt needs Lower 0 +} + +TEST_CASE("cordic engine is always callable and exact on special values", + "[cmath][engines][cordic]") +{ + REQUIRE(rational{math::cordic::sin(Ang{0})} == 0); + REQUIRE(rational{math::cordic::cos(Ang{0})} == 1); + REQUIRE(rational{math::cordic::atan(Ang{0})} == 0); + REQUIRE(rational{math::cordic::sinh(Ang{0})} == 0); + REQUIRE(rational{math::cordic::cosh(Ang{0})} == 1); + REQUIRE(rational{math::cordic::tanh(Ang{0})} == 0); + REQUIRE(rational{math::cordic::sqrt(Sq{4})} == 2); + REQUIRE(rational{math::cordic::cbrt(Ang{8})} == 2); + REQUIRE(rational{math::cordic::log10(Pos{100})} == 2); + REQUIRE(rational{math::cordic::exp(Ang{0})} == 1); + + // expected-returning ops + auto p = math::cordic::pow(Pos{2}, Ang{4}); + REQUIRE(p.has_value()); + REQUIRE(rational{*p} == 16); + + // constexpr: the integer engine evaluates at compile time + constexpr auto cs = math::cordic::sin(Ang{0}); + static_assert(rational{cs} == 0); +} + +#ifndef BND_MATH_NO_FP +TEST_CASE("double engine is callable side-by-side and agrees on special values", + "[cmath][engines][dbl]") +{ + REQUIRE(rational{math::dbl::sin(Ang{0})} == 0); + REQUIRE(rational{math::dbl::cos(Ang{0})} == 1); + REQUIRE(rational{math::dbl::atan(Ang{0})} == 0); + REQUIRE(rational{math::dbl::sinh(Ang{0})} == 0); + REQUIRE(rational{math::dbl::cosh(Ang{0})} == 1); + REQUIRE(rational{math::dbl::tanh(Ang{0})} == 0); + REQUIRE(rational{math::dbl::sqrt(Sq{4})} == 2); + REQUIRE(rational{math::dbl::cbrt(Ang{8})} == 2); + REQUIRE(rational{math::dbl::log10(Pos{100})} == 2); + REQUIRE(rational{math::dbl::exp(Ang{0})} == 1); + + auto p = math::dbl::pow(Pos{2}, Ang{4}); + REQUIRE(p.has_value()); + REQUIRE(rational{*p} == 16); +} + +TEST_CASE("both engines coexist in one binary and meet at exact points", + "[cmath][engines][mix]") +{ + // The defining property of Phase 3: both engines instantiated in the same TU. + // On algebraically-exact inputs they land on the identical grid value. + REQUIRE(rational{math::cordic::sqrt(Sq{4})} == rational{math::dbl::sqrt(Sq{4})}); + REQUIRE(rational{math::cordic::cos(Ang{0})} == rational{math::dbl::cos(Ang{0})}); + + // A pole still errors through the expected channel under both engines. + using TanAng = bound<{{-2, 2}, notch<1, 4096>}, round_nearest | real>; + auto tc = math::cordic::tan(TanAng{0}); + auto td = math::dbl::tan(TanAng{0}); + REQUIRE(tc.has_value()); + REQUIRE(td.has_value()); + REQUIRE(rational{*tc} == 0); + REQUIRE(rational{*td} == 0); +} +#endif // !BND_MATH_NO_FP + +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 + REQUIRE(rational{math::sin(Ang{1})} == rational{math::cordic::sin(Ang{1})}); +#else + REQUIRE(rational{math::sin(Ang{1})} == rational{math::dbl::sin(Ang{1})}); +#endif +}