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
18 changes: 17 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ target_include_directories(bound_tests SYSTEM PRIVATE ${CATCH2_INCLUDE_DIRS})
# PRE_TEST defers discovery to test invocation, under the configured emulator.
catch_discover_tests(bound_tests DISCOVERY_MODE PRE_TEST)

# Compile-fail diagnostics suite — regression-tests that ill-formed uses stay
# ill-formed and keep their intended messages (see tests/fail/CMakeLists.txt).
add_subdirectory(tests/fail)

#---------------------------------------------------------------------------
# ctrack (benchmarking)
#---------------------------------------------------------------------------
Expand All @@ -183,6 +187,18 @@ target_include_directories(bench SYSTEM PUBLIC ${CTRACK_INCLUDE_DIRS})
add_executable(bound_fuzz EXCLUDE_FROM_ALL tests/fuzz.cpp)
target_link_libraries(bound_fuzz PUBLIC bound::bound)

# Accuracy characterization (fpm-style): sweeps every math engine against a
# long-double <cmath> reference and emits a markdown error table. On demand:
# `--target bound_accuracy` to build, `--target accuracy_report` to regenerate
# the committed docs/accuracy.md.
add_executable(bound_accuracy EXCLUDE_FROM_ALL tests/accuracy.cpp)
target_link_libraries(bound_accuracy PUBLIC bound::bound)

add_custom_target(accuracy_report
COMMAND bound_accuracy ${CMAKE_CURRENT_SOURCE_DIR}/docs/accuracy.md
COMMENT "Regenerating docs/accuracy.md")
add_dependencies(accuracy_report bound_accuracy)

