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/// Measures and the Interfaces' errors entry: the session's 1 error slot, homed here so kernels set it from every thread that runs the read's work (rustymath's parallel runtime installs the caller's slot on its workers). It keeps the 1st data error and a count of errors recorded. basedpl's `Session` holds one, rustymath's Python API reads its own; its representation is this module's.
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.
91pub fn checked<R>(run: impl FnOnce() -> Result<R, Error>) -> Result<R, Error> {
92    let (mut run, mut result) = (Some(run), None);
93    let recorded = recorded(&mut || result = run.take().map(|run| run()));
94    let result = result.expect("the run ran")?;
95    recorded.map_or(Ok(result), Err)
96}
97/// 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.
98fn recorded(run: &mut dyn FnMut()) -> Option<Error> {
99    if ACTIVE.with(|active| active.borrow().is_some()) { run(); return None; }
100    thread_local! { static SPARE: ErrorSlot = ErrorSlot::new(); }
101    SPARE.with(|slot| {
102        slot.take();
103        slot.enter(run);
104        slot.take()
105    })
106}