Skip to main content

latexml_core/definition/
conditional.rs

1//! Conditionals Control sequence definitions.
2//! These represent the control sequences for conditionals, as well as
3//! `\else`, `\or` and `\fi`.
4
5use std::{borrow::Cow, cell::RefCell, fmt, rc::Rc};
6
7use libxml::tree::Node;
8
9// use crate::common::numeric_ops::NumericOps;
10use crate::Digested;
11use crate::{
12  common::{
13    error::{emit_warn, *},
14    locator::Locator,
15    object::Object,
16  },
17  definition::{BeforeDigestClosure, ConditionalClosure, Definition, DigestionClosure},
18  document::Document,
19  gullet,
20  parameter::Parameters,
21  pin,
22  state::*,
23  token::*,
24  tokens::Tokens,
25  whatsit::Whatsit,
26};
27
28// Conditional control sequences; Expandable
29//   Expand enough to determine true/false, then maybe skip
30//   record a flag somewhere so that \else or \fi is recognized
31//   (otherwise, they should signal an error)
32
33/// classify the standard pieces of a conditional
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ConditionalType {
36  /// \if
37  If,
38  /// \unless
39  Unless,
40  /// \else
41  Else,
42  /// \or
43  Or,
44  /// \fi
45  Fi,
46  /// fallback?
47  Unknown,
48}
49
50impl From<&str> for ConditionalType {
51  fn from(cs: &str) -> Self {
52    use self::ConditionalType::*;
53    match cs {
54      "\\if" => If,
55      "\\unless" => Unless,
56      "\\else" => Else,
57      "\\or" => Or,
58      "\\fi" => Fi,
59      _ => If,
60    }
61  }
62}
63
64/// configurations for a conditional.
65#[derive(Default)]
66pub struct ConditionalOptions {
67  /// scope to install in state
68  pub scope:   Option<Scope>,
69  /// is this definition locked?
70  pub locked:  Option<bool>,
71  /// skipper, currently only used for \ifcase.
72  // TODO: implement this?
73  pub skipper: Option<bool>,
74}
75
76/// A Conditional definition; Expandable.
77#[derive(Clone)]
78pub struct Conditional {
79  /// the command sequence
80  pub cs:               Token,
81  /// list of parameters, if any
82  pub paramlist:        Option<Parameters>,
83  /// a test closure, if implemented in a binding
84  pub test:             Option<ConditionalClosure>,
85  /// the kind of piece in the syntax (if,else,fi...)
86  pub conditional_type: ConditionalType,
87  /// a skipper for \ifcase
88  pub skipper:          Option<bool>,
89}
90impl Default for Conditional {
91  fn default() -> Self {
92    Conditional {
93      cs:               T_CS!("Conditional"),
94      paramlist:        None,
95      test:             None,
96      conditional_type: ConditionalType::Unknown,
97      skipper:          None,
98    }
99  }
100}
101impl PartialEq for Conditional {
102  fn eq(&self, other: &Conditional) -> bool { self.cs == other.cs }
103}
104
105impl fmt::Display for Conditional {
106  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.cs) }
107}
108impl Object for Conditional {
109  fn is_expandable(&self) -> bool { true }
110  fn stringify(&self) -> String { self.stringify_type("Conditional") }
111}
112impl Definition for Conditional {
113  // sub new {
114  //   my ($class, $cs, $parameters, $test, %traits) = @_;
115  //   my $source = $state->getStomach->getGullet->getMouth;
116  //   return bless { cs => $cs, parameters => $parameters, test => $test,
117  //     locator      => "from " . $source->getLocator(-1),
118  //     isExpandable => 1,
119  //     %traits }, $class; }
120
121  // Note that although conditionals are Expandable,
122  // they are NOT defined as macros, so they don't need to handle doInvocation,
123  fn invoke(&self, _once_only: bool) -> Result<Tokens> {
124    // A real conditional must have condition_type set
125    use self::ConditionalType::*;
126    match self.conditional_type {
127      If | Unless => self.invoke_conditional(),
128      Else | Or => self.invoke_else(),
129      Fi => self.invoke_fi(),
130      _ => {
131        // Diagnostic-only path: format the current CS name, or "\\?" if the
132        // current-token register is empty. Never panic here — we are
133        // already in error-emission territory.
134        let cur = get_current_token()
135          .map(|t| t.stringify())
136          .unwrap_or_else(|| String::from("\\?"));
137        let message = s!("Unknown conditional control sequence {}", cur);
138        Error!("unexpected", self.cs, message);
139        Ok(Tokens!())
140      },
141    }
142  }
143
144  fn get_parameters(&self) -> Option<&Parameters> { self.paramlist.as_ref() }
145  fn get_cs(&self) -> Cow<'_, Token> { Cow::Borrowed(&self.cs) }
146  fn get_cs_name(&self) -> Cow<'_, str> { Cow::Owned(self.cs.with_cs_name(ToString::to_string)) }
147  fn get_alias(&self) -> Option<&String> { None }
148  fn get_test(&self) -> Option<&ConditionalClosure> { self.test.as_ref() }
149  fn get_conditional_type(&self) -> Option<ConditionalType> { Some(self.conditional_type) }
150  // Not implemented for expandable
151  fn invoke_primitive(&self) -> Result<Vec<Digested>> {
152    // Conditionals are expandable, not primitive — this shouldn't be called
153    Ok(Vec::new())
154  }
155  fn before_digest(&self) -> Option<&Vec<BeforeDigestClosure>> { None }
156  fn after_digest(&self) -> Option<&Vec<DigestionClosure>> { None }
157  fn do_absorption(&self, _document: &mut Document, _whatsit: &Whatsit) -> Result<Vec<Node>> {
158    fatal!(
159      Definition,
160      Unexpected,
161      "do_absorption on Conditional should never be called!"
162    );
163  }
164}
165
166/// A Frame of data for the currently active conditional, stored in State
167#[derive(Debug, Clone, PartialEq)]
168pub struct IfFrame {
169  /// the token which started the conditional
170  pub token:   Token,
171  /// source location of the conditional start
172  pub start:   Locator,
173  /// flag: currently parsing the test
174  pub parsing: bool,
175  /// flag: already seen an else at this level
176  pub elses:   bool,
177  /// in nested conditionals, give each an id
178  pub ifid:    i64,
179}
180
181impl Conditional {
182  fn invoke_conditional(&self) -> Result<Tokens> {
183    // Hot path: fires on every \if/\ifx/\ifnum/…; all state probes use
184    // pin!-cached keys (the per-call arena::pin was ~1% of self-time on
185    // \ifnum-dense pgfplots documents).
186    let mut ifid = lookup_int_sym(pin!("if_count"));
187    ifid += 1;
188    assign_value_sym(pin!("if_count"), ifid, Some(Scope::Global));
189    // Perl: if ($LaTeXML::IF_LIMIT and $ifid > $LaTeXML::IF_LIMIT) { Fatal(...) }
190    let if_limit = lookup_int_sym(pin!("if_limit"));
191    if if_limit > 0 && ifid > if_limit {
192      Fatal!(
193        Timeout,
194        IfLimit,
195        s!("Conditional limit of {} exceeded, infinite loop?", if_limit)
196      );
197    }
198    let if_frame = Rc::new(RefCell::new(IfFrame {
199      token: get_current_token().unwrap(),
200      start: gullet::get_locator(),
201      parsing: true,
202      elses: false,
203      ifid,
204    }));
205    set_ifframe(Some(Rc::clone(&if_frame)));
206    unshift_value("if_stack", vec![Rc::clone(&if_frame)]);
207    let args = self.read_arguments()?;
208
209    get_ifframe().unwrap().borrow_mut().parsing = false;
210    // `tracingcommands` is normally unset; defer the state probe to
211    // the path that would actually read it. Conditional::invoke fires
212    // on every \if/\ifx/\ifnum/…, so avoiding a mandatory state
213    // lookup per conditional is a measurable win.
214    if let Some(ref test) = self.test {
215      if (test)(args)? {
216        // true branch: do nothing, tokens follow naturally
217      } else {
218        let to = self.skip_conditional_body(-1);
219        if lookup_bool_sym(pin!("tracingcommands")) {
220          Debug!("{{false}} [skipped to {:?}]\n", to);
221        }
222      }
223    } else {
224      // If there's no test, it must be the Special Case, \ifcase
225      // Note: num == 0 takes the 1st branch, no need to skip
226      // num < 0 should skip all \or & end up on the \else
227      let num = args.first().map(|a| a.value_of()).unwrap_or(0);
228      if num != 0 {
229        let _to = self.skip_conditional_body(num);
230        //       print STDERR "{$num} [skipped to " . ToString($to) . "]\n" if $tracing;
231      }
232    }
233    expire_ifframe();
234    Ok(Tokens!())
235  }
236
237  // =====================================================================
238  // Support for conditionals:
239  //
240  // Skipping for conditionals
241  //   0 : skip to \fi
242  //  -1 : skip to \else, if any, or \fi
243  //   n : skip to n-th \or, if any, or \else, if any, or \fi.
244  //
245  // NOTE that there are 2 kinds of "nested" ifs.
246  //  \if's inside the body of either the true or false branch
247  // are easily skipped by tracking a level of if nesting and skipping over the
248  // same number of \fi as you find \if.
249  //  \if's that get expanded while evaluating the test clause itself
250  // are considerably trickier. There's a frame on the if-stack for this \if
251  // that's above the one we're currently processing; typically the \else & \fi
252  // may still remain, but we need to either evaluate them a normal
253  // if we're continuing to follow the true branch, or skip oever them if
254  // we're trying to find the \else for the false branch.
255  // The danger is mistaking the \else that's associated with the test clause's \if
256  // and taking it for the \else that we're skipping to!
257  // Canonical example:
258  //   \if\ifx AA XY junk \else blah \fi True \else False \fi
259  // The inner \ifx should expand to "XY junk", since A==A
260  // Return the token we've skipped to, and the frame that this applies to.
261  fn skip_conditional_body(&self, nskips: i64) -> Result<Tokens> {
262    let mut level = 1;
263    let mut n_ors = 0;
264    let _start = gullet::get_locator();
265    // NOTE: Open-coded manipulation of if_stack!
266    // [we're only reading tokens & looking up, so state::shouldn't change behind our backs]
267    loop {
268      let (t, cond_type) = match gullet::read_next_conditional()? {
269        Some((tok, typ)) => (Tokens!(tok), Some(typ)),
270        None => (Tokens!(), None),
271      };
272      match cond_type {
273        None => break,
274        Some(ConditionalType::If) => level += 1, //  Found a \ifxx of some sort
275        Some(ConditionalType::Fi) => {
276          // Found a \fi
277          let local_frame = get_ifframe();
278          let maybe_last = with_value_mut("if_stack", |value_opt| {
279            if let Some(Stored::VecDequeStored(stack)) = value_opt
280              && let Some(Stored::IfFrame(stack_frame)) = stack.pop_front()
281            {
282              if *stack_frame.borrow() != *local_frame.as_ref().unwrap().borrow() {
283                // But is it for a condition nested in the test clause?
284                // then DO pop that conditional's frame; it's DONE!
285              } else {
286                level -= 1;
287                if level == 0 {
288                  // otherwise, if no more nesting, we're done.
289                  // Done with this frame, keep it removed
290                  return Some(t); // AND Return the finishing token.
291                } else {
292                  stack.push_front(stack_frame.into());
293                }
294              }
295            }
296            None
297          });
298          if let Some(t) = maybe_last {
299            return Ok(t);
300          }
301        },
302        Some(other_type) => {
303          if level > 1 {
304            // Ignore: \else,\or nested in the body.
305          } else if other_type == ConditionalType::Or {
306            n_ors += 1;
307            if n_ors == nskips {
308              return Ok(t);
309            }
310          } else if other_type == ConditionalType::Else && nskips != 0 {
311            // Found \else and we're looking for one?
312            let local_frame = get_ifframe();
313            // Make sure this \else is NOT for a nested \if that is part of the test clause!
314            let maybe_last = with_value("if_stack", |stack_opt| {
315              if let Some(Stored::VecDequeStored(stack)) = stack_opt
316                && let Some(Stored::IfFrame(stack_frame)) = stack.front()
317                && *stack_frame.borrow() == *local_frame.as_ref().unwrap().borrow()
318              {
319                // No need to actually call elseHandler, but note that we've seen an \else!
320                stack_frame.borrow_mut().elses = true;
321                return Some(t);
322              }
323              None
324            });
325            if let Some(t) = maybe_last {
326              return Ok(t);
327            }
328          }
329        },
330      };
331    }
332    Error!(
333      "expected",
334      "\\fi",
335      self,
336      s!(
337        "Missing \\fi or \\else, conditional fell off end. Conditional started at {:?}",
338        _start
339      )
340    );
341    Ok(Tokens!())
342  }
343
344  fn invoke_else(&self) -> Result<Tokens> {
345    let stack_frame_opt = with_value_mut("if_stack", |stack_opt| {
346      if let Some(Stored::VecDequeStored(stack)) = stack_opt {
347        if let Some(Stored::IfFrame(stack_frame)) = stack.front() {
348          Some(Rc::clone(stack_frame))
349        } else {
350          None
351        }
352      } else {
353        None
354      }
355    });
356    let local_token = get_current_token().unwrap();
357    if local_token.with_str(|s| s == "\\else") && stack_frame_opt.is_none() {
358      let stack_len = with_value("if_stack", |v| match v {
359        Some(Stored::VecDequeStored(s)) => s.len(),
360        _ => 0,
361      });
362      emit_warn(
363        "unexpected",
364        "else",
365        &format!("\\else encountered with no active if-frame (stack_len={stack_len})"),
366      );
367    }
368    if let Some(stack_frame) = stack_frame_opt {
369      if stack_frame.borrow().parsing {
370        // Defer expanding the \else if we're still parsing the test
371        Ok(Tokens!(T_RELAX!(), local_token))
372      } else if stack_frame.borrow().elses {
373        // Already seen an \else's at this level?
374        let message = s!(
375          "Extra {} already saw \\else for {:?} [{:?}] at {:?}",
376          local_token.stringify(),
377          stack_frame.borrow().token,
378          stack_frame.borrow().ifid,
379          stack_frame.borrow().start
380        );
381        let local_token_str = local_token.to_string();
382        Error!("unexpected", local_token_str, message);
383        Ok(Tokens!())
384      } else {
385        set_ifframe(Some(Rc::clone(&stack_frame)));
386        let _t = self.skip_conditional_body(0);
387        //     print STDERR '{' . ToString($LaTeXML::CURRENT_TOKEN) . '}'
388        //       . " [for " . ToString($$LaTeXML::IFFRAME{token}) . " #" .
389        // $$LaTeXML::IFFRAME{ifid}       . " skipping to " . ToString($t) . "]\n"
390        //       if $state->lookupValue('tracingcommands');
391        expire_ifframe();
392        Ok(Tokens!())
393      }
394    } else {
395      // No if stack entry ?
396      let message = s!(
397        "Didn't expect a {:?} since we seem not to be in a conditional",
398        local_token.stringify()
399      );
400      let local_token_str = local_token.to_string();
401      Error!("unexpected", local_token_str, message);
402      Ok(Tokens!())
403    }
404  }
405
406  fn invoke_fi(&self) -> Result<Tokens> {
407    let stack_frame_opt: Option<Rc<RefCell<IfFrame>>> = with_value("if_stack", |stack_opt| {
408      if let Some(Stored::VecDequeStored(stack)) = stack_opt {
409        if let Some(Stored::IfFrame(frame)) = stack.front() {
410          Some(Rc::clone(frame))
411        } else {
412          None
413        }
414      } else {
415        None
416      }
417    });
418    if let Some(stack_frame) = stack_frame_opt {
419      if stack_frame.borrow().parsing {
420        // Defer expanding the \else if we're still parsing the test
421        Ok(Tokens!(T_RELAX!(), get_current_token().unwrap()))
422      } else {
423        // "expand" by removing the stack entry for this level
424        set_ifframe(Some(stack_frame));
425        shift_value("if_stack")?; // Done with this frame
426
427        //     print STDERR '{' . ToString($LaTeXML::CURRENT_TOKEN) . '}'
428        // . " [for " . Stringify($$LaTeXML::IFFRAME{token}) . " #" . $$LaTeXML::IFFRAME{ifid} .
429        // "]\n"       if $state->lookupValue('tracingcommands');
430        expire_ifframe();
431        Ok(Tokens!())
432      }
433    } else {
434      let cur = get_current_token()
435        .map(|t| t.stringify())
436        .unwrap_or_else(|| String::from("\\?"));
437      let message = s!(
438        "Didn't expect a {:?} since we seem not to be in a conditional",
439        cur
440      );
441      Error!("unexpected", "fi", message);
442      Ok(Tokens!())
443    }
444  }
445}
446
447#[cfg(test)]
448mod tests {
449  use super::*;
450
451  #[test]
452  fn conditional_type_from_str_known_variants() {
453    assert_eq!(ConditionalType::from("\\if"), ConditionalType::If);
454    assert_eq!(ConditionalType::from("\\unless"), ConditionalType::Unless);
455    assert_eq!(ConditionalType::from("\\else"), ConditionalType::Else);
456    assert_eq!(ConditionalType::from("\\or"), ConditionalType::Or);
457    assert_eq!(ConditionalType::from("\\fi"), ConditionalType::Fi);
458  }
459
460  #[test]
461  fn conditional_type_from_str_unknown_falls_back_to_if() {
462    // The match's default arm is `_ => If`, not Unknown — that's a
463    // surprising but documented behavior in the source. Lock it in.
464    assert_eq!(ConditionalType::from("\\foo"), ConditionalType::If);
465    assert_eq!(ConditionalType::from(""), ConditionalType::If);
466  }
467
468  #[test]
469  fn conditional_type_equality() {
470    assert_eq!(ConditionalType::If, ConditionalType::If);
471    assert_ne!(ConditionalType::If, ConditionalType::Else);
472  }
473
474  #[test]
475  fn conditional_default_fields() {
476    let c = Conditional::default();
477    assert!(c.paramlist.is_none());
478    assert!(c.test.is_none());
479    assert_eq!(c.conditional_type, ConditionalType::Unknown);
480    assert!(c.skipper.is_none());
481  }
482
483  #[test]
484  fn conditional_partial_eq_by_cs() {
485    // PartialEq ignores conditional_type, skipper etc., compares by cs.
486    let a = Conditional::default();
487    let b = Conditional::default();
488    assert!(a == b, "defaults have same cs");
489  }
490
491  #[test]
492  fn conditional_is_expandable() {
493    let c = Conditional::default();
494    assert!(c.is_expandable());
495    // Conditionals are NOT definitions (they're expandable machinery).
496    // Actually the default Object::is_definition returns false; the
497    // trait default applies here.
498  }
499
500  #[test]
501  fn conditional_display_is_cs_text() {
502    let c = Conditional::default();
503    let s = format!("{c}");
504    assert_eq!(s, "Conditional");
505  }
506
507  #[test]
508  fn conditional_get_parameters_none_by_default() {
509    let c = Conditional::default();
510    assert!(c.get_parameters().is_none());
511  }
512
513  #[test]
514  fn conditional_options_default_all_none() {
515    let o = ConditionalOptions::default();
516    assert!(o.scope.is_none());
517    assert!(o.locked.is_none());
518    assert!(o.skipper.is_none());
519  }
520}