Skip to main content

kohebi_core/
error.rs

1//! Exceptions, before there are classes to make them out of.
2//!
3//! A Python exception is an instance of a class. Two thirds of that is here:
4//! the builtin classes and their instances are real values a program can name,
5//! call, bind and raise, and they are in [`exception`](crate::exception). What
6//! is missing is the third that needs the class machinery, which is a program
7//! defining one of its own and adding attributes and methods to it.
8//!
9//! So a class is a [`Kind`], which is a closed set of exactly the classes
10//! CPython has builtin, and the hierarchy between them is [`Kind::base`]. That
11//! is what lets `except ArithmeticError` catch a `ZeroDivisionError` without
12//! anything that could be called an object model existing yet.
13//!
14//! [`Error`] is what the runtime returns rather than what a program holds. It
15//! is a kind and a message because most of the time that is all there is: an
16//! operator that was handed the wrong type raises out of Rust and no Python
17//! object ever exists. When a program raises one itself the instance it raised
18//! comes along in [`Error::value`], so that the object it raised is the object
19//! it will eventually catch.
20
21use std::fmt;
22
23use crate::exception::Exception;
24use crate::object::Object;
25
26/// Declare the builtin exception classes, their names and their hierarchy.
27///
28/// One table rather than three, because a name and a base that disagreed about
29/// which class they belonged to would be a bug nothing could catch. The
30/// indentation is the tree, and `=> Parent` is the only thing a row has to say
31/// beyond its own name.
32macro_rules! hierarchy {
33    ($( $(#[$about:meta])* $name:ident $(=> $base:ident)? ),+ $(,)?) => {
34        /// Which exception it is, which is to say which class it is an
35        /// instance of.
36        ///
37        /// One arm per exception CPython has builtin, which is a closed set,
38        /// and the set is closed because a class a program defines is not one
39        /// of these and will not be until there are classes.
40        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41        pub enum Kind {
42            $( $(#[$about])* $name, )+
43        }
44
45        impl Kind {
46            /// Every builtin exception class, in the order the tree has them.
47            ///
48            /// This is what binds them as names, so a class missing from here
49            /// is a class a program cannot mention.
50            pub const ALL: &'static [Kind] = &[ $( Kind::$name, )+ ];
51
52            /// The name a traceback prints, which is the class name.
53            #[must_use]
54            pub const fn name(self) -> &'static str {
55                match self { $( Kind::$name => stringify!($name), )+ }
56            }
57
58            /// The class this one derives from.
59            ///
60            /// `None` for `BaseException` alone, which is the root and is the
61            /// reason walking this terminates.
62            #[must_use]
63            pub const fn base(self) -> Option<Kind> {
64                match self { $( Kind::$name => hierarchy!(@base $($base)?), )+ }
65            }
66        }
67    };
68    (@base) => { None };
69    (@base $base:ident) => { Some(Kind::$base) };
70}
71
72hierarchy! {
73    BaseException,
74        Exception => BaseException,
75            ArithmeticError => Exception,
76                FloatingPointError => ArithmeticError,
77                OverflowError => ArithmeticError,
78                ZeroDivisionError => ArithmeticError,
79            AssertionError => Exception,
80            AttributeError => Exception,
81            BufferError => Exception,
82            EOFError => Exception,
83            ImportError => Exception,
84                ModuleNotFoundError => ImportError,
85            LookupError => Exception,
86                IndexError => LookupError,
87                /// The one exception whose `str` is the `repr` of its argument,
88                /// so that a missing key of `''` is something rather than
89                /// nothing. See [`Exception::message`].
90                KeyError => LookupError,
91            MemoryError => Exception,
92            NameError => Exception,
93                /// A slot read before anything was put in it.
94                UnboundLocalError => NameError,
95            OSError => Exception,
96                BlockingIOError => OSError,
97                ChildProcessError => OSError,
98                ConnectionError => OSError,
99                    BrokenPipeError => ConnectionError,
100                    ConnectionAbortedError => ConnectionError,
101                    ConnectionRefusedError => ConnectionError,
102                    ConnectionResetError => ConnectionError,
103                FileExistsError => OSError,
104                FileNotFoundError => OSError,
105                InterruptedError => OSError,
106                IsADirectoryError => OSError,
107                NotADirectoryError => OSError,
108                PermissionError => OSError,
109                ProcessLookupError => OSError,
110                TimeoutError => OSError,
111            ReferenceError => Exception,
112            RuntimeError => Exception,
113                NotImplementedError => RuntimeError,
114                PythonFinalizationError => RuntimeError,
115                RecursionError => RuntimeError,
116            StopAsyncIteration => Exception,
117            StopIteration => Exception,
118            /// A syntax error the runtime raises, which is not the one the
119            /// parser reports. The parser refuses a file before there is a
120            /// program to raise anything, and says so in its own words.
121            SyntaxError => Exception,
122                IndentationError => SyntaxError,
123                    TabError => IndentationError,
124            SystemError => Exception,
125            TypeError => Exception,
126            ValueError => Exception,
127                UnicodeError => ValueError,
128                    UnicodeDecodeError => UnicodeError,
129                    UnicodeEncodeError => UnicodeError,
130                    UnicodeTranslateError => UnicodeError,
131            Warning => Exception,
132                BytesWarning => Warning,
133                DeprecationWarning => Warning,
134                EncodingWarning => Warning,
135                FutureWarning => Warning,
136                ImportWarning => Warning,
137                PendingDeprecationWarning => Warning,
138                ResourceWarning => Warning,
139                RuntimeWarning => Warning,
140                SyntaxWarning => Warning,
141                UnicodeWarning => Warning,
142                UserWarning => Warning,
143        GeneratorExit => BaseException,
144        KeyboardInterrupt => BaseException,
145        SystemExit => BaseException,
146}
147
148impl Kind {
149    /// Whether this class is that one or derives from it, which is the
150    /// question `except` asks and `isinstance` asks after it.
151    #[must_use]
152    pub fn derives_from(self, base: Kind) -> bool {
153        let mut at = Some(self);
154        while let Some(kind) = at {
155            if kind == base {
156                return true;
157            }
158            at = kind.base();
159        }
160        false
161    }
162}
163
164impl fmt::Display for Kind {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.write_str(self.name())
167    }
168}
169
170/// A raised exception, on its way out of the runtime.
171///
172/// Not the same thing as an [`Exception`], which is the object a program holds.
173/// This is the Rust error the `?` in every operation propagates, and it is a
174/// kind and a message because that is all a division by zero has: no Python
175/// object is made for one unless something asks for it.
176#[derive(Debug, Clone)]
177pub struct Error {
178    /// Which one it is.
179    pub kind: Kind,
180    /// What it says, which is empty for the ones that say nothing.
181    pub message: String,
182    /// The instance a program raised, when a program raised one.
183    ///
184    /// Boxed because it is almost always absent and this type is inside every
185    /// `Result` the runtime returns, so an unboxed one would widen all of them
186    /// to pay for a case that hardly ever happens.
187    value: Option<Box<Object>>,
188}
189
190impl Error {
191    /// An exception of this kind with this message.
192    #[must_use]
193    pub fn new(kind: Kind, message: impl Into<String>) -> Self {
194        Error {
195            kind,
196            message: message.into(),
197            value: None,
198        }
199    }
200
201    /// The wrong type for the operation.
202    #[must_use]
203    pub fn type_error(message: impl Into<String>) -> Self {
204        Error::new(Kind::TypeError, message)
205    }
206
207    /// The right type and the wrong value.
208    #[must_use]
209    pub fn value_error(message: impl Into<String>) -> Self {
210        Error::new(Kind::ValueError, message)
211    }
212
213    /// A divisor that was zero.
214    #[must_use]
215    pub fn zero_division(message: impl Into<String>) -> Self {
216        Error::new(Kind::ZeroDivisionError, message)
217    }
218
219    /// A result that will not fit.
220    #[must_use]
221    pub fn overflow(message: impl Into<String>) -> Self {
222        Error::new(Kind::OverflowError, message)
223    }
224
225    /// An exception built from the arguments a program would have written.
226    ///
227    /// The message comes out of the arguments rather than being given
228    /// separately, so an exception the runtime raises this way is the one a
229    /// handler catches: `{}['k']` raises a `KeyError` whose `args` really is
230    /// `('k',)` and not a `KeyError` holding the string `'k'` with its quotes
231    /// already in it.
232    #[must_use]
233    pub fn raised(kind: Kind, args: Vec<Object>) -> Self {
234        let raised = Exception::new(kind, args);
235        let message = raised.message();
236        Error::new(kind, message).with_value(Object::native(raised))
237    }
238
239    /// The same exception, carrying the object a program raised.
240    ///
241    /// Carried rather than rebuilt at the catch, because `raise e` and the
242    /// `except ... as e` that catches it have to be the same object and there
243    /// is no way back to it from a kind and a message.
244    #[must_use]
245    pub fn with_value(mut self, value: Object) -> Self {
246        self.value = Some(Box::new(value));
247        self
248    }
249
250    /// The object that was raised, for a caller that has to hand back the very
251    /// one. `None` when the runtime raised this itself.
252    #[must_use]
253    pub fn value(&self) -> Option<&Object> {
254        self.value.as_deref()
255    }
256
257    /// The exception as an object, which is what an `except` clause tests and
258    /// what `as` binds.
259    ///
260    /// The one a program raised when there is one, so that `raise e` and the
261    /// `except ... as e` catching it are the same object. One built here
262    /// otherwise, out of the message, because a division by zero never made an
263    /// object and `except ZeroDivisionError as e` still has to have something
264    /// to bind. Its arguments come out the way CPython's do, which is one
265    /// argument holding the sentence, or none when there is no sentence.
266    ///
267    /// A [`Kind::KeyError`] is the one that cannot be rebuilt from its message,
268    /// since its message is already the `repr` of its key. That is why every
269    /// `KeyError` the runtime raises is built with [`Error::raised`] and
270    /// carries its key.
271    #[must_use]
272    pub fn instance(&self) -> Object {
273        if let Some(value) = self.value() {
274            return value.clone();
275        }
276        let args = if self.message.is_empty() {
277            Vec::new()
278        } else {
279            vec![Object::str(self.message.as_str())]
280        };
281        Object::native(Exception::new(self.kind, args))
282    }
283
284    /// Everything printed above this exception, oldest first, each with the
285    /// sentence that goes under it.
286    ///
287    /// Two chains rather than one, because Python has two relationships and
288    /// prints a different sentence for each. They interleave: an exception can
289    /// have a cause that has a context, so the walk asks the same question at
290    /// every step rather than following one kind of link the whole way.
291    fn chain(&self) -> Vec<(String, &'static str)> {
292        let mut chain = Vec::new();
293        // `raise e from e` is a ring, and printing one until the heap runs out
294        // is worse than printing it once. The exception being printed counts as
295        // seen before the walk starts, which is what makes an exception that is
296        // its own cause print once rather than twice.
297        let mut seen: Vec<*const Exception> = Vec::new();
298        let head = self.value.as_deref().and_then(Object::exception);
299        if let Some(head) = head {
300            seen.push(head);
301        }
302        let mut next = head.and_then(printed_above);
303        while let Some((value, sentence)) = next {
304            let Some(exception) = value.exception() else {
305                break;
306            };
307            let address: *const Exception = exception;
308            if seen.contains(&address) {
309                break;
310            }
311            seen.push(address);
312            chain.push((last_line(exception.kind(), &exception.message()), sentence));
313            next = printed_above(exception);
314        }
315        chain.reverse();
316        chain
317    }
318}
319
320/// What a traceback prints above an exception, and the sentence in between.
321///
322/// A cause wins over a context, because `raise x from y` is a sentence somebody
323/// wrote and a context is something that happened to be going on. Writing a
324/// `from` at all is also what sets `__suppress_context__`, so `raise x from
325/// None` is how a program says that what it was handling is nobody's business.
326fn printed_above(exception: &Exception) -> Option<(Object, &'static str)> {
327    if let Some(cause) = exception.cause() {
328        return Some((
329            cause,
330            "The above exception was the direct cause of the following exception:",
331        ));
332    }
333    if exception.suppresses_context() {
334        return None;
335    }
336    Some((
337        exception.context()?,
338        "During handling of the above exception, another exception occurred:",
339    ))
340}
341
342/// The last line of a traceback for one exception, which is the class name and
343/// then what the exception says.
344///
345/// A message-less exception prints as its name alone, with no colon, which is
346/// why this is not a format string.
347fn last_line(kind: Kind, message: &str) -> String {
348    if message.is_empty() {
349        kind.name().to_owned()
350    } else {
351        format!("{kind}: {message}")
352    }
353}
354
355impl fmt::Display for Error {
356    /// The tail of a traceback: every exception that led to this one, oldest
357    /// first, and then this one.
358    ///
359    /// There are no `File "x", line n` lines in between because there is no
360    /// line table yet, so what comes out is the part of a traceback that says
361    /// what happened without the part that says where.
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        for (line, sentence) in self.chain() {
364            writeln!(f, "{line}\n\n{sentence}\n")?;
365        }
366        f.write_str(&last_line(self.kind, &self.message))
367    }
368}
369
370impl std::error::Error for Error {}
371
372/// What every operation in the runtime gives back.
373pub type Result<T> = std::result::Result<T, Error>;
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn an_exception_prints_the_way_the_last_line_of_a_traceback_does() {
381        assert_eq!(
382            Error::type_error("unsupported operand type(s) for +: 'int' and 'str'").to_string(),
383            "TypeError: unsupported operand type(s) for +: 'int' and 'str'"
384        );
385        assert_eq!(
386            Error::zero_division("division by zero").to_string(),
387            "ZeroDivisionError: division by zero"
388        );
389    }
390
391    /// `raise MemoryError` prints the name on its own, so an empty message is
392    /// not a colon and a space with nothing after it.
393    #[test]
394    fn an_exception_with_nothing_to_say_prints_its_name_alone() {
395        assert_eq!(Error::new(Kind::MemoryError, "").to_string(), "MemoryError");
396    }
397
398    #[test]
399    fn a_class_derives_from_itself_and_from_everything_above_it() {
400        assert!(Kind::ZeroDivisionError.derives_from(Kind::ZeroDivisionError));
401        assert!(Kind::ZeroDivisionError.derives_from(Kind::ArithmeticError));
402        assert!(Kind::ZeroDivisionError.derives_from(Kind::Exception));
403        assert!(Kind::ZeroDivisionError.derives_from(Kind::BaseException));
404        assert!(!Kind::ZeroDivisionError.derives_from(Kind::ValueError));
405    }
406
407    /// `except Exception` is the line most programs are written with, and it
408    /// is the one that has to not catch a `KeyboardInterrupt`.
409    #[test]
410    fn the_three_that_are_not_exceptions_hang_off_the_root() {
411        for kind in [
412            Kind::GeneratorExit,
413            Kind::KeyboardInterrupt,
414            Kind::SystemExit,
415        ] {
416            assert!(kind.derives_from(Kind::BaseException));
417            assert!(!kind.derives_from(Kind::Exception));
418        }
419    }
420
421    /// A division by zero never made an object, and
422    /// `except ZeroDivisionError as e` still has to have something to bind.
423    #[test]
424    fn an_error_that_never_had_an_object_grows_one_when_it_is_caught() {
425        let caught = Error::zero_division("division by zero").instance();
426        assert_eq!(caught.repr(), "ZeroDivisionError('division by zero')");
427        // And one with nothing to say has no arguments rather than one empty
428        // one, which is what `raise MemoryError` gives CPython.
429        assert_eq!(
430            Error::new(Kind::MemoryError, "").instance().repr(),
431            "MemoryError()"
432        );
433    }
434
435    /// `raise e` and the `except ... as e` catching it are the same object, so
436    /// an attribute a program put on the instance before raising it is still
437    /// there afterwards.
438    #[test]
439    fn an_error_a_program_raised_is_caught_as_the_object_it_raised() {
440        let raised = Object::native(Exception::new(Kind::ValueError, vec![Object::str("x")]));
441        let error = Error::new(Kind::ValueError, "x").with_value(raised.clone());
442        assert!(error.instance().is(&raised));
443    }
444
445    /// The message of a `KeyError` is already the `repr` of its key, so
446    /// rebuilding one from its message would put a second pair of quotes round
447    /// a string key and a handler reading `e.args[0]` would get `"'k'"`.
448    #[test]
449    fn an_error_built_from_its_arguments_keeps_them() {
450        let error = Error::raised(Kind::KeyError, vec![Object::str("k")]);
451        assert_eq!(error.to_string(), "KeyError: 'k'");
452        assert_eq!(error.instance().repr(), "KeyError('k')");
453    }
454
455    /// Every class but the root has a base, so walking up from any of them
456    /// arrives at `BaseException` rather than stopping somewhere in between.
457    #[test]
458    fn every_class_is_reachable_from_the_root() {
459        for &kind in Kind::ALL {
460            assert!(
461                kind.derives_from(Kind::BaseException),
462                "{kind} does not derive from BaseException"
463            );
464        }
465        assert_eq!(Kind::BaseException.base(), None);
466    }
467}