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}