#---------------------------------------------------------------------------
# MinGW link workaround
#---------------------------------------------------------------------------
Expand All @@ -192,7 +208,7 @@ target_link_libraries(bound_fuzz PUBLIC bound::bound)
# "multiple definition of std::type_info::operator==". Tolerate it on MinGW
# (the two definitions are identical); harmless elsewhere, so it stays scoped.
if(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
foreach(tgt bound_tests bench bound_fuzz)
foreach(tgt bound_tests bench bound_fuzz bound_accuracy)
target_link_options(${tgt} PRIVATE -Wl,--allow-multiple-definition)
endforeach()
endif()
Expand Down
61 changes: 61 additions & 0 deletions docs/accuracy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Engine accuracy

Generated by `bound_accuracy` (tests/accuracy.cpp) — do not edit by hand.
Regenerate with `cmake --build <build-dir> --target accuracy_report`.

Error is measured against a long-double `<cmath>` reference, in units of
the **output grid notch**: max < 0.5 means every sampled result is the
correctly rounded grid point. Engines: `cordic` (integer/CORDIC, FPU-free,
bit-exact everywhere), `dbl` (reproducible binary64), `flt` (binary32).
See [math.md](math.md) for the engine model.

| fn | engine | domain | max (notch) | mean (notch) | samples |
|---------|--------|----------------|-------------|--------------|---------|
| sin | cordic | [-8, 8] | 0.7516 | 0.263837 | 20165 |
| sin | dbl | [-8, 8] | 0.5000 | 0.249295 | 20165 |
| sin | flt | [-8, 8] | 0.5004 | 0.249295 | 20165 |
| cos | cordic | [-8, 8] | 0.7423 | 0.260149 | 20165 |
| cos | dbl | [-8, 8] | 0.5000 | 0.248520 | 20165 |
| cos | flt | [-8, 8] | 0.5005 | 0.248520 | 20165 |
| tan | cordic | [-1.5, 1.5] | 0.5006 | 0.247359 | 24577 |
| tan | dbl | [-1.5, 1.5] | 0.5000 | 0.247359 | 24577 |
| tan | flt | [-1.5, 1.5] | 0.5178 | 0.247367 | 24577 |
| asin | cordic | [-1, 1] | 0.5001 | 0.243364 | 32769 |
| asin | dbl | [-1, 1] | 0.5000 | 0.243364 | 32769 |
| asin | flt | [-1, 1] | 0.5015 | 0.243365 | 32769 |
| acos | cordic | [-1, 1] | 0.5000 | 0.244882 | 32769 |
| acos | dbl | [-1, 1] | 0.5000 | 0.244882 | 32769 |
| acos | flt | [-1, 1] | 0.5035 | 0.244887 | 32769 |
| atan | cordic | [-8, 8] | 0.5218 | 0.248050 | 20165 |
| atan | dbl | [-8, 8] | 0.5000 | 0.247945 | 20165 |
| atan | flt | [-8, 8] | 0.5018 | 0.247946 | 20165 |
| sinh | cordic | [-8, 8] | 0.8942 | 0.248963 | 20165 |
| sinh | dbl | [-8, 8] | 0.5000 | 0.247560 | 20165 |
| sinh | flt | [-8, 8] | 3.1212 | 0.298317 | 20165 |
| cosh | cordic | [-8, 8] | 0.8530 | 0.253277 | 20165 |
| cosh | dbl | [-8, 8] | 0.5000 | 0.251411 | 20165 |
| cosh | flt | [-8, 8] | 2.7390 | 0.297414 | 20165 |
| tanh | cordic | [-8, 8] | 0.5000 | 0.204307 | 20165 |
| tanh | dbl | [-8, 8] | 0.5000 | 0.204307 | 20165 |
| tanh | flt | [-8, 8] | 0.5007 | 0.204308 | 20165 |
| exp | cordic | [-8, 8] | 0.8889 | 0.250463 | 20165 |
| exp | dbl | [-8, 8] | 0.5000 | 0.249378 | 20165 |
| exp | flt | [-8, 8] | 2.7675 | 0.284219 | 20165 |
| exp2 | cordic | [-8, 8] | 0.6270 | 0.250352 | 20165 |
| exp2 | dbl | [-8, 8] | 0.5000 | 0.250156 | 20165 |
| exp2 | flt | [-8, 8] | 1.5950 | 0.265997 | 20165 |
| log | cordic | [1, 1000] | 0.5234 | 0.249774 | 20010 |
| log | dbl | [1, 1000] | 0.5000 | 0.249657 | 20010 |
| log | flt | [1, 1000] | 0.5044 | 0.249671 | 20010 |
| log2 | cordic | [1, 1000] | 0.5186 | 0.248581 | 20010 |
| log2 | dbl | [1, 1000] | 0.5000 | 0.248472 | 20010 |
| log2 | flt | [1, 1000] | 0.5106 | 0.248516 | 20010 |
| log10 | cordic | [1, 1000] | 0.5000 | 0.252197 | 20010 |
| log10 | dbl | [1, 1000] | 0.5000 | 0.252197 | 20010 |
| log10 | flt | [1, 1000] | 0.5045 | 0.252205 | 20010 |
| sqrt | cordic | [0, 16] | 0.5087 | 0.248846 | 20165 |
| sqrt | dbl | [0, 16] | 0.5000 | 0.248835 | 20165 |
| sqrt | flt | [0, 16] | 0.5019 | 0.248838 | 20165 |
| cbrt | cordic | [-8, 8] | 0.5004 | 0.250647 | 20165 |
| cbrt | dbl | [-8, 8] | 0.5000 | 0.250647 | 20165 |
| cbrt | flt | [-8, 8] | 0.5016 | 0.250649 | 20165 |
4 changes: 4 additions & 0 deletions docs/math.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ other representation flags.
output grid.
- Algebraically-exact results (e.g. `cbrt(8)`, `hypot(3,4)`, `pow(2,10)`)
land exactly under both engines.
- Measured per-function error tables for all three engines (max/mean in
output-notch units, against a long-double reference) are in
[accuracy.md](accuracy.md), regenerated by the `accuracy_report` build
target.
- **Input-range limits** below are engine-shared `static_assert` envelopes,
kept identical for both engines so the same programs compile everywhere.
The trig/root/atan limits (±2^20) come from the integer engine's working
Expand Down
42 changes: 42 additions & 0 deletions docs/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,48 @@ column. Pick the tool that fits: if you need overflow-safe plain integers,
`bounded::integer` or Boost.SafeNumerics are proven; for pure fixed-point DSP,
`fpm` or CNL; `bound` is for when the *domain* of a value is part of its type.

## Implementation patterns compared

A 2026-07 pass over these libraries' *implementation* patterns (not just their
feature sets), asking what `bound` should adopt. Kept, with the source named:

