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