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// Per-symbol memo for `Token::is_noexpand_family`, indexed by the symbol's
334// arena index: 0 = not yet computed, 1 = not in the family, 2 = in the family.
335// Sound because the arena is append-only (a symbol's text never changes);
336// MUST be cleared alongside `arena::reset()` (see
337// `reset_noexpand_family_memo`, called from `reset_thread_engine`) since a
338// reset renumbers symbols.
339thread_local! {
340  static NOEXPAND_FAMILY_MEMO: std::cell::RefCell<Vec<u8>> =
341    const { std::cell::RefCell::new(Vec::new()) };
342}
343
344/// Clear the [`Token::is_noexpand_family`] per-symbol memo. Companion to
345/// `arena::reset()` — symbol indices are reused after a reset, so stale
346/// entries would alias unrelated strings (same bug class as the REPORT-map
347/// fix, `7b64a48ad1`).
348pub fn reset_noexpand_family_memo() { NOEXPAND_FAMILY_MEMO.with(|m| m.borrow_mut().clear()); }
349
350/// Name of the bare no-op control sequence `\noexpand` collapses to, and the
351/// prefix of every member of the no-expand family. See [`Token::is_noexpand_family`].
352pub const NOEXPAND_PREFIX: &str = "\\special_relax";
353/// Separator byte between the `\special_relax` prefix and the shadowed token's
354/// text in a family token's name. `\x01` is never valid in a CS name or active
355/// char, so `\special_relax\x01\foo` is unambiguous and cannot collide with a
356/// real CS such as a (hypothetical) `\special_relaxfoo`.
357pub const NOEXPAND_SEP: u8 = 1;
358
359/// Build the no-expand family token shadowing `shadowed` — the per-token,
360/// dump-safe representation of `\noexpand <shadowed>` (faithful to TeX's
361/// `no_expand_flag`: the shadowed identity is preserved in the name; the family
362/// resolves to `\relax` meaning). `shadowed` is an expandable/undefined CS or
363/// active token (the only things `\noexpand` wraps; see `is_dont_expandable`).
364pub fn noexpand_family(shadowed: &Token) -> Token {
365  let text = shadowed.with_str(|s| format!("{NOEXPAND_PREFIX}{}{s}", NOEXPAND_SEP as char));
366  Token {
367    text: arena::pin(text),
368    code: Catcode::CS,
369    #[cfg(feature = "token-locators")]
370    loc: 0,
371  }
372}
373
374// Note: given that we are pinning the strings in an arena,
375//  once we have a token of a certain kind it is now faster to clone
376//  a known token than it is to build a new one
377//  (as the arena lookup is a hair slower than copying a u32)
378
379/// constant for a BEGIN "{" token
380#[thread_local]
381pub static TOKEN_BEGIN: Lazy<Token> = Lazy::new(|| Token {
382  text: arena::pin_static("{"),
383  code: Catcode::BEGIN,
384  #[cfg(feature = "token-locators")]
385  loc: 0,
386});
387/// constant for an END "}" token
388#[thread_local]
389pub static TOKEN_END: Lazy<Token> = Lazy::new(|| Token {
390  text: arena::pin_static("}"),
391  code: Catcode::END,
392  #[cfg(feature = "token-locators")]
393  loc: 0,
394});
395/// constant for a MATH "$" token
396#[thread_local]
397pub static TOKEN_MATH: Lazy<Token> = Lazy::new(|| Token {
398  text: arena::pin_static("$"),
399  code: Catcode::MATH,
400  #[cfg(feature = "token-locators")]
401  loc: 0,
402});
403/// constant for an ALIGN "&" token
404#[thread_local]
405pub static TOKEN_ALIGN: Lazy<Token> = Lazy::new(|| Token {
406  text: arena::pin_static("&"),
407  code: Catcode::ALIGN,
408  #[cfg(feature = "token-locators")]
409  loc: 0,
410});
411/// constant for a PARAM "#" token
412#[thread_local]
413pub static TOKEN_PARAM: Lazy<Token> = Lazy::new(|| Token {
414  text: arena::pin_static("#"),
415  code: Catcode::PARAM,
416  #[cfg(feature = "token-locators")]
417  loc: 0,
418});
419/// constant for a SUPER "^" token
420#[thread_local]
421pub static TOKEN_SUPER: Lazy<Token> = Lazy::new(|| Token {
422  text: arena::pin_static("^"),
423  code: Catcode::SUPER,
424  #[cfg(feature = "token-locators")]
425  loc: 0,
426});
427/// constant for a SUB "_" token
428#[thread_local]
429pub static TOKEN_SUB: Lazy<Token> = Lazy::new(|| Token {
430  text: arena::pin_static("_"),
431  code: Catcode::SUB,
432  #[cfg(feature = "token-locators")]
433  loc: 0,
434});
435/// constant for a SPACE " " token
436#[thread_local]
437pub static TOKEN_SPACE: Lazy<Token> = Lazy::new(|| Token {
438  text: arena::pin_static(" "),
439  code: Catcode::SPACE,
440  #[cfg(feature = "token-locators")]
441  loc: 0,
442});
443/// constant for a CR "\n" token
444#[thread_local]
445pub static TOKEN_CR: Lazy<Token> = Lazy::new(|| Token {
446  text: arena::pin_static("\n"),
447  code: Catcode::SPACE,
448  #[cfg(feature = "token-locators")]
449  loc: 0,
450});
451/// constant for T_CS("\relax")
452#[thread_local]
453pub static TOKEN_RELAX: Lazy<Token> = Lazy::new(|| Token {
454  text: arena::pin_static("\\relax"),
455  code: Catcode::CS,
456  #[cfg(feature = "token-locators")]
457  loc: 0,
458});
459/// constant for T_CS("\expandafter")
460#[thread_local]
461pub static TOKEN_EXPANDAFTER: Lazy<Token> = Lazy::new(|| Token {
462  text: arena::pin_static("\\expandafter"),
463  code: Catcode::CS,
464  #[cfg(feature = "token-locators")]
465  loc: 0,
466});
467/// constant for T_CS("\endcsname")
468#[thread_local]
469pub static TOKEN_ENDCSNAME: Lazy<Token> = Lazy::new(|| Token {
470  text: arena::pin_static("\\endcsname"),
471  code: Catcode::CS,
472  #[cfg(feature = "token-locators")]
473  loc: 0,
474});
475
476/// Eagerly initialize this thread's pre-built `#[thread_local]` token
477/// constants. Each one's `Lazy` initializer interns its control-sequence
478/// name via `arena::pin_static`, so they must be forced AFTER
479/// [`arena::force_init`](crate::common::arena::force_init) and before any
480/// code accesses them during another root's initialization — otherwise the
481/// first access re-entrantly initializes the token from within that other
482/// root's init, the macOS `#[thread_local]` hazard (rust-lang/rust#29594,
483/// issue #217). No behavioral change on Linux.
484pub(crate) fn force_init() {
485  Lazy::force(&TOKEN_BEGIN);
486  Lazy::force(&TOKEN_END);
487  Lazy::force(&TOKEN_MATH);
488  Lazy::force(&TOKEN_ALIGN);
489  Lazy::force(&TOKEN_PARAM);
490  Lazy::force(&TOKEN_SUPER);
491  Lazy::force(&TOKEN_SUB);
492  Lazy::force(&TOKEN_SPACE);
493  Lazy::force(&TOKEN_CR);
494  Lazy::force(&TOKEN_RELAX);
495  Lazy::force(&TOKEN_EXPANDAFTER);
496  Lazy::force(&TOKEN_ENDCSNAME);
497}
498
499#[macro_export]
500/// macro for a BEGIN "{" token
501macro_rules! T_BEGIN(() => { *$crate::token::TOKEN_BEGIN });
502#[macro_export]
503/// macro for a new END "{" token
504macro_rules! T_END(() => { *$crate::token::TOKEN_END });
505/// macro for a MATH "$" token
506#[macro_export]
507macro_rules! T_MATH(() => { *$crate::token::TOKEN_MATH });
508/// macro for an ALIGN "&" token
509#[macro_export]
510macro_rules! T_ALIGN(() => { *$crate::token::TOKEN_ALIGN });
511/// macro for a PARAM "#" token
512#[macro_export]
513macro_rules! T_PARAM(() => { *$crate::token::TOKEN_PARAM });
514/// macro for a SUPER "^" token
515#[macro_export]
516macro_rules! T_SUPER(() => { *$crate::token::TOKEN_SUPER });
517/// macro for a SUB "_" token
518#[macro_export]
519macro_rules! T_SUB(() => { *$crate::token::TOKEN_SUB });
520/// macro for a SPACE token (default " ")
521#[macro_export]
522macro_rules! T_SPACE(() => { *$crate::token::TOKEN_SPACE };
523($text:literal) => {
524  Token { text: $crate::pin!($text), code: Catcode::SPACE,
525      #[cfg(feature = "token-locators")] loc: 0
526    }
527});
528/// macro for a CR "\n" token
529#[macro_export]
530macro_rules! T_CR(() => { *$crate::token::TOKEN_CR });
531/// macro for a LETTER token
532#[macro_export]
533macro_rules! T_LETTER {
534  ($text:literal) => {
535    Token {
536      text: $crate::pin!($text),
537      code: Catcode::LETTER,
538      #[cfg(feature = "token-locators")]
539      loc: 0,
540    }
541  };
542  ($text:expr_2021) => {
543    Token {
544      text: $crate::common::arena::pin($text),
545      code: Catcode::LETTER,
546      #[cfg(feature = "token-locators")]
547      loc: 0,
548    }
549  };
550}
551/// macro for an OTHER code token
552#[macro_export]
553macro_rules! T_OTHER {
554  ($text:literal) => {
555    Token {
556      text: $crate::pin!($text),
557      code: Catcode::OTHER,
558      #[cfg(feature = "token-locators")]
559      loc: 0,
560    }
561  };
562  ($text:expr_2021) => {
563    Token {
564      text: $crate::common::arena::pin($text),
565      code: Catcode::OTHER,
566      #[cfg(feature = "token-locators")]
567      loc: 0,
568    }
569  };
570}
571/// T_OTHER from a single character
572#[macro_export]
573macro_rules! T_OTHER_CHAR {
574  ($text:literal) => {
575    Token {
576      text: $crate::common::arena::pin_char($text),
577      code: Catcode::OTHER,
578      #[cfg(feature = "token-locators")]
579      loc: 0,
580    }
581  };
582}
583/// macro for an ACTIVE char token
584#[macro_export]
585macro_rules! T_ACTIVE {
586  ($c:expr_2021) => {{
587    let mut tmp = [0u8; 4];
588    let s = $c.encode_utf8(&mut tmp);
589    Token {
590      text: $crate::common::arena::pin(s),
591      code: Catcode::ACTIVE,
592      #[cfg(feature = "token-locators")]
593      loc: 0,
594    }
595  }};
596}
597/// macro for a COMMENT content token
598#[macro_export]
599macro_rules! T_COMMENT {
600  ($text:expr_2021) => {
601    Token {
602      text: $crate::common::arena::pin($text),
603      code: Catcode::COMMENT,
604      #[cfg(feature = "token-locators")]
605      loc: 0,
606    }
607  };
608}
609/// macro for a command sequence token
610#[macro_export]
611macro_rules! T_CS {
612  ($text:literal) => {
613    $crate::token::Token {
614      text: $crate::pin!($text),
615      code: $crate::token::Catcode::CS,
616      #[cfg(feature = "token-locators")]
617      loc: 0,
618    }
619  };
620  ($text:expr_2021) => {
621    $crate::token::Token {
622      text: $crate::common::arena::pin($text),
623      code: $crate::token::Catcode::CS,
624      #[cfg(feature = "token-locators")]
625      loc: 0,
626    }
627  };
628}
629
630/// macro for T_CS("\\relax")
631#[macro_export]
632macro_rules! T_RELAX(() => { $crate::token::TOKEN_RELAX.clone() });
633
634/// macro for a tracing MARKER token
635#[macro_export]
636macro_rules! T_MARKER {
637  ($text:expr_2021) => {
638    Token {
639      text: $crate::common::arena::pin($text),
640      code: Catcode::MARKER,
641      #[cfg(feature = "token-locators")]
642      loc: 0,
643    }
644  };
645}
646
647/// macro for a numbered ARG token
648#[macro_export]
649macro_rules! T_ARG {
650  ($text:expr_2021) => {
651    Token {
652      text: $crate::common::arena::pin($text.to_string()),
653      code: Catcode::ARG,
654      #[cfg(feature = "token-locators")]
655      loc: 0,
656    }
657  };
658}
659
660/// Token constructor macro (defaults to OTHER code)
661#[macro_export]
662macro_rules! Token {
663  ($text:expr_2021) => {
664    Token!($text, Catcode::OTHER)
665  };
666  ($text:literal, $cc:expr_2021) => {
667    Token {
668      text: $crate::pin!($text),
669      code: $cc,
670      #[cfg(feature = "token-locators")]
671      loc: 0,
672    }
673  };
674  ($text:expr_2021, $cc:expr_2021) => {
675    Token {
676      text: $crate::common::arena::pin($text),
677      code: $cc,
678      #[cfg(feature = "token-locators")]
679      loc: 0,
680    }
681  };
682}
683
684/// Special case: a character needs swift string conversion, so let's use a dedicated macro
685#[macro_export]
686macro_rules! CharToken {
687  ($c:expr_2021) => {
688    CharToken!($c, Catcode::OTHER)
689  };
690  ($c:expr_2021, $cc:expr_2021) => {{
691    let mut tmp = [0u8; 4];
692    let s = $c.encode_utf8(&mut tmp);
693    Token!(s, $cc)
694  }};
695}
696
697/// Explode a string into a list of tokens, all w/catcode OTHER (except space).
698/// Note: newlines are converted to OTHER, NOT SPACE (Perl #2700 reverted #2646).
699/// ^^J in TeX decodes to CC_OTHER by default; let the tokenizer handle catcode
700/// reassignment if needed.
701#[macro_export]
702macro_rules! Explode(($text:expr_2021) => (
703  $text.to_string().chars().map(|c|
704    if c==' ' { T_SPACE!() }
705    else {
706      CharToken!(c)
707    }
708  ).collect::<Vec<Token>>()
709));
710
711#[macro_export]
712macro_rules! ExplodeChars(($text:expr_2021) => (
713  $text.as_str().chars().map(|c|
714    if c==' ' { T_SPACE!() }
715    else {
716      CharToken!(c)
717    }
718  ).collect::<Vec<Token>>()
719));
720
721/// Similar to Explode, but convert letters to catcode LETTER and others to OTHER
722/// Hopefully, this is essentially correct WITHOUT resorting to catcode lookup?
723/// Perl sync: newlines are OTHER, not SPACE (matches Perl #2700 revert of #2646).
724#[macro_export]
725macro_rules! ExplodeText(
726  ($text:expr_2021) => ({
727  use $crate::token::{Catcode,Token};
728  $text.to_string().chars().map(|c|
729    if c==' ' { T_SPACE!() }
730    else {
731      let mut tmp = [0u8; 4];
732      let s = c.encode_utf8(&mut tmp);
733      if c.is_alphabetic() {
734      T_LETTER!(s) }
735    else { T_OTHER!(s) }}
736  ).collect::<Vec<Token>>()
737}));
738
739#[macro_export]
740macro_rules! SymExplodeText(
741  ($sym:expr_2021) => ({
742  use $crate::token::{Catcode,Token};
743  let chars : Vec<char> = arena::with($sym, |text| text.chars().collect());
744  chars.into_iter().map(|c|
745    if c==' ' { T_SPACE!() }
746    else {
747      let mut tmp = [0u8; 4];
748      let s = c.encode_utf8(&mut tmp);
749      if c.is_alphabetic() {
750      T_LETTER!(s) }
751    else { T_OTHER!(s) }}
752  ).collect::<Vec<Token>>()
753}));
754
755// static UNTEX_LINELENGTH: usize = 78; // [CONSTANT]
756
757impl Default for Token {
758  fn default() -> Self {
759    Token {
760      text: arena::pin_static("EXPECTED_TOKEN"),
761      code: Catcode::OTHER,
762      #[cfg(feature = "token-locators")]
763      loc: 0,
764    }
765  }
766}
767
768///======================================================================
769/// Accessors.
770impl Token {
771  /// simple Token constructor, wrapping over text and catcode
772  pub fn new<T: AsRef<str>>(text: T, code: Catcode) -> Self {
773    Token {
774      text: arena::pin(text),
775      code,
776      #[cfg(feature = "token-locators")]
777      loc: 0,
778    }
779  }
780
781  /// A cheap structural fingerprint for the cycle-detection guard
782  /// ([`crate::cycle_guard`]). Matches [`PartialEq`] semantics: SPACE tokens
783  /// fingerprint by catcode alone (their text is irrelevant to equality).
784  /// NOT a stable hash across processes — for in-run loop detection only.
785  #[inline]
786  pub fn cycle_fingerprint(&self) -> u64 {
787    use std::hash::{Hash, Hasher};
788    let mut h = rustc_hash::FxHasher::default();
789    self.code.hash(&mut h);
790    if self.code != Catcode::SPACE {
791      self.text.hash(&mut h);
792    }
793    h.finish()
794  }
795
796  /// Get the CS Name of the token. This is the name that definitions will be
797  /// stored under; It's the same for various `different' BEGIN tokens, eg.
798  pub fn get_cs_name(&self) -> SymStr {
799    if self.code.is_primitive() {
800      self.code.name_sym()
801    } else {
802      self.get_sym()
803    }
804  }
805
806  /// Execute a closure using the CS Name of the token.
807  /// This is the name that definitions will be stored under;
808  /// It's the same for various `different' BEGIN tokens, eg.
809  pub fn with_cs_name<R, FnR>(&self, caller: FnR) -> R
810  where FnR: FnOnce(&str) -> R {
811    if self.code.is_primitive() {
812      caller(self.code.name())
813    } else {
814      self.with_str(caller)
815    }
816  }
817
818  /// artificial, but avoids the data race
819  pub fn pin_cs_name(&self) -> SymStr {
820    if self.code.is_primitive() {
821      self.code.name_sym()
822    } else {
823      self.get_sym()
824    }
825  }
826
827  /// Get the fixed name of a primitive catcode, or empty string otherwise
828  pub fn get_primitive_name(&self) -> Option<&'static str> {
829    if self.code.is_primitive() {
830      Some(self.code.name())
831    } else {
832      None
833    }
834  }
835
836  /// Get the CS name only if the catcode is executable!
837  pub fn get_executable_name(&self) -> String {
838    let cc = self.code;
839    if cc.is_executable() {
840      self
841        .get_primitive_name()
842        .map(ToString::to_string)
843        .unwrap_or_else(|| self.with_str(|text| text.to_string()))
844    } else {
845      String::new()
846    }
847  }
848
849  /// Intersect executable and primitive
850  pub fn get_executable_primitive_name(&self) -> Option<&'static str> {
851    let cc = self.code;
852    if cc.is_executable() && cc.is_primitive() {
853      Some(self.code.name())
854    } else {
855      None
856    }
857  }
858
859  /// Use the ticket representing the interned "text" of the token
860  pub fn get_sym(&self) -> SymStr { self.text }
861  /// Use the interned &str "text" of the token
862  /// use `to_string` instead for an owned String with simpler
863  pub fn with_str<R, FnR>(&self, caller: FnR) -> R
864  where FnR: FnOnce(&str) -> R {
865    arena::with(self.text, caller)
866  }
867
868  /// True for any member of the `\special_relax` no-expand family — the
869  /// representation of a `\noexpand`'d (expandable or undefined) CS/active token.
870  /// Such a token is a CS whose NAME is `\special_relax` `\x01` `<shadowed text>`,
871  /// carrying the shadowed token's identity PER-TOKEN (faithful to TeX's
872  /// `no_expand_flag`, which preserves `cur_cs`), while the whole family resolves
873  /// to `\special_relax`'s `\relax` meaning ([`crate::state::lookup_meaning`]
874  /// fallback). The bare `\special_relax` (no suffix) is the no-shadow case
875  /// (`\dont_expand` at end-of-input). `\x01` is never valid in a CS name or as
876  /// an active char, so the encoding is unambiguous.
877  pub fn is_noexpand_family(&self) -> bool {
878    if self.code != Catcode::CS {
879      return false;
880    }
881    // Per-symbol memo: this runs ×2 per CS token via `state::meaning_key`
882    // (read_x_token decides whether to expand, invoke_token how to invoke),
883    // and the string-prefix probe was ~2% of digest self-time (2026-08-23
884    // audit). A symbol's text never changes (append-only arena), so the
885    // answer is memoized by symbol index: 0 = unknown, 1 = no, 2 = yes.
886    // Cleared with the arena in `reset_thread_engine` (symbol indices are
887    // reused after `arena::reset`).
888    use string_interner::Symbol;
889    let idx = self.text.to_usize();
890    let cached = NOEXPAND_FAMILY_MEMO.with(|m| m.borrow().get(idx).copied().unwrap_or(0));
891    if cached != 0 {
892      return cached == 2;
893    }
894    let is_family = self.with_str(|s| {
895      s.starts_with(NOEXPAND_PREFIX)
896        && (s.len() == NOEXPAND_PREFIX.len() || s.as_bytes()[NOEXPAND_PREFIX.len()] == NOEXPAND_SEP)
897    });
898    NOEXPAND_FAMILY_MEMO.with(|m| {
899      let mut memo = m.borrow_mut();
900      if memo.len() <= idx {
901        memo.resize(idx + 1, 0);
902      }
903      memo[idx] = if is_family { 2 } else { 1 };
904    });
905    is_family
906  }
907
908  /// Recover the shadowed token from a `\special_relax`-family token, if it
909  /// shadows one (i.e. not the bare `\special_relax`). The shadowed token is a
910  /// CS (text begins `\`) or an active char.
911  pub fn noexpand_shadowed(&self) -> Option<Token> {
912    if self.code != Catcode::CS {
913      return None;
914    }
915    // Compute the owned shadowed name + its catcode INSIDE the `with_str`
916    // borrow, then `arena::pin` it OUTSIDE. `with_str`'s `s` (hence `rest`)
917    // aliases the arena's append-only buffer; a re-entrant `arena::pin(rest)`
918    // could reallocate that buffer mid-intern and read freed memory. Owning the
919    // name first (and pinning after the borrow ends) removes the hazard — and
920    // satisfies clippy::unnecessary_to_owned, which can't see the aliasing.
921    let (name, code) = self.with_str(|s| {
922      let rest = s.strip_prefix(NOEXPAND_PREFIX)?;
923      let rest = rest.strip_prefix(NOEXPAND_SEP as char)?;
924      let code = if rest.starts_with('\\') {
925        Catcode::CS
926      } else {
927        Catcode::ACTIVE
928      };
929      Some((rest.to_string(), code))
930    })?;
931    Some(Token {
932      text: arena::pin(name),
933      code,
934      #[cfg(feature = "token-locators")]
935      loc: 0,
936    })
937  }
938
939  /// Return the character code of  character part of the token, or 256 if it is a control
940  /// sequence
941  pub fn get_charcode(&self) -> u32 {
942    if self.code == Catcode::CS {
943      256
944    } else {
945      self.with_str(|text| {
946        if let Some(c) = text.chars().next() {
947          c as u32
948        } else {
949          0
950        }
951      })
952    }
953  }
954
955  /// Return the catcode of the token.
956  pub fn get_catcode(&self) -> Catcode { self.code }
957  /// is the current one
958  pub fn is_executable(&self) -> bool { self.code.is_executable() }
959
960  /// neutralize really should only retroactively imitate what Semiverbatim would have done.
961  /// So, it needs to neutralize those in SPECIALS
962  /// NOTE that although '%' gets it's catcode changed in Semiverbatim,
963  /// I'm pretty sure we do NOT want to neutralize comments (turn them into Catcode::OTHER)
964  /// here, since if comments do get into the Tokens, that will introduce weird crap into the
965  /// stream.
966  pub fn neutralize(self, extraspecials: &[char]) -> Token {
967    let first_c: Option<char> = self.with_str(|text| text.chars().next());
968    let ch = match first_c {
969      Some(ch) => ch,
970      None => return self,
971    };
972    let cc = self.code;
973    if cc.is_neutralizable() {
974      for extra in extraspecials {
975        if extra == &ch {
976          let mut tmp = [0u8; 4];
977          let s = ch.encode_utf8(&mut tmp);
978          return T_OTHER!(s);
979        }
980      }
981      let maybe_return = state::with_value("SPECIALS", |specials_opt| {
982        if let Some(Stored::Chars(specials_list)) = specials_opt {
983          for special in specials_list.iter() {
984            if *special == ch {
985              let mut tmp = [0u8; 4];
986              let s = ch.encode_utf8(&mut tmp);
987              return Some(T_OTHER!(s));
988            }
989          }
990        }
991        None
992      });
993      if let Some(token) = maybe_return {
994        return token;
995      }
996    }
997    self
998  }
999
1000  pub fn as_other(&self) -> Token {
1001    Token {
1002      text: self.text,
1003      code: Catcode::OTHER,
1004      #[cfg(feature = "token-locators")]
1005      loc: 0,
1006    }
1007  }
1008  pub fn as_cs(&self) -> Token {
1009    Token {
1010      text: self.text,
1011      code: Catcode::CS,
1012      #[cfg(feature = "token-locators")]
1013      loc: 0,
1014    }
1015  }
1016
1017  pub fn substitute_parameters(self, args: &[&Token]) -> Self {
1018    if self.code == Catcode::ARG {
1019      self.with_str(|text| {
1020        let arg_idx = text
1021          .parse::<usize>()
1022          .expect("ARG catcode tokens should always contain numeric literals as text");
1023        *args[arg_idx - 1]
1024      })
1025    } else {
1026      self
1027    }
1028  }
1029
1030  /// A Token reverts to itself
1031  pub fn revert(self) -> Token { self }
1032
1033  /// A string form which is primarily used for error-reporting
1034  pub fn stringify(&self) -> String {
1035    self.with_str(|text| {
1036      // Make the token's char content more printable, since this is for a visual messages.
1037      let display_text = if text.len() == 1 {
1038        let c = text.chars().next().unwrap() as u16;
1039        if c < 0x020 {
1040          Cow::Owned(s!("U+{:04x}/{}", c, CONTROLNAME[c as usize]))
1041        } else {
1042          Cow::Borrowed(text)
1043        }
1044      } else {
1045        Cow::Borrowed(text)
1046      };
1047      s!("{}[{}]", self.code.short_name(), display_text)
1048    })
1049  }
1050
1051  pub fn to_register(&self) -> Option<Rc<Register>> { state::lookup_register_definition(self) }
1052
1053  pub fn to_number(&self) -> Number {
1054    Number::new(self.with_str(|text| text.parse::<i64>()).unwrap_or(0))
1055  }
1056
1057  pub fn to_dimension(&self) -> Dimension {
1058    Dimension::new_f64(self.with_str(|text| text.parse::<f64>().unwrap_or(0.0)))
1059  }
1060
1061  pub fn to_mu_dimension(&self) -> MuDimension {
1062    MuDimension::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1063  }
1064
1065  pub fn to_glue(&self) -> Glue {
1066    Glue::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1067  }
1068
1069  pub fn to_mu_glue(&self) -> MuGlue {
1070    MuGlue::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1071  }
1072
1073  pub fn to_float(&self) -> Float {
1074    Float::new_f64(self.with_str(|s| s.parse::<f64>()).unwrap_or(0.0))
1075  }
1076
1077  pub fn be_digested(self) -> Result<Digested> { crate::stomach::digest(Tokens::new(vec![self])) }
1078
1079  /// Check whether the current token is defined as `other`.
1080  /// That is, whether it is equal to `other`, or \let to `other`.
1081  /// `other` is is presumed to be some "constant", explicit token,
1082  /// such as  `T_SPACE` or `T_CS!("\\endcsname")`.
1083  pub fn defined_as(&self, other: &Token) -> bool {
1084    let cc = self.code;
1085    let occ = other.get_catcode();
1086    if (cc == occ) && ((occ == Catcode::SPACE) || (self.text == other.get_sym())) {
1087      return true;
1088    }
1089    if matches!(cc, Catcode::CS | Catcode::ACTIVE) {
1090      // Use the closure-based `with_meaning` — Token is `Copy`, so
1091      // extracting a `Token` from the borrowed Stored is an implicit
1092      // copy, not a clone. Avoids a full `Stored::clone()` per call
1093      // (defined_as fires ~1% of total instructions on siunitx/
1094      // physics-heavy docs).
1095      let letto_opt: Option<Token> = state::with_meaning(self, |defn_opt| {
1096        defn_opt.and_then(|defn| match defn {
1097          Stored::Token(t) => Some(*t),
1098          Stored::Expandable(inner) => Some(*inner.get_cs()),
1099          Stored::Primitive(inner) => Some(*inner.get_cs()),
1100          Stored::MathPrimitive(inner) => Some(*inner.get_cs()),
1101          Stored::Register(inner) => Some(*inner.get_cs()),
1102          Stored::Conditional(inner) => Some(*inner.get_cs()),
1103          Stored::Constructor(inner) => Some(*inner.get_cs()),
1104          _ => None,
1105        })
1106      });
1107      if let Some(letto) = letto_opt
1108        && (letto.get_catcode() == occ)
1109        && ((occ == Catcode::SPACE) || letto.get_sym() == other.get_sym())
1110      {
1111        return true;
1112      }
1113    }
1114    false
1115  }
1116}
1117
1118// A simple (constant!) auto-cast for &str to Token. Beware this will not respect the current
1119// catcodes in state (and @ is OTHER).
1120impl From<&str> for Token {
1121  fn from(text: &str) -> Token {
1122    match text.chars().next() {
1123      Some('{') => T_BEGIN!(),
1124      Some('}') => T_END!(),
1125      Some('$') => T_MATH!(),
1126      Some('#') => T_PARAM!(),
1127      Some('&') => T_ALIGN!(),
1128      Some('^') => T_SUPER!(),
1129      Some('_') => T_SUB!(),
1130      Some('\\') => T_CS!(text),
1131      Some('%') => T_COMMENT!(text),
1132      _ => {
1133        if text.chars().all(|c| c.is_alphabetic()) {
1134          T_LETTER!(text)
1135        } else if text.chars().all(|c| c.is_whitespace()) {
1136          T_SPACE!()
1137        } else {
1138          T_OTHER!(text)
1139        }
1140      },
1141    }
1142  }
1143}
1144
1145// ── Token-origin side arena (token-locators feature) ───────────────────────
1146// Per-conversion store mapping a Token's `loc` handle (1-based; 0 = none) to its
1147// captured source start. Tokens carry only the u32 handle (Token stays 12 bytes);
1148// this holds the (source, line, col). Appended in `read_token`, read by the
1149// digestion consumer to give a text run its true span. Cleared per conversion.
1150// See docs/performance/SOURCE_PROVENANCE.md §3.1.1.
1151#[cfg(feature = "token-locators")]
1152#[derive(Clone, Copy, Debug)]
1153pub struct TokenStart {
1154  pub source:    SymStr,
1155  pub line:      u32,
1156  pub col:       u32,
1157  /// `true` when this origin was *inherited* from a macro invocation rather
1158  /// than read directly from a mouth — i.e. the token is synthesized
1159  /// expansion output (`\today → "May 25, 2026"`) attributed to its `\today`
1160  /// call site. The content-range recovery in `constructor::child_span`
1161  /// prefers genuine (read-from-source) origins and only falls back to
1162  /// inherited ones, so a `\section{Intro}`'s structural body literals — now
1163  /// carrying an inherited origin — never widen the title's content-exact
1164  /// span. See docs/performance/SOURCE_PROVENANCE.md §3.1.3.
1165  pub inherited: bool,
1166}
1167
1168#[cfg(feature = "token-locators")]
1169thread_local! {
1170  static TOKEN_ORIGINS: std::cell::RefCell<Vec<TokenStart>> =
1171    const { std::cell::RefCell::new(Vec::new()) };
1172}
1173
1174/// Append a token's source start, returning its 1-based handle (`0` is reserved
1175/// for "no origin"). Only called on the source-map precision path.
1176#[cfg(feature = "token-locators")]
1177pub fn push_token_origin(source: SymStr, line: u32, col: u32) -> u32 {
1178  TOKEN_ORIGINS.with(|o| {
1179    let mut v = o.borrow_mut();
1180    v.push(TokenStart {
1181      source,
1182      line,
1183      col,
1184      inherited: false,
1185    });
1186    v.len() as u32 // index + 1
1187  })
1188}
1189
1190/// Derive an *inherited* origin from an existing handle: look up the
1191/// invocation token's start, push a copy flagged `inherited`, and return its
1192/// new handle (`0` if `handle` is the no-origin sentinel or out of range). One
1193/// call per macro expansion; the returned handle is shared by every synthesized
1194/// result token. See `push_token_origin` and docs/performance/SOURCE_PROVENANCE.md §3.1.3.
1195#[cfg(feature = "token-locators")]
1196pub fn push_inherited_origin(handle: u32) -> u32 {
1197  if handle == 0 {
1198    return 0;
1199  }
1200  TOKEN_ORIGINS.with(|o| {
1201    let mut v = o.borrow_mut();
1202    let Some(mut start) = v.get((handle - 1) as usize).copied() else {
1203      return 0;
1204    };
1205    start.inherited = true;
1206    v.push(start);
1207    v.len() as u32
1208  })
1209}
1210
1211/// Resolve a token `loc` handle to its origin (`None` for the `0` sentinel or an
1212/// out-of-range handle).
1213#[cfg(feature = "token-locators")]
1214pub fn get_token_origin(handle: u32) -> Option<TokenStart> {
1215  if handle == 0 {
1216    return None;
1217  }
1218  TOKEN_ORIGINS.with(|o| o.borrow().get((handle - 1) as usize).copied())
1219}
1220
1221/// Reset the arena at the start of a conversion (handles are per-conversion).
1222#[cfg(feature = "token-locators")]
1223pub fn clear_token_origins() { TOKEN_ORIGINS.with(|o| o.borrow_mut().clear()); }
1224
1225#[cfg(test)]
1226mod tests {
1227  use super::*;
1228
1229  /// `Token` size invariant (docs/performance/SOURCE_PROVENANCE.md §3.1.1): 8 bytes by
1230  /// default (`SymStr` + `Catcode`), 12 only under the `token-locators`
1231  /// precision build (+ the `u32` origin handle). Guards the corpus/parity/
1232  /// distribution builds against an accidental widening.
1233  #[test]
1234  fn token_size_invariant() {
1235    #[cfg(not(feature = "token-locators"))]
1236    assert_eq!(
1237      size_of::<Token>(),
1238      8,
1239      "default Token must stay 8 bytes (SymStr + Catcode)"
1240    );
1241    #[cfg(feature = "token-locators")]
1242    assert_eq!(
1243      std::mem::size_of::<Token>(),
1244      12,
1245      "token-locators Token is 8 + a u32 origin handle"
1246    );
1247  }
1248
1249  /// Per-token origin capture (token-locators): each char token read from a
1250  /// mouth carries a handle resolving to its exact (line, col). This is the leaf
1251  /// accuracy that mouth-snapshot (Experiments 1–2) and digested-child assembly
1252  /// (Experiment 3) could not provide — the position now travels *with the
1253  /// token*. See docs/performance/SOURCE_PROVENANCE.md §3.1.1.
1254  #[cfg(feature = "token-locators")]
1255  #[test]
1256  fn token_origin_capture() {
1257    super::clear_token_origins();
1258    // "Hello" — five letters at 1-indexed columns 1..=5 on line 1.
1259    let toks = crate::mouth::tokenize("Hello");
1260    let got: Vec<(u32, u32)> = toks
1261      .unlist_ref()
1262      .iter()
1263      .map(|t| {
1264        let o = super::get_token_origin(t.loc).expect("token carries an origin handle");
1265        (o.line, o.col)
1266      })
1267      .collect();
1268    assert_eq!(
1269      got,
1270      vec![(1, 1), (1, 2), (1, 3), (1, 4), (1, 5)],
1271      "each letter's captured (line, col) must be exact"
1272    );
1273  }
1274
1275  #[test]
1276  fn catcode_name_covers_all_variants() {
1277    // Ensure every Catcode variant produces a non-empty name.
1278    use Catcode::*;
1279    for cc in [
1280      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE, IGNORE, LETTER, OTHER,
1281      ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1282    ] {
1283      assert!(!cc.name().is_empty(), "{cc:?}.name() is empty");
1284    }
1285  }
1286
1287  #[test]
1288  fn catcode_name_specific_values() {
1289    assert_eq!(Catcode::ESCAPE.name(), "Escape");
1290    assert_eq!(Catcode::BEGIN.name(), "Begin");
1291    assert_eq!(Catcode::CS.name(), "ControlSequence");
1292    assert_eq!(Catcode::LETTER.name(), "Letter");
1293  }
1294
1295  #[test]
1296  fn catcode_short_name_starts_with_t_prefix() {
1297    use Catcode::*;
1298    for cc in [
1299      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE, IGNORE, LETTER, OTHER,
1300      ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1301    ] {
1302      assert!(
1303        cc.short_name().starts_with("T_"),
1304        "{cc:?}.short_name() = {} lacks T_ prefix",
1305        cc.short_name()
1306      );
1307    }
1308  }
1309
1310  #[test]
1311  fn catcode_meaning_mostly_nonempty() {
1312    // Most variants have a TeX-meaning description.
1313    assert!(!Catcode::ESCAPE.meaning().is_empty());
1314    assert!(!Catcode::LETTER.meaning().is_empty());
1315    assert!(!Catcode::OTHER.meaning().is_empty());
1316    // A few (e.g. CS/MARKER/ARG) fall through to "".
1317    assert_eq!(Catcode::CS.meaning(), "");
1318  }
1319
1320  #[test]
1321  fn is_primitive_checks() {
1322    use Catcode::*;
1323    // TeX primitives:
1324    for cc in [
1325      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE,
1326    ] {
1327      assert!(cc.is_primitive(), "{cc:?} should be primitive");
1328    }
1329    // Non-primitives:
1330    for cc in [
1331      IGNORE, LETTER, OTHER, ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1332    ] {
1333      assert!(!cc.is_primitive(), "{cc:?} should not be primitive");
1334    }
1335  }
1336
1337  #[test]
1338  fn is_executable_checks() {
1339    use Catcode::*;
1340    for cc in [BEGIN, END, MATH, ALIGN, SUPER, SUB, ACTIVE, CS] {
1341      assert!(cc.is_executable(), "{cc:?} should be executable");
1342    }
1343    for cc in [
1344      EOL, ESCAPE, PARAM, SPACE, IGNORE, LETTER, OTHER, COMMENT, INVALID, MARKER, ARG,
1345    ] {
1346      assert!(!cc.is_executable(), "{cc:?} should not be executable");
1347    }
1348  }
1349
1350  #[test]
1351  fn is_neutralizable_set() {
1352    use Catcode::*;
1353    for cc in [MATH, ALIGN, PARAM, SUPER, SUB, ACTIVE] {
1354      assert!(cc.is_neutralizable(), "{cc:?}");
1355    }
1356    assert!(!CS.is_neutralizable());
1357    assert!(!LETTER.is_neutralizable());
1358  }
1359
1360  #[test]
1361  fn is_active_or_cs_narrow_set() {
1362    assert!(Catcode::ACTIVE.is_active_or_cs());
1363    assert!(Catcode::CS.is_active_or_cs());
1364    assert!(!Catcode::LETTER.is_active_or_cs());
1365    assert!(!Catcode::ESCAPE.is_active_or_cs());
1366  }
1367
1368  #[test]
1369  fn is_absorbable_space_letter_other_comment() {
1370    use Catcode::*;
1371    assert!(SPACE.is_absorbable());
1372    assert!(LETTER.is_absorbable());
1373    assert!(OTHER.is_absorbable());
1374    assert!(COMMENT.is_absorbable());
1375    // All else not absorbable.
1376    assert!(!CS.is_absorbable());
1377    assert!(!BEGIN.is_absorbable());
1378  }
1379
1380  #[test]
1381  fn is_gullet_holdable_comment_marker_only() {
1382    assert!(Catcode::COMMENT.is_gullet_holdable());
1383    assert!(Catcode::MARKER.is_gullet_holdable());
1384    assert!(!Catcode::SPACE.is_gullet_holdable());
1385    assert!(!Catcode::LETTER.is_gullet_holdable());
1386  }
1387
1388  #[test]
1389  fn is_balanced_interesting_begin_end_marker() {
1390    assert!(Catcode::BEGIN.is_balanced_interesting());
1391    assert!(Catcode::END.is_balanced_interesting());
1392    assert!(Catcode::MARKER.is_balanced_interesting());
1393    assert!(!Catcode::LETTER.is_balanced_interesting());
1394    assert!(!Catcode::MATH.is_balanced_interesting());
1395  }
1396
1397  #[test]
1398  fn catcode_u8_roundtrip() {
1399    // From<Catcode> for u8 + From<u8> for Catcode should round-trip
1400    // (at least for the documented range 0..=18).
1401    use Catcode::*;
1402    for cc in [
1403      ESCAPE, BEGIN, END, MATH, ALIGN, EOL, PARAM, SUPER, SUB, SPACE, IGNORE, LETTER, OTHER,
1404      ACTIVE, COMMENT, INVALID, CS, MARKER, ARG,
1405    ] {
1406      let b: u8 = cc.into();
1407      let cc2: Catcode = b.into();
1408      assert_eq!(cc, cc2, "roundtrip broke for {cc:?} (u8={b})");
1409    }
1410  }
1411
1412  #[test]
1413  fn token_new_and_display() {
1414    let t = Token::new("foo", Catcode::LETTER);
1415    assert_eq!(format!("{t}"), "foo");
1416    assert_eq!(t.code, Catcode::LETTER);
1417  }
1418
1419  #[test]
1420  fn token_arg_display_prepends_hash() {
1421    // ARG catcode prepends # in Display.
1422    let t = Token::new("1", Catcode::ARG);
1423    assert_eq!(format!("{t}"), "#1");
1424  }
1425}