- **Compile-fail diagnostics tests** (bounded::integer, CNL) — `tests/fail/`
regression-tests that ill-formed uses stay ill-formed *and* keep their
intended `static_assert` message (each TU names its expected diagnostic in a
`// EXPECT:` line; a control TU proves the harness builds valid code).
- **Published accuracy characterization** (fpm) — fpm ships per-function
accuracy tables for its math; `bound` now does too:
[accuracy.md](accuracy.md), generated by `tests/accuracy.cpp` sweeping all
three engines against a long-double reference. (Its first run found a real
bug: a 64-bit overflow terminate in the fp→snap-grid store path.)
- **`std::common_type` interop** (bounded::integer) — mixed-grid bounds now
have a common type (the grid hull: interval hull + notch gcd), plus
mixed-grid `bnd::min`/`max` returning it.
- **Single-concern helpers** (CNL's component style) — the per-operator fp
representation-propagation logic, once copy-pasted across
addition/multiplication/division, is one trait (`detail/rep.hpp`).

Considered and **not** adopted, with reasons:

- **CRTP operator mix-ins** (PSsst, and Boost's operator-generation style) —
`bound`'s operators dispatch on storage shape with `if constexpr`, which a
generic mix-in base can't see; hand-written operators stay.
- **Selectable promotion policies** (Boost.SafeNumerics `native`/`automatic`) —
`bound` always auto-widens the result grid; a "native" mode would reintroduce
exactly the silent narrowing the library exists to prevent.
- **Constraint/verifier splitting** (type_safe) — the policy bitmask plus
tagged `on_*` actions already separate *what* is enforced from *what happens*
on violation, without a second customization axis.
- **Type-per-concern wrapper stacking** (CNL) — composing
`overflow<rounding<scaled<int>>>`-style layers trades one flat policy set for
N interacting wrapper semantics; `bound`'s single `policy_flag` set keeps one
place to reason about flag interaction (mutual exclusions are
`static_assert`ed in one spot).
- **Disjoint interval unions** (Intel safe-arithmetic) — genuinely attractive
(zero-excluding divisors make division total); parked as a
[roadmap sketch](roadmap.md) because every operator and predicate would need
a union story.

## Related libraries

- **[bounded::integer](https://github.com/davidstone/bounded-integer)** (David
Expand Down
15 changes: 15 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ For completeness — these came up alongside the above but are *not* blocked by
with no language dependency. See [freestanding.md](freestanding.md).
- **`dyn_bound`** (runtime-valued bounds) — evaluated and **declined** on design grounds
(no space/time-efficient implementation), not deferred.
- **Type-level interval unions** — Intel's [safe-arithmetic](resources.md) models
*disjoint* interval unions in the type (`ival<-1000,-1> || ival<1,1000>`), so e.g. a
zero-excluding divisor makes division provably total at compile time. `bound`'s
`grid::operator/` already computes the two zero-free halves internally
(`grid.hpp`) — the missing piece is expressing the union in the *type* (a
`bound` over a set of grids) rather than collapsing to the hull. Large surface
(every operator/predicate would need a union story), so this stays a design
sketch until a concrete use case demands it.
- **128-bit rounded store** — the cold assignment path forms `(rhs − Lower)/Notch` as
an exact 64-bit rational before rounding; a full-mantissa `double`-derived source
(denominator 2^52+) on a grid with large `|Lower|` can need 65+ bits *before* the
round even though the rounded slot index is tiny. Mixed-sign offsets are rescued in
128-bit and the rest report `errc::overflow` today; computing the *rounded* index
directly in 128-bit (as `to_fixed` already does in `cmath.hpp`) would make those
stores exact instead of an error.
- **Modules / compile-time-footprint work** — parked for later; modules is C++20, not a
blocker.

Expand Down
41 changes: 41 additions & 0 deletions include/bound/arithmetic.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "bound/core.hpp"
#include "bound/casts.hpp"
#include "bound/detail/rep.hpp"

#include <algorithm>
#include <ranges>
Expand Down Expand Up @@ -245,6 +246,36 @@ namespace bnd
boundable auto t)
{ return a + (b - a) * t; }

//---------------------------------------------------------------------------
// common_bound — the "hull" type able to hold every value of L and R exactly:
// interval hull + notch gcd (grid `hull`), representation propagated by the
// same widest-wins rule as arithmetic results (detail::fp_rep). Backs the
// std::common_type specialisation (numeric_limits.hpp) and mixed-grid
// min/max below. The primary has no `type` when the hull grid is
// unrepresentable, so common_type_t SFINAEs away instead of erroring.
//---------------------------------------------------------------------------
namespace detail
{
template <boundable Lhs, boundable Rhs>
struct common_bound {};

// Same type stays itself (policy included) — mirrors std::common_type<T, T>.
template <boundable Same>
struct common_bound<Same, Same> { using type = Same; };

template <boundable Lhs, boundable Rhs>
requires (!std::same_as<Lhs, Rhs>) && (hull(Grid<Lhs>, Grid<Rhs>).has_value())
struct common_bound<Lhs, Rhs>
{
static constexpr grid hull_grid = *hull(Grid<Lhs>, Grid<Rhs>);
using type = bound<hull_grid,
fp_rep<Lhs, Rhs, hull_grid, /*AllowContinuous=*/true>::result_policy>;
};
}

template <boundable Lhs, boundable Rhs>
using common_bound_t = typename detail::common_bound<Lhs, Rhs>::type;

//---------------------------------------------------------------------------
// std-vocabulary helpers — ADL-found `min` / `max` / `midpoint` for generic
// code. min/max mirror std; midpoint returns the *exact* average on a refined
Expand All @@ -257,6 +288,16 @@ namespace bnd
template <boundable T>
[[nodiscard]] constexpr T max(T a, T b) { return (a < b) ? b : a; }

// Mixed-grid forms return the common hull type (both operands convert
// losslessly — the hull is assignable from each by construction).
template <boundable Lhs, boundable Rhs> requires (!std::same_as<Lhs, Rhs>)
[[nodiscard]] constexpr auto min(Lhs a, Rhs b) -> common_bound_t<Lhs, Rhs>
{ common_bound_t<Lhs, Rhs> ca{a}, cb{b}; return (cb < ca) ? cb : ca; }

template <boundable Lhs, boundable Rhs> requires (!std::same_as<Lhs, Rhs>)
[[nodiscard]] constexpr auto max(Lhs a, Rhs b) -> common_bound_t<Lhs, Rhs>
{ common_bound_t<Lhs, Rhs> ca{a}, cb{b}; return (ca < cb) ? cb : ca; }

template <boundable T>
[[nodiscard]] constexpr auto midpoint(T a, T b) { return (a + b) * just<frac<1, 2>>; }

Expand Down
21 changes: 4 additions & 17 deletions include/bound/detail/addition.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#ifndef BNDadditionHPP
#define BNDadditionHPP

#include "bound/detail/rep.hpp"
#include "bound/generic.hpp"
#include "bound/grid.hpp"
#include "bound/policy.hpp"
Expand All @@ -24,23 +25,9 @@ namespace bnd::detail
"addition: result grid's notch/interval exceeds the representable rational "
"range — coarsen the operand grids");
static constexpr grid result_grid = (Grid<L> + Grid<R>).value();
// Propagate fp storage only when the result grid stays exactly representable
// in the chosen width; otherwise demote (f32→f64) or drop it so storage_pick
// deduces an exact representation (the fp sum would diverge from the exact sum
// — see grid::double_exact / float_exact). Widest-wins: prefer f32 only when
// both operands are f32-only and the result fits float; an f64 operand or a
// too-fine-for-float result widens to f64; too fine for double → exact.
static constexpr bool any_f64 =
(BoundPolicy<L> & bnd::real) == bnd::real || (BoundPolicy<R> & bnd::real) == bnd::real;
static constexpr bool any_f32 =
(BoundPolicy<L> & bnd::f32) == bnd::f32 || (BoundPolicy<R> & bnd::f32) == bnd::f32;
static constexpr bool keep_f32 = any_f32 && !any_f64 && float_exact<result_grid>;
static constexpr bool keep_f64 = !keep_f32 && (any_f64 || any_f32) && double_exact<result_grid>;
// Carry both operands' representation flags (widest-wins at storage selection).
static constexpr policy_flag rep =
((BoundPolicy<L> | BoundPolicy<R>) & (bnd::exact | bnd::direct | bnd::indexed))
| (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none);
using result = bound<result_grid, rep != none ? rep : checked>;
// fp / representation propagation — shared rule in detail/rep.hpp.
using rep_t = fp_rep<L, R, result_grid>;
using result = bound<result_grid, rep_t::result_policy>;

template <policy_flag F>
static constexpr bool needs_overflow_check =
Expand Down
14 changes: 13 additions & 1 deletion include/bound/detail/assignment.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,19 @@ namespace bnd::detail
}
}

