1use std::collections::{HashMap, HashSet};
2
3use super::expr::{aver_name_to_lean, emit_expr_legacy};
4use super::fn_def::{emit_fn_body_for, lower_pure_question_bang_for_emit};
5use super::recurrence::{recurrence_nat_helper_name, render_affine_pair_expr};
6use super::render::{
7 emit_doc_comment, emit_fn_param_names, emit_fn_params, indent_lines, ret_type_or_unit,
8};
9use super::type_def::type_measure_expr;
10use super::types::type_annotation_to_lean;
11use super::{
12 emit_fn_def, emit_fn_def_proof, emit_mutual_group, emit_mutual_group_proof, is_pure_fn,
13 is_recursive_type_def, sizeof_measure_param_indices, type_def_name,
14};
15use crate::ast::*;
16use crate::codegen::CodegenContext;
17use crate::codegen::recursion::{native_aux_name, rewrite_recursive_calls_body};
18
19const STRING_POS_FUEL_VAR: &str = "fuel'";
20
21pub const PROOF_FUEL_EXHAUSTED_MSG: &str = "Aver proof fuel exhausted";
34
35fn fuel_helper_name(name: &str) -> String {
36 crate::codegen::recursion::fuel_helper_name(name)
41}
42
43pub(in crate::codegen::lean) fn law_fuel_simp_names(
59 fn_name: &str,
60 ctx: &CodegenContext,
61) -> Vec<String> {
62 let Some(emitted) = probe_fn_scc_emission(fn_name, ctx) else {
63 return Vec::new();
64 };
65 let fuel = fuel_helper_name(fn_name);
66 if !emitted.contains(&format!("def {fuel}")) {
67 return Vec::new();
68 }
69 let mut names = vec![fuel];
70 names.extend(scan_measure_helper_names(&emitted));
71 names
72}
73
74fn probe_fn_scc_emission(fn_name: &str, ctx: &CodegenContext) -> Option<String> {
81 let scopes: Vec<(Option<String>, Vec<&crate::ast::FnDef>)> =
85 std::iter::once((None, ctx.fn_defs.iter().collect::<Vec<_>>()))
86 .chain(
87 ctx.modules
88 .iter()
89 .map(|m| (Some(m.prefix.clone()), m.fn_defs.iter().collect())),
90 )
91 .collect();
92 for (scope, fns) in scopes {
93 let pure: Vec<&crate::ast::FnDef> = fns.into_iter().filter(|fd| is_pure_fn(fd)).collect();
94 if !pure.iter().any(|fd| fd.name == fn_name) {
95 continue;
96 }
97 let comps = crate::call_graph::ordered_fn_components(&pure, &ctx.module_prefixes);
98 let comp = comps
99 .into_iter()
100 .find(|c| c.iter().any(|fd| fd.name == fn_name))?;
101 let emitted = ctx.with_module_scope(scope.as_deref(), || {
102 if comp.len() > 1 {
103 let all_supported = comp
104 .iter()
105 .all(|fd| crate::codegen::common::fn_contract_exists_for_fn(ctx, fd));
106 if all_supported {
107 emit_mutual_group_proof(&comp, ctx)
108 } else {
109 emit_mutual_group(&comp, ctx)
110 }
111 } else if let Some(fd) = comp.first() {
112 if crate::codegen::common::fn_contract_exists_for_fn(ctx, fd) {
113 emit_fn_def_proof(fd, ctx).unwrap_or_default()
114 } else {
115 emit_fn_def(fd, &std::collections::HashSet::from([fd.name.clone()]), ctx)
116 .unwrap_or_default()
117 }
118 } else {
119 String::new()
120 }
121 });
122 return Some(emitted);
123 }
124 None
125}
126
127pub(in crate::codegen::lean) fn law_string_pos_rank(
135 fn_name: &str,
136 ctx: &CodegenContext,
137) -> Option<usize> {
138 let emitted = probe_fn_scc_emission(fn_name, ctx)?;
139 let fuel = fuel_helper_name(fn_name);
140 if !emitted.contains(&format!("def {fuel}")) {
141 return None;
142 }
143 let marker = format!("{fuel} (averStringPosFuel ");
144 let idx = emitted.find(&marker)?;
145 let rest = &emitted[idx + marker.len()..];
146 let mut tokens = rest.split_whitespace();
147 let _string_arg = tokens.next()?;
148 let _pos_arg = tokens.next()?;
149 tokens.next()?.trim_end_matches(')').parse::<usize>().ok()
150}
151
152fn scan_measure_helper_names(text: &str) -> Vec<String> {
159 let mut found: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
160 for prefix in ["averMeasure", "averStringPosFuel"] {
161 for (idx, _) in text.match_indices(prefix) {
162 if idx > 0
164 && text[..idx]
165 .chars()
166 .next_back()
167 .is_some_and(|c| c.is_alphanumeric() || c == '_')
168 {
169 continue;
170 }
171 let rest = &text[idx..];
172 let end = rest
173 .find(|c: char| !(c.is_alphanumeric() || c == '_'))
174 .unwrap_or(rest.len());
175 found.insert(rest[..end].to_string());
176 }
177 }
178 found.into_iter().collect()
179}
180
181fn emit_fuel_helper_def(
182 helper_name: &str,
183 params: &str,
184 ret_type: &str,
185 body: &str,
186 outer_indent: &str,
187) -> Vec<String> {
188 let branch_indent = format!("{outer_indent} ");
189 [
190 vec![format!(
191 "{outer_indent}def {} (fuel : Nat) {} : {} :=",
192 helper_name, params, ret_type
193 )],
194 vec![format!("{outer_indent} match fuel with")],
195 vec![format!(
196 "{outer_indent} | 0 => panic! \"{}\"",
197 PROOF_FUEL_EXHAUSTED_MSG
198 )],
199 vec![format!("{outer_indent} | {} + 1 =>", STRING_POS_FUEL_VAR)],
200 indent_lines(body, &branch_indent),
201 ]
202 .into_iter()
203 .flatten()
204 .collect()
205}
206
207fn emit_string_pos_wrapper(fd: &FnDef, helper_name: &str, rank_budget: usize) -> Vec<String> {
208 let fn_name = aver_name_to_lean(&fd.name);
209 let params = emit_fn_params(&fd.params);
210 let ret_type = ret_type_or_unit(fd);
211 let arg_names = emit_fn_param_names(&fd.params);
212 let (s_name, _) = &fd.params[0];
213 let (pos_name, _) = &fd.params[1];
214 vec![
215 format!("def {} {} : {} :=", fn_name, params, ret_type),
216 format!(
217 " {} (averStringPosFuel {} {} {}) {}",
218 helper_name,
219 aver_name_to_lean(s_name),
220 aver_name_to_lean(pos_name),
221 rank_budget,
222 arg_names
223 ),
224 ]
225}
226
227fn emit_int_countdown_wrapper(fd: &FnDef, helper_name: &str, param_index: usize) -> Vec<String> {
228 let fn_name = aver_name_to_lean(&fd.name);
229 let params = emit_fn_params(&fd.params);
230 let ret_type = ret_type_or_unit(fd);
231 let arg_names = emit_fn_param_names(&fd.params);
232 let metric_name = fd
233 .params
234 .get(param_index)
235 .map(|(name, _)| aver_name_to_lean(name))
236 .unwrap_or_else(|| "0".to_string());
237 vec![
238 format!("def {} {} : {} :=", fn_name, params, ret_type),
239 format!(
240 " {} ((Int.natAbs {}) + 1) {}",
241 helper_name, metric_name, arg_names
242 ),
243 ]
244}
245
246pub(super) fn emit_nat_linear_recurrence_fn(
247 fd: &FnDef,
248 shape: &super::recurrence::SecondOrderIntLinearRecurrenceShape,
249 ctx: &CodegenContext,
250) -> String {
251 let fn_name = aver_name_to_lean(&fd.name);
252 let nat_helper_name = recurrence_nat_helper_name(&fd.name);
253 let lean_param = aver_name_to_lean(&shape.param_name);
254 let ret_type = ret_type_or_unit(fd);
255 let nat_step = render_affine_pair_expr(
256 shape.recurrence,
257 &format!("{nat_helper_name} n"),
258 &format!("{nat_helper_name} (n + 1)"),
259 );
260
261 [
262 emit_doc_comment(&fd.desc),
263 vec![
264 format!("private def {} : Nat -> {}", nat_helper_name, ret_type),
265 format!(" | 0 => {}", emit_expr_legacy(&shape.base0, ctx, None)),
266 format!(" | 1 => {}", emit_expr_legacy(&shape.base1, ctx, None)),
267 format!(" | n + 2 => {}", nat_step),
268 String::new(),
269 format!("def {} ({} : Int) : {} :=", fn_name, lean_param, ret_type),
270 format!(
271 " if {} < 0 then {} else {} {}.toNat",
272 lean_param,
273 emit_expr_legacy(&shape.negative_branch, ctx, None),
274 nat_helper_name,
275 lean_param
276 ),
277 ],
278 ]
279 .into_iter()
280 .flatten()
281 .collect::<Vec<_>>()
282 .join("\n")
283}
284
285fn emit_sizeof_measure_expr(fd: &FnDef, recursive_types: &HashSet<String>) -> Option<String> {
286 let measure_terms: Vec<String> = sizeof_measure_param_indices(fd)
287 .into_iter()
288 .filter_map(|idx| {
289 fd.params.get(idx).and_then(|(name, type_name)| {
290 type_measure_expr(type_name, &aver_name_to_lean(name), recursive_types, None)
291 })
292 })
293 .collect();
294
295 (!measure_terms.is_empty()).then(|| measure_terms.join(" + "))
296}
297
298fn emit_mutual_sizeof_wrapper(
299 fd: &FnDef,
300 helper_name: &str,
301 rank_budget: usize,
302 recursive_types: &HashSet<String>,
303) -> Vec<String> {
304 let fn_name = aver_name_to_lean(&fd.name);
305 let params = emit_fn_params(&fd.params);
306 let ret_type = ret_type_or_unit(fd);
307 let arg_names = emit_fn_param_names(&fd.params);
308 let fuel_expr = emit_sizeof_measure_expr(fd, recursive_types)
309 .map(|measure| format!("(({}) + 1) * {}", measure, rank_budget))
310 .unwrap_or_else(|| rank_budget.to_string());
311 vec![
312 format!("def {} {} : {} :=", fn_name, params, ret_type),
313 format!(" {} ({}) {}", helper_name, fuel_expr, arg_names),
314 ]
315}
316
317pub(super) fn emit_fuelized_string_pos_fn(fd: &FnDef, ctx: &CodegenContext) -> String {
318 let helper_name = fuel_helper_name(&fd.name);
319 let params = emit_fn_params(&fd.params);
320 let ret_type = ret_type_or_unit(fd);
321 let rewritten = rewrite_recursive_calls_body(
322 &fd.body,
323 &HashSet::from([fd.name.clone()]),
324 STRING_POS_FUEL_VAR,
325 );
326 let body = emit_fn_body_for(fd, &rewritten, ctx);
327
328 [
329 emit_doc_comment(&fd.desc),
330 emit_fuel_helper_def(&helper_name, ¶ms, &ret_type, &body, ""),
331 vec![String::new()],
332 emit_string_pos_wrapper(fd, &helper_name, 1),
333 emit_string_pos_scan_lemma(fd, &helper_name, ctx)
334 .map(|lemma| vec![String::new(), lemma])
335 .unwrap_or_default(),
336 emit_simple_string_pos_stability_lemma(fd, &helper_name, 1, &body)
337 .map(|lemma| vec![String::new(), lemma])
338 .unwrap_or_default(),
339 ]
340 .into_iter()
341 .flatten()
342 .collect::<Vec<_>>()
343 .join("\n")
344}
345
346fn emit_string_pos_scan_lemma(
374 fd: &FnDef,
375 helper_name: &str,
376 ctx: &CodegenContext,
377) -> Option<String> {
378 let shape = crate::codegen::proof_recognize::detect_string_pos_scan(fd)?;
379 let scope = ctx.active_module_scope();
380 let pred_fd = ctx
381 .fn_def_by_name(&shape.predicate_fn, scope.as_deref())
382 .or_else(|| ctx.fn_def_by_name(&shape.predicate_fn, None))?;
383 if !crate::codegen::proof_recognize::scan_predicate_fn_ok(pred_fd) {
384 return None;
385 }
386
387 let s = aver_name_to_lean(&fd.params[0].0);
388 let pos = aver_name_to_lean(&fd.params[1].0);
389 let pred = aver_name_to_lean(&shape.predicate_fn);
390 let lemma_name = format!("{helper_name}_scan");
391
392 let mut carried_binders: Vec<String> = Vec::new();
395 let mut carried_names: Vec<String> = Vec::new();
396 let mut trailing_args: Vec<String> = Vec::new();
397 for (i, pin) in shape.param_pins.iter().enumerate() {
398 let (name, ty) = &fd.params[i + 2];
399 match pin {
400 None => {
401 let lean = aver_name_to_lean(name);
402 carried_binders.push(format!(" ({} : {})", lean, type_annotation_to_lean(ty)));
403 carried_names.push(lean.clone());
404 trailing_args.push(lean);
405 }
406 Some(b) => trailing_args.push(b.to_string()),
407 }
408 }
409 let args = trailing_args
410 .iter()
411 .map(|a| format!(" {a}"))
412 .collect::<String>();
413 let carried_binder_text: String = carried_binders.concat();
414 let carried_intro = carried_names
415 .iter()
416 .map(|n| format!("{n} "))
417 .collect::<String>();
418
419 const LEN_MARKER: &str = "AVERSCANLEN";
424 let mut subst: std::collections::HashMap<String, crate::ast::Expr> =
425 std::collections::HashMap::new();
426 subst.insert(
427 fd.params[1].0.clone(),
428 crate::ast::Expr::Ident(LEN_MARKER.to_string()),
429 );
430 for (i, pin) in shape.param_pins.iter().enumerate() {
431 if let Some(b) = pin {
432 subst.insert(
433 fd.params[i + 2].0.clone(),
434 crate::ast::Expr::Literal(crate::ast::Literal::Bool(*b)),
435 );
436 }
437 }
438 let exit_subst =
439 crate::codegen::proof_recognize::substitute_idents_in_expr(&shape.exit_expr, &subst);
440 let exit = emit_expr_legacy(&exit_subst, ctx, None)
441 .replace('\n', " ")
442 .replace(LEN_MARKER, &format!("(({s}.toList.length : Int))"));
443
444 Some(format!(
445 r#"/-- Auto-synthesized scan lemma: an all-`{pred}` suffix scan runs to the
446 end of the string. Companion to the `{helper_name}` fuel def; proved by
447 the fixed fuel-induction template. -/
448theorem {lemma_name} :
449 ∀ (fuel : Nat) ({s} : String) ({pos} : Int){carried_binder_text},
450 0 ≤ {pos} → {pos}.toNat ≤ {s}.toList.length →
451 {s}.toList.length - {pos}.toNat < fuel →
452 (∀ ch ∈ {s}.toList.drop {pos}.toNat, {pred} (Char.toString ch) = true) →
453 {helper_name} fuel {s} {pos}{args} = {exit} := by
454 intro fuel
455 induction fuel with
456 | zero =>
457 intro {s} {pos} {carried_intro}h0 h1 h2 h3
458 omega
459 | succ fuel ih =>
460 intro {s} {pos} {carried_intro}h0 h1 h2 h3
461 by_cases hlt : {pos}.toNat < {s}.toList.length
462 · have hch := String.charAt_eq_of_lt {s} {pos} h0 hlt
463 have hdrop := List.drop_eq_getElem_cons (l := {s}.toList) (i := {pos}.toNat) hlt
464 have hdig : {pred} (Char.toString ({s}.toList[{pos}.toNat])) = true := by
465 apply h3
466 rw [hdrop]
467 exact List.mem_cons_self
468 have hstep : ∀ ch ∈ {s}.toList.drop (({pos} + 1).toNat), {pred} (Char.toString ch) = true := by
469 intro ch hc
470 apply h3
471 rw [hdrop]
472 refine List.mem_cons_of_mem _ ?_
473 have he : ({pos} + 1).toNat = {pos}.toNat + 1 := by omega
474 rw [he] at hc
475 exact hc
476 have hrec := ih {s} ({pos} + 1) {carried_intro}(by omega) (by omega) (by omega) hstep
477 calc {helper_name} (fuel + 1) {s} {pos}{args}
478 = {helper_name} fuel {s} ({pos} + 1){args} := by
479 simp only [{helper_name}, hch, hdig]
480 simp
481 _ = {exit} := hrec
482 · have hpos : {pos} = ({s}.toList.length : Int) := by omega
483 have hch := String.charAt_none_of_ge {s} {pos} h0 (by omega)
484 simp only [{helper_name}, hch]
485 rw [hpos]"#
486 ))
487}
488
489fn emit_simple_string_pos_stability_lemma(
517 fd: &FnDef,
518 helper_name: &str,
519 rank_budget: usize,
520 emitted_body: &str,
521) -> Option<String> {
522 detect_simple_string_pos_skip_literal(fd).or_else(|| {
525 detect_simple_string_pos_skip_literal_from_body(fd, helper_name, emitted_body)
526 })?;
527 let fn_name = aver_name_to_lean(&fd.name);
528 let s = aver_name_to_lean(&fd.params.first()?.0);
529 let pos = aver_name_to_lean(&fd.params.get(1)?.0);
530 let params = emit_fn_params(&fd.params);
531 let args = emit_fn_param_names(&fd.params);
532 let binders = if params.is_empty() {
533 String::new()
534 } else {
535 format!(" {params}")
536 };
537 let lemma_name = format!("{helper_name}_stable");
538
539 Some(format!(
540 r#"theorem {lemma_name} :
541 ∀ (fuel : Nat){binders},
542 averStringPosFuel {s} {pos} {rank_budget} ≤ fuel →
543 {helper_name} fuel {args} = {fn_name} {args} := by
544 first
545 | (intro fuel
546 induction fuel with
547 | zero =>
548 intro {args} h
549 unfold averStringPosFuel at h
550 omega
551 | succ fuel ih =>
552 intro {args} h
553 unfold {fn_name}
554 have hmeasure_pos : 0 < averStringPosFuel {s} {pos} {rank_budget} := by
555 unfold averStringPosFuel
556 omega
557 cases hmeasure : averStringPosFuel {s} {pos} {rank_budget} with
558 | zero =>
559 omega
560 | succ measure' =>
561 by_cases hneg : {pos} < 0
562 · have hchar : String.charAtAv {s} {pos} = none := by
563 unfold String.charAtAv
564 simp [hneg]
565 simp [{helper_name}, hchar]
566 · have h0 : 0 ≤ {pos} := by omega
567 by_cases hlt : {pos}.toNat < {s}.toList.length
568 · have hchar := String.charAt_eq_of_lt {s} {pos} h0 hlt
569 have hnext_measure : averStringPosFuel {s} ({pos} + 1) {rank_budget} = measure' := by
570 unfold averStringPosFuel at h hmeasure ⊢
571 omega
572 have hleft : {helper_name} fuel {s} ({pos} + 1) = {helper_name} measure' {s} ({pos} + 1) := by
573 have hstep : {helper_name} fuel {s} ({pos} + 1) = {fn_name} {s} ({pos} + 1) := by
574 apply ih
575 rw [hnext_measure]
576 unfold averStringPosFuel at h hmeasure
577 omega
578 rw [hstep]
579 unfold {fn_name}
580 rw [hnext_measure]
581 simp only [{helper_name}, hchar, hleft]
582 · have hchar := String.charAt_none_of_ge {s} {pos} h0 (by omega)
583 simp [{helper_name}, hchar])
584 | sorry"#
585 ))
586}
587
588pub(in crate::codegen::lean) fn detect_simple_string_pos_skip_literal(
589 fd: &FnDef,
590) -> Option<String> {
591 if fd.params.len() != 2 {
592 return None;
593 }
594 let s_name = &fd.params[0].0;
595 let pos_name = &fd.params[1].0;
596 let [Stmt::Expr(expr)] = fd.body.stmts() else {
597 return None;
598 };
599 let Expr::Match { subject, arms } = &expr.node else {
600 return None;
601 };
602 if !is_string_char_at(subject.as_ref(), s_name, pos_name) {
603 return None;
604 }
605
606 let mut saw_none_exit = false;
619 let mut recursive_literal = None;
620 let mut saw_fallback_exit = false;
621 for arm in arms {
622 match &arm.pattern {
623 Pattern::Constructor(name, fields) if name == "Option.None" && fields.is_empty() => {
624 if !expr_is_ident(&arm.body, pos_name) {
625 return None;
626 }
627 saw_none_exit = true;
628 }
629 Pattern::Constructor(name, fields) if name == "Option.Some" && fields.len() == 1 => {
630 let (lit, fallback) = detect_inner_char_match_literal(
631 &arm.body, &fields[0], &fd.name, s_name, pos_name,
632 )?;
633 recursive_literal = Some(lit);
634 saw_fallback_exit = fallback;
635 }
636 _ => return None,
637 }
638 }
639
640 match (saw_none_exit, saw_fallback_exit, recursive_literal) {
641 (true, true, Some(lit)) => Some(lit),
642 _ => None,
643 }
644}
645
646fn detect_simple_string_pos_skip_literal_from_body(
647 fd: &FnDef,
648 helper_name: &str,
649 body: &str,
650) -> Option<String> {
651 let s_name = aver_name_to_lean(&fd.params.first()?.0);
652 let pos_name = aver_name_to_lean(&fd.params.get(1)?.0);
653 let recursive_suffix =
654 format!("=> {helper_name} {STRING_POS_FUEL_VAR} {s_name} ({pos_name} + 1)");
655 let none_exit = format!("| .none => {pos_name}");
656 let fallback_exit = format!("| _ => {pos_name}");
657 if !body.lines().any(|line| line.trim() == none_exit)
658 || !body.lines().any(|line| line.trim() == fallback_exit)
659 {
660 return None;
661 }
662 body.lines().find_map(|line| {
663 let trimmed = line.trim();
664 if !trimmed.ends_with(&recursive_suffix) {
665 return None;
666 }
667 let rest = trimmed.strip_prefix("| \"")?;
668 let end = rest.find("\" =>")?;
669 Some(rest[..end].to_string())
670 })
671}
672
673fn detect_inner_char_match_literal(
674 expr: &Spanned<Expr>,
675 binding: &str,
676 fn_name: &str,
677 s_name: &str,
678 pos_name: &str,
679) -> Option<(String, bool)> {
680 let Expr::Match { subject, arms } = &expr.node else {
681 return None;
682 };
683 if !binding.is_empty() && !expr_is_ident(subject, binding) {
684 return None;
685 }
686 let mut recursive_literal = None;
687 let mut fallback_exit = false;
688 for arm in arms {
689 match &arm.pattern {
690 Pattern::Literal(Literal::Str(lit))
695 if expr_is_self_pos_plus_one_tailcall(&arm.body, fn_name, s_name, pos_name) =>
696 {
697 recursive_literal = Some(lit.clone());
698 }
699 Pattern::Wildcard if expr_is_ident(&arm.body, pos_name) => {
701 fallback_exit = true;
702 }
703 _ => {
712 if expr_contains_self_call(&arm.body, fn_name) {
713 return None;
714 }
715 }
716 }
717 }
718 recursive_literal.map(|lit| (lit, fallback_exit))
719}
720
721fn is_string_char_at(expr: &Spanned<Expr>, s_name: &str, pos_name: &str) -> bool {
722 let Expr::FnCall(callee, args) = &expr.node else {
723 return false;
724 };
725 if args.len() != 2 || !expr_is_ident(&args[0], s_name) || !expr_is_ident(&args[1], pos_name) {
726 return false;
727 }
728 let Expr::Attr(base, method) = &callee.node else {
729 return false;
730 };
731 method == "charAt" && expr_is_ident(base, "String")
732}
733
734fn expr_is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
735 match &expr.node {
743 Expr::Ident(n) => n == name,
744 Expr::Resolved { name: n, .. } => n == name,
745 _ => false,
746 }
747}
748
749fn expr_is_self_pos_plus_one_tailcall(
750 expr: &Spanned<Expr>,
751 fn_name: &str,
752 s_name: &str,
753 pos_name: &str,
754) -> bool {
755 let (target, args) = match &expr.node {
756 Expr::TailCall(data) => (&data.target, &data.args),
757 Expr::FnCall(callee, args) => match &callee.node {
758 Expr::Ident(name) => (name, args),
759 Expr::Resolved { name, .. } => (name, args),
760 _ => return false,
761 },
762 _ => return false,
763 };
764 target == fn_name
765 && args.len() == 2
766 && expr_is_ident(&args[0], s_name)
767 && expr_is_pos_plus_one(&args[1], pos_name)
768}
769
770fn expr_is_pos_plus_one(expr: &Spanned<Expr>, pos_name: &str) -> bool {
771 matches!(
772 &expr.node,
773 Expr::BinOp(
774 BinOp::Add,
775 left,
776 right
777 ) if expr_is_ident(left, pos_name)
778 && matches!(&right.node, Expr::Literal(Literal::Int(1)))
779 )
780}
781
782fn expr_contains_self_call(expr: &Spanned<Expr>, fn_name: &str) -> bool {
793 match &expr.node {
794 Expr::TailCall(data) => {
795 data.target == fn_name
796 || data
797 .args
798 .iter()
799 .any(|a| expr_contains_self_call(a, fn_name))
800 }
801 Expr::FnCall(callee, args) => {
802 matches!(&callee.node, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == fn_name)
803 || expr_contains_self_call(callee, fn_name)
804 || args.iter().any(|a| expr_contains_self_call(a, fn_name))
805 }
806 Expr::Attr(obj, _) => expr_contains_self_call(obj, fn_name),
807 Expr::BinOp(_, l, r) => {
808 expr_contains_self_call(l, fn_name) || expr_contains_self_call(r, fn_name)
809 }
810 Expr::Neg(inner) | Expr::ErrorProp(inner) => expr_contains_self_call(inner, fn_name),
811 Expr::Match { subject, arms } => {
812 expr_contains_self_call(subject, fn_name)
813 || arms
814 .iter()
815 .any(|a| expr_contains_self_call(&a.body, fn_name))
816 }
817 Expr::Constructor(_, inner) => inner
818 .as_deref()
819 .is_some_and(|e| expr_contains_self_call(e, fn_name)),
820 Expr::InterpolatedStr(parts) => parts
821 .iter()
822 .any(|p| matches!(p, StrPart::Parsed(e) if expr_contains_self_call(e, fn_name))),
823 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
824 items.iter().any(|i| expr_contains_self_call(i, fn_name))
825 }
826 Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
827 expr_contains_self_call(k, fn_name) || expr_contains_self_call(v, fn_name)
828 }),
829 Expr::RecordCreate { fields, .. } => fields
830 .iter()
831 .any(|(_, v)| expr_contains_self_call(v, fn_name)),
832 Expr::RecordUpdate { base, updates, .. } => {
833 expr_contains_self_call(base, fn_name)
834 || updates
835 .iter()
836 .any(|(_, v)| expr_contains_self_call(v, fn_name))
837 }
838 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => false,
839 }
840}
841
842fn strip_match_eq_binders(body: String) -> String {
843 body.lines()
844 .map(|line| {
845 let trimmed = line.trim_start();
846 let indent_len = line.len() - trimmed.len();
847 let indent = &line[..indent_len];
848 let Some(rest) = trimmed.strip_prefix("match h_") else {
849 return line.to_string();
850 };
851 let Some(colon_idx) = rest.find(" : ") else {
852 return line.to_string();
853 };
854 format!("{indent}match {}", &rest[colon_idx + 3..])
855 })
856 .collect::<Vec<_>>()
857 .join("\n")
858}
859
860pub(super) fn emit_native_guarded_int_countdown_fn(
879 fd: &FnDef,
880 ctx: &CodegenContext,
881 param_index: usize,
882 base_arm_literal: i64,
883 base_arm_body: &Spanned<crate::ir::hir::ResolvedExpr>,
884 wildcard_arm_body: &Spanned<crate::ir::hir::ResolvedExpr>,
885 precondition: &[Spanned<crate::ir::hir::ResolvedExpr>],
886) -> String {
887 let aux_name = native_aux_name(&fd.name);
888 let main_name = aver_name_to_lean(&fd.name);
889 let lean_aux_name = aver_name_to_lean(&aux_name);
890 let Some((param_name, _)) = fd.params.get(param_index) else {
891 return emit_fuelized_int_countdown_fn(fd, ctx, param_index);
892 };
893 let lean_pname = aver_name_to_lean(param_name);
894
895 let precond_lean = if precondition.is_empty() {
901 format!("{} ≥ 0", lean_pname)
902 } else {
903 precondition
904 .iter()
905 .map(|p| format!("({})", super::expr::emit_expr(p, ctx)))
906 .collect::<Vec<_>>()
907 .join(" ∧ ")
908 };
909
910 let aux_params = format!("{} (h_dom : {})", emit_fn_params(&fd.params), precond_lean);
911 let main_params = emit_fn_params(&fd.params);
912 let ret_type = ret_type_or_unit(fd);
913
914 let target_fn_id = crate::codegen::common::fn_id_for_decl(ctx, fd)
930 .unwrap_or_else(|| panic!("native-guarded fn {} missing FnId", fd.name));
931 let rewritten_wc = crate::codegen::recursion::rewrite_native_guarded_calls_resolved_expr(
932 wildcard_arm_body,
933 target_fn_id,
934 &aux_name,
935 );
936 let base_str = super::expr::emit_expr(base_arm_body, ctx);
937 let rec_str = super::expr::emit_expr(&rewritten_wc, ctx);
938 let arg_names = emit_fn_param_names(&fd.params);
939
940 let mut lines = Vec::new();
941 lines.extend(emit_doc_comment(&fd.desc));
942 lines.push(format!(
943 "def {} {} : {} :=",
944 lean_aux_name, aux_params, ret_type
945 ));
946 lines.push(format!(
947 " if h_zero : {} = {} then {}",
948 lean_pname, base_arm_literal, base_str
949 ));
950 lines.push(format!(" else {}", rec_str));
951 lines.push(format!("termination_by Int.natAbs {}", lean_pname));
952 lines.push("decreasing_by".to_string());
953 lines.push(" simp_wf".to_string());
954 lines.push(" omega".to_string());
955 lines.push(String::new());
956
957 lines.push(format!(
958 "def {} {} : {} :=",
959 main_name, main_params, ret_type
960 ));
961 lines.push(format!(
962 " if h_dom : {} then {} {} h_dom",
963 precond_lean, lean_aux_name, arg_names
964 ));
965 lines.push(format!(" else {}", base_str));
966
967 lines.join("\n")
968}
969
970pub(super) fn emit_fuelized_int_countdown_fn(
971 fd: &FnDef,
972 ctx: &CodegenContext,
973 param_index: usize,
974) -> String {
975 let helper_name = fuel_helper_name(&fd.name);
976 let params = emit_fn_params(&fd.params);
977 let ret_type = ret_type_or_unit(fd);
978 let rewritten = rewrite_recursive_calls_body(
979 &fd.body,
980 &HashSet::from([fd.name.clone()]),
981 STRING_POS_FUEL_VAR,
982 );
983 let body = strip_match_eq_binders(emit_fn_body_for(fd, &rewritten, ctx));
984
985 [
986 emit_doc_comment(&fd.desc),
987 emit_fuel_helper_def(&helper_name, ¶ms, &ret_type, &body, ""),
988 vec![String::new()],
989 emit_int_countdown_wrapper(fd, &helper_name, param_index),
990 ]
991 .into_iter()
992 .flatten()
993 .collect::<Vec<_>>()
994 .join("\n")
995}
996
997pub(super) fn emit_fuelized_int_ascending_fn(
998 fd: &FnDef,
999 ctx: &CodegenContext,
1000 param_index: usize,
1001 bound_lean: &str,
1002) -> String {
1003 let helper_name = fuel_helper_name(&fd.name);
1004 let params = emit_fn_params(&fd.params);
1005 let ret_type = ret_type_or_unit(fd);
1006 let rewritten = rewrite_recursive_calls_body(
1007 &fd.body,
1008 &HashSet::from([fd.name.clone()]),
1009 STRING_POS_FUEL_VAR,
1010 );
1011 let body = strip_match_eq_binders(emit_fn_body_for(fd, &rewritten, ctx));
1012
1013 [
1014 emit_doc_comment(&fd.desc),
1015 emit_fuel_helper_def(&helper_name, ¶ms, &ret_type, &body, ""),
1016 vec![String::new()],
1017 emit_int_ascending_wrapper(fd, &helper_name, param_index, bound_lean),
1018 ]
1019 .into_iter()
1020 .flatten()
1021 .collect::<Vec<_>>()
1022 .join("\n")
1023}
1024
1025fn emit_int_ascending_wrapper(
1026 fd: &FnDef,
1027 helper_name: &str,
1028 param_index: usize,
1029 bound_lean: &str,
1030) -> Vec<String> {
1031 let fn_name = super::expr::aver_name_to_lean(&fd.name);
1032 let params = emit_fn_params(&fd.params);
1033 let ret_type = ret_type_or_unit(fd);
1034 let arg_names = emit_fn_param_names(&fd.params);
1035 let metric_name = fd
1036 .params
1037 .get(param_index)
1038 .map(|(name, _)| super::expr::aver_name_to_lean(name))
1039 .unwrap_or_else(|| "0".to_string());
1040 vec![
1041 format!("def {} {} : {} :=", fn_name, params, ret_type),
1042 format!(
1043 " {} ((Int.natAbs ({} - {})) + 1) {}",
1044 helper_name, bound_lean, metric_name, arg_names
1045 ),
1046 ]
1047}
1048
1049fn contract_lex_rank(ctx: &CodegenContext, fd: &FnDef) -> Option<usize> {
1053 contract_lex_params_rank(ctx, fd).map(|(_, rank)| rank)
1054}
1055
1056pub(super) fn contract_lex_params_rank<'a>(
1064 ctx: &'a CodegenContext,
1065 fd: &FnDef,
1066) -> Option<(&'a [String], usize)> {
1067 let contract = crate::codegen::common::find_fn_contract_for_fn(ctx, fd)?;
1068 let crate::ir::RecursionContract::Fuel {
1069 fuel_metric: crate::ir::FuelMetric::Lex { params, rank },
1070 } = contract.recursion.as_ref()?
1071 else {
1072 return None;
1073 };
1074 Some((params.as_slice(), *rank))
1075}
1076
1077pub(super) fn emit_fuelized_mutual_string_pos_group(
1078 fns: &[&FnDef],
1079 ctx: &CodegenContext,
1080) -> String {
1081 let targets: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1082 let max_rank = fns
1083 .iter()
1084 .filter_map(|fd| contract_lex_rank(ctx, fd))
1085 .max()
1086 .unwrap_or(1);
1087
1088 let mut helper_lines = vec!["mutual".to_string()];
1089 for fd in fns {
1090 if !is_pure_fn(fd) {
1091 continue;
1092 }
1093 let helper_name = fuel_helper_name(&fd.name);
1094 let params = emit_fn_params(&fd.params);
1095 let ret_type = ret_type_or_unit(fd);
1096 let rewritten = rewrite_recursive_calls_body(&fd.body, &targets, STRING_POS_FUEL_VAR);
1097 let body = emit_fn_body_for(fd, &rewritten, ctx);
1098
1099 helper_lines.extend(
1100 emit_doc_comment(&fd.desc)
1101 .into_iter()
1102 .map(|line| format!(" {line}")),
1103 );
1104 helper_lines.extend(emit_fuel_helper_def(
1105 &helper_name,
1106 ¶ms,
1107 &ret_type,
1108 &body,
1109 " ",
1110 ));
1111 helper_lines.push(String::new());
1112 }
1113 helper_lines.push("end".to_string());
1114
1115 let wrapper_lines: Vec<String> = fns
1116 .iter()
1117 .filter(|fd| is_pure_fn(fd))
1118 .flat_map(|fd| {
1119 let helper_name = fuel_helper_name(&fd.name);
1120 let mut lines = emit_string_pos_wrapper(fd, &helper_name, max_rank);
1121 lines.push(String::new());
1122 lines
1123 })
1124 .collect();
1125
1126 [helper_lines, vec![String::new()], wrapper_lines]
1127 .into_iter()
1128 .flatten()
1129 .collect::<Vec<_>>()
1130 .join("\n")
1131}
1132
1133pub(super) fn emit_fuelized_mutual_int_countdown_group(
1134 fns: &[&FnDef],
1135 ctx: &CodegenContext,
1136) -> String {
1137 let targets: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1138
1139 let mut helper_lines = vec!["mutual".to_string()];
1140 for fd in fns {
1141 if !is_pure_fn(fd) {
1142 continue;
1143 }
1144 let helper_name = fuel_helper_name(&fd.name);
1145 let params = emit_fn_params(&fd.params);
1146 let ret_type = ret_type_or_unit(fd);
1147 let rewritten = rewrite_recursive_calls_body(&fd.body, &targets, STRING_POS_FUEL_VAR);
1148 let body = strip_match_eq_binders(emit_fn_body_for(fd, &rewritten, ctx));
1149
1150 helper_lines.extend(
1151 emit_doc_comment(&fd.desc)
1152 .into_iter()
1153 .map(|line| format!(" {line}")),
1154 );
1155 helper_lines.extend(emit_fuel_helper_def(
1156 &helper_name,
1157 ¶ms,
1158 &ret_type,
1159 &body,
1160 " ",
1161 ));
1162 helper_lines.push(String::new());
1163 }
1164 helper_lines.push("end".to_string());
1165
1166 let wrapper_lines: Vec<String> = fns
1167 .iter()
1168 .filter(|fd| is_pure_fn(fd))
1169 .flat_map(|fd| {
1170 let helper_name = fuel_helper_name(&fd.name);
1171 let mut lines = emit_int_countdown_wrapper(fd, &helper_name, 0);
1172 lines.push(String::new());
1173 lines
1174 })
1175 .collect();
1176
1177 [helper_lines, vec![String::new()], wrapper_lines]
1178 .into_iter()
1179 .flatten()
1180 .collect::<Vec<_>>()
1181 .join("\n")
1182}
1183
1184fn emit_native_termination_measure(fd: &FnDef, ctx: &CodegenContext) -> Option<String> {
1194 let indices = crate::codegen::recursion::detect::sizeof_measure_param_indices(fd);
1195 if indices.is_empty() {
1196 return None;
1197 }
1198 let single_sizeof_param = indices.len() == 1;
1205
1206 let resolved_fd = crate::codegen::common::fn_id_for_decl(ctx, fd)
1210 .and_then(|id| ctx.resolved_program.fn_by_id(id));
1211 let resolved_owned = match resolved_fd {
1212 Some(_) => None,
1213 None => Some(ctx.resolve_fn_def(fd, None)),
1214 };
1215 let rfd: &crate::ir::hir::ResolvedFnDef =
1216 resolved_fd.unwrap_or_else(|| resolved_owned.as_ref().unwrap().as_ref());
1217 let mut terms: Vec<String> = Vec::new();
1218 for idx in indices {
1219 let (name, ty) = rfd.params.get(idx)?;
1220 let lean_name = aver_name_to_lean(name);
1221 match ty {
1222 crate::types::Type::List(_)
1223 | crate::types::Type::Vector(_)
1224 | crate::types::Type::Map(_, _) => {
1229 terms.push(format!("sizeOf {lean_name}"));
1234 }
1235 crate::types::Type::Named { name: tname, .. }
1253 if single_sizeof_param
1254 && ctx
1255 .modules
1256 .iter()
1257 .flat_map(|m| m.type_defs.iter())
1258 .chain(ctx.type_defs.iter())
1259 .any(|td| {
1260 type_def_name(td) == tname.as_str() && is_recursive_type_def(td)
1261 }) =>
1262 {
1263 terms.push(format!("sizeOf {lean_name}"));
1264 }
1265 _ => return None,
1268 }
1269 }
1270 (!terms.is_empty()).then(|| terms.join(" + "))
1271}
1272
1273pub(super) fn emit_native_mutual_sizeof_group(
1285 fns: &[&FnDef],
1286 ctx: &CodegenContext,
1287) -> Option<String> {
1288 let mut ranks: HashMap<String, usize> = HashMap::new();
1289 for fd in fns {
1290 if !is_pure_fn(fd) {
1291 return None;
1292 }
1293 match contract_lex_params_rank(ctx, fd) {
1297 Some(([], rank)) => {
1298 ranks.insert(fd.name.clone(), rank);
1299 }
1300 _ => return None,
1301 }
1302 }
1303 let mut measures: HashMap<String, String> = HashMap::new();
1304 for fd in fns {
1305 let measure = emit_native_termination_measure(fd, ctx)?;
1306 measures.insert(fd.name.clone(), measure);
1307 }
1308 if crate::codegen::recursion::detect::scc_has_growing_accumulator(fns) {
1309 return None;
1310 }
1311
1312 let mut lines: Vec<String> = vec!["mutual".to_string()];
1313 for fd in fns {
1314 let measure = measures.get(&fd.name).unwrap();
1315 let rank = ranks.get(&fd.name).unwrap();
1316 let fn_name = aver_name_to_lean(&fd.name);
1317 let params = emit_fn_params(&fd.params);
1318 let ret_type = ret_type_or_unit(fd);
1319 let lowered = lower_pure_question_bang_for_emit(fd);
1320 let body_fn = lowered.as_ref().unwrap_or(fd);
1321 let body_ast = lowered
1322 .as_ref()
1323 .map(|l| l.body.as_ref())
1324 .unwrap_or(fd.body.as_ref());
1325 let body = emit_fn_body_for(body_fn, body_ast, ctx);
1326
1327 lines.extend(
1328 emit_doc_comment(&fd.desc)
1329 .into_iter()
1330 .map(|line| format!(" {line}")),
1331 );
1332 lines.push(format!(" def {} {} : {} :=", fn_name, params, ret_type));
1333 for body_line in body.lines() {
1334 lines.push(format!(" {body_line}"));
1335 }
1336 lines.push(format!(" termination_by ({measure}, {rank})"));
1337 lines.push(
1347 " decreasing_by all_goals (first | decreasing_tactic | (simp_wf; (try simp only [AverMap.entries, AverMap.fromList]); (try simp_all); first | omega | (constructor <;> first | rfl | omega)))"
1348 .to_string(),
1349 );
1350 lines.push(String::new());
1351 }
1352 lines.push("end".to_string());
1353 Some(lines.join("\n"))
1354}
1355
1356pub(super) fn emit_fuelized_mutual_sizeof_group(fns: &[&FnDef], ctx: &CodegenContext) -> String {
1357 let targets: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
1358 let recursive_types: HashSet<String> = ctx
1359 .modules
1360 .iter()
1361 .flat_map(|m| m.type_defs.iter())
1362 .chain(ctx.type_defs.iter())
1363 .filter(|td| is_recursive_type_def(td))
1364 .map(|td| type_def_name(td).to_string())
1365 .collect();
1366 let rank_budget = fns
1367 .iter()
1368 .filter_map(|fd| contract_lex_rank(ctx, fd))
1369 .max()
1370 .unwrap_or(1)
1371 + 1;
1372
1373 let mut helper_lines = vec!["mutual".to_string()];
1374 for fd in fns {
1375 if !is_pure_fn(fd) {
1376 continue;
1377 }
1378 let helper_name = fuel_helper_name(&fd.name);
1379 let params = emit_fn_params(&fd.params);
1380 let ret_type = ret_type_or_unit(fd);
1381 let rewritten = rewrite_recursive_calls_body(&fd.body, &targets, STRING_POS_FUEL_VAR);
1382 let body = emit_fn_body_for(fd, &rewritten, ctx);
1383
1384 helper_lines.extend(
1385 emit_doc_comment(&fd.desc)
1386 .into_iter()
1387 .map(|line| format!(" {line}")),
1388 );
1389 helper_lines.extend(emit_fuel_helper_def(
1390 &helper_name,
1391 ¶ms,
1392 &ret_type,
1393 &body,
1394 " ",
1395 ));
1396 helper_lines.push(String::new());
1397 }
1398 helper_lines.push("end".to_string());
1399
1400 let wrapper_lines: Vec<String> = fns
1401 .iter()
1402 .filter(|fd| is_pure_fn(fd))
1403 .flat_map(|fd| {
1404 let helper_name = fuel_helper_name(&fd.name);
1405 let mut lines =
1406 emit_mutual_sizeof_wrapper(fd, &helper_name, rank_budget, &recursive_types);
1407 lines.push(String::new());
1408 lines
1409 })
1410 .collect();
1411
1412 [helper_lines, vec![String::new()], wrapper_lines]
1413 .into_iter()
1414 .flatten()
1415 .collect::<Vec<_>>()
1416 .join("\n")
1417}