1use colored::Colorize;
2
3use crate::ast::{DecisionBlock, Expr, FnBody, FnDef, Stmt, TopLevel, VerifyBlock, VerifyKind};
4use crate::interpreter::{Interpreter, aver_repr};
5use crate::types::{Type, parse_type_str_strict};
6use crate::value::{RuntimeError, Value};
7
8pub struct VerifyResult {
9 #[allow(dead_code)]
10 pub fn_name: String,
11 pub passed: usize,
12 pub failed: usize,
13 #[allow(dead_code)]
14 pub failures: Vec<(String, String, String)>, }
16
17pub struct ModuleCheckFindings {
18 pub errors: Vec<String>,
19 pub warnings: Vec<String>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23enum VerifyOutputShape {
24 BoolTrue,
25 BoolFalse,
26 Some,
27 None,
28 Ok,
29 Err,
30 Variant(String),
31}
32
33impl VerifyOutputShape {
34 fn display(&self) -> String {
35 match self {
36 VerifyOutputShape::BoolTrue => "true".to_string(),
37 VerifyOutputShape::BoolFalse => "false".to_string(),
38 VerifyOutputShape::Some => "Option.Some".to_string(),
39 VerifyOutputShape::None => "Option.None".to_string(),
40 VerifyOutputShape::Ok => "Result.Ok".to_string(),
41 VerifyOutputShape::Err => "Result.Err".to_string(),
42 VerifyOutputShape::Variant(name) => name.clone(),
43 }
44 }
45}
46
47struct VerifyShapeContract {
48 return_type: Type,
49 expected: Vec<VerifyOutputShape>,
50 seen: std::collections::HashSet<VerifyOutputShape>,
51}
52
53impl VerifyShapeContract {
54 fn observe(&mut self, value: &Value) {
55 if let Some(shape) = observed_output_shape_for_type(&self.return_type, value) {
56 self.seen.insert(shape);
57 }
58 }
59
60 fn observe_shape(&mut self, shape: VerifyOutputShape) {
61 self.seen.insert(shape);
62 }
63
64 fn missing(&self) -> Vec<VerifyOutputShape> {
65 self.expected
66 .iter()
67 .filter(|shape| !self.seen.contains(*shape))
68 .cloned()
69 .collect()
70 }
71}
72
73fn build_verify_shape_contract(
74 block: &VerifyBlock,
75 interp: &Interpreter,
76) -> Option<VerifyShapeContract> {
77 let fn_val = interp.lookup(&block.fn_name).ok()?;
78 let Value::Fn {
79 return_type, body, ..
80 } = fn_val
81 else {
82 return None;
83 };
84 let ret_ty = parse_type_str_strict(&return_type).ok()?;
85
86 let all_shapes = expected_output_shapes_for_type(&ret_ty, interp)?;
87 let mut declared_shapes = std::collections::HashSet::new();
88 collect_declared_output_shapes_from_body(body.as_ref(), &ret_ty, &mut declared_shapes);
89 let expected: Vec<VerifyOutputShape> = all_shapes
90 .into_iter()
91 .filter(|shape| declared_shapes.contains(shape))
92 .collect();
93 if expected.len() < 2 {
94 return None;
95 }
96
97 Some(VerifyShapeContract {
98 return_type: ret_ty,
99 expected,
100 seen: std::collections::HashSet::new(),
101 })
102}
103
104fn expected_output_shapes_for_type(
105 ty: &Type,
106 interp: &Interpreter,
107) -> Option<Vec<VerifyOutputShape>> {
108 match ty {
109 Type::Bool => Some(vec![
110 VerifyOutputShape::BoolTrue,
111 VerifyOutputShape::BoolFalse,
112 ]),
113 Type::Option(_) => Some(vec![VerifyOutputShape::Some, VerifyOutputShape::None]),
114 Type::Result(_, _) => Some(vec![VerifyOutputShape::Ok, VerifyOutputShape::Err]),
115 Type::Named(type_name) => {
116 let ns = interp.lookup(type_name).ok()?;
117 let Value::Namespace { members, .. } = ns else {
118 return None;
119 };
120
121 let ctor_prefix = format!("__ctor:{}:", type_name);
122 let mut variants = std::collections::BTreeSet::new();
123
124 for (member_name, member_value) in members {
125 match member_value {
126 Value::Variant { type_name: t, .. } if t == type_name.as_str() => {
127 variants.insert(member_name);
128 }
129 Value::Builtin(builtin_name) if builtin_name.starts_with(&ctor_prefix) => {
130 variants.insert(member_name);
131 }
132 _ => {}
133 }
134 }
135
136 if variants.is_empty() {
137 return None;
138 }
139
140 Some(
141 variants
142 .into_iter()
143 .map(VerifyOutputShape::Variant)
144 .collect(),
145 )
146 }
147 _ => None,
148 }
149}
150
151fn observed_output_shape_for_type(ty: &Type, value: &Value) -> Option<VerifyOutputShape> {
152 match ty {
153 Type::Bool => match value {
154 Value::Bool(true) => Some(VerifyOutputShape::BoolTrue),
155 Value::Bool(false) => Some(VerifyOutputShape::BoolFalse),
156 _ => None,
157 },
158 Type::Option(_) => match value {
159 Value::Some(_) => Some(VerifyOutputShape::Some),
160 Value::None => Some(VerifyOutputShape::None),
161 _ => None,
162 },
163 Type::Result(_, _) => match value {
164 Value::Ok(_) => Some(VerifyOutputShape::Ok),
165 Value::Err(_) => Some(VerifyOutputShape::Err),
166 _ => None,
167 },
168 Type::Named(type_name) => match value {
169 Value::Variant {
170 type_name: actual_type,
171 variant,
172 ..
173 } if actual_type == type_name => Some(VerifyOutputShape::Variant(variant.clone())),
174 _ => None,
175 },
176 _ => None,
177 }
178}
179
180fn collect_declared_output_shapes_from_body(
181 body: &FnBody,
182 ret_ty: &Type,
183 out: &mut std::collections::HashSet<VerifyOutputShape>,
184) {
185 match body {
186 FnBody::Expr(expr) => collect_declared_output_shapes_from_tail_expr(expr, ret_ty, out),
187 FnBody::Block(stmts) => {
188 if let Some(last) = stmts.last() {
189 match last {
190 Stmt::Expr(expr) => {
191 collect_declared_output_shapes_from_tail_expr(expr, ret_ty, out)
192 }
193 Stmt::Binding(_, _, _) => {}
194 }
195 }
196 }
197 }
198}
199
200fn collect_declared_output_shapes_from_tail_expr(
201 expr: &Expr,
202 ret_ty: &Type,
203 out: &mut std::collections::HashSet<VerifyOutputShape>,
204) {
205 match expr {
206 Expr::Match { arms, .. } => {
207 for arm in arms {
208 collect_declared_output_shapes_from_tail_expr(&arm.body, ret_ty, out);
209 }
210 }
211 _ => {
212 if let Some(shape) = declared_output_shape_from_expr(ret_ty, expr) {
213 out.insert(shape);
214 }
215 }
216 }
217}
218
219fn declared_output_shape_from_expr(ret_ty: &Type, expr: &Expr) -> Option<VerifyOutputShape> {
220 match ret_ty {
221 Type::Bool => match expr {
222 Expr::Literal(crate::ast::Literal::Bool(true)) => Some(VerifyOutputShape::BoolTrue),
223 Expr::Literal(crate::ast::Literal::Bool(false)) => Some(VerifyOutputShape::BoolFalse),
224 _ => None,
225 },
226 Type::Option(_) => match expr {
227 Expr::FnCall(callee, _) => match dotted_name(callee) {
228 Some(path) if path == "Option.Some" => Some(VerifyOutputShape::Some),
229 _ => None,
230 },
231 _ => match dotted_name(expr) {
232 Some(path) if path == "Option.None" => Some(VerifyOutputShape::None),
233 _ => None,
234 },
235 },
236 Type::Result(_, _) => match expr {
237 Expr::FnCall(callee, _) => match dotted_name(callee) {
238 Some(path) if path == "Result.Ok" => Some(VerifyOutputShape::Ok),
239 Some(path) if path == "Result.Err" => Some(VerifyOutputShape::Err),
240 _ => None,
241 },
242 _ => None,
243 },
244 Type::Named(type_name) => {
245 let prefix = format!("{}.", type_name);
246 match expr {
247 Expr::Attr(_, _) => {
248 let path = dotted_name(expr)?;
249 let variant = path.strip_prefix(&prefix)?;
250 if variant.is_empty() {
251 None
252 } else {
253 Some(VerifyOutputShape::Variant(variant.to_string()))
254 }
255 }
256 Expr::FnCall(callee, _) => {
257 let path = dotted_name(callee)?;
258 let variant = path.strip_prefix(&prefix)?;
259 if variant.is_empty() {
260 None
261 } else {
262 Some(VerifyOutputShape::Variant(variant.to_string()))
263 }
264 }
265 _ => None,
266 }
267 }
268 _ => None,
269 }
270}
271
272fn dotted_name(expr: &Expr) -> Option<String> {
273 match expr {
274 Expr::Ident(name) => Some(name.clone()),
275 Expr::Attr(base, field) => {
276 let mut prefix = dotted_name(base)?;
277 prefix.push('.');
278 prefix.push_str(field);
279 Some(prefix)
280 }
281 _ => None,
282 }
283}
284
285fn verify_case_uses_error_prop_on_target(expr: &Expr, fn_name: &str) -> bool {
286 match expr {
287 Expr::ErrorProp(inner) => {
288 verify_case_calls_target(inner, fn_name)
289 || verify_case_uses_error_prop_on_target(inner, fn_name)
290 }
291 Expr::FnCall(callee, args) => {
292 verify_case_uses_error_prop_on_target(callee, fn_name)
293 || args
294 .iter()
295 .any(|arg| verify_case_uses_error_prop_on_target(arg, fn_name))
296 }
297 Expr::Pipe(left, right) | Expr::BinOp(_, left, right) => {
298 verify_case_uses_error_prop_on_target(left, fn_name)
299 || verify_case_uses_error_prop_on_target(right, fn_name)
300 }
301 Expr::Match { subject, arms, .. } => {
302 verify_case_uses_error_prop_on_target(subject, fn_name)
303 || arms
304 .iter()
305 .any(|arm| verify_case_uses_error_prop_on_target(&arm.body, fn_name))
306 }
307 Expr::Constructor(_, Some(inner)) => verify_case_uses_error_prop_on_target(inner, fn_name),
308 Expr::List(elems) => elems
309 .iter()
310 .any(|elem| verify_case_uses_error_prop_on_target(elem, fn_name)),
311 Expr::Tuple(items) => items
312 .iter()
313 .any(|item| verify_case_uses_error_prop_on_target(item, fn_name)),
314 Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
315 verify_case_uses_error_prop_on_target(k, fn_name)
316 || verify_case_uses_error_prop_on_target(v, fn_name)
317 }),
318 Expr::Attr(obj, _) => verify_case_uses_error_prop_on_target(obj, fn_name),
319 Expr::RecordCreate { fields, .. } => fields
320 .iter()
321 .any(|(_, expr)| verify_case_uses_error_prop_on_target(expr, fn_name)),
322 Expr::RecordUpdate { base, updates, .. } => {
323 verify_case_uses_error_prop_on_target(base, fn_name)
324 || updates
325 .iter()
326 .any(|(_, expr)| verify_case_uses_error_prop_on_target(expr, fn_name))
327 }
328 Expr::TailCall(boxed) => {
329 boxed.0 == fn_name
330 || boxed
331 .1
332 .iter()
333 .any(|arg| verify_case_uses_error_prop_on_target(arg, fn_name))
334 }
335 Expr::Literal(_)
336 | Expr::Ident(_)
337 | Expr::InterpolatedStr(_)
338 | Expr::Resolved(_)
339 | Expr::Constructor(_, None) => false,
340 }
341}
342
343pub fn run_verify(block: &VerifyBlock, interp: &mut Interpreter) -> VerifyResult {
344 let mut passed = 0;
345 let mut failed = 0;
346 let mut failures = Vec::new();
347 let is_law = matches!(block.kind, VerifyKind::Law(_));
348 let mut shape_contract = if is_law {
349 None
350 } else {
351 build_verify_shape_contract(block, interp)
352 };
353
354 match &block.kind {
355 VerifyKind::Cases => println!("Verify: {}", block.fn_name.cyan()),
356 VerifyKind::Law(law) => {
357 println!("Verify: {} law {}", block.fn_name.cyan(), law.name.cyan())
358 }
359 }
360 if !is_law {
361 interp.start_verify_match_coverage(&block.fn_name);
362 }
363
364 for (left_expr, right_expr) in &block.cases {
365 let case_str = format!("{} == {}", expr_to_str(left_expr), expr_to_str(right_expr));
366
367 let left_result = interp.eval_expr(left_expr);
368 let right_result = interp.eval_expr(right_expr);
369
370 if let Ok(left_val) = &left_result {
371 if let Some(contract) = shape_contract.as_mut() {
372 contract.observe(left_val);
373 }
374 }
375 if verify_case_uses_error_prop_on_target(left_expr, &block.fn_name) {
376 if let Some(contract) = shape_contract.as_mut() {
377 if matches!(contract.return_type, Type::Result(_, _)) {
378 match &left_result {
379 Ok(_) => contract.observe_shape(VerifyOutputShape::Ok),
380 Err(RuntimeError::ErrProp(_)) => {
381 contract.observe_shape(VerifyOutputShape::Err)
382 }
383 Err(_) => {}
384 }
385 }
386 }
387 }
388
389 match (left_result, right_result) {
390 (Ok(left_val), Ok(right_val)) => {
391 if interp.aver_eq(&left_val, &right_val) {
392 passed += 1;
393 println!(" {} {}", "✓".green(), case_str);
394 } else {
395 failed += 1;
396 println!(" {} {}", "✗".red(), case_str);
397 let expected = aver_repr(&right_val);
398 let actual = aver_repr(&left_val);
399 println!(" expected: {}", expected);
400 println!(" got: {}", actual);
401 failures.push((case_str, expected, actual));
402 }
403 }
404 (Err(RuntimeError::ErrProp(err_val)), _) | (_, Err(RuntimeError::ErrProp(err_val))) => {
406 failed += 1;
407 println!(" {} {}", "✗".red(), case_str);
408 println!(" ? hit Result.Err({})", aver_repr(&err_val));
409 failures.push((
410 case_str,
411 String::new(),
412 format!("? hit Result.Err({})", aver_repr(&err_val)),
413 ));
414 }
415 (Err(e), _) | (_, Err(e)) => {
416 failed += 1;
417 println!(" {} {}", "✗".red(), case_str);
418 println!(" error: {}", e);
419 failures.push((case_str, String::new(), format!("ERROR: {}", e)));
420 }
421 }
422 }
423
424 if !is_law {
425 let coverage_misses = interp.finish_verify_match_coverage();
426 for miss in coverage_misses {
427 failed += 1;
428 let missing_1_based: Vec<String> = miss
429 .missing_arms
430 .iter()
431 .map(|idx| (idx + 1).to_string())
432 .collect();
433 let msg = format!(
434 "match at line {} missing covered arm(s): {} (of {})",
435 miss.line,
436 missing_1_based.join(", "),
437 miss.total_arms
438 );
439 println!(" {} {}", "✗".red(), msg);
440 failures.push((
441 format!("match-coverage:{}", miss.line),
442 format!("all {} arms covered", miss.total_arms),
443 msg,
444 ));
445 }
446 }
447
448 if let Some(contract) = shape_contract {
449 let missing = contract.missing();
450 if !missing.is_empty() {
451 failed += 1;
452 let missing_labels: Vec<String> =
453 missing.iter().map(VerifyOutputShape::display).collect();
454 let expected_labels: Vec<String> = contract
455 .expected
456 .iter()
457 .map(VerifyOutputShape::display)
458 .collect();
459 let msg = format!(
460 "missing output shape(s) for {}: {}",
461 contract.return_type.display(),
462 missing_labels.join(", ")
463 );
464 println!(" {} {}", "✗".red(), msg);
465 failures.push((
466 format!("shape-coverage:{}", block.fn_name),
467 format!("covered output shapes: {}", expected_labels.join(", ")),
468 msg,
469 ));
470 }
471 }
472
473 let total = passed + failed;
474 if failed == 0 {
475 println!(" {}", format!("{}/{} passed", passed, total).green());
476 } else {
477 println!(" {}", format!("{}/{} passed", passed, total).red());
478 }
479
480 VerifyResult {
481 fn_name: block.fn_name.clone(),
482 passed,
483 failed,
484 failures,
485 }
486}
487
488pub fn index_decisions(items: &[TopLevel]) -> Vec<&DecisionBlock> {
489 items
490 .iter()
491 .filter_map(|item| {
492 if let TopLevel::Decision(d) = item {
493 Some(d)
494 } else {
495 None
496 }
497 })
498 .collect()
499}
500
501pub fn merge_verify_blocks(items: &[TopLevel]) -> Vec<VerifyBlock> {
502 let mut merged: Vec<VerifyBlock> = Vec::new();
503 let mut by_fn_cases: std::collections::HashMap<String, usize> =
504 std::collections::HashMap::new();
505
506 for item in items {
507 let TopLevel::Verify(vb) = item else {
508 continue;
509 };
510 match &vb.kind {
511 VerifyKind::Cases => {
512 if let Some(&idx) = by_fn_cases.get(&vb.fn_name) {
513 merged[idx].cases.extend(vb.cases.clone());
514 } else {
515 by_fn_cases.insert(vb.fn_name.clone(), merged.len());
516 merged.push(vb.clone());
517 }
518 }
519 VerifyKind::Law(_) => {
520 merged.push(vb.clone());
521 }
522 }
523 }
524
525 merged
526}
527
528fn fn_needs_desc(f: &FnDef) -> bool {
531 f.name != "main"
532}
533
534fn fn_needs_verify(f: &FnDef) -> bool {
540 if f.name == "main" {
541 return false;
542 }
543 if !f.effects.is_empty() {
544 return false;
545 }
546 !is_trivial_passthrough_wrapper(f)
547}
548
549fn is_trivial_passthrough_wrapper(f: &FnDef) -> bool {
550 let param_names: Vec<&str> = f.params.iter().map(|(name, _)| name.as_str()).collect();
551
552 match f.body.as_ref() {
553 FnBody::Expr(expr) => expr_is_passthrough(expr, ¶m_names),
554 FnBody::Block(stmts) => {
555 if stmts.len() != 1 {
556 return false;
557 }
558 match &stmts[0] {
559 Stmt::Expr(expr) => expr_is_passthrough(expr, ¶m_names),
560 Stmt::Binding(_, _, _) => false,
561 }
562 }
563 }
564}
565
566fn expr_is_passthrough(expr: &Expr, param_names: &[&str]) -> bool {
567 match expr {
568 Expr::Ident(name) => param_names.len() == 1 && name == param_names[0],
570 Expr::FnCall(_, args) => args_match_params(args, param_names),
572 Expr::Constructor(_, Some(arg)) => {
574 if param_names.len() != 1 {
575 return false;
576 }
577 matches!(arg.as_ref(), Expr::Ident(name) if name == param_names[0])
578 }
579 _ => false,
580 }
581}
582
583fn args_match_params(args: &[Expr], param_names: &[&str]) -> bool {
584 if args.len() != param_names.len() {
585 return false;
586 }
587 args.iter()
588 .zip(param_names.iter())
589 .all(|(arg, expected)| matches!(arg, Expr::Ident(name) if name == *expected))
590}
591
592fn verify_case_calls_target(left: &Expr, fn_name: &str) -> bool {
593 match left {
594 Expr::FnCall(callee, args) => {
595 callee_is_target(callee, fn_name)
596 || verify_case_calls_target(callee, fn_name)
597 || args
598 .iter()
599 .any(|arg| verify_case_calls_target(arg, fn_name))
600 }
601 Expr::Pipe(left_expr, right_expr) => {
602 pipe_target_is_target(right_expr, fn_name)
603 || verify_case_calls_target(left_expr, fn_name)
604 || verify_case_calls_target(right_expr, fn_name)
605 }
606 Expr::BinOp(_, left_expr, right_expr) => {
607 verify_case_calls_target(left_expr, fn_name)
608 || verify_case_calls_target(right_expr, fn_name)
609 }
610 Expr::Match { subject, arms, .. } => {
611 verify_case_calls_target(subject, fn_name)
612 || arms
613 .iter()
614 .any(|arm| verify_case_calls_target(&arm.body, fn_name))
615 }
616 Expr::Constructor(_, Some(inner)) => verify_case_calls_target(inner, fn_name),
617 Expr::ErrorProp(inner) => verify_case_calls_target(inner, fn_name),
618 Expr::List(elems) => elems
619 .iter()
620 .any(|elem| verify_case_calls_target(elem, fn_name)),
621 Expr::Tuple(items) => items
622 .iter()
623 .any(|item| verify_case_calls_target(item, fn_name)),
624 Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
625 verify_case_calls_target(k, fn_name) || verify_case_calls_target(v, fn_name)
626 }),
627 Expr::Attr(obj, _) => verify_case_calls_target(obj, fn_name),
628 Expr::RecordCreate { fields, .. } => fields
629 .iter()
630 .any(|(_, expr)| verify_case_calls_target(expr, fn_name)),
631 Expr::RecordUpdate { base, updates, .. } => {
632 verify_case_calls_target(base, fn_name)
633 || updates
634 .iter()
635 .any(|(_, expr)| verify_case_calls_target(expr, fn_name))
636 }
637 Expr::TailCall(boxed) => {
638 boxed.0 == fn_name
639 || boxed
640 .1
641 .iter()
642 .any(|arg| verify_case_calls_target(arg, fn_name))
643 }
644 Expr::Literal(_) | Expr::Ident(_) | Expr::InterpolatedStr(_) | Expr::Resolved(_) => false,
645 Expr::Constructor(_, None) => false,
646 }
647}
648
649fn callee_is_target(callee: &Expr, fn_name: &str) -> bool {
650 matches!(callee, Expr::Ident(name) if name == fn_name)
651}
652
653fn pipe_target_is_target(target: &Expr, fn_name: &str) -> bool {
654 match target {
655 Expr::Ident(name) => name == fn_name,
656 Expr::FnCall(callee, _) => callee_is_target(callee, fn_name),
657 _ => false,
658 }
659}
660
661pub fn check_module_intent(items: &[TopLevel]) -> ModuleCheckFindings {
662 let mut errors = Vec::new();
663 let mut warnings = Vec::new();
664
665 let mut verified_fns: std::collections::HashSet<&str> = std::collections::HashSet::new();
666 let mut empty_verify_fns: std::collections::HashSet<&str> = std::collections::HashSet::new();
667 let mut invalid_verify_fns: std::collections::HashSet<&str> = std::collections::HashSet::new();
668 for item in items {
669 if let TopLevel::Verify(v) = item {
670 if v.cases.is_empty() {
671 errors.push(format!(
672 "Verify block '{}' must contain at least one case",
673 v.fn_name
674 ));
675 empty_verify_fns.insert(v.fn_name.as_str());
676 } else {
677 let mut block_valid = true;
678 if matches!(v.kind, VerifyKind::Cases) {
679 for (idx, (left, _right)) in v.cases.iter().enumerate() {
680 if !verify_case_calls_target(left, &v.fn_name) {
681 errors.push(format!(
682 "line {}: Verify block '{}' case #{} must call '{}' on the left side",
683 v.line,
684 v.fn_name,
685 idx + 1,
686 v.fn_name
687 ));
688 block_valid = false;
689 }
690 }
691 for (idx, (_left, right)) in v.cases.iter().enumerate() {
692 if verify_case_calls_target(right, &v.fn_name) {
693 errors.push(format!(
694 "line {}: Verify block '{}' case #{} must not call '{}' on the right side",
695 v.line,
696 v.fn_name,
697 idx + 1,
698 v.fn_name
699 ));
700 block_valid = false;
701 }
702 }
703 }
704 if block_valid {
705 verified_fns.insert(v.fn_name.as_str());
706 } else {
707 invalid_verify_fns.insert(v.fn_name.as_str());
708 }
709 }
710 }
711 }
712
713 for item in items {
714 match item {
715 TopLevel::Module(m) => {
716 if m.intent.is_empty() {
717 warnings.push(format!("Module '{}' has no intent block", m.name));
718 }
719 }
720 TopLevel::FnDef(f) => {
721 if f.desc.is_none() && fn_needs_desc(f) {
722 warnings.push(format!("Function '{}' has no description (?)", f.name));
723 }
724 if fn_needs_verify(f)
725 && !verified_fns.contains(f.name.as_str())
726 && !empty_verify_fns.contains(f.name.as_str())
727 && !invalid_verify_fns.contains(f.name.as_str())
728 {
729 errors.push(format!("Function '{}' has no verify block", f.name));
730 }
731 }
732 _ => {}
733 }
734 }
735
736 ModuleCheckFindings { errors, warnings }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::lexer::Lexer;
743 use crate::parser::Parser;
744
745 fn parse_items(src: &str) -> Vec<TopLevel> {
746 let mut lexer = Lexer::new(src);
747 let tokens = lexer.tokenize().expect("lex failed");
748 let mut parser = Parser::new(tokens);
749 parser.parse().expect("parse failed")
750 }
751
752 #[test]
753 fn no_verify_warning_for_effectful_function() {
754 let items = parse_items(
755 r#"
756fn log(x: Int) -> Unit
757 ! [Console]
758 = Console.print(x)
759"#,
760 );
761 let findings = check_module_intent(&items);
762 assert!(
763 !findings
764 .warnings
765 .iter()
766 .any(|w| w.contains("no verify block"))
767 && !findings
768 .errors
769 .iter()
770 .any(|e| e.contains("no verify block")),
771 "unexpected findings: errors={:?}, warnings={:?}",
772 findings.errors,
773 findings.warnings
774 );
775 }
776
777 #[test]
778 fn no_verify_warning_for_trivial_passthrough_wrapper() {
779 let items = parse_items(
780 r#"
781fn passthrough(x: Int) -> Int
782 = inner(x)
783"#,
784 );
785 let findings = check_module_intent(&items);
786 assert!(
787 !findings
788 .warnings
789 .iter()
790 .any(|w| w.contains("no verify block"))
791 && !findings
792 .errors
793 .iter()
794 .any(|e| e.contains("no verify block")),
795 "unexpected findings: errors={:?}, warnings={:?}",
796 findings.errors,
797 findings.warnings
798 );
799 }
800
801 #[test]
802 fn verify_error_for_pure_non_trivial_logic() {
803 let items = parse_items(
804 r#"
805fn add1(x: Int) -> Int
806 = x + 1
807"#,
808 );
809 let findings = check_module_intent(&items);
810 assert!(
811 findings
812 .errors
813 .iter()
814 .any(|e| e == "Function 'add1' has no verify block"),
815 "expected verify error, got errors={:?}, warnings={:?}",
816 findings.errors,
817 findings.warnings
818 );
819 }
820
821 #[test]
822 fn empty_verify_block_is_rejected() {
823 let items = parse_items(
824 r#"
825fn add1(x: Int) -> Int
826 = x + 1
827
828verify add1
829"#,
830 );
831 let findings = check_module_intent(&items);
832 assert!(
833 findings
834 .errors
835 .iter()
836 .any(|e| e == "Verify block 'add1' must contain at least one case"),
837 "expected empty verify error, got errors={:?}, warnings={:?}",
838 findings.errors,
839 findings.warnings
840 );
841 assert!(
842 !findings
843 .errors
844 .iter()
845 .any(|e| e == "Function 'add1' has no verify block"),
846 "expected no duplicate missing-verify error, got errors={:?}, warnings={:?}",
847 findings.errors,
848 findings.warnings
849 );
850 }
851
852 #[test]
853 fn verify_case_must_call_verified_function_on_left_side() {
854 let items = parse_items(
855 r#"
856fn add1(x: Int) -> Int
857 = x + 1
858
859verify add1
860 true => true
861"#,
862 );
863 let findings = check_module_intent(&items);
864 assert!(
865 findings.errors.iter().any(|e| {
866 e.contains("Verify block 'add1' case #1 must call 'add1' on the left side")
867 }),
868 "expected verify-case-call error, got errors={:?}, warnings={:?}",
869 findings.errors,
870 findings.warnings
871 );
872 assert!(
873 !findings
874 .errors
875 .iter()
876 .any(|e| e == "Function 'add1' has no verify block"),
877 "expected no duplicate missing-verify error, got errors={:?}, warnings={:?}",
878 findings.errors,
879 findings.warnings
880 );
881 }
882
883 #[test]
884 fn verify_case_pipe_into_target_is_allowed() {
885 let items = parse_items(
886 r#"
887fn add1(x: Int) -> Int
888 = x + 1
889
890verify add1
891 41 |> add1 => 42
892"#,
893 );
894 let findings = check_module_intent(&items);
895 assert!(
896 !findings.errors.iter().any(|e| e.contains("case #")),
897 "did not expect verify-case-call error, got errors={:?}, warnings={:?}",
898 findings.errors,
899 findings.warnings
900 );
901 }
902
903 #[test]
904 fn verify_case_must_not_call_verified_function_on_right_side() {
905 let items = parse_items(
906 r#"
907fn add1(x: Int) -> Int
908 = x + 1
909
910verify add1
911 add1(1) => add1(1)
912"#,
913 );
914 let findings = check_module_intent(&items);
915 assert!(
916 findings.errors.iter().any(|e| {
917 e.contains("Verify block 'add1' case #1 must not call 'add1' on the right side")
918 }),
919 "expected verify-case-rhs error, got errors={:?}, warnings={:?}",
920 findings.errors,
921 findings.warnings
922 );
923 }
924
925 #[test]
926 fn verify_law_skips_left_right_call_heuristics() {
927 let items = parse_items(
928 r#"
929fn add1(x: Int) -> Int
930 = x + 1
931
932verify add1 law reflexive
933 given x: Int = [1, 2, 3]
934 x => x
935"#,
936 );
937 let findings = check_module_intent(&items);
938 assert!(
939 !findings
940 .errors
941 .iter()
942 .any(|e| e.contains("must call 'add1' on the left side")),
943 "did not expect lhs-call heuristic for law verify, got errors={:?}",
944 findings.errors
945 );
946 assert!(
947 !findings
948 .errors
949 .iter()
950 .any(|e| e.contains("must not call 'add1' on the right side")),
951 "did not expect rhs-call heuristic for law verify, got errors={:?}",
952 findings.errors
953 );
954 assert!(
955 !findings
956 .errors
957 .iter()
958 .any(|e| e == "Function 'add1' has no verify block"),
959 "law verify should satisfy verify requirement, got errors={:?}",
960 findings.errors
961 );
962 }
963
964 #[test]
965 fn merge_verify_blocks_coalesces_cases_by_function() {
966 let items = parse_items(
967 r#"
968fn f(x: Int) -> Int
969 = x
970
971verify f
972 f(1) => 1
973
974verify f
975 f(2) => 2
976"#,
977 );
978 let merged = merge_verify_blocks(&items);
979 assert_eq!(merged.len(), 1);
980 assert_eq!(merged[0].fn_name, "f");
981 assert_eq!(merged[0].cases.len(), 2);
982 }
983
984 #[test]
985 fn merge_verify_blocks_keeps_law_blocks_separate() {
986 let items = parse_items(
987 r#"
988fn f(x: Int) -> Int
989 = x
990
991verify f
992 f(1) => 1
993
994verify f law l1
995 given x: Int = [1]
996 x => x
997
998verify f law l2
999 given x: Int = [2]
1000 x => x
1001
1002verify f
1003 f(2) => 2
1004"#,
1005 );
1006 let merged = merge_verify_blocks(&items);
1007 assert_eq!(merged.len(), 3);
1008 assert!(matches!(merged[0].kind, VerifyKind::Cases));
1009 assert_eq!(merged[0].cases.len(), 2);
1010 assert!(matches!(merged[1].kind, VerifyKind::Law(_)));
1011 assert!(matches!(merged[2].kind, VerifyKind::Law(_)));
1012 }
1013}
1014
1015pub fn expr_to_str(expr: &crate::ast::Expr) -> String {
1016 use crate::ast::Expr;
1017 use crate::ast::Literal;
1018
1019 match expr {
1020 Expr::Literal(lit) => match lit {
1021 Literal::Int(i) => i.to_string(),
1022 Literal::Float(f) => f.to_string(),
1023 Literal::Str(s) => format!("\"{}\"", s),
1024 Literal::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1025 },
1026 Expr::Ident(name) => name.clone(),
1027 Expr::FnCall(fn_expr, args) => {
1028 let fn_str = expr_to_str(fn_expr);
1029 let args_str = args.iter().map(expr_to_str).collect::<Vec<_>>().join(", ");
1030 format!("{}({})", fn_str, args_str)
1031 }
1032 Expr::Constructor(name, arg) => match arg {
1033 None => name.clone(),
1034 Some(a) => format!("{}({})", name, expr_to_str(a)),
1035 },
1036 Expr::BinOp(op, left, right) => {
1037 use crate::ast::BinOp;
1038 let op_str = match op {
1039 BinOp::Add => "+",
1040 BinOp::Sub => "-",
1041 BinOp::Mul => "*",
1042 BinOp::Div => "/",
1043 BinOp::Eq => "==",
1044 BinOp::Neq => "!=",
1045 BinOp::Lt => "<",
1046 BinOp::Gt => ">",
1047 BinOp::Lte => "<=",
1048 BinOp::Gte => ">=",
1049 };
1050 format!("{} {} {}", expr_to_str(left), op_str, expr_to_str(right))
1051 }
1052 Expr::InterpolatedStr(parts) => {
1053 use crate::ast::StrPart;
1054 let mut inner = String::new();
1055 for part in parts {
1056 match part {
1057 StrPart::Literal(s) => inner.push_str(s),
1058 StrPart::Parsed(e) => {
1059 inner.push('{');
1060 inner.push_str(&expr_to_str(e));
1061 inner.push('}');
1062 }
1063 }
1064 }
1065 format!("\"{}\"", inner)
1066 }
1067 Expr::List(elements) => {
1068 let parts: Vec<String> = elements.iter().map(expr_to_str).collect();
1069 format!("[{}]", parts.join(", "))
1070 }
1071 Expr::Tuple(items) => {
1072 let parts: Vec<String> = items.iter().map(expr_to_str).collect();
1073 format!("({})", parts.join(", "))
1074 }
1075 Expr::MapLiteral(entries) => {
1076 let parts = entries
1077 .iter()
1078 .map(|(key, value)| format!("{} => {}", expr_to_str(key), expr_to_str(value)))
1079 .collect::<Vec<_>>();
1080 format!("{{{}}}", parts.join(", "))
1081 }
1082 Expr::ErrorProp(inner) => format!("{}?", expr_to_str(inner)),
1083 Expr::Attr(obj, field) => format!("{}.{}", expr_to_str(obj), field),
1084 Expr::Pipe(left, right) => format!("{} |> {}", expr_to_str(left), expr_to_str(right)),
1085 Expr::RecordCreate { type_name, fields } => {
1086 let flds: Vec<String> = fields
1087 .iter()
1088 .map(|(name, expr)| format!("{} = {}", name, expr_to_str(expr)))
1089 .collect();
1090 format!("{}({})", type_name, flds.join(", "))
1091 }
1092 Expr::RecordUpdate {
1093 type_name,
1094 base,
1095 updates,
1096 } => {
1097 let upds: Vec<String> = updates
1098 .iter()
1099 .map(|(name, expr)| format!("{} = {}", name, expr_to_str(expr)))
1100 .collect();
1101 format!(
1102 "{}.update({}, {})",
1103 type_name,
1104 expr_to_str(base),
1105 upds.join(", ")
1106 )
1107 }
1108 Expr::TailCall(boxed) => {
1109 let (target, args) = boxed.as_ref();
1110 let a = args.iter().map(expr_to_str).collect::<Vec<_>>().join(", ");
1111 format!("<tail-call:{}>({})", target, a)
1112 }
1113 Expr::Resolved(_) => "<resolved>".to_string(),
1114 Expr::Match { subject, arms, .. } => {
1115 let s = expr_to_str(subject);
1116 let arms_str: Vec<String> = arms
1117 .iter()
1118 .map(|arm| format!("{:?} -> {}", arm.pattern, expr_to_str(&arm.body)))
1119 .collect();
1120 format!("match {} {}", s, arms_str.join(", "))
1121 }
1122 }
1123}