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