Skip to main content

brink_syntax/
syntax_kind.rs

1/// All syntactic constructs in the Ink language.
2///
3/// Tokens (lexer output) and nodes (parser output) share a single flat enum
4/// so that `rowan` can store them in one `u16` discriminant. Use [`is_token`]
5/// and [`is_node`] to classify at runtime.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7#[repr(u16)]
8#[expect(non_camel_case_types)]
9pub enum SyntaxKind {
10    // ── Trivia tokens ─────────────────────────────────────────────
11    /// Spaces and tabs (NOT newlines).
12    WHITESPACE = 0,
13    /// `\n` or `\r\n`.
14    NEWLINE,
15    /// `// ...` through end-of-line.
16    LINE_COMMENT,
17    /// `/* ... */` (may span lines).
18    BLOCK_COMMENT,
19
20    // ── Keyword tokens ────────────────────────────────────────────
21    KW_INCLUDE,
22    KW_EXTERNAL,
23    KW_VAR,
24    KW_CONST,
25    KW_LIST,
26    KW_TEMP,
27    KW_RETURN,
28    KW_REF,
29    KW_TRUE,
30    KW_FALSE,
31    KW_NOT,
32    KW_AND,
33    KW_OR,
34    KW_MOD,
35    KW_HAS,
36    KW_HASNT,
37    KW_ELSE,
38    KW_FUNCTION,
39    KW_STOPPING,
40    KW_CYCLE,
41    KW_SHUFFLE,
42    KW_ONCE,
43    KW_DONE,
44    KW_END,
45    KW_TODO,
46
47    // ── Punctuation / operator tokens ────────────────────────────
48    /// `=`
49    EQ,
50    /// `+=`
51    PLUS_EQ,
52    /// `-=`
53    MINUS_EQ,
54    /// `==`
55    EQ_EQ,
56    /// `!=`
57    BANG_EQ,
58    /// `<`
59    LT,
60    /// `>`
61    GT,
62    /// `<=`
63    LT_EQ,
64    /// `>=`
65    GT_EQ,
66    /// `&`
67    AMP,
68    /// `&&`
69    AMP_AMP,
70    /// `+`
71    PLUS,
72    /// `-`
73    MINUS,
74    /// `*`
75    STAR,
76    /// `/`
77    SLASH,
78    /// `%`
79    PERCENT,
80    /// `^`
81    CARET,
82    /// `!`
83    BANG,
84    /// `?`
85    QUESTION,
86    /// `!?`
87    BANG_QUESTION,
88    /// `$`
89    DOLLAR,
90    /// `(`
91    L_PAREN,
92    /// `)`
93    R_PAREN,
94    /// `{`
95    L_BRACE,
96    /// `}`
97    R_BRACE,
98    /// `[`
99    L_BRACKET,
100    /// `]`
101    R_BRACKET,
102    /// `|`
103    PIPE,
104    /// `,`
105    COMMA,
106    /// `.`
107    DOT,
108    /// `:`
109    COLON,
110    /// `#`
111    HASH,
112    /// `~`
113    TILDE,
114    /// `\`
115    BACKSLASH,
116
117    // ── Compound tokens ──────────────────────────────────────────
118    /// `<>`
119    GLUE,
120    /// `->`
121    DIVERT,
122    /// `<-`
123    THREAD,
124    /// `->->`
125    TUNNEL_ONWARDS,
126
127    // ── Content tokens ───────────────────────────────────────────
128    /// Integer literal (digits only; no leading sign).
129    INTEGER,
130    /// Float literal (`digits.digits`).
131    FLOAT,
132    /// `"` (opening or closing quote).
133    QUOTE,
134    /// Run of non-special characters inside a string literal.
135    STRING_TEXT,
136    /// Escape sequence inside a string (`\n`, `\t`, `\\`, `\"`).
137    STRING_ESCAPE,
138    /// Identifier: `(IDENT_START IDENT_CONTINUE*) | (DIGIT+ IDENT_START IDENT_CONTINUE*)`.
139    IDENT,
140    /// Any byte the lexer could not classify.
141    ERROR_TOKEN,
142    /// End of file (synthetic).
143    EOF,
144
145    // ── Node kinds (parser) ──────────────────────────────────────
146    SOURCE_FILE,
147    INCLUDE_STMT,
148    FILE_PATH,
149    EXTERNAL_DECL,
150    KNOT_DEF,
151    KNOT_HEADER,
152    KNOT_BODY,
153    KNOT_PARAMS,
154    KNOT_PARAM_DECL,
155    STITCH_DEF,
156    STITCH_HEADER,
157    STITCH_BODY,
158    EMPTY_LINE,
159    AUTHOR_WARNING,
160    LOGIC_LINE,
161    CONTENT_LINE,
162    TAG_LINE,
163    STRAY_CLOSING_BRACE,
164    RETURN_STMT,
165    TEMP_DECL,
166    ASSIGNMENT,
167    MIXED_CONTENT,
168    TEXT,
169    ESCAPE,
170    GLUE_NODE,
171    CHOICE,
172    CHOICE_BULLETS,
173    LABEL,
174    CHOICE_CONDITION,
175    CHOICE_START_CONTENT,
176    CHOICE_BRACKET_CONTENT,
177    CHOICE_INNER_CONTENT,
178    GATHER,
179    GATHER_DASHES,
180    TAGS,
181    TAG,
182    INLINE_LOGIC,
183    MULTILINE_BLOCK,
184    SEQUENCE_WITH_ANNOTATION,
185    SEQUENCE_SYMBOL_ANNOTATION,
186    SEQUENCE_WORD_ANNOTATION,
187    INLINE_BRANCHES_SEQ,
188    MULTILINE_BRANCHES_SEQ,
189    MULTILINE_BRANCH_SEQ,
190    BRANCH_CONTENT,
191    CONDITIONAL_WITH_EXPR,
192    BRANCHLESS_COND_BODY,
193    ELSE_BRANCH,
194    INLINE_BRANCHES_COND,
195    MULTILINE_BRANCHES_COND,
196    MULTILINE_CONDITIONAL,
197    MULTILINE_BRANCH_COND,
198    MULTILINE_BRANCH_BODY,
199    IMPLICIT_SEQUENCE,
200    INNER_EXPRESSION,
201    PREFIX_EXPR,
202    POSTFIX_EXPR,
203    INFIX_EXPR,
204    PAREN_EXPR,
205    FUNCTION_CALL,
206    ARG_LIST,
207    DIVERT_TARGET_EXPR,
208    LIST_EXPR,
209    DIVERT_NODE,
210    SIMPLE_DIVERT,
211    DIVERT_TARGET_WITH_ARGS,
212    THREAD_START,
213    TUNNEL_ONWARDS_NODE,
214    TUNNEL_CALL_NODE,
215    IDENTIFIER,
216    PATH,
217    VAR_DECL,
218    CONST_DECL,
219    LIST_DECL,
220    LIST_DEF,
221    LIST_MEMBER,
222    LIST_MEMBER_ON,
223    LIST_MEMBER_OFF,
224    FUNCTION_PARAM_LIST,
225    INTEGER_LIT,
226    FLOAT_LIT,
227    STRING_LIT,
228    BOOLEAN_LIT,
229    ERROR,
230
231    // Not a real kind — used only for `rowan::Language::kind_to_raw` bounds.
232    #[doc(hidden)]
233    __LAST,
234}
235
236impl SyntaxKind {
237    /// Returns `true` for tokens produced by the lexer (leaf nodes in the CST).
238    #[must_use]
239    pub fn is_token(self) -> bool {
240        matches!(
241            self,
242            Self::WHITESPACE
243                | Self::NEWLINE
244                | Self::LINE_COMMENT
245                | Self::BLOCK_COMMENT
246                | Self::KW_INCLUDE
247                | Self::KW_EXTERNAL
248                | Self::KW_VAR
249                | Self::KW_CONST
250                | Self::KW_LIST
251                | Self::KW_TEMP
252                | Self::KW_RETURN
253                | Self::KW_REF
254                | Self::KW_TRUE
255                | Self::KW_FALSE
256                | Self::KW_NOT
257                | Self::KW_AND
258                | Self::KW_OR
259                | Self::KW_MOD
260                | Self::KW_HAS
261                | Self::KW_HASNT
262                | Self::KW_ELSE
263                | Self::KW_FUNCTION
264                | Self::KW_STOPPING
265                | Self::KW_CYCLE
266                | Self::KW_SHUFFLE
267                | Self::KW_ONCE
268                | Self::KW_DONE
269                | Self::KW_END
270                | Self::KW_TODO
271                | Self::EQ
272                | Self::PLUS_EQ
273                | Self::MINUS_EQ
274                | Self::EQ_EQ
275                | Self::BANG_EQ
276                | Self::LT
277                | Self::GT
278                | Self::LT_EQ
279                | Self::GT_EQ
280                | Self::AMP
281                | Self::AMP_AMP
282                | Self::PLUS
283                | Self::MINUS
284                | Self::STAR
285                | Self::SLASH
286                | Self::PERCENT
287                | Self::CARET
288                | Self::BANG
289                | Self::QUESTION
290                | Self::BANG_QUESTION
291                | Self::DOLLAR
292                | Self::L_PAREN
293                | Self::R_PAREN
294                | Self::L_BRACE
295                | Self::R_BRACE
296                | Self::L_BRACKET
297                | Self::R_BRACKET
298                | Self::PIPE
299                | Self::COMMA
300                | Self::DOT
301                | Self::COLON
302                | Self::HASH
303                | Self::TILDE
304                | Self::BACKSLASH
305                | Self::GLUE
306                | Self::DIVERT
307                | Self::THREAD
308                | Self::TUNNEL_ONWARDS
309                | Self::INTEGER
310                | Self::FLOAT
311                | Self::QUOTE
312                | Self::STRING_TEXT
313                | Self::STRING_ESCAPE
314                | Self::IDENT
315                | Self::ERROR_TOKEN
316                | Self::EOF
317        )
318    }
319
320    /// Returns `true` for composite nodes built by the parser.
321    #[must_use]
322    pub fn is_node(self) -> bool {
323        !self.is_token() && self != Self::__LAST
324    }
325
326    /// Returns `true` for trivia — tokens the parser may skip over.
327    /// `NEWLINE` is **not** trivia; it terminates lines and delimits blocks.
328    #[must_use]
329    pub fn is_trivia(self) -> bool {
330        matches!(
331            self,
332            Self::WHITESPACE | Self::LINE_COMMENT | Self::BLOCK_COMMENT
333        )
334    }
335
336    /// Returns `true` for keyword tokens.
337    #[must_use]
338    pub fn is_keyword(self) -> bool {
339        matches!(
340            self,
341            Self::KW_INCLUDE
342                | Self::KW_EXTERNAL
343                | Self::KW_VAR
344                | Self::KW_CONST
345                | Self::KW_LIST
346                | Self::KW_TEMP
347                | Self::KW_RETURN
348                | Self::KW_REF
349                | Self::KW_TRUE
350                | Self::KW_FALSE
351                | Self::KW_NOT
352                | Self::KW_AND
353                | Self::KW_OR
354                | Self::KW_MOD
355                | Self::KW_HAS
356                | Self::KW_HASNT
357                | Self::KW_ELSE
358                | Self::KW_FUNCTION
359                | Self::KW_STOPPING
360                | Self::KW_CYCLE
361                | Self::KW_SHUFFLE
362                | Self::KW_ONCE
363                | Self::KW_DONE
364                | Self::KW_END
365                | Self::KW_TODO
366        )
367    }
368}
369
370/// Rowan language tag for Ink.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
372pub enum InkLanguage {}
373
374impl rowan::Language for InkLanguage {
375    type Kind = SyntaxKind;
376
377    fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
378        assert!(raw.0 < SyntaxKind::__LAST as u16);
379        // SAFETY: `SyntaxKind` is `#[repr(u16)]` with contiguous discriminants,
380        // and we just checked bounds.
381        #[expect(unsafe_code, reason = "repr(u16) transmute with bounds check")]
382        unsafe {
383            std::mem::transmute::<u16, SyntaxKind>(raw.0)
384        }
385    }
386
387    fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
388        rowan::SyntaxKind(kind as u16)
389    }
390}
391
392/// A rowan `SyntaxNode` parameterized by [`InkLanguage`].
393pub type SyntaxNode = rowan::SyntaxNode<InkLanguage>;
394/// A rowan `SyntaxToken` parameterized by [`InkLanguage`].
395pub type SyntaxToken = rowan::SyntaxToken<InkLanguage>;
396/// A rowan `SyntaxElement` parameterized by [`InkLanguage`].
397pub type SyntaxElement = rowan::SyntaxElement<InkLanguage>;
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use rowan::Language;
403
404    #[test]
405    fn roundtrip_through_rowan() {
406        // Every SyntaxKind (except __LAST) should survive raw → kind → raw.
407        let mut i = 0u16;
408        loop {
409            if i == SyntaxKind::__LAST as u16 {
410                break;
411            }
412            let raw = rowan::SyntaxKind(i);
413            let kind = InkLanguage::kind_from_raw(raw);
414            let back = InkLanguage::kind_to_raw(kind);
415            assert_eq!(raw, back, "roundtrip failed for discriminant {i}");
416            i += 1;
417        }
418    }
419
420    #[test]
421    fn token_node_partition() {
422        // Every kind (except __LAST) is either a token or a node, never both.
423        let mut i = 0u16;
424        loop {
425            if i == SyntaxKind::__LAST as u16 {
426                break;
427            }
428            let kind = InkLanguage::kind_from_raw(rowan::SyntaxKind(i));
429            assert!(
430                kind.is_token() ^ kind.is_node(),
431                "{kind:?} is neither token nor node (or both)"
432            );
433            i += 1;
434        }
435    }
436
437    #[test]
438    fn trivia_is_subset_of_tokens() {
439        let mut i = 0u16;
440        loop {
441            if i == SyntaxKind::__LAST as u16 {
442                break;
443            }
444            let kind = InkLanguage::kind_from_raw(rowan::SyntaxKind(i));
445            if kind.is_trivia() {
446                assert!(kind.is_token(), "{kind:?} is trivia but not a token");
447            }
448            i += 1;
449        }
450    }
451
452    #[test]
453    fn newline_is_not_trivia() {
454        assert!(!SyntaxKind::NEWLINE.is_trivia());
455        assert!(SyntaxKind::NEWLINE.is_token());
456    }
457
458    #[test]
459    fn keywords_are_tokens() {
460        let keywords = [
461            SyntaxKind::KW_INCLUDE,
462            SyntaxKind::KW_EXTERNAL,
463            SyntaxKind::KW_VAR,
464            SyntaxKind::KW_CONST,
465            SyntaxKind::KW_LIST,
466            SyntaxKind::KW_TEMP,
467            SyntaxKind::KW_RETURN,
468            SyntaxKind::KW_REF,
469            SyntaxKind::KW_TRUE,
470            SyntaxKind::KW_FALSE,
471            SyntaxKind::KW_NOT,
472            SyntaxKind::KW_AND,
473            SyntaxKind::KW_OR,
474            SyntaxKind::KW_MOD,
475            SyntaxKind::KW_HAS,
476            SyntaxKind::KW_HASNT,
477            SyntaxKind::KW_ELSE,
478            SyntaxKind::KW_FUNCTION,
479            SyntaxKind::KW_STOPPING,
480            SyntaxKind::KW_CYCLE,
481            SyntaxKind::KW_SHUFFLE,
482            SyntaxKind::KW_ONCE,
483            SyntaxKind::KW_DONE,
484            SyntaxKind::KW_END,
485            SyntaxKind::KW_TODO,
486        ];
487        for kw in keywords {
488            assert!(kw.is_token(), "{kw:?} should be a token");
489            assert!(kw.is_keyword(), "{kw:?} should be a keyword");
490        }
491    }
492
493    #[test]
494    fn non_keywords_are_not_keywords() {
495        assert!(!SyntaxKind::IDENT.is_keyword());
496        assert!(!SyntaxKind::PLUS.is_keyword());
497        assert!(!SyntaxKind::SOURCE_FILE.is_keyword());
498    }
499}