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