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