Skip to main content

harn_parser/
lexical.rs

1//! Lexical binding and capture analysis shared by compiler and typechecker.
2//!
3//! AST visitors answer structural questions. Capture analysis is different: an
4//! identifier only captures a binding when it resolves outside the callable
5//! that contains the reference. Keeping that resolution here avoids each
6//! consumer inventing a slightly different notion of scope and shadowing.
7
8use std::collections::{BTreeSet, HashMap, HashSet};
9
10use harn_lexer::Span;
11
12use crate::ast::{is_discard_name, BindingPattern, Node, SNode, TypedParam};
13
14mod call_resolution;
15mod mutations;
16pub use call_resolution::{
17    lexically_resolved_identifier_spans, module_match_pattern_catalog_with_visible,
18    resolved_identifier_bindings_with_source,
19};
20pub use mutations::{
21    nested_callable_reassigned_names, nested_callable_value_write_names, outer_value_write_names,
22};
23
24/// Stable identity for a source binding. Patterns do not carry individual
25/// spans, so the declaration span plus the bound name is the narrowest source
26/// identity available without changing the AST.
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct BindingId {
29    pub name: String,
30    pub declaration_start: usize,
31    pub declaration_end: usize,
32}
33
34/// Compiler-resolved enum metadata needed to distinguish call-shaped enum
35/// patterns from ordinary expression-equality patterns.
36#[derive(Debug, Clone, Default)]
37pub struct MatchPatternCatalog {
38    enum_names: HashSet<String>,
39    variant_owners: HashMap<String, Vec<String>>,
40}
41
42/// Resolution of a bare call-shaped match pattern such as `Ok(value)`.
43/// Compiler lowering and lexical analysis share this decision so a pattern
44/// cannot bind payload names in one subsystem and act as an expression in the
45/// other.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum BareVariantResolution<'a> {
48    NotVariant,
49    Unique(&'a str),
50    Ambiguous(&'a [String]),
51}
52
53pub fn resolve_bare_variant_owners(owners: Option<&[String]>) -> BareVariantResolution<'_> {
54    match owners {
55        None | Some([]) => BareVariantResolution::NotVariant,
56        Some([owner]) => BareVariantResolution::Unique(owner),
57        Some(owners) => BareVariantResolution::Ambiguous(owners),
58    }
59}
60
61pub fn ambiguous_bare_variant_message(variant: &str, owners: &[String]) -> String {
62    format!(
63        "match pattern `{variant}(...)` is ambiguous: variant `{variant}` is declared by enums {}; qualify it as `{}.{variant}(...)`",
64        owners.join(", "),
65        owners[0],
66    )
67}
68
69pub fn imported_bare_variant_message(variant: &str, owner: &str) -> String {
70    format!(
71        "match pattern `{variant}(...)` names a variant of imported enum `{owner}`; imported variants must be qualified as `{owner}.{variant}(...)`",
72    )
73}
74
75/// Node slices whose declarations are predeclared in the module type scope.
76///
77/// Top-level declarations and declarations directly inside pipeline bodies
78/// share one module-visible namespace. Function, tool, closure, and nested
79/// block declarations remain lexical to those bodies. The typechecker and VM
80/// compiler consume this same projection so an inaccessible nested enum cannot
81/// make a bare match pattern ambiguous in only one subsystem.
82pub fn module_scope_node_slices(program: &[SNode]) -> Vec<&[SNode]> {
83    let mut scopes = vec![program];
84    for node in program {
85        let inner = match &node.node {
86            Node::AttributedDecl { inner, .. } => inner.as_ref(),
87            _ => node,
88        };
89        if let Node::Pipeline { body, .. } = &inner.node {
90            scopes.push(body);
91        }
92    }
93    scopes
94}
95
96impl MatchPatternCatalog {
97    pub fn new(
98        enum_names: &HashSet<String>,
99        variant_owners: &HashMap<String, Vec<String>>,
100    ) -> Self {
101        Self::from_parts(enum_names.clone(), variant_owners.clone())
102    }
103
104    pub fn from_parts(
105        enum_names: HashSet<String>,
106        mut variant_owners: HashMap<String, Vec<String>>,
107    ) -> Self {
108        for owners in variant_owners.values_mut() {
109            owners.sort();
110            owners.dedup();
111        }
112        Self {
113            enum_names,
114            variant_owners,
115        }
116    }
117
118    pub fn resolve_bare_variant(&self, name: &str) -> BareVariantResolution<'_> {
119        resolve_bare_variant_owners(self.variant_owners.get(name).map(Vec::as_slice))
120    }
121
122    pub fn is_enum_name(&self, name: &str) -> bool {
123        self.enum_names.contains(name)
124    }
125
126    /// Add enum declarations visible through an enclosing module scope.
127    pub fn extend_declarations(&mut self, declarations: &[SNode]) {
128        for node in declarations {
129            let declaration = match &node.node {
130                Node::AttributedDecl { inner, .. } => inner.as_ref(),
131                _ => node,
132            };
133            if let Node::EnumDecl { name, variants, .. } = &declaration.node {
134                self.register_enum(name, variants);
135            }
136        }
137    }
138
139    fn register_enum(&mut self, name: &str, variants: &[crate::ast::EnumVariant]) {
140        for owners in self.variant_owners.values_mut() {
141            owners.retain(|owner| owner != name);
142        }
143        self.variant_owners.retain(|_, owners| !owners.is_empty());
144        self.enum_names.insert(name.to_string());
145        for variant in variants {
146            self.variant_owners
147                .entry(variant.name.clone())
148                .or_default()
149                .push(name.to_string());
150        }
151    }
152}
153
154/// Build the final enum catalog visible from module-owned callable bodies.
155///
156/// This mirrors the module scope shared by type checking and lowering. Nested
157/// block enums stay lexical to their body and are registered by the analysis
158/// as it walks that body.
159pub fn module_match_pattern_catalog(program: &[SNode]) -> MatchPatternCatalog {
160    module_match_pattern_catalog_with_visible(program, &[])
161}
162
163impl BindingId {
164    pub fn from_declaration(name: impl Into<String>, span: Span) -> Self {
165        Self {
166            name: name.into(),
167            declaration_start: span.start,
168            declaration_end: span.end,
169        }
170    }
171}
172
173/// Return every name introduced by a destructuring pattern, in source order.
174/// The projection is intentionally shared by the compiler and typechecker.
175pub fn binding_pattern_names(pattern: &BindingPattern) -> Vec<String> {
176    match pattern {
177        BindingPattern::Identifier(name) => vec![name.clone()],
178        BindingPattern::Pair(first, second) => vec![first.clone(), second.clone()],
179        BindingPattern::Dict(fields) => fields
180            .iter()
181            .map(|field| field.alias.clone().unwrap_or_else(|| field.key.clone()))
182            .collect(),
183        BindingPattern::List(elements) => elements
184            .iter()
185            .map(|element| element.name.clone())
186            .collect(),
187    }
188}
189
190/// Return the source identities introduced by `pattern` at `declaration`.
191pub fn binding_pattern_ids(pattern: &BindingPattern, declaration: Span) -> Vec<BindingId> {
192    binding_pattern_names(pattern)
193        .into_iter()
194        .filter(|name| !is_discard_name(name))
195        .map(|name| BindingId::from_declaration(name, declaration))
196        .collect()
197}
198
199/// Bindings in the current compiled body referenced by a nested callable.
200///
201/// A result is declaration-identity based rather than name based. That keeps
202/// `let pin` distinct from a later `{ pin -> ... }` parameter or an inner
203/// block-local `let pin`, which is essential when selecting VM storage.
204pub fn captured_bindings_in_nested_callables(
205    body: &[SNode],
206    match_patterns: &MatchPatternCatalog,
207) -> HashSet<BindingId> {
208    let mut analysis = LexicalAnalysis::new(match_patterns);
209    analysis.walk_body(body, Vec::new(), false, BindingOwner::Current);
210    analysis.captured
211}
212
213/// Bindings captured across one pipeline inheritance chain.
214///
215/// Parent and child bodies share runtime value bindings, so their lexical value
216/// scope is cumulative. Each body is typechecked and compiled from the final
217/// module enum catalog, however, so source-order enum shadowing must reset at a
218/// pipeline boundary instead of leaking from a parent into its child.
219pub fn captured_bindings_in_pipeline_lineage(
220    bodies: &[&[SNode]],
221    match_patterns: &MatchPatternCatalog,
222) -> HashSet<BindingId> {
223    let mut analysis = LexicalAnalysis::new(match_patterns);
224    let mut value_scope = Scope::new();
225    for body in bodies {
226        value_scope.extend(hoisted_callable_scope(body));
227    }
228
229    for body in bodies {
230        analysis.match_patterns = match_patterns.clone();
231        for node in *body {
232            analysis.walk_node(
233                node,
234                std::slice::from_ref(&value_scope),
235                false,
236                &BindingOwner::Current,
237            );
238            let declaration = match &node.node {
239                Node::AttributedDecl { inner, .. } => inner.as_ref(),
240                _ => node,
241            };
242            if let Node::EnumDecl { name, variants, .. } = &declaration.node {
243                analysis.match_patterns.register_enum(name, variants);
244            }
245            extend_scope_with_value_declaration(&mut value_scope, node, &BindingOwner::Current);
246        }
247    }
248
249    analysis.captured
250}
251
252/// Bindings captured under module execution order.
253///
254/// Module statements execute in source order first; callable declarations and
255/// pipeline bodies are materialized only after every statement has run. This
256/// differs from an ordinary block, where a later value is not visible to an
257/// earlier nested callable. Modeling the two phases here keeps boxing aligned
258/// with the bytecode compiler without teaching the VM another scope heuristic.
259pub fn captured_bindings_in_compiled_module(
260    body: &[SNode],
261    match_patterns: &MatchPatternCatalog,
262) -> HashSet<BindingId> {
263    let mut analysis = LexicalAnalysis::new(match_patterns);
264    let mut value_scope = Scope::new();
265
266    for node in body {
267        if is_deferred_module_declaration(node) {
268            continue;
269        }
270        analysis.walk_node(
271            node,
272            std::slice::from_ref(&value_scope),
273            false,
274            &BindingOwner::Current,
275        );
276        extend_scope_with_value_declaration(&mut value_scope, node, &BindingOwner::Current);
277    }
278
279    let mut phase_two_scope = hoisted_callable_scope(body);
280    phase_two_scope.extend(value_scope);
281    for node in body {
282        if is_deferred_module_declaration(node) {
283            analysis.walk_node(
284                node,
285                std::slice::from_ref(&phase_two_scope),
286                false,
287                &BindingOwner::Current,
288            );
289        }
290    }
291    analysis.captured
292}
293
294/// Resolve identifier-use spans to their exact lexical declarations.
295///
296/// This is the semantic bridge for consumers that combine source-local facts
297/// with type facts. Declaration identity preserves source order and nested
298/// shadowing; a name-only map cannot.
299pub fn resolved_identifier_bindings(
300    params: &[TypedParam],
301    body: &[SNode],
302) -> HashMap<(usize, usize), BindingId> {
303    resolved_identifier_bindings_with_source(params, body, None, &MatchPatternCatalog::default())
304}
305
306/// Resolve a property receiver to its root binding and ordered property path.
307///
308/// Both ordinary and optional property access recurse through the same lexical
309/// owner. Consumers can compare the returned [`BindingId`] with the parameter
310/// or local declaration they authorize instead of inferring authority from an
311/// identifier's spelling.
312pub fn resolved_receiver_path<'node, 'facts>(
313    receiver: &'node SNode,
314    resolved: &'facts HashMap<(usize, usize), BindingId>,
315) -> Option<(&'facts BindingId, Vec<&'node str>)> {
316    fn collect<'node, 'facts>(
317        node: &'node SNode,
318        resolved: &'facts HashMap<(usize, usize), BindingId>,
319        properties: &mut Vec<&'node str>,
320    ) -> Option<&'facts BindingId> {
321        match &node.node {
322            Node::Identifier(_) => resolved.get(&(node.span.start, node.span.end)),
323            Node::PropertyAccess { object, property }
324            | Node::OptionalPropertyAccess { object, property } => {
325                let root = collect(object, resolved, properties)?;
326                properties.push(property);
327                Some(root)
328            }
329            _ => None,
330        }
331    }
332
333    let mut properties = Vec::new();
334    let root = collect(receiver, resolved, &mut properties)?;
335    Some((root, properties))
336}
337
338#[derive(Debug, Clone)]
339enum BindingOwner {
340    Current,
341    Nested,
342}
343
344#[derive(Debug, Clone)]
345enum ScopeBinding {
346    Current(BindingId),
347    Nested(Option<BindingId>),
348}
349
350type Scope = HashMap<String, ScopeBinding>;
351
352struct LexicalAnalysis<'source> {
353    record_property_writes: bool,
354    captured: HashSet<BindingId>,
355    reassigned: BTreeSet<String>,
356    resolved: HashMap<(usize, usize), BindingId>,
357    lexically_resolved: HashSet<(usize, usize)>,
358    match_patterns: MatchPatternCatalog,
359    source: Option<&'source str>,
360}
361
362impl<'source> LexicalAnalysis<'source> {
363    fn new(match_patterns: &MatchPatternCatalog) -> Self {
364        Self::new_with_source(match_patterns, None)
365    }
366
367    fn new_with_source(match_patterns: &MatchPatternCatalog, source: Option<&'source str>) -> Self {
368        Self {
369            record_property_writes: false,
370            captured: HashSet::new(),
371            reassigned: BTreeSet::new(),
372            resolved: HashMap::new(),
373            lexically_resolved: HashSet::new(),
374            match_patterns: match_patterns.clone(),
375            source,
376        }
377    }
378
379    fn walk_body(
380        &mut self,
381        body: &[SNode],
382        scopes: Vec<Scope>,
383        inside_nested_callable: bool,
384        owner: BindingOwner,
385    ) {
386        self.walk_body_with_bindings(body, scopes, inside_nested_callable, owner, Scope::new());
387    }
388
389    fn walk_body_with_bindings(
390        &mut self,
391        body: &[SNode],
392        mut scopes: Vec<Scope>,
393        inside_nested_callable: bool,
394        owner: BindingOwner,
395        extra_bindings: Scope,
396    ) {
397        let outer_match_patterns = self.match_patterns.clone();
398        // Named callables are late-bound and may recurse or mutually recurse.
399        // Value bindings become visible only after their declaration executes.
400        let mut scope = hoisted_callable_scope(body);
401        scope.extend(extra_bindings);
402        scopes.push(scope);
403        for node in body {
404            self.walk_node(node, &scopes, inside_nested_callable, &owner);
405            let declaration = match &node.node {
406                Node::AttributedDecl { inner, .. } => inner.as_ref(),
407                _ => node,
408            };
409            if let Node::EnumDecl { name, variants, .. } = &declaration.node {
410                self.match_patterns.register_enum(name, variants);
411            }
412            extend_scope_with_value_declaration(
413                scopes.last_mut().expect("body scope"),
414                node,
415                &owner,
416            );
417        }
418        self.match_patterns = outer_match_patterns;
419    }
420
421    fn walk_node(
422        &mut self,
423        node: &SNode,
424        scopes: &[Scope],
425        inside_nested_callable: bool,
426        owner: &BindingOwner,
427    ) {
428        match &node.node {
429            Node::Identifier(name) => {
430                self.record_reference(name, node.span, scopes, inside_nested_callable);
431            }
432            Node::FunctionCall { name, .. } => {
433                // Bare calls resolve a user binding before falling back to a
434                // builtin. Keep the complete name intact so dotted builtin
435                // names do not become references to their first component.
436                self.record_reference(name, node.span, scopes, inside_nested_callable);
437                self.walk_children(node, scopes, inside_nested_callable, owner);
438            }
439            Node::Assignment { target, .. } => {
440                if inside_nested_callable {
441                    let name = if self.record_property_writes {
442                        mutations::assignment_root_name(target)
443                    } else if let Node::Identifier(name) = &target.node {
444                        Some(name.as_str())
445                    } else {
446                        None
447                    };
448                    if let Some(name) = name {
449                        self.record_reassignment(name, scopes);
450                    }
451                }
452                self.walk_children(node, scopes, inside_nested_callable, owner);
453            }
454            // `xs.push(v)` rewrites `xs` the way `xs[i] = v` does.
455            Node::MethodCall { .. }
456                if crate::builtin_methods::mutating_call_place(node).is_some() =>
457            {
458                if inside_nested_callable && self.record_property_writes {
459                    let place = crate::builtin_methods::mutating_call_place(node);
460                    if let Some(name) = place.and_then(crate::builtin_methods::place_root) {
461                        self.record_reassignment(name, scopes);
462                    }
463                }
464                self.walk_children(node, scopes, inside_nested_callable, owner);
465            }
466            Node::Closure { params, body, .. }
467            | Node::FnDecl { params, body, .. }
468            | Node::ToolDecl { params, body, .. } => {
469                // Defaults run left to right: earlier parameters are visible,
470                // while the current and later parameters still resolve outside
471                // the callable.
472                let mut default_scopes = scopes.to_vec();
473                default_scopes.push(Scope::new());
474                for param in params {
475                    if let Some(default) = &param.default_value {
476                        self.walk_node(default, &default_scopes, true, owner);
477                    }
478                    default_scopes
479                        .last_mut()
480                        .expect("parameter default scope")
481                        .extend(names_scope([param.name.clone()]));
482                }
483                self.walk_callable_body(body, params, scopes);
484            }
485            Node::Pipeline { params, body, .. } => {
486                self.walk_callable_body(body, params, scopes);
487            }
488            Node::OverrideDecl { params, body, .. } => {
489                let bindings = names_scope(params.iter().cloned());
490                self.walk_body_with_bindings(
491                    body,
492                    scopes.to_vec(),
493                    true,
494                    BindingOwner::Nested,
495                    bindings,
496                );
497            }
498            Node::SpawnExpr { body } => {
499                self.walk_body(body, scopes.to_vec(), true, BindingOwner::Nested);
500            }
501            Node::Parallel {
502                expr,
503                variable,
504                body,
505                options,
506                ..
507            } => {
508                self.walk_node(expr, scopes, inside_nested_callable, owner);
509                for (_, option) in options {
510                    self.walk_node(option, scopes, inside_nested_callable, owner);
511                }
512                let bindings = variable.iter().cloned().collect::<Vec<_>>();
513                self.walk_body_with_bindings(
514                    body,
515                    scopes.to_vec(),
516                    true,
517                    BindingOwner::Nested,
518                    names_scope(bindings),
519                );
520            }
521            Node::ForIn {
522                pattern,
523                iterable,
524                body,
525            } => {
526                self.walk_pattern_defaults(pattern, scopes, inside_nested_callable, owner);
527                self.walk_node(iterable, scopes, inside_nested_callable, owner);
528                self.walk_body_with_bindings(
529                    body,
530                    scopes.to_vec(),
531                    inside_nested_callable,
532                    owner.clone(),
533                    pattern_scope(pattern, node.span, owner),
534                );
535            }
536            Node::IfElse {
537                condition,
538                then_body,
539                else_body,
540                ..
541            } => {
542                self.walk_node(condition, scopes, inside_nested_callable, owner);
543                self.walk_body(
544                    then_body,
545                    scopes.to_vec(),
546                    inside_nested_callable,
547                    owner.clone(),
548                );
549                if let Some(else_body) = else_body {
550                    self.walk_body(
551                        else_body,
552                        scopes.to_vec(),
553                        inside_nested_callable,
554                        owner.clone(),
555                    );
556                }
557            }
558            Node::MatchExpr { value, arms } => {
559                self.walk_node(value, scopes, inside_nested_callable, owner);
560                for arm in arms {
561                    let bindings = self.analyze_match_pattern(
562                        &arm.pattern,
563                        scopes,
564                        inside_nested_callable,
565                        owner,
566                    );
567                    let mut arm_scopes = scopes.to_vec();
568                    arm_scopes.push(bindings);
569                    if let Some(guard) = &arm.guard {
570                        self.walk_node(guard, &arm_scopes, inside_nested_callable, owner);
571                    }
572                    self.walk_body(&arm.body, arm_scopes, inside_nested_callable, owner.clone());
573                }
574            }
575            Node::WhileLoop { condition, body } => {
576                self.walk_node(condition, scopes, inside_nested_callable, owner);
577                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
578            }
579            Node::Retry { count, body } => {
580                self.walk_node(count, scopes, inside_nested_callable, owner);
581                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
582            }
583            Node::CostRoute { options, body } => {
584                for (_, option) in options {
585                    self.walk_node(option, scopes, inside_nested_callable, owner);
586                }
587                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
588            }
589            Node::TryCatch {
590                body,
591                error_var,
592                catch_body,
593                finally_body,
594                ..
595            } => {
596                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
597                let catch_binding = names_scope(error_var.iter().cloned());
598                self.walk_body_with_bindings(
599                    catch_body,
600                    scopes.to_vec(),
601                    inside_nested_callable,
602                    owner.clone(),
603                    catch_binding,
604                );
605                if let Some(finally_body) = finally_body {
606                    self.walk_body(
607                        finally_body,
608                        scopes.to_vec(),
609                        inside_nested_callable,
610                        owner.clone(),
611                    );
612                }
613            }
614            Node::TryExpr { body }
615            | Node::ScopeBlock { body }
616            | Node::DeferStmt { body }
617            | Node::Block(body) => {
618                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
619            }
620            Node::GuardStmt {
621                condition,
622                else_body,
623            } => {
624                self.walk_node(condition, scopes, inside_nested_callable, owner);
625                self.walk_body(
626                    else_body,
627                    scopes.to_vec(),
628                    inside_nested_callable,
629                    owner.clone(),
630                );
631            }
632            Node::DeadlineBlock { duration, body } => {
633                self.walk_node(duration, scopes, inside_nested_callable, owner);
634                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
635            }
636            Node::MutexBlock { key, body } => {
637                if let Some(key) = key {
638                    self.walk_node(key, scopes, inside_nested_callable, owner);
639                }
640                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
641            }
642            Node::SelectExpr {
643                cases,
644                timeout,
645                default_body,
646            } => {
647                for case in cases {
648                    self.walk_node(&case.channel, scopes, inside_nested_callable, owner);
649                    self.walk_body_with_bindings(
650                        &case.body,
651                        scopes.to_vec(),
652                        inside_nested_callable,
653                        owner.clone(),
654                        names_scope([case.variable.clone()]),
655                    );
656                }
657                if let Some((duration, body)) = timeout {
658                    self.walk_node(duration, scopes, inside_nested_callable, owner);
659                    self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
660                }
661                if let Some(body) = default_body {
662                    self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
663                }
664            }
665            Node::EvalPackDecl {
666                fields,
667                body,
668                summarize,
669                ..
670            } => {
671                for (_, value) in fields {
672                    self.walk_node(value, scopes, inside_nested_callable, owner);
673                }
674                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
675                if let Some(summary) = summarize {
676                    self.walk_body(
677                        summary,
678                        scopes.to_vec(),
679                        inside_nested_callable,
680                        owner.clone(),
681                    );
682                }
683            }
684            Node::InterpolatedString(segments) => {
685                let Some(source) = self.source else {
686                    return;
687                };
688                for segment in segments {
689                    let harn_lexer::StringSegment::Expression(expression, line, column) = segment
690                    else {
691                        continue;
692                    };
693                    let Some(expression) = crate::interpolation::parse_expression(
694                        Some(source),
695                        expression,
696                        *line,
697                        *column,
698                    ) else {
699                        continue;
700                    };
701                    self.walk_node(&expression, scopes, inside_nested_callable, owner);
702                }
703            }
704            _ => self.walk_children(node, scopes, inside_nested_callable, owner),
705        }
706    }
707
708    fn walk_callable_body(&mut self, body: &[SNode], params: &[TypedParam], scopes: &[Scope]) {
709        self.walk_body_with_bindings(
710            body,
711            scopes.to_vec(),
712            true,
713            BindingOwner::Nested,
714            names_scope(params.iter().map(|param| param.name.clone())),
715        );
716    }
717
718    /// Analyze the expression parts of a match pattern and return the names
719    /// that compiler lowering binds before the arm guard and body execute.
720    fn analyze_match_pattern(
721        &mut self,
722        pattern: &SNode,
723        scopes: &[Scope],
724        inside_nested_callable: bool,
725        owner: &BindingOwner,
726    ) -> Scope {
727        let mut bindings = Vec::new();
728        match &pattern.node {
729            Node::Identifier(name) if name != "_" => bindings.push(name.clone()),
730            Node::Identifier(_) => {}
731            Node::EnumConstruct { args, .. } => {
732                for arg in args {
733                    if let Node::Identifier(name) = &arg.node {
734                        bindings.push(name.clone());
735                    }
736                }
737            }
738            Node::FunctionCall { name, args, .. }
739                if matches!(
740                    self.match_patterns.resolve_bare_variant(name),
741                    BareVariantResolution::Unique(_)
742                ) =>
743            {
744                for arg in args {
745                    if let Node::Identifier(name) = &arg.node {
746                        bindings.push(name.clone());
747                    }
748                }
749            }
750            Node::PropertyAccess { object, .. } if matches!(&object.node, Node::Identifier(name) if self.match_patterns.is_enum_name(name)) =>
751                {}
752            Node::MethodCall { object, args, .. } if matches!(&object.node, Node::Identifier(name) if self.match_patterns.is_enum_name(name)) => {
753                for arg in args {
754                    if let Node::Identifier(name) = &arg.node {
755                        bindings.push(name.clone());
756                    }
757                }
758            }
759            Node::DictLiteral(entries)
760                if entries
761                    .iter()
762                    .all(|entry| matches!(&entry.key.node, Node::StringLiteral(_))) =>
763            {
764                for entry in entries {
765                    if let Node::Identifier(name) = &entry.value.node {
766                        bindings.push(name.clone());
767                    } else {
768                        self.walk_node(&entry.value, scopes, inside_nested_callable, owner);
769                    }
770                }
771            }
772            Node::ListLiteral(elements) => {
773                for element in elements {
774                    match &element.node {
775                        Node::Identifier(name) if name != "_" => bindings.push(name.clone()),
776                        Node::Identifier(_) => {}
777                        Node::Spread(inner) => {
778                            if let Node::Identifier(name) = &inner.node {
779                                bindings.push(name.clone());
780                            } else {
781                                self.walk_node(inner, scopes, inside_nested_callable, owner);
782                            }
783                        }
784                        _ => {
785                            self.walk_node(element, scopes, inside_nested_callable, owner);
786                        }
787                    }
788                }
789            }
790            _ => self.walk_node(pattern, scopes, inside_nested_callable, owner),
791        }
792        names_scope(bindings)
793    }
794
795    fn walk_pattern_defaults(
796        &mut self,
797        pattern: &BindingPattern,
798        scopes: &[Scope],
799        inside_nested_callable: bool,
800        owner: &BindingOwner,
801    ) {
802        match pattern {
803            BindingPattern::Dict(fields) => {
804                for field in fields {
805                    if let Some(default) = &field.default_value {
806                        self.walk_node(default, scopes, inside_nested_callable, owner);
807                    }
808                }
809            }
810            BindingPattern::List(elements) => {
811                for element in elements {
812                    if let Some(default) = &element.default_value {
813                        self.walk_node(default, scopes, inside_nested_callable, owner);
814                    }
815                }
816            }
817            BindingPattern::Identifier(_) | BindingPattern::Pair(_, _) => {}
818        }
819    }
820
821    fn walk_children(
822        &mut self,
823        node: &SNode,
824        scopes: &[Scope],
825        inside_nested_callable: bool,
826        owner: &BindingOwner,
827    ) {
828        crate::visit::for_each_immediate_child(node, &mut |child| {
829            self.walk_node(child, scopes, inside_nested_callable, owner);
830        });
831    }
832
833    fn record_reference(
834        &mut self,
835        name: &str,
836        span: Span,
837        scopes: &[Scope],
838        inside_nested_callable: bool,
839    ) {
840        let resolved = resolve(scopes, name);
841        if resolved.is_some() {
842            self.lexically_resolved.insert((span.start, span.end));
843        }
844        match resolved {
845            Some(ScopeBinding::Current(binding)) => {
846                self.resolved
847                    .insert((span.start, span.end), binding.clone());
848                if inside_nested_callable {
849                    self.captured.insert(binding.clone());
850                }
851            }
852            Some(ScopeBinding::Nested(Some(binding))) => {
853                self.resolved
854                    .insert((span.start, span.end), binding.clone());
855            }
856            Some(ScopeBinding::Nested(None)) | None => {}
857        }
858    }
859
860    fn record_reassignment(&mut self, name: &str, scopes: &[Scope]) {
861        match resolve(scopes, name) {
862            Some(ScopeBinding::Nested(_)) => {}
863            Some(ScopeBinding::Current(binding)) => {
864                self.reassigned.insert(binding.name.clone());
865            }
866            None => {
867                self.reassigned.insert(name.to_string());
868            }
869        }
870    }
871}
872
873fn hoisted_callable_scope(body: &[SNode]) -> Scope {
874    let mut scope = Scope::new();
875    for node in body {
876        if let Some(name) = hoisted_callable_name(node) {
877            scope.insert(name.to_string(), ScopeBinding::Nested(None));
878        }
879    }
880    scope
881}
882
883/// Name introduced at block entry by a function-like declaration.
884///
885/// Capture analysis, type checking, and bytecode lowering consume this one
886/// predicate so a forward callable reference cannot be accepted by one layer
887/// and omitted by another.
888pub fn hoisted_callable_name(node: &SNode) -> Option<&str> {
889    let declaration = match &node.node {
890        Node::AttributedDecl { inner, .. } => inner.as_ref(),
891        _ => node,
892    };
893    match &declaration.node {
894        Node::FnDecl { name, .. }
895        | Node::ToolDecl { name, .. }
896        | Node::Pipeline { name, .. }
897        | Node::OverrideDecl { name, .. } => Some(name),
898        _ => None,
899    }
900}
901
902/// Whether module compilation defers this declaration until after executable
903/// top-level statements. Capture analysis and bytecode lowering share this
904/// predicate so their visibility phases cannot drift.
905pub fn is_deferred_module_declaration(node: &SNode) -> bool {
906    let node = match &node.node {
907        Node::AttributedDecl { inner, .. } => &inner.node,
908        node => node,
909    };
910    matches!(
911        node,
912        Node::Pipeline { .. }
913            | Node::OverrideDecl { .. }
914            | Node::EvalPackDecl { .. }
915            | Node::FnDecl { .. }
916            | Node::ToolDecl { .. }
917            | Node::SkillDecl { .. }
918            | Node::ImplBlock { .. }
919            | Node::StructDecl { .. }
920            | Node::EnumDecl { .. }
921            | Node::InterfaceDecl { .. }
922            | Node::TypeDecl { .. }
923            | Node::ImportDecl { .. }
924            | Node::SelectiveImport { .. }
925            | Node::NamespaceImport { .. }
926    )
927}
928
929fn extend_scope_with_value_declaration(scope: &mut Scope, node: &SNode, owner: &BindingOwner) {
930    let (Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. }) = &node.node else {
931        return;
932    };
933    for binding in binding_pattern_ids(pattern, node.span) {
934        let name = binding.name.clone();
935        let entry = match owner {
936            BindingOwner::Current => ScopeBinding::Current(binding),
937            BindingOwner::Nested => ScopeBinding::Nested(Some(binding)),
938        };
939        scope.insert(name, entry);
940    }
941}
942
943fn pattern_scope(pattern: &BindingPattern, declaration: Span, owner: &BindingOwner) -> Scope {
944    let mut scope = Scope::new();
945    for binding in binding_pattern_ids(pattern, declaration) {
946        let name = binding.name.clone();
947        let entry = match owner {
948            BindingOwner::Current => ScopeBinding::Current(binding),
949            BindingOwner::Nested => ScopeBinding::Nested(Some(binding)),
950        };
951        scope.insert(name, entry);
952    }
953    scope
954}
955
956fn names_scope(names: impl IntoIterator<Item = String>) -> Scope {
957    names
958        .into_iter()
959        .filter(|name| !is_discard_name(name))
960        .map(|name| (name, ScopeBinding::Nested(None)))
961        .collect()
962}
963
964fn parameter_scope(params: &[TypedParam], owner: &BindingOwner) -> Scope {
965    params
966        .iter()
967        .filter(|param| !is_discard_name(&param.name))
968        .map(|param| {
969            let binding = BindingId::from_declaration(param.name.clone(), param.span);
970            let entry = match owner {
971                BindingOwner::Current => ScopeBinding::Current(binding),
972                BindingOwner::Nested => ScopeBinding::Nested(Some(binding)),
973            };
974            (param.name.clone(), entry)
975        })
976        .collect()
977}
978
979fn resolve<'a>(scopes: &'a [Scope], name: &str) -> Option<&'a ScopeBinding> {
980    scopes.iter().rev().find_map(|scope| scope.get(name))
981}
982
983#[cfg(test)]
984mod tests {
985    use harn_lexer::Span;
986
987    use crate::ast::{DictEntry, MatchArm, SelectCase};
988
989    use super::*;
990
991    fn node(offset: usize, node: Node) -> SNode {
992        SNode::new(node, Span::with_offsets(offset, offset + 1, 1, offset + 1))
993    }
994
995    fn identifier(offset: usize, name: &str) -> SNode {
996        node(offset, Node::Identifier(name.to_string()))
997    }
998
999    fn function_call(offset: usize, name: &str) -> SNode {
1000        node(
1001            offset,
1002            Node::FunctionCall {
1003                name: name.to_string(),
1004                type_args: Vec::new(),
1005                args: Vec::new(),
1006            },
1007        )
1008    }
1009
1010    fn let_binding(offset: usize, name: &str) -> SNode {
1011        node(
1012            offset,
1013            Node::LetBinding {
1014                pattern: BindingPattern::Identifier(name.to_string()),
1015                type_ann: None,
1016                value: Box::new(identifier(offset + 100, "value")),
1017                is_pub: false,
1018            },
1019        )
1020    }
1021
1022    fn closure(offset: usize, params: Vec<TypedParam>, body: Vec<SNode>) -> SNode {
1023        node(
1024            offset,
1025            Node::Closure {
1026                params,
1027                return_type: None,
1028                throws: None,
1029                body,
1030                fn_syntax: false,
1031            },
1032        )
1033    }
1034
1035    fn fn_decl(offset: usize, name: &str, body: Vec<SNode>) -> SNode {
1036        node(
1037            offset,
1038            Node::FnDecl {
1039                name: name.to_string(),
1040                type_params: Vec::new(),
1041                params: Vec::new(),
1042                type_predicate: None,
1043                return_type: None,
1044                throws: None,
1045                where_clauses: Vec::new(),
1046                body,
1047                is_pub: false,
1048                is_stream: false,
1049            },
1050        )
1051    }
1052
1053    fn defaulted_param(name: &str, default: SNode) -> TypedParam {
1054        TypedParam {
1055            name: name.to_string(),
1056            type_expr: None,
1057            default_value: Some(Box::new(default)),
1058            rest: false,
1059            span: Span::dummy(),
1060        }
1061    }
1062
1063    fn captured(body: &[SNode]) -> HashSet<BindingId> {
1064        captured_bindings_in_nested_callables(body, &MatchPatternCatalog::default())
1065    }
1066
1067    fn enum_pattern_catalog() -> MatchPatternCatalog {
1068        MatchPatternCatalog::new(
1069            &HashSet::from(["Option".to_string(), "Result".to_string()]),
1070            &HashMap::from([
1071                ("Some".to_string(), vec!["Option".to_string()]),
1072                ("Ok".to_string(), vec!["Result".to_string()]),
1073            ]),
1074        )
1075    }
1076
1077    #[test]
1078    fn function_call_callee_is_a_lexical_reference() {
1079        let callable = let_binding(10, "callable");
1080        let nested = closure(
1081            30,
1082            Vec::new(),
1083            vec![function_call(31, "callable"), function_call(33, "log")],
1084        );
1085
1086        let captured = captured(&[callable.clone(), nested]);
1087        assert!(captured.contains(&BindingId::from_declaration("callable", callable.span)));
1088    }
1089
1090    #[test]
1091    fn earlier_value_binding_shadows_later_hoisted_callable_for_capture() {
1092        let callable = let_binding(10, "callable");
1093        let invoke = node(
1094            20,
1095            Node::ConstBinding {
1096                pattern: BindingPattern::Identifier("invoke".to_string()),
1097                type_ann: None,
1098                value: Box::new(closure(21, Vec::new(), vec![function_call(22, "callable")])),
1099                is_pub: false,
1100            },
1101        );
1102        let later_callable = fn_decl(30, "callable", Vec::new());
1103
1104        let captured = captured(&[callable.clone(), invoke, later_callable]);
1105        assert_eq!(
1106            captured,
1107            HashSet::from([BindingId::from_declaration("callable", callable.span)])
1108        );
1109    }
1110
1111    #[test]
1112    fn deferred_module_callable_sees_later_module_value() {
1113        let read = fn_decl(10, "read", vec![identifier(11, "counter")]);
1114        let counter = let_binding(20, "counter");
1115
1116        let captured = captured_bindings_in_compiled_module(
1117            &[read, counter.clone()],
1118            &MatchPatternCatalog::default(),
1119        );
1120
1121        assert_eq!(
1122            captured,
1123            HashSet::from([BindingId::from_declaration("counter", counter.span)])
1124        );
1125    }
1126
1127    #[test]
1128    fn module_statement_does_not_see_later_module_value() {
1129        let early = node(
1130            10,
1131            Node::ConstBinding {
1132                pattern: BindingPattern::Identifier("read".to_string()),
1133                type_ann: None,
1134                value: Box::new(closure(11, Vec::new(), vec![identifier(12, "counter")])),
1135                is_pub: false,
1136            },
1137        );
1138        let counter = let_binding(20, "counter");
1139
1140        let captured = captured_bindings_in_compiled_module(
1141            &[early, counter],
1142            &MatchPatternCatalog::default(),
1143        );
1144
1145        assert!(captured.is_empty());
1146    }
1147
1148    #[test]
1149    fn match_bindings_shadow_same_named_outer_mutables() {
1150        let pin = let_binding(10, "pin");
1151        let alias = let_binding(20, "alias");
1152        let rest = let_binding(30, "rest");
1153        let match_expr = node(
1154            40,
1155            Node::MatchExpr {
1156                value: Box::new(identifier(41, "value")),
1157                arms: vec![
1158                    MatchArm {
1159                        pattern: identifier(42, "pin"),
1160                        guard: Some(Box::new(identifier(43, "pin"))),
1161                        body: vec![identifier(44, "pin")],
1162                        span: Span::with_offsets(42, 47, 1, 43),
1163                    },
1164                    MatchArm {
1165                        pattern: node(
1166                            50,
1167                            Node::DictLiteral(vec![DictEntry {
1168                                key: node(51, Node::StringLiteral("key".to_string())),
1169                                value: identifier(52, "alias"),
1170                            }]),
1171                        ),
1172                        guard: None,
1173                        body: vec![identifier(53, "alias")],
1174                        span: Span::with_offsets(50, 55, 1, 51),
1175                    },
1176                    MatchArm {
1177                        pattern: node(
1178                            60,
1179                            Node::ListLiteral(vec![
1180                                identifier(61, "pin"),
1181                                node(62, Node::Spread(Box::new(identifier(63, "rest")))),
1182                            ]),
1183                        ),
1184                        guard: None,
1185                        body: vec![identifier(64, "pin"), identifier(65, "rest")],
1186                        span: Span::with_offsets(60, 67, 1, 61),
1187                    },
1188                    MatchArm {
1189                        pattern: node(
1190                            70,
1191                            Node::FunctionCall {
1192                                name: "Some".to_string(),
1193                                type_args: Vec::new(),
1194                                args: vec![identifier(71, "alias")],
1195                            },
1196                        ),
1197                        guard: None,
1198                        body: vec![identifier(72, "alias")],
1199                        span: Span::with_offsets(70, 74, 1, 71),
1200                    },
1201                    MatchArm {
1202                        pattern: node(
1203                            80,
1204                            Node::MethodCall {
1205                                object: Box::new(identifier(81, "Result")),
1206                                method: "Ok".to_string(),
1207                                args: vec![identifier(82, "rest")],
1208                            },
1209                        ),
1210                        guard: None,
1211                        body: vec![identifier(83, "rest")],
1212                        span: Span::with_offsets(80, 85, 1, 81),
1213                    },
1214                ],
1215            },
1216        );
1217
1218        let nested = closure(39, Vec::new(), vec![match_expr]);
1219        let captured = captured_bindings_in_nested_callables(
1220            &[pin.clone(), alias.clone(), rest.clone(), nested],
1221            &enum_pattern_catalog(),
1222        );
1223        assert!(!captured.contains(&BindingId::from_declaration("pin", pin.span)));
1224        assert!(!captured.contains(&BindingId::from_declaration("alias", alias.span)));
1225        assert!(!captured.contains(&BindingId::from_declaration("rest", rest.span)));
1226    }
1227
1228    #[test]
1229    fn unresolved_call_patterns_capture_expression_references() {
1230        let callable = let_binding(10, "callable");
1231        let object = let_binding(20, "object");
1232        let argument = let_binding(30, "argument");
1233        let match_expr = node(
1234            40,
1235            Node::MatchExpr {
1236                value: Box::new(identifier(41, "value")),
1237                arms: vec![
1238                    MatchArm {
1239                        pattern: node(
1240                            42,
1241                            Node::FunctionCall {
1242                                name: "callable".to_string(),
1243                                type_args: Vec::new(),
1244                                args: vec![identifier(43, "argument")],
1245                            },
1246                        ),
1247                        guard: None,
1248                        body: Vec::new(),
1249                        span: Span::with_offsets(42, 44, 1, 43),
1250                    },
1251                    MatchArm {
1252                        pattern: node(
1253                            45,
1254                            Node::MethodCall {
1255                                object: Box::new(identifier(46, "object")),
1256                                method: "compute".to_string(),
1257                                args: vec![identifier(47, "argument")],
1258                            },
1259                        ),
1260                        guard: None,
1261                        body: Vec::new(),
1262                        span: Span::with_offsets(45, 48, 1, 46),
1263                    },
1264                ],
1265            },
1266        );
1267        let nested = closure(39, Vec::new(), vec![match_expr]);
1268
1269        let captured = captured(&[callable.clone(), object.clone(), argument.clone(), nested]);
1270        assert!(captured.contains(&BindingId::from_declaration("callable", callable.span)));
1271        assert!(captured.contains(&BindingId::from_declaration("object", object.span)));
1272        assert!(captured.contains(&BindingId::from_declaration("argument", argument.span)));
1273    }
1274
1275    #[test]
1276    fn qualified_enum_constant_pattern_does_not_capture_enum_name() {
1277        let result = let_binding(10, "Result");
1278        let match_expr = node(
1279            20,
1280            Node::MatchExpr {
1281                value: Box::new(identifier(21, "value")),
1282                arms: vec![MatchArm {
1283                    pattern: node(
1284                        22,
1285                        Node::PropertyAccess {
1286                            object: Box::new(identifier(23, "Result")),
1287                            property: "Ok".to_string(),
1288                        },
1289                    ),
1290                    guard: None,
1291                    body: Vec::new(),
1292                    span: Span::with_offsets(22, 25, 1, 23),
1293                }],
1294            },
1295        );
1296        let nested = closure(19, Vec::new(), vec![match_expr]);
1297
1298        let captured = captured_bindings_in_nested_callables(
1299            &[result.clone(), nested],
1300            &enum_pattern_catalog(),
1301        );
1302        assert!(!captured.contains(&BindingId::from_declaration("Result", result.span)));
1303    }
1304
1305    #[test]
1306    fn parameter_defaults_see_only_earlier_parameters() {
1307        let first = let_binding(10, "first");
1308        let current = let_binding(20, "current");
1309        let later = let_binding(30, "later");
1310        let nested = closure(
1311            40,
1312            vec![
1313                defaulted_param("first", identifier(41, "later")),
1314                defaulted_param("current", identifier(42, "current")),
1315                defaulted_param("later", identifier(43, "first")),
1316            ],
1317            Vec::new(),
1318        );
1319
1320        let captured = captured(&[first.clone(), current.clone(), later.clone(), nested]);
1321        assert!(!captured.contains(&BindingId::from_declaration("first", first.span)));
1322        assert!(captured.contains(&BindingId::from_declaration("current", current.span)));
1323        assert!(captured.contains(&BindingId::from_declaration("later", later.span)));
1324    }
1325
1326    #[test]
1327    fn parameter_shadow_does_not_capture_outer_binding() {
1328        let outer = let_binding(10, "pin");
1329        let body = vec![
1330            outer.clone(),
1331            closure(
1332                20,
1333                vec![TypedParam::untyped("pin")],
1334                vec![identifier(21, "pin")],
1335            ),
1336        ];
1337
1338        assert!(!captured(&body).contains(&BindingId::from_declaration("pin", outer.span)));
1339    }
1340
1341    #[test]
1342    fn block_shadow_captures_exact_inner_binding() {
1343        let outer = let_binding(10, "pin");
1344        let inner = let_binding(20, "pin");
1345        let body = vec![
1346            outer.clone(),
1347            node(
1348                19,
1349                Node::Block(vec![
1350                    inner.clone(),
1351                    closure(30, Vec::new(), vec![identifier(31, "pin")]),
1352                ]),
1353            ),
1354        ];
1355
1356        let captured = captured(&body);
1357        assert!(captured.contains(&BindingId::from_declaration("pin", inner.span)));
1358        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1359    }
1360
1361    #[test]
1362    fn later_block_binding_does_not_shadow_an_earlier_reference() {
1363        let outer = let_binding(10, "pin");
1364        let inner = let_binding(30, "pin");
1365        let body = vec![
1366            outer.clone(),
1367            node(
1368                19,
1369                Node::Block(vec![
1370                    closure(20, Vec::new(), vec![identifier(21, "pin")]),
1371                    inner.clone(),
1372                ]),
1373            ),
1374        ];
1375
1376        let captured = captured(&body);
1377        assert!(captured.contains(&BindingId::from_declaration("pin", outer.span)));
1378        assert!(!captured.contains(&BindingId::from_declaration("pin", inner.span)));
1379    }
1380
1381    #[test]
1382    fn loop_binding_shadows_outer_capture() {
1383        let outer = let_binding(10, "pin");
1384        let loop_node = node(
1385            20,
1386            Node::ForIn {
1387                pattern: BindingPattern::Identifier("pin".to_string()),
1388                iterable: Box::new(identifier(21, "pins")),
1389                body: vec![closure(22, Vec::new(), vec![identifier(23, "pin")])],
1390            },
1391        );
1392        let captured = captured(&[outer.clone(), loop_node.clone()]);
1393
1394        assert!(captured.contains(&BindingId::from_declaration("pin", loop_node.span)));
1395        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1396    }
1397
1398    #[test]
1399    fn catch_and_select_bindings_shadow_outer_capture() {
1400        let outer = let_binding(10, "pin");
1401        let try_catch = node(
1402            20,
1403            Node::TryCatch {
1404                body: Vec::new(),
1405                try_span: Span::dummy(),
1406                has_catch: true,
1407                error_var: Some("pin".to_string()),
1408                error_type: None,
1409                catch_body: vec![closure(21, Vec::new(), vec![identifier(22, "pin")])],
1410                catch_span: Some(Span::dummy()),
1411                finally_body: None,
1412                finally_span: None,
1413            },
1414        );
1415        let select = node(
1416            30,
1417            Node::SelectExpr {
1418                cases: vec![SelectCase {
1419                    variable: "pin".to_string(),
1420                    channel: Box::new(identifier(31, "channel")),
1421                    body: vec![closure(32, Vec::new(), vec![identifier(33, "pin")])],
1422                }],
1423                timeout: None,
1424                default_body: None,
1425            },
1426        );
1427
1428        let captured = captured(&[outer.clone(), try_catch, select]);
1429        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1430    }
1431
1432    #[test]
1433    fn nested_callable_capture_is_transitive() {
1434        let outer = let_binding(10, "pin");
1435        let nested = closure(
1436            20,
1437            Vec::new(),
1438            vec![closure(30, Vec::new(), vec![identifier(31, "pin")])],
1439        );
1440        let captured = captured(&[outer.clone(), nested]);
1441
1442        assert!(captured.contains(&BindingId::from_declaration("pin", outer.span)));
1443    }
1444
1445    #[test]
1446    fn nested_reassignment_ignores_shadowed_parameter() {
1447        let body = vec![closure(
1448            10,
1449            vec![TypedParam::untyped("pin")],
1450            vec![node(
1451                11,
1452                Node::Assignment {
1453                    target: Box::new(identifier(12, "pin")),
1454                    value: Box::new(identifier(13, "next")),
1455                    op: None,
1456                },
1457            )],
1458        )];
1459
1460        assert!(
1461            nested_callable_reassigned_names(&body, &MatchPatternCatalog::default()).is_empty()
1462        );
1463    }
1464
1465    #[test]
1466    fn nested_reassignment_ignores_enum_payload_binding() {
1467        let assignment = node(
1468            14,
1469            Node::Assignment {
1470                target: Box::new(identifier(15, "pin")),
1471                value: Box::new(identifier(16, "next")),
1472                op: None,
1473            },
1474        );
1475        let body = vec![node(
1476            10,
1477            Node::MatchExpr {
1478                value: Box::new(identifier(11, "value")),
1479                arms: vec![MatchArm {
1480                    pattern: node(
1481                        12,
1482                        Node::FunctionCall {
1483                            name: "Some".to_string(),
1484                            type_args: Vec::new(),
1485                            args: vec![identifier(13, "pin")],
1486                        },
1487                    ),
1488                    guard: None,
1489                    body: vec![closure(14, Vec::new(), vec![assignment])],
1490                    span: Span::with_offsets(12, 18, 1, 13),
1491                }],
1492            },
1493        )];
1494
1495        assert_eq!(
1496            nested_callable_reassigned_names(&body, &enum_pattern_catalog()),
1497            Vec::<String>::new()
1498        );
1499    }
1500}