Skip to main content

brink_syntax_native/
syntax_kind.rs

1/// All syntactic constructs in the native `.brink` grammar.
2///
3/// This is a **peer** enum to `brink-syntax`'s ink-shaped `SyntaxKind` — its
4/// own discriminant space, sharing no numbering with the ink frontend (NF-1
5/// ruling, 2026-07-19: a new crate, not a co-located module, because the
6/// only reason to share `SyntaxKind` space was `AstPtr` interop, which the
7/// HIR admission contract's opaque `Provenance` already removed). Tokens
8/// (lexer output) and nodes (parser output) share one flat enum so `rowan`
9/// can store them in a single `u16` discriminant — see [`is_token`] /
10/// [`is_node`].
11///
12/// Scope: B0.5 (`docs/b0-sequencing.md` §B0.5) — the token set and
13/// error-resilient CST for the *ruled* native surface subset (NF-2:
14/// writer-sufficient, not the full charter). No HIR lowering happens in
15/// this crate; that is B0.6/B0.7/B0.8.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[repr(u16)]
18#[expect(non_camel_case_types)]
19pub enum SyntaxKind {
20    // ── Trivia tokens ─────────────────────────────────────────────
21    /// Spaces and tabs (NOT newlines). A leading UTF-8 BOM is folded in here
22    /// too (lossless-roundtrip requirement — see lexer tests).
23    WHITESPACE = 0,
24    /// `\n` or `\r\n`.
25    NEWLINE,
26    /// `// ...` through end-of-line. Also `////+` (four or more slashes) —
27    /// Rust precedent: only *exactly* three slashes is a doc comment (see
28    /// [`Self::DOC_COMMENT_OUTER`]).
29    LINE_COMMENT,
30    /// `/* ... */` (may span lines; unterminated block comments run to EOF —
31    /// recorded as a parse error, never a panic).
32    BLOCK_COMMENT,
33    /// `/// ...` through end-of-line — exactly three slashes (a fourth
34    /// keeps it a plain [`Self::LINE_COMMENT`]). B0.6b
35    /// (`docs/decision-log.md` 2026-07-20): first-class on the native
36    /// surface — **not** trivia (see [`Self::is_trivia`]), since the parser
37    /// dispatches on this token to decide whether a contiguous run attaches
38    /// as a `DOC_COMMENT` CST node to the declaration it immediately
39    /// precedes (`parser::doc_comment`).
40    DOC_COMMENT_OUTER,
41    /// `//! ...` through end-of-line — the inner form (B0.6b, Rust `//!`
42    /// precedent, ink had no equivalent). A contiguous run at the very
43    /// start of a knot/flow/file body documents the *enclosing* container
44    /// rather than a following declaration. Also not trivia.
45    DOC_COMMENT_INNER,
46
47    // ── Keyword tokens — hard-reserved (Finding #1: if/match/else/as are
48    // reserved everywhere, Rust-style, not contextual like ink's T1b
49    // keywords — the charter doesn't rule this either way; RustScript's own
50    // north star reserves them globally, so this is the coherent default) ──
51    /// `flow` — story-time container (Coloring axis, charter §3).
52    KW_FLOW,
53    /// `fn` — expression-time container (Coloring axis, charter §3).
54    KW_FN,
55    KW_VAR,
56    KW_CONST,
57    /// `flags` — renamed LIST (charter §11).
58    KW_FLAGS,
59    KW_STRUCT,
60    KW_EXTERN,
61    KW_IMPORT,
62    KW_USE,
63    KW_MODULE,
64    /// `return` — leave this container; also the tunnel-return respelling's
65    /// first half (`return -> x`, charter §11).
66    KW_RETURN,
67    /// `ref` — ref-argument marker (kept from ink).
68    KW_REF,
69    /// `if` — word-annotated brace family member (charter §6) AND (Finding
70    /// #1) reserved as a code-ground keyword everywhere.
71    KW_IF,
72    /// `match` — word-annotated brace family member (charter §6).
73    KW_MATCH,
74    /// `else` — conditional else-arm AND a choice point's fallback branch
75    /// (charter §11: "a choice point's fallback is its else-branch").
76    KW_ELSE,
77    /// `as` — import/use aliasing (`use a::b as c`).
78    KW_AS,
79    KW_TRUE,
80    KW_FALSE,
81    /// `END` — divert target sentinel (kept verbatim, charter §11).
82    KW_END,
83    /// `DONE` — divert target sentinel (kept verbatim, charter §11).
84    KW_DONE,
85
86    // ── Punctuation / operator tokens ────────────────────────────
87    /// `=`
88    EQ,
89    /// `+=`
90    PLUS_EQ,
91    /// `-=`
92    MINUS_EQ,
93    /// `*=`
94    STAR_EQ,
95    /// `/=`
96    SLASH_EQ,
97    /// `==`
98    EQ_EQ,
99    /// `!=`
100    BANG_EQ,
101    /// `<`
102    LT,
103    /// `>`
104    GT,
105    /// `<=`
106    LT_EQ,
107    /// `>=`
108    GT_EQ,
109    /// `&`
110    AMP,
111    /// `&&`
112    AMP_AMP,
113    /// `+`
114    PLUS,
115    /// `-`. Also the entry-marker sigil (charter §6) and the choice-list
116    /// once-bullet-adjacent dash; the parser, not the lexer, decides which
117    /// role a given `-` plays from structural position.
118    MINUS,
119    /// `*`
120    STAR,
121    /// `/`
122    SLASH,
123    /// `%`
124    PERCENT,
125    /// `^`
126    CARET,
127    /// `!`
128    BANG,
129    /// `?`. Also half of the `{?` choice-point opener — the parser
130    /// recognizes the *adjacent* `L_BRACE QUESTION` pair, no compound token
131    /// (mirrors how `{if`/`{match`/`{~` are recognized: `{` is always plain
132    /// `L_BRACE`, disambiguation is a parser lookahead, not a lexer job).
133    QUESTION,
134    /// `(`
135    L_PAREN,
136    /// `)`
137    R_PAREN,
138    /// `{`
139    L_BRACE,
140    /// `}`
141    R_BRACE,
142    /// `[`
143    L_BRACKET,
144    /// `]`
145    R_BRACKET,
146    /// `|`. Two adjacent `PIPE`s are NOT compounded into a logical-or token
147    /// (mirrors brink-syntax precedent for `||`/`++`/`--`) — the parser
148    /// disambiguates `a || b` (logical or) from `|x|` (lambda params) by
149    /// expression-vs-lambda-head position, not lexical shape.
150    PIPE,
151    /// `,`
152    COMMA,
153    /// `.` — the intra-module separator (containers, fields, variants,
154    /// UFCS; charter §13.2).
155    DOT,
156    /// `:`
157    COLON,
158    /// `;` — recognized only as `use`'s optional statement terminator
159    /// (charter §13.2's literal example: `use story::market::{barter,
160    /// haggle};`). Not a general statement separator anywhere else in this
161    /// skeleton (Finding #6: no other declaration requires one).
162    SEMICOLON,
163    /// `::` — the module-wall separator (charter §13.2). Lexed as one
164    /// compound token so a bare `:` (used in inline `{if cond: …}` bodies)
165    /// never gets swallowed by a stray adjacent colon.
166    COLON_COLON,
167    /// `#` — tag opener (charter §11: tags kept).
168    HASH,
169    /// `~`. Also an alternation-family opener (`{~ }` shuffle, charter §6).
170    TILDE,
171    /// `\`
172    BACKSLASH,
173    /// `@`. A lone `@` outside the `@[` pair is not punctuation in this
174    /// grammar (mirrors the ink `AT_L_BRACKET` precedent) and is emitted as
175    /// `ERROR_TOKEN` so prose containing a bare `@` still round-trips
176    /// losslessly instead of being silently absorbed.
177    AT,
178
179    // ── Compound tokens ──────────────────────────────────────────
180    /// `@[` — annotation-line opener (charter §11 / NS-A2 lineage,
181    /// `docs/directive-annotations-spec.md` §5b). Only the *adjacent* pair
182    /// opens an annotation line.
183    AT_L_BRACKET,
184    /// `<>` — glue (kept, charter §11).
185    GLUE,
186    /// `->` — divert (kept verbatim, charter §11).
187    DIVERT,
188    /// `<-` — splice, valid only inside a choice point (charter §5).
189    THREAD,
190    /// `=>` — match-arm separator.
191    FAT_ARROW,
192
193    // ── Content tokens ───────────────────────────────────────────
194    /// Integer literal (digits only; no leading sign — unary `-` is a
195    /// separate `PREFIX_EXPR`).
196    INTEGER,
197    /// Float literal (`digits.digits`).
198    FLOAT,
199    /// `"` (opening or closing quote).
200    QUOTE,
201    /// Run of non-special characters inside a string literal.
202    STRING_TEXT,
203    /// Escape sequence inside a string (`\n`, `\t`, `\\`, `\"`).
204    STRING_ESCAPE,
205    /// Identifier: ASCII `[A-Za-z_][A-Za-z0-9_]*` (Finding #2: native
206    /// identifiers are ASCII-only in this skeleton — the charter's S4
207    /// casing partition (`snake_case`/`UpperCamel`) is an ASCII-shaped rule
208    /// and the ink Unicode identifier range table is ink-specific baggage
209    /// with no native ruling to inherit it from; widening later is
210    /// additive, not breaking).
211    IDENT,
212    /// Any byte/char the lexer could not classify — including a lone `@`,
213    /// unterminated block comments' unreachable tail (folded into
214    /// `BLOCK_COMMENT` itself, not this), and raw prose bytes at
215    /// declaration scope.
216    ERROR_TOKEN,
217    /// End of file (synthetic).
218    EOF,
219
220    // ── Node kinds — doc comments (B0.6b) ────────────────────────
221    /// A contiguous run of [`Self::DOC_COMMENT_OUTER`] tokens (the leading
222    /// child of the declaration node it documents) or
223    /// [`Self::DOC_COMMENT_INNER`] tokens (the leading child of the
224    /// enclosing knot/flow/file body it documents) — one node shape for
225    /// both variants; `ast::DocComment::is_inner` tells them apart by
226    /// inspecting which token kind the node's children carry
227    /// (`docs/native-surface-charter.md`'s doc-comment section).
228    DOC_COMMENT,
229
230    // ── Node kinds — top level & declarations ───────────────────
231    SOURCE_FILE,
232    /// `flow name(params) { … }` / nested `flow` = stitch (charter §4).
233    FLOW_DECL,
234    /// `fn name(params) { … }`.
235    FN_DECL,
236    /// Shared param-list shape for `FLOW_DECL`/`FN_DECL`.
237    PARAM_LIST,
238    /// One parameter: `ref`? `IDENT`.
239    PARAM,
240    /// `var name = expr`.
241    VAR_DECL,
242    /// `const name = expr`.
243    CONST_DECL,
244    /// `flags Name = (member), member, …` (charter §11).
245    FLAGS_DECL,
246    FLAGS_MEMBER_LIST,
247    /// One flag member; a parenthesized member is the default-on entry.
248    FLAGS_MEMBER,
249    /// `struct Name { field: type, … }` (charter §13.1's sibling; concrete
250    /// grammar shape here, no field-type semantics checked).
251    STRUCT_DECL,
252    STRUCT_FIELD,
253    /// `extern name(params)`.
254    EXTERN_DECL,
255    /// `use path::{a, b as c};` (Rust `use` lifted verbatim, charter
256    /// §13.2).
257    USE_DECL,
258    /// One `use` tree: a path optionally followed by `{ … }` (nested
259    /// group), `as alias`, or bare.
260    USE_TREE,
261    USE_TREE_LIST,
262    /// `import name;` (Finding #3: the charter doesn't separately spell an
263    /// `import` grammar distinct from `use` — b0-sequencing's token-set
264    /// bullet lists `import` as its own decl keyword alongside `use`
265    /// regardless, so this skeleton gives it the minimal reasonable shape:
266    /// a single bare path statement, distinct node from `USE_DECL`. Real
267    /// semantics (whole-module import vs name-import) are B0.6's call).
268    IMPORT_DECL,
269    /// `module name { … }` — a nested module block (charter §13.2: files
270    /// hold declared `module` blocks nesting within them).
271    MODULE_DECL,
272
273    // ── Node kinds — bodies & content ───────────────────────────
274    /// A brace-delimited body: `{ BodyItem* }`. Universal body delimiter
275    /// (charter §4) for flow/fn/module bodies and nested block content.
276    BLOCK,
277    /// A single line of prose content, generic text interspersed with
278    /// interpolation/glue, terminated by `NEWLINE` or EOF.
279    CONTENT_LINE,
280    /// A run of literal text inside a `CONTENT_LINE` (no escapes, no
281    /// interpolation — those break the run).
282    TEXT,
283    /// `{expr}` — bare-brace interpolation, and nothing else, ever (charter
284    /// §6).
285    INTERPOLATION,
286    /// `<>` glue, in content position.
287    GLUE_NODE,
288    /// `# tag text` — a tag line (charter §11: tags kept).
289    TAG_LINE,
290    /// One `#`-prefixed tag inside a `TAG_LINE` or a `CONTENT_LINE`'s
291    /// trailing-tags tail.
292    TAG,
293
294    // ── Node kinds — choice points (charter §5) ─────────────────
295    /// `{? … }` — an explicit choice point.
296    CHOICE_POINT,
297    /// One `*`/`+` choice line inside a `CHOICE_POINT`.
298    CHOICE,
299    /// `*` (once) or `+` (sticky) bullet token wrapper.
300    CHOICE_BULLET,
301    /// `(name)` — a choice label (kept, charter §11).
302    LABEL,
303    /// `{if cond}` — a choice guard.
304    CHOICE_GUARD,
305    /// The `text[bracket]inner` display-split anatomy of a choice line
306    /// (kept as-is, charter §5).
307    CHOICE_START_CONTENT,
308    CHOICE_BRACKET_CONTENT,
309    CHOICE_INNER_CONTENT,
310    /// A choice's braced nested-content body (charter §5: "choice bodies
311    /// take braces when they have nested content").
312    CHOICE_BODY,
313    /// `else { … }` — a choice point's fallback branch (charter §11).
314    ELSE_BRANCH,
315    /// `<- flow(args)` — a splice inside a choice point (charter §5).
316    SPLICE,
317
318    // ── Node kinds — the annotated-brace family (charter §6) ────
319    /// `{if cond { … } else { … }}` / `{if cond: … else: …}` (Finding #4:
320    /// this skeleton accepts BOTH an inline colon-body form and a braced
321    /// multiline-arm form for `if`/`match` rather than the entry-marker-`-`
322    /// form charter §6 documents for the *alternation* family — the
323    /// charter itself flags entry-marker anatomy as "under-understood even
324    /// by the implementer," and nothing in the charter says `-` arms apply
325    /// to `if`/`match` specifically, so branches use the brace delimiter
326    /// charter §4 already declares universal, and dashes are reserved for
327    /// alternation blocks below. Flagged for the Track-B queue to confirm
328    /// or correct.)
329    CONDITIONAL_BLOCK,
330    IF_ARM,
331    MATCH_ARM,
332    /// A `match` arm's pattern (kept intentionally shallow — a bare
333    /// expression grammar reused, not a real pattern language; exhaustive
334    /// pattern matching is out of B0.5's scope).
335    MATCH_PATTERN,
336
337    /// `{~ … }` shuffle / `{& … }` cycle / `{! … }` once / `{| … }`
338    /// stopping-sequence — one node shape, `ALTERNATION_MARKER` child
339    /// records which.
340    ALTERNATION_BLOCK,
341    /// The `~`/`&`/`!`/`|` token that opened an `ALTERNATION_BLOCK`.
342    ALTERNATION_MARKER,
343    /// One `-`-prefixed entry/arm inside a multiline `ALTERNATION_BLOCK`
344    /// (charter §6). Runs until the next `-` or the closing `}`.
345    ENTRY,
346
347    /// `{? … }`'s sibling annotation-position dispatch already lives under
348    /// `CHOICE_POINT` above; this marker exists only so the family's
349    /// dispatch site has one name to log against in doc comments — not a
350    /// real node, never emitted. (Kept out of `is_node`/`is_token` via the
351    /// `__LAST` sentinel below being the true boundary; this variant is
352    /// unused and reserved as a documentation anchor only.)
353    // (intentionally no variant here — CHOICE_POINT already covers it)
354
355    // ── Node kinds — annotations (charter §11, `@[…]`) ──────────
356    /// `@[name(args)]` (directive-annotations-spec.md §5b's paren-clause
357    /// grammar, e.g. `@[effects(pure, silent, reads(gold, hp))]`).
358    ANNOTATION_LINE,
359    /// The parenthesized, comma-separated argument list of an annotation
360    /// or nested paren-clause.
361    ANNOTATION_ARGS,
362    /// One argument: a bare `IDENT`, or `IDENT(ANNOTATION_ARGS)` (the
363    /// nested paren-clause form, e.g. `reads(gold, hp)` nested inside
364    /// `effects(…)`).
365    ANNOTATION_ARG,
366
367    // ── Node kinds — diverts, tunnels, return (charter §11) ─────
368    /// `-> target` — kept verbatim.
369    DIVERT_STMT,
370    /// `-> place ->` — a tunnel call (kept, charter §11): divert, target,
371    /// divert, with nothing else before the line ends.
372    TUNNEL_CALL,
373    /// A divert target: `END` / `DONE` / a `PATH`.
374    DIVERT_TARGET,
375    /// `return` — leave this container.
376    RETURN_STMT,
377    /// `return -> x` — the tunnel-return respelling (charter §11):
378    /// `RETURN_STMT` immediately followed by a divert to `x`.
379    RETURN_REDIRECT,
380
381    // ── Node kinds — paths (charter §13.2) ───────────────────────
382    /// A dotted/`::`-separated name path. `::` crosses module walls, `.`
383    /// walks everything inside.
384    PATH,
385    PATH_SEGMENT,
386
387    // ── Node kinds — a minimal expression grammar ────────────────
388    // Shared by interpolation content, annotation args, choice guards,
389    // divert targets, and conditional/match heads. Real code-dialect
390    // statement grammar (let/assign/if-stmt/while/for/UFCS-calls/etc.) is
391    // explicitly B0.8 (`docs/b0-sequencing.md` §B0.8) — this is the
392    // expression *skeleton* B0.5 needs to give the constructs above a real
393    // (not just balanced-token) internal shape.
394    INTEGER_LIT,
395    FLOAT_LIT,
396    STRING_LIT,
397    BOOLEAN_LIT,
398    PATH_EXPR,
399    PAREN_EXPR,
400    PREFIX_EXPR,
401    INFIX_EXPR,
402    CALL_EXPR,
403    ARG_LIST,
404    /// `|x, y| expr` — lambda pipes, tokenized and structurally parsed;
405    /// lowering is explicitly deferred (charter §7/§8: "B0.5 tokenizes
406    /// pipes; B0.8 does not lower them").
407    LAMBDA_EXPR,
408    LAMBDA_PARAMS,
409
410    /// A parse-error wrapper node — swallows one unexpected token so error
411    /// recovery always makes forward progress.
412    ERROR,
413
414    // Not a real kind — used only for `rowan::Language::kind_to_raw` bounds.
415    #[doc(hidden)]
416    __LAST,
417}
418
419impl SyntaxKind {
420    /// Returns `true` for tokens produced by the lexer (leaf nodes in the CST).
421    #[must_use]
422    pub fn is_token(self) -> bool {
423        matches!(
424            self,
425            Self::WHITESPACE
426                | Self::NEWLINE
427                | Self::LINE_COMMENT
428                | Self::BLOCK_COMMENT
429                | Self::DOC_COMMENT_OUTER
430                | Self::DOC_COMMENT_INNER
431                | Self::KW_FLOW
432                | Self::KW_FN
433                | Self::KW_VAR
434                | Self::KW_CONST
435                | Self::KW_FLAGS
436                | Self::KW_STRUCT
437                | Self::KW_EXTERN
438                | Self::KW_IMPORT
439                | Self::KW_USE
440                | Self::KW_MODULE
441                | Self::KW_RETURN
442                | Self::KW_REF
443                | Self::KW_IF
444                | Self::KW_MATCH
445                | Self::KW_ELSE
446                | Self::KW_AS
447                | Self::KW_TRUE
448                | Self::KW_FALSE
449                | Self::KW_END
450                | Self::KW_DONE
451                | Self::EQ
452                | Self::PLUS_EQ
453                | Self::MINUS_EQ
454                | Self::STAR_EQ
455                | Self::SLASH_EQ
456                | Self::EQ_EQ
457                | Self::BANG_EQ
458                | Self::LT
459                | Self::GT
460                | Self::LT_EQ
461                | Self::GT_EQ
462                | Self::AMP
463                | Self::AMP_AMP
464                | Self::PLUS
465                | Self::MINUS
466                | Self::STAR
467                | Self::SLASH
468                | Self::PERCENT
469                | Self::CARET
470                | Self::BANG
471                | Self::QUESTION
472                | Self::L_PAREN
473                | Self::R_PAREN
474                | Self::L_BRACE
475                | Self::R_BRACE
476                | Self::L_BRACKET
477                | Self::R_BRACKET
478                | Self::PIPE
479                | Self::COMMA
480                | Self::DOT
481                | Self::COLON
482                | Self::SEMICOLON
483                | Self::COLON_COLON
484                | Self::HASH
485                | Self::TILDE
486                | Self::BACKSLASH
487                | Self::AT
488                | Self::AT_L_BRACKET
489                | Self::GLUE
490                | Self::DIVERT
491                | Self::THREAD
492                | Self::FAT_ARROW
493                | Self::INTEGER
494                | Self::FLOAT
495                | Self::QUOTE
496                | Self::STRING_TEXT
497                | Self::STRING_ESCAPE
498                | Self::IDENT
499                | Self::ERROR_TOKEN
500                | Self::EOF
501        )
502    }
503
504    /// Returns `true` for composite nodes built by the parser.
505    #[must_use]
506    pub fn is_node(self) -> bool {
507        !self.is_token() && self != Self::__LAST
508    }
509
510    /// Returns `true` for trivia — tokens the parser may skip over.
511    /// `NEWLINE` is **not** trivia; it terminates lines and delimits blocks.
512    #[must_use]
513    pub fn is_trivia(self) -> bool {
514        matches!(
515            self,
516            Self::WHITESPACE | Self::LINE_COMMENT | Self::BLOCK_COMMENT
517        )
518    }
519
520    /// Returns `true` for keyword tokens.
521    #[must_use]
522    pub fn is_keyword(self) -> bool {
523        matches!(
524            self,
525            Self::KW_FLOW
526                | Self::KW_FN
527                | Self::KW_VAR
528                | Self::KW_CONST
529                | Self::KW_FLAGS
530                | Self::KW_STRUCT
531                | Self::KW_EXTERN
532                | Self::KW_IMPORT
533                | Self::KW_USE
534                | Self::KW_MODULE
535                | Self::KW_RETURN
536                | Self::KW_REF
537                | Self::KW_IF
538                | Self::KW_MATCH
539                | Self::KW_ELSE
540                | Self::KW_AS
541                | Self::KW_TRUE
542                | Self::KW_FALSE
543                | Self::KW_END
544                | Self::KW_DONE
545        )
546    }
547}
548
549/// Rowan language tag for the native `.brink` grammar.
550#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
551pub enum NativeLanguage {}
552
553impl rowan::Language for NativeLanguage {
554    type Kind = SyntaxKind;
555
556    fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
557        assert!(raw.0 < SyntaxKind::__LAST as u16);
558        // SAFETY: `SyntaxKind` is `#[repr(u16)]` with contiguous discriminants,
559        // and we just checked bounds.
560        #[expect(unsafe_code, reason = "repr(u16) transmute with bounds check")]
561        unsafe {
562            std::mem::transmute::<u16, SyntaxKind>(raw.0)
563        }
564    }
565
566    fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
567        rowan::SyntaxKind(kind as u16)
568    }
569}
570
571/// A rowan `SyntaxNode` parameterized by [`NativeLanguage`].
572pub type SyntaxNode = rowan::SyntaxNode<NativeLanguage>;
573/// A rowan `SyntaxToken` parameterized by [`NativeLanguage`].
574pub type SyntaxToken = rowan::SyntaxToken<NativeLanguage>;
575/// A rowan `SyntaxElement` parameterized by [`NativeLanguage`].
576pub type SyntaxElement = rowan::SyntaxElement<NativeLanguage>;
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use rowan::Language;
582
583    #[test]
584    fn roundtrip_through_rowan() {
585        let mut i = 0u16;
586        loop {
587            if i == SyntaxKind::__LAST as u16 {
588                break;
589            }
590            let raw = rowan::SyntaxKind(i);
591            let kind = NativeLanguage::kind_from_raw(raw);
592            let back = NativeLanguage::kind_to_raw(kind);
593            assert_eq!(raw, back, "roundtrip failed for discriminant {i}");
594            i += 1;
595        }
596    }
597
598    #[test]
599    fn token_node_partition() {
600        let mut i = 0u16;
601        loop {
602            if i == SyntaxKind::__LAST as u16 {
603                break;
604            }
605            let kind = NativeLanguage::kind_from_raw(rowan::SyntaxKind(i));
606            assert!(
607                kind.is_token() ^ kind.is_node(),
608                "{kind:?} is neither token nor node (or both)"
609            );
610            i += 1;
611        }
612    }
613
614    #[test]
615    fn trivia_is_subset_of_tokens() {
616        let mut i = 0u16;
617        loop {
618            if i == SyntaxKind::__LAST as u16 {
619                break;
620            }
621            let kind = NativeLanguage::kind_from_raw(rowan::SyntaxKind(i));
622            if kind.is_trivia() {
623                assert!(kind.is_token(), "{kind:?} is trivia but not a token");
624            }
625            i += 1;
626        }
627    }
628
629    #[test]
630    fn newline_is_not_trivia() {
631        assert!(!SyntaxKind::NEWLINE.is_trivia());
632        assert!(SyntaxKind::NEWLINE.is_token());
633    }
634
635    #[test]
636    fn keywords_are_tokens() {
637        let keywords = [
638            SyntaxKind::KW_FLOW,
639            SyntaxKind::KW_FN,
640            SyntaxKind::KW_VAR,
641            SyntaxKind::KW_CONST,
642            SyntaxKind::KW_FLAGS,
643            SyntaxKind::KW_STRUCT,
644            SyntaxKind::KW_EXTERN,
645            SyntaxKind::KW_IMPORT,
646            SyntaxKind::KW_USE,
647            SyntaxKind::KW_MODULE,
648            SyntaxKind::KW_RETURN,
649            SyntaxKind::KW_REF,
650            SyntaxKind::KW_IF,
651            SyntaxKind::KW_MATCH,
652            SyntaxKind::KW_ELSE,
653            SyntaxKind::KW_AS,
654            SyntaxKind::KW_TRUE,
655            SyntaxKind::KW_FALSE,
656            SyntaxKind::KW_END,
657            SyntaxKind::KW_DONE,
658        ];
659        for kw in keywords {
660            assert!(kw.is_token(), "{kw:?} should be a token");
661            assert!(kw.is_keyword(), "{kw:?} should be a keyword");
662        }
663    }
664
665    #[test]
666    fn non_keywords_are_not_keywords() {
667        assert!(!SyntaxKind::IDENT.is_keyword());
668        assert!(!SyntaxKind::PLUS.is_keyword());
669        assert!(!SyntaxKind::SOURCE_FILE.is_keyword());
670    }
671}