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