Skip to main content

aver/
checker.rs

1use colored::Colorize;
2
3use crate::ast::{DecisionBlock, Expr, FnBody, FnDef, Stmt, TopLevel, VerifyBlock};
4use crate::interpreter::{Interpreter, aver_repr};
5use crate::value::RuntimeError;
6
7pub struct VerifyResult {
8    #[allow(dead_code)]
9    pub fn_name: String,
10    pub passed: usize,
11    pub failed: usize,
12    #[allow(dead_code)]
13    pub failures: Vec<(String, String, String)>, // (expr_src, expected, actual)
14}
15
16pub fn run_verify(block: &VerifyBlock, interp: &mut Interpreter) -> VerifyResult {
17    let mut passed = 0;
18    let mut failed = 0;
19    let mut failures = Vec::new();
20
21    println!("Verify: {}", block.fn_name.cyan());
22
23    for (left_expr, right_expr) in &block.cases {
24        let case_str = format!("{} == {}", expr_to_str(left_expr), expr_to_str(right_expr));
25
26        let left_result = interp.eval_expr(left_expr);
27        let right_result = interp.eval_expr(right_expr);
28
29        match (left_result, right_result) {
30            (Ok(left_val), Ok(right_val)) => {
31                if interp.aver_eq(&left_val, &right_val) {
32                    passed += 1;
33                    println!("  {} {}", "✓".green(), case_str);
34                } else {
35                    failed += 1;
36                    println!("  {} {}", "✗".red(), case_str);
37                    let expected = aver_repr(&right_val);
38                    let actual = aver_repr(&left_val);
39                    println!("      expected: {}", expected);
40                    println!("      got:      {}", actual);
41                    failures.push((case_str, expected, actual));
42                }
43            }
44            // `?` in a verify case hitting Err produces ErrProp — treat as test failure.
45            (Err(RuntimeError::ErrProp(err_val)), _) | (_, Err(RuntimeError::ErrProp(err_val))) => {
46                failed += 1;
47                println!("  {} {}", "✗".red(), case_str);
48                println!("      ? hit Result.Err({})", aver_repr(&err_val));
49                failures.push((
50                    case_str,
51                    String::new(),
52                    format!("? hit Result.Err({})", aver_repr(&err_val)),
53                ));
54            }
55            (Err(e), _) | (_, Err(e)) => {
56                failed += 1;
57                println!("  {} {}", "✗".red(), case_str);
58                println!("      error: {}", e);
59                failures.push((case_str, String::new(), format!("ERROR: {}", e)));
60            }
61        }
62    }
63
64    let total = passed + failed;
65    if failed == 0 {
66        println!("  {}", format!("{}/{} passed", passed, total).green());
67    } else {
68        println!("  {}", format!("{}/{} passed", passed, total).red());
69    }
70
71    VerifyResult {
72        fn_name: block.fn_name.clone(),
73        passed,
74        failed,
75        failures,
76    }
77}
78
79pub fn index_decisions(items: &[TopLevel]) -> Vec<&DecisionBlock> {
80    items
81        .iter()
82        .filter_map(|item| {
83            if let TopLevel::Decision(d) = item {
84                Some(d)
85            } else {
86                None
87            }
88        })
89        .collect()
90}
91
92/// Returns true if a function requires a ? description annotation.
93/// All functions except main() require one.
94fn fn_needs_desc(f: &FnDef) -> bool {
95    f.name != "main"
96}
97
98/// Missing verify warning policy:
99/// - skip `main`
100/// - skip effectful functions (tested through replay/recording flow)
101/// - skip trivial pure pass-through wrappers
102/// - require verify for the rest (pure, non-trivial logic)
103fn fn_needs_verify(f: &FnDef) -> bool {
104    if f.name == "main" {
105        return false;
106    }
107    if !f.effects.is_empty() {
108        return false;
109    }
110    !is_trivial_passthrough_wrapper(f)
111}
112
113fn is_trivial_passthrough_wrapper(f: &FnDef) -> bool {
114    let param_names: Vec<&str> = f.params.iter().map(|(name, _)| name.as_str()).collect();
115
116    match f.body.as_ref() {
117        FnBody::Expr(expr) => expr_is_passthrough(expr, &param_names),
118        FnBody::Block(stmts) => {
119            if stmts.len() != 1 {
120                return false;
121            }
122            match &stmts[0] {
123                Stmt::Expr(expr) => expr_is_passthrough(expr, &param_names),
124                Stmt::Binding(_, _, _) => false,
125            }
126        }
127    }
128}
129
130fn expr_is_passthrough(expr: &Expr, param_names: &[&str]) -> bool {
131    match expr {
132        // `fn id(x) = x`
133        Expr::Ident(name) => param_names.len() == 1 && name == param_names[0],
134        // `fn wrap(a,b) = inner(a,b)` (no argument transformation)
135        Expr::FnCall(_, args) => args_match_params(args, param_names),
136        // `fn some(x) = Option.Some(x)` style
137        Expr::Constructor(_, Some(arg)) => {
138            if param_names.len() != 1 {
139                return false;
140            }
141            matches!(arg.as_ref(), Expr::Ident(name) if name == param_names[0])
142        }
143        _ => false,
144    }
145}
146
147fn args_match_params(args: &[Expr], param_names: &[&str]) -> bool {
148    if args.len() != param_names.len() {
149        return false;
150    }
151    args.iter()
152        .zip(param_names.iter())
153        .all(|(arg, expected)| matches!(arg, Expr::Ident(name) if name == *expected))
154}
155
156pub fn check_module_intent(items: &[TopLevel]) -> Vec<String> {
157    let mut warnings = Vec::new();
158
159    let verified_fns: std::collections::HashSet<&str> = items
160        .iter()
161        .filter_map(|item| {
162            if let TopLevel::Verify(v) = item {
163                Some(v.fn_name.as_str())
164            } else {
165                None
166            }
167        })
168        .collect();
169
170    for item in items {
171        match item {
172            TopLevel::Module(m) => {
173                if m.intent.is_empty() {
174                    warnings.push(format!("Module '{}' has no intent block", m.name));
175                }
176            }
177            TopLevel::FnDef(f) => {
178                if f.desc.is_none() && fn_needs_desc(f) {
179                    warnings.push(format!("Function '{}' has no description (?)", f.name));
180                }
181                if fn_needs_verify(f) && !verified_fns.contains(f.name.as_str()) {
182                    warnings.push(format!("Function '{}' has no verify block", f.name));
183                }
184            }
185            _ => {}
186        }
187    }
188
189    warnings
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::lexer::Lexer;
196    use crate::parser::Parser;
197
198    fn parse_items(src: &str) -> Vec<TopLevel> {
199        let mut lexer = Lexer::new(src);
200        let tokens = lexer.tokenize().expect("lex failed");
201        let mut parser = Parser::new(tokens);
202        parser.parse().expect("parse failed")
203    }
204
205    #[test]
206    fn no_verify_warning_for_effectful_function() {
207        let items = parse_items(
208            r#"
209fn log(x: Int) -> Unit
210    ! [Console]
211    = Console.print(x)
212"#,
213        );
214        let warnings = check_module_intent(&items);
215        assert!(
216            !warnings.iter().any(|w| w.contains("no verify block")),
217            "unexpected warnings: {:?}",
218            warnings
219        );
220    }
221
222    #[test]
223    fn no_verify_warning_for_trivial_passthrough_wrapper() {
224        let items = parse_items(
225            r#"
226fn passthrough(x: Int) -> Int
227    = inner(x)
228"#,
229        );
230        let warnings = check_module_intent(&items);
231        assert!(
232            !warnings.iter().any(|w| w.contains("no verify block")),
233            "unexpected warnings: {:?}",
234            warnings
235        );
236    }
237
238    #[test]
239    fn verify_warning_for_pure_non_trivial_logic() {
240        let items = parse_items(
241            r#"
242fn add1(x: Int) -> Int
243    = x + 1
244"#,
245        );
246        let warnings = check_module_intent(&items);
247        assert!(
248            warnings
249                .iter()
250                .any(|w| w == "Function 'add1' has no verify block"),
251            "expected verify warning, got: {:?}",
252            warnings
253        );
254    }
255}
256
257pub fn expr_to_str(expr: &crate::ast::Expr) -> String {
258    use crate::ast::Expr;
259    use crate::ast::Literal;
260
261    match expr {
262        Expr::Literal(lit) => match lit {
263            Literal::Int(i) => i.to_string(),
264            Literal::Float(f) => f.to_string(),
265            Literal::Str(s) => format!("\"{}\"", s),
266            Literal::Bool(b) => if *b { "true" } else { "false" }.to_string(),
267        },
268        Expr::Ident(name) => name.clone(),
269        Expr::FnCall(fn_expr, args) => {
270            let fn_str = expr_to_str(fn_expr);
271            let args_str = args.iter().map(expr_to_str).collect::<Vec<_>>().join(", ");
272            format!("{}({})", fn_str, args_str)
273        }
274        Expr::Constructor(name, arg) => match arg {
275            None => name.clone(),
276            Some(a) => format!("{}({})", name, expr_to_str(a)),
277        },
278        Expr::BinOp(op, left, right) => {
279            use crate::ast::BinOp;
280            let op_str = match op {
281                BinOp::Add => "+",
282                BinOp::Sub => "-",
283                BinOp::Mul => "*",
284                BinOp::Div => "/",
285                BinOp::Eq => "==",
286                BinOp::Neq => "!=",
287                BinOp::Lt => "<",
288                BinOp::Gt => ">",
289                BinOp::Lte => "<=",
290                BinOp::Gte => ">=",
291            };
292            format!("{} {} {}", expr_to_str(left), op_str, expr_to_str(right))
293        }
294        Expr::InterpolatedStr(parts) => {
295            use crate::ast::StrPart;
296            let mut inner = String::new();
297            for part in parts {
298                match part {
299                    StrPart::Literal(s) => inner.push_str(s),
300                    StrPart::Parsed(e) => {
301                        inner.push('{');
302                        inner.push_str(&expr_to_str(e));
303                        inner.push('}');
304                    }
305                }
306            }
307            format!("\"{}\"", inner)
308        }
309        Expr::List(elements) => {
310            let parts: Vec<String> = elements.iter().map(expr_to_str).collect();
311            format!("[{}]", parts.join(", "))
312        }
313        Expr::Tuple(items) => {
314            let parts: Vec<String> = items.iter().map(expr_to_str).collect();
315            format!("({})", parts.join(", "))
316        }
317        Expr::MapLiteral(entries) => {
318            let parts = entries
319                .iter()
320                .map(|(key, value)| format!("{} => {}", expr_to_str(key), expr_to_str(value)))
321                .collect::<Vec<_>>();
322            format!("{{{}}}", parts.join(", "))
323        }
324        Expr::ErrorProp(inner) => format!("{}?", expr_to_str(inner)),
325        Expr::Attr(obj, field) => format!("{}.{}", expr_to_str(obj), field),
326        Expr::Pipe(left, right) => format!("{} |> {}", expr_to_str(left), expr_to_str(right)),
327        Expr::RecordCreate { type_name, fields } => {
328            let flds: Vec<String> = fields
329                .iter()
330                .map(|(name, expr)| format!("{} = {}", name, expr_to_str(expr)))
331                .collect();
332            format!("{}({})", type_name, flds.join(", "))
333        }
334        Expr::RecordUpdate {
335            type_name,
336            base,
337            updates,
338        } => {
339            let upds: Vec<String> = updates
340                .iter()
341                .map(|(name, expr)| format!("{} = {}", name, expr_to_str(expr)))
342                .collect();
343            format!(
344                "{}.update({}, {})",
345                type_name,
346                expr_to_str(base),
347                upds.join(", ")
348            )
349        }
350        Expr::TailCall(boxed) => {
351            let (target, args) = boxed.as_ref();
352            let a = args.iter().map(expr_to_str).collect::<Vec<_>>().join(", ");
353            format!("<tail-call:{}>({})", target, a)
354        }
355        Expr::Resolved(_) => "<resolved>".to_string(),
356        Expr::Match { subject, arms, .. } => {
357            let s = expr_to_str(subject);
358            let arms_str: Vec<String> = arms
359                .iter()
360                .map(|arm| format!("{:?} -> {}", arm.pattern, expr_to_str(&arm.body)))
361                .collect();
362            format!("match {} {}", s, arms_str.join(", "))
363        }
364    }
365}