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