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