Skip to main content

brink_syntax/ast/
nodes.rs

1//! Typed AST node wrappers for every node kind in the ink CST.
2//!
3//! Each struct is a zero-cost newtype around [`SyntaxNode`] generated by
4//! [`ast_node!`]. Structs with hand-written accessors have `impl` blocks
5//! below their definition.
6
7use crate::SyntaxKind::{
8    self, AMP, AMP_AMP, BANG, BANG_EQ, BANG_QUESTION, CARET, COLON, DIVERT, DOLLAR, EQ, EQ_EQ,
9    FLOAT, GT, GT_EQ, HASH, IDENT, INTEGER, KW_AND, KW_CYCLE, KW_DONE, KW_ELSE, KW_END, KW_FALSE,
10    KW_FUNCTION, KW_HAS, KW_HASNT, KW_MOD, KW_NOT, KW_ONCE, KW_OR, KW_REF, KW_SHUFFLE, KW_STOPPING,
11    KW_TODO, KW_TRUE, LT, LT_EQ, MINUS, MINUS_EQ, NEWLINE, PERCENT, PIPE, PLUS, PLUS_EQ, QUESTION,
12    SLASH, STAR, TILDE,
13};
14use crate::ast::AstNode as _;
15use crate::ast::ast_node;
16use crate::ast::support;
17use crate::{SyntaxNode, SyntaxToken};
18
19// ── Top-level ────────────────────────────────────────────────────────
20
21ast_node!(SourceFile, SOURCE_FILE);
22ast_node!(IncludeStmt, INCLUDE_STMT);
23ast_node!(FilePath, FILE_PATH);
24ast_node!(ExternalDecl, EXTERNAL_DECL);
25
26// ── Knots & stitches ─────────────────────────────────────────────────
27
28ast_node!(KnotDef, KNOT_DEF);
29ast_node!(KnotHeader, KNOT_HEADER);
30ast_node!(KnotBody, KNOT_BODY);
31ast_node!(KnotParams, KNOT_PARAMS);
32ast_node!(KnotParamDecl, KNOT_PARAM_DECL);
33ast_node!(StitchDef, STITCH_DEF);
34ast_node!(StitchHeader, STITCH_HEADER);
35ast_node!(StitchBody, STITCH_BODY);
36
37// ── Lines ────────────────────────────────────────────────────────────
38
39ast_node!(EmptyLine, EMPTY_LINE);
40ast_node!(AuthorWarning, AUTHOR_WARNING);
41ast_node!(LogicLine, LOGIC_LINE);
42ast_node!(ContentLine, CONTENT_LINE);
43ast_node!(TagLine, TAG_LINE);
44ast_node!(StrayClosingBrace, STRAY_CLOSING_BRACE);
45
46// ── Logic ────────────────────────────────────────────────────────────
47
48ast_node!(ReturnStmt, RETURN_STMT);
49ast_node!(TempDecl, TEMP_DECL);
50ast_node!(Assignment, ASSIGNMENT);
51
52// ── Content ──────────────────────────────────────────────────────────
53
54ast_node!(MixedContent, MIXED_CONTENT);
55ast_node!(Text, TEXT);
56ast_node!(Escape, ESCAPE);
57ast_node!(GlueNode, GLUE_NODE);
58
59// ── Choices ──────────────────────────────────────────────────────────
60
61ast_node!(Choice, CHOICE);
62ast_node!(ChoiceBullets, CHOICE_BULLETS);
63ast_node!(Label, LABEL);
64ast_node!(ChoiceCondition, CHOICE_CONDITION);
65ast_node!(ChoiceStartContent, CHOICE_START_CONTENT);
66ast_node!(ChoiceBracketContent, CHOICE_BRACKET_CONTENT);
67ast_node!(ChoiceInnerContent, CHOICE_INNER_CONTENT);
68
69// ── Gathers ──────────────────────────────────────────────────────────
70
71ast_node!(Gather, GATHER);
72ast_node!(GatherDashes, GATHER_DASHES);
73
74// ── Tags ─────────────────────────────────────────────────────────────
75
76ast_node!(Tags, TAGS);
77ast_node!(Tag, TAG);
78
79// ── Inline logic ─────────────────────────────────────────────────────
80
81ast_node!(InlineLogic, INLINE_LOGIC);
82ast_node!(MultilineBlock, MULTILINE_BLOCK);
83ast_node!(SequenceWithAnnotation, SEQUENCE_WITH_ANNOTATION);
84ast_node!(SequenceSymbolAnnotation, SEQUENCE_SYMBOL_ANNOTATION);
85ast_node!(SequenceWordAnnotation, SEQUENCE_WORD_ANNOTATION);
86ast_node!(InlineBranchesSeq, INLINE_BRANCHES_SEQ);
87ast_node!(MultilineBranchesSeq, MULTILINE_BRANCHES_SEQ);
88ast_node!(MultilineBranchSeq, MULTILINE_BRANCH_SEQ);
89ast_node!(BranchContent, BRANCH_CONTENT);
90
91// ── Conditionals ─────────────────────────────────────────────────────
92
93ast_node!(ConditionalWithExpr, CONDITIONAL_WITH_EXPR);
94ast_node!(BranchlessCondBody, BRANCHLESS_COND_BODY);
95ast_node!(ElseBranch, ELSE_BRANCH);
96ast_node!(InlineBranchesCond, INLINE_BRANCHES_COND);
97ast_node!(MultilineBranchesCond, MULTILINE_BRANCHES_COND);
98ast_node!(MultilineConditional, MULTILINE_CONDITIONAL);
99ast_node!(MultilineBranchCond, MULTILINE_BRANCH_COND);
100ast_node!(MultilineBranchBody, MULTILINE_BRANCH_BODY);
101ast_node!(ImplicitSequence, IMPLICIT_SEQUENCE);
102
103// ── Expressions ──────────────────────────────────────────────────────
104
105ast_node!(InnerExpression, INNER_EXPRESSION);
106ast_node!(PrefixExpr, PREFIX_EXPR);
107ast_node!(PostfixExpr, POSTFIX_EXPR);
108ast_node!(InfixExpr, INFIX_EXPR);
109ast_node!(ParenExpr, PAREN_EXPR);
110ast_node!(FunctionCall, FUNCTION_CALL);
111ast_node!(ArgList, ARG_LIST);
112ast_node!(DivertTargetExpr, DIVERT_TARGET_EXPR);
113ast_node!(ListExpr, LIST_EXPR);
114
115// ── Diverts ──────────────────────────────────────────────────────────
116
117ast_node!(DivertNode, DIVERT_NODE);
118ast_node!(SimpleDivert, SIMPLE_DIVERT);
119ast_node!(DivertTargetWithArgs, DIVERT_TARGET_WITH_ARGS);
120ast_node!(ThreadStart, THREAD_START);
121ast_node!(TunnelOnwardsNode, TUNNEL_ONWARDS_NODE);
122ast_node!(TunnelCallNode, TUNNEL_CALL_NODE);
123
124// ── Identifiers ──────────────────────────────────────────────────────
125
126ast_node!(Identifier, IDENTIFIER);
127ast_node!(Path, PATH);
128
129// ── Declarations ─────────────────────────────────────────────────────
130
131ast_node!(VarDecl, VAR_DECL);
132ast_node!(ConstDecl, CONST_DECL);
133ast_node!(ListDecl, LIST_DECL);
134ast_node!(ListDef, LIST_DEF);
135ast_node!(ListMember, LIST_MEMBER);
136ast_node!(ListMemberOn, LIST_MEMBER_ON);
137ast_node!(ListMemberOff, LIST_MEMBER_OFF);
138ast_node!(FunctionParamList, FUNCTION_PARAM_LIST);
139
140// ── Literals ─────────────────────────────────────────────────────────
141
142ast_node!(IntegerLit, INTEGER_LIT);
143ast_node!(FloatLit, FLOAT_LIT);
144ast_node!(StringLit, STRING_LIT);
145ast_node!(BooleanLit, BOOLEAN_LIT);
146
147// ── Error recovery ───────────────────────────────────────────────────
148
149ast_node!(Error, ERROR);
150
151// ── Expression enum ──────────────────────────────────────────────────
152
153/// A typed expression node.
154///
155/// Covers every node kind the Pratt expression parser can produce.
156#[derive(Clone, PartialEq, Eq, Hash)]
157pub enum Expr {
158    Prefix(PrefixExpr),
159    Postfix(PostfixExpr),
160    Infix(InfixExpr),
161    Paren(ParenExpr),
162    FunctionCall(FunctionCall),
163    IntegerLit(IntegerLit),
164    FloatLit(FloatLit),
165    StringLit(StringLit),
166    BooleanLit(BooleanLit),
167    Path(Path),
168    ListExpr(ListExpr),
169    DivertTarget(DivertTargetExpr),
170}
171
172impl std::fmt::Debug for Expr {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        std::fmt::Debug::fmt(self.syntax(), f)
175    }
176}
177
178impl std::fmt::Display for Expr {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        std::fmt::Display::fmt(&self.syntax().text(), f)
181    }
182}
183
184impl crate::ast::AstNode for Expr {
185    fn can_cast(kind: SyntaxKind) -> bool {
186        matches!(
187            kind,
188            SyntaxKind::PREFIX_EXPR
189                | SyntaxKind::POSTFIX_EXPR
190                | SyntaxKind::INFIX_EXPR
191                | SyntaxKind::PAREN_EXPR
192                | SyntaxKind::FUNCTION_CALL
193                | SyntaxKind::INTEGER_LIT
194                | SyntaxKind::FLOAT_LIT
195                | SyntaxKind::STRING_LIT
196                | SyntaxKind::BOOLEAN_LIT
197                | SyntaxKind::PATH
198                | SyntaxKind::LIST_EXPR
199                | SyntaxKind::DIVERT_TARGET_EXPR
200        )
201    }
202
203    fn cast(node: SyntaxNode) -> Option<Self> {
204        match node.kind() {
205            SyntaxKind::PREFIX_EXPR => PrefixExpr::cast(node).map(Expr::Prefix),
206            SyntaxKind::POSTFIX_EXPR => PostfixExpr::cast(node).map(Expr::Postfix),
207            SyntaxKind::INFIX_EXPR => InfixExpr::cast(node).map(Expr::Infix),
208            SyntaxKind::PAREN_EXPR => ParenExpr::cast(node).map(Expr::Paren),
209            SyntaxKind::FUNCTION_CALL => FunctionCall::cast(node).map(Expr::FunctionCall),
210            SyntaxKind::INTEGER_LIT => IntegerLit::cast(node).map(Expr::IntegerLit),
211            SyntaxKind::FLOAT_LIT => FloatLit::cast(node).map(Expr::FloatLit),
212            SyntaxKind::STRING_LIT => StringLit::cast(node).map(Expr::StringLit),
213            SyntaxKind::BOOLEAN_LIT => BooleanLit::cast(node).map(Expr::BooleanLit),
214            SyntaxKind::PATH => Path::cast(node).map(Expr::Path),
215            SyntaxKind::LIST_EXPR => ListExpr::cast(node).map(Expr::ListExpr),
216            SyntaxKind::DIVERT_TARGET_EXPR => DivertTargetExpr::cast(node).map(Expr::DivertTarget),
217            _ => None,
218        }
219    }
220
221    fn syntax(&self) -> &SyntaxNode {
222        match self {
223            Expr::Prefix(n) => n.syntax(),
224            Expr::Postfix(n) => n.syntax(),
225            Expr::Infix(n) => n.syntax(),
226            Expr::Paren(n) => n.syntax(),
227            Expr::FunctionCall(n) => n.syntax(),
228            Expr::IntegerLit(n) => n.syntax(),
229            Expr::FloatLit(n) => n.syntax(),
230            Expr::StringLit(n) => n.syntax(),
231            Expr::BooleanLit(n) => n.syntax(),
232            Expr::Path(n) => n.syntax(),
233            Expr::ListExpr(n) => n.syntax(),
234            Expr::DivertTarget(n) => n.syntax(),
235        }
236    }
237}
238
239// ── Content node accessor macro ─────────────────────────────────────
240
241/// Generates shared content-element accessors for nodes that contain
242/// mixed inline content (`TEXT`, `INLINE_LOGIC`, `GLUE_NODE`, `ESCAPE`).
243macro_rules! content_node_accessors {
244    ($name:ident) => {
245        impl $name {
246            pub fn texts(&self) -> impl Iterator<Item = Text> {
247                support::children(&self.syntax)
248            }
249
250            pub fn inline_logics(&self) -> impl Iterator<Item = InlineLogic> {
251                support::children(&self.syntax)
252            }
253
254            pub fn glue_nodes(&self) -> impl Iterator<Item = GlueNode> {
255                support::children(&self.syntax)
256            }
257
258            pub fn escapes(&self) -> impl Iterator<Item = Escape> {
259                support::children(&self.syntax)
260            }
261        }
262    };
263}
264
265content_node_accessors!(ChoiceStartContent);
266content_node_accessors!(ChoiceBracketContent);
267content_node_accessors!(ChoiceInnerContent);
268content_node_accessors!(BranchContent);
269
270// ═══════════════════════════════════════════════════════════════════════
271// Accessors
272// ═══════════════════════════════════════════════════════════════════════
273
274// ── SourceFile ───────────────────────────────────────────────────────
275
276impl SourceFile {
277    pub fn knots(&self) -> impl Iterator<Item = KnotDef> {
278        support::children(&self.syntax)
279    }
280
281    pub fn includes(&self) -> impl Iterator<Item = IncludeStmt> {
282        support::children(&self.syntax)
283    }
284
285    pub fn externals(&self) -> impl Iterator<Item = ExternalDecl> {
286        support::children(&self.syntax)
287    }
288
289    pub fn stitches(&self) -> impl Iterator<Item = StitchDef> {
290        support::children(&self.syntax)
291    }
292
293    pub fn var_decls(&self) -> impl Iterator<Item = VarDecl> {
294        support::children(&self.syntax)
295    }
296
297    pub fn const_decls(&self) -> impl Iterator<Item = ConstDecl> {
298        support::children(&self.syntax)
299    }
300
301    pub fn list_decls(&self) -> impl Iterator<Item = ListDecl> {
302        support::children(&self.syntax)
303    }
304
305    pub fn content_lines(&self) -> impl Iterator<Item = ContentLine> {
306        support::children(&self.syntax)
307    }
308
309    pub fn logic_lines(&self) -> impl Iterator<Item = LogicLine> {
310        support::children(&self.syntax)
311    }
312
313    pub fn choices(&self) -> impl Iterator<Item = Choice> {
314        support::children(&self.syntax)
315    }
316
317    pub fn gathers(&self) -> impl Iterator<Item = Gather> {
318        support::children(&self.syntax)
319    }
320}
321
322// ── IncludeStmt ──────────────────────────────────────────────────────
323
324impl IncludeStmt {
325    pub fn file_path(&self) -> Option<FilePath> {
326        support::child(&self.syntax)
327    }
328}
329
330// ── FilePath ─────────────────────────────────────────────────────────
331
332impl FilePath {
333    /// Returns the raw text of the file path (concatenation of all child tokens).
334    pub fn text(&self) -> String {
335        self.syntax.text().to_string()
336    }
337}
338
339// ── ExternalDecl ─────────────────────────────────────────────────────
340
341impl ExternalDecl {
342    pub fn identifier(&self) -> Option<Identifier> {
343        support::child(&self.syntax)
344    }
345
346    pub fn name(&self) -> Option<String> {
347        self.identifier().and_then(|id| id.name())
348    }
349
350    pub fn param_list(&self) -> Option<FunctionParamList> {
351        support::child(&self.syntax)
352    }
353}
354
355// ── KnotDef ──────────────────────────────────────────────────────────
356
357impl KnotDef {
358    pub fn header(&self) -> Option<KnotHeader> {
359        support::child(&self.syntax)
360    }
361
362    pub fn body(&self) -> Option<KnotBody> {
363        support::child(&self.syntax)
364    }
365}
366
367// ── KnotHeader ───────────────────────────────────────────────────────
368
369impl KnotHeader {
370    pub fn function_kw(&self) -> Option<SyntaxToken> {
371        support::token(&self.syntax, KW_FUNCTION)
372    }
373
374    pub fn is_function(&self) -> bool {
375        self.function_kw().is_some()
376    }
377
378    pub fn identifier(&self) -> Option<Identifier> {
379        support::child(&self.syntax)
380    }
381
382    pub fn name(&self) -> Option<String> {
383        self.identifier().and_then(|id| id.name())
384    }
385
386    pub fn params(&self) -> Option<KnotParams> {
387        support::child(&self.syntax)
388    }
389}
390
391// ── KnotBody ─────────────────────────────────────────────────────────
392
393impl KnotBody {
394    pub fn stitches(&self) -> impl Iterator<Item = StitchDef> {
395        support::children(&self.syntax)
396    }
397
398    pub fn content_lines(&self) -> impl Iterator<Item = ContentLine> {
399        support::children(&self.syntax)
400    }
401
402    pub fn logic_lines(&self) -> impl Iterator<Item = LogicLine> {
403        support::children(&self.syntax)
404    }
405
406    pub fn choices(&self) -> impl Iterator<Item = Choice> {
407        support::children(&self.syntax)
408    }
409
410    pub fn gathers(&self) -> impl Iterator<Item = Gather> {
411        support::children(&self.syntax)
412    }
413}
414
415// ── KnotParams ───────────────────────────────────────────────────────
416
417impl KnotParams {
418    pub fn params(&self) -> impl Iterator<Item = KnotParamDecl> {
419        support::children(&self.syntax)
420    }
421}
422
423// ── KnotParamDecl ────────────────────────────────────────────────────
424
425impl KnotParamDecl {
426    pub fn divert_token(&self) -> Option<SyntaxToken> {
427        support::token(&self.syntax, DIVERT)
428    }
429
430    pub fn is_divert(&self) -> bool {
431        self.divert_token().is_some()
432    }
433
434    pub fn ref_kw(&self) -> Option<SyntaxToken> {
435        support::token(&self.syntax, KW_REF)
436    }
437
438    pub fn is_ref(&self) -> bool {
439        self.ref_kw().is_some()
440    }
441
442    pub fn identifier(&self) -> Option<Identifier> {
443        support::child(&self.syntax)
444    }
445
446    pub fn name(&self) -> Option<String> {
447        self.identifier().and_then(|id| id.name())
448    }
449}
450
451// ── StitchDef ────────────────────────────────────────────────────────
452
453impl StitchDef {
454    pub fn header(&self) -> Option<StitchHeader> {
455        support::child(&self.syntax)
456    }
457
458    pub fn body(&self) -> Option<StitchBody> {
459        support::child(&self.syntax)
460    }
461}
462
463// ── StitchHeader ─────────────────────────────────────────────────────
464
465impl StitchHeader {
466    pub fn identifier(&self) -> Option<Identifier> {
467        support::child(&self.syntax)
468    }
469
470    pub fn name(&self) -> Option<String> {
471        self.identifier().and_then(|id| id.name())
472    }
473
474    pub fn params(&self) -> Option<KnotParams> {
475        support::child(&self.syntax)
476    }
477}
478
479// ── StitchBody ───────────────────────────────────────────────────────
480
481impl StitchBody {
482    pub fn content_lines(&self) -> impl Iterator<Item = ContentLine> {
483        support::children(&self.syntax)
484    }
485
486    pub fn logic_lines(&self) -> impl Iterator<Item = LogicLine> {
487        support::children(&self.syntax)
488    }
489
490    pub fn choices(&self) -> impl Iterator<Item = Choice> {
491        support::children(&self.syntax)
492    }
493
494    pub fn gathers(&self) -> impl Iterator<Item = Gather> {
495        support::children(&self.syntax)
496    }
497}
498
499// ── ContentLine ──────────────────────────────────────────────────────
500
501impl ContentLine {
502    pub fn mixed_content(&self) -> Option<MixedContent> {
503        support::child(&self.syntax)
504    }
505
506    pub fn divert(&self) -> Option<DivertNode> {
507        support::child(&self.syntax)
508    }
509
510    pub fn tags(&self) -> Option<Tags> {
511        support::child(&self.syntax)
512    }
513}
514
515// ── LogicLine ────────────────────────────────────────────────────────
516
517impl LogicLine {
518    pub fn return_stmt(&self) -> Option<ReturnStmt> {
519        support::child(&self.syntax)
520    }
521
522    pub fn temp_decl(&self) -> Option<TempDecl> {
523        support::child(&self.syntax)
524    }
525
526    pub fn assignment(&self) -> Option<Assignment> {
527        support::child(&self.syntax)
528    }
529}
530
531// ── TagLine ──────────────────────────────────────────────────────────
532
533impl TagLine {
534    pub fn tags(&self) -> Option<Tags> {
535        support::child(&self.syntax)
536    }
537}
538
539// ── ReturnStmt ───────────────────────────────────────────────────────
540
541impl ReturnStmt {
542    /// Returns the value expression, if any.
543    ///
544    /// A bare `return` has no child expression node; `return expr` always
545    /// wraps the expression in a typed node (the parser calls `expression()`).
546    pub fn value(&self) -> Option<Expr> {
547        support::child(&self.syntax)
548    }
549
550    /// Returns `true` if the return has a value expression.
551    pub fn has_value(&self) -> bool {
552        self.value().is_some()
553    }
554}
555
556// ── TempDecl ─────────────────────────────────────────────────────────
557
558impl TempDecl {
559    pub fn identifier(&self) -> Option<Identifier> {
560        support::child(&self.syntax)
561    }
562
563    pub fn name(&self) -> Option<String> {
564        self.identifier().and_then(|id| id.name())
565    }
566
567    pub fn eq_token(&self) -> Option<SyntaxToken> {
568        support::token(&self.syntax, EQ)
569    }
570
571    /// Returns the initializer expression after `=`.
572    pub fn value(&self) -> Option<Expr> {
573        support::child(&self.syntax)
574    }
575}
576
577// ── Assignment ───────────────────────────────────────────────────────
578
579impl Assignment {
580    pub fn target(&self) -> Option<Expr> {
581        self.syntax.children().find_map(Expr::cast)
582    }
583
584    /// The assignment operator token (`=`, `+=`, or `-=`).
585    pub fn op_token(&self) -> Option<SyntaxToken> {
586        self.syntax
587            .children_with_tokens()
588            .filter_map(rowan::NodeOrToken::into_token)
589            .find(|tok| matches!(tok.kind(), EQ | PLUS_EQ | MINUS_EQ))
590    }
591
592    /// Returns the right-hand side value expression (the second `Expr` child).
593    pub fn value(&self) -> Option<Expr> {
594        self.syntax.children().filter_map(Expr::cast).nth(1)
595    }
596}
597
598// ── MixedContent ─────────────────────────────────────────────────────
599
600impl MixedContent {
601    pub fn texts(&self) -> impl Iterator<Item = Text> {
602        support::children(&self.syntax)
603    }
604
605    pub fn glue_nodes(&self) -> impl Iterator<Item = GlueNode> {
606        support::children(&self.syntax)
607    }
608
609    pub fn inline_logics(&self) -> impl Iterator<Item = InlineLogic> {
610        support::children(&self.syntax)
611    }
612
613    pub fn escapes(&self) -> impl Iterator<Item = Escape> {
614        support::children(&self.syntax)
615    }
616}
617
618// ── Choice ───────────────────────────────────────────────────────────
619
620impl Choice {
621    pub fn bullets(&self) -> Option<ChoiceBullets> {
622        support::child(&self.syntax)
623    }
624
625    pub fn label(&self) -> Option<Label> {
626        support::child(&self.syntax)
627    }
628
629    pub fn conditions(&self) -> impl Iterator<Item = ChoiceCondition> {
630        support::children(&self.syntax)
631    }
632
633    pub fn start_content(&self) -> Option<ChoiceStartContent> {
634        support::child(&self.syntax)
635    }
636
637    pub fn bracket_content(&self) -> Option<ChoiceBracketContent> {
638        support::child(&self.syntax)
639    }
640
641    pub fn inner_content(&self) -> Option<ChoiceInnerContent> {
642        support::child(&self.syntax)
643    }
644
645    pub fn divert(&self) -> Option<DivertNode> {
646        support::child(&self.syntax)
647    }
648
649    pub fn tags(&self) -> Option<Tags> {
650        support::child(&self.syntax)
651    }
652
653    /// Returns an iterator over all TAGS children (tags can appear on
654    /// each content region within a choice line).
655    pub fn all_tags(&self) -> impl Iterator<Item = Tags> {
656        support::children(&self.syntax)
657    }
658}
659
660// ── ChoiceBullets ────────────────────────────────────────────────────
661
662impl ChoiceBullets {
663    /// Number of bullet characters (nesting depth).
664    pub fn depth(&self) -> usize {
665        self.syntax
666            .children_with_tokens()
667            .filter_map(rowan::NodeOrToken::into_token)
668            .filter(|tok| matches!(tok.kind(), STAR | PLUS))
669            .count()
670    }
671
672    /// Returns `true` if using `+` (sticky), `false` if using `*`.
673    ///
674    /// Determined by the first bullet token, matching the reference ink
675    /// compiler's behavior for degenerate mixed-bullet cases.
676    pub fn is_sticky(&self) -> bool {
677        self.syntax
678            .children_with_tokens()
679            .filter_map(rowan::NodeOrToken::into_token)
680            .find(|tok| matches!(tok.kind(), STAR | PLUS))
681            .is_some_and(|tok| tok.kind() == PLUS)
682    }
683
684    /// Returns `true` if bullets mix `*` and `+` (e.g. `*+`, `+*`).
685    ///
686    /// Mixed bullets are degenerate input — a diagnostic pass should flag them.
687    pub fn is_mixed(&self) -> bool {
688        let mut has_star = false;
689        let mut has_plus = false;
690        for tok in self
691            .syntax
692            .children_with_tokens()
693            .filter_map(rowan::NodeOrToken::into_token)
694        {
695            match tok.kind() {
696                STAR => has_star = true,
697                PLUS => has_plus = true,
698                _ => {}
699            }
700        }
701        has_star && has_plus
702    }
703}
704
705// ── Label ────────────────────────────────────────────────────────────
706
707impl Label {
708    pub fn identifier(&self) -> Option<Identifier> {
709        support::child(&self.syntax)
710    }
711
712    pub fn name(&self) -> Option<String> {
713        self.identifier().and_then(|id| id.name())
714    }
715}
716
717// ── Gather ───────────────────────────────────────────────────────────
718
719impl Gather {
720    pub fn dashes(&self) -> Option<GatherDashes> {
721        support::child(&self.syntax)
722    }
723
724    pub fn label(&self) -> Option<Label> {
725        support::child(&self.syntax)
726    }
727
728    pub fn mixed_content(&self) -> Option<MixedContent> {
729        support::child(&self.syntax)
730    }
731
732    /// Inline choice on the same line as the gather (e.g. `- * hello`).
733    pub fn choice(&self) -> Option<Choice> {
734        support::child(&self.syntax)
735    }
736
737    pub fn divert(&self) -> Option<DivertNode> {
738        support::child(&self.syntax)
739    }
740
741    pub fn tags(&self) -> Option<Tags> {
742        support::child(&self.syntax)
743    }
744}
745
746// ── GatherDashes ─────────────────────────────────────────────────────
747
748impl GatherDashes {
749    /// Number of dashes (nesting depth).
750    pub fn depth(&self) -> usize {
751        support::tokens(&self.syntax, MINUS).count()
752    }
753}
754
755// ── Tags ─────────────────────────────────────────────────────────────
756
757impl Tags {
758    pub fn tags(&self) -> impl Iterator<Item = Tag> {
759        support::children(&self.syntax)
760    }
761}
762
763// ── Tag ──────────────────────────────────────────────────────────────
764
765impl Tag {
766    /// Returns the tag value with the leading `#` stripped.
767    ///
768    /// Walks tokens directly rather than string-manipulating the full node text.
769    /// The parser guarantees a `HASH` token is always present.
770    pub fn text(&self) -> String {
771        self.syntax
772            .children_with_tokens()
773            .filter_map(rowan::NodeOrToken::into_token)
774            .filter(|tok| tok.kind() != HASH)
775            .map(|tok| tok.text().to_string())
776            .collect::<String>()
777            .trim()
778            .to_string()
779    }
780}
781
782// ── InlineLogic ──────────────────────────────────────────────────────
783
784impl InlineLogic {
785    pub fn inner_expression(&self) -> Option<InnerExpression> {
786        support::child(&self.syntax)
787    }
788
789    pub fn conditional(&self) -> Option<ConditionalWithExpr> {
790        support::child(&self.syntax)
791    }
792
793    pub fn sequence(&self) -> Option<SequenceWithAnnotation> {
794        support::child(&self.syntax)
795    }
796
797    pub fn implicit_sequence(&self) -> Option<ImplicitSequence> {
798        support::child(&self.syntax)
799    }
800
801    pub fn multiline_conditional(&self) -> Option<MultilineConditional> {
802        support::child(&self.syntax)
803    }
804}
805
806// ── MultilineBlock ───────────────────────────────────────────────────
807
808impl MultilineBlock {
809    pub fn conditional(&self) -> Option<ConditionalWithExpr> {
810        support::child(&self.syntax)
811    }
812
813    pub fn sequence(&self) -> Option<SequenceWithAnnotation> {
814        support::child(&self.syntax)
815    }
816
817    pub fn branches_cond(&self) -> Option<MultilineBranchesCond> {
818        support::child(&self.syntax)
819    }
820}
821
822// ── SequenceWithAnnotation ───────────────────────────────────────────
823
824impl SequenceWithAnnotation {
825    pub fn symbol_annotation(&self) -> Option<SequenceSymbolAnnotation> {
826        support::child(&self.syntax)
827    }
828
829    pub fn word_annotation(&self) -> Option<SequenceWordAnnotation> {
830        support::child(&self.syntax)
831    }
832
833    pub fn inline_branches(&self) -> Option<InlineBranchesSeq> {
834        support::child(&self.syntax)
835    }
836
837    pub fn multiline_branches(&self) -> Option<MultilineBranchesSeq> {
838        support::child(&self.syntax)
839    }
840}
841
842// ── SequenceSymbolAnnotation ──────────────────────────────────────────
843
844impl SequenceSymbolAnnotation {
845    pub fn amp_token(&self) -> Option<SyntaxToken> {
846        support::token(&self.syntax, AMP)
847    }
848
849    pub fn bang_token(&self) -> Option<SyntaxToken> {
850        support::token(&self.syntax, BANG)
851    }
852
853    pub fn tilde_token(&self) -> Option<SyntaxToken> {
854        support::token(&self.syntax, TILDE)
855    }
856
857    pub fn dollar_token(&self) -> Option<SyntaxToken> {
858        support::token(&self.syntax, DOLLAR)
859    }
860}
861
862// ── SequenceWordAnnotation ───────────────────────────────────────────
863
864impl SequenceWordAnnotation {
865    pub fn stopping_kw(&self) -> Option<SyntaxToken> {
866        support::token(&self.syntax, KW_STOPPING)
867    }
868
869    pub fn cycle_kw(&self) -> Option<SyntaxToken> {
870        support::token(&self.syntax, KW_CYCLE)
871    }
872
873    pub fn shuffle_kw(&self) -> Option<SyntaxToken> {
874        support::token(&self.syntax, KW_SHUFFLE)
875    }
876
877    pub fn once_kw(&self) -> Option<SyntaxToken> {
878        support::token(&self.syntax, KW_ONCE)
879    }
880}
881
882// ── InlineBranchesSeq ────────────────────────────────────────────────
883
884impl InlineBranchesSeq {
885    pub fn branches(&self) -> impl Iterator<Item = BranchContent> {
886        support::children(&self.syntax)
887    }
888}
889
890// ── InlineBranchesCond ───────────────────────────────────────────────
891
892impl InlineBranchesCond {
893    pub fn branches(&self) -> impl Iterator<Item = BranchContent> {
894        support::children(&self.syntax)
895    }
896}
897
898// ── MultilineBranchesSeq ─────────────────────────────────────────────
899
900impl MultilineBranchesSeq {
901    pub fn branches(&self) -> impl Iterator<Item = MultilineBranchSeq> {
902        support::children(&self.syntax)
903    }
904}
905
906// ── MultilineBranchesCond ────────────────────────────────────────────
907
908impl MultilineBranchesCond {
909    pub fn branches(&self) -> impl Iterator<Item = MultilineBranchCond> {
910        support::children(&self.syntax)
911    }
912}
913
914// ── MultilineBranchSeq ───────────────────────────────────────────────
915
916impl MultilineBranchSeq {
917    pub fn body(&self) -> Option<MultilineBranchBody> {
918        support::child(&self.syntax)
919    }
920}
921
922// ── MultilineBranchCond ──────────────────────────────────────────────
923
924impl MultilineBranchCond {
925    /// Returns the branch condition expression (if not an else branch).
926    pub fn condition(&self) -> Option<Expr> {
927        support::child(&self.syntax)
928    }
929
930    pub fn body(&self) -> Option<MultilineBranchBody> {
931        support::child(&self.syntax)
932    }
933
934    pub fn else_kw(&self) -> Option<SyntaxToken> {
935        support::token(&self.syntax, KW_ELSE)
936    }
937
938    pub fn is_else(&self) -> bool {
939        self.else_kw().is_some()
940    }
941}
942
943// ── ConditionalWithExpr ──────────────────────────────────────────────
944
945impl ConditionalWithExpr {
946    /// Returns the condition expression.
947    pub fn condition(&self) -> Option<Expr> {
948        support::child(&self.syntax)
949    }
950
951    pub fn inline_branches(&self) -> Option<InlineBranchesCond> {
952        support::child(&self.syntax)
953    }
954
955    pub fn multiline_branches(&self) -> Option<MultilineBranchesCond> {
956        support::child(&self.syntax)
957    }
958
959    pub fn branchless_body(&self) -> Option<BranchlessCondBody> {
960        support::child(&self.syntax)
961    }
962}
963
964// ── BranchlessCondBody ───────────────────────────────────────────────
965
966impl BranchlessCondBody {
967    pub fn texts(&self) -> impl Iterator<Item = Text> {
968        support::children(&self.syntax)
969    }
970
971    pub fn inline_logics(&self) -> impl Iterator<Item = InlineLogic> {
972        support::children(&self.syntax)
973    }
974
975    pub fn glue_nodes(&self) -> impl Iterator<Item = GlueNode> {
976        support::children(&self.syntax)
977    }
978
979    pub fn escapes(&self) -> impl Iterator<Item = Escape> {
980        support::children(&self.syntax)
981    }
982
983    pub fn logic_lines(&self) -> impl Iterator<Item = LogicLine> {
984        support::children(&self.syntax)
985    }
986
987    pub fn divert(&self) -> Option<DivertNode> {
988        support::child(&self.syntax)
989    }
990
991    pub fn content_lines(&self) -> impl Iterator<Item = ContentLine> {
992        support::children(&self.syntax)
993    }
994
995    pub fn else_branch(&self) -> Option<ElseBranch> {
996        support::child(&self.syntax)
997    }
998}
999
1000// ── ElseBranch ───────────────────────────────────────────────────────
1001
1002impl ElseBranch {
1003    pub fn branch(&self) -> Option<MultilineBranchCond> {
1004        support::child(&self.syntax)
1005    }
1006}
1007
1008// ── MultilineConditional ─────────────────────────────────────────────
1009
1010impl MultilineConditional {
1011    pub fn branches(&self) -> impl Iterator<Item = MultilineBranchCond> {
1012        support::children(&self.syntax)
1013    }
1014}
1015
1016// ── ImplicitSequence ─────────────────────────────────────────────────
1017
1018impl ImplicitSequence {
1019    pub fn branches(&self) -> impl Iterator<Item = BranchContent> {
1020        support::children(&self.syntax)
1021    }
1022}
1023
1024// ── PrefixExpr ───────────────────────────────────────────────────────
1025
1026impl PrefixExpr {
1027    pub fn op_token(&self) -> Option<SyntaxToken> {
1028        self.syntax
1029            .children_with_tokens()
1030            .filter_map(rowan::NodeOrToken::into_token)
1031            .find(|tok| matches!(tok.kind(), MINUS | BANG | KW_NOT))
1032    }
1033
1034    /// Returns the operand expression.
1035    pub fn operand(&self) -> Option<Expr> {
1036        support::child(&self.syntax)
1037    }
1038}
1039
1040// ── PostfixExpr ──────────────────────────────────────────────────────
1041
1042impl PostfixExpr {
1043    /// Returns the first operator token (`PLUS` for `++`, `MINUS` for `--`).
1044    /// Both operators are two adjacent tokens inside this node.
1045    pub fn op_token(&self) -> Option<SyntaxToken> {
1046        self.syntax
1047            .children_with_tokens()
1048            .filter_map(rowan::NodeOrToken::into_token)
1049            .find(|tok| matches!(tok.kind(), PLUS | MINUS))
1050    }
1051
1052    /// Returns the operand expression.
1053    pub fn operand(&self) -> Option<Expr> {
1054        support::child(&self.syntax)
1055    }
1056}
1057
1058// ── InfixExpr ────────────────────────────────────────────────────────
1059
1060impl InfixExpr {
1061    pub fn op_token(&self) -> Option<SyntaxToken> {
1062        self.syntax
1063            .children_with_tokens()
1064            .filter_map(rowan::NodeOrToken::into_token)
1065            .find(|tok| {
1066                matches!(
1067                    tok.kind(),
1068                    PLUS | MINUS
1069                        | STAR
1070                        | SLASH
1071                        | PERCENT
1072                        | CARET
1073                        | EQ_EQ
1074                        | BANG_EQ
1075                        | LT
1076                        | GT
1077                        | LT_EQ
1078                        | GT_EQ
1079                        | KW_AND
1080                        | AMP_AMP
1081                        | KW_OR
1082                        | PIPE
1083                        | KW_MOD
1084                        | KW_HAS
1085                        | KW_HASNT
1086                        | QUESTION
1087                        | BANG_QUESTION
1088                        | PLUS_EQ
1089                        | MINUS_EQ
1090                )
1091            })
1092    }
1093
1094    pub fn lhs(&self) -> Option<Expr> {
1095        self.syntax.children().find_map(Expr::cast)
1096    }
1097
1098    pub fn rhs(&self) -> Option<Expr> {
1099        self.syntax.children().filter_map(Expr::cast).nth(1)
1100    }
1101}
1102
1103// ── FunctionCall ─────────────────────────────────────────────────────
1104
1105impl FunctionCall {
1106    pub fn identifier(&self) -> Option<Identifier> {
1107        support::child(&self.syntax)
1108    }
1109
1110    pub fn name(&self) -> Option<String> {
1111        self.identifier().and_then(|id| id.name())
1112    }
1113
1114    pub fn arg_list(&self) -> Option<ArgList> {
1115        support::child(&self.syntax)
1116    }
1117}
1118
1119// ── ArgList ──────────────────────────────────────────────────────────
1120
1121impl ArgList {
1122    /// Number of arguments (child expression nodes).
1123    pub fn arg_count(&self) -> usize {
1124        self.syntax
1125            .children()
1126            .filter(|child| child.kind() != SyntaxKind::ERROR)
1127            .count()
1128    }
1129
1130    /// Iterator over the argument expressions.
1131    pub fn args(&self) -> impl Iterator<Item = Expr> {
1132        support::children(&self.syntax)
1133    }
1134}
1135
1136// ── DivertTargetExpr ─────────────────────────────────────────────────
1137
1138impl DivertTargetExpr {
1139    pub fn target(&self) -> Option<Path> {
1140        support::child(&self.syntax)
1141    }
1142}
1143
1144// ── ListExpr ─────────────────────────────────────────────────────────
1145
1146impl ListExpr {
1147    pub fn items(&self) -> impl Iterator<Item = Path> {
1148        support::children(&self.syntax)
1149    }
1150}
1151
1152// ── DivertNode ───────────────────────────────────────────────────────
1153
1154impl DivertNode {
1155    pub fn thread_start(&self) -> Option<ThreadStart> {
1156        support::child(&self.syntax)
1157    }
1158
1159    pub fn tunnel_onwards(&self) -> Option<TunnelOnwardsNode> {
1160        support::child(&self.syntax)
1161    }
1162
1163    pub fn tunnel_call(&self) -> Option<TunnelCallNode> {
1164        support::child(&self.syntax)
1165    }
1166
1167    pub fn simple_divert(&self) -> Option<SimpleDivert> {
1168        support::child(&self.syntax)
1169    }
1170}
1171
1172// ── SimpleDivert ─────────────────────────────────────────────────────
1173
1174impl SimpleDivert {
1175    pub fn targets(&self) -> impl Iterator<Item = DivertTargetWithArgs> {
1176        support::children(&self.syntax)
1177    }
1178}
1179
1180// ── DivertTargetWithArgs ─────────────────────────────────────────────
1181
1182impl DivertTargetWithArgs {
1183    pub fn path(&self) -> Option<Path> {
1184        support::child(&self.syntax)
1185    }
1186
1187    pub fn done_kw(&self) -> Option<SyntaxToken> {
1188        support::token(&self.syntax, KW_DONE)
1189    }
1190
1191    pub fn end_kw(&self) -> Option<SyntaxToken> {
1192        support::token(&self.syntax, KW_END)
1193    }
1194
1195    pub fn arg_list(&self) -> Option<ArgList> {
1196        support::child(&self.syntax)
1197    }
1198}
1199
1200// ── ThreadStart ──────────────────────────────────────────────────────
1201
1202impl ThreadStart {
1203    /// Returns the target path.
1204    ///
1205    /// The parser produces a `PATH` child directly (not wrapped in
1206    /// `DivertTargetWithArgs`), so this returns `Option<Path>`.
1207    pub fn target(&self) -> Option<Path> {
1208        support::child(&self.syntax)
1209    }
1210
1211    pub fn arg_list(&self) -> Option<ArgList> {
1212        support::child(&self.syntax)
1213    }
1214}
1215
1216// ── TunnelOnwardsNode ────────────────────────────────────────────────
1217
1218impl TunnelOnwardsNode {
1219    pub fn targets(&self) -> impl Iterator<Item = DivertTargetWithArgs> {
1220        support::children(&self.syntax)
1221    }
1222
1223    pub fn tunnel_call(&self) -> Option<TunnelCallNode> {
1224        support::child(&self.syntax)
1225    }
1226}
1227
1228// ── TunnelCallNode ──────────────────────────────────────────────────
1229
1230impl TunnelCallNode {
1231    pub fn targets(&self) -> impl Iterator<Item = DivertTargetWithArgs> {
1232        support::children(&self.syntax)
1233    }
1234}
1235
1236// ── Identifier ───────────────────────────────────────────────────────
1237
1238impl Identifier {
1239    pub fn ident_token(&self) -> Option<SyntaxToken> {
1240        support::token(&self.syntax, IDENT)
1241    }
1242
1243    /// Returns the name text, accepting either `IDENT` or keyword tokens
1244    /// (ink keywords are contextual and may appear as identifiers).
1245    pub fn name(&self) -> Option<String> {
1246        self.ident_token()
1247            .or_else(|| {
1248                self.syntax
1249                    .children_with_tokens()
1250                    .filter_map(rowan::NodeOrToken::into_token)
1251                    .find(|t| t.kind().is_keyword())
1252            })
1253            .map(|t| t.text().to_string())
1254    }
1255}
1256
1257// ── Path ─────────────────────────────────────────────────────────────
1258
1259impl Path {
1260    /// Iterator over the segment tokens (`IDENT` or keyword tokens between dots).
1261    pub fn segments(&self) -> impl Iterator<Item = SyntaxToken> {
1262        self.syntax
1263            .children_with_tokens()
1264            .filter_map(rowan::NodeOrToken::into_token)
1265            .filter(|t| t.kind() == IDENT || t.kind().is_keyword())
1266    }
1267
1268    /// Full dotted name (e.g. `"knot.stitch"`).
1269    pub fn full_name(&self) -> String {
1270        self.segments()
1271            .map(|t| t.text().to_string())
1272            .collect::<Vec<_>>()
1273            .join(".")
1274    }
1275}
1276
1277// ── VarDecl ──────────────────────────────────────────────────────────
1278
1279impl VarDecl {
1280    pub fn identifier(&self) -> Option<Identifier> {
1281        support::child(&self.syntax)
1282    }
1283
1284    pub fn name(&self) -> Option<String> {
1285        self.identifier().and_then(|id| id.name())
1286    }
1287
1288    /// Returns the initializer expression after `=`.
1289    pub fn value(&self) -> Option<Expr> {
1290        support::child(&self.syntax)
1291    }
1292}
1293
1294// ── ConstDecl ────────────────────────────────────────────────────────
1295
1296impl ConstDecl {
1297    pub fn identifier(&self) -> Option<Identifier> {
1298        support::child(&self.syntax)
1299    }
1300
1301    pub fn name(&self) -> Option<String> {
1302        self.identifier().and_then(|id| id.name())
1303    }
1304
1305    /// Returns the initializer expression after `=`.
1306    pub fn value(&self) -> Option<Expr> {
1307        support::child(&self.syntax)
1308    }
1309}
1310
1311// ── ListDecl ─────────────────────────────────────────────────────────
1312
1313impl ListDecl {
1314    pub fn identifier(&self) -> Option<Identifier> {
1315        support::child(&self.syntax)
1316    }
1317
1318    pub fn name(&self) -> Option<String> {
1319        self.identifier().and_then(|id| id.name())
1320    }
1321
1322    pub fn definition(&self) -> Option<ListDef> {
1323        support::child(&self.syntax)
1324    }
1325}
1326
1327// ── ListDef ──────────────────────────────────────────────────────────
1328
1329impl ListDef {
1330    pub fn members(&self) -> impl Iterator<Item = ListMember> {
1331        support::children(&self.syntax)
1332    }
1333}
1334
1335// ── ListMember ───────────────────────────────────────────────────────
1336
1337impl ListMember {
1338    pub fn on_member(&self) -> Option<ListMemberOn> {
1339        support::child(&self.syntax)
1340    }
1341
1342    pub fn off_member(&self) -> Option<ListMemberOff> {
1343        support::child(&self.syntax)
1344    }
1345}
1346
1347// ── ListMemberOn ─────────────────────────────────────────────────────
1348
1349impl ListMemberOn {
1350    pub fn name_token(&self) -> Option<SyntaxToken> {
1351        // Ink keywords are contextual — accept IDENT or keywords as member names.
1352        support::ident_or_keyword_token(&self.syntax)
1353    }
1354
1355    pub fn name(&self) -> Option<String> {
1356        self.name_token().map(|t| t.text().to_string())
1357    }
1358
1359    pub fn value_token(&self) -> Option<SyntaxToken> {
1360        support::token(&self.syntax, INTEGER)
1361    }
1362
1363    /// Returns the explicit integer value assigned to this member, if any.
1364    pub fn value(&self) -> Option<i64> {
1365        self.value_token()
1366            .and_then(|t| t.text().parse::<i64>().ok())
1367    }
1368}
1369
1370// ── ListMemberOff ────────────────────────────────────────────────────
1371
1372impl ListMemberOff {
1373    pub fn name_token(&self) -> Option<SyntaxToken> {
1374        // Ink keywords are contextual — accept IDENT or keywords as member names.
1375        support::ident_or_keyword_token(&self.syntax)
1376    }
1377
1378    pub fn name(&self) -> Option<String> {
1379        self.name_token().map(|t| t.text().to_string())
1380    }
1381
1382    pub fn value_token(&self) -> Option<SyntaxToken> {
1383        support::token(&self.syntax, INTEGER)
1384    }
1385
1386    /// Returns the explicit integer value assigned to this member, if any.
1387    pub fn value(&self) -> Option<i64> {
1388        self.value_token()
1389            .and_then(|t| t.text().parse::<i64>().ok())
1390    }
1391}
1392
1393// ── FunctionParamList ────────────────────────────────────────────────
1394
1395impl FunctionParamList {
1396    /// Iterator over the `Identifier` nodes in the param list.
1397    pub fn params(&self) -> impl Iterator<Item = Identifier> {
1398        support::children(&self.syntax)
1399    }
1400}
1401
1402// ── IntegerLit ───────────────────────────────────────────────────────
1403
1404impl IntegerLit {
1405    pub fn value_token(&self) -> Option<SyntaxToken> {
1406        support::token(&self.syntax, INTEGER)
1407    }
1408
1409    pub fn value(&self) -> Option<i64> {
1410        self.value_token()
1411            .and_then(|t| t.text().parse::<i64>().ok())
1412    }
1413}
1414
1415// ── FloatLit ─────────────────────────────────────────────────────────
1416
1417impl FloatLit {
1418    pub fn value_token(&self) -> Option<SyntaxToken> {
1419        support::token(&self.syntax, FLOAT)
1420    }
1421
1422    pub fn value(&self) -> Option<f64> {
1423        self.value_token()
1424            .and_then(|t| t.text().parse::<f64>().ok())
1425    }
1426}
1427
1428// ── StringLit ────────────────────────────────────────────────────────
1429
1430impl StringLit {
1431    /// Returns the raw content between the quotes (excluding the quotes themselves).
1432    ///
1433    /// The opening quote is always present (the parser enters `string_literal`
1434    /// only on a `QUOTE` token). The closing quote may be absent if the string
1435    /// is unterminated — the parser emits an error and closes the node without
1436    /// consuming a trailing `QUOTE`. The `strip_suffix` fallback handles that
1437    /// error-recovery case.
1438    pub fn raw_text(&self) -> String {
1439        let full = self.syntax.text().to_string();
1440        let trimmed = full.strip_prefix('"').unwrap_or(&full);
1441        trimmed.strip_suffix('"').unwrap_or(trimmed).to_string()
1442    }
1443}
1444
1445// ── BooleanLit ───────────────────────────────────────────────────────
1446
1447impl BooleanLit {
1448    pub fn value(&self) -> Option<bool> {
1449        let tok = self
1450            .syntax
1451            .children_with_tokens()
1452            .filter_map(rowan::NodeOrToken::into_token)
1453            .find(|tok| matches!(tok.kind(), KW_TRUE | KW_FALSE))?;
1454        match tok.kind() {
1455            KW_TRUE => Some(true),
1456            KW_FALSE => Some(false),
1457            _ => None,
1458        }
1459    }
1460}
1461
1462// ── AuthorWarning ────────────────────────────────────────────────────
1463
1464impl AuthorWarning {
1465    /// Returns the warning text with the `TODO:` prefix stripped.
1466    ///
1467    /// Walks tokens directly — skips the `KW_TODO` token and the optional
1468    /// `COLON`, then collects remaining content until `NEWLINE`.
1469    pub fn text(&self) -> String {
1470        self.syntax
1471            .children_with_tokens()
1472            .filter_map(rowan::NodeOrToken::into_token)
1473            .skip_while(|tok| matches!(tok.kind(), KW_TODO | COLON) || tok.kind().is_trivia())
1474            .take_while(|tok| tok.kind() != NEWLINE)
1475            .map(|tok| tok.text().to_string())
1476            .collect::<String>()
1477            .trim()
1478            .to_string()
1479    }
1480}
1481
1482// ── ChoiceCondition ──────────────────────────────────────────────────
1483
1484impl ChoiceCondition {
1485    /// Returns the condition expression inside `{ expr }`.
1486    pub fn expr(&self) -> Option<Expr> {
1487        support::child(&self.syntax)
1488    }
1489}
1490
1491// ── InnerExpression ──────────────────────────────────────────────────
1492
1493impl InnerExpression {
1494    /// Returns the wrapped expression.
1495    pub fn expr(&self) -> Option<Expr> {
1496        support::child(&self.syntax)
1497    }
1498}
1499
1500// ── ParenExpr ────────────────────────────────────────────────────────
1501
1502impl ParenExpr {
1503    /// Returns the inner expression inside `( expr )`.
1504    pub fn inner(&self) -> Option<Expr> {
1505        support::child(&self.syntax)
1506    }
1507}
1508
1509// ── BranchContent (extra) ────────────────────────────────────────────
1510
1511impl BranchContent {
1512    pub fn divert(&self) -> Option<DivertNode> {
1513        support::child(&self.syntax)
1514    }
1515}
1516
1517// ── MultilineBranchBody ──────────────────────────────────────────────
1518
1519impl MultilineBranchBody {
1520    pub fn texts(&self) -> impl Iterator<Item = Text> {
1521        support::children(&self.syntax)
1522    }
1523
1524    pub fn inline_logics(&self) -> impl Iterator<Item = InlineLogic> {
1525        support::children(&self.syntax)
1526    }
1527
1528    pub fn glue_nodes(&self) -> impl Iterator<Item = GlueNode> {
1529        support::children(&self.syntax)
1530    }
1531
1532    pub fn escapes(&self) -> impl Iterator<Item = Escape> {
1533        support::children(&self.syntax)
1534    }
1535
1536    pub fn logic_lines(&self) -> impl Iterator<Item = LogicLine> {
1537        support::children(&self.syntax)
1538    }
1539
1540    pub fn divert(&self) -> Option<DivertNode> {
1541        support::child(&self.syntax)
1542    }
1543
1544    pub fn content_lines(&self) -> impl Iterator<Item = ContentLine> {
1545        support::children(&self.syntax)
1546    }
1547
1548    pub fn choices(&self) -> impl Iterator<Item = Choice> {
1549        support::children(&self.syntax)
1550    }
1551}