use crate::call::CallProvider;
use crate::data::{Order, Reduction};
use crate::reduce::{reduce, Outcome};
use crate::trace::Tracer;
pub fn reduce_parallel<const N: usize, T: Tracer>(
reduction: &mut Reduction<N>,
object: Order,
formula: Order,
budget: u64,
hints: &dyn CallProvider<N>,
tracer: &mut T,
) -> Outcome {
reduce(reduction, object, formula, budget, hints, tracer)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PathStep {
Left,
Right,
Test,
Yes,
No,
Sub,
Continue,
Tag,
Check,
Row(u16),
}
#[derive(Debug, Clone, Default)]
pub struct StructuralIndex {
path: alloc::vec::Vec<PathStep>,
}
impl StructuralIndex {
pub fn root() -> Self { Self::default() }
pub fn push(&mut self, step: PathStep) { self.path.push(step); }
pub fn pop(&mut self) -> Option<PathStep> { self.path.pop() }
pub fn path(&self) -> &[PathStep] { &self.path }
}
extern crate alloc;
#[cfg(feature = "std")]
pub(crate) fn par_binary<const N: usize, T: Tracer>(
reduction: &mut Reduction<N>,
object: Order,
a: Order,
b: Order,
ba: u64,
bb: u64,
budget: u64,
hints: &dyn CallProvider<N>,
tracer: &mut T,
depth: u64,
) -> core::result::Result<(Order, Order, u64), Outcome> {
use crate::reduce::{evaluate, ErrorKind};
use crate::trace::VecTrace;
use crate::jets::registry::JetRegistry;
let mut sub_a = std::boxed::Box::new(reduction.fork());
let mut sub_b = std::boxed::Box::new(reduction.fork());
let mut trace_a = VecTrace::default();
let mut trace_b = VecTrace::default();
let hints_a: &'static dyn CallProvider<N> = unsafe { std::mem::transmute(hints) };
let hints_b: &'static dyn CallProvider<N> = unsafe { std::mem::transmute(hints) };
let ha = std::thread::spawn(move || {
let r = evaluate(&mut *sub_a, object, a, ba, hints_a, &mut trace_a, depth,
&JetRegistry::empty());
(r, sub_a, trace_a)
});
let hb = std::thread::spawn(move || {
let r = evaluate(&mut *sub_b, object, b, bb, hints_b, &mut trace_b, depth,
&JetRegistry::empty());
(r, sub_b, trace_b)
});
let (result_a, fin_reduction_a, fin_trace_a) = ha.join().unwrap();
let (result_b, fin_reduction_b, fin_trace_b) = hb.join().unwrap();
for row in fin_trace_a.0 {
tracer.record(row);
}
for row in fin_trace_b.0 {
tracer.record(row);
}
match (result_a, result_b) {
(Ok((va, ra)), Ok((vb, rb))) => {
let va_p = reduction
.reinter(&fin_reduction_a, va)
.ok_or(Outcome::Error(ErrorKind::Unavailable))?;
let vb_p = reduction
.reinter(&fin_reduction_b, vb)
.ok_or(Outcome::Error(ErrorKind::Unavailable))?;
let used = (ba - ra).saturating_add(bb - rb);
Ok((va_p, vb_p, budget - used))
}
(Err(o), _) | (_, Err(o)) => Err(o),
}
}
#[cfg(feature = "std")]
pub fn reduce_parallel_threaded<const N: usize, T: Tracer>(
reduction: &mut Reduction<N>,
object: Order,
formula: Order,
budget: u64,
hints: &dyn CallProvider<N>,
tracer: &mut T,
) -> Outcome {
reduce(reduction, object, formula, budget, hints, tracer)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::call::NullCalls;
use crate::data::{Reduction};
use crate::trace::{NoTrace, VecTrace};
use nebu::Goldilocks;
fn g(v: u64) -> Goldilocks { Goldilocks::new(v) }
#[test]
fn reduce_parallel_matches_reduce_on_add() {
let formula = |ar: &mut Reduction<256>| {
let t5 = ar.atom(g(5)).unwrap();
let t1 = ar.atom(g(1)).unwrap();
let v3 = ar.atom(g(3)).unwrap();
let v5 = ar.atom(g(5)).unwrap();
let qa = ar.pair(t1, v3).unwrap();
let qb = ar.pair(t1, v5).unwrap();
let body = ar.pair(qa, qb).unwrap();
ar.pair(t5, body).unwrap()
};
let mut ar1 = Reduction::<256>::new();
let obj1 = ar1.atom(g(0)).unwrap();
let f1 = formula(&mut ar1);
let r1 = reduce(&mut ar1, obj1, f1, 1000, &NullCalls, &mut NoTrace);
let mut ar2 = Reduction::<256>::new();
let obj2 = ar2.atom(g(0)).unwrap();
let f2 = formula(&mut ar2);
let r2 = reduce_parallel(&mut ar2, obj2, f2, 1000, &NullCalls, &mut NoTrace);
match (r1, r2) {
(Outcome::Ok(a, ba), Outcome::Ok(b, bb)) => {
assert_eq!(ar1.atom_value(a).unwrap(), ar2.atom_value(b).unwrap());
assert_eq!(ba, bb, "remaining budget must match");
}
(x, y) => panic!("Outcome variants diverged: {:?} vs {:?}", x, y),
}
}
#[test]
fn reduce_parallel_trace_matches_reduce_on_hash() {
let formula = |ar: &mut Reduction<256>| {
let t15 = ar.atom(g(15)).unwrap();
let t1 = ar.atom(g(1)).unwrap();
let v = ar.atom(g(7)).unwrap();
let body = ar.pair(t1, v).unwrap();
ar.pair(t15, body).unwrap()
};
let mut ar1 = Reduction::<256>::new();
let obj1 = ar1.atom(g(0)).unwrap();
let f1 = formula(&mut ar1);
let mut tr1 = VecTrace::default();
reduce(&mut ar1, obj1, f1, 10_000, &NullCalls, &mut tr1);
let mut ar2 = Reduction::<256>::new();
let obj2 = ar2.atom(g(0)).unwrap();
let f2 = formula(&mut ar2);
let mut tr2 = VecTrace::default();
reduce_parallel(&mut ar2, obj2, f2, 10_000, &NullCalls, &mut tr2);
assert_eq!(tr1.0.len(), tr2.0.len(), "trace lengths must match");
for (i, (a, b)) in tr1.0.iter().zip(tr2.0.iter()).enumerate() {
assert_eq!(a.r(), b.r(), "row {} differs", i);
}
}
#[cfg(feature = "std")]
#[test]
fn threaded_add_matches_sequential() {
let make_add = |ar: &mut Reduction<256>| {
let t5 = ar.atom(g(5)).unwrap();
let t1 = ar.atom(g(1)).unwrap();
let v7 = ar.atom(g(7)).unwrap();
let v11 = ar.atom(g(11)).unwrap();
let qa = ar.pair(t1, v7).unwrap();
let qb = ar.pair(t1, v11).unwrap();
let body = ar.pair(qa, qb).unwrap();
ar.pair(t5, body).unwrap()
};
let mut ar_seq = Reduction::<256>::new();
let obj_seq = ar_seq.atom(g(0)).unwrap();
let f_seq = make_add(&mut ar_seq);
let mut tr_seq = VecTrace::default();
let r_seq = reduce(&mut ar_seq, obj_seq, f_seq, 1000, &NullCalls, &mut tr_seq);
let mut ar_par = Reduction::<256>::new();
let obj_par = ar_par.atom(g(0)).unwrap();
let f_par = make_add(&mut ar_par);
let mut tr_par = VecTrace::default();
let r_par = reduce_parallel_threaded(
&mut ar_par, obj_par, f_par, 1000, &NullCalls, &mut tr_par,
);
match (r_seq, r_par) {
(Outcome::Ok(vs, bs), Outcome::Ok(vp, bp)) => {
assert_eq!(
ar_seq.atom_value(vs).unwrap(),
ar_par.atom_value(vp).unwrap(),
"threaded result must match sequential",
);
assert_eq!(bs, bp, "remaining budget must match");
}
(x, y) => panic!("Outcome variants diverged: {:?} vs {:?}", x, y),
}
assert_eq!(
tr_seq.0.len(),
tr_par.0.len(),
"threaded trace length must match sequential",
);
}
#[cfg(feature = "std")]
#[test]
fn scope_smoke_test() {
let x = 42u64;
let result = std::thread::scope(|s| {
let h = s.spawn(|| x + 1);
h.join().unwrap()
});
assert_eq!(result, 43);
}
#[cfg(feature = "std")]
#[test]
fn fork_is_independent() {
let mut ar = Reduction::<256>::new();
let a = ar.atom(g(3)).unwrap();
let b = ar.atom(g(7)).unwrap();
let c = ar.pair(a, b).unwrap();
let base_count = ar.count();
let mut fork = ar.fork();
assert_eq!(fork.count(), base_count);
fork.atom(g(99)).unwrap();
assert_eq!(ar.count(), base_count, "parent count unchanged after fork write");
let c_back = ar.reinter(&fork, c).unwrap();
assert_eq!(c_back, c, "reinter on pre-fork data returns same ID");
}
#[test]
fn structural_index_push_pop() {
let mut idx = StructuralIndex::root();
idx.push(PathStep::Left);
idx.push(PathStep::Row(7));
assert_eq!(idx.path(), &[PathStep::Left, PathStep::Row(7)]);
assert_eq!(idx.pop(), Some(PathStep::Row(7)));
assert_eq!(idx.path(), &[PathStep::Left]);
}
#[test]
fn path_step_order_is_total() {
let mut path = [
PathStep::Row(0),
PathStep::Check,
PathStep::Tag,
PathStep::Continue,
PathStep::Sub,
PathStep::No,
PathStep::Yes,
PathStep::Test,
PathStep::Right,
PathStep::Left,
];
path.sort();
assert_eq!(
path,
[
PathStep::Left, PathStep::Right, PathStep::Test, PathStep::Yes,
PathStep::No, PathStep::Sub, PathStep::Continue, PathStep::Tag,
PathStep::Check, PathStep::Row(0),
]
);
}
}