rational raw = ((rhs - Lower<L>)/Notch<L>).value();
// The exact quotient can overflow the 64-bit rational range (huge
// source denominator × fine notch). Report it as an overflow through
// the policy channel instead of dereferencing nullopt — a throw here
// would escape noexcept callers (the math engines) as terminate.
const auto quotient = (rhs - Lower<L>)/Notch<L>;
if (!quotient.has_value()) [[unlikely]]
{
if constexpr (error_action<plain<A>>)
{ action.fn(lhs, errc::overflow, errc_message(errc::overflow)); return false; }
policy.report(errc::overflow);
return false;
}
rational raw = *quotient;
umax den = static_cast<umax>(raw.Denominator);
if (den == 1)
{ store_slot(raw.Numerator); return true; }
Expand Down
21 changes: 5 additions & 16 deletions include/bound/detail/division.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#ifndef BNDdivisionHPP
#define BNDdivisionHPP

#include "bound/detail/rep.hpp"
#include "bound/generic.hpp"
#include "bound/grid.hpp"
#include "bound/policy.hpp"
Expand Down Expand Up @@ -154,22 +155,10 @@ namespace bnd::detail
? grid{interval{rational{0}, (Upper<L> / Notch<R>).value()}, Notch<L>}
: *(Grid<L> / Grid<R>);

