diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05af87e..4e93346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ "with": { "profile": "minimal", "toolchain": "nightly", + "components": "miri", "override": true, }, }, @@ -97,6 +98,14 @@ "args": "--workspace --all-features", }, }, + { + "uses": "actions-rs/cargo@v1", + "with": { + "command": "miri", + # `miri` does not support the `macros` feature as it uses IO. + "args": "test --features arbitrary1,bytemuck1,num-traits02,serde1,zerocopy,std", + }, + }, ], }, "fmt": { diff --git a/Cargo.lock b/Cargo.lock index a4766cd..17f17c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,13 @@ dependencies = [ "autocfg", ] +[[package]] +name = "optimization-tests" +version = "0.1.0" +dependencies = [ + "bounded-integer", +] + [[package]] name = "proc-macro2" version = "1.0.93" diff --git a/Cargo.toml b/Cargo.toml index b792d4f..6043ce0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ num-traits02 = { package = "num-traits", version = "0.2.14", default-features = serde1 = { package = "serde", version = "1.0.124", default-features = false, optional = true } zerocopy = { version = "0.8.14", features = ["derive"], optional = true } +[profile.test.package.optimization-tests] +opt-level = 3 + [features] std = ["alloc"] alloc = [] @@ -40,4 +43,4 @@ trybuild = "1.0.110" all-features = true [workspace] -members = ["macro"] +members = ["macro", "optimization-tests"] diff --git a/optimization-tests/Cargo.toml b/optimization-tests/Cargo.toml new file mode 100644 index 0000000..dea38c2 --- /dev/null +++ b/optimization-tests/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "optimization-tests" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +bounded-integer.path = ".." diff --git a/optimization-tests/src/lib.rs b/optimization-tests/src/lib.rs new file mode 100644 index 0000000..6e05043 --- /dev/null +++ b/optimization-tests/src/lib.rs @@ -0,0 +1,64 @@ +//! Checking compilation with optimizations for the `assert_unchecked` range tests in `unsafe_api`. +//! +//! ```rust,compile_fail +//! const LOWER_BOUND: usize = 15; +//! +//! let bounded = bounded_integer::BoundedUsize::::new_saturating(15); +//! let bounded = core::hint::black_box(bounded); +//! +//! optimization_tests::assert_optimized_out!( +//! bounded.get() <= LOWER_BOUND +//! ) +//! optimization_tests::assert_optimized_out!( +//! *bounded.get_ref() <= LOWER_BOUND +//! ) +//! optimization_tests::assert_optimized_out!( +//! *unsafe { bounded.get_mut() } <= LOWER_BOUND +//! ) +//! ``` + +// We should not export anything when not running tests +#![cfg(test)] + +use bounded_integer::BoundedUsize; + +unsafe extern "C" { + /// This function should fail to link if range checks are not optimized out as expected. + safe fn should_be_optimized_out() -> !; +} + +/// Ensure that LLVM has enough information at compile-time to statically ensure that the +/// condition is true. If LLVM cannot statically ensure that the condition is true and +/// emits a run-time branch, the binary will contain a call to a non-existent `extern` +/// function and fail to link. +#[macro_export] +macro_rules! optimizer_assert_guaranteed { + ($cond:expr) => { + if !$cond { + $crate::should_be_optimized_out(); + } + }; +} + +/// Assert that the inner value is statically enforced to be between the bounds `LO` and +/// `HI` inclusive. +fn range_check_optimized_out_usize(expected: usize) { + let i = core::hint::black_box(BoundedUsize::::new(expected).unwrap()); + optimizer_assert_guaranteed!(i.get() >= LO && i.get() <= HI); + let i = core::hint::black_box(i); + optimizer_assert_guaranteed!(*i.get_ref() >= LO && *i.get_ref() <= HI); + let mut i = core::hint::black_box(i); + optimizer_assert_guaranteed!(*unsafe { i.get_mut() } >= LO && *unsafe { i.get_mut() } <= HI); + + assert_eq!( + core::hint::black_box(i.get()), + core::hint::black_box(expected) + ); +} + +#[test] +fn range_check_optimized_out() { + range_check_optimized_out_usize::<10, 20>(15); + range_check_optimized_out_usize::<20, 20>(20); + range_check_optimized_out_usize::<1, { usize::MAX - 1 }>(usize::MAX - 1); +} diff --git a/src/prim_int.rs b/src/prim_int.rs index b95f3ed..a4f9a4c 100644 --- a/src/prim_int.rs +++ b/src/prim_int.rs @@ -1,5 +1,3 @@ -#![expect(clippy::must_use_candidate)] - use core::fmt::{self, Display, Formatter}; use core::num::NonZero; diff --git a/src/unsafe_api.rs b/src/unsafe_api.rs index 0449cd3..c439df3 100644 --- a/src/unsafe_api.rs +++ b/src/unsafe_api.rs @@ -335,10 +335,23 @@ macro_rules! __unsafe_api_internal { } } + #[inline] + const fn assert_range(&self) { + // Safety: As this type cannot be constructed unless the inner value is within + // the given range, we can use `assert_unchecked` to ensure that LLVM always + // maintains the range information no matter what. + unsafe { + ::core::hint::assert_unchecked( + Self::in_range(::core::mem::transmute::(*self)), + ); + } + } + /// Returns the value of the bounded integer as a primitive type. #[must_use] #[inline] pub const fn get(self) -> $inner { + self.assert_range(); unsafe { ::core::mem::transmute(self) } } @@ -346,6 +359,7 @@ macro_rules! __unsafe_api_internal { #[must_use] #[inline] pub const fn get_ref(&self) -> &$inner { + self.assert_range(); unsafe { &*<*const _>::cast(self) } } @@ -357,6 +371,7 @@ macro_rules! __unsafe_api_internal { #[must_use] #[inline] pub const unsafe fn get_mut(&mut self) -> &mut $inner { + self.assert_range(); unsafe { &mut *<*mut _>::cast(self) } } diff --git a/ui/not_zeroable.stderr b/ui/not_zeroable.stderr index a775e65..8373993 100644 --- a/ui/not_zeroable.stderr +++ b/ui/not_zeroable.stderr @@ -1,12 +1,12 @@ error[E0080]: evaluation panicked: used `zero` on a type whose range does not include zero --> ui/not_zeroable.rs:4:1 | -4 | / bounded_integer::unsafe_api! { -5 | | for A, -6 | | unsafe repr: u8, -7 | | min: 1, -8 | | max: 1, -9 | | zero, + 4 | / bounded_integer::unsafe_api! { + 5 | | for A, + 6 | | unsafe repr: u8, + 7 | | min: 1, + 8 | | max: 1, + 9 | | zero, 10 | | } | |_^ evaluation of `_::::default::{constant#0}` failed here | @@ -15,12 +15,12 @@ error[E0080]: evaluation panicked: used `zero` on a type whose range does not in note: erroneous constant encountered --> ui/not_zeroable.rs:4:1 | -4 | / bounded_integer::unsafe_api! { -5 | | for A, -6 | | unsafe repr: u8, -7 | | min: 1, -8 | | max: 1, -9 | | zero, + 4 | / bounded_integer::unsafe_api! { + 5 | | for A, + 6 | | unsafe repr: u8, + 7 | | min: 1, + 8 | | max: 1, + 9 | | zero, 10 | | } | |_^ |