Skip to content

Commit 4d13c1b

Browse files
authored
Merge pull request #6 from GeEom/no_panic
v1.0.0: Guarantee no public function can panic
2 parents d45585b + 11e960b commit 4d13c1b

13 files changed

Lines changed: 388 additions & 232 deletions

File tree

.github/workflows/auto-tag.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-tag
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
tag:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: write
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Create tag if new version
15+
run: |
16+
VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
17+
if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
18+
echo "Tag v$VERSION already exists, skipping"
19+
else
20+
git tag "v$VERSION"
21+
git push origin "v$VERSION"
22+
fi

.github/workflows/ci.yml

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ jobs:
3232
with:
3333
toolchain: ${{ matrix.rust }}
3434
- uses: Swatinem/rust-cache@v2
35-
- run: cargo test --all-features
36-
- run: cargo test --doc --all-features
35+
- run: cargo test --features std
36+
- run: cargo test --doc --features std
3737

3838
no-std:
3939
name: no_std
@@ -53,8 +53,8 @@ jobs:
5353
components: rustfmt, clippy
5454
- uses: Swatinem/rust-cache@v2
5555
- run: cargo fmt --all -- --check
56-
- run: cargo clippy --all-targets --all-features -- -D warnings
57-
- run: cargo doc --no-deps --all-features
56+
- run: cargo clippy --all-targets --features std -- -D warnings
57+
- run: cargo doc --no-deps --features std
5858
env:
5959
RUSTDOCFLAGS: -Dwarnings
6060

@@ -65,7 +65,7 @@ jobs:
6565
- uses: actions/checkout@v4
6666
- uses: dtolnay/rust-toolchain@1.88
6767
- uses: Swatinem/rust-cache@v2
68-
- run: cargo check --all-features
68+
- run: cargo check --features std
6969

7070
coverage:
7171
name: Coverage
@@ -75,7 +75,7 @@ jobs:
7575
- uses: dtolnay/rust-toolchain@stable
7676
- uses: Swatinem/rust-cache@v2
7777
- uses: taiki-e/install-action@cargo-tarpaulin
78-
- run: cargo tarpaulin --out xml --all-features --exclude-files 'tools/*'
78+
- run: cargo tarpaulin --out xml --features std --exclude-files 'tools/*'
7979
- uses: codecov/codecov-action@v4
8080
with:
8181
token: ${{ secrets.CODECOV_TOKEN }}
@@ -91,6 +91,29 @@ jobs:
9191
with:
9292
token: ${{ secrets.GITHUB_TOKEN }}
9393

