flux_lang/syntax.rs
1//! `syntax` — the [`SyntaxKind`] alphabet for the lossless CST front-end and its
2//! [`rowan::Language`] binding.
3//!
4//! This is the shared vocabulary of the concrete syntax tree: every **token** the [`crate::lexer`]
5//! can emit plus every **node** the tolerant parser will build. Tokens and nodes live in one enum
6//! because rowan keys both by a single `u16` [`rowan::SyntaxKind`]. The CST is *lossless* — the
7//! token stream (including trivia and the zero-width layout markers below) reproduces the source
8//! byte-for-byte — which is what gives the language server precise spans and error recovery without
9//! changing the semantic [`crate::ast::Node`] AST.
10//!
11//! The token and node sets cover the complete writable grammar. New variants are appended before
12//! [`SyntaxKind::__LAST`] so the `u16` round-trip stays valid.
13
14/// Every token and node kind in the Flux-Lang CST.
15///
16/// `#[repr(u16)]` with contiguous discriminants (no gaps) so [`FluxLang`] can round-trip a kind
17/// through rowan's `u16` by a checked transmute. Keep [`SyntaxKind::__LAST`] the final variant.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[repr(u16)]
20#[allow(non_camel_case_types)]
21pub enum SyntaxKind {
22 // ---- trivia (carry text; ignored by the parser but kept in the tree) ----
23 /// A run of spaces/tabs within or leading a line.
24 WHITESPACE = 0,
25 /// A `# …` line comment (up to, but not including, the line break).
26 COMMENT,
27 /// A line break (`\n` or `\r\n`).
28 NEWLINE,
29
30 // ---- layout (zero-width synthetic markers; empty text, so still lossless) ----
31 /// Emitted before the first token of a line indented deeper than the enclosing block.
32 INDENT,
33 /// Emitted before the first token of a line dedented out of a block (one per level closed).
34 DEDENT,
35
36 // ---- names & literals ----
37 /// An identifier run `[A-Za-z_][A-Za-z0-9_]*` — also every (contextual) keyword; the parser
38 /// classifies keywords, the lexer does not.
39 IDENT,
40 /// A `$symbol` reference.
41 VAR,
42 /// An `@annotation` (e.g. `@effect`, `@json`).
43 ANNOTATION,
44 /// A `"…"` or `"""…"""` string literal (one token, delimiters included).
45 STRING,
46 /// A numeric literal `[0-9][0-9_]*(\.[0-9]+)?`.
47 NUMBER,
48
49 // ---- single-character punctuation ----
50 L_PAREN, // (
51 R_PAREN, // )
52 L_BRACK, // [
53 R_BRACK, // ]
54 L_BRACE, // {
55 R_BRACE, // }
56 COMMA, // ,
57 COLON, // :
58 DOT, // .
59 QUESTION, // ?
60 PIPE, // |
61 BANG, // !
62 EQ, // =
63 PLUS, // +
64 MINUS, // -
65 STAR, // *
66 SLASH, // /
67 LT, // <
68 GT, // >
69
70 // ---- multi-character operators ----
71 ARROW, // ->
72 PLUS_EQ, // +=
73 EQ_EQ, // ==
74 NEQ, // !=
75 LT_EQ, // <=
76 GT_EQ, // >=
77 AMP_AMP, // &&
78 PIPE_PIPE, // ||
79
80 // ---- sentinels ----
81 /// One or more bytes the lexer could not classify (kept, so the tree stays lossless).
82 ERROR,
83 /// End of input (never materialized into the tree; a parser convenience).
84 EOF,
85
86 // ---- nodes (built by the tolerant parser, L-58) ----
87 /// The document root: a whole `.flux` module.
88 ROOT,
89 /// A single top-level `flow` declaration (the common case) or the module wrapper.
90 FLOW_DECL,
91 /// The `flow [name]([params]) [-> type]` header line.
92 FLOW_HEADER,
93 /// A top-level composite operation declaration.
94 OP_DECL,
95 /// The `op name(params) [-> type]` header line.
96 OP_HEADER,
97 /// One leading composite-op metadata line (`description`, `risk`, `effects`, …).
98 OP_META,
99 /// The parenthesized parameter list of a flow header.
100 PARAM_LIST,
101 /// One `name: Type` parameter.
102 PARAM,
103 /// A pure-data top-level declaration the L6 host interprets (`agent`/`channel`/`datasource`/
104 /// `trigger`/`journey`).
105 DECL,
106 /// The first line of a pure-data declaration.
107 DECL_HEADER,
108 /// One `key value` line inside a pure-data declaration.
109 DECL_ATTR,
110 /// An indented block of statements (a flow/clause body).
111 BLOCK,
112
113 // statements
114 BIND_STMT, // $x = expr / $x: T = expr (also `memo`)
115 CALL_STMT, // do op args / op(args)
116 WHEN_STMT, // when cond … [else …]
117 ELSE_CLAUSE, // else …
118 UNLESS_STMT, // unless cond …
119 EACH_STMT, // each $x in src [-> [flat] $c] …
120 REPEAT_STMT, // repeat n [-> $c] …
121 UNTIL_CLAUSE, // until cond (first body line of repeat/loop)
122 MATCH_STMT, // match subj … (case/default arms)
123 CASE_ARM, // case v …
124 DEFAULT_ARM, // default …
125 ROUTE_STMT, // route sel … (case arms)
126 FALLBACK_STMT, // fallback [-> $b] … (branch arms)
127 BRANCH_ARM, // branch [$name] …
128 PARALLEL_STMT, // parallel … (branch arms)
129 LOOP_STMT, // loop for <ms> every <ms> [-> $b] …
130 TIMEOUT_STMT, // timeout <ms> [-> $b] …
131 BUDGET_STMT, // budget <n> [-> $b] …
132 WITH_TOOLS_STMT, // with_tools [names] …
133 RETRY_STMT, // retry <n> [backoff …] [-> $b] …
134 SEQ_STMT, // seq [-> $b] …
135 CTX_STMT, // ctx $p … (purpose/include/exclude/budget sub-lines)
136 CTX_APPEND_STMT, // $pack += $a, $b
137 RETURN_STMT, // return expr
138 ASSERT_STMT, // assert cond [: "msg"]
139 EFFECT_ANNOT, // @effect(tag) on the line above a bind
140 JSON_ESCAPE, // @json <compact-json> (statement position)
141
142 // native statements added in L-60..L-63
143 MEMO_STMT, // memo $x = expr
144 ONCE_STMT, // once "label" [-> $b] …
145 CHECKPOINT_STMT, // checkpoint "label"
146 AWAIT_STMT, // await [$b =] "source"
147 CONFIRM_STMT, // confirm "msg" [risk r] …
148 THROTTLE_STMT, // throttle "name" <max> per <window> …
149 DEBOUNCE_STMT, // debounce "name" <ms> …
150 VERIFY_STMT, // verify <cmd> contains <expect> [: "msg"]
151 TRY_STMT, // try … catch [$e] …
152 CATCH_CLAUSE, // catch [$e] …
153 RACE_STMT, // race <ms> [-> $b] … (branch arms)
154 SCOPE_STMT, // scope [$r = acquire] … finally …
155 FINALLY_CLAUSE, // finally …
156 SAGA_STMT, // saga … (step/undo arms)
157 STEP_ARM, // step …
158 UNDO_CLAUSE, // undo …
159 PIPE_STMT, // pipe [-> $b] … (call steps)
160
161 // expressions
162 CALL_EXPR, // op(args…)
163 VAR_EXPR, // $sym
164 FIELD_EXPR, // $sym.path (field-access sugar → jq)
165 LIT_EXPR, // number / string / true / false / null literal
166 FMT_EXPR, // fmt("…")
167 PARSE_EXPR, // parse(v, as: "…") (L-61)
168 PEEK_EXPR, // peek $sym (L-61)
169 THING_EXPR, // thing <kind> "…" (L-63)
170 JSON_EXPR, // @json <compact-json> (inline)
171 OBJ_EXPR, // { k: v, … }
172 OBJ_FIELD, // k: v
173 LIST_EXPR, // [ a, b, … ]
174 BIN_EXPR, // a <op> b
175 UNARY_EXPR, // !a / -a
176 PAREN_EXPR, // ( expr )
177 ARG_LIST, // (a, b, …) or bare `a, b`
178 NAME, // an identifier in name position (op names, keys, types)
179
180 /// One `purpose`/`include`/`exclude`/`budget` sub-line inside a `ctx` block. Its ownership is
181 /// structural; semantic lowering decodes the scalar header fields from its leaf tokens.
182 CTX_ENTRY,
183
184 /// Sentinel marking the end of the enum — keep last. Not a real kind.
185 #[doc(hidden)]
186 __LAST,
187}
188
189impl SyntaxKind {
190 /// Trivia is text the parser attaches to the tree but never branches on.
191 pub fn is_trivia(self) -> bool {
192 matches!(
193 self,
194 SyntaxKind::WHITESPACE | SyntaxKind::COMMENT | SyntaxKind::NEWLINE
195 )
196 }
197
198 /// The zero-width layout markers that carry the indentation grammar.
199 pub fn is_layout(self) -> bool {
200 matches!(self, SyntaxKind::INDENT | SyntaxKind::DEDENT)
201 }
202}
203
204/// The rowan [`rowan::Language`] for Flux-Lang: the bridge between [`SyntaxKind`] and rowan's raw
205/// `u16` kind.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
207pub enum FluxLang {}
208
209impl rowan::Language for FluxLang {
210 type Kind = SyntaxKind;
211
212 fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
213 assert!(
214 raw.0 < SyntaxKind::__LAST as u16,
215 "raw SyntaxKind {} out of range",
216 raw.0
217 );
218 // SAFETY: `SyntaxKind` is `#[repr(u16)]` with contiguous discriminants `0..__LAST`, and the
219 // assert above bounds `raw.0` into that range.
220 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
221 }
222
223 fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
224 rowan::SyntaxKind(kind as u16)
225 }
226}
227
228/// A resolved node in the red tree.
229pub type SyntaxNode = rowan::SyntaxNode<FluxLang>;
230/// A resolved token in the red tree.
231pub type SyntaxToken = rowan::SyntaxToken<FluxLang>;
232/// Either a node or a token.
233pub type SyntaxElement = rowan::SyntaxElement<FluxLang>;
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use rowan::Language;
239
240 #[test]
241 fn every_kind_round_trips_through_rowan_u16() {
242 // Walk every real kind (0..__LAST) and confirm the raw round-trip is the identity.
243 for raw in 0..(SyntaxKind::__LAST as u16) {
244 let kind = FluxLang::kind_from_raw(rowan::SyntaxKind(raw));
245 assert_eq!(FluxLang::kind_to_raw(kind).0, raw);
246 }
247 }
248
249 #[test]
250 fn trivia_and_layout_classification() {
251 assert!(SyntaxKind::WHITESPACE.is_trivia());
252 assert!(SyntaxKind::COMMENT.is_trivia());
253 assert!(SyntaxKind::NEWLINE.is_trivia());
254 assert!(!SyntaxKind::IDENT.is_trivia());
255 assert!(SyntaxKind::INDENT.is_layout());
256 assert!(SyntaxKind::DEDENT.is_layout());
257 assert!(!SyntaxKind::NEWLINE.is_layout());
258 }
259}