Skip to main content

retex_engine/
error.rs

1//! Error reporting and recovery (*TeX: The Program* §71–§98, §311–§320).
2//!
3//! Errors print `! message.`, the input context and help text exactly as
4//! TeX does. Fatal errors end the run by unwinding to the top-level driver
5//! with a [`JumpOut`] payload (TeX's `jump_out`). `std::panic::resume_unwind`
6//! is used, so no panic hook runs and nothing is printed. This is the only
7//! non-local exit in the engine.
8
9use crate::Engine;
10use crate::input::{Level, LineSource, TokKind};
11use crate::print::Selector;
12
13/// Interaction level (`interaction`).
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
15pub enum Interaction {
16    /// `\batchmode`.
17    Batch = 0,
18    /// `\nonstopmode`.
19    NonStop = 1,
20    /// `\scrollmode`.
21    Scroll = 2,
22    /// `\errorstopmode`.
23    ErrorStop = 3,
24}
25
26impl Interaction {
27    /// The level with numeric value `n` (0–3).
28    pub fn from_i32(n: i32) -> Interaction {
29        match n {
30            0 => Interaction::Batch,
31            1 => Interaction::NonStop,
32            2 => Interaction::Scroll,
33            _ => Interaction::ErrorStop,
34        }
35    }
36}
37
38/// How bad things are (`history`).
39#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
40pub enum History {
41    /// No problems.
42    Spotless,
43    /// `begin_diagnostic` was called with warnings.
44    WarningIssued,
45    /// An error was reported.
46    ErrorMessageIssued,
47    /// The run was aborted.
48    FatalErrorStop,
49}
50
51/// Payload of the unwind that ends a run (TeX's `jump_out`).
52#[derive(Debug, Clone, Copy)]
53pub struct JumpOut;
54
55/// Error-handling state.
56#[derive(Debug)]
57pub struct ErrorState {
58    /// The interaction level.
59    pub interaction: Interaction,
60    /// The run's history.
61    pub history: History,
62    /// Number of errors since the last paragraph end (`error_count`).
63    pub error_count: i32,
64    /// Help lines for the next error, top line first.
65    pub help: Vec<&'static str>,
66    /// Owned help lines (used instead of `help` when non-empty).
67    pub help_owned: Vec<String>,
68    /// Whether `\errhelp` should be shown instead of the help lines.
69    pub use_err_help: bool,
70    /// Whether deletions are allowed in error recovery.
71    pub deletions_allowed: bool,
72    /// Whether `\setbox` is allowed here.
73    pub set_box_allowed: bool,
74    /// `no_new_control_sequence`: lookups must not create new names.
75    pub no_new_control_sequence: bool,
76    /// The selector saved by `begin_diagnostic`.
77    pub old_setting: Option<Selector>,
78    /// Whether `file:line:error` style messages are requested.
79    pub file_line_error: bool,
80    /// `-halt-on-error`: the first error ends the run.
81    pub halt_on_error: bool,
82    /// Whether a `\par` should not be inserted (`OK_to_interrupt`).
83    pub interrupt: bool,
84}
85
86impl Default for ErrorState {
87    fn default() -> Self {
88        ErrorState {
89            interaction: Interaction::ErrorStop,
90            history: History::FatalErrorStop,
91            error_count: 0,
92            help: Vec::new(),
93            help_owned: Vec::new(),
94            use_err_help: false,
95            deletions_allowed: true,
96            set_box_allowed: true,
97            no_new_control_sequence: true,
98            old_setting: None,
99            file_line_error: false,
100            halt_on_error: false,
101            interrupt: false,
102        }
103    }
104}
105
106impl Engine {
107    /// Starts an error message (`print_err`).
108    pub fn print_err(&mut self, msg: &str) {
109        self.printer.begin_line();
110        if !(self.err.file_line_error && self.print_file_line()) {
111            self.printer.print_str("! ");
112        }
113        if self.diagnostics.is_some() {
114            self.printer.capture = Some(Vec::new());
115        }
116        self.printer.print_str(msg);
117    }
118
119    /// Starts collecting a warning message (default output mode).
120    pub fn diag_capture_start(&mut self) {
121        if self.diagnostics.is_some() {
122            self.printer.capture = Some(Vec::new());
123        }
124    }
125
126    /// The innermost input file: name, line number, line text and the
127    /// reading position in it.
128    pub fn source_location(&self) -> Option<(String, u32, String, usize)> {
129        let elc = self
130            .eqtb
131            .int(crate::primitives_gen::assign_int::ENDLINECHAR);
132        self.input.stack.iter().rev().find_map(|l| match l {
133            crate::input::Level::Text(t) if t.is_input_file => {
134                let mut chars: &[u32] = &t.buf;
135                if let Some((&last, rest)) = chars.split_last()
136                    && i64::from(last) == i64::from(elc)
137                {
138                    chars = rest;
139                }
140                let text: String = chars.iter().filter_map(|&c| char::from_u32(c)).collect();
141                Some((
142                    String::from_utf8_lossy(&t.name).into_owned(),
143                    t.line.max(0) as u32,
144                    text,
145                    t.loc.min(chars.len()),
146                ))
147            }
148            _ => None,
149        })
150    }
151
152    /// Records the captured warning text as a diagnostic located at `line`
153    /// of the current file (or its current line).
154    pub fn diag_capture_warning(&mut self, line: Option<u32>) {
155        let Some(text) = self.printer.capture.take() else {
156            return;
157        };
158        let message = String::from_utf8_lossy(&text).trim().replace('\n', " ");
159        let mut d = crate::diag::Diagnostic::new(crate::diag::Severity::Warning, message);
160        if let Some((file, cur, _, _)) = self.source_location() {
161            d.file = Some(file);
162            d.line = Some(line.unwrap_or(cur));
163        }
164        if let Some(list) = self.diagnostics.as_mut() {
165            list.push(d);
166        }
167    }
168
169    /// Records `text`, written to the terminal or shown, as a note at the
170    /// current place (default output mode).
171    pub fn record_note(&mut self, text: &str) {
172        if self.diagnostics.is_none() {
173            return;
174        }
175        let message = text.trim().replace('\n', " ");
176        // The format's own greeting (`LaTeX2e <…>`) comes from \everyjob.
177        let every_job = self.input.stack.iter().any(|l| {
178            matches!(l, crate::input::Level::Tokens(t) if t.kind == crate::input::TokKind::EveryJobText)
179        });
180        if message.is_empty() || every_job {
181            return;
182        }
183        let mut d = crate::diag::Diagnostic::new(crate::diag::Severity::Note, message);
184        if let Some((file, line, _, _)) = self.source_location() {
185            d.file = Some(file);
186            d.line = Some(line);
187        }
188        // The macro the file's text called, through which the message
189        // came (`typeout`, or `begin` for a package's hook at
190        // `\begin{document}`).
191        let stack = &self.input.stack;
192        if let Some(i) = stack
193            .iter()
194            .rposition(|l| matches!(l, crate::input::Level::Text(t) if t.is_input_file))
195            && let Some(crate::input::Level::Tokens(t)) = stack.get(i + 1)
196            && t.kind == crate::input::TokKind::Macro
197        {
198            let name = String::from_utf8_lossy(self.hash.name(t.name)).into_owned();
199            d.facts.push(("called".to_owned(), name));
200        }
201        if let Some(list) = self.diagnostics.as_mut() {
202            list.push(d);
203        }
204    }
205
206    /// Records the error being reported (default output mode).
207    fn record_error(&mut self) {
208        let Some(text) = self.printer.capture.take() else {
209            return;
210        };
211        let mut message = String::from_utf8_lossy(&text).trim().replace('\n', " ");
212        if message == "Undefined control sequence" && self.cur_cs != crate::hash::NO_CS {
213            let name = self.print_to_string(|e| e.print_cs(e.cur_cs));
214            message = format!(
215                "Undefined control sequence {}",
216                String::from_utf8_lossy(&name).trim_end()
217            );
218        }
219        let mut d = crate::diag::Diagnostic::new(crate::diag::Severity::Error, message);
220        d.facts = self.error_facts(&d.message);
221        if let Some((file, line, source, column)) = self.source_location() {
222            d.file = Some(file);
223            d.line = Some(line);
224            d.source = Some(source);
225            d.column = Some(column);
226        }
227        d.help = if self.err.help_owned.is_empty() {
228            self.err.help.iter().map(|s| (*s).to_owned()).collect()
229        } else {
230            self.err.help_owned.clone()
231        };
232        if let Some(list) = self.diagnostics.as_mut() {
233            list.push(d);
234        }
235    }
236
237    /// Facts about the error being reported, for the explanations of the
238    /// default output mode (see [`crate::diag::Diagnostic::facts`]).
239    fn error_facts(&mut self, message: &str) -> Vec<(String, String)> {
240        let mut facts = Vec::new();
241        if message.starts_with("Undefined control sequence") && self.cur_cs != crate::hash::NO_CS {
242            let name = self.hash.name(self.cur_cs).to_vec();
243            let similar = self.similar_defined_names(&name);
244            if !similar.is_empty() {
245                facts.push(("similar".to_owned(), similar.join(" ")));
246            }
247        }
248        if let Some(option) = bad_option(message) {
249            let similar = self.similar_options(option.as_bytes());
250            if !similar.is_empty() {
251                facts.push(("similar_options".to_owned(), similar.join(" ")));
252            }
253        }
254        if message.starts_with("Missing $ inserted") {
255            let tok = if self.cur_cmd == crate::cmd::Cmd::ParEnd {
256                // `\par` under any name (`\tex_par:D`, an empty line).
257                "\\par".to_owned()
258            } else if self.cur_cs != crate::hash::NO_CS {
259                let cs = self.cur_cs;
260                String::from_utf8_lossy(&self.print_to_string(|e| e.print_cs(cs)))
261                    .trim_end()
262                    .to_owned()
263            } else {
264                char::from_u32(self.cur_chr)
265                    .map(String::from)
266                    .unwrap_or_default()
267            };
268            facts.push(("token".to_owned(), tok));
269            if self.mode.abs_kind() == crate::maincontrol::ModeKind::Math {
270                let line = self.cur_list().mode_line.unsigned_abs();
271                facts.push(("math_line".to_owned(), line.to_string()));
272            }
273        }
274        facts
275    }
276
277    /// Declared package or class options whose names are close to
278    /// `option` (a typo), best first, at most three: options declared with
279    /// `\DeclareOption` (`\ds@…`), keyval keys (`\KV@<family>@…`) and
280    /// l3keys keys (`key code > <module>/…`).
281    fn similar_options(&self, option: &[u8]) -> Vec<String> {
282        let max = if option.len() < 5 { 1 } else { 2 };
283        let mut found: Vec<(usize, Vec<u8>)> = self
284            .hash
285            .iter_named()
286            .filter(|(_, id)| self.eqtb.meaning(*id).cmd != crate::cmd::Cmd::UndefinedCs)
287            .filter_map(|(n, _)| {
288                if let Some(o) = n.strip_prefix(b"ds@") {
289                    Some(o)
290                } else if let Some(r) = n.strip_prefix(b"KV@") {
291                    let k = &r[r.iter().position(|&b| b == b'@')? + 1..];
292                    (!k.ends_with(b"@default")).then_some(k)
293                } else {
294                    let r = n.strip_prefix(b"key code > ")?;
295                    Some(&r[r.iter().rposition(|&b| b == b'/')? + 1..])
296                }
297            })
298            .filter(|o| !o.is_empty() && o.iter().all(|b| !b.is_ascii_whitespace()))
299            .filter_map(|o| {
300                crate::diag::edit_distance_within(option, o, max).map(|d| (d, o.to_vec()))
301            })
302            .filter(|(d, _)| *d > 0)
303            .collect();
304        found.sort();
305        found.dedup();
306        found
307            .into_iter()
308            .take(3)
309            .map(|(_, n)| String::from_utf8_lossy(&n).into_owned())
310            .collect()
311    }
312
313    /// Defined control sequences whose names are close to `name` (a
314    /// typo), best first, at most three; internal names (with `@`, `_` or
315    /// `:`) only when `name` looks internal too.
316    pub(crate) fn similar_defined_names(&self, name: &[u8]) -> Vec<String> {
317        let internal = |n: &[u8]| n.iter().any(|&b| matches!(b, b'@' | b'_' | b':'));
318        let max = if name.len() < 4 { 1 } else { 2 };
319        let mut found: Vec<(usize, Vec<u8>)> = self
320            .hash
321            .iter_named()
322            .filter(|(n, _)| {
323                n.len() > 1
324                    && n.iter()
325                        .all(|b| !b.is_ascii_whitespace() && !b.is_ascii_control())
326            })
327            .filter(|(n, _)| !internal(n) || internal(name))
328            .filter(|(_, id)| self.eqtb.meaning(*id).cmd != crate::cmd::Cmd::UndefinedCs)
329            .filter_map(|(n, _)| {
330                crate::diag::edit_distance_within(name, n, max).map(|d| (d, n.to_vec()))
331            })
332            .filter(|(d, _)| *d > 0)
333            .collect();
334        found.sort();
335        found.dedup();
336        found
337            .into_iter()
338            .take(3)
339            .map(|(_, n)| format!("\\{}", String::from_utf8_lossy(&n)))
340            .collect()
341    }
342
343    /// Starts an error message given as bytes.
344    pub fn print_err_bytes(&mut self, msg: &[u8]) {
345        self.print_err("");
346        self.printer.print_bytes(msg);
347    }
348
349    /// Prints `file:line: ` for `-file-line-error`; returns `false` if no
350    /// file is open.
351    fn print_file_line(&mut self) -> bool {
352        let found = self.input.stack.iter().rev().find_map(|l| match l {
353            Level::Text(t) if t.is_input_file => Some((t.name.clone(), t.line)),
354            _ => None,
355        });
356        match found {
357            Some((name, line)) => {
358                self.printer.print_bytes(&name);
359                self.printer.print_char(':' as u32);
360                self.printer.print_int(line.into());
361                self.printer.print_str(": ");
362                true
363            }
364            None => false,
365        }
366    }
367
368    /// Sets the help lines for the next error (top line first).
369    pub fn help(&mut self, lines: &[&'static str]) {
370        self.err.help.clear();
371        self.err.help.extend_from_slice(lines);
372        self.err.help_owned.clear();
373    }
374
375    /// Sets owned help lines.
376    pub fn help_owned(&mut self, lines: Vec<String>) {
377        self.err.help.clear();
378        self.err.help_owned = lines;
379    }
380
381    /// Completes an error message: prints the context and help, and
382    /// interacts with the user if appropriate (`error`, §82).
383    pub fn error(&mut self) {
384        self.record_error();
385        if self.err.history < History::ErrorMessageIssued {
386            self.err.history = History::ErrorMessageIssued;
387        }
388        self.printer.print_char('.' as u32);
389        self.show_context();
390        if self.err.halt_on_error {
391            self.halt_on_error_stop();
392        }
393        if self.err.interaction == Interaction::ErrorStop {
394            // No interactive terminal support: behave like scroll mode, but
395            // a real terminal session would ask the user here.
396            self.terminal_error_stop();
397        }
398        self.err.error_count += 1;
399        if self.err.error_count == 100 {
400            self.printer
401                .print_nl("(That makes 100 errors; please try again.)");
402            self.err.history = History::FatalErrorStop;
403            self.jump_out();
404        }
405        // Put the help message on the transcript file.
406        let sel = self.printer.selector;
407        if self.err.interaction > Interaction::Batch {
408            self.printer.selector = match sel {
409                Selector::TermAndLog => Selector::LogOnly,
410                Selector::TermOnly => Selector::NoPrint,
411                s => s,
412            };
413        }
414        if self.err.use_err_help {
415            self.printer.print_ln();
416            self.give_err_help();
417        } else {
418            let lines: Vec<String> = if self.err.help_owned.is_empty() {
419                self.err.help.iter().map(|s| s.to_string()).collect()
420            } else {
421                self.err.help_owned.clone()
422            };
423            let empty = lines.is_empty();
424            for l in lines {
425                self.printer.print_nl(&l);
426            }
427            // LuaTeX does not add an empty line when there is no help and
428            // the context line was wrapped at max_print_line.
429            if empty {
430                self.printer.begin_line();
431            } else {
432                self.printer.print_ln();
433            }
434        }
435        if self.err.use_err_help {
436            self.printer.print_ln();
437        }
438        self.err.help.clear();
439        self.err.help_owned.clear();
440        if self.err.interaction > Interaction::Batch {
441            self.printer.selector = sel;
442        }
443        self.printer.print_ln();
444    }
445
446    /// Reads a line from the terminal, echoing it to the log
447    /// (`term_input`, §71). End of input is fatal, as in TeX.
448    pub fn term_input(&mut self) -> Vec<u32> {
449        self.prompted = true;
450        if self.printer.term_quiet {
451            let prompt = std::mem::take(&mut self.printer.quiet_line);
452            self.show_before_prompt(&String::from_utf8_lossy(&prompt));
453        }
454        self.printer.update_terminal();
455        let mut line = String::new();
456        match std::io::stdin().read_line(&mut line) {
457            Ok(0) | Err(_) => self.fatal_error("End of file on the terminal!"),
458            Ok(_) => {}
459        }
460        let chars = crate::input::decode_line(line.trim_end_matches(['\n', '\r']).as_bytes());
461        self.printer.term_offset = 0;
462        let old = self.printer.selector;
463        self.printer.selector = match old {
464            Selector::TermAndLog => Selector::LogOnly,
465            Selector::TermOnly => Selector::NoPrint,
466            s => s,
467        };
468        for &c in &chars {
469            self.printer.print_char(c);
470        }
471        self.printer.print_ln();
472        self.printer.selector = old;
473        chars
474    }
475
476    /// Prints a prompt and reads a line (`prompt_input`).
477    pub fn prompt_input(&mut self, prompt: &str) -> Vec<u32> {
478        self.printer.print_str(prompt);
479        self.term_input()
480    }
481
482    /// Default output mode: calls the prompt hook with the diagnostics not
483    /// shown yet and the prompt.
484    fn show_before_prompt(&mut self, prompt: &str) -> bool {
485        let Some(mut hook) = self.prompt_hook.take() else {
486            return true;
487        };
488        let list = self.diagnostics.as_deref().unwrap_or(&[]);
489        let from = self.diagnostics_shown.min(list.len());
490        let shown = hook(&list[from..], prompt);
491        self.diagnostics_shown = list.len();
492        self.prompt_hook = Some(hook);
493        shown
494    }
495
496    /// Asks the user what to do about an error (§84–§93). In the default
497    /// output mode, the error is shown as a diagnostic and TeX's dialog
498    /// then runs on the terminal as usual.
499    fn terminal_error_stop(&mut self) {
500        let quiet = self.printer.term_quiet;
501        if quiet && self.err.interaction == Interaction::ErrorStop {
502            if !self.show_before_prompt("") {
503                return;
504            }
505            self.printer.term_quiet = false;
506            self.printer.quiet_line.clear();
507        }
508        self.terminal_error_dialog();
509        self.printer.term_quiet = quiet;
510    }
511
512    fn terminal_error_dialog(&mut self) {
513        loop {
514            if self.err.interaction != Interaction::ErrorStop {
515                return;
516            }
517            self.clear_for_error_prompt();
518            let line = self.prompt_input("? ");
519            let Some(&c0) = line.first() else {
520                return;
521            };
522            let c = char::from_u32(c0).unwrap_or(' ').to_ascii_uppercase();
523            match c {
524                '0'..='9' if self.err.deletions_allowed => {
525                    let mut n = c as u32 - '0' as u32;
526                    if let Some(&d) = line.get(1)
527                        && (0x30..=0x39).contains(&d)
528                    {
529                        n = n * 10 + (d - 0x30);
530                    }
531                    let (s1, s2, s3, s4) = (
532                        self.cur_tok,
533                        self.cur_cmd,
534                        self.cur_chr,
535                        self.input.align_state,
536                    );
537                    self.input.align_state = 1_000_000;
538                    for _ in 0..n {
539                        self.get_token();
540                    }
541                    self.cur_tok = s1;
542                    self.cur_cmd = s2;
543                    self.cur_chr = s3;
544                    self.input.align_state = s4;
545                    self.help(&[
546                        "I have just deleted some text, as you asked.",
547                        "You can now delete more, or insert, or whatever.",
548                    ]);
549                    self.show_context();
550                    continue;
551                }
552                'E' => {
553                    let file = self.input.stack.iter().rev().find_map(|l| match l {
554                        Level::Text(t) if t.is_input_file => Some((t.name.clone(), t.line)),
555                        _ => None,
556                    });
557                    if let Some((name, line)) = file {
558                        self.printer.print_nl("You want to edit file ");
559                        self.printer.print_bytes(&name);
560                        self.printer.print_str(" at line ");
561                        self.printer.print_int(line.into());
562                        self.edit_request = Some((name, line));
563                        self.err.interaction = Interaction::Scroll;
564                        self.jump_out();
565                    }
566                }
567                'H' => {
568                    if self.err.use_err_help {
569                        self.give_err_help();
570                        self.err.use_err_help = false;
571                    } else {
572                        let lines: Vec<String> = if !self.err.help_owned.is_empty() {
573                            self.err.help_owned.clone()
574                        } else if !self.err.help.is_empty() {
575                            self.err.help.iter().map(|s| s.to_string()).collect()
576                        } else {
577                            vec![
578                                "Sorry, I don't know how to help in this situation.".into(),
579                                "Maybe you should try asking a human?".into(),
580                            ]
581                        };
582                        for l in lines {
583                            self.printer.print_str(&l);
584                            self.printer.print_ln();
585                        }
586                    }
587                    self.help(&[
588                        "Sorry, I already gave what help I could...",
589                        "Maybe you should try asking a human?",
590                        "An error might have occurred before I noticed any problems.",
591                        "``If all else fails, read the instructions.''",
592                    ]);
593                    continue;
594                }
595                'I' => {
596                    // Insert material from the terminal.
597                    let text: Vec<u32> = if line.len() > 1 {
598                        let mut v = vec![' ' as u32];
599                        v.extend_from_slice(&line[1..]);
600                        v
601                    } else {
602                        self.prompt_input("insert>")
603                    };
604                    self.begin_file_reading(crate::input::LineSource::Terminal, Vec::new(), false);
605                    if let Some(Level::Text(t)) = self.input.stack.last_mut() {
606                        t.buf = text;
607                        t.loc = 0;
608                        t.state = crate::input::LexState::MidLine;
609                    }
610                    return;
611                }
612                'Q' | 'R' | 'S' => {
613                    self.err.error_count = 0;
614                    self.printer.print_str("OK, entering ");
615                    match c {
616                        'Q' => {
617                            self.err.interaction = Interaction::Batch;
618                            self.print_esc_str("batchmode");
619                            self.printer.selector = match self.printer.selector {
620                                Selector::TermAndLog => Selector::LogOnly,
621                                _ => Selector::NoPrint,
622                            };
623                        }
624                        'R' => {
625                            self.err.interaction = Interaction::NonStop;
626                            self.print_esc_str("nonstopmode");
627                        }
628                        _ => {
629                            self.err.interaction = Interaction::Scroll;
630                            self.print_esc_str("scrollmode");
631                        }
632                    }
633                    self.printer.print_str("...");
634                    self.printer.print_ln();
635                    self.printer.update_terminal();
636                    return;
637                }
638                'X' => {
639                    self.err.interaction = Interaction::Scroll;
640                    self.jump_out();
641                }
642                _ => {}
643            }
644            // Print the menu of available options.
645            self.printer
646                .print_str("Type <return> to proceed, S to scroll future error messages,");
647            self.printer
648                .print_nl("R to run without stopping, Q to run quietly,");
649            self.printer.print_nl("I to insert something, ");
650            if self
651                .input
652                .stack
653                .iter()
654                .any(|l| matches!(l, Level::Text(t) if t.is_input_file))
655            {
656                self.printer.print_str("E to edit your file,");
657            }
658            if self.err.deletions_allowed {
659                self.printer
660                    .print_nl("1 or ... or 9 to ignore the next 1 to 9 tokens of input,");
661            }
662            self.printer.print_nl("H for help, X to quit.");
663        }
664    }
665
666    /// Pops finished terminal levels before prompting
667    /// (`clear_for_error_prompt`, §330).
668    fn clear_for_error_prompt(&mut self) {
669        while self.input.stack.len() > 1 {
670            match self.input.stack.last() {
671                Some(Level::Text(t))
672                    if matches!(t.source, LineSource::Terminal) && t.loc >= t.buf.len() =>
673                {
674                    self.end_file_reading();
675                }
676                _ => break,
677            }
678        }
679        self.printer.print_ln();
680    }
681
682    /// Prints `\errhelp` (`give_err_help`).
683    pub fn give_err_help(&mut self) {
684        let list = self
685            .eqtb
686            .toks_param(crate::primitives_gen::assign_toks::ERRHELP)
687            .clone();
688        self.token_show_list(&list, 0, list.len(), 10_000_000);
689    }
690
691    /// Reports a fatal error and ends the run (`fatal_error`).
692    pub fn fatal_error(&mut self, s: &str) -> ! {
693        self.normalize_selector();
694        self.print_err("Emergency stop");
695        self.help_owned(vec![s.to_owned()]);
696        self.succumb();
697    }
698
699    /// A LuaTeX fatal error of category `category` (as LuaTeX prints them:
700    /// `! error:  (category): message`, then `!  ==> Fatal error
701    /// occurred, …`); ends the run without the transcript line.
702    pub fn luatex_fatal(&mut self, category: &str, msg: &str) -> ! {
703        self.normalize_selector();
704        self.printer.print_nl("! error:  (");
705        self.printer.print_str(category);
706        self.printer.print_str("): ");
707        self.printer.print_str(msg);
708        let pdf = self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0;
709        self.printer.print_nl(if pdf {
710            "!  ==> Fatal error occurred, bad output PDF file produced!"
711        } else {
712            "!  ==> Fatal error occurred, bad output DVI file produced!"
713        });
714        self.printer.print_ln();
715        self.err.history = History::FatalErrorStop;
716        for n in 0..128u32 {
717            if self.printer.write_open(n) {
718                self.printer.close_write(n);
719            }
720        }
721        if self.total_pages == 0 {
722            self.printer.print_str("No pages of output.");
723            self.printer.print_ln();
724        } else if !pdf {
725            // Observed: the pages shipped so far are written.
726            self.finish_output();
727        }
728        if self.printer.log_opened() {
729            self.printer.close_log();
730        }
731        self.printer.selector = Selector::TermOnly;
732        self.printer.flush();
733        std::panic::resume_unwind(Box::new(JumpOut));
734    }
735
736    /// Ends the run after the first error with `-halt-on-error`: as
737    /// LuaTeX does, without the help text, with its fatal-error line, and
738    /// with the transcript line.
739    fn halt_on_error_stop(&mut self) -> ! {
740        let pdf = self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0;
741        self.printer.print_nl(if pdf {
742            "!  ==> Fatal error occurred, bad output PDF file produced!"
743        } else {
744            "!  ==> Fatal error occurred, bad output DVI file produced!"
745        });
746        self.printer.print_ln();
747        self.err.history = History::FatalErrorStop;
748        for n in 0..128u32 {
749            if self.printer.write_open(n) {
750                self.printer.close_write(n);
751            }
752        }
753        if self.total_pages == 0 {
754            self.printer.print_str("No pages of output.");
755            self.printer.print_ln();
756        }
757        // Observed: the log (only) ends with an empty line.
758        if self.printer.log_opened() {
759            let sel = self.printer.selector;
760            self.printer.selector = Selector::LogOnly;
761            self.printer.print_ln();
762            self.printer.selector = sel;
763        }
764        self.finish_log();
765        std::panic::resume_unwind(Box::new(JumpOut));
766    }
767
768    /// Reports that a capacity was exceeded and ends the run (`overflow`).
769    pub fn overflow(&mut self, s: &str, n: i64) -> ! {
770        self.normalize_selector();
771        self.print_err("TeX capacity exceeded, sorry [");
772        self.printer.print_str(s);
773        self.printer.print_char('=' as u32);
774        self.printer.print_int(n);
775        self.printer.print_char(']' as u32);
776        self.help(&[
777            "If you really absolutely need more capacity,",
778            "you can ask a wizard to enlarge me.",
779        ]);
780        self.succumb();
781    }
782
783    /// Reports an internal inconsistency (`confusion`).
784    pub fn confusion(&mut self, s: &str) -> ! {
785        self.normalize_selector();
786        if self.err.history < History::ErrorMessageIssued {
787            self.print_err("This can't happen (");
788            self.printer.print_str(s);
789            self.printer.print_char(')' as u32);
790            self.help(&["I'm broken. Please show this to someone who can fix"]);
791        } else {
792            self.print_err("I can't go on meeting you like this");
793            self.help(&[
794                "One of your faux pas seems to have wounded me deeply...",
795                "in fact, I'm barely conscious. Please fix it and try again.",
796            ]);
797        }
798        self.succumb();
799    }
800
801    pub(crate) fn succumb(&mut self) -> ! {
802        if self.err.interaction == Interaction::ErrorStop {
803            self.err.interaction = Interaction::Scroll;
804        }
805        if self.printer.log_opened() {
806            self.error();
807        }
808        self.err.history = History::FatalErrorStop;
809        self.jump_out();
810    }
811
812    /// Ends the run (`jump_out`): closes files and unwinds to the driver.
813    pub fn jump_out(&mut self) -> ! {
814        if self.prompt_hook.is_some() {
815            // Out of an error dialog (`X`): the default mode stays quiet.
816            self.printer.term_quiet = true;
817        }
818        self.close_files_and_terminate();
819        std::panic::resume_unwind(Box::new(JumpOut));
820    }
821
822    /// Makes sure output goes somewhere sensible (`normalize_selector`).
823    pub fn normalize_selector(&mut self) {
824        self.printer.selector = if self.printer.log_opened() {
825            Selector::TermAndLog
826        } else {
827            Selector::TermOnly
828        };
829        if self.job_name.is_none() {
830            self.open_log_file();
831        }
832        if self.err.interaction == Interaction::Batch {
833            self.printer.selector = match self.printer.selector {
834                Selector::TermAndLog => Selector::LogOnly,
835                _ => Selector::NoPrint,
836            };
837        }
838    }
839
840    /// Hook for user interrupts; retex does not support them.
841    pub fn check_interrupt(&mut self) {}
842
843    /// `back_error`: backs up the current token and reports an error.
844    pub fn back_error(&mut self) {
845        self.err.interrupt = false;
846        self.back_input();
847        self.err.interrupt = true;
848        self.error();
849    }
850
851    /// `ins_error`: like `back_error` but the token is marked inserted.
852    pub fn ins_error(&mut self) {
853        self.err.interrupt = false;
854        self.back_input_inserted();
855        self.err.interrupt = true;
856        self.error();
857    }
858
859    /// A LuaTeX warning (`warning  (category): message` on a line of its
860    /// own); it counts as a warning for the final "see the transcript"
861    /// note.
862    pub fn luatex_warning(&mut self, category: &str, msg: &str) {
863        self.printer.luatex_warning(category, msg);
864        if self.err.history == History::Spotless {
865            self.err.history = History::WarningIssued;
866        }
867    }
868
869    /// Starts diagnostic output (`begin_diagnostic`): goes to the log only
870    /// unless `\tracingonline > 0`.
871    pub fn begin_diagnostic(&mut self) {
872        self.err.old_setting = Some(self.printer.selector);
873        let online = self
874            .eqtb
875            .int(crate::primitives_gen::assign_int::TRACINGONLINE);
876        if online <= 0 && self.printer.selector == Selector::TermAndLog {
877            self.printer.selector = Selector::LogOnly;
878            if self.err.history == History::Spotless {
879                self.err.history = History::WarningIssued;
880            }
881        }
882    }
883
884    /// Ends diagnostic output (`end_diagnostic`).
885    pub fn end_diagnostic(&mut self, blank_line: bool) {
886        self.printer.print_nl("");
887        if blank_line {
888            self.printer.print_ln();
889        }
890        if let Some(s) = self.err.old_setting.take() {
891            self.printer.selector = s;
892        }
893    }
894
895    /// Shows where the scanner is (`show_context`, §311).
896    pub fn show_context(&mut self) {
897        let error_context_lines = self
898            .eqtb
899            .int(crate::primitives_gen::assign_int::ERRORCONTEXTLINES);
900        let input_ptr = self.input.stack.len().saturating_sub(1);
901        let mut base_ptr = input_ptr;
902        let mut nn: i32 = -1;
903        loop {
904            let (is_text, bottom) = match &self.input.stack.get(base_ptr) {
905                Some(Level::Text(t)) => (
906                    true,
907                    t.is_input_file || matches!(t.source, LineSource::File { .. }) || base_ptr == 0,
908                ),
909                Some(Level::Tokens(_)) => (false, false),
910                None => (true, true),
911            };
912            let bottom_line = is_text && bottom;
913            if base_ptr == input_ptr || bottom_line || nn < error_context_lines {
914                // Display the current context, omitting backed-up token
915                // lists that have already been read.
916                let show = match self.input.stack.get(base_ptr) {
917                    Some(Level::Tokens(t)) => {
918                        base_ptr == input_ptr
919                            || !matches!(t.kind, TokKind::BackedUp | TokKind::BackedUpChar)
920                            || t.loc < t.list.len()
921                    }
922                    _ => true,
923                };
924                if show {
925                    self.printer.term_just_wrapped = false;
926                    self.display_context(base_ptr);
927                    nn += 1;
928                    // LuaTeX's terminal shows an empty line after a context
929                    // line that filled the terminal line exactly, when more
930                    // context follows (the log does not).
931                    if self.printer.term_just_wrapped
932                        && !(bottom_line || base_ptr == 0)
933                        && matches!(
934                            self.printer.selector,
935                            crate::print::Selector::TermAndLog | crate::print::Selector::TermOnly
936                        )
937                    {
938                        self.printer.term_raw(b"\n");
939                    }
940                }
941            } else if nn == error_context_lines {
942                self.printer.print_nl("...");
943                nn += 1;
944            }
945            if bottom_line || base_ptr == 0 {
946                break;
947            }
948            base_ptr -= 1;
949        }
950    }
951
952    fn display_context(&mut self, base_ptr: usize) {
953        let old_setting = self.printer.selector;
954        self.printer.tally = 0;
955        let level = self.input.stack[base_ptr].clone();
956        let l: usize;
957        match &level {
958            Level::Text(t) => {
959                match &t.source {
960                    LineSource::Terminal => {
961                        if base_ptr == 0 {
962                            self.printer.print_nl("<*>");
963                        } else {
964                            self.printer.print_nl("<insert> ");
965                        }
966                    }
967                    LineSource::Read(n) => {
968                        self.printer.print_nl("<read ");
969                        if *n == 16 {
970                            self.printer.print_char('*' as u32);
971                        } else {
972                            self.printer.print_int(i64::from(*n));
973                        }
974                        self.printer.print_char('>' as u32);
975                    }
976                    _ => {
977                        self.printer.print_nl("l.");
978                        self.printer.print_int(t.line.into());
979                    }
980                }
981                self.printer.print_char(' ' as u32);
982                // Pseudoprint the line.
983                l = self.begin_pseudoprint();
984                let end_line_char = self
985                    .eqtb
986                    .int(crate::primitives_gen::assign_int::ENDLINECHAR);
987                let mut j = t.buf.len();
988                if j > 0 && t.buf[j - 1] as i32 == end_line_char {
989                    j -= 1;
990                }
991                for i in 0..j {
992                    if i == t.loc {
993                        self.set_trick_count();
994                    }
995                    let c = t.buf[i];
996                    if crate::utf8x::is_raw(c) {
997                        // Observed: an invalid byte that looks like the
998                        // start of a sequence is shown with the bytes that
999                        // would complete it; other ones are not shown.
1000                        let n = match c & 0xFF {
1001                            0xC2..=0xDF => 2,
1002                            0xE0..=0xEF => 3,
1003                            0xF0..=0xF4 => 4,
1004                            _ => 0,
1005                        };
1006                        let mut bytes = Vec::new();
1007                        for &e in t.buf.get(i..j).unwrap_or_default() {
1008                            if bytes.len() >= n {
1009                                break;
1010                            }
1011                            crate::utf8x::push_entry(&mut bytes, e);
1012                        }
1013                        bytes.truncate(n);
1014                        self.printer.pseudo_unit(&bytes);
1015                    } else {
1016                        self.printer.print_char(c);
1017                    }
1018                }
1019            }
1020            Level::Tokens(t) => {
1021                match t.kind {
1022                    TokKind::Parameter => self.printer.print_nl("<argument> "),
1023                    TokKind::UTemplate | TokKind::VTemplate => self.printer.print_nl("<template> "),
1024                    TokKind::BackedUp | TokKind::BackedUpChar => {
1025                        if t.loc >= t.list.len() {
1026                            self.printer.print_nl("<recently read> ")
1027                        } else {
1028                            self.printer.print_nl("<to be read again> ")
1029                        }
1030                    }
1031                    TokKind::Inserted => self.printer.print_nl("<inserted text> "),
1032                    TokKind::Macro => {
1033                        self.printer.print_ln();
1034                        self.print_cs(t.name);
1035                    }
1036                    TokKind::OutputText => self.printer.print_nl("<output> "),
1037                    TokKind::EveryParText => self.printer.print_nl("<everypar> "),
1038                    TokKind::EveryMathText => self.printer.print_nl("<everymath> "),
1039                    TokKind::EveryDisplayText => self.printer.print_nl("<everydisplay> "),
1040                    TokKind::EveryHboxText => self.printer.print_nl("<everyhbox> "),
1041                    TokKind::EveryVboxText => self.printer.print_nl("<everyvbox> "),
1042                    TokKind::EveryJobText => self.printer.print_nl("<everyjob> "),
1043                    TokKind::EveryCrText => self.printer.print_nl("<everycr> "),
1044                    TokKind::MarkText => self.printer.print_nl("<mark> "),
1045                    TokKind::EveryEofText => self.printer.print_nl("<everyeof> "),
1046                    TokKind::WriteText => self.printer.print_nl("<write> "),
1047                    TokKind::LocalText => self.printer.print_nl("<local> "),
1048                }
1049                l = self.begin_pseudoprint();
1050                // For macros the list starts with the parameter text; TeX
1051                // shows the whole definition (parameters and body).
1052                self.show_token_list_ctx(&t.list, t.loc, 100_000);
1053            }
1054        }
1055        self.printer.selector = old_setting;
1056        self.print_two_lines(l);
1057    }
1058
1059    fn begin_pseudoprint(&mut self) -> usize {
1060        let l = self.printer.tally;
1061        self.printer.tally = 0;
1062        self.printer.selector = Selector::Pseudo;
1063        self.printer.trick_count = 1_000_000;
1064        self.printer.trick_buf.clear();
1065        self.printer
1066            .trick_buf
1067            .resize(self.printer.error_line, vec![b' ']);
1068        l
1069    }
1070
1071    /// TeX's `set_trick_count`.
1072    pub fn set_trick_count(&mut self) {
1073        let p = &mut self.printer;
1074        p.first_count = p.tally;
1075        p.trick_count = p.tally + 1 + p.error_line - p.half_error_line;
1076        if p.trick_count < p.error_line {
1077            p.trick_count = p.error_line;
1078        }
1079    }
1080
1081    fn print_two_lines(&mut self, l: usize) {
1082        if self.printer.trick_count == 1_000_000 {
1083            self.set_trick_count();
1084        }
1085        let p = &self.printer;
1086        let (tally, first_count, trick_count, half, error_line) = (
1087            p.tally,
1088            p.first_count,
1089            p.trick_count,
1090            p.half_error_line,
1091            p.error_line,
1092        );
1093        let m = if tally < trick_count {
1094            tally - first_count
1095        } else {
1096            trick_count - first_count
1097        };
1098        let (start, n) = if l + first_count <= half {
1099            (0, l + first_count)
1100        } else {
1101            self.printer.print_plain("...");
1102            (l + first_count - half + 3, half)
1103        };
1104        let buf = self.printer.trick_buf.clone();
1105        let mut bytes = Vec::new();
1106        for q in start..first_count {
1107            bytes.extend_from_slice(&buf[q % error_line]);
1108        }
1109        self.print_raw_bytes(&bytes);
1110        self.printer.print_ln();
1111        for _ in 0..n {
1112            self.printer.print_char(' ' as u32);
1113        }
1114        let p_end = if m + n <= error_line {
1115            first_count + m
1116        } else {
1117            first_count + (error_line - n - 3)
1118        };
1119        bytes.clear();
1120        for q in first_count..p_end {
1121            bytes.extend_from_slice(&buf[q % error_line]);
1122        }
1123        self.print_raw_bytes(&bytes);
1124        if m + n > error_line {
1125            self.printer.print_plain("...");
1126        }
1127    }
1128
1129    /// Prints already-converted bytes from the pseudo buffer, decoding
1130    /// complete UTF-8 sequences so that line lengths stay correct.
1131    fn print_raw_bytes(&mut self, bytes: &[u8]) {
1132        let mut i = 0;
1133        while i < bytes.len() {
1134            let (c, n) = crate::utf8x::decode(bytes, i);
1135            if n == 1 && c >= 0x80 {
1136                self.printer.print_raw_byte(c as u8);
1137            } else {
1138                self.printer.print_char(c);
1139            }
1140            i += n;
1141        }
1142    }
1143}
1144
1145/// The option of an error about an unknown package or class option:
1146/// LaTeX's "Unknown option `x' for package `p'" (also with straight quotes,
1147/// from l3keys) and keyval's "x undefined".
1148fn bad_option(message: &str) -> Option<String> {
1149    let m = message.split_once("Error: ").map_or(message, |(_, m)| m);
1150    if let Some(r) = m.strip_prefix("Unknown option ") {
1151        let r = r.strip_prefix(['`', '\''])?;
1152        return Some(r.split(['\'', '`']).next()?.to_owned());
1153    }
1154    if message.starts_with("Package keyval Error: ") {
1155        // The message goes on with LaTeX's pointers to help.
1156        return m.split_once(" undefined").map(|(o, _)| o.to_owned());
1157    }
1158    None
1159}