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