aranya_buggy/
lib.rs

1//! Error handling similar to [`core::unreachable`], but less panicky.
2//!
3//! # Configuration
4//! Panicking is controlled by [`debug_assertions`](https://doc.rust-lang.org/cargo/reference/profiles.html#debug-assertions).
5//! - By default, in debug/test builds, we panic to make it easier to find bugs.
6//! - In release builds, we don't want to panic so we instead return `Result<T, `[`Bug`]`>`.
7//!
8//! # Usage
9//! ```
10//! use aranya_buggy::{bug, Bug, BugExt};
11//!
12//! #[derive(Debug)]
13//! enum MyError {
14//!     TooManyFrobs,
15//!     Bug(Bug),
16//! }
17//!
18//! impl From<Bug> for MyError {
19//!     fn from(err: Bug) -> Self {
20//!         Self::Bug(err)
21//!     }
22//! }
23//!
24//! fn main() -> Result<(), MyError> {
25//!     let x: u32 = 42;
26//!
27//!     let sum = x.checked_add(100).assume("x is small")?;
28//!
29//!     if x % 2 != 0 {
30//!         bug!("x is always even because I said so");
31//!     }
32//!
33//!     Ok(())
34//! }
35//! ```
36
37#![cfg_attr(not(any(test, doctest, feature = "std")), no_std)]
38#![warn(missing_docs)]
39
40#[cfg(feature = "alloc")]
41extern crate alloc;
42
43#[cfg(feature = "alloc")]
44use alloc::boxed::Box;
45use core::{convert::Infallible, fmt, panic::Location};
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48/// Error type for errors that should be unreachable, indicating a bug.
49///
50/// Use [`bug`] to return a `Result<T, Bug>`.
51pub struct Bug(
52    #[cfg(feature = "alloc")] Box<BugInner>,
53    #[cfg(not(feature = "alloc"))] BugInner,
54);
55
56#[derive(Clone, Debug, Eq, PartialEq)]
57struct BugInner {
58    msg: &'static str,
59    location: &'static Location<'static>,
60}
61
62impl Bug {
63    #[cold]
64    #[track_caller]
65    #[doc(hidden)]
66    pub fn new(msg: &'static str) -> Self {
67        cfg_if::cfg_if! {
68            if #[cfg(any(test, doc, not(debug_assertions)))] {
69                #[allow(clippy::useless_conversion)]
70                Self(BugInner {
71                    msg,
72                    location: Location::caller(),
73                }.into())
74            } else {{
75                #![allow(clippy::disallowed_macros)]
76                unreachable!("{}", msg)
77            }}
78        }
79    }
80
81    #[cold]
82    #[track_caller]
83    #[doc(hidden)]
84    pub fn new_with_source(msg: &'static str, _cause: impl fmt::Display) -> Self {
85        cfg_if::cfg_if! {
86            if #[cfg(any(test, doc, not(debug_assertions)))] {
87                Self::new(msg)
88            } else {{
89                #![allow(clippy::disallowed_macros)]
90                unreachable!("{msg}, caused by: {_cause}")
91            }}
92        }
93    }
94
95    /// Get the message used when creating the [`Bug`].
96    pub fn msg(&self) -> &'static str {
97        self.0.msg
98    }
99}
100
101impl fmt::Display for Bug {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        writeln!(f, "bug: {}", self.0.msg)?;
104        writeln!(f, "location: {}", self.0.location)?;
105        Ok(())
106    }
107}
108
109impl core::error::Error for Bug {}
110
111impl From<Infallible> for Bug {
112    fn from(err: Infallible) -> Self {
113        match err {}
114    }
115}
116
117/// Extension trait for assuming an option or result can be unwrapped.
118pub trait BugExt<T> {
119    /// Assume this value can be unwrapped. See `[crate]` docs.
120    fn assume(self, msg: &'static str) -> Result<T, Bug>;
121}
122
123impl<T> BugExt<T> for Option<T> {
124    #[track_caller]
125    fn assume(self, msg: &'static str) -> Result<T, Bug> {
126        match self {
127            Some(val) => Ok(val),
128            None => bug!(msg),
129        }
130    }
131}
132
133impl<T, E: fmt::Display> BugExt<T> for Result<T, E> {
134    #[track_caller]
135    fn assume(self, msg: &'static str) -> Result<T, Bug> {
136        match self {
137            Ok(val) => Ok(val),
138            Err(_err) => bug!(msg, _err),
139        }
140    }
141}
142
143/// Like [`core::unreachable`], but less panicky. See also [`crate`] docs.
144///
145/// # Usage
146/// ```
147/// # use aranya_buggy::bug;
148/// # fn main() -> Result<(), aranya_buggy::Bug> {
149/// # let frobs = 1;
150/// let inverse = match frobs {
151///     0 => 1,
152///     1 => 0,
153///     _ => bug!("frobs is always 0 or 1"),
154/// };
155/// # Ok(())
156/// # }
157/// ```
158#[macro_export]
159macro_rules! bug {
160    ($msg:expr) => {
161        return ::core::result::Result::Err($crate::Bug::new($msg).into()).into()
162    };
163    ($msg:expr, $source:expr) => {
164        return ::core::result::Result::Err($crate::Bug::new_with_source($msg, $source).into())
165            .into()
166    };
167}
168
169#[cfg(test)]
170mod test {
171    #![allow(clippy::panic)]
172
173    use super::*;
174
175    #[test]
176    fn option_some() {
177        assert_eq!(Some(42).assume("").unwrap(), 42);
178    }
179
180    #[test]
181    fn result_ok() {
182        assert_eq!(Ok::<_, &str>(42).assume("").unwrap(), 42);
183    }
184
185    #[test]
186    fn option_none() {
187        let val: Option<()> = None;
188        let msg = "option_none test";
189
190        let before = Location::caller();
191        let err = val.assume(msg).unwrap_err();
192        let after = Location::caller();
193
194        assert_between(err.0.location, before, after);
195        assert_eq!(err.0.msg, msg);
196    }
197
198    #[test]
199    fn result_err() {
200        let val: Result<(), &str> = Err("inner");
201        let msg = "result_err test";
202
203        let before = Location::caller();
204        let err = val.assume(msg).unwrap_err();
205        let after = Location::caller();
206
207        assert_between(err.0.location, before, after);
208        assert_eq!(err.0.msg, msg);
209    }
210
211    fn assert_between(loc: &Location<'_>, before: &Location<'_>, after: &Location<'_>) {
212        assert_eq!(loc.file(), before.file());
213        assert_eq!(loc.file(), after.file());
214        assert!(before.line() < loc.line());
215        assert!(loc.line() < after.line());
216    }
217}