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::TypeDecl { .. }
154        | Node::BreakStmt
155        | Node::ContinueStmt => {}
156        Node::InterfaceDecl { methods, .. } => {
157            for method in methods {
158                collect_typed_param_defaults(&method.params, children);
159            }
160        }
161        Node::ImplBlock { methods, .. } => collect_nodes(methods, children),
162        Node::IfElse {
163            condition,
164            then_body,
165            else_body,
166            ..
167        } => {
168            children.push(condition);
169            collect_nodes(then_body, children);
170            if let Some(body) = else_body {
171                collect_nodes(body, children);
172            }
173        }
174        Node::ForIn {
175            pattern,
176            iterable,
177            body,
178        } => {
179            collect_binding_pattern(pattern, children);
180            children.push(iterable);
181            collect_nodes(body, children);
182        }
183        Node::MatchExpr { value, arms } => {
184            children.push(value);
185            for arm in arms {
186                collect_match_arm(arm, children);
187            }
188        }
189        Node::WhileLoop { condition, body } => {
190            children.push(condition);
191            collect_nodes(body, children);
192        }
193        Node::Retry { count, body } => {
194            children.push(count);
195            collect_nodes(body, children);
196        }
197        Node::CostRoute { options, body } => {
198            collect_option_values(options, children);
199            collect_nodes(body, children);
200        }
201        Node::ReturnStmt { value } | Node::YieldExpr { value } => {
202            if let Some(value) = value {
203                children.push(value);
204            }
205        }
206        Node::TryCatch {
207            has_catch: _,
208            body,
209            catch_body,
210            finally_body,
211            ..
212        } => {
213            collect_nodes(body, children);
214            collect_nodes(catch_body, children);
215            if let Some(body) = finally_body {
216                collect_nodes(body, children);
217            }
218        }
219        Node::TryExpr { body }
220        | Node::SpawnExpr { body }
221        | Node::ScopeBlock { body }
222        | Node::DeferStmt { body }
223        | Node::Block(body) => collect_nodes(body, children),
224        Node::Closure { params, body, .. } => {
225            collect_typed_param_defaults(params, children);
226            collect_nodes(body, children);
227        }
228        Node::MutexBlock { key, body } => {
229            if let Some(key) = key {
230                children.push(key);
231            }
232            collect_nodes(body, children);
233        }
234        Node::FnDecl { params, body, .. } | Node::ToolDecl { params, body, .. } => {
235            collect_typed_param_defaults(params, children);
236            collect_nodes(body, children);
237        }
238        Node::SkillDecl { fields, .. } => collect_field_values(fields, children),
239        Node::EvalPackDecl {
240            fields,
241            body,
242            summarize,
243            ..
244        } => {
245            collect_field_values(fields, children);
246            collect_nodes(body, children);
247            if let Some(body) = summarize {
248                collect_nodes(body, children);
249            }
250        }
251        Node::RangeExpr { start, end, .. } => {
252            children.push(start);
253            children.push(end);
254        }
255        Node::GuardStmt {
256            condition,
257            else_body,
258        } => {
259            children.push(condition);
260            collect_nodes(else_body, children);
261        }
262        Node::RequireStmt { condition, message } => {
263            children.push(condition);
264            if let Some(message) = message {
265                children.push(message);
266            }
267        }
268        Node::DeadlineBlock { duration, body } => {
269            children.push(duration);
270            collect_nodes(body, children);
271        }
272        Node::EmitExpr { value }
273        | Node::ThrowStmt { value }
274        | Node::Spread(value)
275        | Node::TryOperator { operand: value }
276        | Node::TryStar { operand: value }
277        | Node::NonNullAssert { operand: value }
278        | Node::UnaryOp { operand: value, .. } => children.push(value),
279        Node::HitlExpr { args, .. } => {
280            for arg in args {
281                children.push(&arg.value);
282            }
283        }
284        Node::Parallel {
285            expr,
286            body,
287            options,
288            ..
289        } => {
290            children.push(expr);
291            collect_option_values(options, children);
292            collect_nodes(body, children);
293        }
294        Node::SelectExpr {
295            cases,
296            timeout,
297            default_body,
298        } => {
299            for case in cases {
300                collect_select_case(case, children);
301            }
302            if let Some((duration, body)) = timeout {
303                children.push(duration);
304                collect_nodes(body, children);
305            }
306            if let Some(body) = default_body {
307                collect_nodes(body, children);
308            }
309        }
310        Node::FunctionCall { args, .. } | Node::EnumConstruct { args, .. } => {
311            collect_nodes(args, children);
312        }
313        Node::ValueCall { callee, args } => {
314            children.push(callee);
315            collect_nodes(args, children);
316        }
317        Node::MethodCall { object, args, .. } | Node::OptionalMethodCall { object, args, .. } => {
318            children.push(object);
319            collect_nodes(args, children);
320        }
321        Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
322            children.push(object);
323        }
324        Node::SubscriptAccess { object, index }
325        | Node::OptionalSubscriptAccess { object, index } => {
326            children.push(object);
327            children.push(index);
328        }
329        Node::SliceAccess { object, start, end } => {
330            children.push(object);
331            if let Some(start) = start {
332                children.push(start);
333            }
334            if let Some(end) = end {
335                children.push(end);
336            }
337        }
338        Node::BinaryOp { left, right, .. } => {
339            children.push(left);
340            children.push(right);
341        }
342        Node::Ternary {
343            condition,
344            true_expr,
345            false_expr,
346        } => {
347            children.push(condition);
348            children.push(true_expr);
349            children.push(false_expr);
350        }
351        Node::Assignment { target, value, .. } => {
352            children.push(target);
353            children.push(value);
354        }
355        Node::StructConstruct { fields, .. } | Node::DictLiteral(fields) => {
356            collect_dict_entries(fields, children);
357        }
358        Node::ListLiteral(items) | Node::OrPattern(items) => collect_nodes(items, children),
359        Node::InterpolatedString(_)
360        | Node::StringLiteral(_)
361        | Node::RawStringLiteral(_)
362        | Node::IntLiteral(_)
363        | Node::FloatLiteral(_)
364        | Node::BoolLiteral(_)
365        | Node::NilLiteral
366        | Node::Identifier(_)
367        | Node::DurationLiteral(_) => {}
368    }
369}
370
371fn collect_nodes<'a>(nodes: &'a [SNode], children: &mut Vec<&'a SNode>) {
372    children.extend(nodes.iter());
373}
374
375fn collect_dict_entries<'a>(entries: &'a [DictEntry], children: &mut Vec<&'a SNode>) {
376    for entry in entries {
377        children.push(&entry.key);
378        children.push(&entry.value);
379    }
380}
381
382fn collect_field_values<'a>(fields: &'a [(String, SNode)], children: &mut Vec<&'a SNode>) {
383    for (_, value) in fields {
384        children.push(value);
385    }
386}
387
388fn collect_option_values<'a>(options: &'a [(String, SNode)], children: &mut Vec<&'a SNode>) {
389    for (_, value) in options {
390        children.push(value);
391    }
392}
393
394fn collect_typed_param_defaults<'a>(params: &'a [TypedParam], children: &mut Vec<&'a SNode>) {
395    for param in params {
396        if let Some(default) = &param.default_value {
397            children.push(default);
398        }
399    }
400}
401
402fn collect_match_arm<'a>(arm: &'a MatchArm, children: &mut Vec<&'a SNode>) {
403    children.push(&arm.pattern);
404    if let Some(guard) = &arm.guard {
405        children.push(guard);
406    }
407    collect_nodes(&arm.body, children);
408}
409
410fn collect_select_case<'a>(case: &'a SelectCase, children: &mut Vec<&'a SNode>) {
411    children.push(&case.channel);
412    collect_nodes(&case.body, children);
413}
414
415fn collect_binding_pattern<'a>(pattern: &'a BindingPattern, children: &mut Vec<&'a SNode>) {
416    match pattern {
417        BindingPattern::Identifier(_) | BindingPattern::Pair(_, _) => {}
418        BindingPattern::Dict(fields) => {
419            for field in fields {
420                if let Some(default) = &field.default_value {
421                    children.push(default);
422                }
423            }
424        }
425        BindingPattern::List(items) => {
426            for item in items {
427                if let Some(default) = &item.default_value {
428                    children.push(default);
429                }
430            }
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::ast::{spanned, Node, TypedParam};
439    use harn_lexer::Span;
440
441    fn dummy(node: Node) -> SNode {
442        spanned(node, Span::dummy())
443    }
444
445    #[test]
446    fn walk_program_preserves_preorder() {
447        let program = vec![dummy(Node::LetBinding {
448            pattern: BindingPattern::Identifier("x".to_string()),
449            type_ann: None,
450            value: Box::new(dummy(Node::BinaryOp {
451                op: "+".to_string(),
452                left: Box::new(dummy(Node::IntLiteral(1))),
453                right: Box::new(dummy(Node::IntLiteral(2))),
454            })),
455            is_pub: false,
456        })];
457        let mut seen = Vec::new();
458
459        walk_program(&program, &mut |node| {
460            seen.push(match &node.node {
461                Node::LetBinding { .. } => "let",
462                Node::BinaryOp { .. } => "binary",
463                Node::IntLiteral(1) => "one",
464                Node::IntLiteral(2) => "two",
465                other => panic!("unexpected node {other:?}"),
466            });
467        });
468
469        assert_eq!(seen, vec!["let", "binary", "one", "two"]);
470    }
471
472    #[test]
473    fn identifier_receiver_access_predicate_ignores_function_calls() {
474        let plain = vec![dummy(Node::FunctionCall {
475            name: "helper".to_string(),
476            type_args: Vec::new(),
477            args: Vec::new(),
478        })];
479        assert!(!contains_identifier_receiver_access(&plain));
480
481        let qualified = vec![dummy(Node::PropertyAccess {
482            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
483            property: "Ready".to_string(),
484        })];
485        assert!(contains_identifier_receiver_access(&qualified));
486    }
487
488    #[test]
489    fn enum_pattern_predicate_ignores_ordinary_property_access() {
490        let ordinary = vec![dummy(Node::PropertyAccess {
491            object: Box::new(dummy(Node::Identifier("record".to_string()))),
492            property: "field".to_string(),
493        })];
494        assert!(!contains_identifier_enum_pattern(&ordinary));
495
496        let pattern = dummy(Node::PropertyAccess {
497            object: Box::new(dummy(Node::Identifier("Status".to_string()))),
498            property: "Ready".to_string(),
499        });
500        let match_expr = dummy(Node::MatchExpr {
501            value: Box::new(dummy(Node::Identifier("value".to_string()))),
502            arms: vec![MatchArm {
503                pattern,
504                guard: None,
505                body: Vec::new(),
506                span: Span::dummy(),
507            }],
508        });
509        assert!(contains_identifier_enum_pattern(&[match_expr]));
510    }
511
512    #[test]
513    fn walk_node_handles_deep_unary_chain_iteratively() {
514        let mut node = dummy(Node::IntLiteral(0));
515        for _ in 0..10_000 {
516            node = dummy(Node::UnaryOp {
517                op: "!".to_string(),
518                operand: Box::new(node),
519            });
520        }
521
522        let mut count = 0usize;
523        walk_node(&node, &mut |_| count += 1);
524
525        assert_eq!(count, 10_001);
526    }
527
528    #[test]
529    fn walk_node_visits_typed_param_defaults() {
530        let default = dummy(Node::Identifier("fallback".to_string()));
531        let node = dummy(Node::FnDecl {
532            name: "load".to_string(),
533            type_params: Vec::new(),
534            params: vec![TypedParam {
535                name: "root".to_string(),
536                type_expr: None,
537                default_value: Some(Box::new(default)),
538                rest: false,
539            }],
540            return_type: None,
541            throws: None,
542            where_clauses: Vec::new(),
543            body: Vec::new(),
544            is_pub: false,
545            is_stream: false,
546        });
547        let mut seen = Vec::new();
548
549        walk_node(&node, &mut |node| {
550            if let Node::Identifier(name) = &node.node {
551                seen.push(name.clone());
552            }
553        });
554
555        assert_eq!(seen, vec!["fallback"]);
556    }
557}