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}