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
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 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 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 let mut default_scopes = scopes.to_vec();
401 default_scopes.push(Scope::new());
402 for param in params {
403 if let Some(default) = ¶m.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 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
787pub 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
806pub 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(¶m.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}