Skip to main content

retex_engine/
cond.rs

1//! Conditional processing (*TeX: The Program* §487–§510, plus the e-TeX
2//! and LuaTeX conditionals).
3
4use crate::Engine;
5use crate::cmd::Cmd;
6use crate::hash::{CsKind, NO_CS, UNDEFINED_CS};
7use crate::input::{Level, NO_EXPAND_FLAG, ScannerStatus};
8use crate::primitives_gen::assign_int as ip;
9use crate::scan::ValLevel;
10
11/// `if_code`: the condition has not yet been evaluated.
12pub const IF_CODE: u32 = 1;
13/// `fi_code`.
14pub const FI_CODE: u32 = 2;
15/// `else_code`.
16pub const ELSE_CODE: u32 = 3;
17/// `or_code`.
18pub const OR_CODE: u32 = 4;
19
20/// Offset added to the `if_test` modifier by `\unless`.
21pub const UNLESS_CODE: u32 = 32;
22
23/// `if_test` modifiers (LuaTeX numbering).
24#[allow(missing_docs)]
25pub mod if_codes {
26    pub const IF_CHAR: u32 = 0;
27    pub const IF_CAT: u32 = 1;
28    pub const IF_INT: u32 = 2;
29    pub const IF_DIM: u32 = 3;
30    pub const IF_ODD: u32 = 4;
31    pub const IF_VMODE: u32 = 5;
32    pub const IF_HMODE: u32 = 6;
33    pub const IF_MMODE: u32 = 7;
34    pub const IF_INNER: u32 = 8;
35    pub const IF_VOID: u32 = 9;
36    pub const IF_HBOX: u32 = 10;
37    pub const IF_VBOX: u32 = 11;
38    pub const IFX: u32 = 12;
39    pub const IF_EOF: u32 = 13;
40    pub const IF_TRUE: u32 = 14;
41    pub const IF_FALSE: u32 = 15;
42    pub const IF_CASE: u32 = 16;
43    pub const IF_DEF: u32 = 17;
44    pub const IF_CS: u32 = 18;
45    pub const IF_FONT_CHAR: u32 = 19;
46    pub const IF_IN_CSNAME: u32 = 20;
47    pub const IF_PRIMITIVE: u32 = 21;
48    pub const IF_ABS_NUM: u32 = 22;
49    pub const IF_ABS_DIM: u32 = 23;
50    pub const IF_CONDITION: u32 = 24;
51}
52use if_codes::*;
53
54/// A saved outer conditional.
55#[derive(Clone, Copy, Debug)]
56pub struct CondRecord {
57    /// Its `if_limit`.
58    pub if_limit: u32,
59    /// Its `cur_if`.
60    pub cur_if: u32,
61    /// Its `if_line`.
62    pub if_line: i32,
63}
64
65/// Conditional state.
66#[derive(Debug, Default, Clone)]
67pub struct CondState {
68    /// The condition stack (TeX's `cond_ptr` list, innermost last). Its
69    /// length is the number of active conditionals.
70    pub stack: Vec<CondRecord>,
71    /// What ends the current conditional (`if_limit`).
72    pub if_limit: u32,
73    /// The type of the current conditional (`cur_if`).
74    pub cur_if: u32,
75    /// Line where the current conditional began (`if_line`).
76    pub if_line: i32,
77    /// Line where skipping began (`skip_line`).
78    pub skip_line: i32,
79    /// Whether we are inside `\csname`…`\endcsname` (`is_in_csname`).
80    pub is_in_csname: bool,
81}
82
83impl Engine {
84    /// Skips text until `\fi`, `\else` or `\or` at level 0 (`pass_text`).
85    pub fn pass_text(&mut self) {
86        let save = self.input.scanner_status;
87        self.input.scanner_status = ScannerStatus::Skipping;
88        let mut l = 0i32;
89        self.cond.skip_line = self.input.line();
90        loop {
91            self.skip_plain_tokens();
92            self.get_next();
93            if self.cur_cmd == Cmd::FiOrElse {
94                if l == 0 {
95                    break;
96                }
97                if self.cur_chr == FI_CODE {
98                    l -= 1;
99                }
100            } else if self.cur_cmd == Cmd::IfTest {
101                l += 1;
102            }
103        }
104        self.input.scanner_status = save;
105        if self.eqtb.int(ip::TRACINGIFS) > 0 {
106            self.show_cur_cmd_chr();
107        }
108    }
109
110    /// Skips the tokens of the current token list that `get_next` would
111    /// read on its fast path and that are not conditionals: the bulk of a
112    /// skipped branch, without a call per token. Stops before anything
113    /// else (parameters, the end of the list, outer macros, alignment
114    /// characters), which `get_next` then reads as usual.
115    fn skip_plain_tokens(&mut self) {
116        let Some(Level::Tokens(t)) = self.input.stack.last_mut() else {
117            return;
118        };
119        let mut loc = t.loc;
120        let mut align = 0i32;
121        while let Some(&tok) = t.list.get(loc) {
122            if tok.is_cs() {
123                let m = self.eqtb.meaning(tok.cs_id());
124                let plain =
125                    m.cmd < Cmd::TabMark || (m.cmd > Cmd::CarRet && !m.cmd.is_outer_or_special());
126                if !plain || m.cmd == Cmd::FiOrElse || m.cmd == Cmd::IfTest {
127                    break;
128                }
129            } else {
130                let cmd = tok.cmd();
131                if !(cmd > Cmd::CarRet || cmd < Cmd::MathShift) {
132                    break;
133                }
134                match cmd {
135                    Cmd::LeftBrace => align += 1,
136                    Cmd::RightBrace => align -= 1,
137                    _ => {}
138                }
139            }
140            loc += 1;
141        }
142        t.loc = loc;
143        self.input.align_state += align;
144    }
145
146    fn push_cond(&mut self) {
147        self.cond.stack.push(CondRecord {
148            if_limit: self.cond.if_limit,
149            cur_if: self.cond.cur_if,
150            if_line: self.cond.if_line,
151        });
152        self.cond.cur_if = self.cur_chr;
153        self.cond.if_limit = IF_CODE;
154        self.cond.if_line = self.input.line();
155    }
156
157    /// Pops the condition stack.
158    pub fn pop_cond(&mut self) {
159        if let Some(r) = self.cond.stack.pop() {
160            self.cond.if_line = r.if_line;
161            self.cond.cur_if = r.cur_if;
162            self.cond.if_limit = r.if_limit;
163        }
164    }
165
166    fn change_if_limit(&mut self, l: u32, p: usize) {
167        if p == self.cond.stack.len() {
168            self.cond.if_limit = l;
169        } else if let Some(r) = self.cond.stack.get_mut(p) {
170            r.if_limit = l;
171        } else {
172            self.confusion("if");
173        }
174    }
175
176    /// Like `get_x_token`, but a `\noexpand`ed active character counts as
177    /// that character (`get_x_token_or_active_char`).
178    fn get_x_token_or_active_char(&mut self) {
179        self.get_x_token();
180        if self.cur_cmd == Cmd::Relax
181            && self.cur_chr == NO_EXPAND_FLAG
182            && let CsKind::Active(c) = self.hash.kind(self.cur_tok.cs_id())
183        {
184            self.cur_cmd = Cmd::ParEnd; // active_char
185            self.cur_chr = *c;
186        }
187    }
188
189    /// Expands a conditional (`conditional`, §498).
190    pub fn conditional(&mut self) {
191        if self.eqtb.int(ip::TRACINGIFS) > 0 && self.eqtb.int(ip::TRACINGCOMMANDS) <= 1 {
192            self.show_cur_cmd_chr();
193        }
194        let is_unless = self.cur_chr >= UNLESS_CODE;
195        let this_if = self.cur_chr % UNLESS_CODE;
196        if this_if == IF_CONDITION {
197            // LuaTeX: a no-op that only counts as a conditional when skipped.
198            return;
199        }
200        self.push_cond();
201        let save_cond_ptr = self.cond.stack.len();
202        let mut b: bool;
203        match this_if {
204            IF_CHAR | IF_CAT => {
205                self.get_x_token_or_active_char();
206                let (m, n) = if self.cur_cmd > Cmd::ParEnd || self.cur_chr > 0x10_FFFF {
207                    (Cmd::Relax, 0x11_0000)
208                } else {
209                    (self.cur_cmd, self.cur_chr)
210                };
211                self.get_x_token_or_active_char();
212                if self.cur_cmd > Cmd::ParEnd || self.cur_chr > 0x10_FFFF {
213                    self.cur_cmd = Cmd::Relax;
214                    self.cur_chr = 0x11_0000;
215                }
216                b = if this_if == IF_CHAR {
217                    n == self.cur_chr
218                } else {
219                    m == self.cur_cmd
220                };
221            }
222            IF_INT | IF_DIM | IF_ABS_NUM | IF_ABS_DIM => {
223                let dim = this_if == IF_DIM || this_if == IF_ABS_DIM;
224                let abs = this_if == IF_ABS_NUM || this_if == IF_ABS_DIM;
225                if dim {
226                    self.scan_normal_dimen();
227                } else {
228                    self.scan_int();
229                }
230                let mut n = self.cur_val;
231                if abs {
232                    n = n.wrapping_abs();
233                }
234                // Get the next non-blank non-call token.
235                loop {
236                    self.get_x_token();
237                    if self.cur_cmd != Cmd::Spacer {
238                        break;
239                    }
240                }
241                let r = if self.cur_tok.0 == crate::token::Token::other('<' as u32).0
242                    || self.cur_tok.0 == crate::token::Token::other('=' as u32).0
243                    || self.cur_tok.0 == crate::token::Token::other('>' as u32).0
244                {
245                    self.cur_tok.chr()
246                } else {
247                    self.print_err("Missing = inserted for ");
248                    self.print_cmd_chr(Cmd::IfTest, this_if);
249                    self.help(&["I was expecting to see `<', `=', or `>'. Didn't."]);
250                    self.back_error();
251                    '=' as u32
252                };
253                if dim {
254                    self.scan_normal_dimen();
255                } else {
256                    self.scan_int();
257                }
258                let mut v = self.cur_val;
259                if abs {
260                    v = v.wrapping_abs();
261                }
262                b = match char::from_u32(r) {
263                    Some('<') => n < v,
264                    Some('=') => n == v,
265                    _ => n > v,
266                };
267            }
268            IF_ODD => {
269                self.scan_int();
270                b = self.cur_val % 2 != 0;
271            }
272            IF_VMODE => b = self.mode.abs_kind() == crate::maincontrol::ModeKind::Vertical,
273            IF_HMODE => b = self.mode.abs_kind() == crate::maincontrol::ModeKind::Horizontal,
274            IF_MMODE => b = self.mode.abs_kind() == crate::maincontrol::ModeKind::Math,
275            IF_INNER => b = self.mode.is_inner(),
276            IF_VOID | IF_HBOX | IF_VBOX => {
277                self.scan_register_num();
278                let p = self.eqtb.boxes[self.cur_val as usize].val;
279                b = match this_if {
280                    IF_VOID => p.is_null(),
281                    IF_HBOX => !p.is_null() && self.box_is_hlist(p),
282                    _ => !p.is_null() && !self.box_is_hlist(p),
283                };
284            }
285            IFX => {
286                let save = self.input.scanner_status;
287                self.input.scanner_status = ScannerStatus::Normal;
288                self.get_next();
289                let n = self.cur_cs;
290                let p = self.cur_cmd;
291                let q = self.cur_chr;
292                let first_mac = if n != NO_CS {
293                    self.eqtb.meaning(n).mac.clone()
294                } else {
295                    None
296                };
297                self.get_next();
298                b = if self.cur_cmd != p {
299                    false
300                } else if !self.cur_cmd.is_call() {
301                    self.cur_chr == q
302                } else {
303                    let second = if self.cur_cs != NO_CS {
304                        self.eqtb.meaning(self.cur_cs).mac.clone()
305                    } else {
306                        None
307                    };
308                    match (first_mac, second) {
309                        (Some(a), Some(c)) => {
310                            std::rc::Rc::ptr_eq(&a, &c)
311                                || (a.protected == c.protected && a.tokens == c.tokens)
312                        }
313                        _ => false,
314                    }
315                };
316                self.input.scanner_status = save;
317            }
318            IF_EOF => {
319                self.scan_four_bit_int_or_more();
320                b = !self.files.read_open(self.cur_val as u32);
321            }
322            IF_TRUE => b = true,
323            IF_FALSE => b = false,
324            IF_DEF => {
325                let save = self.input.scanner_status;
326                self.input.scanner_status = ScannerStatus::Normal;
327                self.get_next();
328                b = self.cur_cmd != Cmd::UndefinedCs;
329                self.input.scanner_status = save;
330            }
331            IF_CS => {
332                let e = self.cond.is_in_csname;
333                self.cond.is_in_csname = true;
334                let mut name: Vec<u8> = Vec::new();
335                loop {
336                    self.get_x_token();
337                    if self.cur_cs != NO_CS {
338                        break;
339                    }
340                    crate::utf8x::push(&mut name, self.cur_chr);
341                }
342                if self.cur_cmd != Cmd::EndCsName {
343                    self.complain_missing_endcsname();
344                }
345                let cs = self.hash.lookup(&name).unwrap_or(UNDEFINED_CS);
346                self.cur_cs = cs;
347                b = self.eqtb.meaning(cs).cmd != Cmd::UndefinedCs;
348                self.lookup_named_cs(&name);
349                self.cond.is_in_csname = e;
350            }
351            IF_FONT_CHAR => {
352                self.scan_font_ident();
353                let f = self.cur_val as u32;
354                self.scan_char_num();
355                b = self.font_has_char(f, self.cur_val as u32);
356            }
357            IF_IN_CSNAME => b = self.cond.is_in_csname,
358            IF_PRIMITIVE => {
359                let save = self.input.scanner_status;
360                self.input.scanner_status = ScannerStatus::Normal;
361                self.get_next();
362                b = self.cur_cs != NO_CS && self.has_primitive_meaning(self.cur_cs);
363                self.input.scanner_status = save;
364            }
365            IF_CASE => {
366                self.scan_int();
367                let mut n = self.cur_val;
368                if self.eqtb.int(ip::TRACINGCOMMANDS) > 1 {
369                    self.begin_diagnostic();
370                    self.printer.print_str("{case ");
371                    self.printer.print_int(n.into());
372                    self.printer.print_char('}' as u32);
373                    self.end_diagnostic(false);
374                }
375                let mut ended = false;
376                while n != 0 {
377                    self.pass_text();
378                    if self.cond.stack.len() == save_cond_ptr {
379                        if self.cur_chr == OR_CODE {
380                            n -= 1;
381                        } else {
382                            ended = true;
383                            break;
384                        }
385                    } else if self.cur_chr == FI_CODE {
386                        self.pop_cond();
387                    }
388                }
389                if !ended {
390                    self.change_if_limit(OR_CODE, save_cond_ptr);
391                    return;
392                }
393                self.common_ending();
394                return;
395            }
396            _ => b = false,
397        }
398        let _ = ValLevel::Int;
399        if is_unless {
400            b = !b;
401        }
402        if self.eqtb.int(ip::TRACINGCOMMANDS) > 1 {
403            self.begin_diagnostic();
404            self.printer.print_str(if b { "{true}" } else { "{false}" });
405            self.end_diagnostic(false);
406        }
407        if b {
408            self.change_if_limit(ELSE_CODE, save_cond_ptr);
409            return;
410        }
411        // Skip to \else or \fi.
412        loop {
413            self.pass_text();
414            if self.cond.stack.len() == save_cond_ptr {
415                if self.cur_chr != OR_CODE {
416                    break;
417                }
418                self.print_err("Extra ");
419                self.print_esc_str("or");
420                self.help(&["I'm ignoring this; it doesn't match any \\if."]);
421                self.error();
422            } else if self.cur_chr == FI_CODE {
423                self.pop_cond();
424            }
425        }
426        self.common_ending();
427    }
428
429    fn common_ending(&mut self) {
430        if self.cur_chr == FI_CODE {
431            self.pop_cond();
432        } else {
433            self.cond.if_limit = FI_CODE;
434        }
435    }
436
437    /// Handles `\fi`, `\else` and `\or` met during expansion (§510).
438    pub fn fi_or_else(&mut self) {
439        if self.eqtb.int(ip::TRACINGIFS) > 0 && self.eqtb.int(ip::TRACINGCOMMANDS) <= 1 {
440            self.show_cur_cmd_chr();
441        }
442        if self.cur_chr > self.cond.if_limit {
443            if self.cond.if_limit == IF_CODE {
444                self.insert_relax();
445            } else {
446                self.print_err("Extra ");
447                self.print_cmd_chr(Cmd::FiOrElse, self.cur_chr);
448                self.help(&["I'm ignoring this; it doesn't match any \\if."]);
449                self.error();
450            }
451        } else {
452            while self.cur_chr != FI_CODE {
453                self.pass_text();
454            }
455            self.pop_cond();
456        }
457    }
458
459    /// Whether `cs` currently has the meaning of the primitive of the same
460    /// name (`\ifprimitive`).
461    pub fn has_primitive_meaning(&self, cs: crate::hash::CsId) -> bool {
462        let name = self.hash.name(cs);
463        match crate::primitives::lookup(name) {
464            Some(p) => {
465                let m = self.eqtb.meaning(cs);
466                m.cmd == p.cmd && m.chr == p.chr && m.mac.is_none()
467            }
468            None => false,
469        }
470    }
471}