Skip to main content

aver/codegen/proof_lower/
mod.rs

1//! Build `ProofIR` from a `CodegenContext`.
2//!
3//! The lowering producer: types live in `src/ir/proof_ir.rs`, this
4//! file fills them in from a typechecked + analysed codegen
5//! context. Output lands in `CodegenContext.proof_ir`; both proof
6//! backends read from the same field, so any classifier-side
7//! decision flows consistently to Lean and Dafny without each
8//! backend re-running shape detection.
9//!
10//! Populates three IR sections: `refined_types` (refinement-via-
11//! opaque records → Lean Subtype / Dafny subset type),
12//! `fn_contracts` (per-pure-fn recursion shape: native /
13//! sized-fuel / linear recurrence), and `law_theorems` (per-verify-
14//! law strategy + quantifier decomposition + claim shape, with
15//! Oracle-Lift'd impl-spec calls for effectful equivalence).
16//!
17//! `tests/proof_ir_diff.rs` pins the producer's output for each
18//! canonical source pattern — divergence between the classifier and
19//! the IR populator surfaces there.
20//!
21//! # Epic #170 Phase 7 invariant — AST discovery + typed identity
22//!
23//! This module is the **last consumer** of raw `crate::ast::Expr`
24//! patterns in the codegen layer. That is intentional, not
25//! migration debt.
26//!
27//! ## What's AST-shaped (syntax-discovery-only)
28//!
29//! Detector helpers in this file (`detect_*`, `walk_for_*`,
30//! `callee_matches_name`, `call_named_args`, `binary_call_var_const`,
31//! `matches_ident_expr`) walk `ast::Expr` directly. They are
32//! **pattern matchers** over source shape — they look for things
33//! like `match n { 0 -> base; _ -> rec(n - 1) }` or
34//! `Map.has(outer(m, k), k)` to decide which `ProofStrategy` /
35//! `RecursionPlan` variant lowers a given fn or law. The pattern
36//! belongs in source-shape; rewriting them on `ResolvedExpr` would
37//! be the same logic spelled in a different enum, no extra safety.
38//!
39//! Every detector helper carries a `syntax-discovery-only` comment
40//! at its definition.
41//!
42//! ## What's identity-sensitive (typed IDs)
43//!
44//! Decisions that depend on **which fn / type / ctor** a name
45//! refers to (not just "does this name appear") MUST go through
46//! `SymbolTable` or `ProofIR.refined_types` (`TypeId`-keyed) /
47//! `ProofIR.fn_contracts` (`FnId`-keyed). Examples:
48//!
49//! - Refinement-carrier lookups go through `find_refined_type` /
50//!   `resolve_refined_type_in_with_key`, both of which canonicalise
51//!   the name through the symbol table before reaching the IR map.
52//! - Fn-contract lookups go through `find_fn_contract_for_fn` —
53//!   pointer-eq scope on `&FnDef` resolves to the right `FnId`.
54//! - The Lean native-guarded rewriter pins target by `FnId` via
55//!   `rewrite_native_guarded_calls_resolved_expr` (PR 169).
56//!
57//! ## What stays raw-AST as a documented identity exception
58//!
59//! Builtin matchers (`callee_is X for X ∈ {"Bool.and", "Map.set",
60//! …}`) compare against the canonical builtin namespace, which is
61//! global by spec — no per-scope identity to leak. Verify-law
62//! callsites all walk `vb.fn_name` (entry-only by parser grammar);
63//! the `EntryFnIndex` newtype in `verify_law.rs` pins the
64//! entry-only contract at the type level (PR 177).
65//!
66//! Full `ResolvedProofLowerView` + semantic matcher API
67//! (`callee_is_builtin`, `callee_is_fn(FnId)`, `ctor_is`,
68//! `ident_name`, `int_lit`) deferred per
69//! `project_phase_e_scope_b_deferred` memory until a real trigger
70//! lands (module-scoped verify, dotted law targets, LSP rename,
71//! cross-scope inliner).
72
73use std::collections::{HashMap, HashSet};
74
75use crate::ast::{Expr, FnDef, Literal, Spanned, TopLevel, TypeDef};
76use crate::codegen::common::expr_to_dotted_name;
77use crate::codegen::recursion::RecursionPlan;
78use crate::codegen::{CodegenContext, ModuleInfo};
79use crate::ir::proof_ir::{
80    DecreaseProof, FnContract, Measure, NativeIntCountdownBody, Predicate, PreservationProof,
81    ProofIR, QuantifierType, RecursionContract, RefinedTypeDecl,
82};
83
84/// Backend-neutral view of the data `proof_lower` needs. Built once
85/// per lowering call; lets the pipeline pass it through without
86/// requiring a fully-assembled `CodegenContext` (which only exists
87/// after `build_context` runs). Legacy callers still build the view
88/// from `&CodegenContext` via [`ProofLowerInputs::from_ctx`].
89///
90/// All fields are borrows — the struct never owns memory; the pipeline
91/// and `build_context` both already own the data and just lend it.
92///
93/// Post-Step-7c: every helper the lowerer touches
94/// (`refinement_info_for`, `analyze_plans`, the `detect.rs` shape
95/// checkers) reads its inputs through this view. No more
96/// `&CodegenContext` reach-through — the struct stands on its own.
97pub struct ProofLowerInputs<'a> {
98    /// Entry-file top-level items, post-pipeline (TCO etc. applied).
99    pub entry_items: &'a [TopLevel],
100    /// Dependent modules already split into type/fn defs.
101    pub dep_modules: &'a [ModuleInfo],
102    /// Set of dep module prefix strings (e.g. `"Models.User"`).
103    pub module_prefixes: &'a HashSet<String>,
104    /// Recursive fn ids from the `analyze` pipeline stage. Keyed
105    /// by opaque [`crate::ir::FnId`] so entry+module same-bare-name
106    /// fns don't merge. Per-scope helpers below project back to
107    /// `HashSet<String>` for consumers that operate on a single
108    /// scope (the DAG invariant keeps bare-name unambiguous within
109    /// a scope).
110    pub recursive_fns: &'a HashSet<crate::ir::FnId>,
111    /// Resolved-identity table (#138 phase E). When `Some`, the
112    /// populate-side resolves `FnKey` / `TypeKey` to `FnId` /
113    /// `TypeId` once at the IR boundary and keys `ProofIR.fn_contracts`
114    /// / `ProofIR.refined_types` / `LawTheorem.fn_id` by the opaque
115    /// IDs. Callers that haven't wired in the symbol-table stage
116    /// pass `None` and fall through to legacy key-typed maps
117    /// (transitional during phase E migration).
118    pub symbol_table: &'a crate::ir::SymbolTable,
119    /// Optional `ProgramShape` substrate (Stage 6b of #232). When
120    /// `Some`, `refinement_info_for` reads from the typed
121    /// `ModulePattern::RefinementSmartConstructor` entries instead of
122    /// re-walking the AST. `None` keeps the legacy walk path —
123    /// preserved for test fixtures that build `ProofLowerInputs` by
124    /// hand without going through the pipeline.
125    pub program_shape: Option<&'a crate::analysis::shape::ProgramShape>,
126}
127
128impl<'a> ProofLowerInputs<'a> {
129    /// Build a view from a fully-assembled `CodegenContext` — used
130    /// by `refresh_facts` (test helper) and by any caller that
131    /// already owns a built context. Reads only the fields the
132    /// lowerer actually needs.
133    pub fn from_ctx(ctx: &'a CodegenContext) -> Self {
134        Self {
135            entry_items: &ctx.items,
136            dep_modules: &ctx.modules,
137            module_prefixes: &ctx.module_prefixes,
138            recursive_fns: &ctx.recursive_fns,
139            symbol_table: &ctx.symbol_table,
140            program_shape: ctx.program_shape.as_ref(),
141        }
142    }
143
144    /// All pure fn defs across entry items and dep modules, in walk
145    /// order (entry first, then deps). `is_pure_fn` lives in the
146    /// Lean toplevel module today; pure_fns reaches there since the
147    /// pure-ness criterion is the same for every proof backend.
148    pub fn pure_fns(&self) -> Vec<&'a FnDef> {
149        // Order matches the legacy `lean::pure_fns(ctx)`: deps first,
150        // entry last. `call_graph::ordered_fn_components` is order-
151        // sensitive (SCC discovery order changes which member is
152        // chosen as the representative); flipping the order shifted
153        // some classifications between fuel and "outside subset".
154        self.dep_modules
155            .iter()
156            .flat_map(|m| m.fn_defs.iter())
157            .chain(self.entry_items.iter().filter_map(|item| match item {
158                TopLevel::FnDef(fd) => Some(fd),
159                _ => None,
160            }))
161            .filter(|fd| crate::codegen::common::is_pure_fn(fd))
162            .collect()
163    }
164
165    /// Recursive pure fn names. Filters `recursive_fns` by pure-ness.
166    /// Returns bare names (pure_fns view is the whole program here,
167    /// so any FnId in `recursive_fns` that maps back to a pure fn
168    /// gets its bare name surfaced for downstream classifiers).
169    pub fn recursive_pure_fn_names(&self) -> HashSet<String> {
170        let symbols = self.symbol_table;
171        let pure_ids: HashSet<crate::ir::FnId> = self
172            .pure_fns()
173            .into_iter()
174            .filter_map(|fd| {
175                let scope = self
176                    .dep_modules
177                    .iter()
178                    .find(|m| m.fn_defs.iter().any(|d| std::ptr::eq(d, fd)))
179                    .map(|m| m.prefix.as_str());
180                // **syntax-discovery-only** (epic #170 Phase 8
181                // guardrail): scope was just resolved via pointer-eq
182                // against dep modules — the `None` arm is the
183                // correct entry-scope key by construction (same
184                // shape as `fn_key_for_decl` in `codegen::common`).
185                let key = match scope {
186                    Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
187                    None => crate::ir::FnKey::entry(&fd.name),
188                };
189                symbols.fn_id_of(&key)
190            })
191            .collect();
192        self.recursive_fns
193            .intersection(&pure_ids)
194            .map(|id| symbols.fn_entry(*id).key.name.clone())
195            .collect()
196    }
197
198    /// Pure fns restricted to a single scope: `None` = entry only,
199    /// `Some(prefix)` = the dep module with that prefix only. Aver's
200    /// module DAG invariant rules out cross-module recursion SCCs,
201    /// so per-scope classification is the canonical view —
202    /// `populate_fn_contracts` walks this per scope to give each
203    /// `Module.fn` its own canonical key in `ir.fn_contracts`
204    /// instead of letting two same-bare-name fns silently merge.
205    pub fn pure_fns_in_scope(&self, scope: Option<&str>) -> Vec<&'a FnDef> {
206        match scope {
207            None => self
208                .entry_items
209                .iter()
210                .filter_map(|item| match item {
211                    TopLevel::FnDef(fd) => Some(fd),
212                    _ => None,
213                })
214                .filter(|fd| crate::codegen::common::is_pure_fn(fd))
215                .collect(),
216            Some(prefix) => self
217                .dep_modules
218                .iter()
219                .filter(|m| m.prefix == prefix)
220                .flat_map(|m| m.fn_defs.iter())
221                .filter(|fd| crate::codegen::common::is_pure_fn(fd))
222                .collect(),
223        }
224    }
225
226    /// Recursive pure fn names restricted to a single scope. Filters
227    /// the FnId-keyed `recursive_fns` to the ones whose canonical
228    /// scope matches `scope`, then projects back to bare names for
229    /// scope-local consumers (DAG invariant keeps bare-name
230    /// unambiguous within a single scope).
231    pub fn recursive_pure_fn_names_in_scope(&self, scope: Option<&str>) -> HashSet<String> {
232        let symbols = self.symbol_table;
233        let pure_ids: HashSet<crate::ir::FnId> = self
234            .pure_fns_in_scope(scope)
235            .into_iter()
236            .filter_map(|fd| {
237                // **syntax-discovery-only** (epic #170 Phase 8
238                // guardrail): scope is the caller's stated scope —
239                // `None` = entry, `Some(prefix)` = dep module. Both
240                // arms below are the correct key for the matching
241                // arm; bare-name keying is safe because the caller
242                // has already narrowed to a single scope.
243                let key = match scope {
244                    Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
245                    None => crate::ir::FnKey::entry(&fd.name),
246                };
247                symbols.fn_id_of(&key)
248            })
249            .collect();
250        self.recursive_fns
251            .intersection(&pure_ids)
252            .map(|id| symbols.fn_entry(*id).key.name.clone())
253            .collect()
254    }
255
256    /// Iterator over (`None` = entry, `Some(prefix)` = each dep
257    /// module) — drives `populate_fn_contracts`'s per-scope walk.
258    pub fn scopes(&self) -> Vec<Option<String>> {
259        let mut out = vec![None];
260        for m in self.dep_modules {
261            out.push(Some(m.prefix.clone()));
262        }
263        out
264    }
265
266    /// Scope of the dep module that owns `fd`, or `None` for entry
267    /// module fns. Pointer-eq match against `dep_modules`, mirroring
268    /// `crate::codegen::common::fn_owning_scope_for` but reading off
269    /// the lowering view (which doesn't carry a full `CodegenContext`).
270    pub fn fn_owning_scope(&self, fd: &FnDef) -> Option<&'a str> {
271        for m in self.dep_modules {
272            for f in &m.fn_defs {
273                if std::ptr::eq(f, fd) {
274                    return Some(m.prefix.as_str());
275                }
276            }
277        }
278        None
279    }
280
281    /// Resolve a raw-AST expression to its `ResolvedExpr` form under
282    /// the given scope. ProofIR stores resolved expressions (Phase E
283    /// PR 12 Scope A), so this helper is called at every producer
284    /// site that lifts a `Spanned<crate::ast::Expr>` slice from the
285    /// source into an IR field. Mirrors
286    /// `CodegenContext::resolve_expr` but reads only the
287    /// `symbol_table` carried on this view — proof lowering runs
288    /// inside the pipeline, before a full `CodegenContext` exists.
289    pub fn resolve_expr(
290        &self,
291        expr: &crate::ast::Spanned<crate::ast::Expr>,
292        scope: Option<&str>,
293    ) -> crate::ast::Spanned<crate::ir::hir::ResolvedExpr> {
294        use crate::ir::hir::{ResolveCtx, ResolvedStmt};
295        let mut rctx = ResolveCtx::new(self.symbol_table);
296        rctx.current_module = scope.map(String::from);
297        let stmt = crate::ast::Stmt::Expr(expr.clone());
298        match crate::ir::hir::resolve::resolve_stmt_external(&rctx, &stmt) {
299            ResolvedStmt::Expr(s) => s,
300            ResolvedStmt::Binding { value, .. } => value,
301        }
302    }
303
304    /// Names of every recursive user-defined type across entry + deps.
305    pub fn recursive_type_names(&self) -> HashSet<String> {
306        self.entry_items
307            .iter()
308            .filter_map(|item| match item {
309                TopLevel::TypeDef(td) => Some(td),
310                _ => None,
311            })
312            .chain(self.dep_modules.iter().flat_map(|m| m.type_defs.iter()))
313            .filter(|td| crate::codegen::common::is_recursive_type_def(td))
314            .map(|td| crate::codegen::common::type_def_name(td).to_string())
315            .collect()
316    }
317
318    /// Find a fn def by name across entry + deps. Falls back to the
319    /// last segment of a dotted call (e.g. `Module.fn` resolves to
320    /// `fn` when no exact-match candidate exists).
321    pub fn find_fn_def_by_call_name(&self, call_name: &str) -> Option<&'a FnDef> {
322        let find_exact = |name: &str| -> Option<&'a FnDef> {
323            self.dep_modules
324                .iter()
325                .flat_map(|m| m.fn_defs.iter())
326                .chain(self.entry_items.iter().filter_map(|item| match item {
327                    TopLevel::FnDef(fd) => Some(fd),
328                    _ => None,
329                }))
330                .find(|fd| fd.name == name)
331        };
332        find_exact(call_name).or_else(|| {
333            let short = call_name.rsplit('.').next()?;
334            find_exact(short)
335        })
336    }
337
338    /// Find a type def by bare name across entry + deps. None on miss
339    /// or when the name resolves to a non-Product / non-Sum shape.
340    pub fn find_type_def(&self, type_name: &str) -> Option<&'a TypeDef> {
341        self.entry_items
342            .iter()
343            .filter_map(|item| match item {
344                TopLevel::TypeDef(td) => Some(td),
345                _ => None,
346            })
347            .chain(self.dep_modules.iter().flat_map(|m| m.type_defs.iter()))
348            .find(|td| crate::codegen::common::type_def_name(td) == type_name)
349    }
350}
351
352/// Run every proof-export lowering in one shot — convenience for
353/// callers that want a fully-populated ProofIR. The pipeline calls
354/// the three `populate_*` fns directly so it can run them as
355/// independent stages and short-circuit on typecheck failure.
356pub fn lower(inputs: &ProofLowerInputs) -> ProofIR {
357    let mut ir = ProofIR::default();
358    populate_refined_types(inputs, &mut ir);
359    populate_fn_contracts(inputs, &mut ir);
360    populate_law_theorems(inputs, &mut ir);
361    ir
362}
363
364/// Refinement-via-opaque lift. Walks every type definition (entry +
365/// dep modules), classifies the records that pair a single carrier
366/// field with a validating smart constructor, and emits
367/// `RefinedTypeDecl` entries into `ir.refined_types`. Backends
368/// (Lean → Subtype, Dafny → subset type) render these directly.
369pub fn populate_refined_types(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
370    // Walk entry items first, then dep modules. The map is keyed by
371    // opaque `TypeId` resolved through the symbol table — same
372    // collision-safe shape as `fn_contracts: HashMap<FnId, _>`. The
373    // typechecker explicitly permits two modules to expose distinct
374    // types of the same bare name (`A.Shape` vs `B.Shape`; see
375    // `tests/typechecker_spec::cross_module_same_named_types_do_not_
376    // merge`); opaque IDs make their predicates impossible to merge
377    // by construction. Producer resolves `TypeKey -> TypeId` once
378    // here; consumers (`find_refined_type_scoped`) resolve through
379    // the same symbol table at lookup time.
380    //
381    // SymbolTable is always present (`ProofLowerInputs.symbol_table`
382    // is `&SymbolTable`, not `Option<&_>` — the pipeline builds it
383    // unconditionally). Synthetic-ctx callers (test helpers) thread
384    // their own through `from_ctx` / direct construction.
385    let symbols = inputs.symbol_table;
386
387    let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
388        TopLevel::TypeDef(td) => Some((None::<&str>, td)),
389        _ => None,
390    });
391    let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
392        m.type_defs
393            .iter()
394            .map(move |td| (Some(m.prefix.as_str()), td))
395    });
396
397    for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
398        let TypeDef::Product { name, fields, .. } = td else {
399            continue;
400        };
401        if fields.len() != 1 {
402            continue;
403        }
404        let type_key = match module_prefix {
405            Some(prefix) => crate::ir::TypeKey::in_module(prefix.to_string(), name),
406            None => crate::ir::TypeKey::entry(name),
407        };
408        let Some(canonical_key) = symbols.type_id_of(&type_key) else {
409            // Type isn't in the symbol table — built-ins (Result.Ok
410            // etc.) are excluded by construction; for user types
411            // this is a wiring bug surfaced via the symbol-table
412            // builder, so just skip.
413            continue;
414        };
415        if ir.refined_types.contains_key(&canonical_key) {
416            // Same TypeId already populated — possible if a module
417            // is walked twice through dep aliasing. Skip so we don't
418            // overwrite a verified-witness entry with a predicate-
419            // eval fallback witness.
420            continue;
421        }
422        // Scope the smart-constructor lookup to the same module the
423        // record lives in. Refinement-via-opaque keeps the record
424        // opaque (`exposes opaque [X]`); a smart constructor in any
425        // other module couldn't reach the carrier field anyway.
426        // Without the scope, two modules each declaring a `Natural`
427        // with different predicates would both pick up whichever
428        // smart constructor walked first.
429        let Some(info) =
430            crate::codegen::common::refinement_info_for_in_scope(name, inputs, module_prefix)
431        else {
432            continue;
433        };
434        let invariant = Predicate {
435            free_vars: vec![(
436                info.param_name.to_string(),
437                crate::ir::proof_ir::QuantifierType::Plain(info.carrier_type.to_string()),
438            )],
439            expr: inputs.resolve_expr(info.predicate, module_prefix),
440        };
441        let witness = pick_witness(
442            name,
443            canonical_key,
444            inputs,
445            info.predicate,
446            info.param_name,
447            module_prefix,
448        );
449        // Round-4 finding 1: a `None` witness means we couldn't
450        // exhibit any inhabitant satisfying the predicate. Inserting
451        // the slot anyway makes Dafny silently fall back to
452        // `witness 0` even when the predicate excludes 0 — producing
453        // an unsound subset type. Skip the lift entirely: the
454        // backend will emit a plain `datatype` instead, which is
455        // honest about the missing invariant. The pure-fn / law
456        // paths still typecheck against the plain record.
457        let Some(witness) = witness else {
458            continue;
459        };
460        ir.refined_types.insert(
461            canonical_key,
462            RefinedTypeDecl {
463                name: name.clone(),
464                carrier_type: info.carrier_type.to_string(),
465                carrier_field: info.carrier_field.to_string(),
466                predicate_param: info.param_name.to_string(),
467                invariant,
468                witness: Some(witness),
469            },
470        );
471    }
472}
473
474/// Walk `analyze_plans(inputs)` and populate `ProofIR.fn_contracts`.
475///
476/// Translation pass over the classifier output (`RecursionPlan`) —
477/// no re-implementation. The diff test (`tests/proof_ir_diff.rs`)
478/// pins what each `RecursionPlan` variant lowers to so divergence
479/// between the classifier and the IR populator surfaces there.
480/// Coverage today: `IntCountdownGuarded`, `LinearRecurrence2`,
481/// `Sized*` (length / sizeOf / string-pos / int-ascending). Fuel-
482/// only and Mutual* plans don't materialise as `FnContract` (their
483/// recursion shape doesn't need IR-level pre-decisions; backends
484/// emit fuel scaffolding inline).
485pub fn populate_fn_contracts(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
486    // Round-5 finding: walk per-scope so two modules each with a
487    // recursive `foo` (or entry + module both declaring `foo`)
488    // don't collide on the bare-name `plans: HashMap<String, _>`.
489    // Aver's module DAG invariant rules out cross-module recursion
490    // SCCs, so per-scope classification is the canonical view and
491    // each `Module.fn` gets its own slot in `ir.fn_contracts`.
492    for scope in inputs.scopes() {
493        let (plans, issues) =
494            crate::codegen::recursion::analyze_plans_in_scope(inputs, scope.as_deref(), false);
495        ir.unclassified_fns
496            .extend(issues.into_iter().map(|issue| crate::ir::UnclassifiedFn {
497                line: issue.line,
498                message: issue.message,
499            }));
500        populate_fn_contracts_for_scope(inputs, ir, scope.as_deref(), &plans);
501    }
502}
503
504fn populate_fn_contracts_for_scope(
505    inputs: &ProofLowerInputs,
506    ir: &mut ProofIR,
507    scope: Option<&str>,
508    plans: &HashMap<String, RecursionPlan>,
509) {
510    let scoped_fns: Vec<&FnDef> = inputs.pure_fns_in_scope(scope);
511    let qualify = |bare: &str| -> crate::ir::FnKey {
512        match scope {
513            Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), bare),
514            None => crate::ir::FnKey::entry(bare),
515        }
516    };
517    // Contracts key by opaque `FnId`; SymbolTable is always present
518    // (pipeline builds it unconditionally, `ProofLowerInputs.symbol_
519    // table: &SymbolTable`).
520    let symbols = inputs.symbol_table;
521
522    for (fn_name, plan) in plans {
523        let Some(fd) = scoped_fns.iter().find(|fd| fd.name == *fn_name) else {
524            continue;
525        };
526        let fn_key = qualify(fn_name);
527        let Some(canonical_key) = symbols.fn_id_of(&fn_key) else {
528            continue;
529        };
530
531        // IntCountdown — fuel-encoded countdown on a single Int param.
532        // Distinct from IntCountdownGuarded: external callers may pass
533        // negatives (the classifier rejected closed-world status), so
534        // backends emit a fuel helper with `n.natAbs + 1` initial fuel
535        // rather than a native def with a precondition.
536        if let RecursionPlan::IntCountdown { param_index } = plan {
537            if let Some((param_name, _)) = fd.params.get(*param_index) {
538                ir.fn_contracts.insert(
539                    canonical_key,
540                    FnContract {
541                        source_name: fn_name.clone(),
542                        recursion: Some(RecursionContract::Fuel {
543                            fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne {
544                                param: param_name.clone(),
545                            },
546                        }),
547                    },
548                );
549            }
550            continue;
551        }
552
553        // IntFloorDivCountdown — guard-validated literal-divisor
554        // floor-division shrink. The classifier proved both
555        // side-conditions (every self-call shrinks the param through
556        // `Result.withDefault(Int.div(p, k), d)` with literal k >= 2,
557        // and every self-call site's guard chain implies `p >= 1`),
558        // so backends emit a native well-founded def on `p.toNat`.
559        if let RecursionPlan::IntFloorDivCountdown {
560            param_index,
561            divisor,
562            helper_fn,
563        } = plan
564        {
565            if let Some((param_name, _)) = fd.params.get(*param_index) {
566                ir.fn_contracts.insert(
567                    canonical_key,
568                    FnContract {
569                        source_name: fn_name.clone(),
570                        recursion: Some(RecursionContract::WellFoundedToNat {
571                            param: param_name.clone(),
572                            floor_div: Some(crate::ir::FloorDivShrink {
573                                divisor: *divisor,
574                                helper_fn: helper_fn.clone(),
575                            }),
576                        }),
577                    },
578                );
579            }
580            continue;
581        }
582
583        // IntAscending — fuel formula `(bound - n).natAbs + 1`. The
584        // bound stays as `Spanned<Expr>` so backends render it through
585        // their own emitters (it can be a literal, a fn param, or a
586        // small arith expression).
587        if let RecursionPlan::IntAscending { param_index, bound } = plan {
588            if let Some((param_name, _)) = fd.params.get(*param_index) {
589                ir.fn_contracts.insert(
590                    canonical_key,
591                    FnContract {
592                        source_name: fn_name.clone(),
593                        recursion: Some(RecursionContract::Fuel {
594                            fuel_metric: crate::ir::FuelMetric::BoundMinusParamNatAbsPlusOne {
595                                param: param_name.clone(),
596                                bound: inputs.resolve_expr(bound, scope),
597                            },
598                        }),
599                    },
600                );
601            }
602            continue;
603        }
604
605        // ListStructural — structural recursion on a List<_> param.
606        // Lean/Dafny don't actually use a fuel helper for this on
607        // recent backends (structural recursion is natively
608        // terminating); the metric stays as `SeqLenPlusOne` for
609        // backend-symmetric framing, and the consumer ignores it
610        // when emitting plain structural recursion.
611        if let RecursionPlan::ListStructural { param_index } = plan {
612            if let Some((param_name, _)) = fd.params.get(*param_index) {
613                ir.fn_contracts.insert(
614                    canonical_key,
615                    FnContract {
616                        source_name: fn_name.clone(),
617                        recursion: Some(RecursionContract::Fuel {
618                            fuel_metric: crate::ir::FuelMetric::SeqLenPlusOne {
619                                param: param_name.clone(),
620                            },
621                        }),
622                    },
623                );
624            }
625            continue;
626        }
627
628        // SizeOfStructural — recursion on a user ADT (e.g. an AST
629        // type). Fuel metric `sizeOf(call_frame) + 1`. The classifier
630        // doesn't pin a single bound param — `sizeOf` measures the
631        // whole frame — so the IR variant carries no param name.
632        if matches!(plan, RecursionPlan::SizeOfStructural) {
633            ir.fn_contracts.insert(
634                canonical_key,
635                FnContract {
636                    source_name: fn_name.clone(),
637                    recursion: Some(RecursionContract::Fuel {
638                        fuel_metric: crate::ir::FuelMetric::SizeOfPlusOne,
639                    }),
640                },
641            );
642            continue;
643        }
644
645        // StringPosAdvance — `(s, pos)`-shape recursion: `s` invariant
646        // (first param, String), `pos` advances (second param, Int).
647        // Fuel formula `s.length - pos`.
648        if matches!(plan, RecursionPlan::StringPosAdvance) {
649            if let (Some((string_param, _)), Some((pos_param, _))) =
650                (fd.params.first(), fd.params.get(1))
651            {
652                ir.fn_contracts.insert(
653                    canonical_key,
654                    FnContract {
655                        source_name: fn_name.clone(),
656                        recursion: Some(RecursionContract::Fuel {
657                            fuel_metric: crate::ir::FuelMetric::StringLenMinusPos {
658                                string_param: string_param.clone(),
659                                pos_param: pos_param.clone(),
660                            },
661                        }),
662                    },
663                );
664            }
665            continue;
666        }
667
668        // Mutual-recursion SCCs — each member of the SCC gets its own
669        // plan with the same family. All three lower to a Lex fuel
670        // metric; the params vector + rank distinguish per-shape /
671        // per-member roles.
672        //
673        // - MutualIntCountdown: every member counts down its first
674        //   Int param; rank stays 0 (no inter-member ranking — every
675        //   edge decreases the shared dimension).
676        // - MutualStringPosAdvance { rank }: (s, pos) shape across
677        //   the SCC; rank distinguishes members for same-measure
678        //   inter-fn edges.
679        // - MutualSizeOfRanked { rank }: sizeOf measures the whole
680        //   call frame; rank distinguishes members. No bound param —
681        //   the empty params vec signals "frame-level measure".
682        match plan {
683            RecursionPlan::MutualIntCountdown => {
684                let params = fd
685                    .params
686                    .first()
687                    .map(|(n, _)| vec![n.clone()])
688                    .unwrap_or_default();
689                ir.fn_contracts.insert(
690                    canonical_key,
691                    FnContract {
692                        source_name: fn_name.clone(),
693                        recursion: Some(RecursionContract::Fuel {
694                            fuel_metric: crate::ir::FuelMetric::Lex { params, rank: 0 },
695                        }),
696                    },
697                );
698                continue;
699            }
700            RecursionPlan::MutualStringPosAdvance { rank } => {
701                let params = fd.params.iter().take(2).map(|(n, _)| n.clone()).collect();
702                ir.fn_contracts.insert(
703                    canonical_key,
704                    FnContract {
705                        source_name: fn_name.clone(),
706                        recursion: Some(RecursionContract::Fuel {
707                            fuel_metric: crate::ir::FuelMetric::Lex {
708                                params,
709                                rank: *rank,
710                            },
711                        }),
712                    },
713                );
714                continue;
715            }
716            RecursionPlan::MutualSizeOfRanked { rank } => {
717                ir.fn_contracts.insert(
718                    canonical_key,
719                    FnContract {
720                        source_name: fn_name.clone(),
721                        recursion: Some(RecursionContract::Fuel {
722                            fuel_metric: crate::ir::FuelMetric::Lex {
723                                params: Vec::new(),
724                                rank: *rank,
725                            },
726                        }),
727                    },
728                );
729                continue;
730            }
731            RecursionPlan::LinearRecurrence2 => {
732                ir.fn_contracts.insert(
733                    canonical_key,
734                    FnContract {
735                        source_name: fn_name.clone(),
736                        recursion: Some(RecursionContract::LinearRecurrence2),
737                    },
738                );
739                continue;
740            }
741            _ => {}
742        }
743
744        let RecursionPlan::IntCountdownGuarded {
745            param_index,
746            base_arm_literal,
747            base_arm_body,
748            wildcard_arm_body,
749            precondition,
750        } = plan
751        else {
752            continue;
753        };
754        let Some((countdown_param_name, _)) = fd.params.get(*param_index) else {
755            continue;
756        };
757
758        let precondition_predicates: Vec<Predicate> = precondition
759            .iter()
760            .map(|clause| Predicate {
761                free_vars: vec![(
762                    countdown_param_name.clone(),
763                    QuantifierType::Plain("Int".to_string()),
764                )],
765                expr: inputs.resolve_expr(clause, scope),
766            })
767            .collect();
768
769        ir.fn_contracts.insert(
770            canonical_key,
771            FnContract {
772                source_name: fn_name.clone(),
773                recursion: Some(RecursionContract::Native {
774                    precondition: precondition_predicates,
775                    measure: Measure::NatAbsInt {
776                        param: countdown_param_name.clone(),
777                    },
778                    preservation: PreservationProof::IntCountdownLiteralZero,
779                    decrease: DecreaseProof::NatAbsCountdown,
780                    body: NativeIntCountdownBody {
781                        base_arm_literal: *base_arm_literal,
782                        base_arm_body: inputs.resolve_expr(base_arm_body, scope),
783                        wildcard_arm_body: inputs.resolve_expr(wildcard_arm_body, scope),
784                    },
785                }),
786            },
787        );
788    }
789}
790
791/// Walk every verify block, lift `VerifyKind::Law` entries into
792/// `ProofIR.law_theorems`.
793///
794/// Extracts the law's shape (quantifiers from `givens`, premises
795/// from `when`, claim from `lhs == rhs`) and pins a `ProofStrategy`
796/// via [`classify_law_strategy`]. Covered strategies: Reflexive,
797/// Commutative / Associative / IdentityElement / AntiCommutative /
798/// UnaryEqualsBinary (arithmetic wrappers), Induction (recursive
799/// ADTs), LibraryAxiom (Map set/get), MapUpdatePostcondition,
800/// MapKeyTrackedIncrement, SpecEquivalence{,SimpNormalized},
801/// LinearIntSpecEquivalence, EffectfulSpecEquivalence (with Oracle
802/// Lift), LinearArithmetic (catch-all over an unfold chain).
803/// Unmatched shapes pin `BackendDispatch` and fall through to the
804/// backend's residual chain (linear_recurrence2 emit + sampled /
805/// guarded-domain fallback).
806pub fn populate_law_theorems(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
807    use crate::ast::{TopLevel, VerifyKind};
808    use crate::ir::{LawTheorem, Predicate, Quantifier, QuantifierType};
809
810    let symbols = inputs.symbol_table;
811
812    let entry_verifies = inputs.entry_items.iter().filter_map(|item| match item {
813        TopLevel::Verify(vb) => Some(vb),
814        _ => None,
815    });
816    // Dep modules don't expose verify blocks today (ModuleInfo carries
817    // type_defs + fn_defs only), so the walk stays entry-side. When
818    // ModuleInfo gains a `verify_blocks` field, extend here.
819    for vb in entry_verifies {
820        let VerifyKind::Law(law) = &vb.kind else {
821            continue;
822        };
823
824        let quantifiers: Vec<Quantifier> = law
825            .givens
826            .iter()
827            .map(|g| Quantifier {
828                name: g.name.clone(),
829                binder_type: QuantifierType::Plain(g.type_name.clone()),
830            })
831            .collect();
832
833        // Scope for resolving the law's expressions: derived from the
834        // target fn's owning module, NOT hardcoded to entry. Today
835        // laws-in-modules isn't shipped, so the lookup falls back to
836        // entry for every fn; once dep modules carry their own verify
837        // blocks (open follow-up), the same resolution path serves
838        // both. Avoids re-introducing the "scope=None means entry"
839        // assumption the rest of phase E worked to eliminate.
840        let law_scope: Option<String> = symbols
841            .fn_id_of(&crate::ir::FnKey::entry(&vb.fn_name))
842            .or_else(|| {
843                inputs.dep_modules.iter().find_map(|m| {
844                    symbols.fn_id_of(&crate::ir::FnKey::in_module(m.prefix.clone(), &vb.fn_name))
845                })
846            })
847            .and_then(|id| symbols.fn_entry(id).key.scope_str().map(|s| s.to_string()));
848        let law_scope_ref = law_scope.as_deref();
849
850        let premises: Vec<Predicate> = match &law.when {
851            Some(when_expr) => vec![Predicate {
852                free_vars: quantifiers
853                    .iter()
854                    .map(|q| (q.name.clone(), q.binder_type.clone()))
855                    .collect(),
856                expr: inputs.resolve_expr(when_expr, law_scope_ref),
857            }],
858            None => Vec::new(),
859        };
860
861        let strategy = classify_law_strategy(
862            law,
863            &vb.fn_name,
864            inputs,
865            &ir.refined_types,
866            &ir.fn_contracts,
867            law_scope_ref,
868        );
869
870        // Verify laws are entry-only per current model — see
871        // `LawTheorem.fn_id` doc. The bare `vb.fn_name` resolves
872        // through the symbol table to an entry-scope `FnId`; when
873        // the fn isn't in the symbol table (verify block targeting
874        // a fn that doesn't exist), skip the law silently — the
875        // typechecker / verify-driver surfaces the missing target
876        // elsewhere.
877        let Some(fn_id) = symbols.fn_id_of(&crate::ir::FnKey::entry(&vb.fn_name)) else {
878            continue;
879        };
880        ir.law_theorems.push(LawTheorem {
881            fn_id,
882            law_name: law.name.clone(),
883            quantifiers,
884            premises,
885            claim_lhs: inputs.resolve_expr(&law.lhs, law_scope_ref),
886            claim_rhs: inputs.resolve_expr(&law.rhs, law_scope_ref),
887            strategy,
888        });
889    }
890
891    // Demand-driven well-founded graduation for the floor-division
892    // window family: the figures' proof templates rest on the
893    // power-of-two fn's defining equations and functional-induction
894    // principle, which the fuel encoding destroys (the fuel arg on
895    // the recursive call differs from the callee's own measure, so
896    // nothing universal is provable through `__fuel`). Upgrade the
897    // cited pow fn's contract from `Fuel { NatAbsPlusOne }` to the
898    // native `WellFoundedToNat` form (`floor_div: None` — the guarded
899    // subtractive countdown whose `n <= 0` base guard puts `n >= 1`
900    // in the decreasing goal's context, so `omega` closes the
901    // measure bare). Scoped on purpose: a pow-shaped fn in a file
902    // with no recognized window law keeps its established fuel
903    // emission, so nothing outside the family moves.
904    let window_pow_fns: HashSet<String> = ir
905        .law_theorems
906        .iter()
907        .filter_map(|t| match &t.strategy {
908            crate::ir::ProofStrategy::FloorDivWindow { figure } => Some(match figure {
909                crate::ir::FloorWindowFigure::PowPositive { pow_fn } => pow_fn.clone(),
910                crate::ir::FloorWindowFigure::PowSumSplit { pow_fn } => pow_fn.clone(),
911                crate::ir::FloorWindowFigure::SigWindow { pow_fn, .. } => pow_fn.clone(),
912                crate::ir::FloorWindowFigure::ProductWindow { pow_fn, .. } => pow_fn.clone(),
913            }),
914            _ => None,
915        })
916        .collect();
917    for pow_fn in window_pow_fns {
918        let Some(fn_id) = symbols.fn_id_of(&crate::ir::FnKey::entry(&pow_fn)) else {
919            continue;
920        };
921        let Some(contract) = ir.fn_contracts.get_mut(&fn_id) else {
922            continue;
923        };
924        if let Some(crate::ir::RecursionContract::Fuel {
925            fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne { param },
926        }) = &contract.recursion
927        {
928            contract.recursion = Some(crate::ir::RecursionContract::WellFoundedToNat {
929                param: param.clone(),
930                floor_div: None,
931            });
932        }
933    }
934}
935
936/// Pick the strategy `LawLower` should pin on a `(fn, law)` pair.
937///
938/// Decision order — specific algebraic properties first, then
939/// generic linear-arithmetic catch-all, then `BackendDispatch`:
940/// 1. `Reflexive` — `law.lhs ≡ law.rhs` syntactically.
941/// 2. `Commutative { op }` — fn body is `a <op> b`, claim is
942///    `f(a, b) = f(b, a)` (op restricted to commutative ones).
943/// 3. `Associative { op }` — same body, 3 givens, assoc claim.
944/// 4. `IdentityElement { op }` — `f(a, e) = a` (or `f(e, a) = a`),
945///    where `e` is the op's identity. Covers Add/Mul both-sided
946///    plus Sub right-sided.
947/// 5. `AntiCommutative { op: Sub, neg_on_rhs }` — `f(a, b) =
948///    -f(b, a)` form. Sub-only (Mul has no anti-commutative law).
949/// 6. `UnaryEqualsBinary { inner_fn }` — outer fn is unary, claim
950///    binds it to the inner binary fn at a constant.
951/// 7. `LinearArithmetic { unfold_fns, ... }` — catch-all when the
952///    law reduces to linear arith after unfolding the call chain.
953/// 8. `EnumConstantFold { unfold_fns }` — ground law over fixed
954///    enum/ADT constructor args, scalar return (#466).
955/// 9. `FiniteDomainCases { givens }` — every given ranges over a
956///    closed finite domain (Bool / fieldless enum, product ≤ 16);
957///    closes by exhaustive `cases` enumeration.
958/// 10. `RingIdentity { unfold_fns }` — unconditional ring identity
959///     over Int-component records (cross-multiplication equality);
960///     runs before the prelude-simp rung, which would otherwise claim
961///     the shape and park it on a caught sorry.
962/// 11. `IntDecimalRoundtrip { … }` — canonical decimal-Int
963///     parse/serialize roundtrip over a recognized string-pos scanner;
964///     runs before the prelude-simp rung, which would otherwise claim
965///     the shape and park it on a caught sorry.
966/// 12. `SimpOverPreludeLemmas { … }` — builtin-roundtrip shape; the
967///     Lean backend renders it AFTER its legacy chain, so it fires
968///     exactly where the bare-`sorry` universal used to.
969/// 13. `BackendDispatch` — backend's ad-hoc chain decides.
970///
971/// (The induction/spec-equivalence/Map families detected between
972/// these rungs are documented at their detector sites below.)
973fn classify_law_strategy(
974    law: &crate::ast::VerifyLaw,
975    fn_name: &str,
976    inputs: &ProofLowerInputs,
977    refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
978    fn_contracts: &std::collections::HashMap<crate::ir::FnId, crate::ir::FnContract>,
979    scope: Option<&str>,
980) -> crate::ir::ProofStrategy {
981    use crate::ir::ProofStrategy;
982
983    // Match-dispatcher fold equivalence (stage 8c of #232) — two
984    // self-recursive `MatchDispatcherFold` fns over the same list
985    // param. Closes by structural induction on `xs` + `omega` on
986    // each arm.
987    if law.when.is_none()
988        && let Some(s) = detect_match_dispatcher_fold_equivalence(law, fn_name, inputs)
989    {
990        return s;
991    }
992    // Result-pipeline chain equivalence (stage 8b of #232) — `?`
993    // propagation `chain_qm(x)` vs nested-match `chain_manual(x)`.
994    // Both sides unfold to the same nested match; the proof closes
995    // by `unfold + repeat split`.
996    if law.when.is_none()
997        && let Some(s) = detect_result_pipeline_chain_equivalence(law, fn_name, inputs)
998    {
999        return s;
1000    }
1001    // Wrapper-over-recursion with monoidal accumulator (stage 8 of
1002    // #232) — runs before generic induction because its aux-lemma
1003    // template closes laws naive induction can't (e.g. `sum(xs) ==
1004    // sumDirect(xs)` where `sum(xs) = sumTR(xs, 0)`). Detected
1005    // when `fn_name` is registered as a `WrapperOverRecursion`
1006    // pattern in `ProgramShape` AND the law shape is
1007    // `wrapper(g) == other(g)` AND the inner fn body matches the
1008    // monoidal-accumulator template.
1009    if law.when.is_none()
1010        && let Some(s) = detect_wrapper_over_recursion(law, fn_name, inputs)
1011    {
1012        return s;
1013    }
1014    // Structural induction runs first — when any given binds a
1015    // recursive ADT, induction over its variants is the canonical
1016    // proof. Reflexive could also fire on `f(t) = f(t)` for `t: Tree`
1017    // but induction subsumes (one trivial case per variant) and is
1018    // the legacy chain's first pick. `when` clauses block induction
1019    // — a non-closing `when` law would emit a 2-arm induction ladder
1020    // (2 sorries) instead of the bounded sampled-domain fallback,
1021    // regressing output cleanliness; a non-regressing when-aware
1022    // induction path is a follow-up.
1023    if law.when.is_none()
1024        && let Some(param) = detect_induction_target(law, inputs)
1025    {
1026        return ProofStrategy::Induction { param };
1027    }
1028    if law.lhs == law.rhs {
1029        return ProofStrategy::Reflexive;
1030    }
1031    // Binary-wrapper-shaped laws first. `wrapper_binop` returns
1032    // `None` for non-binary fns — unary wrappers are tried after
1033    // this block falls through.
1034    if let Some(op) = wrapper_binop(fn_name, inputs) {
1035        if detect_wrapper_commutative(law, fn_name, op) {
1036            return ProofStrategy::Commutative { op };
1037        }
1038        if detect_wrapper_associative(law, fn_name, op) {
1039            return ProofStrategy::Associative { op };
1040        }
1041        if detect_wrapper_identity(law, fn_name, op) {
1042            return ProofStrategy::IdentityElement { op };
1043        }
1044        // Sub right-identity collapses into IdentityElement —
1045        // same emit (`simp [fn]`), different lhs/rhs shape. The
1046        // detector validates the right-side `f(a, 0) = a` form
1047        // (`f(0, a) = -a` doesn't equal `a`, so Sub is one-sided).
1048        if matches!(op, crate::ast::BinOp::Sub) && detect_wrapper_sub_right_identity(law, fn_name) {
1049            return ProofStrategy::IdentityElement { op };
1050        }
1051        // Anti-commutative is Sub-specific (Add/Mul are
1052        // commutative, no anti-commutativity). The op tag keeps
1053        // it parameterised even though only Sub currently fires.
1054        if matches!(op, crate::ast::BinOp::Sub)
1055            && let Some(neg_on_rhs) = detect_wrapper_sub_anti_commutative(law, fn_name)
1056        {
1057            return ProofStrategy::AntiCommutative { op, neg_on_rhs };
1058        }
1059    }
1060    // Unary fn equal to binary fn at a constant — `fn_name` is the
1061    // unary outer; the binary fn name is captured for backends.
1062    if let Some(inner_fn) = detect_wrapper_unary_equivalence(law, fn_name, inputs) {
1063        return ProofStrategy::UnaryEqualsBinary { inner_fn };
1064    }
1065    // Library axiom instances — Map.has-after-set, Map.get-after-set.
1066    // Specific shape, single-line `simpa using axiom` emit on Lean.
1067    if let Some((axiom, args)) = detect_map_set_axiom(law) {
1068        let resolved_args: Vec<_> = args.iter().map(|a| inputs.resolve_expr(a, scope)).collect();
1069        return ProofStrategy::LibraryAxiom {
1070            axiom,
1071            args: resolved_args,
1072        };
1073    }
1074    // Tracked-counter increment: specialised body template + `+ 1`
1075    // rhs. Checked before the more general MapUpdatePostcondition so
1076    // the tighter strategy wins for this shape.
1077    if let Some(inc) = detect_map_key_tracked_increment(law, fn_name, inputs) {
1078        return ProofStrategy::MapKeyTrackedIncrement {
1079            outer_fn: inc.outer_fn,
1080            map_arg: inputs.resolve_expr(&inc.map_arg, scope),
1081            key_arg: inputs.resolve_expr(&inc.key_arg, scope),
1082        };
1083    }
1084    // Post-condition of an inline-defined map-update fn — case-split
1085    // over `Map.get m k` and apply the `Map.set` axioms.
1086    if let Some(post) = detect_map_update_postcondition(law, fn_name, inputs) {
1087        return ProofStrategy::MapUpdatePostcondition {
1088            outer_fn: post.outer_fn,
1089            kind: post.kind,
1090            map_arg: inputs.resolve_expr(&post.map_arg, scope),
1091            key_arg: inputs.resolve_expr(&post.key_arg, scope),
1092            extra_unfolds: post.extra_unfolds,
1093        };
1094    }
1095    // Functional equivalence of `vb.fn_name` and a same-named spec
1096    // fn whose body is syntactically identical to the impl's.
1097    if let Some(extra_unfolds) = detect_spec_equivalence(law, fn_name, inputs) {
1098        return ProofStrategy::SpecEquivalence { extra_unfolds };
1099    }
1100    // Broader spec equivalence — bodies differ syntactically but
1101    // normalize to same under substitution + arithmetic identity
1102    // folding. Runs after the strict `SpecEquivalence` so the
1103    // tighter detector wins when both would match.
1104    if let Some(extra_unfolds) = detect_simp_normalized_spec_equivalence(law, fn_name, inputs) {
1105        return ProofStrategy::SpecEquivalenceSimpNormalized { extra_unfolds };
1106    }
1107    // Linear-Int spec equivalence — substituted bodies are pure
1108    // linear arithmetic over Int givens; decided by `omega` / LIA.
1109    if let Some((unfolded_impl, unfolded_spec)) =
1110        detect_linear_int_spec_equivalence(law, fn_name, inputs)
1111    {
1112        return ProofStrategy::LinearIntSpecEquivalence {
1113            unfolded_impl: inputs.resolve_expr(&unfolded_impl, scope),
1114            unfolded_spec: inputs.resolve_expr(&unfolded_spec, scope),
1115        };
1116    }
1117    // Effectful counterpart — Oracle Lift normalises both sides
1118    // (oracle args injected into impl call) and the lowerer matches
1119    // the canonical `impl(args) == spec(args)` shape on the
1120    // rewritten form. Fires on real oracle-spec laws like
1121    // `pickPair() => pairSpec(BranchPath.Root, rnd)`.
1122    if let Some(spec_fn) = detect_effectful_spec_equivalence(law, fn_name, inputs) {
1123        return ProofStrategy::EffectfulSpecEquivalence {
1124            impl_fn: fn_name.to_string(),
1125            spec_fn,
1126        };
1127    }
1128    // Second-order linear recurrence (fib / fibSpec shape). Detector
1129    // validates impl as tail-rec wrapper, spec as direct second-order
1130    // recurrence, helper as their shared affine worker — all three
1131    // shapes pinned in `lean::recurrence`. Backends consume the
1132    // (impl_fn, spec_fn, helper_fn) names from IR; the proof template
1133    // differs per target (Lean Nat-helper + induction; Dafny still
1134    // pending — issue #116).
1135    if let Some((spec_fn, helper_fn)) =
1136        detect_linear_recurrence2_spec_equivalence(law, fn_name, inputs)
1137    {
1138        return ProofStrategy::LinearRecurrence2SpecEquivalence {
1139            impl_fn: fn_name.to_string(),
1140            spec_fn,
1141            helper_fn,
1142        };
1143    }
1144    // Linear arithmetic over an unfold chain — generic catch-all.
1145    // Named for the semantic, not the backend tactic.
1146    if let Some(plan) = detect_simp_omega_unfold(law, fn_name, inputs, refined_types) {
1147        return ProofStrategy::LinearArithmetic {
1148            unfold_fns: plan.unfold_fns,
1149            wrapper_return: plan.wrapper_return,
1150            smart_guard: plan.smart_guard,
1151            lifted: plan.lifted,
1152        };
1153    }
1154    // Ground constant-fold over fixed ADT/enum constructors — the
1155    // last typed fallback before `BackendDispatch`. Fires only for the
1156    // narrow shape no earlier detector accepts: a non-recursive fn with
1157    // ≥1 non-Int param, whose every non-Int param is pinned to a
1158    // constructor literal at the law's call site(s). LinearArithmetic
1159    // rejected it (non-Int param), Induction rejected it (no recursive
1160    // ADT given) — so this can't steal a law another strategy owns.
1161    if law.when.is_none()
1162        && let Some(unfold_fns) = detect_enum_constant_fold(law, fn_name, inputs)
1163    {
1164        return ProofStrategy::EnumConstantFold { unfold_fns };
1165    }
1166    // Closed finite-domain enumeration — the final typed fallback
1167    // before `BackendDispatch`. Fires when EVERY given ranges over a
1168    // closed, small domain (Bool or an all-fieldless user enum, ≤ 16
1169    // total combinations): exhaustive `cases` over the givens yields
1170    // ground goals per leaf, so deliberately NO call-shape inspection,
1171    // NO return-type gate and NO recursion gate — closed enumeration
1172    // makes those irrelevant (fuel-wrapped callees compute through
1173    // constant-measure constructor args). That is exactly why this is
1174    // a NEW detector and not a relaxation of `EnumConstantFold`, whose
1175    // literal-pinning / non-recursive / scalar-return gates are
1176    // load-bearing for its simp cascade.
1177    if law.when.is_none()
1178        && let Some(givens) = detect_finite_domain_cases(law, inputs)
1179    {
1180        return ProofStrategy::FiniteDomainCases { givens };
1181    }
1182    // Unconditional ring identity over Int-component records — runs
1183    // BEFORE the prelude-simp rung because that rung would otherwise
1184    // claim the shape (record givens, non-recursive pure cone) and
1185    // park it on a caught sorry: its minimal simp set has no AC-ring
1186    // normalization, and the permutational package this strategy
1187    // emits cannot be added there (it would loop or destroy the
1188    // normal forms other strategies rely on). Every earlier rung has
1189    // already declined: LinearArithmetic rejects non-Int record
1190    // givens, EnumConstantFold needs constructor-literal-pinned
1191    // params, FiniteDomainCases needs closed finite domains — so the
1192    // pin cannot steal a law a cheaper strategy closes today.
1193    if law.when.is_none()
1194        && let Some(unfold_fns) = detect_ring_identity(law, fn_name, inputs)
1195    {
1196        return ProofStrategy::RingIdentity { unfold_fns };
1197    }
1198    // Decimal-Int parse/serialize roundtrip — runs BEFORE the prelude-
1199    // simp rung because that rung would otherwise claim the shape (the
1200    // lhs cone is fuel-wrapped with measure-closed args) and park it on
1201    // a caught sorry the scanner barrier guarantees. The detector
1202    // validates the ENTIRE canonical parser shape (head-char dispatch
1203    // arms, single recognized scanner, slice + `Int.fromString` leaf),
1204    // so it cannot fire on the #469 prelude-simp laws (`finishInt` /
1205    // `finishNumber` / `afterIntChar` / `finishString` — wrong arity or
1206    // non-literal second arg at the law call site).
1207    if law.when.is_none()
1208        && let Some(s) = detect_int_decimal_roundtrip(law, fn_name, inputs, fn_contracts)
1209    {
1210        return s;
1211    }
1212    // Escaped-string parse/serialize roundtrip — the string-escape
1213    // sibling of the decimal roundtrip above, and like it placed
1214    // BEFORE the prelude-simp rung, which would otherwise claim the
1215    // shape (fuel-wrapped lhs cone) and park it on a caught sorry.
1216    // The detector validates the ENTIRE producer/consumer pair
1217    // (classifier escape table aligned arm-by-arm with the consumer's
1218    // escape dispatcher, control-escape prefix, threshold agreement,
1219    // fuel contracts), so it cannot fire on any shape whose
1220    // synthesized suffix-invariant proof would not close.
1221    if law.when.is_none()
1222        && let Some(s) = detect_string_escape_roundtrip(law, inputs, fn_contracts)
1223    {
1224        return s;
1225    }
1226    // Floor-division window family — laws over a power-of-two fn, a
1227    // guard-validated floor-halving binary-exponent fn, and the
1228    // window predicates built from them. The detectors are
1229    // deliberately narrow (exactly the hand-validated figures —
1230    // pow positivity, the pow sum homomorphism, the significand
1231    // window, the product window) and key on structure plus the
1232    // exponent fn's `WellFoundedToNat` contract, never on names.
1233    // Runs after every cheaper rung declined: LinearArithmetic
1234    // rejects the `Result.withDefault` cone and recursive callees,
1235    // Induction needs a recursive-ADT given, EnumConstantFold /
1236    // FiniteDomainCases need non-Int / closed domains — so the pin
1237    // cannot steal a law another strategy closes today.
1238    if let Some(figure) = detect_floor_window(law, fn_name, inputs, fn_contracts) {
1239        return ProofStrategy::FloorDivWindow { figure };
1240    }
1241    // Builtin-roundtrip simp over the prelude's spec-lemma registry —
1242    // the very last typed fallback. The Lean backend deliberately
1243    // renders this strategy AFTER its whole legacy ad-hoc chain (see
1244    // `lean::law_auto`), so pinning it here cannot steal a law any
1245    // legacy fallback closes today: it fires exactly where the
1246    // sampled-sorry path used to emit a bare-`sorry` universal.
1247    if law.when.is_none()
1248        && let Some(s) = detect_simp_over_prelude_lemmas(law, fn_name, inputs, fn_contracts)
1249    {
1250        return s;
1251    }
1252    ProofStrategy::BackendDispatch
1253}
1254
1255mod finite_domain;
1256mod floor_window;
1257mod induction;
1258mod int_decimal_roundtrip;
1259mod map_laws;
1260mod refinement;
1261mod ring;
1262mod simp;
1263mod spec_equivalence;
1264mod string_escape_roundtrip;
1265mod wrapper_laws;
1266
1267pub(crate) use induction::LawProofCone;
1268
1269use finite_domain::*;
1270use floor_window::*;
1271use induction::*;
1272use int_decimal_roundtrip::*;
1273use map_laws::*;
1274use refinement::*;
1275use ring::*;
1276use simp::*;
1277use spec_equivalence::*;
1278use string_escape_roundtrip::*;
1279use wrapper_laws::*;