Skip to main content

retex_engine/
maincontrol.rs

1//! The chief executive (*TeX: The Program* §1029–§1054) and the semantic
2//! nest (§211–§219).
3//!
4//! `main_control` reads tokens and dispatches on the current mode and
5//! command. Assignments go to [`crate::prefixed`]. List-building commands
6//! go to the typesetting modules.
7
8use std::rc::Rc;
9
10use crate::Engine;
11use crate::cmd::Cmd;
12use crate::hash::NO_CS;
13use crate::input::{Level, TokKind};
14use crate::nodes::NodeRef;
15use crate::primitives_gen as pg;
16use crate::save::GroupCode;
17use crate::token::Token;
18
19/// A mode of TeX's semantic nest.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Mode {
22    /// No mode (during `\write` expansion, …).
23    None,
24    /// Vertical mode.
25    Vertical,
26    /// Internal vertical mode.
27    InternalVertical,
28    /// Horizontal mode.
29    Horizontal,
30    /// Restricted horizontal mode.
31    RestrictedHorizontal,
32    /// Display math mode.
33    DisplayMath,
34    /// Non-display math mode.
35    Math,
36}
37
38/// The kind of a mode, ignoring whether it is inner.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum ModeKind {
41    /// No mode.
42    None,
43    /// (Internal) vertical.
44    Vertical,
45    /// (Restricted) horizontal.
46    Horizontal,
47    /// (Display) math.
48    Math,
49}
50
51impl Mode {
52    /// The mode kind (`abs(mode)` in TeX).
53    pub fn abs_kind(self) -> ModeKind {
54        match self {
55            Mode::None => ModeKind::None,
56            Mode::Vertical | Mode::InternalVertical => ModeKind::Vertical,
57            Mode::Horizontal | Mode::RestrictedHorizontal => ModeKind::Horizontal,
58            Mode::DisplayMath | Mode::Math => ModeKind::Math,
59        }
60    }
61
62    /// Whether the mode is inner (`mode < 0` in TeX).
63    pub fn is_inner(self) -> bool {
64        matches!(
65            self,
66            Mode::InternalVertical | Mode::RestrictedHorizontal | Mode::Math
67        )
68    }
69
70    /// TeX's `mode > 0`.
71    pub fn is_privileged(self) -> bool {
72        matches!(self, Mode::Vertical | Mode::Horizontal | Mode::DisplayMath)
73    }
74}
75
76/// The context in which a box is being built (`box_context`).
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum BoxContext {
79    /// Shift by the given amount and append (`\moveleft`, `\raise`, or plain
80    /// appending with 0).
81    Shift(i32),
82    /// `\setbox n=`.
83    SetBox(u32),
84    /// `\global\setbox n=`.
85    GlobalSetBox(u32),
86    /// `\shipout`.
87    Shipout,
88    /// Leaders of the given glue subtype.
89    Leaders(u16),
90    /// Handed to Lua (`token.scan_list`, see `Engine::scan_list`).
91    Lua,
92}
93
94/// One level of the semantic nest (`list_state_record`).
95#[derive(Clone, Debug)]
96pub struct ListState {
97    /// The mode.
98    pub mode: Mode,
99    /// The list head (a temp node).
100    pub head: NodeRef,
101    /// The last node of the list.
102    pub tail: NodeRef,
103    /// `\prevgraf`.
104    pub prev_graf: i32,
105    /// Line where this mode was entered (negative if the mode was entered
106    /// by `\halign` or `\valign`).
107    pub mode_line: i32,
108    /// `\prevdepth` (vertical modes).
109    pub prev_depth: i32,
110    /// `\spacefactor` (horizontal modes).
111    pub space_factor: i32,
112    /// Current language (horizontal modes, `clang`).
113    pub clang: i32,
114    /// Incompleat noad (math modes).
115    pub incompleat_noad: NodeRef,
116    /// Math style (math modes).
117    pub math_style: i32,
118    /// The most recent `\left` or `\middle` fence of a math left group
119    /// (e-TeX's `delim_ptr`).
120    pub delim_ptr: NodeRef,
121}
122
123/// The `\prevdepth` value that suppresses interline glue
124/// (`ignore_depth`).
125pub const IGNORE_DEPTH: i32 = -65_536_000;
126
127impl Engine {
128    /// The current list state.
129    pub fn cur_list(&self) -> &ListState {
130        self.nest.last().expect("nest is never empty")
131    }
132
133    /// The current list state, mutably.
134    pub fn cur_list_mut(&mut self) -> &mut ListState {
135        self.nest.last_mut().expect("nest is never empty")
136    }
137
138    /// Enters a new semantic level (`push_nest`).
139    pub fn push_nest(&mut self, mode: Mode) {
140        let head = self.nodes.new_temp();
141        let cl = ListState {
142            mode,
143            head,
144            tail: head,
145            prev_graf: 0,
146            mode_line: self.input.line(),
147            prev_depth: IGNORE_DEPTH,
148            space_factor: 1000,
149            clang: 0,
150            incompleat_noad: NodeRef::NULL,
151            math_style: 0,
152            delim_ptr: NodeRef::NULL,
153        };
154        self.nest.push(cl);
155        self.mode = mode;
156    }
157
158    /// Leaves a semantic level (`pop_nest`). The list is not freed.
159    pub fn pop_nest(&mut self) {
160        if let Some(cl) = self.nest.pop() {
161            self.nodes.free(cl.head);
162        }
163        self.mode = self.cur_list().mode;
164    }
165
166    /// Appends node `p` to the current list (`tail_append`).
167    pub fn tail_append(&mut self, p: NodeRef) {
168        let tail = self.cur_list().tail;
169        self.nodes.link(tail, p);
170        self.cur_list_mut().tail = p;
171    }
172
173    /// Prints a mode name (`print_mode`).
174    pub fn print_mode(&mut self, m: Mode) {
175        let s = match m {
176            Mode::Vertical => "vertical mode",
177            Mode::Horizontal => "horizontal mode",
178            Mode::DisplayMath => "display math mode",
179            Mode::None => "no mode",
180            Mode::InternalVertical => "internal vertical mode",
181            Mode::RestrictedHorizontal => "restricted horizontal mode",
182            Mode::Math => "math mode",
183        };
184        self.printer.print_str(s);
185    }
186
187    /// The chief executive: reads and executes commands until `\end` or
188    /// `\dump` (`main_control`).
189    pub fn main_control(&mut self) {
190        let every_job = self.eqtb.toks_param(pg::assign_toks::EVERYJOB).clone();
191        if !every_job.is_empty() {
192            self.begin_token_list(every_job, TokKind::EveryJobText);
193        }
194        self.main_loop();
195    }
196
197    /// Goes on with the outermost main loop where it stopped (see
198    /// [`Engine::resume_run`]).
199    pub(crate) fn resume_main_loop(&mut self) {
200        self.main_loop();
201    }
202
203    /// Executes `list` in a nested main loop until it is used up (LuaTeX's
204    /// local control, used by `tex.runtoks`). Lua output pushed before the
205    /// call is read first.
206    pub fn run_local(&mut self, list: Option<crate::token::TokenList>) {
207        let end: crate::token::TokenList = Rc::from(vec![Token::cs(self.cs.end_local)]);
208        // The end marker goes below the material to execute.
209        let depth = self.input.stack.len();
210        self.begin_token_list(end, TokKind::Inserted);
211        if let Some(l) = list {
212            self.begin_token_list(l, TokKind::Inserted);
213        }
214        let _ = depth;
215        self.local_level += 1;
216        self.main_loop();
217        self.local_level -= 1;
218    }
219
220    /// Like [`run_local`](Self::run_local), but the material is whatever
221    /// is already on top of the input stack above the marker inserted
222    /// here. Returns the depth at which the marker was placed.
223    pub fn push_local_marker(&mut self) {
224        let end: crate::token::TokenList = Rc::from(vec![Token::cs(self.cs.end_local)]);
225        self.begin_token_list(end, TokKind::Inserted);
226    }
227
228    /// LuaTeX's `token.scan_list`: scans a box specification and returns
229    /// the box (null if there was none), running the commands of an
230    /// `\hbox{…}` or `\vbox{…}` in local control.
231    pub fn scan_list(&mut self) -> NodeRef {
232        self.lua_scanned_box = None;
233        let level = self.save.cur_level;
234        self.scan_box(BoxContext::Lua);
235        if self.lua_scanned_box.is_none() && self.save.cur_level > level {
236            let waiting = std::mem::replace(&mut self.lua_scan_waiting, true);
237            self.run_local_loop();
238            self.lua_scan_waiting = waiting;
239        }
240        self.lua_scanned_box.take().unwrap_or(NodeRef::NULL)
241    }
242
243    /// Runs the nested main loop until the local-control marker is met.
244    pub fn run_local_loop(&mut self) {
245        self.local_level += 1;
246        self.main_loop();
247        self.local_level -= 1;
248    }
249
250    /// LuaTeX's fatal error for a node freed twice (Lua code can put a
251    /// node in two lists, or link a list into a loop).
252    #[inline]
253    pub fn check_double_free(&mut self) {
254        if let Some((t, p)) = self.nodes.double_free {
255            let name = crate::nodes::type_name(t);
256            self.luatex_fatal(
257                "nodes",
258                &format!("attempt to double-free {name} node {p}, ignored"),
259            );
260        }
261    }
262
263    fn main_loop(&mut self) {
264        loop {
265            // big_switch:
266            if self.loop_hook.is_some()
267                && self.local_level == 0
268                && let Some(h) = self.loop_hook.take()
269            {
270                h(self);
271            }
272            self.profile_tick();
273            self.exec_context = true;
274            self.get_x_token();
275            self.exec_context = false;
276            loop {
277                // reswitch:
278                if self.eqtb.int(pg::assign_int::TRACINGCOMMANDS) > 0 {
279                    self.show_cur_cmd_chr();
280                }
281                match self.dispatch() {
282                    Next::BigSwitch => {
283                        self.check_double_free();
284                        break;
285                    }
286                    Next::Reswitch => continue,
287                    Next::Return => return,
288                }
289            }
290        }
291    }
292
293    /// One step of `main_control`.
294    fn dispatch(&mut self) -> Next {
295        use ModeKind as K;
296        let kind = self.mode.abs_kind();
297        let cmd = self.cur_cmd;
298        match (kind, cmd) {
299            (K::Horizontal, Cmd::Letter | Cmd::OtherChar | Cmd::CharGiven) => {
300                self.main_loop_char();
301                Next::Reswitch
302            }
303            (K::Horizontal, Cmd::CharNum) => {
304                self.scan_char_num();
305                self.cur_chr = self.cur_val as u32;
306                self.main_loop_char();
307                Next::Reswitch
308            }
309            (K::Horizontal, Cmd::Boundary) => {
310                self.append_boundary();
311                Next::BigSwitch
312            }
313            (K::Horizontal, Cmd::Spacer) => {
314                self.app_space_main();
315                Next::BigSwitch
316            }
317            (K::Horizontal | K::Math, Cmd::ExSpace) => {
318                self.app_explicit_space();
319                Next::BigSwitch
320            }
321            // Observed: in local control (`tex.runtoks`), a space in
322            // vertical mode appends an interword space, as in horizontal
323            // mode.
324            (K::Vertical, Cmd::Spacer) if self.local_level > 0 => {
325                self.append_normal_space();
326                Next::BigSwitch
327            }
328            (_, Cmd::Relax) | (K::Vertical | K::Math, Cmd::Spacer) => Next::BigSwitch,
329            (K::Math, Cmd::Boundary) => Next::BigSwitch,
330            (_, Cmd::IgnoreSpaces) => {
331                if self.cur_chr == 0 {
332                    // What a macro expands to here is executed by main
333                    // control (`goto reswitch`), as after the big switch.
334                    self.exec_context = true;
335                    self.get_nonblank_noncall();
336                    self.exec_context = false;
337                    Next::Reswitch
338                } else {
339                    // LuaTeX's \nospaces etc. are parameters; nothing to do.
340                    Next::BigSwitch
341                }
342            }
343            (K::Vertical, Cmd::Stop) => {
344                if self.its_all_over() {
345                    Next::Return
346                } else {
347                    Next::BigSwitch
348                }
349            }
350            (K::Vertical, Cmd::Vmove)
351            | (K::Horizontal | K::Math, Cmd::Hmove)
352            | (_, Cmd::LastItem)
353            | (_, Cmd::MacParam) => {
354                self.report_illegal_case();
355                Next::BigSwitch
356            }
357            (_, c) if c > Cmd::MAX_NON_PREFIXED => {
358                self.prefixed_command();
359                Next::BigSwitch
360            }
361            (_, Cmd::BeginGroup) => {
362                self.new_save_level(GroupCode::SemiSimple);
363                self.reset_local_counts(true);
364                Next::BigSwitch
365            }
366            (_, Cmd::EndGroup) => {
367                if self.save.cur_group == GroupCode::SemiSimple {
368                    self.end_group_text_dirs();
369                    self.unsave();
370                } else {
371                    self.off_save();
372                }
373                Next::BigSwitch
374            }
375            (_, Cmd::LeftBrace) if kind != K::Math => {
376                self.new_save_level(GroupCode::Simple);
377                self.reset_local_counts(true);
378                Next::BigSwitch
379            }
380            (_, Cmd::RightBrace) => {
381                self.handle_right_brace();
382                Next::BigSwitch
383            }
384            (_, Cmd::AfterAssignment) => {
385                self.get_token();
386                self.after_token = self.cur_tok;
387                Next::BigSwitch
388            }
389            (_, Cmd::AfterGroup) => {
390                self.get_token();
391                self.save_for_after(self.cur_tok);
392                Next::BigSwitch
393            }
394            (_, Cmd::InStream) => {
395                self.open_or_close_in();
396                Next::BigSwitch
397            }
398            (_, Cmd::Message) => {
399                self.issue_message();
400                Next::BigSwitch
401            }
402            (_, Cmd::CaseShift) => {
403                self.shift_case();
404                Next::BigSwitch
405            }
406            (_, Cmd::Xray) => {
407                self.show_whatever();
408                Next::BigSwitch
409            }
410            (_, Cmd::Extension)
411                if self.cur_chr == crate::misc::ext::END_LOCAL_CONTROL && self.local_level > 0 =>
412            {
413                Next::Return
414            }
415            (_, Cmd::Extension) => {
416                self.do_extension();
417                Next::BigSwitch
418            }
419            (_, Cmd::Normal) => {
420                self.do_normal();
421                Next::BigSwitch
422            }
423            (_, Cmd::LuaFunctionCall | Cmd::LuaBytecodeCall | Cmd::LuaCall) => {
424                let n = if self.cur_cmd == Cmd::LuaFunctionCall
425                    || self.cur_cmd == Cmd::LuaBytecodeCall
426                {
427                    self.scan_int();
428                    self.cur_val
429                } else {
430                    self.cur_chr as i32
431                };
432                if self.cur_cmd == Cmd::LuaBytecodeCall {
433                    self.call_lua_bytecode(n);
434                } else {
435                    self.call_lua_function(n);
436                }
437                Next::BigSwitch
438            }
439            (_, Cmd::PartokenName) => {
440                self.partoken_name();
441                Next::BigSwitch
442            }
443            _ => {
444                self.typeset_command();
445                Next::BigSwitch
446            }
447        }
448    }
449
450    /// Reports a command that is not allowed in this mode
451    /// (`report_illegal_case`).
452    pub fn report_illegal_case(&mut self) {
453        self.you_cant();
454        self.help(&[
455            "Sorry, but I'm not programmed to handle this case;",
456            "I'll just pretend that you didn't ask for it.",
457            "If you're in the wrong mode, you might be able to",
458            "return to the right one by typing `I}' or `I$' or `I\\par'.",
459        ]);
460        self.error();
461    }
462
463    /// Prints "You can't use … in … mode" (`you_cant`).
464    pub fn you_cant(&mut self) {
465        self.print_err("You can't use `");
466        self.print_cmd_chr(self.cur_cmd, self.cur_chr);
467        self.printer.print_str("' in ");
468        self.print_mode(self.mode);
469    }
470
471    /// Whether the mode is privileged; reports an error if not
472    /// (`privileged`).
473    pub fn privileged(&mut self) -> bool {
474        if self.mode.is_privileged() {
475            true
476        } else {
477            self.report_illegal_case();
478            false
479        }
480    }
481
482    /// Inserts a missing `$` (`insert_dollar_sign`).
483    pub fn insert_dollar_sign(&mut self) {
484        self.back_input();
485        self.cur_tok = Token::char(Cmd::MathShift, '$' as u32);
486        self.print_err("Missing $ inserted");
487        self.help(&[
488            "I've inserted a begin-math/end-math symbol since I think",
489            "you left one out. Proceed, with fingers crossed.",
490        ]);
491        self.ins_error();
492    }
493
494    /// `\end` and `\dump`: finish if everything is shipped out
495    /// (`its_all_over`).
496    fn its_all_over(&mut self) -> bool {
497        if self.privileged() {
498            let cl = self.cur_list();
499            if self.page_is_empty() && cl.head == cl.tail && self.dead_cycles == 0 {
500                return true;
501            }
502            self.back_input();
503            let b = self.nodes.new_null_box(true);
504            let hsize = self.eqtb.dimen(pg::assign_dimen::HSIZE);
505            if let Some(bd) = self.nodes.box_data_mut(b) {
506                bd.width = hsize;
507            }
508            self.tail_append(b);
509            let g = self.nodes.new_glue(
510                crate::glue::Glue {
511                    stretch: crate::glue::Glue::fixed(0).stretch + retex_arith::UNITY,
512                    stretch_order: crate::glue::FILL,
513                    ..crate::glue::Glue::ZERO
514                },
515                0,
516            );
517            self.tail_append(g);
518            let p = self.nodes.new_penalty(-0o10000000000, 0);
519            self.tail_append(p);
520            self.build_page_from("end");
521        }
522        false
523    }
524
525    /// Handles a group-ending `}` (`handle_right_brace`).
526    pub fn handle_right_brace(&mut self) {
527        match self.save.cur_group {
528            GroupCode::Simple => {
529                self.end_group_text_dirs();
530                self.unsave();
531            }
532            GroupCode::BottomLevel => {
533                self.print_err("Too many }'s");
534                self.help(&[
535                    "You've closed more groups than you opened.",
536                    "Such booboos are generally harmless, so keep going.",
537                ]);
538                self.error();
539            }
540            GroupCode::SemiSimple | GroupCode::MathShift | GroupCode::MathLeft => {
541                self.extra_right_brace()
542            }
543            _ => self.handle_right_brace_typesetting(),
544        }
545    }
546
547    fn extra_right_brace(&mut self) {
548        self.print_err("Extra }, or forgotten ");
549        match self.save.cur_group {
550            GroupCode::SemiSimple => self.print_esc_str("endgroup"),
551            GroupCode::MathShift => self.printer.print_char('$' as u32),
552            _ => self.print_esc_str("right"),
553        }
554        self.help(&[
555            "I've deleted a group-closing symbol because it seems to be",
556            "spurious, as in `$x}$'. But perhaps the } is legitimate and",
557            "you forgot something else, as in `\\hbox{$x}'. In such cases",
558            "the way to recover is to insert both the forgotten and the",
559            "deleted material, e.g., by typing `I$}'.",
560        ]);
561        self.error();
562        self.input.align_state += 1;
563    }
564
565    /// Recovers from a mismatched group end (`off_save`, §1064).
566    pub fn off_save(&mut self) {
567        if self.save.cur_group == GroupCode::BottomLevel {
568            // Drop current token and complain that it was unmatched.
569            self.print_err("Extra ");
570            self.print_cmd_chr(self.cur_cmd, self.cur_chr);
571            self.help(&["Things are pretty mixed up, but I think the worst is over."]);
572            self.error();
573            return;
574        }
575        self.back_input();
576        let mut list: Vec<Token> = Vec::new();
577        self.print_err("Missing ");
578        match self.save.cur_group {
579            GroupCode::SemiSimple => {
580                list.push(Token::cs(self.cs.frozen_end_group));
581                self.print_esc_str("endgroup");
582            }
583            GroupCode::MathShift => {
584                list.push(Token::char(Cmd::MathShift, '$' as u32));
585                self.printer.print_char('$' as u32);
586            }
587            GroupCode::MathLeft => {
588                list.push(Token::cs(self.cs.frozen_right));
589                list.push(Token::other('.' as u32));
590                self.print_esc_str("right.");
591            }
592            _ => {
593                list.push(Token::RIGHT_BRACE);
594                self.printer.print_char('}' as u32);
595            }
596        }
597        self.printer.print_str(" inserted");
598        self.ins_list(Rc::from(list));
599        self.help(&[
600            "I've inserted something that you may have forgotten.",
601            "(See the <inserted text> above.)",
602            "With luck, this will get me unwedged. But if you",
603            "really didn't forget anything, try typing `2' now; then",
604            "my insertion and my current dilemma will both disappear.",
605        ]);
606        self.error();
607    }
608
609    /// `\message` and `\errmessage` (`issue_message`).
610    fn issue_message(&mut self) {
611        let c = self.cur_chr;
612        let toks = self.scan_toks_expanded();
613        let s = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), 10_000_000));
614        if c == 0 {
615            self.record_note(&String::from_utf8_lossy(&s));
616            if self.printer.term_offset + s.len() > self.printer.max_print_line.saturating_sub(2) {
617                self.printer.print_ln();
618            } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
619                self.printer.print_char(' ' as u32);
620            }
621            self.printer.print_bytes(&s);
622            self.printer.update_terminal();
623        } else {
624            self.print_err("");
625            self.printer.print_bytes(&s);
626            let err_help = self.eqtb.toks_param(pg::assign_toks::ERRHELP);
627            if !err_help.is_empty() {
628                self.err.use_err_help = true;
629            } else if self.long_help_seen {
630                self.help(&["(That was another \\errmessage.)"]);
631            } else {
632                if self.err.interaction < crate::error::Interaction::ErrorStop {
633                    self.long_help_seen = true;
634                }
635                self.help(&[
636                    "This error message was generated by an \\errmessage",
637                    "command, so I can't give any explicit help.",
638                    "Pretend that you're Hercule Poirot: Examine all clues,",
639                    "and deduce the truth by order and method.",
640                ]);
641            }
642            self.error();
643            self.err.use_err_help = false;
644        }
645    }
646
647    /// `\lowercase` and `\uppercase` (`shift_case`).
648    fn shift_case(&mut self) {
649        let b = self.cur_chr;
650        let toks = self.scan_general_text();
651        let upper = b == pg::def_char_code::UCCODE + UC_LC_OFFSET;
652        let mut out: Vec<Token> = Vec::with_capacity(toks.len());
653        for &t in toks.iter() {
654            if t.is_cs() {
655                if let crate::hash::CsKind::Active(c) = *self.hash.kind(t.cs_id()) {
656                    let m = self.case_map(c, upper);
657                    if m != 0 {
658                        out.push(Token::cs(self.hash.active(m)));
659                        continue;
660                    }
661                }
662                out.push(t);
663            } else {
664                let c = t.chr();
665                let m = self.case_map(c, upper);
666                if m != 0 {
667                    out.push(Token::char(t.cmd(), m));
668                } else {
669                    out.push(t);
670                }
671            }
672        }
673        self.back_list(Rc::from(out));
674    }
675
676    fn case_map(&self, c: u32, upper: bool) -> u32 {
677        (if upper {
678            self.eqtb.uc.get(c)
679        } else {
680            self.eqtb.lc.get(c)
681        }) as u32
682    }
683
684    /// Cleans up after `main_control` ends (`final_cleanup`).
685    pub fn final_cleanup(&mut self) {
686        if self.figures.report {
687            eprint!("{}", self.figures.summary());
688        }
689        let c = self.cur_chr;
690        if c != 1 {
691            self.eqtb.int_par[pg::assign_int::NEWLINECHAR as usize].val = -1;
692            self.printer.new_line_char = -1;
693        }
694        if self.job_name.is_none() {
695            self.open_log_file();
696        }
697        while self.input.stack.len() > 1 {
698            match self.input.stack.last() {
699                Some(Level::Tokens(_)) => self.end_token_list(),
700                _ => self.end_file_reading(),
701            }
702        }
703        while self.input.open_parens > 0 {
704            // LuaTeX prints no space before the parenthesis.
705            self.printer.print_char(')' as u32);
706            self.input.open_parens -= 1;
707        }
708        if self.save.cur_level > crate::eqtb::LEVEL_ONE {
709            self.printer.print_nl("(");
710            self.print_esc_str("end occurred ");
711            self.printer.print_str("inside a group at level ");
712            self.printer
713                .print_int(i64::from(self.save.cur_level - crate::eqtb::LEVEL_ONE));
714            self.printer.print_char(')' as u32);
715            self.show_save_groups();
716        }
717        while !self.cond.stack.is_empty() {
718            self.printer.print_nl("(");
719            self.print_esc_str("end occurred ");
720            self.printer.print_str("when ");
721            self.print_cmd_chr(Cmd::IfTest, self.cond.cur_if);
722            if self.cond.if_line != 0 {
723                self.printer.print_str(" on line ");
724                self.printer.print_int(self.cond.if_line.into());
725            }
726            self.printer.print_str(" was incomplete)");
727            self.pop_cond();
728        }
729        if self.err.history != crate::error::History::Spotless
730            && (self.err.history == crate::error::History::WarningIssued
731                || self.err.interaction < crate::error::Interaction::ErrorStop)
732            && self.printer.selector == crate::print::Selector::TermAndLog
733        {
734            self.printer.selector = crate::print::Selector::TermOnly;
735            self.printer
736                .print_nl("(see the transcript file for additional information)");
737            self.printer.selector = crate::print::Selector::TermAndLog;
738        }
739        if c == 1 {
740            if self.ini_version {
741                self.dump_requested = true;
742                return;
743            }
744            self.printer
745                .print_nl("(\\dump is performed only by INITEX)");
746        }
747    }
748
749    /// LuaTeX's `\partokenname` (pdfTeX extension): sets the name of the
750    /// paragraph-ending token.
751    fn partoken_name(&mut self) {
752        self.get_token();
753        if self.cur_cs != NO_CS {
754            self.par_token_cs = self.cur_cs;
755        }
756    }
757}
758
759/// `\lowercase` and `\uppercase` carry the `def_char_code` modifiers of
760/// `\lccode`/`\uccode` plus this offset (LuaTeX numbering).
761pub const UC_LC_OFFSET: u32 = 333_280;
762
763/// What `main_control` does next.
764pub enum Next {
765    /// Read the next token.
766    BigSwitch,
767    /// Dispatch the current token again.
768    Reswitch,
769    /// Leave `main_control`.
770    Return,
771}