Skip to main content

ronin_core/syntax/
kind.rs

1//! [`SyntaxKind`] — the closed classification of every CST node and token.
2//!
3//! The set is fixed by the pinned RON grammar (ron 0.12.x, per AD-002/TR-004).
4//! Every [`crate::syntax::SyntaxNode`] and [`crate::syntax::SyntaxToken`] carries
5//! exactly one `SyntaxKind`. The enum is `#[repr(u16)]` so it maps directly onto
6//! rowan's raw `u16` kind, but rowan's raw integers are **never** exposed in the
7//! public API (TR-009/INV-7): conversion lives in [`RonLang`] below.
8//!
9//! Kinds are split into two families:
10//!
11//! * **Token kinds** — leaf classifications produced by the lexer. Each source
12//!   byte lands in exactly one token (INV-1). Includes trivia (whitespace,
13//!   comments, BOM) and the error/sentinel kinds.
14//! * **Node kinds** — interior classifications produced by the parser while
15//!   building the green tree, including the `Error` recovery kind and `Root`.
16
17/// Closed classification of every CST node and token.
18///
19/// `#[repr(u16)]` with explicit, stable discriminants so the mapping to/from
20/// rowan's raw kind is total and order-independent. New variants MUST be added
21/// before [`SyntaxKind::__Last`] and existing discriminants MUST NOT be
22/// renumbered (the value is part of the on-tree representation).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[repr(u16)]
25#[non_exhaustive]
26pub enum SyntaxKind {
27    // ---- Trivia tokens (semantically inert, preserved for losslessness) ----
28    /// Run of ASCII/Unicode whitespace (spaces, tabs, newlines — CR and LF
29    /// preserved verbatim).
30    Whitespace = 0,
31    /// A leading UTF-8 byte-order mark (`\u{FEFF}`), kept as trivia (AD-008).
32    Bom,
33    /// Line comment: `// ...` up to (but not including) the line break.
34    LineComment,
35    /// Block comment: `/* ... */`, nestable.
36    BlockComment,
37
38    // ---- Literal / atom tokens ----
39    /// Bare identifier or struct/variant/field name (e.g. `Foo`, `x`).
40    Ident,
41    /// Integer literal (any base, with optional `_` separators / type suffix).
42    Integer,
43    /// Floating-point literal (with optional `_` separators / type suffix).
44    Float,
45    /// Normal string literal `"..."` (with escapes), verbatim including quotes.
46    String,
47    /// Raw string literal `r#"..."#`, verbatim including delimiters.
48    RawString,
49    /// Character literal `'c'` (with escapes), verbatim including quotes.
50    Char,
51    /// The `true` keyword token.
52    TrueKw,
53    /// The `false` keyword token.
54    FalseKw,
55
56    // ---- Punctuation tokens ----
57    /// `(`
58    LParen,
59    /// `)`
60    RParen,
61    /// `[`
62    LBracket,
63    /// `]`
64    RBracket,
65    /// `{`
66    LBrace,
67    /// `}`
68    RBrace,
69    /// `:`
70    Colon,
71    /// `,`
72    Comma,
73
74    // ---- Extension-attribute tokens (`#![enable(...)]`) ----
75    /// `#`
76    Hash,
77    /// `!`
78    Bang,
79    /// `enable` keyword inside an extension attribute.
80    EnableKw,
81
82    /// Any byte run the lexer could not classify (recovery sentinel token).
83    /// Always wrapped in an [`SyntaxKind::Error`] node by the parser.
84    LexError,
85
86    // =====================================================================
87    // Node kinds (interior tree classifications, produced by the parser).
88    // =====================================================================
89    /// The document root node. Holds the single top-level value plus any
90    /// leading extension attributes and surrounding trivia.
91    Root,
92    /// Named struct `Name( field: value, ... )` or anonymous struct
93    /// `( field: value, ... )`.
94    Struct,
95    /// A single `field: value` entry inside a [`SyntaxKind::Struct`].
96    StructField,
97    /// Tuple / tuple-struct `( a, b, c )` (positional, no field names).
98    Tuple,
99    /// List / sequence `[ a, b, c ]`.
100    List,
101    /// Map `{ k: v, ... }` (keys may be non-string values).
102    Map,
103    /// A single `key: value` entry inside a [`SyntaxKind::Map`].
104    MapEntry,
105    /// Enum variant: a bare `Ident`, or `Ident(...)` / `Ident{...}` payload.
106    EnumVariant,
107    /// Unit value `()`.
108    Unit,
109    /// Wrapper around a scalar literal token so scalar values are uniform nodes.
110    Literal,
111    /// Extension attribute `#![enable(ext, ...)]`. Unknown extensions are still
112    /// preserved verbatim as text within this node.
113    ExtensionAttr,
114    /// Recovery node wrapping unexpected/unparseable tokens so the tree still
115    /// covers all input (TR-005/INV-3).
116    Error,
117
118    /// Sentinel marking the exclusive upper bound of the enum. NOT a real kind;
119    /// never assigned to a node or token. Used only for range checks.
120    #[doc(hidden)]
121    __Last,
122}
123
124impl SyntaxKind {
125    /// Reconstruct a `SyntaxKind` from its raw `u16` discriminant.
126    ///
127    /// Returns `None` for any value outside the closed set (including the
128    /// `__Last` sentinel), so a corrupt/foreign raw kind can never silently
129    /// masquerade as a valid kind.
130    ///
131    /// Implemented with a total `match` (no `unsafe`/`transmute`) so the crate
132    /// can keep `#![forbid(unsafe_code)]`. The `ALL` table keeps this in sync
133    /// with the variant set; a missing variant would be caught by
134    /// `raw_roundtrip_is_total`.
135    #[inline]
136    #[must_use]
137    pub(crate) fn from_raw(raw: u16) -> Option<Self> {
138        Self::ALL.get(raw as usize).copied()
139    }
140
141    /// Every real `SyntaxKind` variant in discriminant order (excludes the
142    /// `__Last` sentinel). The slice index equals the variant's `u16` value.
143    const ALL: &'static [SyntaxKind] = &[
144        SyntaxKind::Whitespace,
145        SyntaxKind::Bom,
146        SyntaxKind::LineComment,
147        SyntaxKind::BlockComment,
148        SyntaxKind::Ident,
149        SyntaxKind::Integer,
150        SyntaxKind::Float,
151        SyntaxKind::String,
152        SyntaxKind::RawString,
153        SyntaxKind::Char,
154        SyntaxKind::TrueKw,
155        SyntaxKind::FalseKw,
156        SyntaxKind::LParen,
157        SyntaxKind::RParen,
158        SyntaxKind::LBracket,
159        SyntaxKind::RBracket,
160        SyntaxKind::LBrace,
161        SyntaxKind::RBrace,
162        SyntaxKind::Colon,
163        SyntaxKind::Comma,
164        SyntaxKind::Hash,
165        SyntaxKind::Bang,
166        SyntaxKind::EnableKw,
167        SyntaxKind::LexError,
168        SyntaxKind::Root,
169        SyntaxKind::Struct,
170        SyntaxKind::StructField,
171        SyntaxKind::Tuple,
172        SyntaxKind::List,
173        SyntaxKind::Map,
174        SyntaxKind::MapEntry,
175        SyntaxKind::EnumVariant,
176        SyntaxKind::Unit,
177        SyntaxKind::Literal,
178        SyntaxKind::ExtensionAttr,
179        SyntaxKind::Error,
180    ];
181
182    /// The raw `u16` discriminant for this kind (rowan-facing only).
183    #[inline]
184    #[must_use]
185    pub(crate) fn to_raw(self) -> u16 {
186        self as u16
187    }
188
189    /// `true` for trivia token kinds (whitespace, comments, BOM).
190    ///
191    /// Trivia is semantically inert but preserved verbatim for losslessness
192    /// (AD-001). Used by the printer/accessors, never alters byte coverage.
193    #[inline]
194    #[must_use]
195    pub fn is_trivia(self) -> bool {
196        matches!(
197            self,
198            Self::Whitespace | Self::Bom | Self::LineComment | Self::BlockComment
199        )
200    }
201
202    /// `true` if this kind classifies a token (leaf) rather than a node.
203    #[inline]
204    #[must_use]
205    pub fn is_token(self) -> bool {
206        (self as u16) <= (Self::LexError as u16)
207    }
208}
209
210/// The rowan [`Language`](rowan::Language) implementation for RON.
211///
212/// This is the single bridge between [`SyntaxKind`] and rowan's raw `u16`
213/// kind. It is `pub(crate)` — it MUST NOT appear in the public API so that the
214/// underlying CST library stays swappable (HINT-005/INV-7).
215#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
216pub(crate) enum RonLang {}
217
218impl rowan::Language for RonLang {
219    type Kind = SyntaxKind;
220
221    #[inline]
222    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
223        // A raw kind always originates from a `SyntaxKind` we ourselves wrote
224        // into the green tree, so the range check should always succeed. If it
225        // somehow does not, fall back to `Error` rather than panicking — this
226        // keeps the never-panic contract (TR-001) even under internal misuse.
227        SyntaxKind::from_raw(raw.0).unwrap_or(SyntaxKind::Error)
228    }
229
230    #[inline]
231    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
232        rowan::SyntaxKind(kind.to_raw())
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn raw_roundtrip_is_total() {
242        // The ALL table must hold exactly the variants `0..__Last`.
243        assert_eq!(
244            SyntaxKind::ALL.len(),
245            SyntaxKind::__Last as usize,
246            "ALL table must list every variant before __Last"
247        );
248        // Every discriminant below `__Last` must round-trip exactly.
249        let mut raw = 0u16;
250        while raw < SyntaxKind::__Last as u16 {
251            let kind = SyntaxKind::from_raw(raw).expect("in-range raw must decode");
252            assert_eq!(kind.to_raw(), raw, "raw discriminant must round-trip");
253            raw += 1;
254        }
255    }
256
257    #[test]
258    fn out_of_range_raw_is_rejected() {
259        assert_eq!(SyntaxKind::from_raw(SyntaxKind::__Last as u16), None);
260        assert_eq!(SyntaxKind::from_raw(u16::MAX), None);
261    }
262
263    #[test]
264    fn token_node_partition_is_consistent() {
265        assert!(SyntaxKind::Whitespace.is_token());
266        assert!(SyntaxKind::LexError.is_token());
267        assert!(!SyntaxKind::Root.is_token());
268        assert!(!SyntaxKind::Error.is_token());
269    }
270
271    #[test]
272    fn trivia_classification() {
273        assert!(SyntaxKind::Whitespace.is_trivia());
274        assert!(SyntaxKind::Bom.is_trivia());
275        assert!(SyntaxKind::LineComment.is_trivia());
276        assert!(SyntaxKind::BlockComment.is_trivia());
277        assert!(!SyntaxKind::Ident.is_trivia());
278        assert!(!SyntaxKind::Comma.is_trivia());
279    }
280}