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                // Filled in immediately below by `populate_refined_type_intervals`,
470                // which runs the interval analysis once over the just-built
471                // `refined_types` map. Left empty here so the two passes share
472                // a single source of truth (`interval::analyze`) instead of
473                // each construction site re-deriving the bound.
474                interval: None,
475                op_classes: Vec::new(),
476            },
477        );
478    }
479
480    // Back-fill each decl's derived interval + per-op classification by
481    // running the existing per-module interval analysis over the map we
482    // just built. Reuses `interval::analyze` verbatim (no forked logic),
483    // so the persisted fact on every `RefinedTypeDecl` is byte-identical
484    // to what `aver compile --explain-passes` reports for the same type.
485    // This makes the bound a queryable fact on the standard refinement-
486    // lower path — the home a future carrier-lowering codegen recognizer
487    // reads via `ctx.proof_ir.refined_types` (TypeId-keyed) without
488    // re-running the analysis behind the diagnostic flag.
489    populate_refined_type_intervals(inputs, ir);
490}
491
492/// Attach the interval analysis result to each `RefinedTypeDecl` in
493/// `ir.refined_types`. Called once at the tail of
494/// [`populate_refined_types`]; the analysis is keyed by the same opaque
495/// `TypeId` the decl map uses, so the join is a direct id lookup.
496fn populate_refined_type_intervals(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
497    let analysis = crate::ir::interval::analyze(&ir.refined_types, inputs);
498    for (type_id, decl) in ir.refined_types.iter_mut() {
499        let Some(per_type) = analysis.types.get(type_id) else {
500            continue;
501        };
502        // `interval_known` distinguishes a recognized bound from the
503        // conservative `Interval::unbounded()` decline; persist `None`
504        // for the decline so consumers don't mistake `[-inf, +inf]` for
505        // a real enclosure.
506        decl.interval = per_type.interval_known.then_some(per_type.interval);
507        decl.op_classes = per_type.ops.clone();
508    }
509}
510
511/// ETAP-2 SLICE 0+1: derive, per refinement-via-opaque type in scope, the
512/// constant interval its smart-constructor invariant proves over the
513/// carrier. The table is keyed by the opaque type's *bare* Aver name (e.g.
514/// `"IntRange"`). The bare-`i64` MIR pass matches a carrier
515/// parameter/local/return slot against this key — it extracts the bare name
516/// from the slot's `MirParam.ty` (which the lowerer fills with the Debug
517/// form `Named { id: …, name: "IntRange" }`, NOT the bare name; see
518/// `bare_named_type` in `bare_i64`) and seeds the slot with the proven bound.
519///
520/// The value is `interval_of_invariant(&predicate)` — byte-identical to the
521/// bound [`populate_refined_type_intervals`] persists on each
522/// `RefinedTypeDecl` (both build the same [`Predicate`] from
523/// [`refinement_info_for_in_scope`] + [`ProofLowerInputs::resolve_expr`] and
524/// run the same `interval` recognizer). No forked logic.
525///
526/// **Fail-closed.** A type whose invariant the recognizer does not
527/// understand returns `interval_known == false`; that entry is OMITTED from
528/// the table entirely, so the MIR pass never sees it and the carrier stays
529/// boxed. A carrier whose proven bound does not `fits_i64` is also kept
530/// (the table carries the raw `(Interval, bool)` so the seed site can apply
531/// `fits_i64` itself — see `carrier_interval` in `bare_i64`).
532pub fn carrier_interval_table(
533    inputs: &ProofLowerInputs,
534) -> HashMap<String, (crate::ir::interval::Interval, bool)> {
535    let mut table = HashMap::new();
536
537    let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
538        TopLevel::TypeDef(td) => Some((None::<&str>, td)),
539        _ => None,
540    });
541    let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
542        m.type_defs
543            .iter()
544            .map(move |td| (Some(m.prefix.as_str()), td))
545    });
546
547    for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
548        // Refinement-via-opaque is a single-carrier-field product; mirror the
549        // exact eligibility `populate_refined_types` applies so the keyed
550        // bound is the same fact the proof side carries.
551        let TypeDef::Product { name, fields, .. } = td else {
552            continue;
553        };
554        if fields.len() != 1 {
555            continue;
556        }
557        let Some(info) =
558            crate::codegen::common::refinement_info_for_in_scope(name, inputs, module_prefix)
559        else {
560            continue;
561        };
562        let invariant = Predicate {
563            free_vars: vec![(
564                info.param_name.to_string(),
565                QuantifierType::Plain(info.carrier_type.to_string()),
566            )],
567            expr: inputs.resolve_expr(info.predicate, module_prefix),
568        };
569        let (interval, interval_known) = crate::ir::interval::interval_of_invariant(&invariant);
570        // Fail-closed: an unrecognized invariant (`interval_known == false`)
571        // is OMITTED, so the carrier stays boxed.
572        if !interval_known {
573            continue;
574        }
575        // Key by the bare type name — the `MirParam.ty` string. Two modules
576        // may each declare a same-named carrier with different predicates;
577        // the bare name collides. Keep the FIRST (entry walks before deps),
578        // and on a same-name collision intersect to the tighter common bound
579        // (fail-closed: a narrower interval is always still a valid
580        // over-approximation of either inhabitant set, and a mismatch can
581        // only ever shrink eligibility, never wrongly widen it).
582        table
583            .entry(name.clone())
584            .and_modify(|(iv, known): &mut (crate::ir::interval::Interval, bool)| {
585                *iv = iv.intersect(interval);
586                *known = true;
587            })
588            .or_insert((interval, true));
589    }
590
591    table
592}
593
594/// ETAP-2 multi-field carrier-`i64`: derive, per `(record-type, Int-field)`
595/// pair in scope, the constant interval a MULTI-ARG smart constructor's guard
596/// proves over that field. This generalizes [`carrier_interval_table`] from
597/// the single-`value`-field carrier TYPE to a multi-field record whose 2+-arg
598/// smart constructor bounds each field independently.
599///
600/// The recognized shape is:
601/// ```text
602/// record Coord { x: Int, y: Int }                 // 2+ Int fields
603/// fn coord(x: Int, y: Int) -> Result<Coord, String>
604///     match <guard conjunction over x, y>
605///         true  -> Result.Ok(Coord(x = x, y = y)) // param p_j -> field f_i
606///         false -> Result.Err("...")
607/// ```
608/// The Ok-branch `RecordCreate` maps each constructor PARAM to a record FIELD
609/// (`Coord(x = x, y = y)`). For each field, the guard is split on its
610/// `Bool.and` tree and only the leaf comparisons mentioning that field's bound
611/// param ALONE are kept (a cross-field condition like `x + y <= 50` mentions
612/// two params and is DROPPED — conservative/sound: a narrower per-field bound
613/// is always a valid over-approximation). Running [`interval_of_invariant`]
614/// over those single-var leaves yields the field's interval.
615///
616/// **Fail-closed.** A field with no proven single-var `fits_i64` bound is
617/// OMITTED (the field stays boxed `$AverInt`). A non-`Int` field, a param that
618/// the Ok branch does not bind one-to-one to a field, or an unrecognized guard
619/// all decline. The single-field path ([`carrier_interval_table`]) is
620/// untouched — this table is ADDITIVE and keyed by `(record, field)`.
621///
622/// Returns a map keyed by `(bare record name, field name)`.
623pub fn field_carrier_interval_table(
624    inputs: &ProofLowerInputs,
625) -> HashMap<(String, String), (crate::ir::interval::Interval, bool)> {
626    let mut table = HashMap::new();
627
628    let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
629        TopLevel::TypeDef(td) => Some((None::<&str>, td)),
630        _ => None,
631    });
632    let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
633        m.type_defs
634            .iter()
635            .map(move |td| (Some(m.prefix.as_str()), td))
636    });
637
638    for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
639        let TypeDef::Product { name, fields, .. } = td else {
640            continue;
641        };
642        // Single-field products are the existing carrier-TYPE path; skip them
643        // here so the two tables never both claim the same record.
644        if fields.len() < 2 {
645            continue;
646        }
647        // Every field must be a plain `Int` for the multi-field i64 erasure;
648        // a record mixing Int and non-Int fields keeps its non-Int fields
649        // boxed (only the Int fields with a proven bound become eligible).
650        let Some((ctor, ctor_prefix)) =
651            find_multi_field_smart_ctor(name, fields, inputs, module_prefix)
652        else {
653            continue;
654        };
655        // Map record FIELD name -> the constructor PARAM name that feeds it,
656        // read from the Ok-branch `RecordCreate`.
657        let field_to_param = match ctor_field_param_map(ctor, name, fields) {
658            Some(m) => m,
659            None => continue,
660        };
661        let guard = ctor_guard_predicate(ctor);
662        let Some(guard) = guard else { continue };
663        for (fname, ftype) in fields {
664            if ftype.trim() != "Int" {
665                continue;
666            }
667            let Some(param) = field_to_param.get(fname) else {
668                continue;
669            };
670            // Project the guard onto single-variable leaves mentioning ONLY
671            // this param. A cross-field leaf (two distinct params) is dropped.
672            let resolved_guard = inputs.resolve_expr(guard, ctor_prefix);
673            let leaves =
674                crate::codegen::common::flatten_bool_and_conjuncts_resolved(&resolved_guard);
675            let single_var: Vec<_> = leaves
676                .into_iter()
677                .filter(|leaf| resolved_leaf_mentions_only(leaf, param))
678                .collect();
679            if single_var.is_empty() {
680                continue;
681            }
682            // Rebuild a conjunction predicate over the kept leaves and run the
683            // SAME interval recognizer the single-field path uses.
684            let conj = rebuild_bool_and(single_var);
685            let invariant = Predicate {
686                free_vars: vec![(param.clone(), QuantifierType::Plain("Int".to_string()))],
687                expr: conj,
688            };
689            let (interval, interval_known) = crate::ir::interval::interval_of_invariant(&invariant);
690            if !interval_known {
691                continue;
692            }
693            // Same collision discipline as the type-keyed table: on a
694            // same-`(record, field)` collision across modules, intersect to
695            // the tighter common bound (fail-closed).
696            table
697                .entry((name.clone(), fname.clone()))
698                .and_modify(|(iv, known): &mut (crate::ir::interval::Interval, bool)| {
699                    *iv = iv.intersect(interval);
700                    *known = true;
701                })
702                .or_insert((interval, true));
703        }
704    }
705
706    table
707}
708
709/// Find the single recognized multi-arg smart constructor for `record_name`:
710/// a pure fn `mk(p1, ..., pN) -> Result<Rec, String>` whose body is a single
711/// two-arm `true -> Result.Ok(Rec(...)) | false -> Result.Err(_)` match. The
712/// param count must be >= 2 (a one-arg ctor is the single-field carrier path).
713/// Returns the `&FnDef` plus the module scope it was found in.
714fn find_multi_field_smart_ctor<'a>(
715    record_name: &str,
716    fields: &[(String, String)],
717    inputs: &ProofLowerInputs<'a>,
718    record_scope: Option<&str>,
719) -> Option<(&'a FnDef, Option<&'a str>)> {
720    let entry_fns = inputs.entry_items.iter().filter_map(|item| match item {
721        TopLevel::FnDef(fd) => Some((None::<&str>, fd)),
722        _ => None,
723    });
724    let module_fns = inputs.dep_modules.iter().flat_map(|m| {
725        m.fn_defs
726            .iter()
727            .map(move |fd| (Some(m.prefix.as_str()), fd))
728    });
729    for (scope, fd) in entry_fns.chain(module_fns) {
730        // The constructor lives in the same module as the record (opaque
731        // refinement is single-module); skip a same-named fn in another scope.
732        if scope != record_scope {
733            continue;
734        }
735        if !fd.return_type.starts_with("Result<") {
736            continue;
737        }
738        if !fd.return_type[7..].starts_with(record_name) {
739            continue;
740        }
741        if fd.params.len() < 2 {
742            continue;
743        }
744        let stmts = fd.body.stmts();
745        if stmts.len() != 1 {
746            continue;
747        }
748        let crate::ast::Stmt::Expr(body_expr) = &stmts[0] else {
749            continue;
750        };
751        let Expr::Match { arms, .. } = &body_expr.node else {
752            continue;
753        };
754        if !is_multi_field_ok_err_match(arms, record_name, fields) {
755            continue;
756        }
757        return Some((fd, scope));
758    }
759    None
760}
761
762/// True iff `arms` is the canonical multi-field smart-ctor shape:
763/// `true -> Result.Ok(Rec(f_i = p_j, ...))` covering EVERY field, and
764/// `false -> Result.Err(_)`.
765fn is_multi_field_ok_err_match(
766    arms: &[crate::ast::MatchArm],
767    record_name: &str,
768    fields: &[(String, String)],
769) -> bool {
770    if arms.len() != 2 {
771        return false;
772    }
773    let mut true_ok = false;
774    let mut false_err = false;
775    for arm in arms {
776        match &arm.pattern {
777            crate::ast::Pattern::Literal(Literal::Bool(true)) => {
778                if multi_field_ok_constructor(&arm.body, record_name, fields).is_some() {
779                    true_ok = true;
780                }
781            }
782            crate::ast::Pattern::Literal(Literal::Bool(false)) => {
783                if multi_field_is_err_constructor(&arm.body) {
784                    false_err = true;
785                }
786            }
787            _ => return false,
788        }
789    }
790    true_ok && false_err
791}
792
793/// Local mirror of `common::is_err_constructor` (which is private): the arm
794/// body is `Result.Err(_)`.
795fn multi_field_is_err_constructor(expr: &Spanned<Expr>) -> bool {
796    match &expr.node {
797        Expr::Constructor(name, Some(_)) => name == "Result.Err",
798        Expr::FnCall(callee, args) if args.len() == 1 => {
799            matches!(expr_to_dotted_name(&callee.node), Some(name) if name == "Result.Err")
800        }
801        _ => false,
802    }
803}
804
805/// Inspect a `Result.Ok(Rec(f_i = p_j, ...))` arm body and return the
806/// field-name -> param-name map when every field is set to a bare identifier.
807/// `None` if the body is not the expected `Result.Ok` of a `RecordCreate` of
808/// `record_name` covering exactly the declared fields with identifier values.
809fn multi_field_ok_constructor(
810    expr: &Spanned<Expr>,
811    record_name: &str,
812    fields: &[(String, String)],
813) -> Option<HashMap<String, String>> {
814    let (ctor_name, ctor_arg_node) = match &expr.node {
815        Expr::Constructor(name, Some(arg)) => (name.clone(), &arg.node),
816        Expr::FnCall(callee, args) if args.len() == 1 => {
817            let name = expr_to_dotted_name(&callee.node)?;
818            (name, &args[0].node)
819        }
820        _ => return None,
821    };
822    if ctor_name != "Result.Ok" {
823        return None;
824    }
825    let (t, create_fields) = match ctor_arg_node {
826        Expr::RecordCreate { type_name, fields } => (type_name.as_str(), fields),
827        _ => return None,
828    };
829    if t != record_name || create_fields.len() != fields.len() {
830        return None;
831    }
832    let mut map = HashMap::new();
833    for (fname, fvalue) in create_fields {
834        let param = match &fvalue.node {
835            Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
836            _ => return None,
837        };
838        map.insert(fname.clone(), param);
839    }
840    // Every declared field must be assigned by the Ok branch.
841    if fields.iter().any(|(fname, _)| !map.contains_key(fname)) {
842        return None;
843    }
844    Some(map)
845}
846
847/// The field -> param map of the smart constructor's Ok branch (the
848/// `true -> Result.Ok(Rec(...))` arm). Walks the body's single match.
849fn ctor_field_param_map(
850    fd: &FnDef,
851    record_name: &str,
852    fields: &[(String, String)],
853) -> Option<HashMap<String, String>> {
854    let stmts = fd.body.stmts();
855    let crate::ast::Stmt::Expr(body_expr) = stmts.first()? else {
856        return None;
857    };
858    let Expr::Match { arms, .. } = &body_expr.node else {
859        return None;
860    };
861    for arm in arms {
862        if matches!(
863            &arm.pattern,
864            crate::ast::Pattern::Literal(Literal::Bool(true))
865        ) {
866            return multi_field_ok_constructor(&arm.body, record_name, fields);
867        }
868    }
869    None
870}
871
872/// The smart constructor's guard predicate (the `Match` subject the Ok/Err
873/// arms branch on).
874fn ctor_guard_predicate(fd: &FnDef) -> Option<&Spanned<Expr>> {
875    let stmts = fd.body.stmts();
876    let crate::ast::Stmt::Expr(body_expr) = stmts.first()? else {
877        return None;
878    };
879    let Expr::Match { subject, .. } = &body_expr.node else {
880        return None;
881    };
882    Some(subject)
883}
884
885/// True iff every identifier leaf mentioned in `leaf` is `param` (so the leaf
886/// is a SINGLE-VARIABLE condition over that one param). A leaf naming two
887/// distinct params (a cross-field condition like `x + y <= 50`) returns false
888/// and is dropped by the caller — conservative/sound per-field projection.
889fn resolved_leaf_mentions_only(leaf: &Spanned<crate::ir::hir::ResolvedExpr>, param: &str) -> bool {
890    let mut only = true;
891    let mut saw = false;
892    collect_resolved_idents(leaf, &mut |name| {
893        if name == param {
894            saw = true;
895        } else {
896            only = false;
897        }
898    });
899    only && saw
900}
901
902/// Walk a resolved expression, invoking `f` for every identifier leaf
903/// (`Ident` / `Resolved`).
904fn collect_resolved_idents(e: &Spanned<crate::ir::hir::ResolvedExpr>, f: &mut impl FnMut(&str)) {
905    use crate::ir::hir::ResolvedExpr;
906    match &e.node {
907        ResolvedExpr::Ident(n) | ResolvedExpr::Resolved { name: n, .. } => f(n),
908        ResolvedExpr::BinOp(_, l, r) => {
909            collect_resolved_idents(l, f);
910            collect_resolved_idents(r, f);
911        }
912        ResolvedExpr::Neg(i) => collect_resolved_idents(i, f),
913        ResolvedExpr::Call(_, args) => {
914            for a in args {
915                collect_resolved_idents(a, f);
916            }
917        }
918        ResolvedExpr::Attr(o, _) => collect_resolved_idents(o, f),
919        _ => {}
920    }
921}
922
923/// Rebuild a `Bool.and` conjunction over the kept single-variable leaves.
924/// A single leaf returns itself; >1 fold into nested `Bool.and(...)` calls,
925/// matching the shape [`interval_of_invariant`] recognizes.
926fn rebuild_bool_and(
927    mut leaves: Vec<Spanned<crate::ir::hir::ResolvedExpr>>,
928) -> Spanned<crate::ir::hir::ResolvedExpr> {
929    use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
930    let mut acc = leaves.remove(0);
931    for leaf in leaves {
932        let line = acc.line;
933        acc = Spanned::new(
934            ResolvedExpr::Call(
935                ResolvedCallee::Builtin("Bool.and".to_string()),
936                vec![acc, leaf],
937            ),
938            line,
939        );
940    }
941    acc
942}
943
944/// ETAP-2 multi-field carrier-`i64`: the per-`(record, field)` ELIGIBLE map —
945/// [`field_carrier_interval_table`] tightened the same way the single-field
946/// path tightens its proven-bound set. An entry survives only when its bound
947/// is recognized AND `fits_i64` AND the owning record is NOT demoted by any
948/// whole-program scan ([`multi_field_record_demotions`]):
949///   - the record is constructed UNGATED (a bare `RecordCreate` outside its
950///     own smart-ctor whose args are not all in-bounds literals) — that bypass
951///     could store an out-of-`i64` value the construct bridge would TRAP on;
952///   - the record (or a record reaching it) is used as a `Map` KEY — the
953///     Map-key codegen was not updated for the i64-erased fields;
954///   - the record (or a record reaching it) is used DIRECTLY as a `Map` VALUE
955///     (or through a record field / `Option` / `Result` that keeps it an inline
956///     struct ref in the values array) — that trips a separate, pre-existing
957///     record-as-Map-value validation bug, so the whole record stays boxed
958///     there. A carrier used as a `List` / `Vector` / `Tuple` ELEMENT now STAYS
959///     native: the per-field record eq/hash helpers dispatch a raw `i64.eq` /
960///     `i32.wrap_i64` for its i64-erased fields, so `List<Coord>` keeps
961///     `(field i64)(field i64)` elements that compile + run native. (`Option` /
962///     `Result` payloads are NOT demoted either — they hold the element as an
963///     inline struct ref, so the smart-ctor boundary
964///     `coord(...) -> Result<Coord, String>` keeps the native-i64 win.)
965///
966/// A demoted record keeps EVERY field boxed (the whole struct stays the
967/// pre-slice all-`$AverInt` layout). wasm-gc only; the Rust path passes the
968/// empty registry and never erases a field.
969///
970/// Returns a map keyed by `(bare record name, field name)`; an empty map
971/// reproduces the pre-slice all-`$AverInt` multi-field record byte-for-byte.
972pub fn field_carrier_eligible_intervals(
973    inputs: &ProofLowerInputs,
974    instantiations: &crate::ir::mir::InstantiationRegistry,
975) -> HashMap<(String, String), (crate::ir::interval::Interval, bool)> {
976    let table = field_carrier_interval_table(inputs);
977    if table.is_empty() {
978        return table;
979    }
980    // Proven-bound candidates: bound recognized AND `fits_i64`. The set of
981    // RECORD names with at least one such field drives the demotion scans
982    // (a demotion is per-record — a record constructed ungated / used as a
983    // Map key keeps ALL its fields boxed).
984    let record_candidates: HashSet<String> = table
985        .iter()
986        .filter(|(_, (iv, known))| *known && iv.fits_i64())
987        .map(|((rec, _), _)| rec.clone())
988        .collect();
989    if std::env::var("AVER_CARRIER_I64_SKIP_DEMOTION").is_ok() {
990        return table
991            .into_iter()
992            .filter(|((rec, _), (iv, known))| {
993                *known && iv.fits_i64() && record_candidates.contains(rec)
994            })
995            .collect();
996    }
997    let demoted_records =
998        multi_field_record_demotions(inputs, &record_candidates, &table, instantiations);
999    table
1000        .into_iter()
1001        .filter(|((rec, _), (iv, known))| {
1002            *known
1003                && iv.fits_i64()
1004                && record_candidates.contains(rec)
1005                && !demoted_records.contains(rec)
1006        })
1007        .collect()
1008}
1009
1010/// ETAP-2 multi-field carrier-`i64`: the RECORD-level fail-closed demotion
1011/// scan — the multi-field generalization of [`carrier_eligibility_demotions`].
1012/// A multi-field bounded record is demoted (every field kept boxed) when:
1013///   - **Scan 1 (ungated construction).** A `RecordCreate` / `RecordUpdate` of
1014///     the record in a fn OTHER than its recognized multi-arg smart constructor
1015///     can smuggle an out-of-`i64` value past the per-field gate (the construct
1016///     bridge `__aint_to_i64_checked` would TRAP). SAFE only when every field
1017///     argument is a constant literal provably inside that field's proven
1018///     interval; any non-literal / out-of-range field argument demotes.
1019///   - **Scan 2 (Map-key usage).** The record — directly or transitively as a
1020///     field of a record/Option/List/Tuple used as a Map KEY — reaches a
1021///     `Map<K, V>` key position; the Map-key codegen still expects the boxed
1022///     struct. Driven by the inference-complete resolved Map-key types, with
1023///     the textual annotation scan as a cheap backstop. Both reuse the SAME
1024///     `carriers_reachable_from` / `collect_map_key_carriers` closures the
1025///     single-field path uses.
1026///   - **Scan 3 (direct Map-VALUE usage).** The record reaches a `Map` VALUE
1027///     DIRECTLY (or through a record field / `Option` / `Result` that keeps it
1028///     an inline struct ref in the values array). That trips a separate,
1029///     pre-existing record-as-Map-value validation bug, so the record stays
1030///     boxed there. The walk STOPS at a `List` / `Vector` / `Tuple` boundary: a
1031///     carrier used as such a container ELEMENT now stays native i64 (the
1032///     per-field record eq/hash helpers dispatch the raw `i64.eq` /
1033///     `i32.wrap_i64` for an i64-erased field), so a carrier reachable only
1034///     THROUGH such a container — e.g. a `Map<K, List<Coord>>` value — is NOT
1035///     demoted. Driven by the inference-complete instantiation registry (every
1036///     `Map` the program uses), via `carriers_reachable_as_map_value`.
1037///
1038/// Fail-closed: a trip can only ever SHRINK the eligible set.
1039fn multi_field_record_demotions(
1040    inputs: &ProofLowerInputs,
1041    candidates: &HashSet<String>,
1042    field_intervals: &HashMap<(String, String), (crate::ir::interval::Interval, bool)>,
1043    instantiations: &crate::ir::mir::InstantiationRegistry,
1044) -> HashSet<String> {
1045    let mut demoted: HashSet<String> = HashSet::new();
1046    if candidates.is_empty() {
1047        return demoted;
1048    }
1049
1050    // Map each candidate record to its recognized multi-arg smart-ctor fn name
1051    // (the only construct site that gates the fields). A record with no
1052    // recognizable smart-ctor never reached `field_carrier_interval_table`, so
1053    // every candidate has one — but resolve defensively and demote on failure.
1054    let mut ctor_fn_of: HashMap<String, String> = HashMap::new();
1055    let record_defs = collect_product_defs(inputs);
1056    for name in candidates {
1057        let Some((fields, scope)) = record_defs.get(name) else {
1058            demoted.insert(name.clone());
1059            continue;
1060        };
1061        match find_multi_field_smart_ctor(name, fields, inputs, *scope) {
1062            Some((ctor, _)) => {
1063                ctor_fn_of.insert(name.clone(), ctor.name.clone());
1064            }
1065            None => {
1066                demoted.insert(name.clone());
1067            }
1068        }
1069    }
1070
1071    // ---- Scan 1: ungated construction --------------------------------------
1072    let all_fn_defs = inputs
1073        .entry_items
1074        .iter()
1075        .filter_map(|it| match it {
1076            TopLevel::FnDef(fd) => Some(fd),
1077            _ => None,
1078        })
1079        .chain(inputs.dep_modules.iter().flat_map(|m| m.fn_defs.iter()));
1080    for fd in all_fn_defs {
1081        for stmt in fd.body.stmts() {
1082            let expr = match stmt {
1083                crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => e,
1084            };
1085            carrier_walk_expr(expr, &mut |e| {
1086                let (type_name, create_fields): (&String, &[(String, Spanned<Expr>)]) = match e {
1087                    Expr::RecordCreate { type_name, fields } => (type_name, fields),
1088                    Expr::RecordUpdate {
1089                        type_name, updates, ..
1090                    } => (type_name, updates),
1091                    _ => return,
1092                };
1093                if !candidates.contains(type_name) {
1094                    return;
1095                }
1096                // The construct inside the record's own smart-ctor is gated.
1097                if ctor_fn_of.get(type_name) == Some(&fd.name) {
1098                    return;
1099                }
1100                // Outside the smart-ctor: safe iff EVERY provided field value is
1101                // a constant literal inside that field's proven interval (an
1102                // in-bounds literal cannot smuggle an out-of-bound value past
1103                // the gate). A `RecordUpdate` that omits a bounded field copies
1104                // it from the (already-gated) base, which is safe too.
1105                let all_safe = create_fields.iter().all(|(fname, value)| {
1106                    match field_intervals.get(&(type_name.clone(), fname.clone())) {
1107                        // A non-bounded field (no eligible interval) is stored
1108                        // boxed regardless, so it can't smuggle an i64-overflow.
1109                        None => true,
1110                        Some((iv, true)) => literal_in_interval(value, *iv),
1111                        Some((_, false)) => false,
1112                    }
1113                });
1114                if !all_safe {
1115                    demoted.insert(type_name.clone());
1116                }
1117            });
1118        }
1119    }
1120
1121    // ---- Scan 2: Map-key usage (direct + transitive) -----------------------
1122    let record_fields = collect_record_fields(inputs);
1123    for (key, _value) in &instantiations.maps {
1124        carriers_reachable_from(
1125            key,
1126            candidates,
1127            &record_fields,
1128            &mut HashSet::new(),
1129            &mut demoted,
1130        );
1131    }
1132    for ty_str in all_type_annotations(inputs) {
1133        let ty = crate::types::parse_type_str(&ty_str);
1134        collect_map_key_carriers(&ty, candidates, &record_fields, &mut demoted);
1135    }
1136
1137    // ---- Scan 3: Map-VALUE usage (direct + transitive, list/vec/tuple-stopped)
1138    // A multi-field carrier reachable DIRECTLY as a `Map` VALUE (or through a
1139    // record field / `Option` / `Result` that keeps it an inline struct ref in
1140    // the values backing array) keeps all its fields BOXED — that case trips a
1141    // separate, pre-existing record-as-Map-value validation bug that is out of
1142    // scope here, so we fail closed exactly as before.
1143    //
1144    // A carrier used as a `List` / `Vector` / `Tuple` ELEMENT now STAYS native
1145    // i64: the per-field record eq/hash helpers dispatch a raw `i64.eq` /
1146    // `i32.wrap_i64` for an i64-erased field (gated on `is_eligible_carrier_
1147    // field`), so `List<Coord>` keeps `(field i64)(field i64)` elements and
1148    // `List.contains` / `==` over them compiles and runs native. The reachability
1149    // walk therefore STOPS at a `List` / `Vector` / `Tuple` boundary — a carrier
1150    // only reachable THROUGH such a container (e.g. `Map<K, List<Coord>>`, where
1151    // the Map value is a list ref whose elements are native) is NOT demoted.
1152    //
1153    // `Option` / `Result` payloads are likewise inline struct refs, so a carrier
1154    // behind them as a Map value stays eligible (the common smart-ctor boundary
1155    // `coord(...) -> Result<Coord, String>` keeps the native-i64 win).
1156    let mut map_value_seen: HashSet<String> = HashSet::new();
1157    for (_key, value) in &instantiations.maps {
1158        carriers_reachable_as_map_value(
1159            value,
1160            candidates,
1161            &record_fields,
1162            &mut map_value_seen,
1163            &mut demoted,
1164        );
1165    }
1166
1167    demoted
1168}
1169
1170/// ETAP-2 multi-field carrier-`i64`: the Map-VALUE reachability walk for the
1171/// demotion scan — like [`carriers_reachable_from`] but it STOPS at a `List` /
1172/// `Vector` / `Tuple` boundary. A carrier stored as a `List` / `Vector` /
1173/// `Tuple` element keeps its i64 fields native (the container's per-element
1174/// eq/hash dispatches the raw i64 ops), so reaching one only THROUGH such a
1175/// container (e.g. a `Map<K, List<Coord>>` value, a list-of-tuples value) does
1176/// NOT demote it. A carrier reachable as a DIRECT Map value, or through a
1177/// record field / `Option` / `Result` (all of which hold it as an inline struct
1178/// ref in the values backing array), still demotes — the record-as-Map-value
1179/// validation bug that motivates this scan is unchanged by the container slice.
1180fn carriers_reachable_as_map_value(
1181    value: &crate::ast::Type,
1182    candidates: &HashSet<String>,
1183    record_fields: &HashMap<String, Vec<String>>,
1184    seen: &mut HashSet<String>,
1185    demoted: &mut HashSet<String>,
1186) {
1187    use crate::ast::Type;
1188    let Some(name) = value.named_name() else {
1189        match value {
1190            // `Option` / `Result` keep the payload an inline struct ref in the
1191            // Map values array, so a carrier behind them is still a direct
1192            // value — descend.
1193            Type::Option(a) => {
1194                carriers_reachable_as_map_value(a, candidates, record_fields, seen, demoted);
1195            }
1196            Type::Result(a, b) => {
1197                carriers_reachable_as_map_value(a, candidates, record_fields, seen, demoted);
1198                carriers_reachable_as_map_value(b, candidates, record_fields, seen, demoted);
1199            }
1200            // A nested `Map` value is itself a Map values array — descend into
1201            // its value (its key is a separate Scan-2 concern, handled there).
1202            Type::Map(_k, v) => {
1203                carriers_reachable_as_map_value(v, candidates, record_fields, seen, demoted);
1204            }
1205            // `List` / `Vector` / `Tuple` make the carrier a native container
1206            // ELEMENT (now eligible) — STOP, do not demote anything inside.
1207            Type::List(_) | Type::Vector(_) | Type::Tuple(_) => {}
1208            _ => {}
1209        }
1210        return;
1211    };
1212    if !seen.insert(name.to_string()) {
1213        return;
1214    }
1215    if candidates.contains(name) {
1216        demoted.insert(name.to_string());
1217    }
1218    if let Some(fields) = record_fields.get(name) {
1219        for field_ty in fields {
1220            let parsed = crate::types::parse_type_str(field_ty);
1221            carriers_reachable_as_map_value(&parsed, candidates, record_fields, seen, demoted);
1222        }
1223    }
1224}
1225
1226/// A candidate record's declarations as the demotion scan needs them: its
1227/// `(field, type)` list plus the module scope it was found in (`None` = entry).
1228type ProductDef<'a> = (&'a [(String, String)], Option<&'a str>);
1229
1230/// Bare `Product` type name → its [`ProductDef`]. Lets the demotion scan
1231/// re-locate a candidate record's field declarations + smart constructor.
1232fn collect_product_defs<'a>(inputs: &ProofLowerInputs<'a>) -> HashMap<String, ProductDef<'a>> {
1233    let mut out: HashMap<String, ProductDef<'a>> = HashMap::new();
1234    for item in inputs.entry_items {
1235        if let TopLevel::TypeDef(TypeDef::Product { name, fields, .. }) = item {
1236            out.entry(name.clone()).or_insert((fields.as_slice(), None));
1237        }
1238    }
1239    for m in inputs.dep_modules {
1240        for td in &m.type_defs {
1241            if let TypeDef::Product { name, fields, .. } = td {
1242                out.entry(name.clone())
1243                    .or_insert((fields.as_slice(), Some(m.prefix.as_str())));
1244            }
1245        }
1246    }
1247    out
1248}
1249
1250/// A `RecordCreate`/`RecordUpdate` field value is a constant integer literal
1251/// (`5` or `-5`) provably within the proven interval `iv`. Any other shape
1252/// (a param, a call, arithmetic) is not a provable constant ⇒ `false`.
1253fn literal_in_interval(value: &Spanned<Expr>, iv: crate::ir::interval::Interval) -> bool {
1254    let k: i128 = match &value.node {
1255        Expr::Literal(Literal::Int(n)) => *n as i128,
1256        Expr::Neg(inner) => match &inner.node {
1257            Expr::Literal(Literal::Int(n)) => -(*n as i128),
1258            _ => return false,
1259        },
1260        _ => return false,
1261    };
1262    iv.contains_point(k)
1263}
1264
1265/// ETAP-2 carrier-`i64` SLICE 2b FOLLOW-UP: the fail-closed eligibility
1266/// tightening. The bare carrier interval ([`carrier_interval_table`]) proves
1267/// only that the smart-constructor's invariant `fits_i64`; it does NOT prove
1268/// that every value of the carrier type actually went through that gate, nor
1269/// that the carrier's codegen is exercised only in positions the i64 erasure
1270/// supports. This scan removes a carrier from the eligible set when either
1271/// assumption is violated, so the carrier stays boxed (`$AverInt`) — the
1272/// safe, pre-slice representation that the VM and the boxed wasm-gc path
1273/// agree on.
1274///
1275/// Two whole-program scans, both fail-closed (a trip can only ever SHRINK the
1276/// eligible set, never widen it):
1277///
1278/// 1. **Ungated construction (closes the bare-constructor bypass).** A bare
1279///    record constructor `IntRange(value = n)` callable in the defining
1280///    module bypasses the smart-ctor's `0 <= n <= 100` gate. With `n`
1281///    overflowing `i64` the VM keeps full precision but the wasm-gc construct
1282///    bridge `__aint_to_i64_checked` TRAPS. A carrier `RecordCreate`d outside
1283///    its own recognized smart-constructor function is therefore ineligible —
1284///    UNLESS the construct's carrier-field argument is a constant literal that
1285///    provably lies inside the carrier's proven interval. Such a literal
1286///    construct (`IntRange(value = 0)` against a `[0, 100]` bound) cannot
1287///    smuggle an out-of-bound / i64-overflowing value past the gate, so it is
1288///    SAFE and does not demote — that pattern is exactly the in-bounds Err
1289///    fallback the slice's own carriers use (`unwrap`'s `IntRange(value = 0)`).
1290///    A non-literal argument, or a literal OUTSIDE the interval, is ungated
1291///    and DOES demote (this is the `mk(n) = IntRange(value = n)` bypass).
1292///
1293/// 2. **Map-key usage (closes the i64-erased Map-KEY codegen gap).** A
1294///    carrier used as a `Map` KEY type — directly (`Map<IntRange, V>`) or
1295///    transitively as a field of a record/type used as a key
1296///    (`Map<Coord, V>` with `Coord { x: IntRange }`) — fails wasm validation,
1297///    because the Map-key codegen was not updated for the i64-erased carrier.
1298///    Such a carrier is ineligible (the boxed key path it used before still
1299///    compiles). Map VALUES are unaffected and stay eligible.
1300///
1301///    The COMPLETE source of truth for which carriers are Map keys is
1302///    `resolved_map_keys` — the resolved `Map<K, V>` key types harvested from
1303///    the typed MIR (`ir::mir::discover_instantiations`). Because those types
1304///    come from inference, a carrier used as a key reaches this scan whether
1305///    its `Map` type was written as a fn-signature annotation, a LOCAL-BINDING
1306///    annotation (`m: Map<IntRange, Int> = …`), or NO annotation at all
1307///    (`m = Map.set({}, c, 5)`). A textual annotation scan cannot see the
1308///    last two; the resolved key type can. The annotation scan is retained as
1309///    a cheap fail-closed backstop only.
1310///
1311/// Returns the set of carrier type names (bare names, the same keys
1312/// [`carrier_interval_table`] uses) to REMOVE from the eligible set. The
1313/// caller subtracts this from the proven-bound set.
1314///
1315/// `resolved_map_keys`: every `Map<K, _>` key type the program instantiates,
1316/// per the typed-MIR instantiation registry. This is the inference-complete
1317/// Map-key signal that drives Scan 2.
1318pub fn carrier_eligibility_demotions(
1319    inputs: &ProofLowerInputs,
1320    candidates: &HashSet<String>,
1321    intervals: &HashMap<String, (crate::ir::interval::Interval, bool)>,
1322    resolved_map_keys: &[crate::ast::Type],
1323) -> HashSet<String> {
1324    let mut demoted: HashSet<String> = HashSet::new();
1325    if candidates.is_empty() {
1326        return demoted;
1327    }
1328
1329    // Map each candidate carrier to its recognized smart-constructor fn name.
1330    // `refinement_info_for_in_scope` is THE source of truth for "the
1331    // smart-ctor function" — the exact fn the interval recognizer keyed off.
1332    // For the wasm-gc path `dep_modules` is empty (the program is flattened
1333    // into `entry_items`), so the entry scope (`None`) resolves every
1334    // carrier; we still consult dep scopes for generality.
1335    let mut ctor_fn_of: HashMap<String, String> = HashMap::new();
1336    for name in candidates {
1337        if let Some(info) = crate::codegen::common::refinement_info_for_in_scope(name, inputs, None)
1338        {
1339            ctor_fn_of.insert(name.clone(), info.constructor_fn.to_string());
1340        } else {
1341            for m in inputs.dep_modules {
1342                if let Some(info) = crate::codegen::common::refinement_info_for_in_scope(
1343                    name,
1344                    inputs,
1345                    Some(m.prefix.as_str()),
1346                ) {
1347                    ctor_fn_of.insert(name.clone(), info.constructor_fn.to_string());
1348                    break;
1349                }
1350            }
1351        }
1352    }
1353
1354    // ---- Scan 1: ungated construction --------------------------------------
1355    // Walk every fn body in the program. A `RecordCreate { type_name }` whose
1356    // `type_name` is a candidate carrier and which sits in a fn OTHER than
1357    // that carrier's smart-constructor is an ungated construct ⇒ demote,
1358    // UNLESS its carrier-field argument is a constant literal provably inside
1359    // the carrier's proven interval (an in-bounds literal can't smuggle an
1360    // out-of-bound value past the gate — fail-closed but not over-eager). If
1361    // we can't cleanly identify the smart-ctor (no entry in `ctor_fn_of`),
1362    // every non-literal-safe construct demotes — fail-closed.
1363    let all_fn_defs = inputs
1364        .entry_items
1365        .iter()
1366        .filter_map(|it| match it {
1367            TopLevel::FnDef(fd) => Some(fd),
1368            _ => None,
1369        })
1370        .chain(inputs.dep_modules.iter().flat_map(|m| m.fn_defs.iter()));
1371    for fd in all_fn_defs {
1372        for stmt in fd.body.stmts() {
1373            let expr = match stmt {
1374                crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => e,
1375            };
1376            carrier_walk_expr(expr, &mut |e| {
1377                // Both a fresh construct (`RecordCreate`) and a record-update
1378                // (`RecordUpdate`, which sets the carrier's only field to an
1379                // arbitrary value) can smuggle a value past the smart-ctor
1380                // gate; treat both the same.
1381                let (type_name, fields): (&String, &[(String, Spanned<Expr>)]) = match e {
1382                    Expr::RecordCreate { type_name, fields } => (type_name, fields),
1383                    Expr::RecordUpdate {
1384                        type_name, updates, ..
1385                    } => (type_name, updates),
1386                    _ => return,
1387                };
1388                if !candidates.contains(type_name) {
1389                    return;
1390                }
1391                // The construct inside the carrier's own smart-ctor is the
1392                // gated one — never demotes.
1393                if ctor_fn_of.get(type_name) == Some(&fd.name) {
1394                    return;
1395                }
1396                // Outside the smart-ctor: safe iff the (single) carrier field
1397                // is a constant literal inside the proven interval.
1398                let safe_literal = matches!(
1399                    intervals.get(type_name),
1400                    Some((iv, true)) if construct_arg_is_in_interval(fields, *iv)
1401                );
1402                if !safe_literal {
1403                    demoted.insert(type_name.clone());
1404                }
1405            });
1406        }
1407    }
1408
1409    // ---- Scan 2: Map-key usage (direct + transitive) -----------------------
1410    // Build, per record type, the set of carrier names reachable through its
1411    // (transitive) fields, so a carrier nested inside a record used as a key
1412    // is demoted too.
1413    let record_fields = collect_record_fields(inputs);
1414
1415    // PRIMARY (complete) source of truth: the RESOLVED Map-key types harvested
1416    // from the typed MIR (`discover_instantiations`). Every `Map<K, V>` the
1417    // program actually instantiates is here — including ones whose key type is
1418    // only known after INFERENCE (a local-binding `m: Map<…>` annotation that
1419    // the textual scan never walks, or a fully inferred `m = Map.set({}, c,
1420    // 5)` with no annotation at all). A textual annotation scan is
1421    // fundamentally incomplete for these; the resolved key type is not. Demote
1422    // every carrier reachable from each resolved key (directly, or
1423    // transitively through a record / Option / List / Tuple field — the same
1424    // `carriers_reachable_from` closure the annotation path uses).
1425    for key in resolved_map_keys {
1426        carriers_reachable_from(
1427            key,
1428            candidates,
1429            &record_fields,
1430            &mut HashSet::new(),
1431            &mut demoted,
1432        );
1433    }
1434
1435    // BACKSTOP (cheap): the original textual scan over fn-param / fn-return /
1436    // record-field annotations. Redundant with the resolved-IR path above for
1437    // any Map the MIR sees, but kept so a Map type that appears ONLY in an
1438    // annotation position the MIR instantiation walk doesn't reach (e.g. a
1439    // signature whose body never constructs/uses the Map) still trips. It can
1440    // only ever ADD to `demoted` (fail-closed).
1441    for ty_str in all_type_annotations(inputs) {
1442        let ty = crate::types::parse_type_str(&ty_str);
1443        collect_map_key_carriers(&ty, candidates, &record_fields, &mut demoted);
1444    }
1445
1446    demoted
1447}
1448
1449/// A single-carrier-field `RecordCreate`'s field argument is a constant
1450/// integer literal provably within the proven interval `iv`. Handles the
1451/// bare literal `IntRange(value = 0)` and the negated literal
1452/// `IntRange(value = -5)` (parsed as `Expr::Neg(Literal(Int))`). Any other
1453/// shape (a parameter, a call, an arithmetic expression) is NOT a provable
1454/// constant ⇒ returns `false` ⇒ the construct is treated as ungated.
1455fn construct_arg_is_in_interval(
1456    fields: &[(String, Spanned<Expr>)],
1457    iv: crate::ir::interval::Interval,
1458) -> bool {
1459    // Refinement-via-opaque carriers are single-field products.
1460    let [(_, value)] = fields else {
1461        return false;
1462    };
1463    let k: i128 = match &value.node {
1464        Expr::Literal(Literal::Int(n)) => *n as i128,
1465        Expr::Neg(inner) => match &inner.node {
1466            Expr::Literal(Literal::Int(n)) => -(*n as i128),
1467            _ => return false,
1468        },
1469        _ => return false,
1470    };
1471    iv.contains_point(k)
1472}
1473
1474/// Local AST visitor (the proof-lower module has no shared walker). Pre-order
1475/// visit of every sub-expression. Mirrors `call_graph::walk_expr`.
1476fn carrier_walk_expr(expr: &Spanned<Expr>, visit: &mut impl FnMut(&Expr)) {
1477    visit(&expr.node);
1478    match &expr.node {
1479        Expr::FnCall(func, args) => {
1480            carrier_walk_expr(func, visit);
1481            for arg in args {
1482                carrier_walk_expr(arg, visit);
1483            }
1484        }
1485        Expr::TailCall(boxed) => {
1486            for arg in &boxed.args {
1487                carrier_walk_expr(arg, visit);
1488            }
1489        }
1490        Expr::Attr(obj, _) => carrier_walk_expr(obj, visit),
1491        Expr::BinOp(_, l, r) => {
1492            carrier_walk_expr(l, visit);
1493            carrier_walk_expr(r, visit);
1494        }
1495        Expr::Neg(inner) | Expr::ErrorProp(inner) => carrier_walk_expr(inner, visit),
1496        Expr::Match { subject, arms, .. } => {
1497            carrier_walk_expr(subject, visit);
1498            for arm in arms {
1499                carrier_walk_expr(&arm.body, visit);
1500            }
1501        }
1502        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1503            for item in items {
1504                carrier_walk_expr(item, visit);
1505            }
1506        }
1507        Expr::MapLiteral(entries) => {
1508            for (k, v) in entries {
1509                carrier_walk_expr(k, visit);
1510                carrier_walk_expr(v, visit);
1511            }
1512        }
1513        Expr::Constructor(_, maybe) => {
1514            if let Some(inner) = maybe {
1515                carrier_walk_expr(inner, visit);
1516            }
1517        }
1518        Expr::InterpolatedStr(parts) => {
1519            for part in parts {
1520                if let crate::ast::StrPart::Parsed(e) = part {
1521                    carrier_walk_expr(e, visit);
1522                }
1523            }
1524        }
1525        Expr::RecordCreate { fields, .. } => {
1526            for (_, e) in fields {
1527                carrier_walk_expr(e, visit);
1528            }
1529        }
1530        Expr::RecordUpdate { base, updates, .. } => {
1531            carrier_walk_expr(base, visit);
1532            for (_, e) in updates {
1533                carrier_walk_expr(e, visit);
1534            }
1535        }
1536        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
1537    }
1538}
1539
1540/// Bare record-type name → its field type strings (`Product` types only).
1541/// Used by Scan 2 to chase a carrier nested inside a record used as a Map
1542/// key.
1543fn collect_record_fields(inputs: &ProofLowerInputs) -> HashMap<String, Vec<String>> {
1544    let mut out: HashMap<String, Vec<String>> = HashMap::new();
1545    let entry_tds = inputs.entry_items.iter().filter_map(|it| match it {
1546        TopLevel::TypeDef(td) => Some(td),
1547        _ => None,
1548    });
1549    let dep_tds = inputs.dep_modules.iter().flat_map(|m| m.type_defs.iter());
1550    for td in entry_tds.chain(dep_tds) {
1551        if let TypeDef::Product { name, fields, .. } = td {
1552            out.entry(name.clone())
1553                .or_default()
1554                .extend(fields.iter().map(|(_, ty)| ty.clone()));
1555        }
1556    }
1557    out
1558}
1559
1560/// Every type annotation string in the program: fn param types, fn return
1561/// types, and record field types. A `Map<…>` anywhere here is a Map-key
1562/// usage candidate for Scan 2.
1563fn all_type_annotations(inputs: &ProofLowerInputs) -> Vec<String> {
1564    let mut out: Vec<String> = Vec::new();
1565    let push_fn = |fd: &FnDef, out: &mut Vec<String>| {
1566        for (_, ty) in &fd.params {
1567            out.push(ty.clone());
1568        }
1569        out.push(fd.return_type.clone());
1570    };
1571    for it in inputs.entry_items {
1572        match it {
1573            TopLevel::FnDef(fd) => push_fn(fd, &mut out),
1574            TopLevel::TypeDef(TypeDef::Product { fields, .. }) => {
1575                out.extend(fields.iter().map(|(_, ty)| ty.clone()));
1576            }
1577            _ => {}
1578        }
1579    }
1580    for m in inputs.dep_modules {
1581        for fd in &m.fn_defs {
1582            push_fn(fd, &mut out);
1583        }
1584        for td in &m.type_defs {
1585            if let TypeDef::Product { fields, .. } = td {
1586                out.extend(fields.iter().map(|(_, ty)| ty.clone()));
1587            }
1588        }
1589    }
1590    out
1591}
1592
1593/// Walk a parsed `Type`. For every `Map<K, _>` encountered, add every
1594/// candidate carrier REACHABLE from `K` (directly, or transitively through a
1595/// record's fields) to `demoted`. Recurses through every type constructor so
1596/// a `Map` buried inside `List<Map<…>>`, a `Result`/`Option` payload, a
1597/// tuple, etc. is still found.
1598fn collect_map_key_carriers(
1599    ty: &crate::ast::Type,
1600    candidates: &HashSet<String>,
1601    record_fields: &HashMap<String, Vec<String>>,
1602    demoted: &mut HashSet<String>,
1603) {
1604    use crate::ast::Type;
1605    match ty {
1606        Type::Map(key, value) => {
1607            carriers_reachable_from(key, candidates, record_fields, &mut HashSet::new(), demoted);
1608            // The KEY is the hazard; recurse into both so a nested Map in
1609            // either position is still inspected.
1610            collect_map_key_carriers(key, candidates, record_fields, demoted);
1611            collect_map_key_carriers(value, candidates, record_fields, demoted);
1612        }
1613        Type::Result(a, b) => {
1614            collect_map_key_carriers(a, candidates, record_fields, demoted);
1615            collect_map_key_carriers(b, candidates, record_fields, demoted);
1616        }
1617        Type::Option(a) | Type::List(a) | Type::Vector(a) => {
1618            collect_map_key_carriers(a, candidates, record_fields, demoted);
1619        }
1620        Type::Tuple(items) => {
1621            for t in items {
1622                collect_map_key_carriers(t, candidates, record_fields, demoted);
1623            }
1624        }
1625        Type::Fn(params, ret, _) => {
1626            for p in params {
1627                collect_map_key_carriers(p, candidates, record_fields, demoted);
1628            }
1629            collect_map_key_carriers(ret, candidates, record_fields, demoted);
1630        }
1631        _ => {}
1632    }
1633}
1634
1635/// Collect every candidate carrier reachable from a Map-KEY type `key`:
1636/// `key` itself if it names a carrier, plus (transitively) any carrier among
1637/// the fields of a record `key` names. `seen` guards self-referential
1638/// records.
1639fn carriers_reachable_from(
1640    key: &crate::ast::Type,
1641    candidates: &HashSet<String>,
1642    record_fields: &HashMap<String, Vec<String>>,
1643    seen: &mut HashSet<String>,
1644    demoted: &mut HashSet<String>,
1645) {
1646    use crate::ast::Type;
1647    let Some(name) = key.named_name() else {
1648        // A Map key that is itself a container (`Map<List<Carrier>, V>`) —
1649        // chase the inner element types too.
1650        match key {
1651            Type::Option(a) | Type::List(a) | Type::Vector(a) => {
1652                carriers_reachable_from(a, candidates, record_fields, seen, demoted);
1653            }
1654            Type::Tuple(items) => {
1655                for t in items {
1656                    carriers_reachable_from(t, candidates, record_fields, seen, demoted);
1657                }
1658            }
1659            Type::Map(k, v) => {
1660                carriers_reachable_from(k, candidates, record_fields, seen, demoted);
1661                carriers_reachable_from(v, candidates, record_fields, seen, demoted);
1662            }
1663            Type::Result(a, b) => {
1664                carriers_reachable_from(a, candidates, record_fields, seen, demoted);
1665                carriers_reachable_from(b, candidates, record_fields, seen, demoted);
1666            }
1667            _ => {}
1668        }
1669        return;
1670    };
1671    if !seen.insert(name.to_string()) {
1672        return;
1673    }
1674    if candidates.contains(name) {
1675        demoted.insert(name.to_string());
1676    }
1677    if let Some(fields) = record_fields.get(name) {
1678        for field_ty in fields {
1679            let parsed = crate::types::parse_type_str(field_ty);
1680            carriers_reachable_from(&parsed, candidates, record_fields, seen, demoted);
1681        }
1682    }
1683}
1684
1685/// Walk `analyze_plans(inputs)` and populate `ProofIR.fn_contracts`.
1686///
1687/// Translation pass over the classifier output (`RecursionPlan`) —
1688/// no re-implementation. The diff test (`tests/proof_ir_diff.rs`)
1689/// pins what each `RecursionPlan` variant lowers to so divergence
1690/// between the classifier and the IR populator surfaces there.
1691/// Coverage today: `IntCountdownGuarded`, `LinearRecurrence2`,
1692/// `Sized*` (length / sizeOf / string-pos / int-ascending). Fuel-
1693/// only and Mutual* plans don't materialise as `FnContract` (their
1694/// recursion shape doesn't need IR-level pre-decisions; backends
1695/// emit fuel scaffolding inline).
1696pub fn populate_fn_contracts(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
1697    // Round-5 finding: walk per-scope so two modules each with a
1698    // recursive `foo` (or entry + module both declaring `foo`)
1699    // don't collide on the bare-name `plans: HashMap<String, _>`.
1700    // Aver's module DAG invariant rules out cross-module recursion
1701    // SCCs, so per-scope classification is the canonical view and
1702    // each `Module.fn` gets its own slot in `ir.fn_contracts`.
1703    for scope in inputs.scopes() {
1704        let (plans, issues) =
1705            crate::codegen::recursion::analyze_plans_in_scope(inputs, scope.as_deref(), false);
1706        ir.unclassified_fns
1707            .extend(issues.into_iter().map(|issue| crate::ir::UnclassifiedFn {
1708                line: issue.line,
1709                message: issue.message,
1710            }));
1711        populate_fn_contracts_for_scope(inputs, ir, scope.as_deref(), &plans);
1712    }
1713}
1714
1715fn populate_fn_contracts_for_scope(
1716    inputs: &ProofLowerInputs,
1717    ir: &mut ProofIR,
1718    scope: Option<&str>,
1719    plans: &HashMap<String, RecursionPlan>,
1720) {
1721    let scoped_fns: Vec<&FnDef> = inputs.pure_fns_in_scope(scope);
1722    let qualify = |bare: &str| -> crate::ir::FnKey {
1723        match scope {
1724            Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), bare),
1725            None => crate::ir::FnKey::entry(bare),
1726        }
1727    };
1728    // Contracts key by opaque `FnId`; SymbolTable is always present
1729    // (pipeline builds it unconditionally, `ProofLowerInputs.symbol_
1730    // table: &SymbolTable`).
1731    let symbols = inputs.symbol_table;
1732
1733    for (fn_name, plan) in plans {
1734        let Some(fd) = scoped_fns.iter().find(|fd| fd.name == *fn_name) else {
1735            continue;
1736        };
1737        let fn_key = qualify(fn_name);
1738        let Some(canonical_key) = symbols.fn_id_of(&fn_key) else {
1739            continue;
1740        };
1741
1742        // IntCountdown — fuel-encoded countdown on a single Int param.
1743        // Distinct from IntCountdownGuarded: external callers may pass
1744        // negatives (the classifier rejected closed-world status), so
1745        // backends emit a fuel helper with `n.natAbs + 1` initial fuel
1746        // rather than a native def with a precondition.
1747        if let RecursionPlan::IntCountdown { param_index } = plan {
1748            if let Some((param_name, _)) = fd.params.get(*param_index) {
1749                ir.fn_contracts.insert(
1750                    canonical_key,
1751                    FnContract {
1752                        source_name: fn_name.clone(),
1753                        recursion: Some(RecursionContract::Fuel {
1754                            fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne {
1755                                param: param_name.clone(),
1756                            },
1757                        }),
1758                    },
1759                );
1760            }
1761            continue;
1762        }
1763
1764        // IntFloorDivCountdown — guard-validated literal-divisor
1765        // floor-division shrink. The classifier proved both
1766        // side-conditions (every self-call shrinks the param through
1767        // `Result.withDefault(Int.div(p, k), d)` with literal k >= 2,
1768        // and every self-call site's guard chain implies `p >= 1`),
1769        // so backends emit a native well-founded def on `p.toNat`.
1770        if let RecursionPlan::IntFloorDivCountdown {
1771            param_index,
1772            divisor,
1773            helper_fn,
1774        } = plan
1775        {
1776            if let Some((param_name, _)) = fd.params.get(*param_index) {
1777                ir.fn_contracts.insert(
1778                    canonical_key,
1779                    FnContract {
1780                        source_name: fn_name.clone(),
1781                        recursion: Some(RecursionContract::WellFoundedToNat {
1782                            param: param_name.clone(),
1783                            floor_div: Some(crate::ir::FloorDivShrink {
1784                                divisor: *divisor,
1785                                helper_fn: helper_fn.clone(),
1786                            }),
1787                        }),
1788                    },
1789                );
1790            }
1791            continue;
1792        }
1793
1794        // IntAscending — fuel formula `(bound - n).natAbs + 1`. The
1795        // bound stays as `Spanned<Expr>` so backends render it through
1796        // their own emitters (it can be a literal, a fn param, or a
1797        // small arith expression).
1798        if let RecursionPlan::IntAscending { param_index, bound } = plan {
1799            if let Some((param_name, _)) = fd.params.get(*param_index) {
1800                ir.fn_contracts.insert(
1801                    canonical_key,
1802                    FnContract {
1803                        source_name: fn_name.clone(),
1804                        recursion: Some(RecursionContract::Fuel {
1805                            fuel_metric: crate::ir::FuelMetric::BoundMinusParamNatAbsPlusOne {
1806                                param: param_name.clone(),
1807                                bound: inputs.resolve_expr(bound, scope),
1808                            },
1809                        }),
1810                    },
1811                );
1812            }
1813            continue;
1814        }
1815
1816        // ListStructural — structural recursion on a List<_> param.
1817        // Lean/Dafny don't actually use a fuel helper for this on
1818        // recent backends (structural recursion is natively
1819        // terminating); the metric stays as `SeqLenPlusOne` for
1820        // backend-symmetric framing, and the consumer ignores it
1821        // when emitting plain structural recursion.
1822        if let RecursionPlan::ListStructural { param_index } = plan {
1823            if let Some((param_name, _)) = fd.params.get(*param_index) {
1824                ir.fn_contracts.insert(
1825                    canonical_key,
1826                    FnContract {
1827                        source_name: fn_name.clone(),
1828                        recursion: Some(RecursionContract::Fuel {
1829                            fuel_metric: crate::ir::FuelMetric::SeqLenPlusOne {
1830                                param: param_name.clone(),
1831                            },
1832                        }),
1833                    },
1834                );
1835            }
1836            continue;
1837        }
1838
1839        // SizeOfStructural — recursion on a user ADT (e.g. an AST
1840        // type). Fuel metric `sizeOf(call_frame) + 1`. The classifier
1841        // doesn't pin a single bound param — `sizeOf` measures the
1842        // whole frame — so the IR variant carries no param name.
1843        if matches!(plan, RecursionPlan::SizeOfStructural) {
1844            ir.fn_contracts.insert(
1845                canonical_key,
1846                FnContract {
1847                    source_name: fn_name.clone(),
1848                    recursion: Some(RecursionContract::Fuel {
1849                        fuel_metric: crate::ir::FuelMetric::SizeOfPlusOne,
1850                    }),
1851                },
1852            );
1853            continue;
1854        }
1855
1856        // StringPosAdvance — `(s, pos)`-shape recursion: `s` invariant
1857        // (first param, String), `pos` advances (second param, Int).
1858        // Fuel formula `s.length - pos`.
1859        if matches!(plan, RecursionPlan::StringPosAdvance) {
1860            if let (Some((string_param, _)), Some((pos_param, _))) =
1861                (fd.params.first(), fd.params.get(1))
1862            {
1863                ir.fn_contracts.insert(
1864                    canonical_key,
1865                    FnContract {
1866                        source_name: fn_name.clone(),
1867                        recursion: Some(RecursionContract::Fuel {
1868                            fuel_metric: crate::ir::FuelMetric::StringLenMinusPos {
1869                                string_param: string_param.clone(),
1870                                pos_param: pos_param.clone(),
1871                            },
1872                        }),
1873                    },
1874                );
1875            }
1876            continue;
1877        }
1878
1879        // Mutual-recursion SCCs — each member of the SCC gets its own
1880        // plan with the same family. All three lower to a Lex fuel
1881        // metric; the params vector + rank distinguish per-shape /
1882        // per-member roles.
1883        //
1884        // - MutualIntCountdown: every member counts down its first
1885        //   Int param; rank stays 0 (no inter-member ranking — every
1886        //   edge decreases the shared dimension).
1887        // - MutualStringPosAdvance { rank }: (s, pos) shape across
1888        //   the SCC; rank distinguishes members for same-measure
1889        //   inter-fn edges.
1890        // - MutualSizeOfRanked { rank }: sizeOf measures the whole
1891        //   call frame; rank distinguishes members. No bound param —
1892        //   the empty params vec signals "frame-level measure".
1893        match plan {
1894            RecursionPlan::MutualIntCountdown => {
1895                let params = fd
1896                    .params
1897                    .first()
1898                    .map(|(n, _)| vec![n.clone()])
1899                    .unwrap_or_default();
1900                ir.fn_contracts.insert(
1901                    canonical_key,
1902                    FnContract {
1903                        source_name: fn_name.clone(),
1904                        recursion: Some(RecursionContract::Fuel {
1905                            fuel_metric: crate::ir::FuelMetric::Lex { params, rank: 0 },
1906                        }),
1907                    },
1908                );
1909                continue;
1910            }
1911            RecursionPlan::MutualStringPosAdvance { rank } => {
1912                let params = fd.params.iter().take(2).map(|(n, _)| n.clone()).collect();
1913                ir.fn_contracts.insert(
1914                    canonical_key,
1915                    FnContract {
1916                        source_name: fn_name.clone(),
1917                        recursion: Some(RecursionContract::Fuel {
1918                            fuel_metric: crate::ir::FuelMetric::Lex {
1919                                params,
1920                                rank: *rank,
1921                            },
1922                        }),
1923                    },
1924                );
1925                continue;
1926            }
1927            RecursionPlan::MutualSizeOfRanked { rank } => {
1928                ir.fn_contracts.insert(
1929                    canonical_key,
1930                    FnContract {
1931                        source_name: fn_name.clone(),
1932                        recursion: Some(RecursionContract::Fuel {
1933                            fuel_metric: crate::ir::FuelMetric::Lex {
1934                                params: Vec::new(),
1935                                rank: *rank,
1936                            },
1937                        }),
1938                    },
1939                );
1940                continue;
1941            }
1942            RecursionPlan::LinearRecurrence2 => {
1943                ir.fn_contracts.insert(
1944                    canonical_key,
1945                    FnContract {
1946                        source_name: fn_name.clone(),
1947                        recursion: Some(RecursionContract::LinearRecurrence2),
1948                    },
1949                );
1950                continue;
1951            }
1952            _ => {}
1953        }
1954
1955        let RecursionPlan::IntCountdownGuarded {
1956            param_index,
1957            base_arm_literal,
1958            base_arm_body,
1959            wildcard_arm_body,
1960            precondition,
1961        } = plan
1962        else {
1963            continue;
1964        };
1965        let Some((countdown_param_name, _)) = fd.params.get(*param_index) else {
1966            continue;
1967        };
1968
1969        let precondition_predicates: Vec<Predicate> = precondition
1970            .iter()
1971            .map(|clause| Predicate {
1972                free_vars: vec![(
1973                    countdown_param_name.clone(),
1974                    QuantifierType::Plain("Int".to_string()),
1975                )],
1976                expr: inputs.resolve_expr(clause, scope),
1977            })
1978            .collect();
1979
1980        ir.fn_contracts.insert(
1981            canonical_key,
1982            FnContract {
1983                source_name: fn_name.clone(),
1984                recursion: Some(RecursionContract::Native {
1985                    precondition: precondition_predicates,
1986                    measure: Measure::NatAbsInt {
1987                        param: countdown_param_name.clone(),
1988                    },
1989                    preservation: PreservationProof::IntCountdownLiteralZero,
1990                    decrease: DecreaseProof::NatAbsCountdown,
1991                    body: NativeIntCountdownBody {
1992                        base_arm_literal: *base_arm_literal,
1993                        base_arm_body: inputs.resolve_expr(base_arm_body, scope),
1994                        wildcard_arm_body: inputs.resolve_expr(wildcard_arm_body, scope),
1995                    },
1996                }),
1997            },
1998        );
1999    }
2000}
2001
2002/// Walk every verify block, lift `VerifyKind::Law` entries into
2003/// `ProofIR.law_theorems`.
2004///
2005/// Extracts the law's shape (quantifiers from `givens`, premises
2006/// from `when`, claim from `lhs == rhs`) and pins a `ProofStrategy`
2007/// via [`classify_law_strategy`]. Covered strategies: Reflexive,
2008/// Commutative / Associative / IdentityElement / AntiCommutative /
2009/// UnaryEqualsBinary (arithmetic wrappers), Induction (recursive
2010/// ADTs), LibraryAxiom (Map set/get), MapUpdatePostcondition,
2011/// MapKeyTrackedIncrement, SpecEquivalence{,SimpNormalized},
2012/// LinearIntSpecEquivalence, EffectfulSpecEquivalence (with Oracle
2013/// Lift), LinearArithmetic (catch-all over an unfold chain).
2014/// Unmatched shapes pin `BackendDispatch` and fall through to the
2015/// backend's residual chain (linear_recurrence2 emit + sampled /
2016/// guarded-domain fallback).
2017pub fn populate_law_theorems(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
2018    use crate::ast::{TopLevel, VerifyKind};
2019    use crate::ir::{LawTheorem, Predicate, Quantifier, QuantifierType};
2020
2021    let symbols = inputs.symbol_table;
2022
2023    // Verify-law blocks to lower, each tagged with the prefix of the
2024    // dep module that owns it (`None` = entry). The entry's own verify
2025    // blocks come from `entry_items`; dependency modules' proven laws
2026    // come from `ModuleInfo.verify_laws` (the cross-file law pool — a
2027    // dep law is lowered with the SAME strategy classification an entry
2028    // law of that shape gets, so it auto-proves and can be cited by a
2029    // consumer). The DAG invariant keeps the bare fn name unambiguous
2030    // within each scope.
2031    let entry_verifies = inputs.entry_items.iter().filter_map(|item| match item {
2032        TopLevel::Verify(vb) => Some((None, vb)),
2033        _ => None,
2034    });
2035    let dep_verifies = inputs.dep_modules.iter().flat_map(|m| {
2036        m.verify_laws
2037            .iter()
2038            .map(move |vb| (Some(m.prefix.as_str()), vb))
2039    });
2040    for (owning_prefix, vb) in entry_verifies.chain(dep_verifies) {
2041        let VerifyKind::Law(law) = &vb.kind else {
2042            continue;
2043        };
2044
2045        let quantifiers: Vec<Quantifier> = law
2046            .givens
2047            .iter()
2048            .map(|g| Quantifier {
2049                name: g.name.clone(),
2050                binder_type: QuantifierType::Plain(g.type_name.clone()),
2051            })
2052            .collect();
2053
2054        // The fn this law targets, keyed by its owning scope. For an
2055        // entry law the bare name resolves to an entry `FnId`; for a
2056        // dep law it resolves through `FnKey::in_module(prefix, name)`.
2057        // When the fn isn't in the symbol table (verify block targeting
2058        // a fn that doesn't exist), skip the law silently — the
2059        // typechecker / verify-driver surfaces the missing target
2060        // elsewhere.
2061        let target_key = match owning_prefix {
2062            Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &vb.fn_name),
2063            None => crate::ir::FnKey::entry(&vb.fn_name),
2064        };
2065        let Some(fn_id) = symbols.fn_id_of(&target_key) else {
2066            continue;
2067        };
2068
2069        // Scope for resolving the law's expressions: derived from the
2070        // target fn's owning module, NOT hardcoded to entry.
2071        let law_scope: Option<String> = symbols
2072            .fn_entry(fn_id)
2073            .key
2074            .scope_str()
2075            .map(|s| s.to_string());
2076        let law_scope_ref = law_scope.as_deref();
2077
2078        let premises: Vec<Predicate> = match &law.when {
2079            Some(when_expr) => vec![Predicate {
2080                free_vars: quantifiers
2081                    .iter()
2082                    .map(|q| (q.name.clone(), q.binder_type.clone()))
2083                    .collect(),
2084                expr: inputs.resolve_expr(when_expr, law_scope_ref),
2085            }],
2086            None => Vec::new(),
2087        };
2088
2089        let strategy = classify_law_strategy(
2090            law,
2091            &vb.fn_name,
2092            inputs,
2093            &ir.refined_types,
2094            &ir.fn_contracts,
2095            law_scope_ref,
2096        );
2097
2098        ir.law_theorems.push(LawTheorem {
2099            fn_id,
2100            law_name: law.name.clone(),
2101            quantifiers,
2102            premises,
2103            claim_lhs: inputs.resolve_expr(&law.lhs, law_scope_ref),
2104            claim_rhs: inputs.resolve_expr(&law.rhs, law_scope_ref),
2105            strategy,
2106        });
2107    }
2108
2109    // Demand-driven well-founded graduation for the floor-division
2110    // window family: the figures' proof templates rest on the
2111    // power-of-two fn's defining equations and functional-induction
2112    // principle, which the fuel encoding destroys (the fuel arg on
2113    // the recursive call differs from the callee's own measure, so
2114    // nothing universal is provable through `__fuel`). Upgrade the
2115    // cited pow fn's contract from `Fuel { NatAbsPlusOne }` to the
2116    // native `WellFoundedToNat` form (`floor_div: None` — the guarded
2117    // subtractive countdown whose `n <= 0` base guard puts `n >= 1`
2118    // in the decreasing goal's context, so `omega` closes the
2119    // measure bare). Scoped on purpose: a pow-shaped fn in a file
2120    // with no recognized window law keeps its established fuel
2121    // emission, so nothing outside the family moves.
2122    // Scope-aware resolution: a floor-window law that lives in a DEP
2123    // module cites its power-of-two fn in THAT module's scope, so the
2124    // graduation must resolve the pow fn relative to the law's own
2125    // owning scope (derived from the law subject's `FnId`), not entry
2126    // only. Without this, a dep module keeps its pow fn on the fuel
2127    // encoding while the dep's own window support theorems demand the
2128    // well-founded `.induct` / `.eq_def`, breaking the dep build (the
2129    // emitted dep module references `<pow>.induct`, which a fuel def
2130    // doesn't have).
2131    let window_pow_ids: HashSet<crate::ir::FnId> = ir
2132        .law_theorems
2133        .iter()
2134        .filter_map(|t| {
2135            let pow_fn = match &t.strategy {
2136                crate::ir::ProofStrategy::FloorDivWindow { figure } => match figure {
2137                    crate::ir::FloorWindowFigure::PowPositive { pow_fn } => pow_fn,
2138                    crate::ir::FloorWindowFigure::PowSumSplit { pow_fn } => pow_fn,
2139                    crate::ir::FloorWindowFigure::SigWindow { pow_fn, .. } => pow_fn,
2140                    crate::ir::FloorWindowFigure::ProductWindow { pow_fn, .. } => pow_fn,
2141                    crate::ir::FloorWindowFigure::FloorPow2Window { pow_fn, .. } => pow_fn,
2142                    crate::ir::FloorWindowFigure::FloorPow2Cancel { pow_fn, .. } => pow_fn,
2143                },
2144                _ => return None,
2145            };
2146            let scope = symbols
2147                .fn_entry(t.fn_id)
2148                .key
2149                .scope_str()
2150                .map(|s| s.to_string());
2151            let key = match &scope {
2152                Some(prefix) => crate::ir::FnKey::in_module(prefix.clone(), pow_fn),
2153                None => crate::ir::FnKey::entry(pow_fn),
2154            };
2155            symbols
2156                .fn_id_of(&key)
2157                .or_else(|| symbols.fn_id_of(&crate::ir::FnKey::entry(pow_fn)))
2158        })
2159        .collect();
2160    for fn_id in window_pow_ids {
2161        let Some(contract) = ir.fn_contracts.get_mut(&fn_id) else {
2162            continue;
2163        };
2164        if let Some(crate::ir::RecursionContract::Fuel {
2165            fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne { param },
2166        }) = &contract.recursion
2167        {
2168            contract.recursion = Some(crate::ir::RecursionContract::WellFoundedToNat {
2169                param: param.clone(),
2170                floor_div: None,
2171            });
2172        }
2173    }
2174
2175    // Demand-driven well-founded graduation for the inductive `when`-law
2176    // families that rest on a recursive countdown fn's `.eq_def` defining
2177    // equations and `.induct` functional-induction principle, which the fuel
2178    // encoding destroys exactly as it does for the window family above. Two
2179    // shapes demand it: the GENERAL recursive-monotonicity family (a universal
2180    // `m <= n -> f m <= f n`, or its Fraction-order `isNonNeg(minus(g(HI),
2181    // g(LO)))` sibling) and the content-blind HOMOMORPHISM family (a universal
2182    // `subject(a + b) = subject(a) OP2 subject(b)` over a guarded countdown
2183    // subject — the power-of-two / power-of-three sum split). Graduate every such
2184    // recursive countdown fn from `Fuel { NatAbsPlusOne }` to the native
2185    // `WellFoundedToNat` form. Keyed on the law SHAPE and the fn's existing
2186    // countdown contract (which already certifies the `p <= 0` guard the bare
2187    // `decreasing_by omega` needs), never on a fn name — the same conservatism as
2188    // the window pass: a countdown fn no such law mentions keeps its established
2189    // fuel emission.
2190    let mono_fns = induction_demanded_countdown_fns(inputs);
2191    for (scope, name) in mono_fns {
2192        let key = match &scope {
2193            Some(prefix) => crate::ir::FnKey::in_module(prefix.clone(), &name),
2194            None => crate::ir::FnKey::entry(&name),
2195        };
2196        let Some(fn_id) = symbols
2197            .fn_id_of(&key)
2198            .or_else(|| symbols.fn_id_of(&crate::ir::FnKey::entry(&name)))
2199        else {
2200            continue;
2201        };
2202        let Some(contract) = ir.fn_contracts.get_mut(&fn_id) else {
2203            continue;
2204        };
2205        if let Some(crate::ir::RecursionContract::Fuel {
2206            fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne { param },
2207        }) = &contract.recursion
2208        {
2209            contract.recursion = Some(crate::ir::RecursionContract::WellFoundedToNat {
2210                param: param.clone(),
2211                floor_div: None,
2212            });
2213        }
2214    }
2215}
2216
2217/// `(owning_scope, fn_source_name)` for every recursive countdown fn an
2218/// inductive `when`-law reasons about — the demand the general
2219/// recursive-monotonicity rung, the signed-power-of-two adapter, AND the
2220/// content-blind homomorphism rung place on the well-founded `.induct` /
2221/// `.eq_def`. Detected purely from the SHAPE of each law's subject body / claim:
2222/// the plain order `f(LO) <= f(HI)`, the Fraction order `isNonNeg(minus(g(HI),
2223/// g(LO)))` whose `g` calls a single recursive countdown fn, or the homomorphism
2224/// `subject(a + b) = subject(a) OP2 subject(b)` over a guarded countdown subject.
2225/// Name-blind.
2226fn induction_demanded_countdown_fns(inputs: &ProofLowerInputs) -> Vec<(Option<String>, String)> {
2227    use crate::ast::{TopLevel, VerifyKind};
2228
2229    fn dotted(e: &crate::ast::Spanned<Expr>) -> Option<String> {
2230        match &e.node {
2231            Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.clone()),
2232            Expr::Attr(b, f) => dotted(b).map(|p| format!("{p}.{f}")),
2233            _ => None,
2234        }
2235    }
2236    fn short(name: &str) -> &str {
2237        name.rsplit('.').next().unwrap_or(name)
2238    }
2239    // `f` when `e` is `f(arg)` (single-argument call), as a short name.
2240    fn unary_callee_short(e: &crate::ast::Spanned<Expr>) -> Option<String> {
2241        match &e.node {
2242            Expr::FnCall(c, a) if a.len() == 1 => Some(short(&dotted(c)?).to_string()),
2243            _ => None,
2244        }
2245    }
2246    // Whether `fd` is a `p <= 0`-guarded single-step countdown — the shape whose
2247    // `n <= 0` base guard puts `n >= 1` in the well-founded measure's decreasing
2248    // goal, so the graduated form's bare `decreasing_by omega` closes. Only such
2249    // fns are safe to lift off fuel (an unguarded countdown, e.g. a tail-recursive
2250    // helper that relies on its caller, would leave `omega` unable to prove the
2251    // measure decrease). Mirrors the Lean rung's own firing gate.
2252    fn is_le0_guarded_countdown(fd: &FnDef) -> bool {
2253        let Some((p, ty)) = fd.params.first() else {
2254            return false;
2255        };
2256        if fd.params.len() != 1 || ty.trim() != "Int" {
2257            return false;
2258        }
2259        let [crate::ast::Stmt::Expr(body)] = fd.body.stmts() else {
2260            return false;
2261        };
2262        let Expr::Match { subject, arms } = &body.node else {
2263            return false;
2264        };
2265        let Expr::BinOp(crate::ast::BinOp::Lte, sl, sr) = &subject.node else {
2266            return false;
2267        };
2268        dotted(sl).as_deref() == Some(p.as_str())
2269            && matches!(&sr.node, Expr::Literal(crate::ast::Literal::Int(0)))
2270            && arms.len() == 2
2271    }
2272    // Collect every fn-call short name reachable in `e`.
2273    fn collect_calls(e: &crate::ast::Spanned<Expr>, out: &mut Vec<String>) {
2274        match &e.node {
2275            Expr::FnCall(c, args) => {
2276                if let Some(n) = dotted(c) {
2277                    out.push(short(&n).to_string());
2278                }
2279                for a in args {
2280                    collect_calls(a, out);
2281                }
2282            }
2283            Expr::BinOp(_, l, r) => {
2284                collect_calls(l, out);
2285                collect_calls(r, out);
2286            }
2287            Expr::Neg(b) | Expr::Attr(b, _) | Expr::ErrorProp(b) => collect_calls(b, out),
2288            Expr::Match { subject, arms } => {
2289                collect_calls(subject, out);
2290                for arm in arms {
2291                    collect_calls(&arm.body, out);
2292                }
2293            }
2294            _ => {}
2295        }
2296    }
2297
2298    let mut demanded: Vec<(Option<String>, String)> = Vec::new();
2299    let entry_verifies = inputs.entry_items.iter().filter_map(|item| match item {
2300        TopLevel::Verify(vb) => Some((None, vb)),
2301        _ => None,
2302    });
2303    let dep_verifies = inputs.dep_modules.iter().flat_map(|m| {
2304        m.verify_laws
2305            .iter()
2306            .map(move |vb| (Some(m.prefix.clone()), vb))
2307    });
2308    for (scope, vb) in entry_verifies.chain(dep_verifies) {
2309        let VerifyKind::Law(law) = &vb.kind else {
2310            continue;
2311        };
2312        if law.when.is_none() {
2313            continue;
2314        }
2315        let recursive = inputs.recursive_pure_fn_names_in_scope(scope.as_deref());
2316        // The order comparison is the subject fn's body (the `…Monotone(args)
2317        // holds` form), or the law claim itself.
2318        let body: Option<&crate::ast::Spanned<Expr>> = inputs
2319            .find_fn_def_by_call_name(&vb.fn_name)
2320            .and_then(|fd| match fd.body.stmts() {
2321                [crate::ast::Stmt::Expr(e)] => Some(e),
2322                _ => None,
2323            });
2324        let mut consider = |cmp: &crate::ast::Spanned<Expr>| {
2325            match &cmp.node {
2326                // Plain integer order `f(LO) <= f(HI)` — demand `f` itself.
2327                Expr::BinOp(crate::ast::BinOp::Lte, l, r) => {
2328                    if let (Some(lf), Some(rf)) = (unary_callee_short(l), unary_callee_short(r))
2329                        && lf == rf
2330                        && recursive.contains(&lf)
2331                        && inputs
2332                            .find_fn_def_by_call_name(&lf)
2333                            .is_some_and(is_le0_guarded_countdown)
2334                    {
2335                        demanded.push((scope.clone(), lf));
2336                    }
2337                }
2338                // Fraction order `isNonNeg(minus(g(HI), g(LO)))` — demand the
2339                // recursive countdown fn `g` sign-splits over.
2340                Expr::FnCall(c, a)
2341                    if a.len() == 1 && dotted(c).as_deref().map(short) == Some("isNonNeg") =>
2342                {
2343                    if let Expr::FnCall(mc, ma) = &a[0].node
2344                        && dotted(mc).as_deref().map(short) == Some("minus")
2345                        && ma.len() == 2
2346                        && let (Some(g_hi), Some(g_lo)) =
2347                            (unary_callee_short(&ma[0]), unary_callee_short(&ma[1]))
2348                        && g_hi == g_lo
2349                        && let Some(g_fd) = inputs.find_fn_def_by_call_name(&g_hi)
2350                        && let [crate::ast::Stmt::Expr(g_body)] = g_fd.body.stmts()
2351                    {
2352                        let mut calls = Vec::new();
2353                        collect_calls(g_body, &mut calls);
2354                        for n in calls {
2355                            if recursive.contains(&n)
2356                                && inputs
2357                                    .find_fn_def_by_call_name(&n)
2358                                    .is_some_and(is_le0_guarded_countdown)
2359                            {
2360                                demanded.push((scope.clone(), n));
2361                            }
2362                        }
2363                    }
2364                }
2365                _ => {}
2366            }
2367        };
2368        if let Some(b) = body {
2369            consider(b);
2370        }
2371        consider(&law.lhs);
2372
2373        // Homomorphism `subject(a + b) = subject(a) OP2 subject(b)` — the
2374        // arithmetic (guarded countdown) carrier the content-blind homomorphism
2375        // rung inducts over. Demand the recursive countdown `subject`. (A
2376        // structural-ADT homomorphism, e.g. list length over concatenation, is
2377        // already native — Lean infers its structural termination — so it places
2378        // no fuel-graduation demand and is not detected here.)
2379        if let Expr::BinOp(op2, ra, rb) = &law.rhs.node
2380            && matches!(op2, crate::ast::BinOp::Add | crate::ast::BinOp::Mul)
2381            && let (Some(subj), Some(subj_rb)) = (unary_callee_short(ra), unary_callee_short(rb))
2382            && subj == subj_rb
2383            && let Expr::FnCall(lc, la) = &law.lhs.node
2384            && la.len() == 1
2385            && dotted(lc).as_deref().map(short) == Some(subj.as_str())
2386            && matches!(&la[0].node, Expr::BinOp(crate::ast::BinOp::Add, _, _))
2387            && recursive.contains(&subj)
2388            && inputs
2389                .find_fn_def_by_call_name(&subj)
2390                .is_some_and(is_le0_guarded_countdown)
2391        {
2392            demanded.push((scope.clone(), subj));
2393        }
2394    }
2395    demanded
2396}
2397
2398/// Pick the strategy `LawLower` should pin on a `(fn, law)` pair.
2399///
2400/// Decision order — specific algebraic properties first, then
2401/// generic linear-arithmetic catch-all, then `BackendDispatch`:
2402/// 1. `Reflexive` — `law.lhs ≡ law.rhs` syntactically.
2403/// 2. `Commutative { op }` — fn body is `a <op> b`, claim is
2404///    `f(a, b) = f(b, a)` (op restricted to commutative ones).
2405/// 3. `Associative { op }` — same body, 3 givens, assoc claim.
2406/// 4. `IdentityElement { op }` — `f(a, e) = a` (or `f(e, a) = a`),
2407///    where `e` is the op's identity. Covers Add/Mul both-sided
2408///    plus Sub right-sided.
2409/// 5. `AntiCommutative { op: Sub, neg_on_rhs }` — `f(a, b) =
2410///    -f(b, a)` form. Sub-only (Mul has no anti-commutative law).
2411/// 6. `UnaryEqualsBinary { inner_fn }` — outer fn is unary, claim
2412///    binds it to the inner binary fn at a constant.
2413/// 7. `LinearArithmetic { unfold_fns, ... }` — catch-all when the
2414///    law reduces to linear arith after unfolding the call chain.
2415/// 8. `EnumConstantFold { unfold_fns }` — ground law over fixed
2416///    enum/ADT constructor args, scalar return (#466).
2417/// 9. `FiniteDomainCases { givens }` — every given ranges over a
2418///    closed finite domain (Bool / fieldless enum, product ≤ 16);
2419///    closes by exhaustive `cases` enumeration.
2420/// 10. `RingIdentity { unfold_fns }` — unconditional ring identity
2421///     over Int-component records (cross-multiplication equality);
2422///     runs before the prelude-simp rung, which would otherwise claim
2423///     the shape and park it on a caught sorry.
2424/// 11. `IntDecimalRoundtrip { … }` — canonical decimal-Int
2425///     parse/serialize roundtrip over a recognized string-pos scanner;
2426///     runs before the prelude-simp rung, which would otherwise claim
2427///     the shape and park it on a caught sorry.
2428/// 12. `SimpOverPreludeLemmas { … }` — builtin-roundtrip shape; the
2429///     Lean backend renders it AFTER its legacy chain, so it fires
2430///     exactly where the bare-`sorry` universal used to.
2431/// 13. `BackendDispatch` — backend's ad-hoc chain decides.
2432///
2433/// (The induction/spec-equivalence/Map families detected between
2434/// these rungs are documented at their detector sites below.)
2435fn classify_law_strategy(
2436    law: &crate::ast::VerifyLaw,
2437    fn_name: &str,
2438    inputs: &ProofLowerInputs,
2439    refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
2440    fn_contracts: &std::collections::HashMap<crate::ir::FnId, crate::ir::FnContract>,
2441    scope: Option<&str>,
2442) -> crate::ir::ProofStrategy {
2443    use crate::ir::ProofStrategy;
2444
2445    // Result-pipeline chain equivalence (stage 8b of #232) — `?`
2446    // propagation `chain_qm(x)` vs nested-match `chain_manual(x)`.
2447    // Both sides unfold to the same nested match; the proof closes
2448    // by `unfold + repeat split`.
2449    if law.when.is_none()
2450        && let Some(s) = detect_result_pipeline_chain_equivalence(law, fn_name, inputs)
2451    {
2452        return s;
2453    }
2454    // Wrapper-over-recursion with monoidal accumulator (stage 8 of
2455    // #232) — runs before generic induction because its aux-lemma
2456    // template closes laws naive induction can't (e.g. `sum(xs) ==
2457    // sumDirect(xs)` where `sum(xs) = sumTR(xs, 0)`). Detected
2458    // when `fn_name` is registered as a `WrapperOverRecursion`
2459    // pattern in `ProgramShape` AND the law shape is
2460    // `wrapper(g) == other(g)` AND the inner fn body matches the
2461    // monoidal-accumulator template.
2462    if law.when.is_none()
2463        && let Some(s) = detect_wrapper_over_recursion(law, fn_name, inputs)
2464    {
2465        return s;
2466    }
2467    // Tail-recursive fold with a FIXED base param (TIP prop_35,
2468    // `exp x y = qexp x y one`). The 2-given inline-wrapper shape the
2469    // `WrapperOverRecursion` recognizer can't reach: a 3-arg loop whose
2470    // extra leading param is held fixed and whose combine multiplies the
2471    // accumulator by that fixed param. Emits the accumulator-generalization
2472    // lemma plus the main universal law.
2473    if law.when.is_none()
2474        && let Some(s) = detect_tailrec_fixed_base_fold(law, inputs)
2475    {
2476        return s;
2477    }
2478    // Structural induction runs first — when any given binds a
2479    // recursive ADT, induction over its variants is the canonical
2480    // proof. Reflexive could also fire on `f(t) = f(t)` for `t: Tree`
2481    // but induction subsumes (one trivial case per variant) and is
2482    // the legacy chain's first pick. `when` clauses block induction
2483    // — a non-closing `when` law would emit a 2-arm induction ladder
2484    // (2 sorries) instead of the bounded sampled-domain fallback,
2485    // regressing output cleanliness; a non-regressing when-aware
2486    // induction path is a follow-up.
2487    if law.when.is_none()
2488        && let Some(param) = detect_induction_target(law, inputs)
2489    {
2490        return ProofStrategy::Induction { param };
2491    }
2492    if law.lhs == law.rhs {
2493        return ProofStrategy::Reflexive;
2494    }
2495    // Binary-wrapper-shaped laws first. `wrapper_binop` returns
2496    // `None` for non-binary fns — unary wrappers are tried after
2497    // this block falls through.
2498    if let Some(op) = wrapper_binop(fn_name, inputs) {
2499        if detect_wrapper_commutative(law, fn_name, op) {
2500            return ProofStrategy::Commutative { op };
2501        }
2502        if detect_wrapper_associative(law, fn_name, op) {
2503            return ProofStrategy::Associative { op };
2504        }
2505        if detect_wrapper_identity(law, fn_name, op) {
2506            return ProofStrategy::IdentityElement { op };
2507        }
2508        // Sub right-identity collapses into IdentityElement —
2509        // same emit (`simp [fn]`), different lhs/rhs shape. The
2510        // detector validates the right-side `f(a, 0) = a` form
2511        // (`f(0, a) = -a` doesn't equal `a`, so Sub is one-sided).
2512        if matches!(op, crate::ast::BinOp::Sub) && detect_wrapper_sub_right_identity(law, fn_name) {
2513            return ProofStrategy::IdentityElement { op };
2514        }
2515        // Anti-commutative is Sub-specific (Add/Mul are
2516        // commutative, no anti-commutativity). The op tag keeps
2517        // it parameterised even though only Sub currently fires.
2518        if matches!(op, crate::ast::BinOp::Sub)
2519            && let Some(neg_on_rhs) = detect_wrapper_sub_anti_commutative(law, fn_name)
2520        {
2521            return ProofStrategy::AntiCommutative { op, neg_on_rhs };
2522        }
2523    }
2524    // Unary fn equal to binary fn at a constant — `fn_name` is the
2525    // unary outer; the binary fn name is captured for backends.
2526    if let Some(inner_fn) = detect_wrapper_unary_equivalence(law, fn_name, inputs) {
2527        return ProofStrategy::UnaryEqualsBinary { inner_fn };
2528    }
2529    // Library axiom instances — Map.has-after-set, Map.get-after-set.
2530    // Specific shape, single-line `simpa using axiom` emit on Lean.
2531    if let Some((axiom, args)) = detect_map_set_axiom(law) {
2532        let resolved_args: Vec<_> = args.iter().map(|a| inputs.resolve_expr(a, scope)).collect();
2533        return ProofStrategy::LibraryAxiom {
2534            axiom,
2535            args: resolved_args,
2536        };
2537    }
2538    // Tracked-counter increment: specialised body template + `+ 1`
2539    // rhs. Checked before the more general MapUpdatePostcondition so
2540    // the tighter strategy wins for this shape.
2541    if let Some(inc) = detect_map_key_tracked_increment(law, fn_name, inputs) {
2542        return ProofStrategy::MapKeyTrackedIncrement {
2543            outer_fn: inc.outer_fn,
2544            map_arg: inputs.resolve_expr(&inc.map_arg, scope),
2545            key_arg: inputs.resolve_expr(&inc.key_arg, scope),
2546        };
2547    }
2548    // Post-condition of an inline-defined map-update fn — case-split
2549    // over `Map.get m k` and apply the `Map.set` axioms.
2550    if let Some(post) = detect_map_update_postcondition(law, fn_name, inputs) {
2551        return ProofStrategy::MapUpdatePostcondition {
2552            outer_fn: post.outer_fn,
2553            kind: post.kind,
2554            map_arg: inputs.resolve_expr(&post.map_arg, scope),
2555            key_arg: inputs.resolve_expr(&post.key_arg, scope),
2556            extra_unfolds: post.extra_unfolds,
2557        };
2558    }
2559    // Functional equivalence of `vb.fn_name` and a same-named spec
2560    // fn whose body is syntactically identical to the impl's.
2561    if let Some(extra_unfolds) = detect_spec_equivalence(law, fn_name, inputs) {
2562        return ProofStrategy::SpecEquivalence { extra_unfolds };
2563    }
2564    // Broader spec equivalence — bodies differ syntactically but
2565    // normalize to same under substitution + arithmetic identity
2566    // folding. Runs after the strict `SpecEquivalence` so the
2567    // tighter detector wins when both would match.
2568    if let Some(extra_unfolds) = detect_simp_normalized_spec_equivalence(law, fn_name, inputs) {
2569        return ProofStrategy::SpecEquivalenceSimpNormalized { extra_unfolds };
2570    }
2571    // Linear-Int spec equivalence — substituted bodies are pure
2572    // linear arithmetic over Int givens; decided by `omega` / LIA.
2573    if let Some((unfolded_impl, unfolded_spec)) =
2574        detect_linear_int_spec_equivalence(law, fn_name, inputs)
2575    {
2576        return ProofStrategy::LinearIntSpecEquivalence {
2577            unfolded_impl: inputs.resolve_expr(&unfolded_impl, scope),
2578            unfolded_spec: inputs.resolve_expr(&unfolded_spec, scope),
2579        };
2580    }
2581    // Effectful counterpart — Oracle Lift normalises both sides
2582    // (oracle args injected into impl call) and the lowerer matches
2583    // the canonical `impl(args) == spec(args)` shape on the
2584    // rewritten form. Fires on real oracle-spec laws like
2585    // `pickPair() => pairSpec(BranchPath.Root, rnd)`.
2586    if let Some(spec_fn) = detect_effectful_spec_equivalence(law, fn_name, inputs) {
2587        return ProofStrategy::EffectfulSpecEquivalence {
2588            impl_fn: fn_name.to_string(),
2589            spec_fn,
2590        };
2591    }
2592    // Second-order linear recurrence (fib / fibSpec shape). Detector
2593    // validates impl as tail-rec wrapper, spec as direct second-order
2594    // recurrence, helper as their shared affine worker — all three
2595    // shapes pinned in `lean::recurrence`. Backends consume the
2596    // (impl_fn, spec_fn, helper_fn) names from IR; the proof template
2597    // differs per target (Lean Nat-helper + induction; Dafny still
2598    // pending — issue #116).
2599    if let Some((spec_fn, helper_fn)) =
2600        detect_linear_recurrence2_spec_equivalence(law, fn_name, inputs)
2601    {
2602        return ProofStrategy::LinearRecurrence2SpecEquivalence {
2603            impl_fn: fn_name.to_string(),
2604            spec_fn,
2605            helper_fn,
2606        };
2607    }
2608    // Nonnegativity / order over a NONLINEAR Int product (`E >= 0` or
2609    // `prod <= prod`) — the inequality sibling of `RingIdentity`. Placed
2610    // BEFORE the `LinearArithmetic` catch-all because that rung claims any
2611    // all-Int law (these nonlinear `when`-guarded ones included) and, unable
2612    // to close `0 <= a*b` / `e*e <= b*b` with `omega`, renders them on the
2613    // bounded sampled fallback. The detector requires a genuine
2614    // variable×variable product, so it claims ONLY goals `omega` provably
2615    // cannot decide — every linear law still flows to `LinearArithmetic`.
2616    // NOT `when`-gated: the Newton-Raphson factor-sign guards ride in as a
2617    // `when` premise threaded into the universal statement.
2618    if let Some(unfold_fns) = detect_nonlinear_nonneg(law, fn_name, inputs) {
2619        return ProofStrategy::NonlinearNonneg { unfold_fns };
2620    }
2621    // Linear arithmetic over an unfold chain — generic catch-all.
2622    // Named for the semantic, not the backend tactic.
2623    if let Some(plan) = detect_simp_omega_unfold(law, fn_name, inputs, refined_types) {
2624        return ProofStrategy::LinearArithmetic {
2625            unfold_fns: plan.unfold_fns,
2626            wrapper_return: plan.wrapper_return,
2627            smart_guard: plan.smart_guard,
2628            lifted: plan.lifted,
2629        };
2630    }
2631    // Ground constant-fold over fixed ADT/enum constructors — the
2632    // last typed fallback before `BackendDispatch`. Fires only for the
2633    // narrow shape no earlier detector accepts: a non-recursive fn with
2634    // ≥1 non-Int param, whose every non-Int param is pinned to a
2635    // constructor literal at the law's call site(s). LinearArithmetic
2636    // rejected it (non-Int param), Induction rejected it (no recursive
2637    // ADT given) — so this can't steal a law another strategy owns.
2638    if law.when.is_none()
2639        && let Some(unfold_fns) = detect_enum_constant_fold(law, fn_name, inputs)
2640    {
2641        return ProofStrategy::EnumConstantFold { unfold_fns };
2642    }
2643    // Closed finite-domain enumeration — the final typed fallback
2644    // before `BackendDispatch`. Fires when EVERY given ranges over a
2645    // closed, small domain (Bool or an all-fieldless user enum, ≤ 16
2646    // total combinations): exhaustive `cases` over the givens yields
2647    // ground goals per leaf, so deliberately NO call-shape inspection,
2648    // NO return-type gate and NO recursion gate — closed enumeration
2649    // makes those irrelevant (fuel-wrapped callees compute through
2650    // constant-measure constructor args). That is exactly why this is
2651    // a NEW detector and not a relaxation of `EnumConstantFold`, whose
2652    // literal-pinning / non-recursive / scalar-return gates are
2653    // load-bearing for its simp cascade.
2654    if law.when.is_none()
2655        && let Some(givens) = detect_finite_domain_cases(law, inputs)
2656    {
2657        return ProofStrategy::FiniteDomainCases { givens };
2658    }
2659    // Unconditional ring identity over Int-component records — runs
2660    // BEFORE the prelude-simp rung because that rung would otherwise
2661    // claim the shape (record givens, non-recursive pure cone) and
2662    // park it on a caught sorry: its minimal simp set has no AC-ring
2663    // normalization, and the permutational package this strategy
2664    // emits cannot be added there (it would loop or destroy the
2665    // normal forms other strategies rely on). Every earlier rung has
2666    // already declined: LinearArithmetic rejects non-Int record
2667    // givens, EnumConstantFold needs constructor-literal-pinned
2668    // params, FiniteDomainCases needs closed finite domains — so the
2669    // pin cannot steal a law a cheaper strategy closes today.
2670    if law.when.is_none()
2671        && let Some(unfold_fns) = detect_ring_identity(law, fn_name, inputs)
2672    {
2673        return ProofStrategy::RingIdentity { unfold_fns };
2674    }
2675    // Decimal-Int parse/serialize roundtrip — runs BEFORE the prelude-
2676    // simp rung because that rung would otherwise claim the shape (the
2677    // lhs cone is fuel-wrapped with measure-closed args) and park it on
2678    // a caught sorry the scanner barrier guarantees. The detector
2679    // validates the ENTIRE canonical parser shape (head-char dispatch
2680    // arms, single recognized scanner, slice + `Int.fromString` leaf),
2681    // so it cannot fire on the #469 prelude-simp laws (`finishInt` /
2682    // `finishNumber` / `afterIntChar` / `finishString` — wrong arity or
2683    // non-literal second arg at the law call site).
2684    if law.when.is_none()
2685        && let Some(s) = detect_int_decimal_roundtrip(law, fn_name, inputs, fn_contracts)
2686    {
2687        return s;
2688    }
2689    // Escaped-string parse/serialize roundtrip — the string-escape
2690    // sibling of the decimal roundtrip above, and like it placed
2691    // BEFORE the prelude-simp rung, which would otherwise claim the
2692    // shape (fuel-wrapped lhs cone) and park it on a caught sorry.
2693    // The detector validates the ENTIRE producer/consumer pair
2694    // (classifier escape table aligned arm-by-arm with the consumer's
2695    // escape dispatcher, control-escape prefix, threshold agreement,
2696    // fuel contracts), so it cannot fire on any shape whose
2697    // synthesized suffix-invariant proof would not close.
2698    if law.when.is_none()
2699        && let Some(s) = detect_string_escape_roundtrip(law, inputs, fn_contracts)
2700    {
2701        return s;
2702    }
2703    // Floor-division window family — laws over a power-of-two fn, a
2704    // guard-validated floor-halving binary-exponent fn, and the
2705    // window predicates built from them. The detectors are
2706    // deliberately narrow (exactly the hand-validated figures —
2707    // pow positivity, the pow sum homomorphism, the significand
2708    // window, the product window) and key on structure plus the
2709    // exponent fn's `WellFoundedToNat` contract, never on names.
2710    // Runs after every cheaper rung declined: LinearArithmetic
2711    // rejects the `Result.withDefault` cone and recursive callees,
2712    // Induction needs a recursive-ADT given, EnumConstantFold /
2713    // FiniteDomainCases need non-Int / closed domains — so the pin
2714    // cannot steal a law another strategy closes today.
2715    if let Some(figure) = detect_floor_window(law, fn_name, inputs, fn_contracts) {
2716        return ProofStrategy::FloorDivWindow { figure };
2717    }
2718    // Builtin-roundtrip simp over the prelude's spec-lemma registry —
2719    // the very last typed fallback. The Lean backend deliberately
2720    // renders this strategy AFTER its whole legacy ad-hoc chain (see
2721    // `lean::law_auto`), so pinning it here cannot steal a law any
2722    // legacy fallback closes today: it fires exactly where the
2723    // sampled-sorry path used to emit a bare-`sorry` universal.
2724    if law.when.is_none()
2725        && let Some(s) = detect_simp_over_prelude_lemmas(law, fn_name, inputs, fn_contracts)
2726    {
2727        return s;
2728    }
2729    ProofStrategy::BackendDispatch
2730}
2731
2732mod finite_domain;
2733mod floor_window;
2734mod induction;
2735mod inequality;
2736mod int_decimal_roundtrip;
2737mod map_laws;
2738mod refinement;
2739mod ring;
2740mod simp;
2741mod spec_equivalence;
2742mod string_escape_roundtrip;
2743mod wrapper_laws;
2744
2745pub(crate) use induction::LawProofCone;
2746
2747use finite_domain::*;
2748use floor_window::*;
2749use induction::*;
2750use inequality::*;
2751use int_decimal_roundtrip::*;
2752use map_laws::*;
2753use refinement::*;
2754use ring::*;
2755use simp::*;
2756use spec_equivalence::*;
2757use string_escape_roundtrip::*;
2758use wrapper_laws::*;