1use colored::Colorize;
2use std::collections::BTreeSet;
3
4use crate::ast::{
5 DecisionBlock, DecisionImpact, Expr, FnDef, Pattern, Stmt, TopLevel, VerifyBlock,
6 VerifyGivenDomain, VerifyKind,
7};
8use crate::interpreter::{Interpreter, aver_repr};
9use crate::types::{Type, parse_type_str_strict};
10use crate::value::{RuntimeError, Value};
11use crate::verify_law::{
12 canonical_spec_ref, collect_contextual_helper_law_hints, collect_missing_helper_law_hints,
13 contextual_helper_law_message, missing_helper_law_message, named_law_function,
14};
15
16pub struct VerifyResult {
17 #[allow(dead_code)]
18 pub fn_name: String,
19 pub passed: usize,
20 pub failed: usize,
21 pub skipped: usize,
22 #[allow(dead_code)]
23 pub failures: Vec<(String, String, String)>, }
25
26pub struct ModuleCheckFindings {
27 pub errors: Vec<CheckFinding>,
28 pub warnings: Vec<CheckFinding>,
29}
30
31type FnSigSummary = (Vec<Type>, Type, Vec<String>);
32type FnSigMap = std::collections::HashMap<String, FnSigSummary>;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct CheckFinding {
36 pub line: usize,
37 pub module: Option<String>,
38 pub file: Option<String>,
39 pub message: String,
40}
41
42fn module_name_for_items(items: &[TopLevel]) -> Option<String> {
43 items.iter().find_map(|item| {
44 if let TopLevel::Module(m) = item {
45 Some(m.name.clone())
46 } else {
47 None
48 }
49 })
50}
51
52fn dotted_name(expr: &Expr) -> Option<String> {
53 match expr {
54 Expr::Ident(name) => Some(name.clone()),
55 Expr::Attr(base, field) => {
56 let mut prefix = dotted_name(base)?;
57 prefix.push('.');
58 prefix.push_str(field);
59 Some(prefix)
60 }
61 _ => None,
62 }
63}
64
65fn normalize_constructor_tag(path: &str) -> Option<String> {
66 let mut parts = path.split('.').collect::<Vec<_>>();
67 if parts.len() < 2 {
68 return None;
69 }
70 let variant = parts.pop()?;
71 let type_name = parts.pop()?;
72 Some(format!("{}.{}", type_name, variant))
73}
74
75fn constructor_tag_from_pattern(pattern: &Pattern) -> Option<String> {
76 match pattern {
77 Pattern::Constructor(path, _) => normalize_constructor_tag(path),
78 _ => None,
79 }
80}
81
82fn constructor_tag_from_expr(expr: &Expr) -> Option<String> {
83 match expr {
84 Expr::Attr(_, _) => normalize_constructor_tag(&dotted_name(expr)?),
85 Expr::FnCall(callee, _) => normalize_constructor_tag(&dotted_name(callee)?),
86 Expr::Constructor(name, _) => normalize_constructor_tag(name),
87 _ => None,
88 }
89}
90
91fn collect_target_call_args<'a>(
92 expr: &'a Expr,
93 fn_name: &str,
94 arg_index: usize,
95 out: &mut Vec<&'a Expr>,
96) {
97 match expr {
98 Expr::FnCall(callee, args) => {
99 if callee_is_target(callee, fn_name)
100 && let Some(arg) = args.get(arg_index)
101 {
102 out.push(arg);
103 }
104 collect_target_call_args(callee, fn_name, arg_index, out);
105 for arg in args {
106 collect_target_call_args(arg, fn_name, arg_index, out);
107 }
108 }
109 Expr::BinOp(_, left, right) => {
110 collect_target_call_args(left, fn_name, arg_index, out);
111 collect_target_call_args(right, fn_name, arg_index, out);
112 }
113 Expr::Match { subject, arms, .. } => {
114 collect_target_call_args(subject, fn_name, arg_index, out);
115 for arm in arms {
116 collect_target_call_args(&arm.body, fn_name, arg_index, out);
117 }
118 }
119 Expr::Constructor(_, Some(inner)) | Expr::ErrorProp(inner) => {
120 collect_target_call_args(inner, fn_name, arg_index, out);
121 }
122 Expr::List(items) | Expr::Tuple(items) => {
123 for item in items {
124 collect_target_call_args(item, fn_name, arg_index, out);
125 }
126 }
127 Expr::MapLiteral(entries) => {
128 for (key, value) in entries {
129 collect_target_call_args(key, fn_name, arg_index, out);
130 collect_target_call_args(value, fn_name, arg_index, out);
131 }
132 }
133 Expr::Attr(obj, _) => collect_target_call_args(obj, fn_name, arg_index, out),
134 Expr::RecordCreate { fields, .. } => {
135 for (_, value) in fields {
136 collect_target_call_args(value, fn_name, arg_index, out);
137 }
138 }
139 Expr::RecordUpdate { base, updates, .. } => {
140 collect_target_call_args(base, fn_name, arg_index, out);
141 for (_, value) in updates {
142 collect_target_call_args(value, fn_name, arg_index, out);
143 }
144 }
145 Expr::TailCall(boxed) => {
146 let (target, args) = boxed.as_ref();
147 if target == fn_name
148 && let Some(arg) = args.get(arg_index)
149 {
150 out.push(arg);
151 }
152 for arg in args {
153 collect_target_call_args(arg, fn_name, arg_index, out);
154 }
155 }
156 Expr::Literal(_)
157 | Expr::Ident(_)
158 | Expr::InterpolatedStr(_)
159 | Expr::Resolved(_)
160 | Expr::Constructor(_, None) => {}
161 }
162}
163
164fn expr_is_result_err_case(expr: &Expr) -> bool {
165 match expr {
166 Expr::FnCall(callee, _) => dotted_name(callee)
167 .and_then(|path| normalize_constructor_tag(&path))
168 .is_some_and(|tag| tag == "Result.Err"),
169 Expr::Constructor(name, _) => {
170 normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Err")
171 }
172 _ => false,
173 }
174}
175
176fn expr_is_option_none_case(expr: &Expr) -> bool {
177 match expr {
178 Expr::Attr(_, _) => dotted_name(expr)
179 .and_then(|path| normalize_constructor_tag(&path))
180 .is_some_and(|tag| tag == "Option.None"),
181 Expr::Constructor(name, None) => {
182 normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.None")
183 }
184 _ => false,
185 }
186}
187
188fn verify_cases_block_is_well_formed(block: &VerifyBlock) -> bool {
189 matches!(block.kind, VerifyKind::Cases)
190 && !block.cases.is_empty()
191 && block.cases.iter().all(|(left, right)| {
192 verify_case_calls_target(left, &block.fn_name)
193 && !verify_case_calls_target(right, &block.fn_name)
194 })
195}
196
197fn enum_match_coverage_target(f: &FnDef) -> Option<(usize, Vec<String>)> {
198 let Expr::Match { subject, arms, .. } = f.body.tail_expr()? else {
199 return None;
200 };
201 let Expr::Ident(subject_name) = subject.as_ref() else {
202 return None;
203 };
204 let param_index = f.params.iter().position(|(name, _)| name == subject_name)?;
205 let constructors: BTreeSet<String> = arms
206 .iter()
207 .filter_map(|arm| constructor_tag_from_pattern(&arm.pattern))
208 .collect();
209 if constructors.is_empty() {
210 None
211 } else {
212 Some((param_index, constructors.into_iter().collect()))
213 }
214}
215
216pub fn collect_verify_coverage_warnings(items: &[TopLevel]) -> Vec<CheckFinding> {
217 collect_verify_coverage_warnings_in(items, None)
218}
219
220pub fn collect_verify_law_dependency_warnings(
221 items: &[TopLevel],
222 fn_sigs: &FnSigMap,
223) -> Vec<CheckFinding> {
224 collect_verify_law_dependency_warnings_in(items, fn_sigs, None)
225}
226
227pub fn collect_verify_law_dependency_warnings_in(
228 items: &[TopLevel],
229 fn_sigs: &FnSigMap,
230 source_file: Option<&str>,
231) -> Vec<CheckFinding> {
232 let module_name = module_name_for_items(items);
233 let mut findings = collect_missing_helper_law_hints(items, fn_sigs)
234 .into_iter()
235 .map(|hint| CheckFinding {
236 line: hint.line,
237 module: module_name.clone(),
238 file: source_file.map(|s| s.to_string()),
239 message: missing_helper_law_message(&hint),
240 })
241 .collect::<Vec<_>>();
242 findings.extend(
243 collect_contextual_helper_law_hints(items, fn_sigs)
244 .into_iter()
245 .map(|hint| CheckFinding {
246 line: hint.line,
247 module: module_name.clone(),
248 file: source_file.map(|s| s.to_string()),
249 message: contextual_helper_law_message(&hint),
250 }),
251 );
252 findings
253}
254
255pub fn collect_verify_coverage_warnings_in(
256 items: &[TopLevel],
257 source_file: Option<&str>,
258) -> Vec<CheckFinding> {
259 let module_name = module_name_for_items(items);
260 let fn_defs: std::collections::HashMap<&str, &FnDef> = items
261 .iter()
262 .filter_map(|item| {
263 if let TopLevel::FnDef(f) = item {
264 Some((f.name.as_str(), f))
265 } else {
266 None
267 }
268 })
269 .collect();
270
271 let mut warnings = Vec::new();
272 for block in merge_verify_blocks(items) {
273 if !verify_cases_block_is_well_formed(&block) {
274 continue;
275 }
276 let Some(f) = fn_defs.get(block.fn_name.as_str()).copied() else {
277 continue;
278 };
279
280 if let Ok(ret_ty) = parse_type_str_strict(&f.return_type) {
281 if matches!(ret_ty, Type::Result(_, _))
282 && !block
283 .cases
284 .iter()
285 .any(|(_, right)| expr_is_result_err_case(right))
286 {
287 warnings.push(CheckFinding {
288 line: block.line,
289 module: module_name.clone(),
290 file: source_file.map(|s| s.to_string()),
291 message: format!(
292 "verify examples for {} do not include any Result.Err case",
293 block.fn_name
294 ),
295 });
296 }
297
298 if matches!(ret_ty, Type::Option(_))
299 && !block
300 .cases
301 .iter()
302 .any(|(_, right)| expr_is_option_none_case(right))
303 {
304 warnings.push(CheckFinding {
305 line: block.line,
306 module: module_name.clone(),
307 file: source_file.map(|s| s.to_string()),
308 message: format!(
309 "verify examples for {} do not include any Option.None case",
310 block.fn_name
311 ),
312 });
313 }
314 }
315
316 if let Some((param_index, constructors)) = enum_match_coverage_target(f) {
317 let mut covered = BTreeSet::new();
318 for (left, _) in &block.cases {
319 let mut args = Vec::new();
320 collect_target_call_args(left, &block.fn_name, param_index, &mut args);
321 for arg in args {
322 if let Some(tag) = constructor_tag_from_expr(arg)
323 && constructors.contains(&tag)
324 {
325 covered.insert(tag);
326 }
327 }
328 }
329 if covered.len() < constructors.len() {
330 warnings.push(CheckFinding {
331 line: block.line,
332 module: module_name.clone(),
333 file: source_file.map(|s| s.to_string()),
334 message: format!(
335 "verify examples for {} cover {}/{} enum constructors",
336 block.fn_name,
337 covered.len(),
338 constructors.len()
339 ),
340 });
341 }
342 }
343 }
344
345 warnings
346}
347
348fn verify_given_domain_to_str(domain: &VerifyGivenDomain) -> String {
349 match domain {
350 VerifyGivenDomain::IntRange { start, end } => format!("{start}..{end}"),
351 VerifyGivenDomain::Explicit(values) => {
352 let parts: Vec<String> = values.iter().map(expr_to_str).collect();
353 format!("[{}]", parts.join(", "))
354 }
355 }
356}
357
358pub fn run_verify(block: &VerifyBlock, interp: &mut Interpreter) -> VerifyResult {
359 let mut passed = 0;
360 let mut failed = 0;
361 let mut skipped = 0;
362 let mut failures = Vec::new();
363 let is_law = matches!(block.kind, VerifyKind::Law(_));
364
365 match &block.kind {
366 VerifyKind::Cases => println!("Verify: {}", block.fn_name.cyan()),
367 VerifyKind::Law(law) => {
368 if let Ok(Value::Fn(function)) = interp.lookup(&law.name)
369 && function.effects.is_empty()
370 && crate::verify_law::canonical_spec_shape(&block.fn_name, law, &law.name)
371 {
372 println!("Verify: {} spec {}", block.fn_name.cyan(), law.name.cyan());
373 } else {
374 println!("Verify: {} law {}", block.fn_name.cyan(), law.name.cyan());
375 }
376 for given in &law.givens {
377 println!(
378 " {} {}: {} = {}",
379 "given".dimmed(),
380 given.name,
381 given.type_name,
382 verify_given_domain_to_str(&given.domain)
383 );
384 }
385 if let Some(when_expr) = &law.when {
386 println!(" {} {}", "when".dimmed(), expr_to_str(when_expr));
387 }
388 println!(
389 " {} {} == {}",
390 "law".dimmed(),
391 expr_to_str(&law.lhs),
392 expr_to_str(&law.rhs)
393 );
394 println!(" {} {}", "cases".dimmed(), block.cases.len());
395 }
396 }
397
398 for (idx, (left_expr, right_expr)) in block.cases.iter().enumerate() {
399 let case_str = format!("{} == {}", expr_to_str(left_expr), expr_to_str(right_expr));
400 let case_label = if is_law {
401 format!("case {}/{}", idx + 1, block.cases.len())
402 } else {
403 case_str.clone()
404 };
405 let failure_case = if is_law {
406 format!("{} [{}]", case_label, case_str)
407 } else {
408 case_str.clone()
409 };
410
411 if let VerifyKind::Law(law) = &block.kind
412 && let Some(sample_guard) = law.sample_guards.get(idx)
413 {
414 match interp.eval_expr(sample_guard) {
415 Ok(Value::Bool(true)) => {}
416 Ok(Value::Bool(false)) => {
417 skipped += 1;
418 println!(" {} {} (when false)", "·".dimmed(), case_label.dimmed());
419 continue;
420 }
421 Ok(other) => {
422 failed += 1;
423 println!(" {} {}", "✗".red(), case_label);
424 println!(" when did not evaluate to Bool: {}", aver_repr(&other));
425 failures.push((
426 failure_case,
427 "Bool".to_string(),
428 format!("when produced {}", aver_repr(&other)),
429 ));
430 continue;
431 }
432 Err(RuntimeError::ErrProp(err_val)) => {
433 failed += 1;
434 println!(" {} {}", "✗".red(), case_label);
435 println!(" when ? hit Result.Err({})", aver_repr(&err_val));
436 failures.push((
437 failure_case,
438 String::new(),
439 format!("when ? hit Result.Err({})", aver_repr(&err_val)),
440 ));
441 continue;
442 }
443 Err(e) => {
444 failed += 1;
445 println!(" {} {}", "✗".red(), case_label);
446 println!(" when error: {}", e);
447 failures.push((failure_case, String::new(), format!("WHEN ERROR: {}", e)));
448 continue;
449 }
450 }
451 }
452
453 let left_result = interp.eval_expr(left_expr);
454 let right_result = interp.eval_expr(right_expr);
455
456 match (left_result, right_result) {
457 (Ok(left_val), Ok(right_val)) => {
458 if interp.aver_eq(&left_val, &right_val) {
459 passed += 1;
460 if !is_law {
461 println!(" {} {}", "✓".green(), case_label);
462 }
463 } else {
464 failed += 1;
465 println!(" {} {}", "✗".red(), case_label);
466 if is_law {
467 println!(" expanded: {}", case_str);
468 }
469 let expected = aver_repr(&right_val);
470 let actual = aver_repr(&left_val);
471 println!(" expected: {}", expected);
472 println!(" got: {}", actual);
473 failures.push((failure_case, expected, actual));
474 }
475 }
476 (Err(RuntimeError::ErrProp(err_val)), _) | (_, Err(RuntimeError::ErrProp(err_val))) => {
478 failed += 1;
479 println!(" {} {}", "✗".red(), case_label);
480 if is_law {
481 println!(" expanded: {}", case_str);
482 }
483 println!(" ? hit Result.Err({})", aver_repr(&err_val));
484 failures.push((
485 failure_case,
486 String::new(),
487 format!("? hit Result.Err({})", aver_repr(&err_val)),
488 ));
489 }
490 (Err(e), _) | (_, Err(e)) => {
491 failed += 1;
492 println!(" {} {}", "✗".red(), case_label);
493 if is_law {
494 println!(" expanded: {}", case_str);
495 }
496 println!(" error: {}", e);
497 failures.push((failure_case, String::new(), format!("ERROR: {}", e)));
498 }
499 }
500 }
501
502 let total = passed + failed;
503 if is_law && failed == 0 {
504 if skipped == 0 {
505 println!(
506 " {} all {} generated case(s) passed",
507 "✓".green(),
508 block.cases.len()
509 );
510 } else {
511 println!(
512 " {} all {} active generated case(s) passed ({} skipped by when)",
513 "✓".green(),
514 passed,
515 skipped
516 );
517 }
518 }
519 if failed == 0 && skipped == 0 {
520 println!(" {}", format!("{}/{} passed", passed, total).green());
521 } else if failed == 0 {
522 println!(
523 " {}",
524 format!("{}/{} passed, {} skipped", passed, total + skipped, skipped).green()
525 );
526 } else if skipped == 0 {
527 println!(" {}", format!("{}/{} passed", passed, total).red());
528 } else {
529 println!(
530 " {}",
531 format!("{}/{} passed, {} skipped", passed, total + skipped, skipped).red()
532 );
533 }
534
535 VerifyResult {
536 fn_name: block.fn_name.clone(),
537 passed,
538 failed,
539 skipped,
540 failures,
541 }
542}
543
544pub fn index_decisions(items: &[TopLevel]) -> Vec<&DecisionBlock> {
545 items
546 .iter()
547 .filter_map(|item| {
548 if let TopLevel::Decision(d) = item {
549 Some(d)
550 } else {
551 None
552 }
553 })
554 .collect()
555}
556
557pub fn merge_verify_blocks(items: &[TopLevel]) -> Vec<VerifyBlock> {
558 let mut merged: Vec<VerifyBlock> = Vec::new();
559 let mut by_fn_cases: std::collections::HashMap<String, usize> =
560 std::collections::HashMap::new();
561
562 for item in items {
563 let TopLevel::Verify(vb) = item else {
564 continue;
565 };
566 match &vb.kind {
567 VerifyKind::Cases => {
568 if let Some(&idx) = by_fn_cases.get(&vb.fn_name) {
569 merged[idx].cases.extend(vb.cases.clone());
570 } else {
571 by_fn_cases.insert(vb.fn_name.clone(), merged.len());
572 merged.push(vb.clone());
573 }
574 }
575 VerifyKind::Law(_) => {
576 merged.push(vb.clone());
577 }
578 }
579 }
580
581 merged
582}
583
584fn fn_needs_desc(f: &FnDef) -> bool {
587 f.name != "main"
588}
589
590fn fn_needs_verify(f: &FnDef) -> bool {
596 if f.name == "main" {
597 return false;
598 }
599 if !f.effects.is_empty() {
600 return false;
601 }
602 !is_trivial_passthrough_wrapper(f)
603}
604
605fn is_trivial_passthrough_wrapper(f: &FnDef) -> bool {
606 let param_names: Vec<&str> = f.params.iter().map(|(name, _)| name.as_str()).collect();
607
608 if f.body.stmts().len() != 1 {
609 return false;
610 }
611 f.body
612 .tail_expr()
613 .is_some_and(|expr| expr_is_passthrough(expr, ¶m_names))
614}
615
616fn expr_is_passthrough(expr: &Expr, param_names: &[&str]) -> bool {
617 match expr {
618 Expr::Ident(name) => param_names.len() == 1 && name == param_names[0],
620 Expr::FnCall(_, args) => args_match_params(args, param_names),
622 Expr::Constructor(_, Some(arg)) => {
624 if param_names.len() != 1 {
625 return false;
626 }
627 matches!(arg.as_ref(), Expr::Ident(name) if name == param_names[0])
628 }
629 _ => false,
630 }
631}
632
633fn args_match_params(args: &[Expr], param_names: &[&str]) -> bool {
634 if args.len() != param_names.len() {
635 return false;
636 }
637 args.iter()
638 .zip(param_names.iter())
639 .all(|(arg, expected)| matches!(arg, Expr::Ident(name) if name == *expected))
640}
641
642fn verify_case_calls_target(left: &Expr, fn_name: &str) -> bool {
643 match left {
644 Expr::FnCall(callee, args) => {
645 callee_is_target(callee, fn_name)
646 || verify_case_calls_target(callee, fn_name)
647 || args
648 .iter()
649 .any(|arg| verify_case_calls_target(arg, fn_name))
650 }
651 Expr::BinOp(_, left_expr, right_expr) => {
652 verify_case_calls_target(left_expr, fn_name)
653 || verify_case_calls_target(right_expr, fn_name)
654 }
655 Expr::Match { subject, arms, .. } => {
656 verify_case_calls_target(subject, fn_name)
657 || arms
658 .iter()
659 .any(|arm| verify_case_calls_target(&arm.body, fn_name))
660 }
661 Expr::Constructor(_, Some(inner)) => verify_case_calls_target(inner, fn_name),
662 Expr::ErrorProp(inner) => verify_case_calls_target(inner, fn_name),
663 Expr::List(elems) => elems
664 .iter()
665 .any(|elem| verify_case_calls_target(elem, fn_name)),
666 Expr::Tuple(items) => items
667 .iter()
668 .any(|item| verify_case_calls_target(item, fn_name)),
669 Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
670 verify_case_calls_target(k, fn_name) || verify_case_calls_target(v, fn_name)
671 }),
672 Expr::Attr(obj, _) => verify_case_calls_target(obj, fn_name),
673 Expr::RecordCreate { fields, .. } => fields
674 .iter()
675 .any(|(_, expr)| verify_case_calls_target(expr, fn_name)),
676 Expr::RecordUpdate { base, updates, .. } => {
677 verify_case_calls_target(base, fn_name)
678 || updates
679 .iter()
680 .any(|(_, expr)| verify_case_calls_target(expr, fn_name))
681 }
682 Expr::TailCall(boxed) => {
683 boxed.0 == fn_name
684 || boxed
685 .1
686 .iter()
687 .any(|arg| verify_case_calls_target(arg, fn_name))
688 }
689 Expr::Literal(_) | Expr::Ident(_) | Expr::InterpolatedStr(_) | Expr::Resolved(_) => false,
690 Expr::Constructor(_, None) => false,
691 }
692}
693
694fn callee_is_target(callee: &Expr, fn_name: &str) -> bool {
695 matches!(callee, Expr::Ident(name) if name == fn_name)
696}
697
698fn collect_used_effects_expr(expr: &Expr, fn_sigs: &FnSigMap, out: &mut BTreeSet<String>) {
699 match expr {
700 Expr::FnCall(callee, args) => {
701 if let Some(callee_name) = dotted_name(callee)
702 && let Some((_, _, effects)) = fn_sigs.get(&callee_name)
703 {
704 for effect in effects {
705 out.insert(effect.clone());
706 }
707 }
708 collect_used_effects_expr(callee, fn_sigs, out);
709 for arg in args {
710 collect_used_effects_expr(arg, fn_sigs, out);
711 }
712 }
713 Expr::TailCall(boxed) => {
714 let (target, args) = boxed.as_ref();
715 if let Some((_, _, effects)) = fn_sigs.get(target) {
716 for effect in effects {
717 out.insert(effect.clone());
718 }
719 }
720 for arg in args {
721 collect_used_effects_expr(arg, fn_sigs, out);
722 }
723 }
724 Expr::BinOp(_, left, right) => {
725 collect_used_effects_expr(left, fn_sigs, out);
726 collect_used_effects_expr(right, fn_sigs, out);
727 }
728 Expr::Match { subject, arms, .. } => {
729 collect_used_effects_expr(subject, fn_sigs, out);
730 for arm in arms {
731 collect_used_effects_expr(&arm.body, fn_sigs, out);
732 }
733 }
734 Expr::ErrorProp(inner) => collect_used_effects_expr(inner, fn_sigs, out),
735 Expr::List(items) | Expr::Tuple(items) => {
736 for item in items {
737 collect_used_effects_expr(item, fn_sigs, out);
738 }
739 }
740 Expr::MapLiteral(entries) => {
741 for (key, value) in entries {
742 collect_used_effects_expr(key, fn_sigs, out);
743 collect_used_effects_expr(value, fn_sigs, out);
744 }
745 }
746 Expr::Attr(obj, _) => collect_used_effects_expr(obj, fn_sigs, out),
747 Expr::RecordCreate { fields, .. } => {
748 for (_, expr) in fields {
749 collect_used_effects_expr(expr, fn_sigs, out);
750 }
751 }
752 Expr::RecordUpdate { base, updates, .. } => {
753 collect_used_effects_expr(base, fn_sigs, out);
754 for (_, expr) in updates {
755 collect_used_effects_expr(expr, fn_sigs, out);
756 }
757 }
758 Expr::Constructor(_, Some(inner)) => collect_used_effects_expr(inner, fn_sigs, out),
759 Expr::Literal(_)
760 | Expr::Ident(_)
761 | Expr::InterpolatedStr(_)
762 | Expr::Resolved(_)
763 | Expr::Constructor(_, None) => {}
764 }
765}
766
767fn collect_used_effects(f: &FnDef, fn_sigs: &FnSigMap) -> BTreeSet<String> {
768 let mut used = BTreeSet::new();
769 for stmt in f.body.stmts() {
770 match stmt {
771 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
772 collect_used_effects_expr(expr, fn_sigs, &mut used)
773 }
774 }
775 }
776 used
777}
778
779fn collect_declared_symbols(items: &[TopLevel]) -> std::collections::HashSet<String> {
780 let mut out = std::collections::HashSet::new();
781 for item in items {
782 match item {
783 TopLevel::FnDef(f) => {
784 out.insert(f.name.clone());
785 }
786 TopLevel::Module(m) => {
787 out.insert(m.name.clone());
788 }
789 TopLevel::TypeDef(t) => match t {
790 crate::ast::TypeDef::Sum { name, .. }
791 | crate::ast::TypeDef::Product { name, .. } => {
792 out.insert(name.clone());
793 }
794 },
795 TopLevel::Decision(d) => {
796 out.insert(d.name.clone());
797 }
798 TopLevel::EffectSet { .. } => {}
799 TopLevel::Verify(_) | TopLevel::Stmt(_) => {}
800 }
801 }
802 out
803}
804
805fn collect_known_effect_symbols(fn_sigs: Option<&FnSigMap>) -> std::collections::HashSet<String> {
806 let mut out = std::collections::HashSet::new();
807 for builtin in ["Console", "Http", "Disk", "Tcp", "HttpServer"] {
808 out.insert(builtin.to_string());
809 }
810 if let Some(sigs) = fn_sigs {
811 for (_, _, effects) in sigs.values() {
812 for effect in effects {
813 out.insert(effect.clone());
814 }
815 }
816 }
817 out
818}
819
820fn decision_symbol_known(
821 name: &str,
822 declared_symbols: &std::collections::HashSet<String>,
823 known_effect_symbols: &std::collections::HashSet<String>,
824) -> bool {
825 declared_symbols.contains(name) || known_effect_symbols.contains(name)
826}
827
828pub fn check_module_intent(items: &[TopLevel]) -> ModuleCheckFindings {
829 check_module_intent_with_sigs(items, None)
830}
831
832pub fn check_module_intent_with_sigs(
833 items: &[TopLevel],
834 fn_sigs: Option<&FnSigMap>,
835) -> ModuleCheckFindings {
836 check_module_intent_with_sigs_in(items, fn_sigs, None)
837}
838
839pub fn check_module_intent_with_sigs_in(
840 items: &[TopLevel],
841 fn_sigs: Option<&FnSigMap>,
842 source_file: Option<&str>,
843) -> ModuleCheckFindings {
844 let mut errors = Vec::new();
845 let mut warnings = Vec::new();
846 let declared_symbols = collect_declared_symbols(items);
847 let known_effect_symbols = collect_known_effect_symbols(fn_sigs);
848 let module_name = items.iter().find_map(|item| {
849 if let TopLevel::Module(m) = item {
850 Some(m.name.clone())
851 } else {
852 None
853 }
854 });
855
856 let mut verified_fns: std::collections::HashSet<&str> = std::collections::HashSet::new();
857 let mut spec_fns: std::collections::HashSet<String> = std::collections::HashSet::new();
858 let mut empty_verify_fns: std::collections::HashSet<&str> = std::collections::HashSet::new();
859 let mut invalid_verify_fns: std::collections::HashSet<&str> = std::collections::HashSet::new();
860 for item in items {
861 if let TopLevel::Verify(v) = item {
862 if v.cases.is_empty() {
863 errors.push(CheckFinding {
864 line: v.line,
865 module: module_name.clone(),
866 file: source_file.map(|s| s.to_string()),
867 message: format!(
868 "Verify block '{}' must contain at least one case",
869 v.fn_name
870 ),
871 });
872 empty_verify_fns.insert(v.fn_name.as_str());
873 } else {
874 let mut block_valid = true;
875 if matches!(v.kind, VerifyKind::Cases) {
876 for (idx, (left, _right)) in v.cases.iter().enumerate() {
877 if !verify_case_calls_target(left, &v.fn_name) {
878 errors.push(CheckFinding {
879 line: v.line,
880 module: module_name.clone(),
881 file: source_file.map(|s| s.to_string()),
882 message: format!(
883 "Verify block '{}' case #{} must call '{}' on the left side",
884 v.fn_name,
885 idx + 1,
886 v.fn_name
887 ),
888 });
889 block_valid = false;
890 }
891 }
892 for (idx, (_left, right)) in v.cases.iter().enumerate() {
893 if verify_case_calls_target(right, &v.fn_name) {
894 errors.push(CheckFinding {
895 line: v.line,
896 module: module_name.clone(),
897 file: source_file.map(|s| s.to_string()),
898 message: format!(
899 "Verify block '{}' case #{} must not call '{}' on the right side",
900 v.fn_name,
901 idx + 1,
902 v.fn_name
903 ),
904 });
905 block_valid = false;
906 }
907 }
908 }
909 if let VerifyKind::Law(law) = &v.kind
910 && let Some(sigs) = fn_sigs
911 && let Some(named_fn) = named_law_function(law, sigs)
912 {
913 if !named_fn.is_pure {
914 errors.push(CheckFinding {
915 line: v.line,
916 module: module_name.clone(),
917 file: source_file.map(|s| s.to_string()),
918 message: format!(
919 "Verify law '{}.{}' resolves to effectful function '{}'; spec functions must be pure",
920 v.fn_name, law.name, named_fn.name
921 ),
922 });
923 block_valid = false;
924 } else if let Some(spec_ref) = canonical_spec_ref(&v.fn_name, law, sigs) {
925 spec_fns.insert(spec_ref.spec_fn_name);
926 } else {
927 warnings.push(CheckFinding {
928 line: v.line,
929 module: module_name.clone(),
930 file: source_file.map(|s| s.to_string()),
931 message: format!(
932 "Verify law '{}.{}' names pure function '{}' but the law body never calls it; use '{}' in the assertion or rename the law",
933 v.fn_name, law.name, named_fn.name, named_fn.name
934 ),
935 });
936 }
937 }
938 if block_valid {
939 verified_fns.insert(v.fn_name.as_str());
940 } else {
941 invalid_verify_fns.insert(v.fn_name.as_str());
942 }
943 }
944 }
945 }
946
947 for item in items {
948 match item {
949 TopLevel::Module(m) => {
950 if m.intent.is_empty() {
951 warnings.push(CheckFinding {
952 line: m.line,
953 module: Some(m.name.clone()),
954 file: source_file.map(|s| s.to_string()),
955 message: format!("Module '{}' has no intent block", m.name),
956 });
957 }
958 }
959 TopLevel::FnDef(f) => {
960 if f.desc.is_none() && fn_needs_desc(f) {
961 warnings.push(CheckFinding {
962 line: f.line,
963 module: module_name.clone(),
964 file: source_file.map(|s| s.to_string()),
965 message: format!("Function '{}' has no description (?)", f.name),
966 });
967 }
968 if let Some(sigs) = fn_sigs
969 && let Some((_, _, declared_effects)) = sigs.get(&f.name)
970 && !declared_effects.is_empty()
971 {
972 let used_effects = collect_used_effects(f, sigs);
973 let unused_effects: Vec<String> = declared_effects
974 .iter()
975 .filter(|declared| {
976 !used_effects
977 .iter()
978 .any(|used| crate::effects::effect_satisfies(declared, used))
979 })
980 .cloned()
981 .collect();
982 if !unused_effects.is_empty() {
983 let used = if used_effects.is_empty() {
984 "none".to_string()
985 } else {
986 used_effects.into_iter().collect::<Vec<_>>().join(", ")
987 };
988 warnings.push(CheckFinding {
989 line: f.line,
990 module: module_name.clone(),
991 file: source_file.map(|s| s.to_string()),
992 message: format!(
993 "Function '{}' declares unused effect(s): {} (used: {})",
994 f.name,
995 unused_effects.join(", "),
996 used
997 ),
998 });
999 }
1000 }
1001 if fn_needs_verify(f)
1002 && !verified_fns.contains(f.name.as_str())
1003 && !spec_fns.contains(&f.name)
1004 && !empty_verify_fns.contains(f.name.as_str())
1005 && !invalid_verify_fns.contains(f.name.as_str())
1006 {
1007 errors.push(CheckFinding {
1008 line: f.line,
1009 module: module_name.clone(),
1010 file: source_file.map(|s| s.to_string()),
1011 message: format!("Function '{}' has no verify block", f.name),
1012 });
1013 }
1014 }
1015 TopLevel::Decision(d) => {
1016 if let DecisionImpact::Symbol(name) = &d.chosen
1017 && !decision_symbol_known(name, &declared_symbols, &known_effect_symbols)
1018 {
1019 errors.push(CheckFinding {
1020 line: d.line,
1021 module: module_name.clone(),
1022 file: source_file.map(|s| s.to_string()),
1023 message: format!(
1024 "Decision '{}' references unknown chosen symbol '{}'. Use quoted string for semantic chosen value.",
1025 d.name, name
1026 ),
1027 });
1028 }
1029 for rejected in &d.rejected {
1030 if let DecisionImpact::Symbol(name) = rejected
1031 && !decision_symbol_known(name, &declared_symbols, &known_effect_symbols)
1032 {
1033 errors.push(CheckFinding {
1034 line: d.line,
1035 module: module_name.clone(),
1036 file: source_file.map(|s| s.to_string()),
1037 message: format!(
1038 "Decision '{}' references unknown rejected symbol '{}'. Use quoted string for semantic rejected value.",
1039 d.name, name
1040 ),
1041 });
1042 }
1043 }
1044 for impact in &d.impacts {
1045 if let DecisionImpact::Symbol(name) = impact
1046 && !decision_symbol_known(name, &declared_symbols, &known_effect_symbols)
1047 {
1048 errors.push(CheckFinding {
1049 line: d.line,
1050 module: module_name.clone(),
1051 file: source_file.map(|s| s.to_string()),
1052 message: format!(
1053 "Decision '{}' references unknown impact symbol '{}'. Use quoted string for semantic impact.",
1054 d.name, name
1055 ),
1056 });
1057 }
1058 }
1059 }
1060 _ => {}
1061 }
1062 }
1063
1064 ModuleCheckFindings { errors, warnings }
1065}
1066
1067pub fn expr_to_str(expr: &crate::ast::Expr) -> String {
1068 use crate::ast::Expr;
1069 use crate::ast::Literal;
1070
1071 match expr {
1072 Expr::Literal(lit) => match lit {
1073 Literal::Int(i) => i.to_string(),
1074 Literal::Float(f) => f.to_string(),
1075 Literal::Str(s) => format!("\"{}\"", s),
1076 Literal::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1077 Literal::Unit => "Unit".to_string(),
1078 },
1079 Expr::Ident(name) => name.clone(),
1080 Expr::FnCall(fn_expr, args) => {
1081 let fn_str = expr_to_str(fn_expr);
1082 let args_str = args.iter().map(expr_to_str).collect::<Vec<_>>().join(", ");
1083 format!("{}({})", fn_str, args_str)
1084 }
1085 Expr::Constructor(name, arg) => match arg {
1086 None => name.clone(),
1087 Some(a) => format!("{}({})", name, expr_to_str(a)),
1088 },
1089 Expr::BinOp(op, left, right) => {
1090 use crate::ast::BinOp;
1091 let op_str = match op {
1092 BinOp::Add => "+",
1093 BinOp::Sub => "-",
1094 BinOp::Mul => "*",
1095 BinOp::Div => "/",
1096 BinOp::Eq => "==",
1097 BinOp::Neq => "!=",
1098 BinOp::Lt => "<",
1099 BinOp::Gt => ">",
1100 BinOp::Lte => "<=",
1101 BinOp::Gte => ">=",
1102 };
1103 format!("{} {} {}", expr_to_str(left), op_str, expr_to_str(right))
1104 }
1105 Expr::InterpolatedStr(parts) => {
1106 use crate::ast::StrPart;
1107 let mut inner = String::new();
1108 for part in parts {
1109 match part {
1110 StrPart::Literal(s) => inner.push_str(s),
1111 StrPart::Parsed(e) => {
1112 inner.push('{');
1113 inner.push_str(&expr_to_str(e));
1114 inner.push('}');
1115 }
1116 }
1117 }
1118 format!("\"{}\"", inner)
1119 }
1120 Expr::List(elements) => {
1121 let parts: Vec<String> = elements.iter().map(expr_to_str).collect();
1122 format!("[{}]", parts.join(", "))
1123 }
1124 Expr::Tuple(items) => {
1125 let parts: Vec<String> = items.iter().map(expr_to_str).collect();
1126 format!("({})", parts.join(", "))
1127 }
1128 Expr::MapLiteral(entries) => {
1129 let parts = entries
1130 .iter()
1131 .map(|(key, value)| format!("{} => {}", expr_to_str(key), expr_to_str(value)))
1132 .collect::<Vec<_>>();
1133 format!("{{{}}}", parts.join(", "))
1134 }
1135 Expr::ErrorProp(inner) => format!("{}?", expr_to_str(inner)),
1136 Expr::Attr(obj, field) => format!("{}.{}", expr_to_str(obj), field),
1137 Expr::RecordCreate { type_name, fields } => {
1138 let flds: Vec<String> = fields
1139 .iter()
1140 .map(|(name, expr)| format!("{} = {}", name, expr_to_str(expr)))
1141 .collect();
1142 format!("{}({})", type_name, flds.join(", "))
1143 }
1144 Expr::RecordUpdate {
1145 type_name,
1146 base,
1147 updates,
1148 } => {
1149 let upds: Vec<String> = updates
1150 .iter()
1151 .map(|(name, expr)| format!("{} = {}", name, expr_to_str(expr)))
1152 .collect();
1153 format!(
1154 "{}.update({}, {})",
1155 type_name,
1156 expr_to_str(base),
1157 upds.join(", ")
1158 )
1159 }
1160 Expr::TailCall(boxed) => {
1161 let (target, args) = boxed.as_ref();
1162 let a = args.iter().map(expr_to_str).collect::<Vec<_>>().join(", ");
1163 format!("<tail-call:{}>({})", target, a)
1164 }
1165 Expr::Resolved(_) => "<resolved>".to_string(),
1166 Expr::Match { subject, arms, .. } => {
1167 let s = expr_to_str(subject);
1168 let arms_str: Vec<String> = arms
1169 .iter()
1170 .map(|arm| format!("{:?} -> {}", arm.pattern, expr_to_str(&arm.body)))
1171 .collect();
1172 format!("match {} {}", s, arms_str.join(", "))
1173 }
1174 }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180 use crate::lexer::Lexer;
1181 use crate::parser::Parser;
1182
1183 fn parse_items(src: &str) -> Vec<TopLevel> {
1184 let mut lexer = Lexer::new(src);
1185 let tokens = lexer.tokenize().expect("lex failed");
1186 let mut parser = Parser::new(tokens);
1187 parser.parse().expect("parse failed")
1188 }
1189
1190 #[test]
1191 fn no_verify_warning_for_effectful_function() {
1192 let items = parse_items(
1193 r#"
1194fn log(x: Int) -> Unit
1195 ! [Console]
1196 Console.print(x)
1197"#,
1198 );
1199 let findings = check_module_intent(&items);
1200 assert!(
1201 !findings
1202 .warnings
1203 .iter()
1204 .any(|w| w.message.contains("no verify block"))
1205 && !findings
1206 .errors
1207 .iter()
1208 .any(|e| e.message.contains("no verify block")),
1209 "unexpected findings: errors={:?}, warnings={:?}",
1210 findings.errors,
1211 findings.warnings
1212 );
1213 }
1214
1215 #[test]
1216 fn warns_on_unused_declared_effects() {
1217 let items = parse_items(
1218 r#"
1219fn log(x: Int) -> Unit
1220 ! [Console.print, Http.get]
1221 Console.print(x)
1222"#,
1223 );
1224 let tc = crate::types::checker::run_type_check_full(&items, None);
1225 assert!(
1226 tc.errors.is_empty(),
1227 "unexpected type errors: {:?}",
1228 tc.errors
1229 );
1230 let findings = check_module_intent_with_sigs(&items, Some(&tc.fn_sigs));
1231 assert!(
1232 findings.warnings.iter().any(|w| {
1233 w.message.contains("declares unused effect(s)")
1234 && w.message.contains("Http")
1235 && w.message.contains("used: Console.print")
1236 }),
1237 "expected unused-effect warning, got errors={:?}, warnings={:?}",
1238 findings.errors,
1239 findings.warnings
1240 );
1241 }
1242
1243 #[test]
1244 fn no_unused_effect_warning_when_declared_effects_are_minimal() {
1245 let items = parse_items(
1246 r#"
1247fn log(x: Int) -> Unit
1248 ! [Console.print]
1249 Console.print(x)
1250"#,
1251 );
1252 let tc = crate::types::checker::run_type_check_full(&items, None);
1253 assert!(
1254 tc.errors.is_empty(),
1255 "unexpected type errors: {:?}",
1256 tc.errors
1257 );
1258 let findings = check_module_intent_with_sigs(&items, Some(&tc.fn_sigs));
1259 assert!(
1260 !findings
1261 .warnings
1262 .iter()
1263 .any(|w| w.message.contains("declares unused effect(s)")),
1264 "did not expect unused-effect warning, got errors={:?}, warnings={:?}",
1265 findings.errors,
1266 findings.warnings
1267 );
1268 assert!(
1269 !findings
1270 .warnings
1271 .iter()
1272 .any(|w| w.message.contains("declares broad effect")),
1273 "did not expect broad-effect warning, got errors={:?}, warnings={:?}",
1274 findings.errors,
1275 findings.warnings
1276 );
1277 }
1278
1279 #[test]
1280 fn no_verify_warning_for_trivial_passthrough_wrapper() {
1281 let items = parse_items(
1282 r#"
1283fn passthrough(x: Int) -> Int
1284 inner(x)
1285"#,
1286 );
1287 let findings = check_module_intent(&items);
1288 assert!(
1289 !findings
1290 .warnings
1291 .iter()
1292 .any(|w| w.message.contains("no verify block"))
1293 && !findings
1294 .errors
1295 .iter()
1296 .any(|e| e.message.contains("no verify block")),
1297 "unexpected findings: errors={:?}, warnings={:?}",
1298 findings.errors,
1299 findings.warnings
1300 );
1301 }
1302
1303 #[test]
1304 fn verify_error_for_pure_non_trivial_logic() {
1305 let items = parse_items(
1306 r#"
1307fn add1(x: Int) -> Int
1308 x + 1
1309"#,
1310 );
1311 let findings = check_module_intent(&items);
1312 assert!(
1313 findings
1314 .errors
1315 .iter()
1316 .any(|e| e.message == "Function 'add1' has no verify block"),
1317 "expected verify error, got errors={:?}, warnings={:?}",
1318 findings.errors,
1319 findings.warnings
1320 );
1321 }
1322
1323 #[test]
1324 fn empty_verify_block_is_rejected() {
1325 let items = parse_items(
1326 r#"
1327fn add1(x: Int) -> Int
1328 x + 1
1329
1330verify add1
1331"#,
1332 );
1333 let findings = check_module_intent(&items);
1334 assert!(
1335 findings
1336 .errors
1337 .iter()
1338 .any(|e| e.message == "Verify block 'add1' must contain at least one case"),
1339 "expected empty verify error, got errors={:?}, warnings={:?}",
1340 findings.errors,
1341 findings.warnings
1342 );
1343 assert!(
1344 !findings
1345 .errors
1346 .iter()
1347 .any(|e| e.message == "Function 'add1' has no verify block"),
1348 "expected no duplicate missing-verify error, got errors={:?}, warnings={:?}",
1349 findings.errors,
1350 findings.warnings
1351 );
1352 }
1353
1354 #[test]
1355 fn verify_case_must_call_verified_function_on_left_side() {
1356 let items = parse_items(
1357 r#"
1358fn add1(x: Int) -> Int
1359 x + 1
1360
1361verify add1
1362 true => true
1363"#,
1364 );
1365 let findings = check_module_intent(&items);
1366 assert!(
1367 findings.errors.iter().any(|e| {
1368 e.message
1369 .contains("Verify block 'add1' case #1 must call 'add1' on the left side")
1370 }),
1371 "expected verify-case-call error, got errors={:?}, warnings={:?}",
1372 findings.errors,
1373 findings.warnings
1374 );
1375 assert!(
1376 !findings
1377 .errors
1378 .iter()
1379 .any(|e| e.message == "Function 'add1' has no verify block"),
1380 "expected no duplicate missing-verify error, got errors={:?}, warnings={:?}",
1381 findings.errors,
1382 findings.warnings
1383 );
1384 }
1385
1386 #[test]
1387 fn verify_case_must_not_call_verified_function_on_right_side() {
1388 let items = parse_items(
1389 r#"
1390fn add1(x: Int) -> Int
1391 x + 1
1392
1393verify add1
1394 add1(1) => add1(1)
1395"#,
1396 );
1397 let findings = check_module_intent(&items);
1398 assert!(
1399 findings.errors.iter().any(|e| {
1400 e.message
1401 .contains("Verify block 'add1' case #1 must not call 'add1' on the right side")
1402 }),
1403 "expected verify-case-rhs error, got errors={:?}, warnings={:?}",
1404 findings.errors,
1405 findings.warnings
1406 );
1407 }
1408
1409 #[test]
1410 fn verify_law_skips_left_right_call_heuristics() {
1411 let items = parse_items(
1412 r#"
1413fn add1(x: Int) -> Int
1414 x + 1
1415
1416verify add1 law reflexive
1417 given x: Int = [1, 2, 3]
1418 x => x
1419"#,
1420 );
1421 let findings = check_module_intent(&items);
1422 assert!(
1423 !findings
1424 .errors
1425 .iter()
1426 .any(|e| e.message.contains("must call 'add1' on the left side")),
1427 "did not expect lhs-call heuristic for law verify, got errors={:?}",
1428 findings.errors
1429 );
1430 assert!(
1431 !findings
1432 .errors
1433 .iter()
1434 .any(|e| e.message.contains("must not call 'add1' on the right side")),
1435 "did not expect rhs-call heuristic for law verify, got errors={:?}",
1436 findings.errors
1437 );
1438 assert!(
1439 !findings
1440 .errors
1441 .iter()
1442 .any(|e| e.message == "Function 'add1' has no verify block"),
1443 "law verify should satisfy verify requirement, got errors={:?}",
1444 findings.errors
1445 );
1446 }
1447
1448 #[test]
1449 fn verify_law_when_must_have_bool_type() {
1450 let items = parse_items(
1451 r#"
1452fn add1(x: Int) -> Int
1453 x + 1
1454
1455verify add1 law ordered
1456 given x: Int = [1, 2]
1457 when add1(x)
1458 x => x
1459"#,
1460 );
1461 let tc = crate::types::checker::run_type_check_full(&items, None);
1462 assert!(
1463 tc.errors
1464 .iter()
1465 .any(|e| e.message.contains("when condition must have type Bool")),
1466 "expected Bool type error for when, got errors={:?}",
1467 tc.errors
1468 );
1469 }
1470
1471 #[test]
1472 fn verify_law_when_must_be_pure() {
1473 let items = parse_items(
1474 r#"
1475fn add1(x: Int) -> Int
1476 x + 1
1477
1478fn noisyPositive(x: Int) -> Bool
1479 ! [Console.print]
1480 Console.print("{x}")
1481 x > 0
1482
1483verify add1 law ordered
1484 given x: Int = [1, 2]
1485 when noisyPositive(x)
1486 x => x
1487"#,
1488 );
1489 let tc = crate::types::checker::run_type_check_full(&items, None);
1490 assert!(
1491 tc.errors.iter().any(|e| e.message.contains(
1492 "Function '<verify:add1>' calls 'noisyPositive' which has effect 'Console.print'"
1493 )),
1494 "expected purity error for when, got errors={:?}",
1495 tc.errors
1496 );
1497 }
1498
1499 #[test]
1500 fn verify_law_named_effectful_function_is_an_error() {
1501 let items = parse_items(
1502 r#"
1503fn add1(x: Int) -> Int
1504 x + 1
1505
1506fn specFn(x: Int) -> Int
1507 ! [Console.print]
1508 Console.print("{x}")
1509 x
1510
1511verify add1 law specFn
1512 given x: Int = [1, 2]
1513 add1(x) => add1(x)
1514"#,
1515 );
1516 let tc = crate::types::checker::run_type_check_full(&items, None);
1517 let findings = check_module_intent_with_sigs(&items, Some(&tc.fn_sigs));
1518 assert!(
1519 findings.errors.iter().any(|e| e.message.contains(
1520 "Verify law 'add1.specFn' resolves to effectful function 'specFn'; spec functions must be pure"
1521 )),
1522 "expected effectful-spec error, got errors={:?}, warnings={:?}",
1523 findings.errors,
1524 findings.warnings
1525 );
1526 }
1527
1528 #[test]
1529 fn verify_law_named_pure_function_must_appear_in_law_body() {
1530 let items = parse_items(
1531 r#"
1532fn add1(x: Int) -> Int
1533 x + 1
1534
1535fn add1Spec(x: Int) -> Int
1536 x + 1
1537
1538verify add1 law add1Spec
1539 given x: Int = [1, 2]
1540 add1(x) => x + 1
1541"#,
1542 );
1543 let tc = crate::types::checker::run_type_check_full(&items, None);
1544 let findings = check_module_intent_with_sigs(&items, Some(&tc.fn_sigs));
1545 assert!(
1546 findings.warnings.iter().any(|w| w.message.contains(
1547 "Verify law 'add1.add1Spec' names pure function 'add1Spec' but the law body never calls it"
1548 )),
1549 "expected unused-spec warning, got errors={:?}, warnings={:?}",
1550 findings.errors,
1551 findings.warnings
1552 );
1553 }
1554
1555 #[test]
1556 fn canonical_spec_function_does_not_need_its_own_verify_block() {
1557 let items = parse_items(
1558 r#"
1559fn add1(x: Int) -> Int
1560 x + 1
1561
1562fn add1Spec(x: Int) -> Int
1563 x + 1
1564
1565verify add1 law add1Spec
1566 given x: Int = [1, 2]
1567 add1(x) => add1Spec(x)
1568"#,
1569 );
1570 let tc = crate::types::checker::run_type_check_full(&items, None);
1571 let findings = check_module_intent_with_sigs(&items, Some(&tc.fn_sigs));
1572 assert!(
1573 !findings
1574 .errors
1575 .iter()
1576 .any(|e| e.message == "Function 'add1Spec' has no verify block"),
1577 "spec function should not need its own verify block, got errors={:?}, warnings={:?}",
1578 findings.errors,
1579 findings.warnings
1580 );
1581 }
1582
1583 #[test]
1584 fn coverage_warns_when_result_verify_has_no_err_example() {
1585 let items = parse_items(
1586 r#"
1587fn mayFail(n: Int) -> Result<Int, String>
1588 match n
1589 0 -> Result.Err("zero")
1590 _ -> Result.Ok(n)
1591
1592verify mayFail
1593 mayFail(1) => Result.Ok(1)
1594"#,
1595 );
1596 let warnings = collect_verify_coverage_warnings(&items);
1597 assert!(
1598 warnings.iter().any(|w| {
1599 w.message == "verify examples for mayFail do not include any Result.Err case"
1600 }),
1601 "expected missing-err warning, got {:?}",
1602 warnings
1603 );
1604 }
1605
1606 #[test]
1607 fn coverage_warns_when_option_verify_has_no_none_example() {
1608 let items = parse_items(
1609 r#"
1610fn maybe(n: Int) -> Option<Int>
1611 match n
1612 0 -> Option.None
1613 _ -> Option.Some(n)
1614
1615verify maybe
1616 maybe(1) => Option.Some(1)
1617"#,
1618 );
1619 let warnings = collect_verify_coverage_warnings(&items);
1620 assert!(
1621 warnings.iter().any(|w| {
1622 w.message == "verify examples for maybe do not include any Option.None case"
1623 }),
1624 "expected missing-none warning, got {:?}",
1625 warnings
1626 );
1627 }
1628
1629 #[test]
1630 fn coverage_warns_when_enum_match_examples_cover_subset_of_constructors() {
1631 let items = parse_items(
1632 r#"
1633type Input
1634 Help
1635 Exit
1636 Echo(String)
1637
1638fn dispatch(input: Input) -> String
1639 match input
1640 Input.Help -> "help"
1641 Input.Exit -> "exit"
1642 Input.Echo(value) -> value
1643
1644verify dispatch
1645 dispatch(Input.Help) => "help"
1646"#,
1647 );
1648 let warnings = collect_verify_coverage_warnings(&items);
1649 assert!(
1650 warnings
1651 .iter()
1652 .any(|w| w.message == "verify examples for dispatch cover 1/3 enum constructors"),
1653 "expected enum-coverage warning, got {:?}",
1654 warnings
1655 );
1656 }
1657
1658 #[test]
1659 fn law_dependency_warning_points_at_missing_helper_laws_in_json() {
1660 let items = parse_items(include_str!("../examples/data/json.av"));
1661 let tc = crate::types::checker::run_type_check_full(&items, None);
1662 assert!(
1663 tc.errors.is_empty(),
1664 "expected json example to type-check, got {:?}",
1665 tc.errors
1666 );
1667
1668 let warnings = collect_verify_law_dependency_warnings(&items, &tc.fn_sigs);
1669 assert!(
1670 warnings.is_empty(),
1671 "expected json helper-law ladder to be complete, got {:?}",
1672 warnings
1673 );
1674 }
1675
1676 #[test]
1677 fn coverage_does_not_warn_when_enum_and_output_examples_are_present() {
1678 let items = parse_items(
1679 r#"
1680type Input
1681 Help
1682 Exit
1683
1684fn run(input: Input) -> Result<String, String>
1685 match input
1686 Input.Help -> Result.Ok("help")
1687 Input.Exit -> Result.Err("exit")
1688
1689verify run
1690 run(Input.Help) => Result.Ok("help")
1691 run(Input.Exit) => Result.Err("exit")
1692"#,
1693 );
1694 let warnings = collect_verify_coverage_warnings(&items);
1695 assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
1696 }
1697
1698 #[test]
1699 fn merge_verify_blocks_coalesces_cases_by_function() {
1700 let items = parse_items(
1701 r#"
1702fn f(x: Int) -> Int
1703 x
1704
1705verify f
1706 f(1) => 1
1707
1708verify f
1709 f(2) => 2
1710"#,
1711 );
1712 let merged = merge_verify_blocks(&items);
1713 assert_eq!(merged.len(), 1);
1714 assert_eq!(merged[0].fn_name, "f");
1715 assert_eq!(merged[0].cases.len(), 2);
1716 }
1717
1718 #[test]
1719 fn merge_verify_blocks_keeps_law_blocks_separate() {
1720 let items = parse_items(
1721 r#"
1722fn f(x: Int) -> Int
1723 x
1724
1725verify f
1726 f(1) => 1
1727
1728verify f law l1
1729 given x: Int = [1]
1730 x => x
1731
1732verify f law l2
1733 given x: Int = [2]
1734 x => x
1735
1736verify f
1737 f(2) => 2
1738"#,
1739 );
1740 let merged = merge_verify_blocks(&items);
1741 assert_eq!(merged.len(), 3);
1742 assert!(matches!(merged[0].kind, VerifyKind::Cases));
1743 assert_eq!(merged[0].cases.len(), 2);
1744 assert!(matches!(merged[1].kind, VerifyKind::Law(_)));
1745 assert!(matches!(merged[2].kind, VerifyKind::Law(_)));
1746 }
1747
1748 #[test]
1749 fn decision_unknown_symbol_impact_is_error() {
1750 let items = parse_items(
1751 r#"
1752module M
1753 intent =
1754 "x"
1755
1756fn existing() -> Int
1757 1
1758
1759verify existing
1760 existing() => 1
1761
1762decision D
1763 date = "2026-03-05"
1764 reason =
1765 "x"
1766 chosen = "ExistingChoice"
1767 rejected = []
1768 impacts = [existing, missingThing]
1769"#,
1770 );
1771 let findings = check_module_intent(&items);
1772 assert!(
1773 findings.errors.iter().any(|e| e
1774 .message
1775 .contains("Decision 'D' references unknown impact symbol 'missingThing'")),
1776 "expected unknown-impact error, got errors={:?}, warnings={:?}",
1777 findings.errors,
1778 findings.warnings
1779 );
1780 }
1781
1782 #[test]
1783 fn decision_semantic_string_impact_is_allowed() {
1784 let items = parse_items(
1785 r#"
1786module M
1787 intent =
1788 "x"
1789
1790fn existing() -> Int
1791 1
1792
1793verify existing
1794 existing() => 1
1795
1796decision D
1797 date = "2026-03-05"
1798 reason =
1799 "x"
1800 chosen = "ExistingChoice"
1801 rejected = []
1802 impacts = [existing, "error handling strategy"]
1803"#,
1804 );
1805 let findings = check_module_intent(&items);
1806 assert!(
1807 !findings
1808 .errors
1809 .iter()
1810 .any(|e| e.message.contains("references unknown impact symbol")),
1811 "did not expect unknown-impact error, got errors={:?}, warnings={:?}",
1812 findings.errors,
1813 findings.warnings
1814 );
1815 }
1816
1817 #[test]
1818 fn decision_unknown_chosen_symbol_is_error() {
1819 let items = parse_items(
1820 r#"
1821module M
1822 intent =
1823 "x"
1824
1825fn existing() -> Int
1826 1
1827
1828verify existing
1829 existing() => 1
1830
1831decision D
1832 date = "2026-03-05"
1833 reason =
1834 "x"
1835 chosen = MissingChoice
1836 rejected = []
1837 impacts = [existing]
1838"#,
1839 );
1840 let findings = check_module_intent(&items);
1841 assert!(
1842 findings
1843 .errors
1844 .iter()
1845 .any(|e| e.message.contains("unknown chosen symbol 'MissingChoice'")),
1846 "expected unknown-chosen error, got errors={:?}, warnings={:?}",
1847 findings.errors,
1848 findings.warnings
1849 );
1850 }
1851
1852 #[test]
1853 fn decision_unknown_rejected_symbol_is_error() {
1854 let items = parse_items(
1855 r#"
1856module M
1857 intent =
1858 "x"
1859
1860fn existing() -> Int
1861 1
1862
1863verify existing
1864 existing() => 1
1865
1866decision D
1867 date = "2026-03-05"
1868 reason =
1869 "x"
1870 chosen = "Keep"
1871 rejected = [MissingAlternative]
1872 impacts = [existing]
1873"#,
1874 );
1875 let findings = check_module_intent(&items);
1876 assert!(
1877 findings.errors.iter().any(|e| e
1878 .message
1879 .contains("unknown rejected symbol 'MissingAlternative'")),
1880 "expected unknown-rejected error, got errors={:?}, warnings={:?}",
1881 findings.errors,
1882 findings.warnings
1883 );
1884 }
1885
1886 #[test]
1887 fn decision_semantic_string_chosen_and_rejected_are_allowed() {
1888 let items = parse_items(
1889 r#"
1890module M
1891 intent =
1892 "x"
1893
1894fn existing() -> Int
1895 1
1896
1897verify existing
1898 existing() => 1
1899
1900decision D
1901 date = "2026-03-05"
1902 reason =
1903 "x"
1904 chosen = "Keep explicit context"
1905 rejected = ["Closure capture", "Global mutable state"]
1906 impacts = [existing]
1907"#,
1908 );
1909 let findings = check_module_intent(&items);
1910 assert!(
1911 !findings
1912 .errors
1913 .iter()
1914 .any(|e| e.message.contains("unknown chosen symbol")
1915 || e.message.contains("unknown rejected symbol")),
1916 "did not expect chosen/rejected symbol errors, got errors={:?}, warnings={:?}",
1917 findings.errors,
1918 findings.warnings
1919 );
1920 }
1921
1922 #[test]
1923 fn decision_builtin_effect_impact_is_allowed() {
1924 let items = parse_items(
1925 r#"
1926module M
1927 intent =
1928 "x"
1929
1930fn existing() -> Int
1931 1
1932
1933verify existing
1934 existing() => 1
1935
1936decision D
1937 date = "2026-03-05"
1938 reason =
1939 "x"
1940 chosen = "ExistingChoice"
1941 rejected = []
1942 impacts = [existing, Tcp]
1943"#,
1944 );
1945 let tc = crate::types::checker::run_type_check_full(&items, None);
1946 let findings = check_module_intent_with_sigs(&items, Some(&tc.fn_sigs));
1947 assert!(
1948 !findings
1949 .errors
1950 .iter()
1951 .any(|e| e.message.contains("references unknown impact symbol 'Tcp'")),
1952 "did not expect Tcp impact error, got errors={:?}, warnings={:?}",
1953 findings.errors,
1954 findings.warnings
1955 );
1956 }
1957
1958 #[test]
1959 fn decision_removed_effect_alias_impact_is_error() {
1960 let items = parse_items(
1961 r#"
1962module M
1963 intent =
1964 "x"
1965
1966fn existing() -> Int
1967 1
1968
1969verify existing
1970 existing() => 1
1971
1972decision D
1973 date = "2026-03-05"
1974 reason =
1975 "x"
1976 chosen = "ExistingChoice"
1977 rejected = []
1978 impacts = [existing, AppIO]
1979"#,
1980 );
1981 let findings = check_module_intent(&items);
1982 assert!(
1983 findings.errors.iter().any(|e| e
1984 .message
1985 .contains("references unknown impact symbol 'AppIO'")),
1986 "expected AppIO impact error, got errors={:?}, warnings={:?}",
1987 findings.errors,
1988 findings.warnings
1989 );
1990 }
1991}