Skip to main content

aver/codegen/lean/
mod.rs

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