Skip to main content

latexml_core/
token.rs

1use std::{borrow::Cow, fmt, fmt::Display, rc::Rc};
2
3use once_cell::sync::Lazy;
4
5use crate::{
6  Digested,
7  common::{
8    arena::{self, SymStr},
9    dimension::Dimension,
10    error::*,
11    float::Float,
12    glue::Glue,
13    mudimension::MuDimension,
14    muglue::MuGlue,
15    number::Number,
16    numeric_ops::NumericOps,
17    store::Stored,
18  },
19  definition::{Definition, register::Register},
20  state,
21  tokens::Tokens,
22};
23
24static CONTROLNAME: &[&str] = &[
25  "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT", "LF", "VT", "FF", "CR", "SO",
26  "SI", "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS",
27  "GS", "RS", "US",
28];
29
30/// A Token category code, as in TeX
31#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
32pub enum Catcode {
33  ESCAPE,
34  BEGIN,
35  END,
36  MATH,
37  ALIGN,
38  EOL,
39  PARAM,
40  SUPER,
41  SUB,
42  IGNORE,
43  SPACE,
44  LETTER,
45  OTHER,
46  ACTIVE,
47  COMMENT,
48  INVALID,
49  CS,
50  MARKER,
51  ARG,
52}
53
54impl From<u8> for Catcode {
55  fn from(num: u8) -> Catcode {
56    use crate::token::Catcode::*;
57    match num {
58      0 => ESCAPE,
59      1 => BEGIN,
60      2 => END,
61      3 => MATH,
62      4 => ALIGN,
63      5 => EOL,
64      6 => PARAM,
65      7 => SUPER,
66      8 => SUB,
67      9 => IGNORE,
68      10 => SPACE,
69      11 => LETTER,
70      12 => OTHER,
71      13 => ACTIVE,
72      14 => COMMENT,
73      15 => INVALID,
74      16 => CS,
75      17 => MARKER,
76      18 => ARG,
77      _ => {
78        // let message = s!("Unrecognized catcode: {:?}", num);
79        // Warn!("unknown", "catcode", None, message);
80        IGNORE
81      },
82    }
83  }
84}
85
86impl From<Catcode> for u8 {
87  fn from(cc: Catcode) -> u8 {
88    use crate::token::Catcode::*;
89    match cc {
90      ESCAPE => 0,
91      BEGIN => 1,
92      END => 2,
93      MATH => 3,
94      ALIGN => 4,
95      EOL => 5,
96      PARAM => 6,
97      SUPER => 7,
98      SUB => 8,
99      IGNORE => 9,
100      SPACE => 10,
101      LETTER => 11,
102      OTHER => 12,
103      ACTIVE => 13,
104      COMMENT => 14,
105      INVALID => 15,
106      CS => 16,
107      MARKER => 17,
108      ARG => 18,
109    }
110  }
111}
112
113impl Catcode {
114  /// a debug-friendly name
115  pub fn name(self) -> &'static str {
116    use crate::token::Catcode::*;
117    match self {
118      // Primitive
119      ESCAPE => "Escape",
120      BEGIN => "Begin",
121      END => "End",
122      MATH => "Math",
123      ALIGN => "Align",
124      EOL => "EOL",
125      PARAM => "Parameter",
126      SUPER => "Superscript",
127      SUB => "Subscript",
128      SPACE => "Space",
129      // Non-primitive
130      IGNORE => "Ignore",
131      LETTER => "Letter",
132      OTHER => "Other",
133      ACTIVE => "Active",
134      COMMENT => "Comment",
135      INVALID => "Invalid",
136      CS => "ControlSequence",
137      MARKER => "Marker",
138      ARG => "Arg",
139    }
140  }
141
142  /// SymStr form of `name()` — each variant caches its interned
143  /// symbol via `pin!`. Used by `Token::get_cs_name` /
144  /// `pin_cs_name` to avoid a per-call `pin_static` hash probe
145  /// (fires on every primitive-token definition lookup).
146  pub fn name_sym(self) -> SymStr {
147    use crate::token::Catcode::*;
148    match self {
149      ESCAPE => crate::pin!("Escape"),
150      BEGIN => crate::pin!("Begin"),
151      END => crate::pin!("End"),
152      MATH => crate::pin!("Math"),
153      ALIGN => crate::pin!("Align"),
154      EOL => crate::pin!("EOL"),
155      PARAM => crate::pin!("Parameter"),
156      SUPER => crate::pin!("Superscript"),
157      SUB => crate::pin!("Subscript"),
158      SPACE => crate::pin!("Space"),
159      IGNORE => crate::pin!("Ignore"),
160      LETTER => crate::pin!("Letter"),
161      OTHER => crate::pin!("Other"),
162      ACTIVE => crate::pin!("Active"),
163      COMMENT => crate::pin!("Comment"),
164      INVALID => crate::pin!("Invalid"),
165      CS => crate::pin!("ControlSequence"),
166      MARKER => crate::pin!("Marker"),
167      ARG => crate::pin!("Arg"),
168    }
169  }
170  /// a \meaning-friendly name
171  pub fn meaning(self) -> &'static str {
172    use crate::token::Catcode::*;
173    match self {
174      ESCAPE => "the escape character",
175      BEGIN => "begin-group character",
176      END => "end-group character",
177      MATH => "math shift character",
178      ALIGN => "alignment tab character",
179      EOL => "end-of-line character",
180      PARAM => "macro parameter character",
181      SUPER => "superscript character",
182      SUB => "subscript character",
183      IGNORE => "ignored character",
184      SPACE => "blank space",
185      LETTER => "the letter",
186      OTHER => "the character",
187      ACTIVE => "active character",
188      COMMENT => "comment character",
189      INVALID => "invalid character",
190      _ => "",
191    }
192  }
193  /// a short name helpful for Token debugging
194  pub fn short_name(self) -> &'static str {
195    use crate::token::Catcode::*;
196    match self {
197      ESCAPE => "T_ESCAPE",
198      BEGIN => "T_BEGIN",
199      END => "T_END",
200      MATH => "T_MATH",
201      ALIGN => "T_ALIGN",
202      EOL => "T_EOL",
203      PARAM => "T_PARAM",
204      SUPER => "T_SUPER",
205      SUB => "T_SUB",
206      IGNORE => "T_IGNORE",
207      SPACE => "T_SPACE",
208      LETTER => "T_LETTER",
209      OTHER => "T_OTHER",
210      ACTIVE => "T_ACTIVE",
211      COMMENT => "T_COMMENT",
212      INVALID => "T_INVALID",
213      CS => "T_CS",
214      MARKER => "T_MARKER",
215      ARG => "T_ARG",
216    }
217  }
218
219  // ======================================================================
220  // Categories of Category codes.
221  // For Tokens with these catcodes, only the catcode is relevant for comparison.
222  // (if they even make it to a stage where they get compared)
223  /// TeX-primitive codes
224  pub fn is_primitive(self) -> bool {
225    use crate::token::Catcode::*;
226    match self {
227      // Primitives
228      ESCAPE | BEGIN | END | MATH | ALIGN | EOL | PARAM | SUPER | SUB | SPACE => true,
229      // Non-primitive
230      IGNORE | LETTER | OTHER | ACTIVE | COMMENT | INVALID | CS | MARKER | ARG => false,
231    }
232  }
233  /// Catcodes with associated primitives
234  pub fn is_executable(self) -> bool {
235    use crate::token::Catcode::*;
236    match self {
237      // Executable
238      BEGIN | END | MATH | ALIGN | SUPER | SUB | ACTIVE | CS => true,
239      // Non-executable
240      EOL | ESCAPE | PARAM | SPACE | IGNORE | LETTER | OTHER | COMMENT | INVALID | MARKER | ARG => {
241        false
242      },
243    }
244  }
245  /// Catcodes which can be neutralized
246  pub fn is_neutralizable(self) -> bool {
247    use crate::token::Catcode::*;
248    match self {
249      // Neutralizable
250      MATH | ALIGN | PARAM | SUPER | SUB | ACTIVE => true,
251      // Non-neutralizable
252      ESCAPE | BEGIN | END | EOL | IGNORE | SPACE | LETTER | OTHER | COMMENT | INVALID | CS
253      | MARKER | ARG => false,
254    }
255  }
256  /// Shorthand to match the "active" and "command sequence" catcodes
257  pub fn is_active_or_cs(self) -> bool {
258    use crate::token::Catcode::*;
259    matches!(self, ACTIVE | CS)
260  }
261  /// Tokens which can be absorbed without side-effects
262  pub fn is_absorbable(self) -> bool {
263    use crate::token::Catcode::*;
264    // Absorbable
265    matches!(self, SPACE | LETTER | OTHER | COMMENT)
266  }
267  /// Gullet can only hold comment and marker tokens
268  pub fn is_gullet_holdable(self) -> bool {
269    use crate::token::Catcode::*;
270    matches!(self, COMMENT | MARKER)
271  }
272  /// Catcodes that are of note for balanced reads.
273  pub fn is_balanced_interesting(self) -> bool {
274    use crate::token::Catcode::*;
275    // `gullet::is_balanced` reacts to BEGIN,END,MARKER coded tokens
276    matches!(self, BEGIN | END | MARKER)
277  }
278}
279
280/// The core immutable syntactic primitive resulting from TeX's read-in and expansion process
281/// We allow the fields to be public, so that we can use builder macros such as
282/// ```
283/// macro_rules! T_SPACE(() => {
284///     Token { text: arena::pin_static(" "), code: Catcode::SPACE}
285///   });
286/// ```
287#[derive(Copy, Clone)]
288pub struct Token {
289  /// an arena id the character content for this token
290  pub text: SymStr,
291  /// a TeX catcode
292  pub code: Catcode,
293  /// Origin handle into the per-conversion token-origin side arena (1-based;
294  /// `0` = no recorded origin). Present only under the `token-locators` feature
295  /// (the opt-in source-map precision build); `Token` stays 8 bytes otherwise.
296  /// Set in `read_token`; carried through expansion so a digested run can recover
297  /// its exact source span. **Excluded from `PartialEq`** (tokens compare by
298  /// meaning, not origin — see `impl PartialEq`). See docs/performance/SOURCE_PROVENANCE.md
299  /// §3.1.1.
300  #[cfg(feature = "token-locators")]
301  pub loc:  u32,
302}
303
304impl fmt::Debug for Token {
305  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
306    if self.code == Catcode::ARG {
307      self.with_str(|text| write!(f, "\"#{}\"", text))
308    } else {
309      self.with_str(|text| write!(f, "{:?}", text))
310    }
311  }
312}
313
314impl Display for Token {
315  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
316    if self.code == Catcode::ARG {
317      write!(f, "#")?;
318    }
319    self.with_str(|text| write!(f, "{}", text))
320  }
321}
322
323/// Compare two tokens; They are equal if they both have same catcode & string
324// [We pretend all SPACE's are the same, since we'd like to hide newline's in there!]
325// NOTE: That another popular equality checks whether the "meaning" (defn) are the same.
326// That is NOT done here; see Equals(x,y) and XEquals(x,y)
327impl PartialEq for Token {
328  fn eq(&self, other: &Token) -> bool {
329    self.code == other.code && (self.code == Catcode::SPACE || (self.text == other.text))
330  }
331}
332
333/// Name of the bare no-op control sequence `\noexpand` collapses to, and the
334/// prefix of every member of the no-expand family. See [`Token::is_noexpand_family`].
335pub const NOEXPAND_PREFIX: &str = "\\special_relax";
336/// Separator byte between the `\special_relax` prefix and the shadowed token's
337/// text in a family token's name. `\x01` is never valid in a CS name or active
338/// char, so `\special_relax\x01\foo` is unambiguous and cannot collide with a
339/// real CS such as a (hypothetical) `\special_relaxfoo`.
340pub const NOEXPAND_SEP: u8 = 1;
341
342/// Build the no-expand family token shadowing `shadowed` — the per-token,
343/// dump-safe representation of `\noexpand <shadowed>` (faithful to TeX's
344/// `no_expand_flag`: the shadowed identity is preserved in the name; the family
345/// resolves to `\relax` meaning). `shadowed` is an expandable/undefined CS or
346/// active token (the only things `\noexpand` wraps; see `is_dont_expandable`).
347pub fn noexpand_family(shadowed: &Token) -> Token {
348  let text = shadowed.with_str(|s| format!("{NOEXPAND_PREFIX}{}{s}", NOEXPAND_SEP as char));
349  Token {
350    text: arena::pin(text),
351    code: Catcode::CS,
352    #[cfg(feature = "token-locators")]
353    loc: 0,
354  }
355}
356
357// Note: given that we are pinning the strings in an arena,
358//  once we have a token of a certain kind it is now faster to clone
359//  a known token than it is to build a new one
360//  (as the arena lookup is a hair slower than copying a u32)
361
362/// constant for a BEGIN "{" token
363#[thread_local]
364pub static TOKEN_BEGIN: Lazy<Token> = Lazy::new(|| Token {
365  text: arena::pin_static("{"),
366  code: Catcode::BEGIN,
367  #[cfg(feature = "token-locators")]
368  loc: 0,
369});
370/// constant for an END "}" token
371#[thread_local]
372pub static TOKEN_END: Lazy<Token> = Lazy::new(|| Token {
373  text: arena::pin_static("}"),
374  code: Catcode::END,
375  #[cfg(feature = "token-locators")]
376  loc: 0,
377});
378/// constant for a MATH "$" token
379#[thread_local]
380pub static TOKEN_MATH: Lazy<Token> = Lazy::new(|| Token {
381  text: arena::pin_static("$"),
382  code: Catcode::MATH,
383  #[cfg(feature = "token-locators")]
384  loc: 0,
385});
386/// constant for an ALIGN "&" token
387#[thread_local]
388pub static TOKEN_ALIGN: Lazy<Token> = Lazy::new(|| Token {
389  text: arena::pin_static("&"),
390  code: Catcode::ALIGN,
391  #[cfg(feature = "token-locators")]
392  loc: 0,
393});
394/// constant for a PARAM "#" token
395#[thread_local]
396pub static TOKEN_PARAM: Lazy<Token> = Lazy::new(|| Token {
397  text: arena::pin_static("#"),
398  code: Catcode::PARAM,
399  #[cfg(feature = "token-locators")]
400  loc: 0,
401});
402/// constant for a SUPER "^" token
403#[thread_local]
404pub static TOKEN_SUPER: Lazy<Token> = Lazy::new(|| Token {
405  text: arena::pin_static("^"),
406  code: Catcode::SUPER,
407  #[cfg(feature = "token-locators")]
408  loc: 0,
409});
410/// constant for a SUB "_" token
411#[thread_local]
412pub static TOKEN_SUB: Lazy<Token> = Lazy::new(|| Token {
413  text: arena::pin_static("_"),
414  code: Catcode::SUB,
415  #[cfg(feature = "token-locators")]
416  loc: 0,
417});
418/// constant for a SPACE " " token
419#[thread_local]
420pub static TOKEN_SPACE: Lazy<Token> = Lazy::new(|| Token {
421  text: arena::pin_static(" "),
422  code: Catcode::SPACE,
423  #[cfg(feature = "token-locators")]
424  loc: 0,
425});
426/// constant for a CR "\n" token
427#[thread_local]
428pub static TOKEN_CR: Lazy<Token> = Lazy::new(|| Token {
429  text: arena::pin_static("\n"),
430  code: Catcode::SPACE,
431  #[cfg(feature = "token-locators")]
432  loc: 0,
433});
434/// constant for T_CS("\relax")
435#[thread_local]
436pub static TOKEN_RELAX: Lazy<Token> = Lazy::new(|| Token {
437  text: arena::pin_static("\\relax"),
438  code: Catcode::CS,
439  #[cfg(feature = "token-locators")]
440  loc: 0,
441});
442/// constant for T_CS("\expandafter")
443#[thread_local]
444pub static TOKEN_EXPANDAFTER: Lazy<Token> = Lazy::new(|| Token {
445  text: arena::pin_static("\\expandafter"),
446  code: Catcode::CS,
447  #[cfg(feature = "token-locators")]
448  loc: 0,
449});
450/// constant for T_CS("\endcsname")
451#[thread_local]
452pub static TOKEN_ENDCSNAME: Lazy<Token> = Lazy::new(|| Token {
453  text: arena::pin_static("\\endcsname"),
454  code: Catcode::CS,
455  #[cfg(feature = "token-locators")]
456  loc: 0,
457});
458
459/// Eagerly initialize this thread's pre-built `#[thread_local]` token
460/// constants. Each one's `Lazy` initializer interns its control-sequence
461/// name via `arena::pin_static`, so they must be forced AFTER
462/// [`arena::force_init`](crate::common::arena::force_init) and before any
463/// code accesses them during another root's initialization — otherwise the
464/// first access re-entrantly initializes the token from within that other
465/// root's init, the macOS `#[thread_local]` hazard (rust-lang/rust#29594,
466/// issue #217). No behavioral change on Linux.
467pub(crate) fn force_init() {
468  Lazy::force(&TOKEN_BEGIN);
469  Lazy::force(&TOKEN_END);
470  Lazy::force(&TOKEN_MATH);
471  Lazy::force(&TOKEN_ALIGN);
472  Lazy::force(&TOKEN_PARAM);
473  Lazy::force(&TOKEN_SUPER);
474  Lazy::force(&TOKEN_SUB);
475  Lazy::force(&TOKEN_SPACE);
476  Lazy::force(&TOKEN_CR);
477  Lazy::force(&TOKEN_RELAX);
478  Lazy::force(&TOKEN_EXPANDAFTER);
479  Lazy::force(&TOKEN_ENDCSNAME);
480}
481
482#[macro_export]
483/// macro for a BEGIN "{" token
484macro_rules! T_BEGIN(() => { *$crate::token::TOKEN_BEGIN });
485#[macro_export]
486/// macro for a new END "{" token
487macro_rules! T_END(() => { *$crate::token::TOKEN_END });
488/// macro for a MATH "$" token
489#[macro_export]
490macro_rules! T_MATH(() => { *$crate::token::TOKEN_MATH });
491/// macro for an ALIGN "&" token
492#[macro_export]
493macro_rules! T_ALIGN(() => { *$crate::token::TOKEN_ALIGN });
494/// macro for a PARAM "#" token
495#[macro_export]
496macro_rules! T_PARAM(() => { *$crate::token::TOKEN_PARAM });
497/// macro for a SUPER "^" token
498#[macro_export]
499macro_rules! T_SUPER(() => { *$crate::token::TOKEN_SUPER });
500/// macro for a SUB "_" token
501#[macro_export]
502macro_rules! T_SUB(() => { *$crate::token::TOKEN_SUB });
503/// macro for a SPACE token (default " ")
504#[macro_export]
505macro_rules! T_SPACE(() => { *$crate::token::TOKEN_SPACE };
506($text:literal) => {
507  Token { text: $crate::pin!($text), code: Catcode::SPACE,
508      #[cfg(feature = "token-locators")] loc: 0
509    }
510});
511/// macro for a CR "\n" token
512#[macro_export]
513macro_rules! T_CR(() => { *$crate::token::TOKEN_CR });
514/// macro for a LETTER token
515#[macro_export]
516macro_rules! T_LETTER {
517  ($text:literal) => {
518    Token {
519      text: $crate::pin!($text),
520      code: Catcode::LETTER,
521      #[cfg(feature = "token-locators")]
522      loc: 0,
523    }
524  };
525  ($text:expr_2021) => {
526    Token {
527      text: $crate::common::arena::pin($text),
528      code: Catcode::LETTER,
529      #[cfg(feature = "token-locators")]
530      loc: 0,
531    }
532  };
533}
534/// macro for an OTHER code token
535#[macro_export]
536macro_rules! T_OTHER {
537  ($text:literal) => {
538    Token {
539      text: $crate::pin!($text),
540      code: Catcode::OTHER,
541      #[cfg(feature = "token-locators")]
542      loc: 0,
543    }
544  };
545  ($text:expr_2021) => {
546    Token {
547      text: $crate::common::arena::pin($text),
548      code: Catcode::OTHER,
549      #[cfg(feature = "token-locators")]
550      loc: 0,
551    }
552  };
553}
554/// T_OTHER from a single character
555#[macro_export]
556macro_rules! T_OTHER_CHAR {
557  ($text:literal) => {
558    Token {
559      text: $crate::common::arena::pin_char($text),
560      code: Catcode::OTHER,
561      #[cfg(feature = "token-locators")]
562      loc: 0,
563    }
564  };
565}
566/// macro for an ACTIVE char token
567#[macro_export]
568macro_rules! T_ACTIVE {
569  ($c:expr_2021) => {{
570    let mut tmp = [0u8; 4];
571    let s = $c.encode_utf8(&mut tmp);
572    Token {
573      text: $crate::common::arena::pin(s),
574      code: Catcode::ACTIVE,
575      #[cfg(feature = "token-locators")]
576      loc: 0,
577    }
578  }};
579}
580/// macro for a COMMENT content token
581#[macro_export]
582macro_rules! T_COMMENT {
583  ($text:expr_2021) => {
584    Token {
585      text: $crate::common::arena::pin($text),
586      code: Catcode::COMMENT,
587      #[cfg(feature = "token-locators")]
588      loc: 0,
589    }
590  };
591}
592/// macro for a command sequence token
593#[macro_export]
594macro_rules! T_CS {
595  ($text:literal) => {
596    $crate::token::Token {
597      text: $crate::pin!($text),
598      code: $crate::token::Catcode::CS,
599      #[cfg(feature = "token-locators")]
600      loc: 0,
601    }
602  };
603  ($text:expr_2021) => {
604    $crate::token::Token {
605      text: $crate::common::arena::pin($text),
606      code: $crate::token::Catcode::CS,
607      #[cfg(feature = "token-locators")]
608      loc: 0,
609    }
610  };
611}
612
613/// macro for T_CS("\\relax")
614#[macro_export]
615macro_rules! T_RELAX(() => { $crate::token::TOKEN_RELAX.clone() });
616
617/// macro for a tracing MARKER token
618#[macro_export]
619macro_rules! T_MARKER {
620  ($text:expr_2021) => {
621    Token {
622      text: $crate::common::arena::pin($text),
623      code: Catcode::MARKER,
624      #[cfg(feature = "token-locators")]
625      loc: 0,
626    }
627  };
628}
629
630/// macro for a numbered ARG token
631#[macro_export]
632macro_rules! T_ARG {
633  ($text:expr_2021) => {
634    Token {
635      text: $crate::common::arena::pin($text.to_string()),
636      code: Catcode::ARG,
637      #[cfg(feature = "token-locators")]
638      loc: 0,
639    }
640  };
641}
642
643/// Token constructor macro (defaults to OTHER code)
644#[macro_export]
645macro_rules! Token {
646  ($text:expr_2021) => {
647    Token!($text, Catcode::OTHER)
648  };
649  ($text:literal, $cc:expr_2021) => {
650    Token {
651      text: $crate::pin!($text),
652      code: $cc,
653      #[cfg(feature = "token-locators")]
654      loc: 0,
655    }
656  };
657  ($text:expr_2021, $cc:expr_2021) => {
658    Token {
659      text: $crate::common::arena::pin($text),
660      code: $cc,
661      #[cfg(feature = "token-locators")]
662      loc: 0,
663    }
664  };
665}
666
667/// Special case: a character needs swift string conversion, so let's use a dedicated macro
668#[macro_export]
669macro_rules! CharToken {
670  ($c:expr_2021) => {
671    CharToken!($c, Catcode::OTHER)
672  };
673  ($c:expr_2021, $cc:expr_2021) => {{
674    let mut tmp = [0u8; 4];
675    let s = $c.encode_utf8(&mut tmp);
676    Token!(s, $cc)
677  }};
678}
679
680/// Explode a string into a list of tokens, all w/catcode OTHER (except space).
681/// Note: newlines are converted to OTHER, NOT SPACE (Perl #2700 reverted #2646).
682/// ^^J in TeX decodes to CC_OTHER by default; let the tokenizer handle catcode
683/// reassignment if needed.
684#[macro_export]
685macro_rules! Explode(($text:expr_2021) => (
686  $text.to_string().chars().map(|c|
687    if c==' ' { T_SPACE!() }
688    else {
689      CharToken!(c)
690    }
691  ).collect::<Vec<Token>>()
692));
693
694#[macro_export]
695macro_rules! ExplodeChars(($text:expr_2021) => (
696  $text.as_str().chars().map(|c|
697    if c==' ' { T_SPACE!() }
698    else {
699      CharToken!(c)
700    }
701  ).collect::<Vec<Token>>()
702));
703
704/// Similar to Explode, but convert letters to catcode LETTER and others to OTHER
705/// Hopefully, this is essentially correct WITHOUT resorting to catcode lookup?
706/// Perl sync: newlines are OTHER, not SPACE (matches Perl #2700 revert of #2646).
707#[macro_export]
708macro_rules! ExplodeText(
709  ($text:expr_2021) => ({
710  use $crate::token::{Catcode,Token};
711  $text.to_string().chars().map(|c|
712    if c==' ' { T_SPACE!() }
713    else {
714      let mut tmp = [0u8; 4];
715      let s = c.encode_utf8(&mut tmp);
716      if c.is_alphabetic() {
717      T_LETTER!(s) }
718    else { T_OTHER!(s) }}
719  ).collect::<Vec<Token>>()
720}));
721
722#[macro_export]
723macro_rules! SymExplodeText(
724  ($sym:expr_2021) => ({
725  use $crate::token::{Catcode,Token};
726  let chars : Vec<char> = arena::with($sym, |text| text.chars().collect());
727  chars.into_iter().map(|c|
728    if c==' ' { T_SPACE!() }
729    else {
730      let mut tmp = [0u8; 4];
731      let s = c.encode_utf8(&mut tmp);
732      if c.is_alphabetic() {
733      T_LETTER!(s) }
734    else { T_OTHER!(s) }}
735  ).collect::<Vec<Token>>()
736}));
737
738// static UNTEX_LINELENGTH: usize = 78; // [CONSTANT]
739
740impl Default for Token {
741  fn default() -> Self {
742    Token {
743      text: arena::pin_static("EXPECTED_TOKEN"),
744      code: Catcode::OTHER,
745      #[cfg(feature = "token-locators")]
746      loc: 0,
747    }
748  }
749}
750
751///======================================================================
752/// Accessors.
753impl Token {
754  /// simple Token constructor, wrapping over text and catcode
755  pub fn new<T: AsRef<str>>(text: T, code: Catcode) -> Self {
756    Token {
757      text: arena::pin(text),
758      code,
759      #[cfg(feature = "token-locators")]
760      loc: 0,
761    }
762  }
763
764  /// A cheap structural fingerprint for the cycle-detection guard
765  /// ([`crate::cycle_guard`]). Matches [`PartialEq`] semantics: SPACE tokens
766  /// fingerprint by catcode alone (their text is irrelevant to equality).
767  /// NOT a stable hash across processes — for in-run loop detection only.
768  #[inline]
769  pub fn cycle_fingerprint(&self) -> u64 {
770    use std::hash::{Hash, Hasher};
771    let mut h = rustc_hash::FxHasher::default();
772    self.code.hash(&mut h);
773    if self.code != Catcode::SPACE {
774      self.text.hash(&mut h);
775    }
776    h.finish()
777  }
778
779  /// Get the CS Name of the token. This is the name that definitions will be
780  /// stored under; It's the same for various `different' BEGIN tokens, eg.
781  pub fn get_cs_name(&self) -> SymStr {
782    if self.code.is_primitive() {
783      self.code.name_sym()
784    } else {
785      self.get_sym()
786    }
787  }
788
789  /// Execute a closure using the CS Name of the token.
790  /// This is the name that definitions will be stored under;
791  /// It's the same for various `different' BEGIN tokens, eg.
792  pub fn with_cs_name<R, FnR>(&self, caller: FnR) -> R
793  where FnR: FnOnce(&str) -> R {
794    if self.code.is_primitive() {
795      caller(self.code.name())
796    } else {
797      self.with_str(caller)
798    }
799  }
800
801  /// artificial, but avoids the data race
802  pub fn pin_cs_name(&self) -> SymStr {
803    if self.code.is_primitive() {
804      self.code.name_sym()
805    } else {
806      self.get_sym()
807    }
808  }
809
810  /// Get the fixed name of a primitive catcode, or empty string otherwise
811  pub fn get_primitive_name(&self) -> Option<&'static str> {
812    if self.code.is_primitive() {
813      Some(self.code.name())
814    } else {
815      None
816    }
817  }
818
819  /// Get the CS name only if the catcode is executable!
820  pub fn get_executable_name(&self) -> String {
821    let cc = self.code;
822    if cc.is_executable() {
823      self
824        .get_primitive_name()
825        .map(ToString::to_string)
826        .unwrap_or_else(|| self.with_str(|text| text.to_string()))
827    } else {
828      String::new()
829    }
830  }
831
832  /// Intersect executable and primitive
833  pub fn get_executable_primitive_name(&self) -> Option<&'static str> {
834    let cc = self.code;
835    if cc.is_executable() && cc.is_primitive() {
836      Some(self.code.name())
837    } else {
838      None
839    }
840  }
841
842  /// Use the ticket representing the interned "text" of the token
843  pub fn get_sym(&self) -> SymStr { self.text }
844  /// Use the interned &str "text" of the token
845  /// use `to_string` instead for an owned String with simpler
846  pub fn with_str<R, FnR>(&self, caller: FnR) -> R
847  where FnR: FnOnce(&str) -> R {
848    arena::with(self.text, caller)
849  }
850
851  /// True for any member of the `\special_relax` no-expand family — the
852  /// representation of a `\noexpand`'d (expandable or undefined) CS/active token.
853  /// Such a token is a CS whose NAME is `\special_relax` `\x01` `<shadowed text>`,
854  /// carrying the shadowed token's identity PER-TOKEN (faithful to TeX's
855  /// `no_expand_flag`, which preserves `cur_cs`), while the whole family resolves
856  /// to `\special_relax`'s `\relax` meaning ([`crate::state::lookup_meaning`]
857  /// fallback). The bare `\special_relax` (no suffix) is the no-shadow case
858  /// (`\dont_expand` at end-of-input). `\x01` is never valid in a CS name or as
859  /// an active char, so the encoding is unambiguous.
860  pub fn is_noexpand_family(&self) -> bool {
861    self.code == Catcode::CS
862      && self.with_str(|s| {
863        s.starts_with(NOEXPAND_PREFIX)
864          && (s.len() == NOEXPAND_PREFIX.len()
865            || s.as_bytes()[NOEXPAND_PREFIX.len()] == NOEXPAND_SEP)
866      })
867  }
868
869  /// Recover the shadowed token from a `\special_relax`-family token, if it
870  /// shadows one (i.e. not the bare `\special_relax`). The shadowed token is a
871  /// CS (text begins `\`) or an active char.
872  pub fn noexpand_shadowed(&self) -> Option<Token> {
873    if self.code != Catcode::CS {
874      return None;
875    }
876    // Compute the owned shadowed name + its catcode INSIDE the `with_str`
877    // borrow, then `arena::pin` it OUTSIDE. `with_str`'s `s` (hence `rest`)
878    // aliases the arena's append-only buffer; a re-entrant `arena::pin(rest)`
879    // could reallocate that buffer mid-intern and read freed memory. Owning the
880    // name first (and pinning after the borrow ends) removes the hazard — and
881    // satisfies clippy::unnecessary_to_owned, which can't see the aliasing.
882    let (name, code) = self.with_str(|s| {
883      let rest = s.strip_prefix(NOEXPAND_PREFIX)?;
884      let rest = rest.strip_prefix(NOEXPAND_SEP as char)?;
885      let code = if rest.starts_with('\\') {
886        Catcode::CS
887      } else {
888        Catcode::ACTIVE
889      };
890      Some((rest.to_string(), code))
891    })?;
892    Some(Token {
893      text: arena::pin(name),
894      code,
895      #[cfg(feature = "token-locators")]
896      loc: 0,
897    })
898  }
899
900  /// Return the character code of  character part of the token, or 256 if it is a control
901  /// sequence
902  pub fn get_charcode(&self) -> u32 {
903    if self.code == Catcode::CS {
904      256
905    } else {
906      self.with_str(|text| {
907        if let Some(c) = text.chars().next() {
908          c as u32
909        } else {
910          0
911        }
912      })
913    }
914  }
915
916  /// Return the catcode of the token.
917  pub fn get_catcode(&self) -> Catcode { self.code }
918  /// is the current one
919  pub fn is_executable(&self) -> bool { self.code.is_executable() }
920
921  /// neutralize really should only retroactively imitate what Semiverbatim would have done.
922  /// So, it needs to neutralize those in SPECIALS
923  /// NOTE that although '%' gets it's catcode changed in Semiverbatim,
924  /// I'm pretty sure we do NOT want to neutralize comments (turn them into Catcode::OTHER)
925  /// here, since if comments do get into the Tokens, that will introduce weird crap into the
926  /// stream.
927  pub fn neutralize(self, extraspecials: &[char]) -> Token {
928    let first_c: Option<char> = self.with_str(|text| text.chars().next());
929    let ch = match first_c {
930      Some(ch) => ch,
931      None => return self,
932    };
933    let cc = self.code;
934    if cc.is_neutralizable() {
935      for extra in extraspecials {
936        if extra == &ch {
937          let mut tmp = [0u8; 4];
938          let s = ch.encode_utf8(&mut tmp);
939          return T_OTHER!(s);
940        }
941      }
942      let maybe_return = state::with_value("SPECIALS", |specials_opt| {
943        if let Some(Stored::Chars(specials_list)) = specials_opt {
944          for special in specials_list.iter() {
945            if *special == ch {
946              let mut tmp = [0u8; 4];
947              let s = ch.encode_utf8(&mut tmp);
948              return Some(T_OTHER!(s));
949            }
950          }
951        }
952        None
953      });
954      if let Some(token) = maybe_return {
955        return token;
956      }
957    }
958    self
959  }
960
961  pub fn as_other(&self) -> Token {
962    Token {
963      text: self.text,
964      code: Catcode::OTHER,
965      #[cfg(feature = "token-locators")]
966      loc: 0,
967    }
968  }
969  pub fn as_cs(&self) -> Token {
970    Token {
971      text: self.text,
972      code: Catcode::CS,
973      #[cfg(feature = "token-locators")]
974      loc: 0,
975    }
976  }
977
978  pub fn substitute_parameters(self, args: &[&Token]) -> Self {
979    if self.code == Catcode::ARG {
980      self.with_str(|text| {
981        let arg_idx = text
982          .parse::<usize>()
983          .expect("ARG catcode tokens should always contain numeric literals as text");
984        *args[arg_idx - 1]
985      })
986    } else {
987      self
988    }
989  }
990
991  /// A Token reverts to itself
992  pub fn revert(self) -> Token { self }
993
994  /// A string form which is primarily used for error-reporting
995  pub fn stringify(&self) -> String {
996    self.with_str(|text| {
997      // Make the token's char content more printable, since this is for a visual messages.
998      let display_text = if text.len() == 1 {
999        let c = text.chars().next().unwrap() as u16;
1000        if c < 0x020 {
1001          Cow::Owned(s!("U+{:04x}/{}", c, CONTROLNAME[c as usize]))
1002        } else {
1003          Cow::Borrowed(text)
1004        }
1005      } else {
1006        Cow::Borrowed(text)
1007      };
1008      s!("{}[{}]", self.code.short_name(), display_text)
1009    })
1010  }
1011
1012  pub fn to_register(&self) -> Option<Rc<Register>> { state::lookup_register_definition(self) }
1013
1014  pub fn to_number(&self) -> Number {
1015    Number::new(self.with_str(|text| text.parse::<i64>()).unwrap_or(0))
1016  }
1017
1018  pub fn to_dimension(&self) -> Dimension {
1019    Dimension::new_f64(self.with_str(|text| text.parse::<f64>().unwrap_or(0.0)))
1020  }
1021
1022  pub fn to_mu_dimension(&self) -> MuDimension {
1023    MuDimension::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1024  }
1025
1026  pub fn to_glue(&self) -> Glue {
1027    Glue::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1028  }
1029
1030  pub fn to_mu_glue(&self) -> MuGlue {
1031    MuGlue::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1032  }
1033
1034  pub fn to_float(&self) -> Float {
1035    Float::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1036  }
1037
1038  pub fn be_digested(self) -> Result<Digested> { crate::stomach::digest(Tokens::new(vec![self])) }
1039
1040  /// Check whether the current token is defined as `other`.
1041  /// That is, whether it is equal to `other`, or \let to `other`.
1042  /// `other` is is presumed to be some "constant", explicit token,
1043  /// such as  `T_SPACE` or `T_CS!("\\endcsname")`.
1044  pub fn defined_as(&self, other: &Token) -> bool {
1045    let cc = self.code;
1046    let occ = other.get_catcode();
1047    if (cc == occ) && ((occ == Catcode::SPACE) || (self.text == other.get_sym())) {
1048      return true;
1049    }
1050    if matches!(cc, Catcode::CS | Catcode::ACTIVE) {
1051      // Use the closure-based `with_meaning` — Token is `Copy`, so
1052      // extracting a `Token` from the borrowed Stored is an implicit
1053      // copy, not a clone. Avoids a full `Stored::clone()` per call
1054      // (defined_as fires ~1% of total instructions on siunitx/
1055      // physics-heavy docs).
1056      let letto_opt: Option<Token> = state::with_meaning(self, |defn_opt| {
1057        defn_opt.and_then(|defn| match defn {
1058          Stored::Token(t) => Some(*t),
1059          Stored::Expandable(inner) => Some(*inner.get_cs()),
1060          Stored::Primitive(inner) => Some(*inner.get_cs()),
1061          Stored::MathPrimitive(inner) => Some(*inner.get_cs()),
1062          Stored::Register(inner) => Some(*inner.get_cs()),
1063          Stored::Conditional(inner) => Some(*inner.get_cs()),
1064          Stored::Constructor(inner) => Some(*inner.get_cs()),
1065          _ => None,
1066        })
1067      });
1068      if let Some(letto) = letto_opt
1069        && (letto.get_catcode() == occ)
1070        && ((occ == Catcode::SPACE) || letto.get_sym() == other.get_sym())
1071      {
1072        return true;
1073      }
1074    }
1075    false
1076  }
1077}
1078
1079// A simple (constant!) auto-cast for &str to Token. Beware this will not respect the current
1080// catcodes in state (and @ is OTHER).
1081impl From<&str> for Token {
1082  fn from(text: &str) -> Token {
1083    match text.chars().next() {
1084      Some('{') => T_BEGIN!(),
1085      Some('}') => T_END!(),
1086      Some('$') => T_MATH!(),
1087      Some('#') => T_PARAM!(),
1088      Some('&') => T_ALIGN!(),
1089      Some('^') => T_SUPER!(),
1090      Some('_') => T_SUB!(),
1091      Some('\\') => T_CS!(text),
1092      Some('%') => T_COMMENT!(text),
1093      _ => {
1094        if text.chars().all(|c| c.is_alphabetic()) {
1095          T_LETTER!(text)
1096        } else if text.chars().all(|c| c.is_whitespace()) {
1097          T_SPACE!()
1098        } else {
1099          T_OTHER!(text)
1100        }
1101      },
1102    }
1103  }
1104}
1105
1106// ── Token-origin side arena (token-locators feature) ───────────────────────
1107// Per-conversion store mapping a Token's `loc` handle (1-based; 0 = none) to its
1108// captured source start. Tokens carry only the u32 handle (Token stays 12 bytes);
1109// this holds the (source, line, col). Appended in `read_token`, read by the
1110// digestion consumer to give a text run its true span. Cleared per conversion.
1111// See docs/performance/SOURCE_PROVENANCE.md §3.1.1.
1112#[cfg(feature = "token-locators")]
1113#[derive(Clone, Copy, Debug)]
1114pub struct TokenStart {
1115  pub source:    SymStr,
1116  pub line:      u32,
1117  pub col:       u32,
1118  /// `true` when this origin was *inherited* from a macro invocation rather
1119  /// than read directly from a mouth — i.e. the token is synthesized
1120  /// expansion output (`\today → "May 25, 2026"`) attributed to its `\today`
1121  /// call site. The content-range recovery in `constructor::child_span`
1122  /// prefers genuine (read-from-source) origins and only falls back to
1123  /// inherited ones, so a `\section{Intro}`'s structural body literals — now
1124  /// carrying an inherited origin — never widen the title's content-exact
1125  /// span. See docs/performance/SOURCE_PROVENANCE.md §3.1.3.
1126  pub inherited: bool,
1127}
1128
1129#[cfg(feature = "token-locators")]
1130thread_local! {
1131  static TOKEN_ORIGINS: std::cell::RefCell<Vec<TokenStart>> =
1132    const { std::cell::RefCell::new(Vec::new()) };
1133}
1134
1135/// Append a token's source start, returning its 1-based handle (`0` is reserved
1136/// for "no origin"). Only called on the source-map precision path.
1137#[cfg(feature = "token-locators")]
1138pub fn push_token_origin(source: SymStr, line: u32, col: u32) -> u32 {
1139  TOKEN_ORIGINS.with(|o| {
1140    let mut v = o.borrow_mut();
1141    v.push(TokenStart {
1142      source,
1143      line,
1144      col,
1145      inherited: false,
1146    });
1147    v.len() as u32 // index + 1
1148  })
1149}
1150
1151/// Derive an *inherited* origin from an existing handle: look up the
1152/// invocation token's start, push a copy flagged `inherited`, and return its
1153/// new handle (`0` if `handle` is the no-origin sentinel or out of range). One
1154/// call per macro expansion; the returned handle is shared by every synthesized
1155/// result token. See `push_token_origin` and docs/performance/SOURCE_PROVENANCE.md §3.1.3.
1156#[cfg(feature = "token-locators")]
1157pub fn push_inherited_origin(handle: u32) -> u32 {
1158  if handle == 0 {
1159    return 0;
1160  }
1161  TOKEN_ORIGINS.with(|o| {
1162    let mut v = o.borrow_mut();
1163    let Some(mut start) = v.get((handle - 1) as usize).copied() else {
1164      return 0;
1165    };
1166    start.inherited = true;
1167    v.push(start);
1168    v.len() as u32
1169  })
1170}
1171
1172/// Resolve a token `loc` handle to its origin (`None` for the `0` sentinel or an
1173/// out-of-range handle).
1174#[cfg(feature = "token-locators")]
1175pub fn get_token_origin(handle: u32) -> Option<TokenStart> {
1176  if handle == 0 {
1177    return None;
1178  }
1179  TOKEN_ORIGINS.with(|o| o.borrow().get((handle - 1) as usize).copied())
1180}
1181
1182/// Reset the arena at the start of a conversion (handles are per-conversion).
1183#[cfg(feature = "token-locators")]
1184pub fn clear_token_origins() { TOKEN_ORIGINS.with(|o| o.borrow_mut().clear()); }
1185
1186#[cfg(test)]
1187mod tests {
1188  use super::*;
1189
1190  /// `Token` size invariant (docs/performance/SOURCE_PROVENANCE.md §3.1.1): 8 bytes by
1191  /// default (`SymStr` + `Catcode`), 12 only under the `token-locators`
1192  /// precision build (+ the `u32` origin handle). Guards the corpus/parity/
1193  /// distribution builds against an accidental widening.
1194  #[test]
1195  fn token_size_invariant() {
1196    #[cfg(not(feature = "token-locators"))]
1197    assert_eq!(
1198      size_of::<Token>(),
1199      8,
1200      "default Token must stay 8 bytes (SymStr + Catcode)"
1201    );
1202    #[cfg(feature = "token-locators")]
1203    assert_eq!(
1204      std::mem::size_of::<Token>(),
1205      12,
1206      "token-locators Token is 8 + a u32 origin handle"
1207    );
1208  }
1209
1210  /// Per-token origin capture (token-locators): each char token read from a
1211  /// mouth carries a handle resolving to its exact (line, col). This is the leaf
1212  /// accuracy that mouth-snapshot (Experiments 1–2) and digested-child assembly
1213  /// (Experiment 3) could not provide — the position now travels *with the
1214  /// token*. See docs/performance/SOURCE_PROVENANCE.md §3.1.1.
1215  #[cfg(feature = "token-locators")]
1216  #[test]
1217  fn token_origin_capture() {
1218    super::clear_token_origins();
1219    // "Hello" — five letters at 1-indexed columns 1..=5 on line 1.
1220    let toks = crate::mouth::tokenize("Hello");
1221    let got: Vec<(u32, u32)> = toks
1222      .unlist_ref()
1223      .iter()
1224      .map(|t| {
1225        let o = super::get_token_origin(t.loc).expect("token carries an origin handle");
1226        (o.line, o.col)
1227      })
1228      .collect();
1229    assert_eq!(
1230      got,
1231      vec![(1, 1), (1, 2), (1, 3), (1, 4), (1, 5)],
1232      "each letter's captured (line, col) must be exact"
1233    );
1234  }
1235
1236  #[test]
1237  fn catcode_name_covers_all_variants() {
1238    // Ensure every Catcode variant produces a non-empty name.
1239    use Catcode::*;
1240    for cc in [
1241      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE, IGNORE, LETTER, OTHER,
1242      ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1243    ] {
1244      assert!(!cc.name().is_empty(), "{cc:?}.name() is empty");
1245    }
1246  }
1247
1248  #[test]
1249  fn catcode_name_specific_values() {
1250    assert_eq!(Catcode::ESCAPE.name(), "Escape");
1251    assert_eq!(Catcode::BEGIN.name(), "Begin");
1252    assert_eq!(Catcode::CS.name(), "ControlSequence");
1253    assert_eq!(Catcode::LETTER.name(), "Letter");
1254  }
1255
1256  #[test]
1257  fn catcode_short_name_starts_with_t_prefix() {
1258    use Catcode::*;
1259    for cc in [
1260      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE, IGNORE, LETTER, OTHER,
1261      ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1262    ] {
1263      assert!(
1264        cc.short_name().starts_with("T_"),
1265        "{cc:?}.short_name() = {} lacks T_ prefix",
1266        cc.short_name()
1267      );
1268    }
1269  }
1270
1271  #[test]
1272  fn catcode_meaning_mostly_nonempty() {
1273    // Most variants have a TeX-meaning description.
1274    assert!(!Catcode::ESCAPE.meaning().is_empty());
1275    assert!(!Catcode::LETTER.meaning().is_empty());
1276    assert!(!Catcode::OTHER.meaning().is_empty());
1277    // A few (e.g. CS/MARKER/ARG) fall through to "".
1278    assert_eq!(Catcode::CS.meaning(), "");
1279  }
1280
1281  #[test]
1282  fn is_primitive_checks() {
1283    use Catcode::*;
1284    // TeX primitives:
1285    for cc in [
1286      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE,
1287    ] {
1288      assert!(cc.is_primitive(), "{cc:?} should be primitive");
1289    }
1290    // Non-primitives:
1291    for cc in [
1292      IGNORE, LETTER, OTHER, ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1293    ] {
1294      assert!(!cc.is_primitive(), "{cc:?} should not be primitive");
1295    }
1296  }
1297
1298  #[test]
1299  fn is_executable_checks() {
1300    use Catcode::*;
1301    for cc in [BEGIN, END, MATH, ALIGN, SUPER, SUB, ACTIVE, CS] {
1302      assert!(cc.is_executable(), "{cc:?} should be executable");
1303    }
1304    for cc in [
1305      EOL, ESCAPE, PARAM, SPACE, IGNORE, LETTER, OTHER, COMMENT, INVALID, MARKER, ARG,
1306    ] {
1307      assert!(!cc.is_executable(), "{cc:?} should not be executable");
1308    }
1309  }
1310
1311  #[test]
1312  fn is_neutralizable_set() {
1313    use Catcode::*;
1314    for cc in [MATH, ALIGN, PARAM, SUPER, SUB, ACTIVE] {
1315      assert!(cc.is_neutralizable(), "{cc:?}");
1316    }
1317    assert!(!CS.is_neutralizable());
1318    assert!(!LETTER.is_neutralizable());
1319  }
1320
1321  #[test]
1322  fn is_active_or_cs_narrow_set() {
1323    assert!(Catcode::ACTIVE.is_active_or_cs());
1324    assert!(Catcode::CS.is_active_or_cs());
1325    assert!(!Catcode::LETTER.is_active_or_cs());
1326    assert!(!Catcode::ESCAPE.is_active_or_cs());
1327  }
1328
1329  #[test]
1330  fn is_absorbable_space_letter_other_comment() {
1331    use Catcode::*;
1332    assert!(SPACE.is_absorbable());
1333    assert!(LETTER.is_absorbable());
1334    assert!(OTHER.is_absorbable());
1335    assert!(COMMENT.is_absorbable());
1336    // All else not absorbable.
1337    assert!(!CS.is_absorbable());
1338    assert!(!BEGIN.is_absorbable());
1339  }
1340
1341  #[test]
1342  fn is_gullet_holdable_comment_marker_only() {
1343    assert!(Catcode::COMMENT.is_gullet_holdable());
1344    assert!(Catcode::MARKER.is_gullet_holdable());
1345    assert!(!Catcode::SPACE.is_gullet_holdable());
1346    assert!(!Catcode::LETTER.is_gullet_holdable());
1347  }
1348
1349  #[test]
1350  fn is_balanced_interesting_begin_end_marker() {
1351    assert!(Catcode::BEGIN.is_balanced_interesting());
1352    assert!(Catcode::END.is_balanced_interesting());
1353    assert!(Catcode::MARKER.is_balanced_interesting());
1354    assert!(!Catcode::LETTER.is_balanced_interesting());
1355    assert!(!Catcode::MATH.is_balanced_interesting());
1356  }
1357
1358  #[test]
1359  fn catcode_u8_roundtrip() {
1360    // From<Catcode> for u8 + From<u8> for Catcode should round-trip
1361    // (at least for the documented range 0..=18).
1362    use Catcode::*;
1363    for cc in [
1364      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE, IGNORE, LETTER, OTHER,
1365      ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1366    ] {
1367      let b: u8 = cc.into();
1368      let cc2: Catcode = b.into();
1369      assert_eq!(cc, cc2, "roundtrip broke for {cc:?} (u8={b})");
1370    }
1371  }
1372
1373  #[test]
1374  fn token_new_and_display() {
1375    let t = Token::new("foo", Catcode::LETTER);
1376    assert_eq!(format!("{t}"), "foo");
1377    assert_eq!(t.code, Catcode::LETTER);
1378  }
1379
1380  #[test]
1381  fn token_arg_display_prepends_hash() {
1382    // ARG catcode prepends # in Display.
1383    let t = Token::new("1", Catcode::ARG);
1384    assert_eq!(format!("{t}"), "#1");
1385  }
1386}