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