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, 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#[derive(Debug, Clone)]
276enum BindingOwner {
277    Current,
278    Nested,
279}
280
281#[derive(Debug, Clone)]
282enum ScopeBinding {
283    Current(BindingId),
284    Nested,
285}
286
287type Scope = HashMap<String, ScopeBinding>;
288
289struct LexicalAnalysis {
290    captured: HashSet<BindingId>,
291    reassigned: BTreeSet<String>,
292    match_patterns: MatchPatternCatalog,
293}
294
295impl LexicalAnalysis {
296    fn new(match_patterns: &MatchPatternCatalog) -> Self {
297        Self {
298            captured: HashSet::new(),
299            reassigned: BTreeSet::new(),
300            match_patterns: match_patterns.clone(),
301        }
302    }
303
304    fn walk_body(
305        &mut self,
306        body: &[SNode],
307        scopes: Vec<Scope>,
308        inside_nested_callable: bool,
309        owner: BindingOwner,
310    ) {
311        self.walk_body_with_bindings(body, scopes, inside_nested_callable, owner, Scope::new());
312    }
313
314    fn walk_body_with_bindings(
315        &mut self,
316        body: &[SNode],
317        mut scopes: Vec<Scope>,
318        inside_nested_callable: bool,
319        owner: BindingOwner,
320        extra_bindings: Scope,
321    ) {
322        let outer_match_patterns = self.match_patterns.clone();
323        // Named callables are late-bound and may recurse or mutually recurse.
324        // Value bindings become visible only after their declaration executes.
325        let mut scope = hoisted_callable_scope(body);
326        scope.extend(extra_bindings);
327        scopes.push(scope);
328        for node in body {
329            self.walk_node(node, &scopes, inside_nested_callable, &owner);
330            let declaration = match &node.node {
331                Node::AttributedDecl { inner, .. } => inner.as_ref(),
332                _ => node,
333            };
334            if let Node::EnumDecl { name, variants, .. } = &declaration.node {
335                self.match_patterns.register_enum(name, variants);
336            }
337            extend_scope_with_value_declaration(
338                scopes.last_mut().expect("body scope"),
339                node,
340                &owner,
341            );
342        }
343        self.match_patterns = outer_match_patterns;
344    }
345
346    fn walk_node(
347        &mut self,
348        node: &SNode,
349        scopes: &[Scope],
350        inside_nested_callable: bool,
351        owner: &BindingOwner,
352    ) {
353        match &node.node {
354            Node::Identifier(name) => self.record_reference(name, scopes, inside_nested_callable),
355            Node::FunctionCall { name, .. } => {
356                // Bare calls resolve a user binding before falling back to a
357                // builtin. Keep the complete name intact so dotted builtin
358                // names do not become references to their first component.
359                self.record_reference(name, scopes, inside_nested_callable);
360                self.walk_children(node, scopes, inside_nested_callable, owner);
361            }
362            Node::Assignment { target, .. } => {
363                if inside_nested_callable {
364                    if let Node::Identifier(name) = &target.node {
365                        self.record_reassignment(name, scopes);
366                    }
367                }
368                self.walk_children(node, scopes, inside_nested_callable, owner);
369            }
370            Node::Closure { params, body, .. }
371            | Node::FnDecl { params, body, .. }
372            | Node::ToolDecl { params, body, .. } => {
373                // Defaults run left to right: earlier parameters are visible,
374                // while the current and later parameters still resolve outside
375                // the callable.
376                let mut default_scopes = scopes.to_vec();
377                default_scopes.push(Scope::new());
378                for param in params {
379                    if let Some(default) = &param.default_value {
380                        self.walk_node(default, &default_scopes, true, owner);
381                    }
382                    default_scopes
383                        .last_mut()
384                        .expect("parameter default scope")
385                        .extend(names_scope([param.name.clone()]));
386                }
387                self.walk_callable_body(body, params, scopes);
388            }
389            Node::Pipeline { params, body, .. } => {
390                self.walk_callable_body(body, params, scopes);
391            }
392            Node::OverrideDecl { params, body, .. } => {
393                let bindings = names_scope(params.iter().cloned());
394                self.walk_body_with_bindings(
395                    body,
396                    scopes.to_vec(),
397                    true,
398                    BindingOwner::Nested,
399                    bindings,
400                );
401            }
402            Node::SpawnExpr { body } => {
403                self.walk_body(body, scopes.to_vec(), true, BindingOwner::Nested);
404            }
405            Node::Parallel {
406                expr,
407                variable,
408                body,
409                options,
410                ..
411            } => {
412                self.walk_node(expr, scopes, inside_nested_callable, owner);
413                for (_, option) in options {
414                    self.walk_node(option, scopes, inside_nested_callable, owner);
415                }
416                let bindings = variable.iter().cloned().collect::<Vec<_>>();
417                self.walk_body_with_bindings(
418                    body,
419                    scopes.to_vec(),
420                    true,
421                    BindingOwner::Nested,
422                    names_scope(bindings),
423                );
424            }
425            Node::ForIn {
426                pattern,
427                iterable,
428                body,
429            } => {
430                self.walk_pattern_defaults(pattern, scopes, inside_nested_callable, owner);
431                self.walk_node(iterable, scopes, inside_nested_callable, owner);
432                self.walk_body_with_bindings(
433                    body,
434                    scopes.to_vec(),
435                    inside_nested_callable,
436                    owner.clone(),
437                    pattern_scope(pattern, node.span, owner),
438                );
439            }
440            Node::IfElse {
441                condition,
442                then_body,
443                else_body,
444                ..
445            } => {
446                self.walk_node(condition, scopes, inside_nested_callable, owner);
447                self.walk_body(
448                    then_body,
449                    scopes.to_vec(),
450                    inside_nested_callable,
451                    owner.clone(),
452                );
453                if let Some(else_body) = else_body {
454                    self.walk_body(
455                        else_body,
456                        scopes.to_vec(),
457                        inside_nested_callable,
458                        owner.clone(),
459                    );
460                }
461            }
462            Node::MatchExpr { value, arms } => {
463                self.walk_node(value, scopes, inside_nested_callable, owner);
464                for arm in arms {
465                    let bindings = self.analyze_match_pattern(
466                        &arm.pattern,
467                        scopes,
468                        inside_nested_callable,
469                        owner,
470                    );
471                    let mut arm_scopes = scopes.to_vec();
472                    arm_scopes.push(bindings);
473                    if let Some(guard) = &arm.guard {
474                        self.walk_node(guard, &arm_scopes, inside_nested_callable, owner);
475                    }
476                    self.walk_body(&arm.body, arm_scopes, inside_nested_callable, owner.clone());
477                }
478            }
479            Node::WhileLoop { condition, body } => {
480                self.walk_node(condition, scopes, inside_nested_callable, owner);
481                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
482            }
483            Node::Retry { count, body } => {
484                self.walk_node(count, scopes, inside_nested_callable, owner);
485                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
486            }
487            Node::CostRoute { options, body } => {
488                for (_, option) in options {
489                    self.walk_node(option, scopes, inside_nested_callable, owner);
490                }
491                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
492            }
493            Node::TryCatch {
494                body,
495                error_var,
496                catch_body,
497                finally_body,
498                ..
499            } => {
500                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
501                let catch_binding = names_scope(error_var.iter().cloned());
502                self.walk_body_with_bindings(
503                    catch_body,
504                    scopes.to_vec(),
505                    inside_nested_callable,
506                    owner.clone(),
507                    catch_binding,
508                );
509                if let Some(finally_body) = finally_body {
510                    self.walk_body(
511                        finally_body,
512                        scopes.to_vec(),
513                        inside_nested_callable,
514                        owner.clone(),
515                    );
516                }
517            }
518            Node::TryExpr { body }
519            | Node::ScopeBlock { body }
520            | Node::DeferStmt { body }
521            | Node::Block(body) => {
522                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
523            }
524            Node::GuardStmt {
525                condition,
526                else_body,
527            } => {
528                self.walk_node(condition, scopes, inside_nested_callable, owner);
529                self.walk_body(
530                    else_body,
531                    scopes.to_vec(),
532                    inside_nested_callable,
533                    owner.clone(),
534                );
535            }
536            Node::DeadlineBlock { duration, body } => {
537                self.walk_node(duration, scopes, inside_nested_callable, owner);
538                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
539            }
540            Node::MutexBlock { key, body } => {
541                if let Some(key) = key {
542                    self.walk_node(key, scopes, inside_nested_callable, owner);
543                }
544                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
545            }
546            Node::SelectExpr {
547                cases,
548                timeout,
549                default_body,
550            } => {
551                for case in cases {
552                    self.walk_node(&case.channel, scopes, inside_nested_callable, owner);
553                    self.walk_body_with_bindings(
554                        &case.body,
555                        scopes.to_vec(),
556                        inside_nested_callable,
557                        owner.clone(),
558                        names_scope([case.variable.clone()]),
559                    );
560                }
561                if let Some((duration, body)) = timeout {
562                    self.walk_node(duration, scopes, inside_nested_callable, owner);
563                    self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
564                }
565                if let Some(body) = default_body {
566                    self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
567                }
568            }
569            Node::EvalPackDecl {
570                fields,
571                body,
572                summarize,
573                ..
574            } => {
575                for (_, value) in fields {
576                    self.walk_node(value, scopes, inside_nested_callable, owner);
577                }
578                self.walk_body(body, scopes.to_vec(), inside_nested_callable, owner.clone());
579                if let Some(summary) = summarize {
580                    self.walk_body(
581                        summary,
582                        scopes.to_vec(),
583                        inside_nested_callable,
584                        owner.clone(),
585                    );
586                }
587            }
588            _ => self.walk_children(node, scopes, inside_nested_callable, owner),
589        }
590    }
591
592    fn walk_callable_body(&mut self, body: &[SNode], params: &[TypedParam], scopes: &[Scope]) {
593        self.walk_body_with_bindings(
594            body,
595            scopes.to_vec(),
596            true,
597            BindingOwner::Nested,
598            names_scope(params.iter().map(|param| param.name.clone())),
599        );
600    }
601
602    /// Analyze the expression parts of a match pattern and return the names
603    /// that compiler lowering binds before the arm guard and body execute.
604    fn analyze_match_pattern(
605        &mut self,
606        pattern: &SNode,
607        scopes: &[Scope],
608        inside_nested_callable: bool,
609        owner: &BindingOwner,
610    ) -> Scope {
611        let mut bindings = Vec::new();
612        match &pattern.node {
613            Node::Identifier(name) if name != "_" => bindings.push(name.clone()),
614            Node::Identifier(_) => {}
615            Node::EnumConstruct { args, .. } => {
616                for arg in args {
617                    if let Node::Identifier(name) = &arg.node {
618                        bindings.push(name.clone());
619                    }
620                }
621            }
622            Node::FunctionCall { name, args, .. }
623                if matches!(
624                    self.match_patterns.resolve_bare_variant(name),
625                    BareVariantResolution::Unique(_)
626                ) =>
627            {
628                for arg in args {
629                    if let Node::Identifier(name) = &arg.node {
630                        bindings.push(name.clone());
631                    }
632                }
633            }
634            Node::PropertyAccess { object, .. } if matches!(&object.node, Node::Identifier(name) if self.match_patterns.is_enum_name(name)) =>
635                {}
636            Node::MethodCall { object, args, .. } if matches!(&object.node, Node::Identifier(name) if self.match_patterns.is_enum_name(name)) => {
637                for arg in args {
638                    if let Node::Identifier(name) = &arg.node {
639                        bindings.push(name.clone());
640                    }
641                }
642            }
643            Node::DictLiteral(entries)
644                if entries
645                    .iter()
646                    .all(|entry| matches!(&entry.key.node, Node::StringLiteral(_))) =>
647            {
648                for entry in entries {
649                    if let Node::Identifier(name) = &entry.value.node {
650                        bindings.push(name.clone());
651                    } else {
652                        self.walk_node(&entry.value, scopes, inside_nested_callable, owner);
653                    }
654                }
655            }
656            Node::ListLiteral(elements) => {
657                for element in elements {
658                    match &element.node {
659                        Node::Identifier(name) if name != "_" => bindings.push(name.clone()),
660                        Node::Identifier(_) => {}
661                        Node::Spread(inner) => {
662                            if let Node::Identifier(name) = &inner.node {
663                                bindings.push(name.clone());
664                            } else {
665                                self.walk_node(inner, scopes, inside_nested_callable, owner);
666                            }
667                        }
668                        _ => {
669                            self.walk_node(element, scopes, inside_nested_callable, owner);
670                        }
671                    }
672                }
673            }
674            _ => self.walk_node(pattern, scopes, inside_nested_callable, owner),
675        }
676        names_scope(bindings)
677    }
678
679    fn walk_pattern_defaults(
680        &mut self,
681        pattern: &BindingPattern,
682        scopes: &[Scope],
683        inside_nested_callable: bool,
684        owner: &BindingOwner,
685    ) {
686        match pattern {
687            BindingPattern::Dict(fields) => {
688                for field in fields {
689                    if let Some(default) = &field.default_value {
690                        self.walk_node(default, scopes, inside_nested_callable, owner);
691                    }
692                }
693            }
694            BindingPattern::List(elements) => {
695                for element in elements {
696                    if let Some(default) = &element.default_value {
697                        self.walk_node(default, scopes, inside_nested_callable, owner);
698                    }
699                }
700            }
701            BindingPattern::Identifier(_) | BindingPattern::Pair(_, _) => {}
702        }
703    }
704
705    fn walk_children(
706        &mut self,
707        node: &SNode,
708        scopes: &[Scope],
709        inside_nested_callable: bool,
710        owner: &BindingOwner,
711    ) {
712        for child in crate::visit::immediate_children(node) {
713            self.walk_node(child, scopes, inside_nested_callable, owner);
714        }
715    }
716
717    fn record_reference(&mut self, name: &str, scopes: &[Scope], inside_nested_callable: bool) {
718        if !inside_nested_callable {
719            return;
720        }
721        if let Some(ScopeBinding::Current(binding)) = resolve(scopes, name) {
722            self.captured.insert(binding.clone());
723        }
724    }
725
726    fn record_reassignment(&mut self, name: &str, scopes: &[Scope]) {
727        match resolve(scopes, name) {
728            Some(ScopeBinding::Nested) => {}
729            Some(ScopeBinding::Current(binding)) => {
730                self.reassigned.insert(binding.name.clone());
731            }
732            None => {
733                self.reassigned.insert(name.to_string());
734            }
735        }
736    }
737}
738
739fn hoisted_callable_scope(body: &[SNode]) -> Scope {
740    let mut scope = Scope::new();
741    for node in body {
742        match &node.node {
743            Node::FnDecl { name, .. }
744            | Node::ToolDecl { name, .. }
745            | Node::Pipeline { name, .. }
746            | Node::OverrideDecl { name, .. } => {
747                scope.insert(name.clone(), ScopeBinding::Nested);
748            }
749            Node::AttributedDecl { inner, .. } => {
750                if let Node::FnDecl { name, .. }
751                | Node::ToolDecl { name, .. }
752                | Node::Pipeline { name, .. }
753                | Node::OverrideDecl { name, .. } = &inner.node
754                {
755                    scope.insert(name.clone(), ScopeBinding::Nested);
756                }
757            }
758            _ => {}
759        }
760    }
761    scope
762}
763
764/// Whether module compilation defers this declaration until after executable
765/// top-level statements. Capture analysis and bytecode lowering share this
766/// predicate so their visibility phases cannot drift.
767pub fn is_deferred_module_declaration(node: &SNode) -> bool {
768    let node = match &node.node {
769        Node::AttributedDecl { inner, .. } => &inner.node,
770        node => node,
771    };
772    matches!(
773        node,
774        Node::Pipeline { .. }
775            | Node::OverrideDecl { .. }
776            | Node::EvalPackDecl { .. }
777            | Node::FnDecl { .. }
778            | Node::ToolDecl { .. }
779            | Node::SkillDecl { .. }
780            | Node::ImplBlock { .. }
781            | Node::StructDecl { .. }
782            | Node::EnumDecl { .. }
783            | Node::InterfaceDecl { .. }
784            | Node::TypeDecl { .. }
785            | Node::ImportDecl { .. }
786            | Node::SelectiveImport { .. }
787            | Node::NamespaceImport { .. }
788    )
789}
790
791fn extend_scope_with_value_declaration(scope: &mut Scope, node: &SNode, owner: &BindingOwner) {
792    let (Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. }) = &node.node else {
793        return;
794    };
795    for binding in binding_pattern_ids(pattern, node.span) {
796        let name = binding.name.clone();
797        let entry = match owner {
798            BindingOwner::Current => ScopeBinding::Current(binding),
799            BindingOwner::Nested => ScopeBinding::Nested,
800        };
801        scope.insert(name, entry);
802    }
803}
804
805fn pattern_scope(pattern: &BindingPattern, declaration: Span, owner: &BindingOwner) -> Scope {
806    let mut scope = Scope::new();
807    for binding in binding_pattern_ids(pattern, declaration) {
808        let name = binding.name.clone();
809        let entry = match owner {
810            BindingOwner::Current => ScopeBinding::Current(binding),
811            BindingOwner::Nested => ScopeBinding::Nested,
812        };
813        scope.insert(name, entry);
814    }
815    scope
816}
817
818fn names_scope(names: impl IntoIterator<Item = String>) -> Scope {
819    names
820        .into_iter()
821        .filter(|name| !is_discard_name(name))
822        .map(|name| (name, ScopeBinding::Nested))
823        .collect()
824}
825
826fn resolve<'a>(scopes: &'a [Scope], name: &str) -> Option<&'a ScopeBinding> {
827    scopes.iter().rev().find_map(|scope| scope.get(name))
828}
829
830#[cfg(test)]
831mod tests {
832    use harn_lexer::Span;
833
834    use crate::ast::{DictEntry, MatchArm, SelectCase};
835
836    use super::*;
837
838    fn node(offset: usize, node: Node) -> SNode {
839        SNode::new(node, Span::with_offsets(offset, offset + 1, 1, offset + 1))
840    }
841
842    fn identifier(offset: usize, name: &str) -> SNode {
843        node(offset, Node::Identifier(name.to_string()))
844    }
845
846    fn function_call(offset: usize, name: &str) -> SNode {
847        node(
848            offset,
849            Node::FunctionCall {
850                name: name.to_string(),
851                type_args: Vec::new(),
852                args: Vec::new(),
853            },
854        )
855    }
856
857    fn let_binding(offset: usize, name: &str) -> SNode {
858        node(
859            offset,
860            Node::LetBinding {
861                pattern: BindingPattern::Identifier(name.to_string()),
862                type_ann: None,
863                value: Box::new(identifier(offset + 100, "value")),
864                is_pub: false,
865            },
866        )
867    }
868
869    fn closure(offset: usize, params: Vec<TypedParam>, body: Vec<SNode>) -> SNode {
870        node(
871            offset,
872            Node::Closure {
873                params,
874                return_type: None,
875                throws: None,
876                body,
877                fn_syntax: false,
878            },
879        )
880    }
881
882    fn fn_decl(offset: usize, name: &str, body: Vec<SNode>) -> SNode {
883        node(
884            offset,
885            Node::FnDecl {
886                name: name.to_string(),
887                type_params: Vec::new(),
888                params: Vec::new(),
889                return_type: None,
890                throws: None,
891                where_clauses: Vec::new(),
892                body,
893                is_pub: false,
894                is_stream: false,
895            },
896        )
897    }
898
899    fn defaulted_param(name: &str, default: SNode) -> TypedParam {
900        TypedParam {
901            name: name.to_string(),
902            type_expr: None,
903            default_value: Some(Box::new(default)),
904            rest: false,
905        }
906    }
907
908    fn captured(body: &[SNode]) -> HashSet<BindingId> {
909        captured_bindings_in_nested_callables(body, &MatchPatternCatalog::default())
910    }
911
912    fn enum_pattern_catalog() -> MatchPatternCatalog {
913        MatchPatternCatalog::new(
914            &HashSet::from(["Option".to_string(), "Result".to_string()]),
915            &HashMap::from([
916                ("Some".to_string(), vec!["Option".to_string()]),
917                ("Ok".to_string(), vec!["Result".to_string()]),
918            ]),
919        )
920    }
921
922    #[test]
923    fn function_call_callee_is_a_lexical_reference() {
924        let callable = let_binding(10, "callable");
925        let nested = closure(
926            30,
927            Vec::new(),
928            vec![function_call(31, "callable"), function_call(33, "log")],
929        );
930
931        let captured = captured(&[callable.clone(), nested]);
932        assert!(captured.contains(&BindingId::from_declaration("callable", callable.span)));
933    }
934
935    #[test]
936    fn earlier_value_binding_shadows_later_hoisted_callable_for_capture() {
937        let callable = let_binding(10, "callable");
938        let invoke = node(
939            20,
940            Node::ConstBinding {
941                pattern: BindingPattern::Identifier("invoke".to_string()),
942                type_ann: None,
943                value: Box::new(closure(21, Vec::new(), vec![function_call(22, "callable")])),
944                is_pub: false,
945            },
946        );
947        let later_callable = fn_decl(30, "callable", Vec::new());
948
949        let captured = captured(&[callable.clone(), invoke, later_callable]);
950        assert_eq!(
951            captured,
952            HashSet::from([BindingId::from_declaration("callable", callable.span)])
953        );
954    }
955
956    #[test]
957    fn deferred_module_callable_sees_later_module_value() {
958        let read = fn_decl(10, "read", vec![identifier(11, "counter")]);
959        let counter = let_binding(20, "counter");
960
961        let captured = captured_bindings_in_compiled_module(
962            &[read, counter.clone()],
963            &MatchPatternCatalog::default(),
964        );
965
966        assert_eq!(
967            captured,
968            HashSet::from([BindingId::from_declaration("counter", counter.span)])
969        );
970    }
971
972    #[test]
973    fn module_statement_does_not_see_later_module_value() {
974        let early = node(
975            10,
976            Node::ConstBinding {
977                pattern: BindingPattern::Identifier("read".to_string()),
978                type_ann: None,
979                value: Box::new(closure(11, Vec::new(), vec![identifier(12, "counter")])),
980                is_pub: false,
981            },
982        );
983        let counter = let_binding(20, "counter");
984
985        let captured = captured_bindings_in_compiled_module(
986            &[early, counter],
987            &MatchPatternCatalog::default(),
988        );
989
990        assert!(captured.is_empty());
991    }
992
993    #[test]
994    fn match_bindings_shadow_same_named_outer_mutables() {
995        let pin = let_binding(10, "pin");
996        let alias = let_binding(20, "alias");
997        let rest = let_binding(30, "rest");
998        let match_expr = node(
999            40,
1000            Node::MatchExpr {
1001                value: Box::new(identifier(41, "value")),
1002                arms: vec![
1003                    MatchArm {
1004                        pattern: identifier(42, "pin"),
1005                        guard: Some(Box::new(identifier(43, "pin"))),
1006                        body: vec![identifier(44, "pin")],
1007                        span: Span::with_offsets(42, 47, 1, 43),
1008                    },
1009                    MatchArm {
1010                        pattern: node(
1011                            50,
1012                            Node::DictLiteral(vec![DictEntry {
1013                                key: node(51, Node::StringLiteral("key".to_string())),
1014                                value: identifier(52, "alias"),
1015                            }]),
1016                        ),
1017                        guard: None,
1018                        body: vec![identifier(53, "alias")],
1019                        span: Span::with_offsets(50, 55, 1, 51),
1020                    },
1021                    MatchArm {
1022                        pattern: node(
1023                            60,
1024                            Node::ListLiteral(vec![
1025                                identifier(61, "pin"),
1026                                node(62, Node::Spread(Box::new(identifier(63, "rest")))),
1027                            ]),
1028                        ),
1029                        guard: None,
1030                        body: vec![identifier(64, "pin"), identifier(65, "rest")],
1031                        span: Span::with_offsets(60, 67, 1, 61),
1032                    },
1033                    MatchArm {
1034                        pattern: node(
1035                            70,
1036                            Node::FunctionCall {
1037                                name: "Some".to_string(),
1038                                type_args: Vec::new(),
1039                                args: vec![identifier(71, "alias")],
1040                            },
1041                        ),
1042                        guard: None,
1043                        body: vec![identifier(72, "alias")],
1044                        span: Span::with_offsets(70, 74, 1, 71),
1045                    },
1046                    MatchArm {
1047                        pattern: node(
1048                            80,
1049                            Node::MethodCall {
1050                                object: Box::new(identifier(81, "Result")),
1051                                method: "Ok".to_string(),
1052                                args: vec![identifier(82, "rest")],
1053                            },
1054                        ),
1055                        guard: None,
1056                        body: vec![identifier(83, "rest")],
1057                        span: Span::with_offsets(80, 85, 1, 81),
1058                    },
1059                ],
1060            },
1061        );
1062
1063        let nested = closure(39, Vec::new(), vec![match_expr]);
1064        let captured = captured_bindings_in_nested_callables(
1065            &[pin.clone(), alias.clone(), rest.clone(), nested],
1066            &enum_pattern_catalog(),
1067        );
1068        assert!(!captured.contains(&BindingId::from_declaration("pin", pin.span)));
1069        assert!(!captured.contains(&BindingId::from_declaration("alias", alias.span)));
1070        assert!(!captured.contains(&BindingId::from_declaration("rest", rest.span)));
1071    }
1072
1073    #[test]
1074    fn unresolved_call_patterns_capture_expression_references() {
1075        let callable = let_binding(10, "callable");
1076        let object = let_binding(20, "object");
1077        let argument = let_binding(30, "argument");
1078        let match_expr = node(
1079            40,
1080            Node::MatchExpr {
1081                value: Box::new(identifier(41, "value")),
1082                arms: vec![
1083                    MatchArm {
1084                        pattern: node(
1085                            42,
1086                            Node::FunctionCall {
1087                                name: "callable".to_string(),
1088                                type_args: Vec::new(),
1089                                args: vec![identifier(43, "argument")],
1090                            },
1091                        ),
1092                        guard: None,
1093                        body: Vec::new(),
1094                        span: Span::with_offsets(42, 44, 1, 43),
1095                    },
1096                    MatchArm {
1097                        pattern: node(
1098                            45,
1099                            Node::MethodCall {
1100                                object: Box::new(identifier(46, "object")),
1101                                method: "compute".to_string(),
1102                                args: vec![identifier(47, "argument")],
1103                            },
1104                        ),
1105                        guard: None,
1106                        body: Vec::new(),
1107                        span: Span::with_offsets(45, 48, 1, 46),
1108                    },
1109                ],
1110            },
1111        );
1112        let nested = closure(39, Vec::new(), vec![match_expr]);
1113
1114        let captured = captured(&[callable.clone(), object.clone(), argument.clone(), nested]);
1115        assert!(captured.contains(&BindingId::from_declaration("callable", callable.span)));
1116        assert!(captured.contains(&BindingId::from_declaration("object", object.span)));
1117        assert!(captured.contains(&BindingId::from_declaration("argument", argument.span)));
1118    }
1119
1120    #[test]
1121    fn qualified_enum_constant_pattern_does_not_capture_enum_name() {
1122        let result = let_binding(10, "Result");
1123        let match_expr = node(
1124            20,
1125            Node::MatchExpr {
1126                value: Box::new(identifier(21, "value")),
1127                arms: vec![MatchArm {
1128                    pattern: node(
1129                        22,
1130                        Node::PropertyAccess {
1131                            object: Box::new(identifier(23, "Result")),
1132                            property: "Ok".to_string(),
1133                        },
1134                    ),
1135                    guard: None,
1136                    body: Vec::new(),
1137                    span: Span::with_offsets(22, 25, 1, 23),
1138                }],
1139            },
1140        );
1141        let nested = closure(19, Vec::new(), vec![match_expr]);
1142
1143        let captured = captured_bindings_in_nested_callables(
1144            &[result.clone(), nested],
1145            &enum_pattern_catalog(),
1146        );
1147        assert!(!captured.contains(&BindingId::from_declaration("Result", result.span)));
1148    }
1149
1150    #[test]
1151    fn parameter_defaults_see_only_earlier_parameters() {
1152        let first = let_binding(10, "first");
1153        let current = let_binding(20, "current");
1154        let later = let_binding(30, "later");
1155        let nested = closure(
1156            40,
1157            vec![
1158                defaulted_param("first", identifier(41, "later")),
1159                defaulted_param("current", identifier(42, "current")),
1160                defaulted_param("later", identifier(43, "first")),
1161            ],
1162            Vec::new(),
1163        );
1164
1165        let captured = captured(&[first.clone(), current.clone(), later.clone(), nested]);
1166        assert!(!captured.contains(&BindingId::from_declaration("first", first.span)));
1167        assert!(captured.contains(&BindingId::from_declaration("current", current.span)));
1168        assert!(captured.contains(&BindingId::from_declaration("later", later.span)));
1169    }
1170
1171    #[test]
1172    fn parameter_shadow_does_not_capture_outer_binding() {
1173        let outer = let_binding(10, "pin");
1174        let body = vec![
1175            outer.clone(),
1176            closure(
1177                20,
1178                vec![TypedParam::untyped("pin")],
1179                vec![identifier(21, "pin")],
1180            ),
1181        ];
1182
1183        assert!(!captured(&body).contains(&BindingId::from_declaration("pin", outer.span)));
1184    }
1185
1186    #[test]
1187    fn block_shadow_captures_exact_inner_binding() {
1188        let outer = let_binding(10, "pin");
1189        let inner = let_binding(20, "pin");
1190        let body = vec![
1191            outer.clone(),
1192            node(
1193                19,
1194                Node::Block(vec![
1195                    inner.clone(),
1196                    closure(30, Vec::new(), vec![identifier(31, "pin")]),
1197                ]),
1198            ),
1199        ];
1200
1201        let captured = captured(&body);
1202        assert!(captured.contains(&BindingId::from_declaration("pin", inner.span)));
1203        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1204    }
1205
1206    #[test]
1207    fn later_block_binding_does_not_shadow_an_earlier_reference() {
1208        let outer = let_binding(10, "pin");
1209        let inner = let_binding(30, "pin");
1210        let body = vec![
1211            outer.clone(),
1212            node(
1213                19,
1214                Node::Block(vec![
1215                    closure(20, Vec::new(), vec![identifier(21, "pin")]),
1216                    inner.clone(),
1217                ]),
1218            ),
1219        ];
1220
1221        let captured = captured(&body);
1222        assert!(captured.contains(&BindingId::from_declaration("pin", outer.span)));
1223        assert!(!captured.contains(&BindingId::from_declaration("pin", inner.span)));
1224    }
1225
1226    #[test]
1227    fn loop_binding_shadows_outer_capture() {
1228        let outer = let_binding(10, "pin");
1229        let loop_node = node(
1230            20,
1231            Node::ForIn {
1232                pattern: BindingPattern::Identifier("pin".to_string()),
1233                iterable: Box::new(identifier(21, "pins")),
1234                body: vec![closure(22, Vec::new(), vec![identifier(23, "pin")])],
1235            },
1236        );
1237        let captured = captured(&[outer.clone(), loop_node.clone()]);
1238
1239        assert!(captured.contains(&BindingId::from_declaration("pin", loop_node.span)));
1240        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1241    }
1242
1243    #[test]
1244    fn catch_and_select_bindings_shadow_outer_capture() {
1245        let outer = let_binding(10, "pin");
1246        let try_catch = node(
1247            20,
1248            Node::TryCatch {
1249                body: Vec::new(),
1250                try_span: Span::dummy(),
1251                has_catch: true,
1252                error_var: Some("pin".to_string()),
1253                error_type: None,
1254                catch_body: vec![closure(21, Vec::new(), vec![identifier(22, "pin")])],
1255                catch_span: Some(Span::dummy()),
1256                finally_body: None,
1257                finally_span: None,
1258            },
1259        );
1260        let select = node(
1261            30,
1262            Node::SelectExpr {
1263                cases: vec![SelectCase {
1264                    variable: "pin".to_string(),
1265                    channel: Box::new(identifier(31, "channel")),
1266                    body: vec![closure(32, Vec::new(), vec![identifier(33, "pin")])],
1267                }],
1268                timeout: None,
1269                default_body: None,
1270            },
1271        );
1272
1273        let captured = captured(&[outer.clone(), try_catch, select]);
1274        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1275    }
1276
1277    #[test]
1278    fn nested_callable_capture_is_transitive() {
1279        let outer = let_binding(10, "pin");
1280        let nested = closure(
1281            20,
1282            Vec::new(),
1283            vec![closure(30, Vec::new(), vec![identifier(31, "pin")])],
1284        );
1285        let captured = captured(&[outer.clone(), nested]);
1286
1287        assert!(captured.contains(&BindingId::from_declaration("pin", outer.span)));
1288    }
1289
1290    #[test]
1291    fn nested_reassignment_ignores_shadowed_parameter() {
1292        let body = vec![closure(
1293            10,
1294            vec![TypedParam::untyped("pin")],
1295            vec![node(
1296                11,
1297                Node::Assignment {
1298                    target: Box::new(identifier(12, "pin")),
1299                    value: Box::new(identifier(13, "next")),
1300                    op: None,
1301                },
1302            )],
1303        )];
1304
1305        assert!(
1306            nested_callable_reassigned_names(&body, &MatchPatternCatalog::default()).is_empty()
1307        );
1308    }
1309
1310    #[test]
1311    fn nested_reassignment_ignores_enum_payload_binding() {
1312        let assignment = node(
1313            14,
1314            Node::Assignment {
1315                target: Box::new(identifier(15, "pin")),
1316                value: Box::new(identifier(16, "next")),
1317                op: None,
1318            },
1319        );
1320        let body = vec![node(
1321            10,
1322            Node::MatchExpr {
1323                value: Box::new(identifier(11, "value")),
1324                arms: vec![MatchArm {
1325                    pattern: node(
1326                        12,
1327                        Node::FunctionCall {
1328                            name: "Some".to_string(),
1329                            type_args: Vec::new(),
1330                            args: vec![identifier(13, "pin")],
1331                        },
1332                    ),
1333                    guard: None,
1334                    body: vec![closure(14, Vec::new(), vec![assignment])],
1335                    span: Span::with_offsets(12, 18, 1, 13),
1336                }],
1337            },
1338        )];
1339
1340        assert_eq!(
1341            nested_callable_reassigned_names(&body, &enum_pattern_catalog()),
1342            Vec::<String>::new()
1343        );
1344    }
1345}