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