94+
no-panic:
95+
name: Verify No Panics
96+
runs-on: ubuntu-latest
97+
steps:
98+
- uses: actions/checkout@v4
99+
- uses: dtolnay/rust-toolchain@stable
100+
- uses: Swatinem/rust-cache@v2
101+
102+
- name: Check all pub fns are annotated
103+
run: |
104+
pub_fns=$(grep -r '^pub fn ' src/ops/*.rs | wc -l)
105+
annotations=$(grep -r 'no_panic::no_panic' src/ops/*.rs | wc -l)
106+
echo "pub fn count: $pub_fns"
107+
echo "no_panic annotations: $annotations"
108+
if [ "$pub_fns" -ne "$annotations" ]; then
109+
echo "::error::Not all public functions in src/ops/ have #[no_panic] annotation ($annotations/$pub_fns)"
110+
grep -n '^pub fn ' src/ops/*.rs
111+
exit 1
112+
fi
113+
114+
- name: Build with no-panic verification
115+
run: cargo build --profile no-panic-check --features verify-no-panic --bin verify_no_panic
116+
94117
bench-check:
95118
name: Benchmarks Compile
96119
runs-on: ubuntu-latest

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ jobs:
1717
- uses: actions/checkout@v4
1818
- uses: dtolnay/rust-toolchain@stable
1919
- uses: Swatinem/rust-cache@v2
20-
- run: cargo test --all-features
20+
- run: cargo test --features std
2121
- run: cargo test --no-default-features
22-
- run: cargo doc --no-deps --all-features
22+
- run: cargo doc --no-deps --features std
2323
env:
2424
RUSTDOCFLAGS: -Dwarnings
2525

Cargo.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fixed_analytics"
3-
version = "0.5.1"
3+
version = "1.0.0"
44
edition = "2024"
55
rust-version = "1.88"
66
authors = ["David Gathercole"]
@@ -13,28 +13,40 @@ keywords = ["cordic", "fixed-point", "trigonometry", "math", "no_std"]
1313
categories = ["mathematics", "no-std", "algorithms", "embedded"]
1414

1515
[package.metadata.docs.rs]
16-
all-features = true
16+
features = ["std"]
1717
rustdoc-args = ["--cfg", "docsrs"]
1818

1919
[features]
2020
default = ["std"]
2121
std = []
22+
verify-no-panic = ["dep:no-panic"]
2223

2324
[dependencies]
24-
fixed = "1"
25+
fixed = "1.30"
26+
no-panic = { version = "0.1", optional = true }
2527

2628
[dev-dependencies]
27-
criterion = { version = "0.8.1", features = ["html_reports"] }
29+
criterion = { version = "0.8", features = ["html_reports"] }
2830

2931
[[bench]]
3032
name = "benchmarks"
3133
harness = false
3234

35+
[[bin]]
36+
name = "verify_no_panic"
37+
required-features = ["verify-no-panic"]
38+
3339
[profile.release]
3440
lto = true
3541
codegen-units = 1
3642
panic = "abort"
3743

44+
[profile.no-panic-check]
45+
inherits = "release"
46+
lto = "fat"
47+
codegen-units = 1
48+
panic = "unwind"
49+
3850
[profile.bench]
3951
lto = true
4052
codegen-units = 1

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# fixed_analytics
22

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

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

3131
```toml
3232
[dependencies]
33-
fixed_analytics = "0.5.1"
33+
fixed_analytics = "1.0.0"
3434
```
3535

3636
For `no_std` environments:
3737

3838
```toml
3939
[dependencies]
40-
fixed_analytics = { version = "0.5.1", default-features = false }
40+
fixed_analytics = { version = "1.0.0", default-features = false }
4141
```
4242

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

57-
Functions are calculated via CORDIC, Newton-Raphson, and Taylor series techniques.
57+
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.
5858

5959
### Saturation Behavior
6060

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

78-
Relative error statistics measured against MPFR reference implementations. The file tools/accuracy-bench/baseline.json contains further measurements.
78+
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.
7979

8080
| Function | I16F16 Mean | I16F16 Median | I16F16 P95 | I32F32 Mean | I32F32 Median | I32F32 P95 |
8181
|----------|-------------|---------------|------------|-------------|---------------|------------|

src/bin/verify_no_panic.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//! Binary that instantiates every public function with a concrete type.
2+
//!
3+
//! This exists solely to trigger monomorphization so that `no_panic`'s
4+
//! linker-level check can verify that no panic paths survive optimization.
5+
//! It is only compiled under the `verify-no-panic` feature.
6+
7+
#[cfg(not(feature = "verify-no-panic"))]
8+
compile_error!("this binary should only be built with --features verify-no-panic");
9+
10+
use fixed::types::I16F16;
11+
use fixed_analytics::bounded::{NonNegative, OpenUnitInterval};
12+
use fixed_analytics::ops::algebraic::sqrt_nonneg;
13+
use fixed_analytics::ops::hyperbolic::atanh_open;
14+
use fixed_analytics::{
15+
acos, acosh, acoth, asin, asinh, atan, atan2, atanh, cos, cosh, coth, exp, ln, log2, log10,
16+
pow2, sin, sin_cos, sinh, sinh_cosh, sqrt, tan, tanh,
17+
};
18+
19+
fn main() {
20+
// Use black_box to prevent the optimizer from eliminating calls entirely.
21+
let x = std::hint::black_box(I16F16::from_num(0.5));
22+
let y = std::hint::black_box(I16F16::from_num(0.25));
23+
24+
// Total functions (return T)
25+
let _ = std::hint::black_box(sin(x));
26+
let _ = std::hint::black_box(cos(x));
27+
let _ = std::hint::black_box(tan(x));
28+
let _ = std::hint::black_box(sin_cos(x));
29+
let _ = std::hint::black_box(atan(x));
30+
let _ = std::hint::black_box(atan2(y, x));
31+
let _ = std::hint::black_box(exp(x));
32+
let _ = std::hint::black_box(pow2(x));
33+
let _ = std::hint::black_box(sinh(x));
34+
let _ = std::hint::black_box(cosh(x));
35+
let _ = std::hint::black_box(tanh(x));
36+
let _ = std::hint::black_box(sinh_cosh(x));
37+
let _ = std::hint::black_box(asinh(x));
38+
39+
// Fallible functions (return Result<T>)
40+
let _ = std::hint::black_box(asin(x));
41+
let _ = std::hint::black_box(acos(x));
42+
let _ = std::hint::black_box(sqrt(x));
43+
let _ = std::hint::black_box(ln(x));
44+
let _ = std::hint::black_box(log2(x));
45+
let _ = std::hint::black_box(log10(x));
46+
let _ = std::hint::black_box(acosh(I16F16::from_num(2)));
47+
let _ = std::hint::black_box(atanh(x));
48+
let _ = std::hint::black_box(coth(x));
49+
let _ = std::hint::black_box(acoth(I16F16::from_num(2)));
50+
51+
// Type-safe wrapper functions
52+
let nn = NonNegative::new(x).unwrap();
53+
let _ = std::hint::black_box(sqrt_nonneg(nn));
54+
let ou = OpenUnitInterval::new(x).unwrap();
55+
let _ = std::hint::black_box(atanh_open(ou));
56+
}

src/ops/algebraic.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::traits::CordicNumber;
99
/// # Errors
1010
/// Returns `DomainError` if `x < 0`.
1111
#[must_use = "returns the square root result which should be handled"]
12+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
1213
pub fn sqrt<T: CordicNumber>(x: T) -> Result<T> {
1314
NonNegative::new(x)
1415
.map(sqrt_nonneg)
@@ -23,6 +24,7 @@ pub fn sqrt<T: CordicNumber>(x: T) -> Result<T> {
2324
/// Use this when the non-negativity of the input is already established
2425
/// through mathematical invariants (e.g., `1 + x²`, `1 - x²` for `|x| ≤ 1`).
2526
#[must_use]
27+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
2628
pub fn sqrt_nonneg<T: CordicNumber>(x: NonNegative<T>) -> T {
2729
let x = x.get();
2830
let zero = T::zero();
@@ -85,9 +87,9 @@ pub fn sqrt_nonneg<T: CordicNumber>(x: NonNegative<T>) -> T {
8587
let new_guess = sum.saturating_mul(half);
8688

8789
let diff = if new_guess > guess {
88-
new_guess - guess
90+
new_guess.saturating_sub(guess)
8991
} else {
90-
guess - new_guess
92+
guess.saturating_sub(new_guess)
9193
};
9294

9395
if diff <= epsilon {

src/ops/circular.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::traits::CordicNumber;
88

99
/// Sine and cosine. More efficient than separate calls. Accepts any angle.
1010
#[must_use]
11+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
1112
pub fn sin_cos<T: CordicNumber>(angle: T) -> (T, T) {
1213
let pi = T::pi();
1314
let frac_pi_2 = T::frac_pi_2();
@@ -58,13 +59,15 @@ pub fn sin_cos<T: CordicNumber>(angle: T) -> (T, T) {
5859
/// Sine. Accepts any angle (reduced internally).
5960
#[inline]
6061
#[must_use]
62+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
6163
pub fn sin<T: CordicNumber>(angle: T) -> T {
6264
sin_cos(angle).0
6365
}
6466

6567
/// Cosine. Accepts any angle (reduced internally).
6668
#[inline]
6769
#[must_use]
70+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
6871
pub fn cos<T: CordicNumber>(angle: T) -> T {
6972
sin_cos(angle).1
7073
}
@@ -108,6 +111,7 @@ pub fn cos<T: CordicNumber>(angle: T) -> T {
108111
/// }
109112
/// ```
110113
#[must_use]
114+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
111115
pub fn tan<T: CordicNumber>(angle: T) -> T {
112116
let (s, c) = sin_cos(angle);
113117
s.div(c)
@@ -118,6 +122,7 @@ pub fn tan<T: CordicNumber>(angle: T) -> T {
118122
/// # Errors
119123
/// Returns `DomainError` if `|x| > 1`.
120124
#[must_use = "returns the arcsine result which should be handled"]
125+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
121126
pub fn asin<T: CordicNumber>(x: T) -> Result<T> {
122127
let Some(unit_x) = UnitInterval::new(x) else {
123128
return Err(Error::domain("asin", "value in range [-1, 1]"));
@@ -156,13 +161,15 @@ pub fn asin<T: CordicNumber>(x: T) -> Result<T> {
156161
/// # Errors
157162
/// Returns `DomainError` if `|x| > 1`.
158163
#[must_use = "returns the arccosine result which should be handled"]
164+
#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)]
159165
pub fn acos<T: CordicNumber>(x: T) -> Result<T> {
160166
// acos(x) = π/2 - asin(x)
161-
asin(x).map(|a| T::frac_pi_2() - a)
167+
asin(x).map(|a| T::frac_pi_2().saturating_sub(a))
162168
}
163169

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

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

0 commit comments

Comments
 (0)