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
237 changes: 56 additions & 181 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<N, D>`, and `frac<N, D>` β€” 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

Expand All @@ -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>;
Expand All @@ -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 <https://raw.githubusercontent.com/NiceAndPeter/bound/main/single_include/bound/bound.hpp>
```

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<T>()` / direct `as<T>()`
(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 `<cmath>`-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 `<cmath>` 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<checked>` 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 `<expected>`, 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).
18 changes: 9 additions & 9 deletions docs/arithmetic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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>;
Expand All @@ -377,20 +377,20 @@ auto prod = mul_all(a, b); // bound<{0, 10000}>, value 200
Operations return `slim::optional<bound>` 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.

2. **Rational raw storage** β€” when the result grid can't be represented with
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
Expand Down
7 changes: 4 additions & 3 deletions docs/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading