Skip to main content

aver/
checker.rs

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)>, // (expr_src, expected, actual)
24}
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            // `?` in a verify case hitting Err produces ErrProp — treat as test failure.
477            (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
584/// Returns true if a function requires a ? description annotation.
585/// All functions except main() require one.
586fn fn_needs_desc(f: &FnDef) -> bool {
587    f.name != "main"
588}
589
590/// Missing verify warning policy:
591/// - skip `main`
592/// - skip effectful functions (tested through replay/recording flow)
593/// - skip trivial pure pass-through wrappers
594/// - require verify for the rest (pure, non-trivial logic)
595fn 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, &param_names))
614}
615
616fn expr_is_passthrough(expr: &Expr, param_names: &[&str]) -> bool {
617    match expr {
618        // `fn id(x) = x`
619        Expr::Ident(name) => param_names.len() == 1 && name == param_names[0],
620        // `fn wrap(a,b) = inner(a,b)` (no argument transformation)
621        Expr::FnCall(_, args) => args_match_params(args, param_names),
622        // `fn some(x) = Option.Some(x)` style
623        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}