1mod builtins;
7mod expr;
8mod law_auto;
9mod pattern;
10mod shared;
11mod toplevel;
12mod types;
13
14use std::collections::{HashMap, HashSet};
15
16use crate::ast::{BinOp, Expr, FnBody, FnDef, MatchArm, Pattern, Stmt, TopLevel, VerifyKind};
17use crate::call_graph;
18use crate::codegen::{CodegenContext, ProjectOutput};
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum VerifyEmitMode {
23 NativeDecide,
25 Sorry,
27 TheoremSkeleton,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum RecursionPlan {
36 IntCountdown,
38 ListStructural,
40 StringPosAdvance,
43 MutualIntCountdown,
45 MutualStringPosAdvance { rank: usize },
48 MutualSizeOfRanked {
51 rank: usize,
52 metric_param_index: usize,
53 },
54}
55
56const LEAN_PRELUDE_HEADER: &str = r#"-- Generated by the Aver → Lean 4 transpiler
57-- Pure core logic only (effectful functions are omitted)
58
59-- Prelude: helper definitions for Aver builtins"#;
60
61const LEAN_PRELUDE_FLOAT_COE: &str = r#"instance : Coe Int Float := ⟨fun n => Float.ofInt n⟩"#;
62
63const LEAN_PRELUDE_FLOAT_DEC_EQ: &str = r#"private unsafe def Float.unsafeDecEq (a b : Float) : Decidable (a = b) :=
64 if a == b then isTrue (unsafeCast ()) else isFalse (unsafeCast ())
65@[implemented_by Float.unsafeDecEq]
66private opaque Float.compDecEq (a b : Float) : Decidable (a = b)
67instance : DecidableEq Float := Float.compDecEq"#;
68
69const LEAN_PRELUDE_EXCEPT_DEC_EQ: &str = r#"instance [DecidableEq ε] [DecidableEq α] : DecidableEq (Except ε α)
70 | .ok a, .ok b =>
71 if h : a = b then isTrue (h ▸ rfl) else isFalse (by intro h'; cases h'; exact h rfl)
72 | .error a, .error b =>
73 if h : a = b then isTrue (h ▸ rfl) else isFalse (by intro h'; cases h'; exact h rfl)
74 | .ok _, .error _ => isFalse (by intro h; cases h)
75 | .error _, .ok _ => isFalse (by intro h; cases h)"#;
76
77const LEAN_PRELUDE_EXCEPT_NS: &str = r#"namespace Except
78def withDefault (r : Except ε α) (d : α) : α :=
79 match r with
80 | .ok v => v
81 | .error _ => d
82end Except"#;
83
84const LEAN_PRELUDE_OPTION_TO_EXCEPT: &str = r#"def Option.toExcept (o : Option α) (e : ε) : Except ε α :=
85 match o with
86 | some v => .ok v
87 | none => .error e"#;
88
89const LEAN_PRELUDE_STRING_HADD: &str = r#"instance : HAdd String String String := ⟨String.append⟩"#;
90
91const AVER_MAP_PRELUDE: &str = r#"namespace AverMap
92def empty : List (α × β) := []
93def get [BEq α] (m : List (α × β)) (k : α) : Option β :=
94 match m with
95 | [] => none
96 | (k', v) :: rest => if k == k' then some v else AverMap.get rest k
97def set [BEq α] (m : List (α × β)) (k : α) (v : β) : List (α × β) :=
98 let rec go : List (α × β) → List (α × β)
99 | [] => [(k, v)]
100 | (k', v') :: rest => if k == k' then (k, v) :: rest else (k', v') :: go rest
101 go m
102def has [BEq α] (m : List (α × β)) (k : α) : Bool :=
103 m.any (fun p => p.1 == k)
104def remove [BEq α] (m : List (α × β)) (k : α) : List (α × β) :=
105 m.filter (fun p => !(p.1 == k))
106def keys (m : List (α × β)) : List α := m.map Prod.fst
107def values (m : List (α × β)) : List β := m.map Prod.snd
108def entries (m : List (α × β)) : List (α × β) := m
109def len (m : List (α × β)) : Nat := m.length
110def fromList (entries : List (α × β)) : List (α × β) := entries
111
112private theorem any_set_go_self [BEq α] (k : α) (v : β) :
113 ∀ (m : List (α × β)), List.any (AverMap.set.go k v m) (fun p => p.1 == k) = true := by
114 intro m
115 induction m with
116 | nil =>
117 simp [AverMap.set.go, List.any, beq_self_eq_true]
118 | cons p tl ih =>
119 simp only [AverMap.set.go]
120 split <;> simp_all [List.any, beq_self_eq_true]
121
122theorem has_set_self [BEq α] (m : List (α × β)) (k : α) (v : β) :
123 AverMap.has (AverMap.set m k v) k = true := by
124 simp [AverMap.has, AverMap.set]
125 exact any_set_go_self k v m
126
127private theorem get_set_go_self [BEq α] (k : α) (v : β) :
128 ∀ (m : List (α × β)), AverMap.get (AverMap.set.go k v m) k = some v := by
129 intro m
130 induction m with
131 | nil =>
132 simp [AverMap.set.go, AverMap.get, beq_self_eq_true]
133 | cons p tl ih =>
134 simp only [AverMap.set.go]
135 split
136 · simp [AverMap.get, beq_self_eq_true]
137 · simp [AverMap.get, beq_self_eq_true, ih]
138
139theorem get_set_self [BEq α] (m : List (α × β)) (k : α) (v : β) :
140 AverMap.get (AverMap.set m k v) k = some v := by
141 simp [AverMap.set]
142 exact get_set_go_self k v m
143end AverMap"#;
144
145const LEAN_PRELUDE_AVER_LIST: &str = r#"namespace AverList
146private def insertSorted [Ord α] (x : α) : List α → List α
147 | [] => [x]
148 | y :: ys =>
149 if compare x y == Ordering.lt || compare x y == Ordering.eq then
150 x :: y :: ys
151 else
152 y :: insertSorted x ys
153def sort [Ord α] (xs : List α) : List α :=
154 xs.foldl (fun acc x => insertSorted x acc) []
155end AverList"#;
156
157const LEAN_PRELUDE_STRING_HELPERS: &str = r#"def String.charAt (s : String) (i : Int) : Option String :=
158 if i < 0 then none
159 else (s.toList.get? i.toNat).map Char.toString
160def String.slice (s : String) (start stop : Int) : String :=
161 let startN := if start < 0 then 0 else start.toNat
162 let stopN := if stop < 0 then 0 else stop.toNat
163 let chars := s.toList
164 String.mk ((chars.drop startN).take (stopN - startN))
165private def trimFloatTrailingZerosChars (chars : List Char) : List Char :=
166 let noZeros := (chars.reverse.dropWhile (fun c => c == '0')).reverse
167 match noZeros.reverse with
168 | '.' :: rest => rest.reverse
169 | _ => noZeros
170private def normalizeFloatString (s : String) : String :=
171 if s.toList.any (fun c => c == '.') then
172 String.mk (trimFloatTrailingZerosChars s.toList)
173 else s
174def String.fromInt (n : Int) : String := toString n
175def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)
176def String.chars (s : String) : List String := s.toList.map Char.toString
177namespace AverString
178def split (s delim : String) : List String :=
179 if delim.isEmpty then
180 "" :: (s.toList.map Char.toString) ++ [""]
181 else
182 s.splitOn delim
183end AverString"#;
184
185const LEAN_PRELUDE_NUMERIC_PARSE: &str = r#"def Int.fromString (s : String) : Except String Int :=
186 match s.toInt? with
187 | some n => .ok n
188 | none => .error ("Invalid integer: " ++ s)
189
190private def charDigitsToNat (cs : List Char) : Nat :=
191 cs.foldl (fun acc c => acc * 10 + (c.toNat - '0'.toNat)) 0
192
193private def parseExpPart : List Char → (Bool × List Char)
194 | '-' :: rest => (true, rest.takeWhile Char.isDigit)
195 | '+' :: rest => (false, rest.takeWhile Char.isDigit)
196 | rest => (false, rest.takeWhile Char.isDigit)
197
198def Float.fromString (s : String) : Except String Float :=
199 let chars := s.toList
200 let (neg, chars) := match chars with
201 | '-' :: rest => (true, rest)
202 | _ => (false, chars)
203 let intPart := chars.takeWhile Char.isDigit
204 let rest := chars.dropWhile Char.isDigit
205 let (fracPart, rest) := match rest with
206 | '.' :: rest => (rest.takeWhile Char.isDigit, rest.dropWhile Char.isDigit)
207 | _ => ([], rest)
208 let (expNeg, expDigits) := match rest with
209 | 'e' :: rest => parseExpPart rest
210 | 'E' :: rest => parseExpPart rest
211 | _ => (false, [])
212 if intPart.isEmpty && fracPart.isEmpty then .error ("Invalid float: " ++ s)
213 else
214 let mantissa := charDigitsToNat (intPart ++ fracPart)
215 let fracLen : Int := fracPart.length
216 let expVal : Int := charDigitsToNat expDigits
217 let shift : Int := (if expNeg then -expVal else expVal) - fracLen
218 let f := if shift >= 0 then Float.ofScientific mantissa false shift.toNat
219 else Float.ofScientific mantissa true ((-shift).toNat)
220 .ok (if neg then -f else f)"#;
221
222const LEAN_PRELUDE_CHAR_BYTE: &str = r#"def Char.toCode (s : String) : Int :=
223 match s.toList.head? with
224 | some c => (c.toNat : Int)
225 | none => panic! "Char.toCode: string is empty"
226def Char.fromCode (n : Int) : Option String :=
227 if n < 0 || n > 1114111 then none
228 else if n >= 55296 && n <= 57343 then none
229 else some (Char.toString (Char.ofNat n.toNat))
230
231def hexDigit (n : Int) : String :=
232 match n with
233 | 0 => "0" | 1 => "1" | 2 => "2" | 3 => "3"
234 | 4 => "4" | 5 => "5" | 6 => "6" | 7 => "7"
235 | 8 => "8" | 9 => "9" | 10 => "a" | 11 => "b"
236 | 12 => "c" | 13 => "d" | 14 => "e" | 15 => "f"
237 | _ => "?"
238
239def byteToHex (code : Int) : String :=
240 hexDigit (code / 16) ++ hexDigit (code % 16)
241
242namespace AverByte
243private def hexValue (c : Char) : Option Int :=
244 match c with
245 | '0' => some 0 | '1' => some 1 | '2' => some 2 | '3' => some 3
246 | '4' => some 4 | '5' => some 5 | '6' => some 6 | '7' => some 7
247 | '8' => some 8 | '9' => some 9 | 'a' => some 10 | 'b' => some 11
248 | 'c' => some 12 | 'd' => some 13 | 'e' => some 14 | 'f' => some 15
249 | 'A' => some 10 | 'B' => some 11 | 'C' => some 12 | 'D' => some 13
250 | 'E' => some 14 | 'F' => some 15
251 | _ => none
252def toHex (n : Int) : Except String String :=
253 if n < 0 || n > 255 then
254 .error ("Byte.toHex: " ++ toString n ++ " is out of range 0-255")
255 else
256 .ok (byteToHex n)
257def fromHex (s : String) : Except String Int :=
258 match s.toList with
259 | [hi, lo] =>
260 match hexValue hi, hexValue lo with
261 | some h, some l => .ok (h * 16 + l)
262 | _, _ => .error ("Byte.fromHex: invalid hex '" ++ s ++ "'")
263 | _ => .error ("Byte.fromHex: expected exactly 2 hex chars, got '" ++ s ++ "'")
264end AverByte"#;
265
266fn pure_fns<'a>(ctx: &'a CodegenContext) -> Vec<&'a FnDef> {
267 ctx.modules
268 .iter()
269 .flat_map(|m| m.fn_defs.iter())
270 .chain(ctx.fn_defs.iter())
271 .filter(|fd| toplevel::is_pure_fn(fd))
272 .collect()
273}
274
275fn recursive_pure_fn_names(ctx: &CodegenContext) -> HashSet<String> {
276 let pure_names: HashSet<String> = pure_fns(ctx)
277 .into_iter()
278 .map(|fd| fd.name.clone())
279 .collect();
280 let mut callgraph_items = ctx.items.clone();
281 for module in &ctx.modules {
282 for fd in &module.fn_defs {
283 callgraph_items.push(TopLevel::FnDef(fd.clone()));
284 }
285 }
286 call_graph::find_recursive_fns(&callgraph_items)
287 .into_iter()
288 .filter(|name| pure_names.contains(name))
289 .collect()
290}
291
292fn verify_counter_key(vb: &crate::ast::VerifyBlock) -> String {
293 match &vb.kind {
294 VerifyKind::Cases => format!("fn:{}", vb.fn_name),
295 VerifyKind::Law(law) => format!("law:{}::{}", vb.fn_name, law.name),
296 }
297}
298
299fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
300 match expr {
301 Expr::Ident(name) => Some(name.clone()),
302 Expr::Attr(obj, field) => expr_to_dotted_name(obj).map(|p| format!("{}.{}", p, field)),
303 _ => None,
304 }
305}
306
307fn call_matches(name: &str, target: &str) -> bool {
308 name == target || name.rsplit('.').next() == Some(target)
309}
310
311fn call_is_in_set(name: &str, targets: &HashSet<String>) -> bool {
312 call_matches_any(name, targets)
313}
314
315fn canonical_callee_name(name: &str, targets: &HashSet<String>) -> Option<String> {
316 if targets.contains(name) {
317 return Some(name.to_string());
318 }
319 name.rsplit('.')
320 .next()
321 .filter(|last| targets.contains(*last))
322 .map(ToString::to_string)
323}
324
325fn call_matches_any(name: &str, targets: &HashSet<String>) -> bool {
326 if targets.contains(name) {
327 return true;
328 }
329 match name.rsplit('.').next() {
330 Some(last) => targets.contains(last),
331 None => false,
332 }
333}
334
335fn is_int_minus_one(expr: &Expr, param_name: &str) -> bool {
336 match expr {
337 Expr::BinOp(BinOp::Sub, left, right) => {
338 matches!(left.as_ref(), Expr::Ident(id) if id == param_name)
339 && matches!(right.as_ref(), Expr::Literal(crate::ast::Literal::Int(1)))
340 }
341 Expr::FnCall(callee, args) => {
342 let Some(name) = expr_to_dotted_name(callee) else {
343 return false;
344 };
345 (name == "Int.sub" || name == "int.sub")
346 && args.len() == 2
347 && matches!(&args[0], Expr::Ident(id) if id == param_name)
348 && matches!(&args[1], Expr::Literal(crate::ast::Literal::Int(1)))
349 }
350 _ => false,
351 }
352}
353
354fn collect_calls_from_expr<'a>(expr: &'a Expr, out: &mut Vec<(String, Vec<&'a Expr>)>) {
355 match expr {
356 Expr::FnCall(callee, args) => {
357 if let Some(name) = expr_to_dotted_name(callee) {
358 out.push((name, args.iter().collect()));
359 }
360 collect_calls_from_expr(callee, out);
361 for arg in args {
362 collect_calls_from_expr(arg, out);
363 }
364 }
365 Expr::TailCall(boxed) => {
366 let (name, args) = boxed.as_ref();
367 out.push((name.clone(), args.iter().collect()));
368 for arg in args {
369 collect_calls_from_expr(arg, out);
370 }
371 }
372 Expr::Attr(obj, _) => collect_calls_from_expr(obj, out),
373 Expr::BinOp(_, left, right) => {
374 collect_calls_from_expr(left, out);
375 collect_calls_from_expr(right, out);
376 }
377 Expr::Match { subject, arms, .. } => {
378 collect_calls_from_expr(subject, out);
379 for arm in arms {
380 collect_calls_from_expr(&arm.body, out);
381 }
382 }
383 Expr::Pipe(left, right) => {
384 collect_calls_from_expr(left, out);
385 collect_calls_from_expr(right, out);
386 }
387 Expr::Constructor(_, inner) => {
388 if let Some(inner) = inner {
389 collect_calls_from_expr(inner, out);
390 }
391 }
392 Expr::ErrorProp(inner) => collect_calls_from_expr(inner, out),
393 Expr::InterpolatedStr(parts) => {
394 for p in parts {
395 if let crate::ast::StrPart::Parsed(e) = p {
396 collect_calls_from_expr(e, out);
397 }
398 }
399 }
400 Expr::List(items) | Expr::Tuple(items) => {
401 for item in items {
402 collect_calls_from_expr(item, out);
403 }
404 }
405 Expr::MapLiteral(entries) => {
406 for (k, v) in entries {
407 collect_calls_from_expr(k, out);
408 collect_calls_from_expr(v, out);
409 }
410 }
411 Expr::RecordCreate { fields, .. } => {
412 for (_, v) in fields {
413 collect_calls_from_expr(v, out);
414 }
415 }
416 Expr::RecordUpdate { base, updates, .. } => {
417 collect_calls_from_expr(base, out);
418 for (_, v) in updates {
419 collect_calls_from_expr(v, out);
420 }
421 }
422 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
423 }
424}
425
426fn collect_calls_from_body<'a>(body: &'a FnBody) -> Vec<(String, Vec<&'a Expr>)> {
427 let mut out = Vec::new();
428 match body {
429 FnBody::Expr(expr) => collect_calls_from_expr(expr, &mut out),
430 FnBody::Block(stmts) => {
431 for stmt in stmts {
432 match stmt {
433 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
434 collect_calls_from_expr(expr, &mut out)
435 }
436 }
437 }
438 }
439 }
440 out
441}
442
443fn collect_list_tail_binders_from_expr(
444 expr: &Expr,
445 list_param_name: &str,
446 tails: &mut HashSet<String>,
447) {
448 match expr {
449 Expr::Match { subject, arms, .. } => {
450 if matches!(subject.as_ref(), Expr::Ident(id) if id == list_param_name) {
451 for MatchArm { pattern, .. } in arms {
452 if let Pattern::Cons(_, tail) = pattern {
453 tails.insert(tail.clone());
454 }
455 }
456 }
457 for arm in arms {
458 collect_list_tail_binders_from_expr(&arm.body, list_param_name, tails);
459 }
460 collect_list_tail_binders_from_expr(subject, list_param_name, tails);
461 }
462 Expr::FnCall(callee, args) => {
463 collect_list_tail_binders_from_expr(callee, list_param_name, tails);
464 for arg in args {
465 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
466 }
467 }
468 Expr::TailCall(boxed) => {
469 let (_, args) = boxed.as_ref();
470 for arg in args {
471 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
472 }
473 }
474 Expr::Attr(obj, _) => collect_list_tail_binders_from_expr(obj, list_param_name, tails),
475 Expr::BinOp(_, left, right) | Expr::Pipe(left, right) => {
476 collect_list_tail_binders_from_expr(left, list_param_name, tails);
477 collect_list_tail_binders_from_expr(right, list_param_name, tails);
478 }
479 Expr::Constructor(_, inner) => {
480 if let Some(inner) = inner {
481 collect_list_tail_binders_from_expr(inner, list_param_name, tails);
482 }
483 }
484 Expr::ErrorProp(inner) => {
485 collect_list_tail_binders_from_expr(inner, list_param_name, tails)
486 }
487 Expr::InterpolatedStr(parts) => {
488 for p in parts {
489 if let crate::ast::StrPart::Parsed(e) = p {
490 collect_list_tail_binders_from_expr(e, list_param_name, tails);
491 }
492 }
493 }
494 Expr::List(items) | Expr::Tuple(items) => {
495 for item in items {
496 collect_list_tail_binders_from_expr(item, list_param_name, tails);
497 }
498 }
499 Expr::MapLiteral(entries) => {
500 for (k, v) in entries {
501 collect_list_tail_binders_from_expr(k, list_param_name, tails);
502 collect_list_tail_binders_from_expr(v, list_param_name, tails);
503 }
504 }
505 Expr::RecordCreate { fields, .. } => {
506 for (_, v) in fields {
507 collect_list_tail_binders_from_expr(v, list_param_name, tails);
508 }
509 }
510 Expr::RecordUpdate { base, updates, .. } => {
511 collect_list_tail_binders_from_expr(base, list_param_name, tails);
512 for (_, v) in updates {
513 collect_list_tail_binders_from_expr(v, list_param_name, tails);
514 }
515 }
516 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
517 }
518}
519
520fn collect_list_tail_binders(fd: &FnDef, list_param_name: &str) -> HashSet<String> {
521 let mut tails = HashSet::new();
522 match fd.body.as_ref() {
523 FnBody::Expr(expr) => {
524 collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
525 }
526 FnBody::Block(stmts) => {
527 for stmt in stmts {
528 match stmt {
529 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
530 collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
531 }
532 }
533 }
534 }
535 }
536 tails
537}
538
539fn supports_single_int_countdown(fd: &FnDef) -> bool {
540 let Some((param_name, param_ty)) = fd.params.first() else {
541 return false;
542 };
543 if param_ty != "Int" {
544 return false;
545 }
546 let recursive_calls: Vec<Option<&Expr>> = collect_calls_from_body(fd.body.as_ref())
547 .into_iter()
548 .filter(|(name, _)| call_matches(name, &fd.name))
549 .map(|(_, args)| args.first().copied())
550 .collect();
551 if recursive_calls.is_empty() {
552 return false;
553 }
554 recursive_calls
555 .into_iter()
556 .all(|arg0| arg0.is_some_and(|e| is_int_minus_one(e, param_name)))
557}
558
559fn supports_single_list_structural(fd: &FnDef) -> bool {
560 let Some((param_name, param_ty)) = fd.params.first() else {
561 return false;
562 };
563 if !(param_ty.starts_with("List<") || param_ty == "List") {
564 return false;
565 }
566
567 let tails = collect_list_tail_binders(fd, param_name);
568 if tails.is_empty() {
569 return false;
570 }
571
572 let recursive_calls: Vec<Option<&Expr>> = collect_calls_from_body(fd.body.as_ref())
573 .into_iter()
574 .filter(|(name, _)| call_matches(name, &fd.name))
575 .map(|(_, args)| args.first().copied())
576 .collect();
577 if recursive_calls.is_empty() {
578 return false;
579 }
580
581 recursive_calls.into_iter().all(|arg0| {
582 matches!(
583 arg0,
584 Some(Expr::Ident(id)) if tails.contains(id)
585 )
586 })
587}
588
589fn is_ident(expr: &Expr, name: &str) -> bool {
590 matches!(expr, Expr::Ident(id) if id == name)
591}
592
593fn is_int_plus_positive(expr: &Expr, param_name: &str) -> bool {
594 match expr {
595 Expr::BinOp(BinOp::Add, left, right) => {
596 matches!(left.as_ref(), Expr::Ident(id) if id == param_name)
597 && matches!(right.as_ref(), Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
598 }
599 Expr::FnCall(callee, args) => {
600 let Some(name) = expr_to_dotted_name(callee) else {
601 return false;
602 };
603 (name == "Int.add" || name == "int.add")
604 && args.len() == 2
605 && matches!(&args[0], Expr::Ident(id) if id == param_name)
606 && matches!(&args[1], Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
607 }
608 _ => false,
609 }
610}
611
612fn is_skip_ws_advance(expr: &Expr, string_param: &str, pos_param: &str) -> bool {
613 let Expr::FnCall(callee, args) = expr else {
614 return false;
615 };
616 let Some(name) = expr_to_dotted_name(callee) else {
617 return false;
618 };
619 if !call_matches(&name, "skipWs") || args.len() != 2 {
620 return false;
621 }
622 is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
623}
624
625fn is_skip_ws_same(expr: &Expr, string_param: &str, pos_param: &str) -> bool {
626 let Expr::FnCall(callee, args) = expr else {
627 return false;
628 };
629 let Some(name) = expr_to_dotted_name(callee) else {
630 return false;
631 };
632 if !call_matches(&name, "skipWs") || args.len() != 2 {
633 return false;
634 }
635 is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
636}
637
638fn is_string_pos_advance(expr: &Expr, string_param: &str, pos_param: &str) -> bool {
639 is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
640}
641
642#[derive(Clone, Copy, Debug, Eq, PartialEq)]
643enum StringPosEdge {
644 Same,
645 Advance,
646}
647
648fn classify_string_pos_edge(
649 expr: &Expr,
650 string_param: &str,
651 pos_param: &str,
652) -> Option<StringPosEdge> {
653 if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
654 return Some(StringPosEdge::Same);
655 }
656 if is_string_pos_advance(expr, string_param, pos_param) {
657 return Some(StringPosEdge::Advance);
658 }
659 if let Expr::FnCall(callee, args) = expr {
660 let name = expr_to_dotted_name(callee)?;
661 if call_matches(&name, "skipWs") && args.len() == 2 && is_ident(&args[0], string_param) {
662 if matches!(&args[1], Expr::Ident(id) if id != pos_param) {
663 return Some(StringPosEdge::Advance);
664 }
665 }
666 }
667 if matches!(expr, Expr::Ident(id) if id != pos_param) {
668 return Some(StringPosEdge::Advance);
669 }
670 None
671}
672
673fn ranks_from_same_edges(
674 names: &HashSet<String>,
675 same_edges: &HashMap<String, HashSet<String>>,
676) -> Option<HashMap<String, usize>> {
677 let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
678 for outs in same_edges.values() {
679 for to in outs {
680 if let Some(entry) = indegree.get_mut(to) {
681 *entry += 1;
682 } else {
683 return None;
684 }
685 }
686 }
687
688 let mut queue: Vec<String> = indegree
689 .iter()
690 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
691 .collect();
692 queue.sort();
693 let mut topo = Vec::new();
694 while let Some(node) = queue.pop() {
695 topo.push(node.clone());
696 let outs = same_edges.get(&node).cloned().unwrap_or_default();
697 let mut newly_zero = Vec::new();
698 for to in outs {
699 if let Some(entry) = indegree.get_mut(&to) {
700 *entry -= 1;
701 if *entry == 0 {
702 newly_zero.push(to);
703 }
704 } else {
705 return None;
706 }
707 }
708 newly_zero.sort();
709 queue.extend(newly_zero);
710 }
711
712 if topo.len() != names.len() {
713 return None;
714 }
715
716 let n = topo.len();
717 let mut ranks = HashMap::new();
718 for (idx, name) in topo.into_iter().enumerate() {
719 ranks.insert(name, n - idx);
720 }
721 Some(ranks)
722}
723
724fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
725 let Some((string_param, string_ty)) = fd.params.first() else {
726 return false;
727 };
728 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
729 return false;
730 };
731 if string_ty != "String" || pos_ty != "Int" {
732 return false;
733 }
734
735 let recursive_calls: Vec<(Option<&Expr>, Option<&Expr>)> =
736 collect_calls_from_body(fd.body.as_ref())
737 .into_iter()
738 .filter(|(name, _)| call_matches(name, &fd.name))
739 .map(|(_, args)| (args.first().copied(), args.get(1).copied()))
740 .collect();
741 if recursive_calls.is_empty() {
742 return false;
743 }
744
745 recursive_calls.into_iter().all(|(arg0, arg1)| {
746 arg0.is_some_and(|e| is_ident(e, string_param))
747 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
748 })
749}
750
751fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
752 if component.len() < 2 {
753 return false;
754 }
755 if component
756 .iter()
757 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
758 {
759 return false;
760 }
761 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
762 let mut any_intra = false;
763 for fd in component {
764 let param_name = &fd.params[0].0;
765 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
766 if !call_is_in_set(&callee, &names) {
767 continue;
768 }
769 any_intra = true;
770 let Some(arg0) = args.first().copied() else {
771 return false;
772 };
773 if !is_int_minus_one(arg0, param_name) {
774 return false;
775 }
776 }
777 }
778 any_intra
779}
780
781fn supports_mutual_string_pos_advance(component: &[&FnDef]) -> Option<HashMap<String, usize>> {
782 if component.len() < 2 {
783 return None;
784 }
785 if component.iter().any(|fd| {
786 !matches!(fd.params.first(), Some((_, t)) if t == "String")
787 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
788 }) {
789 return None;
790 }
791
792 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
793 let mut same_edges: HashMap<String, HashSet<String>> =
794 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
795 let mut any_intra = false;
796
797 for fd in component {
798 let string_param = &fd.params[0].0;
799 let pos_param = &fd.params[1].0;
800 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
801 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
802 continue;
803 };
804 any_intra = true;
805
806 let Some(arg0) = args.first().copied() else {
807 return None;
808 };
809 let Some(arg1) = args.get(1).copied() else {
810 return None;
811 };
812
813 if !is_ident(arg0, string_param) {
814 return None;
815 }
816
817 match classify_string_pos_edge(arg1, string_param, pos_param) {
818 Some(StringPosEdge::Same) => {
819 if let Some(edges) = same_edges.get_mut(&fd.name) {
820 edges.insert(callee);
821 } else {
822 return None;
823 }
824 }
825 Some(StringPosEdge::Advance) => {}
826 None => return None,
827 }
828 }
829 }
830
831 if !any_intra {
832 return None;
833 }
834
835 ranks_from_same_edges(&names, &same_edges)
836}
837
838fn metric_param_index_for_fn(fd: &FnDef) -> usize {
839 if fd.params.len() >= 2
840 && fd.params[0].1 == "String"
841 && (fd.params[1].1 == "Int"
842 || fd.params[1].1 == "List"
843 || fd.params[1].1.starts_with("List<"))
844 {
845 return 1;
846 }
847 0
848}
849
850fn supports_mutual_sizeof_ranked(component: &[&FnDef]) -> Option<HashMap<String, (usize, usize)>> {
851 if component.len() < 2 {
852 return None;
853 }
854 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
855 let metric_idx: HashMap<String, usize> = component
856 .iter()
857 .map(|fd| (fd.name.clone(), metric_param_index_for_fn(fd)))
858 .collect();
859 if component.iter().any(|fd| {
860 let idx = metric_idx.get(&fd.name).copied().unwrap_or(usize::MAX);
861 idx >= fd.params.len()
862 }) {
863 return None;
864 }
865
866 let mut same_edges: HashMap<String, HashSet<String>> =
867 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
868 let mut any_intra = false;
869 for fd in component {
870 let caller_metric_idx = metric_idx.get(&fd.name).copied()?;
871 let caller_metric_param = &fd.params[caller_metric_idx].0;
872 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
873 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
874 continue;
875 };
876 any_intra = true;
877 let callee_metric_idx = metric_idx.get(&callee).copied()?;
878 let Some(metric_arg) = args.get(callee_metric_idx).copied() else {
879 return None;
880 };
881 if is_ident(metric_arg, caller_metric_param) {
882 if let Some(edges) = same_edges.get_mut(&fd.name) {
883 edges.insert(callee);
884 } else {
885 return None;
886 }
887 }
888 }
889 }
890 if !any_intra {
891 return None;
892 }
893
894 let ranks = ranks_from_same_edges(&names, &same_edges)?;
895 let mut out = HashMap::new();
896 for fd in component {
897 let rank = ranks.get(&fd.name).copied()?;
898 let idx = metric_idx.get(&fd.name).copied()?;
899 out.insert(fd.name.clone(), (rank, idx));
900 }
901 Some(out)
902}
903
904fn proof_mode_recursion_analysis(
905 ctx: &CodegenContext,
906) -> (HashMap<String, RecursionPlan>, Vec<String>) {
907 let mut plans = HashMap::new();
908 let mut issues = Vec::new();
909
910 let all_pure = pure_fns(ctx);
911 let recursive_names = recursive_pure_fn_names(ctx);
912 let components = call_graph::ordered_fn_components(&all_pure);
913
914 for component in components {
915 if component.is_empty() {
916 continue;
917 }
918 let is_recursive_component =
919 component.len() > 1 || recursive_names.contains(&component[0].name);
920 if !is_recursive_component {
921 continue;
922 }
923
924 if component.len() > 1 {
925 if supports_mutual_int_countdown(&component) {
926 for fd in &component {
927 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
928 }
929 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
930 for fd in &component {
931 if let Some(rank) = ranks.get(&fd.name).copied() {
932 plans.insert(
933 fd.name.clone(),
934 RecursionPlan::MutualStringPosAdvance { rank },
935 );
936 } else {
937 issues.push(format!(
938 "internal proof-mode error: missing mutual String+pos rank for function '{}'",
939 fd.name
940 ));
941 }
942 }
943 } else if let Some(rank_idx) = supports_mutual_sizeof_ranked(&component) {
944 for fd in &component {
945 if let Some((rank, metric_param_index)) = rank_idx.get(&fd.name).copied() {
946 plans.insert(
947 fd.name.clone(),
948 RecursionPlan::MutualSizeOfRanked {
949 rank,
950 metric_param_index,
951 },
952 );
953 } else {
954 issues.push(format!(
955 "internal proof-mode error: missing mutual sizeOf plan for function '{}'",
956 fd.name
957 ));
958 }
959 }
960 } else {
961 let names = component
962 .iter()
963 .map(|fd| fd.name.clone())
964 .collect::<Vec<_>>()
965 .join(", ");
966 issues.push(format!(
967 "unsupported mutual recursion group (supported: Int countdown on first param, String+pos advance, or ranked-sizeOf mutual plan): {}",
968 names
969 ));
970 }
971 continue;
972 }
973
974 let fd = component[0];
975 if supports_single_int_countdown(fd) {
976 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown);
977 } else if supports_single_list_structural(fd) {
978 plans.insert(fd.name.clone(), RecursionPlan::ListStructural);
979 } else if supports_single_string_pos_advance(fd) {
980 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
981 } else {
982 issues.push(format!(
983 "recursive function '{}' is outside proof subset (supported: Int countdown, List structural, String+pos advance, or supported mutual recursion class)",
984 fd.name
985 ));
986 }
987 }
988
989 (plans, issues)
990}
991
992pub fn proof_mode_issues(ctx: &CodegenContext) -> Vec<String> {
997 let (_plans, issues) = proof_mode_recursion_analysis(ctx);
998 issues
999}
1000
1001pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
1003 transpile_with_verify_mode(ctx, VerifyEmitMode::NativeDecide)
1004}
1005
1006pub fn transpile_for_proof_mode(
1011 ctx: &CodegenContext,
1012 verify_mode: VerifyEmitMode,
1013) -> ProjectOutput {
1014 let (plans, _issues) = proof_mode_recursion_analysis(ctx);
1015
1016 let mut sections = Vec::new();
1017 sections.push(generate_prelude());
1018 sections.push(String::new());
1019
1020 for module in &ctx.modules {
1022 for td in &module.type_defs {
1023 sections.push(toplevel::emit_type_def(td));
1024 if toplevel::is_recursive_type_def(td) {
1025 sections.push(toplevel::emit_recursive_decidable_eq(
1026 toplevel::type_def_name(td),
1027 ));
1028 }
1029 sections.push(String::new());
1030 }
1031 }
1032 for td in &ctx.type_defs {
1033 sections.push(toplevel::emit_type_def(td));
1034 if toplevel::is_recursive_type_def(td) {
1035 sections.push(toplevel::emit_recursive_decidable_eq(
1036 toplevel::type_def_name(td),
1037 ));
1038 }
1039 sections.push(String::new());
1040 }
1041
1042 emit_pure_functions_proof(ctx, &plans, &mut sections);
1043
1044 for item in &ctx.items {
1045 if let TopLevel::Decision(db) = item {
1046 sections.push(toplevel::emit_decision(db));
1047 sections.push(String::new());
1048 }
1049 }
1050
1051 let mut verify_case_counters: HashMap<String, usize> = HashMap::new();
1052 for item in &ctx.items {
1053 if let TopLevel::Verify(vb) = item {
1054 let key = verify_counter_key(vb);
1055 let start_idx = *verify_case_counters.get(&key).unwrap_or(&0);
1056 let (emitted, next_idx) = toplevel::emit_verify_block(vb, ctx, verify_mode, start_idx);
1057 verify_case_counters.insert(key, next_idx);
1058 sections.push(emitted);
1059 sections.push(String::new());
1060 }
1061 }
1062
1063 let lean_source = sections.join("\n");
1064 let project_name = capitalize_first(&ctx.project_name);
1065 let lakefile = generate_lakefile(&project_name);
1066 let toolchain = generate_toolchain();
1067 ProjectOutput {
1068 files: vec![
1069 ("lakefile.lean".to_string(), lakefile),
1070 ("lean-toolchain".to_string(), toolchain),
1071 (format!("{}.lean", project_name), lean_source),
1072 ],
1073 }
1074}
1075
1076pub fn transpile_with_verify_mode(
1082 ctx: &CodegenContext,
1083 verify_mode: VerifyEmitMode,
1084) -> ProjectOutput {
1085 let mut sections = Vec::new();
1086
1087 sections.push(generate_prelude());
1089 sections.push(String::new());
1090
1091 let recursive_fns = call_graph::find_recursive_fns(&ctx.items);
1093
1094 for module in &ctx.modules {
1096 for td in &module.type_defs {
1097 sections.push(toplevel::emit_type_def(td));
1098 if toplevel::is_recursive_type_def(td) {
1100 sections.push(toplevel::emit_recursive_decidable_eq(
1101 toplevel::type_def_name(td),
1102 ));
1103 }
1104 sections.push(String::new());
1105 }
1106 }
1107
1108 for td in &ctx.type_defs {
1110 sections.push(toplevel::emit_type_def(td));
1111 if toplevel::is_recursive_type_def(td) {
1113 sections.push(toplevel::emit_recursive_decidable_eq(
1114 toplevel::type_def_name(td),
1115 ));
1116 }
1117 sections.push(String::new());
1118 }
1119
1120 emit_pure_functions(ctx, &recursive_fns, &mut sections);
1123
1124 for item in &ctx.items {
1126 if let TopLevel::Decision(db) = item {
1127 sections.push(toplevel::emit_decision(db));
1128 sections.push(String::new());
1129 }
1130 }
1131
1132 let mut verify_case_counters: HashMap<String, usize> = HashMap::new();
1134 for item in &ctx.items {
1135 if let TopLevel::Verify(vb) = item {
1136 let key = verify_counter_key(vb);
1137 let start_idx = *verify_case_counters.get(&key).unwrap_or(&0);
1138 let (emitted, next_idx) = toplevel::emit_verify_block(vb, ctx, verify_mode, start_idx);
1139 verify_case_counters.insert(key, next_idx);
1140 sections.push(emitted);
1141 sections.push(String::new());
1142 }
1143 }
1144
1145 let lean_source = sections.join("\n");
1146
1147 let project_name = capitalize_first(&ctx.project_name);
1149 let lakefile = generate_lakefile(&project_name);
1150 let toolchain = generate_toolchain();
1151
1152 ProjectOutput {
1153 files: vec![
1154 ("lakefile.lean".to_string(), lakefile),
1155 ("lean-toolchain".to_string(), toolchain),
1156 (format!("{}.lean", project_name), lean_source),
1157 ],
1158 }
1159}
1160
1161fn emit_pure_functions(
1162 ctx: &CodegenContext,
1163 recursive_fns: &HashSet<String>,
1164 sections: &mut Vec<String>,
1165) {
1166 let all_fns: Vec<&FnDef> = ctx
1167 .modules
1168 .iter()
1169 .flat_map(|m| m.fn_defs.iter())
1170 .chain(ctx.fn_defs.iter())
1171 .filter(|fd| toplevel::is_pure_fn(fd))
1172 .collect();
1173 if all_fns.is_empty() {
1174 return;
1175 }
1176
1177 let components = call_graph::ordered_fn_components(&all_fns);
1178 for fns in components {
1179 if fns.is_empty() {
1180 continue;
1181 }
1182
1183 if fns.len() > 1 {
1185 sections.push(toplevel::emit_mutual_group(&fns, ctx));
1186 sections.push(String::new());
1187 continue;
1188 }
1189
1190 if let Some(code) = toplevel::emit_fn_def(fns[0], recursive_fns, ctx) {
1193 sections.push(code);
1194 sections.push(String::new());
1195 }
1196 }
1197}
1198
1199fn emit_pure_functions_proof(
1200 ctx: &CodegenContext,
1201 plans: &HashMap<String, RecursionPlan>,
1202 sections: &mut Vec<String>,
1203) {
1204 let all_fns: Vec<&FnDef> = pure_fns(ctx);
1205 if all_fns.is_empty() {
1206 return;
1207 }
1208
1209 let components = call_graph::ordered_fn_components(&all_fns);
1210 for fns in components {
1211 if fns.is_empty() {
1212 continue;
1213 }
1214 if fns.len() > 1 {
1215 sections.push(toplevel::emit_mutual_group_proof(&fns, ctx, plans));
1216 sections.push(String::new());
1217 continue;
1218 }
1219
1220 if let Some(code) =
1221 toplevel::emit_fn_def_proof(fns[0], plans.get(&fns[0].name).copied(), ctx)
1222 {
1223 sections.push(code);
1224 sections.push(String::new());
1225 }
1226 }
1227}
1228
1229fn generate_prelude() -> String {
1230 [
1231 LEAN_PRELUDE_HEADER,
1232 LEAN_PRELUDE_FLOAT_COE,
1233 LEAN_PRELUDE_FLOAT_DEC_EQ,
1234 LEAN_PRELUDE_EXCEPT_DEC_EQ,
1235 LEAN_PRELUDE_EXCEPT_NS,
1236 LEAN_PRELUDE_OPTION_TO_EXCEPT,
1237 LEAN_PRELUDE_STRING_HADD,
1238 AVER_MAP_PRELUDE,
1239 LEAN_PRELUDE_AVER_LIST,
1240 LEAN_PRELUDE_STRING_HELPERS,
1241 LEAN_PRELUDE_NUMERIC_PARSE,
1242 LEAN_PRELUDE_CHAR_BYTE,
1243 ]
1244 .join("\n\n")
1245}
1246
1247fn generate_lakefile(project_name: &str) -> String {
1248 format!(
1249 r#"import Lake
1250open Lake DSL
1251
1252package «{}» where
1253 version := v!"0.1.0"
1254
1255@[default_target]
1256lean_lib «{}» where
1257 srcDir := "."
1258"#,
1259 project_name.to_lowercase(),
1260 project_name
1261 )
1262}
1263
1264fn generate_toolchain() -> String {
1265 "leanprover/lean4:v4.15.0\n".to_string()
1266}
1267
1268fn capitalize_first(s: &str) -> String {
1269 let mut chars = s.chars();
1270 match chars.next() {
1271 None => String::new(),
1272 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::{
1279 VerifyEmitMode, generate_prelude, proof_mode_issues, transpile, transpile_for_proof_mode,
1280 transpile_with_verify_mode,
1281 };
1282 use crate::ast::{
1283 BinOp, Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Stmt, TopLevel, TypeDef,
1284 TypeVariant, VerifyBlock, VerifyGiven, VerifyGivenDomain, VerifyKind, VerifyLaw,
1285 };
1286 use crate::codegen::CodegenContext;
1287 use std::collections::{HashMap, HashSet};
1288 use std::rc::Rc;
1289
1290 fn empty_ctx() -> CodegenContext {
1291 CodegenContext {
1292 items: vec![],
1293 fn_sigs: HashMap::new(),
1294 memo_fns: HashSet::new(),
1295 memo_safe_types: HashSet::new(),
1296 type_defs: vec![],
1297 fn_defs: vec![],
1298 project_name: "verify_mode".to_string(),
1299 modules: vec![],
1300 module_prefixes: HashSet::new(),
1301 }
1302 }
1303
1304 fn empty_ctx_with_verify_case() -> CodegenContext {
1305 let mut ctx = empty_ctx();
1306 ctx.items.push(TopLevel::Verify(VerifyBlock {
1307 fn_name: "f".to_string(),
1308 line: 1,
1309 cases: vec![(
1310 Expr::Literal(Literal::Int(1)),
1311 Expr::Literal(Literal::Int(1)),
1312 )],
1313 kind: VerifyKind::Cases,
1314 }));
1315 ctx
1316 }
1317
1318 fn empty_ctx_with_two_verify_blocks_same_fn() -> CodegenContext {
1319 let mut ctx = empty_ctx();
1320 ctx.items.push(TopLevel::Verify(VerifyBlock {
1321 fn_name: "f".to_string(),
1322 line: 1,
1323 cases: vec![(
1324 Expr::Literal(Literal::Int(1)),
1325 Expr::Literal(Literal::Int(1)),
1326 )],
1327 kind: VerifyKind::Cases,
1328 }));
1329 ctx.items.push(TopLevel::Verify(VerifyBlock {
1330 fn_name: "f".to_string(),
1331 line: 2,
1332 cases: vec![(
1333 Expr::Literal(Literal::Int(2)),
1334 Expr::Literal(Literal::Int(2)),
1335 )],
1336 kind: VerifyKind::Cases,
1337 }));
1338 ctx
1339 }
1340
1341 fn empty_ctx_with_verify_law() -> CodegenContext {
1342 let mut ctx = empty_ctx();
1343 let add = FnDef {
1344 name: "add".to_string(),
1345 line: 1,
1346 params: vec![
1347 ("a".to_string(), "Int".to_string()),
1348 ("b".to_string(), "Int".to_string()),
1349 ],
1350 return_type: "Int".to_string(),
1351 effects: vec![],
1352 desc: None,
1353 body: Rc::new(FnBody::Expr(Expr::BinOp(
1354 BinOp::Add,
1355 Box::new(Expr::Ident("a".to_string())),
1356 Box::new(Expr::Ident("b".to_string())),
1357 ))),
1358 resolution: None,
1359 };
1360 ctx.fn_defs.push(add.clone());
1361 ctx.items.push(TopLevel::FnDef(add));
1362 ctx.items.push(TopLevel::Verify(VerifyBlock {
1363 fn_name: "add".to_string(),
1364 line: 1,
1365 cases: vec![
1366 (
1367 Expr::FnCall(
1368 Box::new(Expr::Ident("add".to_string())),
1369 vec![
1370 Expr::Literal(Literal::Int(1)),
1371 Expr::Literal(Literal::Int(2)),
1372 ],
1373 ),
1374 Expr::FnCall(
1375 Box::new(Expr::Ident("add".to_string())),
1376 vec![
1377 Expr::Literal(Literal::Int(2)),
1378 Expr::Literal(Literal::Int(1)),
1379 ],
1380 ),
1381 ),
1382 (
1383 Expr::FnCall(
1384 Box::new(Expr::Ident("add".to_string())),
1385 vec![
1386 Expr::Literal(Literal::Int(2)),
1387 Expr::Literal(Literal::Int(3)),
1388 ],
1389 ),
1390 Expr::FnCall(
1391 Box::new(Expr::Ident("add".to_string())),
1392 vec![
1393 Expr::Literal(Literal::Int(3)),
1394 Expr::Literal(Literal::Int(2)),
1395 ],
1396 ),
1397 ),
1398 ],
1399 kind: VerifyKind::Law(VerifyLaw {
1400 name: "commutative".to_string(),
1401 givens: vec![
1402 VerifyGiven {
1403 name: "a".to_string(),
1404 type_name: "Int".to_string(),
1405 domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
1406 },
1407 VerifyGiven {
1408 name: "b".to_string(),
1409 type_name: "Int".to_string(),
1410 domain: VerifyGivenDomain::Explicit(vec![
1411 Expr::Literal(Literal::Int(2)),
1412 Expr::Literal(Literal::Int(3)),
1413 ]),
1414 },
1415 ],
1416 lhs: Expr::FnCall(
1417 Box::new(Expr::Ident("add".to_string())),
1418 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
1419 ),
1420 rhs: Expr::FnCall(
1421 Box::new(Expr::Ident("add".to_string())),
1422 vec![Expr::Ident("b".to_string()), Expr::Ident("a".to_string())],
1423 ),
1424 }),
1425 }));
1426 ctx
1427 }
1428
1429 #[test]
1430 fn prelude_normalizes_float_string_format() {
1431 let prelude = generate_prelude();
1432 assert!(
1433 prelude.contains("private def normalizeFloatString (s : String) : String :="),
1434 "missing normalizeFloatString helper in prelude"
1435 );
1436 assert!(
1437 prelude.contains(
1438 "def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)"
1439 ),
1440 "String.fromFloat should normalize Lean float formatting"
1441 );
1442 }
1443
1444 #[test]
1445 fn prelude_validates_char_from_code_unicode_bounds() {
1446 let prelude = generate_prelude();
1447 assert!(
1448 prelude.contains("if n < 0 || n > 1114111 then none"),
1449 "Char.fromCode should reject code points above Unicode max"
1450 );
1451 assert!(
1452 prelude.contains("else if n >= 55296 && n <= 57343 then none"),
1453 "Char.fromCode should reject surrogate code points"
1454 );
1455 }
1456
1457 #[test]
1458 fn prelude_includes_map_set_helper_lemmas() {
1459 let prelude = generate_prelude();
1460 assert!(
1461 prelude.contains("theorem has_set_self [BEq α]"),
1462 "missing AverMap.has_set_self helper theorem"
1463 );
1464 assert!(
1465 prelude.contains("theorem get_set_self [BEq α]"),
1466 "missing AverMap.get_set_self helper theorem"
1467 );
1468 }
1469
1470 #[test]
1471 fn transpile_emits_native_decide_for_verify_by_default() {
1472 let out = transpile(&empty_ctx_with_verify_case());
1473 let lean = out
1474 .files
1475 .iter()
1476 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1477 .expect("expected generated Lean file");
1478 assert!(lean.contains("example : 1 = 1 := by native_decide"));
1479 }
1480
1481 #[test]
1482 fn transpile_can_emit_sorry_for_verify_when_requested() {
1483 let out = transpile_with_verify_mode(&empty_ctx_with_verify_case(), VerifyEmitMode::Sorry);
1484 let lean = out
1485 .files
1486 .iter()
1487 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1488 .expect("expected generated Lean file");
1489 assert!(lean.contains("example : 1 = 1 := by sorry"));
1490 }
1491
1492 #[test]
1493 fn transpile_can_emit_theorem_skeletons_for_verify() {
1494 let out = transpile_with_verify_mode(
1495 &empty_ctx_with_verify_case(),
1496 VerifyEmitMode::TheoremSkeleton,
1497 );
1498 let lean = out
1499 .files
1500 .iter()
1501 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1502 .expect("expected generated Lean file");
1503 assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
1504 assert!(lean.contains(" sorry"));
1505 }
1506
1507 #[test]
1508 fn theorem_skeleton_numbering_is_global_per_function_across_verify_blocks() {
1509 let out = transpile_with_verify_mode(
1510 &empty_ctx_with_two_verify_blocks_same_fn(),
1511 VerifyEmitMode::TheoremSkeleton,
1512 );
1513 let lean = out
1514 .files
1515 .iter()
1516 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1517 .expect("expected generated Lean file");
1518 assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
1519 assert!(lean.contains("theorem f_verify_2 : 2 = 2 := by"));
1520 }
1521
1522 #[test]
1523 fn transpile_emits_named_theorems_for_verify_law() {
1524 let out = transpile(&empty_ctx_with_verify_law());
1525 let lean = out
1526 .files
1527 .iter()
1528 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1529 .expect("expected generated Lean file");
1530 assert!(lean.contains("-- verify law add.commutative (2 cases)"));
1531 assert!(lean.contains("-- given a: Int = 1..2"));
1532 assert!(lean.contains("-- given b: Int = [2, 3]"));
1533 assert!(lean.contains(
1534 "theorem add_law_commutative : ∀ (a : Int) (b : Int), add a b = add b a := by"
1535 ));
1536 assert!(lean.contains(" intro a b"));
1537 assert!(lean.contains(" simp [add, Int.add_comm]"));
1538 assert!(lean.contains(
1539 "theorem add_law_commutative_sample_1 : add 1 2 = add 2 1 := by native_decide"
1540 ));
1541 assert!(lean.contains(
1542 "theorem add_law_commutative_sample_2 : add 2 3 = add 3 2 := by native_decide"
1543 ));
1544 }
1545
1546 #[test]
1547 fn transpile_auto_proves_reflexive_law_with_rfl() {
1548 let mut ctx = empty_ctx();
1549 ctx.items.push(TopLevel::Verify(VerifyBlock {
1550 fn_name: "idLaw".to_string(),
1551 line: 1,
1552 cases: vec![(
1553 Expr::Literal(Literal::Int(1)),
1554 Expr::Literal(Literal::Int(1)),
1555 )],
1556 kind: VerifyKind::Law(VerifyLaw {
1557 name: "reflexive".to_string(),
1558 givens: vec![VerifyGiven {
1559 name: "x".to_string(),
1560 type_name: "Int".to_string(),
1561 domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
1562 }],
1563 lhs: Expr::Ident("x".to_string()),
1564 rhs: Expr::Ident("x".to_string()),
1565 }),
1566 }));
1567 let out = transpile(&ctx);
1568 let lean = out
1569 .files
1570 .iter()
1571 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1572 .expect("expected generated Lean file");
1573 assert!(lean.contains("theorem idLaw_law_reflexive : ∀ (x : Int), x = x := by"));
1574 assert!(lean.contains(" intro x"));
1575 assert!(lean.contains(" rfl"));
1576 }
1577
1578 #[test]
1579 fn transpile_auto_proves_identity_law_for_int_add_wrapper() {
1580 let mut ctx = empty_ctx_with_verify_law();
1581 ctx.items.push(TopLevel::Verify(VerifyBlock {
1582 fn_name: "add".to_string(),
1583 line: 10,
1584 cases: vec![(
1585 Expr::FnCall(
1586 Box::new(Expr::Ident("add".to_string())),
1587 vec![
1588 Expr::Literal(Literal::Int(1)),
1589 Expr::Literal(Literal::Int(0)),
1590 ],
1591 ),
1592 Expr::Literal(Literal::Int(1)),
1593 )],
1594 kind: VerifyKind::Law(VerifyLaw {
1595 name: "identityZero".to_string(),
1596 givens: vec![VerifyGiven {
1597 name: "a".to_string(),
1598 type_name: "Int".to_string(),
1599 domain: VerifyGivenDomain::Explicit(vec![
1600 Expr::Literal(Literal::Int(0)),
1601 Expr::Literal(Literal::Int(1)),
1602 ]),
1603 }],
1604 lhs: Expr::FnCall(
1605 Box::new(Expr::Ident("add".to_string())),
1606 vec![Expr::Ident("a".to_string()), Expr::Literal(Literal::Int(0))],
1607 ),
1608 rhs: Expr::Ident("a".to_string()),
1609 }),
1610 }));
1611 let out = transpile(&ctx);
1612 let lean = out
1613 .files
1614 .iter()
1615 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1616 .expect("expected generated Lean file");
1617 assert!(lean.contains("theorem add_law_identityZero : ∀ (a : Int), add a 0 = a := by"));
1618 assert!(lean.contains(" intro a"));
1619 assert!(lean.contains(" simp [add]"));
1620 }
1621
1622 #[test]
1623 fn transpile_auto_proves_associative_law_for_int_add_wrapper() {
1624 let mut ctx = empty_ctx_with_verify_law();
1625 ctx.items.push(TopLevel::Verify(VerifyBlock {
1626 fn_name: "add".to_string(),
1627 line: 20,
1628 cases: vec![(
1629 Expr::FnCall(
1630 Box::new(Expr::Ident("add".to_string())),
1631 vec![
1632 Expr::FnCall(
1633 Box::new(Expr::Ident("add".to_string())),
1634 vec![
1635 Expr::Literal(Literal::Int(1)),
1636 Expr::Literal(Literal::Int(2)),
1637 ],
1638 ),
1639 Expr::Literal(Literal::Int(3)),
1640 ],
1641 ),
1642 Expr::FnCall(
1643 Box::new(Expr::Ident("add".to_string())),
1644 vec![
1645 Expr::Literal(Literal::Int(1)),
1646 Expr::FnCall(
1647 Box::new(Expr::Ident("add".to_string())),
1648 vec![
1649 Expr::Literal(Literal::Int(2)),
1650 Expr::Literal(Literal::Int(3)),
1651 ],
1652 ),
1653 ],
1654 ),
1655 )],
1656 kind: VerifyKind::Law(VerifyLaw {
1657 name: "associative".to_string(),
1658 givens: vec![
1659 VerifyGiven {
1660 name: "a".to_string(),
1661 type_name: "Int".to_string(),
1662 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
1663 },
1664 VerifyGiven {
1665 name: "b".to_string(),
1666 type_name: "Int".to_string(),
1667 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1668 },
1669 VerifyGiven {
1670 name: "c".to_string(),
1671 type_name: "Int".to_string(),
1672 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(3))]),
1673 },
1674 ],
1675 lhs: Expr::FnCall(
1676 Box::new(Expr::Ident("add".to_string())),
1677 vec![
1678 Expr::FnCall(
1679 Box::new(Expr::Ident("add".to_string())),
1680 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
1681 ),
1682 Expr::Ident("c".to_string()),
1683 ],
1684 ),
1685 rhs: Expr::FnCall(
1686 Box::new(Expr::Ident("add".to_string())),
1687 vec![
1688 Expr::Ident("a".to_string()),
1689 Expr::FnCall(
1690 Box::new(Expr::Ident("add".to_string())),
1691 vec![Expr::Ident("b".to_string()), Expr::Ident("c".to_string())],
1692 ),
1693 ],
1694 ),
1695 }),
1696 }));
1697 let out = transpile(&ctx);
1698 let lean = out
1699 .files
1700 .iter()
1701 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1702 .expect("expected generated Lean file");
1703 assert!(lean.contains(
1704 "theorem add_law_associative : ∀ (a : Int) (b : Int) (c : Int), add (add a b) c = add a (add b c) := by"
1705 ));
1706 assert!(lean.contains(" intro a b c"));
1707 assert!(lean.contains(" simp [add, Int.add_assoc]"));
1708 }
1709
1710 #[test]
1711 fn transpile_auto_proves_sub_laws() {
1712 let mut ctx = empty_ctx();
1713 let sub = FnDef {
1714 name: "sub".to_string(),
1715 line: 1,
1716 params: vec![
1717 ("a".to_string(), "Int".to_string()),
1718 ("b".to_string(), "Int".to_string()),
1719 ],
1720 return_type: "Int".to_string(),
1721 effects: vec![],
1722 desc: None,
1723 body: Rc::new(FnBody::Expr(Expr::BinOp(
1724 BinOp::Sub,
1725 Box::new(Expr::Ident("a".to_string())),
1726 Box::new(Expr::Ident("b".to_string())),
1727 ))),
1728 resolution: None,
1729 };
1730 ctx.fn_defs.push(sub.clone());
1731 ctx.items.push(TopLevel::FnDef(sub));
1732
1733 ctx.items.push(TopLevel::Verify(VerifyBlock {
1734 fn_name: "sub".to_string(),
1735 line: 10,
1736 cases: vec![(
1737 Expr::FnCall(
1738 Box::new(Expr::Ident("sub".to_string())),
1739 vec![
1740 Expr::Literal(Literal::Int(2)),
1741 Expr::Literal(Literal::Int(0)),
1742 ],
1743 ),
1744 Expr::Literal(Literal::Int(2)),
1745 )],
1746 kind: VerifyKind::Law(VerifyLaw {
1747 name: "rightIdentity".to_string(),
1748 givens: vec![VerifyGiven {
1749 name: "a".to_string(),
1750 type_name: "Int".to_string(),
1751 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1752 }],
1753 lhs: Expr::FnCall(
1754 Box::new(Expr::Ident("sub".to_string())),
1755 vec![Expr::Ident("a".to_string()), Expr::Literal(Literal::Int(0))],
1756 ),
1757 rhs: Expr::Ident("a".to_string()),
1758 }),
1759 }));
1760 ctx.items.push(TopLevel::Verify(VerifyBlock {
1761 fn_name: "sub".to_string(),
1762 line: 20,
1763 cases: vec![(
1764 Expr::FnCall(
1765 Box::new(Expr::Ident("sub".to_string())),
1766 vec![
1767 Expr::Literal(Literal::Int(2)),
1768 Expr::Literal(Literal::Int(1)),
1769 ],
1770 ),
1771 Expr::BinOp(
1772 BinOp::Sub,
1773 Box::new(Expr::Literal(Literal::Int(0))),
1774 Box::new(Expr::FnCall(
1775 Box::new(Expr::Ident("sub".to_string())),
1776 vec![
1777 Expr::Literal(Literal::Int(1)),
1778 Expr::Literal(Literal::Int(2)),
1779 ],
1780 )),
1781 ),
1782 )],
1783 kind: VerifyKind::Law(VerifyLaw {
1784 name: "antiCommutative".to_string(),
1785 givens: vec![
1786 VerifyGiven {
1787 name: "a".to_string(),
1788 type_name: "Int".to_string(),
1789 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1790 },
1791 VerifyGiven {
1792 name: "b".to_string(),
1793 type_name: "Int".to_string(),
1794 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
1795 },
1796 ],
1797 lhs: Expr::FnCall(
1798 Box::new(Expr::Ident("sub".to_string())),
1799 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
1800 ),
1801 rhs: Expr::BinOp(
1802 BinOp::Sub,
1803 Box::new(Expr::Literal(Literal::Int(0))),
1804 Box::new(Expr::FnCall(
1805 Box::new(Expr::Ident("sub".to_string())),
1806 vec![Expr::Ident("b".to_string()), Expr::Ident("a".to_string())],
1807 )),
1808 ),
1809 }),
1810 }));
1811
1812 let out = transpile(&ctx);
1813 let lean = out
1814 .files
1815 .iter()
1816 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1817 .expect("expected generated Lean file");
1818 assert!(lean.contains("theorem sub_law_rightIdentity : ∀ (a : Int), sub a 0 = a := by"));
1819 assert!(lean.contains(" simp [sub]"));
1820 assert!(lean.contains(
1821 "theorem sub_law_antiCommutative : ∀ (a : Int) (b : Int), sub a b = (-sub b a) := by"
1822 ));
1823 assert!(lean.contains(" simp [sub, sub_eq_add_neg, add_comm, add_left_comm, add_assoc]"));
1824 }
1825
1826 #[test]
1827 fn transpile_auto_proves_unary_wrapper_equivalence_law() {
1828 let mut ctx = empty_ctx();
1829 let add = FnDef {
1830 name: "add".to_string(),
1831 line: 1,
1832 params: vec![
1833 ("a".to_string(), "Int".to_string()),
1834 ("b".to_string(), "Int".to_string()),
1835 ],
1836 return_type: "Int".to_string(),
1837 effects: vec![],
1838 desc: None,
1839 body: Rc::new(FnBody::Expr(Expr::BinOp(
1840 BinOp::Add,
1841 Box::new(Expr::Ident("a".to_string())),
1842 Box::new(Expr::Ident("b".to_string())),
1843 ))),
1844 resolution: None,
1845 };
1846 let add_one = FnDef {
1847 name: "addOne".to_string(),
1848 line: 2,
1849 params: vec![("n".to_string(), "Int".to_string())],
1850 return_type: "Int".to_string(),
1851 effects: vec![],
1852 desc: None,
1853 body: Rc::new(FnBody::Expr(Expr::BinOp(
1854 BinOp::Add,
1855 Box::new(Expr::Ident("n".to_string())),
1856 Box::new(Expr::Literal(Literal::Int(1))),
1857 ))),
1858 resolution: None,
1859 };
1860 ctx.fn_defs.push(add.clone());
1861 ctx.fn_defs.push(add_one.clone());
1862 ctx.items.push(TopLevel::FnDef(add));
1863 ctx.items.push(TopLevel::FnDef(add_one));
1864 ctx.items.push(TopLevel::Verify(VerifyBlock {
1865 fn_name: "addOne".to_string(),
1866 line: 3,
1867 cases: vec![(
1868 Expr::FnCall(
1869 Box::new(Expr::Ident("addOne".to_string())),
1870 vec![Expr::Literal(Literal::Int(2))],
1871 ),
1872 Expr::FnCall(
1873 Box::new(Expr::Ident("add".to_string())),
1874 vec![
1875 Expr::Literal(Literal::Int(2)),
1876 Expr::Literal(Literal::Int(1)),
1877 ],
1878 ),
1879 )],
1880 kind: VerifyKind::Law(VerifyLaw {
1881 name: "identityViaAdd".to_string(),
1882 givens: vec![VerifyGiven {
1883 name: "n".to_string(),
1884 type_name: "Int".to_string(),
1885 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1886 }],
1887 lhs: Expr::FnCall(
1888 Box::new(Expr::Ident("addOne".to_string())),
1889 vec![Expr::Ident("n".to_string())],
1890 ),
1891 rhs: Expr::FnCall(
1892 Box::new(Expr::Ident("add".to_string())),
1893 vec![Expr::Ident("n".to_string()), Expr::Literal(Literal::Int(1))],
1894 ),
1895 }),
1896 }));
1897 let out = transpile(&ctx);
1898 let lean = out
1899 .files
1900 .iter()
1901 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1902 .expect("expected generated Lean file");
1903 assert!(
1904 lean.contains(
1905 "theorem addOne_law_identityViaAdd : ∀ (n : Int), addOne n = add n 1 := by"
1906 )
1907 );
1908 assert!(lean.contains(" simp [addOne, add]"));
1909 }
1910
1911 #[test]
1912 fn transpile_auto_proves_direct_map_set_laws() {
1913 let mut ctx = empty_ctx();
1914
1915 let map_set = |m: Expr, k: Expr, v: Expr| {
1916 Expr::FnCall(
1917 Box::new(Expr::Attr(
1918 Box::new(Expr::Ident("Map".to_string())),
1919 "set".to_string(),
1920 )),
1921 vec![m, k, v],
1922 )
1923 };
1924 let map_has = |m: Expr, k: Expr| {
1925 Expr::FnCall(
1926 Box::new(Expr::Attr(
1927 Box::new(Expr::Ident("Map".to_string())),
1928 "has".to_string(),
1929 )),
1930 vec![m, k],
1931 )
1932 };
1933 let map_get = |m: Expr, k: Expr| {
1934 Expr::FnCall(
1935 Box::new(Expr::Attr(
1936 Box::new(Expr::Ident("Map".to_string())),
1937 "get".to_string(),
1938 )),
1939 vec![m, k],
1940 )
1941 };
1942 let some = |v: Expr| {
1943 Expr::FnCall(
1944 Box::new(Expr::Attr(
1945 Box::new(Expr::Ident("Option".to_string())),
1946 "Some".to_string(),
1947 )),
1948 vec![v],
1949 )
1950 };
1951
1952 ctx.items.push(TopLevel::Verify(VerifyBlock {
1953 fn_name: "map".to_string(),
1954 line: 1,
1955 cases: vec![(
1956 map_has(
1957 map_set(
1958 Expr::Ident("m".to_string()),
1959 Expr::Ident("k".to_string()),
1960 Expr::Ident("v".to_string()),
1961 ),
1962 Expr::Ident("k".to_string()),
1963 ),
1964 Expr::Literal(Literal::Bool(true)),
1965 )],
1966 kind: VerifyKind::Law(VerifyLaw {
1967 name: "setHasKey".to_string(),
1968 givens: vec![
1969 VerifyGiven {
1970 name: "m".to_string(),
1971 type_name: "Map<String, Int>".to_string(),
1972 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
1973 Box::new(Expr::Attr(
1974 Box::new(Expr::Ident("Map".to_string())),
1975 "empty".to_string(),
1976 )),
1977 vec![],
1978 )]),
1979 },
1980 VerifyGiven {
1981 name: "k".to_string(),
1982 type_name: "String".to_string(),
1983 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Str(
1984 "a".to_string(),
1985 ))]),
1986 },
1987 VerifyGiven {
1988 name: "v".to_string(),
1989 type_name: "Int".to_string(),
1990 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
1991 },
1992 ],
1993 lhs: map_has(
1994 map_set(
1995 Expr::Ident("m".to_string()),
1996 Expr::Ident("k".to_string()),
1997 Expr::Ident("v".to_string()),
1998 ),
1999 Expr::Ident("k".to_string()),
2000 ),
2001 rhs: Expr::Literal(Literal::Bool(true)),
2002 }),
2003 }));
2004
2005 ctx.items.push(TopLevel::Verify(VerifyBlock {
2006 fn_name: "map".to_string(),
2007 line: 2,
2008 cases: vec![(
2009 map_get(
2010 map_set(
2011 Expr::Ident("m".to_string()),
2012 Expr::Ident("k".to_string()),
2013 Expr::Ident("v".to_string()),
2014 ),
2015 Expr::Ident("k".to_string()),
2016 ),
2017 some(Expr::Ident("v".to_string())),
2018 )],
2019 kind: VerifyKind::Law(VerifyLaw {
2020 name: "setGetKey".to_string(),
2021 givens: vec![
2022 VerifyGiven {
2023 name: "m".to_string(),
2024 type_name: "Map<String, Int>".to_string(),
2025 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
2026 Box::new(Expr::Attr(
2027 Box::new(Expr::Ident("Map".to_string())),
2028 "empty".to_string(),
2029 )),
2030 vec![],
2031 )]),
2032 },
2033 VerifyGiven {
2034 name: "k".to_string(),
2035 type_name: "String".to_string(),
2036 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Str(
2037 "a".to_string(),
2038 ))]),
2039 },
2040 VerifyGiven {
2041 name: "v".to_string(),
2042 type_name: "Int".to_string(),
2043 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
2044 },
2045 ],
2046 lhs: map_get(
2047 map_set(
2048 Expr::Ident("m".to_string()),
2049 Expr::Ident("k".to_string()),
2050 Expr::Ident("v".to_string()),
2051 ),
2052 Expr::Ident("k".to_string()),
2053 ),
2054 rhs: some(Expr::Ident("v".to_string())),
2055 }),
2056 }));
2057
2058 let out = transpile(&ctx);
2059 let lean = out
2060 .files
2061 .iter()
2062 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2063 .expect("expected generated Lean file");
2064 assert!(lean.contains("simpa using AverMap.has_set_self m k v"));
2065 assert!(lean.contains("simpa using AverMap.get_set_self m k v"));
2066 }
2067
2068 #[test]
2069 fn transpile_auto_proves_map_update_laws() {
2070 let mut ctx = empty_ctx();
2071
2072 let map_get = |m: Expr, k: Expr| {
2073 Expr::FnCall(
2074 Box::new(Expr::Attr(
2075 Box::new(Expr::Ident("Map".to_string())),
2076 "get".to_string(),
2077 )),
2078 vec![m, k],
2079 )
2080 };
2081 let map_set = |m: Expr, k: Expr, v: Expr| {
2082 Expr::FnCall(
2083 Box::new(Expr::Attr(
2084 Box::new(Expr::Ident("Map".to_string())),
2085 "set".to_string(),
2086 )),
2087 vec![m, k, v],
2088 )
2089 };
2090 let map_has = |m: Expr, k: Expr| {
2091 Expr::FnCall(
2092 Box::new(Expr::Attr(
2093 Box::new(Expr::Ident("Map".to_string())),
2094 "has".to_string(),
2095 )),
2096 vec![m, k],
2097 )
2098 };
2099 let option_some = |v: Expr| {
2100 Expr::FnCall(
2101 Box::new(Expr::Attr(
2102 Box::new(Expr::Ident("Option".to_string())),
2103 "Some".to_string(),
2104 )),
2105 vec![v],
2106 )
2107 };
2108 let option_with_default = |opt: Expr, def: Expr| {
2109 Expr::FnCall(
2110 Box::new(Expr::Attr(
2111 Box::new(Expr::Ident("Option".to_string())),
2112 "withDefault".to_string(),
2113 )),
2114 vec![opt, def],
2115 )
2116 };
2117
2118 let add_one = FnDef {
2119 name: "addOne".to_string(),
2120 line: 1,
2121 params: vec![("n".to_string(), "Int".to_string())],
2122 return_type: "Int".to_string(),
2123 effects: vec![],
2124 desc: None,
2125 body: Rc::new(FnBody::Expr(Expr::BinOp(
2126 BinOp::Add,
2127 Box::new(Expr::Ident("n".to_string())),
2128 Box::new(Expr::Literal(Literal::Int(1))),
2129 ))),
2130 resolution: None,
2131 };
2132 ctx.fn_defs.push(add_one.clone());
2133 ctx.items.push(TopLevel::FnDef(add_one));
2134
2135 let inc_count = FnDef {
2136 name: "incCount".to_string(),
2137 line: 2,
2138 params: vec![
2139 ("counts".to_string(), "Map<String, Int>".to_string()),
2140 ("word".to_string(), "String".to_string()),
2141 ],
2142 return_type: "Map<String, Int>".to_string(),
2143 effects: vec![],
2144 desc: None,
2145 body: Rc::new(FnBody::Block(vec![
2146 Stmt::Binding(
2147 "current".to_string(),
2148 None,
2149 map_get(
2150 Expr::Ident("counts".to_string()),
2151 Expr::Ident("word".to_string()),
2152 ),
2153 ),
2154 Stmt::Expr(Expr::Match {
2155 subject: Box::new(Expr::Ident("current".to_string())),
2156 arms: vec![
2157 MatchArm {
2158 pattern: Pattern::Constructor(
2159 "Some".to_string(),
2160 vec!["n".to_string()],
2161 ),
2162 body: Box::new(map_set(
2163 Expr::Ident("counts".to_string()),
2164 Expr::Ident("word".to_string()),
2165 Expr::BinOp(
2166 BinOp::Add,
2167 Box::new(Expr::Ident("n".to_string())),
2168 Box::new(Expr::Literal(Literal::Int(1))),
2169 ),
2170 )),
2171 },
2172 MatchArm {
2173 pattern: Pattern::Constructor("None".to_string(), vec![]),
2174 body: Box::new(map_set(
2175 Expr::Ident("counts".to_string()),
2176 Expr::Ident("word".to_string()),
2177 Expr::Literal(Literal::Int(1)),
2178 )),
2179 },
2180 ],
2181 line: 2,
2182 }),
2183 ])),
2184 resolution: None,
2185 };
2186 ctx.fn_defs.push(inc_count.clone());
2187 ctx.items.push(TopLevel::FnDef(inc_count));
2188
2189 ctx.items.push(TopLevel::Verify(VerifyBlock {
2190 fn_name: "incCount".to_string(),
2191 line: 10,
2192 cases: vec![(
2193 map_has(
2194 Expr::FnCall(
2195 Box::new(Expr::Ident("incCount".to_string())),
2196 vec![
2197 Expr::Ident("counts".to_string()),
2198 Expr::Ident("word".to_string()),
2199 ],
2200 ),
2201 Expr::Ident("word".to_string()),
2202 ),
2203 Expr::Literal(Literal::Bool(true)),
2204 )],
2205 kind: VerifyKind::Law(VerifyLaw {
2206 name: "keyPresent".to_string(),
2207 givens: vec![
2208 VerifyGiven {
2209 name: "counts".to_string(),
2210 type_name: "Map<String, Int>".to_string(),
2211 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
2212 Box::new(Expr::Attr(
2213 Box::new(Expr::Ident("Map".to_string())),
2214 "empty".to_string(),
2215 )),
2216 vec![],
2217 )]),
2218 },
2219 VerifyGiven {
2220 name: "word".to_string(),
2221 type_name: "String".to_string(),
2222 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Str(
2223 "a".to_string(),
2224 ))]),
2225 },
2226 ],
2227 lhs: map_has(
2228 Expr::FnCall(
2229 Box::new(Expr::Ident("incCount".to_string())),
2230 vec![
2231 Expr::Ident("counts".to_string()),
2232 Expr::Ident("word".to_string()),
2233 ],
2234 ),
2235 Expr::Ident("word".to_string()),
2236 ),
2237 rhs: Expr::Literal(Literal::Bool(true)),
2238 }),
2239 }));
2240
2241 ctx.items.push(TopLevel::Verify(VerifyBlock {
2242 fn_name: "incCount".to_string(),
2243 line: 20,
2244 cases: vec![(
2245 map_get(
2246 Expr::FnCall(
2247 Box::new(Expr::Ident("incCount".to_string())),
2248 vec![
2249 Expr::Ident("counts".to_string()),
2250 Expr::Literal(Literal::Str("a".to_string())),
2251 ],
2252 ),
2253 Expr::Literal(Literal::Str("a".to_string())),
2254 ),
2255 option_some(Expr::FnCall(
2256 Box::new(Expr::Ident("addOne".to_string())),
2257 vec![option_with_default(
2258 map_get(
2259 Expr::Ident("counts".to_string()),
2260 Expr::Literal(Literal::Str("a".to_string())),
2261 ),
2262 Expr::Literal(Literal::Int(0)),
2263 )],
2264 )),
2265 )],
2266 kind: VerifyKind::Law(VerifyLaw {
2267 name: "existingKeyIncrements".to_string(),
2268 givens: vec![VerifyGiven {
2269 name: "counts".to_string(),
2270 type_name: "Map<String, Int>".to_string(),
2271 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
2272 Box::new(Expr::Attr(
2273 Box::new(Expr::Ident("Map".to_string())),
2274 "empty".to_string(),
2275 )),
2276 vec![],
2277 )]),
2278 }],
2279 lhs: map_get(
2280 Expr::FnCall(
2281 Box::new(Expr::Ident("incCount".to_string())),
2282 vec![
2283 Expr::Ident("counts".to_string()),
2284 Expr::Literal(Literal::Str("a".to_string())),
2285 ],
2286 ),
2287 Expr::Literal(Literal::Str("a".to_string())),
2288 ),
2289 rhs: option_some(Expr::FnCall(
2290 Box::new(Expr::Ident("addOne".to_string())),
2291 vec![option_with_default(
2292 map_get(
2293 Expr::Ident("counts".to_string()),
2294 Expr::Literal(Literal::Str("a".to_string())),
2295 ),
2296 Expr::Literal(Literal::Int(0)),
2297 )],
2298 )),
2299 }),
2300 }));
2301
2302 let out = transpile(&ctx);
2303 let lean = out
2304 .files
2305 .iter()
2306 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2307 .expect("expected generated Lean file");
2308 assert!(
2309 lean.contains("cases h : AverMap.get counts word <;> simp [AverMap.has_set_self]"),
2310 "expected keyPresent auto-proof with has_set_self"
2311 );
2312 assert!(
2313 lean.contains("cases h : AverMap.get counts \"a\" <;> simp [AverMap.get_set_self, addOne, incCount]"),
2314 "expected existingKeyIncrements auto-proof with get_set_self"
2315 );
2316 }
2317
2318 #[test]
2319 fn transpile_parenthesizes_negative_int_call_args_in_law_samples() {
2320 let mut ctx = empty_ctx();
2321 let add = FnDef {
2322 name: "add".to_string(),
2323 line: 1,
2324 params: vec![
2325 ("a".to_string(), "Int".to_string()),
2326 ("b".to_string(), "Int".to_string()),
2327 ],
2328 return_type: "Int".to_string(),
2329 effects: vec![],
2330 desc: None,
2331 body: Rc::new(FnBody::Expr(Expr::BinOp(
2332 BinOp::Add,
2333 Box::new(Expr::Ident("a".to_string())),
2334 Box::new(Expr::Ident("b".to_string())),
2335 ))),
2336 resolution: None,
2337 };
2338 ctx.fn_defs.push(add.clone());
2339 ctx.items.push(TopLevel::FnDef(add));
2340 ctx.items.push(TopLevel::Verify(VerifyBlock {
2341 fn_name: "add".to_string(),
2342 line: 1,
2343 cases: vec![(
2344 Expr::FnCall(
2345 Box::new(Expr::Ident("add".to_string())),
2346 vec![
2347 Expr::Literal(Literal::Int(-2)),
2348 Expr::Literal(Literal::Int(-1)),
2349 ],
2350 ),
2351 Expr::FnCall(
2352 Box::new(Expr::Ident("add".to_string())),
2353 vec![
2354 Expr::Literal(Literal::Int(-1)),
2355 Expr::Literal(Literal::Int(-2)),
2356 ],
2357 ),
2358 )],
2359 kind: VerifyKind::Law(VerifyLaw {
2360 name: "commutative".to_string(),
2361 givens: vec![
2362 VerifyGiven {
2363 name: "a".to_string(),
2364 type_name: "Int".to_string(),
2365 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(-2))]),
2366 },
2367 VerifyGiven {
2368 name: "b".to_string(),
2369 type_name: "Int".to_string(),
2370 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(-1))]),
2371 },
2372 ],
2373 lhs: Expr::FnCall(
2374 Box::new(Expr::Ident("add".to_string())),
2375 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
2376 ),
2377 rhs: Expr::FnCall(
2378 Box::new(Expr::Ident("add".to_string())),
2379 vec![Expr::Ident("b".to_string()), Expr::Ident("a".to_string())],
2380 ),
2381 }),
2382 }));
2383
2384 let out = transpile(&ctx);
2385 let lean = out
2386 .files
2387 .iter()
2388 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2389 .expect("expected generated Lean file");
2390 assert!(lean.contains(
2391 "theorem add_law_commutative_sample_1 : add (-2) (-1) = add (-1) (-2) := by native_decide"
2392 ));
2393 }
2394
2395 #[test]
2396 fn verify_law_numbering_is_scoped_per_law_name() {
2397 let mut ctx = empty_ctx();
2398 let f = FnDef {
2399 name: "f".to_string(),
2400 line: 1,
2401 params: vec![("x".to_string(), "Int".to_string())],
2402 return_type: "Int".to_string(),
2403 effects: vec![],
2404 desc: None,
2405 body: Rc::new(FnBody::Expr(Expr::Ident("x".to_string()))),
2406 resolution: None,
2407 };
2408 ctx.fn_defs.push(f.clone());
2409 ctx.items.push(TopLevel::FnDef(f));
2410 ctx.items.push(TopLevel::Verify(VerifyBlock {
2411 fn_name: "f".to_string(),
2412 line: 1,
2413 cases: vec![(
2414 Expr::Literal(Literal::Int(1)),
2415 Expr::Literal(Literal::Int(1)),
2416 )],
2417 kind: VerifyKind::Cases,
2418 }));
2419 ctx.items.push(TopLevel::Verify(VerifyBlock {
2420 fn_name: "f".to_string(),
2421 line: 2,
2422 cases: vec![(
2423 Expr::Literal(Literal::Int(2)),
2424 Expr::Literal(Literal::Int(2)),
2425 )],
2426 kind: VerifyKind::Law(VerifyLaw {
2427 name: "identity".to_string(),
2428 givens: vec![VerifyGiven {
2429 name: "x".to_string(),
2430 type_name: "Int".to_string(),
2431 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
2432 }],
2433 lhs: Expr::Ident("x".to_string()),
2434 rhs: Expr::Ident("x".to_string()),
2435 }),
2436 }));
2437 let out = transpile_with_verify_mode(&ctx, VerifyEmitMode::TheoremSkeleton);
2438 let lean = out
2439 .files
2440 .iter()
2441 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2442 .expect("expected generated Lean file");
2443 assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
2444 assert!(lean.contains("theorem f_law_identity : ∀ (x : Int), x = x := by"));
2445 assert!(lean.contains("theorem f_law_identity_sample_1 : 2 = 2 := by"));
2446 assert!(!lean.contains("theorem f_law_identity_sample_2 : 2 = 2 := by"));
2447 }
2448
2449 #[test]
2450 fn proof_mode_accepts_single_int_countdown_recursion() {
2451 let mut ctx = empty_ctx();
2452 let down = FnDef {
2453 name: "down".to_string(),
2454 line: 1,
2455 params: vec![("n".to_string(), "Int".to_string())],
2456 return_type: "Int".to_string(),
2457 effects: vec![],
2458 desc: None,
2459 body: Rc::new(FnBody::Expr(Expr::Match {
2460 subject: Box::new(Expr::Ident("n".to_string())),
2461 arms: vec![
2462 MatchArm {
2463 pattern: Pattern::Literal(Literal::Int(0)),
2464 body: Box::new(Expr::Literal(Literal::Int(0))),
2465 },
2466 MatchArm {
2467 pattern: Pattern::Wildcard,
2468 body: Box::new(Expr::TailCall(Box::new((
2469 "down".to_string(),
2470 vec![Expr::BinOp(
2471 BinOp::Sub,
2472 Box::new(Expr::Ident("n".to_string())),
2473 Box::new(Expr::Literal(Literal::Int(1))),
2474 )],
2475 )))),
2476 },
2477 ],
2478 line: 1,
2479 })),
2480 resolution: None,
2481 };
2482 ctx.items.push(TopLevel::FnDef(down.clone()));
2483 ctx.fn_defs.push(down);
2484
2485 let issues = proof_mode_issues(&ctx);
2486 assert!(
2487 issues.is_empty(),
2488 "expected Int countdown recursion to be accepted, got: {:?}",
2489 issues
2490 );
2491
2492 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2493 let lean = out
2494 .files
2495 .iter()
2496 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2497 .expect("expected generated Lean file");
2498 assert!(lean.contains("def down (n : Int) : Int :="));
2499 assert!(lean.contains("termination_by Int.natAbs n"));
2500 assert!(lean.contains("decreasing_by"));
2501 }
2502
2503 #[test]
2504 fn proof_mode_accepts_single_list_structural_recursion() {
2505 let mut ctx = empty_ctx();
2506 let len = FnDef {
2507 name: "len".to_string(),
2508 line: 1,
2509 params: vec![("xs".to_string(), "List<Int>".to_string())],
2510 return_type: "Int".to_string(),
2511 effects: vec![],
2512 desc: None,
2513 body: Rc::new(FnBody::Expr(Expr::Match {
2514 subject: Box::new(Expr::Ident("xs".to_string())),
2515 arms: vec![
2516 MatchArm {
2517 pattern: Pattern::EmptyList,
2518 body: Box::new(Expr::Literal(Literal::Int(0))),
2519 },
2520 MatchArm {
2521 pattern: Pattern::Cons("h".to_string(), "t".to_string()),
2522 body: Box::new(Expr::TailCall(Box::new((
2523 "len".to_string(),
2524 vec![Expr::Ident("t".to_string())],
2525 )))),
2526 },
2527 ],
2528 line: 1,
2529 })),
2530 resolution: None,
2531 };
2532 ctx.items.push(TopLevel::FnDef(len.clone()));
2533 ctx.fn_defs.push(len);
2534
2535 let issues = proof_mode_issues(&ctx);
2536 assert!(
2537 issues.is_empty(),
2538 "expected List structural recursion to be accepted, got: {:?}",
2539 issues
2540 );
2541 }
2542
2543 #[test]
2544 fn proof_mode_accepts_single_string_pos_advance_recursion() {
2545 let mut ctx = empty_ctx();
2546 let skip_ws = FnDef {
2547 name: "skipWs".to_string(),
2548 line: 1,
2549 params: vec![
2550 ("s".to_string(), "String".to_string()),
2551 ("pos".to_string(), "Int".to_string()),
2552 ],
2553 return_type: "Int".to_string(),
2554 effects: vec![],
2555 desc: None,
2556 body: Rc::new(FnBody::Expr(Expr::Match {
2557 subject: Box::new(Expr::FnCall(
2558 Box::new(Expr::Attr(
2559 Box::new(Expr::Ident("String".to_string())),
2560 "charAt".to_string(),
2561 )),
2562 vec![Expr::Ident("s".to_string()), Expr::Ident("pos".to_string())],
2563 )),
2564 arms: vec![
2565 MatchArm {
2566 pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
2567 body: Box::new(Expr::Ident("pos".to_string())),
2568 },
2569 MatchArm {
2570 pattern: Pattern::Wildcard,
2571 body: Box::new(Expr::TailCall(Box::new((
2572 "skipWs".to_string(),
2573 vec![
2574 Expr::Ident("s".to_string()),
2575 Expr::BinOp(
2576 BinOp::Add,
2577 Box::new(Expr::Ident("pos".to_string())),
2578 Box::new(Expr::Literal(Literal::Int(1))),
2579 ),
2580 ],
2581 )))),
2582 },
2583 ],
2584 line: 1,
2585 })),
2586 resolution: None,
2587 };
2588 ctx.items.push(TopLevel::FnDef(skip_ws.clone()));
2589 ctx.fn_defs.push(skip_ws);
2590
2591 let issues = proof_mode_issues(&ctx);
2592 assert!(
2593 issues.is_empty(),
2594 "expected String+pos recursion to be accepted, got: {:?}",
2595 issues
2596 );
2597
2598 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2599 let lean = out
2600 .files
2601 .iter()
2602 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2603 .expect("expected generated Lean file");
2604 assert!(lean.contains("def skipWs (s : String) (pos : Int) : Int :="));
2605 assert!(lean.contains("termination_by ((s.data.length) - (pos.toNat))"));
2606 assert!(lean.contains("decreasing_by"));
2607 }
2608
2609 #[test]
2610 fn proof_mode_accepts_mutual_int_countdown_recursion() {
2611 let mut ctx = empty_ctx();
2612 let even = FnDef {
2613 name: "even".to_string(),
2614 line: 1,
2615 params: vec![("n".to_string(), "Int".to_string())],
2616 return_type: "Bool".to_string(),
2617 effects: vec![],
2618 desc: None,
2619 body: Rc::new(FnBody::Expr(Expr::Match {
2620 subject: Box::new(Expr::Ident("n".to_string())),
2621 arms: vec![
2622 MatchArm {
2623 pattern: Pattern::Literal(Literal::Int(0)),
2624 body: Box::new(Expr::Literal(Literal::Bool(true))),
2625 },
2626 MatchArm {
2627 pattern: Pattern::Wildcard,
2628 body: Box::new(Expr::TailCall(Box::new((
2629 "odd".to_string(),
2630 vec![Expr::BinOp(
2631 BinOp::Sub,
2632 Box::new(Expr::Ident("n".to_string())),
2633 Box::new(Expr::Literal(Literal::Int(1))),
2634 )],
2635 )))),
2636 },
2637 ],
2638 line: 1,
2639 })),
2640 resolution: None,
2641 };
2642 let odd = FnDef {
2643 name: "odd".to_string(),
2644 line: 2,
2645 params: vec![("n".to_string(), "Int".to_string())],
2646 return_type: "Bool".to_string(),
2647 effects: vec![],
2648 desc: None,
2649 body: Rc::new(FnBody::Expr(Expr::Match {
2650 subject: Box::new(Expr::Ident("n".to_string())),
2651 arms: vec![
2652 MatchArm {
2653 pattern: Pattern::Literal(Literal::Int(0)),
2654 body: Box::new(Expr::Literal(Literal::Bool(false))),
2655 },
2656 MatchArm {
2657 pattern: Pattern::Wildcard,
2658 body: Box::new(Expr::TailCall(Box::new((
2659 "even".to_string(),
2660 vec![Expr::BinOp(
2661 BinOp::Sub,
2662 Box::new(Expr::Ident("n".to_string())),
2663 Box::new(Expr::Literal(Literal::Int(1))),
2664 )],
2665 )))),
2666 },
2667 ],
2668 line: 2,
2669 })),
2670 resolution: None,
2671 };
2672 ctx.items.push(TopLevel::FnDef(even.clone()));
2673 ctx.items.push(TopLevel::FnDef(odd.clone()));
2674 ctx.fn_defs.push(even);
2675 ctx.fn_defs.push(odd);
2676
2677 let issues = proof_mode_issues(&ctx);
2678 assert!(
2679 issues.is_empty(),
2680 "expected mutual Int countdown recursion to be accepted, got: {:?}",
2681 issues
2682 );
2683 }
2684
2685 #[test]
2686 fn proof_mode_accepts_mutual_string_pos_recursion_with_ranked_same_edges() {
2687 let mut ctx = empty_ctx();
2688 let f = FnDef {
2689 name: "f".to_string(),
2690 line: 1,
2691 params: vec![
2692 ("s".to_string(), "String".to_string()),
2693 ("pos".to_string(), "Int".to_string()),
2694 ],
2695 return_type: "Int".to_string(),
2696 effects: vec![],
2697 desc: None,
2698 body: Rc::new(FnBody::Expr(Expr::Match {
2699 subject: Box::new(Expr::BinOp(
2700 BinOp::Gte,
2701 Box::new(Expr::Ident("pos".to_string())),
2702 Box::new(Expr::Literal(Literal::Int(3))),
2703 )),
2704 arms: vec![
2705 MatchArm {
2706 pattern: Pattern::Literal(Literal::Bool(true)),
2707 body: Box::new(Expr::Ident("pos".to_string())),
2708 },
2709 MatchArm {
2710 pattern: Pattern::Wildcard,
2711 body: Box::new(Expr::TailCall(Box::new((
2712 "g".to_string(),
2713 vec![Expr::Ident("s".to_string()), Expr::Ident("pos".to_string())],
2714 )))),
2715 },
2716 ],
2717 line: 1,
2718 })),
2719 resolution: None,
2720 };
2721 let g = FnDef {
2722 name: "g".to_string(),
2723 line: 2,
2724 params: vec![
2725 ("s".to_string(), "String".to_string()),
2726 ("pos".to_string(), "Int".to_string()),
2727 ],
2728 return_type: "Int".to_string(),
2729 effects: vec![],
2730 desc: None,
2731 body: Rc::new(FnBody::Expr(Expr::Match {
2732 subject: Box::new(Expr::BinOp(
2733 BinOp::Gte,
2734 Box::new(Expr::Ident("pos".to_string())),
2735 Box::new(Expr::Literal(Literal::Int(3))),
2736 )),
2737 arms: vec![
2738 MatchArm {
2739 pattern: Pattern::Literal(Literal::Bool(true)),
2740 body: Box::new(Expr::Ident("pos".to_string())),
2741 },
2742 MatchArm {
2743 pattern: Pattern::Wildcard,
2744 body: Box::new(Expr::TailCall(Box::new((
2745 "f".to_string(),
2746 vec![
2747 Expr::Ident("s".to_string()),
2748 Expr::BinOp(
2749 BinOp::Add,
2750 Box::new(Expr::Ident("pos".to_string())),
2751 Box::new(Expr::Literal(Literal::Int(1))),
2752 ),
2753 ],
2754 )))),
2755 },
2756 ],
2757 line: 2,
2758 })),
2759 resolution: None,
2760 };
2761 ctx.items.push(TopLevel::FnDef(f.clone()));
2762 ctx.items.push(TopLevel::FnDef(g.clone()));
2763 ctx.fn_defs.push(f);
2764 ctx.fn_defs.push(g);
2765
2766 let issues = proof_mode_issues(&ctx);
2767 assert!(
2768 issues.is_empty(),
2769 "expected mutual String+pos recursion to be accepted, got: {:?}",
2770 issues
2771 );
2772
2773 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2774 let lean = out
2775 .files
2776 .iter()
2777 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2778 .expect("expected generated Lean file");
2779 assert!(lean.contains("termination_by"));
2780 assert!(lean.contains("termination_by ((s.data.length) - (pos.toNat),"));
2781 assert!(lean.contains("decreasing_by"));
2782 }
2783
2784 #[test]
2785 fn proof_mode_accepts_mutual_ranked_sizeof_recursion() {
2786 let mut ctx = empty_ctx();
2787 let f = FnDef {
2788 name: "f".to_string(),
2789 line: 1,
2790 params: vec![("xs".to_string(), "List<Int>".to_string())],
2791 return_type: "Int".to_string(),
2792 effects: vec![],
2793 desc: None,
2794 body: Rc::new(FnBody::Expr(Expr::TailCall(Box::new((
2795 "g".to_string(),
2796 vec![
2797 Expr::Literal(Literal::Str("acc".to_string())),
2798 Expr::Ident("xs".to_string()),
2799 ],
2800 ))))),
2801 resolution: None,
2802 };
2803 let g = FnDef {
2804 name: "g".to_string(),
2805 line: 2,
2806 params: vec![
2807 ("acc".to_string(), "String".to_string()),
2808 ("xs".to_string(), "List<Int>".to_string()),
2809 ],
2810 return_type: "Int".to_string(),
2811 effects: vec![],
2812 desc: None,
2813 body: Rc::new(FnBody::Expr(Expr::Match {
2814 subject: Box::new(Expr::Ident("xs".to_string())),
2815 arms: vec![
2816 MatchArm {
2817 pattern: Pattern::EmptyList,
2818 body: Box::new(Expr::Literal(Literal::Int(0))),
2819 },
2820 MatchArm {
2821 pattern: Pattern::Cons("h".to_string(), "t".to_string()),
2822 body: Box::new(Expr::TailCall(Box::new((
2823 "f".to_string(),
2824 vec![Expr::Ident("t".to_string())],
2825 )))),
2826 },
2827 ],
2828 line: 2,
2829 })),
2830 resolution: None,
2831 };
2832 ctx.items.push(TopLevel::FnDef(f.clone()));
2833 ctx.items.push(TopLevel::FnDef(g.clone()));
2834 ctx.fn_defs.push(f);
2835 ctx.fn_defs.push(g);
2836
2837 let issues = proof_mode_issues(&ctx);
2838 assert!(
2839 issues.is_empty(),
2840 "expected mutual ranked-sizeOf recursion to be accepted, got: {:?}",
2841 issues
2842 );
2843
2844 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2845 let lean = out
2846 .files
2847 .iter()
2848 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2849 .expect("expected generated Lean file");
2850 assert!(lean.contains("termination_by"));
2851 assert!(lean.contains("(sizeOf"));
2852 assert!(lean.contains("decreasing_by"));
2853 }
2854
2855 #[test]
2856 fn proof_mode_rejects_recursive_pure_functions() {
2857 let mut ctx = empty_ctx();
2858 let recursive_fn = FnDef {
2859 name: "loop".to_string(),
2860 line: 1,
2861 params: vec![("n".to_string(), "Int".to_string())],
2862 return_type: "Int".to_string(),
2863 effects: vec![],
2864 desc: None,
2865 body: Rc::new(FnBody::Expr(Expr::FnCall(
2866 Box::new(Expr::Ident("loop".to_string())),
2867 vec![Expr::Ident("n".to_string())],
2868 ))),
2869 resolution: None,
2870 };
2871 ctx.items.push(TopLevel::FnDef(recursive_fn.clone()));
2872 ctx.fn_defs.push(recursive_fn);
2873
2874 let issues = proof_mode_issues(&ctx);
2875 assert!(
2876 issues.iter().any(|i| i.contains("outside proof subset")),
2877 "expected recursive function blocker, got: {:?}",
2878 issues
2879 );
2880 }
2881
2882 #[test]
2883 fn proof_mode_allows_recursive_types() {
2884 let mut ctx = empty_ctx();
2885 let recursive_type = TypeDef::Sum {
2886 name: "Node".to_string(),
2887 variants: vec![TypeVariant {
2888 name: "Cons".to_string(),
2889 fields: vec!["Node".to_string()],
2890 }],
2891 line: 1,
2892 };
2893 ctx.items.push(TopLevel::TypeDef(recursive_type.clone()));
2894 ctx.type_defs.push(recursive_type);
2895
2896 let issues = proof_mode_issues(&ctx);
2897 assert!(
2898 issues
2899 .iter()
2900 .all(|i| !i.contains("recursive types require unsafe DecidableEq shim")),
2901 "did not expect recursive type blocker, got: {:?}",
2902 issues
2903 );
2904 }
2905}