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