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