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