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