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