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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"with": {
"profile": "minimal",
"toolchain": "nightly",
"components": "miri",
"override": true,
},
},
Expand All @@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, really? cargo +nightly miri test --workspace --all-features works for me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I’ll investigate tomorrow

"args": "test --features arbitrary1,bytemuck1,num-traits02,serde1,zerocopy,std",
},
},
],
},
"fmt": {
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -40,4 +43,4 @@ trybuild = "1.0.110"
all-features = true

[workspace]
members = ["macro"]
members = ["macro", "optimization-tests"]
8 changes: 8 additions & 0 deletions optimization-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "optimization-tests"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
bounded-integer.path = ".."
64 changes: 64 additions & 0 deletions optimization-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<LOWER_BOUND, 20>::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<const LO: usize, const HI: usize>(expected: usize) {
let i = core::hint::black_box(BoundedUsize::<LO, HI>::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);
}
2 changes: 0 additions & 2 deletions src/prim_int.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![expect(clippy::must_use_candidate)]

use core::fmt::{self, Display, Formatter};
use core::num::NonZero;

Expand Down
15 changes: 15 additions & 0 deletions src/unsafe_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,17 +335,31 @@ 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, $inner>(*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) }
}

/// Returns a shared reference to the value of the bounded integer.
#[must_use]
#[inline]
pub const fn get_ref(&self) -> &$inner {
self.assert_range();
unsafe { &*<*const _>::cast(self) }
}

Expand All @@ -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) }
}

Expand Down
24 changes: 12 additions & 12 deletions ui/not_zeroable.stderr
Original file line number Diff line number Diff line change
@@ -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 `_::<impl std::default::Default for A>::default::{constant#0}` failed here
|
Expand All @@ -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 | | }
| |_^
|
Expand Down