Skip to main content

core_models/abstractions/
bitvec.rs

1//! This module provides a specification-friendly bit vector type.
2use super::bit::{Bit, MachineInteger};
3use super::funarr::*;
4
5use std::fmt::Formatter;
6
7// This is required due to some hax-lib inconsistencies with versus without `cfg(hax)`.
8#[cfg(hax)]
9use hax_lib::{int, ToInt};
10
11// TODO: this module uses `u128/i128` as mathematic integers. We should use `hax_lib::int` or bigint.
12
13/// A fixed-size bit vector type.
14///
15/// `BitVec<N>` is a specification-friendly, fixed-length bit vector that internally
16/// stores an array of [`Bit`] values, where each `Bit` represents a single binary digit (0 or 1).
17///
18/// This type provides several utility methods for constructing and converting bit vectors:
19///
20/// The [`Debug`] implementation for `BitVec` pretty-prints the bits in groups of eight,
21/// making the bit pattern more human-readable. The type also implements indexing,
22/// allowing for easy access to individual bits.
23#[hax_lib::fstar::before("noeq")]
24#[derive(Copy, Clone, Eq, PartialEq)]
25pub struct BitVec<const N: u64>(FunArray<N, Bit>);
26
27/// Pretty prints a bit slice by group of 8
28#[hax_lib::exclude]
29fn bit_slice_to_string(bits: &[Bit]) -> String {
30    bits.iter()
31        .map(|bit| match bit {
32            Bit::Zero => '0',
33            Bit::One => '1',
34        })
35        .collect::<Vec<_>>()
36        .chunks(8)
37        .map(|bits| bits.iter().collect::<String>())
38        .map(|s| format!("{s} "))
39        .collect::<String>()
40        .trim()
41        .into()
42}
43
44#[hax_lib::exclude]
45impl<const N: u64> core::fmt::Debug for BitVec<N> {
46    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
47        write!(f, "{}", bit_slice_to_string(&self.0.as_vec()))
48    }
49}
50
51#[hax_lib::attributes]
52impl<const N: u64> core::ops::Index<u64> for BitVec<N> {
53    type Output = Bit;
54    #[requires(index < N)]
55    fn index(&self, index: u64) -> &Self::Output {
56        self.0.get(index)
57    }
58}
59
60/// Convert a bit slice into an unsigned number.
61#[hax_lib::exclude]
62fn u128_int_from_bit_slice(bits: &[Bit]) -> u128 {
63    bits.iter()
64        .enumerate()
65        .map(|(i, bit)| u128::from(*bit) << i)
66        .sum::<u128>()
67}
68
69/// Convert a bit slice into a machine integer of type `T`.
70#[hax_lib::exclude]
71fn int_from_bit_slice<T: TryFrom<i128> + MachineInteger + Copy>(bits: &[Bit]) -> T {
72    debug_assert!(bits.len() <= T::bits() as usize);
73    let result = if T::SIGNED {
74        let is_negative = matches!(bits[T::bits() as usize - 1], Bit::One);
75        let s = u128_int_from_bit_slice(&bits[0..T::bits() as usize - 1]) as i128;
76        if is_negative {
77            s + (-2i128).pow(T::bits() - 1)
78        } else {
79            s
80        }
81    } else {
82        u128_int_from_bit_slice(bits) as i128
83    };
84    let Ok(n) = result.try_into() else {
85        // Conversion must succeed as `result` is guaranteed to be in range due to the bit-length check.
86        unreachable!()
87    };
88    n
89}
90
91#[hax_lib::fstar::replace(
92    r#"
93let ${BitVec::<0>::from_fn::<fn(u64)->Bit>}
94    (v_N: u64)
95    (#_v_F: Type0)
96    (f: (i: u64 {v i < v v_N}) -> $:{Bit})
97    : t_BitVec v_N =
98    ${BitVec::<0>}(${FunArray::<0,()>::from_fn::<fn(u64)->()>} v_N #$:{Bit} #(u64 -> $:{Bit}) f)
99"#
100)]
101const _: () = ();
102
103macro_rules! impl_pointwise {
104    ($n:literal, $($i:literal)*) => {
105        impl BitVec<$n> {
106            pub fn pointwise(self) -> Self {
107                Self::from_fn(|i| match i {
108                    $($i => self[$i],)*
109                    _ => unreachable!(),
110                })
111            }
112        }
113    };
114}
115
116impl_pointwise!(128, 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127);
117impl_pointwise!(256, 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255);
118
119/// An F* attribute that indiquates a rewritting lemma should be applied
120pub const REWRITE_RULE: () = {};
121
122#[hax_lib::exclude]
123impl<const N: u64> BitVec<N> {
124    /// Constructor for BitVec. `BitVec::<N>::from_fn` constructs a bitvector out of a function that takes usizes smaller than `N` and produces bits.
125    pub fn from_fn<F: Fn(u64) -> Bit>(f: F) -> Self {
126        Self(FunArray::from_fn(f))
127    }
128    /// Convert a slice of machine integers where only the `d` least significant bits are relevant.
129    pub fn from_slice<T: Into<i128> + MachineInteger + Copy>(x: &[T], d: u64) -> Self {
130        Self::from_fn(|i| Bit::of_int::<T>(x[(i / d) as usize], (i % d) as u32))
131    }
132
133    /// Construct a BitVec out of a machine integer.
134    pub fn from_int<T: Into<i128> + MachineInteger + Copy>(n: T) -> Self {
135        Self::from_slice::<T>(&[n], T::bits() as u64)
136    }
137
138    /// Convert a BitVec into a machine integer of type `T`.
139    pub fn to_int<T: TryFrom<i128> + MachineInteger + Copy>(self) -> T {
140        int_from_bit_slice(&self.0.as_vec())
141    }
142
143    /// Convert a BitVec into a vector of machine integers of type `T`.
144    pub fn to_vec<T: TryFrom<i128> + MachineInteger + Copy>(&self) -> Vec<T> {
145        self.0
146            .as_vec()
147            .chunks(T::bits() as usize)
148            .map(int_from_bit_slice)
149            .collect()
150    }
151
152    /// Generate a random BitVec.
153    pub fn rand() -> Self {
154        use rand::prelude::*;
155        let random_source: Vec<_> = {
156            let mut rng = rand::rng();
157            (0..N).map(|_| rng.random::<bool>()).collect()
158        };
159        Self::from_fn(|i| random_source[i as usize].into())
160    }
161}
162
163#[hax_lib::fstar::replace(
164    r#"
165open FStar.FunctionalExtensionality
166
167let extensionality' (#a: Type) (#b: Type) (f g: FStar.FunctionalExtensionality.(a ^-> b))
168  : Lemma (ensures (FStar.FunctionalExtensionality.feq f g <==> f == g))
169  = ()
170
171let mark_to_normalize #t (x: t): t = x
172
173open FStar.Tactics.V2
174#push-options "--z3rlimit 80 --admit_smt_queries true"
175let bitvec_rewrite_lemma_128 (x: $:{BitVec<128>})
176: Lemma (x == mark_to_normalize (${BitVec::<128>::pointwise} x)) =
177    let a = x._0._0 in
178    let b = (${BitVec::<128>::pointwise} x)._0._0 in
179    assert_norm (FStar.FunctionalExtensionality.feq a b);
180    extensionality' a b
181
182let bitvec_rewrite_lemma_256 (x: $:{BitVec<256>})
183: Lemma (x == mark_to_normalize (${BitVec::<256>::pointwise} x)) =
184    let a = x._0._0 in
185    let b = (${BitVec::<256>::pointwise} x)._0._0 in
186    assert_norm (FStar.FunctionalExtensionality.feq a b);
187    extensionality' a b
188#pop-options
189
190let bitvec_postprocess_norm_aux (): Tac unit = with_compat_pre_core 1 (fun () ->
191    let debug_mode = ext_enabled "debug_bv_postprocess_rewrite" in
192    let crate = match cur_module () with | crate::_ -> crate | _ -> fail "Empty module name" in
193    // Remove indirections
194    norm [primops; iota; delta_namespace [crate; "Libcrux_intrinsics"]; zeta_full];
195    // Rewrite call chains
196    let lemmas = FStar.List.Tot.map (fun f -> pack_ln (FStar.Stubs.Reflection.V2.Data.Tv_FVar f)) (lookup_attr (`${REWRITE_RULE}) (top_env ())) in
197    l_to_r lemmas;
198    /// Get rid of casts
199    norm [primops; iota; delta_namespace ["Rust_primitives"; "Prims.pow2"]; zeta_full];
200    if debug_mode then print ("[postprocess_rewrite_helper] lemmas = " ^ term_to_string (quote lemmas));
201
202    l_to_r [`bitvec_rewrite_lemma_128; `bitvec_rewrite_lemma_256];
203
204    let round _: Tac unit =
205        if debug_mode then dump "[postprocess_rewrite_helper] Rewrote goal";
206        // Normalize as much as possible
207        norm [primops; iota; delta_namespace ["Core"; crate; "Core_models"; "Libcrux_intrinsics"; "FStar.FunctionalExtensionality"; "Rust_primitives"]; zeta_full];
208        if debug_mode then print ("[postprocess_rewrite_helper] first norm done");
209        // Compute the last bits
210        // compute ();
211        // if debug_mode then dump ("[postprocess_rewrite_helper] compute done");
212        // Force full normalization
213        norm [primops; iota; delta; unascribe; zeta_full];
214        if debug_mode then dump "[postprocess_rewrite_helper] after full normalization";
215        // Solves the goal `<normalized body> == ?u`
216        trefl ()
217    in
218
219    ctrl_rewrite BottomUp (fun t ->
220        let f, args = collect_app t in
221        let matches = match inspect f with | Tv_UInst f _ | Tv_FVar f -> (inspect_fv f) = explode_qn (`%mark_to_normalize) | _ -> false in
222        let has_two_args = match args with | [_; _] -> true | _ -> false in
223        (matches && has_two_args, Continue)
224    ) round;
225
226    // Solves the goal `<normalized body> == ?u`
227    trefl ()
228)
229
230let ${bitvec_postprocess_norm} (): Tac unit =
231    if lax_on ()
232    then trefl () // don't bother rewritting the goal
233    else bitvec_postprocess_norm_aux ()
234"#
235)]
236/// This function is useful only for verification in F*.
237/// Used with `postprocess_rewrite`, this tactic:
238///  1. Applies a series of rewrite rules (the lemmas marked with `REWRITE_RULE`)
239///  2. Normalizes, bottom-up, every sub-expressions typed `BitVec<_>` inside the body of a function.
240/// This tactic should be used on expressions that compute a _static_ permutation of bits.
241pub fn bitvec_postprocess_norm() {}
242
243#[hax_lib::attributes]
244impl<const N: u64> BitVec<N> {
245    #[hax_lib::requires(CHUNK > 0 && CHUNK.to_int() * SHIFTS.to_int() == N.to_int())]
246    pub fn chunked_shift<const CHUNK: u64, const SHIFTS: u64>(
247        self,
248        shl: FunArray<SHIFTS, i128>,
249    ) -> BitVec<N> {
250        // TODO: this inner method is because of https://github.com/cryspen/hax-evit/issues/29
251        #[hax_lib::fstar::options("--z3rlimit 50 --split_queries always")]
252        #[hax_lib::requires(CHUNK > 0 && CHUNK.to_int() * SHIFTS.to_int() == N.to_int())]
253        fn chunked_shift<const N: u64, const CHUNK: u64, const SHIFTS: u64>(
254            bitvec: BitVec<N>,
255            shl: FunArray<SHIFTS, i128>,
256        ) -> BitVec<N> {
257            BitVec::from_fn(|i| {
258                let nth_bit = i % CHUNK;
259                let nth_chunk = i / CHUNK;
260                hax_lib::assert_prop!(nth_chunk.to_int() <= SHIFTS.to_int() - int!(1));
261                hax_lib::assert_prop!(
262                    nth_chunk.to_int() * CHUNK.to_int()
263                        <= (SHIFTS.to_int() - int!(1)) * CHUNK.to_int()
264                );
265                let shift: i128 = if nth_chunk < SHIFTS {
266                    shl[nth_chunk]
267                } else {
268                    0
269                };
270                let local_index = (nth_bit as i128).wrapping_sub(shift);
271                if local_index < CHUNK as i128 && local_index >= 0 {
272                    let local_index = local_index as u64;
273                    hax_lib::assert_prop!(
274                        nth_chunk.to_int() * CHUNK.to_int() + local_index.to_int()
275                            < SHIFTS.to_int() * CHUNK.to_int()
276                    );
277                    bitvec[nth_chunk * CHUNK + local_index]
278                } else {
279                    Bit::Zero
280                }
281            })
282        }
283        chunked_shift::<N, CHUNK, SHIFTS>(self, shl)
284    }
285
286    /// Folds over the array, accumulating a result.
287    ///
288    /// # Arguments
289    /// * `init` - The initial value of the accumulator.
290    /// * `f` - A function combining the accumulator and each element.
291    pub fn fold<A>(&self, init: A, f: fn(A, Bit) -> A) -> A {
292        self.0.fold(init, f)
293    }
294}
295
296pub mod int_vec_interp {
297    //! This module defines interpretation for bit vectors as vectors of machine integers of various size and signedness.
298    use super::*;
299
300    /// An F* attribute that marks an item as being an interpretation lemma.
301    #[allow(dead_code)]
302    #[hax_lib::fstar::before("irreducible")]
303    pub const SIMPLIFICATION_LEMMA: () = ();
304
305    /// Derives interpretations functions, simplification lemmas and type
306    /// synonyms.
307    macro_rules! interpretations {
308        ($n:literal; $($name:ident [$ty:ty; $m:literal]),*) => {
309            $(
310                #[doc = concat!(stringify!($ty), " vectors of size ", stringify!($m))]
311                #[allow(non_camel_case_types)]
312                pub type $name = FunArray<$m, $ty>;
313                pastey::paste! {
314                    const _: ()  = {
315                        #[hax_lib::opaque]
316                        impl BitVec<$n> {
317                            #[doc = concat!("Conversion from ", stringify!($ty), " vectors of size ", stringify!($m), "to  bit vectors of size ", stringify!($n))]
318                            pub fn [< from_ $name >](iv: $name) -> BitVec<$n> {
319                                let vec: Vec<$ty> = iv.as_vec();
320                                Self::from_slice(&vec[..], <$ty>::bits() as u64)
321                            }
322                            #[doc = concat!("Conversion from bit vectors of size ", stringify!($n), " to ", stringify!($ty), " vectors of size ", stringify!($m))]
323                            pub fn [< to_ $name >](bv: BitVec<$n>) -> $name {
324                                let vec: Vec<$ty> = bv.to_vec();
325                                $name::from_fn(|i| vec[i as usize])
326                            }
327                        }
328
329                        #[cfg(test)]
330                        impl From<BitVec<$n>> for $name {
331                            fn from(bv: BitVec<$n>) -> Self {
332                                BitVec::[< to_ $name >](bv)
333                            }
334                        }
335                        #[cfg(test)]
336                        impl From<$name> for BitVec<$n> {
337                            fn from(iv: $name) -> Self {
338                                BitVec::[< from_ $name >](iv)
339                            }
340                        }
341                    };
342                }
343            )*
344        };
345    }
346
347    // Defines the types `i32x8` and `i64x4`, and define intepretations function
348    // (`From` instances) from/to those types from/to bit vectors.
349    //
350    // We will need more such interpreations in the future to handle more avx2
351    // intrinsics (e.g. `_mm256_add_epi16` works on 16 bits integers, not on i32
352    // or i64).
353    interpretations!(256; i32x8 [i32; 8], i64x4 [i64; 4], i16x16 [i16; 16], i128x2 [i128; 2], i8x32 [i8; 32],
354		     u32x8 [u32; 8], u64x4 [u64; 4], u16x16 [u16; 16]);
355    interpretations!(128; i32x4 [i32; 4], i64x2 [i64; 2], i16x8 [i16; 8], i128x1 [i128; 1], i8x16 [i8; 16],
356		     u32x4 [u32; 4], u64x2 [u64; 2], u16x8 [u16; 8]);
357
358    impl i64x4 {
359        pub fn into_i32x8(self) -> i32x8 {
360            i32x8::from_fn(|i| {
361                let value = *self.get(i / 2);
362                (if i % 2 == 0 { value } else { value >> 32 }) as i32
363            })
364        }
365    }
366
367    impl i32x8 {
368        pub fn into_i64x4(self) -> i64x4 {
369            i64x4::from_fn(|i| {
370                let low = *self.get(2 * i) as u32 as u64;
371                let high = *self.get(2 * i + 1) as i32 as i64;
372                (high << 32) | low as i64
373            })
374        }
375    }
376
377    impl From<i64x4> for i32x8 {
378        fn from(vec: i64x4) -> Self {
379            vec.into_i32x8()
380        }
381    }
382
383    /// Lemma stating that converting an `i64x4` vector to a `BitVec<256>` and then into an `i32x8`
384    /// yields the same result as directly converting the `i64x4` into an `i32x8`.
385    #[hax_lib::fstar::before("[@@ $SIMPLIFICATION_LEMMA ]")]
386    #[hax_lib::opaque]
387    #[hax_lib::lemma]
388    pub fn lemma_rewrite_i64x4_bv_i32x8(
389        bv: i64x4,
390    ) -> Proof<{ hax_lib::eq(BitVec::to_i32x8(BitVec::from_i64x4(bv)), bv.into_i32x8()) }> {
391    }
392
393    /// Lemma stating that converting an `i64x4` vector to a `BitVec<256>` and then into an `i32x8`
394    /// yields the same result as directly converting the `i64x4` into an `i32x8`.
395    #[hax_lib::fstar::before("[@@ $SIMPLIFICATION_LEMMA ]")]
396    #[hax_lib::opaque]
397    #[hax_lib::lemma]
398    pub fn lemma_rewrite_i32x8_bv_i64x4(
399        bv: i32x8,
400    ) -> Proof<{ hax_lib::eq(BitVec::to_i64x4(BitVec::from_i32x8(bv)), bv.into_i64x4()) }> {
401    }
402
403    /// Normalize `from` calls that convert from one type to itself
404    #[hax_lib::fstar::replace(
405        r#"
406        [@@ $SIMPLIFICATION_LEMMA ]
407        let lemma (t: Type) (i: Core.Convert.t_From t t) (x: t)
408            : Lemma (Core.Convert.f_from #t #t #i x == (norm [primops; iota; delta; zeta] i.f_from) x)
409            = ()
410    "#
411    )]
412    const _: () = ();
413
414    #[cfg(test)]
415    mod direct_convertions_tests {
416        use super::*;
417        use crate::helpers::test::HasRandom;
418
419        #[test]
420        fn into_i32x8() {
421            for _ in 0..10000 {
422                let x: i64x4 = i64x4::random();
423                let y = x.into_i32x8();
424                assert_eq!(BitVec::from_i64x4(x), BitVec::from_i32x8(y));
425            }
426        }
427        #[test]
428        fn into_i64x4() {
429            let x: i32x8 = i32x8::random();
430            let y = x.into_i64x4();
431            assert_eq!(BitVec::from_i32x8(x), BitVec::from_i64x4(y));
432        }
433    }
434}