plotnik_lib/parser/
ast.rs

1//! Typed AST wrappers over CST nodes.
2//!
3//! Each struct wraps a `SyntaxNode` and provides typed accessors.
4//! Cast is infallible for correct `SyntaxKind` - validation happens elsewhere.
5//!
6//! ## String Lifetime Limitation
7//!
8//! `SyntaxToken::text()` returns `&str` tied to the token's lifetime, not to the
9//! source `&'q str`. This is a rowan design: tokens store interned strings, not
10//! spans into the original source.
11//!
12//! When building data structures that need source-lifetime strings (e.g.,
13//! `SymbolTable<'q>`), use [`token_src`] instead of `token.text()`.
14
15use super::cst::{SyntaxKind, SyntaxNode, SyntaxToken};
16use rowan::TextRange;
17
18/// Extracts token text with source lifetime.
19///
20/// Use this instead of `token.text()` when you need `&'q str`.
21pub fn token_src<'q>(token: &SyntaxToken, source: &'q str) -> &'q str {
22    let range = token.text_range();
23    &source[range.start().into()..range.end().into()]
24}
25
26macro_rules! ast_node {
27    ($name:ident, $kind:ident) => {
28        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
29        pub struct $name(SyntaxNode);
30
31        impl $name {
32            pub fn cast(node: SyntaxNode) -> Option<Self> {
33                Self::can_cast(node.kind()).then(|| Self(node))
34            }
35
36            pub fn can_cast(kind: SyntaxKind) -> bool {
37                kind == SyntaxKind::$kind
38            }
39
40            pub fn as_cst(&self) -> &SyntaxNode {
41                &self.0
42            }
43
44            pub fn text_range(&self) -> TextRange {
45                self.0.text_range()
46            }
47        }
48    };
49}
50
51macro_rules! define_expr {
52    ($($variant:ident),+ $(,)?) => {
53        /// Expression: any pattern that can appear in the tree.
54        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
55        pub enum Expr {
56            $($variant($variant)),+
57        }
58
59        impl Expr {
60            pub fn cast(node: SyntaxNode) -> Option<Self> {
61                let kind = node.kind();
62                $(if $variant::can_cast(kind) { return Some(Expr::$variant($variant(node))); })+
63                None
64            }
65
66            pub fn as_cst(&self) -> &SyntaxNode {
67                match self { $(Expr::$variant(n) => n.as_cst()),+ }
68            }
69
70            pub fn text_range(&self) -> TextRange {
71                match self { $(Expr::$variant(n) => n.text_range()),+ }
72            }
73        }
74    };
75}
76
77impl Expr {
78    /// Returns direct child expressions.
79    pub fn children(&self) -> Vec<Expr> {
80        match self {
81            Expr::NamedNode(n) => n.children().collect(),
82            Expr::SeqExpr(s) => s.children().collect(),
83            Expr::CapturedExpr(c) => c.inner().into_iter().collect(),
84            Expr::QuantifiedExpr(q) => q.inner().into_iter().collect(),
85            Expr::FieldExpr(f) => f.value().into_iter().collect(),
86            Expr::AltExpr(a) => a.branches().filter_map(|b| b.body()).collect(),
87            Expr::Ref(_) | Expr::AnonymousNode(_) => vec![],
88        }
89    }
90}
91
92ast_node!(Root, Root);
93ast_node!(Def, Def);
94ast_node!(NamedNode, Tree);
95ast_node!(Ref, Ref);
96ast_node!(AltExpr, Alt);
97ast_node!(Branch, Branch);
98ast_node!(SeqExpr, Seq);
99ast_node!(CapturedExpr, Capture);
100ast_node!(Type, Type);
101ast_node!(QuantifiedExpr, Quantifier);
102ast_node!(FieldExpr, Field);
103ast_node!(NegatedField, NegatedField);
104ast_node!(Anchor, Anchor);
105
106/// Either an expression or an anchor in a sequence.
107#[derive(Debug, Clone, PartialEq, Eq, Hash)]
108pub enum SeqItem {
109    Expr(Expr),
110    Anchor(Anchor),
111}
112
113impl SeqItem {
114    pub fn cast(node: SyntaxNode) -> Option<Self> {
115        if let Some(expr) = Expr::cast(node.clone()) {
116            return Some(SeqItem::Expr(expr));
117        }
118        if let Some(anchor) = Anchor::cast(node) {
119            return Some(SeqItem::Anchor(anchor));
120        }
121        None
122    }
123
124    pub fn as_anchor(&self) -> Option<&Anchor> {
125        match self {
126            SeqItem::Anchor(a) => Some(a),
127            _ => None,
128        }
129    }
130
131    pub fn as_expr(&self) -> Option<&Expr> {
132        match self {
133            SeqItem::Expr(e) => Some(e),
134            _ => None,
135        }
136    }
137}
138
139/// Anonymous node: string literal (`"+"`) or wildcard (`_`).
140/// Maps from CST `Str` or `Wildcard`.
141#[derive(Debug, Clone, PartialEq, Eq, Hash)]
142pub struct AnonymousNode(SyntaxNode);
143
144impl AnonymousNode {
145    pub fn cast(node: SyntaxNode) -> Option<Self> {
146        Self::can_cast(node.kind()).then(|| Self(node))
147    }
148
149    pub fn can_cast(kind: SyntaxKind) -> bool {
150        matches!(kind, SyntaxKind::Str | SyntaxKind::Wildcard)
151    }
152
153    pub fn as_cst(&self) -> &SyntaxNode {
154        &self.0
155    }
156
157    pub fn text_range(&self) -> TextRange {
158        self.0.text_range()
159    }
160
161    /// Returns the string value if this is a literal, `None` if wildcard.
162    pub fn value(&self) -> Option<SyntaxToken> {
163        if self.0.kind() == SyntaxKind::Wildcard {
164            return None;
165        }
166        self.0
167            .children_with_tokens()
168            .filter_map(|it| it.into_token())
169            .find(|t| t.kind() == SyntaxKind::StrVal)
170    }
171
172    /// Returns true if this is the "any" wildcard (`_`).
173    pub fn is_any(&self) -> bool {
174        self.0.kind() == SyntaxKind::Wildcard
175    }
176}
177
178/// Whether an alternation uses tagged or untagged branches.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180pub enum AltKind {
181    /// All branches have labels: `[A: expr1 B: expr2]`
182    Tagged,
183    /// No branches have labels: `[expr1 expr2]`
184    Untagged,
185    /// Mixed tagged and untagged branches (invalid)
186    Mixed,
187}
188
189define_expr!(
190    NamedNode,
191    Ref,
192    AnonymousNode,
193    AltExpr,
194    SeqExpr,
195    CapturedExpr,
196    QuantifiedExpr,
197    FieldExpr,
198);
199
200impl Root {
201    pub fn defs(&self) -> impl Iterator<Item = Def> + '_ {
202        self.0.children().filter_map(Def::cast)
203    }
204
205    pub fn exprs(&self) -> impl Iterator<Item = Expr> + '_ {
206        self.0.children().filter_map(Expr::cast)
207    }
208}
209
210impl Def {
211    pub fn name(&self) -> Option<SyntaxToken> {
212        self.0
213            .children_with_tokens()
214            .filter_map(|it| it.into_token())
215            .find(|t| t.kind() == SyntaxKind::Id)
216    }
217
218    pub fn body(&self) -> Option<Expr> {
219        self.0.children().find_map(Expr::cast)
220    }
221}
222
223impl NamedNode {
224    pub fn node_type(&self) -> Option<SyntaxToken> {
225        self.0
226            .children_with_tokens()
227            .filter_map(|it| it.into_token())
228            .find(|t| {
229                matches!(
230                    t.kind(),
231                    SyntaxKind::Id
232                        | SyntaxKind::Underscore
233                        | SyntaxKind::KwError
234                        | SyntaxKind::KwMissing
235                )
236            })
237    }
238
239    /// Returns true if the node type is wildcard (`_`), matching any named node.
240    pub fn is_any(&self) -> bool {
241        self.node_type()
242            .map(|t| t.kind() == SyntaxKind::Underscore)
243            .unwrap_or(false)
244    }
245
246    /// Returns true if this is a MISSING node: `(MISSING ...)`.
247    pub fn is_missing(&self) -> bool {
248        self.node_type()
249            .map(|t| t.kind() == SyntaxKind::KwMissing)
250            .unwrap_or(false)
251    }
252
253    /// For MISSING nodes, returns the inner type constraint if present.
254    ///
255    /// `(MISSING identifier)` → Some("identifier")
256    /// `(MISSING ";")` → Some(";")
257    /// `(MISSING)` → None
258    pub fn missing_constraint(&self) -> Option<SyntaxToken> {
259        if !self.is_missing() {
260            return None;
261        }
262        // After KwMissing, look for Id or StrVal token
263        let mut found_missing = false;
264        for child in self.0.children_with_tokens() {
265            if let Some(token) = child.into_token() {
266                if token.kind() == SyntaxKind::KwMissing {
267                    found_missing = true;
268                } else if found_missing
269                    && matches!(token.kind(), SyntaxKind::Id | SyntaxKind::StrVal)
270                {
271                    return Some(token);
272                }
273            }
274        }
275        None
276    }
277
278    pub fn children(&self) -> impl Iterator<Item = Expr> + '_ {
279        self.0.children().filter_map(Expr::cast)
280    }
281
282    /// Returns all anchors in this node.
283    pub fn anchors(&self) -> impl Iterator<Item = Anchor> + '_ {
284        self.0.children().filter_map(Anchor::cast)
285    }
286
287    /// Returns children interleaved with anchors, preserving order.
288    pub fn items(&self) -> impl Iterator<Item = SeqItem> + '_ {
289        self.0.children().filter_map(SeqItem::cast)
290    }
291}
292
293impl Ref {
294    pub fn name(&self) -> Option<SyntaxToken> {
295        self.0
296            .children_with_tokens()
297            .filter_map(|it| it.into_token())
298            .find(|t| t.kind() == SyntaxKind::Id)
299    }
300}
301
302impl AltExpr {
303    pub fn kind(&self) -> AltKind {
304        let mut tagged = false;
305        let mut untagged = false;
306
307        for child in self.0.children().filter(|c| c.kind() == SyntaxKind::Branch) {
308            let has_label = child
309                .children_with_tokens()
310                .filter_map(|it| it.into_token())
311                .any(|t| t.kind() == SyntaxKind::Id);
312
313            if has_label {
314                tagged = true;
315            } else {
316                untagged = true;
317            }
318        }
319
320        match (tagged, untagged) {
321            (true, true) => AltKind::Mixed,
322            (true, false) => AltKind::Tagged,
323            _ => AltKind::Untagged,
324        }
325    }
326
327    pub fn branches(&self) -> impl Iterator<Item = Branch> + '_ {
328        self.0.children().filter_map(Branch::cast)
329    }
330
331    pub fn exprs(&self) -> impl Iterator<Item = Expr> + '_ {
332        self.0.children().filter_map(Expr::cast)
333    }
334}
335
336impl Branch {
337    pub fn label(&self) -> Option<SyntaxToken> {
338        self.0
339            .children_with_tokens()
340            .filter_map(|it| it.into_token())
341            .find(|t| t.kind() == SyntaxKind::Id)
342    }
343
344    pub fn body(&self) -> Option<Expr> {
345        self.0.children().find_map(Expr::cast)
346    }
347}
348
349impl SeqExpr {
350    pub fn children(&self) -> impl Iterator<Item = Expr> + '_ {
351        self.0.children().filter_map(Expr::cast)
352    }
353
354    /// Returns all anchors in this sequence.
355    pub fn anchors(&self) -> impl Iterator<Item = Anchor> + '_ {
356        self.0.children().filter_map(Anchor::cast)
357    }
358
359    /// Returns children interleaved with anchors, preserving order.
360    pub fn items(&self) -> impl Iterator<Item = SeqItem> + '_ {
361        self.0.children().filter_map(SeqItem::cast)
362    }
363}
364
365impl CapturedExpr {
366    /// Returns the capture token (@name or @_name).
367    /// The token text includes the @ prefix.
368    pub fn name(&self) -> Option<SyntaxToken> {
369        self.0
370            .children_with_tokens()
371            .filter_map(|it| it.into_token())
372            .find(|t| {
373                matches!(
374                    t.kind(),
375                    SyntaxKind::CaptureToken | SyntaxKind::SuppressiveCapture
376                )
377            })
378    }
379
380    /// Returns true if this is a suppressive capture (@_ or @_name).
381    /// Suppressive captures match structurally but don't contribute to output.
382    pub fn is_suppressive(&self) -> bool {
383        self.0
384            .children_with_tokens()
385            .filter_map(|it| it.into_token())
386            .any(|t| t.kind() == SyntaxKind::SuppressiveCapture)
387    }
388
389    pub fn inner(&self) -> Option<Expr> {
390        self.0.children().find_map(Expr::cast)
391    }
392
393    pub fn type_annotation(&self) -> Option<Type> {
394        self.0.children().find_map(Type::cast)
395    }
396
397    /// Returns true if this capture has a `:: string` type annotation.
398    pub fn has_string_annotation(&self) -> bool {
399        self.type_annotation()
400            .is_some_and(|t| t.name().is_some_and(|n| n.text() == "string"))
401    }
402}
403
404impl Type {
405    pub fn name(&self) -> Option<SyntaxToken> {
406        self.0
407            .children_with_tokens()
408            .filter_map(|it| it.into_token())
409            .find(|t| t.kind() == SyntaxKind::Id)
410    }
411}
412
413impl QuantifiedExpr {
414    pub fn inner(&self) -> Option<Expr> {
415        self.0.children().find_map(Expr::cast)
416    }
417
418    pub fn operator(&self) -> Option<SyntaxToken> {
419        self.0
420            .children_with_tokens()
421            .filter_map(|it| it.into_token())
422            .find(|t| {
423                matches!(
424                    t.kind(),
425                    SyntaxKind::Star
426                        | SyntaxKind::Plus
427                        | SyntaxKind::Question
428                        | SyntaxKind::StarQuestion
429                        | SyntaxKind::PlusQuestion
430                        | SyntaxKind::QuestionQuestion
431                )
432            })
433    }
434
435    /// Returns true if quantifier allows zero matches (?, *, ??, *?).
436    pub fn is_optional(&self) -> bool {
437        self.operator()
438            .map(|op| {
439                matches!(
440                    op.kind(),
441                    SyntaxKind::Question
442                        | SyntaxKind::Star
443                        | SyntaxKind::QuestionQuestion
444                        | SyntaxKind::StarQuestion
445                )
446            })
447            .unwrap_or(false)
448    }
449}
450
451impl FieldExpr {
452    pub fn name(&self) -> Option<SyntaxToken> {
453        self.0
454            .children_with_tokens()
455            .filter_map(|it| it.into_token())
456            .find(|t| t.kind() == SyntaxKind::Id)
457    }
458
459    pub fn value(&self) -> Option<Expr> {
460        self.0.children().find_map(Expr::cast)
461    }
462}
463
464impl NegatedField {
465    pub fn name(&self) -> Option<SyntaxToken> {
466        self.0
467            .children_with_tokens()
468            .filter_map(|it| it.into_token())
469            .find(|t| t.kind() == SyntaxKind::Id)
470    }
471}
472
473/// Checks if expression is a truly empty scope (sequence/alternation with no children).
474/// Used to distinguish `{ } @x` (empty struct) from `{(expr) @_} @x` (Node capture).
475pub fn is_truly_empty_scope(inner: &Expr) -> bool {
476    match inner {
477        Expr::SeqExpr(seq) => seq.children().next().is_none(),
478        Expr::AltExpr(alt) => alt.branches().next().is_none(),
479        Expr::QuantifiedExpr(q) => q.inner().is_some_and(|i| is_truly_empty_scope(&i)),
480        _ => false,
481    }
482}