Skip to main content

retex_engine/
input.rs

1//! The input stack and the tokenizer (*TeX: The Program* §300–§365).
2//!
3//! TeX reads from a stack of input levels. Each level is either a text
4//! source read line by line (a file, the terminal, a `\read` stream, a
5//! `\scantokens` pseudo-file or lines printed from Lua) or a token list (a
6//! macro body, a parameter, backed-up tokens, `\everypar`, …). `get_next`
7//! produces the next token from the top level, converting characters into
8//! tokens according to their category codes.
9
10use std::rc::Rc;
11
12use crate::Engine;
13use crate::cmd::Cmd;
14use crate::hash::{CsId, NO_CS, NULL_CS};
15use crate::token::{Token, TokenList};
16use crate::utf8x;
17
18/// The scanner state of a text level (`state`).
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum LexState {
21    /// In the middle of a line.
22    MidLine,
23    /// Skipping blanks after a control word or space.
24    SkipBlanks,
25    /// At the beginning of a line.
26    NewLine,
27}
28
29/// The kind of a token-list input level (`token_type`).
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum TokKind {
32    /// A macro parameter.
33    Parameter,
34    /// The `u` part of an alignment template.
35    UTemplate,
36    /// The `v` part of an alignment template.
37    VTemplate,
38    /// Tokens backed up by `back_input`.
39    BackedUp,
40    /// A backed-up character (LuaTeX distinguishes it in context lines).
41    BackedUpChar,
42    /// Tokens inserted during error recovery or by TeX itself.
43    Inserted,
44    /// A macro body.
45    Macro,
46    /// `\output`.
47    OutputText,
48    /// `\everypar`.
49    EveryParText,
50    /// `\everymath`.
51    EveryMathText,
52    /// `\everydisplay`.
53    EveryDisplayText,
54    /// `\everyhbox`.
55    EveryHboxText,
56    /// `\everyvbox`.
57    EveryVboxText,
58    /// `\everyjob`.
59    EveryJobText,
60    /// `\everycr`.
61    EveryCrText,
62    /// A `\mark` text.
63    MarkText,
64    /// `\everyeof`.
65    EveryEofText,
66    /// A `\write` text.
67    WriteText,
68    /// Tokens produced by Lua (`token.put_next`, `tex.runtoks`, …).
69    LocalText,
70}
71
72/// The category-code regime of a line printed from Lua.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum CatMode {
75    /// The current catcode table.
76    Current,
77    /// A specific `\catcodetable`.
78    Table(u32),
79    /// `-2`: everything other (12) except space (10), as `\the\toks`.
80    String,
81    /// All characters get this category (`tex.cprint`, `tex.write` uses
82    /// [`CatMode::String`]).
83    Single(u8),
84}
85
86/// How a line printed from Lua is fed to the scanner.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum LuaLineKind {
89    /// `tex.print`: a full line with `\endlinechar`.
90    Print,
91    /// `tex.sprint`: a partial line, no `\endlinechar`, mid-line state.
92    Sprint,
93}
94
95/// One item printed from Lua during a `\directlua` call.
96#[derive(Clone, Debug)]
97pub enum LuaItem {
98    /// A line of text.
99    Text {
100        /// The bytes (UTF-8).
101        text: Vec<u8>,
102        /// How it is scanned.
103        kind: LuaLineKind,
104        /// Its catcode regime.
105        catmode: CatMode,
106    },
107    /// Tokens injected directly.
108    Tokens(Vec<Token>),
109}
110
111/// Where the lines of a text level come from.
112#[derive(Clone, Debug)]
113pub enum LineSource {
114    /// A file held in memory, with the byte offset of the next line.
115    File {
116        /// The file content.
117        data: Rc<[u8]>,
118        /// Offset of the next unread line.
119        pos: usize,
120    },
121    /// The terminal (or the command line for the first line).
122    Terminal,
123    /// A `\read` stream: the level holds one line only.
124    Read(u32),
125    /// Lines of a `\scantokens` pseudo-file or of Lua `tex.print`.
126    Lines {
127        /// The remaining lines.
128        lines: std::collections::VecDeque<Vec<u8>>,
129        /// For Lua lines: the catcode table to use, if any.
130        catcodes: Option<u32>,
131        /// Whether this is a pseudo-file (`\scantokens`) that counts lines.
132        pseudo: bool,
133    },
134    /// Output of `tex.print` and friends.
135    Lua(std::collections::VecDeque<LuaItem>),
136}
137
138/// A text input level.
139#[derive(Clone, Debug)]
140pub struct TextLevel {
141    /// The scanner state.
142    pub state: LexState,
143    /// The characters of the current line, including the end-of-line
144    /// character if one was appended.
145    pub buf: Vec<u32>,
146    /// Index of the next character in `buf`.
147    pub loc: usize,
148    /// Where further lines come from.
149    pub source: LineSource,
150    /// The current line number in this source.
151    pub line: i32,
152    /// The printable name of the file (for context lines and `(` … `)`).
153    pub name: Vec<u8>,
154    /// Whether this level was opened by `\input` (prints parentheses).
155    pub is_input_file: bool,
156    /// Whether `\everyeof` has already been inserted for this file.
157    pub eof_seen: bool,
158    /// The group level when the file was opened (e-TeX `grp_stack`).
159    pub grp_boundary: usize,
160    /// The conditional depth when the file was opened (e-TeX `if_stack`).
161    pub if_depth: usize,
162    /// The catcode regime of the current line.
163    pub catmode: CatMode,
164}
165
166/// A token-list input level.
167#[derive(Clone, Debug)]
168pub struct TokLevel {
169    /// The tokens.
170    pub list: TokenList,
171    /// Index of the next token.
172    pub loc: usize,
173    /// The kind of list.
174    pub kind: TokKind,
175    /// Start of this macro's parameters in the parameter stack.
176    pub param_start: usize,
177    /// For macros: the control sequence being expanded.
178    pub name: CsId,
179}
180
181/// An input level.
182#[derive(Clone, Debug)]
183pub enum Level {
184    /// Reading characters from lines of text.
185    Text(Box<TextLevel>),
186    /// Reading a token list.
187    Tokens(TokLevel),
188}
189
190/// What the scanner is doing, for runaway diagnostics (`scanner_status`).
191#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
192pub enum ScannerStatus {
193    /// Nothing special.
194    Normal,
195    /// Skipping conditional text.
196    Skipping,
197    /// Scanning a macro definition.
198    Defining,
199    /// Scanning macro arguments.
200    Matching,
201    /// Scanning an alignment preamble.
202    Aligning,
203    /// Scanning a balanced text (`\message`, `\write`, …).
204    Absorbing,
205}
206
207/// The input stack.
208#[derive(Debug)]
209pub struct InputState {
210    /// All levels; the last one is the current input (`cur_input`).
211    pub stack: Vec<Level>,
212    /// Arguments of macros being expanded.
213    pub param_stack: Vec<TokenList>,
214    /// Brace balance for alignments (`align_state`).
215    pub align_state: i32,
216    /// What the scanner is doing.
217    pub scanner_status: ScannerStatus,
218    /// The macro or definition being scanned, for diagnostics.
219    pub warning_index: CsId,
220    /// Number of open parentheses printed for input files.
221    pub open_parens: usize,
222    /// Whether `\endinput` was seen in the current file.
223    pub force_eof: bool,
224    /// Line number of the most recent `\read` line, etc.
225    pub last_line: i32,
226}
227
228impl Default for InputState {
229    fn default() -> Self {
230        InputState {
231            stack: Vec::new(),
232            param_stack: Vec::new(),
233            align_state: 1_000_000,
234            scanner_status: ScannerStatus::Normal,
235            warning_index: NO_CS,
236            open_parens: 0,
237            force_eof: false,
238            last_line: 0,
239        }
240    }
241}
242
243/// `no_expand_flag`: the modifier of the special `\relax` produced by
244/// `\noexpand` (TeX uses 257; LuaTeX uses a value outside the char range).
245pub const NO_EXPAND_FLAG: u32 = 0x11_0001;
246
247impl InputState {
248    /// Number of text levels that are input files (TeX's `in_open`).
249    pub fn in_open(&self) -> usize {
250        self.stack
251            .iter()
252            .filter(|l| matches!(l, Level::Text(t) if t.is_input_file))
253            .count()
254    }
255
256    /// The innermost text level, if any.
257    pub fn cur_text(&self) -> Option<&TextLevel> {
258        self.stack.iter().rev().find_map(|l| match l {
259            Level::Text(t) => Some(&**t),
260            Level::Tokens(_) => None,
261        })
262    }
263
264    /// The current line number (`line`): the line of the innermost file.
265    pub fn line(&self) -> i32 {
266        self.stack
267            .iter()
268            .rev()
269            .find_map(|l| match l {
270                Level::Text(t) if !matches!(t.source, LineSource::Read(_)) => Some(t.line),
271                _ => None,
272            })
273            .unwrap_or(0)
274    }
275
276    /// Whether the current level is a token list.
277    pub fn in_token_list(&self) -> bool {
278        matches!(self.stack.last(), Some(Level::Tokens(_)))
279    }
280
281    /// Whether we are reading from the terminal (`terminal_input`).
282    pub fn terminal_input(&self) -> bool {
283        matches!(self.stack.last(), Some(Level::Text(t)) if matches!(t.source, LineSource::Terminal))
284    }
285}
286
287/// Splits the next line off `data[pos..]`. Lines end at LF, CR or CR LF.
288/// Returns the line bytes and the new position, or `None` at end of data.
289pub fn next_line(data: &[u8], pos: usize) -> Option<(&[u8], usize)> {
290    if pos >= data.len() {
291        return None;
292    }
293    let rest = &data[pos..];
294    match rest.iter().position(|&b| b == b'\n' || b == b'\r') {
295        Some(i) => {
296            let mut next = pos + i + 1;
297            if rest[i] == b'\r' && rest.get(i + 1) == Some(&b'\n') {
298                next += 1;
299            }
300            Some((&rest[..i], next))
301        }
302        None => Some((rest, data.len())),
303    }
304}
305
306/// Decodes a line of input into characters, removing trailing spaces
307/// (TeX's `input_ln`; tabs are kept, as in LuaTeX).
308pub fn decode_line(bytes: &[u8]) -> Vec<u32> {
309    let mut end = bytes.len();
310    while end > 0 && bytes[end - 1] == b' ' {
311        end -= 1;
312    }
313    utf8x::chars(&bytes[..end])
314}
315
316fn is_hex(c: u32) -> bool {
317    matches!(c, 0x30..=0x39 | 0x61..=0x66)
318}
319
320fn hex_val(c: u32) -> u32 {
321    if c <= 0x39 { c - 0x30 } else { c - 0x61 + 10 }
322}
323
324/// Result of looking for a `^^` sequence at a position.
325enum Expanded {
326    /// No expanded character here.
327    None,
328    /// An expanded character of `len` input characters.
329    Char { c: u32, len: usize },
330    /// A malformed `^^^^`/`^^^^^^` sequence.
331    Bad { six: bool },
332}
333
334/// Examines `buf[k..]` for an expanded character introduced by the
335/// superscript character `sup` at `buf[k]` (the caller checks its catcode).
336fn expanded_at(buf: &[u32], k: usize) -> Expanded {
337    let sup = buf[k];
338    if buf.get(k + 1) != Some(&sup) {
339        return Expanded::None;
340    }
341    // LuaTeX: ^^^^^^ + six hex digits, or ^^^^ + four hex digits.
342    let run = buf[k..].iter().take(6).take_while(|&&c| c == sup).count();
343    if run >= 4 {
344        let six = run == 6;
345        let n = if six { 6 } else { 4 };
346        let start = k + run;
347        let digits = buf.get(start..start + n);
348        return match digits {
349            Some(d) if d.iter().all(|&c| is_hex(c)) => {
350                let c = d.iter().fold(0u32, |acc, &c| acc * 16 + hex_val(c));
351                if c > 0x10_FFFF {
352                    Expanded::Bad { six }
353                } else {
354                    Expanded::Char { c, len: run + n }
355                }
356            }
357            _ => Expanded::Bad { six },
358        };
359    }
360    // Classic TeX ^^ notation.
361    let Some(&c) = buf.get(k + 2) else {
362        return Expanded::None;
363    };
364    if c >= 0o200 {
365        return Expanded::None;
366    }
367    if is_hex(c)
368        && let Some(&cc) = buf.get(k + 3)
369        && is_hex(cc)
370    {
371        return Expanded::Char {
372            c: hex_val(c) * 16 + hex_val(cc),
373            len: 4,
374        };
375    }
376    Expanded::Char {
377        c: if c < 0o100 { c + 0o100 } else { c - 0o100 },
378        len: 3,
379    }
380}
381
382impl Engine {
383    // ----- input levels ---------------------------------------------------
384
385    /// Pushes a token list onto the input stack (`begin_token_list`).
386    pub fn begin_token_list(&mut self, list: TokenList, kind: TokKind) {
387        self.input.stack.push(Level::Tokens(TokLevel {
388            list,
389            loc: 0,
390            kind,
391            param_start: self.input.param_stack.len(),
392            name: NO_CS,
393        }));
394    }
395
396    /// Pushes a macro body with its parameters (already on the parameter
397    /// stack starting at `param_start`).
398    pub fn begin_macro(&mut self, list: TokenList, start: usize, name: CsId, param_start: usize) {
399        self.input.stack.push(Level::Tokens(TokLevel {
400            list,
401            loc: start,
402            kind: TokKind::Macro,
403            param_start,
404            name,
405        }));
406    }
407
408    /// Inserts a token list for immediate reading (`ins_list`).
409    pub fn ins_list(&mut self, list: TokenList) {
410        self.begin_token_list(list, TokKind::Inserted);
411    }
412
413    /// Pops the current token-list level (`end_token_list`).
414    pub fn end_token_list(&mut self) {
415        if let Some(Level::Tokens(t)) = self.input.stack.pop() {
416            match t.kind {
417                TokKind::Macro => {
418                    self.input.param_stack.truncate(t.param_start);
419                }
420                TokKind::UTemplate => {
421                    if self.input.align_state > 500_000 {
422                        self.input.align_state = 0;
423                    } else {
424                        self.fatal_error("(interwoven alignment preambles are not allowed)");
425                    }
426                }
427                _ => {}
428            }
429        } else {
430            debug_assert!(false, "end_token_list on a text level");
431        }
432        self.check_interrupt();
433    }
434
435    /// Puts the current token back into the input (`back_input`).
436    pub fn back_input(&mut self) {
437        // Pop exhausted token lists first (but not v templates).
438        while let Some(Level::Tokens(t)) = self.input.stack.last() {
439            if t.loc < t.list.len() || t.kind == TokKind::VTemplate {
440                break;
441            }
442            self.end_token_list();
443        }
444        let tok = self.cur_tok;
445        if tok.0 < crate::token::CS_TOKEN_FLAG {
446            let cmd = tok.cmd_code();
447            if cmd == Cmd::RightBrace as u32 {
448                self.input.align_state += 1;
449            } else if cmd == Cmd::LeftBrace as u32 {
450                self.input.align_state -= 1;
451            }
452        }
453        let list: TokenList = Rc::from(vec![tok]);
454        self.input.stack.push(Level::Tokens(TokLevel {
455            list,
456            loc: 0,
457            kind: TokKind::BackedUp,
458            param_start: self.input.param_stack.len(),
459            name: NO_CS,
460        }));
461    }
462
463    /// Backs up the current token and marks it as inserted
464    /// (`back_error` uses this through `ins_error`).
465    pub fn back_input_inserted(&mut self) {
466        self.back_input();
467        if let Some(Level::Tokens(t)) = self.input.stack.last_mut() {
468            t.kind = TokKind::Inserted;
469        }
470    }
471
472    /// Pushes a text level reading `source`.
473    pub fn begin_file_reading(&mut self, source: LineSource, name: Vec<u8>, is_input_file: bool) {
474        self.input.stack.push(Level::Text(Box::new(TextLevel {
475            state: LexState::NewLine,
476            buf: Vec::new(),
477            loc: 0,
478            source,
479            line: 0,
480            name,
481            is_input_file,
482            eof_seen: false,
483            grp_boundary: self.save.cur_boundary,
484            if_depth: self.cond.stack.len(),
485            catmode: CatMode::Current,
486        })));
487    }
488
489    /// Pops the current text level (`end_file_reading`).
490    pub fn end_file_reading(&mut self) {
491        let lvl = self.input.stack.pop();
492        if let Some(Level::Text(t)) = lvl
493            && let LineSource::Read(n) = t.source
494        {
495            let _ = n;
496        }
497    }
498
499    /// Reads the next line of the current text level into its buffer,
500    /// appending `\endlinechar`. Returns `false` at end of source.
501    fn read_next_line(&mut self) -> bool {
502        let end_line_char = self
503            .eqtb
504            .int(crate::primitives_gen::assign_int::ENDLINECHAR);
505        let Some(Level::Text(t)) = self.input.stack.last_mut() else {
506            return false;
507        };
508        // \scantextokens text gets no \endlinechar after its last line.
509        let mut eol = true;
510        let line: Vec<u32> = match &mut t.source {
511            LineSource::File { data, pos } => match next_line(data, *pos) {
512                Some((bytes, next)) => {
513                    let l = decode_line(bytes);
514                    *pos = next;
515                    l
516                }
517                None => return false,
518            },
519            LineSource::Lines { lines, pseudo, .. } => match lines.pop_front() {
520                Some(bytes) => {
521                    eol = *pseudo || !lines.is_empty();
522                    decode_line(&bytes)
523                }
524                None => return false,
525            },
526            LineSource::Lua(items) => {
527                let last = items.len() == 1;
528                match items.pop_front() {
529                    Some(LuaItem::Text {
530                        text,
531                        kind,
532                        catmode,
533                    }) => {
534                        t.catmode = catmode;
535                        t.loc = 0;
536                        if kind == LuaLineKind::Sprint {
537                            t.buf = utf8x::chars(&text);
538                            t.state = LexState::MidLine;
539                            return true;
540                        }
541                        t.buf = decode_line(&text);
542                        t.state = LexState::NewLine;
543                        if !last && (0..=0x10_FFFF).contains(&end_line_char) {
544                            t.buf.push(end_line_char as u32);
545                        }
546                        return true;
547                    }
548                    Some(LuaItem::Tokens(toks)) => {
549                        t.buf.clear();
550                        t.loc = 0;
551                        self.pending_lua_tokens = Some(toks);
552                        return true;
553                    }
554                    None => return false,
555                }
556            }
557            LineSource::Terminal | LineSource::Read(_) => return false,
558        };
559        t.buf = line;
560        if eol && (0..=0x10_FFFF).contains(&end_line_char) {
561            t.buf.push(end_line_char as u32);
562        }
563        t.loc = 0;
564        t.line += 1;
565        true
566    }
567
568    // ----- the tokenizer ---------------------------------------------------
569
570    /// Sets `cur_cmd`, `cur_chr` and `cur_cs` to the next token
571    /// (`get_next`).
572    #[inline]
573    pub fn get_next(&mut self) {
574        // Fast path: the next token of a token list, when it needs none of
575        // the special handling below (parameters, the end of the list,
576        // outer macros, \noexpand markers, alignment entries).
577        if let Some(Level::Tokens(t)) = self.input.stack.last_mut()
578            && let Some(&tok) = t.list.get(t.loc)
579        {
580            if tok.is_cs() {
581                let cs = tok.cs_id();
582                let m = self.eqtb.meaning(cs);
583                if m.cmd < Cmd::TabMark || (m.cmd > Cmd::CarRet && !m.cmd.is_outer_or_special()) {
584                    t.loc += 1;
585                    self.cur_cs = cs;
586                    self.cur_cmd = m.cmd;
587                    self.cur_chr = m.chr;
588                    return;
589                }
590            } else {
591                let cmd = tok.cmd();
592                if cmd > Cmd::CarRet || cmd < Cmd::MathShift {
593                    t.loc += 1;
594                    match cmd {
595                        Cmd::LeftBrace => self.input.align_state += 1,
596                        Cmd::RightBrace => self.input.align_state -= 1,
597                        _ => {}
598                    }
599                    self.cur_cs = NO_CS;
600                    self.cur_cmd = cmd;
601                    self.cur_chr = tok.chr();
602                    return;
603                }
604            }
605        }
606        self.get_next_slow();
607    }
608
609    /// [`get_next`](Self::get_next) for everything but the fast path.
610    #[inline(never)]
611    fn get_next_slow(&mut self) {
612        'restart: loop {
613            self.cur_cs = NO_CS;
614            let Some(top) = self.input.stack.last_mut() else {
615                // Nothing left to read: behave like the end of the terminal.
616                self.cur_cmd = Cmd::Relax;
617                self.cur_chr = 0;
618                self.fatal_error("*** (job aborted, no legal \\end found)");
619            };
620            match top {
621                Level::Tokens(t) => {
622                    if t.loc < t.list.len() {
623                        let tok = t.list[t.loc];
624                        t.loc += 1;
625                        if tok.is_cs() {
626                            let cs = tok.cs_id();
627                            self.cur_cs = cs;
628                            let m = self.eqtb.meaning(cs);
629                            self.cur_cmd = m.cmd;
630                            self.cur_chr = m.chr;
631                            if self.cur_cmd.is_outer_or_special() {
632                                if self.cur_cmd == Cmd::DontExpand {
633                                    // The next token is a cs whose expansion
634                                    // is suppressed.
635                                    let next = t.list[t.loc];
636                                    t.loc = t.list.len();
637                                    let cs = next.cs_id();
638                                    self.cur_cs = cs;
639                                    let m = self.eqtb.meaning(cs);
640                                    self.cur_cmd = m.cmd;
641                                    self.cur_chr = m.chr;
642                                    if self.cur_cmd > Cmd::MAX_COMMAND {
643                                        self.cur_cmd = Cmd::Relax;
644                                        self.cur_chr = NO_EXPAND_FLAG;
645                                    }
646                                } else {
647                                    self.check_outer_validity();
648                                }
649                            }
650                        } else {
651                            self.cur_cmd = tok.cmd();
652                            self.cur_chr = tok.chr();
653                            match self.cur_cmd {
654                                Cmd::LeftBrace => self.input.align_state += 1,
655                                Cmd::RightBrace => self.input.align_state -= 1,
656                                Cmd::CarRet => {
657                                    // out_param: insert the macro parameter.
658                                    let p = self.input.param_stack
659                                        [t.param_start + self.cur_chr as usize - 1]
660                                        .clone();
661                                    self.begin_token_list(p, TokKind::Parameter);
662                                    continue 'restart;
663                                }
664                                _ => {}
665                            }
666                        }
667                    } else {
668                        self.end_token_list();
669                        continue 'restart;
670                    }
671                }
672                Level::Text(_) => {
673                    if !self.get_next_from_text() {
674                        continue 'restart;
675                    }
676                }
677            }
678            // If an alignment entry has just ended, take appropriate action.
679            if self.cur_cmd <= Cmd::CarRet
680                && self.cur_cmd >= Cmd::TabMark
681                && self.input.align_state == 0
682            {
683                self.insert_v_template();
684                continue 'restart;
685            }
686            return;
687        }
688    }
689
690    /// Tokenizes from the current text level. Returns `false` if the caller
691    /// must restart (a level was popped or an error occurred).
692    fn get_next_from_text(&mut self) -> bool {
693        loop {
694            // `switch`: get the next character, reading a new line if needed.
695            let (c, catmode, state) = {
696                let Some(Level::Text(t)) = self.input.stack.last_mut() else {
697                    return false;
698                };
699                if t.loc < t.buf.len() {
700                    let c = t.buf[t.loc];
701                    t.loc += 1;
702                    (c, t.catmode, t.state)
703                } else {
704                    if matches!(t.source, LineSource::Read(_)) {
705                        // End of a \read line: TeX's null token
706                        // (`cur_cmd = cur_chr = 0`) tells `read_toks`.
707                        self.cur_cmd = Cmd::Relax;
708                        self.cur_chr = 0;
709                        return true;
710                    }
711                    if !self.next_line_or_pop() {
712                        return false;
713                    }
714                    continue;
715                }
716            };
717            self.cur_chr = c;
718            // `reswitch`
719            let mut c = c;
720            loop {
721                let cat = self.catcode_in_mode(c, catmode);
722                self.cur_cmd = Cmd::from_u8(cat);
723                match (cat, state) {
724                    // Ignored characters, and spaces at the start of lines or
725                    // after control words.
726                    (9, _) | (10, LexState::SkipBlanks | LexState::NewLine) => break,
727                    (0, _) => {
728                        self.scan_control_sequence();
729                        return true;
730                    }
731                    (13, _) => {
732                        let cs = self.hash.active(c);
733                        self.cur_cs = cs;
734                        let m = self.eqtb.meaning(cs);
735                        self.cur_cmd = m.cmd;
736                        self.cur_chr = m.chr;
737                        self.set_state(LexState::MidLine);
738                        if self.cur_cmd.is_outer_or_special() {
739                            self.check_outer_validity();
740                        }
741                        return true;
742                    }
743                    (7, _) => {
744                        match self.expanded_in_line() {
745                            Expanded::Char { c: nc, len } => {
746                                if let Some(Level::Text(t)) = self.input.stack.last_mut() {
747                                    t.loc += len - 1;
748                                }
749                                c = nc;
750                                self.cur_chr = c;
751                                continue; // reswitch
752                            }
753                            Expanded::Bad { six } => {
754                                self.print_err(if six {
755                                    "^^^^^^ needs six hex digits"
756                                } else {
757                                    "^^^^ needs four hex digits"
758                                });
759                                self.help(&[]);
760                                self.error();
761                                self.set_state(LexState::MidLine);
762                                return true;
763                            }
764                            Expanded::None => {
765                                self.set_state(LexState::MidLine);
766                                return true;
767                            }
768                        }
769                    }
770                    (15, _) => {
771                        self.print_err("Text line contains an invalid character");
772                        self.help(&[
773                            "A funny symbol that I can't read has just been input.",
774                            "Continue, and I'll forget that it ever happened.",
775                        ]);
776                        self.err.deletions_allowed = false;
777                        self.error();
778                        self.err.deletions_allowed = true;
779                        return false;
780                    }
781                    (10, LexState::MidLine) => {
782                        self.set_state(LexState::SkipBlanks);
783                        self.cur_chr = ' ' as u32;
784                        return true;
785                    }
786                    (5, LexState::MidLine) => {
787                        self.finish_line();
788                        self.cur_cmd = Cmd::Spacer;
789                        self.cur_chr = ' ' as u32;
790                        return true;
791                    }
792                    (5, LexState::SkipBlanks) | (14, _) => {
793                        self.finish_line();
794                        break;
795                    }
796                    (5, LexState::NewLine) => {
797                        self.finish_line();
798                        let par = self.cs.par;
799                        self.cur_cs = par;
800                        let m = self.eqtb.meaning(par);
801                        self.cur_cmd = m.cmd;
802                        self.cur_chr = m.chr;
803                        if self.cur_cmd.is_outer_or_special() {
804                            self.check_outer_validity();
805                        }
806                        return true;
807                    }
808                    (1, s) => {
809                        if s != LexState::MidLine {
810                            self.set_state(LexState::MidLine);
811                        }
812                        self.input.align_state += 1;
813                        return true;
814                    }
815                    (2, s) => {
816                        if s != LexState::MidLine {
817                            self.set_state(LexState::MidLine);
818                        }
819                        self.input.align_state -= 1;
820                        return true;
821                    }
822                    (_, s) => {
823                        if s != LexState::MidLine {
824                            self.set_state(LexState::MidLine);
825                        }
826                        return true;
827                    }
828                }
829            }
830        }
831    }
832
833    /// The category code of `c` under a line's catcode regime.
834    #[inline]
835    pub fn catcode_in_mode(&self, c: u32, mode: CatMode) -> u8 {
836        match mode {
837            CatMode::Current => self.eqtb.catcode(c),
838            CatMode::Table(t) => self.eqtb.catcode_in(t, c),
839            CatMode::String => {
840                if c == ' ' as u32 {
841                    10
842                } else {
843                    12
844                }
845            }
846            CatMode::Single(k) => k,
847        }
848    }
849
850    /// Pushes the output collected from a Lua call as a new input level.
851    pub fn push_lua_output(&mut self, items: Vec<LuaItem>) {
852        if items.is_empty() {
853            return;
854        }
855        self.begin_file_reading(
856            LineSource::Lua(items.into_iter().collect()),
857            Vec::new(),
858            false,
859        );
860        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
861            t.line = self.input_line_for_lua;
862        }
863    }
864
865    fn set_state(&mut self, s: LexState) {
866        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
867            t.state = s;
868        }
869    }
870
871    fn finish_line(&mut self) {
872        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
873            t.loc = t.buf.len();
874        }
875    }
876
877    /// Checks for an expanded character at the character just read.
878    fn expanded_in_line(&self) -> Expanded {
879        let Some(Level::Text(t)) = self.input.stack.last() else {
880            return Expanded::None;
881        };
882        expanded_at(&t.buf, t.loc - 1)
883    }
884
885    /// Moves to the next line of the current text level, or pops the level
886    /// at its end (TeX §360–§362). Returns `false` when the caller must
887    /// restart from the top of `get_next`.
888    fn next_line_or_pop(&mut self) -> bool {
889        let (is_terminal, is_read, is_file) = match self.input.stack.last() {
890            Some(Level::Text(t)) => (
891                matches!(t.source, LineSource::Terminal),
892                matches!(t.source, LineSource::Read(_)),
893                t.is_input_file,
894            ),
895            _ => return false,
896        };
897        if is_read {
898            return false;
899        }
900        if is_terminal {
901            // The command line (or terminal input) is exhausted.
902            if self.input.stack.len() > 1 {
903                // Text inserted during error recovery.
904                self.end_file_reading();
905                return false;
906            }
907            self.terminal_exhausted();
908            return false;
909        }
910        let is_lua = matches!(
911            self.input.stack.last(),
912            Some(Level::Text(t)) if matches!(t.source, LineSource::Lua(_))
913        );
914        if !self.input.force_eof && self.read_next_line() {
915            if let Some(toks) = self.pending_lua_tokens.take() {
916                self.begin_token_list(std::rc::Rc::from(toks), TokKind::Inserted);
917                return false;
918            }
919            if !is_lua {
920                self.set_state(LexState::NewLine);
921            }
922            return true;
923        }
924        // End of file (or \endinput).
925        let is_pseudo = matches!(
926            self.input.stack.last(),
927            Some(Level::Text(t)) if matches!(t.source, LineSource::Lines { pseudo: true, .. })
928        );
929        if !self.input.force_eof && (is_file || is_pseudo) {
930            // e-TeX: insert \everyeof once, for files and \scantokens
931            // pseudo-files.
932            let every_eof = self
933                .eqtb
934                .toks_param(crate::primitives_gen::assign_toks::EVERYEOF)
935                .clone();
936            let seen = match self.input.stack.last_mut() {
937                Some(Level::Text(t)) => std::mem::replace(&mut t.eof_seen, true),
938                _ => true,
939            };
940            if !seen && !every_eof.is_empty() {
941                if let Some(Level::Text(t)) = self.input.stack.last_mut() {
942                    t.buf.clear();
943                    t.loc = 0;
944                }
945                self.begin_token_list(every_eof, TokKind::EveryEofText);
946                return false;
947            }
948        }
949        if is_file {
950            self.printer.print_char(')' as u32);
951            self.input.open_parens -= 1;
952            self.printer.update_terminal();
953            self.file_closed_checks();
954        }
955        self.input.force_eof = false;
956        // Output printed from Lua and \scantextokens text may end anywhere;
957        // only files and \scantokens pseudo-files are checked.
958        let check = match self.input.stack.last() {
959            Some(Level::Text(t)) => match &t.source {
960                LineSource::Lua(_) => false,
961                LineSource::Lines { pseudo, .. } => *pseudo,
962                _ => true,
963            },
964            _ => true,
965        };
966        self.end_file_reading();
967        if check {
968            self.check_outer_validity();
969        }
970        false
971    }
972
973    /// Scans a control sequence name after an escape character (§354).
974    fn scan_control_sequence(&mut self) {
975        'start_cs: loop {
976            let Some(Level::Text(t)) = self.input.stack.last() else {
977                return;
978            };
979            let len = t.buf.len();
980            let loc = t.loc;
981            let catmode = t.catmode;
982            if loc >= len {
983                self.cur_cs = NULL_CS;
984                break;
985            }
986            let mut k = loc;
987            let mut ch = t.buf[k];
988            let mut cat = self.catcode_in_mode(ch, catmode);
989            k += 1;
990            let state = if cat == 11 || cat == 10 {
991                LexState::SkipBlanks
992            } else {
993                LexState::MidLine
994            };
995            if cat == 11 && k < len {
996                // Scan ahead until finding a nonletter.
997                loop {
998                    ch = t.buf[k];
999                    cat = self.catcode_in_mode(ch, catmode);
1000                    k += 1;
1001                    if cat != 11 || k >= len {
1002                        break;
1003                    }
1004                }
1005                if self.reduce_expanded(k, ch, cat) {
1006                    continue 'start_cs;
1007                }
1008                if cat != 11 {
1009                    k -= 1;
1010                }
1011                if k > loc + 1 {
1012                    let mut name = Vec::with_capacity(k - loc);
1013                    if let Some(Level::Text(t)) = self.input.stack.last() {
1014                        for &c in &t.buf[loc..k] {
1015                            utf8x::push(&mut name, c);
1016                        }
1017                    }
1018                    self.cur_cs = self.hash.lookup_or_insert(&name);
1019                    if let Some(Level::Text(t)) = self.input.stack.last_mut() {
1020                        t.loc = k;
1021                        t.state = state;
1022                    }
1023                    break;
1024                }
1025            } else if self.reduce_expanded(k, ch, cat) {
1026                continue 'start_cs;
1027            }
1028            let c = match self.input.stack.last() {
1029                Some(Level::Text(t)) => t.buf[loc],
1030                _ => return,
1031            };
1032            self.cur_cs = self.hash.single(c);
1033            if let Some(Level::Text(t)) = self.input.stack.last_mut() {
1034                t.loc = loc + 1;
1035                t.state = state;
1036            }
1037            break;
1038        }
1039        let m = self.eqtb.meaning(self.cur_cs);
1040        self.cur_cmd = m.cmd;
1041        self.cur_chr = m.chr;
1042        if self.cur_cmd.is_outer_or_special() {
1043            self.check_outer_validity();
1044        }
1045    }
1046
1047    /// If an expanded code (`^^A`, `^^df`, `^^^^xxxx`) starts at
1048    /// `buf[k-1]` (whose character is `ch` with category `cat`), reduces it
1049    /// in the buffer and returns `true` (§355).
1050    fn reduce_expanded(&mut self, k: usize, ch: u32, cat: u8) -> bool {
1051        if cat != 7 {
1052            return false;
1053        }
1054        let Some(Level::Text(t)) = self.input.stack.last_mut() else {
1055            return false;
1056        };
1057        if t.buf.get(k) != Some(&ch) {
1058            return false;
1059        }
1060        match expanded_at(&t.buf, k - 1) {
1061            Expanded::Char { c, len } => {
1062                t.buf[k - 1] = c;
1063                t.buf.drain(k..k - 1 + len);
1064                true
1065            }
1066            _ => false,
1067        }
1068    }
1069
1070    /// Gets the next token, setting `cur_tok` too (`get_token`).
1071    pub fn get_token(&mut self) {
1072        self.err.no_new_control_sequence = false;
1073        self.get_next();
1074        self.err.no_new_control_sequence = true;
1075        self.cur_tok = self.make_cur_tok();
1076    }
1077
1078    /// The token corresponding to `cur_cmd`/`cur_chr`/`cur_cs`.
1079    #[inline]
1080    pub fn make_cur_tok(&self) -> Token {
1081        if self.cur_cs == NO_CS {
1082            Token::char(self.cur_cmd, self.cur_chr)
1083        } else {
1084            Token::cs(self.cur_cs)
1085        }
1086    }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use super::*;
1092
1093    #[test]
1094    fn line_splitting() {
1095        let data = b"ab  \r\ncd\rx\n\nlast";
1096        let mut pos = 0;
1097        let mut lines = Vec::new();
1098        while let Some((l, next)) = next_line(data, pos) {
1099            lines.push(decode_line(l));
1100            pos = next;
1101        }
1102        let strs: Vec<String> = lines
1103            .iter()
1104            .map(|l| l.iter().map(|&c| char::from_u32(c).unwrap()).collect())
1105            .collect();
1106        assert_eq!(strs, vec!["ab", "cd", "x", "", "last"]);
1107        assert_eq!(decode_line(b"x\t "), vec!['x' as u32, 9]);
1108    }
1109
1110    #[test]
1111    fn expanded_chars() {
1112        let s = |x: &str| x.chars().map(|c| c as u32).collect::<Vec<_>>();
1113        assert!(matches!(
1114            expanded_at(&s("^^A"), 0),
1115            Expanded::Char { c: 1, len: 3 }
1116        ));
1117        assert!(matches!(
1118            expanded_at(&s("^^e9"), 0),
1119            Expanded::Char { c: 0xE9, len: 4 }
1120        ));
1121        assert!(matches!(
1122            expanded_at(&s("^^?"), 0),
1123            Expanded::Char { c: 127, len: 3 }
1124        ));
1125        assert!(matches!(
1126            expanded_at(&s("^^^^00e9"), 0),
1127            Expanded::Char { c: 0xE9, len: 8 }
1128        ));
1129        assert!(matches!(
1130            expanded_at(&s("^^^^^^01f600"), 0),
1131            Expanded::Char {
1132                c: 0x1F600,
1133                len: 12
1134            }
1135        ));
1136        assert!(matches!(
1137            expanded_at(&s("^^^^zz"), 0),
1138            Expanded::Bad { six: false }
1139        ));
1140        assert!(matches!(expanded_at(&s("^x"), 0), Expanded::None));
1141    }
1142}