From c1c55194f46036facecefe7f7666461cae9c98d1 Mon Sep 17 00:00:00 2001 From: Peter Neiss Date: Sun, 19 Jul 2026 19:57:25 +0200 Subject: [PATCH] Docs update --- README.md | 237 ++++++++++-------------------------------- docs/arithmetic.md | 18 ++-- docs/conversions.md | 7 +- docs/determinism.md | 73 +++++++------ docs/diagnostics.md | 10 +- docs/fixed-point.md | 11 +- docs/freestanding.md | 66 +++++++----- docs/internals.md | 23 ++-- docs/math.md | 61 +++++------ docs/policies.md | 4 +- docs/resources.md | 7 +- docs/roadmap.md | 10 +- docs/single-header.md | 50 +++++++++ docs/storage.md | 9 +- docs/tutorial.md | 15 ++- 15 files changed, 291 insertions(+), 310 deletions(-) create mode 100644 docs/single-header.md diff --git a/README.md b/README.md index e43de5c..800af73 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,26 @@ [![CI](https://github.com/NiceAndPeter/bound/actions/workflows/ci.yml/badge.svg)](https://github.com/NiceAndPeter/bound/actions/workflows/ci.yml) -> **Status: alpha.** The library is under active development and the public API -> may change between versions. `bound` was developed with -> [Claude Code](https://claude.com/claude-code), Anthropic's agentic coding tool. - -A header-only C++23 library providing safe arithmetic on bounded rational -number grids. A grid is defined by a lower and upper (inclusive) bound, hence -the name, and a notch (step size); all three are exact fractions, and -`(Upper − Lower) / Notch` must be an unsigned integer. You write those -fractions with literals, `notch`, and `frac` — the exact-fraction -representation itself is an internal detail you never name. - -Arithmetic operators (`+`, `-`, `*`, `/`) are type-safe by construction: the -result type's grid is computed at compile time to contain every possible -value. Policy only governs what happens when a value is **assigned** or -**converted** into a narrower type. +> **Status: alpha** — the public API may change between versions. Developed with +> [Claude Code](https://claude.com/claude-code). + +A header-only C++23 library (C++20 fallback) for numbers that **cannot go out of +range** — the range and step size live in the *type*. + +- **Arithmetic cannot overflow.** `+ - * /` widen the result type at compile + time to hold every possible value — no runtime surprises. +- **You decide what happens at the edges.** Out-of-range is only possible when a + value is assigned into a narrower type, and a policy you pick — `clamp` + (saturate), `wrap` (modular), `sentinel`, checked error — decides the outcome. +- **It's fixed-point, done by the compiler.** Think Qm.n with the scale and + range checked for you; the optimal raw storage (uint8…int64, double, exact + fraction) is picked automatically. +- **Reproducible math.** `sin`/`cos`/`sqrt`/`exp`/… give bit-identical results + across platforms — three engines, including an FPU-free `constexpr` CORDIC + engine for bare metal. +- **Built for:** audio samples, money, percentages, PID controllers, sensor + fusion, embedded registers — anywhere a plain `int`/`float` silently + overflows, wraps, or drifts. ## Quick start @@ -29,6 +34,7 @@ using pct = bound<{0, 100}>; pct x = 42; pct y = 58; auto sum = x + y; // bound<{0, 200}> — no overflow possible +auto z = x + 1_b; // scalars need a grid: 1_b, not 1 // Fractional grid: −1 .. 1 in 1/16 384 steps (Q1.14 audio sample). using sample = bound<{{-1, 1}, notch<1, 16384>}, round_nearest>; @@ -40,185 +46,54 @@ using safe_pct = bound<{0, 100}, clamp>; safe_pct p = 150; // p == 100 ``` -## Single header +## New to bound? Start with the tutorial -The whole library is also available as one self-contained header at -[`single_include/bound/bound.hpp`](single_include/bound/bound.hpp). It inlines the -entire `bound/` + `slim/` tree, so it needs only the C++ standard library — there -are no `bound/...` or `slim/...` sub-includes left to resolve. Drop the one file -into a project, put `single_include/` on the include path, and use it exactly as -the full tree: +**[docs/tutorial.md](docs/tutorial.md)** is a 10-minute tour of the mental +model — what a bound *is*, why arithmetic widens, and how a value flows through +a program. Read it first; everything else builds on it. -```cpp -#include "bound/bound.hpp" // single_include/ on the include path — nothing else needed -``` +## Documentation -It is behaviourally identical to the multi-header form; the `-DBOUND_MATH_FIXED` -engine switch and the C++20 mode apply the same way, as ordinary compiler flags. - -**On Compiler Explorer**, where there is no include tree to set up, the single -header is the easy way in: - -- paste it into a second source pane named `bound/bound.hpp` and `#include` it; or -- once the repo is on GitHub, pull it in with a single raw-URL include (Compiler - Explorer resolves URL includes only for single-header libraries — which is - exactly what this is): - - ```cpp - #include - ``` - -The header is generated, not hand-edited — regenerate it after changing anything -under `include/` with `cmake --build build --target amalgamate` (see -[Build & Test](#build--test)). - -## Feature highlights - -- **Policy-driven assignment** — `clamp`, `wrap`, `sentinel`, `round_nearest`, - `snap`, plus `on_clamp` / `on_wrap` / `on_overflow` callbacks and - a `bnd::errc` mode for throw-free error reporting. Representation - flags (`real`, `exact`, `direct`, `indexed`) select how the raw value is - stored. See [docs/policies.md](docs/policies.md). -- **Type-safe widening arithmetic** — `+ - * /` widen the result grid at - compile time; integer fast paths keep the common case at native speed. - Scalars need a grid — write `a + 1_b`, not `a + 1` (a raw `int`/`double` - has no grid). Bound-space `dot` / `cross` / `lerp` keep 2-D geometry inside - the bounded world. See [docs/arithmetic.md](docs/arithmetic.md). -- **Conversions and casts** — typed-error `to()` / direct `as()` - (member and free forms), exact `numerator()` / `denominator()` read-out, - conversion predicates (`will_conversion_overflow`, `is_conversion_lossy`), - implicit `operator imax()` so a bound indexes arrays directly, and the - `clamp_cast` / `wrap_cast` / `clamp_round` family. See - [docs/conversions.md](docs/conversions.md). -- **Optimal storage & iteration** — automatic raw-type selection (uint/int - sizes, or an exact-fraction representation for non-dyadic grids), - `slim::optional` with a sentinel - encoding (no size overhead), `bound_range` for compile-time iteration, - and STL/ranges integration. Plus predefined hardware-width aliases - (`bnd::byte`, `bnd::unorm16`, `bnd::q8_8`, …) in `bound/formats.hpp`. - See [docs/storage.md](docs/storage.md). -- **Reproducible math, three engines** — a ``-shaped function set over - bounds (`sin`/`cos`/`tan`, `asin`/`acos`/`atan`/`atan2`, `sinh`/`cosh`/`tanh`, - `exp`/`log`/`log2`/`log10`/`pow`, `sqrt`/`cbrt`/`hypot`). One API; three engines - callable side-by-side by namespace (`dbl::` binary64, `flt::` binary32, - `cordic::` integer/FPU-free `constexpr`), each bit-identical across platforms. - The unqualified `bnd::math::fn` picks the build default (`-DBOUND_MATH_FIXED=ON` - → cordic, `-DBOUND_MATH_FLOAT=ON` → float, else double); `-DBND_MATH_NO_FP` - drops `` for bare metal. Operands need only the `snap` bit (`f64`/`f32` - storage is an optional fast path); angles are radians; output grids auto-deduce. - See [docs/math.md](docs/math.md). -- **Library internals** — grid invariants, storage decision tree, Q-format - fast path, policy cascade. See [docs/internals.md](docs/internals.md). +**Guides** — [policies & error handling](docs/policies.md) · +[arithmetic & rounding](docs/arithmetic.md) · +[conversions & casts](docs/conversions.md) · +[storage & STL integration](docs/storage.md) · +[`bnd::math` — bit-exact math](docs/math.md) -## Documentation +**Special topics** — [for fixed-point users](docs/fixed-point.md) · +[determinism & reproducibility](docs/determinism.md) · +[freestanding & bare-metal](docs/freestanding.md) · +[reading compiler errors](docs/diagnostics.md) · +[the single header](docs/single-header.md) -- [Tutorial — the mental model](docs/tutorial.md) -- [Policies, callbacks & error handling](docs/policies.md) -- [Arithmetic, rounding & compound assignment](docs/arithmetic.md) -- [Conversions, casts & predicates](docs/conversions.md) -- [Storage, iteration & STL integration](docs/storage.md) -- [`bnd::math` — constexpr, bit-exact math](docs/math.md) -- [Determinism & reproducibility](docs/determinism.md) -- [Bound for fixed-point users](docs/fixed-point.md) -- [Freestanding & bare-metal](docs/freestanding.md) -- [Reading a `bound<>` in a compiler error](docs/diagnostics.md) -- [Internals (architecture / design notes)](docs/internals.md) -- [Roadmap — features gated on future C++ standards](docs/roadmap.md) -- [Resources — prior art & talks](docs/resources.md) +**Reference** — [internals & design](docs/internals.md) · +[roadmap](docs/roadmap.md) · [prior art & talks](docs/resources.md) · +[accuracy](docs/accuracy.md) / [performance](docs/performance.md) (generated +reports) ## Examples -The `examples/` directory contains ~30 self-contained programs. A curated -selection: - -| Example | Feature | -|---------|---------| -| [`percentage.cpp`](examples/percentage.cpp) | Clamped percentage with `+=` and `with_clamp()` | -| [`clock.cpp`](examples/clock.cpp) | Cascading wrap with carry (seconds → minutes → hours) | -| [`audio_sample.cpp`](examples/audio_sample.cpp) | Signed Q1.14 audio samples with mixing and clamp | -| [`money.cpp`](examples/money.cpp) | Cents-precision currency arithmetic via fractional notch | -| [`pid_controller.cpp`](examples/pid_controller.cpp) | Fixed-point PID loop with `add_all` and `clamp \| round_nearest` actuator | -| [`audio_mixer.cpp`](examples/audio_mixer.cpp) | 4-channel Q1.14 mix with `with(on_clamp, on_overflow)` peak metering | -| [`sensor_fusion.cpp`](examples/sensor_fusion.cpp) | Weighted average across sensors with disparate fixed-point ranges | -| [`torus_map.cpp`](examples/torus_map.cpp) | 2-D sub-pixel position with `wrap` on both axes and edge-crossing events | -| [`algorithms.cpp`](examples/algorithms.cpp) | STL and ranges algorithms (sort, find, transform, accumulate, …) | -| [`formats.cpp`](examples/formats.cpp) | Predefined hardware-width types (`byte` / `sword` / `unorm16` / `q8_8`) and interop | -| [`storage_flags.cpp`](examples/storage_flags.cpp) | Pin the raw type with the `u8`…`u64` width flags (value vs `indexed`); compile-time fit check | - -Build and run any example: +[`examples/`](examples/) holds 30+ self-contained programs — e.g. +[`clock.cpp`](examples/clock.cpp) (wrap with carry), +[`money.cpp`](examples/money.cpp) (cents-exact currency), +[`pid_controller.cpp`](examples/pid_controller.cpp) (fixed-point control loop). +The normal build compiles them all; run `./build/example_clock` or +`ctest --test-dir build -L example`. -```bash -cmake -B build && cmake --build build -./build/example_clock -./build/example_signed -``` +## Build & test -## Performance - -Full per-operation tables (nanobench: ns/op, hardware instruction/cycle/branch -counters, and a native-baseline `relative` column per group) live in -[docs/performance.md](docs/performance.md), regenerated by the `perf_report` -build target. Headlines from the current run: - -- **Unchecked integer and Q-format arithmetic sits at or near native parity**; - `transform(b += 1_b)` over uint8-width elements vectorizes at native lane - count, and `++`/`--` compile to a bare integer add. -- `checked` accumulation pays a per-element range check (scalar, ~4× a - vectorized native loop); `bnd::sum` defers one bulk check and - vectorizes. -- The `math::*` double engine is within a small factor of this libm's - `std::` calls (own polynomials + a grid store); the CORDIC engine trades - more instructions for FPU-free bit-exactness — see - [docs/accuracy.md](docs/accuracy.md) for the error side of that trade. -- Known slow paths (by design, documented in the tables): cross-grid stores - onto incompatible lattices and mixed direct/index-grid arithmetic take the - exact rational path. - -Absolute nanoseconds in the tables are host-specific; the per-group ratios -are the stable signal. (Mind the sentinel slot: `bound<{0,255}>` stores in -uint16 — see [docs/storage.md](docs/storage.md#choosing-the-representation).) - -## Build & Test - -Requires CMake 3.24+ and a C++23 compiler (GCC 13+, Clang 16+, MSVC 19.36+) for -the full feature set. - -The library also builds against **C++20 on GCC 12**: configure with -`-DBOUND_CXX20=ON`. In that mode the error channel uses the bundled -`slim::expected` backport instead of ``, and the `std::format` -integration is feature-gated off (`to_string()` / `operator<<` remain available) -— everything else is identical. +Requires CMake 3.24+ and a C++23 compiler (GCC 13+, Clang 16+, MSVC 19.36+). ```bash -cmake -B build -cmake --build build - -# C++20 / GCC 12 build: -cmake -B build20 -DBOUND_CXX20=ON -DCMAKE_CXX_COMPILER=g++-12 -cmake --build build20 - -# Integer/CORDIC math engine (constexpr, FPU-free, unconditionally -# bit-identical — see docs/math.md): -cmake -B build-fixed -DBOUND_CXX20=ON -DBOUND_MATH_FIXED=ON -cmake --build build-fixed - -ctest --test-dir build --output-on-failure # runs unit + algo suites -./build/bound_tests # unit tests directly -./build/bench # performance benchmarks (native vs bound) -./build/example_algorithms # one of the example binaries +cmake -B build && cmake --build build +ctest --test-dir build --output-on-failure ``` -### Regenerating the single header - -The committed [single header](#single-header) is generated from `include/` by a -pure-CMake amalgamator (`cmake/amalgamate.cmake` — no Python or other tooling). -After editing any header under `include/`, regenerate and commit it: +C++20/GCC-12 mode, math-engine selection, and bare-metal builds are covered in +[docs/freestanding.md](docs/freestanding.md) and [docs/math.md](docs/math.md). -```bash -cmake --build build --target amalgamate # rewrites single_include/bound/bound.hpp -ctest --test-dir build -L tooling # amalgamate_up_to_date: fails if it drifted -cmake --build build --target single_header_smoke # compiles a TU seeing ONLY single_include/ -``` +## Single header -`ctest` runs `amalgamate_up_to_date` as part of the normal suite, so a stale -single header fails the build until it is regenerated. +The library also ships as one self-contained file, +[`single_include/bound/bound.hpp`](single_include/bound/bound.hpp) — ideal for +Compiler Explorer. See [docs/single-header.md](docs/single-header.md). diff --git a/docs/arithmetic.md b/docs/arithmetic.md index 6a9778a..00d6a1a 100644 --- a/docs/arithmetic.md +++ b/docs/arithmetic.md @@ -219,9 +219,9 @@ auto p = bnd::lerp (a, b, t); // a + (b - a) * t (t a [0,1] bound) ``` A sqrt-free squared distance is just `dot(dx, dy, dx, dy)`; compare it against -a squared-radius point-bound to test a hit without leaving the bounded world. -See [`usebound/spacesim`](../../usebound) for these driving collision and -autopilot steering entirely in bound-space. +a squared-radius point-bound to test a hit without leaving the bounded world — +e.g. a 2-D space sim can drive collision and autopilot steering entirely in +bound-space. ## Rounding @@ -352,7 +352,7 @@ world. The overload dispatches per RHS kind: error code, or is silent under `ignore_zero` — see [policies.md](policies.md#error-code-mode)). This is the same per-path zero check used by `bound / bound`; see -[Division § Why the result is `slim::optional`](#why-the-result-is-slimoptional). +[Division § When the result is `slim::optional`](#when-the-result-is-slimoptional-and-when-it-isnt). ```cpp using rn = bound<{{0, 100}, notch<1, 100>}, round_nearest>; @@ -377,10 +377,10 @@ auto prod = mul_all(a, b); // bound<{0, 10000}>, value 200 Operations return `slim::optional` in two cases: 1. **Division and modulo** — the divisor could be zero. See - [Division § Why the result is `slim::optional`](#why-the-result-is-slimoptional) + [Division § When the result is `slim::optional`](#when-the-result-is-slimoptional-and-when-it-isnt) for the two distinct failure modes division has (divide-by-zero on every path, plus denominator overflow on the rational path under `checked`). - `real` (double-backed) division participates identically: a real `÷` whose + `f64` (double-backed) division participates identically: an `f64` `÷` whose divisor grid can be zero returns `slim::optional` and reports `errc::division_by_zero` on a zero divisor — it is not a silent path. @@ -388,9 +388,9 @@ Operations return `slim::optional` in two cases: an integer raw type (see [storage.md](storage.md)), the result uses `rational` as its raw storage. Addition and multiplication on such types return `slim::optional` because rational arithmetic can overflow - (`lcm(b, d)` of the denominators may exceed `imax`). This also covers a `real` - result grid too fine for `double`: `real` is dropped and the exact result is - stored as `rational`, so an unrepresentable `real ×` returns `slim::optional` + (`lcm(b, d)` of the denominators may exceed `imax`). This also covers an `f64` + result grid too fine for `double`: `f64` is dropped and the exact result is + stored as `rational`, so an unrepresentable `f64 ×` returns `slim::optional` (overflow-checked) rather than silently losing precision. All `optional`-returning operators propagate `nullopt`: if either operand is diff --git a/docs/conversions.md b/docs/conversions.md index d9d3ea6..4779304 100644 --- a/docs/conversions.md +++ b/docs/conversions.md @@ -17,7 +17,7 @@ writing literal values into bounds. | Conversion | When it applies | Purpose | |---|---|---| | `operator imax()` (implicit) | integer-notch grid (notch denom = 1) with Lower ≥ imax_min and Upper ≤ imax_max | drop-in for integer contexts: accumulators, comparisons, and indexing (`vec[b]` converts imax → size_t) | -| `operator double()` (**implicit** for `real`-policy bounds, explicit otherwise) | `real`: always (the double-exact grid makes every value exact in `double`). Others: grid carries a rounding policy (`round_*` or `snap`) | floating-point arithmetic / printf | +| `operator double()` (**implicit** for `f64`-policy bounds, explicit otherwise) | `f64`: always (the double-exact grid makes every value exact in `double`). Others: grid carries a rounding policy (`round_*` or `snap`) | floating-point arithmetic / printf | `operator imax()` is deliberately the **only** implicit integer conversion — a second one (e.g. `size_t`) would make built-in mixed arithmetic like @@ -36,8 +36,9 @@ vec[b] = 0; // no .as<>() — imax, then imax → size_t double e = double(b); // explicit (rounding-gated operator double()) -using gain = bound<{{0, 4}, notch<1, 65536>}, round_nearest | real>; -double d = gain{0.5}; // implicit — a real bound's value is exact in double (double-exact grid) +using gain = bound<{{0, 4}, notch<1, 65536>}, round_nearest | f64>; +double d = gain{0.5}; // implicit — an f64 bound's value is exact in double (double-exact grid) + // (`real` is the deprecated spelling of `f64`) ``` For wide grids (Upper > imax_max) the implicit operators are SFINAE-disabled diff --git a/docs/determinism.md b/docs/determinism.md index a30edf3..16c9e5d 100644 --- a/docs/determinism.md +++ b/docs/determinism.md @@ -16,8 +16,9 @@ die." Reproducibility is the antidote.) | Layer | Reproducible? | Condition | |---|---|---| | Integer & rational storage / `+ − × ÷` | **Always** | none — fixed-width `int64`, exact rational | -| `real` (double-backed) storage & arithmetic | **Yes** | IEEE-754 binary64, round-to-nearest, no `-ffast-math` | +| `f64` / `f32` (float-backed) storage & arithmetic | **Yes** | IEEE-754 binary64/binary32, round-to-nearest, no `-ffast-math` | | `bnd::math` — default `double` engine | **Yes** | same as above | +| `bnd::math` — `float` engine (`-DBOUND_MATH_FLOAT=ON`) | **Yes** | IEEE-754 binary32, round-to-nearest, no `-ffast-math` | | `bnd::math` — integer/CORDIC engine (`-DBOUND_MATH_FIXED=ON`) | **Yes, unconditionally** | any platform, any flags, no FPU | | Compile-time constants & coefficients | **Always** | `constexpr`, no external codegen | @@ -26,15 +27,15 @@ assumptions (e.g. an x86 host replaying a soft-float embedded core), build with `-DBOUND_MATH_FIXED=ON`. Otherwise the default engine is reproducible on every conforming IEEE-754 platform built without `-ffast-math`. -> **"Reproducible" means per engine.** Each `bnd::math` engine is bit-identical for -> a given engine, but the `double` and integer/CORDIC engines are **not** -> value-identical to *each other* — their transcendentals can differ by up to one -> output notch, so switching engines is not value-preserving. See -> [The two engines are not value-identical](#the-two-engines-are-not-value-identical-switching-engines-changes-results). +> **"Reproducible" means per engine.** Each `bnd::math` engine is bit-identical +> within itself, but the three engines (`dbl` / `flt` / `cordic`) are **not** +> value-identical to *each other* — their transcendentals can differ by a notch +> or two, so switching engines is not value-preserving. See +> [The engines are not value-identical](#the-engines-are-not-value-identical-switching-engines-changes-results). ## The integer & rational core is deterministic by construction -Every non-`real` bound stores its value as a fixed-width integer index/value, and +Every bound without `f64`/`f32` storage holds a fixed-width integer index/value, and all of its arithmetic is integer or exact-rational. There is no floating point on these paths, so there is nothing for the platform to round differently: @@ -51,12 +52,14 @@ these paths, so there is nothing for the platform to round differently: Two builds on two architectures that take an integer/rational path produce the same bits, period. -## The `real` (double-backed) path +## The `f64` (double-backed) path -A `real` bound holds its value as an IEEE-754 `double`. It is only ever selected -on a **`double_exact`** grid — dyadic *and* every on-grid value within the 53-bit -significand (see [math.md](math.md#the-real-policy-requirement) and -[storage.md](storage.md#choosing-the-representation)). Consequences for +An `f64` bound (`real` is the deprecated spelling) holds its value as an +IEEE-754 `double`. It is only ever selected on a **`double_exact`** grid — +dyadic *and* every on-grid value within the 53-bit significand (see +[math.md](math.md#the-snap-requirement-and-f64-as-a-fast-storage-option) and +[storage.md](storage.md#choosing-the-representation)). The same reasoning +applies to `f32` with binary32's 24-bit significand. Consequences for determinism: - On-grid values are *exactly* representable, so storing/loading is lossless. @@ -65,18 +68,19 @@ determinism: safe — the same rounding rule as the integer engine. - On-grid `+ − ×` whose exact result still fits the result grid are computed exactly; an operation whose result `double` *cannot* represent **drops the - `real` flag** and stores the result in exact (rational/integer) storage, so - `real` math never silently diverges from the exact grid arithmetic. + `f64` flag** and stores the result in exact (rational/integer) storage, so + `f64` math never silently diverges from the exact grid arithmetic. **Condition.** IEEE-754 correctly-rounded `+ − × ÷` are deterministic given: round-to-nearest-even (the default), IEEE-754 binary64, and **no `-ffast-math`** (which permits value-changing reassociation). On 32-bit x86, compile for SSE2 — the legacy x87 stack evaluates at 80-bit extended precision and will not match. -## The two `bnd::math` engines +## The three `bnd::math` engines -`bnd::math` (`sin`/`cos`/`tan`/`exp`/`log`/`sqrt`/…) ships **one API over two -compile-time engines**, both reproducible — they differ only in how strong the +`bnd::math` (`sin`/`cos`/`tan`/`exp`/`log`/`sqrt`/…) ships **one API over three +engines** (`dbl` / `flt` / `cordic`, all reachable by namespace; one is the +build default) — each reproducible; they differ only in how strong the guarantee is. ### Default — the `double` engine @@ -96,6 +100,14 @@ the three IEEE-754-well-defined primitives. Explicit `std::fma` removes the "did the compiler contract `a*b+c`?" ambiguity. Fast (~ns), needs an FPU, runs at runtime. +### `-DBOUND_MATH_FLOAT=ON` — the `float` engine + +The same construction in single precision: fixed polynomials, explicit +`std::fma(float)`, its own compile-time-derived range-reduction constants. +**Bit-identical on every IEEE-754 binary32 platform** under the same conditions +as the double engine. It exists for single-precision-only FPUs (Cortex-M4F and +similar) — see [math.md](math.md#the-flt-binary32-engine). + ### `-DBOUND_MATH_FIXED=ON` — the integer/CORDIC engine The reproducibility contract, verbatim from `include/bound/cmath.hpp`: @@ -116,17 +128,17 @@ This engine is **unconditionally** bit-identical — any platform, any flags, no FPU required — at the cost of speed. Use it for embedded / soft-float targets or when you must match results across a heterogeneous fleet. -Both engines write to the same auto-deduced output grids, so each is reproducible -and correct to within the grid — but that does **not** mean the two engines produce +All engines write to the same auto-deduced output grids, so each is reproducible +and correct to within the grid — but that does **not** mean two engines produce the *same* grid value for every input (see below). -### The two engines are not value-identical (switching engines changes results) +### The engines are not value-identical (switching engines changes results) Each engine is deterministic **per engine** — bit-identical for a given engine -across platform, compiler, optimisation level, and FP flags. But the two engines -are **independent approximations** of the same irrational result, so for some inputs -they land on **adjacent notches**: they can differ by **up to one notch** (one ULP -of the output grid). +across platform, compiler, optimisation level, and FP flags. But the engines +are **independent approximations** of the same irrational result, so for some +inputs they land on **adjacent notches**: any two engines can differ by a notch +(a couple on fine grids under `flt`) — a ULP or two of the output grid. **Why — the table-maker's dilemma.** When the exact mathematical result falls extremely close to the midpoint between two grid points, each engine's tiny @@ -141,11 +153,12 @@ of a notch above the midpoint `111779.5`. The default `double` engine rounds up within the grid's resolution of the true value; they simply disagree by one notch. **Consequence — switching engines is not value-preserving.** You **cannot** rebuild -with the other engine and expect bit-identical results: toggling `-DBOUND_MATH_FIXED` -can change individual transcendental values by up to a notch. Golden vectors, -record-and-replay corpora, lockstep peers, and any cross-build comparison are valid -**within a single engine only** — pick one engine for any dataset that must stay -bit-comparable, and never mix outputs from the two engines. (Algebraic results — +with another engine and expect bit-identical results: toggling `-DBOUND_MATH_FIXED` +or `-DBOUND_MATH_FLOAT` can change individual transcendental values by a notch. +Golden vectors, record-and-replay corpora, lockstep peers, and any cross-build +comparison are valid **within a single engine only** — pick one engine for any +dataset that must stay bit-comparable, and never mix outputs from different +engines. (Algebraic results — `+ − × ÷`, conversions, rounding — *are* identical across engines; this caveat is specific to the transcendental `bnd::math` functions.) @@ -170,6 +183,6 @@ could drift from the source. | You want to… | Read | |---|---| | call sin/cos/sqrt/… and pick an engine | [math.md](math.md) | -| understand `real` / double-backed storage | [storage.md](storage.md) | +| understand `f64` / double-backed storage | [storage.md](storage.md) | | pick fast grids (fixed-point) | [fixed-point.md](fixed-point.md) | | know *why* it's shaped this way | [internals.md](internals.md) | diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 51d24b4..d7a62d8 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -23,12 +23,20 @@ bound | `1<<2` | 4 | `ignore_domain` | | `1<<4` | 16 | `snap` | | `1<<5` | 32 | `round_nearest` (+ snap) | +| `1<<6` | 64 | `round_floor` (+ snap) | +| `1<<7` | 128 | `round_ceil` (+ snap) | +| `1<<8` | 256 | `round_half_even` (+ snap) | | `1<<32` | 4294967296 | `clamp` | | `1<<33` | 8589934592 | `wrap` | | `1<<34` | 17179869184 | `checked` (the default `P`) | | `1<<35` | 34359738368 | `sentinel` | -| `1<<37` | … | `real` | +| `1<<36` | … | `unsafe` (+ ignore_domain, snap, ignore_zero) | +| `1<<37` | … | `f64` (+ round_nearest; `real` is the deprecated alias) | | `1<<38` | … | `exact` | +| `1<<39` | … | `direct` | +| `1<<40` | … | `indexed` | +| `1<<41` | … | `f32` (+ round_nearest) | +| `1<<42`…`1<<49` | … | width flags `i8 u8 i16 u16 i32 u32 i64 u64` | So `17179869184` is simply `checked`, the default policy every `bound` carries unless you choose another. (Full table: `include/bound/policy_flag.hpp`.) diff --git a/docs/fixed-point.md b/docs/fixed-point.md index 7a80bca..6bb25fb 100644 --- a/docs/fixed-point.md +++ b/docs/fixed-point.md @@ -61,13 +61,14 @@ template inline constexpr bool IsQFormat = |---|---|---|---| | `direct` | the value, as a plain integer (Notch 1) | cheapest — one int | integer ranges, interop (`raw()` == wire value) | | deduced / `indexed` | 0-based notch index | one int (+ a shift/offset to read the value) | Q-format, dense serialization | -| `real` | the value as IEEE-754 `double` | one double; FPU | math operands (sin/cos/…) | +| `f64` | the value as IEEE-754 `double` | one double; FPU | math operands (sin/cos/…) | +| `f32` | the value as IEEE-754 `float` | one float; single-precision FPU is enough | math operands on float-only FPUs (Cortex-M4F), `flt` engine | | `exact` | exact fraction (`rational`) | **gcd/lcm per op** | when rounding is unacceptable | Storage is deduced from the grid unless a representation flag overrides it (see [storage.md](storage.md#choosing-the-representation)). Rule of thumb: integer-raw -(direct/indexed) and `real` are cheap; **rational is the slow one** — avoid it in -hot loops. +(direct/indexed) and `f64`/`f32` are cheap; **rational is the slow one** — avoid it +in hot loops. ## Which grids are fast @@ -80,7 +81,7 @@ hot loops. `(a << log2 N) / b` (`HasQFormatFastPath` / `q_format_encode` in `generic.hpp`; the Q-format divide in `detail/division.hpp`). Construction is ~native (Q8.8 / Q16.16 measure at ~0.97×). -3. **`real` dyadic, `double_exact` grids** — the right choice for transcendental +3. **`f64` dyadic, `double_exact` grids** — the right choice for transcendental math: the raw *is* the `double`, so feeding `bnd::math` is free marshalling. 4. **Avoid in hot loops:** non-power-of-two notches and continuous (Notch 0) grids fall to rational storage (gcd/lcm every op). For bulk reductions use @@ -109,7 +110,7 @@ breaks autovectorisation — use `unsafe` inside proven-safe inner loops, or - **Hot integer/fixed-point math:** integer-aligned or Q-format grids; `unsafe` or `snap` in the inner loop, convert back to `checked` after. -- **Transcendentals:** `real` on a dyadic `double_exact` grid (see +- **Transcendentals:** `f64` on a dyadic `double_exact` grid (see [math.md](math.md)). - **No rounding allowed:** `exact` — accept the rational cost. - **SIMD byte/halfword loops:** keep the range one below the type max (use the diff --git a/docs/freestanding.md b/docs/freestanding.md index 5b762ca..5e2657c 100644 --- a/docs/freestanding.md +++ b/docs/freestanding.md @@ -8,9 +8,10 @@ where the current limits are. > **TL;DR** — Compile with `-fno-exceptions`, don't include `bound/io.hpp` (or define > `BND_NO_STRING` for the single header), and install a `bnd::set_error_handler`. The -> core arithmetic library then needs no hosted-only header. The one remaining caveat is -> transcendental math (`bound/cmath.hpp`), which still pulls `` — see -> [Math & the `` limit](#math--the-cmath-limit). +> core arithmetic library then needs no hosted-only header. Transcendental math is +> included: under `BND_MATH_NO_FP` (auto-enabled by `-ffreestanding`) the `` +> dependency is compiled out entirely — see +> [Math without ``](#math-without-cmath-bnd_math_no_fp). ## What you get @@ -69,8 +70,8 @@ specializations — lives in the opt-in header `bound/io.hpp`, the only place th ``, ``, and ``. **Simply don't include `bound/io.hpp`** and the core never sees those headers. -For the single-header amalgamation, that block is wrapped in a guard — define -`BND_NO_STRING` to drop it (and its heavy includes) wholesale: +For the [single-header amalgamation](single-header.md), that block is wrapped in +a guard — define `BND_NO_STRING` to drop it (and its heavy includes) wholesale: ```cpp #define BND_NO_STRING @@ -100,25 +101,39 @@ See [Error code mode](policies.md#error-code-mode) for the full surface. `clamp` `wrap` / `sentinel` policies and `slim::optional` / `slim::expected` results are all non-throwing and work unchanged on freestanding. -## Math & the `` limit - -The core is free of floating-point headers, but the transcendental math API -(`bnd::math::sin/cos/exp/log/sqrt/pow/atan/…` in **`bound/cmath.hpp`**) currently pulls -**``** — a hosted-only header — **regardless of engine**: - -- The **default double engine** uses three `` operations (`std::fma`, - `std::sqrt`, `std::nearbyint`) and a hardware FPU. -- The **`-DBND_MATH_FIXED` integer/CORDIC engine** is FPU-free and `constexpr` at - *runtime*, but `bound/cmath.hpp` still includes the double engine's header, so - `` is pulled at *compile* time today. - -So for a strict `-ffreestanding` **compile** right now: **don't include -`bound/cmath.hpp`** (skip transcendentals), or provide a `` for the target. -`-DBND_MATH_FIXED` is still the right choice when you *do* use the math API on -embedded — it removes the FPU requirement and makes the math `constexpr` and -unconditionally bit-exact (see [docs/math.md](math.md)). This `` include is a -known residual that a future change can gate away; the rest of the library is already -clean. +## Math without `` (`BND_MATH_NO_FP`) + +The transcendental math API (`bnd::math::sin/cos/exp/log/sqrt/pow/atan/…` in +**`bound/cmath.hpp`**) works on freestanding targets too. Define **`BND_MATH_NO_FP`** +and the floating-point engines — including their `#include ` — are compiled 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. + +- **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. +- Holds for the modular headers **and** the amalgamated + [single header](single-header.md). A CI smoke (`single_header_nofp_smoke`) compiles + the single header with a *poison* `` shim first on the include path, so the + build fails if any `` sneaks in. +- All transcendentals are `constexpr` under `BND_MATH_NO_FP`, so they evaluate at + compile time as well as runtime. + +See [Compiling without floating point](math.md#compiling-without-floating-point-bnd_math_no_fp) +in the math guide for the full story and the engine trade-offs. + +## Older toolchains: C++20 / GCC 12 mode + +The library also builds against **C++20 on GCC 12**: configure with +`-DBOUND_CXX20=ON`. In that mode the error channel uses the bundled +`slim::expected` backport instead of ``, and the `std::format` +integration is feature-gated off (`to_string()` / `operator<<` remain available) +— everything else is identical. + +```bash +cmake -B build20 -DBOUND_CXX20=ON -DCMAKE_CXX_COMPILER=g++-12 +cmake --build build20 +``` ## Limitations & caveats @@ -129,7 +144,8 @@ clean. would also block `-ffreestanding`; the library does not work around that. - **Compile-time vs. link-time.** `-ffreestanding` here is a *compile* property. A real bare-metal **link** additionally needs: libm symbols that the FP path may call - (`sqrt`, `fma`); `abort` / `__builtin_trap` for the no-exceptions failure path; and a + (`sqrt`, `fma` — not needed under `BND_MATH_NO_FP`); `abort` / `__builtin_trap` for + the no-exceptions failure path; and a startup/runtime that does not assume a hosted environment. - **Exceptions on + freestanding don't mix** out of the box — the default throwing handler needs ``. Use `-fno-exceptions`, or replace the handler and avoid diff --git a/docs/internals.md b/docs/internals.md index 238f9ac..6018d34 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -1,7 +1,7 @@ # bound library — internals This document explains *why* the library is shaped the way it is. It is not a -tutorial; for that see `README.md`. Use this when you need to add a new +tutorial; for that see [tutorial.md](tutorial.md). Use this when you need to add a new arithmetic operator, debug a storage-shape edge case, or reason about performance. @@ -38,7 +38,7 @@ reachable value of `a + b` for `a : A, b : B` is by construction inside ## 2. Storage encoding Representation is selected by the policy's **representation flags** -(`exact` / `real` / `direct` / `indexed`, see +(`exact` / `f64` / `f32` / `direct` / `indexed` / the `i8`…`u64` width flags, see [policies.md](policies.md#representation-flags)), with grid deduction as the default. `storage_pick` (`include/bound/grid.hpp`) resolves the flags **widest-wins** — a result of mixed-representation arithmetic ORs both @@ -47,11 +47,11 @@ operand policies, and the widest representation present wins: ```text exact in P ──────────────────────────▶ rational raw (raw IS the value, exact fraction) │ no - real in P AND double_exact grid ─────▶ double raw (raw IS the value; default engine + f64 in P AND double_exact grid ──────▶ double raw (raw IS the value; default engine │ no (elided under BND_MATH_FIXED) only — fixed engine falls through. │ Direct misuse on a too-fine grid is a │ static_assert; arithmetic instead DROPS - │ `real` when the result isn't double_exact) + │ `f64` when the result isn't double_exact) direct in P AND Notch == 1 ──────────▶ integer raw (raw IS the value) │ no indexed in P AND Notch != 0 ─────────▶ unsigned raw (raw = 0-based notch index) @@ -189,8 +189,8 @@ ambiguous. Indexing reaches `size_t` through imax's standard conversion. `bound::operator rational()` — **implicit**. Lossless and mathematically exact, so no risk in letting it happen silently. -`bound::operator double()` — `explicit((P & real) != real)`. A -`real`-policy bound lives on a double-exact grid, so every value is exactly +`bound::operator double()` — `explicit((P & f64) != f64)`. An +`f64`-policy bound lives on a double-exact grid, so every value is exactly representable in `double` and the conversion is lossless — implicit, by the same rule as `operator rational`. For everything else the conversion can round, so it is **explicit** AND gated on a rounding policy flag; strict @@ -241,7 +241,7 @@ Per-operation audit: | Operation | Shape | Causes | |---|---|---| -| `a + b`, `a − b`, `a × b` (integer/real raws) | `bound` | total — result grid contains every value by construction | +| `a + b`, `a − b`, `a × b` (integer/float-backed raws) | `bound` | total — result grid contains every value by construction | | same, rational raw + `checked`, overflow not provably excluded | `optional` | exact-arithmetic overflow. Notched grids bound the denominators, so most `exact` arithmetic PROVES safety at compile time and returns a plain `bound`; continuous (Notch 0) grids hold arbitrary rationals and keep the wrapper | | `a / b`, `mod` (divisor grid excludes 0) | `bound` | total | | `a / b`, `mod` (divisor may be 0) | `optional` | division by zero (rational overflow folds in) | @@ -279,8 +279,9 @@ is what keeps the core free of ``/``/``/``: | `bound/range.hpp` | `bound_range` iterator helper | | `bound/generic.hpp` | Public grid/policy introspection (`Grid` / `BoundPolicy` / `Interval` / `Lower` / `Upper` / `Notch`) and the `boundable` / `numeric` / `bound_assignable` concepts. Storage/raw/dispatch plumbing (`raw_t`, the `rational_raw` / `real_raw` / `value_raw` / `index_raw` predicates, `as_double`, `to_value` / `from_value`, `raw_cast` / `raw_imax`, `q_format_encode/decode`, `NotchCount`, `RawLo/Hi`, `sentinel_raw`, `detail::as_rational`, …) lives in `bnd::detail` | | `bound/detail/assignment.hpp` | `bnd::detail::assignment` specialisations for integral / fractional / boundable rhs (incl. the Q-format integer shortcut for fractional rhs) | -| `bound/cmath.hpp` | `bnd::math` — the ``-shaped public API (trig, inverse trig, hyperbolic, exp/log/pow, sqrt/cbrt/hypot) over bounds, dispatching to one of two engines. The integer/CORDIC cores live in `bnd::math::detail` here — they also serve as the compile-time output-grid oracle for **both** engines. See [math.md](math.md) | -| `bound/cmath_double.hpp` | The default **double engine** cores (`d_sin`, `d_exp`, … — own `std::fma`-Horner polynomials, Cody-Waite reduction, correctly-rounded `std::sqrt`); selected unless `BND_MATH_FIXED` is defined | +| `bound/cmath.hpp` | `bnd::math` — the ``-shaped public API (trig, inverse trig, hyperbolic, exp/log/pow, sqrt/cbrt/hypot) over bounds, dispatching to one of three engines (`dbl` / `flt` / `cordic`). The integer/CORDIC cores live in `bnd::math::detail` here — they also serve as the compile-time output-grid oracle for **every** engine. See [math.md](math.md) | +| `bound/cmath_double.hpp` | The default **double engine** cores (`d_sin`, `d_exp`, … — own `std::fma`-Horner polynomials, Cody-Waite reduction, correctly-rounded `std::sqrt`); compiled out under `BND_MATH_NO_FP` | +| `bound/cmath_float.hpp` | The **float engine** cores (binary32 siblings of the double cores, own compile-time-derived range-reduction constants); default under `BND_MATH_FLOAT`, compiled out under `BND_MATH_NO_FP` | | `bound/detail/addition.hpp`, `multiplication.hpp`, `division.hpp` | `bnd::detail::addition`, `multiplication`, `division`, `modulo` — implementation detail, included via `bound.hpp` | | `bound/detail/overflow.hpp`, `debug.hpp` | `add_overflow` / `sub_overflow` / `mul_overflow` (builtins + portable fallback); `errc`, the replaceable `error_handler` + `detail::raise` funnel — implementation detail | | `bound/detail/rational.hpp` | `rational`, its arithmetic, sentinel traits | @@ -289,3 +290,7 @@ is what keeps the core free of ``/``/``/``: | `bound/io.hpp` | **All** string/stream/`std::format` support — `to_string`, `to_string_debug`, `operator<<`, `std::formatter`, `type_name`. Opt-in and the *only* place ``/``/`` enter; gated by `BND_NO_STRING` in the single header (see [freestanding.md](freestanding.md)) | | `bound/formats.hpp` | Curated Q-format aliases (`q4_4`, `q8_8`, `q16_16`, …); opt-in | | `slim/optional.hpp` | Reusable sentinel-based optional; `bnd::` consumes it via `sentinel_traits` | + +The whole tree is also amalgamated into `single_include/bound/bound.hpp` by a +pure-CMake generator — see [single-header.md](single-header.md) for usage and +the regeneration workflow. diff --git a/docs/math.md b/docs/math.md index 28f159b..171465c 100644 --- a/docs/math.md +++ b/docs/math.md @@ -1,13 +1,15 @@ -# `bnd::math` — reproducible math, one API, two engines +# `bnd::math` — reproducible math, one API, three engines `bound/cmath.hpp` provides a ``-shaped function set that operates on `bound` values instead of `float`/`double`. There is **one public API** and -**two engines**, selected at build time — interchangeable at the source/API level, -but **not** value-for-value (see the engine caveat below): +**three engines** — interchangeable at the source/API level, but pairwise **not** +value-for-value (see the engine caveat below). One is picked as the build +default; all three stay callable by namespace in the same binary: -| Engine | Selected by | Reproducibility | constexpr | Speed | +| Engine | Default when | Reproducibility | constexpr | Speed | |---|---|---|---|---| -| **double** (default) | — | bit-identical on every IEEE-754 binary64 platform compiled without `-ffast-math` (round-to-nearest) | no | ~2× faster | +| **double** (binary64, default) | — | bit-identical on every IEEE-754 binary64 platform compiled without `-ffast-math` (round-to-nearest) | no | ~2× faster | +| **float** (binary32) | CMake `-DBOUND_MATH_FLOAT=ON` (macro `BND_MATH_FLOAT`) | bit-identical on every IEEE-754 binary32 platform (same contract as double) | no | single-precision FPUs (Cortex-M4F) | | **integer / CORDIC** | CMake `-DBOUND_MATH_FIXED=ON` (macro `BND_MATH_FIXED`) | bit-identical **unconditionally** — any platform, any flags, no FPU required | yes | embedded-friendly | > The double engine's "constexpr: no" lifts automatically on C++26 toolchains @@ -16,18 +18,19 @@ but **not** value-for-value (see the engine caveat below): The double engine evaluates its own fixed polynomials (`std::fma` Horner, hex-float coefficients, Cody-Waite range reduction) plus the correctly-rounded -`std::sqrt` — no `` transcendentals anywhere. The integer engine runs -Q.30 fixed-point CORDIC/Newton cores. Both snap results onto the same -auto-deduced output grid, so the two engines are **feature- and -signature-identical**: the same source compiles against either. +`std::sqrt` — no `` transcendentals anywhere. The float engine runs the +same polynomial shapes in single precision. The integer engine runs Q.30 +fixed-point CORDIC/Newton cores. All three snap results onto the same +auto-deduced output grid, so the engines are **feature- and +signature-identical**: the same source compiles against any of them. > Engine = speed/representation; grid = precision. The result **type** does not > depend on the engine, and each engine is bit-reproducible across platforms. -> The grid-snapped **value**, however, can differ between the two engines by up to -> one notch on rare rounding ties (the table-maker's dilemma): the engines are +> The grid-snapped **value**, however, can differ between engines by up to +> a notch or two on rare rounding ties (the table-maker's dilemma): the engines are > independent approximations, so **switching engines is not value-preserving**. > Don't mix or compare outputs from different engines — see -> [determinism.md](determinism.md) ("The two engines are not value-identical"). +> [determinism.md](determinism.md) ("The engines are not value-identical"). > (Algebraic ops — `+ − × ÷`, conversions, rounding — *are* identical across > engines; only the transcendentals can differ.) @@ -38,8 +41,8 @@ For the full reproducibility story across the whole library (not just #include "bound/cmath.hpp" using namespace bnd; -// Math operands carry the `real` policy (see below). -using angle = bound<{{-8, 8}, notch<1, 16384>}, round_nearest | real>; +// Math operands here carry the `f64` storage flag (optional — see below). +using angle = bound<{{-8, 8}, notch<1, 16384>}, round_nearest | f64>; auto s = math::sin(angle{1}); // amplitude bound in [-1, 1] auto h = math::hypot(s, s); // √(s²+s²), output grid auto-deduced ``` @@ -62,17 +65,17 @@ rounding. `f64` is **not** required — it is an optional **storage** flag that buys speed: -- Under the default engine `real` selects **double-backed storage** on the +- Under the default engine `f64` selects **double-backed storage** on the bound's grid — the raw *is* the value, so input marshalling into the engine is free (the large speedup over integer-index I/O). Values still obey the grid: they snap to the notch on store. Out-of-range stores run the usual policy cascade (clamp / wrap / sentinel / checked report). -- Without `real`, a snap-capable grid still works — the engine's `double`/integer +- Without `f64`, a snap-capable grid still works — the engine's `double`/integer result is snapped to the grid through the assignment path (a touch slower; no - double fast path). Use `real` when the grid is dyadic and you want the speed. -- Under `BND_MATH_FIXED` `real` is an ordinary `round_nearest` integer-backed + double fast path). Use `f64` when the grid is dyadic and you want the speed. +- Under `BND_MATH_FIXED` `f64` is an ordinary `round_nearest` integer-backed bound — the source compiles unchanged. -- `real` requires a grid that is **exactly representable in `double`**: dyadic +- `f64` requires a grid that is **exactly representable in `double`**: dyadic (power-of-two notch and Lower) **and** within the 53-bit significand — writing a value as `N·2^(−f)` with `f = log2(notch denominator)`, every on-grid value must satisfy `|N| < 2^53` (and `f ≤ 1022`, so no value is subnormal). That is @@ -81,14 +84,14 @@ rounding. (*"grid exceeds double's 53-bit significand — coarsen the notch/range or use `exact`"*). - An operation whose **result** grid would exceed that bound automatically drops - `real` and stores the result exactly (rational/integer), so `real` math never + `f64` and stores the result exactly (rational/integer), so `f64` math never silently diverges from the exact grid arithmetic — it trades the double fast path for exactness only where `double` cannot represent the result. Pure grid operations — `abs` / `floor` / `ceil` / `round` / `trunc` / -`fmod` — do **not** require `real`: they have no engine and act on any bound. +`fmod` — do **not** require `f64`: they have no engine and act on any bound. -See [policies.md](policies.md#representation-flags) for `real` among the +See [policies.md](policies.md#representation-flags) for `f64` among the other representation flags. ## Conventions @@ -116,30 +119,30 @@ other representation flags. (~1e-6–1e-9 depending on composition depth), then quantize onto the output grid. - Algebraically-exact results (e.g. `cbrt(8)`, `hypot(3,4)`, `pow(2,10)`) - land exactly under both engines. + land exactly under every engine. - 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. + kept identical across all engines so the same programs compile everywhere. The trig/root/atan limits (±2^20) come from the integer engine's working scale; the `exp`/`exp2`/`pow` limits are **output representability** — e.g. e^44's exact numerator exceeds any grid's integer range — and cannot widen without coupling them to the output grid. - **constexpr.** The math functions are `constexpr` only under `BND_MATH_FIXED` (the double engine's `std::fma`/`std::sqrt` are runtime). - The compile-time output-grid deduction uses the integer cores in **both** - builds, so grids and types never depend on the engine. + The compile-time output-grid deduction uses the integer cores in **every** + build, so grids and types never depend on the engine. -## Algebraic tier (exact, no polynomials, no `real` needed) +## Algebraic tier (exact, no polynomials, no `f64` needed) | Function | Domain | Output | Errors | Notes | |---|---|---|---|---| | `abs(x)` | all | `[0, max\|·\|]` | — | exact | | `floor(x)` / `ceil(x)` / `round(x)` / `trunc(x)` | all | integer notch | — | exact; `round` is half-away-from-zero | | `fmod(x, y)` | `y` must not span 0 | sign of `x` | — | truncated-division convention, exact. Integer-backed operands on commensurable notches take a single-integer-remainder fast path (faster than `std::fmod`). | -| `pown(x)` | all, `E ≥ 0` compile-time | corner-widened per multiply | optional per the checked-exact rules | repeated squaring in bound-space — exact, negative bases fine, no `real` needed | +| `pown(x)` | all, `E ≥ 0` compile-time | corner-widened per multiply | optional per the checked-exact rules | repeated squaring in bound-space — exact, negative bases fine, no `f64` needed | ## Roots @@ -246,7 +249,7 @@ 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>; +using A = bound<{{-8, 8}, notch<1, 16384>}, round_nearest | f64>; 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 diff --git a/docs/policies.md b/docs/policies.md index 603a119..d44bb12 100644 --- a/docs/policies.md +++ b/docs/policies.md @@ -67,7 +67,7 @@ Besides the *behavior* flags above, these flags select the **representation** | `indexed` | raw == 0-based notch index | `Notch != 0` | e.g. `bound<{-5, 5}, indexed>` stores 0..10 unsigned — dense layout for serialization | ```cpp -using gain = bound<{{0, 4}, notch<1, 65536>}, round_nearest | real>; // math operand +using gain = bound<{{0, 4}, notch<1, 65536>}, round_nearest | f64>; // math operand using ratio = bound<{{0, 1}, notch<1, 3>}, exact>; // thirds, exactly using regval = bound<{5, 100}, direct>; // raw() == value, interop-friendly using slot = bound<{-5, 5}, indexed>; // raw() == 0..10, dense unsigned @@ -157,7 +157,7 @@ auto q = div(d, z, on_overflow([&](auto& res, errc c) { })); ``` -### `policy_ref` compound assignment with a real or bound RHS +### `policy_ref` compound assignment with a floating-point or bound RHS `x.on_wrap(...) += rhs` (and `-=`, `*=`, `/=`) accept a `float` / `double` RHS or another bound. So a runtime `double` delta flows straight through the diff --git a/docs/resources.md b/docs/resources.md index a842550..257135d 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -18,7 +18,7 @@ partial / related · **○** no · **—** not applicable. | Library | Domain | Range in type `[lo,hi]` | Auto-widening result | Out-of-range handling | Fixed-point | Rational / exact | Transcendental math | Determinism / FPU-free | Freestanding / `-fno-exceptions` | |---|---|---|---|---|---|---|---|---|---| -| **bound** | bounded rational grids | ● | ● | clamp / wrap / sentinel / round / snap / throw / `errc` | ● | ● | ● (two engines) | ● | ◐ ¹⁰ | +| **bound** | bounded rational grids | ● | ● | clamp / wrap / sentinel / round / snap / throw / `errc` | ● | ● | ● (three engines) | ● | ◐ ¹⁰ | | **bounded::integer** | integers | ● | ● | policy on narrowing (clamp / modulo / throw / assume) | ○ | ○ | ○ | — | ○ ¹¹ | | **Intel safe-arithmetic** | integers | ● ¹⁹ | ● | compile-time proof (reject) / runtime `safe::function` | ○ | ○ | ○ | — | ○ ²⁰ | | **Boost.SafeNumerics** | integers | ◐ ¹ | ◐ ² | detect → exception (custom exception / trap policy) | ○ | ○ | ○ | — | ◐ ¹² | @@ -58,8 +58,9 @@ instead authored as a single file; the rest ship a multi-header tree (fpm, e.g., splits `fixed.hpp` / `math.hpp` / `ios.hpp`). ¹⁰ core compiles `-ffreestanding -fno-exceptions` (no ``; a replaceable `error_handler`; strings/printing in the opt-in `bound/io.hpp`, droppable -via `BND_NO_STRING`). The `bnd::math` API still pulls ``, and full -`-ffreestanding` depends on the stdlib's freestanding maturity — see +via `BND_NO_STRING`). `BND_MATH_NO_FP` (auto under `-ffreestanding`) compiles the +`` dependency out of `bnd::math` entirely; full `-ffreestanding` still depends +on the stdlib's freestanding maturity — see [freestanding.md](freestanding.md). ¹¹ C++20 modules (clang-only) with throwing policies; no documented freestanding / no-exceptions path. ¹² `-fno-exceptions` via the compile-time `trap` exception policy; the default policy throws. ¹³ non-throwing diff --git a/docs/roadmap.md b/docs/roadmap.md index a8c729c..738b92c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -75,10 +75,12 @@ this page that C++26 does **not** unblock. For completeness — these came up alongside the above but are *not* blocked by the language: -- **Freestanding `` removal** — the core is already freestanding; only the math - engine pulls ``. Gating the double-engine include under `BND_MATH_FIXED` and - swapping its three `std::` calls for `__builtin_*` would remove the header on GCC/Clang - with no language dependency. See [freestanding.md](freestanding.md). +- **Freestanding `` removal** — **done.** `BND_MATH_NO_FP` compiles the + floating-point engines — and their `#include ` — out wholesale (no + `__builtin_*` swap needed), leaving the always-present integer/CORDIC engine to + serve the full `bnd::math` API. Auto-enabled under `-ffreestanding`, implied by + `BND_MATH_FIXED`; a CI smoke compiles the single header against a poison `` + shim to keep it that way. See [freestanding.md](freestanding.md#math-without-cmath-bnd_math_no_fp). - **`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 diff --git a/docs/single-header.md b/docs/single-header.md new file mode 100644 index 0000000..95e2266 --- /dev/null +++ b/docs/single-header.md @@ -0,0 +1,50 @@ +# The single header + +The whole library is also available as one self-contained header at +[`single_include/bound/bound.hpp`](../single_include/bound/bound.hpp). It inlines +the entire `bound/` + `slim/` tree, so it needs only the C++ standard library — +there are no `bound/...` or `slim/...` sub-includes left to resolve. Drop the one +file into a project, put `single_include/` on the include path, and use it exactly +as the full tree: + +```cpp +#include "bound/bound.hpp" // single_include/ on the include path — nothing else needed +``` + +It is behaviourally identical to the multi-header form; the engine switches +(`-DBOUND_MATH_FIXED`, `-DBOUND_MATH_FLOAT`), `BND_MATH_NO_FP`, and the C++20 +mode apply the same way, as ordinary compiler flags. The string/printing layer +is wrapped in a guard — define `BND_NO_STRING` to drop it and its +``/``/`` includes wholesale (see +[freestanding.md](freestanding.md)). + +## On Compiler Explorer + +Where there is no include tree to set up, the single header is the easy way in: + +- paste it into a second source pane named `bound/bound.hpp` and `#include` it; or +- pull it in with a single raw-URL include (Compiler Explorer resolves URL + includes only for single-header libraries — which is exactly what this is): + + ```cpp + #include + ``` + +## Regenerating it + +The committed single header is **generated, not hand-edited** — it is produced +from `include/` by a pure-CMake amalgamator (`cmake/amalgamate.cmake` — no Python +or other tooling). After editing any header under `include/`, regenerate and +commit it: + +```bash +cmake --build build --target amalgamate # rewrites single_include/bound/bound.hpp +ctest --test-dir build -L tooling # amalgamate_up_to_date: fails if it drifted +cmake --build build --target single_header_smoke # compiles a TU seeing ONLY single_include/ +``` + +`ctest` runs `amalgamate_up_to_date` as part of the normal suite, so a stale +single header fails the build until it is regenerated. CI additionally builds +`single_header_freestanding_smoke` (exceptions off, `BND_NO_STRING`) and +`single_header_nofp_smoke` (a poison `` shim proving `BND_MATH_NO_FP` +pulls no floating-point header) against it. diff --git a/docs/storage.md b/docs/storage.md index e83b9d4..638e933 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -64,7 +64,7 @@ The rules above are the **default deduction**. Several policy flags override it (see [policies.md](policies.md#representation-flags) for the full table): ```cpp -using gain = bound<{{0, 4}, notch<1, 65536>}, round_nearest | real>; +using gain = bound<{{0, 4}, notch<1, 65536>}, round_nearest | f64>; // Raw: double (math operand, double-exact grid) using ratio = bound<{{0, 1}, notch<1, 3>}, exact>; // Raw: exact fraction on a NOTCHED grid @@ -74,7 +74,8 @@ using wide = bound<{0, 100}, u16>; // Raw: uint16_t (pinned width, raw() == using sidx = bound<{0, 4, notch<1,16>}, u32 | indexed>; // Raw: uint32_t index ``` -`real`/`f32`/`f64` are the math-operand flags ([math.md](math.md)); `exact` lifts the +`f64`/`f32` are the math-operand flags ([math.md](math.md); `real` is the +deprecated spelling of `f64`); `exact` lifts the notch-count limit and removes `double` entirely; `direct` makes the raw equal the wire/debugger value for interop; `indexed` gives signed grids a dense unsigned layout for serialization. @@ -89,7 +90,7 @@ compile error (`storage_pick` static_asserts the range fits). Mixed-flag results from arithmetic resolve widest-wins: `exact > f64 > f32 > {width} > direct > indexed > deduced` (width flags are dropped on arithmetic results, which deduce their own width). See [`examples/storage_flags.cpp`](../examples/storage_flags.cpp) -for value/index storage and the compile-time fit check. `real` is selected only +for value/index storage and the compile-time fit check. `f64` is selected only when the grid is **double-exact** (every value fits `double`'s 53-bit significand); otherwise it is dropped and deduction proceeds — and a result grid finer than the `uint64` index space deduces `rational`, keeping the result exact. @@ -106,7 +107,7 @@ otherwise it is dropped and deduction proceeds — and a result grid finer than flag, so `sizeof(slim::optional) == sizeof(bound)`. The sentinel is `numeric_limits::max()` for unsigned types and `numeric_limits::min()` for signed types. This costs one value from the representable range (e.g. -`int8_t` gives 255 usable values: −127..127). For `real` (double) raw the +`int8_t` gives 255 usable values: −127..127). For `f64` (double) raw the sentinel is that same `numeric_limits::max()` rule applied to `double`, i.e. the finite, comparable `DBL_MAX` — unreachable as an on-grid value, never a NaN/Inf. diff --git a/docs/tutorial.md b/docs/tutorial.md index 6b49e81..1dbcd7f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -79,7 +79,7 @@ explicit precisely because it drops the guarantee. - `numerator()` / `denominator()` — exact read-out, no rounding. - implicit `operator imax()` for integer-aligned grids (so a bound indexes an array directly). -- explicit `double(b)` — opt in to floating point. (`real`-policy math +- explicit `double(b)` — opt in to floating point. (`f64`-policy math operands convert implicitly — their value is exact in `double`.) See [conversions.md](conversions.md). @@ -87,10 +87,11 @@ See [conversions.md](conversions.md). ## Math on bounds `bnd::math` is a ``-shaped, reproducible function set over bounds — -`sin`/`cos`/`sqrt`/`exp`/`log`/`atan2`/… Math operands carry the `real` -policy flag (`bound`); one API runs on either of -two engines — a fast IEEE-754 `double` engine (default) or a bit-exact, -`constexpr` integer engine (`-DBOUND_MATH_FIXED=ON`). Angles are radians; +`sin`/`cos`/`sqrt`/`exp`/`log`/`atan2`/… Math operands typically carry the +`f64` storage flag (`bound`); one API runs on any of +three engines — a fast IEEE-754 `double` engine (`dbl`, the default), a +binary32 engine for single-precision FPUs (`flt`), and a bit-exact, +`constexpr`, FPU-free integer engine (`cordic`). Angles are radians; output grids auto-deduce from the input; runtime-conditional failures (a `tan` pole, a negative `sqrt`) surface as `slim::expected`. See [math.md](math.md). @@ -104,4 +105,8 @@ See [math.md](math.md). | convert / cast / read out | [conversions.md](conversions.md) | | iterate, store, use with the STL | [storage.md](storage.md) | | call sin/cos/sqrt/… | [math.md](math.md) | +| map your Qm.n fixed-point habits | [fixed-point.md](fixed-point.md) | +| replay bit-identically (lockstep, fuzzing) | [determinism.md](determinism.md) | +| build for bare metal / no exceptions | [freestanding.md](freestanding.md) | +| use the one-file header (Compiler Explorer) | [single-header.md](single-header.md) | | know *why* it's shaped this way | [internals.md](internals.md) |