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::{BinOp, Expr, FnBody, FnDef, MatchArm, Pattern, Stmt, TopLevel, VerifyKind};
17use crate::call_graph;
18use crate::codegen::{CodegenContext, ProjectOutput};
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum VerifyEmitMode {
23 NativeDecide,
25 Sorry,
27 TheoremSkeleton,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum RecursionPlan {
36 IntCountdown,
38 ListStructural,
40 StringPosAdvance,
43 MutualIntCountdown,
45 MutualStringPosAdvance { rank: usize },
48 MutualSizeOfRanked {
51 rank: usize,
52 metric_param_index: usize,
53 },
54}
55
56const LEAN_PRELUDE_HEADER: &str = r#"-- Generated by the Aver → Lean 4 transpiler
57-- Pure core logic only (effectful functions are omitted)
58
59-- Prelude: helper definitions for Aver builtins"#;
60
61const LEAN_PRELUDE_FLOAT_COE: &str = r#"instance : Coe Int Float := ⟨fun n => Float.ofInt n⟩"#;
62
63const LEAN_PRELUDE_FLOAT_DEC_EQ: &str = r#"private unsafe def Float.unsafeDecEq (a b : Float) : Decidable (a = b) :=
64 if a == b then isTrue (unsafeCast ()) else isFalse (unsafeCast ())
65@[implemented_by Float.unsafeDecEq]
66private opaque Float.compDecEq (a b : Float) : Decidable (a = b)
67instance : DecidableEq Float := Float.compDecEq"#;
68
69const LEAN_PRELUDE_EXCEPT_DEC_EQ: &str = r#"instance [DecidableEq ε] [DecidableEq α] : DecidableEq (Except ε α)
70 | .ok a, .ok b =>
71 if h : a = b then isTrue (h ▸ rfl) else isFalse (by intro h'; cases h'; exact h rfl)
72 | .error a, .error b =>
73 if h : a = b then isTrue (h ▸ rfl) else isFalse (by intro h'; cases h'; exact h rfl)
74 | .ok _, .error _ => isFalse (by intro h; cases h)
75 | .error _, .ok _ => isFalse (by intro h; cases h)"#;
76
77const LEAN_PRELUDE_EXCEPT_NS: &str = r#"namespace Except
78def withDefault (r : Except ε α) (d : α) : α :=
79 match r with
80 | .ok v => v
81 | .error _ => d
82end Except"#;
83
84const LEAN_PRELUDE_OPTION_TO_EXCEPT: &str = r#"def Option.toExcept (o : Option α) (e : ε) : Except ε α :=
85 match o with
86 | some v => .ok v
87 | none => .error e"#;
88
89const LEAN_PRELUDE_STRING_HADD: &str = r#"instance : HAdd String String String := ⟨String.append⟩"#;
90
91const AVER_MAP_PRELUDE: &str = r#"namespace AverMap
92def empty : List (α × β) := []
93def get [BEq α] (m : List (α × β)) (k : α) : Option β :=
94 match m with
95 | [] => none
96 | (k', v) :: rest => if k == k' then some v else AverMap.get rest k
97def set [BEq α] (m : List (α × β)) (k : α) (v : β) : List (α × β) :=
98 let rec go : List (α × β) → List (α × β)
99 | [] => [(k, v)]
100 | (k', v') :: rest => if k == k' then (k, v) :: rest else (k', v') :: go rest
101 go m
102def has [BEq α] (m : List (α × β)) (k : α) : Bool :=
103 m.any (fun p => p.1 == k)
104def remove [BEq α] (m : List (α × β)) (k : α) : List (α × β) :=
105 m.filter (fun p => !(p.1 == k))
106def keys (m : List (α × β)) : List α := m.map Prod.fst
107def values (m : List (α × β)) : List β := m.map Prod.snd
108def entries (m : List (α × β)) : List (α × β) := m
109def len (m : List (α × β)) : Nat := m.length
110def fromList (entries : List (α × β)) : List (α × β) := entries
111
112private theorem any_set_go_self [BEq α] (k : α) (v : β) :
113 ∀ (m : List (α × β)), List.any (AverMap.set.go k v m) (fun p => p.1 == k) = true := by
114 intro m
115 induction m with
116 | nil =>
117 simp [AverMap.set.go, List.any, beq_self_eq_true]
118 | cons p tl ih =>
119 simp only [AverMap.set.go]
120 split <;> simp_all [List.any, beq_self_eq_true]
121
122theorem has_set_self [BEq α] (m : List (α × β)) (k : α) (v : β) :
123 AverMap.has (AverMap.set m k v) k = true := by
124 simp [AverMap.has, AverMap.set]
125 exact any_set_go_self k v m
126
127private theorem get_set_go_self [BEq α] (k : α) (v : β) :
128 ∀ (m : List (α × β)), AverMap.get (AverMap.set.go k v m) k = some v := by
129 intro m
130 induction m with
131 | nil =>
132 simp [AverMap.set.go, AverMap.get, beq_self_eq_true]
133 | cons p tl ih =>
134 simp only [AverMap.set.go]
135 split
136 · simp [AverMap.get, beq_self_eq_true]
137 · simp [AverMap.get, beq_self_eq_true, ih]
138
139theorem get_set_self [BEq α] (m : List (α × β)) (k : α) (v : β) :
140 AverMap.get (AverMap.set m k v) k = some v := by
141 simp [AverMap.set]
142 exact get_set_go_self k v m
143end AverMap"#;
144
145const LEAN_PRELUDE_AVER_LIST: &str = r#"namespace AverList
146private def insertSorted [Ord α] (x : α) : List α → List α
147 | [] => [x]
148 | y :: ys =>
149 if compare x y == Ordering.lt || compare x y == Ordering.eq then
150 x :: y :: ys
151 else
152 y :: insertSorted x ys
153def sort [Ord α] (xs : List α) : List α :=
154 xs.foldl (fun acc x => insertSorted x acc) []
155end AverList"#;
156
157const LEAN_PRELUDE_STRING_HELPERS: &str = r#"def String.charAt (s : String) (i : Int) : Option String :=
158 if i < 0 then none
159 else (s.toList.get? i.toNat).map Char.toString
160def String.slice (s : String) (start stop : Int) : String :=
161 let startN := if start < 0 then 0 else start.toNat
162 let stopN := if stop < 0 then 0 else stop.toNat
163 let chars := s.toList
164 String.mk ((chars.drop startN).take (stopN - startN))
165private def trimFloatTrailingZerosChars (chars : List Char) : List Char :=
166 let noZeros := (chars.reverse.dropWhile (fun c => c == '0')).reverse
167 match noZeros.reverse with
168 | '.' :: rest => rest.reverse
169 | _ => noZeros
170private def normalizeFloatString (s : String) : String :=
171 if s.toList.any (fun c => c == '.') then
172 String.mk (trimFloatTrailingZerosChars s.toList)
173 else s
174def String.fromInt (n : Int) : String := toString n
175def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)
176def String.chars (s : String) : List String := s.toList.map Char.toString
177namespace AverString
178def split (s delim : String) : List String :=
179 if delim.isEmpty then
180 "" :: (s.toList.map Char.toString) ++ [""]
181 else
182 s.splitOn delim
183end AverString"#;
184
185const LEAN_PRELUDE_NUMERIC_PARSE: &str = r#"def Int.fromString (s : String) : Except String Int :=
186 match s.toInt? with
187 | some n => .ok n
188 | none => .error ("Invalid integer: " ++ s)
189
190private def charDigitsToNat (cs : List Char) : Nat :=
191 cs.foldl (fun acc c => acc * 10 + (c.toNat - '0'.toNat)) 0
192
193private def parseExpPart : List Char → (Bool × List Char)
194 | '-' :: rest => (true, rest.takeWhile Char.isDigit)
195 | '+' :: rest => (false, rest.takeWhile Char.isDigit)
196 | rest => (false, rest.takeWhile Char.isDigit)
197
198def Float.fromString (s : String) : Except String Float :=
199 let chars := s.toList
200 let (neg, chars) := match chars with
201 | '-' :: rest => (true, rest)
202 | _ => (false, chars)
203 let intPart := chars.takeWhile Char.isDigit
204 let rest := chars.dropWhile Char.isDigit
205 let (fracPart, rest) := match rest with
206 | '.' :: rest => (rest.takeWhile Char.isDigit, rest.dropWhile Char.isDigit)
207 | _ => ([], rest)
208 let (expNeg, expDigits) := match rest with
209 | 'e' :: rest => parseExpPart rest
210 | 'E' :: rest => parseExpPart rest
211 | _ => (false, [])
212 if intPart.isEmpty && fracPart.isEmpty then .error ("Invalid float: " ++ s)
213 else
214 let mantissa := charDigitsToNat (intPart ++ fracPart)
215 let fracLen : Int := fracPart.length
216 let expVal : Int := charDigitsToNat expDigits
217 let shift : Int := (if expNeg then -expVal else expVal) - fracLen
218 let f := if shift >= 0 then Float.ofScientific mantissa false shift.toNat
219 else Float.ofScientific mantissa true ((-shift).toNat)
220 .ok (if neg then -f else f)"#;
221
222const LEAN_PRELUDE_CHAR_BYTE: &str = r#"def Char.toCode (s : String) : Int :=
223 match s.toList.head? with
224 | some c => (c.toNat : Int)
225 | none => panic! "Char.toCode: string is empty"
226def Char.fromCode (n : Int) : Option String :=
227 if n < 0 || n > 1114111 then none
228 else if n >= 55296 && n <= 57343 then none
229 else some (Char.toString (Char.ofNat n.toNat))
230
231def hexDigit (n : Int) : String :=
232 match n with
233 | 0 => "0" | 1 => "1" | 2 => "2" | 3 => "3"
234 | 4 => "4" | 5 => "5" | 6 => "6" | 7 => "7"
235 | 8 => "8" | 9 => "9" | 10 => "a" | 11 => "b"
236 | 12 => "c" | 13 => "d" | 14 => "e" | 15 => "f"
237 | _ => "?"
238
239def byteToHex (code : Int) : String :=
240 hexDigit (code / 16) ++ hexDigit (code % 16)
241
242namespace AverByte
243private def hexValue (c : Char) : Option Int :=
244 match c with
245 | '0' => some 0 | '1' => some 1 | '2' => some 2 | '3' => some 3
246 | '4' => some 4 | '5' => some 5 | '6' => some 6 | '7' => some 7
247 | '8' => some 8 | '9' => some 9 | 'a' => some 10 | 'b' => some 11
248 | 'c' => some 12 | 'd' => some 13 | 'e' => some 14 | 'f' => some 15
249 | 'A' => some 10 | 'B' => some 11 | 'C' => some 12 | 'D' => some 13
250 | 'E' => some 14 | 'F' => some 15
251 | _ => none
252def toHex (n : Int) : Except String String :=
253 if n < 0 || n > 255 then
254 .error ("Byte.toHex: " ++ toString n ++ " is out of range 0-255")
255 else
256 .ok (byteToHex n)
257def fromHex (s : String) : Except String Int :=
258 match s.toList with
259 | [hi, lo] =>
260 match hexValue hi, hexValue lo with
261 | some h, some l => .ok (h * 16 + l)
262 | _, _ => .error ("Byte.fromHex: invalid hex '" ++ s ++ "'")
263 | _ => .error ("Byte.fromHex: expected exactly 2 hex chars, got '" ++ s ++ "'")
264end AverByte"#;
265
266fn pure_fns(ctx: &CodegenContext) -> Vec<&FnDef> {
267 ctx.modules
268 .iter()
269 .flat_map(|m| m.fn_defs.iter())
270 .chain(ctx.fn_defs.iter())
271 .filter(|fd| toplevel::is_pure_fn(fd))
272 .collect()
273}
274
275fn recursive_pure_fn_names(ctx: &CodegenContext) -> HashSet<String> {
276 let pure_names: HashSet<String> = pure_fns(ctx)
277 .into_iter()
278 .map(|fd| fd.name.clone())
279 .collect();
280 let mut callgraph_items = ctx.items.clone();
281 for module in &ctx.modules {
282 for fd in &module.fn_defs {
283 callgraph_items.push(TopLevel::FnDef(fd.clone()));
284 }
285 }
286 call_graph::find_recursive_fns(&callgraph_items)
287 .into_iter()
288 .filter(|name| pure_names.contains(name))
289 .collect()
290}
291
292fn verify_counter_key(vb: &crate::ast::VerifyBlock) -> String {
293 match &vb.kind {
294 VerifyKind::Cases => format!("fn:{}", vb.fn_name),
295 VerifyKind::Law(law) => format!("law:{}::{}", vb.fn_name, law.name),
296 }
297}
298
299fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
300 match expr {
301 Expr::Ident(name) => Some(name.clone()),
302 Expr::Attr(obj, field) => expr_to_dotted_name(obj).map(|p| format!("{}.{}", p, field)),
303 _ => None,
304 }
305}
306
307fn call_matches(name: &str, target: &str) -> bool {
308 name == target || name.rsplit('.').next() == Some(target)
309}
310
311fn call_is_in_set(name: &str, targets: &HashSet<String>) -> bool {
312 call_matches_any(name, targets)
313}
314
315fn canonical_callee_name(name: &str, targets: &HashSet<String>) -> Option<String> {
316 if targets.contains(name) {
317 return Some(name.to_string());
318 }
319 name.rsplit('.')
320 .next()
321 .filter(|last| targets.contains(*last))
322 .map(ToString::to_string)
323}
324
325fn call_matches_any(name: &str, targets: &HashSet<String>) -> bool {
326 if targets.contains(name) {
327 return true;
328 }
329 match name.rsplit('.').next() {
330 Some(last) => targets.contains(last),
331 None => false,
332 }
333}
334
335fn is_int_minus_one(expr: &Expr, param_name: &str) -> bool {
336 match expr {
337 Expr::BinOp(BinOp::Sub, left, right) => {
338 matches!(left.as_ref(), Expr::Ident(id) if id == param_name)
339 && matches!(right.as_ref(), Expr::Literal(crate::ast::Literal::Int(1)))
340 }
341 Expr::FnCall(callee, args) => {
342 let Some(name) = expr_to_dotted_name(callee) else {
343 return false;
344 };
345 (name == "Int.sub" || name == "int.sub")
346 && args.len() == 2
347 && matches!(&args[0], Expr::Ident(id) if id == param_name)
348 && matches!(&args[1], Expr::Literal(crate::ast::Literal::Int(1)))
349 }
350 _ => false,
351 }
352}
353
354fn collect_calls_from_expr<'a>(expr: &'a Expr, out: &mut Vec<(String, Vec<&'a Expr>)>) {
355 match expr {
356 Expr::FnCall(callee, args) => {
357 if let Some(name) = expr_to_dotted_name(callee) {
358 out.push((name, args.iter().collect()));
359 }
360 collect_calls_from_expr(callee, out);
361 for arg in args {
362 collect_calls_from_expr(arg, out);
363 }
364 }
365 Expr::TailCall(boxed) => {
366 let (name, args) = boxed.as_ref();
367 out.push((name.clone(), args.iter().collect()));
368 for arg in args {
369 collect_calls_from_expr(arg, out);
370 }
371 }
372 Expr::Attr(obj, _) => collect_calls_from_expr(obj, out),
373 Expr::BinOp(_, left, right) => {
374 collect_calls_from_expr(left, out);
375 collect_calls_from_expr(right, out);
376 }
377 Expr::Match { subject, arms, .. } => {
378 collect_calls_from_expr(subject, out);
379 for arm in arms {
380 collect_calls_from_expr(&arm.body, out);
381 }
382 }
383 Expr::Constructor(_, inner) => {
384 if let Some(inner) = inner {
385 collect_calls_from_expr(inner, out);
386 }
387 }
388 Expr::ErrorProp(inner) => collect_calls_from_expr(inner, out),
389 Expr::InterpolatedStr(parts) => {
390 for p in parts {
391 if let crate::ast::StrPart::Parsed(e) = p {
392 collect_calls_from_expr(e, out);
393 }
394 }
395 }
396 Expr::List(items) | Expr::Tuple(items) => {
397 for item in items {
398 collect_calls_from_expr(item, out);
399 }
400 }
401 Expr::MapLiteral(entries) => {
402 for (k, v) in entries {
403 collect_calls_from_expr(k, out);
404 collect_calls_from_expr(v, out);
405 }
406 }
407 Expr::RecordCreate { fields, .. } => {
408 for (_, v) in fields {
409 collect_calls_from_expr(v, out);
410 }
411 }
412 Expr::RecordUpdate { base, updates, .. } => {
413 collect_calls_from_expr(base, out);
414 for (_, v) in updates {
415 collect_calls_from_expr(v, out);
416 }
417 }
418 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
419 }
420}
421
422fn collect_calls_from_body(body: &FnBody) -> Vec<(String, Vec<&Expr>)> {
423 let mut out = Vec::new();
424 match body {
425 FnBody::Expr(expr) => collect_calls_from_expr(expr, &mut out),
426 FnBody::Block(stmts) => {
427 for stmt in stmts {
428 match stmt {
429 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
430 collect_calls_from_expr(expr, &mut out)
431 }
432 }
433 }
434 }
435 }
436 out
437}
438
439fn collect_list_tail_binders_from_expr(
440 expr: &Expr,
441 list_param_name: &str,
442 tails: &mut HashSet<String>,
443) {
444 match expr {
445 Expr::Match { subject, arms, .. } => {
446 if matches!(subject.as_ref(), Expr::Ident(id) if id == list_param_name) {
447 for MatchArm { pattern, .. } in arms {
448 if let Pattern::Cons(_, tail) = pattern {
449 tails.insert(tail.clone());
450 }
451 }
452 }
453 for arm in arms {
454 collect_list_tail_binders_from_expr(&arm.body, list_param_name, tails);
455 }
456 collect_list_tail_binders_from_expr(subject, list_param_name, tails);
457 }
458 Expr::FnCall(callee, args) => {
459 collect_list_tail_binders_from_expr(callee, list_param_name, tails);
460 for arg in args {
461 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
462 }
463 }
464 Expr::TailCall(boxed) => {
465 let (_, args) = boxed.as_ref();
466 for arg in args {
467 collect_list_tail_binders_from_expr(arg, list_param_name, tails);
468 }
469 }
470 Expr::Attr(obj, _) => collect_list_tail_binders_from_expr(obj, list_param_name, tails),
471 Expr::BinOp(_, left, right) => {
472 collect_list_tail_binders_from_expr(left, list_param_name, tails);
473 collect_list_tail_binders_from_expr(right, list_param_name, tails);
474 }
475 Expr::Constructor(_, inner) => {
476 if let Some(inner) = inner {
477 collect_list_tail_binders_from_expr(inner, list_param_name, tails);
478 }
479 }
480 Expr::ErrorProp(inner) => {
481 collect_list_tail_binders_from_expr(inner, list_param_name, tails)
482 }
483 Expr::InterpolatedStr(parts) => {
484 for p in parts {
485 if let crate::ast::StrPart::Parsed(e) = p {
486 collect_list_tail_binders_from_expr(e, list_param_name, tails);
487 }
488 }
489 }
490 Expr::List(items) | Expr::Tuple(items) => {
491 for item in items {
492 collect_list_tail_binders_from_expr(item, list_param_name, tails);
493 }
494 }
495 Expr::MapLiteral(entries) => {
496 for (k, v) in entries {
497 collect_list_tail_binders_from_expr(k, list_param_name, tails);
498 collect_list_tail_binders_from_expr(v, list_param_name, tails);
499 }
500 }
501 Expr::RecordCreate { fields, .. } => {
502 for (_, v) in fields {
503 collect_list_tail_binders_from_expr(v, list_param_name, tails);
504 }
505 }
506 Expr::RecordUpdate { base, updates, .. } => {
507 collect_list_tail_binders_from_expr(base, list_param_name, tails);
508 for (_, v) in updates {
509 collect_list_tail_binders_from_expr(v, list_param_name, tails);
510 }
511 }
512 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
513 }
514}
515
516fn collect_list_tail_binders(fd: &FnDef, list_param_name: &str) -> HashSet<String> {
517 let mut tails = HashSet::new();
518 match fd.body.as_ref() {
519 FnBody::Expr(expr) => {
520 collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
521 }
522 FnBody::Block(stmts) => {
523 for stmt in stmts {
524 match stmt {
525 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
526 collect_list_tail_binders_from_expr(expr, list_param_name, &mut tails)
527 }
528 }
529 }
530 }
531 }
532 tails
533}
534
535fn supports_single_int_countdown(fd: &FnDef) -> bool {
536 let Some((param_name, param_ty)) = fd.params.first() else {
537 return false;
538 };
539 if param_ty != "Int" {
540 return false;
541 }
542 let recursive_calls: Vec<Option<&Expr>> = collect_calls_from_body(fd.body.as_ref())
543 .into_iter()
544 .filter(|(name, _)| call_matches(name, &fd.name))
545 .map(|(_, args)| args.first().copied())
546 .collect();
547 if recursive_calls.is_empty() {
548 return false;
549 }
550 recursive_calls
551 .into_iter()
552 .all(|arg0| arg0.is_some_and(|e| is_int_minus_one(e, param_name)))
553}
554
555fn supports_single_list_structural(fd: &FnDef) -> bool {
556 let Some((param_name, param_ty)) = fd.params.first() else {
557 return false;
558 };
559 if !(param_ty.starts_with("List<") || param_ty == "List") {
560 return false;
561 }
562
563 let tails = collect_list_tail_binders(fd, param_name);
564 if tails.is_empty() {
565 return false;
566 }
567
568 let recursive_calls: Vec<Option<&Expr>> = collect_calls_from_body(fd.body.as_ref())
569 .into_iter()
570 .filter(|(name, _)| call_matches(name, &fd.name))
571 .map(|(_, args)| args.first().copied())
572 .collect();
573 if recursive_calls.is_empty() {
574 return false;
575 }
576
577 recursive_calls.into_iter().all(|arg0| {
578 matches!(
579 arg0,
580 Some(Expr::Ident(id)) if tails.contains(id)
581 )
582 })
583}
584
585fn is_ident(expr: &Expr, name: &str) -> bool {
586 matches!(expr, Expr::Ident(id) if id == name)
587}
588
589fn is_int_plus_positive(expr: &Expr, param_name: &str) -> bool {
590 match expr {
591 Expr::BinOp(BinOp::Add, left, right) => {
592 matches!(left.as_ref(), Expr::Ident(id) if id == param_name)
593 && matches!(right.as_ref(), Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
594 }
595 Expr::FnCall(callee, args) => {
596 let Some(name) = expr_to_dotted_name(callee) else {
597 return false;
598 };
599 (name == "Int.add" || name == "int.add")
600 && args.len() == 2
601 && matches!(&args[0], Expr::Ident(id) if id == param_name)
602 && matches!(&args[1], Expr::Literal(crate::ast::Literal::Int(n)) if *n >= 1)
603 }
604 _ => false,
605 }
606}
607
608fn is_skip_ws_advance(expr: &Expr, string_param: &str, pos_param: &str) -> bool {
609 let Expr::FnCall(callee, args) = expr else {
610 return false;
611 };
612 let Some(name) = expr_to_dotted_name(callee) else {
613 return false;
614 };
615 if !call_matches(&name, "skipWs") || args.len() != 2 {
616 return false;
617 }
618 is_ident(&args[0], string_param) && is_int_plus_positive(&args[1], pos_param)
619}
620
621fn is_skip_ws_same(expr: &Expr, string_param: &str, pos_param: &str) -> bool {
622 let Expr::FnCall(callee, args) = expr else {
623 return false;
624 };
625 let Some(name) = expr_to_dotted_name(callee) else {
626 return false;
627 };
628 if !call_matches(&name, "skipWs") || args.len() != 2 {
629 return false;
630 }
631 is_ident(&args[0], string_param) && is_ident(&args[1], pos_param)
632}
633
634fn is_string_pos_advance(expr: &Expr, string_param: &str, pos_param: &str) -> bool {
635 is_int_plus_positive(expr, pos_param) || is_skip_ws_advance(expr, string_param, pos_param)
636}
637
638#[derive(Clone, Copy, Debug, Eq, PartialEq)]
639enum StringPosEdge {
640 Same,
641 Advance,
642}
643
644fn classify_string_pos_edge(
645 expr: &Expr,
646 string_param: &str,
647 pos_param: &str,
648) -> Option<StringPosEdge> {
649 if is_ident(expr, pos_param) || is_skip_ws_same(expr, string_param, pos_param) {
650 return Some(StringPosEdge::Same);
651 }
652 if is_string_pos_advance(expr, string_param, pos_param) {
653 return Some(StringPosEdge::Advance);
654 }
655 if let Expr::FnCall(callee, args) = expr {
656 let name = expr_to_dotted_name(callee)?;
657 if call_matches(&name, "skipWs")
658 && args.len() == 2
659 && is_ident(&args[0], string_param)
660 && matches!(&args[1], Expr::Ident(id) if id != pos_param)
661 {
662 return Some(StringPosEdge::Advance);
663 }
664 }
665 if matches!(expr, Expr::Ident(id) if id != pos_param) {
666 return Some(StringPosEdge::Advance);
667 }
668 None
669}
670
671fn ranks_from_same_edges(
672 names: &HashSet<String>,
673 same_edges: &HashMap<String, HashSet<String>>,
674) -> Option<HashMap<String, usize>> {
675 let mut indegree: HashMap<String, usize> = names.iter().map(|n| (n.clone(), 0)).collect();
676 for outs in same_edges.values() {
677 for to in outs {
678 if let Some(entry) = indegree.get_mut(to) {
679 *entry += 1;
680 } else {
681 return None;
682 }
683 }
684 }
685
686 let mut queue: Vec<String> = indegree
687 .iter()
688 .filter_map(|(name, °)| (deg == 0).then_some(name.clone()))
689 .collect();
690 queue.sort();
691 let mut topo = Vec::new();
692 while let Some(node) = queue.pop() {
693 topo.push(node.clone());
694 let outs = same_edges.get(&node).cloned().unwrap_or_default();
695 let mut newly_zero = Vec::new();
696 for to in outs {
697 if let Some(entry) = indegree.get_mut(&to) {
698 *entry -= 1;
699 if *entry == 0 {
700 newly_zero.push(to);
701 }
702 } else {
703 return None;
704 }
705 }
706 newly_zero.sort();
707 queue.extend(newly_zero);
708 }
709
710 if topo.len() != names.len() {
711 return None;
712 }
713
714 let n = topo.len();
715 let mut ranks = HashMap::new();
716 for (idx, name) in topo.into_iter().enumerate() {
717 ranks.insert(name, n - idx);
718 }
719 Some(ranks)
720}
721
722fn supports_single_string_pos_advance(fd: &FnDef) -> bool {
723 let Some((string_param, string_ty)) = fd.params.first() else {
724 return false;
725 };
726 let Some((pos_param, pos_ty)) = fd.params.get(1) else {
727 return false;
728 };
729 if string_ty != "String" || pos_ty != "Int" {
730 return false;
731 }
732
733 let recursive_calls: Vec<(Option<&Expr>, Option<&Expr>)> =
734 collect_calls_from_body(fd.body.as_ref())
735 .into_iter()
736 .filter(|(name, _)| call_matches(name, &fd.name))
737 .map(|(_, args)| (args.first().copied(), args.get(1).copied()))
738 .collect();
739 if recursive_calls.is_empty() {
740 return false;
741 }
742
743 recursive_calls.into_iter().all(|(arg0, arg1)| {
744 arg0.is_some_and(|e| is_ident(e, string_param))
745 && arg1.is_some_and(|e| is_string_pos_advance(e, string_param, pos_param))
746 })
747}
748
749fn supports_mutual_int_countdown(component: &[&FnDef]) -> bool {
750 if component.len() < 2 {
751 return false;
752 }
753 if component
754 .iter()
755 .any(|fd| !matches!(fd.params.first(), Some((_, t)) if t == "Int"))
756 {
757 return false;
758 }
759 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
760 let mut any_intra = false;
761 for fd in component {
762 let param_name = &fd.params[0].0;
763 for (callee, args) in collect_calls_from_body(fd.body.as_ref()) {
764 if !call_is_in_set(&callee, &names) {
765 continue;
766 }
767 any_intra = true;
768 let Some(arg0) = args.first().copied() else {
769 return false;
770 };
771 if !is_int_minus_one(arg0, param_name) {
772 return false;
773 }
774 }
775 }
776 any_intra
777}
778
779fn supports_mutual_string_pos_advance(component: &[&FnDef]) -> Option<HashMap<String, usize>> {
780 if component.len() < 2 {
781 return None;
782 }
783 if component.iter().any(|fd| {
784 !matches!(fd.params.first(), Some((_, t)) if t == "String")
785 || !matches!(fd.params.get(1), Some((_, t)) if t == "Int")
786 }) {
787 return None;
788 }
789
790 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
791 let mut same_edges: HashMap<String, HashSet<String>> =
792 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
793 let mut any_intra = false;
794
795 for fd in component {
796 let string_param = &fd.params[0].0;
797 let pos_param = &fd.params[1].0;
798 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
799 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
800 continue;
801 };
802 any_intra = true;
803
804 let arg0 = args.first().copied()?;
805 let arg1 = args.get(1).copied()?;
806
807 if !is_ident(arg0, string_param) {
808 return None;
809 }
810
811 match classify_string_pos_edge(arg1, string_param, pos_param) {
812 Some(StringPosEdge::Same) => {
813 if let Some(edges) = same_edges.get_mut(&fd.name) {
814 edges.insert(callee);
815 } else {
816 return None;
817 }
818 }
819 Some(StringPosEdge::Advance) => {}
820 None => return None,
821 }
822 }
823 }
824
825 if !any_intra {
826 return None;
827 }
828
829 ranks_from_same_edges(&names, &same_edges)
830}
831
832fn metric_param_index_for_fn(fd: &FnDef) -> usize {
833 if fd.params.len() >= 2
834 && fd.params[0].1 == "String"
835 && (fd.params[1].1 == "Int"
836 || fd.params[1].1 == "List"
837 || fd.params[1].1.starts_with("List<"))
838 {
839 return 1;
840 }
841 0
842}
843
844fn supports_mutual_sizeof_ranked(component: &[&FnDef]) -> Option<HashMap<String, (usize, usize)>> {
845 if component.len() < 2 {
846 return None;
847 }
848 let names: HashSet<String> = component.iter().map(|fd| fd.name.clone()).collect();
849 let metric_idx: HashMap<String, usize> = component
850 .iter()
851 .map(|fd| (fd.name.clone(), metric_param_index_for_fn(fd)))
852 .collect();
853 if component.iter().any(|fd| {
854 let idx = metric_idx.get(&fd.name).copied().unwrap_or(usize::MAX);
855 idx >= fd.params.len()
856 }) {
857 return None;
858 }
859
860 let mut same_edges: HashMap<String, HashSet<String>> =
861 names.iter().map(|n| (n.clone(), HashSet::new())).collect();
862 let mut any_intra = false;
863 for fd in component {
864 let caller_metric_idx = metric_idx.get(&fd.name).copied()?;
865 let caller_metric_param = &fd.params[caller_metric_idx].0;
866 for (callee_raw, args) in collect_calls_from_body(fd.body.as_ref()) {
867 let Some(callee) = canonical_callee_name(&callee_raw, &names) else {
868 continue;
869 };
870 any_intra = true;
871 let callee_metric_idx = metric_idx.get(&callee).copied()?;
872 let metric_arg = args.get(callee_metric_idx).copied()?;
873 if is_ident(metric_arg, caller_metric_param) {
874 if let Some(edges) = same_edges.get_mut(&fd.name) {
875 edges.insert(callee);
876 } else {
877 return None;
878 }
879 }
880 }
881 }
882 if !any_intra {
883 return None;
884 }
885
886 let ranks = ranks_from_same_edges(&names, &same_edges)?;
887 let mut out = HashMap::new();
888 for fd in component {
889 let rank = ranks.get(&fd.name).copied()?;
890 let idx = metric_idx.get(&fd.name).copied()?;
891 out.insert(fd.name.clone(), (rank, idx));
892 }
893 Some(out)
894}
895
896fn proof_mode_recursion_analysis(
897 ctx: &CodegenContext,
898) -> (HashMap<String, RecursionPlan>, Vec<String>) {
899 let mut plans = HashMap::new();
900 let mut issues = Vec::new();
901
902 let all_pure = pure_fns(ctx);
903 let recursive_names = recursive_pure_fn_names(ctx);
904 let components = call_graph::ordered_fn_components(&all_pure);
905
906 for component in components {
907 if component.is_empty() {
908 continue;
909 }
910 let is_recursive_component =
911 component.len() > 1 || recursive_names.contains(&component[0].name);
912 if !is_recursive_component {
913 continue;
914 }
915
916 if component.len() > 1 {
917 if supports_mutual_int_countdown(&component) {
918 for fd in &component {
919 plans.insert(fd.name.clone(), RecursionPlan::MutualIntCountdown);
920 }
921 } else if let Some(ranks) = supports_mutual_string_pos_advance(&component) {
922 for fd in &component {
923 if let Some(rank) = ranks.get(&fd.name).copied() {
924 plans.insert(
925 fd.name.clone(),
926 RecursionPlan::MutualStringPosAdvance { rank },
927 );
928 } else {
929 issues.push(format!(
930 "internal proof-mode error: missing mutual String+pos rank for function '{}'",
931 fd.name
932 ));
933 }
934 }
935 } else if let Some(rank_idx) = supports_mutual_sizeof_ranked(&component) {
936 for fd in &component {
937 if let Some((rank, metric_param_index)) = rank_idx.get(&fd.name).copied() {
938 plans.insert(
939 fd.name.clone(),
940 RecursionPlan::MutualSizeOfRanked {
941 rank,
942 metric_param_index,
943 },
944 );
945 } else {
946 issues.push(format!(
947 "internal proof-mode error: missing mutual sizeOf plan for function '{}'",
948 fd.name
949 ));
950 }
951 }
952 } else {
953 let names = component
954 .iter()
955 .map(|fd| fd.name.clone())
956 .collect::<Vec<_>>()
957 .join(", ");
958 issues.push(format!(
959 "unsupported mutual recursion group (supported: Int countdown on first param, String+pos advance, or ranked-sizeOf mutual plan): {}",
960 names
961 ));
962 }
963 continue;
964 }
965
966 let fd = component[0];
967 if supports_single_int_countdown(fd) {
968 plans.insert(fd.name.clone(), RecursionPlan::IntCountdown);
969 } else if supports_single_list_structural(fd) {
970 plans.insert(fd.name.clone(), RecursionPlan::ListStructural);
971 } else if supports_single_string_pos_advance(fd) {
972 plans.insert(fd.name.clone(), RecursionPlan::StringPosAdvance);
973 } else {
974 issues.push(format!(
975 "recursive function '{}' is outside proof subset (supported: Int countdown, List structural, String+pos advance, or supported mutual recursion class)",
976 fd.name
977 ));
978 }
979 }
980
981 (plans, issues)
982}
983
984pub fn proof_mode_issues(ctx: &CodegenContext) -> Vec<String> {
989 let (_plans, issues) = proof_mode_recursion_analysis(ctx);
990 issues
991}
992
993pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
995 transpile_with_verify_mode(ctx, VerifyEmitMode::NativeDecide)
996}
997
998pub fn transpile_for_proof_mode(
1003 ctx: &CodegenContext,
1004 verify_mode: VerifyEmitMode,
1005) -> ProjectOutput {
1006 let (plans, _issues) = proof_mode_recursion_analysis(ctx);
1007
1008 let mut sections = Vec::new();
1009 sections.push(generate_prelude());
1010 sections.push(String::new());
1011
1012 for module in &ctx.modules {
1014 for td in &module.type_defs {
1015 sections.push(toplevel::emit_type_def(td));
1016 if toplevel::is_recursive_type_def(td) {
1017 sections.push(toplevel::emit_recursive_decidable_eq(
1018 toplevel::type_def_name(td),
1019 ));
1020 }
1021 sections.push(String::new());
1022 }
1023 }
1024 for td in &ctx.type_defs {
1025 sections.push(toplevel::emit_type_def(td));
1026 if toplevel::is_recursive_type_def(td) {
1027 sections.push(toplevel::emit_recursive_decidable_eq(
1028 toplevel::type_def_name(td),
1029 ));
1030 }
1031 sections.push(String::new());
1032 }
1033
1034 emit_pure_functions_proof(ctx, &plans, &mut sections);
1035
1036 for item in &ctx.items {
1037 if let TopLevel::Decision(db) = item {
1038 sections.push(toplevel::emit_decision(db));
1039 sections.push(String::new());
1040 }
1041 }
1042
1043 let mut verify_case_counters: HashMap<String, usize> = HashMap::new();
1044 for item in &ctx.items {
1045 if let TopLevel::Verify(vb) = item {
1046 let key = verify_counter_key(vb);
1047 let start_idx = *verify_case_counters.get(&key).unwrap_or(&0);
1048 let (emitted, next_idx) = toplevel::emit_verify_block(vb, ctx, verify_mode, start_idx);
1049 verify_case_counters.insert(key, next_idx);
1050 sections.push(emitted);
1051 sections.push(String::new());
1052 }
1053 }
1054
1055 let lean_source = sections.join("\n");
1056 let project_name = capitalize_first(&ctx.project_name);
1057 let lakefile = generate_lakefile(&project_name);
1058 let toolchain = generate_toolchain();
1059 ProjectOutput {
1060 files: vec![
1061 ("lakefile.lean".to_string(), lakefile),
1062 ("lean-toolchain".to_string(), toolchain),
1063 (format!("{}.lean", project_name), lean_source),
1064 ],
1065 }
1066}
1067
1068pub fn transpile_with_verify_mode(
1074 ctx: &CodegenContext,
1075 verify_mode: VerifyEmitMode,
1076) -> ProjectOutput {
1077 let mut sections = Vec::new();
1078
1079 sections.push(generate_prelude());
1081 sections.push(String::new());
1082
1083 let recursive_fns = call_graph::find_recursive_fns(&ctx.items);
1085
1086 for module in &ctx.modules {
1088 for td in &module.type_defs {
1089 sections.push(toplevel::emit_type_def(td));
1090 if toplevel::is_recursive_type_def(td) {
1092 sections.push(toplevel::emit_recursive_decidable_eq(
1093 toplevel::type_def_name(td),
1094 ));
1095 }
1096 sections.push(String::new());
1097 }
1098 }
1099
1100 for td in &ctx.type_defs {
1102 sections.push(toplevel::emit_type_def(td));
1103 if toplevel::is_recursive_type_def(td) {
1105 sections.push(toplevel::emit_recursive_decidable_eq(
1106 toplevel::type_def_name(td),
1107 ));
1108 }
1109 sections.push(String::new());
1110 }
1111
1112 emit_pure_functions(ctx, &recursive_fns, &mut sections);
1115
1116 for item in &ctx.items {
1118 if let TopLevel::Decision(db) = item {
1119 sections.push(toplevel::emit_decision(db));
1120 sections.push(String::new());
1121 }
1122 }
1123
1124 let mut verify_case_counters: HashMap<String, usize> = HashMap::new();
1126 for item in &ctx.items {
1127 if let TopLevel::Verify(vb) = item {
1128 let key = verify_counter_key(vb);
1129 let start_idx = *verify_case_counters.get(&key).unwrap_or(&0);
1130 let (emitted, next_idx) = toplevel::emit_verify_block(vb, ctx, verify_mode, start_idx);
1131 verify_case_counters.insert(key, next_idx);
1132 sections.push(emitted);
1133 sections.push(String::new());
1134 }
1135 }
1136
1137 let lean_source = sections.join("\n");
1138
1139 let project_name = capitalize_first(&ctx.project_name);
1141 let lakefile = generate_lakefile(&project_name);
1142 let toolchain = generate_toolchain();
1143
1144 ProjectOutput {
1145 files: vec![
1146 ("lakefile.lean".to_string(), lakefile),
1147 ("lean-toolchain".to_string(), toolchain),
1148 (format!("{}.lean", project_name), lean_source),
1149 ],
1150 }
1151}
1152
1153fn emit_pure_functions(
1154 ctx: &CodegenContext,
1155 recursive_fns: &HashSet<String>,
1156 sections: &mut Vec<String>,
1157) {
1158 let all_fns: Vec<&FnDef> = ctx
1159 .modules
1160 .iter()
1161 .flat_map(|m| m.fn_defs.iter())
1162 .chain(ctx.fn_defs.iter())
1163 .filter(|fd| toplevel::is_pure_fn(fd))
1164 .collect();
1165 if all_fns.is_empty() {
1166 return;
1167 }
1168
1169 let components = call_graph::ordered_fn_components(&all_fns);
1170 for fns in components {
1171 if fns.is_empty() {
1172 continue;
1173 }
1174
1175 if fns.len() > 1 {
1177 sections.push(toplevel::emit_mutual_group(&fns, ctx));
1178 sections.push(String::new());
1179 continue;
1180 }
1181
1182 if let Some(code) = toplevel::emit_fn_def(fns[0], recursive_fns, ctx) {
1185 sections.push(code);
1186 sections.push(String::new());
1187 }
1188 }
1189}
1190
1191fn emit_pure_functions_proof(
1192 ctx: &CodegenContext,
1193 plans: &HashMap<String, RecursionPlan>,
1194 sections: &mut Vec<String>,
1195) {
1196 let all_fns: Vec<&FnDef> = pure_fns(ctx);
1197 if all_fns.is_empty() {
1198 return;
1199 }
1200
1201 let components = call_graph::ordered_fn_components(&all_fns);
1202 for fns in components {
1203 if fns.is_empty() {
1204 continue;
1205 }
1206 if fns.len() > 1 {
1207 sections.push(toplevel::emit_mutual_group_proof(&fns, ctx, plans));
1208 sections.push(String::new());
1209 continue;
1210 }
1211
1212 if let Some(code) =
1213 toplevel::emit_fn_def_proof(fns[0], plans.get(&fns[0].name).copied(), ctx)
1214 {
1215 sections.push(code);
1216 sections.push(String::new());
1217 }
1218 }
1219}
1220
1221fn generate_prelude() -> String {
1222 [
1223 LEAN_PRELUDE_HEADER,
1224 LEAN_PRELUDE_FLOAT_COE,
1225 LEAN_PRELUDE_FLOAT_DEC_EQ,
1226 LEAN_PRELUDE_EXCEPT_DEC_EQ,
1227 LEAN_PRELUDE_EXCEPT_NS,
1228 LEAN_PRELUDE_OPTION_TO_EXCEPT,
1229 LEAN_PRELUDE_STRING_HADD,
1230 AVER_MAP_PRELUDE,
1231 LEAN_PRELUDE_AVER_LIST,
1232 LEAN_PRELUDE_STRING_HELPERS,
1233 LEAN_PRELUDE_NUMERIC_PARSE,
1234 LEAN_PRELUDE_CHAR_BYTE,
1235 ]
1236 .join("\n\n")
1237}
1238
1239fn generate_lakefile(project_name: &str) -> String {
1240 format!(
1241 r#"import Lake
1242open Lake DSL
1243
1244package «{}» where
1245 version := v!"0.1.0"
1246
1247@[default_target]
1248lean_lib «{}» where
1249 srcDir := "."
1250"#,
1251 project_name.to_lowercase(),
1252 project_name
1253 )
1254}
1255
1256fn generate_toolchain() -> String {
1257 "leanprover/lean4:v4.15.0\n".to_string()
1258}
1259
1260fn capitalize_first(s: &str) -> String {
1261 let mut chars = s.chars();
1262 match chars.next() {
1263 None => String::new(),
1264 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
1265 }
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::{
1271 VerifyEmitMode, generate_prelude, proof_mode_issues, transpile, transpile_for_proof_mode,
1272 transpile_with_verify_mode,
1273 };
1274 use crate::ast::{
1275 BinOp, Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Stmt, TopLevel, TypeDef,
1276 TypeVariant, VerifyBlock, VerifyGiven, VerifyGivenDomain, VerifyKind, VerifyLaw,
1277 };
1278 use crate::codegen::CodegenContext;
1279 use std::collections::{HashMap, HashSet};
1280 use std::rc::Rc;
1281
1282 fn empty_ctx() -> CodegenContext {
1283 CodegenContext {
1284 items: vec![],
1285 fn_sigs: HashMap::new(),
1286 memo_fns: HashSet::new(),
1287 memo_safe_types: HashSet::new(),
1288 type_defs: vec![],
1289 fn_defs: vec![],
1290 project_name: "verify_mode".to_string(),
1291 modules: vec![],
1292 module_prefixes: HashSet::new(),
1293 policy: None,
1294 }
1295 }
1296
1297 fn empty_ctx_with_verify_case() -> CodegenContext {
1298 let mut ctx = empty_ctx();
1299 ctx.items.push(TopLevel::Verify(VerifyBlock {
1300 fn_name: "f".to_string(),
1301 line: 1,
1302 cases: vec![(
1303 Expr::Literal(Literal::Int(1)),
1304 Expr::Literal(Literal::Int(1)),
1305 )],
1306 kind: VerifyKind::Cases,
1307 }));
1308 ctx
1309 }
1310
1311 fn empty_ctx_with_two_verify_blocks_same_fn() -> CodegenContext {
1312 let mut ctx = empty_ctx();
1313 ctx.items.push(TopLevel::Verify(VerifyBlock {
1314 fn_name: "f".to_string(),
1315 line: 1,
1316 cases: vec![(
1317 Expr::Literal(Literal::Int(1)),
1318 Expr::Literal(Literal::Int(1)),
1319 )],
1320 kind: VerifyKind::Cases,
1321 }));
1322 ctx.items.push(TopLevel::Verify(VerifyBlock {
1323 fn_name: "f".to_string(),
1324 line: 2,
1325 cases: vec![(
1326 Expr::Literal(Literal::Int(2)),
1327 Expr::Literal(Literal::Int(2)),
1328 )],
1329 kind: VerifyKind::Cases,
1330 }));
1331 ctx
1332 }
1333
1334 fn empty_ctx_with_verify_law() -> CodegenContext {
1335 let mut ctx = empty_ctx();
1336 let add = FnDef {
1337 name: "add".to_string(),
1338 line: 1,
1339 params: vec![
1340 ("a".to_string(), "Int".to_string()),
1341 ("b".to_string(), "Int".to_string()),
1342 ],
1343 return_type: "Int".to_string(),
1344 effects: vec![],
1345 desc: None,
1346 body: Rc::new(FnBody::Expr(Expr::BinOp(
1347 BinOp::Add,
1348 Box::new(Expr::Ident("a".to_string())),
1349 Box::new(Expr::Ident("b".to_string())),
1350 ))),
1351 resolution: None,
1352 };
1353 ctx.fn_defs.push(add.clone());
1354 ctx.items.push(TopLevel::FnDef(add));
1355 ctx.items.push(TopLevel::Verify(VerifyBlock {
1356 fn_name: "add".to_string(),
1357 line: 1,
1358 cases: vec![
1359 (
1360 Expr::FnCall(
1361 Box::new(Expr::Ident("add".to_string())),
1362 vec![
1363 Expr::Literal(Literal::Int(1)),
1364 Expr::Literal(Literal::Int(2)),
1365 ],
1366 ),
1367 Expr::FnCall(
1368 Box::new(Expr::Ident("add".to_string())),
1369 vec![
1370 Expr::Literal(Literal::Int(2)),
1371 Expr::Literal(Literal::Int(1)),
1372 ],
1373 ),
1374 ),
1375 (
1376 Expr::FnCall(
1377 Box::new(Expr::Ident("add".to_string())),
1378 vec![
1379 Expr::Literal(Literal::Int(2)),
1380 Expr::Literal(Literal::Int(3)),
1381 ],
1382 ),
1383 Expr::FnCall(
1384 Box::new(Expr::Ident("add".to_string())),
1385 vec![
1386 Expr::Literal(Literal::Int(3)),
1387 Expr::Literal(Literal::Int(2)),
1388 ],
1389 ),
1390 ),
1391 ],
1392 kind: VerifyKind::Law(VerifyLaw {
1393 name: "commutative".to_string(),
1394 givens: vec![
1395 VerifyGiven {
1396 name: "a".to_string(),
1397 type_name: "Int".to_string(),
1398 domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
1399 },
1400 VerifyGiven {
1401 name: "b".to_string(),
1402 type_name: "Int".to_string(),
1403 domain: VerifyGivenDomain::Explicit(vec![
1404 Expr::Literal(Literal::Int(2)),
1405 Expr::Literal(Literal::Int(3)),
1406 ]),
1407 },
1408 ],
1409 lhs: Expr::FnCall(
1410 Box::new(Expr::Ident("add".to_string())),
1411 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
1412 ),
1413 rhs: Expr::FnCall(
1414 Box::new(Expr::Ident("add".to_string())),
1415 vec![Expr::Ident("b".to_string()), Expr::Ident("a".to_string())],
1416 ),
1417 }),
1418 }));
1419 ctx
1420 }
1421
1422 #[test]
1423 fn prelude_normalizes_float_string_format() {
1424 let prelude = generate_prelude();
1425 assert!(
1426 prelude.contains("private def normalizeFloatString (s : String) : String :="),
1427 "missing normalizeFloatString helper in prelude"
1428 );
1429 assert!(
1430 prelude.contains(
1431 "def String.fromFloat (f : Float) : String := normalizeFloatString (toString f)"
1432 ),
1433 "String.fromFloat should normalize Lean float formatting"
1434 );
1435 }
1436
1437 #[test]
1438 fn prelude_validates_char_from_code_unicode_bounds() {
1439 let prelude = generate_prelude();
1440 assert!(
1441 prelude.contains("if n < 0 || n > 1114111 then none"),
1442 "Char.fromCode should reject code points above Unicode max"
1443 );
1444 assert!(
1445 prelude.contains("else if n >= 55296 && n <= 57343 then none"),
1446 "Char.fromCode should reject surrogate code points"
1447 );
1448 }
1449
1450 #[test]
1451 fn prelude_includes_map_set_helper_lemmas() {
1452 let prelude = generate_prelude();
1453 assert!(
1454 prelude.contains("theorem has_set_self [BEq α]"),
1455 "missing AverMap.has_set_self helper theorem"
1456 );
1457 assert!(
1458 prelude.contains("theorem get_set_self [BEq α]"),
1459 "missing AverMap.get_set_self helper theorem"
1460 );
1461 }
1462
1463 #[test]
1464 fn transpile_emits_native_decide_for_verify_by_default() {
1465 let out = transpile(&empty_ctx_with_verify_case());
1466 let lean = out
1467 .files
1468 .iter()
1469 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1470 .expect("expected generated Lean file");
1471 assert!(lean.contains("example : 1 = 1 := by native_decide"));
1472 }
1473
1474 #[test]
1475 fn transpile_can_emit_sorry_for_verify_when_requested() {
1476 let out = transpile_with_verify_mode(&empty_ctx_with_verify_case(), VerifyEmitMode::Sorry);
1477 let lean = out
1478 .files
1479 .iter()
1480 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1481 .expect("expected generated Lean file");
1482 assert!(lean.contains("example : 1 = 1 := by sorry"));
1483 }
1484
1485 #[test]
1486 fn transpile_can_emit_theorem_skeletons_for_verify() {
1487 let out = transpile_with_verify_mode(
1488 &empty_ctx_with_verify_case(),
1489 VerifyEmitMode::TheoremSkeleton,
1490 );
1491 let lean = out
1492 .files
1493 .iter()
1494 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1495 .expect("expected generated Lean file");
1496 assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
1497 assert!(lean.contains(" sorry"));
1498 }
1499
1500 #[test]
1501 fn theorem_skeleton_numbering_is_global_per_function_across_verify_blocks() {
1502 let out = transpile_with_verify_mode(
1503 &empty_ctx_with_two_verify_blocks_same_fn(),
1504 VerifyEmitMode::TheoremSkeleton,
1505 );
1506 let lean = out
1507 .files
1508 .iter()
1509 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1510 .expect("expected generated Lean file");
1511 assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
1512 assert!(lean.contains("theorem f_verify_2 : 2 = 2 := by"));
1513 }
1514
1515 #[test]
1516 fn transpile_emits_named_theorems_for_verify_law() {
1517 let out = transpile(&empty_ctx_with_verify_law());
1518 let lean = out
1519 .files
1520 .iter()
1521 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1522 .expect("expected generated Lean file");
1523 assert!(lean.contains("-- verify law add.commutative (2 cases)"));
1524 assert!(lean.contains("-- given a: Int = 1..2"));
1525 assert!(lean.contains("-- given b: Int = [2, 3]"));
1526 assert!(lean.contains(
1527 "theorem add_law_commutative : ∀ (a : Int) (b : Int), add a b = add b a := by"
1528 ));
1529 assert!(lean.contains(" intro a b"));
1530 assert!(lean.contains(" simp [add, Int.add_comm]"));
1531 assert!(lean.contains(
1532 "theorem add_law_commutative_sample_1 : add 1 2 = add 2 1 := by native_decide"
1533 ));
1534 assert!(lean.contains(
1535 "theorem add_law_commutative_sample_2 : add 2 3 = add 3 2 := by native_decide"
1536 ));
1537 }
1538
1539 #[test]
1540 fn transpile_auto_proves_reflexive_law_with_rfl() {
1541 let mut ctx = empty_ctx();
1542 ctx.items.push(TopLevel::Verify(VerifyBlock {
1543 fn_name: "idLaw".to_string(),
1544 line: 1,
1545 cases: vec![(
1546 Expr::Literal(Literal::Int(1)),
1547 Expr::Literal(Literal::Int(1)),
1548 )],
1549 kind: VerifyKind::Law(VerifyLaw {
1550 name: "reflexive".to_string(),
1551 givens: vec![VerifyGiven {
1552 name: "x".to_string(),
1553 type_name: "Int".to_string(),
1554 domain: VerifyGivenDomain::IntRange { start: 1, end: 2 },
1555 }],
1556 lhs: Expr::Ident("x".to_string()),
1557 rhs: Expr::Ident("x".to_string()),
1558 }),
1559 }));
1560 let out = transpile(&ctx);
1561 let lean = out
1562 .files
1563 .iter()
1564 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1565 .expect("expected generated Lean file");
1566 assert!(lean.contains("theorem idLaw_law_reflexive : ∀ (x : Int), x = x := by"));
1567 assert!(lean.contains(" intro x"));
1568 assert!(lean.contains(" rfl"));
1569 }
1570
1571 #[test]
1572 fn transpile_auto_proves_identity_law_for_int_add_wrapper() {
1573 let mut ctx = empty_ctx_with_verify_law();
1574 ctx.items.push(TopLevel::Verify(VerifyBlock {
1575 fn_name: "add".to_string(),
1576 line: 10,
1577 cases: vec![(
1578 Expr::FnCall(
1579 Box::new(Expr::Ident("add".to_string())),
1580 vec![
1581 Expr::Literal(Literal::Int(1)),
1582 Expr::Literal(Literal::Int(0)),
1583 ],
1584 ),
1585 Expr::Literal(Literal::Int(1)),
1586 )],
1587 kind: VerifyKind::Law(VerifyLaw {
1588 name: "identityZero".to_string(),
1589 givens: vec![VerifyGiven {
1590 name: "a".to_string(),
1591 type_name: "Int".to_string(),
1592 domain: VerifyGivenDomain::Explicit(vec![
1593 Expr::Literal(Literal::Int(0)),
1594 Expr::Literal(Literal::Int(1)),
1595 ]),
1596 }],
1597 lhs: Expr::FnCall(
1598 Box::new(Expr::Ident("add".to_string())),
1599 vec![Expr::Ident("a".to_string()), Expr::Literal(Literal::Int(0))],
1600 ),
1601 rhs: Expr::Ident("a".to_string()),
1602 }),
1603 }));
1604 let out = transpile(&ctx);
1605 let lean = out
1606 .files
1607 .iter()
1608 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1609 .expect("expected generated Lean file");
1610 assert!(lean.contains("theorem add_law_identityZero : ∀ (a : Int), add a 0 = a := by"));
1611 assert!(lean.contains(" intro a"));
1612 assert!(lean.contains(" simp [add]"));
1613 }
1614
1615 #[test]
1616 fn transpile_auto_proves_associative_law_for_int_add_wrapper() {
1617 let mut ctx = empty_ctx_with_verify_law();
1618 ctx.items.push(TopLevel::Verify(VerifyBlock {
1619 fn_name: "add".to_string(),
1620 line: 20,
1621 cases: vec![(
1622 Expr::FnCall(
1623 Box::new(Expr::Ident("add".to_string())),
1624 vec![
1625 Expr::FnCall(
1626 Box::new(Expr::Ident("add".to_string())),
1627 vec![
1628 Expr::Literal(Literal::Int(1)),
1629 Expr::Literal(Literal::Int(2)),
1630 ],
1631 ),
1632 Expr::Literal(Literal::Int(3)),
1633 ],
1634 ),
1635 Expr::FnCall(
1636 Box::new(Expr::Ident("add".to_string())),
1637 vec![
1638 Expr::Literal(Literal::Int(1)),
1639 Expr::FnCall(
1640 Box::new(Expr::Ident("add".to_string())),
1641 vec![
1642 Expr::Literal(Literal::Int(2)),
1643 Expr::Literal(Literal::Int(3)),
1644 ],
1645 ),
1646 ],
1647 ),
1648 )],
1649 kind: VerifyKind::Law(VerifyLaw {
1650 name: "associative".to_string(),
1651 givens: vec![
1652 VerifyGiven {
1653 name: "a".to_string(),
1654 type_name: "Int".to_string(),
1655 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
1656 },
1657 VerifyGiven {
1658 name: "b".to_string(),
1659 type_name: "Int".to_string(),
1660 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1661 },
1662 VerifyGiven {
1663 name: "c".to_string(),
1664 type_name: "Int".to_string(),
1665 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(3))]),
1666 },
1667 ],
1668 lhs: Expr::FnCall(
1669 Box::new(Expr::Ident("add".to_string())),
1670 vec![
1671 Expr::FnCall(
1672 Box::new(Expr::Ident("add".to_string())),
1673 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
1674 ),
1675 Expr::Ident("c".to_string()),
1676 ],
1677 ),
1678 rhs: Expr::FnCall(
1679 Box::new(Expr::Ident("add".to_string())),
1680 vec![
1681 Expr::Ident("a".to_string()),
1682 Expr::FnCall(
1683 Box::new(Expr::Ident("add".to_string())),
1684 vec![Expr::Ident("b".to_string()), Expr::Ident("c".to_string())],
1685 ),
1686 ],
1687 ),
1688 }),
1689 }));
1690 let out = transpile(&ctx);
1691 let lean = out
1692 .files
1693 .iter()
1694 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1695 .expect("expected generated Lean file");
1696 assert!(lean.contains(
1697 "theorem add_law_associative : ∀ (a : Int) (b : Int) (c : Int), add (add a b) c = add a (add b c) := by"
1698 ));
1699 assert!(lean.contains(" intro a b c"));
1700 assert!(lean.contains(" simp [add, Int.add_assoc]"));
1701 }
1702
1703 #[test]
1704 fn transpile_auto_proves_sub_laws() {
1705 let mut ctx = empty_ctx();
1706 let sub = FnDef {
1707 name: "sub".to_string(),
1708 line: 1,
1709 params: vec![
1710 ("a".to_string(), "Int".to_string()),
1711 ("b".to_string(), "Int".to_string()),
1712 ],
1713 return_type: "Int".to_string(),
1714 effects: vec![],
1715 desc: None,
1716 body: Rc::new(FnBody::Expr(Expr::BinOp(
1717 BinOp::Sub,
1718 Box::new(Expr::Ident("a".to_string())),
1719 Box::new(Expr::Ident("b".to_string())),
1720 ))),
1721 resolution: None,
1722 };
1723 ctx.fn_defs.push(sub.clone());
1724 ctx.items.push(TopLevel::FnDef(sub));
1725
1726 ctx.items.push(TopLevel::Verify(VerifyBlock {
1727 fn_name: "sub".to_string(),
1728 line: 10,
1729 cases: vec![(
1730 Expr::FnCall(
1731 Box::new(Expr::Ident("sub".to_string())),
1732 vec![
1733 Expr::Literal(Literal::Int(2)),
1734 Expr::Literal(Literal::Int(0)),
1735 ],
1736 ),
1737 Expr::Literal(Literal::Int(2)),
1738 )],
1739 kind: VerifyKind::Law(VerifyLaw {
1740 name: "rightIdentity".to_string(),
1741 givens: vec![VerifyGiven {
1742 name: "a".to_string(),
1743 type_name: "Int".to_string(),
1744 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1745 }],
1746 lhs: Expr::FnCall(
1747 Box::new(Expr::Ident("sub".to_string())),
1748 vec![Expr::Ident("a".to_string()), Expr::Literal(Literal::Int(0))],
1749 ),
1750 rhs: Expr::Ident("a".to_string()),
1751 }),
1752 }));
1753 ctx.items.push(TopLevel::Verify(VerifyBlock {
1754 fn_name: "sub".to_string(),
1755 line: 20,
1756 cases: vec![(
1757 Expr::FnCall(
1758 Box::new(Expr::Ident("sub".to_string())),
1759 vec![
1760 Expr::Literal(Literal::Int(2)),
1761 Expr::Literal(Literal::Int(1)),
1762 ],
1763 ),
1764 Expr::BinOp(
1765 BinOp::Sub,
1766 Box::new(Expr::Literal(Literal::Int(0))),
1767 Box::new(Expr::FnCall(
1768 Box::new(Expr::Ident("sub".to_string())),
1769 vec![
1770 Expr::Literal(Literal::Int(1)),
1771 Expr::Literal(Literal::Int(2)),
1772 ],
1773 )),
1774 ),
1775 )],
1776 kind: VerifyKind::Law(VerifyLaw {
1777 name: "antiCommutative".to_string(),
1778 givens: vec![
1779 VerifyGiven {
1780 name: "a".to_string(),
1781 type_name: "Int".to_string(),
1782 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1783 },
1784 VerifyGiven {
1785 name: "b".to_string(),
1786 type_name: "Int".to_string(),
1787 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
1788 },
1789 ],
1790 lhs: Expr::FnCall(
1791 Box::new(Expr::Ident("sub".to_string())),
1792 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
1793 ),
1794 rhs: Expr::BinOp(
1795 BinOp::Sub,
1796 Box::new(Expr::Literal(Literal::Int(0))),
1797 Box::new(Expr::FnCall(
1798 Box::new(Expr::Ident("sub".to_string())),
1799 vec![Expr::Ident("b".to_string()), Expr::Ident("a".to_string())],
1800 )),
1801 ),
1802 }),
1803 }));
1804
1805 let out = transpile(&ctx);
1806 let lean = out
1807 .files
1808 .iter()
1809 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1810 .expect("expected generated Lean file");
1811 assert!(lean.contains("theorem sub_law_rightIdentity : ∀ (a : Int), sub a 0 = a := by"));
1812 assert!(lean.contains(" simp [sub]"));
1813 assert!(lean.contains(
1814 "theorem sub_law_antiCommutative : ∀ (a : Int) (b : Int), sub a b = (-sub b a) := by"
1815 ));
1816 assert!(lean.contains(" simp [sub, sub_eq_add_neg, add_comm, add_left_comm, add_assoc]"));
1817 }
1818
1819 #[test]
1820 fn transpile_auto_proves_unary_wrapper_equivalence_law() {
1821 let mut ctx = empty_ctx();
1822 let add = FnDef {
1823 name: "add".to_string(),
1824 line: 1,
1825 params: vec![
1826 ("a".to_string(), "Int".to_string()),
1827 ("b".to_string(), "Int".to_string()),
1828 ],
1829 return_type: "Int".to_string(),
1830 effects: vec![],
1831 desc: None,
1832 body: Rc::new(FnBody::Expr(Expr::BinOp(
1833 BinOp::Add,
1834 Box::new(Expr::Ident("a".to_string())),
1835 Box::new(Expr::Ident("b".to_string())),
1836 ))),
1837 resolution: None,
1838 };
1839 let add_one = FnDef {
1840 name: "addOne".to_string(),
1841 line: 2,
1842 params: vec![("n".to_string(), "Int".to_string())],
1843 return_type: "Int".to_string(),
1844 effects: vec![],
1845 desc: None,
1846 body: Rc::new(FnBody::Expr(Expr::BinOp(
1847 BinOp::Add,
1848 Box::new(Expr::Ident("n".to_string())),
1849 Box::new(Expr::Literal(Literal::Int(1))),
1850 ))),
1851 resolution: None,
1852 };
1853 ctx.fn_defs.push(add.clone());
1854 ctx.fn_defs.push(add_one.clone());
1855 ctx.items.push(TopLevel::FnDef(add));
1856 ctx.items.push(TopLevel::FnDef(add_one));
1857 ctx.items.push(TopLevel::Verify(VerifyBlock {
1858 fn_name: "addOne".to_string(),
1859 line: 3,
1860 cases: vec![(
1861 Expr::FnCall(
1862 Box::new(Expr::Ident("addOne".to_string())),
1863 vec![Expr::Literal(Literal::Int(2))],
1864 ),
1865 Expr::FnCall(
1866 Box::new(Expr::Ident("add".to_string())),
1867 vec![
1868 Expr::Literal(Literal::Int(2)),
1869 Expr::Literal(Literal::Int(1)),
1870 ],
1871 ),
1872 )],
1873 kind: VerifyKind::Law(VerifyLaw {
1874 name: "identityViaAdd".to_string(),
1875 givens: vec![VerifyGiven {
1876 name: "n".to_string(),
1877 type_name: "Int".to_string(),
1878 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
1879 }],
1880 lhs: Expr::FnCall(
1881 Box::new(Expr::Ident("addOne".to_string())),
1882 vec![Expr::Ident("n".to_string())],
1883 ),
1884 rhs: Expr::FnCall(
1885 Box::new(Expr::Ident("add".to_string())),
1886 vec![Expr::Ident("n".to_string()), Expr::Literal(Literal::Int(1))],
1887 ),
1888 }),
1889 }));
1890 let out = transpile(&ctx);
1891 let lean = out
1892 .files
1893 .iter()
1894 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
1895 .expect("expected generated Lean file");
1896 assert!(
1897 lean.contains(
1898 "theorem addOne_law_identityViaAdd : ∀ (n : Int), addOne n = add n 1 := by"
1899 )
1900 );
1901 assert!(lean.contains(" simp [addOne, add]"));
1902 }
1903
1904 #[test]
1905 fn transpile_auto_proves_direct_map_set_laws() {
1906 let mut ctx = empty_ctx();
1907
1908 let map_set = |m: Expr, k: Expr, v: Expr| {
1909 Expr::FnCall(
1910 Box::new(Expr::Attr(
1911 Box::new(Expr::Ident("Map".to_string())),
1912 "set".to_string(),
1913 )),
1914 vec![m, k, v],
1915 )
1916 };
1917 let map_has = |m: Expr, k: Expr| {
1918 Expr::FnCall(
1919 Box::new(Expr::Attr(
1920 Box::new(Expr::Ident("Map".to_string())),
1921 "has".to_string(),
1922 )),
1923 vec![m, k],
1924 )
1925 };
1926 let map_get = |m: Expr, k: Expr| {
1927 Expr::FnCall(
1928 Box::new(Expr::Attr(
1929 Box::new(Expr::Ident("Map".to_string())),
1930 "get".to_string(),
1931 )),
1932 vec![m, k],
1933 )
1934 };
1935 let some = |v: Expr| {
1936 Expr::FnCall(
1937 Box::new(Expr::Attr(
1938 Box::new(Expr::Ident("Option".to_string())),
1939 "Some".to_string(),
1940 )),
1941 vec![v],
1942 )
1943 };
1944
1945 ctx.items.push(TopLevel::Verify(VerifyBlock {
1946 fn_name: "map".to_string(),
1947 line: 1,
1948 cases: vec![(
1949 map_has(
1950 map_set(
1951 Expr::Ident("m".to_string()),
1952 Expr::Ident("k".to_string()),
1953 Expr::Ident("v".to_string()),
1954 ),
1955 Expr::Ident("k".to_string()),
1956 ),
1957 Expr::Literal(Literal::Bool(true)),
1958 )],
1959 kind: VerifyKind::Law(VerifyLaw {
1960 name: "setHasKey".to_string(),
1961 givens: vec![
1962 VerifyGiven {
1963 name: "m".to_string(),
1964 type_name: "Map<String, Int>".to_string(),
1965 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
1966 Box::new(Expr::Attr(
1967 Box::new(Expr::Ident("Map".to_string())),
1968 "empty".to_string(),
1969 )),
1970 vec![],
1971 )]),
1972 },
1973 VerifyGiven {
1974 name: "k".to_string(),
1975 type_name: "String".to_string(),
1976 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Str(
1977 "a".to_string(),
1978 ))]),
1979 },
1980 VerifyGiven {
1981 name: "v".to_string(),
1982 type_name: "Int".to_string(),
1983 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
1984 },
1985 ],
1986 lhs: map_has(
1987 map_set(
1988 Expr::Ident("m".to_string()),
1989 Expr::Ident("k".to_string()),
1990 Expr::Ident("v".to_string()),
1991 ),
1992 Expr::Ident("k".to_string()),
1993 ),
1994 rhs: Expr::Literal(Literal::Bool(true)),
1995 }),
1996 }));
1997
1998 ctx.items.push(TopLevel::Verify(VerifyBlock {
1999 fn_name: "map".to_string(),
2000 line: 2,
2001 cases: vec![(
2002 map_get(
2003 map_set(
2004 Expr::Ident("m".to_string()),
2005 Expr::Ident("k".to_string()),
2006 Expr::Ident("v".to_string()),
2007 ),
2008 Expr::Ident("k".to_string()),
2009 ),
2010 some(Expr::Ident("v".to_string())),
2011 )],
2012 kind: VerifyKind::Law(VerifyLaw {
2013 name: "setGetKey".to_string(),
2014 givens: vec![
2015 VerifyGiven {
2016 name: "m".to_string(),
2017 type_name: "Map<String, Int>".to_string(),
2018 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
2019 Box::new(Expr::Attr(
2020 Box::new(Expr::Ident("Map".to_string())),
2021 "empty".to_string(),
2022 )),
2023 vec![],
2024 )]),
2025 },
2026 VerifyGiven {
2027 name: "k".to_string(),
2028 type_name: "String".to_string(),
2029 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Str(
2030 "a".to_string(),
2031 ))]),
2032 },
2033 VerifyGiven {
2034 name: "v".to_string(),
2035 type_name: "Int".to_string(),
2036 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(1))]),
2037 },
2038 ],
2039 lhs: map_get(
2040 map_set(
2041 Expr::Ident("m".to_string()),
2042 Expr::Ident("k".to_string()),
2043 Expr::Ident("v".to_string()),
2044 ),
2045 Expr::Ident("k".to_string()),
2046 ),
2047 rhs: some(Expr::Ident("v".to_string())),
2048 }),
2049 }));
2050
2051 let out = transpile(&ctx);
2052 let lean = out
2053 .files
2054 .iter()
2055 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2056 .expect("expected generated Lean file");
2057 assert!(lean.contains("simpa using AverMap.has_set_self m k v"));
2058 assert!(lean.contains("simpa using AverMap.get_set_self m k v"));
2059 }
2060
2061 #[test]
2062 fn transpile_auto_proves_map_update_laws() {
2063 let mut ctx = empty_ctx();
2064
2065 let map_get = |m: Expr, k: Expr| {
2066 Expr::FnCall(
2067 Box::new(Expr::Attr(
2068 Box::new(Expr::Ident("Map".to_string())),
2069 "get".to_string(),
2070 )),
2071 vec![m, k],
2072 )
2073 };
2074 let map_set = |m: Expr, k: Expr, v: Expr| {
2075 Expr::FnCall(
2076 Box::new(Expr::Attr(
2077 Box::new(Expr::Ident("Map".to_string())),
2078 "set".to_string(),
2079 )),
2080 vec![m, k, v],
2081 )
2082 };
2083 let map_has = |m: Expr, k: Expr| {
2084 Expr::FnCall(
2085 Box::new(Expr::Attr(
2086 Box::new(Expr::Ident("Map".to_string())),
2087 "has".to_string(),
2088 )),
2089 vec![m, k],
2090 )
2091 };
2092 let option_some = |v: Expr| {
2093 Expr::FnCall(
2094 Box::new(Expr::Attr(
2095 Box::new(Expr::Ident("Option".to_string())),
2096 "Some".to_string(),
2097 )),
2098 vec![v],
2099 )
2100 };
2101 let option_with_default = |opt: Expr, def: Expr| {
2102 Expr::FnCall(
2103 Box::new(Expr::Attr(
2104 Box::new(Expr::Ident("Option".to_string())),
2105 "withDefault".to_string(),
2106 )),
2107 vec![opt, def],
2108 )
2109 };
2110
2111 let add_one = FnDef {
2112 name: "addOne".to_string(),
2113 line: 1,
2114 params: vec![("n".to_string(), "Int".to_string())],
2115 return_type: "Int".to_string(),
2116 effects: vec![],
2117 desc: None,
2118 body: Rc::new(FnBody::Expr(Expr::BinOp(
2119 BinOp::Add,
2120 Box::new(Expr::Ident("n".to_string())),
2121 Box::new(Expr::Literal(Literal::Int(1))),
2122 ))),
2123 resolution: None,
2124 };
2125 ctx.fn_defs.push(add_one.clone());
2126 ctx.items.push(TopLevel::FnDef(add_one));
2127
2128 let inc_count = FnDef {
2129 name: "incCount".to_string(),
2130 line: 2,
2131 params: vec![
2132 ("counts".to_string(), "Map<String, Int>".to_string()),
2133 ("word".to_string(), "String".to_string()),
2134 ],
2135 return_type: "Map<String, Int>".to_string(),
2136 effects: vec![],
2137 desc: None,
2138 body: Rc::new(FnBody::Block(vec![
2139 Stmt::Binding(
2140 "current".to_string(),
2141 None,
2142 map_get(
2143 Expr::Ident("counts".to_string()),
2144 Expr::Ident("word".to_string()),
2145 ),
2146 ),
2147 Stmt::Expr(Expr::Match {
2148 subject: Box::new(Expr::Ident("current".to_string())),
2149 arms: vec![
2150 MatchArm {
2151 pattern: Pattern::Constructor(
2152 "Some".to_string(),
2153 vec!["n".to_string()],
2154 ),
2155 body: Box::new(map_set(
2156 Expr::Ident("counts".to_string()),
2157 Expr::Ident("word".to_string()),
2158 Expr::BinOp(
2159 BinOp::Add,
2160 Box::new(Expr::Ident("n".to_string())),
2161 Box::new(Expr::Literal(Literal::Int(1))),
2162 ),
2163 )),
2164 },
2165 MatchArm {
2166 pattern: Pattern::Constructor("None".to_string(), vec![]),
2167 body: Box::new(map_set(
2168 Expr::Ident("counts".to_string()),
2169 Expr::Ident("word".to_string()),
2170 Expr::Literal(Literal::Int(1)),
2171 )),
2172 },
2173 ],
2174 line: 2,
2175 }),
2176 ])),
2177 resolution: None,
2178 };
2179 ctx.fn_defs.push(inc_count.clone());
2180 ctx.items.push(TopLevel::FnDef(inc_count));
2181
2182 ctx.items.push(TopLevel::Verify(VerifyBlock {
2183 fn_name: "incCount".to_string(),
2184 line: 10,
2185 cases: vec![(
2186 map_has(
2187 Expr::FnCall(
2188 Box::new(Expr::Ident("incCount".to_string())),
2189 vec![
2190 Expr::Ident("counts".to_string()),
2191 Expr::Ident("word".to_string()),
2192 ],
2193 ),
2194 Expr::Ident("word".to_string()),
2195 ),
2196 Expr::Literal(Literal::Bool(true)),
2197 )],
2198 kind: VerifyKind::Law(VerifyLaw {
2199 name: "keyPresent".to_string(),
2200 givens: vec![
2201 VerifyGiven {
2202 name: "counts".to_string(),
2203 type_name: "Map<String, Int>".to_string(),
2204 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
2205 Box::new(Expr::Attr(
2206 Box::new(Expr::Ident("Map".to_string())),
2207 "empty".to_string(),
2208 )),
2209 vec![],
2210 )]),
2211 },
2212 VerifyGiven {
2213 name: "word".to_string(),
2214 type_name: "String".to_string(),
2215 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Str(
2216 "a".to_string(),
2217 ))]),
2218 },
2219 ],
2220 lhs: map_has(
2221 Expr::FnCall(
2222 Box::new(Expr::Ident("incCount".to_string())),
2223 vec![
2224 Expr::Ident("counts".to_string()),
2225 Expr::Ident("word".to_string()),
2226 ],
2227 ),
2228 Expr::Ident("word".to_string()),
2229 ),
2230 rhs: Expr::Literal(Literal::Bool(true)),
2231 }),
2232 }));
2233
2234 ctx.items.push(TopLevel::Verify(VerifyBlock {
2235 fn_name: "incCount".to_string(),
2236 line: 20,
2237 cases: vec![(
2238 map_get(
2239 Expr::FnCall(
2240 Box::new(Expr::Ident("incCount".to_string())),
2241 vec![
2242 Expr::Ident("counts".to_string()),
2243 Expr::Literal(Literal::Str("a".to_string())),
2244 ],
2245 ),
2246 Expr::Literal(Literal::Str("a".to_string())),
2247 ),
2248 option_some(Expr::FnCall(
2249 Box::new(Expr::Ident("addOne".to_string())),
2250 vec![option_with_default(
2251 map_get(
2252 Expr::Ident("counts".to_string()),
2253 Expr::Literal(Literal::Str("a".to_string())),
2254 ),
2255 Expr::Literal(Literal::Int(0)),
2256 )],
2257 )),
2258 )],
2259 kind: VerifyKind::Law(VerifyLaw {
2260 name: "existingKeyIncrements".to_string(),
2261 givens: vec![VerifyGiven {
2262 name: "counts".to_string(),
2263 type_name: "Map<String, Int>".to_string(),
2264 domain: VerifyGivenDomain::Explicit(vec![Expr::FnCall(
2265 Box::new(Expr::Attr(
2266 Box::new(Expr::Ident("Map".to_string())),
2267 "empty".to_string(),
2268 )),
2269 vec![],
2270 )]),
2271 }],
2272 lhs: map_get(
2273 Expr::FnCall(
2274 Box::new(Expr::Ident("incCount".to_string())),
2275 vec![
2276 Expr::Ident("counts".to_string()),
2277 Expr::Literal(Literal::Str("a".to_string())),
2278 ],
2279 ),
2280 Expr::Literal(Literal::Str("a".to_string())),
2281 ),
2282 rhs: option_some(Expr::FnCall(
2283 Box::new(Expr::Ident("addOne".to_string())),
2284 vec![option_with_default(
2285 map_get(
2286 Expr::Ident("counts".to_string()),
2287 Expr::Literal(Literal::Str("a".to_string())),
2288 ),
2289 Expr::Literal(Literal::Int(0)),
2290 )],
2291 )),
2292 }),
2293 }));
2294
2295 let out = transpile(&ctx);
2296 let lean = out
2297 .files
2298 .iter()
2299 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2300 .expect("expected generated Lean file");
2301 assert!(
2302 lean.contains("cases h : AverMap.get counts word <;> simp [AverMap.has_set_self]"),
2303 "expected keyPresent auto-proof with has_set_self"
2304 );
2305 assert!(
2306 lean.contains("cases h : AverMap.get counts \"a\" <;> simp [AverMap.get_set_self, addOne, incCount]"),
2307 "expected existingKeyIncrements auto-proof with get_set_self"
2308 );
2309 }
2310
2311 #[test]
2312 fn transpile_parenthesizes_negative_int_call_args_in_law_samples() {
2313 let mut ctx = empty_ctx();
2314 let add = FnDef {
2315 name: "add".to_string(),
2316 line: 1,
2317 params: vec![
2318 ("a".to_string(), "Int".to_string()),
2319 ("b".to_string(), "Int".to_string()),
2320 ],
2321 return_type: "Int".to_string(),
2322 effects: vec![],
2323 desc: None,
2324 body: Rc::new(FnBody::Expr(Expr::BinOp(
2325 BinOp::Add,
2326 Box::new(Expr::Ident("a".to_string())),
2327 Box::new(Expr::Ident("b".to_string())),
2328 ))),
2329 resolution: None,
2330 };
2331 ctx.fn_defs.push(add.clone());
2332 ctx.items.push(TopLevel::FnDef(add));
2333 ctx.items.push(TopLevel::Verify(VerifyBlock {
2334 fn_name: "add".to_string(),
2335 line: 1,
2336 cases: vec![(
2337 Expr::FnCall(
2338 Box::new(Expr::Ident("add".to_string())),
2339 vec![
2340 Expr::Literal(Literal::Int(-2)),
2341 Expr::Literal(Literal::Int(-1)),
2342 ],
2343 ),
2344 Expr::FnCall(
2345 Box::new(Expr::Ident("add".to_string())),
2346 vec![
2347 Expr::Literal(Literal::Int(-1)),
2348 Expr::Literal(Literal::Int(-2)),
2349 ],
2350 ),
2351 )],
2352 kind: VerifyKind::Law(VerifyLaw {
2353 name: "commutative".to_string(),
2354 givens: vec![
2355 VerifyGiven {
2356 name: "a".to_string(),
2357 type_name: "Int".to_string(),
2358 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(-2))]),
2359 },
2360 VerifyGiven {
2361 name: "b".to_string(),
2362 type_name: "Int".to_string(),
2363 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(-1))]),
2364 },
2365 ],
2366 lhs: Expr::FnCall(
2367 Box::new(Expr::Ident("add".to_string())),
2368 vec![Expr::Ident("a".to_string()), Expr::Ident("b".to_string())],
2369 ),
2370 rhs: Expr::FnCall(
2371 Box::new(Expr::Ident("add".to_string())),
2372 vec![Expr::Ident("b".to_string()), Expr::Ident("a".to_string())],
2373 ),
2374 }),
2375 }));
2376
2377 let out = transpile(&ctx);
2378 let lean = out
2379 .files
2380 .iter()
2381 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2382 .expect("expected generated Lean file");
2383 assert!(lean.contains(
2384 "theorem add_law_commutative_sample_1 : add (-2) (-1) = add (-1) (-2) := by native_decide"
2385 ));
2386 }
2387
2388 #[test]
2389 fn verify_law_numbering_is_scoped_per_law_name() {
2390 let mut ctx = empty_ctx();
2391 let f = FnDef {
2392 name: "f".to_string(),
2393 line: 1,
2394 params: vec![("x".to_string(), "Int".to_string())],
2395 return_type: "Int".to_string(),
2396 effects: vec![],
2397 desc: None,
2398 body: Rc::new(FnBody::Expr(Expr::Ident("x".to_string()))),
2399 resolution: None,
2400 };
2401 ctx.fn_defs.push(f.clone());
2402 ctx.items.push(TopLevel::FnDef(f));
2403 ctx.items.push(TopLevel::Verify(VerifyBlock {
2404 fn_name: "f".to_string(),
2405 line: 1,
2406 cases: vec![(
2407 Expr::Literal(Literal::Int(1)),
2408 Expr::Literal(Literal::Int(1)),
2409 )],
2410 kind: VerifyKind::Cases,
2411 }));
2412 ctx.items.push(TopLevel::Verify(VerifyBlock {
2413 fn_name: "f".to_string(),
2414 line: 2,
2415 cases: vec![(
2416 Expr::Literal(Literal::Int(2)),
2417 Expr::Literal(Literal::Int(2)),
2418 )],
2419 kind: VerifyKind::Law(VerifyLaw {
2420 name: "identity".to_string(),
2421 givens: vec![VerifyGiven {
2422 name: "x".to_string(),
2423 type_name: "Int".to_string(),
2424 domain: VerifyGivenDomain::Explicit(vec![Expr::Literal(Literal::Int(2))]),
2425 }],
2426 lhs: Expr::Ident("x".to_string()),
2427 rhs: Expr::Ident("x".to_string()),
2428 }),
2429 }));
2430 let out = transpile_with_verify_mode(&ctx, VerifyEmitMode::TheoremSkeleton);
2431 let lean = out
2432 .files
2433 .iter()
2434 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2435 .expect("expected generated Lean file");
2436 assert!(lean.contains("theorem f_verify_1 : 1 = 1 := by"));
2437 assert!(lean.contains("theorem f_law_identity : ∀ (x : Int), x = x := by"));
2438 assert!(lean.contains("theorem f_law_identity_sample_1 : 2 = 2 := by"));
2439 assert!(!lean.contains("theorem f_law_identity_sample_2 : 2 = 2 := by"));
2440 }
2441
2442 #[test]
2443 fn proof_mode_accepts_single_int_countdown_recursion() {
2444 let mut ctx = empty_ctx();
2445 let down = FnDef {
2446 name: "down".to_string(),
2447 line: 1,
2448 params: vec![("n".to_string(), "Int".to_string())],
2449 return_type: "Int".to_string(),
2450 effects: vec![],
2451 desc: None,
2452 body: Rc::new(FnBody::Expr(Expr::Match {
2453 subject: Box::new(Expr::Ident("n".to_string())),
2454 arms: vec![
2455 MatchArm {
2456 pattern: Pattern::Literal(Literal::Int(0)),
2457 body: Box::new(Expr::Literal(Literal::Int(0))),
2458 },
2459 MatchArm {
2460 pattern: Pattern::Wildcard,
2461 body: Box::new(Expr::TailCall(Box::new((
2462 "down".to_string(),
2463 vec![Expr::BinOp(
2464 BinOp::Sub,
2465 Box::new(Expr::Ident("n".to_string())),
2466 Box::new(Expr::Literal(Literal::Int(1))),
2467 )],
2468 )))),
2469 },
2470 ],
2471 line: 1,
2472 })),
2473 resolution: None,
2474 };
2475 ctx.items.push(TopLevel::FnDef(down.clone()));
2476 ctx.fn_defs.push(down);
2477
2478 let issues = proof_mode_issues(&ctx);
2479 assert!(
2480 issues.is_empty(),
2481 "expected Int countdown recursion to be accepted, got: {:?}",
2482 issues
2483 );
2484
2485 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2486 let lean = out
2487 .files
2488 .iter()
2489 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2490 .expect("expected generated Lean file");
2491 assert!(lean.contains("def down (n : Int) : Int :="));
2492 assert!(lean.contains("termination_by Int.natAbs n"));
2493 assert!(lean.contains("decreasing_by"));
2494 }
2495
2496 #[test]
2497 fn proof_mode_accepts_single_list_structural_recursion() {
2498 let mut ctx = empty_ctx();
2499 let len = FnDef {
2500 name: "len".to_string(),
2501 line: 1,
2502 params: vec![("xs".to_string(), "List<Int>".to_string())],
2503 return_type: "Int".to_string(),
2504 effects: vec![],
2505 desc: None,
2506 body: Rc::new(FnBody::Expr(Expr::Match {
2507 subject: Box::new(Expr::Ident("xs".to_string())),
2508 arms: vec![
2509 MatchArm {
2510 pattern: Pattern::EmptyList,
2511 body: Box::new(Expr::Literal(Literal::Int(0))),
2512 },
2513 MatchArm {
2514 pattern: Pattern::Cons("h".to_string(), "t".to_string()),
2515 body: Box::new(Expr::TailCall(Box::new((
2516 "len".to_string(),
2517 vec![Expr::Ident("t".to_string())],
2518 )))),
2519 },
2520 ],
2521 line: 1,
2522 })),
2523 resolution: None,
2524 };
2525 ctx.items.push(TopLevel::FnDef(len.clone()));
2526 ctx.fn_defs.push(len);
2527
2528 let issues = proof_mode_issues(&ctx);
2529 assert!(
2530 issues.is_empty(),
2531 "expected List structural recursion to be accepted, got: {:?}",
2532 issues
2533 );
2534 }
2535
2536 #[test]
2537 fn proof_mode_accepts_single_string_pos_advance_recursion() {
2538 let mut ctx = empty_ctx();
2539 let skip_ws = FnDef {
2540 name: "skipWs".to_string(),
2541 line: 1,
2542 params: vec![
2543 ("s".to_string(), "String".to_string()),
2544 ("pos".to_string(), "Int".to_string()),
2545 ],
2546 return_type: "Int".to_string(),
2547 effects: vec![],
2548 desc: None,
2549 body: Rc::new(FnBody::Expr(Expr::Match {
2550 subject: Box::new(Expr::FnCall(
2551 Box::new(Expr::Attr(
2552 Box::new(Expr::Ident("String".to_string())),
2553 "charAt".to_string(),
2554 )),
2555 vec![Expr::Ident("s".to_string()), Expr::Ident("pos".to_string())],
2556 )),
2557 arms: vec![
2558 MatchArm {
2559 pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
2560 body: Box::new(Expr::Ident("pos".to_string())),
2561 },
2562 MatchArm {
2563 pattern: Pattern::Wildcard,
2564 body: Box::new(Expr::TailCall(Box::new((
2565 "skipWs".to_string(),
2566 vec![
2567 Expr::Ident("s".to_string()),
2568 Expr::BinOp(
2569 BinOp::Add,
2570 Box::new(Expr::Ident("pos".to_string())),
2571 Box::new(Expr::Literal(Literal::Int(1))),
2572 ),
2573 ],
2574 )))),
2575 },
2576 ],
2577 line: 1,
2578 })),
2579 resolution: None,
2580 };
2581 ctx.items.push(TopLevel::FnDef(skip_ws.clone()));
2582 ctx.fn_defs.push(skip_ws);
2583
2584 let issues = proof_mode_issues(&ctx);
2585 assert!(
2586 issues.is_empty(),
2587 "expected String+pos recursion to be accepted, got: {:?}",
2588 issues
2589 );
2590
2591 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2592 let lean = out
2593 .files
2594 .iter()
2595 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2596 .expect("expected generated Lean file");
2597 assert!(lean.contains("def skipWs (s : String) (pos : Int) : Int :="));
2598 assert!(lean.contains("termination_by ((s.data.length) - (pos.toNat))"));
2599 assert!(lean.contains("decreasing_by"));
2600 }
2601
2602 #[test]
2603 fn proof_mode_accepts_mutual_int_countdown_recursion() {
2604 let mut ctx = empty_ctx();
2605 let even = FnDef {
2606 name: "even".to_string(),
2607 line: 1,
2608 params: vec![("n".to_string(), "Int".to_string())],
2609 return_type: "Bool".to_string(),
2610 effects: vec![],
2611 desc: None,
2612 body: Rc::new(FnBody::Expr(Expr::Match {
2613 subject: Box::new(Expr::Ident("n".to_string())),
2614 arms: vec![
2615 MatchArm {
2616 pattern: Pattern::Literal(Literal::Int(0)),
2617 body: Box::new(Expr::Literal(Literal::Bool(true))),
2618 },
2619 MatchArm {
2620 pattern: Pattern::Wildcard,
2621 body: Box::new(Expr::TailCall(Box::new((
2622 "odd".to_string(),
2623 vec![Expr::BinOp(
2624 BinOp::Sub,
2625 Box::new(Expr::Ident("n".to_string())),
2626 Box::new(Expr::Literal(Literal::Int(1))),
2627 )],
2628 )))),
2629 },
2630 ],
2631 line: 1,
2632 })),
2633 resolution: None,
2634 };
2635 let odd = FnDef {
2636 name: "odd".to_string(),
2637 line: 2,
2638 params: vec![("n".to_string(), "Int".to_string())],
2639 return_type: "Bool".to_string(),
2640 effects: vec![],
2641 desc: None,
2642 body: Rc::new(FnBody::Expr(Expr::Match {
2643 subject: Box::new(Expr::Ident("n".to_string())),
2644 arms: vec![
2645 MatchArm {
2646 pattern: Pattern::Literal(Literal::Int(0)),
2647 body: Box::new(Expr::Literal(Literal::Bool(false))),
2648 },
2649 MatchArm {
2650 pattern: Pattern::Wildcard,
2651 body: Box::new(Expr::TailCall(Box::new((
2652 "even".to_string(),
2653 vec![Expr::BinOp(
2654 BinOp::Sub,
2655 Box::new(Expr::Ident("n".to_string())),
2656 Box::new(Expr::Literal(Literal::Int(1))),
2657 )],
2658 )))),
2659 },
2660 ],
2661 line: 2,
2662 })),
2663 resolution: None,
2664 };
2665 ctx.items.push(TopLevel::FnDef(even.clone()));
2666 ctx.items.push(TopLevel::FnDef(odd.clone()));
2667 ctx.fn_defs.push(even);
2668 ctx.fn_defs.push(odd);
2669
2670 let issues = proof_mode_issues(&ctx);
2671 assert!(
2672 issues.is_empty(),
2673 "expected mutual Int countdown recursion to be accepted, got: {:?}",
2674 issues
2675 );
2676 }
2677
2678 #[test]
2679 fn proof_mode_accepts_mutual_string_pos_recursion_with_ranked_same_edges() {
2680 let mut ctx = empty_ctx();
2681 let f = FnDef {
2682 name: "f".to_string(),
2683 line: 1,
2684 params: vec![
2685 ("s".to_string(), "String".to_string()),
2686 ("pos".to_string(), "Int".to_string()),
2687 ],
2688 return_type: "Int".to_string(),
2689 effects: vec![],
2690 desc: None,
2691 body: Rc::new(FnBody::Expr(Expr::Match {
2692 subject: Box::new(Expr::BinOp(
2693 BinOp::Gte,
2694 Box::new(Expr::Ident("pos".to_string())),
2695 Box::new(Expr::Literal(Literal::Int(3))),
2696 )),
2697 arms: vec![
2698 MatchArm {
2699 pattern: Pattern::Literal(Literal::Bool(true)),
2700 body: Box::new(Expr::Ident("pos".to_string())),
2701 },
2702 MatchArm {
2703 pattern: Pattern::Wildcard,
2704 body: Box::new(Expr::TailCall(Box::new((
2705 "g".to_string(),
2706 vec![Expr::Ident("s".to_string()), Expr::Ident("pos".to_string())],
2707 )))),
2708 },
2709 ],
2710 line: 1,
2711 })),
2712 resolution: None,
2713 };
2714 let g = FnDef {
2715 name: "g".to_string(),
2716 line: 2,
2717 params: vec![
2718 ("s".to_string(), "String".to_string()),
2719 ("pos".to_string(), "Int".to_string()),
2720 ],
2721 return_type: "Int".to_string(),
2722 effects: vec![],
2723 desc: None,
2724 body: Rc::new(FnBody::Expr(Expr::Match {
2725 subject: Box::new(Expr::BinOp(
2726 BinOp::Gte,
2727 Box::new(Expr::Ident("pos".to_string())),
2728 Box::new(Expr::Literal(Literal::Int(3))),
2729 )),
2730 arms: vec![
2731 MatchArm {
2732 pattern: Pattern::Literal(Literal::Bool(true)),
2733 body: Box::new(Expr::Ident("pos".to_string())),
2734 },
2735 MatchArm {
2736 pattern: Pattern::Wildcard,
2737 body: Box::new(Expr::TailCall(Box::new((
2738 "f".to_string(),
2739 vec![
2740 Expr::Ident("s".to_string()),
2741 Expr::BinOp(
2742 BinOp::Add,
2743 Box::new(Expr::Ident("pos".to_string())),
2744 Box::new(Expr::Literal(Literal::Int(1))),
2745 ),
2746 ],
2747 )))),
2748 },
2749 ],
2750 line: 2,
2751 })),
2752 resolution: None,
2753 };
2754 ctx.items.push(TopLevel::FnDef(f.clone()));
2755 ctx.items.push(TopLevel::FnDef(g.clone()));
2756 ctx.fn_defs.push(f);
2757 ctx.fn_defs.push(g);
2758
2759 let issues = proof_mode_issues(&ctx);
2760 assert!(
2761 issues.is_empty(),
2762 "expected mutual String+pos recursion to be accepted, got: {:?}",
2763 issues
2764 );
2765
2766 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2767 let lean = out
2768 .files
2769 .iter()
2770 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2771 .expect("expected generated Lean file");
2772 assert!(lean.contains("termination_by"));
2773 assert!(lean.contains("termination_by ((s.data.length) - (pos.toNat),"));
2774 assert!(lean.contains("decreasing_by"));
2775 }
2776
2777 #[test]
2778 fn proof_mode_accepts_mutual_ranked_sizeof_recursion() {
2779 let mut ctx = empty_ctx();
2780 let f = FnDef {
2781 name: "f".to_string(),
2782 line: 1,
2783 params: vec![("xs".to_string(), "List<Int>".to_string())],
2784 return_type: "Int".to_string(),
2785 effects: vec![],
2786 desc: None,
2787 body: Rc::new(FnBody::Expr(Expr::TailCall(Box::new((
2788 "g".to_string(),
2789 vec![
2790 Expr::Literal(Literal::Str("acc".to_string())),
2791 Expr::Ident("xs".to_string()),
2792 ],
2793 ))))),
2794 resolution: None,
2795 };
2796 let g = FnDef {
2797 name: "g".to_string(),
2798 line: 2,
2799 params: vec![
2800 ("acc".to_string(), "String".to_string()),
2801 ("xs".to_string(), "List<Int>".to_string()),
2802 ],
2803 return_type: "Int".to_string(),
2804 effects: vec![],
2805 desc: None,
2806 body: Rc::new(FnBody::Expr(Expr::Match {
2807 subject: Box::new(Expr::Ident("xs".to_string())),
2808 arms: vec![
2809 MatchArm {
2810 pattern: Pattern::EmptyList,
2811 body: Box::new(Expr::Literal(Literal::Int(0))),
2812 },
2813 MatchArm {
2814 pattern: Pattern::Cons("h".to_string(), "t".to_string()),
2815 body: Box::new(Expr::TailCall(Box::new((
2816 "f".to_string(),
2817 vec![Expr::Ident("t".to_string())],
2818 )))),
2819 },
2820 ],
2821 line: 2,
2822 })),
2823 resolution: None,
2824 };
2825 ctx.items.push(TopLevel::FnDef(f.clone()));
2826 ctx.items.push(TopLevel::FnDef(g.clone()));
2827 ctx.fn_defs.push(f);
2828 ctx.fn_defs.push(g);
2829
2830 let issues = proof_mode_issues(&ctx);
2831 assert!(
2832 issues.is_empty(),
2833 "expected mutual ranked-sizeOf recursion to be accepted, got: {:?}",
2834 issues
2835 );
2836
2837 let out = transpile_for_proof_mode(&ctx, VerifyEmitMode::NativeDecide);
2838 let lean = out
2839 .files
2840 .iter()
2841 .find_map(|(name, content)| (name == "Verify_mode.lean").then_some(content))
2842 .expect("expected generated Lean file");
2843 assert!(lean.contains("termination_by"));
2844 assert!(lean.contains("(sizeOf"));
2845 assert!(lean.contains("decreasing_by"));
2846 }
2847
2848 #[test]
2849 fn proof_mode_rejects_recursive_pure_functions() {
2850 let mut ctx = empty_ctx();
2851 let recursive_fn = FnDef {
2852 name: "loop".to_string(),
2853 line: 1,
2854 params: vec![("n".to_string(), "Int".to_string())],
2855 return_type: "Int".to_string(),
2856 effects: vec![],
2857 desc: None,
2858 body: Rc::new(FnBody::Expr(Expr::FnCall(
2859 Box::new(Expr::Ident("loop".to_string())),
2860 vec![Expr::Ident("n".to_string())],
2861 ))),
2862 resolution: None,
2863 };
2864 ctx.items.push(TopLevel::FnDef(recursive_fn.clone()));
2865 ctx.fn_defs.push(recursive_fn);
2866
2867 let issues = proof_mode_issues(&ctx);
2868 assert!(
2869 issues.iter().any(|i| i.contains("outside proof subset")),
2870 "expected recursive function blocker, got: {:?}",
2871 issues
2872 );
2873 }
2874
2875 #[test]
2876 fn proof_mode_allows_recursive_types() {
2877 let mut ctx = empty_ctx();
2878 let recursive_type = TypeDef::Sum {
2879 name: "Node".to_string(),
2880 variants: vec![TypeVariant {
2881 name: "Cons".to_string(),
2882 fields: vec!["Node".to_string()],
2883 }],
2884 line: 1,
2885 };
2886 ctx.items.push(TopLevel::TypeDef(recursive_type.clone()));
2887 ctx.type_defs.push(recursive_type);
2888
2889 let issues = proof_mode_issues(&ctx);
2890 assert!(
2891 issues
2892 .iter()
2893 .all(|i| !i.contains("recursive types require unsafe DecidableEq shim")),
2894 "did not expect recursive type blocker, got: {:?}",
2895 issues
2896 );
2897 }
2898}