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