Skip to main content

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}