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 (composites, produced by the Phase 1 parser) ---
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 INLINE_MATH, // $ … $ or \( … \)
49 DISPLAY_MATH, // $$ … $$ or \[ … \]
50 MATH, // a math body (the atoms between the delimiters)
51 SCRIPTED, // a base atom with attached scripts: base (SUBSCRIPT | SUPERSCRIPT)+
52 SUBSCRIPT, // `_` and its tightly-bound script argument
53 SUPERSCRIPT, // `^` and its tightly-bound script argument
54 LEFT_RIGHT, // `\left( … \right)` — a matched delimiter pair wrapping a MATH body
55 PARAGRAPH, // text delimited by blank lines
56 DOC_COMMENT, // a bound leading-`%` comment run, grouped before its construct
57 TEXT, // a run of text and trivia
58 LINE_BREAK, // `\\`, with a tightly-bound `*` and/or `[len]` (`\\*[2ex]`)
59 ROOT, // the document root (keep LAST)
60}
61
62impl SyntaxKind {
63 /// The number of `SyntaxKind` variants. Sound because the enum is
64 /// `#[repr(u16)]` with contiguous discriminants `0..=ROOT` and `ROOT` is kept
65 /// last; used to size kind-indexed tables (e.g. the linter's dispatch table).
66 pub const COUNT: usize = SyntaxKind::ROOT as usize + 1;
67}
68
69impl From<SyntaxKind> for rowan::SyntaxKind {
70 fn from(kind: SyntaxKind) -> Self {
71 Self(kind as u16)
72 }
73}
74
75/// The rowan language marker for badness's CST.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub enum BadnessLang {}
78
79impl Language for BadnessLang {
80 type Kind = SyntaxKind;
81
82 fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
83 assert!(
84 raw.0 <= SyntaxKind::ROOT as u16,
85 "invalid SyntaxKind discriminant: {}",
86 raw.0
87 );
88 // SAFETY: `SyntaxKind` is `#[repr(u16)]` with contiguous discriminants
89 // `0..=ROOT`, and the assert above bounds `raw.0` into that range.
90 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
91 }
92
93 fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
94 kind.into()
95 }
96}
97
98pub type SyntaxNode = rowan::SyntaxNode<BadnessLang>;
99pub type SyntaxToken = rowan::SyntaxToken<BadnessLang>;
100pub type SyntaxElement = rowan::SyntaxElement<BadnessLang>;
101
102/// Whitespace and newlines are the only trivia the formatter rewrites; comments
103/// are preserved verbatim and so are *not* collapsible. A shared shape
104/// predicate: the formatter's gap normalization and the semantic layer's expl3
105/// statement segmentation both skip exactly this class.
106pub fn is_collapsible_trivia(kind: SyntaxKind) -> bool {
107 matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
108}
109
110/// Whether a `WORD` token is a single TeX parameter digit (`1`..=`9`) — the
111/// shape that follows `#` in a parameter reference. Reads only the token text.
112pub fn is_param_digit(t: &SyntaxToken) -> bool {
113 matches!(t.text().as_bytes(), [b'1'..=b'9'])
114}