hermes_simd_core/cow/combinators.rs
1//! CoW combinators: `zip_cow`, `transform_in_place`, `reduce`, arithmetic shorthands,
2//! and ergonomic `From`/`Extend` conversions for `SimdCow`.
3//!
4//! # Safety
5//!
6//! Two obligations recur here. Kernel calls are `#[target_feature]`-gated, and
7//! that precondition holds by construction: a `SimdCow` exists only for an
8//! architecture the host can execute, since its borrowed form comes from
9//! [`SimdView::new`](crate::view::SimdView::new) and its owned constructors
10//! assert the same condition. The second is local — these routines build their
11//! output buffer with `with_capacity` and write it through a raw pointer,
12//! raising the length only once every element is initialized. That avoids both
13//! a zero-fill of a buffer about to be overwritten and any `&mut [T]` spanning
14//! uninitialized elements, so each such site carries a `SAFETY` comment showing
15//! the write coverage. `gather` and `prefix_scan` reserve capacity and fill it
16//! through the view's `*_into_uninit` methods over
17//! [`AlignedVec::spare_capacity_mut`](crate::vec::AlignedVec::spare_capacity_mut),
18//! then raise the length once those report success, so those paths never zero
19//! the buffer either.
20
21use super::types::SimdCow;
22use crate::align::Alignment;
23use crate::arch::SimdArch;
24use crate::execution::Unmasked;
25use crate::kernel::SimdKernel;
26use crate::ops::{ElementOp, ReductionOp};
27use crate::scalar::Scalar;
28use crate::vec::AlignedVec;
29use crate::view::{SimdError, SimdView};
30
31impl<'a, T: 'a, Arch, Align> SimdCow<'a, T, Arch, Align>
32where
33 T: Scalar,
34 Arch: SimdArch + SimdKernel<T>,
35 Align: Alignment,
36{
37 /// Sum all elements using `SimdView::sum`.
38 #[inline(always)]
39 pub fn sum(&self) -> T {
40 self.view().sum()
41 }
42
43 /// Compute the dot product with another `SimdCow`.
44 ///
45 /// # Errors
46 /// Returns `SimdError::LengthMismatch` if lengths differ.
47 #[inline(always)]
48 pub fn dot(&self, other: &Self) -> Result<T, SimdError> {
49 self.view().dot(&other.view())
50 }
51
52 /// Apply an `ElementOp` pairwise to `self` and `other`, returning a fully-owned
53 /// `SimdCow<'static, T, Arch, Align>` backed by a single `AlignedVec` allocation.
54 ///
55 /// The SIMD vectorized loop processes `floor(len / LANE_COUNT) * LANE_COUNT` elements.
56 /// The scalar tail uses `Op::apply_scalar` directly on individual elements — no
57 /// vector loads or stores are performed in the tail, avoiding all boundary-condition UB.
58 ///
59 /// # Errors
60 /// Returns `SimdError::LengthMismatch` if `self.len() != other.len()`.
61 pub fn zip_cow<Op: ElementOp<T>>(
62 &self,
63 other: &SimdCow<'_, T, Arch, Align>,
64 _op: Op,
65 ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
66 if self.len() != other.len() {
67 return Err(SimdError::LengthMismatch);
68 }
69 let len = self.len();
70 if len == 0 {
71 return Ok(SimdCow::Owned(AlignedVec::new()));
72 }
73
74 let mut out = AlignedVec::with_capacity(len);
75 let out_ptr: *mut T = out.as_mut_ptr();
76
77 let view_self = self.view();
78 let view_other = other.view();
79
80 let mut chunks_self = view_self.simd_chunks();
81 let mut chunks_other = view_other.simd_chunks();
82
83 let mut i = 0usize;
84 for (chunk_self, chunk_other) in (&mut chunks_self).zip(&mut chunks_other) {
85 unsafe {
86 let va = if crate::align::is_aligned_for_arch::<Arch, Align>() {
87 Arch::load_aligned(chunk_self.as_ptr())
88 } else {
89 Arch::load_unaligned(chunk_self.as_ptr())
90 };
91 let vb = if crate::align::is_aligned_for_arch::<Arch, Align>() {
92 Arch::load_aligned(chunk_other.as_ptr())
93 } else {
94 Arch::load_unaligned(chunk_other.as_ptr())
95 };
96 let vr = _op.apply::<Arch>(va, vb);
97 if crate::align::is_aligned_for_arch::<Arch, Align>() {
98 Arch::store_aligned(out_ptr.add(i), vr);
99 } else {
100 Arch::store_unaligned(out_ptr.add(i), vr);
101 }
102 }
103 i += Arch::LANE_COUNT;
104 }
105
106 let remainder_self = chunks_self.remainder();
107 let remainder_other = chunks_other.remainder();
108
109 for (&a, &b) in remainder_self.iter().zip(remainder_other.iter()) {
110 unsafe {
111 core::ptr::write(out_ptr.add(i), _op.apply_scalar(a, b));
112 }
113 i += 1;
114 }
115
116 // SAFETY: `with_capacity(len)` reserved `len` elements, and the vector
117 // and remainder loops above have together written every one of them
118 // through `out_ptr`. The length is raised only now, so no reference to
119 // this buffer ever spanned uninitialized memory.
120 unsafe {
121 out.set_len(len);
122 }
123
124 Ok(SimdCow::Owned(out))
125 }
126
127 /// Apply an `ElementOp` in-place: `self[i] = op(self[i], other[i])`.
128 ///
129 /// If `self` is `Borrowed`, promotes to `Owned` first (one allocation).
130 /// Subsequent calls on the same already-owned `self` are allocation-free.
131 ///
132 /// The scalar tail uses `Op::apply_scalar` to avoid vector boundary UB.
133 ///
134 /// # Errors
135 /// Returns `SimdError::LengthMismatch` if `self.len() != other.len()`.
136 pub fn transform_in_place<Op: ElementOp<T>>(
137 &mut self,
138 other: &SimdCow<'_, T, Arch, Align>,
139 _op: Op,
140 ) -> Result<(), SimdError> {
141 if self.len() != other.len() {
142 return Err(SimdError::LengthMismatch);
143 }
144 // `to_mut` promotes borrowed → owned (one allocation if borrowed, free if owned).
145 // Use the returned reference directly — no secondary match required.
146 let out_slice = self.to_mut().as_mut_slice();
147
148 let other_view = other.view();
149
150 let self_view: SimdView<'_, T, Arch, Align, Unmasked, &mut [T]> =
151 SimdView::new_mut(out_slice).expect("alignment invariant violated");
152
153 let mut chunks_self = self_view.simd_chunks_mut();
154 let mut chunks_other = other_view.simd_chunks();
155
156 for (mut chunk_self, chunk_other) in (&mut chunks_self).zip(&mut chunks_other) {
157 unsafe {
158 let va = if crate::align::is_aligned_for_arch::<Arch, Align>() {
159 Arch::load_aligned(chunk_self.as_ptr())
160 } else {
161 Arch::load_unaligned(chunk_self.as_ptr())
162 };
163 let vb = if crate::align::is_aligned_for_arch::<Arch, Align>() {
164 Arch::load_aligned(chunk_other.as_ptr())
165 } else {
166 Arch::load_unaligned(chunk_other.as_ptr())
167 };
168 let vr = _op.apply::<Arch>(va, vb);
169 if crate::align::is_aligned_for_arch::<Arch, Align>() {
170 Arch::store_aligned(chunk_self.as_mut_ptr(), vr);
171 } else {
172 Arch::store_unaligned(chunk_self.as_mut_ptr(), vr);
173 }
174 }
175 }
176
177 let tail_self = chunks_self.into_remainder();
178 let tail_other = chunks_other.remainder();
179 for (a, &b) in tail_self.iter_mut().zip(tail_other.iter()) {
180 *a = _op.apply_scalar(*a, b);
181 }
182
183 Ok(())
184 }
185
186 /// Apply a `ReductionOp` to this `SimdCow`, delegating to `SimdView::reduce`.
187 ///
188 /// Monomorphization is shared with the view path — no duplicate code.
189 #[inline(always)]
190 pub fn reduce<Op: ReductionOp<T>>(&self, op: Op) -> T {
191 self.view().reduce(op)
192 }
193
194 // -----------------------------------------------------------------------
195 // Arithmetic combinators — each returns `SimdCow<'static, ...>` (owned)
196 // -----------------------------------------------------------------------
197
198 /// Elementwise addition: `self[i] + other[i]`.
199 ///
200 /// Allocates one `AlignedVec` output. Zero-copy on both operands.
201 ///
202 /// # Errors
203 /// Returns `SimdError::LengthMismatch` if lengths differ.
204 #[inline(always)]
205 pub fn add_cow(
206 &self,
207 other: &SimdCow<'_, T, Arch, Align>,
208 ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
209 self.zip_cow(other, crate::ops::Add)
210 }
211
212 /// Elementwise subtraction: `self[i] - other[i]`.
213 ///
214 /// # Errors
215 /// Returns `SimdError::LengthMismatch` if lengths differ.
216 #[inline(always)]
217 pub fn sub_cow(
218 &self,
219 other: &SimdCow<'_, T, Arch, Align>,
220 ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
221 self.zip_cow(other, crate::ops::Sub)
222 }
223
224 /// Elementwise multiplication: `self[i] * other[i]`.
225 ///
226 /// # Errors
227 /// Returns `SimdError::LengthMismatch` if lengths differ.
228 #[inline(always)]
229 pub fn mul_cow(
230 &self,
231 other: &SimdCow<'_, T, Arch, Align>,
232 ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
233 self.zip_cow(other, crate::ops::Mul)
234 }
235}
236
237// ---------------------------------------------------------------------------
238// Ergonomic conversions
239// ---------------------------------------------------------------------------
240
241/// Adopt an `AlignedVec` as an owned `SimdCow` — zero-cost, no allocation.
242impl<'a, T, Arch, Align> From<AlignedVec<T, Align>> for SimdCow<'a, T, Arch, Align>
243where
244 Arch: SimdArch,
245 Align: Alignment,
246{
247 #[inline]
248 fn from(vec: AlignedVec<T, Align>) -> Self {
249 Self::Owned(vec)
250 }
251}
252
253/// Copy a standard `Vec<T>` into a new owned `SimdCow`, allocating one aligned buffer.
254impl<'a, T: Copy, Arch, Align> From<alloc::vec::Vec<T>> for SimdCow<'a, T, Arch, Align>
255where
256 Arch: SimdArch,
257 Align: Alignment,
258{
259 #[inline]
260 fn from(v: alloc::vec::Vec<T>) -> Self {
261 Self::Owned(AlignedVec::from_slice(&v))
262 }
263}
264
265impl<'a, T: Copy + 'a, Arch, Align> Extend<T> for SimdCow<'a, T, Arch, Align>
266where
267 Arch: SimdArch + SimdKernel<T>,
268 Align: Alignment,
269 T: Scalar,
270{
271 /// Extend the `SimdCow`, promoting to owned if currently borrowed.
272 ///
273 /// After promotion, subsequent `extend` calls are allocation-free as long as the
274 /// `AlignedVec` has sufficient capacity.
275 #[inline]
276 fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
277 let iter = iter.into_iter();
278 let vec = self.to_mut();
279 // Reserve the iterator's lower-bound size up front so a bulk extend does
280 // one reallocation rather than the ⌈log₂ n⌉ a push loop would incur
281 // (`size_hint().0` is exact for the common sized-iterator sources).
282 vec.reserve(iter.size_hint().0);
283 for item in iter {
284 vec.push(item);
285 }
286 }
287}