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    /// `IMPORT` — module import statement (brink extension, M-2,
47    /// docs/modules-spec.md §2). `FROM`/`AS` stay contextual `IDENT`s.
48    KW_IMPORT,
49
50    // ── Punctuation / operator tokens ────────────────────────────
51    /// `=`
52    EQ,
53    /// `+=`
54    PLUS_EQ,
55    /// `-=`
56    MINUS_EQ,
57    /// `==`
58    EQ_EQ,
59    /// `!=`
60    BANG_EQ,
61    /// `<`
62    LT,
63    /// `>`
64    GT,
65    /// `<=`
66    LT_EQ,
67    /// `>=`
68    GT_EQ,
69    /// `&`
70    AMP,
71    /// `&&`
72    AMP_AMP,
73    /// `+`
74    PLUS,
75    /// `-`
76    MINUS,
77    /// `*`
78    STAR,
79    /// `/`
80    SLASH,
81    /// `%`
82    PERCENT,
83    /// `^`
84    CARET,
85    /// `!`
86    BANG,
87    /// `?`
88    QUESTION,
89    /// `!?`
90    BANG_QUESTION,
91    /// `$`
92    DOLLAR,
93    /// `(`
94    L_PAREN,
95    /// `)`
96    R_PAREN,
97    /// `{`
98    L_BRACE,
99    /// `}`
100    R_BRACE,
101    /// `[`
102    L_BRACKET,
103    /// `]`
104    R_BRACKET,
105    /// `|`
106    PIPE,
107    /// `,`
108    COMMA,
109    /// `.`
110    DOT,
111    /// `:`
112    COLON,
113    /// `#`
114    HASH,
115    /// `~`
116    TILDE,
117    /// `\`
118    BACKSLASH,
119
120    // ── Compound tokens ──────────────────────────────────────────
121    /// `@[` — annotation-line opener (brink extension, NS-A2: the
122    /// `@[effects(…)]` assertion final form, `docs/stdlib-spec.md` §9.2).
123    /// Lexed as one compound token so only the *adjacent* pair opens an
124    /// annotation line; a lone `@` in prose stays an `ERROR_TOKEN`
125    /// swallowed into text, exactly as before.
126    AT_L_BRACKET,
127    /// `<>`
128    GLUE,
129    /// `->`
130    DIVERT,
131    /// `<-`
132    THREAD,
133    /// `->->`
134    TUNNEL_ONWARDS,
135
136    // ── Content tokens ───────────────────────────────────────────
137    /// Integer literal (digits only; no leading sign).
138    INTEGER,
139    /// Float literal (`digits.digits`).
140    FLOAT,
141    /// `"` (opening or closing quote).
142    QUOTE,
143    /// Run of non-special characters inside a string literal.
144    STRING_TEXT,
145    /// Escape sequence inside a string (`\n`, `\t`, `\\`, `\"`).
146    STRING_ESCAPE,
147    /// Identifier: `(IDENT_START IDENT_CONTINUE*) | (DIGIT+ IDENT_START IDENT_CONTINUE*)`.
148    IDENT,
149    /// Any byte the lexer could not classify.
150    ERROR_TOKEN,
151    /// End of file (synthetic).
152    EOF,
153
154    // ── Node kinds (parser) ──────────────────────────────────────
155    SOURCE_FILE,
156    INCLUDE_STMT,
157    /// `IMPORT { a, b AS c } FROM mod` or `IMPORT mod` (M-2).
158    IMPORT_STMT,
159    /// The `{ … }` name list of a bare-form import.
160    IMPORT_LIST,
161    /// One `name` or `name AS alias` entry in an import list.
162    IMPORT_ITEM,
163    /// The module name of an import (both forms).
164    IMPORT_MODULE,
165    FILE_PATH,
166    EXTERNAL_DECL,
167    KNOT_DEF,
168    KNOT_HEADER,
169    KNOT_BODY,
170    KNOT_PARAMS,
171    KNOT_PARAM_DECL,
172    STITCH_DEF,
173    STITCH_HEADER,
174    STITCH_BODY,
175    EMPTY_LINE,
176    AUTHOR_WARNING,
177    LOGIC_LINE,
178    CONTENT_LINE,
179    TAG_LINE,
180    /// `@[name(args)]` — a brink annotation line (NS-A2, the
181    /// `@[effects(…)]` assertion surface). Superset-parsed under every
182    /// dialect; `strict-ink` rejects it in `brink-analyzer::dialect_gate`
183    /// (E051), the standard extension posture.
184    ANNOTATION_LINE,
185    STRAY_CLOSING_BRACE,
186    RETURN_STMT,
187    TEMP_DECL,
188    ASSIGNMENT,
189    MIXED_CONTENT,
190    TEXT,
191    ESCAPE,
192    GLUE_NODE,
193    CHOICE,
194    CHOICE_BULLETS,
195    LABEL,
196    CHOICE_CONDITION,
197    CHOICE_START_CONTENT,
198    CHOICE_BRACKET_CONTENT,
199    CHOICE_INNER_CONTENT,
200    GATHER,
201    GATHER_DASHES,
202    TAGS,
203    TAG,
204    INLINE_LOGIC,
205    MULTILINE_BLOCK,
206    SEQUENCE_WITH_ANNOTATION,
207    SEQUENCE_SYMBOL_ANNOTATION,
208    SEQUENCE_WORD_ANNOTATION,
209    INLINE_BRANCHES_SEQ,
210    MULTILINE_BRANCHES_SEQ,
211    MULTILINE_BRANCH_SEQ,
212    BRANCH_CONTENT,
213    CONDITIONAL_WITH_EXPR,
214    BRANCHLESS_COND_BODY,
215    ELSE_BRANCH,
216    INLINE_BRANCHES_COND,
217    MULTILINE_BRANCHES_COND,
218    MULTILINE_CONDITIONAL,
219    MULTILINE_BRANCH_COND,
220    MULTILINE_BRANCH_BODY,
221    IMPLICIT_SEQUENCE,
222    INNER_EXPRESSION,
223    PREFIX_EXPR,
224    POSTFIX_EXPR,
225    INFIX_EXPR,
226    PAREN_EXPR,
227    FUNCTION_CALL,
228    ARG_LIST,
229    DIVERT_TARGET_EXPR,
230    LIST_EXPR,
231    DIVERT_NODE,
232    SIMPLE_DIVERT,
233    DIVERT_TARGET_WITH_ARGS,
234    THREAD_START,
235    TUNNEL_ONWARDS_NODE,
236    TUNNEL_CALL_NODE,
237    IDENTIFIER,
238    PATH,
239    VAR_DECL,
240    CONST_DECL,
241    LIST_DECL,
242    LIST_DEF,
243    LIST_MEMBER,
244    LIST_MEMBER_ON,
245    LIST_MEMBER_OFF,
246    FUNCTION_PARAM_LIST,
247    INTEGER_LIT,
248    FLOAT_LIT,
249    STRING_LIT,
250    BOOLEAN_LIT,
251    ERROR,
252
253    // ── T1b superset grammar (docs/t1b-surface-spec.md) ────────────
254    // Multi-line `~ { … }` logic blocks (§2). Parse-only in T1b-1 — every
255    // node below is dialect-gated at analysis and never reaches LIR.
256    /// `{ stmt* }` — a braced statement list. Used for the top-level
257    /// `~ { … }` block body and every nested `if`/`while`/`for` body.
258    STMT_BLOCK,
259    /// `if cond { … } (else …)?`. `if`/`else if` are contextual keywords
260    /// (plain `IDENT` tokens) — see `parser::logic`.
261    IF_STMT,
262    /// The `else` arm of an `IF_STMT`: either a nested `IF_STMT` (else-if)
263    /// or a bare `STMT_BLOCK` (else).
264    ELSE_CLAUSE,
265    /// `while cond { … }`.
266    WHILE_STMT,
267    /// `for name in expr { … }`.
268    FOR_STMT,
269    /// `break`.
270    BREAK_STMT,
271    /// `continue`.
272    CONTINUE_STMT,
273    /// A bare expression statement inside a block (function/external calls).
274    EXPR_STMT,
275    /// `await <cond>` — `FlowFrame` suspension point (docs/flow-suspension-spec.md
276    /// §3). Statement/logic position only (`~ await …` logic line or inside a
277    /// `~ { … }` block). Brink extension; `await` is a contextual (soft)
278    /// keyword recognized only in this statement position, so it stays an
279    /// ordinary identifier everywhere else. Wraps the condition expression.
280    AWAIT_STMT,
281    /// `#[expr, …]` — array sigil literal (§3). Expression position only.
282    ARRAY_LITERAL,
283    /// `#{key: expr, …}` — map sigil literal (§3). Expression position only.
284    MAP_LITERAL,
285    /// One `key: expr` pair inside a `MAP_LITERAL`.
286    MAP_ENTRY,
287    /// `base[index]` — postfix indexing, chainable (§4).
288    INDEX_EXPR,
289
290    // ── TM-2 inline type annotations (docs/typed-mode-spec.md §3) ──
291    // `name: type` after params/VAR/temp declarations, `): type ===` return
292    // position. Superset grammar — always parses; dialect-gated (E051 under
293    // strict-ink) at analysis, same pattern as T1b.
294    /// `: type_expr` — one annotation, attached after an identifier (param,
295    /// `VAR`/`temp` name) or a knot header's params (return position).
296    TYPE_ANNOTATION,
297    /// A type expression: wraps exactly one of `TYPE_NAME`, `TYPE_GENERIC`,
298    /// or `TYPE_FN`.
299    TYPE_EXPR,
300    /// A bare nominal type name (`int`, `float`, `bool`, `string`, `divert`,
301    /// `void`, or an unrecognized identifier — semantic validity is an
302    /// analyzer concern, not a grammar one).
303    TYPE_NAME,
304    /// `name<type_expr, …>` — `List<L>`, `Array<T>`, `Map<K, V>`.
305    TYPE_GENERIC,
306    /// `fn(type_expr, …): type_expr` — function type. Parses in T1b/TM-2;
307    /// types as reserved until T1c.
308    TYPE_FN,
309
310    // ── TM-4b structs (docs/typed-mode-spec.md §6) ─────────────────
311    // `STRUCT Name = #{ field: type, … }` declaration, `Name#{field: expr, …}`
312    // construction literal, postfix `.field` access. Superset grammar —
313    // always parses (dialect-gated at analysis, same T1b/TM-2 pattern);
314    // `STRUCT` is a contextual (soft) keyword recognized only at top-level
315    // declaration-start position (`STRUCT` `IDENT` `=` `#` `{`), so it never
316    // reserves the word elsewhere. LIR lowering rejects every construct
317    // below — codegen lands with TM-4c (#666).
318    /// `STRUCT Name = #{ field: type, … }`. Top-level only.
319    STRUCT_DECL,
320    /// One `field: type` pair inside a `STRUCT_DECL`'s body.
321    STRUCT_FIELD_DECL,
322    /// `Name#{field: expr, …}` — struct construction literal. Expression
323    /// position only, like `ARRAY_LITERAL`/`MAP_LITERAL`.
324    STRUCT_LITERAL,
325    /// One `field: expr` pair inside a `STRUCT_LITERAL`.
326    STRUCT_FIELD_INIT,
327    /// `base.field` — postfix field access. Only used where the existing
328    /// dotted-`PATH` grammar doesn't already cover the shape (e.g. after a
329    /// `STRUCT_LITERAL`, an `INDEX_EXPR`, or a parenthesized expression); a
330    /// bare `ident.ident` chain still parses as one `PATH` node and the
331    /// static-path-vs-field-access ambiguity is resolved by
332    /// `brink-analyzer`'s resolution fallback (typed-mode-spec §6), not here.
333    FIELD_ACCESS_EXPR,
334
335    // ── T1c function values (docs/t1c-spec.md §2) ──────────────────
336    /// `#fn(target, args…)` — function-value creation (partial application
337    /// over a named function). Joins the `#[…]`/`#{…}`/`Name#{…}` sigil
338    /// family: expression position only — in prose position `#` still opens
339    /// a tag, unchanged. Superset grammar — always parses; dialect-gated
340    /// (E051 under strict-ink) at analysis, same pattern as T1b/TM-4b.
341    FN_LITERAL,
342
343    // ── T1e path projections (docs/t1e-spec.md §2) ──────────────────
344    // `ref lvalue-path` — path-projection creation in ref-argument position
345    // (`heal(ref npc.hp, 5)`, `#fn(heal, ref party[leader].hp)`,
346    // `bind(f, ref inventory[idx])`). Superset grammar — always parses in
347    // expression position (mirrors `FN_LITERAL`'s dialect-gate pattern);
348    // whether the position is legal (ref-argument only, never standalone)
349    // and whether the root is a durable cell is `brink-analyzer`'s job.
350    /// `ref` followed by a single lvalue-shaped operand — a plain path, a
351    /// dotted field chain, `[…]` indexing, or a mix of the two.
352    REF_EXPR,
353
354    // ── Computed-callee call attempt (docs/t1c-spec.md §3/§10, issue #869) ──
355    // `expr(args…)` where `expr` isn't a bare identifier immediately
356    // followed by `(` (that shape is `FUNCTION_CALL`, consumed at `atom()`).
357    // Direct-call syntax is RULED (t1c-spec §3) to a bare variable/temp/param
358    // callee only; "method-call syntax" (dispatch through an indexed/field/
359    // call-result callee via bare-call sugar) is explicitly out of T1c
360    // (§10). Superset grammar — always parses, so the author's `(args…)`
361    // is captured instead of silently reinterpreted as trailing prose text
362    // (the pre-existing behavior, and the exact silent-no-op class #869
363    // reports); `brink-ir`'s HIR lowering always rejects it (E104), pointing
364    // at the ratified `call(f, args…)` form.
365    /// A postfix call applied to a callee that isn't a bare name — always
366    /// rejected at HIR lowering (E104).
367    CALL_EXPR,
368
369    // ── NS-A5 range literals (docs/stdlib-spec.md §7, F7) ───────────
370    // `a..b` (exclusive) / `a..=b` (inclusive) — integer range values,
371    // joining the closed iterable set (`for i in 0..n`) and feeding the
372    // inhabited-range refinement (`int(1..=6)`, `non_empty(a..b)`).
373    // Superset grammar — always parses; dialect-gated (E051 under
374    // strict-ink) at analysis, same pattern as T1b/T1c. The operator is
375    // two adjacent `DOT` tokens (plus an adjacent `EQ` for `..=`) —
376    // detected in the Pratt loop like `||`/`++`, no new lexer token.
377    /// `start .. end` / `start ..= end` — a range literal expression.
378    RANGE_EXPR,
379
380    // Not a real kind — used only for `rowan::Language::kind_to_raw` bounds.
381    #[doc(hidden)]
382    __LAST,
383}
384
385impl SyntaxKind {
386    /// Returns `true` for tokens produced by the lexer (leaf nodes in the CST).
387    #[must_use]
388    pub fn is_token(self) -> bool {
389        matches!(
390            self,
391            Self::WHITESPACE
392                | Self::NEWLINE
393                | Self::LINE_COMMENT
394                | Self::BLOCK_COMMENT
395                | Self::KW_INCLUDE
396                | Self::KW_EXTERNAL
397                | Self::KW_VAR
398                | Self::KW_CONST
399                | Self::KW_LIST
400                | Self::KW_TEMP
401                | Self::KW_RETURN
402                | Self::KW_REF
403                | Self::KW_TRUE
404                | Self::KW_FALSE
405                | Self::KW_NOT
406                | Self::KW_AND
407                | Self::KW_OR
408                | Self::KW_MOD
409                | Self::KW_HAS
410                | Self::KW_HASNT
411                | Self::KW_ELSE
412                | Self::KW_FUNCTION
413                | Self::KW_STOPPING
414                | Self::KW_CYCLE
415                | Self::KW_SHUFFLE
416                | Self::KW_ONCE
417                | Self::KW_DONE
418                | Self::KW_END
419                | Self::KW_TODO
420                | Self::KW_IMPORT
421                | Self::EQ
422                | Self::PLUS_EQ
423                | Self::MINUS_EQ
424                | Self::EQ_EQ
425                | Self::BANG_EQ
426                | Self::LT
427                | Self::GT
428                | Self::LT_EQ
429                | Self::GT_EQ
430                | Self::AMP
431                | Self::AMP_AMP
432                | Self::PLUS
433                | Self::MINUS
434                | Self::STAR
435                | Self::SLASH
436                | Self::PERCENT
437                | Self::CARET
438                | Self::BANG
439                | Self::QUESTION
440                | Self::BANG_QUESTION
441                | Self::DOLLAR
442                | Self::L_PAREN
443                | Self::R_PAREN
444                | Self::L_BRACE
445                | Self::R_BRACE
446                | Self::L_BRACKET
447                | Self::R_BRACKET
448                | Self::PIPE
449                | Self::COMMA
450                | Self::DOT
451                | Self::COLON
452                | Self::HASH
453                | Self::TILDE
454                | Self::BACKSLASH
455                | Self::AT_L_BRACKET
456                | Self::GLUE
457                | Self::DIVERT
458                | Self::THREAD
459                | Self::TUNNEL_ONWARDS
460                | Self::INTEGER
461                | Self::FLOAT
462                | Self::QUOTE
463                | Self::STRING_TEXT
464                | Self::STRING_ESCAPE
465                | Self::IDENT
466                | Self::ERROR_TOKEN
467                | Self::EOF
468        )
469    }
470
471    /// Returns `true` for composite nodes built by the parser.
472    #[must_use]
473    pub fn is_node(self) -> bool {
474        !self.is_token() && self != Self::__LAST
475    }
476
477    /// Returns `true` for trivia — tokens the parser may skip over.
478    /// `NEWLINE` is **not** trivia; it terminates lines and delimits blocks.
479    #[must_use]
480    pub fn is_trivia(self) -> bool {
481        matches!(
482            self,
483            Self::WHITESPACE | Self::LINE_COMMENT | Self::BLOCK_COMMENT
484        )
485    }
486
487    /// Returns `true` for keyword tokens.
488    #[must_use]
489    pub fn is_keyword(self) -> bool {
490        matches!(
491            self,
492            Self::KW_INCLUDE
493                | Self::KW_EXTERNAL
494                | Self::KW_VAR
495                | Self::KW_CONST
496                | Self::KW_LIST
497                | Self::KW_TEMP
498                | Self::KW_RETURN
499                | Self::KW_REF
500                | Self::KW_TRUE
501                | Self::KW_FALSE
502                | Self::KW_NOT
503                | Self::KW_AND
504                | Self::KW_OR
505                | Self::KW_MOD
506                | Self::KW_HAS
507                | Self::KW_HASNT
508                | Self::KW_ELSE
509                | Self::KW_FUNCTION
510                | Self::KW_STOPPING
511                | Self::KW_CYCLE
512                | Self::KW_SHUFFLE
513                | Self::KW_ONCE
514                | Self::KW_DONE
515                | Self::KW_END
516                | Self::KW_TODO
517                | Self::KW_IMPORT
518        )
519    }
520}
521
522/// Rowan language tag for Ink.
523#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
524pub enum InkLanguage {}
525
526impl rowan::Language for InkLanguage {
527    type Kind = SyntaxKind;
528
529    fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
530        assert!(raw.0 < SyntaxKind::__LAST as u16);
531        // SAFETY: `SyntaxKind` is `#[repr(u16)]` with contiguous discriminants,
532        // and we just checked bounds.
533        #[expect(unsafe_code, reason = "repr(u16) transmute with bounds check")]
534        unsafe {
535            std::mem::transmute::<u16, SyntaxKind>(raw.0)
536        }
537    }
538
539    fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
540        rowan::SyntaxKind(kind as u16)
541    }
542}
543
544/// A rowan `SyntaxNode` parameterized by [`InkLanguage`].
545pub type SyntaxNode = rowan::SyntaxNode<InkLanguage>;
546/// A rowan `SyntaxToken` parameterized by [`InkLanguage`].
547pub type SyntaxToken = rowan::SyntaxToken<InkLanguage>;
548/// A rowan `SyntaxElement` parameterized by [`InkLanguage`].
549pub type SyntaxElement = rowan::SyntaxElement<InkLanguage>;
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use rowan::Language;
555
556    #[test]
557    fn roundtrip_through_rowan() {
558        // Every SyntaxKind (except __LAST) should survive raw → kind → raw.
559        let mut i = 0u16;
560        loop {
561            if i == SyntaxKind::__LAST as u16 {
562                break;
563            }
564            let raw = rowan::SyntaxKind(i);
565            let kind = InkLanguage::kind_from_raw(raw);
566            let back = InkLanguage::kind_to_raw(kind);
567            assert_eq!(raw, back, "roundtrip failed for discriminant {i}");
568            i += 1;
569        }
570    }
571
572    #[test]
573    fn token_node_partition() {
574        // Every kind (except __LAST) is either a token or a node, never both.
575        let mut i = 0u16;
576        loop {
577            if i == SyntaxKind::__LAST as u16 {
578                break;
579            }
580            let kind = InkLanguage::kind_from_raw(rowan::SyntaxKind(i));
581            assert!(
582                kind.is_token() ^ kind.is_node(),
583                "{kind:?} is neither token nor node (or both)"
584            );
585            i += 1;
586        }
587    }
588
589    #[test]
590    fn trivia_is_subset_of_tokens() {
591        let mut i = 0u16;
592        loop {
593            if i == SyntaxKind::__LAST as u16 {
594                break;
595            }
596            let kind = InkLanguage::kind_from_raw(rowan::SyntaxKind(i));
597            if kind.is_trivia() {
598                assert!(kind.is_token(), "{kind:?} is trivia but not a token");
599            }
600            i += 1;
601        }
602    }
603
604    #[test]
605    fn newline_is_not_trivia() {
606        assert!(!SyntaxKind::NEWLINE.is_trivia());
607        assert!(SyntaxKind::NEWLINE.is_token());
608    }
609
610    #[test]
611    fn keywords_are_tokens() {
612        let keywords = [
613            SyntaxKind::KW_INCLUDE,
614            SyntaxKind::KW_EXTERNAL,
615            SyntaxKind::KW_VAR,
616            SyntaxKind::KW_CONST,
617            SyntaxKind::KW_LIST,
618            SyntaxKind::KW_TEMP,
619            SyntaxKind::KW_RETURN,
620            SyntaxKind::KW_REF,
621            SyntaxKind::KW_TRUE,
622            SyntaxKind::KW_FALSE,
623            SyntaxKind::KW_NOT,
624            SyntaxKind::KW_AND,
625            SyntaxKind::KW_OR,
626            SyntaxKind::KW_MOD,
627            SyntaxKind::KW_HAS,
628            SyntaxKind::KW_HASNT,
629            SyntaxKind::KW_ELSE,
630            SyntaxKind::KW_FUNCTION,
631            SyntaxKind::KW_STOPPING,
632            SyntaxKind::KW_CYCLE,
633            SyntaxKind::KW_SHUFFLE,
634            SyntaxKind::KW_ONCE,
635            SyntaxKind::KW_DONE,
636            SyntaxKind::KW_END,
637            SyntaxKind::KW_TODO,
638        ];
639        for kw in keywords {
640            assert!(kw.is_token(), "{kw:?} should be a token");
641            assert!(kw.is_keyword(), "{kw:?} should be a keyword");
642        }
643    }
644
645    #[test]
646    fn non_keywords_are_not_keywords() {
647        assert!(!SyntaxKind::IDENT.is_keyword());
648        assert!(!SyntaxKind::PLUS.is_keyword());
649        assert!(!SyntaxKind::SOURCE_FILE.is_keyword());
650    }
651}