Skip to main content

aver/codegen/lean/
mod.rs

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