From 7a8c517668bbc225ce036c826485de63a03fd8cd Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 23 Sep 2025 11:07:22 +0200 Subject: [PATCH 1/6] Maintain range information on get for optimisation purposes --- src/unsafe_api.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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) } } From 7364647c9509f33d769d0a26264e4f737df050b0 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 24 Sep 2025 09:30:45 +0200 Subject: [PATCH 2/6] Update trybuild output to match v1.90.0-stable --- ui/not_zeroable.stderr | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) 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 | | } | |_^ | From 243af125c136d3ef829e648a7ea1a433f29488cf Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 24 Sep 2025 09:33:57 +0200 Subject: [PATCH 3/6] It seems like `must_use_candidate` isn't triggering in `prim_int` on 1.90.0, even though it seems like it should --- src/prim_int.rs | 2 -- 1 file changed, 2 deletions(-) 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; From 42a8e398a52e867d60170034cda3d88118a83ffc Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 24 Sep 2025 10:22:13 +0200 Subject: [PATCH 4/6] Add tests to ensure that range checks are optimized out in release builds, add `miri test` for additional checks for undef --- .github/workflows/ci.yml | 9 ++++++ Cargo.lock | 7 ++++ Cargo.toml | 5 ++- optimization-tests/Cargo.toml | 8 +++++ optimization-tests/src/lib.rs | 60 +++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 optimization-tests/Cargo.toml create mode 100644 optimization-tests/src/lib.rs 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..ccf51db --- /dev/null +++ b/optimization-tests/src/lib.rs @@ -0,0 +1,60 @@ +//! Checking compilation with optimizations for the `assert_unchecked` range tests in `unsafe_api`. +//! +//! > *TODO*: Rust seems to ignore optimization for doctests even if they are specified in the profile, +//! > and even if running `cargo test --doc --release`. The `compile_fail` test _does_ correctly fail +//! > to compile, but that is probably true even for out-of-range comparisons. +//! +//! ```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 +//! ) +//! ``` + +unsafe extern "C" { + // This function should fail to link if range checks are not optimized out as expected. + pub safe fn should_be_optimized_out() -> !; +} + +#[macro_export] +macro_rules! assert_optimized_out { + ($cond:expr) => { + if $cond { + $crate::should_be_optimized_out(); + } + } +} + +#[cfg(test)] +mod tests { + use bounded_integer::BoundedUsize; + use crate::assert_optimized_out; + + fn range_check_optimized_out_usize(expected: usize) { + let i = core::hint::black_box(BoundedUsize::::new(expected).unwrap()); + assert_optimized_out!(i.get() < LO || i.get() > HI); + let i = core::hint::black_box(i); + assert_optimized_out!(*i.get_ref() < LO || i.get() > HI); + let mut i = core::hint::black_box(i); + assert_optimized_out!(*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); + } +} From 01d7474b3922acb32b52ea0ecbed298734646182 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 1 Oct 2025 11:43:14 +0200 Subject: [PATCH 5/6] Clean up tests based on review --- optimization-tests/src/lib.rs | 62 +++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/optimization-tests/src/lib.rs b/optimization-tests/src/lib.rs index ccf51db..8cdc529 100644 --- a/optimization-tests/src/lib.rs +++ b/optimization-tests/src/lib.rs @@ -1,9 +1,5 @@ //! Checking compilation with optimizations for the `assert_unchecked` range tests in `unsafe_api`. //! -//! > *TODO*: Rust seems to ignore optimization for doctests even if they are specified in the profile, -//! > and even if running `cargo test --doc --release`. The `compile_fail` test _does_ correctly fail -//! > to compile, but that is probably true even for out-of-range comparisons. -//! //! ```rust,compile_fail //! const LOWER_BOUND: usize = 15; //! @@ -21,40 +17,48 @@ //! ) //! ``` +// 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. - pub safe fn should_be_optimized_out() -> !; + /// 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! assert_optimized_out { +macro_rules! const_assert { ($cond:expr) => { - if $cond { + if !$cond { $crate::should_be_optimized_out(); } - } + }; } -#[cfg(test)] -mod tests { - use bounded_integer::BoundedUsize; - use crate::assert_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()); + const_assert!(i.get() >= LO && i.get() <= HI); + let i = core::hint::black_box(i); + const_assert!(*i.get_ref() >= LO && *i.get_ref() <= HI); + let mut i = core::hint::black_box(i); + const_assert!(*unsafe { i.get_mut() } >= LO && *unsafe { i.get_mut() } <= HI); - fn range_check_optimized_out_usize(expected: usize) { - let i = core::hint::black_box(BoundedUsize::::new(expected).unwrap()); - assert_optimized_out!(i.get() < LO || i.get() > HI); - let i = core::hint::black_box(i); - assert_optimized_out!(*i.get_ref() < LO || i.get() > HI); - let mut i = core::hint::black_box(i); - assert_optimized_out!(*unsafe { i.get_mut() } < LO || *unsafe { i.get_mut() } > HI); - - assert_eq!(core::hint::black_box(i.get()), core::hint::black_box(expected)); - } + 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); - } +#[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); } From b27ef97dea5304e8fc445d9c98d0ecb0011d0ab8 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 1 Oct 2025 11:47:43 +0200 Subject: [PATCH 6/6] Rename to `optimizer_assert_guaranteed` --- optimization-tests/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/optimization-tests/src/lib.rs b/optimization-tests/src/lib.rs index 8cdc529..6e05043 100644 --- a/optimization-tests/src/lib.rs +++ b/optimization-tests/src/lib.rs @@ -32,7 +32,7 @@ unsafe extern "C" { /// 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! const_assert { +macro_rules! optimizer_assert_guaranteed { ($cond:expr) => { if !$cond { $crate::should_be_optimized_out(); @@ -44,11 +44,11 @@ macro_rules! const_assert { /// `HI` inclusive. fn range_check_optimized_out_usize(expected: usize) { let i = core::hint::black_box(BoundedUsize::::new(expected).unwrap()); - const_assert!(i.get() >= LO && i.get() <= HI); + optimizer_assert_guaranteed!(i.get() >= LO && i.get() <= HI); let i = core::hint::black_box(i); - const_assert!(*i.get_ref() >= LO && *i.get_ref() <= HI); + optimizer_assert_guaranteed!(*i.get_ref() >= LO && *i.get_ref() <= HI); let mut i = core::hint::black_box(i); - const_assert!(*unsafe { i.get_mut() } >= LO && *unsafe { i.get_mut() } <= HI); + optimizer_assert_guaranteed!(*unsafe { i.get_mut() } >= LO && *unsafe { i.get_mut() } <= HI); assert_eq!( core::hint::black_box(i.get()),