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