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