Skip to main content

la_stack/
vector.rs

1#![forbid(unsafe_code)]
2
3//! Fixed-size, stack-allocated vectors.
4
5use core::hint::cold_path;
6
7use crate::LaError;
8
9/// Finite fixed-size vector of length `D`, stored inline.
10///
11/// Public construction rejects NaN and infinity through [`try_new`](Self::try_new),
12/// and the storage field is private, so a `Vector` value carries the invariant
13/// that every stored entry is finite. Algorithms therefore do not re-scan stored
14/// entries at every use; user-visible non-finite errors come from construction
15/// boundaries or from values computed during arithmetic, such as overflowed
16/// accumulators.
17///
18/// Direct field construction is intentionally unavailable to downstream callers:
19///
20/// ```compile_fail
21/// use la_stack::Vector;
22///
23/// let _ = Vector::<2> {
24///     data: [1.0, f64::NAN],
25/// };
26/// ```
27#[must_use]
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Vector<const D: usize> {
30    data: [f64; D],
31}
32
33impl<const D: usize> Vector<D> {
34    /// Test-only infallible constructor for finite literal fixtures.
35    #[cfg(test)]
36    #[inline]
37    pub(crate) const fn new(data: [f64; D]) -> Self {
38        match Self::try_new(data) {
39            Ok(vector) => vector,
40            Err(_) => panic!("Vector::new requires finite entries"),
41        }
42    }
43
44    /// Try to create a finite vector from a backing array.
45    ///
46    /// This is the public raw-storage boundary for vectors. Successful
47    /// construction makes the returned [`Vector`] a finite-storage proof.
48    ///
49    /// # Examples
50    /// ```
51    /// use la_stack::prelude::*;
52    ///
53    /// # fn main() -> Result<(), LaError> {
54    /// let v = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
55    /// assert_eq!(v.into_array(), [1.0, 2.0, 3.0]);
56    /// # Ok(())
57    /// # }
58    /// ```
59    ///
60    /// # Errors
61    /// Returns [`LaError::NonFinite`] with the first offending entry index when
62    /// `data` contains NaN or infinity.
63    #[inline]
64    pub const fn try_new(data: [f64; D]) -> Result<Self, LaError> {
65        if let Some(index) = Self::first_non_finite_entry_in(&data) {
66            Err(LaError::non_finite_at(index))
67        } else {
68            Ok(Self::new_unchecked(data))
69        }
70    }
71
72    /// Construct a vector without checking that entries are finite.
73    ///
74    /// This crate-internal escape hatch is reserved for finite literals and
75    /// algorithm outputs whose finite invariant is visible at the call site.
76    /// Computed outputs must check their intermediates before using this
77    /// constructor to create an observable [`Vector`].
78    #[inline]
79    pub(crate) const fn new_unchecked(data: [f64; D]) -> Self {
80        Self { data }
81    }
82
83    /// Return the first non-finite stored entry in index order.
84    ///
85    /// Shared by the public raw-storage boundary and crate-internal reparsing
86    /// paths so both report the same first offending index with
87    /// [`LaError::NonFinite`].
88    const fn first_non_finite_entry_in(data: &[f64; D]) -> Option<usize> {
89        let mut i = 0;
90        while i < D {
91            if !data[i].is_finite() {
92                return Some(i);
93            }
94            i += 1;
95        }
96        None
97    }
98
99    /// All-zeros finite vector.
100    ///
101    /// # Examples
102    /// ```
103    /// use la_stack::prelude::*;
104    ///
105    /// let z = Vector::<2>::zero();
106    /// assert_eq!(z.into_array(), [0.0, 0.0]);
107    /// ```
108    #[inline]
109    pub const fn zero() -> Self {
110        Self::new_unchecked([0.0; D])
111    }
112
113    /// Borrow the finite backing array.
114    ///
115    /// # Examples
116    /// ```
117    /// use la_stack::prelude::*;
118    ///
119    /// # fn main() -> Result<(), LaError> {
120    /// let v = Vector::<2>::try_new([1.0, -2.0])?;
121    /// assert_eq!(v.as_array(), &[1.0, -2.0]);
122    /// # Ok(())
123    /// # }
124    /// ```
125    #[inline]
126    #[must_use]
127    pub const fn as_array(&self) -> &[f64; D] {
128        &self.data
129    }
130
131    /// Consume and return the finite backing array.
132    ///
133    /// # Examples
134    /// ```
135    /// use la_stack::prelude::*;
136    ///
137    /// # fn main() -> Result<(), LaError> {
138    /// let v = Vector::<2>::try_new([1.0, 2.0])?;
139    /// let a = v.into_array();
140    /// assert_eq!(a, [1.0, 2.0]);
141    /// # Ok(())
142    /// # }
143    /// ```
144    #[inline]
145    #[must_use]
146    pub const fn into_array(self) -> [f64; D] {
147        self.data
148    }
149
150    /// Dot product.
151    ///
152    /// Terms are accumulated in `f64` using [`f64::mul_add`] at each index.
153    /// Intermediate rounding occurs, and this method does not provide a
154    /// certified absolute rounding bound for the returned dot product. Raw
155    /// `Vector` values are finite by construction, so this method only checks
156    /// whether the accumulation overflows to NaN or infinity.
157    ///
158    /// # Examples
159    /// ```
160    /// use la_stack::prelude::*;
161    ///
162    /// # fn main() -> Result<(), LaError> {
163    /// let a = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
164    /// let b = Vector::<3>::try_new([-2.0, 0.5, 4.0])?;
165    /// assert!((a.dot(b)? - 11.0).abs() <= 1e-12);
166    /// # Ok(())
167    /// # }
168    /// ```
169    ///
170    /// # Errors
171    /// Returns [`LaError::NonFinite`] when the accumulated dot product overflows
172    /// to NaN or infinity.
173    #[inline]
174    pub const fn dot(self, other: Self) -> Result<f64, LaError> {
175        let lhs = self.as_array();
176        let rhs = other.as_array();
177        let mut acc = 0.0;
178        let mut i = 0;
179        while i < D {
180            acc = lhs[i].mul_add(rhs[i], acc);
181            if !acc.is_finite() {
182                cold_path();
183                return Err(LaError::non_finite_at(i));
184            }
185            i += 1;
186        }
187        Ok(acc)
188    }
189
190    /// Squared Euclidean norm.
191    ///
192    /// This is computed as `dot(self, self)`, so `norm2_sq` has the same
193    /// `f64` [`mul_add`](f64::mul_add) accumulation behavior as [`dot`](Self::dot).
194    /// Intermediate rounding occurs, and this method does not provide a
195    /// certified absolute rounding bound for the returned squared norm.
196    /// `Vector` values are finite by construction, so this method only checks
197    /// whether the accumulation overflows to NaN or infinity.
198    ///
199    /// # Examples
200    /// ```
201    /// use la_stack::prelude::*;
202    ///
203    /// # fn main() -> Result<(), LaError> {
204    /// let v = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
205    /// assert!((v.norm2_sq()? - 14.0).abs() <= 1e-12);
206    /// # Ok(())
207    /// # }
208    /// ```
209    ///
210    /// # Errors
211    /// Returns [`LaError::NonFinite`] when the accumulated norm overflows to NaN
212    /// or infinity.
213    #[inline]
214    pub const fn norm2_sq(self) -> Result<f64, LaError> {
215        self.dot(self)
216    }
217}
218
219impl<const D: usize> Default for Vector<D> {
220    #[inline]
221    fn default() -> Self {
222        Self::zero()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    use core::hint::black_box;
231
232    use approx::assert_abs_diff_eq;
233    use pastey::paste;
234
235    macro_rules! gen_public_api_vector_tests {
236        ($d:literal) => {
237            paste! {
238                #[test]
239                fn [<public_api_vector_new_as_array_into_array_ $d d>]() {
240                    let arr = {
241                        let mut arr = [0.0f64; $d];
242                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
243                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
244                            *dst = *src;
245                        }
246                        arr
247                    };
248
249                    let v = Vector::<$d>::new(arr);
250
251                    for i in 0..$d {
252                        assert_abs_diff_eq!(v.as_array()[i], arr[i], epsilon = 0.0);
253                    }
254
255                    let out = v.into_array();
256                    for i in 0..$d {
257                        assert_abs_diff_eq!(out[i], arr[i], epsilon = 0.0);
258                    }
259                }
260
261                #[test]
262                fn [<public_api_vector_zero_as_array_into_array_default_ $d d>]() {
263                    let z = Vector::<$d>::zero();
264                    for &x in z.as_array() {
265                        assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
266                    }
267                    for x in z.into_array() {
268                        assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
269                    }
270
271                    let d = Vector::<$d>::default();
272                    for x in d.into_array() {
273                        assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
274                    }
275                }
276
277                #[test]
278                fn [<public_api_vector_dot_and_norm2_sq_ $d d>]() {
279                    // Use black_box to avoid constant-folding/inlining eliminating the actual dot loop,
280                    // which can make coverage tools report the mul_add line as uncovered.
281
282                    let a_arr = {
283                        let mut arr = [0.0f64; $d];
284                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
285                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
286                            *dst = black_box(*src);
287                        }
288                        arr
289                    };
290                    let b_arr = {
291                        let mut arr = [0.0f64; $d];
292                        let values = [-2.0f64, 0.5, 4.0, -1.0, 2.0];
293                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
294                            *dst = black_box(*src);
295                        }
296                        arr
297                    };
298
299                    let expected_dot = {
300                        let mut acc = 0.0;
301                        let mut i = 0;
302                        while i < $d {
303                            acc = a_arr[i].mul_add(b_arr[i], acc);
304                            i += 1;
305                        }
306                        acc
307                    };
308                    let expected_norm2_sq = {
309                        let mut acc = 0.0;
310                        let mut i = 0;
311                        while i < $d {
312                            acc = a_arr[i].mul_add(a_arr[i], acc);
313                            i += 1;
314                        }
315                        acc
316                    };
317
318                    let a = Vector::<$d>::new(black_box(a_arr));
319                    let b = Vector::<$d>::new(black_box(b_arr));
320
321                    // Call via (black_boxed) fn pointers to discourage inlining, improving line-level coverage
322                    // attribution for the loop body.
323                    let dot_fn: fn(Vector<$d>, Vector<$d>) -> Result<f64, LaError> =
324                        black_box(Vector::<$d>::dot);
325                    let norm2_sq_fn: fn(Vector<$d>) -> Result<f64, LaError> =
326                        black_box(Vector::<$d>::norm2_sq);
327
328                    assert_abs_diff_eq!(
329                        dot_fn(black_box(a), black_box(b)).unwrap(),
330                        expected_dot,
331                        epsilon = 1e-14
332                    );
333                    assert_abs_diff_eq!(
334                        norm2_sq_fn(black_box(a)).unwrap(),
335                        expected_norm2_sq,
336                        epsilon = 1e-14
337                    );
338                }
339
340                #[test]
341                fn [<public_api_vector_try_new_rejects_nonfinite_ $d d>]() {
342                    let mut a_arr = [1.0f64; $d];
343                    a_arr[$d - 1] = f64::NAN;
344
345                    assert_eq!(
346                        Vector::<$d>::try_new(a_arr),
347                        Err(LaError::NonFinite {
348                            row: None,
349                            col: $d - 1,
350                        })
351                    );
352                }
353
354                #[test]
355                fn [<public_api_vector_try_new_rejects_nonfinite_rhs_fixture_ $d d>]() {
356                    let mut b_arr = [1.0f64; $d];
357                    b_arr[0] = f64::INFINITY;
358
359                    assert_eq!(
360                        Vector::<$d>::try_new(b_arr),
361                        Err(LaError::NonFinite { row: None, col: 0 })
362                    );
363                }
364
365                #[test]
366                fn [<public_api_vector_dot_and_norm2_sq_reject_overflow_ $d d>]() {
367                    let mut a_arr = [1.0f64; $d];
368                    a_arr[0] = f64::MAX;
369                    let a = Vector::<$d>::new(a_arr);
370
371                    let mut b_arr = [1.0f64; $d];
372                    b_arr[0] = 2.0;
373                    let b = Vector::<$d>::new(b_arr);
374
375                    assert_eq!(a.dot(b), Err(LaError::NonFinite { row: None, col: 0 }));
376                    assert_eq!(a.norm2_sq(), Err(LaError::NonFinite { row: None, col: 0 }));
377                }
378
379            }
380        };
381    }
382
383    // Mirror delaunay-style multi-dimension tests.
384    gen_public_api_vector_tests!(2);
385    gen_public_api_vector_tests!(3);
386    gen_public_api_vector_tests!(4);
387    gen_public_api_vector_tests!(5);
388}