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}