Skip to main content

brink_syntax_native/ast/
nodes.rs

1//! Typed AST node wrappers for every node kind in the native CST.
2//!
3//! Every struct is a zero-cost newtype generated by [`ast_node!`]. A
4//! representative subset has hand-written accessors below its
5//! `ast_node!` line — enough to prove the pattern end-to-end for B0.6+
6//! without pre-building every accessor a later lowering pass might want
7//! (that's additive, not a re-architecture, when it's actually needed).
8
9use crate::SyntaxKind::{self, DOC_COMMENT_INNER, DOC_COMMENT_OUTER, HASH, IDENT, L_PAREN};
10use crate::ast::AstNode as _;
11use crate::ast::ast_node;
12use crate::ast::support;
13use crate::{SyntaxNode, SyntaxToken};
14
15// ── Doc comments (B0.6b) ──────────────────────────────────────────────
16
17ast_node!(DocComment, DOC_COMMENT);
18
19// ── Top level & declarations ────────────────────────────────────────
20
21ast_node!(SourceFile, SOURCE_FILE);
22ast_node!(FlowDecl, FLOW_DECL);
23ast_node!(FnDecl, FN_DECL);
24ast_node!(ParamList, PARAM_LIST);
25ast_node!(Param, PARAM);
26ast_node!(VarDecl, VAR_DECL);
27ast_node!(ConstDecl, CONST_DECL);
28ast_node!(FlagsDecl, FLAGS_DECL);
29ast_node!(FlagsMemberList, FLAGS_MEMBER_LIST);
30ast_node!(FlagsMember, FLAGS_MEMBER);
31ast_node!(StructDecl, STRUCT_DECL);
32ast_node!(StructField, STRUCT_FIELD);
33ast_node!(ExternDecl, EXTERN_DECL);
34ast_node!(UseDecl, USE_DECL);
35ast_node!(UseTree, USE_TREE);
36ast_node!(UseTreeList, USE_TREE_LIST);
37ast_node!(ImportDecl, IMPORT_DECL);
38ast_node!(ModuleDecl, MODULE_DECL);
39
40// ── Bodies & content ─────────────────────────────────────────────────
41
42ast_node!(Block, BLOCK);
43ast_node!(ContentLine, CONTENT_LINE);
44ast_node!(LogicLine, LOGIC_LINE);
45ast_node!(ProseLine, PROSE_LINE);
46ast_node!(Text, TEXT);
47ast_node!(Interpolation, INTERPOLATION);
48ast_node!(GlueNode, GLUE_NODE);
49ast_node!(TagLine, TAG_LINE);
50ast_node!(Tag, TAG);
51
52// ── Prose block elements (docs/prose-dialect-spec.md §8b/§8d) ────────
53
54ast_node!(SceneStitch, SCENE_STITCH);
55ast_node!(SceneHeading, SCENE_HEADING);
56ast_node!(SceneTitle, SCENE_TITLE);
57ast_node!(SceneSlug, SCENE_SLUG);
58ast_node!(SceneBody, SCENE_BODY);
59ast_node!(Cue, CUE);
60ast_node!(CueName, CUE_NAME);
61ast_node!(CompactCue, COMPACT_CUE);
62ast_node!(Parenthetical, PARENTHETICAL);
63ast_node!(BangDispatch, BANG_DISPATCH);
64ast_node!(DispatchName, DISPATCH_NAME);
65
66// ── Inline markup (docs/prose-dialect-spec.md §4, issue #1716) ──────
67ast_node!(Span, SPAN);
68ast_node!(SpanName, SPAN_NAME);
69ast_node!(SpanAttr, SPAN_ATTR);
70ast_node!(SpanAttrValue, SPAN_ATTR_VALUE);
71ast_node!(Escape, ESCAPE);
72
73// ── Choice points ────────────────────────────────────────────────────
74
75ast_node!(ChoicePoint, CHOICE_POINT);
76ast_node!(Choice, CHOICE);
77ast_node!(ChoiceBullet, CHOICE_BULLET);
78ast_node!(Label, LABEL);
79ast_node!(ChoiceGuard, CHOICE_GUARD);
80ast_node!(ChoiceStartContent, CHOICE_START_CONTENT);
81ast_node!(ChoiceBracketContent, CHOICE_BRACKET_CONTENT);
82ast_node!(ChoiceInnerContent, CHOICE_INNER_CONTENT);
83ast_node!(ChoiceBody, CHOICE_BODY);
84ast_node!(ElseBranch, ELSE_BRANCH);
85ast_node!(Splice, SPLICE);
86
87// ── The annotated-brace family: conditional / alternation ───────────
88
89ast_node!(ConditionalBlock, CONDITIONAL_BLOCK);
90ast_node!(IfArm, IF_ARM);
91ast_node!(MatchArm, MATCH_ARM);
92ast_node!(MatchPattern, MATCH_PATTERN);
93ast_node!(AlternationBlock, ALTERNATION_BLOCK);
94ast_node!(AlternationMarker, ALTERNATION_MARKER);
95ast_node!(Entry, ENTRY);
96
97// ── Annotations ──────────────────────────────────────────────────────
98
99ast_node!(AnnotationLine, ANNOTATION_LINE);
100ast_node!(AnnotationArgs, ANNOTATION_ARGS);
101ast_node!(AnnotationArg, ANNOTATION_ARG);
102
103// ── Diverts, tunnels, return ─────────────────────────────────────────
104
105ast_node!(DivertStmt, DIVERT_STMT);
106ast_node!(TunnelCall, TUNNEL_CALL);
107ast_node!(DivertTarget, DIVERT_TARGET);
108ast_node!(ReturnStmt, RETURN_STMT);
109ast_node!(ReturnRedirect, RETURN_REDIRECT);
110
111// ── Paths ────────────────────────────────────────────────────────────
112
113ast_node!(Path, PATH);
114ast_node!(PathSegment, PATH_SEGMENT);
115
116// ── Expressions ──────────────────────────────────────────────────────
117
118ast_node!(IntegerLit, INTEGER_LIT);
119ast_node!(FloatLit, FLOAT_LIT);
120ast_node!(StringLit, STRING_LIT);
121ast_node!(BooleanLit, BOOLEAN_LIT);
122ast_node!(PathExpr, PATH_EXPR);
123ast_node!(ParenExpr, PAREN_EXPR);
124ast_node!(PrefixExpr, PREFIX_EXPR);
125ast_node!(InfixExpr, INFIX_EXPR);
126ast_node!(CallExpr, CALL_EXPR);
127ast_node!(ArgList, ARG_LIST);
128ast_node!(LambdaExpr, LAMBDA_EXPR);
129ast_node!(LambdaParams, LAMBDA_PARAMS);
130
131// ── The array/sequence literal (NG-D, issue #1490) ────────────────────
132
133ast_node!(ArrayLiteral, ARRAY_LITERAL);
134
135// ── The construction initializer (B5, issue #1464) ───────────────────
136
137ast_node!(ConstructLiteral, CONSTRUCT_LITERAL);
138ast_node!(ConstructEntry, CONSTRUCT_ENTRY);
139
140// ── The code-ground statement layer (B0.8 Wave A) ────────────────────
141
142ast_node!(StmtBlock, STMT_BLOCK);
143ast_node!(LetStmt, LET_STMT);
144ast_node!(AssignStmt, ASSIGN_STMT);
145ast_node!(ExprStmt, EXPR_STMT);
146
147// ── The code-ground control-flow layer (B0.8 Wave B) ─────────────────
148
149ast_node!(IfStmt, IF_STMT);
150ast_node!(ElseClause, ELSE_CLAUSE);
151ast_node!(WhileStmt, WHILE_STMT);
152ast_node!(ForStmt, FOR_STMT);
153ast_node!(UntilStmt, UNTIL_STMT);
154
155// ── The code-ground statement tail (B0.8 Wave B tail, issue #1322) ──
156
157ast_node!(BreakStmt, BREAK_STMT);
158ast_node!(ContinueStmt, CONTINUE_STMT);
159
160// ── The type-annotation grammar (NG-A/B/C, #1487/#1488/#1489) ────────
161
162ast_node!(TypeAnnotation, TYPE_ANNOTATION);
163ast_node!(TypeExpr, TYPE_EXPR);
164ast_node!(TypeName, TYPE_NAME);
165ast_node!(TypeGeneric, TYPE_GENERIC);
166ast_node!(TypeFn, TYPE_FN);
167
168// ── The `as` binding (B1b, issue #1475) ──────────────────────────────
169
170ast_node!(AsBinding, AS_BINDING);
171
172// ── Error recovery ───────────────────────────────────────────────────
173
174ast_node!(Error, ERROR);
175
176// ── Hand-written accessors ───────────────────────────────────────────
177
178impl DocComment {
179    /// `true` for the inner (`//!`) form — a run whose tokens are
180    /// `DOC_COMMENT_INNER` rather than `DOC_COMMENT_OUTER`. One node shape
181    /// covers both variants (`syntax_kind.rs`'s `DOC_COMMENT` doc); this is
182    /// how callers tell them apart. A well-formed `DOC_COMMENT` node's
183    /// comment tokens are always uniformly one kind or the other (the
184    /// parser's `doc_comment::consume_doc_run` never mixes them within a
185    /// single run), so checking the first one is sufficient.
186    pub fn is_inner(&self) -> bool {
187        self.syntax
188            .children_with_tokens()
189            .filter_map(rowan::NodeOrToken::into_token)
190            .any(|t| t.kind() == DOC_COMMENT_INNER)
191    }
192
193    /// Every doc-comment line in source order: the token's text with its
194    /// `///`/`//!` marker stripped and a single leading space (if any)
195    /// trimmed, paired with that token's source range — the same shape the
196    /// OLD ink parser's `collect_doc_lines` produces, so both frontends
197    /// feed the identical format-agnostic `hir::doc_block::parse_lines`
198    /// tag parser.
199    pub fn lines(&self) -> Vec<(String, rowan::TextRange)> {
200        self.syntax
201            .children_with_tokens()
202            .filter_map(rowan::NodeOrToken::into_token)
203            .filter(|t| matches!(t.kind(), DOC_COMMENT_OUTER | DOC_COMMENT_INNER))
204            .map(|t| {
205                let text = t.text();
206                let body = text
207                    .strip_prefix("///")
208                    .or_else(|| text.strip_prefix("//!"))
209                    .unwrap_or(text);
210                (body.trim_start().to_string(), t.text_range())
211            })
212            .collect()
213    }
214}
215
216impl SourceFile {
217    /// Every top-level `flow`/`fn` declaration in the file (charter §4:
218    /// "no one-flow-per-file constraint — files hold many declarations").
219    pub fn flows(&self) -> impl Iterator<Item = FlowDecl> {
220        support::children(&self.syntax)
221    }
222
223    /// Every top-level `fn` declaration in the file.
224    pub fn fns(&self) -> impl Iterator<Item = FnDecl> {
225        support::children(&self.syntax)
226    }
227
228    /// Every direct child node, typed as its own `SyntaxKind` where a
229    /// wrapper exists — the generic escape hatch for callers that want to
230    /// walk the whole item list without matching on every variant twice.
231    pub fn syntax_children(&self) -> impl Iterator<Item = SyntaxNode> {
232        self.syntax.children()
233    }
234
235    /// The file-level inner `//!` doc comment, if the file opens with one
236    /// (B0.6b: "documents the enclosing ... file"). CST-only for now — no
237    /// native HIR type represents whole-file identity yet (`lower_native`'s
238    /// module doc, judgment call #7), so nothing consumes this today; kept
239    /// for the LSP/fmt/source-map consumers the ruling names.
240    pub fn doc(&self) -> Option<DocComment> {
241        support::child(&self.syntax)
242    }
243}
244
245/// A `flow`/`fn` declaration's body — either the prose-ground [`Block`]
246/// (`BLOCK`) or the code-ground [`StmtBlock`] (`STMT_BLOCK`), whichever the
247/// body-dialect selector on the opening brace chose (charter §4, RULED
248/// 2026-07-23: plain `{ }` = per-keyword default, `~{ }` = code-ground,
249/// `>{ }` = prose-ground — see `parser::decl::decl_body`). One enum rather
250/// than two separate `body()`/`code_body()` accessors, since a given
251/// declaration's body is always exactly one or the other, never both.
252#[derive(Debug, Clone, PartialEq, Eq, Hash)]
253pub enum Body {
254    /// Prose-ground: content lines, choices, diverts (`fn`'s non-default
255    /// spelling; `flow`'s default).
256    Prose(Block),
257    /// Code-ground: statements directly, no per-line `~` (`flow`'s
258    /// non-default spelling — the "Compound guard"; `fn`'s default).
259    Code(StmtBlock),
260}
261
262impl Body {
263    /// The underlying syntax node, whichever variant this is.
264    pub fn syntax(&self) -> &SyntaxNode {
265        match self {
266            Self::Prose(b) => b.syntax(),
267            Self::Code(b) => b.syntax(),
268        }
269    }
270
271    fn cast(node: SyntaxNode) -> Option<Self> {
272        match node.kind() {
273            SyntaxKind::BLOCK => Block::cast(node).map(Self::Prose),
274            SyntaxKind::STMT_BLOCK => StmtBlock::cast(node).map(Self::Code),
275            _ => None,
276        }
277    }
278}
279
280impl FlowDecl {
281    /// The declared name (the `IDENT` immediately after `flow`).
282    pub fn name_token(&self) -> Option<SyntaxToken> {
283        support::token(&self.syntax, IDENT)
284    }
285
286    pub fn param_list(&self) -> Option<ParamList> {
287        support::child(&self.syntax)
288    }
289
290    /// The body-dialect selector chose either a prose [`Block`] (the
291    /// `flow` default) or a code [`StmtBlock`] (the `~{ }` override, §3's
292    /// "Compound guard") — see [`Body`].
293    pub fn body(&self) -> Option<Body> {
294        self.syntax.children().find_map(Body::cast)
295    }
296
297    /// Nested `flow` declarations directly inside this one's body — a
298    /// stitch (charter §4: "stitches are nested `flow`s"). Only a
299    /// prose-ground body can contain one (a code-ground `STMT_BLOCK`'s
300    /// statement grammar has no declaration-dispatch arm — `parser/stmt.rs`
301    /// never produces a `FLOW_DECL` child), so a `~{ }`-bodied flow simply
302    /// yields none here, same as an empty body would.
303    pub fn stitches(&self) -> impl Iterator<Item = FlowDecl> {
304        match self.body() {
305            Some(Body::Prose(b)) => support::children::<FlowDecl>(&b.syntax).collect::<Vec<_>>(),
306            _ => Vec::new(),
307        }
308        .into_iter()
309    }
310
311    /// The header's `: type` return clause, if written (NG-C, #1489).
312    /// Declaring one is the ruled coroutine-vs-state toggle: a flow *with*
313    /// a return type must produce a value, and (unlike a plain flow) does
314    /// not pick up the implicit `-> DONE` on fall-through.
315    pub fn return_type(&self) -> Option<TypeAnnotation> {
316        support::child(&self.syntax)
317    }
318
319    /// The leading `///` doc comment, if one is attached (B0.6b).
320    pub fn doc(&self) -> Option<DocComment> {
321        support::child(&self.syntax)
322    }
323
324    /// `true` if a `pub` keyword precedes this header (issue #1582, RULED
325    /// 2026-08-03): opts the declaration into `VisibilityMark::Public`
326    /// (`hir::lower_native::container::lower_top_level_container`/
327    /// `lower_stitch` read this to populate `Knot`/`Stitch::visibility`).
328    /// Absent, the declaration stays `Private` — the already-ratified
329    /// 2026-07-23 default, unchanged by this accessor.
330    pub fn is_pub(&self) -> bool {
331        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
332    }
333}
334
335impl FnDecl {
336    pub fn name_token(&self) -> Option<SyntaxToken> {
337        support::token(&self.syntax, IDENT)
338    }
339
340    pub fn param_list(&self) -> Option<ParamList> {
341        support::child(&self.syntax)
342    }
343
344    /// The header's `: type` return clause, if written (NG-C, #1489) —
345    /// `fn probability(g: Guest): float { … }`.
346    pub fn return_type(&self) -> Option<TypeAnnotation> {
347        support::child(&self.syntax)
348    }
349
350    /// The body-dialect selector chose either a code [`StmtBlock`] (the
351    /// `fn` default) or a prose [`Block`] (the `>{ }` override) — see
352    /// [`Body`].
353    pub fn body(&self) -> Option<Body> {
354        self.syntax.children().find_map(Body::cast)
355    }
356
357    /// The leading `///` doc comment, if one is attached (B0.6b).
358    pub fn doc(&self) -> Option<DocComment> {
359        support::child(&self.syntax)
360    }
361
362    /// See [`FlowDecl::is_pub`].
363    pub fn is_pub(&self) -> bool {
364        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
365    }
366}
367
368impl ParamList {
369    pub fn params(&self) -> impl Iterator<Item = Param> {
370        support::children(&self.syntax)
371    }
372}
373
374impl Param {
375    pub fn name_token(&self) -> Option<SyntaxToken> {
376        support::token(&self.syntax, IDENT)
377    }
378
379    /// `true` if this parameter is `ref`-marked. Always `false` for a
380    /// lambda parameter — `ref` captures don't exist (RULED 2026-07-23) and
381    /// `parser/expr.rs::lambda_param` never accepts the keyword.
382    pub fn is_ref(&self) -> bool {
383        support::token(&self.syntax, SyntaxKind::KW_REF).is_some()
384    }
385
386    /// The parameter's `: type` annotation, if written (NG-A, #1487).
387    pub fn type_annotation(&self) -> Option<TypeAnnotation> {
388        support::child(&self.syntax)
389    }
390}
391
392impl Block {
393    /// Every direct-child node in this block's body, in source order —
394    /// the untyped escape hatch, since a `BLOCK`'s items span every
395    /// declaration/body-line kind this crate defines. Includes a leading
396    /// inner `DOC_COMMENT`, if present — callers that don't want it in
397    /// their item stream (e.g. `hir::lower_native::body::lower_block`)
398    /// filter it out themselves, same as they already skip other
399    /// non-statement node kinds.
400    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
401        self.syntax.children()
402    }
403
404    /// The inner `//!` doc comment, if this block opens with one (B0.6b):
405    /// documents the enclosing knot/flow/stitch, not a following
406    /// declaration. `None` for a `CHOICE_BODY`/`ELSE_BRANCH` — the parser
407    /// never attaches an inner doc there (`parser::block::braced_item_list`
408    /// only checks for one when building a real `BLOCK`).
409    pub fn doc(&self) -> Option<DocComment> {
410        support::child(&self.syntax)
411    }
412}
413
414impl VarDecl {
415    pub fn name_token(&self) -> Option<SyntaxToken> {
416        support::token(&self.syntax, IDENT)
417    }
418
419    /// The initializer expression's root node, if the `=` clause was
420    /// present. `var name = expr` always parses the initializer as exactly
421    /// one child node (whatever expression-grammar kind it is) after the
422    /// `IDENT`, so "the first child node that is neither the leading doc
423    /// comment nor the `: type` annotation" is unambiguous.
424    pub fn value(&self) -> Option<SyntaxNode> {
425        self.syntax
426            .children()
427            .find(|n| !is_binding_prefix(n.kind()))
428    }
429
430    /// The binding's `: type` annotation, if written (NG-B, #1488).
431    pub fn type_annotation(&self) -> Option<TypeAnnotation> {
432        support::child(&self.syntax)
433    }
434
435    /// The leading `///` doc comment, if one is attached (B0.6b).
436    pub fn doc(&self) -> Option<DocComment> {
437        support::child(&self.syntax)
438    }
439
440    /// See [`FlowDecl::is_pub`].
441    pub fn is_pub(&self) -> bool {
442        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
443    }
444}
445
446/// Child-node kinds that precede a binding's initializer and must never be
447/// mistaken for it: the leading `///` doc comment and the `: type`
448/// annotation (NG-B, #1488). Shared by [`VarDecl::value`],
449/// [`ConstDecl::value`] and [`LetStmt::value`] so a future prefix child
450/// only has to be listed once.
451fn is_binding_prefix(kind: SyntaxKind) -> bool {
452    matches!(kind, SyntaxKind::DOC_COMMENT | SyntaxKind::TYPE_ANNOTATION)
453}
454
455impl ConstDecl {
456    pub fn name_token(&self) -> Option<SyntaxToken> {
457        support::token(&self.syntax, IDENT)
458    }
459
460    /// See [`VarDecl::value`].
461    pub fn value(&self) -> Option<SyntaxNode> {
462        self.syntax
463            .children()
464            .find(|n| !is_binding_prefix(n.kind()))
465    }
466
467    /// See [`VarDecl::type_annotation`].
468    pub fn type_annotation(&self) -> Option<TypeAnnotation> {
469        support::child(&self.syntax)
470    }
471
472    /// The leading `///` doc comment, if one is attached (B0.6b).
473    pub fn doc(&self) -> Option<DocComment> {
474        support::child(&self.syntax)
475    }
476
477    /// See [`FlowDecl::is_pub`].
478    pub fn is_pub(&self) -> bool {
479        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
480    }
481}
482
483impl FlagsDecl {
484    pub fn name_token(&self) -> Option<SyntaxToken> {
485        support::token(&self.syntax, IDENT)
486    }
487
488    pub fn member_list(&self) -> Option<FlagsMemberList> {
489        support::child(&self.syntax)
490    }
491
492    /// The leading `///` doc comment, if one is attached (B0.6b).
493    pub fn doc(&self) -> Option<DocComment> {
494        support::child(&self.syntax)
495    }
496
497    /// See [`FlowDecl::is_pub`].
498    pub fn is_pub(&self) -> bool {
499        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
500    }
501}
502
503impl FlagsMemberList {
504    pub fn members(&self) -> impl Iterator<Item = FlagsMember> {
505        support::children(&self.syntax)
506    }
507}
508
509impl FlagsMember {
510    pub fn name_token(&self) -> Option<SyntaxToken> {
511        support::token(&self.syntax, IDENT)
512    }
513
514    /// `true` for a parenthesized member (`(name)`, the default-on entry).
515    pub fn is_active(&self) -> bool {
516        support::token(&self.syntax, L_PAREN).is_some()
517    }
518}
519
520impl StructDecl {
521    pub fn name_token(&self) -> Option<SyntaxToken> {
522        support::token(&self.syntax, IDENT)
523    }
524
525    pub fn fields(&self) -> impl Iterator<Item = StructField> {
526        support::children(&self.syntax)
527    }
528
529    /// The leading `///` doc comment, if one is attached (B0.6b).
530    pub fn doc(&self) -> Option<DocComment> {
531        support::child(&self.syntax)
532    }
533
534    /// See [`FlowDecl::is_pub`].
535    pub fn is_pub(&self) -> bool {
536        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
537    }
538}
539
540// ── The type-annotation grammar (NG-A/B/C, #1487/#1488/#1489) ────────
541//
542// Shapes mirror the brink dialect's own TM-2 AST (`brink-syntax`'s
543// `TypeAnnotation`/`TypeExpr`/`TypeName`/`TypeGeneric`/`TypeFn`) so both
544// frontends can lower to the same `brink_ir::hir::TypeExpr`.
545
546impl TypeAnnotation {
547    /// The annotated type expression after the `:`.
548    pub fn type_expr(&self) -> Option<TypeExpr> {
549        support::child(&self.syntax)
550    }
551}
552
553/// What a [`TypeExpr`] wraps — exactly one of these per node.
554pub enum TypeExprKind {
555    Name(TypeName),
556    Generic(TypeGeneric),
557    Fn(TypeFn),
558}
559
560impl TypeExpr {
561    /// The single child this type expression wraps.
562    ///
563    /// `None` only for a malformed or depth-limited `TYPE_EXPR` — every
564    /// well-formed one has exactly one of these.
565    pub fn kind(&self) -> Option<TypeExprKind> {
566        if let Some(n) = support::child::<TypeName>(&self.syntax) {
567            Some(TypeExprKind::Name(n))
568        } else if let Some(g) = support::child::<TypeGeneric>(&self.syntax) {
569            Some(TypeExprKind::Generic(g))
570        } else {
571            support::child::<TypeFn>(&self.syntax).map(TypeExprKind::Fn)
572        }
573    }
574}
575
576impl TypeName {
577    pub fn name_token(&self) -> Option<SyntaxToken> {
578        support::token(&self.syntax, IDENT)
579    }
580
581    /// The bare type name text (e.g. `"int"`, or an unrecognized name — the
582    /// grammar accepts any identifier; validity is a semantic check).
583    pub fn name(&self) -> Option<String> {
584        self.name_token().map(|t| t.text().to_string())
585    }
586}
587
588impl TypeGeneric {
589    pub fn name_token(&self) -> Option<SyntaxToken> {
590        support::token(&self.syntax, IDENT)
591    }
592
593    /// The generic head name (e.g. `"list"`, `"map"`).
594    pub fn name(&self) -> Option<String> {
595        self.name_token().map(|t| t.text().to_string())
596    }
597
598    /// The type arguments in source order (e.g. `[K, V]` for `Map<K, V>`).
599    pub fn args(&self) -> impl Iterator<Item = TypeExpr> {
600        support::children(&self.syntax)
601    }
602}
603
604impl TypeFn {
605    /// Every `TYPE_EXPR` child in source order: the last is the return
606    /// type, every earlier one is a parameter type.
607    fn type_exprs(&self) -> Vec<TypeExpr> {
608        support::children(&self.syntax).collect()
609    }
610
611    /// Parameter types, in declaration order.
612    pub fn params(&self) -> Vec<TypeExpr> {
613        let mut exprs = self.type_exprs();
614        exprs.pop(); // drop the return type (no-op when the list is empty)
615        exprs
616    }
617
618    /// The return type after `:`.
619    pub fn return_type(&self) -> Option<TypeExpr> {
620        self.type_exprs().pop()
621    }
622}
623
624impl StructField {
625    pub fn name_token(&self) -> Option<SyntaxToken> {
626        support::token(&self.syntax, IDENT)
627    }
628
629    /// The field's `: type` annotation (NG-E, issue #1505) — a full
630    /// `type_expr` (bare name, generic instantiation, or function type),
631    /// the same production every other `: type` position in this grammar
632    /// uses (`Param::type_annotation`, `VarDecl::type_annotation`, …).
633    pub fn type_annotation(&self) -> Option<TypeAnnotation> {
634        support::child(&self.syntax)
635    }
636}
637
638impl ExternDecl {
639    pub fn name_token(&self) -> Option<SyntaxToken> {
640        support::token(&self.syntax, IDENT)
641    }
642
643    pub fn param_list(&self) -> Option<ParamList> {
644        support::child(&self.syntax)
645    }
646
647    /// The leading `///` doc comment, if one is attached (B0.6b).
648    pub fn doc(&self) -> Option<DocComment> {
649        support::child(&self.syntax)
650    }
651
652    /// See [`FlowDecl::is_pub`].
653    pub fn is_pub(&self) -> bool {
654        support::token(&self.syntax, SyntaxKind::KW_PUB).is_some()
655    }
656}
657
658impl ImportDecl {
659    pub fn path(&self) -> Option<Path> {
660        support::child(&self.syntax)
661    }
662
663    /// The leading `///` doc comment, if one is attached (B0.6b). No native
664    /// HIR field consumes this yet (`Import` carries no `doc`, matching the
665    /// OLD ink frontend's own `Import` shape) — CST-only for now, same
666    /// status as [`SourceFile::doc`].
667    pub fn doc(&self) -> Option<DocComment> {
668        support::child(&self.syntax)
669    }
670}
671
672impl UseDecl {
673    pub fn tree(&self) -> Option<UseTree> {
674        support::child(&self.syntax)
675    }
676
677    /// See [`ImportDecl::doc`].
678    pub fn doc(&self) -> Option<DocComment> {
679        support::child(&self.syntax)
680    }
681}
682
683impl UseTree {
684    /// The leading dotted/`::`-separated path segments, in order —
685    /// `use_tree`'s grammar lays these out as bare `IDENT` tokens
686    /// interspersed with `::` directly inside `USE_TREE` (no nested `PATH`
687    /// node, unlike `import`'s path), so this walks direct-child tokens up
688    /// to (not including) an `as`-alias or a nested `{ … }` list.
689    pub fn path_segments(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
690        self.syntax
691            .children_with_tokens()
692            .filter_map(rowan::NodeOrToken::into_token)
693            .take_while(|t| t.kind() != SyntaxKind::KW_AS)
694            .filter(|t| t.kind() == IDENT)
695    }
696
697    /// The `as alias` name, if this tree ends in one.
698    pub fn alias_token(&self) -> Option<SyntaxToken> {
699        let mut saw_as = false;
700        for el in self.syntax.children_with_tokens() {
701            let Some(tok) = el.into_token() else {
702                continue;
703            };
704            if tok.kind().is_trivia() {
705                continue;
706            }
707            if saw_as {
708                return if tok.kind() == IDENT { Some(tok) } else { None };
709            }
710            if tok.kind() == SyntaxKind::KW_AS {
711                saw_as = true;
712            }
713        }
714        None
715    }
716
717    /// The nested `{ a, b as c, … }` group, if this tree has one.
718    pub fn nested_list(&self) -> Option<UseTreeList> {
719        support::child(&self.syntax)
720    }
721}
722
723impl UseTreeList {
724    pub fn trees(&self) -> impl Iterator<Item = UseTree> {
725        support::children(&self.syntax)
726    }
727}
728
729impl ModuleDecl {
730    pub fn name_token(&self) -> Option<SyntaxToken> {
731        support::token(&self.syntax, IDENT)
732    }
733
734    pub fn body(&self) -> Option<Block> {
735        support::child(&self.syntax)
736    }
737
738    /// See [`ImportDecl::doc`] — no native HIR "module container" node
739    /// exists yet either (`lower_native`'s module doc, judgment call #4).
740    pub fn doc(&self) -> Option<DocComment> {
741        support::child(&self.syntax)
742    }
743}
744
745impl Path {
746    /// Every `PATH_SEGMENT`'s `IDENT`, in order.
747    pub fn segments(&self) -> impl Iterator<Item = SyntaxToken> {
748        support::children::<PathSegment>(&self.syntax)
749            .filter_map(|seg| support::token(&seg.syntax, IDENT))
750    }
751
752    /// `true` if any separator is `::` (crosses a module wall, charter
753    /// §13.2) rather than only `.`.
754    pub fn crosses_module_wall(&self) -> bool {
755        support::tokens(&self.syntax, SyntaxKind::COLON_COLON)
756            .next()
757            .is_some()
758    }
759}
760
761impl ChoicePoint {
762    pub fn choices(&self) -> impl Iterator<Item = Choice> {
763        support::children(&self.syntax)
764    }
765
766    pub fn else_branch(&self) -> Option<ElseBranch> {
767        support::child(&self.syntax)
768    }
769}
770
771impl Choice {
772    /// `true` for a sticky (`+`) choice, `false` for a once-only (`*`) one.
773    ///
774    /// B0.7 fix (`docs/b0-sequencing.md` §B0.7, issue #1176): the bullet
775    /// token is wrapped in a nested `CHOICE_BULLET` node
776    /// (`choice.rs::choice`: `p.start_node(CHOICE_BULLET); p.bump(); …`),
777    /// never a direct token of `CHOICE` itself — `support::token` only
778    /// looks at direct children, so the original B0.5/B0.6 implementation
779    /// (`support::token(&self.syntax, PLUS)`) always returned `false`. Every
780    /// choice line in the corpus was silently lowering as once-only; caught
781    /// by B0.7's `choice_point_lowers_to_choice_set_with_sticky_and_once`
782    /// test, the first real exercise of a sticky (`+`) choice line.
783    pub fn is_sticky(&self) -> bool {
784        support::child::<ChoiceBullet>(&self.syntax)
785            .is_some_and(|b| support::token(&b.syntax, SyntaxKind::PLUS).is_some())
786    }
787
788    pub fn label(&self) -> Option<Label> {
789        support::child(&self.syntax)
790    }
791
792    pub fn guard(&self) -> Option<ChoiceGuard> {
793        support::child(&self.syntax)
794    }
795
796    pub fn body(&self) -> Option<ChoiceBody> {
797        support::child(&self.syntax)
798    }
799}
800
801impl AnnotationLine {
802    /// The directive/annotation name (`effects`, …).
803    pub fn name_token(&self) -> Option<SyntaxToken> {
804        support::token(&self.syntax, IDENT)
805    }
806
807    pub fn args(&self) -> Option<AnnotationArgs> {
808        support::child(&self.syntax)
809    }
810}
811
812impl AnnotationArgs {
813    pub fn args(&self) -> impl Iterator<Item = AnnotationArg> {
814        support::children(&self.syntax)
815    }
816}
817
818impl AnnotationArg {
819    pub fn name_token(&self) -> Option<SyntaxToken> {
820        support::token(&self.syntax, IDENT)
821    }
822
823    /// The nested paren-clause (`reads(gold, hp)` inside `effects(…)`), if
824    /// this argument has one.
825    pub fn nested_args(&self) -> Option<AnnotationArgs> {
826        support::child(&self.syntax)
827    }
828
829    /// The unquoted `::`-separated module `PATH` (`story::old::path`), if
830    /// this argument is one (issue #1349, `@[was(story::old::path)]`'s
831    /// arg form). `name_token` is `None` for this shape — the arg's direct
832    /// child is a `PATH` node, not a bare `IDENT` token.
833    pub fn path(&self) -> Option<Path> {
834        support::child(&self.syntax)
835    }
836
837    /// The `= "value"` clause's string-literal value (issue #1719,
838    /// `@[element(args = "…")]` / `@[style(chan = "…")]`), if this argument
839    /// has one. `None` for the bare-`IDENT`, nested-clause, path, and
840    /// numeric-literal shapes — only the key/value form parses a
841    /// `STRING_LIT` as a **sibling** of the key `IDENT` rather than the
842    /// arg's sole child.
843    pub fn eq_value(&self) -> Option<StringLit> {
844        support::child(&self.syntax)
845    }
846
847    /// The `= <integer>` clause's integer-literal value (issue #2164,
848    /// `@[convention(…, order = 30)]` — the ordering key is a bare integer,
849    /// RULED). `None` for every other clause shape, including the string
850    /// key/value form [`Self::eq_value`] reads — a clause carries at most
851    /// one of the two, never both.
852    pub fn eq_int_value(&self) -> Option<IntegerLit> {
853        support::child(&self.syntax)
854    }
855
856    /// The `= <ident>` clause's bare-identifier value (issue #2178,
857    /// `@[convention(…, attach = Cue)]` — the attached schema names a
858    /// declared `struct`, so this clause is a bare identifier, not a
859    /// quoted string or integer literal). `None` for every other clause
860    /// shape.
861    ///
862    /// Unlike [`Self::eq_value`]/[`Self::eq_int_value`], this is not a
863    /// distinct child *node* kind — the value is bumped as a second bare
864    /// `IDENT` token, a sibling of the key's own `IDENT` token (see
865    /// `parser::annotation::annotation_arg`'s `IDENT` arm). So this reads
866    /// the **second** direct-child `IDENT` token, not the first (which
867    /// [`Self::name_token`] already owns).
868    pub fn eq_ident_value(&self) -> Option<SyntaxToken> {
869        support::tokens(&self.syntax, IDENT).nth(1)
870    }
871}
872
873impl CallExpr {
874    /// The callee path. Fixed for B0.6 (`docs/b0-sequencing.md` §B0.6):
875    /// `expr::path_or_call` wraps a bare `PATH` node directly (not a nested
876    /// `PATH_EXPR`) when it commits to `CALL_EXPR` — the previous
877    /// `Option<PathExpr>` signature could never cast successfully against
878    /// the real grammar shape and always returned `None` for every call
879    /// expression. No test exercised it before B0.6's lowering needed it.
880    pub fn callee(&self) -> Option<Path> {
881        support::child(&self.syntax)
882    }
883
884    pub fn arg_list(&self) -> Option<ArgList> {
885        support::child(&self.syntax)
886    }
887}
888
889impl PathExpr {
890    pub fn path(&self) -> Option<Path> {
891        support::child(&self.syntax)
892    }
893}
894
895impl ArrayLiteral {
896    /// The literal's element expressions, in source order. Empty for `[]`.
897    /// Raw `SyntaxNode` children, same idiom `CallExpr::arg_list`'s callers
898    /// use for `ARG_LIST` — this crate has no `ast::Expr` union type to cast
899    /// into (unlike `brink-syntax`'s own `ArrayLiteral::elements`).
900    pub fn elements(&self) -> impl Iterator<Item = SyntaxNode> {
901        self.syntax.children()
902    }
903}
904
905impl ConstructLiteral {
906    /// The constructed type's name path — `Map`, `Flags`, `Weighted`, a
907    /// declared struct's name, or a `::`-qualified spelling of any of them.
908    /// Which of those it *is* is dispatch, not grammar: `brink-ir`'s
909    /// `construct` registry resolves it (`docs/stdlib-spec.md` §9.6).
910    pub fn type_path(&self) -> Option<Path> {
911        support::child(&self.syntax)
912    }
913
914    /// The literal's entries, in source order. Empty for `TypeName { }`.
915    pub fn entries(&self) -> impl Iterator<Item = ConstructEntry> {
916        support::children(&self.syntax)
917    }
918}
919
920impl ConstructEntry {
921    /// `true` for the pair/field form (`k: v`), `false` for the element
922    /// form (`v`) — read off the `COLON` token the parser emits between the
923    /// two expressions, so it never depends on child-count guessing.
924    pub fn is_pair(&self) -> bool {
925        support::token(&self.syntax, crate::SyntaxKind::COLON).is_some()
926    }
927
928    /// The left-hand expression of a pair/field entry (`k` in `k: v`), or
929    /// `None` for the element form.
930    pub fn key(&self) -> Option<SyntaxNode> {
931        if self.is_pair() {
932            self.syntax.children().next()
933        } else {
934            None
935        }
936    }
937
938    /// The entry's value expression: the right-hand side of a pair/field
939    /// entry, or the single expression of an element entry.
940    pub fn value(&self) -> Option<SyntaxNode> {
941        let mut children = self.syntax.children();
942        let first = children.next();
943        if self.is_pair() {
944            children.next()
945        } else {
946            first
947        }
948    }
949}
950
951impl IntegerLit {
952    pub fn value_token(&self) -> Option<SyntaxToken> {
953        support::token(&self.syntax, crate::SyntaxKind::INTEGER)
954    }
955
956    pub fn value(&self) -> Option<i64> {
957        self.value_token()
958            .and_then(|t| t.text().parse::<i64>().ok())
959    }
960}
961
962impl FloatLit {
963    pub fn value_token(&self) -> Option<SyntaxToken> {
964        support::token(&self.syntax, crate::SyntaxKind::FLOAT)
965    }
966
967    pub fn value(&self) -> Option<f64> {
968        self.value_token()
969            .and_then(|t| t.text().parse::<f64>().ok())
970    }
971}
972
973impl BooleanLit {
974    pub fn value(&self) -> Option<bool> {
975        let tok = self
976            .syntax
977            .children_with_tokens()
978            .filter_map(rowan::NodeOrToken::into_token)
979            .find(|t| matches!(t.kind(), SyntaxKind::KW_TRUE | SyntaxKind::KW_FALSE))?;
980        match tok.kind() {
981            SyntaxKind::KW_TRUE => Some(true),
982            SyntaxKind::KW_FALSE => Some(false),
983            _ => None,
984        }
985    }
986}
987
988impl ParenExpr {
989    /// The parenthesized inner expression's root node.
990    pub fn inner(&self) -> Option<SyntaxNode> {
991        self.syntax.children().next()
992    }
993}
994
995impl LambdaExpr {
996    /// The `|…|` parameter row (issue #1685). `None` only for a malformed
997    /// node — `parser/expr.rs::lambda_expr` always opens with one, even for
998    /// the zero-arg `||` form (whose row is simply empty).
999    pub fn params(&self) -> Option<LambdaParams> {
1000        support::child(&self.syntax)
1001    }
1002
1003    /// The `: type` **return** annotation (`|g|: bool { … }`), if written.
1004    ///
1005    /// Unambiguous as a direct child: a *parameter's* own annotation lives
1006    /// inside that parameter's `PARAM` node, itself inside `LAMBDA_PARAMS`,
1007    /// so the only `TYPE_ANNOTATION` directly under `LAMBDA_EXPR` is the
1008    /// return one.
1009    pub fn return_annotation(&self) -> Option<TypeAnnotation> {
1010        support::child(&self.syntax)
1011    }
1012
1013    /// The body's root node — the single expression (`|g| g.awake`) or the
1014    /// braced `STMT_BLOCK` (`|g|: bool { … }`). The last child, since the
1015    /// parser emits params, then the optional return annotation, then the
1016    /// body.
1017    pub fn body(&self) -> Option<SyntaxNode> {
1018        self.syntax
1019            .children()
1020            .filter(|n| {
1021                !matches!(
1022                    n.kind(),
1023                    SyntaxKind::LAMBDA_PARAMS | SyntaxKind::TYPE_ANNOTATION
1024                )
1025            })
1026            .last()
1027    }
1028}
1029
1030impl LambdaParams {
1031    /// The parameters, in source order — the same `PARAM` node the
1032    /// declaration grammar uses, so `Param`'s accessors read a lambda
1033    /// parameter exactly as they read a `fn` one. Empty for `||`.
1034    pub fn params(&self) -> impl Iterator<Item = Param> {
1035        support::children(&self.syntax)
1036    }
1037}
1038
1039impl PrefixExpr {
1040    /// The prefix operator token (`-` or `!`).
1041    pub fn op_token(&self) -> Option<SyntaxToken> {
1042        self.syntax
1043            .children_with_tokens()
1044            .filter_map(rowan::NodeOrToken::into_token)
1045            .find(|t| matches!(t.kind(), SyntaxKind::MINUS | SyntaxKind::BANG))
1046    }
1047
1048    /// The operand's root node.
1049    pub fn operand(&self) -> Option<SyntaxNode> {
1050        self.syntax.children().next()
1051    }
1052}
1053
1054impl InfixExpr {
1055    /// The left-hand operand's root node (the first child node).
1056    pub fn lhs(&self) -> Option<SyntaxNode> {
1057        self.syntax.children().next()
1058    }
1059
1060    /// The right-hand operand's root node (the last child node).
1061    pub fn rhs(&self) -> Option<SyntaxNode> {
1062        self.syntax.children().last()
1063    }
1064
1065    /// The operator token. Two adjacent `PIPE`s (`||`) are represented as
1066    /// two tokens (see `expr::infix_binding_power`'s doc) — this returns
1067    /// the *first* one; callers that need to distinguish `|` from `||`
1068    /// check [`Self::is_double_pipe`].
1069    pub fn op_token(&self) -> Option<SyntaxToken> {
1070        self.syntax
1071            .children_with_tokens()
1072            .filter_map(rowan::NodeOrToken::into_token)
1073            .find(|t| {
1074                matches!(
1075                    t.kind(),
1076                    SyntaxKind::AMP_AMP
1077                        | SyntaxKind::EQ_EQ
1078                        | SyntaxKind::BANG_EQ
1079                        | SyntaxKind::LT
1080                        | SyntaxKind::GT
1081                        | SyntaxKind::LT_EQ
1082                        | SyntaxKind::GT_EQ
1083                        | SyntaxKind::PLUS
1084                        | SyntaxKind::MINUS
1085                        | SyntaxKind::STAR
1086                        | SyntaxKind::SLASH
1087                        | SyntaxKind::PERCENT
1088                        | SyntaxKind::PIPE
1089                        | SyntaxKind::KW_OR
1090                )
1091            })
1092    }
1093
1094    /// `true` if the operator is `||` (two adjacent `PIPE` tokens) rather
1095    /// than a single-token operator.
1096    pub fn is_double_pipe(&self) -> bool {
1097        let mut pipes = self
1098            .syntax
1099            .children_with_tokens()
1100            .filter_map(rowan::NodeOrToken::into_token)
1101            .filter(|t| t.kind() == SyntaxKind::PIPE);
1102        pipes.next().is_some() && pipes.next().is_some()
1103    }
1104}
1105
1106impl ArgList {
1107    /// `true` if this arg list opens with `(` at all (always true for a
1108    /// well-formed parse — exposed for error-recovery callers that walk a
1109    /// possibly-malformed tree).
1110    pub fn is_open(&self) -> bool {
1111        support::token(&self.syntax, L_PAREN).is_some()
1112    }
1113}
1114
1115impl DivertTarget {
1116    pub fn is_end(&self) -> bool {
1117        support::token(&self.syntax, SyntaxKind::KW_END).is_some()
1118    }
1119
1120    pub fn is_done(&self) -> bool {
1121        support::token(&self.syntax, SyntaxKind::KW_DONE).is_some()
1122    }
1123
1124    pub fn path(&self) -> Option<Path> {
1125        support::child(&self.syntax)
1126    }
1127
1128    /// The `(args)` in `-> knot(args)`, if any (charter §11: diverts keep
1129    /// call-style args verbatim from ink). A direct `ARG_LIST` sibling of
1130    /// `path()` under `DIVERT_TARGET`, not wrapped in a `CALL_EXPR` — a
1131    /// divert target is not an expression.
1132    pub fn call_args(&self) -> Option<ArgList> {
1133        support::child(&self.syntax)
1134    }
1135}
1136
1137// ── B0.7 additions: body-dialect accessors ──────────────────────────
1138//
1139// Everything below was added for `hir::lower_native`'s body lowering
1140// (`docs/b0-sequencing.md` §B0.7). B0.5/B0.6 hand-wrote a representative
1141// subset of accessors "enough to prove the pattern end-to-end ... not
1142// pre-building every accessor a later lowering pass might want" (this
1143// file's module doc) — B0.7 is exactly that later pass.
1144
1145impl ContentLine {
1146    /// The leading `(name)` label, if this line opens with one (G-1).
1147    pub fn label(&self) -> Option<Label> {
1148        support::child(&self.syntax)
1149    }
1150}
1151
1152impl LogicLine {
1153    /// The wrapped `~ let name = expr`, when this logic line is a temp
1154    /// declaration (`parser/stmt.rs::logic_line`'s `KW_LET` branch, issue
1155    /// #1972).
1156    pub fn let_stmt(&self) -> Option<LetStmt> {
1157        support::child(&self.syntax)
1158    }
1159
1160    /// The wrapped `~ x = expr` / `~ x += expr`, when this logic line is an
1161    /// assignment (`parser/stmt.rs::logic_line`'s `at_assignment` branch).
1162    pub fn assign_stmt(&self) -> Option<AssignStmt> {
1163        support::child(&self.syntax)
1164    }
1165
1166    /// The wrapped `~ expr` — an expression evaluated for its side effect
1167    /// (e.g. a function call) — when this logic line is neither a temp
1168    /// declaration nor an assignment.
1169    pub fn expr_stmt(&self) -> Option<ExprStmt> {
1170        support::child(&self.syntax)
1171    }
1172
1173    /// The wrapped `~ until cond`, when this logic line is a condition-park
1174    /// escape — native's sole `await` spelling (issue #1972,
1175    /// `parser/stmt.rs::logic_line`'s `KW_UNTIL` branch).
1176    pub fn until_stmt(&self) -> Option<UntilStmt> {
1177        support::child(&self.syntax)
1178    }
1179
1180    /// The wrapped `~{ … }` multi-statement logic block, when this logic
1181    /// line is a T1b-style block escape (issue #1972,
1182    /// `parser/stmt.rs::logic_line`'s `L_BRACE` branch). Reuses
1183    /// [`StmtBlock`]'s grammar unmodified — the same node kind a `fn`'s
1184    /// default body or a `flow`'s whole-body `~{ }` override use.
1185    pub fn stmt_block(&self) -> Option<StmtBlock> {
1186        support::child(&self.syntax)
1187    }
1188}
1189
1190impl ProseLine {
1191    /// The wrapped `> text` content line — the mirror image of
1192    /// [`LogicLine`]'s own wrapped children (`parser/stmt.rs::prose_line`),
1193    /// reusing [`ContentLine`]'s grammar unmodified.
1194    pub fn content_line(&self) -> Option<ContentLine> {
1195        support::child(&self.syntax)
1196    }
1197}
1198
1199impl Label {
1200    pub fn name_token(&self) -> Option<SyntaxToken> {
1201        support::token(&self.syntax, IDENT)
1202    }
1203}
1204
1205impl DivertStmt {
1206    pub fn target(&self) -> Option<DivertTarget> {
1207        support::child(&self.syntax)
1208    }
1209}
1210
1211impl TunnelCall {
1212    /// The one divert target between the opening and closing `->` (native's
1213    /// `-> place ->` shape carries exactly one target, unlike ink's chained
1214    /// `-> a -> b ->`).
1215    pub fn target(&self) -> Option<DivertTarget> {
1216        support::child(&self.syntax)
1217    }
1218}
1219
1220impl ReturnRedirect {
1221    pub fn target(&self) -> Option<DivertTarget> {
1222        support::child(&self.syntax)
1223    }
1224}
1225
1226impl ReturnStmt {
1227    /// The value expression, if any — `RETURN_STMT`'s only child node.
1228    /// `Some`/`None` for both grammars now: the content-ground bare
1229    /// `return`/`return <expr>`/`return -> x` (`parser/divert.rs::
1230    /// return_stmt` — the value is optional, and `-> x` is a distinct
1231    /// `RETURN_REDIRECT` node, never this accessor's concern; issue #1973
1232    /// added the value case, previously always `None` here) and the
1233    /// code-ground `return e?;` (B0.8 Wave B tail, issue #1322,
1234    /// `parser/stmt.rs::return_stmt` — the initializer was already
1235    /// optional there). See `syntax_kind.rs`'s `RETURN_STMT` doc for why
1236    /// one node shape serves both grammars.
1237    pub fn value(&self) -> Option<SyntaxNode> {
1238        self.syntax.children().next()
1239    }
1240}
1241
1242impl Choice {
1243    pub fn start_content(&self) -> Option<ChoiceStartContent> {
1244        support::child(&self.syntax)
1245    }
1246
1247    pub fn bracket_content(&self) -> Option<ChoiceBracketContent> {
1248        support::child(&self.syntax)
1249    }
1250
1251    pub fn inner_content(&self) -> Option<ChoiceInnerContent> {
1252        support::child(&self.syntax)
1253    }
1254}
1255
1256impl ChoiceGuard {
1257    /// The guard's condition expression — `CHOICE_GUARD`'s first child node
1258    /// (`L_BRACE KW_IF expression (AS_BINDING)? R_BRACE`; the braces/keyword
1259    /// are tokens).
1260    pub fn expr(&self) -> Option<SyntaxNode> {
1261        self.syntax
1262            .children()
1263            .find(|n| n.kind() != SyntaxKind::AS_BINDING)
1264    }
1265
1266    /// The `as NAME` binding, when the author wrote one. Implemented
1267    /// (issue #1508): `brink-ir`'s choice lowering (`hir::Choice::binding`)
1268    /// captures at presentation time via the same `OptionBind`
1269    /// frame-slot machinery the statement/template forms already use
1270    /// (`parser/choice.rs::choice_guard`'s doc). `E146` is retired.
1271    pub fn as_binding(&self) -> Option<AsBinding> {
1272        support::child(&self.syntax)
1273    }
1274}
1275
1276impl ChoiceBody {
1277    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
1278        self.syntax.children()
1279    }
1280}
1281
1282impl ElseBranch {
1283    /// The nested `CHOICE_BODY`, when this `else` belongs to a choice point
1284    /// (`choice.rs::else_branch` — always braced, no colon form).
1285    pub fn choice_body(&self) -> Option<ChoiceBody> {
1286        support::child(&self.syntax)
1287    }
1288
1289    /// The nested `BLOCK`, when this `else` belongs to the conditional
1290    /// family's braced-arm form (`{if cond {…} else {…}}`).
1291    pub fn block(&self) -> Option<Block> {
1292        support::child(&self.syntax)
1293    }
1294
1295    /// Every direct-child item, for the conditional family's colon-body
1296    /// form (`{if cond: … else: …}`), where body items are direct children
1297    /// with no wrapper node (`family.rs::colon_body`).
1298    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
1299        self.syntax.children()
1300    }
1301}
1302
1303impl Splice {
1304    pub fn path(&self) -> Option<Path> {
1305        support::child(&self.syntax)
1306    }
1307
1308    pub fn arg_list(&self) -> Option<ArgList> {
1309        support::child(&self.syntax)
1310    }
1311}
1312
1313impl ConditionalBlock {
1314    pub fn is_if(&self) -> bool {
1315        support::token(&self.syntax, SyntaxKind::KW_IF).is_some()
1316    }
1317
1318    pub fn is_match(&self) -> bool {
1319        support::token(&self.syntax, SyntaxKind::KW_MATCH).is_some()
1320    }
1321
1322    /// The head expression: the `if` condition or the `match` subject —
1323    /// `CONDITIONAL_BLOCK`'s only child node that isn't an arm/else
1324    /// (`family.rs::conditional_block`: the expression is parsed directly
1325    /// into this node before the arm(s)).
1326    pub fn condition(&self) -> Option<SyntaxNode> {
1327        self.syntax.children().find(|n| {
1328            !matches!(
1329                n.kind(),
1330                SyntaxKind::IF_ARM
1331                    | SyntaxKind::ELSE_BRANCH
1332                    | SyntaxKind::MATCH_ARM
1333                    | SyntaxKind::AS_BINDING
1334            )
1335        })
1336    }
1337
1338    /// The `as NAME` binding (B1b, issue #1475) of the template condition
1339    /// form `{if EXPR as NAME: … else: …}`, when present. Never set for
1340    /// `match` — a `match` head is a subject, not a condition.
1341    pub fn as_binding(&self) -> Option<AsBinding> {
1342        support::child(&self.syntax)
1343    }
1344
1345    pub fn if_arm(&self) -> Option<IfArm> {
1346        support::child(&self.syntax)
1347    }
1348
1349    pub fn else_arm(&self) -> Option<ElseBranch> {
1350        support::child(&self.syntax)
1351    }
1352
1353    /// `match`'s arms — direct children of `CONDITIONAL_BLOCK` itself
1354    /// (`family.rs::match_arm_list` opens no wrapper node of its own).
1355    pub fn match_arms(&self) -> impl Iterator<Item = MatchArm> {
1356        support::children(&self.syntax)
1357    }
1358}
1359
1360impl IfArm {
1361    /// The nested `BLOCK`, for the braced-arm form.
1362    pub fn block(&self) -> Option<Block> {
1363        support::child(&self.syntax)
1364    }
1365
1366    /// Direct-child items, for the colon-body form (see `ElseBranch::items`).
1367    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
1368        self.syntax.children()
1369    }
1370}
1371
1372impl MatchArm {
1373    /// The pattern's expression — `MATCH_PATTERN`'s only child node.
1374    pub fn pattern_expr(&self) -> Option<SyntaxNode> {
1375        support::child::<MatchPattern>(&self.syntax).and_then(|p| p.syntax.children().next())
1376    }
1377
1378    /// The nested `BLOCK`, for a braced arm body (`pattern => { … }`).
1379    pub fn block(&self) -> Option<Block> {
1380        support::child(&self.syntax)
1381    }
1382
1383    /// The bare expression, for an unbraced arm body (`pattern => expr`) —
1384    /// the one child node that is neither `MATCH_PATTERN` nor `BLOCK`.
1385    pub fn bare_expr(&self) -> Option<SyntaxNode> {
1386        self.syntax
1387            .children()
1388            .find(|n| !matches!(n.kind(), SyntaxKind::MATCH_PATTERN | SyntaxKind::BLOCK))
1389    }
1390}
1391
1392impl AlternationBlock {
1393    /// The `~`/`&`/`!`/`|` marker token.
1394    pub fn marker_token(&self) -> Option<SyntaxToken> {
1395        support::child::<AlternationMarker>(&self.syntax).and_then(|m| {
1396            m.syntax
1397                .children_with_tokens()
1398                .filter_map(rowan::NodeOrToken::into_token)
1399                .find(|t| {
1400                    matches!(
1401                        t.kind(),
1402                        SyntaxKind::TILDE | SyntaxKind::AMP | SyntaxKind::BANG | SyntaxKind::PIPE
1403                    )
1404                })
1405        })
1406    }
1407
1408    /// The multiline `-`-prefixed entries, if this block used that form
1409    /// (`family.rs::multiline_entries`). Empty for the single-line
1410    /// pipe-separated form — see [`Self::syntax`] for the raw child walk
1411    /// callers need for that form instead (no per-alternative wrapper node
1412    /// exists for it, `family.rs::inline_alternatives`).
1413    pub fn entries(&self) -> impl Iterator<Item = Entry> {
1414        support::children(&self.syntax)
1415    }
1416}
1417
1418impl Entry {
1419    /// Every direct-child item inside this `-`-prefixed entry (the leading
1420    /// `MINUS` is a token, filtered out by `.children()`).
1421    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
1422        self.syntax.children()
1423    }
1424}
1425
1426impl TagLine {
1427    pub fn tags(&self) -> impl Iterator<Item = Tag> {
1428        support::children(&self.syntax)
1429    }
1430}
1431
1432impl Tag {
1433    /// The tag's own text: the leading `#` sigil dropped, surrounding
1434    /// source whitespace trimmed, and a *recognized* inline escape's
1435    /// backslash stripped (§8d.6, issue #2045) — parity with
1436    /// `markup::escape`'s stripping in ordinary content. `tag()`'s raw
1437    /// free-text scan (`parser::content::tag`) never builds an `ESCAPE`
1438    /// sub-node the way the shared content engine does, so there is no
1439    /// node-level place to skip the backslash the way `push_escape` does
1440    /// downstream; this accessor is that place instead — the single
1441    /// materialization point every consumer of "the tag's text" should go
1442    /// through, mirroring [`SceneTitle::text`]/[`CueName::text`].
1443    pub fn text(&self) -> String {
1444        let mut skipped_leading_hash = false;
1445        let mut raw = String::new();
1446        for tok in self
1447            .syntax
1448            .children_with_tokens()
1449            .filter_map(rowan::NodeOrToken::into_token)
1450        {
1451            if !skipped_leading_hash && tok.kind() == HASH {
1452                skipped_leading_hash = true;
1453                continue;
1454            }
1455            raw.push_str(tok.text());
1456        }
1457        strip_recognized_escape_backslashes(raw.trim())
1458    }
1459}
1460
1461/// Strip the backslash from a *recognized* inline escape (`\< \{ \# \\`,
1462/// §8d.6 — the set is final) inside already-assembled raw text, achieving
1463/// parity with `markup::escape`'s stripping behavior for ordinary content
1464/// (issue #2045). `tag()`/`cue_name()`/`scene_title()` are raw free-text
1465/// scanners with no `ESCAPE` sub-node to strip at build time (unlike the
1466/// shared content engine); this is the one shared place their `text()`
1467/// accessors funnel through instead, so the three stay self-consistent
1468/// rather than drifting into three near-identical hand-rolled copies.
1469///
1470/// This is the *same* greedy left-to-right consumption `markup::escape`
1471/// itself performs: scanning forward, a lone `\` immediately followed by
1472/// one of `< { # \` consumes both and emits the escaped char literally;
1473/// any other `\` (followed by something else, or by nothing) is emitted
1474/// as itself and only that one character is consumed before continuing.
1475/// This is provably identical to the parser's own odd/even run-parity
1476/// reading these scanners use for structural purposes (#1852/#1738: `N`
1477/// consecutive backslashes before `<`/`{`/`#` only escape it when `N` is
1478/// odd, because greedily consuming pairs left-to-right leaves exactly one
1479/// unpaired backslash when `N` is odd and none when `N` is even) — so no
1480/// parser change is needed here, and no structural test moves. Unlike the
1481/// prior run-parity-only reading, this also collapses a bare `\\` pair
1482/// with nothing recognized following it (`a\\b` -> `a\b`), because that is
1483/// exactly what `markup::escape`'s greedy consumption does too: the first
1484/// backslash of the pair escapes the second, regardless of what follows.
1485fn strip_recognized_escape_backslashes(text: &str) -> String {
1486    let chars: Vec<char> = text.chars().collect();
1487    let mut out = String::with_capacity(text.len());
1488    let mut i = 0;
1489    while i < chars.len() {
1490        if chars[i] == '\\' && matches!(chars.get(i + 1), Some('<' | '{' | '#' | '\\')) {
1491            let escaped = chars[i + 1];
1492            out.push(escaped);
1493            i += 2;
1494        } else {
1495            out.push(chars[i]);
1496            i += 1;
1497        }
1498    }
1499    out
1500}
1501
1502// ── B0.8 Wave A additions: the code-ground statement layer ──────────
1503//
1504// `docs/decision-log.md` 2026-07-23 "Code-ground sitting" — parser only,
1505// no lowering yet (`parser/stmt.rs`'s module doc).
1506
1507impl StmtBlock {
1508    /// Every direct-child statement (`LET_STMT`/`ASSIGN_STMT`/`EXPR_STMT`)
1509    /// AND the tail expression if present, in source order — the untyped
1510    /// escape hatch, same shape as [`Block::items`].
1511    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
1512        self.syntax.children()
1513    }
1514
1515    /// The block's unterminated trailing expression (blocks-as-values), if
1516    /// one is present — the last child, when its kind is none of the
1517    /// statement-wrapper kinds this grammar can produce as a genuine
1518    /// statement (`parser/stmt.rs::stmt_block` never emits any node after
1519    /// the tail, so the last child is *always* the tail when it isn't one
1520    /// of those). B0.8 Wave B's four control-flow kinds
1521    /// (`IF_STMT`/`WHILE_STMT`/`FOR_STMT`/`UNTIL_STMT`) never produce a
1522    /// value and are always fully delimited by their own body/`;` — same
1523    /// non-tail treatment as the three Wave A kinds, not new behavior.
1524    /// B0.8 Wave B tail (issue #1322) adds `RETURN_STMT`/`BREAK_STMT`/
1525    /// `CONTINUE_STMT` to this same non-tail set — all three are always
1526    /// `;`-terminated in code-ground position (`parser/stmt.rs`'s
1527    /// `return_stmt`/`break_stmt`/`continue_stmt`), never a bare tail
1528    /// value. The `> text` prose-line escape (issue #1992) adds
1529    /// `PROSE_LINE` to the set for the same reason: it never produces a
1530    /// value (`parser/stmt.rs::statement`'s doc), so a `STMT_BLOCK` ending
1531    /// in one has no tail expression, not a prose line masquerading as one
1532    /// (review finding F2 — `lower_native/lambda.rs`'s `block.tail()` call
1533    /// would otherwise lower it as a lambda's return value instead of
1534    /// reaching `lower_block_item`'s loud `E129` arm).
1535    pub fn tail(&self) -> Option<SyntaxNode> {
1536        let last = self.syntax.children().last()?;
1537        (!matches!(
1538            last.kind(),
1539            SyntaxKind::LET_STMT
1540                | SyntaxKind::ASSIGN_STMT
1541                | SyntaxKind::EXPR_STMT
1542                | SyntaxKind::IF_STMT
1543                | SyntaxKind::WHILE_STMT
1544                | SyntaxKind::FOR_STMT
1545                | SyntaxKind::UNTIL_STMT
1546                | SyntaxKind::RETURN_STMT
1547                | SyntaxKind::BREAK_STMT
1548                | SyntaxKind::CONTINUE_STMT
1549                | SyntaxKind::PROSE_LINE
1550        ))
1551        .then_some(last)
1552    }
1553}
1554
1555impl LetStmt {
1556    pub fn name_token(&self) -> Option<SyntaxToken> {
1557        support::token(&self.syntax, IDENT)
1558    }
1559
1560    /// The initializer expression's root node, if the `=` clause was
1561    /// present (optional, see `parser/stmt.rs::let_stmt`'s doc comment).
1562    pub fn value(&self) -> Option<SyntaxNode> {
1563        self.syntax
1564            .children()
1565            .find(|n| !is_binding_prefix(n.kind()))
1566    }
1567
1568    /// The binding's `: type` annotation, if written (NG-B, #1488).
1569    pub fn type_annotation(&self) -> Option<TypeAnnotation> {
1570        support::child(&self.syntax)
1571    }
1572}
1573
1574impl AssignStmt {
1575    /// The assignment's place path (`x` / `x.field`).
1576    pub fn place(&self) -> Option<Path> {
1577        support::child(&self.syntax)
1578    }
1579
1580    /// The right-hand-side expression's root node (the last child node,
1581    /// same convention as [`InfixExpr::rhs`]).
1582    pub fn value(&self) -> Option<SyntaxNode> {
1583        self.syntax.children().last()
1584    }
1585
1586    /// The assignment operator token (`=`, `+=`, or `-=` — B0.8 Wave B
1587    /// tail, issue #1322, decision-log 2026-07-23 "Code-ground sitting":
1588    /// "compound/RMW assignment"). Mirrors the brink-dialect's own
1589    /// `Assignment::op_token` (`brink-syntax`) exactly, including which
1590    /// operators exist — `AssignOp` (`brink-ir`) only has `Set`/`Add`/`Sub`,
1591    /// so `*=`/`/=` (lexed as `STAR_EQ`/`SLASH_EQ` but never produced by
1592    /// `parser/stmt.rs::assign_stmt`) have no lowering target and aren't
1593    /// looked for here either.
1594    pub fn op_token(&self) -> Option<SyntaxToken> {
1595        self.syntax
1596            .children_with_tokens()
1597            .filter_map(rowan::NodeOrToken::into_token)
1598            .find(|tok| {
1599                matches!(
1600                    tok.kind(),
1601                    SyntaxKind::EQ | SyntaxKind::PLUS_EQ | SyntaxKind::MINUS_EQ
1602                )
1603            })
1604    }
1605}
1606
1607impl ExprStmt {
1608    /// The wrapped expression's root node.
1609    pub fn expr(&self) -> Option<SyntaxNode> {
1610        self.syntax.children().next()
1611    }
1612}
1613
1614// ── B0.8 Wave B additions: the code-ground control-flow layer ───────
1615//
1616// `docs/decision-log.md` 2026-07-23 "Code-ground sitting", issue #1177.
1617// `parser/control_flow.rs`'s module doc has the full grammar shape.
1618
1619impl IfStmt {
1620    /// The head condition — `IF_STMT`'s only child node that isn't the
1621    /// `STMT_BLOCK` body, the trailing `ELSE_CLAUSE`, or the `AS_BINDING`
1622    /// suffix (mirrors `ConditionalBlock::condition`'s same-shaped lookup).
1623    pub fn condition(&self) -> Option<SyntaxNode> {
1624        self.syntax.children().find(|n| {
1625            !matches!(
1626                n.kind(),
1627                SyntaxKind::STMT_BLOCK | SyntaxKind::ELSE_CLAUSE | SyntaxKind::AS_BINDING
1628            )
1629        })
1630    }
1631
1632    /// The `as NAME` binding (B1b, issue #1475), when the condition carries
1633    /// one.
1634    pub fn as_binding(&self) -> Option<AsBinding> {
1635        support::child(&self.syntax)
1636    }
1637
1638    pub fn body(&self) -> Option<StmtBlock> {
1639        support::child(&self.syntax)
1640    }
1641
1642    pub fn else_clause(&self) -> Option<ElseClause> {
1643        support::child(&self.syntax)
1644    }
1645}
1646
1647impl AsBinding {
1648    /// The bound name (`as NAME`). `None` only for a malformed binding the
1649    /// parser already diagnosed (`as` with no identifier after it).
1650    pub fn name_token(&self) -> Option<SyntaxToken> {
1651        support::tokens(&self.syntax, IDENT).next()
1652    }
1653}
1654
1655impl ElseClause {
1656    /// `else if cond { … }` — the arm's entire body is a nested `IF_STMT`,
1657    /// with no `STMT_BLOCK` wrapper of its own (`control_flow.rs::
1658    /// else_clause`'s flat-chain shape).
1659    pub fn if_stmt(&self) -> Option<IfStmt> {
1660        support::child(&self.syntax)
1661    }
1662
1663    /// `else { … }` — the plain form.
1664    pub fn body(&self) -> Option<StmtBlock> {
1665        support::child(&self.syntax)
1666    }
1667}
1668
1669impl WhileStmt {
1670    /// The loop condition — `WHILE_STMT`'s only child node that isn't the
1671    /// `STMT_BLOCK` body or the `AS_BINDING` suffix.
1672    pub fn condition(&self) -> Option<SyntaxNode> {
1673        self.syntax
1674            .children()
1675            .find(|n| !matches!(n.kind(), SyntaxKind::STMT_BLOCK | SyntaxKind::AS_BINDING))
1676    }
1677
1678    /// The `as NAME` binding (B1b, issue #1475), when the condition carries
1679    /// one. Rebinds on every iteration — the condition (and with it this
1680    /// binding) is re-evaluated per pass.
1681    pub fn as_binding(&self) -> Option<AsBinding> {
1682        support::child(&self.syntax)
1683    }
1684
1685    pub fn body(&self) -> Option<StmtBlock> {
1686        support::child(&self.syntax)
1687    }
1688}
1689
1690impl ForStmt {
1691    /// The loop-binding identifier (`for NAME in …`) — the key binding for
1692    /// the two-binding form (`for NAME, val_name in …`).
1693    pub fn name_token(&self) -> Option<SyntaxToken> {
1694        support::tokens(&self.syntax, IDENT).next()
1695    }
1696
1697    /// The second loop-binding identifier (`for key, VAL in …`), when
1698    /// present — two-binding map iteration (B2, issue #1461,
1699    /// docs/stdlib-spec.md §5/§9's F10 ruling). `None` for the
1700    /// single-binding form. Both binding idents are direct `FOR_STMT`
1701    /// tokens (the iterable and body are nested nodes, never direct
1702    /// `IDENT` children), so the second direct `IDENT` token is
1703    /// unambiguously this binding.
1704    pub fn val_name_token(&self) -> Option<SyntaxToken> {
1705        support::tokens(&self.syntax, IDENT).nth(1)
1706    }
1707
1708    /// The iterable expression — `FOR_STMT`'s only child node that isn't
1709    /// the `STMT_BLOCK` body.
1710    pub fn iterable(&self) -> Option<SyntaxNode> {
1711        self.syntax
1712            .children()
1713            .find(|n| n.kind() != SyntaxKind::STMT_BLOCK)
1714    }
1715
1716    pub fn body(&self) -> Option<StmtBlock> {
1717        support::child(&self.syntax)
1718    }
1719}
1720
1721impl UntilStmt {
1722    /// The park condition — `UNTIL_STMT`'s only child node.
1723    pub fn condition(&self) -> Option<SyntaxNode> {
1724        self.syntax.children().next()
1725    }
1726}
1727
1728// ── Prose block elements (#1715; docs/prose-dialect-spec.md §8b/§8d) ──
1729//
1730// Accessors for the ruled screenplay-preset shapes. Nothing here lowers —
1731// `hir::lower_native` meets these nodes at its loud-`E129` arm until the
1732// attachment/`lower:` slice (issue #1717) lands; these exist so that slice,
1733// the formatter and the editor read the shapes through one typed surface
1734// rather than each re-deriving them from raw `SyntaxKind`s.
1735
1736impl SceneStitch {
1737    /// The heading line that opens this header-scoped stitch.
1738    pub fn heading(&self) -> Option<SceneHeading> {
1739        support::child(&self.syntax)
1740    }
1741
1742    /// The braceless body the heading scopes — every item up to the next
1743    /// heading or the enclosing close (§8b.2).
1744    pub fn body(&self) -> Option<SceneBody> {
1745        support::child(&self.syntax)
1746    }
1747
1748    /// The leading `///` run documenting this stitch, if any (B0.6b — a
1749    /// heading declares a stitch, so it documents like one).
1750    pub fn doc(&self) -> Option<DocComment> {
1751        support::child(&self.syntax)
1752    }
1753}
1754
1755impl SceneHeading {
1756    /// The title run — the scene's **display name** (§3.3).
1757    pub fn title(&self) -> Option<SceneTitle> {
1758        support::child(&self.syntax)
1759    }
1760
1761    /// The explicit `[slug]`, if the heading spells its address (§8b.3).
1762    /// `None` means the address is inferred from the title, which makes
1763    /// the title load-bearing for `DefinitionId` (§3.3's save-key note).
1764    pub fn slug(&self) -> Option<SceneSlug> {
1765        support::child(&self.syntax)
1766    }
1767
1768    /// Trailing `#tag`s — container-level per-flow tags (§8b.4).
1769    pub fn tags(&self) -> impl Iterator<Item = Tag> {
1770        support::children(&self.syntax)
1771    }
1772}
1773
1774impl SceneTitle {
1775    /// The title text, with the surrounding source whitespace trimmed and
1776    /// a *recognized* inline escape's backslash stripped (§8d.6, issue
1777    /// #2045) — parity with `markup::escape`'s stripping in ordinary
1778    /// content. See [`Tag::text`] for the shared rationale.
1779    pub fn text(&self) -> String {
1780        strip_recognized_escape_backslashes(self.syntax.text().to_string().trim())
1781    }
1782}
1783
1784impl SceneSlug {
1785    pub fn name_token(&self) -> Option<SyntaxToken> {
1786        support::token(&self.syntax, IDENT)
1787    }
1788}
1789
1790impl SceneBody {
1791    /// The body's items, in source order.
1792    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
1793        self.syntax.children()
1794    }
1795}
1796
1797impl Cue {
1798    /// The speaker name after the `@` sigil.
1799    pub fn name(&self) -> Option<CueName> {
1800        support::child(&self.syntax)
1801    }
1802
1803    /// The cue's trailing tags — the ruled home for cue *extensions*
1804    /// (§8d.4: `@VENDOR #(v.o.)`, no parsed `ext` capture).
1805    pub fn tags(&self) -> impl Iterator<Item = Tag> {
1806        support::children(&self.syntax)
1807    }
1808}
1809
1810impl CueName {
1811    /// The speaker name, with the surrounding source whitespace trimmed
1812    /// and a *recognized* inline escape's backslash stripped (§8d.6,
1813    /// issue #2045) — parity with `markup::escape`'s stripping in
1814    /// ordinary content. See [`Tag::text`] for the shared rationale.
1815    pub fn text(&self) -> String {
1816        strip_recognized_escape_backslashes(self.syntax.text().to_string().trim())
1817    }
1818}
1819
1820impl CompactCue {
1821    /// The speaker name before the `:`.
1822    pub fn name(&self) -> Option<CueName> {
1823        support::child(&self.syntax)
1824    }
1825
1826    /// The fused dialogue line after the `:` (§8b.9).
1827    pub fn line(&self) -> Option<ContentLine> {
1828        support::child(&self.syntax)
1829    }
1830}
1831
1832impl BangDispatch {
1833    /// The dispatching name after the `!` sigil.
1834    pub fn name(&self) -> Option<DispatchName> {
1835        support::child(&self.syntax)
1836    }
1837
1838    /// The remainder after the name — a fused content line, the same way
1839    /// [`CompactCue::line`] fuses its dialogue line.
1840    pub fn line(&self) -> Option<ContentLine> {
1841        support::child(&self.syntax)
1842    }
1843}
1844
1845impl DispatchName {
1846    /// The dispatching name, with the surrounding source whitespace
1847    /// trimmed.
1848    pub fn text(&self) -> String {
1849        self.syntax.text().to_string().trim().to_owned()
1850    }
1851}
1852
1853impl Parenthetical {
1854    /// The delivery text between the parentheses, trimmed.
1855    pub fn text(&self) -> String {
1856        support::child::<Text>(&self.syntax)
1857            .map(|t| t.syntax().text().to_string().trim().to_owned())
1858            .unwrap_or_default()
1859    }
1860
1861    /// Trailing `#tag`s on the parenthetical line, if any.
1862    pub fn tags(&self) -> impl Iterator<Item = Tag> {
1863        support::children(&self.syntax)
1864    }
1865}
1866
1867impl FlowDecl {
1868    /// Trailing `#tag`s on the `flow` header line — container-level
1869    /// per-flow tags (§8b.4, the authoring surface issue #474's per-flow
1870    /// tag APIs were iceboxed waiting for). Parsed here; the runtime-side
1871    /// API is #474's own work, so `hir::lower_native` reports them as
1872    /// not-yet-lowered rather than dropping them.
1873    pub fn tags(&self) -> impl Iterator<Item = Tag> {
1874        support::children(&self.syntax)
1875    }
1876}