texlang/error/
mod.rs

1//! Error handling
2//!
3//! This is reference documentation.
4//! The Texlang documentation has a [dedicated page about error handling
5//! ](<https://texcraft.dev/texlang/09-errors.html>).
6
7use std::collections::HashMap;
8
9use crate::token;
10use crate::token::trace;
11use crate::vm;
12use texcraft_stdext::algorithms::spellcheck::{self, WordDiff};
13
14pub mod display;
15
16/// A fully traced error
17///
18/// Note that serializing and deserializing this type results in type erasure.
19/// Also the serialization format is private.
20/// This is not by design: the minimal amount of work was done to make the type
21///     serializable, and future work to make this better is welcome!
22#[derive(Debug)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct TracedTexError {
25    #[cfg_attr(
26        feature = "serde",
27        serde(
28            serialize_with = "serialize_error",
29            deserialize_with = "deserialize_error"
30        )
31    )]
32    pub error: Box<dyn TexError>,
33    pub stack_trace: Vec<StackTraceElement>,
34    pub token_traces: HashMap<token::Token, trace::SourceCodeTrace>,
35    pub end_of_input_trace: Option<trace::SourceCodeTrace>,
36}
37
38#[cfg(feature = "serde")]
39#[allow(clippy::borrowed_box)] // we need this exact function signature for serde.
40fn serialize_error<S>(value: &Box<dyn TexError>, serializer: S) -> Result<S::Ok, S::Error>
41where
42    S: serde::Serializer,
43{
44    use serde::Serialize;
45    let serializable_error = SerializableError {
46        kind: value.kind().clone(),
47        title: value.title(),
48        notes: value.notes(),
49        source_annotation: value.source_annotation(),
50    };
51    serializable_error.serialize(serializer)
52}
53
54#[cfg(feature = "serde")]
55fn deserialize_error<'de, D>(deserializer: D) -> Result<Box<dyn TexError>, D::Error>
56where
57    D: serde::Deserializer<'de>,
58{
59    use serde::Deserialize;
60    let serializable_error = SerializableError::deserialize(deserializer)?;
61    Ok(Box::new(serializable_error))
62}
63
64#[derive(Clone, Debug)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66struct SerializableError {
67    kind: Kind,
68    title: String,
69    notes: Vec<display::Note>,
70    source_annotation: String,
71}
72
73impl TexError for SerializableError {
74    fn kind(&self) -> Kind {
75        self.kind.clone()
76    }
77    fn title(&self) -> String {
78        self.title.clone()
79    }
80    fn notes(&self) -> Vec<display::Note> {
81        self.notes.clone()
82    }
83    fn source_annotation(&self) -> String {
84        self.source_annotation.clone()
85    }
86}
87
88impl TracedTexError {
89    pub(crate) fn new(
90        error: Box<dyn TexError>,
91        tracer: &trace::Tracer,
92        cs_name_interner: &token::CsNameInterner,
93        stack_trace: Vec<StackTraceElement>,
94    ) -> Self {
95        let (end_of_input_trace, mut tokens) = match error.kind() {
96            Kind::Token(token) => (None, vec![token]),
97            Kind::EndOfInput => (Some(tracer.trace_end_of_input()), vec![]),
98            Kind::FailedPrecondition => (None, vec![]),
99        };
100        for note in error.notes() {
101            if let display::Note::SourceCodeTrace(_, token) = note {
102                tokens.push(token);
103            }
104        }
105        let token_traces: HashMap<token::Token, trace::SourceCodeTrace> = tokens
106            .into_iter()
107            .map(|token| (token, tracer.trace(token, cs_name_interner)))
108            .collect();
109        TracedTexError {
110            error,
111            stack_trace,
112            token_traces,
113            end_of_input_trace,
114        }
115    }
116}
117
118/// Implementations of this trait describe an error in which in the input ended prematurely.
119pub trait EndOfInputError: std::fmt::Debug + 'static {
120    fn doing(&self) -> String;
121    fn notes(&self) -> Vec<display::Note> {
122        vec![]
123    }
124}
125
126/// An error for work-in-progress Texlang code.
127///
128/// When working on Texlang code it's often nice to figure out the logic first,
129///     and then go through later to polish the error cases.
130/// This function returns a "TODO" error that helps with this process.
131///
132/// Use the return value of this function in any place you plan to generate an error.
133/// Later on, follow Texlang best practices and create a specific error
134///     type for the case with a good error message.
135#[allow(non_snake_case)]
136pub fn TODO() -> impl TexError + EndOfInputError {
137    TodoError {}
138}
139
140#[derive(Debug)]
141struct TodoError {}
142
143impl EndOfInputError for TodoError {
144    fn doing(&self) -> String {
145        "? (TODO: add a specific end of input error for this case.)".into()
146    }
147    fn notes(&self) -> Vec<display::Note> {
148        vec![
149            "the Rust source code uses `texlang::error::TODO()` for this error case".into(),
150            "a more specific end of input error needs to be added".into(),
151        ]
152    }
153}
154
155impl TexError for TodoError {
156    fn kind(&self) -> Kind {
157        Kind::FailedPrecondition
158    }
159    fn title(&self) -> String {
160        "? (TODO: add a specific error for this case.)".into()
161    }
162    fn notes(&self) -> Vec<display::Note> {
163        vec![
164            "the Rust source code uses `texlang::error::TODO()` for this error case".into(),
165            "a more specific error needs to be added".into(),
166        ]
167    }
168}
169
170#[derive(Debug)]
171pub(crate) struct EofError {
172    doing: String,
173    notes: Vec<display::Note>,
174}
175
176impl EofError {
177    pub(crate) fn new<E: EndOfInputError>(err: E) -> Self {
178        Self {
179            doing: err.doing(),
180            notes: err.notes(),
181        }
182    }
183}
184
185impl TexError for EofError {
186    fn kind(&self) -> Kind {
187        Kind::EndOfInput
188    }
189
190    fn title(&self) -> String {
191        format!("Unexpected end of input while {}", self.doing)
192    }
193    fn notes(&self) -> Vec<display::Note> {
194        self.notes.clone()
195    }
196}
197
198/// Element of a stack trace.
199#[derive(Clone, Debug)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201pub struct StackTraceElement {
202    pub context: OperationKind,
203    pub token: token::Token,
204    pub trace: trace::SourceCodeTrace,
205}
206
207impl std::fmt::Display for TracedTexError {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        display::format_error(f, self)
210    }
211}
212
213/// The type of an error.
214#[derive(Clone, Debug, PartialEq, Eq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216pub enum Kind {
217    /// An error at a particular TeX token.
218    ///
219    /// For example, a TeX command expects a number but the next token is a letter.
220    Token(token::Token),
221    /// An end-of-input error.
222    ///
223    /// For example, a TeX command expects a number but there is no more input.
224    EndOfInput,
225    /// Some external condition does not hold and so the TeX code is incorrect.
226    ///
227    /// For example, a TeX command tries to open a file a particular path,
228    ///     but the file does not exist.
229    FailedPrecondition,
230}
231
232/// Implementations of this trait describe an error in TeX source code.
233pub trait TexError: std::fmt::Debug + 'static {
234    fn kind(&self) -> Kind;
235
236    fn title(&self) -> String;
237
238    fn notes(&self) -> Vec<display::Note> {
239        vec![]
240    }
241
242    fn source_annotation(&self) -> String {
243        TexError::default_source_annotation(self)
244    }
245
246    fn default_source_annotation(&self) -> String {
247        match TexError::kind(self) {
248            Kind::Token(t) => match (t.char(), t.cat_code()) {
249                (Some(c), Some(code)) => {
250                    format!["character token with value {c} and category code {code}",]
251                }
252                _ => "control sequence".to_string(),
253            },
254            Kind::EndOfInput => "input ended here".into(),
255            Kind::FailedPrecondition => "error occurred while running this command".into(),
256        }
257    }
258
259    fn source_code_trace_override(&self) -> Option<&trace::SourceCodeTrace> {
260        None
261    }
262    // TODO: have a method that returns the exact error messages as Knuth's TeX
263    // The method will return a vector of static strings
264}
265
266#[derive(Copy, Clone, Debug)]
267#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
268pub enum OperationKind {
269    Expansion,
270    Execution,
271    VariableIndex,
272    VariableAssignment,
273}
274
275impl OperationKind {
276    fn action(&self) -> &'static str {
277        match self {
278            OperationKind::Expansion => "expanding this command",
279            OperationKind::Execution => "executing this command",
280            OperationKind::VariableIndex => "determining the index of this variable",
281            OperationKind::VariableAssignment => "determining the value to assign to this variable",
282        }
283    }
284}
285
286#[derive(Debug)]
287pub struct SimpleTokenError {
288    pub token: token::Token,
289    pub title: String,
290}
291
292impl SimpleTokenError {
293    /// Create a new simple token error.
294    pub fn new<T: AsRef<str>>(token: token::Token, title: T) -> SimpleTokenError {
295        SimpleTokenError {
296            token,
297            title: title.as_ref().into(),
298        }
299    }
300}
301
302impl TexError for SimpleTokenError {
303    fn kind(&self) -> Kind {
304        Kind::Token(self.token)
305    }
306
307    fn title(&self) -> String {
308        self.title.clone()
309    }
310}
311
312#[derive(Debug)]
313pub struct SimpleFailedPreconditionError {
314    pub title: String,
315    pub text_notes: Vec<String>,
316}
317
318impl SimpleFailedPreconditionError {
319    /// Create a new simple failed precondition error.
320    pub fn new<T: AsRef<str>>(title: T) -> Self {
321        Self {
322            title: title.as_ref().into(),
323            text_notes: vec![],
324        }
325    }
326
327    pub fn with_note<T: Into<String>>(mut self, note: T) -> Self {
328        self.text_notes.push(note.into());
329        self
330    }
331}
332
333impl TexError for SimpleFailedPreconditionError {
334    fn kind(&self) -> Kind {
335        Kind::FailedPrecondition
336    }
337
338    fn title(&self) -> String {
339        self.title.clone()
340    }
341
342    fn notes(&self) -> Vec<display::Note> {
343        let mut notes = vec![];
344        for text_note in &self.text_notes {
345            notes.push(text_note.into())
346        }
347        notes
348    }
349}
350
351/// Concrete error for the case when a command is undefined.
352///
353/// This error is returned when a control sequence or active character
354///     is not defined.
355#[derive(Debug)]
356pub struct UndefinedCommandError {
357    /// The token that was referred to an undefined command.
358    pub token: token::Token,
359    /// Control sequences that are spelled similarly to the token.
360    pub close_names: Vec<WordDiff>,
361}
362
363impl UndefinedCommandError {
364    /// Create a new undefined command error.
365    pub fn new<S>(vm: &vm::VM<S>, token: token::Token) -> UndefinedCommandError {
366        let name = match &token.value() {
367            token::Value::CommandRef(command_ref) => command_ref.to_string(vm.cs_name_interner()),
368            _ => panic!("undefined command error does not work for non-command-ref tokens"),
369        };
370        let mut all_names = Vec::<&str>::new();
371        for (cs_name, _) in vm.get_commands_as_map_slow().into_iter() {
372            all_names.push(cs_name);
373        }
374        let close_names = spellcheck::find_close_words(&all_names, &name);
375
376        UndefinedCommandError { token, close_names }
377    }
378}
379
380impl TexError for UndefinedCommandError {
381    fn kind(&self) -> Kind {
382        Kind::Token(self.token)
383    }
384
385    fn title(&self) -> String {
386        "undefined control sequence".into()
387    }
388
389    fn notes(&self) -> Vec<display::Note> {
390        let mut notes: Vec<display::Note> = Default::default();
391        use texcraft_stdext::color::Colorize;
392        if let Some(close_name) = self.close_names.first() {
393            notes.push(format!["did you mean \\{}?\n", close_name.right().bold(),].into());
394        }
395        notes
396    }
397}