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
8 changes: 8 additions & 0 deletions docs/math.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ size/speed where double-grade precision isn't needed.
quantized onto the output grid. Ships its own golden pins
(`tests/test_math_engines.cpp`).

**Pair `flt` with `f32` storage.** An `f32`-backed operand holds a binary32 raw,
so `flt` reads it, computes, and stores the result straight in `float` — no
`double` round-trip. On a single-precision-only FPU that keeps the whole path in
hardware float; with `f64`/rational storage the boundary marshalling goes through
`double` (soft-float on such targets). Because binary32 has only a 24-bit
significand, an `f32` result grid that a function would overflow (e.g. `exp` of a
large argument on a fine grid) is a compile error — widen the grid or use `f64`.

## Compiling without floating point (`BND_MATH_NO_FP`)

On a target with no hardware FPU and no `<cmath>`, define **`BND_MATH_NO_FP`**
Expand Down
5 changes: 3 additions & 2 deletions docs/policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ is proven elsewhere. `clamp`, `wrap`, and `sentinel` are mutually exclusive
| `round_half_even` | banker's rounding — half to even (implies `snap`) |
| `ignore_zero` | skip the divide-by-zero check — `a / 0` / `a % 0` is UB (binary `div`/`mod`); compound `/= 0` / `%= 0` no-op |
| `ignore_domain` | suppress the runtime domain check |
| `f64` / `exact` / `direct` / `indexed` | **representation flags** — select how the raw value is stored; see the next section (`real` is a deprecated alias of `f64`) |
| `f64` / `f32` / `exact` / `direct` / `indexed` | **representation flags** — select how the raw value is stored; see the next section (`real` is a deprecated alias of `f64`) |

## Representation flags

