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