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 recurrence;
11mod shared;
12mod toplevel;
13mod types;
14
15use std::collections::{HashMap, HashSet};
16
17use crate::ast::{
18    BinOp, Expr, FnBody, FnDef, MatchArm, Pattern, Spanned, Stmt, TopLevel, TypeDef, VerifyKind,
19};
20use crate::call_graph;
21use crate::codegen::{CodegenContext, ProjectOutput};
22
23/// How verify blocks should be emitted in generated Lean.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum VerifyEmitMode {
26    /// `example : lhs = rhs := by native_decide`
27    NativeDecide,
28    /// `example : lhs = rhs := by sorry`
29    Sorry,
30    /// Named theorem stubs:
31    /// `theorem <fn>_verify_<n> : lhs = rhs := by`
32    /// `  sorry`
33    TheoremSkeleton,
34}
35
36/// Proof-mode recursion support class for emitted Lean definitions.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum RecursionPlan {
39    /// Single-function recursion where an `Int` parameter decreases by 1.
40    IntCountdown { param_index: usize },
41    /// Single-function recursion where an `Int` parameter increases by 1 up to a bound.
42    /// `bound_lean` is the Lean expression of the upper bound (e.g. `8` or `xs.length`).
43    IntAscending {
44        param_index: usize,
45        bound_lean: String,
46    },
47    /// Single-function recurrence with `n < 0` guard, `0/1` bases, and a linear
48    /// second-order recurrence over previous results, emitted through a private
49    /// Nat helper.
50    LinearRecurrence2,
51    /// Single-function structural recursion on a `List<_>` parameter.
52    ListStructural { param_index: usize },
53    /// Single-function structural recursion on a recursive user-defined type parameter.
54    SizeOfStructural,
55    /// Single-function recursion where first `String` stays the same and second `Int`
56    /// parameter strictly advances (`pos + k`, `k >= 1`).
57    StringPosAdvance,
58    /// Mutual recursion group where first `Int` parameter decreases by 1 across SCC calls.
59    MutualIntCountdown,
60    /// Mutual recursion group where first `String` is preserved and second `Int`
61    /// either stays the same across rank-decreasing edges, or strictly advances.
62    MutualStringPosAdvance { rank: usize },
63    /// Generic mutual recursion plan using `sizeOf` on structural parameters,
64    /// plus rank for same-measure edges.
65    MutualSizeOfRanked { rank: usize },
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ProofModeIssue {
70    pub line: usize,
71    pub message: String,
72}
73
74const LEAN_PRELUDE_HEADER: &str = r#"-- Generated by the Aver → Lean 4 transpiler
75-- Pure core logic only (effectful functions are omitted)
76
77set_option linter.unusedVariables false
78
79-- Prelude: helper definitions for Aver builtins"#;
80
81const LEAN_PRELUDE_FLOAT_COE: &str = r#"instance : Coe Int Float := ⟨fun n => Float.ofInt n⟩
82
83def Float.fromInt (n : Int) : Float := Float.ofInt n"#;
84
85const LEAN_PRELUDE_FLOAT_DEC_EQ: &str = r#"private unsafe def Float.unsafeDecEq (a b : Float) : Decidable (a = b) :=
86  if a == b then isTrue (unsafeCast ()) else isFalse (unsafeCast ())
87@[implemented_by Float.unsafeDecEq]
88private opaque Float.compDecEq (a b : Float) : Decidable (a = b)
89instance : DecidableEq Float := Float.compDecEq"#;
90
91const LEAN_PRELUDE_EXCEPT_DEC_EQ: &str = r#"instance [DecidableEq ε] [DecidableEq α] : DecidableEq (Except ε α)
92  | .ok a, .ok b =>
93    if h : a = b then isTrue (h ▸ rfl) else isFalse (by intro h'; cases h'; exact h rfl)
94  | .error a, .error b =>
95    if h : a = b then isTrue (h ▸ rfl) else isFalse (by intro h'; cases h'; exact h rfl)
96  | .ok _, .error _ => isFalse (by intro h; cases h)
97  | .error _, .ok _ => isFalse (by intro h; cases h)"#;
98
99const LEAN_PRELUDE_EXCEPT_NS: &str = r#"namespace Except
100def withDefault (r : Except ε α) (d : α) : α :=
101  match r with
102  | .ok v => v
103  | .error _ => d
104end Except"#;
105
106const LEAN_PRELUDE_OPTION_TO_EXCEPT: &str = r#"def Option.toExcept (o : Option α) (e : ε) : Except ε α :=
107  match o with
108  | some v => .ok v
109  | none => .error e"#;
110
111const LEAN_PRELUDE_STRING_HADD: &str = r#"instance : HAdd String String String := ⟨String.append⟩"#;
112
113const LEAN_PRELUDE_PROOF_FUEL: &str = r#"def averStringPosFuel (s : String) (pos : Int) (rankBudget : Nat) : Nat :=
114  (((s.data.length) - pos.toNat) + 1) * rankBudget"#;
115
116const LEAN_PRELUDE_AVER_MEASURE: &str = r#"namespace AverMeasure
117def list (elemMeasure : α → Nat) : List α → Nat
118  | [] => 1
119  | x :: xs => elemMeasure x + list elemMeasure xs + 1
120def option (elemMeasure : α → Nat) : Option α → Nat
121  | none => 1
122  | some x => elemMeasure x + 1
123def except (errMeasure : ε → Nat) (okMeasure : α → Nat) : Except ε α → Nat
124  | .error e => errMeasure e + 1
125  | .ok v => okMeasure v + 1
126end AverMeasure"#;
127
128const AVER_MAP_PRELUDE_BASE: &str = r#"namespace AverMap
129def empty : List (α × β) := []
130def get [DecidableEq α] (m : List (α × β)) (k : α) : Option β :=
131  match m with
132  | [] => none
133  | (k', v) :: rest => if k = k' then some v else AverMap.get rest k
134def set [DecidableEq α] (m : List (α × β)) (k : α) (v : β) : List (α × β) :=
135  let rec go : List (α × β) → List (α × β)
136    | [] => [(k, v)]
137    | (k', v') :: rest => if k = k' then (k, v) :: rest else (k', v') :: go rest
138  go m
139def has [DecidableEq α] (m : List (α × β)) (k : α) : Bool :=
140  m.any (fun p => decide (k = p.1))
141def remove [DecidableEq α] (m : List (α × β)) (k : α) : List (α × β) :=
142  m.filter (fun p => !(decide (k = p.1)))
143def keys (m : List (α × β)) : List α := m.map Prod.fst
144def values (m : List (α × β)) : List β := m.map Prod.snd
145def entries (m : List (α × β)) : List (α × β) := m
146def len (m : List (α × β)) : Nat := m.length
147def fromList (entries : List (α × β)) : List (α × β) := entries"#;
148
149const AVER_MAP_PRELUDE_HAS_SET_SELF: &str = r#"private theorem any_set_go_self [DecidableEq α] (k : α) (v : β) :
150    ∀ (m : List (α × β)), List.any (AverMap.set.go k v m) (fun p => decide (k = p.1)) = true := by
151  intro m
152  induction m with
153  | nil =>
154      simp [AverMap.set.go, List.any]
155  | cons p tl ih =>
156      cases p with
157      | mk k' v' =>
158          by_cases h : k = k'
159          · simp [AverMap.set.go, List.any, h]
160          · simp [AverMap.set.go, List.any, h, ih]
161
162theorem has_set_self [DecidableEq α] (m : List (α × β)) (k : α) (v : β) :
163    AverMap.has (AverMap.set m k v) k = true := by
164  simpa [AverMap.has, AverMap.set] using any_set_go_self k v m"#;
165
166const AVER_MAP_PRELUDE_GET_SET_SELF: &str = r#"private theorem get_set_go_self [DecidableEq α] (k : α) (v : β) :
167    ∀ (m : List (α × β)), AverMap.get (AverMap.set.go k v m) k = some v := by
168  intro m
169  induction m with
170  | nil =>
171      simp [AverMap.set.go, AverMap.get]
172  | cons p tl ih =>
173      cases p with
174      | mk k' v' =>
175          by_cases h : k = k'
176          · simp [AverMap.set.go, AverMap.get, h]
177          · simp [AverMap.set.go, AverMap.get, h, ih]
178
179theorem get_set_self [DecidableEq α] (m : List (α × β)) (k : α) (v : β) :
180    AverMap.get (AverMap.set m k v) k = some v := by
181  simpa [AverMap.set] using get_set_go_self k v m"#;
182
183const AVER_MAP_PRELUDE_GET_SET_OTHER: &str = r#"private theorem get_set_go_other [DecidableEq α] (k key : α) (v : β) (h : key ≠ k) :
184    ∀ (m : List (α × β)), AverMap.get (AverMap.set.go k v m) key = AverMap.get m key := by
185  intro m
186  induction m with
187  | nil =>
188      simp [AverMap.set.go, AverMap.get, h]
189  | cons p tl ih =>
190      cases p with
191      | mk k' v' =>
192          by_cases hk : k = k'
193          · have hkey : key ≠ k' := by simpa [hk] using h
194            simp [AverMap.set.go, AverMap.get, hk, hkey]
195          · by_cases hkey : key = k'
196            · simp [AverMap.set.go, AverMap.get, hk, hkey]
197            · simp [AverMap.set.go, AverMap.get, hk, hkey, ih]
198
199theorem get_set_other [DecidableEq α] (m : List (α × β)) (k key : α) (v : β) (h : key ≠ k) :
200    AverMap.get (AverMap.set m k v) key = AverMap.get m key := by
201  simpa [AverMap.set] using get_set_go_other k key v h m"#;
202
203const AVER_MAP_PRELUDE_HAS_SET_OTHER: &str = r#"theorem has_eq_isSome_get [DecidableEq α] (m : List (α × β)) (k : α) :
204    AverMap.has m k = (AverMap.get m k).isSome := by
205  induction m with
206  | nil =>
207      simp [AverMap.has, AverMap.get]
208  | cons p tl ih =>
209      cases p with
210      | mk k' v' =>
211          by_cases h : k = k'
212          · simp [AverMap.has, AverMap.get, List.any, h]
213          · simpa [AverMap.has, AverMap.get, List.any, h] using ih
214
215theorem has_set_other [DecidableEq α] (m : List (α × β)) (k key : α) (v : β) (h : key ≠ k) :
216    AverMap.has (AverMap.set m k v) key = AverMap.has m key := by
217  rw [AverMap.has_eq_isSome_get, AverMap.has_eq_isSome_get]
218  simp [AverMap.get_set_other, h]"#;
219
220const AVER_MAP_PRELUDE_END: &str = r#"end AverMap"#;
221
222const LEAN_PRELUDE_AVER_LIST: &str = r#"namespace AverList
223def get (xs : List α) (i : Int) : Option α :=
224  if i < 0 then none else xs.get? i.toNat
225private def insertSorted [Ord α] (x : α) : List α → List α
226  | [] => [x]
227  | y :: ys =>
228    if compare x y == Ordering.lt || compare x y == Ordering.eq then
229      x :: y :: ys
230    else
231      y :: insertSorted x ys
232def sort [Ord α] (xs : List α) : List α :=
233  xs.foldl (fun acc x => insertSorted x acc) []
234end AverList"#;
235
236const LEAN_PRELUDE_HEADER_TYPE: &str = r#"structure Header where
237  name : String
238  value : String
239  deriving Repr, BEq, Inhabited, DecidableEq"#;
240
241const LEAN_PRELUDE_HTTP_RESPONSE_TYPE: &str = r#"structure HttpResponse where
242  status : Int
243  body : String
244  headers : List Header
245  deriving Repr, BEq, Inhabited, DecidableEq"#;
246
247const LEAN_PRELUDE_HTTP_REQUEST_TYPE: &str = r#"structure HttpRequest where
248  method : String
249  path : String
250  body : String
251  headers : List Header
252  deriving Repr, BEq, Inhabited, DecidableEq"#;
253
254const LEAN_PRELUDE_TCP_CONNECTION_TYPE: &str = r#"structure Tcp_Connection where
255  id : String
256  host : String
257  port : Int
258  deriving Repr, BEq, Inhabited, DecidableEq"#;
259
260const LEAN_PRELUDE_STRING_HELPERS: &str = r#"def String.charAt (s : String) (i : Int) : Option String :=
261  if i < 0 then none
262  else (s.toList.get? i.toNat).map Char.toString
263theorem String.charAt_length_none (s : String) : String.charAt s s.length = none := by
264  have hs : ¬ ((s.length : Int) < 0) := by omega
265  unfold String.charAt
266  simp [hs]
267  change s.data.length ≤ s.length
268  exact Nat.le_refl _
269def String.slice (s : String) (start stop : Int) : String :=
270  let startN := if start < 0 then 0 else start.toNat
271  let stopN := if stop < 0 then 0 else stop.toNat
272  let chars := s.toList
273  String.mk ((chars.drop startN).take (stopN - startN))
274private def trimFloatTrailingZerosChars (chars : List Char) : List Char :=
275  let noZeros := (chars.reverse.dropWhile (fun c => c == '0')).reverse
276  match noZeros.reverse with
277  | '.' :: rest => rest.reverse
278  | _ => noZeros
279private def normalizeFloatString (s : String) : String :=
280  if s.toList.any (fun c => c == '.') then
281    String.mk (trimFloatTrailingZerosChars s.toList)
282  else s
283def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)
284def String.chars (s : String) : List String := s.toList.map Char.toString
285private theorem char_to_string_append_mk (c : Char) (chars : List Char) :
286    Char.toString c ++ String.mk chars = String.mk (c :: chars) := by
287  rfl
288private theorem string_intercalate_empty_char_strings_go (acc : String) (chars : List Char) :
289    String.intercalate.go acc "" (chars.map Char.toString) = acc ++ String.mk chars := by
290  induction chars generalizing acc with
291  | nil =>
292      simp [String.intercalate.go]
293  | cons c rest ih =>
294      calc
295        String.intercalate.go acc "" ((c :: rest).map Char.toString)
296            = String.intercalate.go (acc ++ Char.toString c) "" (rest.map Char.toString) := by
297                simp [String.intercalate.go]
298        _ = (acc ++ Char.toString c) ++ String.mk rest := by
299                simpa using ih (acc ++ Char.toString c)
300        _ = acc ++ String.mk (c :: rest) := by
301                simp [String.append_assoc, char_to_string_append_mk]
302private theorem string_intercalate_empty_char_strings (chars : List Char) :
303    String.intercalate "" (chars.map Char.toString) = String.mk chars := by
304  cases chars with
305  | nil =>
306      simp [String.intercalate]
307  | cons c rest =>
308      simpa [String.intercalate] using string_intercalate_empty_char_strings_go c.toString rest
309theorem String.intercalate_empty_chars (s : String) :
310    String.intercalate "" (String.chars s) = s := by
311  cases s with
312  | mk chars =>
313      simpa [String.chars] using string_intercalate_empty_char_strings chars
314namespace AverString
315def splitOnCharGo (currentRev : List Char) (sep : Char) : List Char → List String
316  | [] => [String.mk currentRev.reverse]
317  | c :: cs =>
318      if c == sep then
319        String.mk currentRev.reverse :: splitOnCharGo [] sep cs
320      else
321        splitOnCharGo (c :: currentRev) sep cs
322def splitOnChar (s : String) (sep : Char) : List String :=
323  splitOnCharGo [] sep s.toList
324def split (s delim : String) : List String :=
325  match delim.toList with
326  | [] => "" :: (s.toList.map Char.toString) ++ [""]
327  | [c] => splitOnChar s c
328  | _ => s.splitOn delim
329@[simp] private theorem char_toString_data (c : Char) : c.toString.data = [c] := by
330  rfl
331private theorem splitOnCharGo_until_sep
332    (prefixRev part tail : List Char) (sep : Char) :
333    part.all (fun c => c != sep) = true ->
334    splitOnCharGo prefixRev sep (part ++ sep :: tail) =
335      String.mk (prefixRev.reverse ++ part) :: splitOnCharGo [] sep tail := by
336  intro h_safe
337  induction part generalizing prefixRev with
338  | nil =>
339      simp [splitOnCharGo]
340  | cons c rest ih =>
341      simp at h_safe
342      have h_rest : (rest.all fun c => c != sep) = true := by
343        simpa using h_safe.2
344      simpa [splitOnCharGo, h_safe.1, List.reverse_cons, List.append_assoc] using
345        (ih (c :: prefixRev) h_rest)
346private theorem splitOnCharGo_no_sep
347    (prefixRev chars : List Char) (sep : Char) :
348    chars.all (fun c => c != sep) = true ->
349    splitOnCharGo prefixRev sep chars =
350      [String.mk (prefixRev.reverse ++ chars)] := by
351  intro h_safe
352  induction chars generalizing prefixRev with
353  | nil =>
354      simp [splitOnCharGo]
355  | cons c rest ih =>
356      simp at h_safe
357      have h_rest : (rest.all fun c => c != sep) = true := by
358        simpa using h_safe.2
359      simpa [splitOnCharGo, h_safe.1, List.reverse_cons, List.append_assoc] using
360        (ih (c :: prefixRev) h_rest)
361@[simp] theorem split_single_char_append
362    (head tail : String) (sep : Char) :
363    head.toList.all (fun c => c != sep) = true ->
364    split (head ++ Char.toString sep ++ tail) (Char.toString sep) =
365      head :: split tail (Char.toString sep) := by
366  intro h_safe
367  simpa [split, splitOnChar] using
368    (splitOnCharGo_until_sep [] head.data tail.data sep h_safe)
369@[simp] theorem split_single_char_no_sep
370    (s : String) (sep : Char) :
371    s.toList.all (fun c => c != sep) = true ->
372    split s (Char.toString sep) = [s] := by
373  intro h_safe
374  simpa [split, splitOnChar] using
375    (splitOnCharGo_no_sep [] s.data sep h_safe)
376private theorem intercalate_go_prefix
377    (pref acc sep : String) (rest : List String) :
378    String.intercalate.go (pref ++ sep ++ acc) sep rest =
379      pref ++ sep ++ String.intercalate.go acc sep rest := by
380  induction rest generalizing acc with
381  | nil =>
382      simp [String.intercalate.go, String.append_assoc]
383  | cons x xs ih =>
384      simpa [String.intercalate.go, String.append_assoc] using
385        (ih (acc ++ sep ++ x))
386@[simp] theorem split_intercalate_trailing_single_char
387    (parts : List String) (sep : Char) :
388    parts.all (fun part => part.toList.all (fun c => c != sep)) = true ->
389    split (String.intercalate (Char.toString sep) parts ++ Char.toString sep) (Char.toString sep) =
390      match parts with
391      | [] => ["", ""]
392      | _ => parts ++ [""] := by
393  intro h_safe
394  induction parts with
395  | nil =>
396      simp [split, splitOnChar, splitOnCharGo]
397  | cons part rest ih =>
398      simp at h_safe
399      have h_part : (part.toList.all fun c => c != sep) = true := by
400        simpa using h_safe.1
401      cases rest with
402      | nil =>
403          have h_empty : ("".toList.all fun c => c != sep) = true := by simp
404          calc
405            split (String.intercalate.go part (Char.toString sep) [] ++ Char.toString sep) (Char.toString sep)
406                = split (part ++ Char.toString sep) (Char.toString sep) := by
407                    simp [String.intercalate.go]
408            _ = part :: split "" (Char.toString sep) := by
409                    simpa using split_single_char_append part "" sep h_part
410            _ = [part, ""] := by
411                    simp [split_single_char_no_sep, h_empty]
412      | cons next rest' =>
413          have h_rest : ((next :: rest').all fun part => part.toList.all fun c => c != sep) = true := by
414            simpa using h_safe.2
415          calc
416            split
417                  (String.intercalate.go part (Char.toString sep) (next :: rest') ++ Char.toString sep)
418                  (Char.toString sep)
419                =
420                  split
421                    (part ++ Char.toString sep ++ (String.intercalate (Char.toString sep) (next :: rest') ++ Char.toString sep))
422                    (Char.toString sep) := by
423                    have h_join :
424                        String.intercalate.go part (Char.toString sep) (next :: rest') ++ Char.toString sep
425                          = part ++ Char.toString sep ++ (String.intercalate (Char.toString sep) (next :: rest') ++ Char.toString sep) := by
426                        calc
427                          String.intercalate.go part (Char.toString sep) (next :: rest') ++ Char.toString sep
428                              = String.intercalate.go (part ++ Char.toString sep ++ next) (Char.toString sep) rest' ++ Char.toString sep := by
429                                  simp [String.intercalate.go]
430                          _ = part ++ Char.toString sep ++ (String.intercalate.go next (Char.toString sep) rest' ++ Char.toString sep) := by
431                                  rw [intercalate_go_prefix part next (Char.toString sep) rest']
432                                  simp [String.append_assoc]
433                          _ = part ++ Char.toString sep ++ (String.intercalate (Char.toString sep) (next :: rest') ++ Char.toString sep) := by
434                                  simp [String.intercalate, String.intercalate.go]
435                    simpa using congrArg (fun s => split s (Char.toString sep)) h_join
436            _ = part :: split
437                    (String.intercalate (Char.toString sep) (next :: rest') ++ Char.toString sep)
438                    (Char.toString sep) := by
439                    simpa using split_single_char_append
440                      part
441                      (String.intercalate (Char.toString sep) (next :: rest') ++ Char.toString sep)
442                      sep
443                      h_part
444            _ = part :: (next :: rest' ++ [""]) := by
445                    simpa using ih h_rest
446end AverString"#;
447
448const LEAN_PRELUDE_NUMERIC_PARSE: &str = r#"namespace AverDigits
449def foldDigitsAcc (acc : Nat) : List Nat -> Nat
450  | [] => acc
451  | d :: ds => foldDigitsAcc (acc * 10 + d) ds
452
453def foldDigits (digits : List Nat) : Nat :=
454  foldDigitsAcc 0 digits
455
456private theorem foldDigitsAcc_append_singleton (acc : Nat) (xs : List Nat) (d : Nat) :
457    foldDigitsAcc acc (xs ++ [d]) = foldDigitsAcc acc xs * 10 + d := by
458  induction xs generalizing acc with
459  | nil =>
460      simp [foldDigitsAcc]
461  | cons x xs ih =>
462      simp [foldDigitsAcc, ih, Nat.left_distrib, Nat.add_assoc, Nat.add_left_comm]
463
464private theorem foldDigits_append_singleton (xs : List Nat) (d : Nat) :
465    foldDigits (xs ++ [d]) = foldDigits xs * 10 + d := by
466  simpa [foldDigits] using foldDigitsAcc_append_singleton 0 xs d
467
468def natDigits : Nat -> List Nat
469  | n =>
470      if n < 10 then
471        [n]
472      else
473        natDigits (n / 10) ++ [n % 10]
474termination_by
475  n => n
476
477theorem natDigits_nonempty (n : Nat) : natDigits n ≠ [] := by
478  by_cases h : n < 10
479  · rw [natDigits.eq_1]
480    simp [h]
481  · rw [natDigits.eq_1]
482    simp [h]
483
484theorem natDigits_digits_lt_ten : ∀ n : Nat, ∀ d ∈ natDigits n, d < 10 := by
485  intro n d hd
486  by_cases h : n < 10
487  · rw [natDigits.eq_1] at hd
488    simp [h] at hd
489    rcases hd with rfl
490    exact h
491  · rw [natDigits.eq_1] at hd
492    simp [h] at hd
493    rcases hd with hd | hd
494    · exact natDigits_digits_lt_ten (n / 10) d hd
495    · subst hd
496      exact Nat.mod_lt n (by omega)
497
498theorem foldDigits_natDigits : ∀ n : Nat, foldDigits (natDigits n) = n := by
499  intro n
500  by_cases h : n < 10
501  · rw [natDigits.eq_1]
502    simp [h, foldDigits, foldDigitsAcc]
503  · rw [natDigits.eq_1]
504    simp [h]
505    rw [foldDigits_append_singleton]
506    rw [foldDigits_natDigits (n / 10)]
507    omega
508
509def digitChar : Nat -> Char
510  | 0 => '0' | 1 => '1' | 2 => '2' | 3 => '3' | 4 => '4'
511  | 5 => '5' | 6 => '6' | 7 => '7' | 8 => '8' | 9 => '9'
512  | _ => '0'
513
514def charToDigit? : Char -> Option Nat
515  | '0' => some 0 | '1' => some 1 | '2' => some 2 | '3' => some 3 | '4' => some 4
516  | '5' => some 5 | '6' => some 6 | '7' => some 7 | '8' => some 8 | '9' => some 9
517  | _ => none
518
519theorem charToDigit_digitChar : ∀ d : Nat, d < 10 -> charToDigit? (digitChar d) = some d
520  | 0, _ => by simp [digitChar, charToDigit?]
521  | 1, _ => by simp [digitChar, charToDigit?]
522  | 2, _ => by simp [digitChar, charToDigit?]
523  | 3, _ => by simp [digitChar, charToDigit?]
524  | 4, _ => by simp [digitChar, charToDigit?]
525  | 5, _ => by simp [digitChar, charToDigit?]
526  | 6, _ => by simp [digitChar, charToDigit?]
527  | 7, _ => by simp [digitChar, charToDigit?]
528  | 8, _ => by simp [digitChar, charToDigit?]
529  | 9, _ => by simp [digitChar, charToDigit?]
530  | Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ n))))))))), h => by
531      omega
532
533theorem digitChar_ne_minus : ∀ d : Nat, d < 10 -> digitChar d ≠ '-'
534  | 0, _ => by decide
535  | 1, _ => by decide
536  | 2, _ => by decide
537  | 3, _ => by decide
538  | 4, _ => by decide
539  | 5, _ => by decide
540  | 6, _ => by decide
541  | 7, _ => by decide
542  | 8, _ => by decide
543  | 9, _ => by decide
544  | Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ n))))))))), h => by
545      omega
546
547theorem digitChar_not_ws : ∀ d : Nat, d < 10 ->
548    Char.toString (digitChar d) ≠ " " ∧
549    Char.toString (digitChar d) ≠ "\t" ∧
550    Char.toString (digitChar d) ≠ "\n" ∧
551    Char.toString (digitChar d) ≠ "\r"
552  | 0, _ => by decide
553  | 1, _ => by decide
554  | 2, _ => by decide
555  | 3, _ => by decide
556  | 4, _ => by decide
557  | 5, _ => by decide
558  | 6, _ => by decide
559  | 7, _ => by decide
560  | 8, _ => by decide
561  | 9, _ => by decide
562  | Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ (Nat.succ n))))))))), h => by
563      omega
564
565theorem mapM_charToDigit_digits : ∀ ds : List Nat,
566    (∀ d ∈ ds, d < 10) -> List.mapM charToDigit? (ds.map digitChar) = some ds := by
567  intro ds hds
568  induction ds with
569  | nil =>
570      simp
571  | cons d ds ih =>
572      have hd : d < 10 := hds d (by simp)
573      have htail : ∀ x ∈ ds, x < 10 := by
574        intro x hx
575        exact hds x (by simp [hx])
576      simp [charToDigit_digitChar d hd, ih htail]
577
578def natDigitsChars (n : Nat) : List Char :=
579  (natDigits n).map digitChar
580
581def parseNatChars (chars : List Char) : Option Nat :=
582  match chars with
583  | [] => none
584  | _ => do
585      let digits <- List.mapM charToDigit? chars
586      pure (foldDigits digits)
587
588theorem parseNatChars_nat (n : Nat) :
589    parseNatChars (natDigitsChars n) = some n := by
590  unfold parseNatChars natDigitsChars
591  cases h : (natDigits n).map digitChar with
592  | nil =>
593      exfalso
594      exact natDigits_nonempty n (List.map_eq_nil_iff.mp h)
595  | cons hd tl =>
596      have hdigits : List.mapM charToDigit? (List.map digitChar (natDigits n)) = some (natDigits n) :=
597        mapM_charToDigit_digits (natDigits n) (fun d hd => natDigits_digits_lt_ten n d hd)
598      rw [h] at hdigits
599      simp [h, hdigits, foldDigits_natDigits]
600end AverDigits
601
602def String.fromInt (n : Int) : String :=
603  match n with
604  | .ofNat m => String.mk (AverDigits.natDigitsChars m)
605  | .negSucc m => String.mk ('-' :: AverDigits.natDigitsChars (m + 1))
606
607def Int.fromString (s : String) : Except String Int :=
608  match s.toList with
609  | [] => .error ("Cannot parse '" ++ s ++ "' as Int")
610  | '-' :: rest =>
611    match AverDigits.parseNatChars rest with
612    | some n => .ok (-Int.ofNat n)
613    | none => .error ("Cannot parse '" ++ s ++ "' as Int")
614  | chars =>
615    match AverDigits.parseNatChars chars with
616    | some n => .ok (Int.ofNat n)
617    | none => .error ("Cannot parse '" ++ s ++ "' as Int")
618
619theorem Int.fromString_fromInt : ∀ n : Int, Int.fromString (String.fromInt n) = .ok n
620  | .ofNat m => by
621      cases h : AverDigits.natDigits m with
622      | nil =>
623          exfalso
624          exact AverDigits.natDigits_nonempty m h
625      | cons d ds =>
626          have hd : d < 10 := AverDigits.natDigits_digits_lt_ten m d (by simp [h])
627          have hne : AverDigits.digitChar d ≠ '-' := AverDigits.digitChar_ne_minus d hd
628          have hparse : AverDigits.parseNatChars (AverDigits.digitChar d :: List.map AverDigits.digitChar ds) = some m := by
629            simpa [AverDigits.natDigitsChars, h] using AverDigits.parseNatChars_nat m
630          simp [String.fromInt, Int.fromString, AverDigits.natDigitsChars, h, hne, hparse]
631  | .negSucc m => by
632      simp [String.fromInt, Int.fromString, AverDigits.parseNatChars_nat]
633      rfl
634
635private def charDigitsToNat (cs : List Char) : Nat :=
636  cs.foldl (fun acc c => acc * 10 + (c.toNat - '0'.toNat)) 0
637
638private def parseExpPart : List Char → (Bool × List Char)
639  | '-' :: rest => (true, rest.takeWhile Char.isDigit)
640  | '+' :: rest => (false, rest.takeWhile Char.isDigit)
641  | rest => (false, rest.takeWhile Char.isDigit)
642
643def Float.fromString (s : String) : Except String Float :=
644  let chars := s.toList
645  let (neg, chars) := match chars with
646    | '-' :: rest => (true, rest)
647    | _ => (false, chars)
648  let intPart := chars.takeWhile Char.isDigit
649  let rest := chars.dropWhile Char.isDigit
650  let (fracPart, rest) := match rest with
651    | '.' :: rest => (rest.takeWhile Char.isDigit, rest.dropWhile Char.isDigit)
652    | _ => ([], rest)
653  let (expNeg, expDigits) := match rest with
654    | 'e' :: rest => parseExpPart rest
655    | 'E' :: rest => parseExpPart rest
656    | _ => (false, [])
657  if intPart.isEmpty && fracPart.isEmpty then .error ("Invalid float: " ++ s)
658  else
659    let mantissa := charDigitsToNat (intPart ++ fracPart)
660    let fracLen : Int := fracPart.length
661    let expVal : Int := charDigitsToNat expDigits
662    let shift : Int := (if expNeg then -expVal else expVal) - fracLen
663    let f := if shift >= 0 then Float.ofScientific mantissa false shift.toNat
664             else Float.ofScientific mantissa true ((-shift).toNat)
665    .ok (if neg then -f else f)"#;
666
667const LEAN_PRELUDE_CHAR_BYTE: &str = r#"def Char.toCode (s : String) : Int :=
668  match s.toList.head? with
669  | some c => (c.toNat : Int)
670  | none => panic! "Char.toCode: string is empty"
671def Char.fromCode (n : Int) : Option String :=
672  if n < 0 || n > 1114111 then none
673  else if n >= 55296 && n <= 57343 then none
674  else some (Char.toString (Char.ofNat n.toNat))
675
676def hexDigit (n : Int) : String :=
677  match n with
678  | 0 => "0" | 1 => "1" | 2 => "2" | 3 => "3"
679  | 4 => "4" | 5 => "5" | 6 => "6" | 7 => "7"
680  | 8 => "8" | 9 => "9" | 10 => "a" | 11 => "b"
681  | 12 => "c" | 13 => "d" | 14 => "e" | 15 => "f"
682  | _ => "?"
683
684def byteToHex (code : Int) : String :=
685  hexDigit (code / 16) ++ hexDigit (code % 16)
686
687namespace AverByte
688private def hexValue (c : Char) : Option Int :=
689  match c with
690  | '0' => some 0  | '1' => some 1  | '2' => some 2  | '3' => some 3
691  | '4' => some 4  | '5' => some 5  | '6' => some 6  | '7' => some 7
692  | '8' => some 8  | '9' => some 9  | 'a' => some 10 | 'b' => some 11
693  | 'c' => some 12 | 'd' => some 13 | 'e' => some 14 | 'f' => some 15
694  | 'A' => some 10 | 'B' => some 11 | 'C' => some 12 | 'D' => some 13
695  | 'E' => some 14 | 'F' => some 15
696  | _ => none
697def toHex (n : Int) : Except String String :=
698  if n < 0 || n > 255 then
699    .error ("Byte.toHex: " ++ toString n ++ " is out of range 0-255")
700  else
701    .ok (byteToHex n)
702def fromHex (s : String) : Except String Int :=
703  match s.toList with
704  | [hi, lo] =>
705    match hexValue hi, hexValue lo with
706    | some h, some l => .ok (h * 16 + l)
707    | _, _ => .error ("Byte.fromHex: invalid hex '" ++ s ++ "'")
708  | _ => .error ("Byte.fromHex: expected exactly 2 hex chars, got '" ++ s ++ "'")
709end AverByte"#;
710
711fn pure_fns(ctx: &CodegenContext) -> Vec<&FnDef> {
712    ctx.modules
713        .iter()
714        .flat_map(|m| m.fn_defs.iter())
715        .chain(ctx.fn_defs.iter())
716        .filter(|fd| toplevel::is_pure_fn(fd))
717        .collect()
718}
719
720fn recursive_type_names(ctx: &CodegenContext) -> HashSet<String> {
721    ctx.modules
722        .iter()
723        .flat_map(|m| m.type_defs.iter())
724        .chain(ctx.type_defs.iter())
725        .filter(|td| toplevel::is_recursive_type_def(td))
726        .map(|td| toplevel::type_def_name(td).to_string())
727        .collect()
728}
729
730fn recursive_pure_fn_names(ctx: &CodegenContext) -> HashSet<String> {
731    let pure_names: HashSet<String> = pure_fns(ctx)
732        .into_iter()
733        .map(|fd| fd.name.clone())
734        .collect();
735    let mut callgraph_items = ctx.items.clone();
736    for module in &ctx.modules {
737        for fd in &module.fn_defs {
738            callgraph_items.push(TopLevel::FnDef(fd.clone()));
739        }
740    }
741    call_graph::find_recursive_fns(&callgraph_items)
742        .into_iter()
743        .filter(|name| pure_names.contains(name))
744        .collect()
745}
746
747fn verify_counter_key(vb: &crate::ast::VerifyBlock) -> String {
748    match &vb.kind {
749        VerifyKind::Cases => format!("fn:{}", vb.fn_name),
750        VerifyKind::Law(law) => format!("law:{}::{}", vb.fn_name, law.name),
751    }
752}
753
754fn lean_project_name(ctx: &CodegenContext) -> String {
755    ctx.items
756        .iter()
757        .find_map(|item| match item {
758            TopLevel::Module(m) => Some(m.name.clone()),
759            _ => None,
760        })
761        .unwrap_or_else(|| capitalize_first(&ctx.project_name))
762}
763
764fn expr_to_dotted_name(expr: &Spanned<Expr>) -> Option<String> {
765    match &expr.node {
766        Expr::Ident(name) => Some(name.clone()),
767        Expr::Attr(obj, field) => expr_to_dotted_name(obj).map(|p| format!("{}.{}", p, field)),
768        _ => None,
769    }
770}
771
772fn call_matches(name: &str, target: &str) -> bool {
773    name == target || name.rsplit('.').next() == Some(target)
774}
775
776fn call_is_in_set(name: &str, targets: &HashSet<String>) -> bool {
777    call_matches_any(name, targets)
778}
779
780fn canonical_callee_name(name: &str, targets: &HashSet<String>) -> Option<String> {
781    if targets.contains(name) {
782        return Some(name.to_string());
783    }
784    name.rsplit('.')
785        .next()
786        .filter(|last| targets.contains(*last))
787        .map(ToString::to_string)
788}
789
790fn call_matches_any(name: &str, targets: &HashSet<String>) -> bool {
791    if targets.contains(name) {
792        return true;
793    }
794    match name.rsplit('.').next() {
795        Some(last) => targets.contains(last),
796        None => false,
797    }
798}
799
800fn is_int_minus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
801    match &expr.node {
802        Expr::BinOp(BinOp::Sub, left, right) => {
803            matches!(&left.node, Expr::Ident(id) if id == param_name)
804                && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
805        }
806        Expr::FnCall(callee, args) => {
807            let Some(name) = expr_to_dotted_name(callee) else {
808                return false;
809            };
810            (name == "Int.sub" || name == "int.sub")
811                && args.len() == 2
812                && matches!(&args[0].node, Expr::Ident(id) if id == param_name)
813                && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
814        }
815        _ => false,
816    }
817}
818
819fn collect_calls_from_expr<'a>(
820    expr: &'a Spanned<Expr>,
821    out: &mut Vec<(String, Vec<&'a Spanned<Expr>>)>,
822) {
823    match &expr.node {
824        Expr::FnCall(callee, args) => {
825            if let Some(name) = expr_to_dotted_name(callee) {
826                out.push((name, args.iter().collect()));
827            }
828            collect_calls_from_expr(callee, out);
829            for arg in args {
830                collect_calls_from_expr(arg, out);
831            }
832        }
833        Expr::TailCall(boxed) => {
834            let (name, args) = boxed.as_ref();
835            out.push((name.clone(), args.iter().collect()));
836            for arg in args {
837                collect_calls_from_expr(arg, out);
838            }
839        }
840        Expr::Attr(obj, _) => collect_calls_from_expr(obj, out),
841        Expr::BinOp(_, left, right) => {
842            collect_calls_from_expr(left, out);
843            collect_calls_from_expr(right, out);
844        }
845        Expr::Match { subject, arms, .. } => {
846            collect_calls_from_expr(subject, out);
847            for arm in arms {
848                collect_calls_from_expr(&arm.body, out);
849            }
850        }
851        Expr::Constructor(_, inner) => {
852            if let Some(inner) = inner {
853                collect_calls_from_expr(inner, out);
854            }
855        }
856        Expr::ErrorProp(inner) => collect_calls_from_expr(inner, out),
857        Expr::InterpolatedStr(parts) => {
858            for p in parts {
859                if let crate::ast::StrPart::Parsed(e) = p {
860                    collect_calls_from_expr(e, out);
861                }
862            }
863        }
864        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
865            for item in items {
866                collect_calls_from_expr(item, out);
867            }
868        }
869        Expr::MapLiteral(entries) => {
870            for (k, v) in entries {
871                collect_calls_from_expr(k, out);
872                collect_calls_from_expr(v, out);
873            }
874        }
875        Expr::RecordCreate { fields, .. } => {
876            for (_, v) in fields {
877                collect_calls_from_expr(v, out);
878            }
879        }
880        Expr::RecordUpdate { base, updates, .. } => {
881            collect_calls_from_expr(base, out);
882            for (_, v) in updates {
883                collect_calls_from_expr(v, out);
884            }
885        }
886        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
887    }
888}
889
890fn collect_calls_from_body(body: &FnBody) -> Vec<(String, Vec<&Spanned<Expr>>)> {
891    let mut out = Vec::new();
892    for stmt in body.stmts() {
893        match stmt {
894            Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => collect_calls_from_expr(expr, &mut out),
895        }
896    }
897    out
898}
899
900fn collect_list_tail_binders_from_expr(
901    expr: &Spanned<Expr>,
902    list_param_name: &str,
903    tails: &mut HashSet<String>,
904) {
905    match &expr.node {
906        Expr::Match { subject, arms, .. } => {
907            if matches!(&subject.node, Expr::Ident(id) if id == list_param_name) {
908                for MatchArm { pattern, .. } in arms {
909                    if let Pattern::Cons(_, tail) = pattern {
910                        tails.insert(tail.clone());
911                    }
912                }
913            }
914            for arm in arms {
915                collect_list_tail_binders_from_expr(&arm.body, list_param_name, tails);
916            }
917            collect_list_tail_binders_from_expr(subject, list_param_name, tails);
918        }
919        Expr::FnCall(callee, args) => {
920            collect_list_tail_binders_from_expr(callee, list_param_name, tails);
921            for arg in args {
922                collect_list_tail_binders_from_expr(arg, list_param_name, tails);
923            }
924        }
925        Expr::TailCall(boxed) => {
926            let (_, args) = boxed.as_ref();
927            for arg in args {
928                collect_list_tail_binders_from_expr(arg, list_param_name, tails);
929            }
930        }
931        Expr::Attr(obj, _) => collect_list_tail_binders_from_expr(obj, list_param_name, tails),
932        Expr::BinOp(_, left, right) => {
933            collect_list_tail_binders_from_expr(left, list_param_name, tails);
934            collect_list_tail_binders_from_expr(right, list_param_name, tails);
935        }
936        Expr::Constructor(_, inner) => {
937            if let Some(inner) = inner {
938                collect_list_tail_binders_from_expr(inner, list_param_name, tails);
939            }
940        }
941        Expr::ErrorProp(inner) => {
942            collect_list_tail_binders_from_expr(inner, list_param_name, tails)
943        }
944        Expr::InterpolatedStr(parts) => {
945            for p in parts {
946                if let crate::ast::StrPart::Parsed(e) = p {
947                    collect_list_tail_binders_from_expr(e, list_param_name, tails);
948                }
949            }
950        }
951        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
952            for item in items {
953                collect_list_tail_binders_from_expr(item, list_param_name, tails);
954            }
955        }
956        Expr::MapLiteral(entries) => {
957            for (k, v) in entries {
958                collect_list_tail_binders_from_expr(k, list_param_name, tails);
959                collect_list_tail_binders_from_expr(v, list_param_name, tails);
960            }
961        }
962        Expr::RecordCreate { fields, .. } => {
963            for (_, v) in fields {
964                collect_list_tail_binders_from_expr(v, list_param_name, tails);
965            }
966        }
967        Expr::RecordUpdate { base, updates, .. } => {
968            collect_list_tail_binders_from_expr(base, list_param_name, tails);
969            for (_, v) in updates {
970                collect_list_tail_binders_from_expr(v, list_param_name, tails);
971            }
972        }
973        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
974    }
975}
976
977fn collect_list_tail_binders(fd: &FnDef, list_param_name: &str) -> HashSet<String> {
978    let mut tails = HashSet::new();
979    for stmt in fd.body.stmts() {
980        match stmt {
981            Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
982                collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
983            }
984        }
985    }
986    tails
987}
988
989fn find_type_def<'a>(ctx: &'a CodegenContext, type_name: &str) -> Option<&'a TypeDef> {
990    ctx.modules
991        .iter()
992        .flat_map(|m| m.type_defs.iter())
993        .chain(ctx.type_defs.iter())
994        .find(|td| toplevel::type_def_name(td) == type_name)
995}
996
997fn recursive_constructor_binders(
998    td: &TypeDef,
999    variant_name: &str,
1000    binders: &[String],
1001) -> Vec<String> {
1002    let variant_short = variant_name.rsplit('.').next().unwrap_or(variant_name);
1003    match td {
1004        TypeDef::Sum { name, variants, .. } => variants
1005            .iter()
1006            .find(|variant| variant.name == variant_short)
1007            .map(|variant| {
1008                variant
1009                    .fields
1010                    .iter()
1011                    .zip(binders.iter())
1012                    .filter_map(|(field_ty, binder)| {
1013                        (field_ty.trim() == name).then_some(binder.clone())
1014                    })
1015                    .collect()
1016            })
1017            .unwrap_or_default(),
1018        TypeDef::Product { .. } => Vec::new(),
1019    }
1020}
1021
1022fn grow_recursive_subterm_binders_from_expr(
1023    expr: &Spanned<Expr>,
1024    tracked: &HashSet<String>,
1025    td: &TypeDef,
1026    out: &mut HashSet<String>,
1027) {
1028    match &expr.node {
1029        Expr::Match { subject, arms, .. } => {
1030            if let Expr::Ident(subject_name) = &subject.node
1031                && tracked.contains(subject_name)
1032            {
1033                for arm in arms {
1034                    if let Pattern::Constructor(variant_name, binders) = &arm.pattern {
1035                        out.extend(recursive_constructor_binders(td, variant_name, binders));
1036                    }
1037                }
1038            }
1039            grow_recursive_subterm_binders_from_expr(subject, tracked, td, out);
1040            for arm in arms {
1041                grow_recursive_subterm_binders_from_expr(&arm.body, tracked, td, out);
1042            }
1043        }
1044        Expr::FnCall(callee, args) => {
1045            grow_recursive_subterm_binders_from_expr(callee, tracked, td, out);
1046            for arg in args {
1047                grow_recursive_subterm_binders_from_expr(arg, tracked, td, out);
1048            }
1049        }
1050        Expr::Attr(obj, _) => grow_recursive_subterm_binders_from_expr(obj, tracked, td, out),
1051        Expr::BinOp(_, left, right) => {
1052            grow_recursive_subterm_binders_from_expr(left, tracked, td, out);
1053            grow_recursive_subterm_binders_from_expr(right, tracked, td, out);
1054        }
1055        Expr::Constructor(_, Some(inner)) | Expr::ErrorProp(inner) => {
1056            grow_recursive_subterm_binders_from_expr(inner, tracked, td, out)
1057        }
1058        Expr::InterpolatedStr(parts) => {
1059            for part in parts {
1060                if let crate::ast::StrPart::Parsed(inner) = part {
1061                    grow_recursive_subterm_binders_from_expr(inner, tracked, td, out);
1062                }
1063            }
1064        }
1065        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1066            for item in items {
1067                grow_recursive_subterm_binders_from_expr(item, tracked, td, out);
1068            }
1069        }
1070        Expr::MapLiteral(entries) => {
1071            for (k, v) in entries {
1072                grow_recursive_subterm_binders_from_expr(k, tracked, td, out);
1073                grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
1074            }
1075        }
1076        Expr::RecordCreate { fields, .. } => {
1077            for (_, v) in fields {
1078                grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
1079            }
1080        }
1081        Expr::RecordUpdate { base, updates, .. } => {
1082            grow_recursive_subterm_binders_from_expr(base, tracked, td, out);
1083            for (_, v) in updates {
1084                grow_recursive_subterm_binders_from_expr(v, tracked, td, out);
1085            }
1086        }
1087        Expr::TailCall(boxed) => {
1088            for arg in &boxed.1 {
1089                grow_recursive_subterm_binders_from_expr(arg, tracked, td, out);
1090            }
1091        }
1092        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) | Expr::Constructor(_, None) => {}
1093    }
1094}
1095
1096fn collect_recursive_subterm_binders(
1097    fd: &FnDef,
1098    param_name: &str,
1099    param_type: &str,
1100    ctx: &CodegenContext,
1101) -> HashSet<String> {
1102    let Some(td) = find_type_def(ctx, param_type) else {
1103        return HashSet::new();
1104    };
1105    let mut tracked: HashSet<String> = HashSet::from([param_name.to_string()]);
1106    loop {
1107        let mut discovered = HashSet::new();
1108        for stmt in fd.body.stmts() {
1109            match stmt {
1110                Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
1111                    grow_recursive_subterm_binders_from_expr(expr, &tracked, td, &mut discovered);
1112                }
1113            }
1114        }
1115        let before = tracked.len();
1116        tracked.extend(discovered);
1117        if tracked.len() == before {
1118            break;
1119        }
1120    }
1121    tracked.remove(param_name);
1122    tracked
1123}
1124
1125fn single_int_countdown_param_index(fd: &FnDef) -> Option<usize> {
1126    let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1127        .into_iter()
1128        .filter(|(name, _)| call_matches(name, &fd.name))
1129        .map(|(_, args)| args)
1130        .collect();
1131    if recursive_calls.is_empty() {
1132        return None;
1133    }
1134
1135    fd.params
1136        .iter()
1137        .enumerate()
1138        .find_map(|(idx, (param_name, param_ty))| {
1139            if param_ty != "Int" {
1140                return None;
1141            }
1142            let countdown_ok = recursive_calls.iter().all(|args| {
1143                args.get(idx)
1144                    .cloned()
1145                    .is_some_and(|arg| is_int_minus_positive(arg, param_name))
1146            });
1147            if countdown_ok {
1148                return Some(idx);
1149            }
1150
1151            // Negative-guarded ascent (match n < 0) is handled as countdown
1152            // because the fuel is natAbs(n) which works for both directions.
1153            let ascent_ok = recursive_calls.iter().all(|args| {
1154                args.get(idx)
1155                    .copied()
1156                    .is_some_and(|arg| is_int_plus_positive(arg, param_name))
1157            });
1158            (ascent_ok && has_negative_guarded_ascent(fd, param_name)).then_some(idx)
1159        })
1160}
1161
1162fn has_negative_guarded_ascent(fd: &FnDef, param_name: &str) -> bool {
1163    let Some(tail) = fd.body.tail_expr() else {
1164        return false;
1165    };
1166    let Expr::Match { subject, arms, .. } = &tail.node else {
1167        return false;
1168    };
1169    let Expr::BinOp(BinOp::Lt, left, right) = &subject.node else {
1170        return false;
1171    };
1172    if !is_ident(left, param_name)
1173        || !matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(0)))
1174    {
1175        return false;
1176    }
1177
1178    let mut true_arm = None;
1179    let mut false_arm = None;
1180    for arm in arms {
1181        match arm.pattern {
1182            Pattern::Literal(crate::ast::Literal::Bool(true)) => true_arm = Some(arm.body.as_ref()),
1183            Pattern::Literal(crate::ast::Literal::Bool(false)) => {
1184                false_arm = Some(arm.body.as_ref())
1185            }
1186            _ => return false,
1187        }
1188    }
1189
1190    let Some(true_arm) = true_arm else {
1191        return false;
1192    };
1193    let Some(false_arm) = false_arm else {
1194        return false;
1195    };
1196
1197    let mut true_calls = Vec::new();
1198    collect_calls_from_expr(true_arm, &mut true_calls);
1199    let mut false_calls = Vec::new();
1200    collect_calls_from_expr(false_arm, &mut false_calls);
1201
1202    true_calls
1203        .iter()
1204        .any(|(name, _)| call_matches(name, &fd.name))
1205        && false_calls
1206            .iter()
1207            .all(|(name, _)| !call_matches(name, &fd.name))
1208}
1209
1210/// Detect ascending-index recursion and extract the bound expression.
1211/// Returns (param_index, bound_lean_string) if found.
1212fn single_int_ascending_param(fd: &FnDef) -> Option<(usize, String)> {
1213    let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1214        .into_iter()
1215        .filter(|(name, _)| call_matches(name, &fd.name))
1216        .map(|(_, args)| args)
1217        .collect();
1218    if recursive_calls.is_empty() {
1219        return None;
1220    }
1221
1222    for (idx, (param_name, param_ty)) in fd.params.iter().enumerate() {
1223        if param_ty != "Int" {
1224            continue;
1225        }
1226        let ascent_ok = recursive_calls.iter().all(|args| {
1227            args.get(idx)
1228                .cloned()
1229                .is_some_and(|arg| is_int_plus_positive(arg, param_name))
1230        });
1231        if !ascent_ok {
1232            continue;
1233        }
1234        if let Some(bound_lean) = extract_equality_bound_lean(fd, param_name) {
1235            return Some((idx, bound_lean));
1236        }
1237    }
1238    None
1239}
1240
1241/// Extract the bound expression from `match param == BOUND` as a Lean string.
1242fn extract_equality_bound_lean(fd: &FnDef, param_name: &str) -> Option<String> {
1243    let tail = fd.body.tail_expr()?;
1244    let Expr::Match { subject, arms, .. } = &tail.node else {
1245        return None;
1246    };
1247    let Expr::BinOp(BinOp::Eq, left, right) = &subject.node else {
1248        return None;
1249    };
1250    if !is_ident(left, param_name) {
1251        return None;
1252    }
1253    // Verify: true arm = base (no self-call), false arm = recursive (has self-call)
1254    let mut true_has_self = false;
1255    let mut false_has_self = false;
1256    for arm in arms {
1257        match arm.pattern {
1258            Pattern::Literal(crate::ast::Literal::Bool(true)) => {
1259                let mut calls = Vec::new();
1260                collect_calls_from_expr(&arm.body, &mut calls);
1261                true_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
1262            }
1263            Pattern::Literal(crate::ast::Literal::Bool(false)) => {
1264                let mut calls = Vec::new();
1265                collect_calls_from_expr(&arm.body, &mut calls);
1266                false_has_self = calls.iter().any(|(n, _)| call_matches(n, &fd.name));
1267            }
1268            _ => return None,
1269        }
1270    }
1271    if true_has_self || !false_has_self {
1272        return None;
1273    }
1274    // Convert bound expr to Lean string — works for literals and simple expressions
1275    Some(bound_expr_to_lean(right))
1276}
1277
1278fn bound_expr_to_lean(expr: &Spanned<Expr>) -> String {
1279    match &expr.node {
1280        Expr::Literal(crate::ast::Literal::Int(n)) => format!("{}", n),
1281        Expr::Ident(name) => expr::aver_name_to_lean(name),
1282        Expr::FnCall(f, args) => {
1283            if let Some(dotted) = crate::codegen::common::expr_to_dotted_name(&f.node) {
1284                // List.len(xs) → xs.length in Lean
1285                if dotted == "List.len" && args.len() == 1 {
1286                    return format!("{}.length", bound_expr_to_lean(&args[0]));
1287                }
1288                let lean_args: Vec<String> = args.iter().map(bound_expr_to_lean).collect();
1289                format!(
1290                    "({} {})",
1291                    expr::aver_name_to_lean(&dotted),
1292                    lean_args.join(" ")
1293                )
1294            } else {
1295                "0".to_string()
1296            }
1297        }
1298        Expr::Attr(obj, field) => format!(
1299            "{}.{}",
1300            bound_expr_to_lean(obj),
1301            expr::aver_name_to_lean(field)
1302        ),
1303        _ => "0".to_string(),
1304    }
1305}
1306
1307fn supports_single_sizeof_structural(fd: &FnDef, ctx: &CodegenContext) -> bool {
1308    let recursive_calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1309        .into_iter()
1310        .filter(|(name, _)| call_matches(name, &fd.name))
1311        .map(|(_, args)| args)
1312        .collect();
1313    if recursive_calls.is_empty() {
1314        return false;
1315    }
1316
1317    let metric_indices = sizeof_measure_param_indices(fd);
1318    if metric_indices.is_empty() {
1319        return false;
1320    }
1321
1322    let binder_sets: HashMap<usize, HashSet<String>> = metric_indices
1323        .iter()
1324        .filter_map(|idx| {
1325            let (param_name, param_type) = fd.params.get(*idx)?;
1326            recursive_type_names(ctx).contains(param_type).then(|| {
1327                (
1328                    *idx,
1329                    collect_recursive_subterm_binders(fd, param_name, param_type, ctx),
1330                )
1331            })
1332        })
1333        .collect();
1334
1335    if binder_sets.values().all(HashSet::is_empty) {
1336        return false;
1337    }
1338
1339    recursive_calls.iter().all(|args| {
1340        let mut strictly_smaller = false;
1341        for idx in &metric_indices {
1342            let Some((param_name, _)) = fd.params.get(*idx) else {
1343                return false;
1344            };
1345            let Some(arg) = args.get(*idx).cloned() else {
1346                return false;
1347            };
1348            if is_ident(arg, param_name) {
1349                continue;
1350            }
1351            let Some(binders) = binder_sets.get(idx) else {
1352                return false;
1353            };
1354            if matches!(&arg.node, Expr::Ident(id) if binders.contains(id)) {
1355                strictly_smaller = true;
1356                continue;
1357            }
1358            return false;
1359        }
1360        strictly_smaller
1361    })
1362}
1363
1364fn single_list_structural_param_index(fd: &FnDef) -> Option<usize> {
1365    fd.params
1366        .iter()
1367        .enumerate()
1368        .find_map(|(param_index, (param_name, param_ty))| {
1369            if !(param_ty.starts_with("List<") || param_ty == "List") {
1370                return None;
1371            }
1372
1373            let tails = collect_list_tail_binders(fd, param_name);
1374            if tails.is_empty() {
1375                return None;
1376            }
1377
1378            let recursive_calls: Vec<Option<&Spanned<Expr>>> =
1379                collect_calls_from_body(fd.body.as_ref())
1380                    .into_iter()
1381                    .filter(|(name, _)| call_matches(name, &fd.name))
1382                    .map(|(_, args)| args.get(param_index).cloned())
1383                    .collect();
1384            if recursive_calls.is_empty() {
1385                return None;
1386            }
1387
1388            recursive_calls
1389                .into_iter()
1390                .all(|arg| {
1391                    arg.is_some_and(|a| matches!(&a.node, Expr::Ident(id) if tails.contains(id)))
1392                })
1393                .then_some(param_index)
1394        })
1395}
1396
1397fn is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
1398    matches!(&expr.node, Expr::Ident(id) if id == name)
1399}
1400
1401fn is_int_plus_positive(expr: &Spanned<Expr>, param_name: &str) -> bool {
1402    match &expr.node {
1403        Expr::BinOp(BinOp::Add, left, right) => {
1404            matches!(&left.node, Expr::Ident(id) if id == param_name)
1405                && matches!(&right.node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
1406        }
1407        Expr::FnCall(callee, args) => {
1408            let Some(name) = expr_to_dotted_name(callee) else {
1409                return false;
1410            };
1411            (name == "Int.add" || name == "int.add")
1412                && args.len() == 2
1413                && matches!(&args[0].node, Expr::Ident(id) if id == param_name)
1414                && matches!(&args[1].node, Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
1415        }
1416        _ => false,
1417    }
1418}
1419
1420fn is_skip_ws_advance(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
1421    let Expr::FnCall(callee, args) = &expr.node else {
1422        return false;
1423    };
1424    let Some(name) = expr_to_dotted_name(callee) else {
1425        return false;
1426    };
1427    if !call_matches(&name, "skipWs") || args.len() != 2 {
1428        return false;
1429    }
1430    is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
1431}
1432
1433fn is_skip_ws_same(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
1434    let Expr::FnCall(callee, args) = &expr.node else {
1435        return false;
1436    };
1437    let Some(name) = expr_to_dotted_name(callee) else {
1438        return false;
1439    };
1440    if !call_matches(&name, "skipWs") || args.len() != 2 {
1441        return false;
1442    }
1443    is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
1444}
1445
1446fn is_string_pos_advance(expr: &Spanned<Expr>, string_param: &str, pos_param: &str) -> bool {
1447    is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
1448}
1449
1450#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1451enum StringPosEdge {
1452    Same,
1453    Advance,
1454}
1455
1456fn classify_string_pos_edge(
1457    expr: &Spanned<Expr>,
1458    string_param: &str,
1459    pos_param: &str,
1460) -> Option<StringPosEdge> {
1461    if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
1462        return Some(StringPosEdge::Same);
1463    }
1464    if is_string_pos_advance(expr, string_param, pos_param) {
1465        return Some(StringPosEdge::Advance);
1466    }
1467    if let Expr::FnCall(callee, args) = &expr.node {
1468        let name = expr_to_dotted_name(callee)?;
1469        if call_matches(&name, "skipWs")
1470            && args.len() == 2
1471            && is_ident(&args[0], string_param)
1472            && matches!(&args[1].node, Expr::Ident(id) if id != pos_param)
1473        {
1474            return Some(StringPosEdge::Advance);
1475        }
1476    }
1477    if matches!(&expr.node, Expr::Ident(id) if id != pos_param) {
1478        return Some(StringPosEdge::Advance);
1479    }
1480    None
1481}
1482
1483fn ranks_from_same_edges(
1484    names: &HashSet<String>,
1485    same_edges: &HashMap<String, HashSet<String>>,
1486) -> Option<HashMap<String, usize>> {
1487    let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
1488    for outs in same_edges.values() {
1489        for to in outs {
1490            if let Some(entry) = indegree.get_mut(to) {
1491                *entry += 1;
1492            } else {
1493                return None;
1494            }
1495        }
1496    }
1497
1498    let mut queue: Vec<String> = indegree
1499        .iter()
1500        .filter_map(|(name, &deg)| (deg == 0).then_some(name.clone()))
1501        .collect();
1502    queue.sort();
1503    let mut topo = Vec::new();
1504    while let Some(node) = queue.pop() {
1505        topo.push(node.clone());
1506        let outs = same_edges.get(&node).cloned().unwrap_or_default();
1507        let mut newly_zero = Vec::new();
1508        for to in outs {
1509            if let Some(entry) = indegree.get_mut(&to) {
1510                *entry -= 1;
1511                if *entry == 0 {
1512                    newly_zero.push(to);
1513                }
1514            } else {
1515                return None;
1516            }
1517        }
1518        newly_zero.sort();
1519        queue.extend(newly_zero);
1520    }
1521
1522    if topo.len() != names.len() {
1523        return None;
1524    }
1525
1526    let n = topo.len();
1527    let mut ranks = HashMap::new();
1528    for (idx, name) in topo.into_iter().enumerate() {
1529        ranks.insert(name, n - idx);
1530    }
1531    Some(ranks)
1532}
1533
1534fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
1535    let Some((string_param, string_ty)) = fd.params.first() else {
1536        return false;
1537    };
1538    let Some((pos_param, pos_ty)) = fd.params.get(1) else {
1539        return false;
1540    };
1541    if string_ty != "String" || pos_ty != "Int" {
1542        return false;
1543    }
1544
1545    type CallPair<'a> = (Option<&'a Spanned<Expr>>, Option<&'a Spanned<Expr>>);
1546    let recursive_calls: Vec<CallPair<'_>> = collect_calls_from_body(fd.body.as_ref())
1547        .into_iter()
1548        .filter(|(name, _)| call_matches(name, &fd.name))
1549        .map(|(_, args)| (args.first().cloned(), args.get(1).cloned()))
1550        .collect();
1551    if recursive_calls.is_empty() {
1552        return false;
1553    }
1554
1555    recursive_calls.into_iter().all(|(arg0, arg1)| {
1556        arg0.is_some_and(|e| is_ident(e, string_param))
1557            && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
1558    })
1559}
1560
1561fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
1562    if component.len() < 2 {
1563        return false;
1564    }
1565    if component
1566        .iter()
1567        .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
1568    {
1569        return false;
1570    }
1571    let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1572    let mut any_intra = false;
1573    for fd in component {
1574        let param_name = &fd.params[0].0;
1575        for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
1576            if !call_is_in_set(&callee, &names) {
1577                continue;
1578            }
1579            any_intra = true;
1580            let Some(arg0) = args.first().cloned() else {
1581                return false;
1582            };
1583            if !is_int_minus_positive(arg0, param_name) {
1584                return false;
1585            }
1586        }
1587    }
1588    any_intra
1589}
1590
1591fn supports_mutual_string_pos_advance(component: &[&FnDef]) -> Option<HashMap<String, usize>> {
1592    if component.len() < 2 {
1593        return None;
1594    }
1595    if component.iter().any(|fd| {
1596        !matches!(fd.params.first(), Some((_, t)) if t == "String")
1597            || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
1598    }) {
1599        return None;
1600    }
1601
1602    let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1603    let mut same_edges: HashMap<String, HashSet<String>> =
1604        names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1605    let mut any_intra = false;
1606
1607    for fd in component {
1608        let string_param = &fd.params[0].0;
1609        let pos_param = &fd.params[1].0;
1610        for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1611            let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1612                continue;
1613            };
1614            any_intra = true;
1615
1616            let arg0 = args.first().cloned()?;
1617            let arg1 = args.get(1).cloned()?;
1618
1619            if !is_ident(arg0, string_param) {
1620                return None;
1621            }
1622
1623            match classify_string_pos_edge(arg1, string_param, pos_param) {
1624                Some(StringPosEdge::Same) => {
1625                    if let Some(edges) = same_edges.get_mut(&fd.name) {
1626                        edges.insert(callee);
1627                    } else {
1628                        return None;
1629                    }
1630                }
1631                Some(StringPosEdge::Advance) => {}
1632                None => return None,
1633            }
1634        }
1635    }
1636
1637    if !any_intra {
1638        return None;
1639    }
1640
1641    ranks_from_same_edges(&names, &same_edges)
1642}
1643
1644fn is_scalar_like_type(type_name: &str) -> bool {
1645    matches!(
1646        type_name,
1647        "Int" | "Float" | "Bool" | "String" | "Char" | "Byte" | "Unit"
1648    )
1649}
1650
1651pub(super) fn sizeof_measure_param_indices(fd: &FnDef) -> Vec<usize> {
1652    fd.params
1653        .iter()
1654        .enumerate()
1655        .filter_map(|(idx, (_, type_name))| (!is_scalar_like_type(type_name)).then_some(idx))
1656        .collect()
1657}
1658
1659fn supports_mutual_sizeof_ranked(component: &[&FnDef]) -> Option<HashMap<String, usize>> {
1660    if component.len() < 2 {
1661        return None;
1662    }
1663    let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
1664    let metric_indices: HashMap<String, Vec<usize>> = component
1665        .iter()
1666        .map(|fd| (fd.name.clone(), sizeof_measure_param_indices(fd)))
1667        .collect();
1668    if component.iter().any(|fd| {
1669        metric_indices
1670            .get(&fd.name)
1671            .is_none_or(|indices| indices.is_empty())
1672    }) {
1673        return None;
1674    }
1675
1676    let mut same_edges: HashMap<String, HashSet<String>> =
1677        names.iter().map(|n| (n.clone(), HashSet::new())).collect();
1678    let mut any_intra = false;
1679    for fd in component {
1680        let caller_metric_indices = metric_indices.get(&fd.name)?;
1681        let caller_metric_params: Vec<&str> = caller_metric_indices
1682            .iter()
1683            .filter_map(|idx| fd.params.get(*idx).map(|(name, _)| name.as_str()))
1684            .collect();
1685        for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
1686            let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
1687                continue;
1688            };
1689            any_intra = true;
1690            let callee_metric_indices = metric_indices.get(&callee)?;
1691            let is_same_edge = callee_metric_indices.len() == caller_metric_params.len()
1692                && callee_metric_indices
1693                    .iter()
1694                    .enumerate()
1695                    .all(|(pos, callee_idx)| {
1696                        let Some(arg) = args.get(*callee_idx).cloned() else {
1697                            return false;
1698                        };
1699                        is_ident(arg, caller_metric_params[pos])
1700                    });
1701            if is_same_edge {
1702                if let Some(edges) = same_edges.get_mut(&fd.name) {
1703                    edges.insert(callee);
1704                } else {
1705                    return None;
1706                }
1707            }
1708        }
1709    }
1710    if !any_intra {
1711        return None;
1712    }
1713
1714    let ranks = ranks_from_same_edges(&names, &same_edges)?;
1715    let mut out = HashMap::new();
1716    for fd in component {
1717        let rank = ranks.get(&fd.name).cloned()?;
1718        out.insert(fd.name.clone(), rank);
1719    }
1720    Some(out)
1721}
1722
1723fn proof_mode_recursion_analysis(
1724    ctx: &CodegenContext,
1725) -> (HashMap<String, RecursionPlan>, Vec<ProofModeIssue>) {
1726    let mut plans = HashMap::new();
1727    let mut issues = Vec::new();
1728
1729    let all_pure = pure_fns(ctx);
1730    let recursive_names = recursive_pure_fn_names(ctx);
1731    let components = call_graph::ordered_fn_components(&all_pure, &ctx.module_prefixes);
1732
1733    for component in components {
1734        if component.is_empty() {
1735            continue;
1736        }
1737        let is_recursive_component =
1738            component.len() > 1 || recursive_names.contains(&component[0].name);
1739        if !is_recursive_component {
1740            continue;
1741        }
1742
1743        if component.len() > 1 {
1744            if supports_mutual_int_countdown(&component) {
1745                for fd in &component {
1746                    plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
1747                }
1748            } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
1749                for fd in &component {
1750                    if let Some(rank) = ranks.get(&fd.name).cloned() {
1751                        plans.insert(
1752                            fd.name.clone(),
1753                            RecursionPlan::MutualStringPosAdvance { rank },
1754                        );
1755                    }
1756                }
1757            } else if let Some(rankings) = supports_mutual_sizeof_ranked(&component) {
1758                for fd in &component {
1759                    if let Some(rank) = rankings.get(&fd.name).cloned() {
1760                        plans.insert(fd.name.clone(), RecursionPlan::MutualSizeOfRanked { rank });
1761                    }
1762                }
1763            } else {
1764                let names = component
1765                    .iter()
1766                    .map(|fd| fd.name.clone())
1767                    .collect::<Vec<_>>()
1768                    .join(", ");
1769                let line = component.iter().map(|fd| fd.line).min().unwrap_or(1);
1770                issues.push(ProofModeIssue {
1771                    line,
1772                    message: format!(
1773                        "unsupported mutual recursion group (currently supported in proof mode: Int countdown on first param): {}",
1774                        names
1775                    ),
1776                });
1777            }
1778            continue;
1779        }
1780
1781        let fd = component[0];
1782        if recurrence::detect_second_order_int_linear_recurrence(fd).is_some() {
1783            plans.insert(fd.name.clone(), RecursionPlan::LinearRecurrence2);
1784        } else if let Some((param_index, bound_lean)) = single_int_ascending_param(fd) {
1785            plans.insert(
1786                fd.name.clone(),
1787                RecursionPlan::IntAscending {
1788                    param_index,
1789                    bound_lean,
1790                },
1791            );
1792        } else if let Some(param_index) = single_int_countdown_param_index(fd) {
1793            plans.insert(fd.name.clone(), RecursionPlan::IntCountdown { param_index });
1794        } else if supports_single_sizeof_structural(fd, ctx) {
1795            plans.insert(fd.name.clone(), RecursionPlan::SizeOfStructural);
1796        } else if let Some(param_index) = single_list_structural_param_index(fd) {
1797            plans.insert(
1798                fd.name.clone(),
1799                RecursionPlan::ListStructural { param_index },
1800            );
1801        } else if supports_single_string_pos_advance(fd) {
1802            plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
1803        } else {
1804            issues.push(ProofModeIssue {
1805                line: fd.line,
1806                message: format!(
1807                    "recursive function '{}' is outside proof subset (currently supported: Int countdown, second-order affine Int recurrences with pair-state worker, structural recursion on List/recursive ADTs, String+position, mutual Int countdown, mutual String+position, and ranked sizeOf recursion)",
1808                    fd.name
1809                ),
1810            });
1811        }
1812    }
1813
1814    (plans, issues)
1815}
1816
1817/// Proof-mode diagnostics for Lean transpilation.
1818///
1819/// Returns human-readable notices for recursive shapes that still fall back to
1820/// regular `partial` Lean defs instead of total proof-mode emission.
1821pub fn proof_mode_findings(ctx: &CodegenContext) -> Vec<ProofModeIssue> {
1822    let (_plans, issues) = proof_mode_recursion_analysis(ctx);
1823    issues
1824}
1825
1826pub fn proof_mode_issues(ctx: &CodegenContext) -> Vec<String> {
1827    proof_mode_findings(ctx)
1828        .into_iter()
1829        .map(|issue| issue.message)
1830        .collect()
1831}
1832
1833/// Transpile an Aver program to a Lean 4 project.
1834pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
1835    transpile_with_verify_mode(ctx, VerifyEmitMode::NativeDecide)
1836}
1837
1838/// Proof-mode transpilation.
1839///
1840/// Uses recursion plans validated by `proof_mode_issues` and emits supported
1841/// recursive functions without `partial`, adding `termination_by` scaffolding.
1842pub fn transpile_for_proof_mode(
1843    ctx: &CodegenContext,
1844    verify_mode: VerifyEmitMode,
1845) -> ProjectOutput {
1846    let (plans, _issues) = proof_mode_recursion_analysis(ctx);
1847    let recursive_names = recursive_pure_fn_names(ctx);
1848    let recursive_types = recursive_type_names(ctx);
1849
1850    let mut sections = Vec::new();
1851
1852    // Reuse the same type emission as standard mode.
1853    for module in &ctx.modules {
1854        for td in &module.type_defs {
1855            sections.push(toplevel::emit_type_def(td));
1856            if toplevel::is_recursive_type_def(td) {
1857                sections.push(toplevel::emit_recursive_decidable_eq(
1858                    toplevel::type_def_name(td),
1859                ));
1860                if let Some(measure) = toplevel::emit_recursive_measure(td, &recursive_types) {
1861                    sections.push(measure);
1862                }
1863            }
1864            sections.push(String::new());
1865        }
1866    }
1867    for td in &ctx.type_defs {
1868        sections.push(toplevel::emit_type_def(td));
1869        if toplevel::is_recursive_type_def(td) {
1870            sections.push(toplevel::emit_recursive_decidable_eq(
1871                toplevel::type_def_name(td),
1872            ));
1873            if let Some(measure) = toplevel::emit_recursive_measure(td, &recursive_types) {
1874                sections.push(measure);
1875            }
1876        }
1877        sections.push(String::new());
1878    }
1879
1880    emit_pure_functions_proof(ctx, &plans, &recursive_names, &mut sections);
1881
1882    for item in &ctx.items {
1883        if let TopLevel::Decision(db) = item {
1884            sections.push(toplevel::emit_decision(db));
1885            sections.push(String::new());
1886        }
1887    }
1888
1889    let mut verify_case_counters: HashMap<String, usize> = HashMap::new();
1890    for item in &ctx.items {
1891        if let TopLevel::Verify(vb) = item {
1892            let key = verify_counter_key(vb);
1893            let start_idx = *verify_case_counters.get(&key).unwrap_or(&0);
1894            let (emitted, next_idx) = toplevel::emit_verify_block(vb, ctx, verify_mode, start_idx);
1895            verify_case_counters.insert(key, next_idx);
1896            sections.push(emitted);
1897            sections.push(String::new());
1898        }
1899    }
1900
1901    let lean_body = sections.join("\n");
1902    let prelude = generate_prelude_for_body(&lean_body, false);
1903    let lean_source = format!("{prelude}\n\n{lean_body}");
1904    let project_name = lean_project_name(ctx);
1905    let lakefile = generate_lakefile(&project_name);
1906    let toolchain = generate_toolchain();
1907    ProjectOutput {
1908        files: vec![
1909            ("lakefile.lean".to_string(), lakefile),
1910            ("lean-toolchain".to_string(), toolchain),
1911            (format!("{}.lean", project_name), lean_source),
1912        ],
1913    }
1914}
1915
1916/// Transpile an Aver program to a Lean 4 project with configurable verify proof mode.
1917///
1918/// - `NativeDecide` emits `example ... := by native_decide`
1919/// - `Sorry` emits `example ... := by sorry`
1920/// - `TheoremSkeleton` emits named theorem skeletons with `sorry`
1921pub fn transpile_with_verify_mode(
1922    ctx: &CodegenContext,
1923    verify_mode: VerifyEmitMode,
1924) -> ProjectOutput {
1925    let mut sections = Vec::new();
1926
1927    // Detect recursive functions for `partial` annotation
1928    let recursive_fns = call_graph::find_recursive_fns(&ctx.items);
1929
1930    // Module type definitions (from depends)
1931    for module in &ctx.modules {
1932        for td in &module.type_defs {
1933            sections.push(toplevel::emit_type_def(td));
1934            // #18: Recursive types need unsafe DecidableEq instance
1935            if toplevel::is_recursive_type_def(td) {
1936                sections.push(toplevel::emit_recursive_decidable_eq(
1937                    toplevel::type_def_name(td),
1938                ));
1939            }
1940            sections.push(String::new());
1941        }
1942    }
1943
1944    // Type definitions
1945    for td in &ctx.type_defs {
1946        sections.push(toplevel::emit_type_def(td));
1947        // #18: Recursive types need unsafe DecidableEq instance
1948        if toplevel::is_recursive_type_def(td) {
1949            sections.push(toplevel::emit_recursive_decidable_eq(
1950                toplevel::type_def_name(td),
1951            ));
1952        }
1953        sections.push(String::new());
1954    }
1955
1956    // Emit pure functions in SCC-topological order:
1957    // callees first, then callers; SCCs as `mutual` blocks.
1958    emit_pure_functions(ctx, &recursive_fns, &mut sections);
1959
1960    // Decision blocks (as comments)
1961    for item in &ctx.items {
1962        if let TopLevel::Decision(db) = item {
1963            sections.push(toplevel::emit_decision(db));
1964            sections.push(String::new());
1965        }
1966    }
1967
1968    // Verify blocks → proof items (`native_decide` by default, optional `sorry`/theorem stubs).
1969    let mut verify_case_counters: HashMap<String, usize> = HashMap::new();
1970    for item in &ctx.items {
1971        if let TopLevel::Verify(vb) = item {
1972            let key = verify_counter_key(vb);
1973            let start_idx = *verify_case_counters.get(&key).unwrap_or(&0);
1974            let (emitted, next_idx) = toplevel::emit_verify_block(vb, ctx, verify_mode, start_idx);
1975            verify_case_counters.insert(key, next_idx);
1976            sections.push(emitted);
1977            sections.push(String::new());
1978        }
1979    }
1980
1981    let lean_body = sections.join("\n");
1982    let prelude = generate_prelude_for_body(&lean_body, false);
1983    let lean_source = format!("{prelude}\n\n{lean_body}");
1984
1985    // Project files
1986    let project_name = lean_project_name(ctx);
1987    let lakefile = generate_lakefile(&project_name);
1988    let toolchain = generate_toolchain();
1989
1990    ProjectOutput {
1991        files: vec![
1992            ("lakefile.lean".to_string(), lakefile),
1993            ("lean-toolchain".to_string(), toolchain),
1994            (format!("{}.lean", project_name), lean_source),
1995        ],
1996    }
1997}
1998
1999fn emit_pure_functions(
2000    ctx: &CodegenContext,
2001    recursive_fns: &HashSet<String>,
2002    sections: &mut Vec<String>,
2003) {
2004    let all_fns: Vec<&FnDef> = ctx
2005        .modules
2006        .iter()
2007        .flat_map(|m| m.fn_defs.iter())
2008        .chain(ctx.fn_defs.iter())
2009        .filter(|fd| toplevel::is_pure_fn(fd))
2010        .collect();
2011    if all_fns.is_empty() {
2012        return;
2013    }
2014
2015    let components = call_graph::ordered_fn_components(&all_fns, &ctx.module_prefixes);
2016    for fns in components {
2017        if fns.is_empty() {
2018            continue;
2019        }
2020
2021        // Multi-node SCC => emit `mutual ... end`.
2022        if fns.len() > 1 {
2023            sections.push(toplevel::emit_mutual_group(&fns, ctx));
2024            sections.push(String::new());
2025            continue;
2026        }
2027
2028        // Singleton SCC => regular `def` (recursive singletons still get `partial`
2029        // via `recursive_fns` in emit_fn_def).
2030        if let Some(code) = toplevel::emit_fn_def(fns[0], recursive_fns, ctx) {
2031            sections.push(code);
2032            sections.push(String::new());
2033        }
2034    }
2035}
2036
2037fn emit_pure_functions_proof(
2038    ctx: &CodegenContext,
2039    plans: &HashMap<String, RecursionPlan>,
2040    recursive_names: &HashSet<String>,
2041    sections: &mut Vec<String>,
2042) {
2043    let all_fns: Vec<&FnDef> = pure_fns(ctx);
2044    if all_fns.is_empty() {
2045        return;
2046    }
2047
2048    let components = call_graph::ordered_fn_components(&all_fns, &ctx.module_prefixes);
2049    for fns in components {
2050        if fns.is_empty() {
2051            continue;
2052        }
2053        if fns.len() > 1 {
2054            let all_supported = fns.iter().all(|fd| plans.contains_key(&fd.name));
2055            if all_supported {
2056                sections.push(toplevel::emit_mutual_group_proof(&fns, ctx, plans));
2057            } else {
2058                sections.push(toplevel::emit_mutual_group(&fns, ctx));
2059            }
2060            sections.push(String::new());
2061            continue;
2062        }
2063
2064        let fd = fns[0];
2065        let is_recursive = recursive_names.contains(&fd.name);
2066        let emitted = if is_recursive && !plans.contains_key(&fd.name) {
2067            toplevel::emit_fn_def(fd, recursive_names, ctx)
2068        } else {
2069            toplevel::emit_fn_def_proof(fd, plans.get(&fd.name).cloned(), ctx)
2070        };
2071        if let Some(code) = emitted {
2072            sections.push(code);
2073            sections.push(String::new());
2074        }
2075    }
2076}
2077
2078#[cfg(test)]
2079fn generate_prelude() -> String {
2080    generate_prelude_for_body("", true)
2081}
2082
2083fn generate_prelude_for_body(body: &str, include_all_helpers: bool) -> String {
2084    let mut parts = vec![
2085        LEAN_PRELUDE_HEADER.to_string(),
2086        LEAN_PRELUDE_FLOAT_COE.to_string(),
2087        LEAN_PRELUDE_FLOAT_DEC_EQ.to_string(),
2088        LEAN_PRELUDE_EXCEPT_DEC_EQ.to_string(),
2089        LEAN_PRELUDE_EXCEPT_NS.to_string(),
2090        LEAN_PRELUDE_OPTION_TO_EXCEPT.to_string(),
2091        LEAN_PRELUDE_STRING_HADD.to_string(),
2092    ];
2093
2094    if include_all_helpers || needs_builtin_named_type(body, "Header") {
2095        parts.push(LEAN_PRELUDE_HEADER_TYPE.to_string());
2096    }
2097    if include_all_helpers || needs_builtin_named_type(body, "HttpResponse") {
2098        parts.push(LEAN_PRELUDE_HTTP_RESPONSE_TYPE.to_string());
2099    }
2100    if include_all_helpers || needs_builtin_named_type(body, "HttpRequest") {
2101        parts.push(LEAN_PRELUDE_HTTP_REQUEST_TYPE.to_string());
2102    }
2103    if include_all_helpers || needs_builtin_named_type(body, "Tcp_Connection") {
2104        parts.push(LEAN_PRELUDE_TCP_CONNECTION_TYPE.to_string());
2105    }
2106
2107    if include_all_helpers || body.contains("averStringPosFuel") {
2108        parts.push(LEAN_PRELUDE_PROOF_FUEL.to_string());
2109    }
2110
2111    if include_all_helpers || body.contains("AverMeasure.") {
2112        parts.push(LEAN_PRELUDE_AVER_MEASURE.to_string());
2113    }
2114
2115    if include_all_helpers || body.contains("AverMap.") {
2116        parts.push(generate_map_prelude(body, include_all_helpers));
2117    }
2118
2119    parts.extend([
2120        LEAN_PRELUDE_AVER_LIST.to_string(),
2121        LEAN_PRELUDE_STRING_HELPERS.to_string(),
2122        LEAN_PRELUDE_NUMERIC_PARSE.to_string(),
2123        LEAN_PRELUDE_CHAR_BYTE.to_string(),
2124    ]);
2125
2126    parts.join("\n\n")
2127}
2128
2129fn needs_builtin_named_type(body: &str, type_name: &str) -> bool {
2130    references_named_type(body, type_name) && !declares_named_type(body, type_name)
2131}
2132
2133fn references_named_type(body: &str, type_name: &str) -> bool {
2134    let mut token = String::new();
2135    for ch in body.chars() {
2136        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' {
2137            token.push(ch);
2138            continue;
2139        }
2140        if token == type_name {
2141            return true;
2142        }
2143        token.clear();
2144    }
2145    token == type_name
2146}
2147
2148fn declares_named_type(body: &str, type_name: &str) -> bool {
2149    let structure_decl = format!("structure {} where", type_name);
2150    let inductive_decl = format!("inductive {} where", type_name);
2151    body.lines()
2152        .map(str::trim_start)
2153        .any(|line| line == structure_decl || line == inductive_decl)
2154}
2155
2156fn generate_map_prelude(body: &str, include_all_helpers: bool) -> String {
2157    let mut parts = vec![AVER_MAP_PRELUDE_BASE.to_string()];
2158
2159    let needs_has_set_self = include_all_helpers || body.contains("AverMap.has_set_self");
2160    let needs_get_set_self = include_all_helpers || body.contains("AverMap.get_set_self");
2161    let needs_get_set_other = include_all_helpers
2162        || body.contains("AverMap.get_set_other")
2163        || body.contains("AverMap.has_set_other");
2164    let needs_has_set_other = include_all_helpers || body.contains("AverMap.has_set_other");
2165
2166    if needs_has_set_self {
2167        parts.push(AVER_MAP_PRELUDE_HAS_SET_SELF.to_string());
2168    }
2169    if needs_get_set_self {
2170        parts.push(AVER_MAP_PRELUDE_GET_SET_SELF.to_string());
2171    }
2172    if needs_get_set_other {
2173        parts.push(AVER_MAP_PRELUDE_GET_SET_OTHER.to_string());
2174    }
2175    if needs_has_set_other {
2176        parts.push(AVER_MAP_PRELUDE_HAS_SET_OTHER.to_string());
2177    }
2178
2179    parts.push(AVER_MAP_PRELUDE_END.to_string());
2180    parts.join("\n\n")
2181}
2182
2183fn generate_lakefile(project_name: &str) -> String {
2184    format!(
2185        r#"import Lake
2186open Lake DSL
2187
2188package «{}» where
2189  version := v!"0.1.0"
2190
2191@[default_target]
2192lean_lib «{}» where
2193  srcDir := "."
2194"#,
2195        project_name.to_lowercase(),
2196        project_name
2197    )
2198}
2199
2200fn generate_toolchain() -> String {
2201    "leanprover/lean4:v4.15.0\n".to_string()
2202}
2203
2204fn capitalize_first(s: &str) -> String {
2205    let mut chars = s.chars();
2206    match chars.next() {
2207        None => String::new(),
2208        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
2209    }
2210}
2211
2212#[cfg(test)]
2213mod tests {
2214    use super::{
2215        VerifyEmitMode, generate_prelude, proof_mode_issues, recurrence, transpile,
2216        transpile_for_proof_mode, transpile_with_verify_mode,
2217    };
2218    use crate::ast::{
2219        BinOp, Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Spanned, Stmt, TopLevel, TypeDef,
2220        TypeVariant, VerifyBlock, VerifyGiven, VerifyGivenDomain, VerifyKind, VerifyLaw,
2221    };
2222
2223    /// Shorthand: wrap an Expr in Spanned with line=0.
2224    fn sb(e: Expr) -> Spanned<Expr> {
2225        Spanned::bare(e)
2226    }
2227    /// Shorthand: wrap an Expr in Box<Spanned> with line=0.
2228    fn sbb(e: Expr) -> Box<Spanned<Expr>> {
2229        Box::new(Spanned::bare(e))
2230    }
2231    use crate::codegen::{CodegenContext, build_context};
2232    use crate::source::parse_source;
2233    use crate::tco;
2234    use crate::types::checker::run_type_check_full;
2235    use std::collections::{HashMap, HashSet};
2236    use std::sync::Arc as Rc;
2237
2238    fn empty_ctx() -> CodegenContext {
2239        CodegenContext {
2240            items: vec![],
2241            fn_sigs: HashMap::new(),
2242            memo_fns: HashSet::new(),
2243            memo_safe_types: HashSet::new(),
2244            type_defs: vec![],
2245            fn_defs: vec![],
2246            project_name: "verify_mode".to_string(),
2247            modules: vec![],
2248            module_prefixes: HashSet::new(),
2249            policy: None,
2250            emit_replay_runtime: false,
2251            runtime_policy_from_env: false,
2252            guest_entry: None,
2253            emit_self_host_support: false,
2254            extra_fn_defs: Vec::new(),
2255            mutual_tco_members: HashSet::new(),
2256        }
2257    }
2258
2259    fn ctx_from_source(source: &str, project_name: &str) -> CodegenContext {
2260        let mut items = parse_source(source).expect("source should parse");
2261        tco::transform_program(&mut items);
2262        let tc = run_type_check_full(&items, None);
2263        assert!(
2264            tc.errors.is_empty(),
2265            "source should typecheck without errors: {:?}",
2266            tc.errors
2267        );
2268        build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
2269    }
2270
2271    fn generated_lean_file(out: &crate::codegen::ProjectOutput) -> &str {
2272        out.files
2273            .iter()
2274            .find_map(|(name, content)| {
2275                (name.ends_with(".lean") && name != "lakefile.lean").then_some(content.as_str())
2276            })
2277            .expect("expected generated Lean file")
2278    }
2279
2280    fn empty_ctx_with_verify_case() -> CodegenContext {
2281        let mut ctx = empty_ctx();
2282        ctx.items.push(TopLevel::Verify(VerifyBlock {
2283            fn_name: "f".to_string(),
2284            line: 1,
2285            cases: vec![(
2286                sb(Expr::Literal(Literal::Int(1))),
2287                sb(Expr::Literal(Literal::Int(1))),
2288            )],
2289            case_spans: vec![],
2290            case_givens: vec![],
2291            kind: VerifyKind::Cases,
2292        }));
2293        ctx
2294    }
2295
2296    fn empty_ctx_with_two_verify_blocks_same_fn() -> CodegenContext {
2297        let mut ctx = empty_ctx();
2298        ctx.items.push(TopLevel::Verify(VerifyBlock {
2299            fn_name: "f".to_string(),
2300            line: 1,
2301            cases: vec![(
2302                sb(Expr::Literal(Literal::Int(1))),
2303                sb(Expr::Literal(Literal::Int(1))),
2304            )],
2305            case_spans: vec![],
2306            case_givens: vec![],
2307            kind: VerifyKind::Cases,
2308        }));
2309        ctx.items.push(TopLevel::Verify(VerifyBlock {
2310            fn_name: "f".to_string(),
2311            line: 2,
2312            cases: vec![(
2313                sb(Expr::Literal(Literal::Int(2))),
2314                sb(Expr::Literal(Literal::Int(2))),
2315            )],
2316            case_spans: vec![],
2317            case_givens: vec![],
2318            kind: VerifyKind::Cases,
2319        }));
2320        ctx
2321    }
2322
2323    fn empty_ctx_with_verify_law() -> CodegenContext {
2324        let mut ctx = empty_ctx();
2325        let add = FnDef {
2326            name: "add".to_string(),
2327            line: 1,
2328            params: vec![
2329                ("a".to_string(), "Int".to_string()),
2330                ("b".to_string(), "Int".to_string()),
2331            ],
2332            return_type: "Int".to_string(),
2333            effects: vec![],
2334            desc: None,
2335            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
2336                BinOp::Add,
2337                sbb(Expr::Ident("a".to_string())),
2338                sbb(Expr::Ident("b".to_string())),
2339            )))),
2340            resolution: None,
2341        };
2342        ctx.fn_defs.push(add.clone());
2343        ctx.items.push(TopLevel::FnDef(add));
2344        ctx.items.push(TopLevel::Verify(VerifyBlock {
2345            fn_name: "add".to_string(),
2346            line: 1,
2347            cases: vec![
2348                (
2349                    sb(Expr::FnCall(
2350                        sbb(Expr::Ident("add".to_string())),
2351                        vec![
2352                            sb(Expr::Literal(Literal::Int(1))),
2353                            sb(Expr::Literal(Literal::Int(2))),
2354                        ],
2355                    )),
2356                    sb(Expr::FnCall(
2357                        sbb(Expr::Ident("add".to_string())),
2358                        vec![
2359                            sb(Expr::Literal(Literal::Int(2))),
2360                            sb(Expr::Literal(Literal::Int(1))),
2361                        ],
2362                    )),
2363                ),
2364                (
2365                    sb(Expr::FnCall(
2366                        sbb(Expr::Ident("add".to_string())),
2367                        vec![
2368                            sb(Expr::Literal(Literal::Int(2))),
2369                            sb(Expr::Literal(Literal::Int(3))),
2370                        ],
2371                    )),
2372                    sb(Expr::FnCall(
2373                        sbb(Expr::Ident("add".to_string())),
2374                        vec![
2375                            sb(Expr::Literal(Literal::Int(3))),
2376                            sb(Expr::Literal(Literal::Int(2))),
2377                        ],
2378                    )),
2379                ),
2380            ],
2381            case_spans: vec![],
2382            case_givens: vec![],
2383            kind: VerifyKind::Law(Box::new(VerifyLaw {
2384                name: "commutative".to_string(),
2385                givens: vec![
2386                    VerifyGiven {
2387                        name: "a".to_string(),
2388                        type_name: "Int".to_string(),
2389                        domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
2390                    },
2391                    VerifyGiven {
2392                        name: "b".to_string(),
2393                        type_name: "Int".to_string(),
2394                        domain: VerifyGivenDomain::Explicit(vec![
2395                            sb(Expr::Literal(Literal::Int(2))),
2396                            sb(Expr::Literal(Literal::Int(3))),
2397                        ]),
2398                    },
2399                ],
2400                when: None,
2401                lhs: sb(Expr::FnCall(
2402                    sbb(Expr::Ident("add".to_string())),
2403                    vec![
2404                        sb(Expr::Ident("a".to_string())),
2405                        sb(Expr::Ident("b".to_string())),
2406                    ],
2407                )),
2408                rhs: sb(Expr::FnCall(
2409                    sbb(Expr::Ident("add".to_string())),
2410                    vec![
2411                        sb(Expr::Ident("b".to_string())),
2412                        sb(Expr::Ident("a".to_string())),
2413                    ],
2414                )),
2415                sample_guards: vec![],
2416            })),
2417        }));
2418        ctx
2419    }
2420
2421    #[test]
2422    fn prelude_normalizes_float_string_format() {
2423        let prelude = generate_prelude();
2424        assert!(
2425            prelude.contains("private def normalizeFloatString (s : String) : String :="),
2426            "missing normalizeFloatString helper in prelude"
2427        );
2428        assert!(
2429            prelude.contains(
2430                "def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)"
2431            ),
2432            "String.fromFloat should normalize Lean float formatting"
2433        );
2434    }
2435
2436    #[test]
2437    fn prelude_validates_char_from_code_unicode_bounds() {
2438        let prelude = generate_prelude();
2439        assert!(
2440            prelude.contains("if n < 0 || n > 1114111 then none"),
2441            "Char.fromCode should reject code points above Unicode max"
2442        );
2443        assert!(
2444            prelude.contains("else if n >= 55296 && n <= 57343 then none"),
2445            "Char.fromCode should reject surrogate code points"
2446        );
2447    }
2448
2449    #[test]
2450    fn prelude_includes_map_set_helper_lemmas() {
2451        let prelude = generate_prelude();
2452        assert!(
2453            prelude.contains("theorem has_set_self [DecidableEq α]"),
2454            "missing AverMap.has_set_self helper theorem"
2455        );
2456        assert!(
2457            prelude.contains("theorem get_set_self [DecidableEq α]"),
2458            "missing AverMap.get_set_self helper theorem"
2459        );
2460    }
2461
2462    #[test]
2463    fn lean_output_without_map_usage_omits_map_prelude() {
2464        let ctx = ctx_from_source(
2465            r#"
2466module NoMap
2467    intent = "Simple pure program without maps."
2468
2469fn addOne(n: Int) -> Int
2470    n + 1
2471
2472verify addOne
2473    addOne(1) => 2
2474"#,
2475            "nomap",
2476        );
2477        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2478        let lean = generated_lean_file(&out);
2479
2480        assert!(
2481            !lean.contains("namespace AverMap"),
2482            "did not expect AverMap prelude in program without map usage:\n{}",
2483            lean
2484        );
2485    }
2486
2487    #[test]
2488    fn transpile_emits_native_decide_for_verify_by_default() {
2489        let out = transpile(&empty_ctx_with_verify_case());
2490        let lean = out
2491            .files
2492            .iter()
2493            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2494            .expect("expected generated Lean file");
2495        assert!(lean.contains("example : 1 = 1 := by native_decide"));
2496    }
2497
2498    #[test]
2499    fn transpile_can_emit_sorry_for_verify_when_requested() {
2500        let out = transpile_with_verify_mode(&empty_ctx_with_verify_case(), VerifyEmitMode::Sorry);
2501        let lean = out
2502            .files
2503            .iter()
2504            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2505            .expect("expected generated Lean file");
2506        assert!(lean.contains("example : 1 = 1 := by sorry"));
2507    }
2508
2509    #[test]
2510    fn transpile_can_emit_theorem_skeletons_for_verify() {
2511        let out = transpile_with_verify_mode(
2512            &empty_ctx_with_verify_case(),
2513            VerifyEmitMode::TheoremSkeleton,
2514        );
2515        let lean = out
2516            .files
2517            .iter()
2518            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2519            .expect("expected generated Lean file");
2520        assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
2521        assert!(lean.contains("  sorry"));
2522    }
2523
2524    #[test]
2525    fn theorem_skeleton_numbering_is_global_per_function_across_verify_blocks() {
2526        let out = transpile_with_verify_mode(
2527            &empty_ctx_with_two_verify_blocks_same_fn(),
2528            VerifyEmitMode::TheoremSkeleton,
2529        );
2530        let lean = out
2531            .files
2532            .iter()
2533            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2534            .expect("expected generated Lean file");
2535        assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
2536        assert!(lean.contains("theorem f_verify_2 : 2 = 2 := by"));
2537    }
2538
2539    #[test]
2540    fn transpile_emits_named_theorems_for_verify_law() {
2541        let out = transpile(&empty_ctx_with_verify_law());
2542        let lean = out
2543            .files
2544            .iter()
2545            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2546            .expect("expected generated Lean file");
2547        assert!(lean.contains("-- verify law add.commutative (2 cases)"));
2548        assert!(lean.contains("-- given a: Int = 1..2"));
2549        assert!(lean.contains("-- given b: Int = [2, 3]"));
2550        assert!(lean.contains(
2551            "theorem add_law_commutative : ∀ (a : Int) (b : Int), add a b = add b a := by"
2552        ));
2553        assert!(lean.contains("  intro a b"));
2554        assert!(lean.contains("  simp [add, Int.add_comm]"));
2555        assert!(lean.contains(
2556            "theorem add_law_commutative_sample_1 : add 1 2 = add 2 1 := by native_decide"
2557        ));
2558        assert!(lean.contains(
2559            "theorem add_law_commutative_sample_2 : add 2 3 = add 3 2 := by native_decide"
2560        ));
2561    }
2562
2563    #[test]
2564    fn generate_prelude_emits_int_roundtrip_theorem() {
2565        let lean = generate_prelude();
2566        assert!(lean.contains(
2567            "theorem Int.fromString_fromInt : ∀ n : Int, Int.fromString (String.fromInt n) = .ok n"
2568        ));
2569        assert!(lean.contains("theorem String.intercalate_empty_chars (s : String) :"));
2570        assert!(lean.contains("def splitOnCharGo"));
2571        assert!(lean.contains("theorem split_single_char_append"));
2572        assert!(lean.contains("theorem split_intercalate_trailing_single_char"));
2573        assert!(lean.contains("namespace AverDigits"));
2574        assert!(lean.contains("theorem String.charAt_length_none (s : String)"));
2575        assert!(lean.contains("theorem digitChar_not_ws : ∀ d : Nat, d < 10 ->"));
2576    }
2577
2578    #[test]
2579    fn transpile_emits_guarded_theorems_for_verify_law_when_clause() {
2580        let ctx = ctx_from_source(
2581            r#"
2582module GuardedLaw
2583    intent =
2584        "verify law with precondition"
2585
2586fn pickGreater(a: Int, b: Int) -> Int
2587    match a > b
2588        true -> a
2589        false -> b
2590
2591verify pickGreater law ordered
2592    given a: Int = [1, 2]
2593    given b: Int = [1, 2]
2594    when a > b
2595    pickGreater(a, b) => a
2596"#,
2597            "guarded_law",
2598        );
2599        let out = transpile_with_verify_mode(&ctx, VerifyEmitMode::TheoremSkeleton);
2600        let lean = generated_lean_file(&out);
2601
2602        assert!(lean.contains("-- when (a > b)"));
2603        assert!(lean.contains(
2604            "theorem pickGreater_law_ordered : ∀ (a : Int) (b : Int), a = 1 ∨ a = 2 -> b = 1 ∨ b = 2 -> (a > b) = true -> pickGreater a b = a := by"
2605        ));
2606        assert!(lean.contains(
2607            "theorem pickGreater_law_ordered_sample_1 : (1 > 1) = true -> pickGreater 1 1 = 1 := by"
2608        ));
2609        assert!(lean.contains(
2610            "theorem pickGreater_law_ordered_sample_4 : (2 > 2) = true -> pickGreater 2 2 = 2 := by"
2611        ));
2612    }
2613
2614    #[test]
2615    fn transpile_uses_spec_theorem_names_for_declared_spec_laws() {
2616        let ctx = ctx_from_source(
2617            r#"
2618module SpecDemo
2619    intent =
2620        "spec demo"
2621
2622fn absVal(x: Int) -> Int
2623    match x < 0
2624        true -> 0 - x
2625        false -> x
2626
2627fn absValSpec(x: Int) -> Int
2628    match x < 0
2629        true -> 0 - x
2630        false -> x
2631
2632verify absVal law absValSpec
2633    given x: Int = [-2, -1, 0, 1, 2]
2634    absVal(x) => absValSpec(x)
2635"#,
2636            "spec_demo",
2637        );
2638        let out = transpile_with_verify_mode(&ctx, VerifyEmitMode::TheoremSkeleton);
2639        let lean = generated_lean_file(&out);
2640
2641        assert!(lean.contains("-- verify law absVal.spec absValSpec (5 cases)"));
2642        assert!(
2643            lean.contains(
2644                "theorem absVal_eq_absValSpec : ∀ (x : Int), absVal x = absValSpec x := by"
2645            )
2646        );
2647        assert!(lean.contains("theorem absVal_eq_absValSpec_checked_domain :"));
2648        assert!(lean.contains("theorem absVal_eq_absValSpec_sample_1 :"));
2649        assert!(!lean.contains("theorem absVal_law_absValSpec :"));
2650    }
2651
2652    #[test]
2653    fn transpile_keeps_noncanonical_spec_laws_as_regular_law_names() {
2654        let ctx = ctx_from_source(
2655            r#"
2656module SpecLawShape
2657    intent =
2658        "shape probe"
2659
2660fn foo(x: Int) -> Int
2661    x + 1
2662
2663fn fooSpec(seed: Int, x: Int) -> Int
2664    x + seed
2665
2666verify foo law fooSpec
2667    given x: Int = [1, 2]
2668    foo(x) => fooSpec(1, x)
2669"#,
2670            "spec_law_shape",
2671        );
2672        let out = transpile_with_verify_mode(&ctx, VerifyEmitMode::TheoremSkeleton);
2673        let lean = generated_lean_file(&out);
2674
2675        assert!(lean.contains("-- verify law foo.fooSpec (2 cases)"));
2676        assert!(lean.contains("theorem foo_law_fooSpec : ∀ (x : Int), foo x = fooSpec 1 x := by"));
2677        assert!(!lean.contains("theorem foo_eq_fooSpec :"));
2678    }
2679
2680    #[test]
2681    fn transpile_auto_proves_linear_int_canonical_spec_law_in_auto_mode() {
2682        let ctx = ctx_from_source(
2683            r#"
2684module SpecGap
2685    intent =
2686        "nontrivial canonical spec law"
2687
2688fn inc(x: Int) -> Int
2689    x + 1
2690
2691fn incSpec(x: Int) -> Int
2692    x + 2 - 1
2693
2694verify inc law incSpec
2695    given x: Int = [0, 1, 2]
2696    inc(x) => incSpec(x)
2697"#,
2698            "spec_gap",
2699        );
2700        let out = transpile(&ctx);
2701        let lean = generated_lean_file(&out);
2702
2703        assert!(lean.contains("-- verify law inc.spec incSpec (3 cases)"));
2704        assert!(lean.contains("theorem inc_eq_incSpec : ∀ (x : Int), inc x = incSpec x := by"));
2705        assert!(lean.contains("change (x + 1) = ((x + 2) - 1)"));
2706        assert!(lean.contains("omega"));
2707        assert!(!lean.contains(
2708            "-- universal theorem inc_eq_incSpec omitted: sampled law shape is not auto-proved yet"
2709        ));
2710        assert!(lean.contains("theorem inc_eq_incSpec_checked_domain :"));
2711    }
2712
2713    #[test]
2714    fn transpile_auto_proves_guarded_canonical_spec_law_in_auto_mode() {
2715        let ctx = ctx_from_source(
2716            r#"
2717module GuardedSpecGap
2718    intent =
2719        "guarded canonical spec law"
2720
2721fn clampNonNegative(x: Int) -> Int
2722    match x < 0
2723        true -> 0
2724        false -> x
2725
2726fn clampNonNegativeSpec(x: Int) -> Int
2727    match x < 0
2728        true -> 0
2729        false -> x
2730
2731verify clampNonNegative law clampNonNegativeSpec
2732    given x: Int = [-2, -1, 0, 1, 2]
2733    when x >= 0
2734    clampNonNegative(x) => clampNonNegativeSpec(x)
2735"#,
2736            "guarded_spec_gap",
2737        );
2738        let out = transpile(&ctx);
2739        let lean = generated_lean_file(&out);
2740
2741        assert!(lean.contains("-- when (x >= 0)"));
2742        assert!(lean.contains(
2743            "theorem clampNonNegative_eq_clampNonNegativeSpec : ∀ (x : Int), x = (-2) ∨ x = (-1) ∨ x = 0 ∨ x = 1 ∨ x = 2 -> (x >= 0) = true -> clampNonNegative x = clampNonNegativeSpec x := by"
2744        ));
2745        assert!(lean.contains("intro x h_x h_when"));
2746        assert!(lean.contains("simpa [clampNonNegative, clampNonNegativeSpec]"));
2747        assert!(!lean.contains(
2748            "-- universal theorem clampNonNegative_eq_clampNonNegativeSpec omitted: sampled law shape is not auto-proved yet"
2749        ));
2750        assert!(!lean.contains("cases h_x"));
2751    }
2752
2753    #[test]
2754    fn transpile_auto_proves_simp_normalized_canonical_spec_law_in_auto_mode() {
2755        let ctx = ctx_from_source(
2756            r#"
2757module SpecGapNonlinear
2758    intent =
2759        "nonlinear canonical spec law"
2760
2761fn square(x: Int) -> Int
2762    x * x
2763
2764fn squareSpec(x: Int) -> Int
2765    x * x + 0
2766
2767verify square law squareSpec
2768    given x: Int = [0, 1, 2]
2769    square(x) => squareSpec(x)
2770"#,
2771            "spec_gap_nonlinear",
2772        );
2773        let out = transpile(&ctx);
2774        let lean = generated_lean_file(&out);
2775
2776        assert!(lean.contains("-- verify law square.spec squareSpec (3 cases)"));
2777        assert!(
2778            lean.contains(
2779                "theorem square_eq_squareSpec : ∀ (x : Int), square x = squareSpec x := by"
2780            )
2781        );
2782        assert!(lean.contains("simp [square, squareSpec]"));
2783        assert!(!lean.contains(
2784            "-- universal theorem square_eq_squareSpec omitted: sampled law shape is not auto-proved yet"
2785        ));
2786        assert!(lean.contains("theorem square_eq_squareSpec_checked_domain :"));
2787        assert!(lean.contains("theorem square_eq_squareSpec_sample_1 :"));
2788    }
2789
2790    #[test]
2791    fn transpile_auto_proves_reflexive_law_with_rfl() {
2792        let mut ctx = empty_ctx();
2793        ctx.items.push(TopLevel::Verify(VerifyBlock {
2794            fn_name: "idLaw".to_string(),
2795            line: 1,
2796            cases: vec![(
2797                sb(Expr::Literal(Literal::Int(1))),
2798                sb(Expr::Literal(Literal::Int(1))),
2799            )],
2800            case_spans: vec![],
2801            case_givens: vec![],
2802            kind: VerifyKind::Law(Box::new(VerifyLaw {
2803                name: "reflexive".to_string(),
2804                givens: vec![VerifyGiven {
2805                    name: "x".to_string(),
2806                    type_name: "Int".to_string(),
2807                    domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
2808                }],
2809                when: None,
2810                lhs: sb(Expr::Ident("x".to_string())),
2811                rhs: sb(Expr::Ident("x".to_string())),
2812                sample_guards: vec![],
2813            })),
2814        }));
2815        let out = transpile(&ctx);
2816        let lean = out
2817            .files
2818            .iter()
2819            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2820            .expect("expected generated Lean file");
2821        assert!(lean.contains("theorem idLaw_law_reflexive : ∀ (x : Int), x = x := by"));
2822        assert!(lean.contains("  intro x"));
2823        assert!(lean.contains("  rfl"));
2824    }
2825
2826    #[test]
2827    fn transpile_auto_proves_identity_law_for_int_add_wrapper() {
2828        let mut ctx = empty_ctx_with_verify_law();
2829        ctx.items.push(TopLevel::Verify(VerifyBlock {
2830            fn_name: "add".to_string(),
2831            line: 10,
2832            cases: vec![(
2833                sb(Expr::FnCall(
2834                    sbb(Expr::Ident("add".to_string())),
2835                    vec![
2836                        sb(Expr::Literal(Literal::Int(1))),
2837                        sb(Expr::Literal(Literal::Int(0))),
2838                    ],
2839                )),
2840                sb(Expr::Literal(Literal::Int(1))),
2841            )],
2842            case_spans: vec![],
2843            case_givens: vec![],
2844            kind: VerifyKind::Law(Box::new(VerifyLaw {
2845                name: "identityZero".to_string(),
2846                givens: vec![VerifyGiven {
2847                    name: "a".to_string(),
2848                    type_name: "Int".to_string(),
2849                    domain: VerifyGivenDomain::Explicit(vec![
2850                        sb(Expr::Literal(Literal::Int(0))),
2851                        sb(Expr::Literal(Literal::Int(1))),
2852                    ]),
2853                }],
2854                when: None,
2855                lhs: sb(Expr::FnCall(
2856                    sbb(Expr::Ident("add".to_string())),
2857                    vec![
2858                        sb(Expr::Ident("a".to_string())),
2859                        sb(Expr::Literal(Literal::Int(0))),
2860                    ],
2861                )),
2862                rhs: sb(Expr::Ident("a".to_string())),
2863                sample_guards: vec![],
2864            })),
2865        }));
2866        let out = transpile(&ctx);
2867        let lean = out
2868            .files
2869            .iter()
2870            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2871            .expect("expected generated Lean file");
2872        assert!(lean.contains("theorem add_law_identityZero : ∀ (a : Int), add a 0 = a := by"));
2873        assert!(lean.contains("  intro a"));
2874        assert!(lean.contains("  simp [add]"));
2875    }
2876
2877    #[test]
2878    fn transpile_auto_proves_associative_law_for_int_add_wrapper() {
2879        let mut ctx = empty_ctx_with_verify_law();
2880        ctx.items.push(TopLevel::Verify(VerifyBlock {
2881            fn_name: "add".to_string(),
2882            line: 20,
2883            cases: vec![(
2884                sb(Expr::FnCall(
2885                    sbb(Expr::Ident("add".to_string())),
2886                    vec![
2887                        sb(Expr::FnCall(
2888                            sbb(Expr::Ident("add".to_string())),
2889                            vec![
2890                                sb(Expr::Literal(Literal::Int(1))),
2891                                sb(Expr::Literal(Literal::Int(2))),
2892                            ],
2893                        )),
2894                        sb(Expr::Literal(Literal::Int(3))),
2895                    ],
2896                )),
2897                sb(Expr::FnCall(
2898                    sbb(Expr::Ident("add".to_string())),
2899                    vec![
2900                        sb(Expr::Literal(Literal::Int(1))),
2901                        sb(Expr::FnCall(
2902                            sbb(Expr::Ident("add".to_string())),
2903                            vec![
2904                                sb(Expr::Literal(Literal::Int(2))),
2905                                sb(Expr::Literal(Literal::Int(3))),
2906                            ],
2907                        )),
2908                    ],
2909                )),
2910            )],
2911            case_spans: vec![],
2912            case_givens: vec![],
2913            kind: VerifyKind::Law(Box::new(VerifyLaw {
2914                name: "associative".to_string(),
2915                givens: vec![
2916                    VerifyGiven {
2917                        name: "a".to_string(),
2918                        type_name: "Int".to_string(),
2919                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2920                            1,
2921                        )))]),
2922                    },
2923                    VerifyGiven {
2924                        name: "b".to_string(),
2925                        type_name: "Int".to_string(),
2926                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2927                            2,
2928                        )))]),
2929                    },
2930                    VerifyGiven {
2931                        name: "c".to_string(),
2932                        type_name: "Int".to_string(),
2933                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2934                            3,
2935                        )))]),
2936                    },
2937                ],
2938                when: None,
2939                lhs: sb(Expr::FnCall(
2940                    sbb(Expr::Ident("add".to_string())),
2941                    vec![
2942                        sb(Expr::FnCall(
2943                            sbb(Expr::Ident("add".to_string())),
2944                            vec![
2945                                sb(Expr::Ident("a".to_string())),
2946                                sb(Expr::Ident("b".to_string())),
2947                            ],
2948                        )),
2949                        sb(Expr::Ident("c".to_string())),
2950                    ],
2951                )),
2952                rhs: sb(Expr::FnCall(
2953                    sbb(Expr::Ident("add".to_string())),
2954                    vec![
2955                        sb(Expr::Ident("a".to_string())),
2956                        sb(Expr::FnCall(
2957                            sbb(Expr::Ident("add".to_string())),
2958                            vec![
2959                                sb(Expr::Ident("b".to_string())),
2960                                sb(Expr::Ident("c".to_string())),
2961                            ],
2962                        )),
2963                    ],
2964                )),
2965                sample_guards: vec![],
2966            })),
2967        }));
2968        let out = transpile(&ctx);
2969        let lean = out
2970            .files
2971            .iter()
2972            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2973            .expect("expected generated Lean file");
2974        assert!(lean.contains(
2975            "theorem add_law_associative : ∀ (a : Int) (b : Int) (c : Int), add (add a b) c = add a (add b c) := by"
2976        ));
2977        assert!(lean.contains("  intro a b c"));
2978        assert!(lean.contains("  simp [add, Int.add_assoc]"));
2979    }
2980
2981    #[test]
2982    fn transpile_auto_proves_sub_laws() {
2983        let mut ctx = empty_ctx();
2984        let sub = FnDef {
2985            name: "sub".to_string(),
2986            line: 1,
2987            params: vec![
2988                ("a".to_string(), "Int".to_string()),
2989                ("b".to_string(), "Int".to_string()),
2990            ],
2991            return_type: "Int".to_string(),
2992            effects: vec![],
2993            desc: None,
2994            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
2995                BinOp::Sub,
2996                sbb(Expr::Ident("a".to_string())),
2997                sbb(Expr::Ident("b".to_string())),
2998            )))),
2999            resolution: None,
3000        };
3001        ctx.fn_defs.push(sub.clone());
3002        ctx.items.push(TopLevel::FnDef(sub));
3003
3004        ctx.items.push(TopLevel::Verify(VerifyBlock {
3005            fn_name: "sub".to_string(),
3006            line: 10,
3007            cases: vec![(
3008                sb(Expr::FnCall(
3009                    sbb(Expr::Ident("sub".to_string())),
3010                    vec![
3011                        sb(Expr::Literal(Literal::Int(2))),
3012                        sb(Expr::Literal(Literal::Int(0))),
3013                    ],
3014                )),
3015                sb(Expr::Literal(Literal::Int(2))),
3016            )],
3017            case_spans: vec![],
3018            case_givens: vec![],
3019            kind: VerifyKind::Law(Box::new(VerifyLaw {
3020                name: "rightIdentity".to_string(),
3021                givens: vec![VerifyGiven {
3022                    name: "a".to_string(),
3023                    type_name: "Int".to_string(),
3024                    domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(2)))]),
3025                }],
3026                when: None,
3027                lhs: sb(Expr::FnCall(
3028                    sbb(Expr::Ident("sub".to_string())),
3029                    vec![
3030                        sb(Expr::Ident("a".to_string())),
3031                        sb(Expr::Literal(Literal::Int(0))),
3032                    ],
3033                )),
3034                rhs: sb(Expr::Ident("a".to_string())),
3035                sample_guards: vec![],
3036            })),
3037        }));
3038        ctx.items.push(TopLevel::Verify(VerifyBlock {
3039            fn_name: "sub".to_string(),
3040            line: 20,
3041            cases: vec![(
3042                sb(Expr::FnCall(
3043                    sbb(Expr::Ident("sub".to_string())),
3044                    vec![
3045                        sb(Expr::Literal(Literal::Int(2))),
3046                        sb(Expr::Literal(Literal::Int(1))),
3047                    ],
3048                )),
3049                sb(Expr::BinOp(
3050                    BinOp::Sub,
3051                    sbb(Expr::Literal(Literal::Int(0))),
3052                    sbb(Expr::FnCall(
3053                        sbb(Expr::Ident("sub".to_string())),
3054                        vec![
3055                            sb(Expr::Literal(Literal::Int(1))),
3056                            sb(Expr::Literal(Literal::Int(2))),
3057                        ],
3058                    )),
3059                )),
3060            )],
3061            case_spans: vec![],
3062            case_givens: vec![],
3063            kind: VerifyKind::Law(Box::new(VerifyLaw {
3064                name: "antiCommutative".to_string(),
3065                givens: vec![
3066                    VerifyGiven {
3067                        name: "a".to_string(),
3068                        type_name: "Int".to_string(),
3069                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3070                            2,
3071                        )))]),
3072                    },
3073                    VerifyGiven {
3074                        name: "b".to_string(),
3075                        type_name: "Int".to_string(),
3076                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3077                            1,
3078                        )))]),
3079                    },
3080                ],
3081                when: None,
3082                lhs: sb(Expr::FnCall(
3083                    sbb(Expr::Ident("sub".to_string())),
3084                    vec![
3085                        sb(Expr::Ident("a".to_string())),
3086                        sb(Expr::Ident("b".to_string())),
3087                    ],
3088                )),
3089                rhs: sb(Expr::BinOp(
3090                    BinOp::Sub,
3091                    sbb(Expr::Literal(Literal::Int(0))),
3092                    sbb(Expr::FnCall(
3093                        sbb(Expr::Ident("sub".to_string())),
3094                        vec![
3095                            sb(Expr::Ident("b".to_string())),
3096                            sb(Expr::Ident("a".to_string())),
3097                        ],
3098                    )),
3099                )),
3100                sample_guards: vec![],
3101            })),
3102        }));
3103
3104        let out = transpile(&ctx);
3105        let lean = out
3106            .files
3107            .iter()
3108            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3109            .expect("expected generated Lean file");
3110        assert!(lean.contains("theorem sub_law_rightIdentity : ∀ (a : Int), sub a 0 = a := by"));
3111        assert!(lean.contains("  simp [sub]"));
3112        assert!(lean.contains(
3113            "theorem sub_law_antiCommutative : ∀ (a : Int) (b : Int), sub a b = (-sub b a) := by"
3114        ));
3115        assert!(lean.contains("  simpa [sub] using (Int.neg_sub b a).symm"));
3116    }
3117
3118    #[test]
3119    fn transpile_auto_proves_unary_wrapper_equivalence_law() {
3120        let mut ctx = empty_ctx();
3121        let add = FnDef {
3122            name: "add".to_string(),
3123            line: 1,
3124            params: vec![
3125                ("a".to_string(), "Int".to_string()),
3126                ("b".to_string(), "Int".to_string()),
3127            ],
3128            return_type: "Int".to_string(),
3129            effects: vec![],
3130            desc: None,
3131            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
3132                BinOp::Add,
3133                sbb(Expr::Ident("a".to_string())),
3134                sbb(Expr::Ident("b".to_string())),
3135            )))),
3136            resolution: None,
3137        };
3138        let add_one = FnDef {
3139            name: "addOne".to_string(),
3140            line: 2,
3141            params: vec![("n".to_string(), "Int".to_string())],
3142            return_type: "Int".to_string(),
3143            effects: vec![],
3144            desc: None,
3145            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
3146                BinOp::Add,
3147                sbb(Expr::Ident("n".to_string())),
3148                sbb(Expr::Literal(Literal::Int(1))),
3149            )))),
3150            resolution: None,
3151        };
3152        ctx.fn_defs.push(add.clone());
3153        ctx.fn_defs.push(add_one.clone());
3154        ctx.items.push(TopLevel::FnDef(add));
3155        ctx.items.push(TopLevel::FnDef(add_one));
3156        ctx.items.push(TopLevel::Verify(VerifyBlock {
3157            fn_name: "addOne".to_string(),
3158            line: 3,
3159            cases: vec![(
3160                sb(Expr::FnCall(
3161                    sbb(Expr::Ident("addOne".to_string())),
3162                    vec![sb(Expr::Literal(Literal::Int(2)))],
3163                )),
3164                sb(Expr::FnCall(
3165                    sbb(Expr::Ident("add".to_string())),
3166                    vec![
3167                        sb(Expr::Literal(Literal::Int(2))),
3168                        sb(Expr::Literal(Literal::Int(1))),
3169                    ],
3170                )),
3171            )],
3172            case_spans: vec![],
3173            case_givens: vec![],
3174            kind: VerifyKind::Law(Box::new(VerifyLaw {
3175                name: "identityViaAdd".to_string(),
3176                givens: vec![VerifyGiven {
3177                    name: "n".to_string(),
3178                    type_name: "Int".to_string(),
3179                    domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(2)))]),
3180                }],
3181                when: None,
3182                lhs: sb(Expr::FnCall(
3183                    sbb(Expr::Ident("addOne".to_string())),
3184                    vec![sb(Expr::Ident("n".to_string()))],
3185                )),
3186                rhs: sb(Expr::FnCall(
3187                    sbb(Expr::Ident("add".to_string())),
3188                    vec![
3189                        sb(Expr::Ident("n".to_string())),
3190                        sb(Expr::Literal(Literal::Int(1))),
3191                    ],
3192                )),
3193                sample_guards: vec![],
3194            })),
3195        }));
3196        let out = transpile(&ctx);
3197        let lean = out
3198            .files
3199            .iter()
3200            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3201            .expect("expected generated Lean file");
3202        assert!(
3203            lean.contains(
3204                "theorem addOne_law_identityViaAdd : ∀ (n : Int), addOne n = add n 1 := by"
3205            )
3206        );
3207        assert!(lean.contains("  simp [addOne, add]"));
3208    }
3209
3210    #[test]
3211    fn transpile_auto_proves_direct_map_set_laws() {
3212        let mut ctx = empty_ctx();
3213
3214        let map_set = |m: Spanned<Expr>, k: Spanned<Expr>, v: Spanned<Expr>| {
3215            sb(Expr::FnCall(
3216                sbb(Expr::Attr(
3217                    sbb(Expr::Ident("Map".to_string())),
3218                    "set".to_string(),
3219                )),
3220                vec![m, k, v],
3221            ))
3222        };
3223        let map_has = |m: Spanned<Expr>, k: Spanned<Expr>| {
3224            sb(Expr::FnCall(
3225                sbb(Expr::Attr(
3226                    sbb(Expr::Ident("Map".to_string())),
3227                    "has".to_string(),
3228                )),
3229                vec![m, k],
3230            ))
3231        };
3232        let map_get = |m: Spanned<Expr>, k: Spanned<Expr>| {
3233            sb(Expr::FnCall(
3234                sbb(Expr::Attr(
3235                    sbb(Expr::Ident("Map".to_string())),
3236                    "get".to_string(),
3237                )),
3238                vec![m, k],
3239            ))
3240        };
3241        let some = |v: Spanned<Expr>| {
3242            sb(Expr::FnCall(
3243                sbb(Expr::Attr(
3244                    sbb(Expr::Ident("Option".to_string())),
3245                    "Some".to_string(),
3246                )),
3247                vec![v],
3248            ))
3249        };
3250        let map_empty = || {
3251            sb(Expr::FnCall(
3252                sbb(Expr::Attr(
3253                    sbb(Expr::Ident("Map".to_string())),
3254                    "empty".to_string(),
3255                )),
3256                vec![],
3257            ))
3258        };
3259
3260        ctx.items.push(TopLevel::Verify(VerifyBlock {
3261            fn_name: "map".to_string(),
3262            line: 1,
3263            cases: vec![(
3264                map_has(
3265                    map_set(
3266                        sb(Expr::Ident("m".to_string())),
3267                        sb(Expr::Ident("k".to_string())),
3268                        sb(Expr::Ident("v".to_string())),
3269                    ),
3270                    sb(Expr::Ident("k".to_string())),
3271                ),
3272                sb(Expr::Literal(Literal::Bool(true))),
3273            )],
3274            case_spans: vec![],
3275            case_givens: vec![],
3276            kind: VerifyKind::Law(Box::new(VerifyLaw {
3277                name: "setHasKey".to_string(),
3278                givens: vec![
3279                    VerifyGiven {
3280                        name: "m".to_string(),
3281                        type_name: "Map<String, Int>".to_string(),
3282                        domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
3283                    },
3284                    VerifyGiven {
3285                        name: "k".to_string(),
3286                        type_name: "String".to_string(),
3287                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Str(
3288                            "a".to_string(),
3289                        )))]),
3290                    },
3291                    VerifyGiven {
3292                        name: "v".to_string(),
3293                        type_name: "Int".to_string(),
3294                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3295                            1,
3296                        )))]),
3297                    },
3298                ],
3299                when: None,
3300                lhs: map_has(
3301                    map_set(
3302                        sb(Expr::Ident("m".to_string())),
3303                        sb(Expr::Ident("k".to_string())),
3304                        sb(Expr::Ident("v".to_string())),
3305                    ),
3306                    sb(Expr::Ident("k".to_string())),
3307                ),
3308                rhs: sb(Expr::Literal(Literal::Bool(true))),
3309                sample_guards: vec![],
3310            })),
3311        }));
3312
3313        ctx.items.push(TopLevel::Verify(VerifyBlock {
3314            fn_name: "map".to_string(),
3315            line: 2,
3316            cases: vec![(
3317                map_get(
3318                    map_set(
3319                        sb(Expr::Ident("m".to_string())),
3320                        sb(Expr::Ident("k".to_string())),
3321                        sb(Expr::Ident("v".to_string())),
3322                    ),
3323                    sb(Expr::Ident("k".to_string())),
3324                ),
3325                some(sb(Expr::Ident("v".to_string()))),
3326            )],
3327            case_spans: vec![],
3328            case_givens: vec![],
3329            kind: VerifyKind::Law(Box::new(VerifyLaw {
3330                name: "setGetKey".to_string(),
3331                givens: vec![
3332                    VerifyGiven {
3333                        name: "m".to_string(),
3334                        type_name: "Map<String, Int>".to_string(),
3335                        domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
3336                    },
3337                    VerifyGiven {
3338                        name: "k".to_string(),
3339                        type_name: "String".to_string(),
3340                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Str(
3341                            "a".to_string(),
3342                        )))]),
3343                    },
3344                    VerifyGiven {
3345                        name: "v".to_string(),
3346                        type_name: "Int".to_string(),
3347                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3348                            1,
3349                        )))]),
3350                    },
3351                ],
3352                when: None,
3353                lhs: map_get(
3354                    map_set(
3355                        sb(Expr::Ident("m".to_string())),
3356                        sb(Expr::Ident("k".to_string())),
3357                        sb(Expr::Ident("v".to_string())),
3358                    ),
3359                    sb(Expr::Ident("k".to_string())),
3360                ),
3361                rhs: some(sb(Expr::Ident("v".to_string()))),
3362                sample_guards: vec![],
3363            })),
3364        }));
3365
3366        let out = transpile(&ctx);
3367        let lean = out
3368            .files
3369            .iter()
3370            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3371            .expect("expected generated Lean file");
3372        assert!(lean.contains("simpa using AverMap.has_set_self m k v"));
3373        assert!(lean.contains("simpa using AverMap.get_set_self m k v"));
3374    }
3375
3376    #[test]
3377    fn transpile_auto_proves_direct_recursive_sum_law_by_structural_induction() {
3378        let ctx = ctx_from_source(
3379            r#"
3380module Mirror
3381    intent =
3382        "direct recursive sum induction probe"
3383
3384type Tree
3385    Leaf(Int)
3386    Node(Tree, Tree)
3387
3388fn mirror(t: Tree) -> Tree
3389    match t
3390        Tree.Leaf(v) -> Tree.Leaf(v)
3391        Tree.Node(left, right) -> Tree.Node(mirror(right), mirror(left))
3392
3393verify mirror law involutive
3394    given t: Tree = [Tree.Leaf(1), Tree.Node(Tree.Leaf(1), Tree.Leaf(2))]
3395    mirror(mirror(t)) => t
3396"#,
3397            "mirror",
3398        );
3399        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
3400        let lean = generated_lean_file(&out);
3401
3402        assert!(
3403            lean.contains(
3404                "theorem mirror_law_involutive : ∀ (t : Tree), mirror (mirror t) = t := by"
3405            )
3406        );
3407        assert!(lean.contains("  induction t with"));
3408        assert!(lean.contains("  | leaf f0 => simp [mirror]"));
3409        assert!(lean.contains("  | node f0 f1 ih0 ih1 => simp_all [mirror]"));
3410        assert!(!lean.contains(
3411            "-- universal theorem mirror_law_involutive omitted: sampled law shape is not auto-proved yet"
3412        ));
3413    }
3414
3415    #[test]
3416    fn transpile_auto_proves_map_update_laws() {
3417        let mut ctx = empty_ctx();
3418
3419        let map_get = |m: Spanned<Expr>, k: Spanned<Expr>| {
3420            sb(Expr::FnCall(
3421                sbb(Expr::Attr(
3422                    sbb(Expr::Ident("Map".to_string())),
3423                    "get".to_string(),
3424                )),
3425                vec![m, k],
3426            ))
3427        };
3428        let map_set = |m: Spanned<Expr>, k: Spanned<Expr>, v: Spanned<Expr>| {
3429            sb(Expr::FnCall(
3430                sbb(Expr::Attr(
3431                    sbb(Expr::Ident("Map".to_string())),
3432                    "set".to_string(),
3433                )),
3434                vec![m, k, v],
3435            ))
3436        };
3437        let map_has = |m: Spanned<Expr>, k: Spanned<Expr>| {
3438            sb(Expr::FnCall(
3439                sbb(Expr::Attr(
3440                    sbb(Expr::Ident("Map".to_string())),
3441                    "has".to_string(),
3442                )),
3443                vec![m, k],
3444            ))
3445        };
3446        let option_some = |v: Spanned<Expr>| {
3447            sb(Expr::FnCall(
3448                sbb(Expr::Attr(
3449                    sbb(Expr::Ident("Option".to_string())),
3450                    "Some".to_string(),
3451                )),
3452                vec![v],
3453            ))
3454        };
3455        let option_with_default = |opt: Spanned<Expr>, def: Spanned<Expr>| {
3456            sb(Expr::FnCall(
3457                sbb(Expr::Attr(
3458                    sbb(Expr::Ident("Option".to_string())),
3459                    "withDefault".to_string(),
3460                )),
3461                vec![opt, def],
3462            ))
3463        };
3464        let map_empty = || {
3465            sb(Expr::FnCall(
3466                sbb(Expr::Attr(
3467                    sbb(Expr::Ident("Map".to_string())),
3468                    "empty".to_string(),
3469                )),
3470                vec![],
3471            ))
3472        };
3473
3474        let add_one = FnDef {
3475            name: "addOne".to_string(),
3476            line: 1,
3477            params: vec![("n".to_string(), "Int".to_string())],
3478            return_type: "Int".to_string(),
3479            effects: vec![],
3480            desc: None,
3481            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
3482                BinOp::Add,
3483                sbb(Expr::Ident("n".to_string())),
3484                sbb(Expr::Literal(Literal::Int(1))),
3485            )))),
3486            resolution: None,
3487        };
3488        ctx.fn_defs.push(add_one.clone());
3489        ctx.items.push(TopLevel::FnDef(add_one));
3490
3491        let inc_count = FnDef {
3492            name: "incCount".to_string(),
3493            line: 2,
3494            params: vec![
3495                ("counts".to_string(), "Map<String, Int>".to_string()),
3496                ("word".to_string(), "String".to_string()),
3497            ],
3498            return_type: "Map<String, Int>".to_string(),
3499            effects: vec![],
3500            desc: None,
3501            body: Rc::new(FnBody::Block(vec![
3502                Stmt::Binding(
3503                    "current".to_string(),
3504                    None,
3505                    map_get(
3506                        sb(Expr::Ident("counts".to_string())),
3507                        sb(Expr::Ident("word".to_string())),
3508                    ),
3509                ),
3510                Stmt::Expr(sb(Expr::Match {
3511                    subject: sbb(Expr::Ident("current".to_string())),
3512                    arms: vec![
3513                        MatchArm {
3514                            pattern: Pattern::Constructor(
3515                                "Option.Some".to_string(),
3516                                vec!["n".to_string()],
3517                            ),
3518                            body: Box::new(map_set(
3519                                sb(Expr::Ident("counts".to_string())),
3520                                sb(Expr::Ident("word".to_string())),
3521                                sb(Expr::BinOp(
3522                                    BinOp::Add,
3523                                    sbb(Expr::Ident("n".to_string())),
3524                                    sbb(Expr::Literal(Literal::Int(1))),
3525                                )),
3526                            )),
3527                        },
3528                        MatchArm {
3529                            pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
3530                            body: Box::new(map_set(
3531                                sb(Expr::Ident("counts".to_string())),
3532                                sb(Expr::Ident("word".to_string())),
3533                                sb(Expr::Literal(Literal::Int(1))),
3534                            )),
3535                        },
3536                    ],
3537                })),
3538            ])),
3539            resolution: None,
3540        };
3541        ctx.fn_defs.push(inc_count.clone());
3542        ctx.items.push(TopLevel::FnDef(inc_count));
3543
3544        ctx.items.push(TopLevel::Verify(VerifyBlock {
3545            fn_name: "incCount".to_string(),
3546            line: 10,
3547            cases: vec![(
3548                map_has(
3549                    sb(Expr::FnCall(
3550                        sbb(Expr::Ident("incCount".to_string())),
3551                        vec![
3552                            sb(Expr::Ident("counts".to_string())),
3553                            sb(Expr::Ident("word".to_string())),
3554                        ],
3555                    )),
3556                    sb(Expr::Ident("word".to_string())),
3557                ),
3558                sb(Expr::Literal(Literal::Bool(true))),
3559            )],
3560            case_spans: vec![],
3561            case_givens: vec![],
3562            kind: VerifyKind::Law(Box::new(VerifyLaw {
3563                name: "keyPresent".to_string(),
3564                givens: vec![
3565                    VerifyGiven {
3566                        name: "counts".to_string(),
3567                        type_name: "Map<String, Int>".to_string(),
3568                        domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
3569                    },
3570                    VerifyGiven {
3571                        name: "word".to_string(),
3572                        type_name: "String".to_string(),
3573                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Str(
3574                            "a".to_string(),
3575                        )))]),
3576                    },
3577                ],
3578                when: None,
3579                lhs: map_has(
3580                    sb(Expr::FnCall(
3581                        sbb(Expr::Ident("incCount".to_string())),
3582                        vec![
3583                            sb(Expr::Ident("counts".to_string())),
3584                            sb(Expr::Ident("word".to_string())),
3585                        ],
3586                    )),
3587                    sb(Expr::Ident("word".to_string())),
3588                ),
3589                rhs: sb(Expr::Literal(Literal::Bool(true))),
3590                sample_guards: vec![],
3591            })),
3592        }));
3593
3594        ctx.items.push(TopLevel::Verify(VerifyBlock {
3595            fn_name: "incCount".to_string(),
3596            line: 20,
3597            cases: vec![(
3598                map_get(
3599                    sb(Expr::FnCall(
3600                        sbb(Expr::Ident("incCount".to_string())),
3601                        vec![
3602                            sb(Expr::Ident("counts".to_string())),
3603                            sb(Expr::Literal(Literal::Str("a".to_string()))),
3604                        ],
3605                    )),
3606                    sb(Expr::Literal(Literal::Str("a".to_string()))),
3607                ),
3608                option_some(sb(Expr::FnCall(
3609                    sbb(Expr::Ident("addOne".to_string())),
3610                    vec![option_with_default(
3611                        map_get(
3612                            sb(Expr::Ident("counts".to_string())),
3613                            sb(Expr::Literal(Literal::Str("a".to_string()))),
3614                        ),
3615                        sb(Expr::Literal(Literal::Int(0))),
3616                    )],
3617                ))),
3618            )],
3619            case_spans: vec![],
3620            case_givens: vec![],
3621            kind: VerifyKind::Law(Box::new(VerifyLaw {
3622                name: "existingKeyIncrements".to_string(),
3623                givens: vec![VerifyGiven {
3624                    name: "counts".to_string(),
3625                    type_name: "Map<String, Int>".to_string(),
3626                    domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
3627                }],
3628                when: None,
3629                lhs: map_get(
3630                    sb(Expr::FnCall(
3631                        sbb(Expr::Ident("incCount".to_string())),
3632                        vec![
3633                            sb(Expr::Ident("counts".to_string())),
3634                            sb(Expr::Literal(Literal::Str("a".to_string()))),
3635                        ],
3636                    )),
3637                    sb(Expr::Literal(Literal::Str("a".to_string()))),
3638                ),
3639                rhs: option_some(sb(Expr::FnCall(
3640                    sbb(Expr::Ident("addOne".to_string())),
3641                    vec![option_with_default(
3642                        map_get(
3643                            sb(Expr::Ident("counts".to_string())),
3644                            sb(Expr::Literal(Literal::Str("a".to_string()))),
3645                        ),
3646                        sb(Expr::Literal(Literal::Int(0))),
3647                    )],
3648                ))),
3649                sample_guards: vec![],
3650            })),
3651        }));
3652
3653        let out = transpile(&ctx);
3654        let lean = out
3655            .files
3656            .iter()
3657            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3658            .expect("expected generated Lean file");
3659        assert!(
3660            lean.contains("cases h : AverMap.get counts word <;> simp [AverMap.has_set_self]"),
3661            "expected keyPresent auto-proof with has_set_self"
3662        );
3663        assert!(
3664            lean.contains("cases h : AverMap.get counts \"a\" <;> simp [AverMap.get_set_self, addOne, incCount]"),
3665            "expected existingKeyIncrements auto-proof with get_set_self"
3666        );
3667    }
3668
3669    #[test]
3670    fn transpile_parenthesizes_negative_int_call_args_in_law_samples() {
3671        let mut ctx = empty_ctx();
3672        let add = FnDef {
3673            name: "add".to_string(),
3674            line: 1,
3675            params: vec![
3676                ("a".to_string(), "Int".to_string()),
3677                ("b".to_string(), "Int".to_string()),
3678            ],
3679            return_type: "Int".to_string(),
3680            effects: vec![],
3681            desc: None,
3682            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
3683                BinOp::Add,
3684                sbb(Expr::Ident("a".to_string())),
3685                sbb(Expr::Ident("b".to_string())),
3686            )))),
3687            resolution: None,
3688        };
3689        ctx.fn_defs.push(add.clone());
3690        ctx.items.push(TopLevel::FnDef(add));
3691        ctx.items.push(TopLevel::Verify(VerifyBlock {
3692            fn_name: "add".to_string(),
3693            line: 1,
3694            cases: vec![(
3695                sb(Expr::FnCall(
3696                    sbb(Expr::Ident("add".to_string())),
3697                    vec![
3698                        sb(Expr::Literal(Literal::Int(-2))),
3699                        sb(Expr::Literal(Literal::Int(-1))),
3700                    ],
3701                )),
3702                sb(Expr::FnCall(
3703                    sbb(Expr::Ident("add".to_string())),
3704                    vec![
3705                        sb(Expr::Literal(Literal::Int(-1))),
3706                        sb(Expr::Literal(Literal::Int(-2))),
3707                    ],
3708                )),
3709            )],
3710            case_spans: vec![],
3711            case_givens: vec![],
3712            kind: VerifyKind::Law(Box::new(VerifyLaw {
3713                name: "commutative".to_string(),
3714                givens: vec![
3715                    VerifyGiven {
3716                        name: "a".to_string(),
3717                        type_name: "Int".to_string(),
3718                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3719                            -2,
3720                        )))]),
3721                    },
3722                    VerifyGiven {
3723                        name: "b".to_string(),
3724                        type_name: "Int".to_string(),
3725                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3726                            -1,
3727                        )))]),
3728                    },
3729                ],
3730                when: None,
3731                lhs: sb(Expr::FnCall(
3732                    sbb(Expr::Ident("add".to_string())),
3733                    vec![
3734                        sb(Expr::Ident("a".to_string())),
3735                        sb(Expr::Ident("b".to_string())),
3736                    ],
3737                )),
3738                rhs: sb(Expr::FnCall(
3739                    sbb(Expr::Ident("add".to_string())),
3740                    vec![
3741                        sb(Expr::Ident("b".to_string())),
3742                        sb(Expr::Ident("a".to_string())),
3743                    ],
3744                )),
3745                sample_guards: vec![],
3746            })),
3747        }));
3748
3749        let out = transpile(&ctx);
3750        let lean = out
3751            .files
3752            .iter()
3753            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3754            .expect("expected generated Lean file");
3755        assert!(lean.contains(
3756            "theorem add_law_commutative_sample_1 : add (-2) (-1) = add (-1) (-2) := by native_decide"
3757        ));
3758    }
3759
3760    #[test]
3761    fn verify_law_numbering_is_scoped_per_law_name() {
3762        let mut ctx = empty_ctx();
3763        let f = FnDef {
3764            name: "f".to_string(),
3765            line: 1,
3766            params: vec![("x".to_string(), "Int".to_string())],
3767            return_type: "Int".to_string(),
3768            effects: vec![],
3769            desc: None,
3770            body: Rc::new(FnBody::from_expr(sb(Expr::Ident("x".to_string())))),
3771            resolution: None,
3772        };
3773        ctx.fn_defs.push(f.clone());
3774        ctx.items.push(TopLevel::FnDef(f));
3775        ctx.items.push(TopLevel::Verify(VerifyBlock {
3776            fn_name: "f".to_string(),
3777            line: 1,
3778            cases: vec![(
3779                sb(Expr::Literal(Literal::Int(1))),
3780                sb(Expr::Literal(Literal::Int(1))),
3781            )],
3782            case_spans: vec![],
3783            case_givens: vec![],
3784            kind: VerifyKind::Cases,
3785        }));
3786        ctx.items.push(TopLevel::Verify(VerifyBlock {
3787            fn_name: "f".to_string(),
3788            line: 2,
3789            cases: vec![(
3790                sb(Expr::Literal(Literal::Int(2))),
3791                sb(Expr::Literal(Literal::Int(2))),
3792            )],
3793            case_spans: vec![],
3794            case_givens: vec![],
3795            kind: VerifyKind::Law(Box::new(VerifyLaw {
3796                name: "identity".to_string(),
3797                givens: vec![VerifyGiven {
3798                    name: "x".to_string(),
3799                    type_name: "Int".to_string(),
3800                    domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(2)))]),
3801                }],
3802                when: None,
3803                lhs: sb(Expr::Ident("x".to_string())),
3804                rhs: sb(Expr::Ident("x".to_string())),
3805                sample_guards: vec![],
3806            })),
3807        }));
3808        let out = transpile_with_verify_mode(&ctx, VerifyEmitMode::TheoremSkeleton);
3809        let lean = out
3810            .files
3811            .iter()
3812            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3813            .expect("expected generated Lean file");
3814        assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
3815        assert!(lean.contains("theorem f_law_identity : ∀ (x : Int), x = x := by"));
3816        assert!(lean.contains("theorem f_law_identity_sample_1 : 2 = 2 := by"));
3817        assert!(!lean.contains("theorem f_law_identity_sample_2 : 2 = 2 := by"));
3818    }
3819
3820    #[test]
3821    fn proof_mode_accepts_single_int_countdown_recursion() {
3822        let mut ctx = empty_ctx();
3823        let down = FnDef {
3824            name: "down".to_string(),
3825            line: 1,
3826            params: vec![("n".to_string(), "Int".to_string())],
3827            return_type: "Int".to_string(),
3828            effects: vec![],
3829            desc: None,
3830            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3831                subject: sbb(Expr::Ident("n".to_string())),
3832                arms: vec![
3833                    MatchArm {
3834                        pattern: Pattern::Literal(Literal::Int(0)),
3835                        body: sbb(Expr::Literal(Literal::Int(0))),
3836                    },
3837                    MatchArm {
3838                        pattern: Pattern::Wildcard,
3839                        body: sbb(Expr::TailCall(Box::new((
3840                            "down".to_string(),
3841                            vec![sb(Expr::BinOp(
3842                                BinOp::Sub,
3843                                sbb(Expr::Ident("n".to_string())),
3844                                sbb(Expr::Literal(Literal::Int(1))),
3845                            ))],
3846                        )))),
3847                    },
3848                ],
3849            }))),
3850            resolution: None,
3851        };
3852        ctx.items.push(TopLevel::FnDef(down.clone()));
3853        ctx.fn_defs.push(down);
3854
3855        let issues = proof_mode_issues(&ctx);
3856        assert!(
3857            issues.is_empty(),
3858            "expected Int countdown recursion to be accepted, got: {:?}",
3859            issues
3860        );
3861
3862        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
3863        let lean = out
3864            .files
3865            .iter()
3866            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3867            .expect("expected generated Lean file");
3868        assert!(lean.contains("def down__fuel"));
3869        assert!(lean.contains("def down (n : Int) : Int :="));
3870        assert!(lean.contains("down__fuel ((Int.natAbs n) + 1) n"));
3871    }
3872
3873    #[test]
3874    fn proof_mode_accepts_single_int_countdown_on_nonfirst_param() {
3875        let mut ctx = empty_ctx();
3876        let repeat_like = FnDef {
3877            name: "repeatLike".to_string(),
3878            line: 1,
3879            params: vec![
3880                ("char".to_string(), "String".to_string()),
3881                ("n".to_string(), "Int".to_string()),
3882            ],
3883            return_type: "List<String>".to_string(),
3884            effects: vec![],
3885            desc: None,
3886            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3887                subject: sbb(Expr::BinOp(
3888                    BinOp::Lte,
3889                    sbb(Expr::Ident("n".to_string())),
3890                    sbb(Expr::Literal(Literal::Int(0))),
3891                )),
3892                arms: vec![
3893                    MatchArm {
3894                        pattern: Pattern::Literal(Literal::Bool(true)),
3895                        body: sbb(Expr::List(vec![])),
3896                    },
3897                    MatchArm {
3898                        pattern: Pattern::Literal(Literal::Bool(false)),
3899                        body: sbb(Expr::TailCall(Box::new((
3900                            "repeatLike".to_string(),
3901                            vec![
3902                                sb(Expr::Ident("char".to_string())),
3903                                sb(Expr::BinOp(
3904                                    BinOp::Sub,
3905                                    sbb(Expr::Ident("n".to_string())),
3906                                    sbb(Expr::Literal(Literal::Int(1))),
3907                                )),
3908                            ],
3909                        )))),
3910                    },
3911                ],
3912            }))),
3913            resolution: None,
3914        };
3915        ctx.items.push(TopLevel::FnDef(repeat_like.clone()));
3916        ctx.fn_defs.push(repeat_like);
3917
3918        let issues = proof_mode_issues(&ctx);
3919        assert!(
3920            issues.is_empty(),
3921            "expected non-first Int countdown recursion to be accepted, got: {:?}",
3922            issues
3923        );
3924
3925        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
3926        let lean = out
3927            .files
3928            .iter()
3929            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3930            .expect("expected generated Lean file");
3931        assert!(lean.contains("def repeatLike__fuel"));
3932        assert!(lean.contains("def repeatLike (char : String) (n : Int) : List String :="));
3933        assert!(lean.contains("repeatLike__fuel ((Int.natAbs n) + 1) char n"));
3934    }
3935
3936    #[test]
3937    fn proof_mode_accepts_negative_guarded_int_ascent() {
3938        let mut ctx = empty_ctx();
3939        let normalize = FnDef {
3940            name: "normalize".to_string(),
3941            line: 1,
3942            params: vec![("angle".to_string(), "Int".to_string())],
3943            return_type: "Int".to_string(),
3944            effects: vec![],
3945            desc: None,
3946            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3947                subject: sbb(Expr::BinOp(
3948                    BinOp::Lt,
3949                    sbb(Expr::Ident("angle".to_string())),
3950                    sbb(Expr::Literal(Literal::Int(0))),
3951                )),
3952                arms: vec![
3953                    MatchArm {
3954                        pattern: Pattern::Literal(Literal::Bool(true)),
3955                        body: sbb(Expr::TailCall(Box::new((
3956                            "normalize".to_string(),
3957                            vec![sb(Expr::BinOp(
3958                                BinOp::Add,
3959                                sbb(Expr::Ident("angle".to_string())),
3960                                sbb(Expr::Literal(Literal::Int(360))),
3961                            ))],
3962                        )))),
3963                    },
3964                    MatchArm {
3965                        pattern: Pattern::Literal(Literal::Bool(false)),
3966                        body: sbb(Expr::Ident("angle".to_string())),
3967                    },
3968                ],
3969            }))),
3970            resolution: None,
3971        };
3972        ctx.items.push(TopLevel::FnDef(normalize.clone()));
3973        ctx.fn_defs.push(normalize);
3974
3975        let issues = proof_mode_issues(&ctx);
3976        assert!(
3977            issues.is_empty(),
3978            "expected negative-guarded Int ascent recursion to be accepted, got: {:?}",
3979            issues
3980        );
3981
3982        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
3983        let lean = out
3984            .files
3985            .iter()
3986            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3987            .expect("expected generated Lean file");
3988        assert!(lean.contains("def normalize__fuel"));
3989        assert!(lean.contains("normalize__fuel ((Int.natAbs angle) + 1) angle"));
3990    }
3991
3992    #[test]
3993    fn proof_mode_accepts_single_list_structural_recursion() {
3994        let mut ctx = empty_ctx();
3995        let len = FnDef {
3996            name: "len".to_string(),
3997            line: 1,
3998            params: vec![("xs".to_string(), "List<Int>".to_string())],
3999            return_type: "Int".to_string(),
4000            effects: vec![],
4001            desc: None,
4002            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4003                subject: sbb(Expr::Ident("xs".to_string())),
4004                arms: vec![
4005                    MatchArm {
4006                        pattern: Pattern::EmptyList,
4007                        body: sbb(Expr::Literal(Literal::Int(0))),
4008                    },
4009                    MatchArm {
4010                        pattern: Pattern::Cons("h".to_string(), "t".to_string()),
4011                        body: sbb(Expr::TailCall(Box::new((
4012                            "len".to_string(),
4013                            vec![sb(Expr::Ident("t".to_string()))],
4014                        )))),
4015                    },
4016                ],
4017            }))),
4018            resolution: None,
4019        };
4020        ctx.items.push(TopLevel::FnDef(len.clone()));
4021        ctx.fn_defs.push(len);
4022
4023        let issues = proof_mode_issues(&ctx);
4024        assert!(
4025            issues.is_empty(),
4026            "expected List structural recursion to be accepted, got: {:?}",
4027            issues
4028        );
4029    }
4030
4031    #[test]
4032    fn proof_mode_accepts_single_list_structural_recursion_on_nonfirst_param() {
4033        let mut ctx = empty_ctx();
4034        let len_from = FnDef {
4035            name: "lenFrom".to_string(),
4036            line: 1,
4037            params: vec![
4038                ("count".to_string(), "Int".to_string()),
4039                ("xs".to_string(), "List<Int>".to_string()),
4040            ],
4041            return_type: "Int".to_string(),
4042            effects: vec![],
4043            desc: None,
4044            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4045                subject: sbb(Expr::Ident("xs".to_string())),
4046                arms: vec![
4047                    MatchArm {
4048                        pattern: Pattern::EmptyList,
4049                        body: sbb(Expr::Ident("count".to_string())),
4050                    },
4051                    MatchArm {
4052                        pattern: Pattern::Cons("h".to_string(), "t".to_string()),
4053                        body: sbb(Expr::TailCall(Box::new((
4054                            "lenFrom".to_string(),
4055                            vec![
4056                                sb(Expr::BinOp(
4057                                    BinOp::Add,
4058                                    sbb(Expr::Ident("count".to_string())),
4059                                    sbb(Expr::Literal(Literal::Int(1))),
4060                                )),
4061                                sb(Expr::Ident("t".to_string())),
4062                            ],
4063                        )))),
4064                    },
4065                ],
4066            }))),
4067            resolution: None,
4068        };
4069        ctx.items.push(TopLevel::FnDef(len_from.clone()));
4070        ctx.fn_defs.push(len_from);
4071
4072        let issues = proof_mode_issues(&ctx);
4073        assert!(
4074            issues.is_empty(),
4075            "expected non-first List structural recursion to be accepted, got: {:?}",
4076            issues
4077        );
4078
4079        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4080        let lean = generated_lean_file(&out);
4081        assert!(lean.contains("termination_by xs.length"));
4082        assert!(!lean.contains("partial def lenFrom"));
4083    }
4084
4085    #[test]
4086    fn proof_mode_accepts_single_string_pos_advance_recursion() {
4087        let mut ctx = empty_ctx();
4088        let skip_ws = FnDef {
4089            name: "skipWs".to_string(),
4090            line: 1,
4091            params: vec![
4092                ("s".to_string(), "String".to_string()),
4093                ("pos".to_string(), "Int".to_string()),
4094            ],
4095            return_type: "Int".to_string(),
4096            effects: vec![],
4097            desc: None,
4098            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4099                subject: sbb(Expr::FnCall(
4100                    sbb(Expr::Attr(
4101                        sbb(Expr::Ident("String".to_string())),
4102                        "charAt".to_string(),
4103                    )),
4104                    vec![
4105                        sb(Expr::Ident("s".to_string())),
4106                        sb(Expr::Ident("pos".to_string())),
4107                    ],
4108                )),
4109                arms: vec![
4110                    MatchArm {
4111                        pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
4112                        body: sbb(Expr::Ident("pos".to_string())),
4113                    },
4114                    MatchArm {
4115                        pattern: Pattern::Wildcard,
4116                        body: sbb(Expr::TailCall(Box::new((
4117                            "skipWs".to_string(),
4118                            vec![
4119                                sb(Expr::Ident("s".to_string())),
4120                                sb(Expr::BinOp(
4121                                    BinOp::Add,
4122                                    sbb(Expr::Ident("pos".to_string())),
4123                                    sbb(Expr::Literal(Literal::Int(1))),
4124                                )),
4125                            ],
4126                        )))),
4127                    },
4128                ],
4129            }))),
4130            resolution: None,
4131        };
4132        ctx.items.push(TopLevel::FnDef(skip_ws.clone()));
4133        ctx.fn_defs.push(skip_ws);
4134
4135        let issues = proof_mode_issues(&ctx);
4136        assert!(
4137            issues.is_empty(),
4138            "expected String+pos recursion to be accepted, got: {:?}",
4139            issues
4140        );
4141
4142        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4143        let lean = generated_lean_file(&out);
4144        assert!(lean.contains("def skipWs__fuel"));
4145        assert!(!lean.contains("partial def skipWs"));
4146    }
4147
4148    #[test]
4149    fn proof_mode_accepts_mutual_int_countdown_recursion() {
4150        let mut ctx = empty_ctx();
4151        let even = FnDef {
4152            name: "even".to_string(),
4153            line: 1,
4154            params: vec![("n".to_string(), "Int".to_string())],
4155            return_type: "Bool".to_string(),
4156            effects: vec![],
4157            desc: None,
4158            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4159                subject: sbb(Expr::Ident("n".to_string())),
4160                arms: vec![
4161                    MatchArm {
4162                        pattern: Pattern::Literal(Literal::Int(0)),
4163                        body: sbb(Expr::Literal(Literal::Bool(true))),
4164                    },
4165                    MatchArm {
4166                        pattern: Pattern::Wildcard,
4167                        body: sbb(Expr::TailCall(Box::new((
4168                            "odd".to_string(),
4169                            vec![sb(Expr::BinOp(
4170                                BinOp::Sub,
4171                                sbb(Expr::Ident("n".to_string())),
4172                                sbb(Expr::Literal(Literal::Int(1))),
4173                            ))],
4174                        )))),
4175                    },
4176                ],
4177            }))),
4178            resolution: None,
4179        };
4180        let odd = FnDef {
4181            name: "odd".to_string(),
4182            line: 2,
4183            params: vec![("n".to_string(), "Int".to_string())],
4184            return_type: "Bool".to_string(),
4185            effects: vec![],
4186            desc: None,
4187            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4188                subject: sbb(Expr::Ident("n".to_string())),
4189                arms: vec![
4190                    MatchArm {
4191                        pattern: Pattern::Literal(Literal::Int(0)),
4192                        body: sbb(Expr::Literal(Literal::Bool(false))),
4193                    },
4194                    MatchArm {
4195                        pattern: Pattern::Wildcard,
4196                        body: sbb(Expr::TailCall(Box::new((
4197                            "even".to_string(),
4198                            vec![sb(Expr::BinOp(
4199                                BinOp::Sub,
4200                                sbb(Expr::Ident("n".to_string())),
4201                                sbb(Expr::Literal(Literal::Int(1))),
4202                            ))],
4203                        )))),
4204                    },
4205                ],
4206            }))),
4207            resolution: None,
4208        };
4209        ctx.items.push(TopLevel::FnDef(even.clone()));
4210        ctx.items.push(TopLevel::FnDef(odd.clone()));
4211        ctx.fn_defs.push(even);
4212        ctx.fn_defs.push(odd);
4213
4214        let issues = proof_mode_issues(&ctx);
4215        assert!(
4216            issues.is_empty(),
4217            "expected mutual Int countdown recursion to be accepted, got: {:?}",
4218            issues
4219        );
4220
4221        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4222        let lean = generated_lean_file(&out);
4223        assert!(lean.contains("def even__fuel"));
4224        assert!(lean.contains("def odd__fuel"));
4225        assert!(lean.contains("def even (n : Int) : Bool :="));
4226        assert!(lean.contains("even__fuel ((Int.natAbs n) + 1) n"));
4227    }
4228
4229    #[test]
4230    fn proof_mode_accepts_mutual_string_pos_recursion_with_ranked_same_edges() {
4231        let mut ctx = empty_ctx();
4232        let f = FnDef {
4233            name: "f".to_string(),
4234            line: 1,
4235            params: vec![
4236                ("s".to_string(), "String".to_string()),
4237                ("pos".to_string(), "Int".to_string()),
4238            ],
4239            return_type: "Int".to_string(),
4240            effects: vec![],
4241            desc: None,
4242            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4243                subject: sbb(Expr::BinOp(
4244                    BinOp::Gte,
4245                    sbb(Expr::Ident("pos".to_string())),
4246                    sbb(Expr::Literal(Literal::Int(3))),
4247                )),
4248                arms: vec![
4249                    MatchArm {
4250                        pattern: Pattern::Literal(Literal::Bool(true)),
4251                        body: sbb(Expr::Ident("pos".to_string())),
4252                    },
4253                    MatchArm {
4254                        pattern: Pattern::Wildcard,
4255                        body: sbb(Expr::TailCall(Box::new((
4256                            "g".to_string(),
4257                            vec![
4258                                sb(Expr::Ident("s".to_string())),
4259                                sb(Expr::Ident("pos".to_string())),
4260                            ],
4261                        )))),
4262                    },
4263                ],
4264            }))),
4265            resolution: None,
4266        };
4267        let g = FnDef {
4268            name: "g".to_string(),
4269            line: 2,
4270            params: vec![
4271                ("s".to_string(), "String".to_string()),
4272                ("pos".to_string(), "Int".to_string()),
4273            ],
4274            return_type: "Int".to_string(),
4275            effects: vec![],
4276            desc: None,
4277            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4278                subject: sbb(Expr::BinOp(
4279                    BinOp::Gte,
4280                    sbb(Expr::Ident("pos".to_string())),
4281                    sbb(Expr::Literal(Literal::Int(3))),
4282                )),
4283                arms: vec![
4284                    MatchArm {
4285                        pattern: Pattern::Literal(Literal::Bool(true)),
4286                        body: sbb(Expr::Ident("pos".to_string())),
4287                    },
4288                    MatchArm {
4289                        pattern: Pattern::Wildcard,
4290                        body: sbb(Expr::TailCall(Box::new((
4291                            "f".to_string(),
4292                            vec![
4293                                sb(Expr::Ident("s".to_string())),
4294                                sb(Expr::BinOp(
4295                                    BinOp::Add,
4296                                    sbb(Expr::Ident("pos".to_string())),
4297                                    sbb(Expr::Literal(Literal::Int(1))),
4298                                )),
4299                            ],
4300                        )))),
4301                    },
4302                ],
4303            }))),
4304            resolution: None,
4305        };
4306        ctx.items.push(TopLevel::FnDef(f.clone()));
4307        ctx.items.push(TopLevel::FnDef(g.clone()));
4308        ctx.fn_defs.push(f);
4309        ctx.fn_defs.push(g);
4310
4311        let issues = proof_mode_issues(&ctx);
4312        assert!(
4313            issues.is_empty(),
4314            "expected mutual String+pos recursion to be accepted, got: {:?}",
4315            issues
4316        );
4317
4318        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4319        let lean = generated_lean_file(&out);
4320        assert!(lean.contains("def f__fuel"));
4321        assert!(lean.contains("def g__fuel"));
4322        assert!(!lean.contains("partial def f"));
4323    }
4324
4325    #[test]
4326    fn proof_mode_accepts_mutual_ranked_sizeof_recursion() {
4327        let mut ctx = empty_ctx();
4328        let f = FnDef {
4329            name: "f".to_string(),
4330            line: 1,
4331            params: vec![("xs".to_string(), "List<Int>".to_string())],
4332            return_type: "Int".to_string(),
4333            effects: vec![],
4334            desc: None,
4335            body: Rc::new(FnBody::from_expr(sb(Expr::TailCall(Box::new((
4336                "g".to_string(),
4337                vec![
4338                    sb(Expr::Literal(Literal::Str("acc".to_string()))),
4339                    sb(Expr::Ident("xs".to_string())),
4340                ],
4341            )))))),
4342            resolution: None,
4343        };
4344        let g = FnDef {
4345            name: "g".to_string(),
4346            line: 2,
4347            params: vec![
4348                ("acc".to_string(), "String".to_string()),
4349                ("xs".to_string(), "List<Int>".to_string()),
4350            ],
4351            return_type: "Int".to_string(),
4352            effects: vec![],
4353            desc: None,
4354            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
4355                subject: sbb(Expr::Ident("xs".to_string())),
4356                arms: vec![
4357                    MatchArm {
4358                        pattern: Pattern::EmptyList,
4359                        body: sbb(Expr::Literal(Literal::Int(0))),
4360                    },
4361                    MatchArm {
4362                        pattern: Pattern::Cons("h".to_string(), "t".to_string()),
4363                        body: sbb(Expr::TailCall(Box::new((
4364                            "f".to_string(),
4365                            vec![sb(Expr::Ident("t".to_string()))],
4366                        )))),
4367                    },
4368                ],
4369            }))),
4370            resolution: None,
4371        };
4372        ctx.items.push(TopLevel::FnDef(f.clone()));
4373        ctx.items.push(TopLevel::FnDef(g.clone()));
4374        ctx.fn_defs.push(f);
4375        ctx.fn_defs.push(g);
4376
4377        let issues = proof_mode_issues(&ctx);
4378        assert!(
4379            issues.is_empty(),
4380            "expected mutual ranked-sizeOf recursion to be accepted, got: {:?}",
4381            issues
4382        );
4383
4384        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4385        let lean = generated_lean_file(&out);
4386        assert!(lean.contains("mutual"));
4387        assert!(lean.contains("def f__fuel"));
4388        assert!(lean.contains("def g__fuel"));
4389        assert!(!lean.contains("partial def f"));
4390        assert!(!lean.contains("partial def g"));
4391    }
4392
4393    #[test]
4394    fn proof_mode_rejects_recursive_pure_functions() {
4395        let mut ctx = empty_ctx();
4396        let recursive_fn = FnDef {
4397            name: "loop".to_string(),
4398            line: 1,
4399            params: vec![("n".to_string(), "Int".to_string())],
4400            return_type: "Int".to_string(),
4401            effects: vec![],
4402            desc: None,
4403            body: Rc::new(FnBody::from_expr(sb(Expr::FnCall(
4404                sbb(Expr::Ident("loop".to_string())),
4405                vec![sb(Expr::Ident("n".to_string()))],
4406            )))),
4407            resolution: None,
4408        };
4409        ctx.items.push(TopLevel::FnDef(recursive_fn.clone()));
4410        ctx.fn_defs.push(recursive_fn);
4411
4412        let issues = proof_mode_issues(&ctx);
4413        assert!(
4414            issues.iter().any(|i| i.contains("outside proof subset")),
4415            "expected recursive function blocker, got: {:?}",
4416            issues
4417        );
4418    }
4419
4420    #[test]
4421    fn proof_mode_allows_recursive_types() {
4422        let mut ctx = empty_ctx();
4423        let recursive_type = TypeDef::Sum {
4424            name: "Node".to_string(),
4425            variants: vec![TypeVariant {
4426                name: "Cons".to_string(),
4427                fields: vec!["Node".to_string()],
4428            }],
4429            line: 1,
4430        };
4431        ctx.items.push(TopLevel::TypeDef(recursive_type.clone()));
4432        ctx.type_defs.push(recursive_type);
4433
4434        let issues = proof_mode_issues(&ctx);
4435        assert!(
4436            issues
4437                .iter()
4438                .all(|i| !i.contains("recursive types require unsafe DecidableEq shim")),
4439            "did not expect recursive type blocker, got: {:?}",
4440            issues
4441        );
4442    }
4443
4444    #[test]
4445    fn law_auto_example_exports_real_proof_artifacts() {
4446        let ctx = ctx_from_source(
4447            include_str!("../../../examples/formal/law_auto.av"),
4448            "law_auto",
4449        );
4450        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4451        let lean = generated_lean_file(&out);
4452
4453        assert!(lean.contains("theorem add_law_commutative :"));
4454        assert!(lean.contains("theorem id'_law_reflexive : ∀ (x : Int), x = x := by"));
4455        assert!(lean.contains("theorem incCount_law_keyPresent :"));
4456        assert!(lean.contains("AverMap.has_set_self"));
4457        assert!(lean.contains("theorem add_law_commutative_sample_1 :"));
4458        assert!(lean.contains(":= by native_decide"));
4459    }
4460
4461    #[test]
4462    fn json_example_stays_inside_proof_subset() {
4463        let ctx = ctx_from_source(include_str!("../../../examples/data/json.av"), "json");
4464        let issues = proof_mode_issues(&ctx);
4465        assert!(
4466            issues.is_empty(),
4467            "expected json example to stay inside proof subset, got: {:?}",
4468            issues
4469        );
4470    }
4471
4472    #[test]
4473    fn json_example_uses_total_defs_and_domain_guarded_laws_in_proof_mode() {
4474        let ctx = ctx_from_source(include_str!("../../../examples/data/json.av"), "json");
4475        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4476        let lean = generated_lean_file(&out);
4477
4478        assert!(!lean.contains("partial def"));
4479        assert!(lean.contains("def skipWs__fuel"));
4480        assert!(lean.contains("def parseValue__fuel"));
4481        assert!(lean.contains("def toString' (j : Json) : String :="));
4482        assert!(
4483            lean.contains(
4484                "def averMeasureJsonEntries_String (items : List (String × Json)) : Nat :="
4485            )
4486        );
4487        assert!(lean.contains(
4488            "| .jsonObject x0 => (averMeasureJsonEntries_String (AverMap.entries x0)) + 1"
4489        ));
4490        assert!(lean.contains("-- when jsonRoundtripSafe j"));
4491        assert!(!lean.contains("-- hint: verify law '"));
4492        assert!(!lean.contains("private theorem toString'_law_parseRoundtrip_aux"));
4493        assert!(
4494            lean.contains(
4495                "theorem toString'_law_parseRoundtrip : ∀ (j : Json), j = Json.jsonNull ∨"
4496            )
4497        );
4498        assert!(lean.contains(
4499            "jsonRoundtripSafe j = true -> fromString (toString' j) = Except.ok j := by"
4500        ));
4501        assert!(
4502            lean.contains("theorem finishFloat_law_fromCanonicalFloat : ∀ (f : Float), f = 3.5 ∨")
4503        );
4504        assert!(lean.contains("theorem finishInt_law_fromCanonicalInt_checked_domain :"));
4505        assert!(lean.contains(
4506            "theorem toString'_law_parseValueRoundtrip : ∀ (j : Json), j = Json.jsonNull ∨"
4507        ));
4508        assert!(lean.contains("theorem toString'_law_parseRoundtrip_sample_1 :"));
4509        assert!(lean.contains(
4510            "example : fromString \"null\" = Except.ok Json.jsonNull := by native_decide"
4511        ));
4512    }
4513
4514    #[test]
4515    fn transpile_injects_builtin_network_types_and_vector_get_support() {
4516        let ctx = ctx_from_source(
4517            r#"
4518fn firstOrMissing(xs: Vector<String>) -> Result<String, String>
4519    Option.toResult(Vector.get(xs, 0), "missing")
4520
4521fn defaultHeader() -> Header
4522    Header(name = "Content-Type", value = "application/json")
4523
4524fn mkResponse(body: String) -> HttpResponse
4525    HttpResponse(status = 200, body = body, headers = [defaultHeader()])
4526
4527fn requestPath(req: HttpRequest) -> String
4528    req.path
4529
4530fn connPort(conn: Tcp.Connection) -> Int
4531    conn.port
4532"#,
4533            "network_helpers",
4534        );
4535        let out = transpile(&ctx);
4536        let lean = generated_lean_file(&out);
4537
4538        assert!(lean.contains("structure Header where"));
4539        assert!(lean.contains("structure HttpResponse where"));
4540        assert!(lean.contains("structure HttpRequest where"));
4541        assert!(lean.contains("structure Tcp_Connection where"));
4542        assert!(lean.contains("port : Int"));
4543    }
4544
4545    #[test]
4546    fn law_auto_example_has_no_sorry_in_proof_mode() {
4547        let ctx = ctx_from_source(
4548            include_str!("../../../examples/formal/law_auto.av"),
4549            "law_auto",
4550        );
4551        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4552        let lean = generated_lean_file(&out);
4553        assert!(
4554            !lean.contains("sorry"),
4555            "expected law_auto proof export to avoid sorry, got:\n{}",
4556            lean
4557        );
4558    }
4559
4560    #[test]
4561    fn map_example_has_no_sorry_in_proof_mode() {
4562        let ctx = ctx_from_source(include_str!("../../../examples/data/map.av"), "map");
4563        let issues = proof_mode_issues(&ctx);
4564        assert!(
4565            issues.is_empty(),
4566            "expected map example to stay inside proof subset, got: {:?}",
4567            issues
4568        );
4569
4570        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4571        let lean = generated_lean_file(&out);
4572        // After codegen change: universal theorems that can't be auto-proved get sorry
4573        assert!(lean.contains("theorem incCount_law_trackedCountStepsByOne :"));
4574        assert!(lean.contains("sorry"));
4575        // Universal theorems that can't be auto-proved now get sorry instead of being omitted
4576        assert!(lean.contains("theorem countWords_law_presenceMatchesContains_sample_1 :"));
4577        assert!(lean.contains("theorem countWords_law_trackedWordCount_sample_1 :"));
4578        assert!(lean.contains("AverMap.has_set_self"));
4579        assert!(lean.contains("AverMap.get_set_self"));
4580    }
4581
4582    #[test]
4583    fn spec_laws_example_has_no_sorry_in_proof_mode() {
4584        let ctx = ctx_from_source(
4585            include_str!("../../../examples/formal/spec_laws.av"),
4586            "spec_laws",
4587        );
4588        let issues = proof_mode_issues(&ctx);
4589        assert!(
4590            issues.is_empty(),
4591            "expected spec_laws example to stay inside proof subset, got: {:?}",
4592            issues
4593        );
4594
4595        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4596        let lean = generated_lean_file(&out);
4597        assert!(
4598            !lean.contains("sorry"),
4599            "expected spec_laws proof export to avoid sorry, got:\n{}",
4600            lean
4601        );
4602        assert!(lean.contains("theorem absVal_eq_absValSpec :"));
4603        assert!(lean.contains("theorem clampNonNegative_eq_clampNonNegativeSpec :"));
4604    }
4605
4606    #[test]
4607    fn rle_example_exports_sampled_roundtrip_laws_without_sorry() {
4608        let ctx = ctx_from_source(include_str!("../../../examples/data/rle.av"), "rle");
4609        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4610        let lean = generated_lean_file(&out);
4611
4612        assert!(
4613            lean.contains("sorry"),
4614            "expected rle proof export to contain sorry for unproved universal theorems"
4615        );
4616        assert!(lean.contains(
4617            "theorem encode_law_roundtrip_sample_1 : decode (encode []) = [] := by native_decide"
4618        ));
4619        assert!(lean.contains(
4620            "theorem encodeString_law_string_roundtrip_sample_1 : decodeString (encodeString \"\") = \"\" := by native_decide"
4621        ));
4622    }
4623
4624    #[test]
4625    fn fibonacci_example_uses_fuelized_int_countdown_in_proof_mode() {
4626        let ctx = ctx_from_source(
4627            include_str!("../../../examples/data/fibonacci.av"),
4628            "fibonacci",
4629        );
4630        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4631        let lean = generated_lean_file(&out);
4632
4633        assert!(lean.contains("def fibTR__fuel"));
4634        assert!(lean.contains("def fibTR (n : Int) (a : Int) (b : Int) : Int :="));
4635        assert!(lean.contains("fibTR__fuel ((Int.natAbs n) + 1) n a b"));
4636        assert!(!lean.contains("partial def fibTR"));
4637    }
4638
4639    #[test]
4640    fn fibonacci_example_stays_inside_proof_subset() {
4641        let ctx = ctx_from_source(
4642            include_str!("../../../examples/data/fibonacci.av"),
4643            "fibonacci",
4644        );
4645        let issues = proof_mode_issues(&ctx);
4646        assert!(
4647            issues.is_empty(),
4648            "expected fibonacci example to stay inside proof subset, got: {:?}",
4649            issues
4650        );
4651    }
4652
4653    #[test]
4654    fn fibonacci_example_matches_general_linear_recurrence_shapes() {
4655        let ctx = ctx_from_source(
4656            include_str!("../../../examples/data/fibonacci.av"),
4657            "fibonacci",
4658        );
4659        let fib = ctx.fn_defs.iter().find(|fd| fd.name == "fib").unwrap();
4660        let fib_tr = ctx.fn_defs.iter().find(|fd| fd.name == "fibTR").unwrap();
4661        let fib_spec = ctx.fn_defs.iter().find(|fd| fd.name == "fibSpec").unwrap();
4662
4663        assert!(recurrence::detect_tailrec_int_linear_pair_wrapper(fib).is_some());
4664        assert!(recurrence::detect_tailrec_int_linear_pair_worker(fib_tr).is_some());
4665        assert!(recurrence::detect_second_order_int_linear_recurrence(fib_spec).is_some());
4666    }
4667
4668    #[test]
4669    fn fibonacci_example_auto_proves_general_linear_recurrence_spec_law() {
4670        let ctx = ctx_from_source(
4671            include_str!("../../../examples/data/fibonacci.av"),
4672            "fibonacci",
4673        );
4674        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4675        let lean = generated_lean_file(&out);
4676
4677        assert!(lean.contains("private def fibSpec__nat : Nat -> Int"));
4678        assert!(!lean.contains("partial def fibSpec"));
4679        assert!(lean.contains("private theorem fib_eq_fibSpec__worker_nat_shift"));
4680        assert!(lean.contains("private theorem fib_eq_fibSpec__helper_nat"));
4681        assert!(lean.contains("private theorem fib_eq_fibSpec__helper_seed"));
4682        assert!(lean.contains("theorem fib_eq_fibSpec : ∀ (n : Int), fib n = fibSpec n := by"));
4683        assert!(!lean.contains(
4684            "-- universal theorem fib_eq_fibSpec omitted: sampled law shape is not auto-proved yet"
4685        ));
4686    }
4687
4688    #[test]
4689    fn pell_like_example_auto_proves_same_general_shape() {
4690        let ctx = ctx_from_source(
4691            r#"
4692module Pell
4693    intent =
4694        "linear recurrence probe"
4695
4696fn pellTR(n: Int, a: Int, b: Int) -> Int
4697    match n
4698        0 -> a
4699        _ -> pellTR(n - 1, b, a + 2 * b)
4700
4701fn pell(n: Int) -> Int
4702    match n < 0
4703        true -> 0
4704        false -> pellTR(n, 0, 1)
4705
4706fn pellSpec(n: Int) -> Int
4707    match n < 0
4708        true -> 0
4709        false -> match n
4710            0 -> 0
4711            1 -> 1
4712            _ -> pellSpec(n - 2) + 2 * pellSpec(n - 1)
4713
4714verify pell law pellSpec
4715    given n: Int = [0, 1, 2, 3]
4716    pell(n) => pellSpec(n)
4717"#,
4718            "pell",
4719        );
4720        let issues = proof_mode_issues(&ctx);
4721        assert!(
4722            issues.is_empty(),
4723            "expected pell example to stay inside proof subset, got: {:?}",
4724            issues
4725        );
4726
4727        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4728        let lean = generated_lean_file(&out);
4729        assert!(lean.contains("private def pellSpec__nat : Nat -> Int"));
4730        assert!(lean.contains("private theorem pell_eq_pellSpec__worker_nat_shift"));
4731        assert!(lean.contains("theorem pell_eq_pellSpec : ∀ (n : Int), pell n = pellSpec n := by"));
4732        assert!(!lean.contains(
4733            "-- universal theorem pell_eq_pellSpec omitted: sampled law shape is not auto-proved yet"
4734        ));
4735    }
4736
4737    #[test]
4738    fn nonlinear_pair_state_recurrence_is_not_auto_proved_as_linear_shape() {
4739        let ctx = ctx_from_source(
4740            r#"
4741module WeirdRec
4742    intent =
4743        "reject nonlinear pair-state recurrence from linear recurrence prover"
4744
4745fn weirdTR(n: Int, a: Int, b: Int) -> Int
4746    match n
4747        0 -> a
4748        _ -> weirdTR(n - 1, b, a * b)
4749
4750fn weird(n: Int) -> Int
4751    match n < 0
4752        true -> 0
4753        false -> weirdTR(n, 0, 1)
4754
4755fn weirdSpec(n: Int) -> Int
4756    match n < 0
4757        true -> 0
4758        false -> match n
4759            0 -> 0
4760            1 -> 1
4761            _ -> weirdSpec(n - 1) * weirdSpec(n - 2)
4762
4763verify weird law weirdSpec
4764    given n: Int = [0, 1, 2, 3]
4765    weird(n) => weirdSpec(n)
4766"#,
4767            "weirdrec",
4768        );
4769        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4770        let lean = generated_lean_file(&out);
4771
4772        // After codegen change: emit sorry instead of omitting universal theorems
4773        assert!(lean.contains("sorry"));
4774        assert!(!lean.contains("private theorem weird_eq_weirdSpec__worker_nat_shift"));
4775        assert!(lean.contains("theorem weird_eq_weirdSpec_sample_1 :"));
4776    }
4777
4778    #[test]
4779    fn date_example_stays_inside_proof_subset() {
4780        let ctx = ctx_from_source(include_str!("../../../examples/data/date.av"), "date");
4781        let issues = proof_mode_issues(&ctx);
4782        assert!(
4783            issues.is_empty(),
4784            "expected date example to stay inside proof subset, got: {:?}",
4785            issues
4786        );
4787
4788        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4789        let lean = generated_lean_file(&out);
4790        assert!(!lean.contains("partial def"));
4791        assert!(lean.contains("def parseIntSlice (s : String) (from' : Int) (to : Int) : Int :="));
4792    }
4793
4794    #[test]
4795    fn temperature_example_stays_inside_proof_subset() {
4796        let ctx = ctx_from_source(
4797            include_str!("../../../examples/core/temperature.av"),
4798            "temperature",
4799        );
4800        let issues = proof_mode_issues(&ctx);
4801        assert!(
4802            issues.is_empty(),
4803            "expected temperature example to stay inside proof subset, got: {:?}",
4804            issues
4805        );
4806
4807        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4808        let lean = generated_lean_file(&out);
4809        assert!(!lean.contains("partial def"));
4810        assert!(
4811            lean.contains("example : celsiusToFahr 0.0 = 32.0 := by native_decide"),
4812            "expected verify examples to survive proof export, got:\n{}",
4813            lean
4814        );
4815    }
4816
4817    #[test]
4818    fn quicksort_example_stays_inside_proof_subset() {
4819        let ctx = ctx_from_source(
4820            include_str!("../../../examples/data/quicksort.av"),
4821            "quicksort",
4822        );
4823        let issues = proof_mode_issues(&ctx);
4824        assert!(
4825            issues.is_empty(),
4826            "expected quicksort example to stay inside proof subset, got: {:?}",
4827            issues
4828        );
4829
4830        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4831        let lean = generated_lean_file(&out);
4832        assert!(lean.contains("def isOrderedFrom"));
4833        assert!(!lean.contains("partial def isOrderedFrom"));
4834        assert!(lean.contains("termination_by xs.length"));
4835    }
4836
4837    #[test]
4838    fn grok_s_language_example_uses_total_ranked_sizeof_mutual_recursion() {
4839        let ctx = ctx_from_source(
4840            include_str!("../../../examples/core/grok_s_language.av"),
4841            "grok_s_language",
4842        );
4843        let issues = proof_mode_issues(&ctx);
4844        assert!(
4845            issues.is_empty(),
4846            "expected grok_s_language example to stay inside proof subset, got: {:?}",
4847            issues
4848        );
4849
4850        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4851        let lean = generated_lean_file(&out);
4852        assert!(lean.contains("mutual"));
4853        assert!(lean.contains("def eval__fuel"));
4854        assert!(lean.contains("def parseListItems__fuel"));
4855        assert!(!lean.contains("partial def eval"));
4856        assert!(!lean.contains("termination_by (sizeOf e,"));
4857        assert!(lean.contains("-- when validSymbolNames e"));
4858        assert!(!lean.contains("private theorem toString'_law_parseRoundtrip_aux"));
4859        assert!(lean.contains(
4860            "theorem toString'_law_parseRoundtrip : ∀ (e : Sexpr), e = Sexpr.atomNum 42 ∨"
4861        ));
4862        assert!(
4863            lean.contains("validSymbolNames e = true -> parse (toString' e) = Except.ok e := by")
4864        );
4865        assert!(lean.contains("theorem toString'_law_parseSexprRoundtrip :"));
4866        assert!(lean.contains("theorem toString'_law_parseRoundtrip_sample_1 :"));
4867    }
4868
4869    #[test]
4870    fn lambda_example_keeps_only_eval_outside_proof_subset() {
4871        let ctx = ctx_from_source(include_str!("../../../examples/core/lambda.av"), "lambda");
4872        let issues = proof_mode_issues(&ctx);
4873        assert_eq!(
4874            issues,
4875            vec!["recursive function 'eval' is outside proof subset (currently supported: Int countdown, second-order affine Int recurrences with pair-state worker, structural recursion on List/recursive ADTs, String+position, mutual Int countdown, mutual String+position, and ranked sizeOf recursion)".to_string()]
4876        );
4877
4878        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4879        let lean = generated_lean_file(&out);
4880        assert!(lean.contains("def termToString__fuel"));
4881        assert!(lean.contains("def subst__fuel"));
4882        assert!(lean.contains("def countS__fuel"));
4883        assert!(!lean.contains("partial def termToString"));
4884        assert!(!lean.contains("partial def subst"));
4885        assert!(!lean.contains("partial def countS"));
4886        assert!(lean.contains("partial def eval"));
4887    }
4888
4889    #[test]
4890    fn mission_control_example_stays_inside_proof_subset() {
4891        let ctx = ctx_from_source(
4892            include_str!("../../../examples/apps/mission_control.av"),
4893            "mission_control",
4894        );
4895        let issues = proof_mode_issues(&ctx);
4896        assert!(
4897            issues.is_empty(),
4898            "expected mission_control example to stay inside proof subset, got: {:?}",
4899            issues
4900        );
4901
4902        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4903        let lean = generated_lean_file(&out);
4904        assert!(!lean.contains("partial def normalizeAngle"));
4905        assert!(lean.contains("def normalizeAngle__fuel"));
4906    }
4907
4908    #[test]
4909    fn notepad_store_example_stays_inside_proof_subset() {
4910        let ctx = ctx_from_source(
4911            include_str!("../../../examples/apps/notepad/store.av"),
4912            "notepad_store",
4913        );
4914        let issues = proof_mode_issues(&ctx);
4915        assert!(
4916            issues.is_empty(),
4917            "expected notepad/store example to stay inside proof subset, got: {:?}",
4918            issues
4919        );
4920
4921        let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
4922        let lean = generated_lean_file(&out);
4923        assert!(lean.contains("def deserializeLine (line : String) : Except String Note :="));
4924        assert!(lean.contains("Except String (List Note)"));
4925        assert!(!lean.contains("partial def deserializeLine"));
4926        assert!(lean.contains("-- when noteRoundtripSafe note"));
4927        assert!(lean.contains("-- when notesRoundtripSafe notes"));
4928        assert!(lean.contains(
4929            "theorem serializeLine_law_lineRoundtrip : ∀ (note : Note), note = { id' := 1, title := \"Hello\", body := \"World\" : Note } ∨"
4930        ));
4931        assert!(lean.contains(
4932            "theorem serializeLines_law_notesRoundtrip : ∀ (notes : List Note), notes = [] ∨"
4933        ));
4934        assert!(lean.contains("notesRoundtripSafe notes = true ->"));
4935        assert!(lean.contains("parseNotes (s!\"{String.intercalate \"\\n\" (serializeLines notes)}\\n\") = Except.ok notes"));
4936        assert!(lean.contains("theorem serializeLine_law_lineRoundtrip_sample_1 :"));
4937        assert!(lean.contains("theorem serializeLines_law_notesRoundtrip_sample_1 :"));
4938    }
4939}