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)>, }
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 (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
92fn fn_needs_desc(f: &FnDef) -> bool {
95 f.name != "main"
96}
97
98fn 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, ¶m_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, ¶m_names),
124 Stmt::Binding(_, _, _) => false,
125 }
126 }
127 }
128}
129
130fn expr_is_passthrough(expr: &Expr, param_names: &[&str]) -> bool {
131 match expr {
132 Expr::Ident(name) => param_names.len() == 1 && name == param_names[0],
134 Expr::FnCall(_, args) => args_match_params(args, param_names),
136 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}