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