1use std::collections::HashSet;
10
11use bock_ast::Visibility;
12use bock_errors::{DiagnosticBag, DiagnosticCode};
13
14use crate::node::{AIRNode, NodeKind};
15use crate::stubs::{security_level_rank, Capability, ContextBlock, SecurityInfo, SECURITY_LEVELS};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StrictnessLevel {
28 Lax,
31 Standard,
34 Strict,
37}
38
39impl StrictnessLevel {
40 #[must_use]
49 pub fn from_name(name: &str) -> Option<Self> {
50 match name.to_ascii_lowercase().as_str() {
51 "sketch" | "lax" => Some(Self::Lax),
52 "development" | "standard" => Some(Self::Standard),
53 "production" | "strict" => Some(Self::Strict),
54 _ => None,
55 }
56 }
57}
58
59#[must_use]
69pub fn validate_context(root: &AIRNode, strictness: StrictnessLevel) -> DiagnosticBag {
70 let mut diags = DiagnosticBag::new();
71 validate_node(root, None, &HashSet::new(), strictness, &mut diags);
72 diags
73}
74
75fn validate_node(
80 node: &AIRNode,
81 parent_security: Option<&SecurityInfo>,
82 parent_capabilities: &HashSet<Capability>,
83 strictness: StrictnessLevel,
84 diags: &mut DiagnosticBag,
85) {
86 let node_security = node
88 .context
89 .as_ref()
90 .and_then(|c| c.security.as_ref())
91 .or(parent_security);
92
93 let mut effective_caps = parent_capabilities.clone();
95 if let Some(ctx) = &node.context {
96 for cap in &ctx.capabilities {
97 effective_caps.insert(cap.clone());
98 }
99 }
100
101 if let Some(ctx) = &node.context {
103 validate_security_consistency(ctx, parent_security, node.span, diags);
104 validate_performance_budget(ctx, node.span, diags);
105 validate_security_level_known(ctx, node.span, diags);
106 }
107
108 if strictness == StrictnessLevel::Standard || strictness == StrictnessLevel::Strict {
110 validate_completeness(node, strictness, diags);
111 }
112
113 validate_children(node, node_security, &effective_caps, strictness, diags);
115}
116
117fn validate_security_consistency(
122 ctx: &ContextBlock,
123 parent_security: Option<&SecurityInfo>,
124 span: bock_errors::Span,
125 diags: &mut DiagnosticBag,
126) {
127 let Some(child_sec) = &ctx.security else {
128 return;
129 };
130 let Some(parent_sec) = parent_security else {
131 return;
132 };
133
134 let parent_rank = security_level_rank(&parent_sec.level);
135 let child_rank = security_level_rank(&child_sec.level);
136
137 if let (Some(p), Some(c)) = (parent_rank, child_rank) {
138 if c < p {
139 diags.error(
140 DiagnosticCode {
141 prefix: 'E',
142 number: 8011,
143 },
144 format!(
145 "security level `{}` is less restrictive than parent level `{}`",
146 child_sec.level, parent_sec.level
147 ),
148 span,
149 );
150 }
151 }
152
153 if parent_sec.pii && !child_sec.pii {
155 diags.warning(
156 DiagnosticCode {
157 prefix: 'W',
158 number: 8011,
159 },
160 "child declares pii=false but parent declares pii=true; PII status is inherited"
161 .to_string(),
162 span,
163 );
164 }
165}
166
167fn validate_security_level_known(
169 ctx: &ContextBlock,
170 span: bock_errors::Span,
171 diags: &mut DiagnosticBag,
172) {
173 if let Some(sec) = &ctx.security {
174 if !sec.level.is_empty() && security_level_rank(&sec.level).is_none() {
175 diags.warning(
176 DiagnosticCode {
177 prefix: 'W',
178 number: 8015,
179 },
180 format!(
181 "unknown security level `{}`; known levels are: {}",
182 sec.level,
183 SECURITY_LEVELS.join(", ")
184 ),
185 span,
186 );
187 }
188 }
189}
190
191fn validate_performance_budget(
193 ctx: &ContextBlock,
194 span: bock_errors::Span,
195 diags: &mut DiagnosticBag,
196) {
197 if let Some(perf) = &ctx.performance {
198 if let Some(lat) = &perf.max_latency {
199 if lat.value <= 0.0 {
200 diags.error(
201 DiagnosticCode {
202 prefix: 'E',
203 number: 8016,
204 },
205 "performance max_latency must be a positive value".to_string(),
206 span,
207 );
208 }
209 }
210 if let Some(mem) = &perf.max_memory {
211 if mem.value <= 0.0 {
212 diags.error(
213 DiagnosticCode {
214 prefix: 'E',
215 number: 8016,
216 },
217 "performance max_memory must be a positive value".to_string(),
218 span,
219 );
220 }
221 }
222 }
223}
224
225fn validate_completeness(node: &AIRNode, strictness: StrictnessLevel, diags: &mut DiagnosticBag) {
230 let is_strict = strictness == StrictnessLevel::Strict;
231 let mode_label = if is_strict { "production" } else { "standard" };
232
233 match &node.kind {
234 NodeKind::Module { .. } => {
235 if node.context.is_none() {
236 if is_strict {
237 diags.error(
238 DiagnosticCode {
239 prefix: 'E',
240 number: 8014,
241 },
242 format!(
243 "module is missing @context annotation (required in {mode_label} mode)"
244 ),
245 node.span,
246 );
247 } else {
248 diags.warning(
249 DiagnosticCode {
250 prefix: 'W',
251 number: 8014,
252 },
253 format!("module is missing @context annotation (recommended in {mode_label} mode)"),
254 node.span,
255 );
256 }
257 }
258 }
259 NodeKind::FnDecl {
260 visibility: Visibility::Public,
261 name,
262 ..
263 }
264 | NodeKind::ClassDecl {
265 visibility: Visibility::Public,
266 name,
267 ..
268 }
269 | NodeKind::TraitDecl {
270 visibility: Visibility::Public,
271 name,
272 ..
273 }
274 | NodeKind::RecordDecl {
275 visibility: Visibility::Public,
276 name,
277 ..
278 }
279 | NodeKind::EnumDecl {
280 visibility: Visibility::Public,
281 name,
282 ..
283 } => {
284 if node.context.is_none() {
285 if is_strict {
286 diags.error(
287 DiagnosticCode {
288 prefix: 'E',
289 number: 8013,
290 },
291 format!(
292 "public item `{}` is missing context annotations (required in {mode_label} mode)",
293 name.name
294 ),
295 node.span,
296 );
297 } else {
298 diags.warning(
299 DiagnosticCode {
300 prefix: 'W',
301 number: 8013,
302 },
303 format!(
304 "public item `{}` is missing context annotations (recommended in {mode_label} mode)",
305 name.name
306 ),
307 node.span,
308 );
309 }
310 }
311 }
312 _ => {}
313 }
314}
315
316fn validate_children(
318 node: &AIRNode,
319 parent_security: Option<&SecurityInfo>,
320 parent_capabilities: &HashSet<Capability>,
321 strictness: StrictnessLevel,
322 diags: &mut DiagnosticBag,
323) {
324 match &node.kind {
325 NodeKind::Module { imports, items, .. } => {
326 for child in imports.iter().chain(items.iter()) {
327 validate_node(
328 child,
329 parent_security,
330 parent_capabilities,
331 strictness,
332 diags,
333 );
334 }
335 }
336 NodeKind::FnDecl {
337 params,
338 return_type,
339 body,
340 ..
341 } => {
342 for p in params {
343 validate_node(p, parent_security, parent_capabilities, strictness, diags);
344 }
345 if let Some(rt) = return_type.as_ref() {
346 validate_node(rt, parent_security, parent_capabilities, strictness, diags);
347 }
348 validate_node(
349 body,
350 parent_security,
351 parent_capabilities,
352 strictness,
353 diags,
354 );
355 }
356 NodeKind::ClassDecl { methods, .. } | NodeKind::TraitDecl { methods, .. } => {
357 for m in methods {
358 validate_node(m, parent_security, parent_capabilities, strictness, diags);
359 }
360 }
361 NodeKind::ImplBlock {
362 target, methods, ..
363 } => {
364 validate_node(
365 target,
366 parent_security,
367 parent_capabilities,
368 strictness,
369 diags,
370 );
371 for m in methods {
372 validate_node(m, parent_security, parent_capabilities, strictness, diags);
373 }
374 }
375 NodeKind::EffectDecl { operations, .. } => {
376 for op in operations {
377 validate_node(op, parent_security, parent_capabilities, strictness, diags);
378 }
379 }
380 NodeKind::EnumDecl { variants, .. } => {
381 for v in variants {
382 validate_node(v, parent_security, parent_capabilities, strictness, diags);
383 }
384 }
385 NodeKind::Block { stmts, tail, .. } => {
386 for stmt in stmts {
387 validate_node(
388 stmt,
389 parent_security,
390 parent_capabilities,
391 strictness,
392 diags,
393 );
394 }
395 if let Some(t) = tail.as_ref() {
396 validate_node(t, parent_security, parent_capabilities, strictness, diags);
397 }
398 }
399 NodeKind::If {
400 condition,
401 then_block,
402 else_block,
403 ..
404 } => {
405 validate_node(
406 condition,
407 parent_security,
408 parent_capabilities,
409 strictness,
410 diags,
411 );
412 validate_node(
413 then_block,
414 parent_security,
415 parent_capabilities,
416 strictness,
417 diags,
418 );
419 if let Some(e) = else_block.as_ref() {
420 validate_node(e, parent_security, parent_capabilities, strictness, diags);
421 }
422 }
423 NodeKind::Match {
424 scrutinee, arms, ..
425 } => {
426 validate_node(
427 scrutinee,
428 parent_security,
429 parent_capabilities,
430 strictness,
431 diags,
432 );
433 for arm in arms {
434 validate_node(arm, parent_security, parent_capabilities, strictness, diags);
435 }
436 }
437 NodeKind::MatchArm {
438 pattern,
439 guard,
440 body,
441 ..
442 } => {
443 validate_node(
444 pattern,
445 parent_security,
446 parent_capabilities,
447 strictness,
448 diags,
449 );
450 if let Some(g) = guard.as_ref() {
451 validate_node(g, parent_security, parent_capabilities, strictness, diags);
452 }
453 validate_node(
454 body,
455 parent_security,
456 parent_capabilities,
457 strictness,
458 diags,
459 );
460 }
461 NodeKind::For {
462 pattern,
463 iterable,
464 body,
465 ..
466 } => {
467 validate_node(
468 pattern,
469 parent_security,
470 parent_capabilities,
471 strictness,
472 diags,
473 );
474 validate_node(
475 iterable,
476 parent_security,
477 parent_capabilities,
478 strictness,
479 diags,
480 );
481 validate_node(
482 body,
483 parent_security,
484 parent_capabilities,
485 strictness,
486 diags,
487 );
488 }
489 NodeKind::While {
490 condition, body, ..
491 } => {
492 validate_node(
493 condition,
494 parent_security,
495 parent_capabilities,
496 strictness,
497 diags,
498 );
499 validate_node(
500 body,
501 parent_security,
502 parent_capabilities,
503 strictness,
504 diags,
505 );
506 }
507 NodeKind::Loop { body, .. } => {
508 validate_node(
509 body,
510 parent_security,
511 parent_capabilities,
512 strictness,
513 diags,
514 );
515 }
516 NodeKind::LetBinding { value, .. } => {
517 validate_node(
518 value,
519 parent_security,
520 parent_capabilities,
521 strictness,
522 diags,
523 );
524 }
525 NodeKind::HandlingBlock { body, handlers, .. } => {
526 validate_node(
527 body,
528 parent_security,
529 parent_capabilities,
530 strictness,
531 diags,
532 );
533 for h in handlers {
534 validate_node(
535 &h.handler,
536 parent_security,
537 parent_capabilities,
538 strictness,
539 diags,
540 );
541 }
542 }
543 _ => {}
544 }
545}
546
547#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::context::interpret_context;
553 use crate::node::{NodeIdGen, NodeKind};
554 use crate::stubs::{
555 ByteSize, Capability, ContextBlock, Duration, PerformanceBudget, SecurityInfo, SizeUnit,
556 TimeUnit,
557 };
558 use bock_ast::{Annotation, Ident, Visibility};
559 use bock_errors::Span;
560
561 fn test_span() -> Span {
562 Span::dummy()
563 }
564
565 fn str_expr(s: &str) -> bock_ast::Expr {
566 bock_ast::Expr::Literal {
567 id: 0,
568 span: test_span(),
569 lit: bock_ast::Literal::String(s.to_string()),
570 }
571 }
572
573 fn bool_expr(b: bool) -> bock_ast::Expr {
574 bock_ast::Expr::Literal {
575 id: 0,
576 span: test_span(),
577 lit: bock_ast::Literal::Bool(b),
578 }
579 }
580
581 fn capability_expr(name: &str) -> bock_ast::Expr {
582 bock_ast::Expr::FieldAccess {
583 id: 0,
584 span: test_span(),
585 object: Box::new(bock_ast::Expr::Identifier {
586 id: 0,
587 span: test_span(),
588 name: Ident {
589 name: "Capability".to_string(),
590 span: test_span(),
591 },
592 }),
593 field: Ident {
594 name: name.to_string(),
595 span: test_span(),
596 },
597 }
598 }
599
600 fn method_call_expr(value: &str, method: &str) -> bock_ast::Expr {
601 bock_ast::Expr::MethodCall {
602 id: 0,
603 span: test_span(),
604 receiver: Box::new(bock_ast::Expr::Literal {
605 id: 0,
606 span: test_span(),
607 lit: bock_ast::Literal::Int(value.to_string()),
608 }),
609 method: Ident {
610 name: method.to_string(),
611 span: test_span(),
612 },
613 type_args: vec![],
614 args: vec![],
615 }
616 }
617
618 fn ann(name: &str, args: Vec<bock_ast::Expr>) -> Annotation {
619 Annotation {
620 id: 0,
621 span: test_span(),
622 name: Ident {
623 name: name.to_string(),
624 span: test_span(),
625 },
626 args: args
627 .into_iter()
628 .map(|e| bock_ast::AnnotationArg {
629 label: None,
630 value: e,
631 })
632 .collect(),
633 }
634 }
635
636 fn fn_node(
637 id_gen: &NodeIdGen,
638 annotations: Vec<Annotation>,
639 visibility: Visibility,
640 ) -> AIRNode {
641 let body = AIRNode::new(
642 id_gen.next(),
643 test_span(),
644 NodeKind::Block {
645 stmts: vec![],
646 tail: None,
647 },
648 );
649 AIRNode::new(
650 id_gen.next(),
651 test_span(),
652 NodeKind::FnDecl {
653 annotations,
654 visibility,
655 is_async: false,
656 name: Ident {
657 name: "test_fn".to_string(),
658 span: test_span(),
659 },
660 generic_params: vec![],
661 params: vec![],
662 return_type: None,
663 effect_clause: vec![],
664 where_clause: vec![],
665 body: Box::new(body),
666 },
667 )
668 }
669
670 fn fn_node_named(
671 id_gen: &NodeIdGen,
672 name: &str,
673 annotations: Vec<Annotation>,
674 visibility: Visibility,
675 ) -> AIRNode {
676 let body = AIRNode::new(
677 id_gen.next(),
678 test_span(),
679 NodeKind::Block {
680 stmts: vec![],
681 tail: None,
682 },
683 );
684 AIRNode::new(
685 id_gen.next(),
686 test_span(),
687 NodeKind::FnDecl {
688 annotations,
689 visibility,
690 is_async: false,
691 name: Ident {
692 name: name.to_string(),
693 span: test_span(),
694 },
695 generic_params: vec![],
696 params: vec![],
697 return_type: None,
698 effect_clause: vec![],
699 where_clause: vec![],
700 body: Box::new(body),
701 },
702 )
703 }
704
705 fn module_with_items(id_gen: &NodeIdGen, items: Vec<AIRNode>) -> AIRNode {
706 AIRNode::new(
707 id_gen.next(),
708 test_span(),
709 NodeKind::Module {
710 path: None,
711 annotations: vec![],
712 imports: vec![],
713 items,
714 },
715 )
716 }
717
718 #[test]
721 fn security_consistent_levels_no_error() {
722 let id_gen = NodeIdGen::new();
723 let child = fn_node(
724 &id_gen,
725 vec![ann("security", vec![str_expr("confidential")])],
726 Visibility::Public,
727 );
728 let mut module = module_with_items(&id_gen, vec![child]);
729
730 module.context = Some(ContextBlock {
732 security: Some(SecurityInfo {
733 level: "internal".to_string(),
734 pii: false,
735 }),
736 ..Default::default()
737 });
738
739 let _ = interpret_context(&mut module);
741
742 let diags = validate_context(&module, StrictnessLevel::Lax);
743 assert_eq!(diags.error_count(), 0);
745 }
746
747 #[test]
748 fn security_child_less_restrictive_than_parent_error() {
749 let id_gen = NodeIdGen::new();
750 let child = fn_node(
751 &id_gen,
752 vec![ann("security", vec![str_expr("public")])],
753 Visibility::Public,
754 );
755
756 let mut module = module_with_items(&id_gen, vec![child]);
757 module.context = Some(ContextBlock {
758 security: Some(SecurityInfo {
759 level: "confidential".to_string(),
760 pii: false,
761 }),
762 ..Default::default()
763 });
764
765 let _ = interpret_context(&mut module);
767
768 let diags = validate_context(&module, StrictnessLevel::Lax);
769 assert!(
770 diags.error_count() > 0,
771 "should error on security level contradiction"
772 );
773 }
774
775 #[test]
776 fn security_pii_inheritance_warning() {
777 let id_gen = NodeIdGen::new();
778 let mut child = fn_node(
779 &id_gen,
780 vec![ann(
781 "security",
782 vec![str_expr("confidential"), bool_expr(false)],
783 )],
784 Visibility::Public,
785 );
786 let _ = interpret_context(&mut child);
787
788 let mut module = module_with_items(&id_gen, vec![child]);
789 module.context = Some(ContextBlock {
790 security: Some(SecurityInfo {
791 level: "confidential".to_string(),
792 pii: true,
793 }),
794 ..Default::default()
795 });
796
797 let diags = validate_context(&module, StrictnessLevel::Lax);
798 assert!(
799 diags.warning_count() > 0,
800 "should warn on PII contradiction"
801 );
802 }
803
804 #[test]
805 fn security_unknown_level_warning() {
806 let id_gen = NodeIdGen::new();
807 let mut node = fn_node(
808 &id_gen,
809 vec![ann("security", vec![str_expr("top-secret")])],
810 Visibility::Public,
811 );
812 let _ = interpret_context(&mut node);
813
814 let diags = validate_context(&node, StrictnessLevel::Lax);
815 assert!(
816 diags.warning_count() > 0,
817 "should warn on unknown security level"
818 );
819 }
820
821 #[test]
824 fn performance_valid_budget_no_error() {
825 let id_gen = NodeIdGen::new();
826 let mut node = fn_node(
827 &id_gen,
828 vec![ann(
829 "performance",
830 vec![method_call_expr("100", "ms"), method_call_expr("50", "mb")],
831 )],
832 Visibility::Public,
833 );
834 let _ = interpret_context(&mut node);
835
836 let diags = validate_context(&node, StrictnessLevel::Lax);
837 assert_eq!(diags.error_count(), 0);
838 }
839
840 #[test]
841 fn performance_negative_latency_error() {
842 let id_gen = NodeIdGen::new();
843 let mut node = fn_node(&id_gen, vec![], Visibility::Public);
844 node.context = Some(ContextBlock {
845 performance: Some(PerformanceBudget {
846 max_latency: Some(Duration {
847 value: -10.0,
848 unit: TimeUnit::Ms,
849 }),
850 max_memory: None,
851 }),
852 ..Default::default()
853 });
854
855 let diags = validate_context(&node, StrictnessLevel::Lax);
856 assert!(diags.error_count() > 0, "should error on negative latency");
857 }
858
859 #[test]
860 fn performance_zero_memory_error() {
861 let id_gen = NodeIdGen::new();
862 let mut node = fn_node(&id_gen, vec![], Visibility::Public);
863 node.context = Some(ContextBlock {
864 performance: Some(PerformanceBudget {
865 max_latency: None,
866 max_memory: Some(ByteSize {
867 value: 0.0,
868 unit: SizeUnit::Mb,
869 }),
870 }),
871 ..Default::default()
872 });
873
874 let diags = validate_context(&node, StrictnessLevel::Lax);
875 assert!(
876 diags.error_count() > 0,
877 "should error on zero memory budget"
878 );
879 }
880
881 #[test]
884 fn completeness_lax_no_warnings() {
885 let id_gen = NodeIdGen::new();
886 let node = fn_node(&id_gen, vec![], Visibility::Public);
887 let module = module_with_items(&id_gen, vec![node]);
888
889 let diags = validate_context(&module, StrictnessLevel::Lax);
890 assert_eq!(
891 diags.warning_count(),
892 0,
893 "lax: no warnings on missing context"
894 );
895 assert_eq!(diags.error_count(), 0, "lax: no errors on missing context");
896 }
897
898 #[test]
899 fn completeness_lax_private_fn_ok() {
900 let id_gen = NodeIdGen::new();
901 let node = fn_node(&id_gen, vec![], Visibility::Private);
902 let diags = validate_context(&node, StrictnessLevel::Lax);
903 assert_eq!(diags.warning_count(), 0);
904 assert_eq!(diags.error_count(), 0);
905 }
906
907 #[test]
910 fn completeness_standard_public_fn_without_context_warns() {
911 let id_gen = NodeIdGen::new();
912 let node = fn_node(&id_gen, vec![], Visibility::Public);
913
914 let diags = validate_context(&node, StrictnessLevel::Standard);
915 assert!(
916 diags.warning_count() > 0,
917 "standard mode should warn on public fn without context"
918 );
919 assert_eq!(
920 diags.error_count(),
921 0,
922 "standard mode should not error on public fn without context"
923 );
924 }
925
926 #[test]
927 fn completeness_standard_private_fn_without_context_ok() {
928 let id_gen = NodeIdGen::new();
929 let node = fn_node(&id_gen, vec![], Visibility::Private);
930
931 let diags = validate_context(&node, StrictnessLevel::Standard);
932 assert_eq!(diags.warning_count(), 0);
933 assert_eq!(diags.error_count(), 0);
934 }
935
936 #[test]
937 fn completeness_standard_module_without_context_warns() {
938 let id_gen = NodeIdGen::new();
939 let module = module_with_items(&id_gen, vec![]);
940
941 let diags = validate_context(&module, StrictnessLevel::Standard);
942 assert!(
943 diags.warning_count() > 0,
944 "standard mode should warn on module without context"
945 );
946 assert_eq!(diags.error_count(), 0);
947 }
948
949 #[test]
950 fn completeness_standard_module_with_context_ok() {
951 let id_gen = NodeIdGen::new();
952 let mut module = module_with_items(&id_gen, vec![]);
953 module.context = Some(ContextBlock {
954 context_text: Some("Payment module.".to_string()),
955 ..Default::default()
956 });
957
958 let diags = validate_context(&module, StrictnessLevel::Standard);
959 assert_eq!(diags.warning_count(), 0);
960 assert_eq!(diags.error_count(), 0);
961 }
962
963 #[test]
966 fn completeness_strict_public_fn_without_context_errors() {
967 let id_gen = NodeIdGen::new();
968 let node = fn_node(&id_gen, vec![], Visibility::Public);
969
970 let diags = validate_context(&node, StrictnessLevel::Strict);
971 assert!(
972 diags.error_count() > 0,
973 "strict mode should error on public fn without context"
974 );
975 }
976
977 #[test]
978 fn completeness_strict_private_fn_without_context_ok() {
979 let id_gen = NodeIdGen::new();
980 let node = fn_node(&id_gen, vec![], Visibility::Private);
981
982 let diags = validate_context(&node, StrictnessLevel::Strict);
983 assert_eq!(
984 diags.error_count(),
985 0,
986 "strict mode should not error on private fn without context"
987 );
988 }
989
990 #[test]
991 fn completeness_strict_module_without_context_errors() {
992 let id_gen = NodeIdGen::new();
993 let module = module_with_items(&id_gen, vec![]);
994
995 let diags = validate_context(&module, StrictnessLevel::Strict);
996 assert!(
997 diags.error_count() > 0,
998 "strict mode should error on module without context"
999 );
1000 }
1001
1002 #[test]
1003 fn completeness_strict_module_with_context_ok() {
1004 let id_gen = NodeIdGen::new();
1005 let mut module = module_with_items(&id_gen, vec![]);
1006 module.context = Some(ContextBlock {
1007 context_text: Some("Payment module.".to_string()),
1008 ..Default::default()
1009 });
1010
1011 let diags = validate_context(&module, StrictnessLevel::Strict);
1012 assert_eq!(diags.error_count(), 0);
1013 }
1014
1015 #[test]
1018 fn strictness_from_name_sketch() {
1019 assert_eq!(
1020 StrictnessLevel::from_name("sketch"),
1021 Some(StrictnessLevel::Lax)
1022 );
1023 assert_eq!(
1024 StrictnessLevel::from_name("lax"),
1025 Some(StrictnessLevel::Lax)
1026 );
1027 assert_eq!(
1028 StrictnessLevel::from_name("Sketch"),
1029 Some(StrictnessLevel::Lax)
1030 );
1031 }
1032
1033 #[test]
1034 fn strictness_from_name_development() {
1035 assert_eq!(
1036 StrictnessLevel::from_name("development"),
1037 Some(StrictnessLevel::Standard)
1038 );
1039 assert_eq!(
1040 StrictnessLevel::from_name("standard"),
1041 Some(StrictnessLevel::Standard)
1042 );
1043 assert_eq!(
1044 StrictnessLevel::from_name("Development"),
1045 Some(StrictnessLevel::Standard)
1046 );
1047 }
1048
1049 #[test]
1050 fn strictness_from_name_production() {
1051 assert_eq!(
1052 StrictnessLevel::from_name("production"),
1053 Some(StrictnessLevel::Strict)
1054 );
1055 assert_eq!(
1056 StrictnessLevel::from_name("strict"),
1057 Some(StrictnessLevel::Strict)
1058 );
1059 assert_eq!(
1060 StrictnessLevel::from_name("Production"),
1061 Some(StrictnessLevel::Strict)
1062 );
1063 }
1064
1065 #[test]
1066 fn strictness_from_name_unknown() {
1067 assert_eq!(StrictnessLevel::from_name(""), None);
1068 assert_eq!(StrictnessLevel::from_name("debug"), None);
1069 }
1070
1071 #[test]
1074 fn three_levels_differ_on_public_fn_without_context() {
1075 let id_gen = NodeIdGen::new();
1076
1077 let node_lax = fn_node(&id_gen, vec![], Visibility::Public);
1079 let node_std = fn_node(&id_gen, vec![], Visibility::Public);
1080 let node_strict = fn_node(&id_gen, vec![], Visibility::Public);
1081
1082 let d_lax = validate_context(&node_lax, StrictnessLevel::Lax);
1083 let d_std = validate_context(&node_std, StrictnessLevel::Standard);
1084 let d_strict = validate_context(&node_strict, StrictnessLevel::Strict);
1085
1086 assert_eq!(
1088 d_lax.warning_count() + d_lax.error_count(),
1089 0,
1090 "lax: silent"
1091 );
1092 assert!(d_std.warning_count() > 0, "standard: warns");
1094 assert_eq!(d_std.error_count(), 0, "standard: no errors");
1095 assert!(d_strict.error_count() > 0, "strict: errors");
1097 }
1098
1099 #[test]
1100 fn three_levels_differ_on_module_without_context() {
1101 let id_gen = NodeIdGen::new();
1102
1103 let mod_lax = module_with_items(&id_gen, vec![]);
1104 let mod_std = module_with_items(&id_gen, vec![]);
1105 let mod_strict = module_with_items(&id_gen, vec![]);
1106
1107 let d_lax = validate_context(&mod_lax, StrictnessLevel::Lax);
1108 let d_std = validate_context(&mod_std, StrictnessLevel::Standard);
1109 let d_strict = validate_context(&mod_strict, StrictnessLevel::Strict);
1110
1111 assert_eq!(
1112 d_lax.warning_count() + d_lax.error_count(),
1113 0,
1114 "lax: silent"
1115 );
1116 assert!(d_std.warning_count() > 0, "standard: warns");
1117 assert_eq!(d_std.error_count(), 0, "standard: no errors");
1118 assert!(d_strict.error_count() > 0, "strict: errors");
1119 }
1120
1121 #[test]
1124 fn invariant_comparison_expr_ok() {
1125 let id_gen = NodeIdGen::new();
1126 let invariant_expr = bock_ast::Expr::Binary {
1127 id: 0,
1128 span: test_span(),
1129 op: bock_ast::BinOp::Le,
1130 left: Box::new(bock_ast::Expr::Identifier {
1131 id: 0,
1132 span: test_span(),
1133 name: Ident {
1134 name: "a".to_string(),
1135 span: test_span(),
1136 },
1137 }),
1138 right: Box::new(bock_ast::Expr::Identifier {
1139 id: 0,
1140 span: test_span(),
1141 name: Ident {
1142 name: "b".to_string(),
1143 span: test_span(),
1144 },
1145 }),
1146 };
1147 let mut node = fn_node(
1148 &id_gen,
1149 vec![ann("invariant", vec![invariant_expr])],
1150 Visibility::Public,
1151 );
1152 let diags = interpret_context(&mut node);
1153 assert_eq!(diags.error_count(), 0, "comparison invariant should pass");
1154 }
1155
1156 #[test]
1157 fn invariant_arithmetic_expr_error() {
1158 let id_gen = NodeIdGen::new();
1159 let invariant_expr = bock_ast::Expr::Binary {
1161 id: 0,
1162 span: test_span(),
1163 op: bock_ast::BinOp::Add,
1164 left: Box::new(bock_ast::Expr::Identifier {
1165 id: 0,
1166 span: test_span(),
1167 name: Ident {
1168 name: "a".to_string(),
1169 span: test_span(),
1170 },
1171 }),
1172 right: Box::new(bock_ast::Expr::Identifier {
1173 id: 0,
1174 span: test_span(),
1175 name: Ident {
1176 name: "b".to_string(),
1177 span: test_span(),
1178 },
1179 }),
1180 };
1181 let mut node = fn_node(
1182 &id_gen,
1183 vec![ann("invariant", vec![invariant_expr])],
1184 Visibility::Public,
1185 );
1186 let diags = interpret_context(&mut node);
1187 assert!(
1188 diags.error_count() > 0,
1189 "arithmetic invariant should produce E8010 error"
1190 );
1191 }
1192
1193 #[test]
1194 fn invariant_logical_expr_ok() {
1195 let id_gen = NodeIdGen::new();
1196 let invariant_expr = bock_ast::Expr::Binary {
1197 id: 0,
1198 span: test_span(),
1199 op: bock_ast::BinOp::And,
1200 left: Box::new(bock_ast::Expr::Identifier {
1201 id: 0,
1202 span: test_span(),
1203 name: Ident {
1204 name: "x".to_string(),
1205 span: test_span(),
1206 },
1207 }),
1208 right: Box::new(bock_ast::Expr::Identifier {
1209 id: 0,
1210 span: test_span(),
1211 name: Ident {
1212 name: "y".to_string(),
1213 span: test_span(),
1214 },
1215 }),
1216 };
1217 let mut node = fn_node(
1218 &id_gen,
1219 vec![ann("invariant", vec![invariant_expr])],
1220 Visibility::Public,
1221 );
1222 let diags = interpret_context(&mut node);
1223 assert_eq!(diags.error_count(), 0, "logical invariant should pass");
1224 }
1225
1226 #[test]
1227 fn invariant_not_expr_ok() {
1228 let id_gen = NodeIdGen::new();
1229 let invariant_expr = bock_ast::Expr::Unary {
1230 id: 0,
1231 span: test_span(),
1232 op: bock_ast::UnaryOp::Not,
1233 operand: Box::new(bock_ast::Expr::Identifier {
1234 id: 0,
1235 span: test_span(),
1236 name: Ident {
1237 name: "flag".to_string(),
1238 span: test_span(),
1239 },
1240 }),
1241 };
1242 let mut node = fn_node(
1243 &id_gen,
1244 vec![ann("invariant", vec![invariant_expr])],
1245 Visibility::Public,
1246 );
1247 let diags = interpret_context(&mut node);
1248 assert_eq!(diags.error_count(), 0, "negation invariant should pass");
1249 }
1250
1251 #[test]
1252 fn invariant_negate_numeric_error() {
1253 let id_gen = NodeIdGen::new();
1254 let invariant_expr = bock_ast::Expr::Unary {
1256 id: 0,
1257 span: test_span(),
1258 op: bock_ast::UnaryOp::Neg,
1259 operand: Box::new(bock_ast::Expr::Identifier {
1260 id: 0,
1261 span: test_span(),
1262 name: Ident {
1263 name: "x".to_string(),
1264 span: test_span(),
1265 },
1266 }),
1267 };
1268 let mut node = fn_node(
1269 &id_gen,
1270 vec![ann("invariant", vec![invariant_expr])],
1271 Visibility::Public,
1272 );
1273 let diags = interpret_context(&mut node);
1274 assert!(
1275 diags.error_count() > 0,
1276 "numeric negation invariant should produce E8010 error"
1277 );
1278 }
1279
1280 #[test]
1281 fn invariant_call_expr_ok() {
1282 let id_gen = NodeIdGen::new();
1283 let invariant_expr = bock_ast::Expr::Call {
1285 id: 0,
1286 span: test_span(),
1287 callee: Box::new(bock_ast::Expr::Identifier {
1288 id: 0,
1289 span: test_span(),
1290 name: Ident {
1291 name: "is_valid".to_string(),
1292 span: test_span(),
1293 },
1294 }),
1295 args: vec![],
1296 type_args: vec![],
1297 };
1298 let mut node = fn_node(
1299 &id_gen,
1300 vec![ann("invariant", vec![invariant_expr])],
1301 Visibility::Public,
1302 );
1303 let diags = interpret_context(&mut node);
1304 assert_eq!(diags.error_count(), 0, "call invariant should be accepted");
1305 }
1306
1307 #[test]
1310 fn capabilities_additive_no_error() {
1311 let id_gen = NodeIdGen::new();
1312 let mut child = fn_node(
1313 &id_gen,
1314 vec![ann("requires", vec![capability_expr("Crypto")])],
1315 Visibility::Public,
1316 );
1317 let _ = interpret_context(&mut child);
1318
1319 let mut module = module_with_items(&id_gen, vec![child]);
1320 module.context = Some(ContextBlock {
1321 capabilities: {
1322 let mut s = HashSet::new();
1323 s.insert(Capability::new("Network"));
1324 s
1325 },
1326 ..Default::default()
1327 });
1328
1329 let diags = validate_context(&module, StrictnessLevel::Standard);
1330 assert_eq!(diags.error_count(), 0);
1332 }
1333
1334 #[test]
1337 fn full_tree_validation() {
1338 let id_gen = NodeIdGen::new();
1339
1340 let mut child1 = fn_node_named(
1342 &id_gen,
1343 "process_payment",
1344 vec![
1345 ann("context", vec![str_expr("Process a payment.")]),
1346 ann("security", vec![str_expr("secret"), bool_expr(true)]),
1347 ann("requires", vec![capability_expr("Network")]),
1348 ],
1349 Visibility::Public,
1350 );
1351 let _ = interpret_context(&mut child1);
1352
1353 let child2 = fn_node_named(&id_gen, "helper", vec![], Visibility::Private);
1355
1356 let mut module = module_with_items(&id_gen, vec![child1, child2]);
1357 module.context = Some(ContextBlock {
1358 context_text: Some("Payment module.".to_string()),
1359 security: Some(SecurityInfo {
1360 level: "confidential".to_string(),
1361 pii: true,
1362 }),
1363 ..Default::default()
1364 });
1365
1366 let diags = validate_context(&module, StrictnessLevel::Strict);
1367 assert_eq!(diags.error_count(), 0);
1371 assert_eq!(diags.warning_count(), 0);
1372 }
1373}