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            kind: VerifyKind::Cases,
1576            trace: false,
1577            cases_givens: vec![],
1578        }));
1579        ctx
1580    }
1581
1582    fn empty_ctx_with_two_verify_blocks_same_fn() -> CodegenContext {
1583        let mut ctx = empty_ctx();
1584        ctx.items.push(TopLevel::Verify(VerifyBlock {
1585            fn_name: "f".to_string(),
1586            line: 1,
1587            cases: vec![(
1588                sb(Expr::Literal(Literal::Int(1))),
1589                sb(Expr::Literal(Literal::Int(1))),
1590            )],
1591            case_spans: vec![],
1592            case_givens: vec![],
1593            case_hostile_origins: vec![],
1594            case_hostile_profiles: vec![],
1595            kind: VerifyKind::Cases,
1596            trace: false,
1597            cases_givens: vec![],
1598        }));
1599        ctx.items.push(TopLevel::Verify(VerifyBlock {
1600            fn_name: "f".to_string(),
1601            line: 2,
1602            cases: vec![(
1603                sb(Expr::Literal(Literal::Int(2))),
1604                sb(Expr::Literal(Literal::Int(2))),
1605            )],
1606            case_spans: vec![],
1607            case_givens: vec![],
1608            case_hostile_origins: vec![],
1609            case_hostile_profiles: vec![],
1610            kind: VerifyKind::Cases,
1611            trace: false,
1612            cases_givens: vec![],
1613        }));
1614        ctx
1615    }
1616
1617    fn empty_ctx_with_verify_law() -> CodegenContext {
1618        let mut ctx = empty_ctx();
1619        let add = FnDef {
1620            name: "add".to_string(),
1621            line: 1,
1622            params: vec![
1623                ("a".to_string(), "Int".to_string()),
1624                ("b".to_string(), "Int".to_string()),
1625            ],
1626            return_type: "Int".to_string(),
1627            effects: vec![],
1628            desc: None,
1629            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
1630                BinOp::Add,
1631                sbb(Expr::Ident("a".to_string())),
1632                sbb(Expr::Ident("b".to_string())),
1633            )))),
1634            resolution: None,
1635        };
1636        ctx.fn_defs.push(add.clone());
1637        ctx.items.push(TopLevel::FnDef(add));
1638        ctx.items.push(TopLevel::Verify(VerifyBlock {
1639            fn_name: "add".to_string(),
1640            line: 1,
1641            cases: vec![
1642                (
1643                    sb(Expr::FnCall(
1644                        sbb(Expr::Ident("add".to_string())),
1645                        vec![
1646                            sb(Expr::Literal(Literal::Int(1))),
1647                            sb(Expr::Literal(Literal::Int(2))),
1648                        ],
1649                    )),
1650                    sb(Expr::FnCall(
1651                        sbb(Expr::Ident("add".to_string())),
1652                        vec![
1653                            sb(Expr::Literal(Literal::Int(2))),
1654                            sb(Expr::Literal(Literal::Int(1))),
1655                        ],
1656                    )),
1657                ),
1658                (
1659                    sb(Expr::FnCall(
1660                        sbb(Expr::Ident("add".to_string())),
1661                        vec![
1662                            sb(Expr::Literal(Literal::Int(2))),
1663                            sb(Expr::Literal(Literal::Int(3))),
1664                        ],
1665                    )),
1666                    sb(Expr::FnCall(
1667                        sbb(Expr::Ident("add".to_string())),
1668                        vec![
1669                            sb(Expr::Literal(Literal::Int(3))),
1670                            sb(Expr::Literal(Literal::Int(2))),
1671                        ],
1672                    )),
1673                ),
1674            ],
1675            case_spans: vec![],
1676            case_givens: vec![],
1677            case_hostile_origins: vec![],
1678            case_hostile_profiles: vec![],
1679            kind: VerifyKind::Law(Box::new(VerifyLaw {
1680                name: "commutative".to_string(),
1681                givens: vec![
1682                    VerifyGiven {
1683                        name: "a".to_string(),
1684                        type_name: "Int".to_string(),
1685                        domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
1686                    },
1687                    VerifyGiven {
1688                        name: "b".to_string(),
1689                        type_name: "Int".to_string(),
1690                        domain: VerifyGivenDomain::Explicit(vec![
1691                            sb(Expr::Literal(Literal::Int(2))),
1692                            sb(Expr::Literal(Literal::Int(3))),
1693                        ]),
1694                    },
1695                ],
1696                when: None,
1697                lhs: sb(Expr::FnCall(
1698                    sbb(Expr::Ident("add".to_string())),
1699                    vec![
1700                        sb(Expr::Ident("a".to_string())),
1701                        sb(Expr::Ident("b".to_string())),
1702                    ],
1703                )),
1704                rhs: sb(Expr::FnCall(
1705                    sbb(Expr::Ident("add".to_string())),
1706                    vec![
1707                        sb(Expr::Ident("b".to_string())),
1708                        sb(Expr::Ident("a".to_string())),
1709                    ],
1710                )),
1711                sample_guards: vec![],
1712            })),
1713            trace: false,
1714            cases_givens: vec![],
1715        }));
1716        ctx
1717    }
1718
1719    #[test]
1720    fn prelude_normalizes_float_string_format() {
1721        let prelude = generate_prelude();
1722        assert!(
1723            prelude.contains("private def normalizeFloatString (s : String) : String :="),
1724            "missing normalizeFloatString helper in prelude"
1725        );
1726        assert!(
1727            prelude.contains(
1728                "def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)"
1729            ),
1730            "String.fromFloat should normalize Lean float formatting"
1731        );
1732    }
1733
1734    #[test]
1735    fn prelude_validates_char_from_code_unicode_bounds() {
1736        let prelude = generate_prelude();
1737        assert!(
1738            prelude.contains("if n < 0 || n > 1114111 then none"),
1739            "Char.fromCode should reject code points above Unicode max"
1740        );
1741        assert!(
1742            prelude.contains("else if n >= 55296 && n <= 57343 then none"),
1743            "Char.fromCode should reject surrogate code points"
1744        );
1745    }
1746
1747    #[test]
1748    fn prelude_includes_map_set_helper_lemmas() {
1749        let prelude = generate_prelude();
1750        assert!(
1751            prelude.contains("theorem has_set_self [DecidableEq α]"),
1752            "missing AverMap.has_set_self helper theorem"
1753        );
1754        assert!(
1755            prelude.contains("theorem get_set_self [DecidableEq α]"),
1756            "missing AverMap.get_set_self helper theorem"
1757        );
1758    }
1759
1760    #[test]
1761    fn lean_output_without_map_usage_omits_map_prelude() {
1762        let mut ctx = ctx_from_source(
1763            r#"
1764module NoMap
1765    intent = "Simple pure program without maps."
1766
1767fn addOne(n: Int) -> Int
1768    n + 1
1769
1770verify addOne
1771    addOne(1) => 2
1772"#,
1773            "nomap",
1774        );
1775        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
1776        let lean = generated_lean_file(&out);
1777
1778        assert!(
1779            !lean.contains("namespace AverMap"),
1780            "did not expect AverMap prelude in program without map usage:\n{}",
1781            lean
1782        );
1783    }
1784
1785    #[test]
1786    fn transpile_emits_native_decide_for_verify_by_default() {
1787        let mut ctx = empty_ctx_with_verify_case();
1788        let out = transpile(&mut ctx);
1789        let lean = out
1790            .files
1791            .iter()
1792            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1793            .expect("expected generated Lean file");
1794        assert!(lean.contains("example : 1 = 1 := by native_decide"));
1795    }
1796
1797    #[test]
1798    fn transpile_can_emit_sorry_for_verify_when_requested() {
1799        let mut ctx = empty_ctx_with_verify_case();
1800        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::Sorry);
1801        let lean = out
1802            .files
1803            .iter()
1804            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1805            .expect("expected generated Lean file");
1806        assert!(lean.contains("example : 1 = 1 := by sorry"));
1807    }
1808
1809    #[test]
1810    fn transpile_can_emit_theorem_skeletons_for_verify() {
1811        let mut ctx = empty_ctx_with_verify_case();
1812        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::TheoremSkeleton);
1813        let lean = out
1814            .files
1815            .iter()
1816            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1817            .expect("expected generated Lean file");
1818        assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
1819        assert!(lean.contains("  sorry"));
1820    }
1821
1822    #[test]
1823    fn theorem_skeleton_numbering_is_global_per_function_across_verify_blocks() {
1824        let mut ctx = empty_ctx_with_two_verify_blocks_same_fn();
1825        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::TheoremSkeleton);
1826        let lean = out
1827            .files
1828            .iter()
1829            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1830            .expect("expected generated Lean file");
1831        assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
1832        assert!(lean.contains("theorem f_verify_2 : 2 = 2 := by"));
1833    }
1834
1835    #[test]
1836    fn transpile_emits_named_theorems_for_verify_law() {
1837        let mut ctx = empty_ctx_with_verify_law();
1838        let out = transpile(&mut ctx);
1839        let lean = out
1840            .files
1841            .iter()
1842            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1843            .expect("expected generated Lean file");
1844        assert!(lean.contains("-- verify law add.commutative (2 cases)"));
1845        assert!(lean.contains("-- given a: Int = 1..2"));
1846        assert!(lean.contains("-- given b: Int = [2, 3]"));
1847        assert!(lean.contains(
1848            "theorem add_law_commutative : ∀ (a : Int) (b : Int), add a b = add b a := by"
1849        ));
1850        assert!(lean.contains("  intro a b"));
1851        assert!(lean.contains("  simp [add, Int.add_comm]"));
1852        assert!(lean.contains(
1853            "theorem add_law_commutative_sample_1 : add 1 2 = add 2 1 := by native_decide"
1854        ));
1855        assert!(lean.contains(
1856            "theorem add_law_commutative_sample_2 : add 2 3 = add 3 2 := by native_decide"
1857        ));
1858    }
1859
1860    #[test]
1861    fn generate_prelude_emits_int_roundtrip_theorem() {
1862        let lean = generate_prelude();
1863        assert!(lean.contains(
1864            "theorem Int.fromString_fromInt : ∀ n : Int, Int.fromString (String.fromInt n) = .ok n"
1865        ));
1866        assert!(lean.contains("theorem String.intercalate_empty_chars (s : String) :"));
1867        assert!(lean.contains("def splitOnCharGo"));
1868        assert!(lean.contains("theorem split_single_char_append"));
1869        assert!(lean.contains("theorem split_intercalate_trailing_single_char"));
1870        assert!(lean.contains("namespace AverDigits"));
1871        assert!(lean.contains("theorem String.charAt_length_none (s : String)"));
1872        assert!(lean.contains("theorem digitChar_not_ws : ∀ d : Nat, d < 10 ->"));
1873    }
1874
1875    #[test]
1876    fn transpile_emits_guarded_theorems_for_verify_law_when_clause() {
1877        let mut ctx = ctx_from_source(
1878            r#"
1879module GuardedLaw
1880    intent =
1881        "verify law with precondition"
1882
1883fn pickGreater(a: Int, b: Int) -> Int
1884    match a > b
1885        true -> a
1886        false -> b
1887
1888verify pickGreater law ordered
1889    given a: Int = [1, 2]
1890    given b: Int = [1, 2]
1891    when a > b
1892    pickGreater(a, b) => a
1893"#,
1894            "guarded_law",
1895        );
1896        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::TheoremSkeleton);
1897        let lean = generated_lean_file(&out);
1898
1899        assert!(lean.contains("-- when (a > b)"));
1900        assert!(lean.contains(
1901            "theorem pickGreater_law_ordered : ∀ (a : Int) (b : Int), a = 1 ∨ a = 2 -> b = 1 ∨ b = 2 -> (a > b) = true -> pickGreater a b = a := by"
1902        ));
1903        assert!(lean.contains(
1904            "theorem pickGreater_law_ordered_sample_1 : (1 > 1) = true -> pickGreater 1 1 = 1 := by"
1905        ));
1906        assert!(lean.contains(
1907            "theorem pickGreater_law_ordered_sample_4 : (2 > 2) = true -> pickGreater 2 2 = 2 := by"
1908        ));
1909    }
1910
1911    #[test]
1912    fn transpile_uses_spec_theorem_names_for_declared_spec_laws() {
1913        let mut ctx = ctx_from_source(
1914            r#"
1915module SpecDemo
1916    intent =
1917        "spec demo"
1918
1919fn absVal(x: Int) -> Int
1920    match x < 0
1921        true -> 0 - x
1922        false -> x
1923
1924fn absValSpec(x: Int) -> Int
1925    match x < 0
1926        true -> 0 - x
1927        false -> x
1928
1929verify absVal law absValSpec
1930    given x: Int = [-2, -1, 0, 1, 2]
1931    absVal(x) => absValSpec(x)
1932"#,
1933            "spec_demo",
1934        );
1935        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::TheoremSkeleton);
1936        let lean = generated_lean_file(&out);
1937
1938        assert!(lean.contains("-- verify law absVal.spec absValSpec (5 cases)"));
1939        assert!(
1940            lean.contains(
1941                "theorem absVal_eq_absValSpec : ∀ (x : Int), absVal x = absValSpec x := by"
1942            )
1943        );
1944        assert!(lean.contains("theorem absVal_eq_absValSpec_checked_domain :"));
1945        assert!(lean.contains("theorem absVal_eq_absValSpec_sample_1 :"));
1946        assert!(!lean.contains("theorem absVal_law_absValSpec :"));
1947    }
1948
1949    #[test]
1950    fn transpile_keeps_noncanonical_spec_laws_as_regular_law_names() {
1951        let mut ctx = ctx_from_source(
1952            r#"
1953module SpecLawShape
1954    intent =
1955        "shape probe"
1956
1957fn foo(x: Int) -> Int
1958    x + 1
1959
1960fn fooSpec(seed: Int, x: Int) -> Int
1961    x + seed
1962
1963verify foo law fooSpec
1964    given x: Int = [1, 2]
1965    foo(x) => fooSpec(1, x)
1966"#,
1967            "spec_law_shape",
1968        );
1969        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::TheoremSkeleton);
1970        let lean = generated_lean_file(&out);
1971
1972        assert!(lean.contains("-- verify law foo.fooSpec (2 cases)"));
1973        assert!(lean.contains("theorem foo_law_fooSpec : ∀ (x : Int), foo x = fooSpec 1 x := by"));
1974        assert!(!lean.contains("theorem foo_eq_fooSpec :"));
1975    }
1976
1977    #[test]
1978    fn transpile_auto_proves_linear_int_canonical_spec_law_in_auto_mode() {
1979        let mut ctx = ctx_from_source(
1980            r#"
1981module SpecGap
1982    intent =
1983        "nontrivial canonical spec law"
1984
1985fn inc(x: Int) -> Int
1986    x + 1
1987
1988fn incSpec(x: Int) -> Int
1989    x + 2 - 1
1990
1991verify inc law incSpec
1992    given x: Int = [0, 1, 2]
1993    inc(x) => incSpec(x)
1994"#,
1995            "spec_gap",
1996        );
1997        let out = transpile(&mut ctx);
1998        let lean = generated_lean_file(&out);
1999
2000        assert!(lean.contains("-- verify law inc.spec incSpec (3 cases)"));
2001        assert!(lean.contains("theorem inc_eq_incSpec : ∀ (x : Int), inc x = incSpec x := by"));
2002        assert!(lean.contains("change (x + 1) = ((x + 2) - 1)"));
2003        assert!(lean.contains("omega"));
2004        assert!(!lean.contains(
2005            "-- universal theorem inc_eq_incSpec omitted: sampled law shape is not auto-proved yet"
2006        ));
2007        assert!(lean.contains("theorem inc_eq_incSpec_checked_domain :"));
2008    }
2009
2010    #[test]
2011    fn transpile_auto_proves_guarded_canonical_spec_law_in_auto_mode() {
2012        let mut ctx = ctx_from_source(
2013            r#"
2014module GuardedSpecGap
2015    intent =
2016        "guarded canonical spec law"
2017
2018fn clampNonNegative(x: Int) -> Int
2019    match x < 0
2020        true -> 0
2021        false -> x
2022
2023fn clampNonNegativeSpec(x: Int) -> Int
2024    match x < 0
2025        true -> 0
2026        false -> x
2027
2028verify clampNonNegative law clampNonNegativeSpec
2029    given x: Int = [-2, -1, 0, 1, 2]
2030    when x >= 0
2031    clampNonNegative(x) => clampNonNegativeSpec(x)
2032"#,
2033            "guarded_spec_gap",
2034        );
2035        let out = transpile(&mut ctx);
2036        let lean = generated_lean_file(&out);
2037
2038        assert!(lean.contains("-- when (x >= 0)"));
2039        assert!(lean.contains(
2040            "theorem clampNonNegative_eq_clampNonNegativeSpec : ∀ (x : Int), x = (-2) ∨ x = (-1) ∨ x = 0 ∨ x = 1 ∨ x = 2 -> (x >= 0) = true -> clampNonNegative x = clampNonNegativeSpec x := by"
2041        ));
2042        assert!(lean.contains("intro x h_x h_when"));
2043        assert!(lean.contains("simpa [clampNonNegative, clampNonNegativeSpec]"));
2044        assert!(!lean.contains(
2045            "-- universal theorem clampNonNegative_eq_clampNonNegativeSpec omitted: sampled law shape is not auto-proved yet"
2046        ));
2047        assert!(!lean.contains("cases h_x"));
2048    }
2049
2050    #[test]
2051    fn transpile_auto_proves_simp_normalized_canonical_spec_law_in_auto_mode() {
2052        let mut ctx = ctx_from_source(
2053            r#"
2054module SpecGapNonlinear
2055    intent =
2056        "nonlinear canonical spec law"
2057
2058fn square(x: Int) -> Int
2059    x * x
2060
2061fn squareSpec(x: Int) -> Int
2062    x * x + 0
2063
2064verify square law squareSpec
2065    given x: Int = [0, 1, 2]
2066    square(x) => squareSpec(x)
2067"#,
2068            "spec_gap_nonlinear",
2069        );
2070        let out = transpile(&mut ctx);
2071        let lean = generated_lean_file(&out);
2072
2073        assert!(lean.contains("-- verify law square.spec squareSpec (3 cases)"));
2074        assert!(
2075            lean.contains(
2076                "theorem square_eq_squareSpec : ∀ (x : Int), square x = squareSpec x := by"
2077            )
2078        );
2079        assert!(lean.contains("simp [square, squareSpec]"));
2080        assert!(!lean.contains(
2081            "-- universal theorem square_eq_squareSpec omitted: sampled law shape is not auto-proved yet"
2082        ));
2083        assert!(lean.contains("theorem square_eq_squareSpec_checked_domain :"));
2084        assert!(lean.contains("theorem square_eq_squareSpec_sample_1 :"));
2085    }
2086
2087    #[test]
2088    fn transpile_auto_proves_reflexive_law_with_rfl() {
2089        let mut ctx = empty_ctx();
2090        ctx.items.push(TopLevel::Verify(VerifyBlock {
2091            fn_name: "idLaw".to_string(),
2092            line: 1,
2093            cases: vec![(
2094                sb(Expr::Literal(Literal::Int(1))),
2095                sb(Expr::Literal(Literal::Int(1))),
2096            )],
2097            case_spans: vec![],
2098            case_givens: vec![],
2099            case_hostile_origins: vec![],
2100            case_hostile_profiles: vec![],
2101            kind: VerifyKind::Law(Box::new(VerifyLaw {
2102                name: "reflexive".to_string(),
2103                givens: vec![VerifyGiven {
2104                    name: "x".to_string(),
2105                    type_name: "Int".to_string(),
2106                    domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
2107                }],
2108                when: None,
2109                lhs: sb(Expr::Ident("x".to_string())),
2110                rhs: sb(Expr::Ident("x".to_string())),
2111                sample_guards: vec![],
2112            })),
2113            trace: false,
2114            cases_givens: vec![],
2115        }));
2116        let out = transpile(&mut ctx);
2117        let lean = out
2118            .files
2119            .iter()
2120            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2121            .expect("expected generated Lean file");
2122        assert!(lean.contains("theorem idLaw_law_reflexive : ∀ (x : Int), x = x := by"));
2123        assert!(lean.contains("  intro x"));
2124        assert!(lean.contains("  rfl"));
2125    }
2126
2127    #[test]
2128    fn transpile_auto_proves_identity_law_for_int_add_wrapper() {
2129        let mut ctx = empty_ctx_with_verify_law();
2130        ctx.items.push(TopLevel::Verify(VerifyBlock {
2131            fn_name: "add".to_string(),
2132            line: 10,
2133            cases: vec![(
2134                sb(Expr::FnCall(
2135                    sbb(Expr::Ident("add".to_string())),
2136                    vec![
2137                        sb(Expr::Literal(Literal::Int(1))),
2138                        sb(Expr::Literal(Literal::Int(0))),
2139                    ],
2140                )),
2141                sb(Expr::Literal(Literal::Int(1))),
2142            )],
2143            case_spans: vec![],
2144            case_givens: vec![],
2145            case_hostile_origins: vec![],
2146            case_hostile_profiles: vec![],
2147            kind: VerifyKind::Law(Box::new(VerifyLaw {
2148                name: "identityZero".to_string(),
2149                givens: vec![VerifyGiven {
2150                    name: "a".to_string(),
2151                    type_name: "Int".to_string(),
2152                    domain: VerifyGivenDomain::Explicit(vec![
2153                        sb(Expr::Literal(Literal::Int(0))),
2154                        sb(Expr::Literal(Literal::Int(1))),
2155                    ]),
2156                }],
2157                when: None,
2158                lhs: sb(Expr::FnCall(
2159                    sbb(Expr::Ident("add".to_string())),
2160                    vec![
2161                        sb(Expr::Ident("a".to_string())),
2162                        sb(Expr::Literal(Literal::Int(0))),
2163                    ],
2164                )),
2165                rhs: sb(Expr::Ident("a".to_string())),
2166                sample_guards: vec![],
2167            })),
2168            trace: false,
2169            cases_givens: vec![],
2170        }));
2171        let out = transpile(&mut ctx);
2172        let lean = out
2173            .files
2174            .iter()
2175            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2176            .expect("expected generated Lean file");
2177        assert!(lean.contains("theorem add_law_identityZero : ∀ (a : Int), add a 0 = a := by"));
2178        assert!(lean.contains("  intro a"));
2179        assert!(lean.contains("  simp [add]"));
2180    }
2181
2182    #[test]
2183    fn transpile_auto_proves_associative_law_for_int_add_wrapper() {
2184        let mut ctx = empty_ctx_with_verify_law();
2185        ctx.items.push(TopLevel::Verify(VerifyBlock {
2186            fn_name: "add".to_string(),
2187            line: 20,
2188            cases: vec![(
2189                sb(Expr::FnCall(
2190                    sbb(Expr::Ident("add".to_string())),
2191                    vec![
2192                        sb(Expr::FnCall(
2193                            sbb(Expr::Ident("add".to_string())),
2194                            vec![
2195                                sb(Expr::Literal(Literal::Int(1))),
2196                                sb(Expr::Literal(Literal::Int(2))),
2197                            ],
2198                        )),
2199                        sb(Expr::Literal(Literal::Int(3))),
2200                    ],
2201                )),
2202                sb(Expr::FnCall(
2203                    sbb(Expr::Ident("add".to_string())),
2204                    vec![
2205                        sb(Expr::Literal(Literal::Int(1))),
2206                        sb(Expr::FnCall(
2207                            sbb(Expr::Ident("add".to_string())),
2208                            vec![
2209                                sb(Expr::Literal(Literal::Int(2))),
2210                                sb(Expr::Literal(Literal::Int(3))),
2211                            ],
2212                        )),
2213                    ],
2214                )),
2215            )],
2216            case_spans: vec![],
2217            case_givens: vec![],
2218            case_hostile_origins: vec![],
2219            case_hostile_profiles: vec![],
2220            kind: VerifyKind::Law(Box::new(VerifyLaw {
2221                name: "associative".to_string(),
2222                givens: vec![
2223                    VerifyGiven {
2224                        name: "a".to_string(),
2225                        type_name: "Int".to_string(),
2226                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2227                            1,
2228                        )))]),
2229                    },
2230                    VerifyGiven {
2231                        name: "b".to_string(),
2232                        type_name: "Int".to_string(),
2233                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2234                            2,
2235                        )))]),
2236                    },
2237                    VerifyGiven {
2238                        name: "c".to_string(),
2239                        type_name: "Int".to_string(),
2240                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2241                            3,
2242                        )))]),
2243                    },
2244                ],
2245                when: None,
2246                lhs: sb(Expr::FnCall(
2247                    sbb(Expr::Ident("add".to_string())),
2248                    vec![
2249                        sb(Expr::FnCall(
2250                            sbb(Expr::Ident("add".to_string())),
2251                            vec![
2252                                sb(Expr::Ident("a".to_string())),
2253                                sb(Expr::Ident("b".to_string())),
2254                            ],
2255                        )),
2256                        sb(Expr::Ident("c".to_string())),
2257                    ],
2258                )),
2259                rhs: sb(Expr::FnCall(
2260                    sbb(Expr::Ident("add".to_string())),
2261                    vec![
2262                        sb(Expr::Ident("a".to_string())),
2263                        sb(Expr::FnCall(
2264                            sbb(Expr::Ident("add".to_string())),
2265                            vec![
2266                                sb(Expr::Ident("b".to_string())),
2267                                sb(Expr::Ident("c".to_string())),
2268                            ],
2269                        )),
2270                    ],
2271                )),
2272                sample_guards: vec![],
2273            })),
2274            trace: false,
2275            cases_givens: vec![],
2276        }));
2277        let out = transpile(&mut ctx);
2278        let lean = out
2279            .files
2280            .iter()
2281            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2282            .expect("expected generated Lean file");
2283        assert!(lean.contains(
2284            "theorem add_law_associative : ∀ (a : Int) (b : Int) (c : Int), add (add a b) c = add a (add b c) := by"
2285        ));
2286        assert!(lean.contains("  intro a b c"));
2287        assert!(lean.contains("  simp [add, Int.add_assoc]"));
2288    }
2289
2290    #[test]
2291    fn transpile_auto_proves_sub_laws() {
2292        let mut ctx = empty_ctx();
2293        let sub = FnDef {
2294            name: "sub".to_string(),
2295            line: 1,
2296            params: vec![
2297                ("a".to_string(), "Int".to_string()),
2298                ("b".to_string(), "Int".to_string()),
2299            ],
2300            return_type: "Int".to_string(),
2301            effects: vec![],
2302            desc: None,
2303            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
2304                BinOp::Sub,
2305                sbb(Expr::Ident("a".to_string())),
2306                sbb(Expr::Ident("b".to_string())),
2307            )))),
2308            resolution: None,
2309        };
2310        ctx.fn_defs.push(sub.clone());
2311        ctx.items.push(TopLevel::FnDef(sub));
2312
2313        ctx.items.push(TopLevel::Verify(VerifyBlock {
2314            fn_name: "sub".to_string(),
2315            line: 10,
2316            cases: vec![(
2317                sb(Expr::FnCall(
2318                    sbb(Expr::Ident("sub".to_string())),
2319                    vec![
2320                        sb(Expr::Literal(Literal::Int(2))),
2321                        sb(Expr::Literal(Literal::Int(0))),
2322                    ],
2323                )),
2324                sb(Expr::Literal(Literal::Int(2))),
2325            )],
2326            case_spans: vec![],
2327            case_givens: vec![],
2328            case_hostile_origins: vec![],
2329            case_hostile_profiles: vec![],
2330            kind: VerifyKind::Law(Box::new(VerifyLaw {
2331                name: "rightIdentity".to_string(),
2332                givens: vec![VerifyGiven {
2333                    name: "a".to_string(),
2334                    type_name: "Int".to_string(),
2335                    domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(2)))]),
2336                }],
2337                when: None,
2338                lhs: sb(Expr::FnCall(
2339                    sbb(Expr::Ident("sub".to_string())),
2340                    vec![
2341                        sb(Expr::Ident("a".to_string())),
2342                        sb(Expr::Literal(Literal::Int(0))),
2343                    ],
2344                )),
2345                rhs: sb(Expr::Ident("a".to_string())),
2346                sample_guards: vec![],
2347            })),
2348            trace: false,
2349            cases_givens: vec![],
2350        }));
2351        ctx.items.push(TopLevel::Verify(VerifyBlock {
2352            fn_name: "sub".to_string(),
2353            line: 20,
2354            cases: vec![(
2355                sb(Expr::FnCall(
2356                    sbb(Expr::Ident("sub".to_string())),
2357                    vec![
2358                        sb(Expr::Literal(Literal::Int(2))),
2359                        sb(Expr::Literal(Literal::Int(1))),
2360                    ],
2361                )),
2362                sb(Expr::Neg(sbb(Expr::FnCall(
2363                    sbb(Expr::Ident("sub".to_string())),
2364                    vec![
2365                        sb(Expr::Literal(Literal::Int(1))),
2366                        sb(Expr::Literal(Literal::Int(2))),
2367                    ],
2368                )))),
2369            )],
2370            case_spans: vec![],
2371            case_givens: vec![],
2372            case_hostile_origins: vec![],
2373            case_hostile_profiles: vec![],
2374            kind: VerifyKind::Law(Box::new(VerifyLaw {
2375                name: "antiCommutative".to_string(),
2376                givens: vec![
2377                    VerifyGiven {
2378                        name: "a".to_string(),
2379                        type_name: "Int".to_string(),
2380                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2381                            2,
2382                        )))]),
2383                    },
2384                    VerifyGiven {
2385                        name: "b".to_string(),
2386                        type_name: "Int".to_string(),
2387                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2388                            1,
2389                        )))]),
2390                    },
2391                ],
2392                when: None,
2393                lhs: sb(Expr::FnCall(
2394                    sbb(Expr::Ident("sub".to_string())),
2395                    vec![
2396                        sb(Expr::Ident("a".to_string())),
2397                        sb(Expr::Ident("b".to_string())),
2398                    ],
2399                )),
2400                rhs: sb(Expr::Neg(sbb(Expr::FnCall(
2401                    sbb(Expr::Ident("sub".to_string())),
2402                    vec![
2403                        sb(Expr::Ident("b".to_string())),
2404                        sb(Expr::Ident("a".to_string())),
2405                    ],
2406                )))),
2407                sample_guards: vec![],
2408            })),
2409            trace: false,
2410            cases_givens: vec![],
2411        }));
2412
2413        let out = transpile(&mut ctx);
2414        let lean = out
2415            .files
2416            .iter()
2417            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2418            .expect("expected generated Lean file");
2419        assert!(lean.contains("theorem sub_law_rightIdentity : ∀ (a : Int), sub a 0 = a := by"));
2420        assert!(lean.contains("  simp [sub]"));
2421        assert!(lean.contains(
2422            "theorem sub_law_antiCommutative : ∀ (a : Int) (b : Int), sub a b = (-sub b a) := by"
2423        ));
2424        assert!(lean.contains("  simpa [sub] using (Int.neg_sub b a).symm"));
2425    }
2426
2427    #[test]
2428    fn transpile_auto_proves_unary_wrapper_equivalence_law() {
2429        let mut ctx = empty_ctx();
2430        let add = FnDef {
2431            name: "add".to_string(),
2432            line: 1,
2433            params: vec![
2434                ("a".to_string(), "Int".to_string()),
2435                ("b".to_string(), "Int".to_string()),
2436            ],
2437            return_type: "Int".to_string(),
2438            effects: vec![],
2439            desc: None,
2440            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
2441                BinOp::Add,
2442                sbb(Expr::Ident("a".to_string())),
2443                sbb(Expr::Ident("b".to_string())),
2444            )))),
2445            resolution: None,
2446        };
2447        let add_one = FnDef {
2448            name: "addOne".to_string(),
2449            line: 2,
2450            params: vec![("n".to_string(), "Int".to_string())],
2451            return_type: "Int".to_string(),
2452            effects: vec![],
2453            desc: None,
2454            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
2455                BinOp::Add,
2456                sbb(Expr::Ident("n".to_string())),
2457                sbb(Expr::Literal(Literal::Int(1))),
2458            )))),
2459            resolution: None,
2460        };
2461        ctx.fn_defs.push(add.clone());
2462        ctx.fn_defs.push(add_one.clone());
2463        ctx.items.push(TopLevel::FnDef(add));
2464        ctx.items.push(TopLevel::FnDef(add_one));
2465        ctx.items.push(TopLevel::Verify(VerifyBlock {
2466            fn_name: "addOne".to_string(),
2467            line: 3,
2468            cases: vec![(
2469                sb(Expr::FnCall(
2470                    sbb(Expr::Ident("addOne".to_string())),
2471                    vec![sb(Expr::Literal(Literal::Int(2)))],
2472                )),
2473                sb(Expr::FnCall(
2474                    sbb(Expr::Ident("add".to_string())),
2475                    vec![
2476                        sb(Expr::Literal(Literal::Int(2))),
2477                        sb(Expr::Literal(Literal::Int(1))),
2478                    ],
2479                )),
2480            )],
2481            case_spans: vec![],
2482            case_givens: vec![],
2483            case_hostile_origins: vec![],
2484            case_hostile_profiles: vec![],
2485            kind: VerifyKind::Law(Box::new(VerifyLaw {
2486                name: "identityViaAdd".to_string(),
2487                givens: vec![VerifyGiven {
2488                    name: "n".to_string(),
2489                    type_name: "Int".to_string(),
2490                    domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(2)))]),
2491                }],
2492                when: None,
2493                lhs: sb(Expr::FnCall(
2494                    sbb(Expr::Ident("addOne".to_string())),
2495                    vec![sb(Expr::Ident("n".to_string()))],
2496                )),
2497                rhs: sb(Expr::FnCall(
2498                    sbb(Expr::Ident("add".to_string())),
2499                    vec![
2500                        sb(Expr::Ident("n".to_string())),
2501                        sb(Expr::Literal(Literal::Int(1))),
2502                    ],
2503                )),
2504                sample_guards: vec![],
2505            })),
2506            trace: false,
2507            cases_givens: vec![],
2508        }));
2509        let out = transpile(&mut ctx);
2510        let lean = out
2511            .files
2512            .iter()
2513            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2514            .expect("expected generated Lean file");
2515        assert!(
2516            lean.contains(
2517                "theorem addOne_law_identityViaAdd : ∀ (n : Int), addOne n = add n 1 := by"
2518            )
2519        );
2520        assert!(lean.contains("  simp [addOne, add]"));
2521    }
2522
2523    #[test]
2524    fn transpile_auto_proves_direct_map_set_laws() {
2525        let mut ctx = empty_ctx();
2526
2527        let map_set = |m: Spanned<Expr>, k: Spanned<Expr>, v: Spanned<Expr>| {
2528            sb(Expr::FnCall(
2529                sbb(Expr::Attr(
2530                    sbb(Expr::Ident("Map".to_string())),
2531                    "set".to_string(),
2532                )),
2533                vec![m, k, v],
2534            ))
2535        };
2536        let map_has = |m: Spanned<Expr>, k: Spanned<Expr>| {
2537            sb(Expr::FnCall(
2538                sbb(Expr::Attr(
2539                    sbb(Expr::Ident("Map".to_string())),
2540                    "has".to_string(),
2541                )),
2542                vec![m, k],
2543            ))
2544        };
2545        let map_get = |m: Spanned<Expr>, k: Spanned<Expr>| {
2546            sb(Expr::FnCall(
2547                sbb(Expr::Attr(
2548                    sbb(Expr::Ident("Map".to_string())),
2549                    "get".to_string(),
2550                )),
2551                vec![m, k],
2552            ))
2553        };
2554        let some = |v: Spanned<Expr>| {
2555            sb(Expr::FnCall(
2556                sbb(Expr::Attr(
2557                    sbb(Expr::Ident("Option".to_string())),
2558                    "Some".to_string(),
2559                )),
2560                vec![v],
2561            ))
2562        };
2563        let map_empty = || {
2564            sb(Expr::FnCall(
2565                sbb(Expr::Attr(
2566                    sbb(Expr::Ident("Map".to_string())),
2567                    "empty".to_string(),
2568                )),
2569                vec![],
2570            ))
2571        };
2572
2573        ctx.items.push(TopLevel::Verify(VerifyBlock {
2574            fn_name: "map".to_string(),
2575            line: 1,
2576            cases: vec![(
2577                map_has(
2578                    map_set(
2579                        sb(Expr::Ident("m".to_string())),
2580                        sb(Expr::Ident("k".to_string())),
2581                        sb(Expr::Ident("v".to_string())),
2582                    ),
2583                    sb(Expr::Ident("k".to_string())),
2584                ),
2585                sb(Expr::Literal(Literal::Bool(true))),
2586            )],
2587            case_spans: vec![],
2588            case_givens: vec![],
2589            case_hostile_origins: vec![],
2590            case_hostile_profiles: vec![],
2591            kind: VerifyKind::Law(Box::new(VerifyLaw {
2592                name: "setHasKey".to_string(),
2593                givens: vec![
2594                    VerifyGiven {
2595                        name: "m".to_string(),
2596                        type_name: "Map<String, Int>".to_string(),
2597                        domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
2598                    },
2599                    VerifyGiven {
2600                        name: "k".to_string(),
2601                        type_name: "String".to_string(),
2602                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Str(
2603                            "a".to_string(),
2604                        )))]),
2605                    },
2606                    VerifyGiven {
2607                        name: "v".to_string(),
2608                        type_name: "Int".to_string(),
2609                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2610                            1,
2611                        )))]),
2612                    },
2613                ],
2614                when: None,
2615                lhs: map_has(
2616                    map_set(
2617                        sb(Expr::Ident("m".to_string())),
2618                        sb(Expr::Ident("k".to_string())),
2619                        sb(Expr::Ident("v".to_string())),
2620                    ),
2621                    sb(Expr::Ident("k".to_string())),
2622                ),
2623                rhs: sb(Expr::Literal(Literal::Bool(true))),
2624                sample_guards: vec![],
2625            })),
2626            trace: false,
2627            cases_givens: vec![],
2628        }));
2629
2630        ctx.items.push(TopLevel::Verify(VerifyBlock {
2631            fn_name: "map".to_string(),
2632            line: 2,
2633            cases: vec![(
2634                map_get(
2635                    map_set(
2636                        sb(Expr::Ident("m".to_string())),
2637                        sb(Expr::Ident("k".to_string())),
2638                        sb(Expr::Ident("v".to_string())),
2639                    ),
2640                    sb(Expr::Ident("k".to_string())),
2641                ),
2642                some(sb(Expr::Ident("v".to_string()))),
2643            )],
2644            case_spans: vec![],
2645            case_givens: vec![],
2646            case_hostile_origins: vec![],
2647            case_hostile_profiles: vec![],
2648            kind: VerifyKind::Law(Box::new(VerifyLaw {
2649                name: "setGetKey".to_string(),
2650                givens: vec![
2651                    VerifyGiven {
2652                        name: "m".to_string(),
2653                        type_name: "Map<String, Int>".to_string(),
2654                        domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
2655                    },
2656                    VerifyGiven {
2657                        name: "k".to_string(),
2658                        type_name: "String".to_string(),
2659                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Str(
2660                            "a".to_string(),
2661                        )))]),
2662                    },
2663                    VerifyGiven {
2664                        name: "v".to_string(),
2665                        type_name: "Int".to_string(),
2666                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
2667                            1,
2668                        )))]),
2669                    },
2670                ],
2671                when: None,
2672                lhs: map_get(
2673                    map_set(
2674                        sb(Expr::Ident("m".to_string())),
2675                        sb(Expr::Ident("k".to_string())),
2676                        sb(Expr::Ident("v".to_string())),
2677                    ),
2678                    sb(Expr::Ident("k".to_string())),
2679                ),
2680                rhs: some(sb(Expr::Ident("v".to_string()))),
2681                sample_guards: vec![],
2682            })),
2683            trace: false,
2684            cases_givens: vec![],
2685        }));
2686
2687        let out = transpile(&mut ctx);
2688        let lean = out
2689            .files
2690            .iter()
2691            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2692            .expect("expected generated Lean file");
2693        assert!(lean.contains("simpa using AverMap.has_set_self m k v"));
2694        assert!(lean.contains("simpa using AverMap.get_set_self m k v"));
2695    }
2696
2697    #[test]
2698    fn transpile_auto_proves_direct_recursive_sum_law_by_structural_induction() {
2699        let mut ctx = ctx_from_source(
2700            r#"
2701module Mirror
2702    intent =
2703        "direct recursive sum induction probe"
2704
2705type Tree
2706    Leaf(Int)
2707    Node(Tree, Tree)
2708
2709fn mirror(t: Tree) -> Tree
2710    match t
2711        Tree.Leaf(v) -> Tree.Leaf(v)
2712        Tree.Node(left, right) -> Tree.Node(mirror(right), mirror(left))
2713
2714verify mirror law involutive
2715    given t: Tree = [Tree.Leaf(1), Tree.Node(Tree.Leaf(1), Tree.Leaf(2))]
2716    mirror(mirror(t)) => t
2717"#,
2718            "mirror",
2719        );
2720        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
2721        let lean = generated_lean_file(&out);
2722
2723        assert!(
2724            lean.contains(
2725                "theorem mirror_law_involutive : ∀ (t : Tree), mirror (mirror t) = t := by"
2726            )
2727        );
2728        assert!(lean.contains("  induction t with"));
2729        assert!(lean.contains("  | leaf f0 => simp [mirror]"));
2730        assert!(lean.contains("  | node f0 f1 ih0 ih1 => simp_all [mirror]"));
2731        assert!(!lean.contains(
2732            "-- universal theorem mirror_law_involutive omitted: sampled law shape is not auto-proved yet"
2733        ));
2734    }
2735
2736    #[test]
2737    fn transpile_auto_proves_map_update_laws() {
2738        let mut ctx = empty_ctx();
2739
2740        let map_get = |m: Spanned<Expr>, k: Spanned<Expr>| {
2741            sb(Expr::FnCall(
2742                sbb(Expr::Attr(
2743                    sbb(Expr::Ident("Map".to_string())),
2744                    "get".to_string(),
2745                )),
2746                vec![m, k],
2747            ))
2748        };
2749        let map_set = |m: Spanned<Expr>, k: Spanned<Expr>, v: Spanned<Expr>| {
2750            sb(Expr::FnCall(
2751                sbb(Expr::Attr(
2752                    sbb(Expr::Ident("Map".to_string())),
2753                    "set".to_string(),
2754                )),
2755                vec![m, k, v],
2756            ))
2757        };
2758        let map_has = |m: Spanned<Expr>, k: Spanned<Expr>| {
2759            sb(Expr::FnCall(
2760                sbb(Expr::Attr(
2761                    sbb(Expr::Ident("Map".to_string())),
2762                    "has".to_string(),
2763                )),
2764                vec![m, k],
2765            ))
2766        };
2767        let option_some = |v: Spanned<Expr>| {
2768            sb(Expr::FnCall(
2769                sbb(Expr::Attr(
2770                    sbb(Expr::Ident("Option".to_string())),
2771                    "Some".to_string(),
2772                )),
2773                vec![v],
2774            ))
2775        };
2776        let option_with_default = |opt: Spanned<Expr>, def: Spanned<Expr>| {
2777            sb(Expr::FnCall(
2778                sbb(Expr::Attr(
2779                    sbb(Expr::Ident("Option".to_string())),
2780                    "withDefault".to_string(),
2781                )),
2782                vec![opt, def],
2783            ))
2784        };
2785        let map_empty = || {
2786            sb(Expr::FnCall(
2787                sbb(Expr::Attr(
2788                    sbb(Expr::Ident("Map".to_string())),
2789                    "empty".to_string(),
2790                )),
2791                vec![],
2792            ))
2793        };
2794
2795        let add_one = FnDef {
2796            name: "addOne".to_string(),
2797            line: 1,
2798            params: vec![("n".to_string(), "Int".to_string())],
2799            return_type: "Int".to_string(),
2800            effects: vec![],
2801            desc: None,
2802            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
2803                BinOp::Add,
2804                sbb(Expr::Ident("n".to_string())),
2805                sbb(Expr::Literal(Literal::Int(1))),
2806            )))),
2807            resolution: None,
2808        };
2809        ctx.fn_defs.push(add_one.clone());
2810        ctx.items.push(TopLevel::FnDef(add_one));
2811
2812        let inc_count = FnDef {
2813            name: "incCount".to_string(),
2814            line: 2,
2815            params: vec![
2816                ("counts".to_string(), "Map<String, Int>".to_string()),
2817                ("word".to_string(), "String".to_string()),
2818            ],
2819            return_type: "Map<String, Int>".to_string(),
2820            effects: vec![],
2821            desc: None,
2822            body: Rc::new(FnBody::Block(vec![
2823                Stmt::Binding(
2824                    "current".to_string(),
2825                    None,
2826                    map_get(
2827                        sb(Expr::Ident("counts".to_string())),
2828                        sb(Expr::Ident("word".to_string())),
2829                    ),
2830                ),
2831                Stmt::Expr(sb(Expr::Match {
2832                    subject: sbb(Expr::Ident("current".to_string())),
2833                    arms: vec![
2834                        MatchArm {
2835                            pattern: Pattern::Constructor(
2836                                "Option.Some".to_string(),
2837                                vec!["n".to_string()],
2838                            ),
2839                            body: Box::new(map_set(
2840                                sb(Expr::Ident("counts".to_string())),
2841                                sb(Expr::Ident("word".to_string())),
2842                                sb(Expr::BinOp(
2843                                    BinOp::Add,
2844                                    sbb(Expr::Ident("n".to_string())),
2845                                    sbb(Expr::Literal(Literal::Int(1))),
2846                                )),
2847                            )),
2848                            binding_slots: std::sync::OnceLock::new(),
2849                        },
2850                        MatchArm {
2851                            pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
2852                            body: Box::new(map_set(
2853                                sb(Expr::Ident("counts".to_string())),
2854                                sb(Expr::Ident("word".to_string())),
2855                                sb(Expr::Literal(Literal::Int(1))),
2856                            )),
2857                            binding_slots: std::sync::OnceLock::new(),
2858                        },
2859                    ],
2860                })),
2861            ])),
2862            resolution: None,
2863        };
2864        ctx.fn_defs.push(inc_count.clone());
2865        ctx.items.push(TopLevel::FnDef(inc_count));
2866
2867        ctx.items.push(TopLevel::Verify(VerifyBlock {
2868            fn_name: "incCount".to_string(),
2869            line: 10,
2870            cases: vec![(
2871                map_has(
2872                    sb(Expr::FnCall(
2873                        sbb(Expr::Ident("incCount".to_string())),
2874                        vec![
2875                            sb(Expr::Ident("counts".to_string())),
2876                            sb(Expr::Ident("word".to_string())),
2877                        ],
2878                    )),
2879                    sb(Expr::Ident("word".to_string())),
2880                ),
2881                sb(Expr::Literal(Literal::Bool(true))),
2882            )],
2883            case_spans: vec![],
2884            case_givens: vec![],
2885            case_hostile_origins: vec![],
2886            case_hostile_profiles: vec![],
2887            kind: VerifyKind::Law(Box::new(VerifyLaw {
2888                name: "keyPresent".to_string(),
2889                givens: vec![
2890                    VerifyGiven {
2891                        name: "counts".to_string(),
2892                        type_name: "Map<String, Int>".to_string(),
2893                        domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
2894                    },
2895                    VerifyGiven {
2896                        name: "word".to_string(),
2897                        type_name: "String".to_string(),
2898                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Str(
2899                            "a".to_string(),
2900                        )))]),
2901                    },
2902                ],
2903                when: None,
2904                lhs: map_has(
2905                    sb(Expr::FnCall(
2906                        sbb(Expr::Ident("incCount".to_string())),
2907                        vec![
2908                            sb(Expr::Ident("counts".to_string())),
2909                            sb(Expr::Ident("word".to_string())),
2910                        ],
2911                    )),
2912                    sb(Expr::Ident("word".to_string())),
2913                ),
2914                rhs: sb(Expr::Literal(Literal::Bool(true))),
2915                sample_guards: vec![],
2916            })),
2917            trace: false,
2918            cases_givens: vec![],
2919        }));
2920
2921        ctx.items.push(TopLevel::Verify(VerifyBlock {
2922            fn_name: "incCount".to_string(),
2923            line: 20,
2924            cases: vec![(
2925                map_get(
2926                    sb(Expr::FnCall(
2927                        sbb(Expr::Ident("incCount".to_string())),
2928                        vec![
2929                            sb(Expr::Ident("counts".to_string())),
2930                            sb(Expr::Literal(Literal::Str("a".to_string()))),
2931                        ],
2932                    )),
2933                    sb(Expr::Literal(Literal::Str("a".to_string()))),
2934                ),
2935                option_some(sb(Expr::FnCall(
2936                    sbb(Expr::Ident("addOne".to_string())),
2937                    vec![option_with_default(
2938                        map_get(
2939                            sb(Expr::Ident("counts".to_string())),
2940                            sb(Expr::Literal(Literal::Str("a".to_string()))),
2941                        ),
2942                        sb(Expr::Literal(Literal::Int(0))),
2943                    )],
2944                ))),
2945            )],
2946            case_spans: vec![],
2947            case_givens: vec![],
2948            case_hostile_origins: vec![],
2949            case_hostile_profiles: vec![],
2950            kind: VerifyKind::Law(Box::new(VerifyLaw {
2951                name: "existingKeyIncrements".to_string(),
2952                givens: vec![VerifyGiven {
2953                    name: "counts".to_string(),
2954                    type_name: "Map<String, Int>".to_string(),
2955                    domain: VerifyGivenDomain::Explicit(vec![map_empty()]),
2956                }],
2957                when: None,
2958                lhs: map_get(
2959                    sb(Expr::FnCall(
2960                        sbb(Expr::Ident("incCount".to_string())),
2961                        vec![
2962                            sb(Expr::Ident("counts".to_string())),
2963                            sb(Expr::Literal(Literal::Str("a".to_string()))),
2964                        ],
2965                    )),
2966                    sb(Expr::Literal(Literal::Str("a".to_string()))),
2967                ),
2968                rhs: option_some(sb(Expr::FnCall(
2969                    sbb(Expr::Ident("addOne".to_string())),
2970                    vec![option_with_default(
2971                        map_get(
2972                            sb(Expr::Ident("counts".to_string())),
2973                            sb(Expr::Literal(Literal::Str("a".to_string()))),
2974                        ),
2975                        sb(Expr::Literal(Literal::Int(0))),
2976                    )],
2977                ))),
2978                sample_guards: vec![],
2979            })),
2980            trace: false,
2981            cases_givens: vec![],
2982        }));
2983
2984        let out = transpile(&mut ctx);
2985        let lean = out
2986            .files
2987            .iter()
2988            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2989            .expect("expected generated Lean file");
2990        assert!(
2991            lean.contains("cases h : AverMap.get counts word <;> simp [AverMap.has_set_self]"),
2992            "expected keyPresent auto-proof with has_set_self"
2993        );
2994        assert!(
2995            lean.contains("cases h : AverMap.get counts \"a\" <;> simp [AverMap.get_set_self, addOne, incCount]"),
2996            "expected existingKeyIncrements auto-proof with get_set_self"
2997        );
2998    }
2999
3000    #[test]
3001    fn transpile_parenthesizes_negative_int_call_args_in_law_samples() {
3002        let mut ctx = empty_ctx();
3003        let add = FnDef {
3004            name: "add".to_string(),
3005            line: 1,
3006            params: vec![
3007                ("a".to_string(), "Int".to_string()),
3008                ("b".to_string(), "Int".to_string()),
3009            ],
3010            return_type: "Int".to_string(),
3011            effects: vec![],
3012            desc: None,
3013            body: Rc::new(FnBody::from_expr(sb(Expr::BinOp(
3014                BinOp::Add,
3015                sbb(Expr::Ident("a".to_string())),
3016                sbb(Expr::Ident("b".to_string())),
3017            )))),
3018            resolution: None,
3019        };
3020        ctx.fn_defs.push(add.clone());
3021        ctx.items.push(TopLevel::FnDef(add));
3022        ctx.items.push(TopLevel::Verify(VerifyBlock {
3023            fn_name: "add".to_string(),
3024            line: 1,
3025            cases: vec![(
3026                sb(Expr::FnCall(
3027                    sbb(Expr::Ident("add".to_string())),
3028                    vec![
3029                        sb(Expr::Literal(Literal::Int(-2))),
3030                        sb(Expr::Literal(Literal::Int(-1))),
3031                    ],
3032                )),
3033                sb(Expr::FnCall(
3034                    sbb(Expr::Ident("add".to_string())),
3035                    vec![
3036                        sb(Expr::Literal(Literal::Int(-1))),
3037                        sb(Expr::Literal(Literal::Int(-2))),
3038                    ],
3039                )),
3040            )],
3041            case_spans: vec![],
3042            case_givens: vec![],
3043            case_hostile_origins: vec![],
3044            case_hostile_profiles: vec![],
3045            kind: VerifyKind::Law(Box::new(VerifyLaw {
3046                name: "commutative".to_string(),
3047                givens: vec![
3048                    VerifyGiven {
3049                        name: "a".to_string(),
3050                        type_name: "Int".to_string(),
3051                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3052                            -2,
3053                        )))]),
3054                    },
3055                    VerifyGiven {
3056                        name: "b".to_string(),
3057                        type_name: "Int".to_string(),
3058                        domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(
3059                            -1,
3060                        )))]),
3061                    },
3062                ],
3063                when: None,
3064                lhs: sb(Expr::FnCall(
3065                    sbb(Expr::Ident("add".to_string())),
3066                    vec![
3067                        sb(Expr::Ident("a".to_string())),
3068                        sb(Expr::Ident("b".to_string())),
3069                    ],
3070                )),
3071                rhs: sb(Expr::FnCall(
3072                    sbb(Expr::Ident("add".to_string())),
3073                    vec![
3074                        sb(Expr::Ident("b".to_string())),
3075                        sb(Expr::Ident("a".to_string())),
3076                    ],
3077                )),
3078                sample_guards: vec![],
3079            })),
3080            trace: false,
3081            cases_givens: vec![],
3082        }));
3083
3084        let out = transpile(&mut ctx);
3085        let lean = out
3086            .files
3087            .iter()
3088            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3089            .expect("expected generated Lean file");
3090        assert!(lean.contains(
3091            "theorem add_law_commutative_sample_1 : add (-2) (-1) = add (-1) (-2) := by native_decide"
3092        ));
3093    }
3094
3095    #[test]
3096    fn verify_law_numbering_is_scoped_per_law_name() {
3097        let mut ctx = empty_ctx();
3098        let f = FnDef {
3099            name: "f".to_string(),
3100            line: 1,
3101            params: vec![("x".to_string(), "Int".to_string())],
3102            return_type: "Int".to_string(),
3103            effects: vec![],
3104            desc: None,
3105            body: Rc::new(FnBody::from_expr(sb(Expr::Ident("x".to_string())))),
3106            resolution: None,
3107        };
3108        ctx.fn_defs.push(f.clone());
3109        ctx.items.push(TopLevel::FnDef(f));
3110        ctx.items.push(TopLevel::Verify(VerifyBlock {
3111            fn_name: "f".to_string(),
3112            line: 1,
3113            cases: vec![(
3114                sb(Expr::Literal(Literal::Int(1))),
3115                sb(Expr::Literal(Literal::Int(1))),
3116            )],
3117            case_spans: vec![],
3118            case_givens: vec![],
3119            case_hostile_origins: vec![],
3120            case_hostile_profiles: vec![],
3121            kind: VerifyKind::Cases,
3122            trace: false,
3123            cases_givens: vec![],
3124        }));
3125        ctx.items.push(TopLevel::Verify(VerifyBlock {
3126            fn_name: "f".to_string(),
3127            line: 2,
3128            cases: vec![(
3129                sb(Expr::Literal(Literal::Int(2))),
3130                sb(Expr::Literal(Literal::Int(2))),
3131            )],
3132            case_spans: vec![],
3133            case_givens: vec![],
3134            case_hostile_origins: vec![],
3135            case_hostile_profiles: vec![],
3136            kind: VerifyKind::Law(Box::new(VerifyLaw {
3137                name: "identity".to_string(),
3138                givens: vec![VerifyGiven {
3139                    name: "x".to_string(),
3140                    type_name: "Int".to_string(),
3141                    domain: VerifyGivenDomain::Explicit(vec![sb(Expr::Literal(Literal::Int(2)))]),
3142                }],
3143                when: None,
3144                lhs: sb(Expr::Ident("x".to_string())),
3145                rhs: sb(Expr::Ident("x".to_string())),
3146                sample_guards: vec![],
3147            })),
3148            trace: false,
3149            cases_givens: vec![],
3150        }));
3151        let out = transpile_with_verify_mode(&mut ctx, VerifyEmitMode::TheoremSkeleton);
3152        let lean = out
3153            .files
3154            .iter()
3155            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3156            .expect("expected generated Lean file");
3157        assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
3158        assert!(lean.contains("theorem f_law_identity : ∀ (x : Int), x = x := by"));
3159        assert!(lean.contains("theorem f_law_identity_sample_1 : 2 = 2 := by"));
3160        assert!(!lean.contains("theorem f_law_identity_sample_2 : 2 = 2 := by"));
3161    }
3162
3163    #[test]
3164    fn proof_mode_accepts_single_int_countdown_recursion() {
3165        let mut ctx = empty_ctx();
3166        let down = FnDef {
3167            name: "down".to_string(),
3168            line: 1,
3169            params: vec![("n".to_string(), "Int".to_string())],
3170            return_type: "Int".to_string(),
3171            effects: vec![],
3172            desc: None,
3173            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3174                subject: sbb(Expr::Ident("n".to_string())),
3175                arms: vec![
3176                    MatchArm {
3177                        pattern: Pattern::Literal(Literal::Int(0)),
3178                        body: sbb(Expr::Literal(Literal::Int(0))),
3179                        binding_slots: std::sync::OnceLock::new(),
3180                    },
3181                    MatchArm {
3182                        pattern: Pattern::Wildcard,
3183                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3184                            "down".to_string(),
3185                            vec![sb(Expr::BinOp(
3186                                BinOp::Sub,
3187                                sbb(Expr::Ident("n".to_string())),
3188                                sbb(Expr::Literal(Literal::Int(1))),
3189                            ))],
3190                        )))),
3191                        binding_slots: std::sync::OnceLock::new(),
3192                    },
3193                ],
3194            }))),
3195            resolution: None,
3196        };
3197        ctx.items.push(TopLevel::FnDef(down.clone()));
3198        ctx.fn_defs.push(down);
3199
3200        ctx.refresh_facts();
3201        let issues = proof_mode_issues(&ctx);
3202        assert!(
3203            issues.is_empty(),
3204            "expected Int countdown recursion to be accepted, got: {:?}",
3205            issues
3206        );
3207
3208        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3209        let lean = out
3210            .files
3211            .iter()
3212            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3213            .expect("expected generated Lean file");
3214        assert!(lean.contains("def down__fuel"));
3215        assert!(lean.contains("def down (n : Int) : Int :="));
3216        assert!(lean.contains("down__fuel ((Int.natAbs n) + 1) n"));
3217    }
3218
3219    #[test]
3220    fn proof_mode_accepts_single_int_countdown_on_nonfirst_param() {
3221        let mut ctx = empty_ctx();
3222        let repeat_like = FnDef {
3223            name: "repeatLike".to_string(),
3224            line: 1,
3225            params: vec![
3226                ("char".to_string(), "String".to_string()),
3227                ("n".to_string(), "Int".to_string()),
3228            ],
3229            return_type: "List<String>".to_string(),
3230            effects: vec![],
3231            desc: None,
3232            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3233                subject: sbb(Expr::BinOp(
3234                    BinOp::Lte,
3235                    sbb(Expr::Ident("n".to_string())),
3236                    sbb(Expr::Literal(Literal::Int(0))),
3237                )),
3238                arms: vec![
3239                    MatchArm {
3240                        pattern: Pattern::Literal(Literal::Bool(true)),
3241                        body: sbb(Expr::List(vec![])),
3242                        binding_slots: std::sync::OnceLock::new(),
3243                    },
3244                    MatchArm {
3245                        pattern: Pattern::Literal(Literal::Bool(false)),
3246                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3247                            "repeatLike".to_string(),
3248                            vec![
3249                                sb(Expr::Ident("char".to_string())),
3250                                sb(Expr::BinOp(
3251                                    BinOp::Sub,
3252                                    sbb(Expr::Ident("n".to_string())),
3253                                    sbb(Expr::Literal(Literal::Int(1))),
3254                                )),
3255                            ],
3256                        )))),
3257                        binding_slots: std::sync::OnceLock::new(),
3258                    },
3259                ],
3260            }))),
3261            resolution: None,
3262        };
3263        ctx.items.push(TopLevel::FnDef(repeat_like.clone()));
3264        ctx.fn_defs.push(repeat_like);
3265
3266        ctx.refresh_facts();
3267        let issues = proof_mode_issues(&ctx);
3268        assert!(
3269            issues.is_empty(),
3270            "expected non-first Int countdown recursion to be accepted, got: {:?}",
3271            issues
3272        );
3273
3274        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3275        let lean = out
3276            .files
3277            .iter()
3278            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3279            .expect("expected generated Lean file");
3280        assert!(lean.contains("def repeatLike__fuel"));
3281        assert!(lean.contains("def repeatLike (char : String) (n : Int) : List String :="));
3282        assert!(lean.contains("repeatLike__fuel ((Int.natAbs n) + 1) char n"));
3283    }
3284
3285    #[test]
3286    fn proof_mode_accepts_negative_guarded_int_ascent() {
3287        let mut ctx = empty_ctx();
3288        let normalize = FnDef {
3289            name: "normalize".to_string(),
3290            line: 1,
3291            params: vec![("angle".to_string(), "Int".to_string())],
3292            return_type: "Int".to_string(),
3293            effects: vec![],
3294            desc: None,
3295            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3296                subject: sbb(Expr::BinOp(
3297                    BinOp::Lt,
3298                    sbb(Expr::Ident("angle".to_string())),
3299                    sbb(Expr::Literal(Literal::Int(0))),
3300                )),
3301                arms: vec![
3302                    MatchArm {
3303                        pattern: Pattern::Literal(Literal::Bool(true)),
3304                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3305                            "normalize".to_string(),
3306                            vec![sb(Expr::BinOp(
3307                                BinOp::Add,
3308                                sbb(Expr::Ident("angle".to_string())),
3309                                sbb(Expr::Literal(Literal::Int(360))),
3310                            ))],
3311                        )))),
3312                        binding_slots: std::sync::OnceLock::new(),
3313                    },
3314                    MatchArm {
3315                        pattern: Pattern::Literal(Literal::Bool(false)),
3316                        body: sbb(Expr::Ident("angle".to_string())),
3317                        binding_slots: std::sync::OnceLock::new(),
3318                    },
3319                ],
3320            }))),
3321            resolution: None,
3322        };
3323        ctx.items.push(TopLevel::FnDef(normalize.clone()));
3324        ctx.fn_defs.push(normalize);
3325
3326        ctx.refresh_facts();
3327        let issues = proof_mode_issues(&ctx);
3328        assert!(
3329            issues.is_empty(),
3330            "expected negative-guarded Int ascent recursion to be accepted, got: {:?}",
3331            issues
3332        );
3333
3334        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3335        let lean = out
3336            .files
3337            .iter()
3338            .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
3339            .expect("expected generated Lean file");
3340        assert!(lean.contains("def normalize__fuel"));
3341        assert!(lean.contains("normalize__fuel ((Int.natAbs angle) + 1) angle"));
3342    }
3343
3344    #[test]
3345    fn proof_mode_accepts_single_list_structural_recursion() {
3346        let mut ctx = empty_ctx();
3347        let len = FnDef {
3348            name: "len".to_string(),
3349            line: 1,
3350            params: vec![("xs".to_string(), "List<Int>".to_string())],
3351            return_type: "Int".to_string(),
3352            effects: vec![],
3353            desc: None,
3354            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3355                subject: sbb(Expr::Ident("xs".to_string())),
3356                arms: vec![
3357                    MatchArm {
3358                        pattern: Pattern::EmptyList,
3359                        body: sbb(Expr::Literal(Literal::Int(0))),
3360                        binding_slots: std::sync::OnceLock::new(),
3361                    },
3362                    MatchArm {
3363                        pattern: Pattern::Cons("h".to_string(), "t".to_string()),
3364                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3365                            "len".to_string(),
3366                            vec![sb(Expr::Ident("t".to_string()))],
3367                        )))),
3368                        binding_slots: std::sync::OnceLock::new(),
3369                    },
3370                ],
3371            }))),
3372            resolution: None,
3373        };
3374        ctx.items.push(TopLevel::FnDef(len.clone()));
3375        ctx.fn_defs.push(len);
3376
3377        ctx.refresh_facts();
3378        let issues = proof_mode_issues(&ctx);
3379        assert!(
3380            issues.is_empty(),
3381            "expected List structural recursion to be accepted, got: {:?}",
3382            issues
3383        );
3384    }
3385
3386    #[test]
3387    fn proof_mode_accepts_single_list_structural_recursion_on_nonfirst_param() {
3388        let mut ctx = empty_ctx();
3389        let len_from = FnDef {
3390            name: "lenFrom".to_string(),
3391            line: 1,
3392            params: vec![
3393                ("count".to_string(), "Int".to_string()),
3394                ("xs".to_string(), "List<Int>".to_string()),
3395            ],
3396            return_type: "Int".to_string(),
3397            effects: vec![],
3398            desc: None,
3399            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3400                subject: sbb(Expr::Ident("xs".to_string())),
3401                arms: vec![
3402                    MatchArm {
3403                        pattern: Pattern::EmptyList,
3404                        body: sbb(Expr::Ident("count".to_string())),
3405                        binding_slots: std::sync::OnceLock::new(),
3406                    },
3407                    MatchArm {
3408                        pattern: Pattern::Cons("h".to_string(), "t".to_string()),
3409                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3410                            "lenFrom".to_string(),
3411                            vec![
3412                                sb(Expr::BinOp(
3413                                    BinOp::Add,
3414                                    sbb(Expr::Ident("count".to_string())),
3415                                    sbb(Expr::Literal(Literal::Int(1))),
3416                                )),
3417                                sb(Expr::Ident("t".to_string())),
3418                            ],
3419                        )))),
3420                        binding_slots: std::sync::OnceLock::new(),
3421                    },
3422                ],
3423            }))),
3424            resolution: None,
3425        };
3426        ctx.items.push(TopLevel::FnDef(len_from.clone()));
3427        ctx.fn_defs.push(len_from);
3428
3429        ctx.refresh_facts();
3430        let issues = proof_mode_issues(&ctx);
3431        assert!(
3432            issues.is_empty(),
3433            "expected non-first List structural recursion to be accepted, got: {:?}",
3434            issues
3435        );
3436
3437        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3438        let lean = generated_lean_file(&out);
3439        assert!(lean.contains("termination_by xs.length"));
3440        assert!(!lean.contains("partial def lenFrom"));
3441    }
3442
3443    #[test]
3444    fn proof_mode_accepts_single_string_pos_advance_recursion() {
3445        let mut ctx = empty_ctx();
3446        let skip_ws = FnDef {
3447            name: "skipWs".to_string(),
3448            line: 1,
3449            params: vec![
3450                ("s".to_string(), "String".to_string()),
3451                ("pos".to_string(), "Int".to_string()),
3452            ],
3453            return_type: "Int".to_string(),
3454            effects: vec![],
3455            desc: None,
3456            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3457                subject: sbb(Expr::FnCall(
3458                    sbb(Expr::Attr(
3459                        sbb(Expr::Ident("String".to_string())),
3460                        "charAt".to_string(),
3461                    )),
3462                    vec![
3463                        sb(Expr::Ident("s".to_string())),
3464                        sb(Expr::Ident("pos".to_string())),
3465                    ],
3466                )),
3467                arms: vec![
3468                    MatchArm {
3469                        pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
3470                        body: sbb(Expr::Ident("pos".to_string())),
3471                        binding_slots: std::sync::OnceLock::new(),
3472                    },
3473                    MatchArm {
3474                        pattern: Pattern::Wildcard,
3475                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3476                            "skipWs".to_string(),
3477                            vec![
3478                                sb(Expr::Ident("s".to_string())),
3479                                sb(Expr::BinOp(
3480                                    BinOp::Add,
3481                                    sbb(Expr::Ident("pos".to_string())),
3482                                    sbb(Expr::Literal(Literal::Int(1))),
3483                                )),
3484                            ],
3485                        )))),
3486                        binding_slots: std::sync::OnceLock::new(),
3487                    },
3488                ],
3489            }))),
3490            resolution: None,
3491        };
3492        ctx.items.push(TopLevel::FnDef(skip_ws.clone()));
3493        ctx.fn_defs.push(skip_ws);
3494
3495        ctx.refresh_facts();
3496        let issues = proof_mode_issues(&ctx);
3497        assert!(
3498            issues.is_empty(),
3499            "expected String+pos recursion to be accepted, got: {:?}",
3500            issues
3501        );
3502
3503        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3504        let lean = generated_lean_file(&out);
3505        assert!(lean.contains("def skipWs__fuel"));
3506        assert!(!lean.contains("partial def skipWs"));
3507    }
3508
3509    #[test]
3510    fn proof_mode_accepts_mutual_int_countdown_recursion() {
3511        let mut ctx = empty_ctx();
3512        let even = FnDef {
3513            name: "even".to_string(),
3514            line: 1,
3515            params: vec![("n".to_string(), "Int".to_string())],
3516            return_type: "Bool".to_string(),
3517            effects: vec![],
3518            desc: None,
3519            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3520                subject: sbb(Expr::Ident("n".to_string())),
3521                arms: vec![
3522                    MatchArm {
3523                        pattern: Pattern::Literal(Literal::Int(0)),
3524                        body: sbb(Expr::Literal(Literal::Bool(true))),
3525                        binding_slots: std::sync::OnceLock::new(),
3526                    },
3527                    MatchArm {
3528                        pattern: Pattern::Wildcard,
3529                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3530                            "odd".to_string(),
3531                            vec![sb(Expr::BinOp(
3532                                BinOp::Sub,
3533                                sbb(Expr::Ident("n".to_string())),
3534                                sbb(Expr::Literal(Literal::Int(1))),
3535                            ))],
3536                        )))),
3537                        binding_slots: std::sync::OnceLock::new(),
3538                    },
3539                ],
3540            }))),
3541            resolution: None,
3542        };
3543        let odd = FnDef {
3544            name: "odd".to_string(),
3545            line: 2,
3546            params: vec![("n".to_string(), "Int".to_string())],
3547            return_type: "Bool".to_string(),
3548            effects: vec![],
3549            desc: None,
3550            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3551                subject: sbb(Expr::Ident("n".to_string())),
3552                arms: vec![
3553                    MatchArm {
3554                        pattern: Pattern::Literal(Literal::Int(0)),
3555                        body: sbb(Expr::Literal(Literal::Bool(false))),
3556                        binding_slots: std::sync::OnceLock::new(),
3557                    },
3558                    MatchArm {
3559                        pattern: Pattern::Wildcard,
3560                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3561                            "even".to_string(),
3562                            vec![sb(Expr::BinOp(
3563                                BinOp::Sub,
3564                                sbb(Expr::Ident("n".to_string())),
3565                                sbb(Expr::Literal(Literal::Int(1))),
3566                            ))],
3567                        )))),
3568                        binding_slots: std::sync::OnceLock::new(),
3569                    },
3570                ],
3571            }))),
3572            resolution: None,
3573        };
3574        ctx.items.push(TopLevel::FnDef(even.clone()));
3575        ctx.items.push(TopLevel::FnDef(odd.clone()));
3576        ctx.fn_defs.push(even);
3577        ctx.fn_defs.push(odd);
3578
3579        ctx.refresh_facts();
3580        let issues = proof_mode_issues(&ctx);
3581        assert!(
3582            issues.is_empty(),
3583            "expected mutual Int countdown recursion to be accepted, got: {:?}",
3584            issues
3585        );
3586
3587        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3588        let lean = generated_lean_file(&out);
3589        assert!(lean.contains("def even__fuel"));
3590        assert!(lean.contains("def odd__fuel"));
3591        assert!(lean.contains("def even (n : Int) : Bool :="));
3592        assert!(lean.contains("even__fuel ((Int.natAbs n) + 1) n"));
3593    }
3594
3595    #[test]
3596    fn proof_mode_accepts_mutual_string_pos_recursion_with_ranked_same_edges() {
3597        let mut ctx = empty_ctx();
3598        let f = FnDef {
3599            name: "f".to_string(),
3600            line: 1,
3601            params: vec![
3602                ("s".to_string(), "String".to_string()),
3603                ("pos".to_string(), "Int".to_string()),
3604            ],
3605            return_type: "Int".to_string(),
3606            effects: vec![],
3607            desc: None,
3608            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3609                subject: sbb(Expr::BinOp(
3610                    BinOp::Gte,
3611                    sbb(Expr::Ident("pos".to_string())),
3612                    sbb(Expr::Literal(Literal::Int(3))),
3613                )),
3614                arms: vec![
3615                    MatchArm {
3616                        pattern: Pattern::Literal(Literal::Bool(true)),
3617                        body: sbb(Expr::Ident("pos".to_string())),
3618                        binding_slots: std::sync::OnceLock::new(),
3619                    },
3620                    MatchArm {
3621                        pattern: Pattern::Wildcard,
3622                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3623                            "g".to_string(),
3624                            vec![
3625                                sb(Expr::Ident("s".to_string())),
3626                                sb(Expr::Ident("pos".to_string())),
3627                            ],
3628                        )))),
3629                        binding_slots: std::sync::OnceLock::new(),
3630                    },
3631                ],
3632            }))),
3633            resolution: None,
3634        };
3635        let g = FnDef {
3636            name: "g".to_string(),
3637            line: 2,
3638            params: vec![
3639                ("s".to_string(), "String".to_string()),
3640                ("pos".to_string(), "Int".to_string()),
3641            ],
3642            return_type: "Int".to_string(),
3643            effects: vec![],
3644            desc: None,
3645            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3646                subject: sbb(Expr::BinOp(
3647                    BinOp::Gte,
3648                    sbb(Expr::Ident("pos".to_string())),
3649                    sbb(Expr::Literal(Literal::Int(3))),
3650                )),
3651                arms: vec![
3652                    MatchArm {
3653                        pattern: Pattern::Literal(Literal::Bool(true)),
3654                        body: sbb(Expr::Ident("pos".to_string())),
3655                        binding_slots: std::sync::OnceLock::new(),
3656                    },
3657                    MatchArm {
3658                        pattern: Pattern::Wildcard,
3659                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3660                            "f".to_string(),
3661                            vec![
3662                                sb(Expr::Ident("s".to_string())),
3663                                sb(Expr::BinOp(
3664                                    BinOp::Add,
3665                                    sbb(Expr::Ident("pos".to_string())),
3666                                    sbb(Expr::Literal(Literal::Int(1))),
3667                                )),
3668                            ],
3669                        )))),
3670                        binding_slots: std::sync::OnceLock::new(),
3671                    },
3672                ],
3673            }))),
3674            resolution: None,
3675        };
3676        ctx.items.push(TopLevel::FnDef(f.clone()));
3677        ctx.items.push(TopLevel::FnDef(g.clone()));
3678        ctx.fn_defs.push(f);
3679        ctx.fn_defs.push(g);
3680
3681        ctx.refresh_facts();
3682        let issues = proof_mode_issues(&ctx);
3683        assert!(
3684            issues.is_empty(),
3685            "expected mutual String+pos recursion to be accepted, got: {:?}",
3686            issues
3687        );
3688
3689        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3690        let lean = generated_lean_file(&out);
3691        assert!(lean.contains("def f__fuel"));
3692        assert!(lean.contains("def g__fuel"));
3693        assert!(!lean.contains("partial def f"));
3694    }
3695
3696    #[test]
3697    fn proof_mode_accepts_mutual_ranked_sizeof_recursion() {
3698        let mut ctx = empty_ctx();
3699        let f = FnDef {
3700            name: "f".to_string(),
3701            line: 1,
3702            params: vec![("xs".to_string(), "List<Int>".to_string())],
3703            return_type: "Int".to_string(),
3704            effects: vec![],
3705            desc: None,
3706            body: Rc::new(FnBody::from_expr(sb(Expr::TailCall(Box::new(
3707                TailCallData::new(
3708                    "g".to_string(),
3709                    vec![
3710                        sb(Expr::Literal(Literal::Str("acc".to_string()))),
3711                        sb(Expr::Ident("xs".to_string())),
3712                    ],
3713                ),
3714            ))))),
3715            resolution: None,
3716        };
3717        let g = FnDef {
3718            name: "g".to_string(),
3719            line: 2,
3720            params: vec![
3721                ("acc".to_string(), "String".to_string()),
3722                ("xs".to_string(), "List<Int>".to_string()),
3723            ],
3724            return_type: "Int".to_string(),
3725            effects: vec![],
3726            desc: None,
3727            body: Rc::new(FnBody::from_expr(sb(Expr::Match {
3728                subject: sbb(Expr::Ident("xs".to_string())),
3729                arms: vec![
3730                    MatchArm {
3731                        pattern: Pattern::EmptyList,
3732                        body: sbb(Expr::Literal(Literal::Int(0))),
3733                        binding_slots: std::sync::OnceLock::new(),
3734                    },
3735                    MatchArm {
3736                        pattern: Pattern::Cons("h".to_string(), "t".to_string()),
3737                        body: sbb(Expr::TailCall(Box::new(TailCallData::new(
3738                            "f".to_string(),
3739                            vec![sb(Expr::Ident("t".to_string()))],
3740                        )))),
3741                        binding_slots: std::sync::OnceLock::new(),
3742                    },
3743                ],
3744            }))),
3745            resolution: None,
3746        };
3747        ctx.items.push(TopLevel::FnDef(f.clone()));
3748        ctx.items.push(TopLevel::FnDef(g.clone()));
3749        ctx.fn_defs.push(f);
3750        ctx.fn_defs.push(g);
3751
3752        ctx.refresh_facts();
3753        let issues = proof_mode_issues(&ctx);
3754        assert!(
3755            issues.is_empty(),
3756            "expected mutual ranked-sizeOf recursion to be accepted, got: {:?}",
3757            issues
3758        );
3759
3760        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3761        let lean = generated_lean_file(&out);
3762        assert!(lean.contains("mutual"));
3763        assert!(lean.contains("def f__fuel"));
3764        assert!(lean.contains("def g__fuel"));
3765        assert!(!lean.contains("partial def f"));
3766        assert!(!lean.contains("partial def g"));
3767    }
3768
3769    #[test]
3770    fn proof_mode_rejects_recursive_pure_functions() {
3771        let mut ctx = empty_ctx();
3772        let recursive_fn = FnDef {
3773            name: "loop".to_string(),
3774            line: 1,
3775            params: vec![("n".to_string(), "Int".to_string())],
3776            return_type: "Int".to_string(),
3777            effects: vec![],
3778            desc: None,
3779            body: Rc::new(FnBody::from_expr(sb(Expr::FnCall(
3780                sbb(Expr::Ident("loop".to_string())),
3781                vec![sb(Expr::Ident("n".to_string()))],
3782            )))),
3783            resolution: None,
3784        };
3785        ctx.items.push(TopLevel::FnDef(recursive_fn.clone()));
3786        ctx.fn_defs.push(recursive_fn);
3787
3788        ctx.refresh_facts();
3789        let issues = proof_mode_issues(&ctx);
3790        assert!(
3791            issues.iter().any(|i| i.contains("outside proof subset")),
3792            "expected recursive function blocker, got: {:?}",
3793            issues
3794        );
3795    }
3796
3797    #[test]
3798    fn proof_mode_allows_recursive_types() {
3799        let mut ctx = empty_ctx();
3800        let recursive_type = TypeDef::Sum {
3801            name: "Node".to_string(),
3802            variants: vec![TypeVariant {
3803                name: "Cons".to_string(),
3804                fields: vec!["Node".to_string()],
3805            }],
3806            line: 1,
3807        };
3808        ctx.items.push(TopLevel::TypeDef(recursive_type.clone()));
3809        ctx.type_defs.push(recursive_type);
3810
3811        ctx.refresh_facts();
3812        let issues = proof_mode_issues(&ctx);
3813        assert!(
3814            issues
3815                .iter()
3816                .all(|i| !i.contains("recursive types require unsafe DecidableEq shim")),
3817            "did not expect recursive type blocker, got: {:?}",
3818            issues
3819        );
3820    }
3821
3822    #[test]
3823    fn law_auto_example_exports_real_proof_artifacts() {
3824        let mut ctx = ctx_from_source(
3825            include_str!("../../../examples/formal/law_auto.av"),
3826            "law_auto",
3827        );
3828        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3829        let lean = generated_lean_file(&out);
3830
3831        assert!(lean.contains("theorem add_law_commutative :"));
3832        assert!(lean.contains("theorem id'_law_reflexive : ∀ (x : Int), x = x := by"));
3833        assert!(lean.contains("theorem incCount_law_keyPresent :"));
3834        assert!(lean.contains("AverMap.has_set_self"));
3835        assert!(lean.contains("theorem add_law_commutative_sample_1 :"));
3836        assert!(lean.contains(":= by native_decide"));
3837    }
3838
3839    #[test]
3840    fn json_example_stays_inside_proof_subset() {
3841        let mut ctx = ctx_from_source(include_str!("../../../examples/data/json.av"), "json");
3842        ctx.refresh_facts();
3843        let issues = proof_mode_issues(&ctx);
3844        assert!(
3845            issues.is_empty(),
3846            "expected json example to stay inside proof subset, got: {:?}",
3847            issues
3848        );
3849    }
3850
3851    #[test]
3852    fn json_example_uses_total_defs_and_domain_guarded_laws_in_proof_mode() {
3853        let mut ctx = ctx_from_source(include_str!("../../../examples/data/json.av"), "json");
3854        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3855        let lean = generated_lean_file(&out);
3856
3857        assert!(!lean.contains("partial def"));
3858        assert!(lean.contains("def skipWs__fuel"));
3859        assert!(lean.contains("def parseValue__fuel"));
3860        assert!(lean.contains("def toString' (j : Json) : String :="));
3861        assert!(
3862            lean.contains(
3863                "def averMeasureJsonEntries_String (items : List (String × Json)) : Nat :="
3864            )
3865        );
3866        assert!(lean.contains(
3867            "| .jsonObject x0 => (averMeasureJsonEntries_String (AverMap.entries x0)) + 1"
3868        ));
3869        assert!(lean.contains("-- when jsonRoundtripSafe j"));
3870        assert!(!lean.contains("-- hint: verify law '"));
3871        assert!(!lean.contains("private theorem toString'_law_parseRoundtrip_aux"));
3872        assert!(
3873            lean.contains(
3874                "theorem toString'_law_parseRoundtrip : ∀ (j : Json), j = Json.jsonNull ∨"
3875            )
3876        );
3877        assert!(lean.contains(
3878            "jsonRoundtripSafe j = true -> fromString (toString' j) = Except.ok j := by"
3879        ));
3880        assert!(
3881            lean.contains("theorem finishFloat_law_fromCanonicalFloat : ∀ (f : Float), f = 3.5 ∨")
3882        );
3883        assert!(lean.contains("theorem finishInt_law_fromCanonicalInt_checked_domain :"));
3884        assert!(lean.contains(
3885            "theorem toString'_law_parseValueRoundtrip : ∀ (j : Json), j = Json.jsonNull ∨"
3886        ));
3887        assert!(lean.contains("theorem toString'_law_parseRoundtrip_sample_1 :"));
3888        assert!(lean.contains(
3889            "example : fromString \"null\" = Except.ok Json.jsonNull := by native_decide"
3890        ));
3891    }
3892
3893    #[test]
3894    fn transpile_injects_builtin_network_types_and_vector_get_support() {
3895        let mut ctx = ctx_from_source(
3896            r#"
3897fn firstOrMissing(xs: Vector<String>) -> Result<String, String>
3898    Option.toResult(Vector.get(xs, 0), "missing")
3899
3900fn defaultHeaders() -> Map<String, List<String>>
3901    {"content-type" => ["application/json"]}
3902
3903fn mkResponse(body: String) -> HttpResponse
3904    HttpResponse(status = 200, body = body, headers = defaultHeaders())
3905
3906fn requestPath(req: HttpRequest) -> String
3907    req.path
3908
3909fn echoConn(conn: Tcp.Connection) -> Tcp.Connection
3910    conn
3911"#,
3912            "network_helpers",
3913        );
3914        let out = transpile(&mut ctx);
3915        let lean = generated_lean_file(&out);
3916
3917        assert!(lean.contains("structure HttpResponse where"));
3918        assert!(lean.contains("structure HttpRequest where"));
3919        // `Tcp.Connection` is opaque from the surface (Phase 4.7+
3920        // fix #11), but the Lean prelude still ships its struct
3921        // so functions that take/return `Tcp.Connection` typecheck.
3922        assert!(lean.contains("structure Tcp_Connection where"));
3923        assert!(lean.contains("port : Int"));
3924        // Headers field renders as the Map shape (Lean uses List of pairs).
3925        assert!(lean.contains("List (String × List String)"));
3926    }
3927
3928    #[test]
3929    fn law_auto_example_has_no_sorry_in_proof_mode() {
3930        let mut ctx = ctx_from_source(
3931            include_str!("../../../examples/formal/law_auto.av"),
3932            "law_auto",
3933        );
3934        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3935        let lean = generated_lean_file(&out);
3936        assert!(
3937            !lean.contains("sorry"),
3938            "expected law_auto proof export to avoid sorry, got:\n{}",
3939            lean
3940        );
3941    }
3942
3943    #[test]
3944    fn map_example_has_no_sorry_in_proof_mode() {
3945        let mut ctx = ctx_from_source(include_str!("../../../examples/data/map.av"), "map");
3946        ctx.refresh_facts();
3947        let issues = proof_mode_issues(&ctx);
3948        assert!(
3949            issues.is_empty(),
3950            "expected map example to stay inside proof subset, got: {:?}",
3951            issues
3952        );
3953
3954        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3955        let lean = generated_lean_file(&out);
3956        // After codegen change: universal theorems that can't be auto-proved get sorry
3957        assert!(lean.contains("theorem incCount_law_trackedCountStepsByOne :"));
3958        assert!(lean.contains("sorry"));
3959        // Universal theorems that can't be auto-proved now get sorry instead of being omitted
3960        assert!(lean.contains("theorem countWords_law_presenceMatchesContains_sample_1 :"));
3961        assert!(lean.contains("theorem countWords_law_trackedWordCount_sample_1 :"));
3962        assert!(lean.contains("AverMap.has_set_self"));
3963        assert!(lean.contains("AverMap.get_set_self"));
3964    }
3965
3966    #[test]
3967    fn spec_laws_example_has_no_sorry_in_proof_mode() {
3968        let mut ctx = ctx_from_source(
3969            include_str!("../../../examples/formal/spec_laws.av"),
3970            "spec_laws",
3971        );
3972        ctx.refresh_facts();
3973        let issues = proof_mode_issues(&ctx);
3974        assert!(
3975            issues.is_empty(),
3976            "expected spec_laws example to stay inside proof subset, got: {:?}",
3977            issues
3978        );
3979
3980        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3981        let lean = generated_lean_file(&out);
3982        assert!(
3983            !lean.contains("sorry"),
3984            "expected spec_laws proof export to avoid sorry, got:\n{}",
3985            lean
3986        );
3987        assert!(lean.contains("theorem absVal_eq_absValSpec :"));
3988        assert!(lean.contains("theorem clampNonNegative_eq_clampNonNegativeSpec :"));
3989    }
3990
3991    #[test]
3992    fn rle_example_exports_sampled_roundtrip_laws_without_sorry() {
3993        let mut ctx = ctx_from_source(include_str!("../../../examples/data/rle.av"), "rle");
3994        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
3995        let lean = generated_lean_file(&out);
3996
3997        assert!(
3998            lean.contains("sorry"),
3999            "expected rle proof export to contain sorry for unproved universal theorems"
4000        );
4001        assert!(lean.contains(
4002            "theorem encode_law_roundtrip_sample_1 : decode (encode []) = [] := by native_decide"
4003        ));
4004        assert!(lean.contains(
4005            "theorem encodeString_law_string_roundtrip_sample_1 : decodeString (encodeString \"\") = \"\" := by native_decide"
4006        ));
4007    }
4008
4009    #[test]
4010    fn fibonacci_example_uses_fuelized_int_countdown_in_proof_mode() {
4011        let mut ctx = ctx_from_source(
4012            include_str!("../../../examples/data/fibonacci.av"),
4013            "fibonacci",
4014        );
4015        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4016        let lean = generated_lean_file(&out);
4017
4018        assert!(lean.contains("def fibTR__fuel"));
4019        assert!(lean.contains("def fibTR (n : Int) (a : Int) (b : Int) : Int :="));
4020        assert!(lean.contains("fibTR__fuel ((Int.natAbs n) + 1) n a b"));
4021        assert!(!lean.contains("partial def fibTR"));
4022    }
4023
4024    #[test]
4025    fn fibonacci_example_stays_inside_proof_subset() {
4026        let mut ctx = ctx_from_source(
4027            include_str!("../../../examples/data/fibonacci.av"),
4028            "fibonacci",
4029        );
4030        ctx.refresh_facts();
4031        let issues = proof_mode_issues(&ctx);
4032        assert!(
4033            issues.is_empty(),
4034            "expected fibonacci example to stay inside proof subset, got: {:?}",
4035            issues
4036        );
4037    }
4038
4039    #[test]
4040    fn fibonacci_example_matches_general_linear_recurrence_shapes() {
4041        let ctx = ctx_from_source(
4042            include_str!("../../../examples/data/fibonacci.av"),
4043            "fibonacci",
4044        );
4045        let fib = ctx.fn_defs.iter().find(|fd| fd.name == "fib").unwrap();
4046        let fib_tr = ctx.fn_defs.iter().find(|fd| fd.name == "fibTR").unwrap();
4047        let fib_spec = ctx.fn_defs.iter().find(|fd| fd.name == "fibSpec").unwrap();
4048
4049        assert!(recurrence::detect_tailrec_int_linear_pair_wrapper(fib).is_some());
4050        assert!(recurrence::detect_tailrec_int_linear_pair_worker(fib_tr).is_some());
4051        assert!(recurrence::detect_second_order_int_linear_recurrence(fib_spec).is_some());
4052    }
4053
4054    #[test]
4055    fn fibonacci_example_auto_proves_general_linear_recurrence_spec_law() {
4056        let mut ctx = ctx_from_source(
4057            include_str!("../../../examples/data/fibonacci.av"),
4058            "fibonacci",
4059        );
4060        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4061        let lean = generated_lean_file(&out);
4062
4063        assert!(lean.contains("private def fibSpec__nat : Nat -> Int"));
4064        assert!(!lean.contains("partial def fibSpec"));
4065        assert!(lean.contains("private theorem fib_eq_fibSpec__worker_nat_shift"));
4066        assert!(lean.contains("private theorem fib_eq_fibSpec__helper_nat"));
4067        assert!(lean.contains("private theorem fib_eq_fibSpec__helper_seed"));
4068        assert!(lean.contains("theorem fib_eq_fibSpec : ∀ (n : Int), fib n = fibSpec n := by"));
4069        assert!(!lean.contains(
4070            "-- universal theorem fib_eq_fibSpec omitted: sampled law shape is not auto-proved yet"
4071        ));
4072    }
4073
4074    #[test]
4075    fn pell_like_example_auto_proves_same_general_shape() {
4076        let mut ctx = ctx_from_source(
4077            r#"
4078module Pell
4079    intent =
4080        "linear recurrence probe"
4081
4082fn pellTR(n: Int, a: Int, b: Int) -> Int
4083    match n
4084        0 -> a
4085        _ -> pellTR(n - 1, b, a + 2 * b)
4086
4087fn pell(n: Int) -> Int
4088    match n < 0
4089        true -> 0
4090        false -> pellTR(n, 0, 1)
4091
4092fn pellSpec(n: Int) -> Int
4093    match n < 0
4094        true -> 0
4095        false -> match n
4096            0 -> 0
4097            1 -> 1
4098            _ -> pellSpec(n - 2) + 2 * pellSpec(n - 1)
4099
4100verify pell law pellSpec
4101    given n: Int = [0, 1, 2, 3]
4102    pell(n) => pellSpec(n)
4103"#,
4104            "pell",
4105        );
4106        ctx.refresh_facts();
4107        let issues = proof_mode_issues(&ctx);
4108        assert!(
4109            issues.is_empty(),
4110            "expected pell example to stay inside proof subset, got: {:?}",
4111            issues
4112        );
4113
4114        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4115        let lean = generated_lean_file(&out);
4116        assert!(lean.contains("private def pellSpec__nat : Nat -> Int"));
4117        assert!(lean.contains("private theorem pell_eq_pellSpec__worker_nat_shift"));
4118        assert!(lean.contains("theorem pell_eq_pellSpec : ∀ (n : Int), pell n = pellSpec n := by"));
4119        assert!(!lean.contains(
4120            "-- universal theorem pell_eq_pellSpec omitted: sampled law shape is not auto-proved yet"
4121        ));
4122    }
4123
4124    #[test]
4125    fn nonlinear_pair_state_recurrence_is_not_auto_proved_as_linear_shape() {
4126        let mut ctx = ctx_from_source(
4127            r#"
4128module WeirdRec
4129    intent =
4130        "reject nonlinear pair-state recurrence from linear recurrence prover"
4131
4132fn weirdTR(n: Int, a: Int, b: Int) -> Int
4133    match n
4134        0 -> a
4135        _ -> weirdTR(n - 1, b, a * b)
4136
4137fn weird(n: Int) -> Int
4138    match n < 0
4139        true -> 0
4140        false -> weirdTR(n, 0, 1)
4141
4142fn weirdSpec(n: Int) -> Int
4143    match n < 0
4144        true -> 0
4145        false -> match n
4146            0 -> 0
4147            1 -> 1
4148            _ -> weirdSpec(n - 1) * weirdSpec(n - 2)
4149
4150verify weird law weirdSpec
4151    given n: Int = [0, 1, 2, 3]
4152    weird(n) => weirdSpec(n)
4153"#,
4154            "weirdrec",
4155        );
4156        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4157        let lean = generated_lean_file(&out);
4158
4159        // After codegen change: emit sorry instead of omitting universal theorems
4160        assert!(lean.contains("sorry"));
4161        assert!(!lean.contains("private theorem weird_eq_weirdSpec__worker_nat_shift"));
4162        assert!(lean.contains("theorem weird_eq_weirdSpec_sample_1 :"));
4163    }
4164
4165    #[test]
4166    fn date_example_stays_inside_proof_subset() {
4167        let mut ctx = ctx_from_source(include_str!("../../../examples/data/date.av"), "date");
4168        ctx.refresh_facts();
4169        let issues = proof_mode_issues(&ctx);
4170        assert!(
4171            issues.is_empty(),
4172            "expected date example to stay inside proof subset, got: {:?}",
4173            issues
4174        );
4175
4176        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4177        let lean = generated_lean_file(&out);
4178        assert!(!lean.contains("partial def"));
4179        assert!(lean.contains("def parseIntSlice (s : String) (from' : Int) (to : Int) : Int :="));
4180    }
4181
4182    #[test]
4183    fn temperature_example_stays_inside_proof_subset() {
4184        let mut ctx = ctx_from_source(
4185            include_str!("../../../examples/core/temperature.av"),
4186            "temperature",
4187        );
4188        ctx.refresh_facts();
4189        let issues = proof_mode_issues(&ctx);
4190        assert!(
4191            issues.is_empty(),
4192            "expected temperature example to stay inside proof subset, got: {:?}",
4193            issues
4194        );
4195
4196        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4197        let lean = generated_lean_file(&out);
4198        assert!(!lean.contains("partial def"));
4199        assert!(
4200            lean.contains("example : celsiusToFahr 0.0 = 32.0 := by native_decide"),
4201            "expected verify examples to survive proof export, got:\n{}",
4202            lean
4203        );
4204    }
4205
4206    #[test]
4207    fn quicksort_example_stays_inside_proof_subset() {
4208        let mut ctx = ctx_from_source(
4209            include_str!("../../../examples/data/quicksort.av"),
4210            "quicksort",
4211        );
4212        ctx.refresh_facts();
4213        let issues = proof_mode_issues(&ctx);
4214        assert!(
4215            issues.is_empty(),
4216            "expected quicksort example to stay inside proof subset, got: {:?}",
4217            issues
4218        );
4219
4220        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4221        let lean = generated_lean_file(&out);
4222        assert!(lean.contains("def isOrderedFrom"));
4223        assert!(!lean.contains("partial def isOrderedFrom"));
4224        assert!(lean.contains("termination_by xs.length"));
4225    }
4226
4227    #[test]
4228    fn grok_s_language_example_uses_total_ranked_sizeof_mutual_recursion() {
4229        let mut ctx = ctx_from_source(
4230            include_str!("../../../examples/core/grok_s_language.av"),
4231            "grok_s_language",
4232        );
4233        ctx.refresh_facts();
4234        let issues = proof_mode_issues(&ctx);
4235        assert!(
4236            issues.is_empty(),
4237            "expected grok_s_language example to stay inside proof subset, got: {:?}",
4238            issues
4239        );
4240
4241        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4242        let lean = generated_lean_file(&out);
4243        assert!(lean.contains("mutual"));
4244        assert!(lean.contains("def eval__fuel"));
4245        assert!(lean.contains("def parseListItems__fuel"));
4246        assert!(!lean.contains("partial def eval"));
4247        assert!(!lean.contains("termination_by (sizeOf e,"));
4248        assert!(lean.contains("-- when validSymbolNames e"));
4249        assert!(!lean.contains("private theorem toString'_law_parseRoundtrip_aux"));
4250        assert!(lean.contains(
4251            "theorem toString'_law_parseRoundtrip : ∀ (e : Sexpr), e = Sexpr.atomNum 42 ∨"
4252        ));
4253        assert!(
4254            lean.contains("validSymbolNames e = true -> parse (toString' e) = Except.ok e := by")
4255        );
4256        assert!(lean.contains("theorem toString'_law_parseSexprRoundtrip :"));
4257        assert!(lean.contains("theorem toString'_law_parseRoundtrip_sample_1 :"));
4258    }
4259
4260    #[test]
4261    fn lambda_example_keeps_only_eval_outside_proof_subset() {
4262        let mut ctx = ctx_from_source(include_str!("../../../examples/core/lambda.av"), "lambda");
4263        ctx.refresh_facts();
4264        let issues = proof_mode_issues(&ctx);
4265        assert_eq!(
4266            issues,
4267            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()]
4268        );
4269
4270        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4271        let lean = generated_lean_file(&out);
4272        assert!(lean.contains("def termToString__fuel"));
4273        assert!(lean.contains("def subst__fuel"));
4274        assert!(lean.contains("def countS__fuel"));
4275        assert!(!lean.contains("partial def termToString"));
4276        assert!(!lean.contains("partial def subst"));
4277        assert!(!lean.contains("partial def countS"));
4278        assert!(lean.contains("partial def eval"));
4279    }
4280
4281    #[test]
4282    fn mission_control_example_stays_inside_proof_subset() {
4283        let mut ctx = ctx_from_source(
4284            include_str!("../../../examples/apps/mission_control.av"),
4285            "mission_control",
4286        );
4287        ctx.refresh_facts();
4288        let issues = proof_mode_issues(&ctx);
4289        assert!(
4290            issues.is_empty(),
4291            "expected mission_control example to stay inside proof subset, got: {:?}",
4292            issues
4293        );
4294
4295        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4296        let lean = generated_lean_file(&out);
4297        assert!(!lean.contains("partial def normalizeAngle"));
4298        assert!(lean.contains("def normalizeAngle__fuel"));
4299    }
4300
4301    #[test]
4302    fn notepad_store_example_stays_inside_proof_subset() {
4303        let mut ctx = ctx_from_source(
4304            include_str!("../../../examples/apps/notepad/store.av"),
4305            "notepad_store",
4306        );
4307        ctx.refresh_facts();
4308        let issues = proof_mode_issues(&ctx);
4309        assert!(
4310            issues.is_empty(),
4311            "expected notepad/store example to stay inside proof subset, got: {:?}",
4312            issues
4313        );
4314
4315        let out = transpile_for_proof_mode(&mut ctx, VerifyEmitMode::NativeDecide);
4316        let lean = generated_lean_file(&out);
4317        assert!(lean.contains("def deserializeLine (line : String) : Except String Note :="));
4318        assert!(lean.contains("Except String (List Note)"));
4319        assert!(!lean.contains("partial def deserializeLine"));
4320        assert!(lean.contains("-- when noteRoundtripSafe note"));
4321        assert!(lean.contains("-- when notesRoundtripSafe notes"));
4322        assert!(lean.contains(
4323            "theorem serializeLine_law_lineRoundtrip : ∀ (note : Note), note = { id' := 1, title := \"Hello\", body := \"World\" : Note } ∨"
4324        ));
4325        assert!(lean.contains(
4326            "theorem serializeLines_law_notesRoundtrip : ∀ (notes : List Note), notes = [] ∨"
4327        ));
4328        assert!(lean.contains("notesRoundtripSafe notes = true ->"));
4329        assert!(lean.contains("parseNotes (s!\"{String.intercalate \"\\n\" (serializeLines notes)}\\n\") = Except.ok notes"));
4330        assert!(lean.contains("theorem serializeLine_law_lineRoundtrip_sample_1 :"));
4331        assert!(lean.contains("theorem serializeLines_law_notesRoundtrip_sample_1 :"));
4332    }
4333}