1use std::collections::{HashMap, HashSet};
28
29use bock_air::{AIRNode, AirInterpolationPart, NodeKind};
30use bock_ast::{Expr, Visibility};
31use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
32
33pub use bock_air::stubs::{Capability, EffectRef};
34use bock_air::NodeId;
35
36use crate::AIRModule;
37pub use crate::Strictness;
38
39const E_MISSING_CAPABILITY: DiagnosticCode = DiagnosticCode {
42 prefix: 'E',
43 number: 7001,
44};
45const W_MISSING_CAPABILITY: DiagnosticCode = DiagnosticCode {
46 prefix: 'W',
47 number: 7002,
48};
49const E_PROPAGATED_CAPABILITY: DiagnosticCode = DiagnosticCode {
50 prefix: 'E',
51 number: 7003,
52};
53const W_PROPAGATED_CAPABILITY: DiagnosticCode = DiagnosticCode {
54 prefix: 'W',
55 number: 7004,
56};
57
58pub type CapabilitySet = HashSet<Capability>;
62
63fn expr_to_capability_name(expr: &Expr) -> Option<String> {
73 match expr {
74 Expr::FieldAccess { object, field, .. } => {
75 let prefix = expr_to_capability_name(object)?;
76 Some(format!("{}.{}", prefix, field.name))
77 }
78 Expr::Identifier { name, .. } => Some(name.name.clone()),
79 _ => None,
80 }
81}
82
83fn extract_requires_annotation(annotations: &[bock_ast::Annotation]) -> CapabilitySet {
88 let mut caps = CapabilitySet::new();
89 for ann in annotations {
90 if ann.name.name == "requires" {
91 for arg in &ann.args {
92 if let Some(name) = expr_to_capability_name(&arg.value) {
93 caps.insert(Capability::new(name));
94 }
95 }
96 }
97 }
98 caps
99}
100
101fn capability_for_effect(effect: &EffectRef) -> Option<Capability> {
107 let name = effect.name.to_lowercase();
108 if name.contains("log") || name.contains("print") || name.contains("console") {
109 Some(Capability::new("Io.Stdout"))
110 } else if name.contains("http") || name.contains("net") || name.contains("socket") {
111 Some(Capability::new("Io.Network"))
112 } else if name.contains("file") || name.contains("fs") || name.contains("disk") {
113 Some(Capability::new("Io.FileSystem"))
114 } else if name.contains("clock") || name.contains("time") || name.contains("date") {
115 Some(Capability::new("Io.Clock"))
116 } else if name.contains("env") || name.contains("os") || name.contains("process") {
117 Some(Capability::new("Io.Process"))
118 } else {
119 None
120 }
121}
122
123fn collect_effects_and_calls(
129 node: &AIRNode,
130 used_effects: &mut HashSet<EffectRef>,
131 called_fns: &mut HashSet<String>,
132) {
133 match &node.kind {
134 NodeKind::EffectOp { effect, args, .. } => {
135 let name = effect
136 .segments
137 .iter()
138 .map(|s| s.name.as_str())
139 .collect::<Vec<_>>()
140 .join(".");
141 used_effects.insert(EffectRef::new(name));
142 for arg in args {
143 collect_effects_and_calls(&arg.value, used_effects, called_fns);
144 }
145 }
146
147 NodeKind::Call {
148 callee,
149 args,
150 type_args,
151 } => {
152 if let NodeKind::Identifier { name } = &callee.kind {
153 called_fns.insert(name.name.clone());
154 }
155 collect_effects_and_calls(callee, used_effects, called_fns);
156 for arg in args {
157 collect_effects_and_calls(&arg.value, used_effects, called_fns);
158 }
159 for ta in type_args {
160 collect_effects_and_calls(ta, used_effects, called_fns);
161 }
162 }
163
164 NodeKind::MethodCall { receiver, args, .. } => {
165 collect_effects_and_calls(receiver, used_effects, called_fns);
166 for arg in args {
167 collect_effects_and_calls(&arg.value, used_effects, called_fns);
168 }
169 }
170
171 NodeKind::HandlingBlock { handlers, body } => {
173 let handled: HashSet<String> = handlers
174 .iter()
175 .map(|h| {
176 h.effect
177 .segments
178 .iter()
179 .map(|s| s.name.as_str())
180 .collect::<Vec<_>>()
181 .join(".")
182 })
183 .collect();
184
185 let mut body_effects = HashSet::new();
186 let mut body_calls = HashSet::new();
187 collect_effects_and_calls(body, &mut body_effects, &mut body_calls);
188
189 for e in body_effects {
190 if !handled.contains(&e.name) {
191 used_effects.insert(e);
192 }
193 }
194 called_fns.extend(body_calls);
195 }
196
197 NodeKind::FnDecl { .. } => {}
199
200 NodeKind::Lambda { body, .. } => {
201 collect_effects_and_calls(body, used_effects, called_fns);
202 }
203
204 NodeKind::Block { stmts, tail } => {
205 for s in stmts {
206 collect_effects_and_calls(s, used_effects, called_fns);
207 }
208 if let Some(t) = tail {
209 collect_effects_and_calls(t, used_effects, called_fns);
210 }
211 }
212
213 NodeKind::LetBinding { value, .. } => {
214 collect_effects_and_calls(value, used_effects, called_fns);
215 }
216
217 NodeKind::Assign { target, value, .. } => {
218 collect_effects_and_calls(target, used_effects, called_fns);
219 collect_effects_and_calls(value, used_effects, called_fns);
220 }
221
222 NodeKind::If {
223 condition,
224 then_block,
225 else_block,
226 ..
227 } => {
228 collect_effects_and_calls(condition, used_effects, called_fns);
229 collect_effects_and_calls(then_block, used_effects, called_fns);
230 if let Some(e) = else_block {
231 collect_effects_and_calls(e, used_effects, called_fns);
232 }
233 }
234
235 NodeKind::Guard {
236 let_pattern,
237 condition,
238 else_block,
239 } => {
240 if let Some(pat) = let_pattern {
241 collect_effects_and_calls(pat, used_effects, called_fns);
242 }
243 collect_effects_and_calls(condition, used_effects, called_fns);
244 collect_effects_and_calls(else_block, used_effects, called_fns);
245 }
246
247 NodeKind::Match { scrutinee, arms } => {
248 collect_effects_and_calls(scrutinee, used_effects, called_fns);
249 for arm in arms {
250 collect_effects_and_calls(arm, used_effects, called_fns);
251 }
252 }
253
254 NodeKind::MatchArm { guard, body, .. } => {
255 if let Some(g) = guard {
256 collect_effects_and_calls(g, used_effects, called_fns);
257 }
258 collect_effects_and_calls(body, used_effects, called_fns);
259 }
260
261 NodeKind::For { iterable, body, .. } => {
262 collect_effects_and_calls(iterable, used_effects, called_fns);
263 collect_effects_and_calls(body, used_effects, called_fns);
264 }
265
266 NodeKind::While { condition, body } => {
267 collect_effects_and_calls(condition, used_effects, called_fns);
268 collect_effects_and_calls(body, used_effects, called_fns);
269 }
270
271 NodeKind::Loop { body } => {
272 collect_effects_and_calls(body, used_effects, called_fns);
273 }
274
275 NodeKind::Return { value: Some(v) } | NodeKind::Break { value: Some(v) } => {
276 collect_effects_and_calls(v, used_effects, called_fns);
277 }
278
279 NodeKind::Return { value: None } | NodeKind::Break { value: None } => {}
280
281 NodeKind::BinaryOp { left, right, .. } => {
282 collect_effects_and_calls(left, used_effects, called_fns);
283 collect_effects_and_calls(right, used_effects, called_fns);
284 }
285
286 NodeKind::UnaryOp { operand, .. } => {
287 collect_effects_and_calls(operand, used_effects, called_fns);
288 }
289
290 NodeKind::FieldAccess { object, .. } => {
291 collect_effects_and_calls(object, used_effects, called_fns);
292 }
293
294 NodeKind::Index { object, index } => {
295 collect_effects_and_calls(object, used_effects, called_fns);
296 collect_effects_and_calls(index, used_effects, called_fns);
297 }
298
299 NodeKind::Propagate { expr } => {
300 collect_effects_and_calls(expr, used_effects, called_fns);
301 }
302
303 NodeKind::Await { expr } => {
304 collect_effects_and_calls(expr, used_effects, called_fns);
305 }
306
307 NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } | NodeKind::Move { expr } => {
308 collect_effects_and_calls(expr, used_effects, called_fns);
309 }
310
311 NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
312 collect_effects_and_calls(left, used_effects, called_fns);
313 collect_effects_and_calls(right, used_effects, called_fns);
314 }
315
316 NodeKind::Range { lo, hi, .. } => {
317 collect_effects_and_calls(lo, used_effects, called_fns);
318 collect_effects_and_calls(hi, used_effects, called_fns);
319 }
320
321 NodeKind::RecordConstruct { fields, spread, .. } => {
322 for f in fields {
323 if let Some(v) = &f.value {
324 collect_effects_and_calls(v, used_effects, called_fns);
325 }
326 }
327 if let Some(s) = spread {
328 collect_effects_and_calls(s, used_effects, called_fns);
329 }
330 }
331
332 NodeKind::ListLiteral { elems }
333 | NodeKind::SetLiteral { elems }
334 | NodeKind::TupleLiteral { elems } => {
335 for e in elems {
336 collect_effects_and_calls(e, used_effects, called_fns);
337 }
338 }
339
340 NodeKind::MapLiteral { entries } => {
341 for e in entries {
342 collect_effects_and_calls(&e.key, used_effects, called_fns);
343 collect_effects_and_calls(&e.value, used_effects, called_fns);
344 }
345 }
346
347 NodeKind::Interpolation { parts } => {
348 for p in parts {
349 if let AirInterpolationPart::Expr(e) = p {
350 collect_effects_and_calls(e, used_effects, called_fns);
351 }
352 }
353 }
354
355 NodeKind::ResultConstruct { value: Some(v), .. } => {
356 collect_effects_and_calls(v, used_effects, called_fns);
357 }
358
359 _ => {}
361 }
362}
363
364struct FnRecord {
368 node_id: NodeId,
369 span: Span,
370 is_public: bool,
371 declared: CapabilitySet,
373 from_effects: CapabilitySet,
375 called_fns: HashSet<String>,
377}
378
379struct CapabilityEngine {
382 records: HashMap<String, FnRecord>,
384 module_caps: CapabilitySet,
386}
387
388impl CapabilityEngine {
389 fn new() -> Self {
390 Self {
391 records: HashMap::new(),
392 module_caps: CapabilitySet::new(),
393 }
394 }
395
396 fn collect(&mut self, module: &AIRModule) {
399 match &module.kind {
400 NodeKind::Module { items, .. } => {
401 for item in items {
409 self.collect_item(item);
410 }
411 }
412 _ => self.collect_item(module),
413 }
414 }
415
416 fn collect_item(&mut self, node: &AIRNode) {
417 match &node.kind {
418 NodeKind::FnDecl {
419 name,
420 annotations,
421 visibility,
422 body,
423 ..
424 } => {
425 let declared = {
426 let mut caps = extract_requires_annotation(annotations);
427 caps.extend(self.module_caps.iter().cloned());
429 caps
430 };
431
432 let mut used_effects = HashSet::new();
433 let mut called_fns = HashSet::new();
434 collect_effects_and_calls(body, &mut used_effects, &mut called_fns);
435
436 let from_effects: CapabilitySet = used_effects
437 .iter()
438 .filter_map(capability_for_effect)
439 .collect();
440
441 let record = FnRecord {
442 node_id: node.id,
443 span: node.span,
444 is_public: matches!(visibility, Visibility::Public),
445 declared,
446 from_effects,
447 called_fns,
448 };
449 self.records.insert(name.name.clone(), record);
450 }
451
452 NodeKind::ImplBlock { methods, .. } | NodeKind::TraitDecl { methods, .. } => {
453 for m in methods {
454 self.collect_item(m);
455 }
456 }
457
458 NodeKind::ClassDecl { methods, .. } => {
459 for m in methods {
460 self.collect_item(m);
461 }
462 }
463
464 _ => {}
465 }
466 }
467
468 fn required_caps(&self, name: &str, visiting: &mut HashSet<String>) -> CapabilitySet {
475 if !visiting.insert(name.to_string()) {
476 return CapabilitySet::new();
478 }
479
480 let mut caps = CapabilitySet::new();
481
482 if let Some(rec) = self.records.get(name) {
483 caps.extend(rec.from_effects.iter().cloned());
485
486 let callees: Vec<String> = rec.called_fns.iter().cloned().collect();
488 for callee in &callees {
489 let callee_caps = self.required_caps(callee, visiting);
490 caps.extend(callee_caps);
491 }
492 }
493
494 visiting.remove(name);
495 caps
496 }
497
498 fn build_map(&self) -> HashMap<NodeId, CapabilitySet> {
505 let mut map = HashMap::new();
506 for (name, rec) in &self.records {
507 let mut visiting = HashSet::new();
508 let caps = self.required_caps(name, &mut visiting);
509 map.insert(rec.node_id, caps);
510 }
511 map
512 }
513
514 fn verify(&self, strictness: Strictness) -> DiagnosticBag {
517 let mut diags = DiagnosticBag::new();
518
519 if strictness == Strictness::Sketch {
520 return diags;
521 }
522
523 let use_errors = strictness == Strictness::Production;
524
525 for (name, rec) in &self.records {
526 let should_check = match strictness {
527 Strictness::Development => rec.is_public,
528 Strictness::Production => true,
529 Strictness::Sketch => false,
530 };
531
532 if !should_check {
533 continue;
534 }
535
536 let mut visiting = HashSet::new();
537 let required = self.required_caps(name, &mut visiting);
538
539 for cap in rec.from_effects.iter() {
541 if !rec.declared.contains(cap) {
542 let msg = format!(
543 "function `{name}` requires capability `{}` (from IO effects) \
544 but does not declare it via `@requires`",
545 cap.name
546 );
547 let code = if use_errors {
548 E_MISSING_CAPABILITY
549 } else {
550 W_MISSING_CAPABILITY
551 };
552 if use_errors {
553 diags.error(code, msg, rec.span);
554 } else {
555 diags.warning(code, msg, rec.span);
556 }
557 }
558 }
559
560 let propagated: CapabilitySet = required
562 .iter()
563 .filter(|c| !rec.from_effects.contains(*c) && !rec.declared.contains(*c))
564 .cloned()
565 .collect();
566
567 for cap in &propagated {
568 let callee_name = rec
570 .called_fns
571 .iter()
572 .find(|c| {
573 let mut v = HashSet::new();
574 self.required_caps(c, &mut v).contains(cap)
575 })
576 .cloned()
577 .unwrap_or_default();
578
579 let msg = format!(
580 "function `{name}` calls `{callee_name}` which requires capability `{}`, \
581 but `{name}` does not declare it via `@requires`",
582 cap.name
583 );
584 let code = if use_errors {
585 E_PROPAGATED_CAPABILITY
586 } else {
587 W_PROPAGATED_CAPABILITY
588 };
589 if use_errors {
590 diags.error(code, msg, rec.span);
591 } else {
592 diags.warning(code, msg, rec.span);
593 }
594 }
595 }
596
597 diags
598 }
599}
600
601#[must_use]
614pub fn compute_capabilities(module: &AIRModule) -> HashMap<NodeId, CapabilitySet> {
615 let mut engine = CapabilityEngine::new();
616 engine.collect(module);
617 engine.build_map()
618}
619
620#[must_use]
630pub fn verify_capabilities(module: &AIRModule, strictness: Strictness) -> DiagnosticBag {
631 let mut engine = CapabilityEngine::new();
632 engine.collect(module);
633 engine.verify(strictness)
634}
635
636#[cfg(test)]
639mod tests {
640 use super::*;
641 use bock_air::{AIRNode, AirHandlerPair, NodeIdGen, NodeKind};
642 use bock_ast::{Annotation, Ident, TypePath, Visibility};
643 use bock_errors::{FileId, Severity, Span};
644
645 fn dummy_span() -> Span {
646 Span {
647 file: FileId(0),
648 start: 0,
649 end: 0,
650 }
651 }
652
653 fn dummy_ident(name: &str) -> Ident {
654 Ident {
655 name: name.to_string(),
656 span: dummy_span(),
657 }
658 }
659
660 fn dummy_type_path(name: &str) -> TypePath {
661 TypePath {
662 segments: vec![dummy_ident(name)],
663 span: dummy_span(),
664 }
665 }
666
667 fn make_node(gen: &NodeIdGen, kind: NodeKind) -> AIRNode {
668 AIRNode::new(gen.next(), dummy_span(), kind)
669 }
670
671 fn empty_block(gen: &NodeIdGen) -> AIRNode {
672 make_node(
673 gen,
674 NodeKind::Block {
675 stmts: vec![],
676 tail: None,
677 },
678 )
679 }
680
681 fn make_effect_op(gen: &NodeIdGen, effect: &str) -> AIRNode {
682 make_node(
683 gen,
684 NodeKind::EffectOp {
685 effect: dummy_type_path(effect),
686 operation: dummy_ident("op"),
687 args: vec![],
688 },
689 )
690 }
691
692 fn make_requires_annotation(caps: &[&str]) -> Annotation {
695 use bock_ast::AnnotationArg;
696 use bock_ast::Expr;
697 use bock_ast::NodeId as AstNodeId;
698
699 let args = caps
700 .iter()
701 .map(|cap| AnnotationArg {
702 label: None,
703 value: Expr::Identifier {
704 id: 0 as AstNodeId,
705 span: dummy_span(),
706 name: dummy_ident(cap),
707 },
708 })
709 .collect();
710
711 Annotation {
712 id: 0,
713 span: dummy_span(),
714 name: dummy_ident("requires"),
715 args,
716 }
717 }
718
719 fn make_fn(
720 gen: &NodeIdGen,
721 name: &str,
722 annotations: Vec<Annotation>,
723 body: AIRNode,
724 vis: Visibility,
725 ) -> AIRNode {
726 make_node(
727 gen,
728 NodeKind::FnDecl {
729 annotations,
730 visibility: vis,
731 is_async: false,
732 name: dummy_ident(name),
733 generic_params: vec![],
734 params: vec![],
735 return_type: None,
736 effect_clause: vec![],
737 where_clause: vec![],
738 body: Box::new(body),
739 },
740 )
741 }
742
743 fn make_module(gen: &NodeIdGen, items: Vec<AIRNode>) -> AIRNode {
744 make_node(
745 gen,
746 NodeKind::Module {
747 path: None,
748 annotations: vec![],
749 imports: vec![],
750 items,
751 },
752 )
753 }
754
755 fn warning_count(bag: &DiagnosticBag) -> usize {
756 bag.iter()
757 .filter(|d| d.severity == Severity::Warning)
758 .count()
759 }
760
761 #[test]
764 fn expr_capability_name_identifier() {
765 use bock_ast::Expr;
766 let expr = Expr::Identifier {
767 id: 0,
768 span: dummy_span(),
769 name: dummy_ident("Network"),
770 };
771 assert_eq!(expr_to_capability_name(&expr), Some("Network".into()));
772 }
773
774 #[test]
775 fn expr_capability_name_field_access() {
776 use bock_ast::Expr;
777 let expr = Expr::FieldAccess {
778 id: 0,
779 span: dummy_span(),
780 object: Box::new(Expr::Identifier {
781 id: 0,
782 span: dummy_span(),
783 name: dummy_ident("Capability"),
784 }),
785 field: dummy_ident("Network"),
786 };
787 assert_eq!(
788 expr_to_capability_name(&expr),
789 Some("Capability.Network".into())
790 );
791 }
792
793 #[test]
794 fn expr_capability_name_unknown_returns_none() {
795 use bock_ast::{Expr, Literal};
796 let expr = Expr::Literal {
797 id: 0,
798 span: dummy_span(),
799 lit: Literal::Bool(true),
800 };
801 assert_eq!(expr_to_capability_name(&expr), None);
802 }
803
804 #[test]
807 fn effect_log_gives_stdout_cap() {
808 let e = EffectRef::new("Log");
809 assert_eq!(
810 capability_for_effect(&e),
811 Some(Capability::new("Io.Stdout"))
812 );
813 }
814
815 #[test]
816 fn effect_http_gives_network_cap() {
817 let e = EffectRef::new("Http");
818 assert_eq!(
819 capability_for_effect(&e),
820 Some(Capability::new("Io.Network"))
821 );
822 }
823
824 #[test]
825 fn effect_clock_gives_clock_cap() {
826 let e = EffectRef::new("Clock");
827 assert_eq!(capability_for_effect(&e), Some(Capability::new("Io.Clock")));
828 }
829
830 #[test]
831 fn effect_pure_gives_no_cap() {
832 let e = EffectRef::new("Pure");
833 assert_eq!(capability_for_effect(&e), None);
834 }
835
836 #[test]
839 fn empty_fn_has_no_capabilities() {
840 let gen = NodeIdGen::new();
841 let body = empty_block(&gen);
842 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
843 let module = make_module(&gen, vec![fn_node.clone()]);
844
845 let map = compute_capabilities(&module);
846 let caps = map.get(&fn_node.id).cloned().unwrap_or_default();
847 assert!(caps.is_empty());
848 }
849
850 #[test]
851 fn fn_with_log_effect_gets_stdout_cap() {
852 let gen = NodeIdGen::new();
853 let op = make_effect_op(&gen, "Log");
854 let body = make_node(
855 &gen,
856 NodeKind::Block {
857 stmts: vec![op],
858 tail: None,
859 },
860 );
861 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
862 let module = make_module(&gen, vec![fn_node.clone()]);
863
864 let map = compute_capabilities(&module);
865 let caps = map.get(&fn_node.id).cloned().unwrap_or_default();
866 assert!(caps.contains(&Capability::new("Io.Stdout")));
867 }
868
869 #[test]
870 fn fn_with_http_effect_gets_network_cap() {
871 let gen = NodeIdGen::new();
872 let op = make_effect_op(&gen, "Http");
873 let body = make_node(
874 &gen,
875 NodeKind::Block {
876 stmts: vec![op],
877 tail: None,
878 },
879 );
880 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
881 let module = make_module(&gen, vec![fn_node.clone()]);
882
883 let map = compute_capabilities(&module);
884 let caps = map.get(&fn_node.id).cloned().unwrap_or_default();
885 assert!(caps.contains(&Capability::new("Io.Network")));
886 }
887
888 #[test]
889 fn requires_annotation_included_in_capability_map() {
890 let gen = NodeIdGen::new();
891 let ann = make_requires_annotation(&["Storage"]);
892 let body = empty_block(&gen);
893 let fn_node = make_fn(&gen, "f", vec![ann], body, Visibility::Public);
898 let module = make_module(&gen, vec![fn_node.clone()]);
899
900 let map = compute_capabilities(&module);
901 assert!(map.contains_key(&fn_node.id));
902 }
903
904 #[test]
905 fn capability_propagates_through_call_graph() {
906 let gen = NodeIdGen::new();
907
908 let callee_op = make_effect_op(&gen, "Http");
910 let callee_body = make_node(
911 &gen,
912 NodeKind::Block {
913 stmts: vec![callee_op],
914 tail: None,
915 },
916 );
917 let callee = make_fn(&gen, "callee", vec![], callee_body, Visibility::Public);
918 let callee_id = callee.id;
919
920 let call_node = make_node(
922 &gen,
923 NodeKind::Call {
924 callee: Box::new(make_node(
925 &gen,
926 NodeKind::Identifier {
927 name: dummy_ident("callee"),
928 },
929 )),
930 args: vec![],
931 type_args: vec![],
932 },
933 );
934 let caller_body = make_node(
935 &gen,
936 NodeKind::Block {
937 stmts: vec![call_node],
938 tail: None,
939 },
940 );
941 let caller = make_fn(&gen, "caller", vec![], caller_body, Visibility::Public);
942 let caller_id = caller.id;
943
944 let module = make_module(&gen, vec![callee, caller]);
945 let map = compute_capabilities(&module);
946
947 let callee_caps = map.get(&callee_id).cloned().unwrap_or_default();
949 assert!(callee_caps.contains(&Capability::new("Io.Network")));
950
951 let caller_caps = map.get(&caller_id).cloned().unwrap_or_default();
953 assert!(caller_caps.contains(&Capability::new("Io.Network")));
954 }
955
956 #[test]
957 fn handling_block_suppresses_effect_capability() {
958 let gen = NodeIdGen::new();
959 let op = make_effect_op(&gen, "Log");
960 let inner_body = make_node(
961 &gen,
962 NodeKind::Block {
963 stmts: vec![op],
964 tail: None,
965 },
966 );
967 let handling = make_node(
968 &gen,
969 NodeKind::HandlingBlock {
970 handlers: vec![AirHandlerPair {
971 effect: dummy_type_path("Log"),
972 handler: Box::new(empty_block(&gen)),
973 }],
974 body: Box::new(inner_body),
975 },
976 );
977 let body = make_node(
978 &gen,
979 NodeKind::Block {
980 stmts: vec![handling],
981 tail: None,
982 },
983 );
984 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
985 let module = make_module(&gen, vec![fn_node.clone()]);
986
987 let map = compute_capabilities(&module);
988 let caps = map.get(&fn_node.id).cloned().unwrap_or_default();
989 assert!(!caps.contains(&Capability::new("Io.Stdout")));
991 }
992
993 #[test]
996 fn sketch_mode_no_diagnostics() {
997 let gen = NodeIdGen::new();
998 let op = make_effect_op(&gen, "Log");
999 let body = make_node(
1000 &gen,
1001 NodeKind::Block {
1002 stmts: vec![op],
1003 tail: None,
1004 },
1005 );
1006 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
1007 let module = make_module(&gen, vec![fn_node]);
1008
1009 let bag = verify_capabilities(&module, Strictness::Sketch);
1010 assert_eq!(bag.error_count(), 0);
1011 assert_eq!(warning_count(&bag), 0);
1012 }
1013
1014 #[test]
1015 fn dev_mode_warns_public_missing_requires() {
1016 let gen = NodeIdGen::new();
1017 let op = make_effect_op(&gen, "Log");
1018 let body = make_node(
1019 &gen,
1020 NodeKind::Block {
1021 stmts: vec![op],
1022 tail: None,
1023 },
1024 );
1025 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Public);
1026 let module = make_module(&gen, vec![fn_node]);
1027
1028 let bag = verify_capabilities(&module, Strictness::Development);
1029 assert_eq!(bag.error_count(), 0);
1030 assert!(warning_count(&bag) > 0);
1031 }
1032
1033 #[test]
1034 fn dev_mode_no_warning_private_missing_requires() {
1035 let gen = NodeIdGen::new();
1036 let op = make_effect_op(&gen, "Log");
1037 let body = make_node(
1038 &gen,
1039 NodeKind::Block {
1040 stmts: vec![op],
1041 tail: None,
1042 },
1043 );
1044 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
1045 let module = make_module(&gen, vec![fn_node]);
1046
1047 let bag = verify_capabilities(&module, Strictness::Development);
1048 assert_eq!(bag.error_count(), 0);
1049 assert_eq!(warning_count(&bag), 0);
1050 }
1051
1052 #[test]
1053 fn prod_mode_errors_all_missing_requires() {
1054 let gen = NodeIdGen::new();
1055 let op = make_effect_op(&gen, "Log");
1056 let body = make_node(
1057 &gen,
1058 NodeKind::Block {
1059 stmts: vec![op],
1060 tail: None,
1061 },
1062 );
1063 let fn_node = make_fn(&gen, "f", vec![], body, Visibility::Private);
1064 let module = make_module(&gen, vec![fn_node]);
1065
1066 let bag = verify_capabilities(&module, Strictness::Production);
1067 assert!(bag.error_count() > 0);
1068 }
1069
1070 #[test]
1071 fn declared_capability_suppresses_diagnostic() {
1072 let gen = NodeIdGen::new();
1073 let op = make_effect_op(&gen, "Log");
1074 let body = make_node(
1075 &gen,
1076 NodeKind::Block {
1077 stmts: vec![op],
1078 tail: None,
1079 },
1080 );
1081 let ann = make_requires_annotation(&["Io.Stdout"]);
1084 let fn_node = make_fn(&gen, "f", vec![ann], body, Visibility::Public);
1085 let module = make_module(&gen, vec![fn_node]);
1086
1087 let bag = verify_capabilities(&module, Strictness::Production);
1088 assert_eq!(bag.error_count(), 0);
1089 }
1090
1091 #[test]
1092 fn propagated_capability_missing_produces_error_in_prod() {
1093 let gen = NodeIdGen::new();
1094
1095 let callee_op = make_effect_op(&gen, "Http");
1097 let callee_body = make_node(
1098 &gen,
1099 NodeKind::Block {
1100 stmts: vec![callee_op],
1101 tail: None,
1102 },
1103 );
1104 let callee = make_fn(&gen, "callee", vec![], callee_body, Visibility::Private);
1105
1106 let call_node = make_node(
1108 &gen,
1109 NodeKind::Call {
1110 callee: Box::new(make_node(
1111 &gen,
1112 NodeKind::Identifier {
1113 name: dummy_ident("callee"),
1114 },
1115 )),
1116 args: vec![],
1117 type_args: vec![],
1118 },
1119 );
1120 let caller_body = make_node(
1121 &gen,
1122 NodeKind::Block {
1123 stmts: vec![call_node],
1124 tail: None,
1125 },
1126 );
1127 let caller = make_fn(&gen, "caller", vec![], caller_body, Visibility::Public);
1128
1129 let module = make_module(&gen, vec![callee, caller]);
1130 let bag = verify_capabilities(&module, Strictness::Production);
1131 assert!(bag.error_count() > 0);
1132 }
1133
1134 #[test]
1135 fn propagated_capability_declared_ok() {
1136 let gen = NodeIdGen::new();
1137
1138 let callee_op = make_effect_op(&gen, "Http");
1140 let callee_body = make_node(
1141 &gen,
1142 NodeKind::Block {
1143 stmts: vec![callee_op],
1144 tail: None,
1145 },
1146 );
1147 let callee_ann = make_requires_annotation(&["Io.Network"]);
1148 let callee = make_fn(
1149 &gen,
1150 "callee",
1151 vec![callee_ann],
1152 callee_body,
1153 Visibility::Private,
1154 );
1155
1156 let call_node = make_node(
1157 &gen,
1158 NodeKind::Call {
1159 callee: Box::new(make_node(
1160 &gen,
1161 NodeKind::Identifier {
1162 name: dummy_ident("callee"),
1163 },
1164 )),
1165 args: vec![],
1166 type_args: vec![],
1167 },
1168 );
1169 let caller_body = make_node(
1170 &gen,
1171 NodeKind::Block {
1172 stmts: vec![call_node],
1173 tail: None,
1174 },
1175 );
1176 let caller_ann = make_requires_annotation(&["Io.Network"]);
1178 let caller = make_fn(
1179 &gen,
1180 "caller",
1181 vec![caller_ann],
1182 caller_body,
1183 Visibility::Public,
1184 );
1185
1186 let module = make_module(&gen, vec![callee, caller]);
1187 let bag = verify_capabilities(&module, Strictness::Production);
1188 assert_eq!(bag.error_count(), 0);
1189 }
1190}