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