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, IDENT, L_PAREN};
10use crate::ast::ast_node;
11use crate::ast::support;
12use crate::{SyntaxNode, SyntaxToken};
13
14// ── Doc comments (B0.6b) ──────────────────────────────────────────────
15
16ast_node!(DocComment, DOC_COMMENT);
17
18// ── Top level & declarations ────────────────────────────────────────
19
20ast_node!(SourceFile, SOURCE_FILE);
21ast_node!(FlowDecl, FLOW_DECL);
22ast_node!(FnDecl, FN_DECL);
23ast_node!(ParamList, PARAM_LIST);
24ast_node!(Param, PARAM);
25ast_node!(VarDecl, VAR_DECL);
26ast_node!(ConstDecl, CONST_DECL);
27ast_node!(FlagsDecl, FLAGS_DECL);
28ast_node!(FlagsMemberList, FLAGS_MEMBER_LIST);
29ast_node!(FlagsMember, FLAGS_MEMBER);
30ast_node!(StructDecl, STRUCT_DECL);
31ast_node!(StructField, STRUCT_FIELD);
32ast_node!(ExternDecl, EXTERN_DECL);
33ast_node!(UseDecl, USE_DECL);
34ast_node!(UseTree, USE_TREE);
35ast_node!(UseTreeList, USE_TREE_LIST);
36ast_node!(ImportDecl, IMPORT_DECL);
37ast_node!(ModuleDecl, MODULE_DECL);
38
39// ── Bodies & content ─────────────────────────────────────────────────
40
41ast_node!(Block, BLOCK);
42ast_node!(ContentLine, CONTENT_LINE);
43ast_node!(Text, TEXT);
44ast_node!(Interpolation, INTERPOLATION);
45ast_node!(GlueNode, GLUE_NODE);
46ast_node!(TagLine, TAG_LINE);
47ast_node!(Tag, TAG);
48
49// ── Choice points ────────────────────────────────────────────────────
50
51ast_node!(ChoicePoint, CHOICE_POINT);
52ast_node!(Choice, CHOICE);
53ast_node!(ChoiceBullet, CHOICE_BULLET);
54ast_node!(Label, LABEL);
55ast_node!(ChoiceGuard, CHOICE_GUARD);
56ast_node!(ChoiceStartContent, CHOICE_START_CONTENT);
57ast_node!(ChoiceBracketContent, CHOICE_BRACKET_CONTENT);
58ast_node!(ChoiceInnerContent, CHOICE_INNER_CONTENT);
59ast_node!(ChoiceBody, CHOICE_BODY);
60ast_node!(ElseBranch, ELSE_BRANCH);
61ast_node!(Splice, SPLICE);
62
63// ── The annotated-brace family: conditional / alternation ───────────
64
65ast_node!(ConditionalBlock, CONDITIONAL_BLOCK);
66ast_node!(IfArm, IF_ARM);
67ast_node!(MatchArm, MATCH_ARM);
68ast_node!(MatchPattern, MATCH_PATTERN);
69ast_node!(AlternationBlock, ALTERNATION_BLOCK);
70ast_node!(AlternationMarker, ALTERNATION_MARKER);
71ast_node!(Entry, ENTRY);
72
73// ── Annotations ──────────────────────────────────────────────────────
74
75ast_node!(AnnotationLine, ANNOTATION_LINE);
76ast_node!(AnnotationArgs, ANNOTATION_ARGS);
77ast_node!(AnnotationArg, ANNOTATION_ARG);
78
79// ── Diverts, tunnels, return ─────────────────────────────────────────
80
81ast_node!(DivertStmt, DIVERT_STMT);
82ast_node!(TunnelCall, TUNNEL_CALL);
83ast_node!(DivertTarget, DIVERT_TARGET);
84ast_node!(ReturnStmt, RETURN_STMT);
85ast_node!(ReturnRedirect, RETURN_REDIRECT);
86
87// ── Paths ────────────────────────────────────────────────────────────
88
89ast_node!(Path, PATH);
90ast_node!(PathSegment, PATH_SEGMENT);
91
92// ── Expressions ──────────────────────────────────────────────────────
93
94ast_node!(IntegerLit, INTEGER_LIT);
95ast_node!(FloatLit, FLOAT_LIT);
96ast_node!(StringLit, STRING_LIT);
97ast_node!(BooleanLit, BOOLEAN_LIT);
98ast_node!(PathExpr, PATH_EXPR);
99ast_node!(ParenExpr, PAREN_EXPR);
100ast_node!(PrefixExpr, PREFIX_EXPR);
101ast_node!(InfixExpr, INFIX_EXPR);
102ast_node!(CallExpr, CALL_EXPR);
103ast_node!(ArgList, ARG_LIST);
104ast_node!(LambdaExpr, LAMBDA_EXPR);
105ast_node!(LambdaParams, LAMBDA_PARAMS);
106
107// ── Error recovery ───────────────────────────────────────────────────
108
109ast_node!(Error, ERROR);
110
111// ── Hand-written accessors ───────────────────────────────────────────
112
113impl DocComment {
114    /// `true` for the inner (`//!`) form — a run whose tokens are
115    /// `DOC_COMMENT_INNER` rather than `DOC_COMMENT_OUTER`. One node shape
116    /// covers both variants (`syntax_kind.rs`'s `DOC_COMMENT` doc); this is
117    /// how callers tell them apart. A well-formed `DOC_COMMENT` node's
118    /// comment tokens are always uniformly one kind or the other (the
119    /// parser's `doc_comment::consume_doc_run` never mixes them within a
120    /// single run), so checking the first one is sufficient.
121    pub fn is_inner(&self) -> bool {
122        self.syntax
123            .children_with_tokens()
124            .filter_map(rowan::NodeOrToken::into_token)
125            .any(|t| t.kind() == DOC_COMMENT_INNER)
126    }
127
128    /// Every doc-comment line in source order: the token's text with its
129    /// `///`/`//!` marker stripped and a single leading space (if any)
130    /// trimmed, paired with that token's source range — the same shape the
131    /// OLD ink parser's `collect_doc_lines` produces, so both frontends
132    /// feed the identical format-agnostic `hir::doc_block::parse_lines`
133    /// tag parser.
134    pub fn lines(&self) -> Vec<(String, rowan::TextRange)> {
135        self.syntax
136            .children_with_tokens()
137            .filter_map(rowan::NodeOrToken::into_token)
138            .filter(|t| matches!(t.kind(), DOC_COMMENT_OUTER | DOC_COMMENT_INNER))
139            .map(|t| {
140                let text = t.text();
141                let body = text
142                    .strip_prefix("///")
143                    .or_else(|| text.strip_prefix("//!"))
144                    .unwrap_or(text);
145                (body.trim_start().to_string(), t.text_range())
146            })
147            .collect()
148    }
149}
150
151impl SourceFile {
152    /// Every top-level `flow`/`fn` declaration in the file (charter §4:
153    /// "no one-flow-per-file constraint — files hold many declarations").
154    pub fn flows(&self) -> impl Iterator<Item = FlowDecl> {
155        support::children(&self.syntax)
156    }
157
158    /// Every top-level `fn` declaration in the file.
159    pub fn fns(&self) -> impl Iterator<Item = FnDecl> {
160        support::children(&self.syntax)
161    }
162
163    /// Every direct child node, typed as its own `SyntaxKind` where a
164    /// wrapper exists — the generic escape hatch for callers that want to
165    /// walk the whole item list without matching on every variant twice.
166    pub fn syntax_children(&self) -> impl Iterator<Item = SyntaxNode> {
167        self.syntax.children()
168    }
169
170    /// The file-level inner `//!` doc comment, if the file opens with one
171    /// (B0.6b: "documents the enclosing ... file"). CST-only for now — no
172    /// native HIR type represents whole-file identity yet (`lower_native`'s
173    /// module doc, judgment call #7), so nothing consumes this today; kept
174    /// for the LSP/fmt/source-map consumers the ruling names.
175    pub fn doc(&self) -> Option<DocComment> {
176        support::child(&self.syntax)
177    }
178}
179
180impl FlowDecl {
181    /// The declared name (the `IDENT` immediately after `flow`).
182    pub fn name_token(&self) -> Option<SyntaxToken> {
183        support::token(&self.syntax, IDENT)
184    }
185
186    pub fn param_list(&self) -> Option<ParamList> {
187        support::child(&self.syntax)
188    }
189
190    pub fn body(&self) -> Option<Block> {
191        support::child(&self.syntax)
192    }
193
194    /// Nested `flow` declarations directly inside this one's body — a
195    /// stitch (charter §4: "stitches are nested `flow`s").
196    pub fn stitches(&self) -> impl Iterator<Item = FlowDecl> {
197        self.body()
198            .into_iter()
199            .flat_map(|b| support::children::<FlowDecl>(&b.syntax).collect::<Vec<_>>())
200    }
201
202    /// The leading `///` doc comment, if one is attached (B0.6b).
203    pub fn doc(&self) -> Option<DocComment> {
204        support::child(&self.syntax)
205    }
206}
207
208impl FnDecl {
209    pub fn name_token(&self) -> Option<SyntaxToken> {
210        support::token(&self.syntax, IDENT)
211    }
212
213    pub fn param_list(&self) -> Option<ParamList> {
214        support::child(&self.syntax)
215    }
216
217    pub fn body(&self) -> Option<Block> {
218        support::child(&self.syntax)
219    }
220
221    /// The leading `///` doc comment, if one is attached (B0.6b).
222    pub fn doc(&self) -> Option<DocComment> {
223        support::child(&self.syntax)
224    }
225}
226
227impl ParamList {
228    pub fn params(&self) -> impl Iterator<Item = Param> {
229        support::children(&self.syntax)
230    }
231}
232
233impl Param {
234    pub fn name_token(&self) -> Option<SyntaxToken> {
235        support::token(&self.syntax, IDENT)
236    }
237
238    /// `true` if this parameter is `ref`-marked.
239    pub fn is_ref(&self) -> bool {
240        support::token(&self.syntax, SyntaxKind::KW_REF).is_some()
241    }
242}
243
244impl Block {
245    /// Every direct-child node in this block's body, in source order —
246    /// the untyped escape hatch, since a `BLOCK`'s items span every
247    /// declaration/body-line kind this crate defines. Includes a leading
248    /// inner `DOC_COMMENT`, if present — callers that don't want it in
249    /// their item stream (e.g. `hir::lower_native::body::lower_block`)
250    /// filter it out themselves, same as they already skip other
251    /// non-statement node kinds.
252    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
253        self.syntax.children()
254    }
255
256    /// The inner `//!` doc comment, if this block opens with one (B0.6b):
257    /// documents the enclosing knot/flow/stitch, not a following
258    /// declaration. `None` for a `CHOICE_BODY`/`ELSE_BRANCH` — the parser
259    /// never attaches an inner doc there (`parser::block::braced_item_list`
260    /// only checks for one when building a real `BLOCK`).
261    pub fn doc(&self) -> Option<DocComment> {
262        support::child(&self.syntax)
263    }
264}
265
266impl VarDecl {
267    pub fn name_token(&self) -> Option<SyntaxToken> {
268        support::token(&self.syntax, IDENT)
269    }
270
271    /// The initializer expression's root node, if the `=` clause was
272    /// present. `var name = expr` always parses the initializer as exactly
273    /// one child node (whatever expression-grammar kind it is) after the
274    /// `IDENT`, so "the first child node" is unambiguous.
275    pub fn value(&self) -> Option<SyntaxNode> {
276        self.syntax
277            .children()
278            .find(|n| n.kind() != SyntaxKind::DOC_COMMENT)
279    }
280
281    /// The leading `///` doc comment, if one is attached (B0.6b).
282    pub fn doc(&self) -> Option<DocComment> {
283        support::child(&self.syntax)
284    }
285}
286
287impl ConstDecl {
288    pub fn name_token(&self) -> Option<SyntaxToken> {
289        support::token(&self.syntax, IDENT)
290    }
291
292    /// See [`VarDecl::value`].
293    pub fn value(&self) -> Option<SyntaxNode> {
294        self.syntax
295            .children()
296            .find(|n| n.kind() != SyntaxKind::DOC_COMMENT)
297    }
298
299    /// The leading `///` doc comment, if one is attached (B0.6b).
300    pub fn doc(&self) -> Option<DocComment> {
301        support::child(&self.syntax)
302    }
303}
304
305impl FlagsDecl {
306    pub fn name_token(&self) -> Option<SyntaxToken> {
307        support::token(&self.syntax, IDENT)
308    }
309
310    pub fn member_list(&self) -> Option<FlagsMemberList> {
311        support::child(&self.syntax)
312    }
313
314    /// The leading `///` doc comment, if one is attached (B0.6b).
315    pub fn doc(&self) -> Option<DocComment> {
316        support::child(&self.syntax)
317    }
318}
319
320impl FlagsMemberList {
321    pub fn members(&self) -> impl Iterator<Item = FlagsMember> {
322        support::children(&self.syntax)
323    }
324}
325
326impl FlagsMember {
327    pub fn name_token(&self) -> Option<SyntaxToken> {
328        support::token(&self.syntax, IDENT)
329    }
330
331    /// `true` for a parenthesized member (`(name)`, the default-on entry).
332    pub fn is_active(&self) -> bool {
333        support::token(&self.syntax, L_PAREN).is_some()
334    }
335}
336
337impl StructDecl {
338    pub fn name_token(&self) -> Option<SyntaxToken> {
339        support::token(&self.syntax, IDENT)
340    }
341
342    pub fn fields(&self) -> impl Iterator<Item = StructField> {
343        support::children(&self.syntax)
344    }
345
346    /// The leading `///` doc comment, if one is attached (B0.6b).
347    pub fn doc(&self) -> Option<DocComment> {
348        support::child(&self.syntax)
349    }
350}
351
352impl StructField {
353    pub fn name_token(&self) -> Option<SyntaxToken> {
354        support::token(&self.syntax, IDENT)
355    }
356
357    /// The field's declared type — a bare dotted path in this skeleton
358    /// grammar (no generics/fn-types, `parser/decl.rs::struct_field`).
359    pub fn type_path(&self) -> Option<Path> {
360        support::child(&self.syntax)
361    }
362}
363
364impl ExternDecl {
365    pub fn name_token(&self) -> Option<SyntaxToken> {
366        support::token(&self.syntax, IDENT)
367    }
368
369    pub fn param_list(&self) -> Option<ParamList> {
370        support::child(&self.syntax)
371    }
372
373    /// The leading `///` doc comment, if one is attached (B0.6b).
374    pub fn doc(&self) -> Option<DocComment> {
375        support::child(&self.syntax)
376    }
377}
378
379impl ImportDecl {
380    pub fn path(&self) -> Option<Path> {
381        support::child(&self.syntax)
382    }
383
384    /// The leading `///` doc comment, if one is attached (B0.6b). No native
385    /// HIR field consumes this yet (`Import` carries no `doc`, matching the
386    /// OLD ink frontend's own `Import` shape) — CST-only for now, same
387    /// status as [`SourceFile::doc`].
388    pub fn doc(&self) -> Option<DocComment> {
389        support::child(&self.syntax)
390    }
391}
392
393impl UseDecl {
394    pub fn tree(&self) -> Option<UseTree> {
395        support::child(&self.syntax)
396    }
397
398    /// See [`ImportDecl::doc`].
399    pub fn doc(&self) -> Option<DocComment> {
400        support::child(&self.syntax)
401    }
402}
403
404impl UseTree {
405    /// The leading dotted/`::`-separated path segments, in order —
406    /// `use_tree`'s grammar lays these out as bare `IDENT` tokens
407    /// interspersed with `::` directly inside `USE_TREE` (no nested `PATH`
408    /// node, unlike `import`'s path), so this walks direct-child tokens up
409    /// to (not including) an `as`-alias or a nested `{ … }` list.
410    pub fn path_segments(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
411        self.syntax
412            .children_with_tokens()
413            .filter_map(rowan::NodeOrToken::into_token)
414            .take_while(|t| t.kind() != SyntaxKind::KW_AS)
415            .filter(|t| t.kind() == IDENT)
416    }
417
418    /// The `as alias` name, if this tree ends in one.
419    pub fn alias_token(&self) -> Option<SyntaxToken> {
420        let mut saw_as = false;
421        for el in self.syntax.children_with_tokens() {
422            let Some(tok) = el.into_token() else {
423                continue;
424            };
425            if tok.kind().is_trivia() {
426                continue;
427            }
428            if saw_as {
429                return if tok.kind() == IDENT { Some(tok) } else { None };
430            }
431            if tok.kind() == SyntaxKind::KW_AS {
432                saw_as = true;
433            }
434        }
435        None
436    }
437
438    /// The nested `{ a, b as c, … }` group, if this tree has one.
439    pub fn nested_list(&self) -> Option<UseTreeList> {
440        support::child(&self.syntax)
441    }
442}
443
444impl UseTreeList {
445    pub fn trees(&self) -> impl Iterator<Item = UseTree> {
446        support::children(&self.syntax)
447    }
448}
449
450impl ModuleDecl {
451    pub fn name_token(&self) -> Option<SyntaxToken> {
452        support::token(&self.syntax, IDENT)
453    }
454
455    pub fn body(&self) -> Option<Block> {
456        support::child(&self.syntax)
457    }
458
459    /// See [`ImportDecl::doc`] — no native HIR "module container" node
460    /// exists yet either (`lower_native`'s module doc, judgment call #4).
461    pub fn doc(&self) -> Option<DocComment> {
462        support::child(&self.syntax)
463    }
464}
465
466impl Path {
467    /// Every `PATH_SEGMENT`'s `IDENT`, in order.
468    pub fn segments(&self) -> impl Iterator<Item = SyntaxToken> {
469        support::children::<PathSegment>(&self.syntax)
470            .filter_map(|seg| support::token(&seg.syntax, IDENT))
471    }
472
473    /// `true` if any separator is `::` (crosses a module wall, charter
474    /// §13.2) rather than only `.`.
475    pub fn crosses_module_wall(&self) -> bool {
476        support::tokens(&self.syntax, SyntaxKind::COLON_COLON)
477            .next()
478            .is_some()
479    }
480}
481
482impl ChoicePoint {
483    pub fn choices(&self) -> impl Iterator<Item = Choice> {
484        support::children(&self.syntax)
485    }
486
487    pub fn else_branch(&self) -> Option<ElseBranch> {
488        support::child(&self.syntax)
489    }
490}
491
492impl Choice {
493    /// `true` for a sticky (`+`) choice, `false` for a once-only (`*`) one.
494    ///
495    /// B0.7 fix (`docs/b0-sequencing.md` §B0.7, issue #1176): the bullet
496    /// token is wrapped in a nested `CHOICE_BULLET` node
497    /// (`choice.rs::choice`: `p.start_node(CHOICE_BULLET); p.bump(); …`),
498    /// never a direct token of `CHOICE` itself — `support::token` only
499    /// looks at direct children, so the original B0.5/B0.6 implementation
500    /// (`support::token(&self.syntax, PLUS)`) always returned `false`. Every
501    /// choice line in the corpus was silently lowering as once-only; caught
502    /// by B0.7's `choice_point_lowers_to_choice_set_with_sticky_and_once`
503    /// test, the first real exercise of a sticky (`+`) choice line.
504    pub fn is_sticky(&self) -> bool {
505        support::child::<ChoiceBullet>(&self.syntax)
506            .is_some_and(|b| support::token(&b.syntax, SyntaxKind::PLUS).is_some())
507    }
508
509    pub fn label(&self) -> Option<Label> {
510        support::child(&self.syntax)
511    }
512
513    pub fn guard(&self) -> Option<ChoiceGuard> {
514        support::child(&self.syntax)
515    }
516
517    pub fn body(&self) -> Option<ChoiceBody> {
518        support::child(&self.syntax)
519    }
520}
521
522impl AnnotationLine {
523    /// The directive/annotation name (`effects`, …).
524    pub fn name_token(&self) -> Option<SyntaxToken> {
525        support::token(&self.syntax, IDENT)
526    }
527
528    pub fn args(&self) -> Option<AnnotationArgs> {
529        support::child(&self.syntax)
530    }
531}
532
533impl AnnotationArgs {
534    pub fn args(&self) -> impl Iterator<Item = AnnotationArg> {
535        support::children(&self.syntax)
536    }
537}
538
539impl AnnotationArg {
540    pub fn name_token(&self) -> Option<SyntaxToken> {
541        support::token(&self.syntax, IDENT)
542    }
543
544    /// The nested paren-clause (`reads(gold, hp)` inside `effects(…)`), if
545    /// this argument has one.
546    pub fn nested_args(&self) -> Option<AnnotationArgs> {
547        support::child(&self.syntax)
548    }
549}
550
551impl CallExpr {
552    /// The callee path. Fixed for B0.6 (`docs/b0-sequencing.md` §B0.6):
553    /// `expr::path_or_call` wraps a bare `PATH` node directly (not a nested
554    /// `PATH_EXPR`) when it commits to `CALL_EXPR` — the previous
555    /// `Option<PathExpr>` signature could never cast successfully against
556    /// the real grammar shape and always returned `None` for every call
557    /// expression. No test exercised it before B0.6's lowering needed it.
558    pub fn callee(&self) -> Option<Path> {
559        support::child(&self.syntax)
560    }
561
562    pub fn arg_list(&self) -> Option<ArgList> {
563        support::child(&self.syntax)
564    }
565}
566
567impl PathExpr {
568    pub fn path(&self) -> Option<Path> {
569        support::child(&self.syntax)
570    }
571}
572
573impl IntegerLit {
574    pub fn value_token(&self) -> Option<SyntaxToken> {
575        support::token(&self.syntax, crate::SyntaxKind::INTEGER)
576    }
577
578    pub fn value(&self) -> Option<i64> {
579        self.value_token()
580            .and_then(|t| t.text().parse::<i64>().ok())
581    }
582}
583
584impl FloatLit {
585    pub fn value_token(&self) -> Option<SyntaxToken> {
586        support::token(&self.syntax, crate::SyntaxKind::FLOAT)
587    }
588
589    pub fn value(&self) -> Option<f64> {
590        self.value_token()
591            .and_then(|t| t.text().parse::<f64>().ok())
592    }
593}
594
595impl BooleanLit {
596    pub fn value(&self) -> Option<bool> {
597        let tok = self
598            .syntax
599            .children_with_tokens()
600            .filter_map(rowan::NodeOrToken::into_token)
601            .find(|t| matches!(t.kind(), SyntaxKind::KW_TRUE | SyntaxKind::KW_FALSE))?;
602        match tok.kind() {
603            SyntaxKind::KW_TRUE => Some(true),
604            SyntaxKind::KW_FALSE => Some(false),
605            _ => None,
606        }
607    }
608}
609
610impl ParenExpr {
611    /// The parenthesized inner expression's root node.
612    pub fn inner(&self) -> Option<SyntaxNode> {
613        self.syntax.children().next()
614    }
615}
616
617impl PrefixExpr {
618    /// The prefix operator token (`-` or `!`).
619    pub fn op_token(&self) -> Option<SyntaxToken> {
620        self.syntax
621            .children_with_tokens()
622            .filter_map(rowan::NodeOrToken::into_token)
623            .find(|t| matches!(t.kind(), SyntaxKind::MINUS | SyntaxKind::BANG))
624    }
625
626    /// The operand's root node.
627    pub fn operand(&self) -> Option<SyntaxNode> {
628        self.syntax.children().next()
629    }
630}
631
632impl InfixExpr {
633    /// The left-hand operand's root node (the first child node).
634    pub fn lhs(&self) -> Option<SyntaxNode> {
635        self.syntax.children().next()
636    }
637
638    /// The right-hand operand's root node (the last child node).
639    pub fn rhs(&self) -> Option<SyntaxNode> {
640        self.syntax.children().last()
641    }
642
643    /// The operator token. Two adjacent `PIPE`s (`||`) are represented as
644    /// two tokens (see `expr::infix_binding_power`'s doc) — this returns
645    /// the *first* one; callers that need to distinguish `|` from `||`
646    /// check [`Self::is_double_pipe`].
647    pub fn op_token(&self) -> Option<SyntaxToken> {
648        self.syntax
649            .children_with_tokens()
650            .filter_map(rowan::NodeOrToken::into_token)
651            .find(|t| {
652                matches!(
653                    t.kind(),
654                    SyntaxKind::AMP_AMP
655                        | SyntaxKind::EQ_EQ
656                        | SyntaxKind::BANG_EQ
657                        | SyntaxKind::LT
658                        | SyntaxKind::GT
659                        | SyntaxKind::LT_EQ
660                        | SyntaxKind::GT_EQ
661                        | SyntaxKind::PLUS
662                        | SyntaxKind::MINUS
663                        | SyntaxKind::STAR
664                        | SyntaxKind::SLASH
665                        | SyntaxKind::PERCENT
666                        | SyntaxKind::PIPE
667                )
668            })
669    }
670
671    /// `true` if the operator is `||` (two adjacent `PIPE` tokens) rather
672    /// than a single-token operator.
673    pub fn is_double_pipe(&self) -> bool {
674        let mut pipes = self
675            .syntax
676            .children_with_tokens()
677            .filter_map(rowan::NodeOrToken::into_token)
678            .filter(|t| t.kind() == SyntaxKind::PIPE);
679        pipes.next().is_some() && pipes.next().is_some()
680    }
681}
682
683impl ArgList {
684    /// `true` if this arg list opens with `(` at all (always true for a
685    /// well-formed parse — exposed for error-recovery callers that walk a
686    /// possibly-malformed tree).
687    pub fn is_open(&self) -> bool {
688        support::token(&self.syntax, L_PAREN).is_some()
689    }
690}
691
692impl DivertTarget {
693    pub fn is_end(&self) -> bool {
694        support::token(&self.syntax, SyntaxKind::KW_END).is_some()
695    }
696
697    pub fn is_done(&self) -> bool {
698        support::token(&self.syntax, SyntaxKind::KW_DONE).is_some()
699    }
700
701    pub fn path(&self) -> Option<Path> {
702        support::child(&self.syntax)
703    }
704}
705
706// ── B0.7 additions: body-dialect accessors ──────────────────────────
707//
708// Everything below was added for `hir::lower_native`'s body lowering
709// (`docs/b0-sequencing.md` §B0.7). B0.5/B0.6 hand-wrote a representative
710// subset of accessors "enough to prove the pattern end-to-end ... not
711// pre-building every accessor a later lowering pass might want" (this
712// file's module doc) — B0.7 is exactly that later pass.
713
714impl ContentLine {
715    /// The leading `(name)` label, if this line opens with one (G-1).
716    pub fn label(&self) -> Option<Label> {
717        support::child(&self.syntax)
718    }
719}
720
721impl Label {
722    pub fn name_token(&self) -> Option<SyntaxToken> {
723        support::token(&self.syntax, IDENT)
724    }
725}
726
727impl DivertStmt {
728    pub fn target(&self) -> Option<DivertTarget> {
729        support::child(&self.syntax)
730    }
731}
732
733impl TunnelCall {
734    /// The one divert target between the opening and closing `->` (native's
735    /// `-> place ->` shape carries exactly one target, unlike ink's chained
736    /// `-> a -> b ->`).
737    pub fn target(&self) -> Option<DivertTarget> {
738        support::child(&self.syntax)
739    }
740}
741
742impl ReturnRedirect {
743    pub fn target(&self) -> Option<DivertTarget> {
744        support::child(&self.syntax)
745    }
746}
747
748impl Choice {
749    pub fn start_content(&self) -> Option<ChoiceStartContent> {
750        support::child(&self.syntax)
751    }
752
753    pub fn bracket_content(&self) -> Option<ChoiceBracketContent> {
754        support::child(&self.syntax)
755    }
756
757    pub fn inner_content(&self) -> Option<ChoiceInnerContent> {
758        support::child(&self.syntax)
759    }
760}
761
762impl ChoiceGuard {
763    /// The guard's condition expression — `CHOICE_GUARD`'s only child node
764    /// (`L_BRACE KW_IF expression R_BRACE`; the braces/keyword are tokens).
765    pub fn expr(&self) -> Option<SyntaxNode> {
766        self.syntax.children().next()
767    }
768}
769
770impl ChoiceBody {
771    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
772        self.syntax.children()
773    }
774}
775
776impl ElseBranch {
777    /// The nested `CHOICE_BODY`, when this `else` belongs to a choice point
778    /// (`choice.rs::else_branch` — always braced, no colon form).
779    pub fn choice_body(&self) -> Option<ChoiceBody> {
780        support::child(&self.syntax)
781    }
782
783    /// The nested `BLOCK`, when this `else` belongs to the conditional
784    /// family's braced-arm form (`{if cond {…} else {…}}`).
785    pub fn block(&self) -> Option<Block> {
786        support::child(&self.syntax)
787    }
788
789    /// Every direct-child item, for the conditional family's colon-body
790    /// form (`{if cond: … else: …}`), where body items are direct children
791    /// with no wrapper node (`family.rs::colon_body`).
792    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
793        self.syntax.children()
794    }
795}
796
797impl Splice {
798    pub fn path(&self) -> Option<Path> {
799        support::child(&self.syntax)
800    }
801
802    pub fn arg_list(&self) -> Option<ArgList> {
803        support::child(&self.syntax)
804    }
805}
806
807impl ConditionalBlock {
808    pub fn is_if(&self) -> bool {
809        support::token(&self.syntax, SyntaxKind::KW_IF).is_some()
810    }
811
812    pub fn is_match(&self) -> bool {
813        support::token(&self.syntax, SyntaxKind::KW_MATCH).is_some()
814    }
815
816    /// The head expression: the `if` condition or the `match` subject —
817    /// `CONDITIONAL_BLOCK`'s only child node that isn't an arm/else
818    /// (`family.rs::conditional_block`: the expression is parsed directly
819    /// into this node before the arm(s)).
820    pub fn condition(&self) -> Option<SyntaxNode> {
821        self.syntax.children().find(|n| {
822            !matches!(
823                n.kind(),
824                SyntaxKind::IF_ARM | SyntaxKind::ELSE_BRANCH | SyntaxKind::MATCH_ARM
825            )
826        })
827    }
828
829    pub fn if_arm(&self) -> Option<IfArm> {
830        support::child(&self.syntax)
831    }
832
833    pub fn else_arm(&self) -> Option<ElseBranch> {
834        support::child(&self.syntax)
835    }
836
837    /// `match`'s arms — direct children of `CONDITIONAL_BLOCK` itself
838    /// (`family.rs::match_arm_list` opens no wrapper node of its own).
839    pub fn match_arms(&self) -> impl Iterator<Item = MatchArm> {
840        support::children(&self.syntax)
841    }
842}
843
844impl IfArm {
845    /// The nested `BLOCK`, for the braced-arm form.
846    pub fn block(&self) -> Option<Block> {
847        support::child(&self.syntax)
848    }
849
850    /// Direct-child items, for the colon-body form (see `ElseBranch::items`).
851    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
852        self.syntax.children()
853    }
854}
855
856impl MatchArm {
857    /// The pattern's expression — `MATCH_PATTERN`'s only child node.
858    pub fn pattern_expr(&self) -> Option<SyntaxNode> {
859        support::child::<MatchPattern>(&self.syntax).and_then(|p| p.syntax.children().next())
860    }
861
862    /// The nested `BLOCK`, for a braced arm body (`pattern => { … }`).
863    pub fn block(&self) -> Option<Block> {
864        support::child(&self.syntax)
865    }
866
867    /// The bare expression, for an unbraced arm body (`pattern => expr`) —
868    /// the one child node that is neither `MATCH_PATTERN` nor `BLOCK`.
869    pub fn bare_expr(&self) -> Option<SyntaxNode> {
870        self.syntax
871            .children()
872            .find(|n| !matches!(n.kind(), SyntaxKind::MATCH_PATTERN | SyntaxKind::BLOCK))
873    }
874}
875
876impl AlternationBlock {
877    /// The `~`/`&`/`!`/`|` marker token.
878    pub fn marker_token(&self) -> Option<SyntaxToken> {
879        support::child::<AlternationMarker>(&self.syntax).and_then(|m| {
880            m.syntax
881                .children_with_tokens()
882                .filter_map(rowan::NodeOrToken::into_token)
883                .find(|t| {
884                    matches!(
885                        t.kind(),
886                        SyntaxKind::TILDE | SyntaxKind::AMP | SyntaxKind::BANG | SyntaxKind::PIPE
887                    )
888                })
889        })
890    }
891
892    /// The multiline `-`-prefixed entries, if this block used that form
893    /// (`family.rs::multiline_entries`). Empty for the single-line
894    /// pipe-separated form — see [`Self::syntax`] for the raw child walk
895    /// callers need for that form instead (no per-alternative wrapper node
896    /// exists for it, `family.rs::inline_alternatives`).
897    pub fn entries(&self) -> impl Iterator<Item = Entry> {
898        support::children(&self.syntax)
899    }
900}
901
902impl Entry {
903    /// Every direct-child item inside this `-`-prefixed entry (the leading
904    /// `MINUS` is a token, filtered out by `.children()`).
905    pub fn items(&self) -> impl Iterator<Item = SyntaxNode> {
906        self.syntax.children()
907    }
908}
909
910impl TagLine {
911    pub fn tags(&self) -> impl Iterator<Item = Tag> {
912        support::children(&self.syntax)
913    }
914}