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