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