static constexpr bool any_f64 =
(BoundPolicy<L> & bnd::real) == bnd::real || (BoundPolicy<R> & bnd::real) == bnd::real;
static constexpr bool any_f32 =
(BoundPolicy<L> & bnd::f32) == bnd::f32 || (BoundPolicy<R> & bnd::f32) == bnd::f32;
// Keep fp for a continuous result (Notch 0: the raw stores the quotient
// verbatim) or an fp-exact dyadic result; otherwise demote f32→f64 / drop f64
// (the fp quotient would not land on the result grid). Widest-wins as in mul.
static constexpr bool keep_f32 =
any_f32 && !any_f64 && (result_grid.Notch == 0 || float_exact<result_grid>);
static constexpr bool keep_f64 =
!keep_f32 && (any_f64 || any_f32) && (result_grid.Notch == 0 || double_exact<result_grid>);
// Carry both operands' representation flags (widest-wins at storage selection).
static constexpr policy_flag rep =
((BoundPolicy<L> | BoundPolicy<R>) & (bnd::exact | bnd::direct | bnd::indexed))
| (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none);
using result = bound<result_grid, rep != none ? rep : checked>;
// fp / representation propagation — shared rule in detail/rep.hpp.
// AllowContinuous: a continuous quotient (Notch 0) keeps fp verbatim.
using rep_t = fp_rep<L, R, result_grid, /*AllowContinuous=*/true>;
using result = bound<result_grid, rep_t::result_policy>;

template <policy_flag G = F>
static constexpr bool needs_overflow_check =
Expand Down
Loading
Loading