Skip to content

Commit e6696fc

Browse files
Testable Models for SIMD Intrinsics
1 parent 099ad08 commit e6696fc

File tree

28 files changed

+8560
-0
lines changed

28 files changed

+8560
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ goto-transcoder
5656
# already existing elements were commented out
5757

5858
#/target
59+
testable-simd-models/target

testable-simd-models/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "testable-simd-models"
3+
version = "0.0.2"
4+
authors = ["Cryspen"]
5+
license = "Apache-2.0"
6+
homepage = "https://github.com/cryspen/verify-rust-std/testable-simd-models"
7+
edition = "2021"
8+
repository = "https://github.com/cryspen/verify-rust-std/testable-simd-models"
9+
readme = "README.md"
10+
11+
[dependencies]
12+
rand = "0.9"
13+
pastey = "0.1.0"
14+
15+
[lints.rust]
16+
unexpected_cfgs = { level = "warn" }

testable-simd-models/README.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# testable-simd-models
2+
3+
This crate contains executable, independently testable specifications
4+
for the SIMD intrinsics provided by the `core::arch` library in Rust.
5+
The structure of this crate is based on [rust-lang/stdarch/crates/core_arch](https://github.com/rust-lang/stdarch/tree/master/crates/core_arch).
6+
7+
## Code Structure
8+
Within the `core_arch` folder in this crate, there is a different
9+
folder for each architecture for which we have wrtten models.
10+
In particular, it contains folders for `x86` and `arm_shared`.
11+
Each such folder has 3 sub-folders, `models`, `tests`, and `specs`.
12+
13+
The `models` folder contains the models of the intrinsics, with a file
14+
corresponding to different target features, and are written using the
15+
various abstractions implementedin `crate::abstractions`, especially
16+
those in `crate::abstractions::simd`. These models are meant to
17+
closely resemble their implementations within the Rust core itself.
18+
19+
The `tests` folder contains the tests of these models, and is
20+
structured the same way as `models`. Each file additionally contains
21+
the definition of a macro that makes writing these tests easier. The
22+
tests work by testing the models against the intrinsics in the Rust
23+
core, trying out random inputs (generally 1000), and comparing their
24+
outputs.
25+
26+
## Modeling Process
27+
The process of adding a specific intrinsic's model goes as follows.
28+
For this example, let us say the intrinsic we are adding is
29+
`_mm256_bsrli_epi128` from the avx2 feature set.
30+
31+
1. We go to [rust-lang/stdarch/crates/core_arch/src/x86/](https://github.com/rust-lang/stdarch/tree/master/crates/core_arch/src/x86/), and find the implementation of the intrinsic in `avx2.rs`.
32+
33+
2. We see that the implementation looks like this:
34+
``` rust
35+
/// Shifts 128-bit lanes in `a` right by `imm8` bytes while shifting in zeros.
36+
///
37+
/// [Intel's documentation](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm256_bsrli_epi128)
38+
#[inline]
39+
#[target_feature(enable = "avx2")]
40+
#[cfg_attr(test, assert_instr(vpsrldq, IMM8 = 1))]
41+
#[rustc_legacy_const_generics(1)]
42+
#[stable(feature = "simd_x86", since = "1.27.0")]
43+
pub fn _mm256_bsrli_epi128<const IMM8: i32>(a: __m256i) -> __m256i {
44+
static_assert_uimm_bits!(IMM8, 8);
45+
const fn mask(shift: i32, i: u32) -> u32 {
46+
let shift = shift as u32 & 0xff;
47+
if shift > 15 || (15 - (i % 16)) < shift {
48+
0
49+
} else {
50+
32 + (i + shift)
51+
}
52+
}
53+
unsafe {
54+
let a = a.as_i8x32();
55+
let r: i8x32 = simd_shuffle!(
56+
i8x32::ZERO,
57+
a,
58+
[
59+
mask(IMM8, 0),
60+
mask(IMM8, 1),
61+
mask(IMM8, 2),
62+
mask(IMM8, 3),
63+
...
64+
mask(IMM8, 31),
65+
],
66+
);
67+
transmute(r)
68+
}
69+
}
70+
```
71+
Thus, we then go to to `core_arch/x86/models/avx2.rs`, and add the implementation. After some modification, it ends up looking like this.
72+
``` rust
73+
/// Shifts 128-bit lanes in `a` right by `imm8` bytes while shifting in zeros.
74+
///
75+
/// [Intel's documentation](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm256_bsrli_epi128)
76+
77+
pub fn _mm256_bsrli_epi128<const IMM8: i32>(a: __m256i) -> __m256i {
78+
const fn mask(shift: i32, i: u32) -> u64 {
79+
let shift = shift as u32 & 0xff;
80+
if shift > 15 || (15 - (i % 16)) < shift {
81+
0 as u64
82+
} else {
83+
(32 + (i + shift)) as u64
84+
}
85+
}
86+
87+
let a = BitVec::to_i8x32(a);
88+
let r: i8x32 = simd_shuffle(
89+
i8x32::from_fn(|_| 0),
90+
a,
91+
[
92+
mask(IMM8, 0),
93+
mask(IMM8, 1),
94+
mask(IMM8, 2),
95+
mask(IMM8, 3),
96+
...
97+
mask(IMM8, 31),
98+
],
99+
);
100+
r.into()
101+
}
102+
```
103+
104+
3. Next, we add a test for this intrinsic. For this, we navigate to `core_arch/avx2/tests/avx2.rs`. Since the value of
105+
`IMM8` can be up to 8 bits, we want to test constant arguments up to 255. Thus, we write the following macro invocation.
106+
```rust
107+
mk!([100]_mm256_bsrli_epi128{<0>,<1>,<2>,<3>,...,<255>}(a: BitVec));
108+
```
109+
Here, the `[100]` means we test 100 random inputs for each constant value. This concludes the necessary steps for implementing an intrinsic.
110+
111+
112+
## Contributing Models
113+
114+
To contribute new models of intrinsics, we expect the author to follow
115+
the above steps and provide comprehensive tests. It is important that
116+
the model author look carefully at both the Intel/ARM specification
117+
and the Rust `stdarch` implementation, because the Rust implementation
118+
may not necessarily be correct.
119+
120+
Indeed, the previous implementation of `_mm256_bsrli_epi128` (and a
121+
similar intrinsic called `_mm512_bsrli_epi128`) in `stdarch` had a
122+
bug, which we found during the process of modeling and testing this
123+
intrinsic. This bug was [reported by
124+
us](https://github.com/rust-lang/stdarch/issues/1822) using a failing
125+
test case generated from the testable model and then fixed by [our
126+
PR](https://github.com/rust-lang/stdarch/pull/1823) in the 2025-06-30
127+
version of `stdarch`.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
//! # Bit Manipulation and Machine Integer Utilities
2+
//!
3+
//! This module provides utilities for working with individual bits and machine integer types.
4+
//! It defines a [`Bit`] enum to represent a single bit (`0` or `1`) along with convenient
5+
//! conversion implementations between `Bit`, [`bool`], and various primitive integer types.
6+
//!
7+
//! In addition, the module introduces the [`MachineInteger`] trait which abstracts over
8+
//! integer types, providing associated constants:
9+
//!
10+
//! - `BITS`: The size of the integer type in bits.
11+
//! - `SIGNED`: A flag indicating whether the type is signed.
12+
//!
13+
//! The [`Bit`] type includes methods for extracting the value of a specific bit from an integer.
14+
//! For example, [`Bit::of_int`] returns the bit at a given position for a provided integer,
15+
//! handling both positive and negative values (assuming a two's complement representation).
16+
//!
17+
//! # Examples
18+
//!
19+
//! ```rust
20+
//! use testable_simd_models::abstractions::bit::{Bit, MachineInteger};
21+
//!
22+
//! // Extract the 3rd bit (0-indexed) from an integer.
23+
//! let bit = Bit::of_int(42, 2);
24+
//! println!("The extracted bit is: {:?}", bit);
25+
//!
26+
//! // Convert Bit to a primitive integer type.
27+
//! let num: u8 = bit.into();
28+
//! println!("As an integer: {}", num);
29+
//! ```
30+
//!
31+
//! [`bool`]: https://doc.rust-lang.org/std/primitive.bool.html
32+
//! [`Bit::of_int`]: enum.Bit.html#method.of_int
33+
34+
/// Represent a bit: `0` or `1`.
35+
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
36+
pub enum Bit {
37+
Zero,
38+
One,
39+
}
40+
impl std::ops::BitAnd for Bit {
41+
type Output = Self;
42+
fn bitand(self, rhs: Self) -> Self {
43+
match self {
44+
Bit::Zero => Bit::Zero,
45+
Bit::One => rhs,
46+
}
47+
}
48+
}
49+
50+
impl std::ops::BitOr for Bit {
51+
type Output = Self;
52+
fn bitor(self, rhs: Self) -> Self {
53+
match self {
54+
Bit::Zero => rhs,
55+
Bit::One => Bit::One,
56+
}
57+
}
58+
}
59+
60+
impl std::ops::BitXor for Bit {
61+
type Output = Self;
62+
fn bitxor(self, rhs: Self) -> Self {
63+
match (self, rhs) {
64+
(Bit::Zero, Bit::Zero) => Bit::Zero,
65+
(Bit::One, Bit::One) => Bit::Zero,
66+
_ => Bit::One,
67+
}
68+
}
69+
}
70+
71+
impl std::ops::Neg for Bit {
72+
type Output = Self;
73+
fn neg(self) -> Self {
74+
match self {
75+
Bit::One => Bit::Zero,
76+
Bit::Zero => Bit::One,
77+
}
78+
}
79+
}
80+
macro_rules! generate_from_bit_impls {
81+
($($ty:ident),*) => {
82+
$(impl From<Bit> for $ty {
83+
fn from(bit: Bit) -> Self {
84+
bool::from(bit) as $ty
85+
}
86+
})*
87+
};
88+
}
89+
generate_from_bit_impls!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
90+
91+
impl From<Bit> for bool {
92+
fn from(bit: Bit) -> Self {
93+
match bit {
94+
Bit::Zero => false,
95+
Bit::One => true,
96+
}
97+
}
98+
}
99+
100+
impl From<bool> for Bit {
101+
fn from(b: bool) -> Bit {
102+
match b {
103+
false => Bit::Zero,
104+
true => Bit::One,
105+
}
106+
}
107+
}
108+
109+
/// A trait for types that represent machine integers.
110+
pub trait MachineInteger {
111+
/// The size of this integer type in bits.
112+
fn bits() -> u32;
113+
114+
/// The signedness of this integer type.
115+
const SIGNED: bool;
116+
/// Element of the integer type with every bit as 0.
117+
const ZEROS: Self;
118+
/// Element of the integer type with every bit as 1.
119+
const ONES: Self;
120+
/// Minimum value of the integer type.
121+
const MIN: Self;
122+
/// Maximum value of the integer type.
123+
const MAX: Self;
124+
125+
/// Implements functionality for `simd_add` in `crate::abstractions::simd`.
126+
fn wrapping_add(self, rhs: Self) -> Self;
127+
/// Implements functionality for `simd_sub` in `crate::abstractions::simd`.
128+
fn wrapping_sub(self, rhs: Self) -> Self;
129+
/// Implements functionality for `simd_mul` in `crate::abstractions::simd`.
130+
fn overflowing_mul(self, rhs: Self) -> Self;
131+
/// Implements functionality for `simd_saturating_add` in `crate::abstractions::simd`.
132+
fn saturating_add(self, rhs: Self) -> Self;
133+
/// Implements functionality for `simd_saturating_sub` in `crate::abstractions::simd`.
134+
fn saturating_sub(self, rhs: Self) -> Self;
135+
/// Implements functionality for `simd_abs_diff` in `crate::abstractions::simd`.
136+
fn absolute_diff(self, rhs: Self) -> Self;
137+
/// Implements functionality for `simd_abs` in `crate::abstractions::simd`.
138+
fn absolute_val(self) -> Self;
139+
}
140+
141+
macro_rules! generate_imachine_integer_impls {
142+
($($ty:ident),*) => {
143+
$(
144+
impl MachineInteger for $ty {
145+
const SIGNED: bool = true;
146+
const ZEROS: $ty = 0;
147+
const ONES: $ty = -1;
148+
const MIN: $ty = $ty::MIN;
149+
const MAX: $ty = $ty::MAX;
150+
fn bits() -> u32 { $ty::BITS }
151+
fn wrapping_add(self, rhs: Self) -> Self { self.wrapping_add(rhs) }
152+
fn wrapping_sub(self, rhs: Self) -> Self { self.wrapping_sub(rhs) }
153+
fn overflowing_mul(self, rhs: Self) -> Self { self.overflowing_mul(rhs).0 }
154+
fn saturating_add(self, rhs: Self) -> Self { self.saturating_add(rhs)}
155+
fn saturating_sub(self, rhs: Self) -> Self { self.saturating_sub(rhs) }
156+
fn absolute_diff(self, rhs: Self) -> Self {if self > rhs {$ty::wrapping_sub(self, rhs)} else {$ty::wrapping_sub(rhs, self)}}
157+
fn absolute_val(self) -> Self {if self == $ty::MIN {self} else {self.abs()}}
158+
})*
159+
};
160+
}
161+
162+
macro_rules! generate_umachine_integer_impls {
163+
($($ty:ident),*) => {
164+
$(
165+
impl MachineInteger for $ty {
166+
const SIGNED: bool = false;
167+
const ZEROS: $ty = 0;
168+
const ONES: $ty = $ty::MAX;
169+
const MIN: $ty = $ty::MIN;
170+
const MAX: $ty = $ty::MAX;
171+
172+
173+
fn bits() -> u32 { $ty::BITS }
174+
fn wrapping_add(self, rhs: Self) -> Self { self.wrapping_add(rhs) }
175+
fn wrapping_sub(self, rhs: Self) -> Self { self.wrapping_sub(rhs) }
176+
fn overflowing_mul(self, rhs: Self) -> Self { self.overflowing_mul(rhs).0 }
177+
fn saturating_add(self, rhs: Self) -> Self { self.saturating_add(rhs)}
178+
fn saturating_sub(self, rhs: Self) -> Self { self.saturating_sub(rhs)}
179+
fn absolute_diff(self, rhs: Self) -> Self {if self > rhs {self - rhs} else {rhs - self}}
180+
fn absolute_val(self) -> Self {self}
181+
})*
182+
};
183+
}
184+
generate_imachine_integer_impls!(i8, i16, i32, i64, i128);
185+
generate_umachine_integer_impls!(u8, u16, u32, u64, u128);
186+
187+
impl Bit {
188+
fn of_raw_int(x: u128, nth: u32) -> Self {
189+
if x / 2u128.pow(nth) % 2 == 1 {
190+
Self::One
191+
} else {
192+
Self::Zero
193+
}
194+
}
195+
196+
pub fn of_int<T: Into<i128> + MachineInteger>(x: T, nth: u32) -> Bit {
197+
let x: i128 = x.into();
198+
if x >= 0 {
199+
Self::of_raw_int(x as u128, nth)
200+
} else {
201+
Self::of_raw_int((2i128.pow(T::bits()) + x) as u128, nth)
202+
}
203+
}
204+
}

0 commit comments

Comments
 (0)