Skip to main content

aver/codegen/lean/toplevel/
fuel.rs

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
21/// Panic message baked into every fuel wrapper's exhaustion arm. This is a
22/// SOUNDNESS marker, not just a diagnostic: Lean's `panic!` does NOT abort
23/// evaluation — it prints `PANIC at … <this message>` and returns the type's
24/// `default` value, so under `native_decide` an exhausted-fuel sample reduces
25/// both sides of a model-vs-model equation to `default` and the kernel
26/// certifies a vacuous (possibly FALSE) equality with `lake` still exiting 0.
27/// `aver proof --check` therefore scans captured lake output for panic lines
28/// ([`crate::codegen::lean::count_model_panic_lines`]) and treats any hit as
29/// a hard check failure. The scan keys on Lean's generic `PANIC at ` line
30/// marker — every prelude `panic!` site shares the vacuity vector, not just
31/// this one — so this constant is purely the emission message; changing it
32/// cannot blind the gate.
33pub const PROOF_FUEL_EXHAUSTED_MSG: &str = "Aver proof fuel exhausted";
34
35fn fuel_helper_name(name: &str) -> String {
36    // Use the shared helper so the name matches what the shared AST
37    // rewrite emits into `Expr::Ident(...)` call sites. The `__fuel`
38    // suffix keeps the result a plain ASCII identifier regardless of
39    // the source name, so no Lean-specific escaping is needed.
40    crate::codegen::recursion::fuel_helper_name(name)
41}
42
43/// Simp-set names for a fuel-emitted fn cited by the
44/// `SimpOverPreludeLemmas` law rung: `<name>__fuel` plus the measure
45/// helper names (`averMeasure*` / `averStringPosFuel`) the wrapper's
46/// fuel expression references. Rather than re-deriving the
47/// plan→emission mapping (which `recognize_lex_list_wf_scc` can flip
48/// per-SCC to native `termination_by`, no `__fuel` def at all), this
49/// PROBES the proof-mode emission itself: re-emit the fn's SCC group
50/// through the exact dispatch `transpile_unified` uses and scan the
51/// text. Returns `[]` when the emission carries no `def <name>__fuel`
52/// — citing a non-existent constant in `simp [...]` would be a hard
53/// `unknown constant` build error, the one failure mode the rung's
54/// `first | … | sorry` floor cannot catch. Cost: one re-emit of one
55/// SCC per fuel-citing law (string building only, no side effects).
56/// Assumes proof-mode emission — every production Lean export goes
57/// through `transpile_for_proof_mode`.
58pub(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
74/// Re-emit the SCC group that owns `fn_name` through the exact
75/// dispatch `transpile_unified` uses and return the emitted text.
76/// Shared probe for [`law_fuel_simp_names`] and
77/// [`law_string_pos_rank`] — see the former's doc for why probing the
78/// emission beats re-deriving the plan→emission mapping. `None` when
79/// the fn isn't a pure fn of any scope.
80fn probe_fn_scc_emission(fn_name: &str, ctx: &CodegenContext) -> Option<String> {
81    // Locate the fn's owning scope (entry first, then dep modules) and
82    // the pure-fn population of that scope — the same component
83    // universe `transpile_unified` routes.
84    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
127/// The `averStringPosFuel` rank literal of `fn_name`'s emitted fuel
128/// wrapper (`def <fn> … := <fn>__fuel (averStringPosFuel s pos RANK)
129/// …`), probed from the actual proof-mode emission so the
130/// `StringEscapeRoundtrip` skeleton's `show`-line quotes the exact
131/// fuel expression the wrapper carries. `None` when the fn isn't
132/// fuel-emitted with a string-pos wrapper — the renderer declines
133/// rather than quoting a fuel expression that doesn't exist.
134pub(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
152/// Harvest measure-helper identifiers (`averMeasure*`,
153/// `averStringPosFuel`) from emitted Lean text. These are the names a
154/// fuel wrapper's initial-fuel expression references; the
155/// `SimpOverPreludeLemmas` rung needs them in its simp set so the fuel
156/// value computes to a `Nat` literal before the `__fuel` equations
157/// fire. Sorted + deduped for deterministic emit.
158fn 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            // Reject mid-identifier hits (`xaverMeasure`).
163            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, &params, &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
346/// Companion theorem for a fuelized string-position SCANNER — the
347/// general crack in the fuel-unfolding barrier (#125 family): when the
348/// body matches the canonical shape `match String.charAtAv s pos with |
349/// none => EXIT | some c => if P c then SELF(s, pos+1, …) else OTHER`
350/// (recognized by `proof_recognize::detect_string_pos_scan`), emit
351///
352/// ```text
353/// theorem <fn>__fuel_scan : ∀ fuel s pos <carried>,
354///   0 ≤ pos → pos.toNat ≤ s.toList.length →
355///   s.toList.length - pos.toNat < fuel →
356///   (∀ ch ∈ s.toList.drop pos.toNat, P (Char.toString ch) = true) →
357///   <fn>__fuel fuel s pos <args@pins> = EXIT[pos := ↑s.toList.length]
358/// ```
359///
360/// proved by a FIXED fuel-induction template (`String.charAt_eq_of_lt`
361/// / `String.charAt_none_of_ge` + `List.drop_eq_getElem_cons` + omega —
362/// ported verbatim from the verified json hand proof). Universal-law
363/// emissions (`IntDecimalRoundtrip`) rewrite through this lemma to run
364/// a symbolic all-`P` suffix to the end of the string.
365///
366/// CONSERVATIVELY SHAPE-GATED: when the body does not match the exact
367/// recognizer shape, NOTHING is emitted — every emission must be
368/// provable by the uniform template BY CONSTRUCTION of the gate (a
369/// synthesized lemma that fails to prove would be a build error in the
370/// export). The predicate must also resolve to a pure single-`String`-
371/// param `Bool` fn (the lemma cites it by name as a hypothesis key; it
372/// is never unfolded).
373fn 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    // Trailing args: carried params stay variables (quantified), pinned
393    // params bake their Bool literal into statement + calc steps.
394    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    // EXIT[pos := ↑s.data.length, pinned := literal]: substitute at the
420    // AST level (a unique marker stands in for the length cast, which
421    // has no Aver-AST form), render through the SAME expr emitter the
422    // body used, then swap the marker for the cast.
423    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
489/// Companion stability lemma for a simple string-position fuel SKIPPER —
490/// `<fn>__fuel fuel s pos = <fn> s pos` whenever `fuel` meets the wrapper's
491/// initial `averStringPosFuel s pos rank` budget. Gated to the skip shape
492/// (`match charAt with none => pos | some c => match c with <lit> => self(s,
493/// pos+1) … | _ => pos`) by [`detect_simple_string_pos_skip_literal`]; the
494/// literal itself is only the shape witness — the proof is literal-COUNT
495/// agnostic (handles single- and multi-literal skippers like json's four-way
496/// `skipWs` uniformly), so the detected literal value is discarded.
497///
498/// The recursive branch never case-splits on the character: once
499/// `charAt = some c` is fixed, both `<fn>__fuel (fuel+1)` and the wrapper's
500/// `<fn>__fuel (measure'+1)` unfold to the SAME inner `match Char.toString c`
501/// whose only difference is the recursive fuel argument (`fuel` vs `measure'`),
502/// and one `simp only [<fn>__fuel, hchar, hleft]` closes it by rewriting that
503/// argument — independent of how many literal arms recurse.
504///
505/// FAIL-SOFT: the whole proof is wrapped in `first | (<skeleton>) | sorry`
506/// (mirroring the off-probe floor of
507/// [`crate::codegen::lean::law_auto::transparent_chain`]). A gated shape the
508/// skeleton cannot close degrades to a non-fatal `declaration uses 'sorry'`
509/// warning instead of a hard `unsolved goals` build error. The lemma is not
510/// cited by any law, so its own `sorry` never enters another law's
511/// `#print axioms` set — universal credit for laws that do not reference it is
512/// untouched. A bare `sorry` (not the `AVERSPEC_SORRY:<id>` trace) is used
513/// deliberately: the trace exists so `speculative::parse_failures` can demote a
514/// non-closing conditional LAW to bounded, and this support lemma has no law
515/// tier to demote and no `speculative::admits` consumer.
516fn 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    // Gate only: the detected literal is the shape witness; the robust proof
523    // below is literal-agnostic, so the value is discarded.
524    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    // FAIL-CLOSED: the outer match must be EXACTLY the `charAtAv s pos`
607    // none/some pair — `Option.None -> pos` (terminal exit) and
608    // `Option.Some(c) -> <inner char match>` (the recursion). Every other
609    // outer arm declines graduation so the fn stays fueled:
610    //   * a catch-all `_` (BLOCKER 2): the native emitter names the sole
611    //     outer discriminant (`match hc_scan : charAtAv …`) so
612    //     `decreasing_by` can read `charAtAv s pos = some c`. A wildcard
613    //     arm produces no `= some c` equation, so the graduated def
614    //     hard-errors on its termination proof — a native def cannot
615    //     sorry-floor termination, so this must never be emitted.
616    //   * a `None` arm returning anything but `pos`, or an unexpected extra
617    //     constructor — not the graduating skip shape.
618    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            // An advancing arm: EXACTLY `self(s, pos + 1)` under the
691            // `charAtAv s pos = some c` guard — the only recursive step
692            // whose strict decrease of `s.length - pos` the native
693            // `decreasing_by` can close.
694            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            // The catch-all terminal exit `_ -> pos`.
700            Pattern::Wildcard if expr_is_ident(&arm.body, pos_name) => {
701                fallback_exit = true;
702            }
703            // FAIL-CLOSED: every remaining arm must be a TERMINAL,
704            // non-recursive arm (no self-call anywhere in its body). An arm
705            // carrying a self-call that is NOT the exact `self(s, pos + 1)`
706            // advance — a non-advancing `self(s, pos)`, a non-unit
707            // `self(s, pos + 2)`, or an advance routed through a helper
708            // `self(s, f(pos))` — graduates a native def whose
709            // `decreasing_by` cannot close (or silently changes the proven
710            // step shape), so it is unclassifiable: decline and stay fueled.
711            _ => {
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    // Accept both raw `Ident` (pre-resolution, e.g. the recursion
736    // classifier) and the resolver's `Resolved { name, .. }` slot form
737    // (fn bodies at proof-emit time are resolved in place). Without the
738    // `Resolved` arm this shape detection silently declines on every
739    // emit-time body — the reason the graduation gate and the #643
740    // stability gate previously depended on the string `_from_body`
741    // fallback.
742    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
782/// True iff `expr` (recursively) contains a call whose target is the
783/// recursive fn itself (`fn_name`). Covers post-TCO `TailCall`s — whose
784/// target the generic `recursion::expr_references_ident` walker skips,
785/// checking only its args — and both `Ident` and resolver-`Resolved`
786/// `FnCall` callees. Used by [`detect_inner_char_match_literal`] to
787/// reject an inner-match arm carrying a self-call that is NOT the exact
788/// `self(s, pos + 1)` advance: such an arm graduates a native def whose
789/// `decreasing_by` cannot close, so the fn must stay fueled. No existing
790/// helper reports TailCall targets, so this focused walker is the
791/// smallest fail-closed check.
792fn 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
860/// Native `IntCountdown` emission for closed-world fns with the canonical
861/// `match p { 0 -> BASE; _ -> rec(p-1, ...) }` shape. Splits the fn into:
862///
863/// - `<name>__aux` — the real recursion carrying an explicit `(h : p ≥ 0)`
864///   precondition. Recursive callsites in its body are rewritten to call
865///   `<name>__aux` instead of `<name>` with an extra `(by omega)` proof
866///   obligation appended (synthesized via the
867///   `OMEGA_PROOF_SENTINEL` ident — see `lean::expr::emit_expr`).
868/// - `<name>` — the public wrapper preserving the original Aver signature.
869///   Dispatches on `p ≥ 0` to the aux; the `p < 0` branch returns `BASE`
870///   (the source's `0` arm). That falls outside the Aver well-formed
871///   domain for the issue-84 fibonacci-style targets, but keeping the aux
872///   private and total avoids forcing every call site (verify samples,
873///   peer fn bodies) to thread proof obligations.
874///
875/// Lean accepts this because the aux's `termination_by p.natAbs` together
876/// with `(h : p ≥ 0)` + the `_` arm's implicit `p ≠ 0` lets `omega`
877/// discharge `(p - 1).natAbs < p.natAbs` mechanically.
878pub(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    // Precondition: AND of caller-derived predicates, or `(p ≥ 0)`
896    // when the artifact has no single external caller (free-standing
897    // fns / test fixtures). Same `Spanned<Expr>`-as-predicate path
898    // opaque types use, so `emit_expr` is the single emitter — no
899    // parallel infrastructure.
900    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    // Emit `if h_zero : n = LITERAL then BASE else REC` rather than
915    // `match n with | LITERAL => ... | _ => ...`. The dependent `if h :
916    // c then ... else` form puts `h : c` / `h : ¬c` in scope for the
917    // corresponding branch, which `omega` needs to discharge `(n - 1)
918    // ≥ 0` and `(n - 1).natAbs < n.natAbs` at the recursive callsite +
919    // termination check. Plain `match` would leave the case-split
920    // implicit (only an unnamed `casesOn` motive carries it) and
921    // `omega` can't see it.
922    // Resolve the recursive fn's `FnId` via the same pointer-eq path
923    // `ProofIR.fn_contracts` was keyed by — `fn_id_for_decl` picks
924    // the owning module's prefix when `fd` came from a dep, the
925    // entry slot when it sits in `ctx.fn_defs`. Bare-name
926    // `FnKey::entry(fd.name)` would collide for any module-owned
927    // recursive fn whose bare name also exists at entry (the very
928    // class of bug #147 phase E is killing).
929    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, &params, &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, &params, &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
1049/// Read the rank component of a `Fuel { Lex { .., rank } }` contract.
1050/// Returns `None` when the fn has no contract or the contract isn't
1051/// a Lex shape (non-mutual variant or non-recursive).
1052fn contract_lex_rank(ctx: &CodegenContext, fd: &FnDef) -> Option<usize> {
1053    contract_lex_params_rank(ctx, fd).map(|(_, rank)| rank)
1054}
1055
1056/// Read both the params Vec and rank of a `Fuel { Lex { params, rank } }`
1057/// contract. Returns `None` for non-Lex / non-recursive / missing
1058/// contracts. Used by mutual-SCC dispatchers to distinguish:
1059///
1060/// - `MutualIntCountdown`: `params.len() == 1`, rank == 0
1061/// - `MutualStringPosAdvance`: `params.len() == 2`
1062/// - `MutualSizeOfRanked`: `params.is_empty()`
1063pub(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            &params,
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            &params,
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
1184/// Termination measure for Lean's native `termination_by` clause —
1185/// `name.length` for List/Vector/String params (Lean's stdlib
1186/// `List.length` is what `decreasing_tactic` knows how to chase),
1187/// `sizeOf name` fallback for recursive ADTs. Sum across every
1188/// sizeOf-relevant param.
1189///
1190/// Epic #180 Phase 4 — reads param types from the resolved fn def
1191/// (already typed by the typechecker) instead of re-parsing the
1192/// AST annotation string.
1193fn 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    // SINGLE-carrier only for the recursive-ADT arm. Multi-carrier ADT sum
1199    // measures (`sizeOf f + sizeOf g`) are native-CLOSABLE for some shapes
1200    // (an `eval` SCC builds) but NOT all — a red-black-tree mutual SCC's
1201    // `decreasing_by` does not close, hard-failing the build. So multi-carrier
1202    // ADT stays on fuel pending a per-SCC closure check; lifting it blanket
1203    // regressed `proof_export_lake_builds_red_black_tree`.
1204    let single_sizeof_param = indices.len() == 1;
1205
1206    // Pointer-eq scope so a same-bare-name twin never provides
1207    // these param types. Synthetic / mid-rewrite fns fall back to
1208    // on-demand resolve.
1209    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            // `Map` lowers to `List (k × v)` in the proof backend (AverMap is
1225            // list-backed), so `sizeOf` measures it exactly like a List — e.g.
1226            // a json-shaped `objectSafe(m: Map) = entriesSafe(Map.entries m)`
1227            // delegates at the SAME `sizeOf m` and settles on the lex rank.
1228            | crate::types::Type::Map(_, _) => {
1229                // `sizeOf` instead of `.length` so the user measure
1230                // matches what Lean's mutual-block wf elaboration
1231                // generates internally — `decreasing_tactic` then
1232                // closes the chain without `simp_wf` scrambling.
1233                terms.push(format!("sizeOf {lean_name}"));
1234            }
1235            // A recursive ADT param recurses STRUCTURALLY: its `sizeOf`
1236            // strictly drops on every constructor sub-term, so it carries a
1237            // native `sizeOf` measure exactly like a List — the same
1238            // `(sizeOf, rank)` lex tuple the mutual block emits, with the
1239            // robust `decreasing_by` closing the ADT-field decrease and the
1240            // rank settling any same-`sizeOf` delegation (e.g. a Map-as-list
1241            // `entries` identity hop in a json-shaped SCC). Restricted to the
1242            // Proof side only, and the kernel's own termination check is the
1243            // fail-closed backstop — if the measure is wrong for some SCC the
1244            // build fails (that SCC stays open), never a false theorem. So we
1245            // try native here instead of conservatively forcing a fuel wrapper,
1246            // which would tax every law touching the predicate with
1247            // fuel-congruence. SINGLE-carrier only (see above).
1248            // backend-link-stage: name-keyed recursive-type-def lookup (same as
1249            // the fuel path's `recursive_types`); the measure only needs WHETHER
1250            // this Named type is recursive, not its `id`, so the bare-name match
1251            // is sufficient here.
1252            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            // String (parser position-recursion, not structural) and every
1266            // other shape still decline to fuel.
1267            _ => return None,
1268        }
1269    }
1270    (!terms.is_empty()).then(|| terms.join(" + "))
1271}
1272
1273/// Native termination emission for mutual-recursion SCCs whose
1274/// every member has a sizeOf measure (List / Vector / String) and a
1275/// classifier rank — Lean 4 `mutual ... end` block with one
1276/// `termination_by` per def, lex tuple `(sizeOf_sum, rank)` from
1277/// `MutualSizeOfRanked`. Mirrors the Dafny native path from #83.
1278///
1279/// Returns `None` when:
1280/// - SCC isn't fully `MutualSizeOfRanked` (caller picks fuel)
1281/// - Any member has no inferable sizeOf measure
1282/// - Growing-accumulator pattern detected (tail-rec `[x] + acc`
1283///   shapes won't decrease the lex tuple)
1284pub(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        // MutualSizeOfRanked carries `params: vec![]` + rank>=1; any
1294        // other Lex shape (single-param mutual int-countdown, two-
1295        // param string-pos) fails this group's pre-conditions.
1296        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        // Robust tactic chain — `decreasing_tactic` alone bottoms out
1338        // on simple shapes (BigInt) but Lean elaborator on multi-arg
1339        // mutual SCCs sometimes needs `simp_wf` to unfold sizeOf
1340        // before omega can close the arithmetic on lengths. The
1341        // `simp only [AverMap.entries, AverMap.fromList]` step unfolds the
1342        // list-backed-Map identities so a `objectSafe(m)=entriesSafe(Map.entries m)`
1343        // delegation's goal reduces `sizeOf (AverMap.entries m)` to `sizeOf m`
1344        // (then the lex rank closes the same-size step); `AverMap.*` is always
1345        // in scope via the imported prelude, and `try` no-ops where absent.
1346        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            &params,
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}