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