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