Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
run: ctest --test-dir build --output-on-failure -L example

- name: Single-header smokes
run: cmake --build build --target single_header_smoke single_header_freestanding_smoke
run: cmake --build build --target single_header_smoke single_header_freestanding_smoke single_header_nofp_smoke

# bound_fuzz and bench are large, slow-to-compile EXCLUDE_FROM_ALL targets,
# so they are exercised only in Release on representative cells (one per
Expand Down
18 changes: 18 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,24 @@ if(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_options(single_header_freestanding_smoke PRIVATE -Wl,--allow-multiple-definition)
endif()

# FP-free smoke: compiles the single header with BND_MATH_NO_FP and a poison
# <cmath> shim placed FIRST on the include path, so the build hard-errors if any
# <cmath> is pulled in. Proves the single header honors the FP-free guard (only
# the integer/CORDIC engine remains). GNU/Clang only — the proof relies on -I
# ordering for angle-bracket includes; bare-metal toolchains are GCC/Clang.
# EXCLUDE_FROM_ALL — build with `--target single_header_nofp_smoke`.
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_executable(single_header_nofp_smoke EXCLUDE_FROM_ALL
tests/single_header_nofp_smoke.cpp)
# The poison dir must precede single_include AND the system dirs. BEFORE keeps
# it ahead of single_include; angle-bracket search already places -I dirs ahead
# of the system headers, so <cmath> resolves to the poison shim.
target_include_directories(single_header_nofp_smoke BEFORE PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/tests/nofp_poison"
"${CMAKE_CURRENT_SOURCE_DIR}/single_include")
target_compile_definitions(single_header_nofp_smoke PRIVATE BND_MATH_NO_FP)
endif()

#---------------------------------------------------------------------------
# Install / package config
#
Expand Down
26 changes: 26 additions & 0 deletions docs/math.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,29 @@ Use it when you need bit-identical results across heterogeneous targets
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`.

## Compiling without floating point (`BND_MATH_NO_FP`)

On a target with no hardware FPU and no `<cmath>`, define **`BND_MATH_NO_FP`**
(any value). It compiles the double engine — and its `#include <cmath>` — out
**entirely**, leaving the always-present integer/CORDIC engine to serve the full
`bnd::math` API. The public surface, output grids, and types are unchanged: only
the compute backend differs.

- **No `<cmath>`, no `std::fma`/`std::sqrt`** are referenced anywhere in the
library when the macro is set — this holds for the modular headers **and** the
amalgamated single header (the `<cmath>` include is emitted under the same
guard). A CI smoke (`single_header_nofp_smoke`) compiles the single header with
a *poison* `<cmath>` shim first on the include path, so the build fails if any
`<cmath>` is pulled in.
- **Auto-enabled** when `__STDC_HOSTED__ == 0` (i.e. `-ffreestanding`) and
**implied by `BND_MATH_FIXED`** — selecting the integer engine is itself an
FP-free build.
- All transcendentals are `constexpr` under `BND_MATH_NO_FP` (the integer engine),
so they evaluate at compile time as well as runtime.

```bash
# bare-metal: integer engine, no <cmath>, single header
g++ -std=c++23 -ffreestanding -I single_include my_app.cpp # NO_FP auto-on
g++ -std=c++23 -DBND_MATH_NO_FP -I single_include my_app.cpp # or force it
```
59 changes: 34 additions & 25 deletions include/bound/cmath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
// std::nearbyint; feature macro __cpp_lib_constexpr_cmath). That branch is
// inert (and untested) until such a toolchain exists. Decision 2026-06-12:
// no compile-time softfloat emulation — wait for the standard.
#if defined(BND_MATH_FIXED) \
// BND_MATH_NO_FP (resolved in cmath_double.hpp, included above) selects the
// integer/CORDIC engine and is implied by BND_MATH_FIXED — so the integer engine
// is constexpr here. The double engine becomes constexpr only on a C++26 toolchain
// with constexpr <cmath> (P1383); that branch is inert until such a toolchain.
#if defined(BND_MATH_NO_FP) \
|| (defined(__cpp_lib_constexpr_cmath) && __cpp_lib_constexpr_cmath >= 202202L)
# define BND_MATH_FN constexpr
#else
Expand All @@ -37,6 +41,11 @@
// * `BND_MATH_FIXED` — integer/CORDIC engine (this file): FPU-free, constexpr,
// UNCONDITIONALLY bit-identical (any platform/flags). For embedded/portability.
//
// `BND_MATH_NO_FP` (implied by `BND_MATH_FIXED`, auto-enabled when
// `__STDC_HOSTED__ == 0`) compiles the double engine and its `<cmath>` 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.
//
// =====================================================================
// INTEGER (CORDIC) ENGINE — BIT-EXACT REPRODUCIBILITY CONTRACT
// =====================================================================
Expand Down Expand Up @@ -1177,7 +1186,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto sqrt(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return sqrt_impl<detail::sqrt_auto_t<In>>(x);
#else
return dbl::sqrt_core<detail::sqrt_auto_t<In>>(x);
Expand All @@ -1193,7 +1202,7 @@ namespace bnd::math
{
static_assert(detail::require_snap<In>());
using Out = detail::sqrt_signed_auto_t<In>;
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return sqrt_signed_impl<Out>(x);
#else
double v = static_cast<double>(x);
Expand All @@ -1207,7 +1216,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto exp2(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return exp2_impl<detail::exp2_auto_t<In>>(x);
#else
return dbl::exp2_core<detail::exp2_auto_t<In>>(x);
Expand All @@ -1222,7 +1231,7 @@ namespace bnd::math
// 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).
static_assert(Lower<In> > 0, "bnd::math::log2: input must be strictly positive");
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return log2_impl<detail::log2_auto_t<In>>(x);
#else
return dbl::log2_core<detail::log2_auto_t<In>>(x);
Expand All @@ -1233,7 +1242,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto exp(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return exp_impl<detail::exp_auto_t<In>>(x);
#else
return dbl::exp_core<detail::exp_auto_t<In>>(x);
Expand All @@ -1245,7 +1254,7 @@ namespace bnd::math
{
static_assert(detail::require_snap<In>());
static_assert(Lower<In> > 0, "bnd::math::log: input must be strictly positive");
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return log_impl<detail::log_auto_t<In>>(x);
#else
return dbl::log_core<detail::log_auto_t<In>>(x);
Expand All @@ -1257,7 +1266,7 @@ namespace bnd::math
{
static_assert(detail::require_snap<In>());
using Out = detail::pow_base_auto_t<Base, In>;
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return pow_base_impl<Base, Out>(x);
#else
return dbl::store<Out>(dbl::detail::d_pow(static_cast<double>(Base), static_cast<double>(x)));
Expand Down Expand Up @@ -1308,7 +1317,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto sin(In angle) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return sin_impl<detail::sin_auto_t<In>>(angle);
#else
return dbl::sin_core<detail::sin_auto_t<In>>(angle);
Expand All @@ -1319,7 +1328,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto cos(In angle) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return cos_impl<detail::cos_auto_t<In>>(angle);
#else
return dbl::cos_core<detail::cos_auto_t<In>>(angle);
Expand All @@ -1330,7 +1339,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto atan2(In y, In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return atan2_impl<detail::atan2_auto_t<In>>(y, x);
#else
return dbl::atan2_core<detail::atan2_auto_t<In>>(y, x);
Expand All @@ -1342,7 +1351,7 @@ namespace bnd::math
{
static_assert(detail::require_snap<In>());
using Out = detail::tan_auto_t<In>;
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return tan_impl<Out>(angle);
#else
double x = static_cast<double>(angle);
Expand Down Expand Up @@ -1453,7 +1462,7 @@ namespace bnd::math
BND_MATH_FN void sin(DEG angle, AMP& out) noexcept
{
static_assert(detail::valid_circle<DEG>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
constexpr imax M = detail::circle_slots<DEG>;
constexpr int W = detail::working_bits<AMP>();
out = detail::sin_slot<M, W>(bnd::detail::raw_imax(angle));
Expand All @@ -1467,7 +1476,7 @@ namespace bnd::math
BND_MATH_FN void cos(DEG angle, AMP& out) noexcept
{
static_assert(detail::valid_circle<DEG>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
constexpr imax M = detail::circle_slots<DEG>;
constexpr int W = detail::working_bits<AMP>();
out = detail::sin_slot<M, W>(bnd::detail::raw_imax(angle) + M / 4);
Expand All @@ -1483,7 +1492,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN bool tan(DEG angle, AMP& out) noexcept
{
static_assert(detail::valid_circle<DEG>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
constexpr imax M = detail::circle_slots<DEG>;
constexpr int W = detail::working_bits<AMP>();
imax i = bnd::detail::raw_imax(angle);
Expand Down Expand Up @@ -1842,7 +1851,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto atan(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return atan_impl<detail::atan_auto_t<In>>(x);
#else
return dbl::atan_core<detail::atan_auto_t<In>>(x);
Expand All @@ -1853,7 +1862,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto asin(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return asin_impl<detail::asin_auto_t<In>>(x);
#else
return dbl::asin_core<detail::asin_auto_t<In>>(x);
Expand All @@ -1864,7 +1873,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto acos(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return acos_impl<detail::acos_auto_t<In>>(x);
#else
return dbl::acos_core<detail::acos_auto_t<In>>(x);
Expand All @@ -1875,7 +1884,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto sinh(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return sinh_impl<detail::sinh_auto_t<In>>(x);
#else
return dbl::sinh_core<detail::sinh_auto_t<In>>(x);
Expand All @@ -1886,7 +1895,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto cosh(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return cosh_impl<detail::cosh_auto_t<In>>(x);
#else
return dbl::cosh_core<detail::cosh_auto_t<In>>(x);
Expand All @@ -1897,7 +1906,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto tanh(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return tanh_impl<detail::tanh_auto_t<In>>(x);
#else
return dbl::tanh_core<detail::tanh_auto_t<In>>(x);
Expand All @@ -1909,7 +1918,7 @@ namespace bnd::math
{
static_assert(detail::require_snap<In>());
static_assert(Lower<In> > 0, "bnd::math::log10: input must be strictly positive");
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return log10_impl<detail::log10_auto_t<In>>(x);
#else
return dbl::log10_core<detail::log10_auto_t<In>>(x);
Expand All @@ -1920,7 +1929,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto cbrt(In x) noexcept
{
static_assert(detail::require_snap<In>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return cbrt_impl<detail::cbrt_auto_t<In>>(x);
#else
return dbl::cbrt_core<detail::cbrt_auto_t<In>>(x);
Expand All @@ -1931,7 +1940,7 @@ namespace bnd::math
[[nodiscard]] BND_MATH_FN auto hypot(InX x, InY y) noexcept
{
static_assert(detail::require_snap<InX>() && detail::require_snap<InY>());
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return hypot_impl<detail::hypot_auto_t<InX, InY>>(x, y);
#else
return dbl::hypot_core<detail::hypot_auto_t<InX, InY>>(x, y);
Expand All @@ -1944,7 +1953,7 @@ namespace bnd::math
{
static_assert(detail::require_snap<InB>() && detail::require_snap<InE>());
using Out = detail::pow_auto_t<InB, InE>;
#ifdef BND_MATH_FIXED
#ifdef BND_MATH_NO_FP
return pow_impl<Out>(base, exp);
#else
double b = static_cast<double>(base);
Expand Down
18 changes: 18 additions & 0 deletions include/bound/cmath_double.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
#include "bound/math.hpp" // bnd::detail::ldexp (constexpr, reproducible)
#include "bound/bound.hpp" // complete bound/rational + has_flag/BoundPolicy/real (store<>)

// BND_MATH_NO_FP — resolved here (the lowest math header) so both this file and
// cmath.hpp see it. When defined, the library uses NO hardware floating point and
// NO <cmath>: the double (FP) engine below compiles out and the always-present
// integer/CORDIC engine carries every transcendental. Define it (any value) to
// force the FP-free path; it is auto-enabled on freestanding targets
// (__STDC_HOSTED__ == 0) and whenever the integer engine is selected as the
// default (BND_MATH_FIXED). The integer engine is constexpr and bit-exact, so the
// public API and grid deduction are unchanged — only the compute backend differs.
#if !defined(BND_MATH_NO_FP)
# if defined(BND_MATH_FIXED) || (defined(__STDC_HOSTED__) && __STDC_HOSTED__ == 0)
# define BND_MATH_NO_FP
# endif
#endif

#ifndef BND_MATH_NO_FP // ===== FP engine present (needs <cmath> + an FPU) =====

#include <cmath> // std::fma, std::sqrt, std::nearbyint ONLY

// `BND_DBL_FN`: the engine cores become `constexpr` on C++26 toolchains with
Expand Down Expand Up @@ -254,4 +270,6 @@ namespace bnd::math::dbl
{ return store<Out>(detail::d_hypot(static_cast<double>(x), static_cast<double>(y))); }
} // namespace bnd::math::dbl

#endif // !BND_MATH_NO_FP

#endif // BNDcmathdoubleHPP
Loading
Loading