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
22 changes: 22 additions & 0 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Auto-tag

on:
push:
branches: [main]

jobs:
tag:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create tag if new version
run: |
VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
echo "Tag v$VERSION already exists, skipping"
else
git tag "v$VERSION"
git push origin "v$VERSION"
fi
35 changes: 29 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ jobs:
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features
- run: cargo test --doc --all-features
- run: cargo test --features std
- run: cargo test --doc --features std

no-std:
name: no_std
Expand All @@ -53,8 +53,8 @@ jobs:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo doc --no-deps --all-features
- run: cargo clippy --all-targets --features std -- -D warnings
- run: cargo doc --no-deps --features std
env:
RUSTDOCFLAGS: -Dwarnings

Expand All @@ -65,7 +65,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.88
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-features
- run: cargo check --features std

coverage:
name: Coverage
Expand All @@ -75,7 +75,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-tarpaulin
- run: cargo tarpaulin --out xml --all-features --exclude-files 'tools/*'
- run: cargo tarpaulin --out xml --features std --exclude-files 'tools/*'
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -91,6 +91,29 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}

no-panic:
name: Verify No Panics
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2

- name: Check all pub fns are annotated
run: |
pub_fns=$(grep -r '^pub fn ' src/ops/*.rs | wc -l)
annotations=$(grep -r 'no_panic::no_panic' src/ops/*.rs | wc -l)
echo "pub fn count: $pub_fns"
echo "no_panic annotations: $annotations"
if [ "$pub_fns" -ne "$annotations" ]; then
echo "::error::Not all public functions in src/ops/ have #[no_panic] annotation ($annotations/$pub_fns)"
grep -n '^pub fn ' src/ops/*.rs
exit 1
fi

- name: Build with no-panic verification
run: cargo build --profile no-panic-check --features verify-no-panic --bin verify_no_panic

bench-check:
name: Benchmarks Compile
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features
- run: cargo test --features std
- run: cargo test --no-default-features
- run: cargo doc --no-deps --all-features
- run: cargo doc --no-deps --features std
env:
RUSTDOCFLAGS: -Dwarnings

Expand Down
20 changes: 16 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fixed_analytics"
version = "0.5.1"
version = "1.0.0"
edition = "2024"
rust-version = "1.88"
authors = ["David Gathercole"]
Expand All @@ -13,28 +13,40 @@ keywords = ["cordic", "fixed-point", "trigonometry", "math", "no_std"]
categories = ["mathematics", "no-std", "algorithms", "embedded"]

[package.metadata.docs.rs]
all-features = true
features = ["std"]
rustdoc-args = ["--cfg", "docsrs"]

[features]
default = ["std"]
std = []
verify-no-panic = ["dep:no-panic"]

[dependencies]
fixed = "1"
fixed = "1.30"
no-panic = { version = "0.1", optional = true }

[dev-dependencies]
criterion = { version = "0.8.1", features = ["html_reports"] }
criterion = { version = "0.8", features = ["html_reports"] }

[[bench]]
name = "benchmarks"
harness = false

[[bin]]
name = "verify_no_panic"
required-features = ["verify-no-panic"]

[profile.release]
lto = true
codegen-units = 1
panic = "abort"

[profile.no-panic-check]
inherits = "release"
lto = "fat"
codegen-units = 1
panic = "unwind"

[profile.bench]
lto = true
codegen-units = 1
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# fixed_analytics

Fixed-point mathematical functions which are accurate, fast, safe, and machine independent.
Fixed-point mathematical functions which are accurate, deterministic, and guaranteed not to panic.

[![Crates.io](https://img.shields.io/crates/v/fixed_analytics.svg)](https://crates.io/crates/fixed_analytics)
[![CI](https://github.com/GeEom/fixed_analytics/actions/workflows/ci.yml/badge.svg)](https://github.com/GeEom/fixed_analytics/actions/workflows/ci.yml)
Expand Down Expand Up @@ -30,14 +30,14 @@ Requires Rust 1.88 or later.

```toml
[dependencies]
fixed_analytics = "0.5.1"
fixed_analytics = "1.0.0"
```

For `no_std` environments:

```toml
[dependencies]
fixed_analytics = { version = "0.5.1", default-features = false }
fixed_analytics = { version = "1.0.0", default-features = false }
```

## Available Functions
Expand All @@ -54,7 +54,7 @@ fixed_analytics = { version = "0.5.1", default-features = false }
| Exponential | `exp`, `pow2` | `ln`, `log2`, `log10` |
| Algebraic | — | `sqrt` |

Functions are calculated via CORDIC, Newton-Raphson, and Taylor series techniques.
Functions are calculated via CORDIC, Newton-Raphson, and Taylor series techniques. Complete absence of panic is verified at the linker level via the [`no-panic`](https://github.com/dtolnay/no-panic) crate.

### Saturation Behavior

Expand All @@ -75,7 +75,7 @@ Where for `tan`, "pole" refers to ±π/2, ±3π/2, ±5π/2, ...
<!-- ACCURACY_START -->
### Accuracy

Relative error statistics measured against MPFR reference implementations. The file tools/accuracy-bench/baseline.json contains further measurements.
Relative error statistics measured against MPFR reference implementations. Accuracy regressions are not permitted; every change is benchmarked against the baseline before merging. The file tools/accuracy-bench/baseline.json contains further measurements.

| Function | I16F16 Mean | I16F16 Median | I16F16 P95 | I32F32 Mean | I32F32 Median | I32F32 P95 |
|----------|-------------|---------------|------------|-------------|---------------|------------|
Expand Down
56 changes: 56 additions & 0 deletions src/bin/verify_no_panic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Binary that instantiates every public function with a concrete type.
//!
//! This exists solely to trigger monomorphization so that `no_panic`'s
//! linker-level check can verify that no panic paths survive optimization.
//! It is only compiled under the `verify-no-panic` feature.

#[cfg(not(feature = "verify-no-panic"))]
compile_error!("this binary should only be built with --features verify-no-panic");

use fixed::types::I16F16;
use fixed_analytics::bounded::{NonNegative, OpenUnitInterval};
use fixed_analytics::ops::algebraic::sqrt_nonneg;
use fixed_analytics::ops::hyperbolic::atanh_open;
use fixed_analytics::{
acos, acosh, acoth, asin, asinh, atan, atan2, atanh, cos, cosh, coth, exp, ln, log2, log10,
pow2, sin, sin_cos, sinh, sinh_cosh, sqrt, tan, tanh,
};

fn main() {
// Use black_box to prevent the optimizer from eliminating calls entirely.
let x = std::hint::black_box(I16F16::from_num(0.5));
let y = std::hint::black_box(I16F16::from_num(0.25));

// Total functions (return T)
let _ = std::hint::black_box(sin(x));
let _ = std::hint::black_box(cos(x));
let _ = std::hint::black_box(tan(x));
let _ = std::hint::black_box(sin_cos(x));
let _ = std::hint::black_box(atan(x));
let _ = std::hint::black_box(atan2(y, x));
let _ = std::hint::black_box(exp(x));
let _ = std::hint::black_box(pow2(x));
let _ = std::hint::black_box(sinh(x));
let _ = std::hint::black_box(cosh(x));
let _ = std::hint::black_box(tanh(x));
let _ = std::hint::black_box(sinh_cosh(x));
let _ = std::hint::black_box(asinh(x));

// Fallible functions (return Result<T>)
let _ = std::hint::black_box(asin(x));
let _ = std::hint::black_box(acos(x));
let _ = std::hint::black_box(sqrt(x));
let _ = std::hint::black_box(ln(x));
let _ = std::hint::black_box(log2(x));
let _ = std::hint::black_box(log10(x));
let _ = std::hint::black_box(acosh(I16F16::from_num(2)));
let _ = std::hint::black_box(atanh(x));
let _ = std::hint::black_box(coth(x));
let _ = std::hint::black_box(acoth(I16F16::from_num(2)));

// Type-safe wrapper functions
let nn = NonNegative::new(x).unwrap();
let _ = std::hint::black_box(sqrt_nonneg(nn));
let ou = OpenUnitInterval::new(x).unwrap();
let _ = std::hint::black_box(atanh_open(ou));
}
6 changes: 4 additions & 2 deletions src/ops/algebraic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::traits::CordicNumber;
/// # Errors
/// Returns `DomainError` if `x < 0`.
#[must_use = "returns the square root result which should be handled"]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn sqrt<T: CordicNumber>(x: T) -> Result<T> {
NonNegative::new(x)
.map(sqrt_nonneg)
Expand All @@ -23,6 +24,7 @@ pub fn sqrt<T: CordicNumber>(x: T) -> Result<T> {
/// Use this when the non-negativity of the input is already established
/// through mathematical invariants (e.g., `1 + x²`, `1 - x²` for `|x| ≤ 1`).
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn sqrt_nonneg<T: CordicNumber>(x: NonNegative<T>) -> T {
let x = x.get();
let zero = T::zero();
Expand Down Expand Up @@ -85,9 +87,9 @@ pub fn sqrt_nonneg<T: CordicNumber>(x: NonNegative<T>) -> T {
let new_guess = sum.saturating_mul(half);

let diff = if new_guess > guess {
new_guess - guess
new_guess.saturating_sub(guess)
} else {
guess - new_guess
guess.saturating_sub(new_guess)
};

if diff <= epsilon {
Expand Down
10 changes: 9 additions & 1 deletion src/ops/circular.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::traits::CordicNumber;

/// Sine and cosine. More efficient than separate calls. Accepts any angle.
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn sin_cos<T: CordicNumber>(angle: T) -> (T, T) {
let pi = T::pi();
let frac_pi_2 = T::frac_pi_2();
Expand Down Expand Up @@ -58,13 +59,15 @@ pub fn sin_cos<T: CordicNumber>(angle: T) -> (T, T) {
/// Sine. Accepts any angle (reduced internally).
#[inline]
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn sin<T: CordicNumber>(angle: T) -> T {
sin_cos(angle).0
}

/// Cosine. Accepts any angle (reduced internally).
#[inline]
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn cos<T: CordicNumber>(angle: T) -> T {
sin_cos(angle).1
}
Expand Down Expand Up @@ -108,6 +111,7 @@ pub fn cos<T: CordicNumber>(angle: T) -> T {
/// }
/// ```
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn tan<T: CordicNumber>(angle: T) -> T {
let (s, c) = sin_cos(angle);
s.div(c)
Expand All @@ -118,6 +122,7 @@ pub fn tan<T: CordicNumber>(angle: T) -> T {
/// # Errors
/// Returns `DomainError` if `|x| > 1`.
#[must_use = "returns the arcsine result which should be handled"]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn asin<T: CordicNumber>(x: T) -> Result<T> {
let Some(unit_x) = UnitInterval::new(x) else {
return Err(Error::domain("asin", "value in range [-1, 1]"));
Expand Down Expand Up @@ -156,13 +161,15 @@ pub fn asin<T: CordicNumber>(x: T) -> Result<T> {
/// # Errors
/// Returns `DomainError` if `|x| > 1`.
#[must_use = "returns the arccosine result which should be handled"]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn acos<T: CordicNumber>(x: T) -> Result<T> {
// acos(x) = π/2 - asin(x)
asin(x).map(|a| T::frac_pi_2() - a)
asin(x).map(|a| T::frac_pi_2().saturating_sub(a))
}

/// Arctangent. Accepts any value. Returns angle in `(-π/2, π/2)`.
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn atan<T: CordicNumber>(x: T) -> T {
let zero = T::zero();
let one = T::one();
Expand Down Expand Up @@ -192,6 +199,7 @@ pub fn atan<T: CordicNumber>(x: T) -> T {

/// Four-quadrant arctangent. Returns angle in `[-π, π]`. Returns 0 for (0, 0).
#[must_use]
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
pub fn atan2<T: CordicNumber>(y: T, x: T) -> T {
let zero = T::zero();
let pi = T::pi();
Expand Down
Loading
Loading