Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 = ".."
60 changes: 60 additions & 0 deletions optimization-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<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
//! )
//! ```

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<const LO: usize, const HI: usize>(expected: usize) {
let i = core::hint::black_box(BoundedUsize::<LO, HI>::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);
}
}
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