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