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