Skip to main content

aver/
checker.rs

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)>, // (expr_src, expected, actual)
15}
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            // `?` in a verify case hitting Err produces ErrProp — treat as test failure.
405            (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
528/// Returns true if a function requires a ? description annotation.
529/// All functions except main() require one.
530fn fn_needs_desc(f: &FnDef) -> bool {
531    f.name != "main"
532}
533
534/// Missing verify warning policy:
535/// - skip `main`
536/// - skip effectful functions (tested through replay/recording flow)
537/// - skip trivial pure pass-through wrappers
538/// - require verify for the rest (pure, non-trivial logic)
539fn 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, &param_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, &param_names),
560                Stmt::Binding(_, _, _) => false,
561            }
562        }
563    }
564}
565
566fn expr_is_passthrough(expr: &Expr, param_names: &[&str]) -> bool {
567    match expr {
568        // `fn id(x) = x`
569        Expr::Ident(name) => param_names.len() == 1 && name == param_names[0],
570        // `fn wrap(a,b) = inner(a,b)` (no argument transformation)
571        Expr::FnCall(_, args) => args_match_params(args, param_names),
572        // `fn some(x) = Option.Some(x)` style
573        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}