Skip to main content

moirai_parallel/
ops.rs

1//! Synchronous data-parallel operators over the unified scheduler.
2//!
3//! # Safety
4//!
5//! The mutable operators split a buffer across worker tasks through
6//! `DisjointMutPtr`, which hands out `&mut` by raw pointer with no
7//! borrow-checker aliasing proof. Two executor contracts make
8//! every such access sound; each per-site `SAFETY` comment appeals to one of
9//! them:
10//!
11//! - **Disjoint partition.** `global().for_each_indexed(count, f)` invokes `f`
12//!   with each index in `0..count` exactly once. The operators map that index
13//!   (or a `chunk_size`-strided range derived from it) to a *disjoint* slice of
14//!   the buffer, so no two concurrent tasks ever form `&mut` to the same
15//!   element. Multi-buffer operators additionally rely on the caller's distinct
16//!   `&mut [_]` arguments being non-aliasing (guaranteed by the borrow checker
17//!   at the call site).
18//! - **All-or-error collect.** The `map_collect_*` helpers build a
19//!   `Vec<MaybeUninit<R>>`, `set_len` it (sound — `MaybeUninit` needs no
20//!   initialization), fill every slot through the disjoint-partition contract,
21//!   then reinterpret it as `Vec<R>`. `for_each_indexed` returns `Ok` only after
22//!   writing every index, so the reinterpretation is reached only when all slots
23//!   are initialized; a task panic instead surfaces as `Err`, which `.expect()`
24//!   turns into a propagating panic that unwinds with the buffer still typed
25//!   `MaybeUninit<R>` (its contents are not dropped — a leak of the written
26//!   values on panic, never a use of uninitialized memory).
27
28use super::DisjointMutPtr;
29use crate::policy::{ExecutionPolicy, Parallel};
30use moirai_core::error::{ExecutorError, ExecutorResult};
31use moirai_executor::{global, HybridExecutor, SchedulerScope, SyncTask};
32use std::sync::Mutex;
33
34/// State of the scheduled branch of a join.
35///
36/// The scheduler can refuse a job — while shutting down, or when a worker's
37/// bounded admission queue is full — and it drops the refused job before
38/// returning the error. A branch owned by that job would go with it, so the
39/// closure lives here instead and whichever lane reaches it first takes it.
40/// The caller can therefore still run a branch the scheduler never did.
41enum Branch<F, R> {
42    /// Nobody has claimed this branch yet.
43    Pending(F),
44    /// A lane claimed the branch and has not published a result. Observing
45    /// this once the scope has joined means that lane unwound.
46    Claimed,
47    /// Ran to completion.
48    Done(R),
49}
50
51impl<F, R> Branch<F, R>
52where
53    F: FnOnce() -> R,
54{
55    /// Take the closure if this lane is the one that gets to run it.
56    fn claim(&mut self) -> Option<F> {
57        match std::mem::replace(self, Self::Claimed) {
58            Self::Pending(branch) => Some(branch),
59            other => {
60                *self = other;
61                None
62            }
63        }
64    }
65
66    fn complete(&mut self, result: R) {
67        *self = Self::Done(result);
68    }
69
70    /// Run the branch on a lane that shares the slot, unless another lane
71    /// already claimed it.
72    ///
73    /// The lock is released before the closure runs, so a branch never holds it
74    /// across arbitrary caller code and a panicking branch cannot poison it.
75    fn run_shared(slot: &Mutex<Self>) {
76        let Some(branch) = lock(slot).claim() else {
77            return;
78        };
79        let result = branch();
80        lock(slot).complete(result);
81    }
82
83    /// Run a branch that never leaves this thread.
84    fn run_here(&mut self) {
85        let Some(branch) = self.claim() else {
86            return;
87        };
88        let result = branch();
89        self.complete(result);
90    }
91
92    /// Take the finished value.
93    fn into_result(self) -> R {
94        match self {
95            Self::Done(result) => result,
96            _ => panic!("invariant: a join branch neither ran nor reported failure"),
97        }
98    }
99}
100
101/// Lock without propagating poisoning: every path that touches a slot leaves
102/// it in a consistent state, and [`Branch::run_shared`] never holds the lock
103/// across the branch closure, so a poisoned flag carries no information here.
104fn lock<T>(slot: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
105    slot.lock()
106        .unwrap_or_else(std::sync::PoisonError::into_inner)
107}
108
109/// Reclaim a slot once every lane that shared it has finished.
110fn lock_owned<T>(slot: Mutex<T>) -> T {
111    slot.into_inner()
112        .unwrap_or_else(std::sync::PoisonError::into_inner)
113}
114
115/// Run two closures to completion and return both results.
116///
117/// This is the synchronous Rayon-style `join` shape. The policy is selected at
118/// compile time; [`Sequential`](crate::Sequential) runs both closures on the
119/// caller, [`Parallel`] schedules the left closure on the unified scheduler and
120/// runs the right closure on the caller lane, and [`crate::Adaptive`] currently
121/// stays sequential for a fixed two-branch join.
122///
123/// A branch the scheduler refuses runs on the caller instead, so a shutting-down
124/// or saturated executor makes the join sequential rather than losing a branch.
125///
126/// # Panics
127///
128/// Panics if a branch panicked, propagating the failure on the caller's thread
129/// as rayon does.
130pub fn join_with<P, A, B, RA, RB>(left: A, right: B) -> (RA, RB)
131where
132    P: ExecutionPolicy,
133    A: FnOnce() -> RA + Send,
134    B: FnOnce() -> RB,
135    RA: Send,
136{
137    if !P::parallelize_pair() {
138        return (left(), right());
139    }
140
141    join_on(global(), left, right)
142}
143
144/// [`join_with`]'s parallel path against a named executor.
145///
146/// Separate from the public entry so the refusal path can be exercised against
147/// a shut-down executor in tests.
148pub(crate) fn join_on<A, B, RA, RB>(executor: &HybridExecutor, left: A, right: B) -> (RA, RB)
149where
150    A: FnOnce() -> RA + Send,
151    B: FnOnce() -> RB,
152    RA: Send,
153{
154    // Only the left branch crosses a lane boundary, so only it needs a shared
155    // slot. The right branch stays on this thread and is reborrowed by the
156    // scope body, which leaves it runnable here if the body returns early.
157    let left_slot = Mutex::new(Branch::Pending(left));
158    let mut right_slot = Branch::Pending(right);
159
160    let forked = executor.scope::<SyncTask, _>(|scope| {
161        scope.spawn(|_| Branch::run_shared(&left_slot))?;
162        // Enter the scheduler before the caller takes its own branch, so the
163        // two overlap instead of running back to back.
164        scope.flush()?;
165        right_slot.run_here();
166        Ok(())
167    });
168
169    match forked {
170        Ok(()) => {}
171        // The scheduler refused the job and dropped it unexecuted, so neither
172        // branch is guaranteed to have run. Both claims are idempotent: a
173        // branch that did run is no longer `Pending`.
174        Err(ExecutorError::ShuttingDown | ExecutorError::ResourceExhausted(_)) => {
175            Branch::run_shared(&left_slot);
176            right_slot.run_here();
177        }
178        Err(error) => panic!("invariant: scheduled join branch failed ({error})"),
179    }
180
181    (
182        lock_owned(left_slot).into_result(),
183        right_slot.into_result(),
184    )
185}
186
187/// Adaptive Rayon-style two-closure join.
188///
189/// Use [`join_with`] to force a specific execution policy.
190pub fn join<A, B, RA, RB>(left: A, right: B) -> (RA, RB)
191where
192    A: FnOnce() -> RA + Send,
193    B: FnOnce() -> RB,
194    RA: Send,
195{
196    join_with::<crate::Adaptive, _, _, _, _>(left, right)
197}
198
199/// Borrowing scope for spawning parallel sub-tasks that may capture non-`'static`
200/// references.
201///
202/// Created by [`scope`]. Each [`Scope::spawn`] call registers a job on the unified
203/// scheduler; the scope blocks until every spawned job has completed before the
204/// body closure returns, so borrowed data cannot escape the scope.
205///
206/// This is the Rayon-style `scope` shape, adapted to Moirai's unified hybrid
207/// scheduler. Unlike [`join`], which forks exactly two branches, `scope` allows
208/// an arbitrary number of sub-tasks to be spawned and joined within a single
209/// region.
210pub struct Scope<'scope> {
211    inner: &'scope SchedulerScope<'scope, SyncTask>,
212}
213
214impl<'scope> Scope<'scope> {
215    /// Spawn a parallel sub-task within this scope.
216    ///
217    /// The task may borrow values that outlive the scope call. The scope waits
218    /// for every spawned task before returning, so borrowed data cannot escape.
219    /// A task the scheduler's admission queue turns away runs on the calling
220    /// thread instead, so backpressure costs parallelism rather than the task.
221    ///
222    /// # Panics
223    ///
224    /// Panics if the underlying scheduler refuses to register the task, which
225    /// registration itself does not do — the failure surfaces from [`scope`]
226    /// when the scheduler is shutting down.
227    #[inline]
228    pub fn spawn<F>(&self, task: F)
229    where
230        F: FnOnce() + Send + 'scope,
231    {
232        self.inner
233            .spawn(move |_| task())
234            .expect("moirai global executor: scope spawn");
235    }
236}
237
238/// Create a borrowing scope for parallel sub-tasks.
239///
240/// Within the body closure, [`Scope::spawn`] registers jobs on the unified
241/// scheduler. The scope blocks until every spawned job has completed before
242/// returning, so tasks may borrow non-`'static` data from the enclosing
243/// environment.
244///
245/// This is the Rayon-style `scope` shape, adapted to Moirai's unified hybrid
246/// scheduler.
247///
248/// # Examples
249///
250/// ```
251/// use moirai_parallel::scope;
252///
253/// let data: Vec<u64> = (0..1000).collect();
254/// use std::sync::atomic::{AtomicU64, Ordering};
255///
256/// let sum = AtomicU64::new(0);
257/// scope(|s| {
258///     s.spawn(|| {
259///         sum.fetch_add(data.iter().sum::<u64>(), Ordering::Relaxed);
260///     });
261///     s.spawn(|| {
262///         sum.fetch_add(data.len() as u64, Ordering::Relaxed);
263///     });
264/// });
265/// assert_eq!(sum.load(Ordering::Relaxed), data.iter().sum::<u64>() + 1000);
266/// ```
267#[inline]
268pub fn scope<F, R>(body: F) -> R
269where
270    F: for<'scope> FnOnce(&Scope<'scope>) -> R,
271    R: Send,
272{
273    let mut result = None;
274    global()
275        .scope::<SyncTask, _>(|inner| {
276            let scope = Scope { inner };
277            result = Some(body(&scope));
278            ExecutorResult::Ok(())
279        })
280        .expect("moirai global executor: scope");
281
282    result.expect("scoped body must complete")
283}
284
285/// Apply `f` to every element of `data`, scheduled by policy `P`.
286pub fn for_each_with<P, T, F>(data: &[T], f: F)
287where
288    P: ExecutionPolicy,
289    T: Sync,
290    F: Fn(&T) + Send + Sync,
291{
292    let n = data.len();
293    if n == 0 {
294        return;
295    }
296    if !P::parallelize(n) {
297        data.iter().for_each(f);
298        return;
299    }
300    let f = &f;
301    global()
302        .for_each_indexed::<SyncTask, _>(n, move |i| f(&data[i]))
303        .expect("moirai global executor: for_each_with");
304}
305
306/// Apply `f` to every element of `data` in place, scheduled by policy `P`.
307pub fn for_each_mut_with<P, T, F>(data: &mut [T], f: F)
308where
309    P: ExecutionPolicy,
310    T: Send,
311    F: Fn(&mut T) + Send + Sync,
312{
313    let n = data.len();
314    if n == 0 {
315        return;
316    }
317    if !P::parallelize(n) {
318        data.iter_mut().for_each(f);
319        return;
320    }
321    let base = DisjointMutPtr(data.as_mut_ptr());
322    let f = &f;
323    global()
324        .for_each_indexed::<SyncTask, _>(n, move |i| {
325            // SAFETY: the scheduler visits each index in `0..n` exactly once
326            // across disjoint chunks, so no two tasks alias element `i`; `data`
327            // is borrowed mutably for the whole joined call.
328            f(unsafe { base.get_mut(i) });
329        })
330        .expect("moirai global executor: for_each_mut_with");
331}
332
333/// Apply `f(index, &element)` to every element of `data`, scheduled by policy `P`.
334pub fn enumerate_with<P, T, F>(data: &[T], f: F)
335where
336    P: ExecutionPolicy,
337    T: Sync,
338    F: Fn(usize, &T) + Send + Sync,
339{
340    let n = data.len();
341    if n == 0 {
342        return;
343    }
344    if !P::parallelize(n) {
345        data.iter().enumerate().for_each(|(i, x)| f(i, x));
346        return;
347    }
348    let f = &f;
349    global()
350        .for_each_indexed::<SyncTask, _>(n, move |i| f(i, &data[i]))
351        .expect("moirai global executor: enumerate_with");
352}
353
354/// Apply `f(index, &mut element)` to every element of `data` in place,
355/// scheduled by policy `P`.
356pub fn enumerate_mut_with<P, T, F>(data: &mut [T], f: F)
357where
358    P: ExecutionPolicy,
359    T: Send,
360    F: Fn(usize, &mut T) + Send + Sync,
361{
362    let n = data.len();
363    if n == 0 {
364        return;
365    }
366    if !P::parallelize(n) {
367        data.iter_mut().enumerate().for_each(|(i, x)| f(i, x));
368        return;
369    }
370    let base = DisjointMutPtr(data.as_mut_ptr());
371    let f = &f;
372    global()
373        .for_each_indexed::<SyncTask, _>(n, move |i| {
374            // SAFETY: each index in `0..n` is visited exactly once; see
375            // `for_each_mut_with`.
376            f(i, unsafe { base.get_mut(i) });
377        })
378        .expect("moirai global executor: enumerate_mut_with");
379}
380
381/// Apply `f` to every index in `0..len` in parallel, scheduled by policy `P`.
382///
383/// Synchronous equivalent of rayon's `(0..len).into_par_iter().for_each(f)`. Use
384/// when the work is keyed by index and writes through external disjoint state
385/// (atomics, per-index channels) rather than returning a value.
386pub fn for_each_index_with<P, F>(len: usize, f: F)
387where
388    P: ExecutionPolicy,
389    F: Fn(usize) + Send + Sync,
390{
391    if len == 0 {
392        return;
393    }
394    if !P::parallelize(len) {
395        (0..len).for_each(f);
396        return;
397    }
398    let f = &f;
399    global()
400        .for_each_indexed::<SyncTask, _>(len, f)
401        .expect("moirai global executor: for_each_index_with");
402}
403
404/// Apply `f` to each consecutive `chunk_size`-element mutable chunk of `data` in
405/// parallel, scheduled by policy `P`. The final chunk may be shorter.
406///
407/// Synchronous equivalent of rayon's `data.par_chunks_mut(chunk_size).for_each(f)`
408/// — the natural shape for batched/lane-wise transforms.
409pub fn for_each_chunk_mut_with<P, T, F>(data: &mut [T], chunk_size: usize, f: F)
410where
411    P: ExecutionPolicy,
412    T: Send,
413    F: Fn(&mut [T]) + Send + Sync,
414{
415    let n = data.len();
416    if n == 0 || chunk_size == 0 {
417        return;
418    }
419    let num_chunks = n.div_ceil(chunk_size);
420    if !P::parallelize(n) || num_chunks <= 1 {
421        data.chunks_mut(chunk_size).for_each(&f);
422        return;
423    }
424    let base = DisjointMutPtr(data.as_mut_ptr());
425    let f = &f;
426    global()
427        .for_each_indexed::<SyncTask, _>(num_chunks, move |c| {
428            let start = c * chunk_size;
429            if start >= n {
430                return;
431            }
432            let end = (start + chunk_size).min(n);
433            // SAFETY: the chunks `[start, end)` for distinct `c` are pairwise
434            // disjoint and each is visited exactly once, so no two tasks alias.
435            let chunk =
436                unsafe { core::slice::from_raw_parts_mut(base.base().add(start), end - start) };
437            f(chunk);
438        })
439        .expect("moirai global executor: for_each_chunk_mut_with");
440}
441
442/// Apply `f(state, chunk)` to each consecutive mutable chunk, creating one
443/// reusable state value per scheduled worker shard.
444///
445/// This is the scratch-buffer form of [`for_each_chunk_mut_with`]. It matches
446/// the allocation discipline of Rayon-style `for_each_init`/`for_each_with`
447/// loops: a worker shard initializes `S` once, then reuses it for every logical
448/// chunk assigned to that shard.
449pub fn for_each_chunk_mut_with_state<P, T, S, Init, F>(
450    data: &mut [T],
451    chunk_size: usize,
452    init: Init,
453    f: F,
454) where
455    P: ExecutionPolicy,
456    T: Send,
457    S: Send,
458    Init: Fn() -> S + Send + Sync,
459    F: Fn(&mut S, &mut [T]) + Send + Sync,
460{
461    let n = data.len();
462    if n == 0 || chunk_size == 0 {
463        return;
464    }
465    let num_chunks = n.div_ceil(chunk_size);
466    if !P::parallelize(n) || num_chunks <= 1 {
467        let mut state = init();
468        for chunk in data.chunks_mut(chunk_size) {
469            f(&mut state, chunk);
470        }
471        return;
472    }
473
474    let workers = themis::CpuTopology::detect()
475        .map(|topology| topology.logical_processors())
476        .or_else(|| std::thread::available_parallelism().ok().map(|n| n.get()))
477        .unwrap_or(1)
478        .min(num_chunks)
479        .max(1);
480    let chunks_per_worker = num_chunks.div_ceil(workers);
481    let base = DisjointMutPtr(data.as_mut_ptr());
482    let init = &init;
483    let f = &f;
484    global()
485        .for_each_indexed::<SyncTask, _>(workers, move |worker| {
486            let first_chunk = worker * chunks_per_worker;
487            let last_chunk = ((worker + 1) * chunks_per_worker).min(num_chunks);
488            if first_chunk >= last_chunk {
489                return;
490            }
491            let mut state = init();
492            for chunk_index in first_chunk..last_chunk {
493                let start = chunk_index * chunk_size;
494                let end = (start + chunk_size).min(n);
495                // SAFETY: logical chunks are assigned to exactly one worker and
496                // are pairwise disjoint, so each mutable slice is exclusive.
497                let chunk =
498                    unsafe { core::slice::from_raw_parts_mut(base.base().add(start), end - start) };
499                f(&mut state, chunk);
500            }
501        })
502        .expect("moirai global executor: for_each_chunk_mut_with_state");
503}
504
505/// Apply `f(index, a_chunk, b_chunk)` to paired `chunk_size`-element mutable
506/// chunks of two **distinct** buffers in parallel, scheduled by policy `P`.
507///
508/// Synchronous equivalent of
509/// `a.par_chunks_mut(n).zip(b.par_chunks_mut(n)).enumerate().for_each(f)`. The
510/// number of chunks is derived from `a`; `b` is chunked identically, so callers
511/// must ensure `b.len() >= a.len()` (typically equal). The two buffers must not
512/// alias.
513pub fn for_each_chunk_pair_mut_enumerated_with<P, A, B, F>(
514    a: &mut [A],
515    b: &mut [B],
516    chunk_size: usize,
517    f: F,
518) where
519    P: ExecutionPolicy,
520    A: Send,
521    B: Send,
522    F: Fn(usize, &mut [A], &mut [B]) + Send + Sync,
523{
524    let na = a.len();
525    let nb = b.len();
526    if chunk_size == 0 || na == 0 {
527        return;
528    }
529    let num_chunks = na.div_ceil(chunk_size);
530    if !P::parallelize(na) || num_chunks <= 1 {
531        a.chunks_mut(chunk_size)
532            .zip(b.chunks_mut(chunk_size))
533            .enumerate()
534            .for_each(|(i, (ca, cb))| f(i, ca, cb));
535        return;
536    }
537    let abase = DisjointMutPtr(a.as_mut_ptr());
538    let bbase = DisjointMutPtr(b.as_mut_ptr());
539    let f = &f;
540    global()
541        .for_each_indexed::<SyncTask, _>(num_chunks, move |c| {
542            let start = c * chunk_size;
543            if start >= na || start >= nb {
544                return;
545            }
546            let ea = (start + chunk_size).min(na);
547            let eb = (start + chunk_size).min(nb);
548            // SAFETY: chunks `[start, e*)` for distinct `c` are pairwise disjoint
549            // within each buffer and each is visited once; `a` and `b` are
550            // distinct, non-aliasing buffers, so the two references never alias.
551            let ca =
552                unsafe { core::slice::from_raw_parts_mut(abase.base().add(start), ea - start) };
553            let cb =
554                unsafe { core::slice::from_raw_parts_mut(bbase.base().add(start), eb - start) };
555            f(c, ca, cb);
556        })
557        .expect("moirai global executor: for_each_chunk_pair_mut_enumerated_with");
558}
559
560/// Apply `f(index, a_chunk, b_chunk, c_chunk, d_chunk)` to four **distinct**
561/// mutable buffers chunked identically, scheduled by policy `P`.
562///
563/// This is the four-buffer counterpart to
564/// [`for_each_chunk_pair_mut_enumerated_with`]. It is intended for fused
565/// statistics and stencil bookkeeping kernels where one authoritative pass
566/// updates several output arrays without allocating intermediate tuples.
567pub fn for_each_chunk_quad_mut_enumerated_with<P, A, B, C, D, F>(
568    a: &mut [A],
569    b: &mut [B],
570    c: &mut [C],
571    d: &mut [D],
572    chunk_size: usize,
573    f: F,
574) where
575    P: ExecutionPolicy,
576    A: Send,
577    B: Send,
578    C: Send,
579    D: Send,
580    F: Fn(usize, &mut [A], &mut [B], &mut [C], &mut [D]) + Send + Sync,
581{
582    let na = a.len();
583    let nb = b.len();
584    let nc = c.len();
585    let nd = d.len();
586    assert_eq!(na, nb, "quad chunk buffers must have equal lengths");
587    assert_eq!(na, nc, "quad chunk buffers must have equal lengths");
588    assert_eq!(na, nd, "quad chunk buffers must have equal lengths");
589    if chunk_size == 0 || na == 0 {
590        return;
591    }
592    let num_chunks = na.div_ceil(chunk_size);
593    if !P::parallelize(na) || num_chunks <= 1 {
594        a.chunks_mut(chunk_size)
595            .zip(b.chunks_mut(chunk_size))
596            .zip(c.chunks_mut(chunk_size))
597            .zip(d.chunks_mut(chunk_size))
598            .enumerate()
599            .for_each(|(i, (((ca, cb), cc), cd))| f(i, ca, cb, cc, cd));
600        return;
601    }
602    let abase = DisjointMutPtr(a.as_mut_ptr());
603    let bbase = DisjointMutPtr(b.as_mut_ptr());
604    let cbase = DisjointMutPtr(c.as_mut_ptr());
605    let dbase = DisjointMutPtr(d.as_mut_ptr());
606    let f = &f;
607    global()
608        .for_each_indexed::<SyncTask, _>(num_chunks, move |chunk_index| {
609            let start = chunk_index * chunk_size;
610            if start >= na || start >= nb || start >= nc || start >= nd {
611                return;
612            }
613            let ea = (start + chunk_size).min(na);
614            let eb = (start + chunk_size).min(nb);
615            let ec = (start + chunk_size).min(nc);
616            let ed = (start + chunk_size).min(nd);
617            // SAFETY: chunks `[start, e*)` for distinct `chunk_index` values
618            // are pairwise disjoint within each buffer and each is visited at
619            // most once. The four input buffers are distinct non-aliasing
620            // `&mut` slices, so the returned mutable chunk references cannot
621            // alias each other.
622            let ca =
623                unsafe { core::slice::from_raw_parts_mut(abase.base().add(start), ea - start) };
624            let cb =
625                unsafe { core::slice::from_raw_parts_mut(bbase.base().add(start), eb - start) };
626            let cc =
627                unsafe { core::slice::from_raw_parts_mut(cbase.base().add(start), ec - start) };
628            let cd =
629                unsafe { core::slice::from_raw_parts_mut(dbase.base().add(start), ed - start) };
630            f(chunk_index, ca, cb, cc, cd);
631        })
632        .expect("moirai global executor: for_each_chunk_quad_mut_enumerated_with");
633}
634
635/// Apply `f(index, a_chunk, b_chunk, c_chunk)` to three **distinct** mutable
636/// buffers chunked identically, scheduled by policy `P`.
637///
638/// This is the three-buffer counterpart to
639/// [`for_each_chunk_pair_mut_enumerated_with`].
640pub fn for_each_chunk_triple_mut_enumerated_with<P, A, B, C, F>(
641    a: &mut [A],
642    b: &mut [B],
643    c: &mut [C],
644    chunk_size: usize,
645    f: F,
646) where
647    P: ExecutionPolicy,
648    A: Send,
649    B: Send,
650    C: Send,
651    F: Fn(usize, &mut [A], &mut [B], &mut [C]) + Send + Sync,
652{
653    let na = a.len();
654    let nb = b.len();
655    let nc = c.len();
656    assert_eq!(na, nb, "triple chunk buffers must have equal lengths");
657    assert_eq!(na, nc, "triple chunk buffers must have equal lengths");
658    if chunk_size == 0 || na == 0 {
659        return;
660    }
661    let num_chunks = na.div_ceil(chunk_size);
662    if !P::parallelize(na) || num_chunks <= 1 {
663        a.chunks_mut(chunk_size)
664            .zip(b.chunks_mut(chunk_size))
665            .zip(c.chunks_mut(chunk_size))
666            .enumerate()
667            .for_each(|(i, ((ca, cb), cc))| f(i, ca, cb, cc));
668        return;
669    }
670    let abase = DisjointMutPtr(a.as_mut_ptr());
671    let bbase = DisjointMutPtr(b.as_mut_ptr());
672    let cbase = DisjointMutPtr(c.as_mut_ptr());
673    let f = &f;
674    global()
675        .for_each_indexed::<SyncTask, _>(num_chunks, move |chunk_index| {
676            let start = chunk_index * chunk_size;
677            if start >= na || start >= nb || start >= nc {
678                return;
679            }
680            let ea = (start + chunk_size).min(na);
681            let eb = (start + chunk_size).min(nb);
682            let ec = (start + chunk_size).min(nc);
683            // SAFETY: chunks `[start, e*)` for distinct `chunk_index` values
684            // are pairwise disjoint within each buffer and each is visited at
685            // most once. The three input buffers are distinct non-aliasing
686            // `&mut` slices, so the returned mutable chunk references cannot
687            // alias each other.
688            let ca =
689                unsafe { core::slice::from_raw_parts_mut(abase.base().add(start), ea - start) };
690            let cb =
691                unsafe { core::slice::from_raw_parts_mut(bbase.base().add(start), eb - start) };
692            let cc =
693                unsafe { core::slice::from_raw_parts_mut(cbase.base().add(start), ec - start) };
694            f(chunk_index, ca, cb, cc);
695        })
696        .expect("moirai global executor: for_each_chunk_triple_mut_enumerated_with");
697}
698
699/// Like [`for_each_chunk_mut_with`] but also passes the zero-based chunk index to
700/// `f` (synchronous equivalent of
701/// `data.par_chunks_mut(chunk_size).enumerate().for_each(f)`).
702pub fn for_each_chunk_mut_enumerated_with<P, T, F>(data: &mut [T], chunk_size: usize, f: F)
703where
704    P: ExecutionPolicy,
705    T: Send,
706    F: Fn(usize, &mut [T]) + Send + Sync,
707{
708    let n = data.len();
709    if n == 0 || chunk_size == 0 {
710        return;
711    }
712    let num_chunks = n.div_ceil(chunk_size);
713    if !P::parallelize(n) || num_chunks <= 1 {
714        data.chunks_mut(chunk_size)
715            .enumerate()
716            .for_each(|(i, c)| f(i, c));
717        return;
718    }
719    let base = DisjointMutPtr(data.as_mut_ptr());
720    let f = &f;
721    global()
722        .for_each_indexed::<SyncTask, _>(num_chunks, move |c| {
723            let start = c * chunk_size;
724            if start >= n {
725                return;
726            }
727            let end = (start + chunk_size).min(n);
728            // SAFETY: chunks `[start, end)` for distinct `c` are pairwise disjoint
729            // and each visited exactly once, so no two tasks alias.
730            let chunk =
731                unsafe { core::slice::from_raw_parts_mut(base.base().add(start), end - start) };
732            f(c, chunk);
733        })
734        .expect("moirai global executor: for_each_chunk_mut_enumerated_with");
735}
736
737/// Map each element of `data` with `f`, collecting into a `Vec<R>` in order,
738/// scheduled by policy `P`.
739pub fn map_collect_with<P, T, R, F>(data: &[T], f: F) -> Vec<R>
740where
741    P: ExecutionPolicy,
742    T: Sync,
743    R: Send,
744    F: Fn(&T) -> R + Send + Sync,
745{
746    let n = data.len();
747    if !P::parallelize(n) {
748        return data.iter().map(f).collect();
749    }
750    let mut out: Vec<core::mem::MaybeUninit<R>> = Vec::with_capacity(n);
751    // SAFETY: capacity is `n`; every slot is written exactly once below before
752    // being read, and `MaybeUninit` makes `set_len` sound without initialization.
753    unsafe {
754        out.set_len(n);
755    }
756    enumerate_mut_with::<Parallel, _, _>(&mut out, |i, slot| {
757        slot.write(f(&data[i]));
758    });
759    // SAFETY: every slot initialized above; `MaybeUninit<R>` shares `R`'s layout.
760    let mut out = core::mem::ManuallyDrop::new(out);
761    unsafe { Vec::from_raw_parts(out.as_mut_ptr().cast::<R>(), n, out.capacity()) }
762}
763
764/// Map-reduce over `data`, scheduled by policy `P`.
765///
766/// `reduce` must be associative and `identity` its neutral element, since chunk
767/// boundaries and combination order are unspecified.
768pub fn map_reduce_with<P, T, R, M, Rd>(data: &[T], identity: R, map: M, reduce: Rd) -> R
769where
770    P: ExecutionPolicy,
771    T: Sync,
772    R: Send + Sync + Clone,
773    M: Fn(&T) -> R + Send + Sync,
774    Rd: Fn(R, R) -> R + Send + Sync,
775{
776    let n = data.len();
777    if n == 0 || !P::parallelize(n) {
778        let mut acc = identity;
779        for item in data {
780            acc = reduce(acc, map(item));
781        }
782        return acc;
783    }
784    let map = &map;
785    let reduce = &reduce;
786    // The executor folds each worker chunk locally (seeded by `identity`) then
787    // combines chunk results, so `map` is per-element and `reduce` per-pair.
788    global()
789        .map_reduce_indexed::<SyncTask, _, _, _>(n, identity, move |i| map(&data[i]), reduce)
790        .expect("moirai global executor: map_reduce_with")
791}
792
793/// Parallel fold-reduce over the index domain `0..len`, scheduled by policy `P`.
794///
795/// Each worker chunk creates one accumulator with `init()`, folds its indices
796/// into it with `fold`, and the per-chunk accumulators are combined with
797/// `reduce`. Unlike [`reduce_index_with`], `fold` mutates a single accumulator
798/// per chunk (no per-element temporary), which is the efficient shape for
799/// accumulating into a collection — e.g. grouping entries into a `HashMap`.
800/// `reduce` must be associative; `init()` must yield its neutral element.
801pub fn fold_reduce_with<P, A, Init, Fold, Red>(len: usize, init: Init, fold: Fold, reduce: Red) -> A
802where
803    P: ExecutionPolicy,
804    A: Send,
805    Init: Fn() -> A + Send + Sync,
806    Fold: Fn(A, usize) -> A + Send + Sync,
807    Red: Fn(A, A) -> A,
808{
809    if len == 0 {
810        return init();
811    }
812    if !P::parallelize(len) {
813        let mut acc = init();
814        for i in 0..len {
815            acc = fold(acc, i);
816        }
817        return acc;
818    }
819    let workers = themis::CpuTopology::detect()
820        .map(|topology| topology.logical_processors())
821        .or_else(|| std::thread::available_parallelism().ok().map(|n| n.get()))
822        .unwrap_or(1)
823        .max(1);
824    let chunks = workers.min(len).max(1);
825    let chunk = len.div_ceil(chunks);
826    let mut slots: Vec<Option<A>> = (0..chunks).map(|_| None).collect();
827    let base = DisjointMutPtr(slots.as_mut_ptr());
828    let init_ref = &init;
829    let fold_ref = &fold;
830    global()
831        .for_each_indexed::<SyncTask, _>(chunks, move |ci| {
832            let start = ci * chunk;
833            if start >= len {
834                return;
835            }
836            let end = (start + chunk).min(len);
837            let mut acc = init_ref();
838            for i in start..end {
839                acc = fold_ref(acc, i);
840            }
841            // SAFETY: each `ci` writes its own slot exactly once; slots are
842            // disjoint and `slots` outlives the joined call.
843            unsafe {
844                *base.get_mut(ci) = Some(acc);
845            }
846        })
847        .expect("moirai global executor: fold_reduce_with");
848    slots
849        .into_iter()
850        .flatten()
851        .reduce(reduce)
852        .unwrap_or_else(init)
853}
854
855/// Parallel map over the index domain `0..len`, collecting into a `Vec<R>` in
856/// order, scheduled by policy `P`.
857///
858/// `map(i)` produces the element at index `i`. Use this for index-aligned maps
859/// over multiple slices that [`map_collect_with`] cannot express — e.g. an
860/// elementwise product `map_collect_index_with::<Adaptive>(n, |i| a[i] * b[i])`.
861pub fn map_collect_index_with<P, R, Map>(len: usize, map: Map) -> Vec<R>
862where
863    P: ExecutionPolicy,
864    R: Send,
865    Map: Fn(usize) -> R + Send + Sync,
866{
867    if !P::parallelize(len) {
868        return (0..len).map(map).collect();
869    }
870    let mut out: Vec<core::mem::MaybeUninit<R>> = Vec::with_capacity(len);
871    // SAFETY: capacity is `len`; every slot is written exactly once below.
872    unsafe {
873        out.set_len(len);
874    }
875    enumerate_mut_with::<Parallel, _, _>(&mut out, |i, slot| {
876        slot.write(map(i));
877    });
878    // SAFETY: every slot initialized; `MaybeUninit<R>` shares `R`'s layout.
879    let mut out = core::mem::ManuallyDrop::new(out);
880    unsafe { Vec::from_raw_parts(out.as_mut_ptr().cast::<R>(), len, out.capacity()) }
881}
882
883/// Map each element of `data` in place with `f(index, &mut element)`, collecting
884/// each returned value into a `Vec<R>` in order, scheduled by policy `P`.
885///
886/// The synchronous equivalent of rayon's
887/// `data.par_iter_mut().enumerate().map(f).collect()`: each element is mutated
888/// and produces a result. Use for parallel solve-in-place-and-collect loops.
889pub fn map_collect_mut_with<P, T, R, F>(data: &mut [T], f: F) -> Vec<R>
890where
891    P: ExecutionPolicy,
892    T: Send,
893    R: Send,
894    F: Fn(usize, &mut T) -> R + Send + Sync,
895{
896    let n = data.len();
897    if !P::parallelize(n) {
898        return data.iter_mut().enumerate().map(|(i, x)| f(i, x)).collect();
899    }
900    let mut out: Vec<core::mem::MaybeUninit<R>> = Vec::with_capacity(n);
901    // SAFETY: capacity is `n`; every slot is written exactly once below.
902    unsafe {
903        out.set_len(n);
904    }
905    let data_ptr = DisjointMutPtr(data.as_mut_ptr());
906    let out_ptr = DisjointMutPtr(out.as_mut_ptr());
907    let f = &f;
908    global()
909        .for_each_indexed::<SyncTask, _>(n, move |i| {
910            // SAFETY: each index in `0..n` is visited exactly once, so neither the
911            // input element nor the output slot at `i` aliases another task's.
912            let elem = unsafe { data_ptr.get_mut(i) };
913            let result = f(i, elem);
914            unsafe { out_ptr.get_mut(i).write(result) };
915        })
916        .expect("moirai global executor: map_collect_mut_with");
917    // SAFETY: every slot initialized; `MaybeUninit<R>` shares `R`'s layout.
918    let mut out = core::mem::ManuallyDrop::new(out);
919    unsafe { Vec::from_raw_parts(out.as_mut_ptr().cast::<R>(), n, out.capacity()) }
920}
921
922/// Parallel reduction over the index domain `0..len`, scheduled by policy `P`.
923///
924/// `map(i)` produces a value for index `i`; results are folded within and across
925/// chunks with `reduce`, seeded by `identity` (which must be `reduce`'s neutral
926/// element). Use this for index-aligned reductions over multiple slices that
927/// [`map_reduce_with`] cannot express — e.g. a dot product
928/// `reduce_index_with::<Adaptive>(n, T::zero(), |i| a[i] * b[i], |x, y| x + y)`.
929pub fn reduce_index_with<P, R, Map, Red>(len: usize, identity: R, map: Map, reduce: Red) -> R
930where
931    P: ExecutionPolicy,
932    R: Send + Sync + Clone,
933    Map: Fn(usize) -> R + Send + Sync,
934    Red: Fn(R, R) -> R + Send + Sync,
935{
936    if len == 0 || !P::parallelize(len) {
937        let mut acc = identity;
938        for i in 0..len {
939            acc = reduce(acc, map(i));
940        }
941        return acc;
942    }
943    global()
944        .map_reduce_indexed::<SyncTask, _, _, _>(len, identity, map, reduce)
945        .expect("moirai global executor: reduce_index_with")
946}