Skip to main content

harn_parser/
visit.rs

1//! Generic AST visitor used by the linter, formatter, and any other
2//! crate that needs to walk every `SNode` in a parsed program.
3//!
4//! Centralizing this here keeps a single source of truth for which
5//! children each `Node` variant has — adding a new variant requires
6//! one edit (in `collect_children`) and every consumer benefits.
7//!
8//! # Usage
9//!
10//! ```ignore
11//! use harn_parser::visit::walk_program;
12//! let mut count = 0;
13//! walk_program(&program, &mut |node| {
14//!     if matches!(&node.node, harn_parser::Node::FunctionCall { .. }) {
15//!         count += 1;
16//!     }
17//! });
18//! ```
19//!
20//! The visitor invokes the closure on each node *before* recursing
21//! into its children (pre-order). To stop recursion at a particular
22//! node, prefer using [`walk_children`] directly.
23
24use crate::ast::{BindingPattern, DictEntry, MatchArm, Node, SNode, SelectCase, TypedParam};
25
26/// Walk every node in `program` in pre-order, invoking `visitor` on
27/// each.
28pub fn walk_program<'a>(program: &'a [SNode], visitor: &mut impl FnMut(&'a SNode)) {
29    let mut stack = Vec::with_capacity(program.len());
30    push_nodes_reversed(program, &mut stack);
31    walk_stack(&mut stack, visitor);
32}
33
34/// Walk declaration bodies and expression children in pre-order, excluding
35/// attribute arguments. Attributed declarations are visited as their inner
36/// declaration, so compile-time metadata cannot look like an executed call.
37pub fn walk_program_unattributed<'a>(program: &'a [SNode], visitor: &mut impl FnMut(&'a SNode)) {
38    let mut stack = Vec::with_capacity(program.len());
39    push_nodes_reversed(program, &mut stack);
40    walk_projected_stack(&mut stack, visitor, |node| {
41        crate::ast::peel_attributes(node).1
42    });
43}
44
45/// Walk `program` INCLUDING the expressions inside `${...}` string
46/// interpolation.
47///
48/// [`walk_program`] cannot reach them: the lexer stores a hole as unparsed
49/// source text plus a position, so `Node::InterpolatedString` is a leaf with no
50/// AST children by construction. Any analysis that asks "is this name used?"
51/// and walks with [`walk_program`] alone answers NO for a name used only inside
52/// a hole — an unsound answer, not merely an incomplete one.
53///
54/// That is not hypothetical. The whole-program capability solver computed a
55/// helper's required capabilities that way, concluded `random` was unused
56/// because its only use was `"...${harness.random.uuid_v7()}"`, and deleted it
57/// from the parameter type and every call site. The repair runs automatically
58/// in the fleet bump workflow, so it rewrote working code into code that does
59/// not compile (`value of type nil has no method uuid_v7`) and shipped it as a
60/// bump PR (harn-cloud#1469).
61///
62/// `source` must be the whole file `program` was parsed from, so re-parsed
63/// holes carry spans in the containing file's coordinates and stay safe to edit
64/// against. A hole that fails to re-parse is skipped: it cannot be a well-typed
65/// use, and the containing parse already reported it.
66pub fn walk_program_interpolated(
67    source: &str,
68    program: &[SNode],
69    visitor: &mut impl FnMut(&SNode),
70) {
71    walk_interpolated(source, program, visitor, |node| node);
72}
73
74/// Walk executable children and string interpolation, excluding attribute metadata.
75pub fn walk_program_unattributed_interpolated(
76    source: &str,
77    program: &[SNode],
78    visitor: &mut impl FnMut(&SNode),
79) {
80    walk_interpolated(source, program, visitor, |node| {
81        crate::ast::peel_attributes(node).1
82    });
83}
84
85fn walk_interpolated(
86    source: &str,
87    program: &[SNode],
88    visitor: &mut impl FnMut(&SNode),
89    project: fn(&SNode) -> &SNode,
90) {
91    let mut holes = Vec::new();
92    let mut stack = Vec::with_capacity(program.len());
93    push_nodes_reversed(program, &mut stack);
94    walk_projected_stack(
95        &mut stack,
96        &mut |node| {
97            if let Node::InterpolatedString(segments) = &node.node {
98                for segment in segments {
99                    if let harn_lexer::StringSegment::Expression(text, line, column) = segment {
100                        holes.push((text.clone(), *line, *column));
101                    }
102                }
103            }
104            visitor(node);
105        },
106        project,
107    );
108    // Recurse: an interpolation inside an interpolation is rare but legal, and
109    // stopping at one level would restore the same unsoundness one layer down.
110    while let Some((text, line, column)) = holes.pop() {
111        let Some(expression) =
112            crate::interpolation::parse_expression(Some(source), &text, line, column)
113        else {
114            continue;
115        };
116        walk_interpolated(source, std::slice::from_ref(&expression), visitor, project);
117    }
118}
119
120/// Return whether a program contains a member access or method call whose
121/// receiver is a bare identifier (`Name.Member`). This is the only syntax
122/// whose lowering needs to distinguish an imported enum namespace from an
123/// ordinary runtime object; callers can use the predicate to avoid resolving
124/// the full import graph for files that cannot contain that ambiguity.
125pub fn contains_identifier_receiver_access(program: &[SNode]) -> bool {
126    any_node(program, &mut |node| {
127        let object = match &node.node {
128            Node::PropertyAccess { object, .. }
129            | Node::OptionalPropertyAccess { object, .. }
130            | Node::MethodCall { object, .. }
131            | Node::OptionalMethodCall { object, .. } => object,
132            _ => return false,
133        };
134        matches!(&object.node, Node::Identifier(_))
135    })
136}
137
138/// Return whether a program contains an enum-shaped match pattern whose
139/// receiver is a bare identifier (`Status.Ready` or `Status.Error(value)`).
140///
141/// Ordinary property access does not need enum metadata: the runtime module
142/// namespace supplies the same value through normal property lookup. The
143/// compiler only needs the imported-enum catalog when lowering these match
144/// patterns, where a dotted expression is otherwise indistinguishable from a
145/// value comparison. Keeping this predicate pattern-specific avoids forcing a
146/// full import-graph walk on modules that merely use record or namespace
147/// property access.
148pub fn contains_identifier_enum_pattern(program: &[SNode]) -> bool {
149    any_node(program, &mut |node| {
150        let Node::MatchExpr { arms, .. } = &node.node else {
151            return false;
152        };
153        arms.iter()
154            .any(|arm| contains_identifier_enum_pattern_node(&arm.pattern))
155    })
156}
157
158/// Pre-order walk that stops as soon as `predicate` returns true for a node.
159/// The existence predicates above use this so a match near the top of a large
160/// program does not pay for walking the rest of it.
161fn any_node(program: &[SNode], predicate: &mut impl FnMut(&SNode) -> bool) -> bool {
162    let mut stack = Vec::with_capacity(program.len());
163    push_nodes_reversed(program, &mut stack);
164    let mut scratch: Vec<&SNode> = Vec::new();
165    while let Some(node) = stack.pop() {
166        if predicate(node) {
167            return true;
168        }
169        scratch.clear();
170        collect_children(node, &mut |child| scratch.push(child));
171        stack.extend(scratch.iter().rev().copied());
172    }
173    false
174}
175
176fn contains_identifier_enum_pattern_node(node: &SNode) -> bool {
177    match &node.node {
178        Node::PropertyAccess { object, .. } | Node::MethodCall { object, .. } => {
179            matches!(&object.node, Node::Identifier(_))
180        }
181        Node::OrPattern(patterns) => patterns.iter().any(contains_identifier_enum_pattern_node),
182        _ => false,
183    }
184}
185
186/// Visit `node`, then recurse into its children.
187pub fn walk_node<'a>(node: &'a SNode, visitor: &mut impl FnMut(&'a SNode)) {
188    let mut stack = vec![node];
189    walk_stack(&mut stack, visitor);
190}
191
192/// Recurse into `node`'s children without re-visiting `node` itself.
193/// Useful when a caller wants to handle the parent specially and then
194/// continue the default traversal.
195pub fn walk_children<'a>(node: &'a SNode, visitor: &mut impl FnMut(&'a SNode)) {
196    let mut stack = Vec::new();
197    collect_children(node, &mut |child| stack.push(child));
198    stack.reverse();
199    walk_stack(&mut stack, visitor);
200}
201
202fn walk_stack<'a>(stack: &mut Vec<&'a SNode>, visitor: &mut impl FnMut(&'a SNode)) {
203    walk_projected_stack(stack, visitor, |node| node);
204}
205
206fn walk_projected_stack<'a>(
207    stack: &mut Vec<&'a SNode>,
208    visitor: &mut impl FnMut(&'a SNode),
209    project: impl Fn(&'a SNode) -> &'a SNode,
210) {
211    // One scratch buffer reused across the whole walk: collecting children
212    // into a fresh Vec per node made every walker pay one heap alloc/free
213    // per AST node, which dominated whole-module analyses.
214    let mut scratch: Vec<&'a SNode> = Vec::new();
215    while let Some(node) = stack.pop() {
216        let node = project(node);
217        visitor(node);
218        scratch.clear();
219        collect_children(node, &mut |child| scratch.push(child));
220        stack.extend(scratch.iter().rev().copied());
221    }
222}
223
224fn push_nodes_reversed<'a>(nodes: &'a [SNode], stack: &mut Vec<&'a SNode>) {
225    stack.extend(nodes.iter().rev());
226}
227
228/// Collect `node`'s immediate children without recursing. Lets callers walk
229/// selectively (e.g. stop descending at nested loops) while still relying on
230/// this module's single source of truth for each variant's children.
231pub fn immediate_children(node: &SNode) -> Vec<&SNode> {
232    let mut children = Vec::new();
233    collect_children(node, &mut |child| children.push(child));
234    children
235}
236
237/// Invoke `f` on each of `node`'s immediate children in source order,
238/// without materializing a `Vec`. Recursive walkers that visit children
239/// in place should prefer this over [`immediate_children`].
240pub fn for_each_immediate_child<'a>(node: &'a SNode, f: &mut impl FnMut(&'a SNode)) {
241    collect_children(node, f);
242}
243
244fn collect_children<'a>(node: &'a SNode, children: &mut impl FnMut(&'a SNode)) {
245    match &node.node {
246        Node::AttributedDecl { attributes, inner } => {
247            for attr in attributes {
248                for arg in &attr.args {
249                    children(&arg.value);
250                }
251            }
252            children(inner);
253        }
254        Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
255            collect_nodes(body, children);
256        }
257        Node::LetBinding { pattern, value, .. } | Node::ConstBinding { pattern, value, .. } => {
258            collect_binding_pattern(pattern, children);
259            children(value);
260        }
261        Node::EnumDecl { variants, .. } => {
262            for variant in variants {
263                collect_typed_param_defaults(&variant.fields, children);
264            }
265        }
266        Node::StructDecl { .. }
267        | Node::ImportDecl { .. }
268        | Node::SelectiveImport { .. }
269        | Node::NamespaceImport { .. }
270        | Node::TypeDecl { .. }
271        | Node::BreakStmt
272        | Node::ContinueStmt => {}
273        Node::InterfaceDecl { methods, .. } => {
274            for method in methods {
275                collect_typed_param_defaults(&method.params, children);
276            }
277        }
278        Node::ImplBlock { methods, .. } => collect_nodes(methods, children),
279        Node::IfElse {
280            condition,
281            then_body,
282            else_body,
283            ..
284        } => {
285            children(condition);
286            collect_nodes(then_body, children);
287            if let Some(body) = else_body {
288                collect_nodes(body, children);
289            }
290        }
291        Node::ForIn {
292            pattern,
293            iterable,
294            body,
295        } => {
296            collect_binding_pattern(pattern, children);
297            children(iterable);
298            collect_nodes(body, children);
299        }
300        Node::MatchExpr { value, arms } => {
301            children(value);
302            for arm in arms {
303                collect_match_arm(arm, children);
304            }
305        }
306        Node::WhileLoop { condition, body } => {
307            children(condition);
308            collect_nodes(body, children);
309        }
310        Node::Retry { count, body } => {
311            children(count);
312            collect_nodes(body, children);
313        }
314        Node::CostRoute { options, body } => {
315            collect_option_values(options, children);
316            collect_nodes(body, children);
317        }
318        Node::ReturnStmt { value } | Node::YieldExpr { value } => {
319            if let Some(value) = value {
320                children(value);
321            }
322        }
323        Node::TryCatch {
324            has_catch: _,
325            body,
326            catch_body,
327            finally_body,
328            ..
329        } => {
330            collect_nodes(body, children);
331            collect_nodes(catch_body, children);
332            if let Some(body) = finally_body {
333                collect_nodes(body, children);
334            }
335        }
336        Node::TryExpr { body }
337        | Node::SpawnExpr { body }
338        | Node::ScopeBlock { body }
339        | Node::DeferStmt { body }
340        | Node::Block(body) => collect_nodes(body, children),
341        Node::Closure { params, body, .. } => {
342            collect_typed_param_defaults(params, children);
343            collect_nodes(body, children);
344        }
345        Node::MutexBlock { key, body } => {
346            if let Some(key) = key {
347                children(key);
348            }
349            collect_nodes(body, children);
350        }
351        Node::FnDecl { params, body, .. } | Node::ToolDecl { params, body, .. } => {
352            collect_typed_param_defaults(params, children);
353            collect_nodes(body, children);
354        }
355        Node::SkillDecl { fields, .. } => collect_field_values(fields, children),
356        Node::EvalPackDecl {
357            fields,
358            body,
359            summarize,
360            ..
361        } => {
362            collect_field_values(fields, children);
363            collect_nodes(body, children);
364            if let Some(body) = summarize {
365                collect_nodes(body, children);
366            }
367        }
368        Node::RangeExpr { start, end, .. } => {
369            children(start);
370            children(end);
371        }
372        Node::GuardStmt {
373            condition,
374            else_body,
375        } => {
376            children(condition);
377            collect_nodes(else_body, children);
378        }
379        Node::RequireStmt { condition, message } => {
380            children(condition);
381            if let Some(message) = message {
382                children(message);
383            }
384        }
385        Node::DeadlineBlock { duration, body } => {
386            children(duration);
387            collect_nodes(body, children);
388        }
389        Node::EmitExpr { value }
390        | Node::ThrowStmt { value }
391        | Node::Spread(value)
392        | Node::TryOperator { operand: value }
393        | Node::TryStar { operand: value }
394        | Node::NonNullAssert { operand: value }
395        | Node::UnaryOp { operand: value, .. } => children(value),
396        Node::Parallel {
397            expr,
398            body,
399            options,
400            ..
401        } => {
402            children(expr);
403            collect_option_values(options, children);
404            collect_nodes(body, children);
405        }
406        Node::SelectExpr {
407            cases,
408            timeout,
409            default_body,
410        } => {
411            for case in cases {
412                collect_select_case(case, children);
413            }
414            if let Some((duration, body)) = timeout {
415                children(duration);
416                collect_nodes(body, children);
417            }
418            if let Some(body) = default_body {
419                collect_nodes(body, children);
420            }
421        }
422        Node::FunctionCall { args, .. } | Node::EnumConstruct { args, .. } => {
423            collect_nodes(args, children);
424        }
425        Node::ValueCall { callee, args } => {
426            children(callee);
427            collect_nodes(args, children);
428        }
429        Node::MethodCall { object, args, .. } | Node::OptionalMethodCall { object, args, .. } => {
430            children(object);
431            collect_nodes(args, children);
432        }
433        Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
434            children(object);
435        }
436        Node::SubscriptAccess { object, index }
437        | Node::OptionalSubscriptAccess { object, index } => {
438            children(object);
439            children(index);
440        }
441        Node::SliceAccess { object, start, end } => {
442            children(object);
443            if let Some(start) = start {
444                children(start);
445            }
446            if let Some(end) = end {
447                children(end);
448            }
449        }
450        Node::BinaryOp { left, right, .. } => {
451            children(left);
452            children(right);
453        }
454        Node::Ternary {
455            condition,
456            true_expr,
457            false_expr,
458        } => {
459            children(condition);
460            children(true_expr);
461            children(false_expr);
462        }
463        Node::Assignment { target, value, .. } => {
464            children(target);
465            children(value);
466        }
467        Node::StructConstruct { fields, .. } | Node::DictLiteral(fields) => {
468            collect_dict_entries(fields, children);
469        }
470        Node::ListLiteral(items) | Node::OrPattern(items) => collect_nodes(items, children),
471        Node::InterpolatedString(_)
472        | Node::StringLiteral(_)
473        | Node::RawStringLiteral(_)
474        | Node::IntLiteral(_)
475        | Node::FloatLiteral(_)
476        | Node::BoolLiteral(_)
477        | Node::NilLiteral
478        | Node::Identifier(_)
479        | Node::DurationLiteral(_) => {}
480    }
481}
482
483fn collect_nodes<'a>(nodes: &'a [SNode], children: &mut impl FnMut(&'a SNode)) {
484    for node in nodes {
485        children(node);
486    }
487}
488
489fn collect_dict_entries<'a>(entries: &'a [DictEntry], children: &mut impl FnMut(&'a SNode)) {
490    for entry in entries {
491        children(&entry.key);
492        children(&entry.value);
493    }
494}
495
496fn collect_field_values<'a>(fields: &'a [(String, SNode)], children: &mut impl FnMut(&'a SNode)) {
497    for (_, value) in fields {
498        children(value);
499    }
500}
501
502fn collect_option_values<'a>(options: &'a [(String, SNode)], children: &mut impl FnMut(&'a SNode)) {
503    for (_, value) in options {
504        children(value);
505    }
506}
507
508fn collect_typed_param_defaults<'a>(
509    params: &'a [TypedParam],
510    children: &mut impl FnMut(&'a SNode),
511) {
512    for param in params {
513        if let Some(default) = &param.default_value {
514            children(default);
515        }
516    }
517}
518
519fn collect_match_arm<'a>(arm: &'a MatchArm, children: &mut impl FnMut(&'a SNode)) {
520    children(&arm.pattern);
521    if let Some(guard) = &arm.guard {
522        children(guard);
523    }
524    collect_nodes(&arm.body, children);
525}
526
527fn collect_select_case<'a>(case: &'a SelectCase, children: &mut impl FnMut(&'a SNode)) {
528    children(&case.channel);
529    collect_nodes(&case.body, children);
530}
531
532fn collect_binding_pattern<'a>(pattern: &'a BindingPattern, children: &mut impl FnMut(&'a SNode)) {
533    match pattern {
534        BindingPattern::Identifier(_) | BindingPattern::Pair(_, _) => {}
535        BindingPattern::Dict(fields) => {
536            for field in fields {
537                if let Some(default) = &field.default_value {
538                    children(default);
539                }
540            }
541        }
542        BindingPattern::List(items) => {
543            for item in items {
544                if let Some(default) = &item.default_value {
545                    children(default);
546                }
547            }
548        }
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::ast::{spanned, BinaryOperator, Node, TypedParam, UnaryOperator};
556    use harn_lexer::Span;
557
558    fn dummy(node: Node) -> SNode {
559        spanned(node, Span::dummy())
560    }
561
562    #[test]
563    fn walk_program_preserves_preorder() {
564        let program = vec![dummy(Node::LetBinding {
565            pattern: BindingPattern::Identifier("x".to_string()),
566            type_ann: None,
567            value: Box::new(dummy(Node::BinaryOp {
568                op: BinaryOperator::Add,
569                left: Box::new(dummy(Node::IntLiteral(1))),
570                right: Box::new(dummy(Node::IntLiteral(2))),
571            })),
572            is_pub: false,
573        })];
574        let mut seen = Vec::new();
575
576        walk_program(&program, &mut |node| {
577            seen.push(match &node.node {
578                Node::LetBinding { .. } => "let",
579                Node::BinaryOp { .. } => "binary",
580                Node::IntLiteral(1) => "one",
581                Node::IntLiteral(2) => "two",
582                other => panic!("unexpected node {other:?}"),
583            });
584        });
585
586        assert_eq!(seen, vec!["let", "binary", "one", "two"]);
587    }
588
589    #[test]
590    fn identifier_receiver_access_predicate_ignores_function_calls() {
591        let plain = vec![dummy(Node::FunctionCall {
592            name: "helper".to_string(),
593            type_args: Vec::new(),
594            args: Vec::new(),
595        })];
596        assert!(!contains_identifier_receiver_access(&plain));
597
598        let qualified = vec![dummy(Node::PropertyAccess {
599            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
600            property: "Ready".to_string(),
601        })];
602        assert!(contains_identifier_receiver_access(&qualified));
603    }
604
605    #[test]
606    fn enum_pattern_predicate_ignores_ordinary_property_access() {
607        let ordinary = vec![dummy(Node::PropertyAccess {
608            object: Box::new(dummy(Node::Identifier("record".to_string()))),
609            property: "field".to_string(),
610        })];
611        assert!(!contains_identifier_enum_pattern(&ordinary));
612
613        let pattern = dummy(Node::PropertyAccess {
614            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
615            property: "Ready".to_string(),
616        });
617        let match_expr = dummy(Node::MatchExpr {
618            value: Box::new(dummy(Node::Identifier("value".to_string()))),
619            arms: vec![MatchArm {
620                pattern,
621                guard: None,
622                body: Vec::new(),
623                span: Span::dummy(),
624            }],
625        });
626        assert!(contains_identifier_enum_pattern(&[match_expr]));
627    }
628
629    #[test]
630    fn walk_node_handles_deep_unary_chain_iteratively() {
631        let mut node = dummy(Node::IntLiteral(0));
632        for _ in 0..10_000 {
633            node = dummy(Node::UnaryOp {
634                op: UnaryOperator::Not,
635                operand: Box::new(node),
636            });
637        }
638
639        let mut count = 0usize;
640        walk_node(&node, &mut |_| count += 1);
641
642        assert_eq!(count, 10_001);
643    }
644
645    #[test]
646    fn walk_node_visits_typed_param_defaults() {
647        let default = dummy(Node::Identifier("fallback".to_string()));
648        let node = dummy(Node::FnDecl {
649            name: "load".to_string(),
650            type_params: Vec::new(),
651            params: vec![TypedParam {
652                name: "root".to_string(),
653                type_expr: None,
654                default_value: Some(Box::new(default)),
655                rest: false,
656                span: harn_lexer::Span::dummy(),
657            }],
658            type_predicate: None,
659            return_type: None,
660            throws: None,
661            where_clauses: Vec::new(),
662            body: Vec::new(),
663            is_pub: false,
664            is_stream: false,
665        });
666        let mut seen = Vec::new();
667
668        walk_node(&node, &mut |node| {
669            if let Node::Identifier(name) = &node.node {
670                seen.push(name.clone());
671            }
672        });
673
674        assert_eq!(seen, vec!["fallback"]);
675    }
676}