styx_cst/
syntax_kind.rs

1//! Syntax node and token kinds for the Styx CST.
2
3use styx_parse::TokenKind;
4
5/// The kind of a syntax element (node or token).
6///
7/// Tokens are terminal elements (leaves), while nodes are non-terminal
8/// (contain children). The distinction is made by value: tokens have
9/// lower values than `__LAST_TOKEN`.
10///
11/// The SCREAMING_CASE naming convention is used to match rowan/rust-analyzer
12/// conventions for syntax kinds.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u16)]
15#[allow(non_camel_case_types)]
16#[allow(clippy::manual_non_exhaustive)] // __LAST_TOKEN is used for token/node distinction
17pub enum SyntaxKind {
18    // ========== TOKENS (terminals) ==========
19    // Structural tokens
20    /// `{`
21    L_BRACE = 0,
22    /// `}`
23    R_BRACE,
24    /// `(`
25    L_PAREN,
26    /// `)`
27    R_PAREN,
28    /// `,`
29    COMMA,
30    /// `=`
31    GT,
32    /// `@`
33    AT,
34
35    // Scalar tokens
36    /// Bare (unquoted) scalar: `hello`, `42`, `true`
37    BARE_SCALAR,
38    /// Quoted scalar: `"hello world"`
39    QUOTED_SCALAR,
40    /// Raw scalar: `r#"..."#`
41    RAW_SCALAR,
42    /// Heredoc start marker: `<<DELIM\n`
43    HEREDOC_START,
44    /// Heredoc content
45    HEREDOC_CONTENT,
46    /// Heredoc end marker
47    HEREDOC_END,
48
49    // Comment tokens
50    /// Line comment: `// ...`
51    LINE_COMMENT,
52    /// Doc comment: `/// ...`
53    DOC_COMMENT,
54
55    // Whitespace tokens
56    /// Horizontal whitespace (spaces, tabs)
57    WHITESPACE,
58    /// Newline (`\n` or `\r\n`)
59    NEWLINE,
60
61    // Special tokens
62    /// End of file
63    EOF,
64    /// Lexer/parser error
65    ERROR,
66
67    // Marker for end of tokens
68    #[doc(hidden)]
69    __LAST_TOKEN,
70
71    // ========== NODES (non-terminals) ==========
72    /// Root document node
73    DOCUMENT,
74    /// An entry (key-value pair or sequence element)
75    ENTRY,
76    /// An explicit object `{ ... }`
77    OBJECT,
78    /// A sequence `( ... )`
79    SEQUENCE,
80    /// A scalar value wrapper
81    SCALAR,
82    /// Unit value `@`
83    UNIT,
84    /// A tag `@name` with optional payload
85    TAG,
86    /// Tag name (without @)
87    TAG_NAME,
88    /// Tag payload (the value after the tag name)
89    TAG_PAYLOAD,
90    /// Key in an entry
91    KEY,
92    /// Value in an entry
93    VALUE,
94    /// A heredoc (groups start, content, end)
95    HEREDOC,
96    /// A group of attributes (key=value pairs)
97    ATTRIBUTES,
98    /// A single attribute (key=value)
99    ATTRIBUTE,
100}
101
102impl SyntaxKind {
103    /// Whether this is a token (terminal) kind.
104    pub fn is_token(self) -> bool {
105        (self as u16) < (Self::__LAST_TOKEN as u16)
106    }
107
108    /// Whether this is a node (non-terminal) kind.
109    pub fn is_node(self) -> bool {
110        (self as u16) > (Self::__LAST_TOKEN as u16)
111    }
112
113    /// Whether this is trivia (whitespace or comments).
114    pub fn is_trivia(self) -> bool {
115        matches!(self, Self::WHITESPACE | Self::NEWLINE | Self::LINE_COMMENT)
116    }
117}
118
119impl From<TokenKind> for SyntaxKind {
120    fn from(kind: TokenKind) -> Self {
121        match kind {
122            TokenKind::LBrace => Self::L_BRACE,
123            TokenKind::RBrace => Self::R_BRACE,
124            TokenKind::LParen => Self::L_PAREN,
125            TokenKind::RParen => Self::R_PAREN,
126            TokenKind::Comma => Self::COMMA,
127            TokenKind::Gt => Self::GT,
128            TokenKind::At => Self::AT,
129            TokenKind::BareScalar => Self::BARE_SCALAR,
130            TokenKind::QuotedScalar => Self::QUOTED_SCALAR,
131            TokenKind::RawScalar => Self::RAW_SCALAR,
132            TokenKind::HeredocStart => Self::HEREDOC_START,
133            TokenKind::HeredocContent => Self::HEREDOC_CONTENT,
134            TokenKind::HeredocEnd => Self::HEREDOC_END,
135            TokenKind::LineComment => Self::LINE_COMMENT,
136            TokenKind::DocComment => Self::DOC_COMMENT,
137            TokenKind::Whitespace => Self::WHITESPACE,
138            TokenKind::Newline => Self::NEWLINE,
139            TokenKind::Eof => Self::EOF,
140            TokenKind::Error => Self::ERROR,
141        }
142    }
143}
144
145impl From<SyntaxKind> for rowan::SyntaxKind {
146    fn from(kind: SyntaxKind) -> Self {
147        rowan::SyntaxKind(kind as u16)
148    }
149}
150
151/// Language definition for Styx, used by rowan.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
153pub enum StyxLanguage {}
154
155impl rowan::Language for StyxLanguage {
156    type Kind = SyntaxKind;
157
158    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
159        Self::Kind::from_raw(raw.0).expect("invalid SyntaxKind value from rowan")
160    }
161
162    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
163        rowan::SyntaxKind(kind as u16)
164    }
165}
166
167impl SyntaxKind {
168    /// Convert from a raw u16 value to SyntaxKind.
169    /// Returns None if the value is out of range or corresponds to __LAST_TOKEN.
170    pub const fn from_raw(raw: u16) -> Option<Self> {
171        match raw {
172            0 => Some(Self::L_BRACE),
173            1 => Some(Self::R_BRACE),
174            2 => Some(Self::L_PAREN),
175            3 => Some(Self::R_PAREN),
176            4 => Some(Self::COMMA),
177            5 => Some(Self::GT),
178            6 => Some(Self::AT),
179            7 => Some(Self::BARE_SCALAR),
180            8 => Some(Self::QUOTED_SCALAR),
181            9 => Some(Self::RAW_SCALAR),
182            10 => Some(Self::HEREDOC_START),
183            11 => Some(Self::HEREDOC_CONTENT),
184            12 => Some(Self::HEREDOC_END),
185            13 => Some(Self::LINE_COMMENT),
186            14 => Some(Self::DOC_COMMENT),
187            15 => Some(Self::WHITESPACE),
188            16 => Some(Self::NEWLINE),
189            17 => Some(Self::EOF),
190            18 => Some(Self::ERROR),
191            // 19 is __LAST_TOKEN - skip it
192            20 => Some(Self::DOCUMENT),
193            21 => Some(Self::ENTRY),
194            22 => Some(Self::OBJECT),
195            23 => Some(Self::SEQUENCE),
196            24 => Some(Self::SCALAR),
197            25 => Some(Self::UNIT),
198            26 => Some(Self::TAG),
199            27 => Some(Self::TAG_NAME),
200            28 => Some(Self::TAG_PAYLOAD),
201            29 => Some(Self::KEY),
202            30 => Some(Self::VALUE),
203            31 => Some(Self::HEREDOC),
204            32 => Some(Self::ATTRIBUTES),
205            33 => Some(Self::ATTRIBUTE),
206            _ => None,
207        }
208    }
209}
210
211/// A syntax node in the Styx CST.
212pub type SyntaxNode = rowan::SyntaxNode<StyxLanguage>;
213
214/// A syntax token in the Styx CST.
215pub type SyntaxToken = rowan::SyntaxToken<StyxLanguage>;
216
217/// A syntax element (either node or token) in the Styx CST.
218pub type SyntaxElement = rowan::SyntaxElement<StyxLanguage>;
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use rowan::Language;
224
225    #[test]
226    fn token_vs_node() {
227        assert!(SyntaxKind::L_BRACE.is_token());
228        assert!(SyntaxKind::WHITESPACE.is_token());
229        assert!(SyntaxKind::ERROR.is_token());
230
231        assert!(SyntaxKind::DOCUMENT.is_node());
232        assert!(SyntaxKind::ENTRY.is_node());
233        assert!(SyntaxKind::OBJECT.is_node());
234    }
235
236    #[test]
237    fn trivia() {
238        assert!(SyntaxKind::WHITESPACE.is_trivia());
239        assert!(SyntaxKind::NEWLINE.is_trivia());
240        assert!(SyntaxKind::LINE_COMMENT.is_trivia());
241
242        assert!(!SyntaxKind::DOC_COMMENT.is_trivia());
243        assert!(!SyntaxKind::BARE_SCALAR.is_trivia());
244    }
245
246    #[test]
247    fn token_kind_conversion() {
248        assert_eq!(SyntaxKind::from(TokenKind::LBrace), SyntaxKind::L_BRACE);
249        assert_eq!(
250            SyntaxKind::from(TokenKind::BareScalar),
251            SyntaxKind::BARE_SCALAR
252        );
253        assert_eq!(SyntaxKind::from(TokenKind::Newline), SyntaxKind::NEWLINE);
254    }
255
256    #[test]
257    fn rowan_roundtrip() {
258        let kind = SyntaxKind::DOCUMENT;
259        let raw = StyxLanguage::kind_to_raw(kind);
260        let back = StyxLanguage::kind_from_raw(raw);
261        assert_eq!(kind, back);
262    }
263}