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}