1use std::collections::{HashMap, HashSet};
27
28use bock_air::{AIRNode, AirInterpolationPart, NodeKind};
29use bock_ast::{TypePath, Visibility};
30use bock_errors::{DiagnosticBag, DiagnosticCode};
31
32use crate::AIRModule;
33pub use bock_air::stubs::EffectRef;
34
35const E_UNDECLARED_EFFECT: DiagnosticCode = DiagnosticCode {
38 prefix: 'E',
39 number: 6001,
40};
41const W_UNDECLARED_EFFECT: DiagnosticCode = DiagnosticCode {
42 prefix: 'W',
43 number: 6002,
44};
45const E_PROPAGATED_EFFECT: DiagnosticCode = DiagnosticCode {
46 prefix: 'E',
47 number: 6003,
48};
49const W_PROPAGATED_EFFECT: DiagnosticCode = DiagnosticCode {
50 prefix: 'W',
51 number: 6004,
52};
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Strictness {
61 Sketch,
65
66 Development,
69
70 Production,
73}
74
75fn type_path_to_effect_ref(path: &TypePath) -> EffectRef {
79 let name = path
80 .segments
81 .iter()
82 .map(|s| s.name.as_str())
83 .collect::<Vec<_>>()
84 .join(".");
85 EffectRef::new(name)
86}
87
88fn is_ambient(effect: &EffectRef) -> bool {
92 matches!(effect.name.as_str(), "Panic" | "Allocate" | "Pure")
93}
94
95fn capability_for_effect(effect: &EffectRef) -> Option<String> {
100 let name = effect.name.to_lowercase();
101 if name.contains("log") || name.contains("print") || name.contains("console") {
102 Some("Io.Stdout".into())
103 } else if name.contains("http") || name.contains("net") || name.contains("socket") {
104 Some("Io.Network".into())
105 } else if name.contains("file") || name.contains("fs") || name.contains("disk") {
106 Some("Io.FileSystem".into())
107 } else if name.contains("clock") || name.contains("time") || name.contains("date") {
108 Some("Io.Clock".into())
109 } else if name.contains("env") || name.contains("os") || name.contains("process") {
110 Some("Io.Process".into())
111 } else {
112 None
113 }
114}
115
116fn collect_node_effects(
125 node: &AIRNode,
126 used_effects: &mut HashSet<EffectRef>,
127 called_fns: &mut HashSet<String>,
128) {
129 match &node.kind {
130 NodeKind::EffectOp { effect, args, .. } => {
132 used_effects.insert(type_path_to_effect_ref(effect));
133 for arg in args {
134 collect_node_effects(&arg.value, used_effects, called_fns);
135 }
136 }
137
138 NodeKind::Call {
140 callee,
141 args,
142 type_args,
143 } => {
144 if let NodeKind::Identifier { name } = &callee.kind {
145 called_fns.insert(name.name.clone());
146 }
147 collect_node_effects(callee, used_effects, called_fns);
148 for arg in args {
149 collect_node_effects(&arg.value, used_effects, called_fns);
150 }
151 for ta in type_args {
152 collect_node_effects(ta, used_effects, called_fns);
153 }
154 }
155
156 NodeKind::MethodCall { receiver, args, .. } => {
158 collect_node_effects(receiver, used_effects, called_fns);
159 for arg in args {
160 collect_node_effects(&arg.value, used_effects, called_fns);
161 }
162 }
163
164 NodeKind::HandlingBlock { handlers, body } => {
166 let handled: HashSet<EffectRef> = handlers
167 .iter()
168 .map(|h| type_path_to_effect_ref(&h.effect))
169 .collect();
170
171 let mut body_effects = HashSet::new();
172 let mut body_calls = HashSet::new();
173 collect_node_effects(body, &mut body_effects, &mut body_calls);
174
175 for e in body_effects {
177 if !handled.contains(&e) {
178 used_effects.insert(e);
179 }
180 }
181 called_fns.extend(body_calls);
182 }
183
184 NodeKind::FnDecl { .. } => {}
186
187 NodeKind::Lambda { body, .. } => {
189 collect_node_effects(body, used_effects, called_fns);
190 }
191
192 NodeKind::Block { stmts, tail } => {
193 for s in stmts {
194 collect_node_effects(s, used_effects, called_fns);
195 }
196 if let Some(t) = tail {
197 collect_node_effects(t, used_effects, called_fns);
198 }
199 }
200
201 NodeKind::LetBinding { value, .. } => {
202 collect_node_effects(value, used_effects, called_fns);
203 }
204
205 NodeKind::Assign { target, value, .. } => {
206 collect_node_effects(target, used_effects, called_fns);
207 collect_node_effects(value, used_effects, called_fns);
208 }
209
210 NodeKind::If {
211 condition,
212 then_block,
213 else_block,
214 ..
215 } => {
216 collect_node_effects(condition, used_effects, called_fns);
217 collect_node_effects(then_block, used_effects, called_fns);
218 if let Some(e) = else_block {
219 collect_node_effects(e, used_effects, called_fns);
220 }
221 }
222
223 NodeKind::Guard {
224 let_pattern,
225 condition,
226 else_block,
227 } => {
228 if let Some(pat) = let_pattern {
229 collect_node_effects(pat, used_effects, called_fns);
230 }
231 collect_node_effects(condition, used_effects, called_fns);
232 collect_node_effects(else_block, used_effects, called_fns);
233 }
234
235 NodeKind::Match { scrutinee, arms } => {
236 collect_node_effects(scrutinee, used_effects, called_fns);
237 for arm in arms {
238 collect_node_effects(arm, used_effects, called_fns);
239 }
240 }
241
242 NodeKind::MatchArm { guard, body, .. } => {
243 if let Some(g) = guard {
244 collect_node_effects(g, used_effects, called_fns);
245 }
246 collect_node_effects(body, used_effects, called_fns);
247 }
248
249 NodeKind::For { iterable, body, .. } => {
250 collect_node_effects(iterable, used_effects, called_fns);
251 collect_node_effects(body, used_effects, called_fns);
252 }
253
254 NodeKind::While { condition, body } => {
255 collect_node_effects(condition, used_effects, called_fns);
256 collect_node_effects(body, used_effects, called_fns);
257 }
258
259 NodeKind::Loop { body } => {
260 collect_node_effects(body, used_effects, called_fns);
261 }
262
263 NodeKind::Return { value: Some(v) } | NodeKind::Break { value: Some(v) } => {
264 collect_node_effects(v, used_effects, called_fns);
265 }
266
267 NodeKind::Return { value: None } | NodeKind::Break { value: None } => {}
268
269 NodeKind::BinaryOp { left, right, .. } => {
270 collect_node_effects(left, used_effects, called_fns);
271 collect_node_effects(right, used_effects, called_fns);
272 }
273
274 NodeKind::UnaryOp { operand, .. } => {
275 collect_node_effects(operand, used_effects, called_fns);
276 }
277
278 NodeKind::FieldAccess { object, .. } => {
279 collect_node_effects(object, used_effects, called_fns);
280 }
281
282 NodeKind::Index { object, index } => {
283 collect_node_effects(object, used_effects, called_fns);
284 collect_node_effects(index, used_effects, called_fns);
285 }
286
287 NodeKind::Propagate { expr } => {
288 collect_node_effects(expr, used_effects, called_fns);
289 }
290
291 NodeKind::Await { expr } => {
292 collect_node_effects(expr, used_effects, called_fns);
293 }
294
295 NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } | NodeKind::Move { expr } => {
296 collect_node_effects(expr, used_effects, called_fns);
297 }
298
299 NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
300 collect_node_effects(left, used_effects, called_fns);
301 collect_node_effects(right, used_effects, called_fns);
302 }
303
304 NodeKind::Range { lo, hi, .. } => {
305 collect_node_effects(lo, used_effects, called_fns);
306 collect_node_effects(hi, used_effects, called_fns);
307 }
308
309 NodeKind::RecordConstruct { fields, spread, .. } => {
310 for f in fields {
311 if let Some(v) = &f.value {
312 collect_node_effects(v, used_effects, called_fns);
313 }
314 }
315 if let Some(s) = spread {
316 collect_node_effects(s, used_effects, called_fns);
317 }
318 }
319
320 NodeKind::ListLiteral { elems }
321 | NodeKind::SetLiteral { elems }
322 | NodeKind::TupleLiteral { elems } => {
323 for e in elems {
324 collect_node_effects(e, used_effects, called_fns);
325 }
326 }
327
328 NodeKind::MapLiteral { entries } => {
329 for e in entries {
330 collect_node_effects(&e.key, used_effects, called_fns);
331 collect_node_effects(&e.value, used_effects, called_fns);
332 }
333 }
334
335 NodeKind::Interpolation { parts } => {
336 for p in parts {
337 if let AirInterpolationPart::Expr(e) = p {
338 collect_node_effects(e, used_effects, called_fns);
339 }
340 }
341 }
342
343 NodeKind::ResultConstruct { value: Some(v), .. } => {
344 collect_node_effects(v, used_effects, called_fns);
345 }
346
347 NodeKind::ResultConstruct { value: None, .. } => {}
348
349 _ => {}
351 }
352}
353
354#[must_use]
364pub fn infer_effects(fn_node: &AIRNode) -> HashSet<EffectRef> {
365 let mut effects = HashSet::new();
366 if let NodeKind::FnDecl { body, .. } = &fn_node.kind {
367 let mut called_fns = HashSet::new();
368 collect_node_effects(body, &mut effects, &mut called_fns);
369 }
370 effects
371}
372
373#[must_use]
380pub fn track_effects(module: &AIRModule, strictness: Strictness) -> DiagnosticBag {
381 let mut tracker = EffectTracker::new(strictness);
382 tracker.collect_declarations(module);
383 tracker.check_module(module);
384 tracker.diags
385}
386
387struct EffectTracker {
390 diags: DiagnosticBag,
391 strictness: Strictness,
392 fn_declared: HashMap<String, HashSet<EffectRef>>,
394 composite_effects: HashMap<String, HashSet<EffectRef>>,
396}
397
398impl EffectTracker {
399 fn new(strictness: Strictness) -> Self {
400 Self {
401 diags: DiagnosticBag::new(),
402 strictness,
403 fn_declared: HashMap::new(),
404 composite_effects: HashMap::new(),
405 }
406 }
407
408 fn expand_effects(&self, effects: &HashSet<EffectRef>) -> HashSet<EffectRef> {
412 let mut expanded = HashSet::new();
413 for eff in effects {
414 if let Some(components) = self.composite_effects.get(&eff.name) {
415 expanded.extend(components.iter().cloned());
416 } else {
417 expanded.insert(eff.clone());
418 }
419 }
420 expanded
421 }
422
423 fn collect_declarations(&mut self, module: &AIRModule) {
426 match &module.kind {
427 NodeKind::Module { items, .. } => {
428 for item in items {
429 self.collect_item_declaration(item);
430 }
431 }
432 _ => self.collect_item_declaration(module),
433 }
434 }
435
436 fn collect_item_declaration(&mut self, node: &AIRNode) {
437 match &node.kind {
438 NodeKind::FnDecl {
439 name,
440 effect_clause,
441 ..
442 } => {
443 let declared: HashSet<EffectRef> =
444 effect_clause.iter().map(type_path_to_effect_ref).collect();
445 self.fn_declared.insert(name.name.clone(), declared);
446 }
447 NodeKind::EffectDecl {
448 name, components, ..
449 } if !components.is_empty() => {
450 let component_refs: HashSet<EffectRef> =
451 components.iter().map(type_path_to_effect_ref).collect();
452 self.composite_effects
453 .insert(name.name.clone(), component_refs);
454 }
455 NodeKind::ImplBlock { methods, .. } | NodeKind::TraitDecl { methods, .. } => {
456 for m in methods {
457 self.collect_item_declaration(m);
458 }
459 }
460 NodeKind::ClassDecl { methods, .. } => {
461 for m in methods {
462 self.collect_item_declaration(m);
463 }
464 }
465 _ => {}
466 }
467 }
468
469 fn check_module(&mut self, module: &AIRModule) {
472 match &module.kind {
473 NodeKind::Module { items, .. } => {
474 for item in items {
475 self.check_item(item);
476 }
477 }
478 _ => self.check_item(module),
479 }
480 }
481
482 fn check_item(&mut self, node: &AIRNode) {
483 match &node.kind {
484 NodeKind::FnDecl { .. } => self.check_fn(node),
485 NodeKind::ImplBlock { methods, .. } | NodeKind::TraitDecl { methods, .. } => {
486 for m in methods {
487 self.check_item(m);
488 }
489 }
490 NodeKind::ClassDecl { methods, .. } => {
491 for m in methods {
492 self.check_item(m);
493 }
494 }
495 _ => {}
496 }
497 }
498
499 fn check_fn(&mut self, fn_node: &AIRNode) {
500 let NodeKind::FnDecl {
501 name,
502 effect_clause,
503 body,
504 visibility,
505 ..
506 } = &fn_node.kind
507 else {
508 return;
509 };
510
511 let fn_span = fn_node.span;
512 let fn_name = &name.name;
513 let is_public = matches!(visibility, Visibility::Public);
514
515 let raw_declared: HashSet<EffectRef> =
516 effect_clause.iter().map(type_path_to_effect_ref).collect();
517
518 let declared = self.expand_effects(&raw_declared);
520
521 let mut used_effects = HashSet::new();
523 let mut called_fns = HashSet::new();
524 collect_node_effects(body, &mut used_effects, &mut called_fns);
525
526 let mut propagated: HashSet<EffectRef> = HashSet::new();
529 for callee in &called_fns {
530 if let Some(callee_effects) = self.fn_declared.get(callee) {
531 let expanded_callee = self.expand_effects(callee_effects);
533 for eff in expanded_callee {
534 if !is_ambient(&eff) && !declared.contains(&eff) {
535 propagated.insert(eff);
536 }
537 }
538 }
539 }
540
541 if self.strictness == Strictness::Sketch {
542 return;
544 }
545
546 let should_check = match self.strictness {
547 Strictness::Development => is_public,
548 Strictness::Production => true,
549 Strictness::Sketch => false,
550 };
551
552 if !should_check {
553 return;
554 }
555
556 let use_errors = self.strictness == Strictness::Production;
557
558 for eff in used_effects
560 .iter()
561 .filter(|e| !is_ambient(e) && !declared.contains(*e))
562 {
563 let msg = format!(
564 "function `{fn_name}` uses effect `{}` but does not declare it in its `with` clause",
565 eff.name
566 );
567 let code = if use_errors {
568 E_UNDECLARED_EFFECT
569 } else {
570 W_UNDECLARED_EFFECT
571 };
572 let diag = if use_errors {
573 self.diags.error(code, msg, fn_span)
574 } else {
575 self.diags.warning(code, msg, fn_span)
576 };
577 if let Some(cap) = capability_for_effect(eff) {
578 diag.note(format!(
579 "effect `{}` correlates with capability `{cap}`",
580 eff.name
581 ));
582 }
583 }
584
585 for eff in &propagated {
587 let callee_name = called_fns
588 .iter()
589 .find(|c| self.fn_declared.get(*c).is_some_and(|e| e.contains(eff)))
590 .cloned()
591 .unwrap_or_default();
592
593 let msg = format!(
594 "function `{fn_name}` calls `{callee_name}` which requires effect `{}`, \
595 but `{fn_name}` does not declare it",
596 eff.name
597 );
598 let code = if use_errors {
599 E_PROPAGATED_EFFECT
600 } else {
601 W_PROPAGATED_EFFECT
602 };
603 let diag = if use_errors {
604 self.diags.error(code, msg, fn_span)
605 } else {
606 self.diags.warning(code, msg, fn_span)
607 };
608 if let Some(cap) = capability_for_effect(eff) {
609 diag.note(format!(
610 "effect `{}` correlates with capability `{cap}`",
611 eff.name
612 ));
613 }
614 }
615 }
616}
617
618#[cfg(test)]
621mod tests {
622 use super::*;
623 use bock_air::{AIRNode, AirHandlerPair, NodeIdGen, NodeKind};
624 use bock_ast::{Ident, TypePath, Visibility};
625 use bock_errors::{FileId, Severity, Span};
626
627 fn dummy_span() -> Span {
628 Span {
629 file: FileId(0),
630 start: 0,
631 end: 0,
632 }
633 }
634
635 fn dummy_ident(name: &str) -> Ident {
636 Ident {
637 name: name.to_string(),
638 span: dummy_span(),
639 }
640 }
641
642 fn dummy_type_path(name: &str) -> TypePath {
643 TypePath {
644 segments: vec![dummy_ident(name)],
645 span: dummy_span(),
646 }
647 }
648
649 fn make_node(gen: &NodeIdGen, kind: NodeKind) -> AIRNode {
650 AIRNode::new(gen.next(), dummy_span(), kind)
651 }
652
653 fn make_fn(
654 gen: &NodeIdGen,
655 name: &str,
656 effects: Vec<&str>,
657 body: AIRNode,
658 vis: Visibility,
659 ) -> AIRNode {
660 make_node(
661 gen,
662 NodeKind::FnDecl {
663 annotations: vec![],
664 visibility: vis,
665 is_async: false,
666 name: dummy_ident(name),
667 generic_params: vec![],
668 params: vec![],
669 return_type: None,
670 effect_clause: effects.into_iter().map(dummy_type_path).collect(),
671 where_clause: vec![],
672 body: Box::new(body),
673 },
674 )
675 }
676
677 fn make_effect_op(gen: &NodeIdGen, effect: &str) -> AIRNode {
678 make_node(
679 gen,
680 NodeKind::EffectOp {
681 effect: dummy_type_path(effect),
682 operation: dummy_ident("op"),
683 args: vec![],
684 },
685 )
686 }
687
688 fn make_module(gen: &NodeIdGen, items: Vec<AIRNode>) -> AIRNode {
689 make_node(
690 gen,
691 NodeKind::Module {
692 path: None,
693 annotations: vec![],
694 imports: vec![],
695 items,
696 },
697 )
698 }
699
700 fn empty_block(gen: &NodeIdGen) -> AIRNode {
701 make_node(
702 gen,
703 NodeKind::Block {
704 stmts: vec![],
705 tail: None,
706 },
707 )
708 }
709
710 fn warning_count(bag: &DiagnosticBag) -> usize {
711 bag.iter()
712 .filter(|d| d.severity == Severity::Warning)
713 .count()
714 }
715
716 #[test]
719 fn infer_effects_empty_body() {
720 let gen = NodeIdGen::new();
721 let body = empty_block(&gen);
722 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
723 let effects = infer_effects(&fn_node);
724 assert!(effects.is_empty());
725 }
726
727 #[test]
728 fn infer_effects_direct_effect_op() {
729 let gen = NodeIdGen::new();
730 let op = make_effect_op(&gen, "Log");
731 let body = make_node(
732 &gen,
733 NodeKind::Block {
734 stmts: vec![op],
735 tail: None,
736 },
737 );
738 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
739 let effects = infer_effects(&fn_node);
740 assert!(effects.contains(&EffectRef::new("Log")));
741 }
742
743 #[test]
744 fn infer_effects_multiple_effects() {
745 let gen = NodeIdGen::new();
746 let log_op = make_effect_op(&gen, "Log");
747 let clock_op = make_effect_op(&gen, "Clock");
748 let body = make_node(
749 &gen,
750 NodeKind::Block {
751 stmts: vec![log_op, clock_op],
752 tail: None,
753 },
754 );
755 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
756 let effects = infer_effects(&fn_node);
757 assert_eq!(effects.len(), 2);
758 assert!(effects.contains(&EffectRef::new("Log")));
759 assert!(effects.contains(&EffectRef::new("Clock")));
760 }
761
762 #[test]
763 fn infer_effects_handling_block_suppresses_handled() {
764 let gen = NodeIdGen::new();
765 let op = make_effect_op(&gen, "Log");
766 let inner_body = make_node(
767 &gen,
768 NodeKind::Block {
769 stmts: vec![op],
770 tail: None,
771 },
772 );
773 let handling = make_node(
774 &gen,
775 NodeKind::HandlingBlock {
776 handlers: vec![AirHandlerPair {
777 effect: dummy_type_path("Log"),
778 handler: Box::new(empty_block(&gen)),
779 }],
780 body: Box::new(inner_body),
781 },
782 );
783 let body = make_node(
784 &gen,
785 NodeKind::Block {
786 stmts: vec![handling],
787 tail: None,
788 },
789 );
790 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
791 assert!(!infer_effects(&fn_node).contains(&EffectRef::new("Log")));
793 }
794
795 #[test]
796 fn infer_effects_returns_empty_for_non_fn() {
797 let gen = NodeIdGen::new();
798 let node = empty_block(&gen);
799 assert!(infer_effects(&node).is_empty());
800 }
801
802 #[test]
805 fn sketch_mode_no_diagnostics_for_undeclared() {
806 let gen = NodeIdGen::new();
807 let op = make_effect_op(&gen, "Log");
808 let body = make_node(
809 &gen,
810 NodeKind::Block {
811 stmts: vec![op],
812 tail: None,
813 },
814 );
815 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
816 let module = make_module(&gen, vec![fn_node]);
817
818 let bag = track_effects(&module, Strictness::Sketch);
819 assert_eq!(bag.error_count(), 0);
820 assert_eq!(warning_count(&bag), 0);
821 }
822
823 #[test]
824 fn dev_mode_warns_public_undeclared() {
825 let gen = NodeIdGen::new();
826 let op = make_effect_op(&gen, "Log");
827 let body = make_node(
828 &gen,
829 NodeKind::Block {
830 stmts: vec![op],
831 tail: None,
832 },
833 );
834 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
835 let module = make_module(&gen, vec![fn_node]);
836
837 let bag = track_effects(&module, Strictness::Development);
838 assert_eq!(bag.error_count(), 0);
839 assert!(warning_count(&bag) > 0);
840 }
841
842 #[test]
843 fn dev_mode_no_warning_for_private() {
844 let gen = NodeIdGen::new();
845 let op = make_effect_op(&gen, "Log");
846 let body = make_node(
847 &gen,
848 NodeKind::Block {
849 stmts: vec![op],
850 tail: None,
851 },
852 );
853 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
854 let module = make_module(&gen, vec![fn_node]);
855
856 let bag = track_effects(&module, Strictness::Development);
857 assert_eq!(bag.error_count(), 0);
858 assert_eq!(warning_count(&bag), 0);
859 }
860
861 #[test]
862 fn prod_mode_errors_all_undeclared() {
863 let gen = NodeIdGen::new();
864 let op = make_effect_op(&gen, "Log");
865 let body = make_node(
866 &gen,
867 NodeKind::Block {
868 stmts: vec![op],
869 tail: None,
870 },
871 );
872 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
873 let module = make_module(&gen, vec![fn_node]);
874
875 let bag = track_effects(&module, Strictness::Production);
876 assert!(bag.error_count() > 0);
877 }
878
879 #[test]
880 fn declared_effect_produces_no_diagnostic() {
881 let gen = NodeIdGen::new();
882 let op = make_effect_op(&gen, "Log");
883 let body = make_node(
884 &gen,
885 NodeKind::Block {
886 stmts: vec![op],
887 tail: None,
888 },
889 );
890 let fn_node = make_fn(&gen, "f", vec!["Log"], body, Visibility::Public);
891 let module = make_module(&gen, vec![fn_node]);
892
893 let bag = track_effects(&module, Strictness::Production);
894 assert_eq!(bag.error_count(), 0);
895 }
896
897 #[test]
898 fn ambient_effect_never_flagged() {
899 let gen = NodeIdGen::new();
900 let op = make_effect_op(&gen, "Panic");
901 let body = make_node(
902 &gen,
903 NodeKind::Block {
904 stmts: vec![op],
905 tail: None,
906 },
907 );
908 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
909 let module = make_module(&gen, vec![fn_node]);
910
911 let bag = track_effects(&module, Strictness::Production);
912 assert_eq!(bag.error_count(), 0);
913 }
914
915 #[test]
916 fn propagation_caller_must_declare_callee_effects() {
917 let gen = NodeIdGen::new();
918
919 let callee_body = empty_block(&gen);
921 let callee = make_fn(&gen, "callee", vec!["Log"], callee_body, Visibility::Public);
922
923 let call_node = make_node(
925 &gen,
926 NodeKind::Call {
927 callee: Box::new(make_node(
928 &gen,
929 NodeKind::Identifier {
930 name: dummy_ident("callee"),
931 },
932 )),
933 args: vec![],
934 type_args: vec![],
935 },
936 );
937 let caller_body = make_node(
938 &gen,
939 NodeKind::Block {
940 stmts: vec![call_node],
941 tail: None,
942 },
943 );
944 let caller = make_fn(&gen, "caller", vec![], caller_body, Visibility::Public);
945
946 let module = make_module(&gen, vec![callee, caller]);
947 let bag = track_effects(&module, Strictness::Production);
948 assert!(bag.error_count() > 0);
949 }
950
951 #[test]
952 fn propagation_caller_declares_callee_effects_ok() {
953 let gen = NodeIdGen::new();
954
955 let callee_body = empty_block(&gen);
956 let callee = make_fn(&gen, "callee", vec!["Log"], callee_body, Visibility::Public);
957
958 let call_node = make_node(
959 &gen,
960 NodeKind::Call {
961 callee: Box::new(make_node(
962 &gen,
963 NodeKind::Identifier {
964 name: dummy_ident("callee"),
965 },
966 )),
967 args: vec![],
968 type_args: vec![],
969 },
970 );
971 let caller_body = make_node(
972 &gen,
973 NodeKind::Block {
974 stmts: vec![call_node],
975 tail: None,
976 },
977 );
978 let caller = make_fn(&gen, "caller", vec!["Log"], caller_body, Visibility::Public);
980
981 let module = make_module(&gen, vec![callee, caller]);
982 let bag = track_effects(&module, Strictness::Production);
983 assert_eq!(bag.error_count(), 0);
984 }
985
986 fn make_effect_decl(gen: &NodeIdGen, name: &str, components: Vec<&str>) -> AIRNode {
989 make_node(
990 gen,
991 NodeKind::EffectDecl {
992 annotations: vec![],
993 visibility: Visibility::Public,
994 name: dummy_ident(name),
995 generic_params: vec![],
996 components: components.into_iter().map(dummy_type_path).collect(),
997 operations: vec![],
998 },
999 )
1000 }
1001
1002 #[test]
1003 fn composite_effect_expands_to_components() {
1004 let gen = NodeIdGen::new();
1005
1006 let io_decl = make_effect_decl(&gen, "IO", vec!["Log", "Clock"]);
1008
1009 let op = make_effect_op(&gen, "Log");
1011 let body = make_node(
1012 &gen,
1013 NodeKind::Block {
1014 stmts: vec![op],
1015 tail: None,
1016 },
1017 );
1018 let fn_node = make_fn(&gen, "f", vec!["IO"], body, Visibility::Public);
1019
1020 let module = make_module(&gen, vec![io_decl, fn_node]);
1021 let bag = track_effects(&module, Strictness::Production);
1022 assert_eq!(bag.error_count(), 0);
1024 }
1025
1026 #[test]
1027 fn composite_effect_covers_all_components() {
1028 let gen = NodeIdGen::new();
1029
1030 let io_decl = make_effect_decl(&gen, "IO", vec!["Log", "Clock"]);
1032
1033 let log_op = make_effect_op(&gen, "Log");
1035 let clock_op = make_effect_op(&gen, "Clock");
1036 let body = make_node(
1037 &gen,
1038 NodeKind::Block {
1039 stmts: vec![log_op, clock_op],
1040 tail: None,
1041 },
1042 );
1043 let fn_node = make_fn(&gen, "f", vec!["IO"], body, Visibility::Public);
1044
1045 let module = make_module(&gen, vec![io_decl, fn_node]);
1046 let bag = track_effects(&module, Strictness::Production);
1047 assert_eq!(bag.error_count(), 0);
1048 }
1049
1050 #[test]
1051 fn composite_effect_does_not_cover_unrelated() {
1052 let gen = NodeIdGen::new();
1053
1054 let io_decl = make_effect_decl(&gen, "IO", vec!["Log", "Clock"]);
1056
1057 let op = make_effect_op(&gen, "Http");
1059 let body = make_node(
1060 &gen,
1061 NodeKind::Block {
1062 stmts: vec![op],
1063 tail: None,
1064 },
1065 );
1066 let fn_node = make_fn(&gen, "f", vec!["IO"], body, Visibility::Public);
1067
1068 let module = make_module(&gen, vec![io_decl, fn_node]);
1069 let bag = track_effects(&module, Strictness::Production);
1070 assert!(bag.error_count() > 0);
1071 }
1072
1073 #[test]
1074 fn composite_effect_propagation_through_call_graph() {
1075 let gen = NodeIdGen::new();
1076
1077 let io_decl = make_effect_decl(&gen, "IO", vec!["Log", "Clock"]);
1079
1080 let callee_body = empty_block(&gen);
1082 let callee = make_fn(&gen, "callee", vec!["Log"], callee_body, Visibility::Public);
1083
1084 let call_node = make_node(
1087 &gen,
1088 NodeKind::Call {
1089 callee: Box::new(make_node(
1090 &gen,
1091 NodeKind::Identifier {
1092 name: dummy_ident("callee"),
1093 },
1094 )),
1095 args: vec![],
1096 type_args: vec![],
1097 },
1098 );
1099 let caller_body = make_node(
1100 &gen,
1101 NodeKind::Block {
1102 stmts: vec![call_node],
1103 tail: None,
1104 },
1105 );
1106 let caller = make_fn(&gen, "caller", vec!["IO"], caller_body, Visibility::Public);
1107
1108 let module = make_module(&gen, vec![io_decl, callee, caller]);
1109 let bag = track_effects(&module, Strictness::Production);
1110 assert_eq!(bag.error_count(), 0);
1111 }
1112}