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