Expand All @@ -60,6 +60,7 @@ Besides the *behavior* flags above, four flags select the **representation**
| Flag | Forces | Grid requirement | Notes |
|---|---|---|---|
| `f64` | IEEE-754 `double` raw (the value itself, snapped to the grid) | dyadic **and** double-exact (every value fits `double`'s 53-bit significand) | bundles `round_nearest`; the fast math-storage flag. Arithmetic drops `f64` to an exact representation when a result grid is too fine for `double`. Under `BND_MATH_FIXED` it falls back to integer storage. **`real` is a deprecated alias of `f64`.** |
| `f32` | IEEE-754 `float` raw (the value itself, snapped to the grid) | dyadic **and** float-exact (every value fits `float`'s 24-bit significand) | the binary32 sibling of `f64`, for single-precision FPUs and the `flt` engine. Arithmetic **demotes `f32`→`f64`** when a result grid outgrows `float` (then drops to exact when it outgrows `double`). Under `BND_MATH_FIXED` it falls back to integer storage. |
| `exact` | exact-fraction raw on **any** grid | none | no notch-count limit, no `double` anywhere; arithmetic is exact — on notched grids overflow is usually provably impossible and `+ − ×` return plain bounds (no `optional`) |
| `direct` | raw == value as a plain integer | `Notch == 1` | e.g. `bound<{5, 100}, direct>` stores 5..100, not index 0..95 — the raw equals the wire/debugger value |
| `indexed` | raw == 0-based notch index | `Notch != 0` | e.g. `bound<{-5, 5}, indexed>` stores 0..10 unsigned — dense layout for serialization |
Expand All @@ -73,7 +74,7 @@ using slot = bound<{-5, 5}, indexed>; // raw() == 0..10, dense unsigned

Binary arithmetic ORs the policies of both operands, so a result can carry
several representation flags; storage selection resolves them
**widest-wins**: `exact > f64 > direct > indexed > deduced`. An
**widest-wins**: `exact > f64 > f32 > direct > indexed > deduced`. An
`exact + f64` sum is therefore exact, and a `f64` math chain stays
double-backed end to end — no errors at mixed call sites.

Expand Down
8 changes: 4 additions & 4 deletions include/bound/cmath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ namespace bnd::math
&& !rational_raw<Out>
// `real` storage holds the VALUE, not an offset index, so route it
// through the rational fallback `Out{r}` (same guard as fmod_int_fast).
&& !real_raw<Out>
&& !fp_raw<Out>
&& has_flag(BoundPolicy<Out>, round_nearest)
&& (std::signed_integral<raw_t<Out>>
|| NotchCount<Out>
Expand Down Expand Up @@ -907,9 +907,9 @@ namespace bnd::math
// * all unit counts fit comfortably in imax (headroom 4).
template <boundable Out, boundable InX, boundable InY>
inline constexpr bool fmod_int_fast = []{
if (rational_raw<InX> || real_raw<InX>
|| rational_raw<InY> || real_raw<InY>
|| rational_raw<Out> || real_raw<Out>)
if (rational_raw<InX> || fp_raw<InX>
|| rational_raw<InY> || fp_raw<InY>
|| rational_raw<Out> || fp_raw<Out>)
return false;
if (Notch<InX> == 0 || Notch<InY> == 0 || Notch<Out> == 0)
return false;
Expand Down
7 changes: 4 additions & 3 deletions include/bound/cmath_double.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,10 @@ namespace bnd::math::dbl
template <typename Out>
[[nodiscard]] BND_DBL_FN Out store(double d)
{
// `real` (double-backed) Out stores the double directly; a non-`real` snap grid
// assigns through the rational path, snapping via Out's round policy.
if constexpr (has_flag(BoundPolicy<Out>, real)) return Out{d};
// An fp-backed Out (f64 or f32) stores the value directly via its raw (an f32
// Out narrows double→float, lossless on its float-exact grid); a non-fp snap
// grid assigns through the rational path, snapping via Out's round policy.
if constexpr (bnd::detail::fp_raw<Out>) return Out{d};
else { Out o{}; o = bnd::detail::rational{d}; return o; }
}

Expand Down
10 changes: 5 additions & 5 deletions include/bound/cmath_float.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,14 @@ namespace bnd::math::flt::detail
namespace bnd::math::flt
{
// Engine cores: bound in → `float` math → bound out. Storing the float result:
// a `real` (double-backed) bound takes Out{double(f)} (widening is exact); a
// non-`real` snap grid assigns through the rational path, snapping via Out's
// round policy. (An f32-backed bound — Phase 4b — will store the float raw
// directly; until then it routes through the same paths.)
// an fp-backed Out (f32 OR f64) stores the value directly via its float/double
// raw (the natural pairing for `flt` is `f32` — no rational, no double round-
// trip on the result); any other snap grid assigns through the rational path,
// snapping via Out's round policy.
template <typename Out>
[[nodiscard]] BND_DBL_FN Out store(float f)
{
if constexpr (has_flag(BoundPolicy<Out>, real)) return Out{static_cast<double>(f)};
if constexpr (bnd::detail::fp_raw<Out>) return Out{static_cast<double>(f)};
else { Out o{}; o = bnd::detail::rational{static_cast<double>(f)}; return o; }
}

Expand Down
45 changes: 26 additions & 19 deletions include/bound/core.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,12 @@ namespace bnd
// no grid to snap to. Anything else is rejected here rather than silently
// demoted to integer storage.
static_assert(!has_flag(P, real) || detail::dyadic_grid<G> || G.Notch == 0,
"bnd: the `real` policy requires a dyadic grid (power-of-two "
"bnd: the `real`/`f64` policy requires a dyadic grid (power-of-two "
"notch and Lower, so values are exactly representable in double)");
static_assert(!has_flag(P, f32) || detail::dyadic_grid<G> || G.Notch == 0,
"bnd: the `f32` policy requires a dyadic grid (power-of-two notch "
"and Lower); values must also fit float's 24-bit significand "
"(checked at storage selection — see `float_exact`)");
#endif
// Representation flags vs grid shape (exact has no requirement; a result
// policy may carry several flags — storage selection resolves widest-wins,
Expand Down Expand Up @@ -110,8 +114,9 @@ namespace bnd
// Value-init `bound{}` still zero-fills where a zero raw is genuinely wanted.)
constexpr bound() = default;

// `real` storage holds the value as a double directly. An arithmetic rhs
// casts straight to double; a bound rhs goes through its exact rational view.
// fp storage (f64/f32) holds the value as a floating raw directly. An
// arithmetic rhs casts straight to double; a bound rhs goes through its exact
// rational view.
private:
template <numeric A>
constexpr double to_double(A const& value)
Expand All @@ -120,10 +125,12 @@ namespace bnd
else return static_cast<double>(detail::as_rational(value));
}
public:
// Snap a value onto `real` storage: lossless on the dyadic grid. Out-of-range
// values run the same policy cascade as the fractional path (clamp → wrap →
// sentinel/checked-report → store as-is); all arithmetic stays in double.
constexpr void store_real(double v)
// Snap a value onto fp storage: lossless on the (fp-exact) dyadic grid — the
// snap is computed in double and narrowed to the raw type (double or float),
// which is exact because every grid point fits the raw's significand. Out-of-
// range values run the same policy cascade as the fractional path (clamp →
// wrap → sentinel/checked-report → store as-is).
constexpr void store_fp(double v)
{
// NaN/±inf would reach snap_double's integer cast (UB); reject like the
// non-real path. `v - v` is 0 for every finite v, NaN otherwise.
Expand Down Expand Up @@ -159,20 +166,20 @@ namespace bnd
return; // sentinel stored / reported (error_code mode)
// no handler (unchecked policy): fall through and store snapped as-is
}
Raw = G.snap_double(v);
Raw = static_cast<raw_type>(G.snap_double(v)); // narrow to float for f32 (lossless)
}

template <numeric A>
constexpr void store_value(A const& value)
{
if constexpr (detail::real_raw<bound>)
store_real(to_double(value));
if constexpr (detail::fp_raw<bound>)
store_fp(to_double(value));
else if constexpr (is_bound_v<A>)
{
// A `real` SOURCE holds its value as a double raw; the assignment engine's
// integer offset formula (Lower + raw·Notch) would misread it. Extract as
// a double and route through the arithmetic-source path.
if constexpr (detail::real_raw<A>)
if constexpr (detail::fp_raw<A>)
detail::assignment<bound, double>::assign(*this, detail::as_double(value), make_policy<P>());
else
detail::assignment<bound, A>::assign(*this, value, make_policy<P>());
Expand All @@ -193,8 +200,8 @@ namespace bnd
// The one-shot `pol` widens the assignable check (a clamp/round passed here
// relaxes the notch/interval clause), so a notch-incompatible boundable source
// is accepted — e.g. clamp_round<B>(some_bound). Body honours `pol` as before.
if constexpr (detail::real_raw<bound>)
store_real(to_double(value));
if constexpr (detail::fp_raw<bound>)
store_fp(to_double(value));
else
detail::assignment<bound, A>::assign(*this, value, pol);
}
Expand All @@ -207,8 +214,8 @@ namespace bnd
requires bound_assignable<bound, A, P>
constexpr bound(A value, errc& ec)
{
if constexpr (detail::real_raw<bound>)
store_real(to_double(value));
if constexpr (detail::fp_raw<bound>)
store_fp(to_double(value));
else
detail::assignment<bound, A>::assign(*this, value, make_policy<P>(ec));
}
Expand Down Expand Up @@ -315,7 +322,7 @@ namespace bnd
&& G.Interval.Upper <= bnd::detail::rational{std::numeric_limits<imax>::max()})
{ return detail::to_value(*this); }

constexpr explicit(!has_flag(P, real)) operator double() const
constexpr explicit(!has_flag(P, real) && !has_flag(P, f32)) operator double() const
requires ((P & (round_floor | round_ceil | round_nearest
| round_half_even | snap)) != 0)
{ return detail::as_double(*this); }
Expand Down Expand Up @@ -430,7 +437,7 @@ namespace bnd
[[nodiscard]] constexpr negative operator-() const
{
negative neg;
if constexpr (detail::real_raw<bound>)
if constexpr (detail::fp_raw<bound>)
neg = negative::from_raw(-Raw);
else if constexpr (detail::rational_raw<bound>)
neg = negative::from_raw(-(Raw));
Expand Down Expand Up @@ -703,7 +710,7 @@ namespace bnd
if constexpr (Grid<L> == Grid<R>)
return lhs.raw() <=> rhs.raw();
// double-backed (`real`) operand: compare in double (raw_imax would truncate)
else if constexpr (detail::real_raw<L> || detail::real_raw<R>)
else if constexpr (detail::fp_raw<L> || detail::fp_raw<R>)
return detail::as_double(lhs) <=> detail::as_double(rhs);
// both integer-direct (notch=1, Raw==value): compare as integers
else if constexpr (!detail::rational_raw<L> && !detail::rational_raw<R>
Expand All @@ -718,7 +725,7 @@ namespace bnd
{
if constexpr (Grid<L> == Grid<R>)
return lhs.raw() == rhs.raw();
else if constexpr (detail::real_raw<L> || detail::real_raw<R>)
else if constexpr (detail::fp_raw<L> || detail::fp_raw<R>)
return detail::as_double(lhs) == detail::as_double(rhs);
else if constexpr (!detail::rational_raw<L> && !detail::rational_raw<R>
&& !detail::index_raw<L> && !detail::index_raw<R>)
Expand Down
22 changes: 14 additions & 8 deletions include/bound/detail/addition.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@ 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 `real` only when the result grid stays exactly representable in
// double; otherwise drop it so storage_pick deduces an exact representation
// (the double sum would diverge from the exact sum — see grid::double_exact).
static constexpr bool any_real =
// 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 keep_real = any_real && double_exact<result_grid>;
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_real ? bnd::real : none);
| (keep_f64 ? bnd::real : none) | (keep_f32 ? bnd::f32 : none);
using result = bound<result_grid, rep != none ? rep : checked>;

template <policy_flag F>
Expand Down Expand Up @@ -68,9 +74,9 @@ namespace bnd::detail
static constexpr auto add(L lhs, R rhs, policy<F, E> policy = {}, A&& action = {}) -> add_return_t<F, A>
{
result res;
if constexpr (real_raw<result>)
if constexpr (fp_raw<result>)
{
res = result::from_raw(Grid<result>.snap_double(as_double(lhs) + as_double(rhs)));
res = result::from_raw(raw_cast<result>(Grid<result>.snap_double(as_double(lhs) + as_double(rhs))));
}
else if constexpr (rational_raw<result>)
{
Expand Down
18 changes: 9 additions & 9 deletions include/bound/detail/assignment.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ namespace bnd::detail
// or NotchCount, no rounding. real takes the endpoint as a double, rational
// the exact constant (a double round-trip would lose non-dyadic endpoints);
// raw_from_offset<L> adds Lower back for direct-encoded storage.
if constexpr (real_raw<L>)
if constexpr (fp_raw<L>)
lhs = L::from_raw((rhs < Lower<L>) ? static_cast<double>(Lower<L>)
: static_cast<double>(Upper<L>));
else if constexpr (rational_raw<L>)
Expand Down Expand Up @@ -263,10 +263,10 @@ namespace bnd::detail
{
if constexpr (rational_raw<L> && Notch<L> == 0)
{ lhs = L::from_raw(rhs); return true; } // continuous: store verbatim
else if constexpr (real_raw<L>)
else if constexpr (fp_raw<L>)
{
// real target: raw IS the value — snap to the dyadic grid (range handling
// already ran in the assign cascade; finite guard mirrors store_real's).
// already ran in the assign cascade; finite guard mirrors store_f64's).
const double v = static_cast<double>(rhs);
if (!(v - v == 0))
detail::raise(errc::not_finite, "non-finite double");
Expand Down Expand Up @@ -306,7 +306,7 @@ namespace bnd::detail
// instead of two rational ops. round_quotient is invariant under reduction,
// so the slot is bit-identical to the rational path. Oversized denominators
// fall through (the kMaxDen guard keeps every product inside imax).
if constexpr (HasQFormatFastPath<L> && !real_raw<L> && Notch<L> != 0)
if constexpr (HasQFormatFastPath<L> && !fp_raw<L> && Notch<L> != 0)
{
constexpr imax K = abs_den(Notch<L>.Denominator);
constexpr imax Lo = LowerImax<L>;
Expand Down Expand Up @@ -399,7 +399,7 @@ namespace bnd::detail
if constexpr (rational_raw<L>)
return Lower<R>;
else if constexpr (Notch<L> == 0)
// Continuous real_raw L: no grid to land on, mapping unused (store
// Continuous fp_raw L: no grid to land on, mapping unused (store
// routes through snap_double). 0 avoids the /Notch<L> divide-by-zero.
return rational{0};
else if constexpr (rational_raw<R>)
Expand All @@ -413,7 +413,7 @@ namespace bnd::detail
if constexpr (rational_raw<L>)
return Notch<R>;
else if constexpr (Notch<L> == 0)
// Continuous real_raw L (see calcOffset). A denominator-1 Factor also
// Continuous fp_raw L (see calcOffset). A denominator-1 Factor also
// makes assign_notch_ok vacuously true (any value representable).
return rational{0};
else if constexpr (rational_raw<R>)
Expand All @@ -430,7 +430,7 @@ namespace bnd::detail
// sides (not rational, not real).
static constexpr bool is_integer_mapping =
!rational_raw<L> && !rational_raw<R>
&& !real_raw<L> && !real_raw<R>
&& !fp_raw<L> && !fp_raw<R>
&& abs_den(Factor.Denominator) == 1 && abs_den(Offset.Denominator) == 1;

// Map rhs.Raw into L's raw space (requires is_integer_mapping). The
Expand Down Expand Up @@ -469,7 +469,7 @@ namespace bnd::detail
{
// RawLo/RawHi are already the correct Raw (no raw_from_offset). Real storage
// takes the endpoint as a double (RawLo/Hi truncate fractional dyadic endpoints).
if constexpr (real_raw<L>)
if constexpr (fp_raw<L>)
lhs = L::from_raw((as_rational(rhs) < Lower<L>)
? static_cast<double>(Lower<L>) : static_cast<double>(Upper<L>));
else
Expand Down Expand Up @@ -538,7 +538,7 @@ namespace bnd::detail
template<typename P>
static constexpr void store(L& lhs, R const& rhs, P&&)
{
if constexpr (real_raw<L>)
if constexpr (fp_raw<L>)
// real target: raw IS the value — decode the source and snap to the dyadic
// grid (the offset machinery below mis-encodes a double raw).
lhs = L::from_raw(Grid<L>.snap_double(as_double(rhs)));
Expand Down
Loading
Loading