Skip to main content

badness_parser/
syntax.rs

1//! `SyntaxKind` — the kinds of CST tokens and nodes — and the rowan `Language`
2//! binding for badness's LaTeX surface CST.
3
4use rowan::Language;
5
6/// Kinds of tokens (terminals, from the lexer) and nodes (composites, from the
7/// parser) in the CST.
8///
9/// Token kinds come first, node kinds after; `ROOT` is kept **last** so
10/// [`BadnessLang::kind_from_raw`] can bounds-check the raw discriminant with a
11/// single comparison. Do not add variants after `ROOT`.
12#[allow(non_camel_case_types)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u16)]
15pub enum SyntaxKind {
16    // --- Tokens (terminals, produced by the lexer) ---
17    CONTROL_WORD,   // `\foo`  (backslash + ASCII letters)
18    CONTROL_SYMBOL, // `\\`, `\{`, `\%`, `\,` … (backslash + one non-letter)
19    L_BRACE,        // {
20    R_BRACE,        // }
21    L_BRACKET,      // [
22    R_BRACKET,      // ]
23    DOLLAR,         // $
24    AMPERSAND,      // &
25    HASH,           // #
26    CARET,          // ^
27    UNDERSCORE,     // _
28    TILDE,          // ~
29    COMMENT,        // `% …` up to (not including) the line break
30    WHITESPACE,     // spaces / tabs
31    NEWLINE,        // `\n`, `\r\n`, or `\r`
32    WORD,           // a run of ordinary text characters
33    VERB,           // `\verb|…|` / `\verb*|…|` inline verbatim (a single token)
34    VERBATIM_BODY,  // the raw body of a verbatim-like environment (a single token)
35    DOC_MARGIN,     // a `.dtx` documentation line's leading `%` margin (trivia)
36    GUARD,          // a `.dtx` docstrip guard `%<…>` (`%<*t>`/`%</t>`/inline) (trivia)
37    ERROR,          // lexer fallback; the lexer is total, so this is unused today
38
39    // Nodes
40    GROUP,       // { … }
41    OPTIONAL,    // [ … ] optional argument
42    ARGUMENT,    // an argument attached to a command
43    COMMAND,     // a control sequence with its arguments
44    ENVIRONMENT, // \begin{…} … \end{…}
45    BEGIN,       // \begin{name}
46    END,         // \end{name}
47    NAME_GROUP,  // {name} following \begin / \end
48    // `\if…\else…\or…\fi`, when the shape gate pairs it. The construct is a run
49    // of `CONDITIONAL_BRANCH` nodes followed by the closing `\fi` as the last
50    // child, mirroring `ENVIRONMENT > BEGIN … END`. The `\if` test's extent is
51    // not statically resolvable (`\ifnum\radius>5` scans ⟨number⟩⟨rel⟩⟨number⟩
52    // by TeX's own scanner), so the opener and its test ride the *first* branch
53    // rather than a head node of their own.
54    CONDITIONAL,
55    // One branch of a `CONDITIONAL`. The first holds the opener, its test, and
56    // the then-body; every later one *starts with* its `\else`/`\or` divider, so
57    // a consumer finds the boundaries positionally and never by command name.
58    CONDITIONAL_BRANCH,
59    INLINE_MATH,  // $ … $   or   \( … \)
60    DISPLAY_MATH, // $$ … $$  or   \[ … \]
61    MATH,         // a math body (the atoms between the delimiters)
62    SCRIPTED,     // a base atom with attached scripts: base (SUBSCRIPT | SUPERSCRIPT)+
63    SUBSCRIPT,    // `_` and its tightly-bound script argument
64    SUPERSCRIPT,  // `^` and its tightly-bound script argument
65    LEFT_RIGHT,   // `\left( … \right)` — a matched delimiter pair wrapping a MATH body
66    PARAGRAPH,    // text delimited by blank lines
67    DOC_COMMENT,  // a bound leading-`%` comment run, grouped before its construct
68    TEXT,         // a run of text and trivia
69    LINE_BREAK,   // `\\`, with a tightly-bound `*` and/or `[len]` (`\\*[2ex]`)
70    // A `;`-terminated statement in a curated `statementBody` environment body
71    // (the TikZ/pgf picture family). Owns everything from the statement's first
72    // non-trivia element through the top-level WORD carrying the terminating
73    // `;`, so layout can derive statement boundaries from structure rather than
74    // authored newlines. A run that never reaches a `;` is left as plain
75    // paragraph content — recognition is retrospective and degrades silently.
76    STATEMENT,
77    ROOT, // the document root  (keep LAST)
78}
79
80impl SyntaxKind {
81    /// The number of `SyntaxKind` variants. Sound because the enum is
82    /// `#[repr(u16)]` with contiguous discriminants `0..=ROOT` and `ROOT` is kept
83    /// last; used to size kind-indexed tables (e.g. the linter's dispatch table).
84    pub const COUNT: usize = SyntaxKind::ROOT as usize + 1;
85}
86
87impl From<SyntaxKind> for rowan::SyntaxKind {
88    fn from(kind: SyntaxKind) -> Self {
89        Self(kind as u16)
90    }
91}
92
93/// The rowan language marker for badness's CST.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
95pub enum BadnessLang {}
96
97impl Language for BadnessLang {
98    type Kind = SyntaxKind;
99
100    fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
101        assert!(
102            raw.0 <= SyntaxKind::ROOT as u16,
103            "invalid SyntaxKind discriminant: {}",
104            raw.0
105        );
106        // SAFETY: `SyntaxKind` is `#[repr(u16)]` with contiguous discriminants
107        // `0..=ROOT`, and the assert above bounds `raw.0` into that range.
108        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
109    }
110
111    fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
112        kind.into()
113    }
114}
115
116pub type SyntaxNode = rowan::SyntaxNode<BadnessLang>;
117pub type SyntaxToken = rowan::SyntaxToken<BadnessLang>;
118pub type SyntaxElement = rowan::SyntaxElement<BadnessLang>;
119
120/// Whitespace and newlines are the only trivia the formatter rewrites; comments
121/// are preserved verbatim and so are *not* collapsible. A shared shape
122/// predicate: the formatter's gap normalization and the semantic layer's expl3
123/// statement segmentation both skip exactly this class.
124pub fn is_collapsible_trivia(kind: SyntaxKind) -> bool {
125    matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
126}
127
128/// Whether a `WORD` token is a single TeX parameter digit (`1`..=`9`) — the
129/// shape that follows `#` in a parameter reference. Reads only the token text.
130pub fn is_param_digit(t: &SyntaxToken) -> bool {
131    matches!(t.text().as_bytes(), [b'1'..=b'9'])
132}