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