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