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    )
788}
789
790fn extend_scope_with_value_declaration(scope: &mut Scope, node: &SNode, owner: &BindingOwner) {
791    let (Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. }) = &node.node else {
792        return;
793    };
794    for binding in binding_pattern_ids(pattern, node.span) {
795        let name = binding.name.clone();
796        let entry = match owner {
797            BindingOwner::Current => ScopeBinding::Current(binding),
798            BindingOwner::Nested => ScopeBinding::Nested,
799        };
800        scope.insert(name, entry);
801    }
802}
803
804fn pattern_scope(pattern: &BindingPattern, declaration: Span, owner: &BindingOwner) -> Scope {
805    let mut scope = Scope::new();
806    for binding in binding_pattern_ids(pattern, declaration) {
807        let name = binding.name.clone();
808        let entry = match owner {
809            BindingOwner::Current => ScopeBinding::Current(binding),
810            BindingOwner::Nested => ScopeBinding::Nested,
811        };
812        scope.insert(name, entry);
813    }
814    scope
815}
816
817fn names_scope(names: impl IntoIterator<Item = String>) -> Scope {
818    names
819        .into_iter()
820        .filter(|name| !is_discard_name(name))
821        .map(|name| (name, ScopeBinding::Nested))
822        .collect()
823}
824
825fn resolve<'a>(scopes: &'a [Scope], name: &str) -> Option<&'a ScopeBinding> {
826    scopes.iter().rev().find_map(|scope| scope.get(name))
827}
828
829#[cfg(test)]
830mod tests {
831    use harn_lexer::Span;
832
833    use crate::ast::{DictEntry, MatchArm, SelectCase};
834
835    use super::*;
836
837    fn node(offset: usize, node: Node) -> SNode {
838        SNode::new(node, Span::with_offsets(offset, offset + 1, 1, offset + 1))
839    }
840
841    fn identifier(offset: usize, name: &str) -> SNode {
842        node(offset, Node::Identifier(name.to_string()))
843    }
844
845    fn function_call(offset: usize, name: &str) -> SNode {
846        node(
847            offset,
848            Node::FunctionCall {
849                name: name.to_string(),
850                type_args: Vec::new(),
851                args: Vec::new(),
852            },
853        )
854    }
855
856    fn let_binding(offset: usize, name: &str) -> SNode {
857        node(
858            offset,
859            Node::LetBinding {
860                pattern: BindingPattern::Identifier(name.to_string()),
861                type_ann: None,
862                value: Box::new(identifier(offset + 100, "value")),
863                is_pub: false,
864            },
865        )
866    }
867
868    fn closure(offset: usize, params: Vec<TypedParam>, body: Vec<SNode>) -> SNode {
869        node(
870            offset,
871            Node::Closure {
872                params,
873                return_type: None,
874                throws: None,
875                body,
876                fn_syntax: false,
877            },
878        )
879    }
880
881    fn fn_decl(offset: usize, name: &str, body: Vec<SNode>) -> SNode {
882        node(
883            offset,
884            Node::FnDecl {
885                name: name.to_string(),
886                type_params: Vec::new(),
887                params: Vec::new(),
888                return_type: None,
889                throws: None,
890                where_clauses: Vec::new(),
891                body,
892                is_pub: false,
893                is_stream: false,
894            },
895        )
896    }
897
898    fn defaulted_param(name: &str, default: SNode) -> TypedParam {
899        TypedParam {
900            name: name.to_string(),
901            type_expr: None,
902            default_value: Some(Box::new(default)),
903            rest: false,
904        }
905    }
906
907    fn captured(body: &[SNode]) -> HashSet<BindingId> {
908        captured_bindings_in_nested_callables(body, &MatchPatternCatalog::default())
909    }
910
911    fn enum_pattern_catalog() -> MatchPatternCatalog {
912        MatchPatternCatalog::new(
913            &HashSet::from(["Option".to_string(), "Result".to_string()]),
914            &HashMap::from([
915                ("Some".to_string(), vec!["Option".to_string()]),
916                ("Ok".to_string(), vec!["Result".to_string()]),
917            ]),
918        )
919    }
920
921    #[test]
922    fn function_call_callee_is_a_lexical_reference() {
923        let callable = let_binding(10, "callable");
924        let nested = closure(
925            30,
926            Vec::new(),
927            vec![function_call(31, "callable"), function_call(33, "log")],
928        );
929
930        let captured = captured(&[callable.clone(), nested]);
931        assert!(captured.contains(&BindingId::from_declaration("callable", callable.span)));
932    }
933
934    #[test]
935    fn earlier_value_binding_shadows_later_hoisted_callable_for_capture() {
936        let callable = let_binding(10, "callable");
937        let invoke = node(
938            20,
939            Node::ConstBinding {
940                pattern: BindingPattern::Identifier("invoke".to_string()),
941                type_ann: None,
942                value: Box::new(closure(21, Vec::new(), vec![function_call(22, "callable")])),
943                is_pub: false,
944            },
945        );
946        let later_callable = fn_decl(30, "callable", Vec::new());
947
948        let captured = captured(&[callable.clone(), invoke, later_callable]);
949        assert_eq!(
950            captured,
951            HashSet::from([BindingId::from_declaration("callable", callable.span)])
952        );
953    }
954
955    #[test]
956    fn deferred_module_callable_sees_later_module_value() {
957        let read = fn_decl(10, "read", vec![identifier(11, "counter")]);
958        let counter = let_binding(20, "counter");
959
960        let captured = captured_bindings_in_compiled_module(
961            &[read, counter.clone()],
962            &MatchPatternCatalog::default(),
963        );
964
965        assert_eq!(
966            captured,
967            HashSet::from([BindingId::from_declaration("counter", counter.span)])
968        );
969    }
970
971    #[test]
972    fn module_statement_does_not_see_later_module_value() {
973        let early = node(
974            10,
975            Node::ConstBinding {
976                pattern: BindingPattern::Identifier("read".to_string()),
977                type_ann: None,
978                value: Box::new(closure(11, Vec::new(), vec![identifier(12, "counter")])),
979                is_pub: false,
980            },
981        );
982        let counter = let_binding(20, "counter");
983
984        let captured = captured_bindings_in_compiled_module(
985            &[early, counter],
986            &MatchPatternCatalog::default(),
987        );
988
989        assert!(captured.is_empty());
990    }
991
992    #[test]
993    fn match_bindings_shadow_same_named_outer_mutables() {
994        let pin = let_binding(10, "pin");
995        let alias = let_binding(20, "alias");
996        let rest = let_binding(30, "rest");
997        let match_expr = node(
998            40,
999            Node::MatchExpr {
1000                value: Box::new(identifier(41, "value")),
1001                arms: vec![
1002                    MatchArm {
1003                        pattern: identifier(42, "pin"),
1004                        guard: Some(Box::new(identifier(43, "pin"))),
1005                        body: vec![identifier(44, "pin")],
1006                        span: Span::with_offsets(42, 47, 1, 43),
1007                    },
1008                    MatchArm {
1009                        pattern: node(
1010                            50,
1011                            Node::DictLiteral(vec![DictEntry {
1012                                key: node(51, Node::StringLiteral("key".to_string())),
1013                                value: identifier(52, "alias"),
1014                            }]),
1015                        ),
1016                        guard: None,
1017                        body: vec![identifier(53, "alias")],
1018                        span: Span::with_offsets(50, 55, 1, 51),
1019                    },
1020                    MatchArm {
1021                        pattern: node(
1022                            60,
1023                            Node::ListLiteral(vec![
1024                                identifier(61, "pin"),
1025                                node(62, Node::Spread(Box::new(identifier(63, "rest")))),
1026                            ]),
1027                        ),
1028                        guard: None,
1029                        body: vec![identifier(64, "pin"), identifier(65, "rest")],
1030                        span: Span::with_offsets(60, 67, 1, 61),
1031                    },
1032                    MatchArm {
1033                        pattern: node(
1034                            70,
1035                            Node::FunctionCall {
1036                                name: "Some".to_string(),
1037                                type_args: Vec::new(),
1038                                args: vec![identifier(71, "alias")],
1039                            },
1040                        ),
1041                        guard: None,
1042                        body: vec![identifier(72, "alias")],
1043                        span: Span::with_offsets(70, 74, 1, 71),
1044                    },
1045                    MatchArm {
1046                        pattern: node(
1047                            80,
1048                            Node::MethodCall {
1049                                object: Box::new(identifier(81, "Result")),
1050                                method: "Ok".to_string(),
1051                                args: vec![identifier(82, "rest")],
1052                            },
1053                        ),
1054                        guard: None,
1055                        body: vec![identifier(83, "rest")],
1056                        span: Span::with_offsets(80, 85, 1, 81),
1057                    },
1058                ],
1059            },
1060        );
1061
1062        let nested = closure(39, Vec::new(), vec![match_expr]);
1063        let captured = captured_bindings_in_nested_callables(
1064            &[pin.clone(), alias.clone(), rest.clone(), nested],
1065            &enum_pattern_catalog(),
1066        );
1067        assert!(!captured.contains(&BindingId::from_declaration("pin", pin.span)));
1068        assert!(!captured.contains(&BindingId::from_declaration("alias", alias.span)));
1069        assert!(!captured.contains(&BindingId::from_declaration("rest", rest.span)));
1070    }
1071
1072    #[test]
1073    fn unresolved_call_patterns_capture_expression_references() {
1074        let callable = let_binding(10, "callable");
1075        let object = let_binding(20, "object");
1076        let argument = let_binding(30, "argument");
1077        let match_expr = node(
1078            40,
1079            Node::MatchExpr {
1080                value: Box::new(identifier(41, "value")),
1081                arms: vec![
1082                    MatchArm {
1083                        pattern: node(
1084                            42,
1085                            Node::FunctionCall {
1086                                name: "callable".to_string(),
1087                                type_args: Vec::new(),
1088                                args: vec![identifier(43, "argument")],
1089                            },
1090                        ),
1091                        guard: None,
1092                        body: Vec::new(),
1093                        span: Span::with_offsets(42, 44, 1, 43),
1094                    },
1095                    MatchArm {
1096                        pattern: node(
1097                            45,
1098                            Node::MethodCall {
1099                                object: Box::new(identifier(46, "object")),
1100                                method: "compute".to_string(),
1101                                args: vec![identifier(47, "argument")],
1102                            },
1103                        ),
1104                        guard: None,
1105                        body: Vec::new(),
1106                        span: Span::with_offsets(45, 48, 1, 46),
1107                    },
1108                ],
1109            },
1110        );
1111        let nested = closure(39, Vec::new(), vec![match_expr]);
1112
1113        let captured = captured(&[callable.clone(), object.clone(), argument.clone(), nested]);
1114        assert!(captured.contains(&BindingId::from_declaration("callable", callable.span)));
1115        assert!(captured.contains(&BindingId::from_declaration("object", object.span)));
1116        assert!(captured.contains(&BindingId::from_declaration("argument", argument.span)));
1117    }
1118
1119    #[test]
1120    fn qualified_enum_constant_pattern_does_not_capture_enum_name() {
1121        let result = let_binding(10, "Result");
1122        let match_expr = node(
1123            20,
1124            Node::MatchExpr {
1125                value: Box::new(identifier(21, "value")),
1126                arms: vec![MatchArm {
1127                    pattern: node(
1128                        22,
1129                        Node::PropertyAccess {
1130                            object: Box::new(identifier(23, "Result")),
1131                            property: "Ok".to_string(),
1132                        },
1133                    ),
1134                    guard: None,
1135                    body: Vec::new(),
1136                    span: Span::with_offsets(22, 25, 1, 23),
1137                }],
1138            },
1139        );
1140        let nested = closure(19, Vec::new(), vec![match_expr]);
1141
1142        let captured = captured_bindings_in_nested_callables(
1143            &[result.clone(), nested],
1144            &enum_pattern_catalog(),
1145        );
1146        assert!(!captured.contains(&BindingId::from_declaration("Result", result.span)));
1147    }
1148
1149    #[test]
1150    fn parameter_defaults_see_only_earlier_parameters() {
1151        let first = let_binding(10, "first");
1152        let current = let_binding(20, "current");
1153        let later = let_binding(30, "later");
1154        let nested = closure(
1155            40,
1156            vec![
1157                defaulted_param("first", identifier(41, "later")),
1158                defaulted_param("current", identifier(42, "current")),
1159                defaulted_param("later", identifier(43, "first")),
1160            ],
1161            Vec::new(),
1162        );
1163
1164        let captured = captured(&[first.clone(), current.clone(), later.clone(), nested]);
1165        assert!(!captured.contains(&BindingId::from_declaration("first", first.span)));
1166        assert!(captured.contains(&BindingId::from_declaration("current", current.span)));
1167        assert!(captured.contains(&BindingId::from_declaration("later", later.span)));
1168    }
1169
1170    #[test]
1171    fn parameter_shadow_does_not_capture_outer_binding() {
1172        let outer = let_binding(10, "pin");
1173        let body = vec![
1174            outer.clone(),
1175            closure(
1176                20,
1177                vec![TypedParam::untyped("pin")],
1178                vec![identifier(21, "pin")],
1179            ),
1180        ];
1181
1182        assert!(!captured(&body).contains(&BindingId::from_declaration("pin", outer.span)));
1183    }
1184
1185    #[test]
1186    fn block_shadow_captures_exact_inner_binding() {
1187        let outer = let_binding(10, "pin");
1188        let inner = let_binding(20, "pin");
1189        let body = vec![
1190            outer.clone(),
1191            node(
1192                19,
1193                Node::Block(vec![
1194                    inner.clone(),
1195                    closure(30, Vec::new(), vec![identifier(31, "pin")]),
1196                ]),
1197            ),
1198        ];
1199
1200        let captured = captured(&body);
1201        assert!(captured.contains(&BindingId::from_declaration("pin", inner.span)));
1202        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1203    }
1204
1205    #[test]
1206    fn later_block_binding_does_not_shadow_an_earlier_reference() {
1207        let outer = let_binding(10, "pin");
1208        let inner = let_binding(30, "pin");
1209        let body = vec![
1210            outer.clone(),
1211            node(
1212                19,
1213                Node::Block(vec![
1214                    closure(20, Vec::new(), vec![identifier(21, "pin")]),
1215                    inner.clone(),
1216                ]),
1217            ),
1218        ];
1219
1220        let captured = captured(&body);
1221        assert!(captured.contains(&BindingId::from_declaration("pin", outer.span)));
1222        assert!(!captured.contains(&BindingId::from_declaration("pin", inner.span)));
1223    }
1224
1225    #[test]
1226    fn loop_binding_shadows_outer_capture() {
1227        let outer = let_binding(10, "pin");
1228        let loop_node = node(
1229            20,
1230            Node::ForIn {
1231                pattern: BindingPattern::Identifier("pin".to_string()),
1232                iterable: Box::new(identifier(21, "pins")),
1233                body: vec![closure(22, Vec::new(), vec![identifier(23, "pin")])],
1234            },
1235        );
1236        let captured = captured(&[outer.clone(), loop_node.clone()]);
1237
1238        assert!(captured.contains(&BindingId::from_declaration("pin", loop_node.span)));
1239        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1240    }
1241
1242    #[test]
1243    fn catch_and_select_bindings_shadow_outer_capture() {
1244        let outer = let_binding(10, "pin");
1245        let try_catch = node(
1246            20,
1247            Node::TryCatch {
1248                body: Vec::new(),
1249                try_span: Span::dummy(),
1250                has_catch: true,
1251                error_var: Some("pin".to_string()),
1252                error_type: None,
1253                catch_body: vec![closure(21, Vec::new(), vec![identifier(22, "pin")])],
1254                catch_span: Some(Span::dummy()),
1255                finally_body: None,
1256                finally_span: None,
1257            },
1258        );
1259        let select = node(
1260            30,
1261            Node::SelectExpr {
1262                cases: vec![SelectCase {
1263                    variable: "pin".to_string(),
1264                    channel: Box::new(identifier(31, "channel")),
1265                    body: vec![closure(32, Vec::new(), vec![identifier(33, "pin")])],
1266                }],
1267                timeout: None,
1268                default_body: None,
1269            },
1270        );
1271
1272        let captured = captured(&[outer.clone(), try_catch, select]);
1273        assert!(!captured.contains(&BindingId::from_declaration("pin", outer.span)));
1274    }
1275
1276    #[test]
1277    fn nested_callable_capture_is_transitive() {
1278        let outer = let_binding(10, "pin");
1279        let nested = closure(
1280            20,
1281            Vec::new(),
1282            vec![closure(30, Vec::new(), vec![identifier(31, "pin")])],
1283        );
1284        let captured = captured(&[outer.clone(), nested]);
1285
1286        assert!(captured.contains(&BindingId::from_declaration("pin", outer.span)));
1287    }
1288
1289    #[test]
1290    fn nested_reassignment_ignores_shadowed_parameter() {
1291        let body = vec![closure(
1292            10,
1293            vec![TypedParam::untyped("pin")],
1294            vec![node(
1295                11,
1296                Node::Assignment {
1297                    target: Box::new(identifier(12, "pin")),
1298                    value: Box::new(identifier(13, "next")),
1299                    op: None,
1300                },
1301            )],
1302        )];
1303
1304        assert!(
1305            nested_callable_reassigned_names(&body, &MatchPatternCatalog::default()).is_empty()
1306        );
1307    }
1308
1309    #[test]
1310    fn nested_reassignment_ignores_enum_payload_binding() {
1311        let assignment = node(
1312            14,
1313            Node::Assignment {
1314                target: Box::new(identifier(15, "pin")),
1315                value: Box::new(identifier(16, "next")),
1316                op: None,
1317            },
1318        );
1319        let body = vec![node(
1320            10,
1321            Node::MatchExpr {
1322                value: Box::new(identifier(11, "value")),
1323                arms: vec![MatchArm {
1324                    pattern: node(
1325                        12,
1326                        Node::FunctionCall {
1327                            name: "Some".to_string(),
1328                            type_args: Vec::new(),
1329                            args: vec![identifier(13, "pin")],
1330                        },
1331                    ),
1332                    guard: None,
1333                    body: vec![closure(14, Vec::new(), vec![assignment])],
1334                    span: Span::with_offsets(12, 18, 1, 13),
1335                }],
1336            },
1337        )];
1338
1339        assert_eq!(
1340            nested_callable_reassigned_names(&body, &enum_pattern_catalog()),
1341            Vec::<String>::new()
1342        );
1343    }
1344}