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::Parallel {
351            expr,
352            body,
353            options,
354            ..
355        } => {
356            children(expr);
357            collect_option_values(options, children);
358            collect_nodes(body, children);
359        }
360        Node::SelectExpr {
361            cases,
362            timeout,
363            default_body,
364        } => {
365            for case in cases {
366                collect_select_case(case, children);
367            }
368            if let Some((duration, body)) = timeout {
369                children(duration);
370                collect_nodes(body, children);
371            }
372            if let Some(body) = default_body {
373                collect_nodes(body, children);
374            }
375        }
376        Node::FunctionCall { args, .. } | Node::EnumConstruct { args, .. } => {
377            collect_nodes(args, children);
378        }
379        Node::ValueCall { callee, args } => {
380            children(callee);
381            collect_nodes(args, children);
382        }
383        Node::MethodCall { object, args, .. } | Node::OptionalMethodCall { object, args, .. } => {
384            children(object);
385            collect_nodes(args, children);
386        }
387        Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
388            children(object);
389        }
390        Node::SubscriptAccess { object, index }
391        | Node::OptionalSubscriptAccess { object, index } => {
392            children(object);
393            children(index);
394        }
395        Node::SliceAccess { object, start, end } => {
396            children(object);
397            if let Some(start) = start {
398                children(start);
399            }
400            if let Some(end) = end {
401                children(end);
402            }
403        }
404        Node::BinaryOp { left, right, .. } => {
405            children(left);
406            children(right);
407        }
408        Node::Ternary {
409            condition,
410            true_expr,
411            false_expr,
412        } => {
413            children(condition);
414            children(true_expr);
415            children(false_expr);
416        }
417        Node::Assignment { target, value, .. } => {
418            children(target);
419            children(value);
420        }
421        Node::StructConstruct { fields, .. } | Node::DictLiteral(fields) => {
422            collect_dict_entries(fields, children);
423        }
424        Node::ListLiteral(items) | Node::OrPattern(items) => collect_nodes(items, children),
425        Node::InterpolatedString(_)
426        | Node::StringLiteral(_)
427        | Node::RawStringLiteral(_)
428        | Node::IntLiteral(_)
429        | Node::FloatLiteral(_)
430        | Node::BoolLiteral(_)
431        | Node::NilLiteral
432        | Node::Identifier(_)
433        | Node::DurationLiteral(_) => {}
434    }
435}
436
437fn collect_nodes<'a>(nodes: &'a [SNode], children: &mut impl FnMut(&'a SNode)) {
438    for node in nodes {
439        children(node);
440    }
441}
442
443fn collect_dict_entries<'a>(entries: &'a [DictEntry], children: &mut impl FnMut(&'a SNode)) {
444    for entry in entries {
445        children(&entry.key);
446        children(&entry.value);
447    }
448}
449
450fn collect_field_values<'a>(fields: &'a [(String, SNode)], children: &mut impl FnMut(&'a SNode)) {
451    for (_, value) in fields {
452        children(value);
453    }
454}
455
456fn collect_option_values<'a>(options: &'a [(String, SNode)], children: &mut impl FnMut(&'a SNode)) {
457    for (_, value) in options {
458        children(value);
459    }
460}
461
462fn collect_typed_param_defaults<'a>(
463    params: &'a [TypedParam],
464    children: &mut impl FnMut(&'a SNode),
465) {
466    for param in params {
467        if let Some(default) = &param.default_value {
468            children(default);
469        }
470    }
471}
472
473fn collect_match_arm<'a>(arm: &'a MatchArm, children: &mut impl FnMut(&'a SNode)) {
474    children(&arm.pattern);
475    if let Some(guard) = &arm.guard {
476        children(guard);
477    }
478    collect_nodes(&arm.body, children);
479}
480
481fn collect_select_case<'a>(case: &'a SelectCase, children: &mut impl FnMut(&'a SNode)) {
482    children(&case.channel);
483    collect_nodes(&case.body, children);
484}
485
486fn collect_binding_pattern<'a>(pattern: &'a BindingPattern, children: &mut impl FnMut(&'a SNode)) {
487    match pattern {
488        BindingPattern::Identifier(_) | BindingPattern::Pair(_, _) => {}
489        BindingPattern::Dict(fields) => {
490            for field in fields {
491                if let Some(default) = &field.default_value {
492                    children(default);
493                }
494            }
495        }
496        BindingPattern::List(items) => {
497            for item in items {
498                if let Some(default) = &item.default_value {
499                    children(default);
500                }
501            }
502        }
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use crate::ast::{spanned, Node, TypedParam};
510    use harn_lexer::Span;
511
512    fn dummy(node: Node) -> SNode {
513        spanned(node, Span::dummy())
514    }
515
516    #[test]
517    fn walk_program_preserves_preorder() {
518        let program = vec![dummy(Node::LetBinding {
519            pattern: BindingPattern::Identifier("x".to_string()),
520            type_ann: None,
521            value: Box::new(dummy(Node::BinaryOp {
522                op: "+".to_string(),
523                left: Box::new(dummy(Node::IntLiteral(1))),
524                right: Box::new(dummy(Node::IntLiteral(2))),
525            })),
526            is_pub: false,
527        })];
528        let mut seen = Vec::new();
529
530        walk_program(&program, &mut |node| {
531            seen.push(match &node.node {
532                Node::LetBinding { .. } => "let",
533                Node::BinaryOp { .. } => "binary",
534                Node::IntLiteral(1) => "one",
535                Node::IntLiteral(2) => "two",
536                other => panic!("unexpected node {other:?}"),
537            });
538        });
539
540        assert_eq!(seen, vec!["let", "binary", "one", "two"]);
541    }
542
543    #[test]
544    fn identifier_receiver_access_predicate_ignores_function_calls() {
545        let plain = vec![dummy(Node::FunctionCall {
546            name: "helper".to_string(),
547            type_args: Vec::new(),
548            args: Vec::new(),
549        })];
550        assert!(!contains_identifier_receiver_access(&plain));
551
552        let qualified = vec![dummy(Node::PropertyAccess {
553            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
554            property: "Ready".to_string(),
555        })];
556        assert!(contains_identifier_receiver_access(&qualified));
557    }
558
559    #[test]
560    fn enum_pattern_predicate_ignores_ordinary_property_access() {
561        let ordinary = vec![dummy(Node::PropertyAccess {
562            object: Box::new(dummy(Node::Identifier("record".to_string()))),
563            property: "field".to_string(),
564        })];
565        assert!(!contains_identifier_enum_pattern(&ordinary));
566
567        let pattern = dummy(Node::PropertyAccess {
568            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
569            property: "Ready".to_string(),
570        });
571        let match_expr = dummy(Node::MatchExpr {
572            value: Box::new(dummy(Node::Identifier("value".to_string()))),
573            arms: vec![MatchArm {
574                pattern,
575                guard: None,
576                body: Vec::new(),
577                span: Span::dummy(),
578            }],
579        });
580        assert!(contains_identifier_enum_pattern(&[match_expr]));
581    }
582
583    #[test]
584    fn walk_node_handles_deep_unary_chain_iteratively() {
585        let mut node = dummy(Node::IntLiteral(0));
586        for _ in 0..10_000 {
587            node = dummy(Node::UnaryOp {
588                op: "!".to_string(),
589                operand: Box::new(node),
590            });
591        }
592
593        let mut count = 0usize;
594        walk_node(&node, &mut |_| count += 1);
595
596        assert_eq!(count, 10_001);
597    }
598
599    #[test]
600    fn walk_node_visits_typed_param_defaults() {
601        let default = dummy(Node::Identifier("fallback".to_string()));
602        let node = dummy(Node::FnDecl {
603            name: "load".to_string(),
604            type_params: Vec::new(),
605            params: vec![TypedParam {
606                name: "root".to_string(),
607                type_expr: None,
608                default_value: Some(Box::new(default)),
609                rest: false,
610                span: harn_lexer::Span::dummy(),
611            }],
612            type_predicate: None,
613            return_type: None,
614            throws: None,
615            where_clauses: Vec::new(),
616            body: Vec::new(),
617            is_pub: false,
618            is_stream: false,
619        });
620        let mut seen = Vec::new();
621
622        walk_node(&node, &mut |node| {
623            if let Node::Identifier(name) = &node.node {
624                seen.push(name.clone());
625            }
626        });
627
628        assert_eq!(seen, vec!["fallback"]);
629    }
630}