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    let mut found = false;
90    walk_program(program, &mut |node| {
91        let object = match &node.node {
92            Node::PropertyAccess { object, .. }
93            | Node::OptionalPropertyAccess { object, .. }
94            | Node::MethodCall { object, .. }
95            | Node::OptionalMethodCall { object, .. } => object,
96            _ => return,
97        };
98        found |= matches!(&object.node, Node::Identifier(_));
99    });
100    found
101}
102
103/// Return whether a program contains an enum-shaped match pattern whose
104/// receiver is a bare identifier (`Status.Ready` or `Status.Error(value)`).
105///
106/// Ordinary property access does not need enum metadata: the runtime module
107/// namespace supplies the same value through normal property lookup. The
108/// compiler only needs the imported-enum catalog when lowering these match
109/// patterns, where a dotted expression is otherwise indistinguishable from a
110/// value comparison. Keeping this predicate pattern-specific avoids forcing a
111/// full import-graph walk on modules that merely use record or namespace
112/// property access.
113pub fn contains_identifier_enum_pattern(program: &[SNode]) -> bool {
114    let mut found = false;
115    walk_program(program, &mut |node| {
116        let Node::MatchExpr { arms, .. } = &node.node else {
117            return;
118        };
119        found |= arms
120            .iter()
121            .any(|arm| contains_identifier_enum_pattern_node(&arm.pattern));
122    });
123    found
124}
125
126fn contains_identifier_enum_pattern_node(node: &SNode) -> bool {
127    match &node.node {
128        Node::PropertyAccess { object, .. } | Node::MethodCall { object, .. } => {
129            matches!(&object.node, Node::Identifier(_))
130        }
131        Node::OrPattern(patterns) => patterns.iter().any(contains_identifier_enum_pattern_node),
132        _ => false,
133    }
134}
135
136/// Visit `node`, then recurse into its children.
137pub fn walk_node(node: &SNode, visitor: &mut impl FnMut(&SNode)) {
138    let mut stack = vec![node];
139    walk_stack(&mut stack, visitor);
140}
141
142/// Recurse into `node`'s children without re-visiting `node` itself.
143/// Useful when a caller wants to handle the parent specially and then
144/// continue the default traversal.
145pub fn walk_children(node: &SNode, visitor: &mut impl FnMut(&SNode)) {
146    let mut stack = Vec::new();
147    push_children_reversed(node, &mut stack);
148    walk_stack(&mut stack, visitor);
149}
150
151fn walk_stack(stack: &mut Vec<&SNode>, visitor: &mut impl FnMut(&SNode)) {
152    while let Some(node) = stack.pop() {
153        visitor(node);
154        push_children_reversed(node, stack);
155    }
156}
157
158fn push_children_reversed<'a>(node: &'a SNode, stack: &mut Vec<&'a SNode>) {
159    let mut children = Vec::new();
160    collect_children(node, &mut children);
161    stack.extend(children.into_iter().rev());
162}
163
164fn push_nodes_reversed<'a>(nodes: &'a [SNode], stack: &mut Vec<&'a SNode>) {
165    stack.extend(nodes.iter().rev());
166}
167
168/// Collect `node`'s immediate children without recursing. Lets callers walk
169/// selectively (e.g. stop descending at nested loops) while still relying on
170/// this module's single source of truth for each variant's children.
171pub fn immediate_children(node: &SNode) -> Vec<&SNode> {
172    let mut children = Vec::new();
173    collect_children(node, &mut children);
174    children
175}
176
177fn collect_children<'a>(node: &'a SNode, children: &mut Vec<&'a SNode>) {
178    match &node.node {
179        Node::AttributedDecl { attributes, inner } => {
180            for attr in attributes {
181                for arg in &attr.args {
182                    children.push(&arg.value);
183                }
184            }
185            children.push(inner);
186        }
187        Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
188            collect_nodes(body, children);
189        }
190        Node::LetBinding { pattern, value, .. } | Node::ConstBinding { pattern, value, .. } => {
191            collect_binding_pattern(pattern, children);
192            children.push(value);
193        }
194        Node::EnumDecl { variants, .. } => {
195            for variant in variants {
196                collect_typed_param_defaults(&variant.fields, children);
197            }
198        }
199        Node::StructDecl { .. }
200        | Node::ImportDecl { .. }
201        | Node::SelectiveImport { .. }
202        | Node::NamespaceImport { .. }
203        | Node::TypeDecl { .. }
204        | Node::BreakStmt
205        | Node::ContinueStmt => {}
206        Node::InterfaceDecl { methods, .. } => {
207            for method in methods {
208                collect_typed_param_defaults(&method.params, children);
209            }
210        }
211        Node::ImplBlock { methods, .. } => collect_nodes(methods, children),
212        Node::IfElse {
213            condition,
214            then_body,
215            else_body,
216            ..
217        } => {
218            children.push(condition);
219            collect_nodes(then_body, children);
220            if let Some(body) = else_body {
221                collect_nodes(body, children);
222            }
223        }
224        Node::ForIn {
225            pattern,
226            iterable,
227            body,
228        } => {
229            collect_binding_pattern(pattern, children);
230            children.push(iterable);
231            collect_nodes(body, children);
232        }
233        Node::MatchExpr { value, arms } => {
234            children.push(value);
235            for arm in arms {
236                collect_match_arm(arm, children);
237            }
238        }
239        Node::WhileLoop { condition, body } => {
240            children.push(condition);
241            collect_nodes(body, children);
242        }
243        Node::Retry { count, body } => {
244            children.push(count);
245            collect_nodes(body, children);
246        }
247        Node::CostRoute { options, body } => {
248            collect_option_values(options, children);
249            collect_nodes(body, children);
250        }
251        Node::ReturnStmt { value } | Node::YieldExpr { value } => {
252            if let Some(value) = value {
253                children.push(value);
254            }
255        }
256        Node::TryCatch {
257            has_catch: _,
258            body,
259            catch_body,
260            finally_body,
261            ..
262        } => {
263            collect_nodes(body, children);
264            collect_nodes(catch_body, children);
265            if let Some(body) = finally_body {
266                collect_nodes(body, children);
267            }
268        }
269        Node::TryExpr { body }
270        | Node::SpawnExpr { body }
271        | Node::ScopeBlock { body }
272        | Node::DeferStmt { body }
273        | Node::Block(body) => collect_nodes(body, children),
274        Node::Closure { params, body, .. } => {
275            collect_typed_param_defaults(params, children);
276            collect_nodes(body, children);
277        }
278        Node::MutexBlock { key, body } => {
279            if let Some(key) = key {
280                children.push(key);
281            }
282            collect_nodes(body, children);
283        }
284        Node::FnDecl { params, body, .. } | Node::ToolDecl { params, body, .. } => {
285            collect_typed_param_defaults(params, children);
286            collect_nodes(body, children);
287        }
288        Node::SkillDecl { fields, .. } => collect_field_values(fields, children),
289        Node::EvalPackDecl {
290            fields,
291            body,
292            summarize,
293            ..
294        } => {
295            collect_field_values(fields, children);
296            collect_nodes(body, children);
297            if let Some(body) = summarize {
298                collect_nodes(body, children);
299            }
300        }
301        Node::RangeExpr { start, end, .. } => {
302            children.push(start);
303            children.push(end);
304        }
305        Node::GuardStmt {
306            condition,
307            else_body,
308        } => {
309            children.push(condition);
310            collect_nodes(else_body, children);
311        }
312        Node::RequireStmt { condition, message } => {
313            children.push(condition);
314            if let Some(message) = message {
315                children.push(message);
316            }
317        }
318        Node::DeadlineBlock { duration, body } => {
319            children.push(duration);
320            collect_nodes(body, children);
321        }
322        Node::EmitExpr { value }
323        | Node::ThrowStmt { value }
324        | Node::Spread(value)
325        | Node::TryOperator { operand: value }
326        | Node::TryStar { operand: value }
327        | Node::NonNullAssert { operand: value }
328        | Node::UnaryOp { operand: value, .. } => children.push(value),
329        Node::HitlExpr { args, .. } => {
330            for arg in args {
331                children.push(&arg.value);
332            }
333        }
334        Node::Parallel {
335            expr,
336            body,
337            options,
338            ..
339        } => {
340            children.push(expr);
341            collect_option_values(options, children);
342            collect_nodes(body, children);
343        }
344        Node::SelectExpr {
345            cases,
346            timeout,
347            default_body,
348        } => {
349            for case in cases {
350                collect_select_case(case, children);
351            }
352            if let Some((duration, body)) = timeout {
353                children.push(duration);
354                collect_nodes(body, children);
355            }
356            if let Some(body) = default_body {
357                collect_nodes(body, children);
358            }
359        }
360        Node::FunctionCall { args, .. } | Node::EnumConstruct { args, .. } => {
361            collect_nodes(args, children);
362        }
363        Node::ValueCall { callee, args } => {
364            children.push(callee);
365            collect_nodes(args, children);
366        }
367        Node::MethodCall { object, args, .. } | Node::OptionalMethodCall { object, args, .. } => {
368            children.push(object);
369            collect_nodes(args, children);
370        }
371        Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
372            children.push(object);
373        }
374        Node::SubscriptAccess { object, index }
375        | Node::OptionalSubscriptAccess { object, index } => {
376            children.push(object);
377            children.push(index);
378        }
379        Node::SliceAccess { object, start, end } => {
380            children.push(object);
381            if let Some(start) = start {
382                children.push(start);
383            }
384            if let Some(end) = end {
385                children.push(end);
386            }
387        }
388        Node::BinaryOp { left, right, .. } => {
389            children.push(left);
390            children.push(right);
391        }
392        Node::Ternary {
393            condition,
394            true_expr,
395            false_expr,
396        } => {
397            children.push(condition);
398            children.push(true_expr);
399            children.push(false_expr);
400        }
401        Node::Assignment { target, value, .. } => {
402            children.push(target);
403            children.push(value);
404        }
405        Node::StructConstruct { fields, .. } | Node::DictLiteral(fields) => {
406            collect_dict_entries(fields, children);
407        }
408        Node::ListLiteral(items) | Node::OrPattern(items) => collect_nodes(items, children),
409        Node::InterpolatedString(_)
410        | Node::StringLiteral(_)
411        | Node::RawStringLiteral(_)
412        | Node::IntLiteral(_)
413        | Node::FloatLiteral(_)
414        | Node::BoolLiteral(_)
415        | Node::NilLiteral
416        | Node::Identifier(_)
417        | Node::DurationLiteral(_) => {}
418    }
419}
420
421fn collect_nodes<'a>(nodes: &'a [SNode], children: &mut Vec<&'a SNode>) {
422    children.extend(nodes.iter());
423}
424
425fn collect_dict_entries<'a>(entries: &'a [DictEntry], children: &mut Vec<&'a SNode>) {
426    for entry in entries {
427        children.push(&entry.key);
428        children.push(&entry.value);
429    }
430}
431
432fn collect_field_values<'a>(fields: &'a [(String, SNode)], children: &mut Vec<&'a SNode>) {
433    for (_, value) in fields {
434        children.push(value);
435    }
436}
437
438fn collect_option_values<'a>(options: &'a [(String, SNode)], children: &mut Vec<&'a SNode>) {
439    for (_, value) in options {
440        children.push(value);
441    }
442}
443
444fn collect_typed_param_defaults<'a>(params: &'a [TypedParam], children: &mut Vec<&'a SNode>) {
445    for param in params {
446        if let Some(default) = &param.default_value {
447            children.push(default);
448        }
449    }
450}
451
452fn collect_match_arm<'a>(arm: &'a MatchArm, children: &mut Vec<&'a SNode>) {
453    children.push(&arm.pattern);
454    if let Some(guard) = &arm.guard {
455        children.push(guard);
456    }
457    collect_nodes(&arm.body, children);
458}
459
460fn collect_select_case<'a>(case: &'a SelectCase, children: &mut Vec<&'a SNode>) {
461    children.push(&case.channel);
462    collect_nodes(&case.body, children);
463}
464
465fn collect_binding_pattern<'a>(pattern: &'a BindingPattern, children: &mut Vec<&'a SNode>) {
466    match pattern {
467        BindingPattern::Identifier(_) | BindingPattern::Pair(_, _) => {}
468        BindingPattern::Dict(fields) => {
469            for field in fields {
470                if let Some(default) = &field.default_value {
471                    children.push(default);
472                }
473            }
474        }
475        BindingPattern::List(items) => {
476            for item in items {
477                if let Some(default) = &item.default_value {
478                    children.push(default);
479                }
480            }
481        }
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488    use crate::ast::{spanned, Node, TypedParam};
489    use harn_lexer::Span;
490
491    fn dummy(node: Node) -> SNode {
492        spanned(node, Span::dummy())
493    }
494
495    #[test]
496    fn walk_program_preserves_preorder() {
497        let program = vec![dummy(Node::LetBinding {
498            pattern: BindingPattern::Identifier("x".to_string()),
499            type_ann: None,
500            value: Box::new(dummy(Node::BinaryOp {
501                op: "+".to_string(),
502                left: Box::new(dummy(Node::IntLiteral(1))),
503                right: Box::new(dummy(Node::IntLiteral(2))),
504            })),
505            is_pub: false,
506        })];
507        let mut seen = Vec::new();
508
509        walk_program(&program, &mut |node| {
510            seen.push(match &node.node {
511                Node::LetBinding { .. } => "let",
512                Node::BinaryOp { .. } => "binary",
513                Node::IntLiteral(1) => "one",
514                Node::IntLiteral(2) => "two",
515                other => panic!("unexpected node {other:?}"),
516            });
517        });
518
519        assert_eq!(seen, vec!["let", "binary", "one", "two"]);
520    }
521
522    #[test]
523    fn identifier_receiver_access_predicate_ignores_function_calls() {
524        let plain = vec![dummy(Node::FunctionCall {
525            name: "helper".to_string(),
526            type_args: Vec::new(),
527            args: Vec::new(),
528        })];
529        assert!(!contains_identifier_receiver_access(&plain));
530
531        let qualified = vec![dummy(Node::PropertyAccess {
532            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
533            property: "Ready".to_string(),
534        })];
535        assert!(contains_identifier_receiver_access(&qualified));
536    }
537
538    #[test]
539    fn enum_pattern_predicate_ignores_ordinary_property_access() {
540        let ordinary = vec![dummy(Node::PropertyAccess {
541            object: Box::new(dummy(Node::Identifier("record".to_string()))),
542            property: "field".to_string(),
543        })];
544        assert!(!contains_identifier_enum_pattern(&ordinary));
545
546        let pattern = dummy(Node::PropertyAccess {
547            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
548            property: "Ready".to_string(),
549        });
550        let match_expr = dummy(Node::MatchExpr {
551            value: Box::new(dummy(Node::Identifier("value".to_string()))),
552            arms: vec![MatchArm {
553                pattern,
554                guard: None,
555                body: Vec::new(),
556                span: Span::dummy(),
557            }],
558        });
559        assert!(contains_identifier_enum_pattern(&[match_expr]));
560    }
561
562    #[test]
563    fn walk_node_handles_deep_unary_chain_iteratively() {
564        let mut node = dummy(Node::IntLiteral(0));
565        for _ in 0..10_000 {
566            node = dummy(Node::UnaryOp {
567                op: "!".to_string(),
568                operand: Box::new(node),
569            });
570        }
571
572        let mut count = 0usize;
573        walk_node(&node, &mut |_| count += 1);
574
575        assert_eq!(count, 10_001);
576    }
577
578    #[test]
579    fn walk_node_visits_typed_param_defaults() {
580        let default = dummy(Node::Identifier("fallback".to_string()));
581        let node = dummy(Node::FnDecl {
582            name: "load".to_string(),
583            type_params: Vec::new(),
584            params: vec![TypedParam {
585                name: "root".to_string(),
586                type_expr: None,
587                default_value: Some(Box::new(default)),
588                rest: false,
589                span: harn_lexer::Span::dummy(),
590            }],
591            return_type: None,
592            throws: None,
593            where_clauses: Vec::new(),
594            body: Vec::new(),
595            is_pub: false,
596            is_stream: false,
597        });
598        let mut seen = Vec::new();
599
600        walk_node(&node, &mut |node| {
601            if let Node::Identifier(name) = &node.node {
602                seen.push(name.clone());
603            }
604        });
605
606        assert_eq!(seen, vec!["fallback"]);
607    }
608}