Skip to main content

rustymath_core/
error.rs

1//! Errors from rustymath's functions on arrays. Each error has a kind, and a message that says what went wrong.
2use std::borrow::Cow;
3
4/// What went wrong.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum Kind {
7    /// Lengths that don't agree.
8    Length,
9    /// An argument of the wrong rank.
10    Rank,
11    /// A position outside an axis.
12    Index,
13    /// A result past an array limit, such as `layout::MAX_GENERATED_ELEMENTS`.
14    Limit,
15    /// An argument outside the function's domain.
16    Domain,
17    /// An integer result that doesn't fit its dtype.
18    Overflow,
19    /// A computation that the installed interrupt check stopped, as at a Ctrl-C ([`interrupt`](crate::interrupt)).
20    Interrupt,
21    /// A computation that the installed interrupt check stopped at a deadline.
22    Timeout,
23}
24
25macro_rules! reporters {
26    ($($kind:ident: $message:literal),+ $(,)?) => {
27        impl Kind {
28            pub fn reporter(self) -> extern "C" fn() {
29                match self { $(Self::$kind => {
30                    extern "C" fn report() { record(Error::new(Kind::$kind, $message)); }
31                    report
32                }),+ }
33            }
34        }
35    };
36}
37reporters! {
38    Length: "lengths do not agree", Rank: "argument has the wrong rank", Index: "read or write position is outside its source",
39    Limit: "array limit exceeded", Domain: "argument is outside the function's domain", Overflow: "integer arithmetic overflowed",
40    Interrupt: "computation interrupted", Timeout: "computation timed out",
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct Error { pub kind: Kind, pub message: Cow<'static, str> }
45
46impl Error { pub fn new(kind: Kind, message: impl Into<Cow<'static, str>>) -> Self { Self { kind, message: message.into() } } }
47
48impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.message) } }
49
50impl std::error::Error for Error {}
51
52/// The first recorded numerical error and its count. The parallel runtime installs the caller's active slot on workers; `checked` supplies one for a standalone operation.
53#[derive(Clone, Default)]
54pub struct ErrorSlot(std::sync::Arc<Errors>);
55
56#[derive(Default)]
57struct Errors { count: std::sync::atomic::AtomicU64, first: std::sync::Mutex<Option<Error>> }
58
59thread_local! { static ACTIVE: std::cell::RefCell<Option<ErrorSlot>> = const { std::cell::RefCell::new(None) }; }
60
61struct Restore(Option<ErrorSlot>);
62impl Drop for Restore { fn drop(&mut self) { ACTIVE.with(|active| active.replace(self.0.take())); } }
63
64impl ErrorSlot {
65    pub fn new() -> Self { Self::default() }
66    pub fn count(&self) -> u64 { self.0.count.load(std::sync::atomic::Ordering::Relaxed) }
67    pub fn pending(&self) -> bool { self.0.first.lock().unwrap().is_some() }
68    pub fn take(&self) -> Option<Error> { self.0.first.lock().unwrap().take() }
69    /// Captured by the parallel runtime before it starts workers.
70    pub fn current() -> Option<Self> { ACTIVE.with(|active| active.borrow().clone()) }
71    pub fn within<R>(&self, run: impl FnOnce() -> R) -> R {
72        let (mut run, mut result) = (Some(run), None);
73        self.enter(&mut || result = run.take().map(|run| run()));
74        result.expect("the run ran")
75    }
76    fn enter(&self, run: &mut dyn FnMut()) { let _restore = Restore(ACTIVE.with(|active| active.replace(Some(self.clone())))); run() }
77}
78
79/// Record in the active read's slot; standalone scalar maths has no slot.
80pub fn record(error: Error) {
81    ACTIVE.with(|active| {
82        if let Some(slot) = active.borrow().as_ref() {
83            slot.0.count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
84            let mut first = slot.0.first.lock().unwrap();
85            if first.is_none() { *first = Some(error); }
86        }
87    });
88}
89
90/// Checked eager entry for Rust and Python callers.
91/// Returns direct failures and, when no slot is active, the first recorded failure. With an
92/// active slot, recorded failures remain for its enclosing operation to collect.
93pub fn checked<R>(run: impl FnOnce() -> Result<R, Error>) -> Result<R, Error> {
94    let (mut run, mut result) = (Some(run), None);
95    let recorded = recorded(&mut || result = run.take().map(|run| run()));
96    let result = result.expect("the run ran")?;
97    recorded.map_or(Ok(result), Err)
98}
99/// Runs `run` in the thread's spare error slot unless one is active, giving the first error recorded in it. Every caller of [`checked`] shares this one non-generic copy. The spare is cleared before each run: a run that unwound can leave its error in it.
100fn recorded(run: &mut dyn FnMut()) -> Option<Error> {
101    if ACTIVE.with(|active| active.borrow().is_some()) { run(); return None; }
102    thread_local! { static SPARE: ErrorSlot = ErrorSlot::new(); }
103    SPARE.with(|slot| {
104        slot.take();
105        slot.enter(run);
106        slot.take()
107    })
108}