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