Skip to main content

aver/codegen/
proof_lower.rs

1//! Build `ProofIR` from a `CodegenContext`.
2//!
3//! The lowering producer: types live in `src/ir/proof_ir.rs`, this
4//! file fills them in from a typechecked + analysed codegen
5//! context. Output lands in `CodegenContext.proof_ir`; both proof
6//! backends read from the same field, so any classifier-side
7//! decision flows consistently to Lean and Dafny without each
8//! backend re-running shape detection.
9//!
10//! Populates three IR sections: `refined_types` (refinement-via-
11//! opaque records → Lean Subtype / Dafny subset type),
12//! `fn_contracts` (per-pure-fn recursion shape: native /
13//! sized-fuel / linear recurrence), and `law_theorems` (per-verify-
14//! law strategy + quantifier decomposition + claim shape, with
15//! Oracle-Lift'd impl-spec calls for effectful equivalence).
16//!
17//! `tests/proof_ir_diff.rs` pins the producer's output for each
18//! canonical source pattern — divergence between the classifier and
19//! the IR populator surfaces there.
20//!
21//! # Epic #170 Phase 7 invariant — AST discovery + typed identity
22//!
23//! This module is the **last consumer** of raw `crate::ast::Expr`
24//! patterns in the codegen layer. That is intentional, not
25//! migration debt.
26//!
27//! ## What's AST-shaped (syntax-discovery-only)
28//!
29//! Detector helpers in this file (`detect_*`, `walk_for_*`,
30//! `callee_matches_name`, `call_named_args`, `binary_call_var_const`,
31//! `matches_ident_expr`) walk `ast::Expr` directly. They are
32//! **pattern matchers** over source shape — they look for things
33//! like `match n { 0 -> base; _ -> rec(n - 1) }` or
34//! `Map.has(outer(m, k), k)` to decide which `ProofStrategy` /
35//! `RecursionPlan` variant lowers a given fn or law. The pattern
36//! belongs in source-shape; rewriting them on `ResolvedExpr` would
37//! be the same logic spelled in a different enum, no extra safety.
38//!
39//! Every detector helper carries a `syntax-discovery-only` comment
40//! at its definition.
41//!
42//! ## What's identity-sensitive (typed IDs)
43//!
44//! Decisions that depend on **which fn / type / ctor** a name
45//! refers to (not just "does this name appear") MUST go through
46//! `SymbolTable` or `ProofIR.refined_types` (`TypeId`-keyed) /
47//! `ProofIR.fn_contracts` (`FnId`-keyed). Examples:
48//!
49//! - Refinement-carrier lookups go through `find_refined_type` /
50//!   `resolve_refined_type_in_with_key`, both of which canonicalise
51//!   the name through the symbol table before reaching the IR map.
52//! - Fn-contract lookups go through `find_fn_contract_for_fn` —
53//!   pointer-eq scope on `&FnDef` resolves to the right `FnId`.
54//! - The Lean native-guarded rewriter pins target by `FnId` via
55//!   `rewrite_native_guarded_calls_resolved_expr` (PR 169).
56//!
57//! ## What stays raw-AST as a documented identity exception
58//!
59//! Builtin matchers (`callee_is X for X ∈ {"Bool.and", "Map.set",
60//! …}`) compare against the canonical builtin namespace, which is
61//! global by spec — no per-scope identity to leak. Verify-law
62//! callsites all walk `vb.fn_name` (entry-only by parser grammar);
63//! the `EntryFnIndex` newtype in `verify_law.rs` pins the
64//! entry-only contract at the type level (PR 177).
65//!
66//! Full `ResolvedProofLowerView` + semantic matcher API
67//! (`callee_is_builtin`, `callee_is_fn(FnId)`, `ctor_is`,
68//! `ident_name`, `int_lit`) deferred per
69//! `project_phase_e_scope_b_deferred` memory until a real trigger
70//! lands (module-scoped verify, dotted law targets, LSP rename,
71//! cross-scope inliner).
72
73use std::collections::{HashMap, HashSet};
74
75use crate::ast::{Expr, FnDef, Literal, Spanned, TopLevel, TypeDef};
76use crate::codegen::common::expr_to_dotted_name;
77use crate::codegen::recursion::RecursionPlan;
78use crate::codegen::{CodegenContext, ModuleInfo};
79use crate::ir::proof_ir::{
80    DecreaseProof, FnContract, Measure, NativeIntCountdownBody, Predicate, PreservationProof,
81    ProofIR, QuantifierType, RecursionContract, RefinedTypeDecl,
82};
83
84/// Backend-neutral view of the data `proof_lower` needs. Built once
85/// per lowering call; lets the pipeline pass it through without
86/// requiring a fully-assembled `CodegenContext` (which only exists
87/// after `build_context` runs). Legacy callers still build the view
88/// from `&CodegenContext` via [`ProofLowerInputs::from_ctx`].
89///
90/// All fields are borrows — the struct never owns memory; the pipeline
91/// and `build_context` both already own the data and just lend it.
92///
93/// Post-Step-7c: every helper the lowerer touches
94/// (`refinement_info_for`, `analyze_plans`, the `detect.rs` shape
95/// checkers) reads its inputs through this view. No more
96/// `&CodegenContext` reach-through — the struct stands on its own.
97pub struct ProofLowerInputs<'a> {
98    /// Entry-file top-level items, post-pipeline (TCO etc. applied).
99    pub entry_items: &'a [TopLevel],
100    /// Dependent modules already split into type/fn defs.
101    pub dep_modules: &'a [ModuleInfo],
102    /// Set of dep module prefix strings (e.g. `"Models.User"`).
103    pub module_prefixes: &'a HashSet<String>,
104    /// Recursive fn ids from the `analyze` pipeline stage. Keyed
105    /// by opaque [`crate::ir::FnId`] so entry+module same-bare-name
106    /// fns don't merge. Per-scope helpers below project back to
107    /// `HashSet<String>` for consumers that operate on a single
108    /// scope (the DAG invariant keeps bare-name unambiguous within
109    /// a scope).
110    pub recursive_fns: &'a HashSet<crate::ir::FnId>,
111    /// Resolved-identity table (#138 phase E). When `Some`, the
112    /// populate-side resolves `FnKey` / `TypeKey` to `FnId` /
113    /// `TypeId` once at the IR boundary and keys `ProofIR.fn_contracts`
114    /// / `ProofIR.refined_types` / `LawTheorem.fn_id` by the opaque
115    /// IDs. Callers that haven't wired in the symbol-table stage
116    /// pass `None` and fall through to legacy key-typed maps
117    /// (transitional during phase E migration).
118    pub symbol_table: &'a crate::ir::SymbolTable,
119    /// Optional `ProgramShape` substrate (Stage 6b of #232). When
120    /// `Some`, `refinement_info_for` reads from the typed
121    /// `ModulePattern::RefinementSmartConstructor` entries instead of
122    /// re-walking the AST. `None` keeps the legacy walk path —
123    /// preserved for test fixtures that build `ProofLowerInputs` by
124    /// hand without going through the pipeline.
125    pub program_shape: Option<&'a crate::analysis::shape::ProgramShape>,
126}
127
128impl<'a> ProofLowerInputs<'a> {
129    /// Build a view from a fully-assembled `CodegenContext` — used
130    /// by `refresh_facts` (test helper) and by any caller that
131    /// already owns a built context. Reads only the fields the
132    /// lowerer actually needs.
133    pub fn from_ctx(ctx: &'a CodegenContext) -> Self {
134        Self {
135            entry_items: &ctx.items,
136            dep_modules: &ctx.modules,
137            module_prefixes: &ctx.module_prefixes,
138            recursive_fns: &ctx.recursive_fns,
139            symbol_table: &ctx.symbol_table,
140            program_shape: ctx.program_shape.as_ref(),
141        }
142    }
143
144    /// All pure fn defs across entry items and dep modules, in walk
145    /// order (entry first, then deps). `is_pure_fn` lives in the
146    /// Lean toplevel module today; pure_fns reaches there since the
147    /// pure-ness criterion is the same for every proof backend.
148    pub fn pure_fns(&self) -> Vec<&'a FnDef> {
149        // Order matches the legacy `lean::pure_fns(ctx)`: deps first,
150        // entry last. `call_graph::ordered_fn_components` is order-
151        // sensitive (SCC discovery order changes which member is
152        // chosen as the representative); flipping the order shifted
153        // some classifications between fuel and "outside subset".
154        self.dep_modules
155            .iter()
156            .flat_map(|m| m.fn_defs.iter())
157            .chain(self.entry_items.iter().filter_map(|item| match item {
158                TopLevel::FnDef(fd) => Some(fd),
159                _ => None,
160            }))
161            .filter(|fd| crate::codegen::common::is_pure_fn(fd))
162            .collect()
163    }
164
165    /// Recursive pure fn names. Filters `recursive_fns` by pure-ness.
166    /// Returns bare names (pure_fns view is the whole program here,
167    /// so any FnId in `recursive_fns` that maps back to a pure fn
168    /// gets its bare name surfaced for downstream classifiers).
169    pub fn recursive_pure_fn_names(&self) -> HashSet<String> {
170        let symbols = self.symbol_table;
171        let pure_ids: HashSet<crate::ir::FnId> = self
172            .pure_fns()
173            .into_iter()
174            .filter_map(|fd| {
175                let scope = self
176                    .dep_modules
177                    .iter()
178                    .find(|m| m.fn_defs.iter().any(|d| std::ptr::eq(d, fd)))
179                    .map(|m| m.prefix.as_str());
180                // **syntax-discovery-only** (epic #170 Phase 8
181                // guardrail): scope was just resolved via pointer-eq
182                // against dep modules — the `None` arm is the
183                // correct entry-scope key by construction (same
184                // shape as `fn_key_for_decl` in `codegen::common`).
185                let key = match scope {
186                    Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
187                    None => crate::ir::FnKey::entry(&fd.name),
188                };
189                symbols.fn_id_of(&key)
190            })
191            .collect();
192        self.recursive_fns
193            .intersection(&pure_ids)
194            .map(|id| symbols.fn_entry(*id).key.name.clone())
195            .collect()
196    }
197
198    /// Pure fns restricted to a single scope: `None` = entry only,
199    /// `Some(prefix)` = the dep module with that prefix only. Aver's
200    /// module DAG invariant rules out cross-module recursion SCCs,
201    /// so per-scope classification is the canonical view —
202    /// `populate_fn_contracts` walks this per scope to give each
203    /// `Module.fn` its own canonical key in `ir.fn_contracts`
204    /// instead of letting two same-bare-name fns silently merge.
205    pub fn pure_fns_in_scope(&self, scope: Option<&str>) -> Vec<&'a FnDef> {
206        match scope {
207            None => self
208                .entry_items
209                .iter()
210                .filter_map(|item| match item {
211                    TopLevel::FnDef(fd) => Some(fd),
212                    _ => None,
213                })
214                .filter(|fd| crate::codegen::common::is_pure_fn(fd))
215                .collect(),
216            Some(prefix) => self
217                .dep_modules
218                .iter()
219                .filter(|m| m.prefix == prefix)
220                .flat_map(|m| m.fn_defs.iter())
221                .filter(|fd| crate::codegen::common::is_pure_fn(fd))
222                .collect(),
223        }
224    }
225
226    /// Recursive pure fn names restricted to a single scope. Filters
227    /// the FnId-keyed `recursive_fns` to the ones whose canonical
228    /// scope matches `scope`, then projects back to bare names for
229    /// scope-local consumers (DAG invariant keeps bare-name
230    /// unambiguous within a single scope).
231    pub fn recursive_pure_fn_names_in_scope(&self, scope: Option<&str>) -> HashSet<String> {
232        let symbols = self.symbol_table;
233        let pure_ids: HashSet<crate::ir::FnId> = self
234            .pure_fns_in_scope(scope)
235            .into_iter()
236            .filter_map(|fd| {
237                // **syntax-discovery-only** (epic #170 Phase 8
238                // guardrail): scope is the caller's stated scope —
239                // `None` = entry, `Some(prefix)` = dep module. Both
240                // arms below are the correct key for the matching
241                // arm; bare-name keying is safe because the caller
242                // has already narrowed to a single scope.
243                let key = match scope {
244                    Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
245                    None => crate::ir::FnKey::entry(&fd.name),
246                };
247                symbols.fn_id_of(&key)
248            })
249            .collect();
250        self.recursive_fns
251            .intersection(&pure_ids)
252            .map(|id| symbols.fn_entry(*id).key.name.clone())
253            .collect()
254    }
255
256    /// Iterator over (`None` = entry, `Some(prefix)` = each dep
257    /// module) — drives `populate_fn_contracts`'s per-scope walk.
258    pub fn scopes(&self) -> Vec<Option<String>> {
259        let mut out = vec![None];
260        for m in self.dep_modules {
261            out.push(Some(m.prefix.clone()));
262        }
263        out
264    }
265
266    /// Scope of the dep module that owns `fd`, or `None` for entry
267    /// module fns. Pointer-eq match against `dep_modules`, mirroring
268    /// `crate::codegen::common::fn_owning_scope_for` but reading off
269    /// the lowering view (which doesn't carry a full `CodegenContext`).
270    pub fn fn_owning_scope(&self, fd: &FnDef) -> Option<&'a str> {
271        for m in self.dep_modules {
272            for f in &m.fn_defs {
273                if std::ptr::eq(f, fd) {
274                    return Some(m.prefix.as_str());
275                }
276            }
277        }
278        None
279    }
280
281    /// Resolve a raw-AST expression to its `ResolvedExpr` form under
282    /// the given scope. ProofIR stores resolved expressions (Phase E
283    /// PR 12 Scope A), so this helper is called at every producer
284    /// site that lifts a `Spanned<crate::ast::Expr>` slice from the
285    /// source into an IR field. Mirrors
286    /// `CodegenContext::resolve_expr` but reads only the
287    /// `symbol_table` carried on this view — proof lowering runs
288    /// inside the pipeline, before a full `CodegenContext` exists.
289    pub fn resolve_expr(
290        &self,
291        expr: &crate::ast::Spanned<crate::ast::Expr>,
292        scope: Option<&str>,
293    ) -> crate::ast::Spanned<crate::ir::hir::ResolvedExpr> {
294        use crate::ir::hir::{ResolveCtx, ResolvedStmt};
295        let mut rctx = ResolveCtx::new(self.symbol_table);
296        rctx.current_module = scope.map(String::from);
297        let stmt = crate::ast::Stmt::Expr(expr.clone());
298        match crate::ir::hir::resolve::resolve_stmt_external(&rctx, &stmt) {
299            ResolvedStmt::Expr(s) => s,
300            ResolvedStmt::Binding { value, .. } => value,
301        }
302    }
303
304    /// Names of every recursive user-defined type across entry + deps.
305    pub fn recursive_type_names(&self) -> HashSet<String> {
306        self.entry_items
307            .iter()
308            .filter_map(|item| match item {
309                TopLevel::TypeDef(td) => Some(td),
310                _ => None,
311            })
312            .chain(self.dep_modules.iter().flat_map(|m| m.type_defs.iter()))
313            .filter(|td| crate::codegen::common::is_recursive_type_def(td))
314            .map(|td| crate::codegen::common::type_def_name(td).to_string())
315            .collect()
316    }
317
318    /// Find a fn def by name across entry + deps. Falls back to the
319    /// last segment of a dotted call (e.g. `Module.fn` resolves to
320    /// `fn` when no exact-match candidate exists).
321    pub fn find_fn_def_by_call_name(&self, call_name: &str) -> Option<&'a FnDef> {
322        let find_exact = |name: &str| -> Option<&'a FnDef> {
323            self.dep_modules
324                .iter()
325                .flat_map(|m| m.fn_defs.iter())
326                .chain(self.entry_items.iter().filter_map(|item| match item {
327                    TopLevel::FnDef(fd) => Some(fd),
328                    _ => None,
329                }))
330                .find(|fd| fd.name == name)
331        };
332        find_exact(call_name).or_else(|| {
333            let short = call_name.rsplit('.').next()?;
334            find_exact(short)
335        })
336    }
337
338    /// Find a type def by bare name across entry + deps. None on miss
339    /// or when the name resolves to a non-Product / non-Sum shape.
340    pub fn find_type_def(&self, type_name: &str) -> Option<&'a TypeDef> {
341        self.entry_items
342            .iter()
343            .filter_map(|item| match item {
344                TopLevel::TypeDef(td) => Some(td),
345                _ => None,
346            })
347            .chain(self.dep_modules.iter().flat_map(|m| m.type_defs.iter()))
348            .find(|td| crate::codegen::common::type_def_name(td) == type_name)
349    }
350}
351
352/// Run every proof-export lowering in one shot — convenience for
353/// callers that want a fully-populated ProofIR. The pipeline calls
354/// the three `populate_*` fns directly so it can run them as
355/// independent stages and short-circuit on typecheck failure.
356pub fn lower(inputs: &ProofLowerInputs) -> ProofIR {
357    let mut ir = ProofIR::default();
358    populate_refined_types(inputs, &mut ir);
359    populate_fn_contracts(inputs, &mut ir);
360    populate_law_theorems(inputs, &mut ir);
361    ir
362}
363
364/// Refinement-via-opaque lift. Walks every type definition (entry +
365/// dep modules), classifies the records that pair a single carrier
366/// field with a validating smart constructor, and emits
367/// `RefinedTypeDecl` entries into `ir.refined_types`. Backends
368/// (Lean → Subtype, Dafny → subset type) render these directly.
369pub fn populate_refined_types(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
370    // Walk entry items first, then dep modules. The map is keyed by
371    // opaque `TypeId` resolved through the symbol table — same
372    // collision-safe shape as `fn_contracts: HashMap<FnId, _>`. The
373    // typechecker explicitly permits two modules to expose distinct
374    // types of the same bare name (`A.Shape` vs `B.Shape`; see
375    // `tests/typechecker_spec::cross_module_same_named_types_do_not_
376    // merge`); opaque IDs make their predicates impossible to merge
377    // by construction. Producer resolves `TypeKey -> TypeId` once
378    // here; consumers (`find_refined_type_scoped`) resolve through
379    // the same symbol table at lookup time.
380    //
381    // SymbolTable is always present (`ProofLowerInputs.symbol_table`
382    // is `&SymbolTable`, not `Option<&_>` — the pipeline builds it
383    // unconditionally). Synthetic-ctx callers (test helpers) thread
384    // their own through `from_ctx` / direct construction.
385    let symbols = inputs.symbol_table;
386
387    let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
388        TopLevel::TypeDef(td) => Some((None::<&str>, td)),
389        _ => None,
390    });
391    let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
392        m.type_defs
393            .iter()
394            .map(move |td| (Some(m.prefix.as_str()), td))
395    });
396
397    for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
398        let TypeDef::Product { name, fields, .. } = td else {
399            continue;
400        };
401        if fields.len() != 1 {
402            continue;
403        }
404        let type_key = match module_prefix {
405            Some(prefix) => crate::ir::TypeKey::in_module(prefix.to_string(), name),
406            None => crate::ir::TypeKey::entry(name),
407        };
408        let Some(canonical_key) = symbols.type_id_of(&type_key) else {
409            // Type isn't in the symbol table — built-ins (Result.Ok
410            // etc.) are excluded by construction; for user types
411            // this is a wiring bug surfaced via the symbol-table
412            // builder, so just skip.
413            continue;
414        };
415        if ir.refined_types.contains_key(&canonical_key) {
416            // Same TypeId already populated — possible if a module
417            // is walked twice through dep aliasing. Skip so we don't
418            // overwrite a verified-witness entry with a predicate-
419            // eval fallback witness.
420            continue;
421        }
422        // Scope the smart-constructor lookup to the same module the
423        // record lives in. Refinement-via-opaque keeps the record
424        // opaque (`exposes opaque [X]`); a smart constructor in any
425        // other module couldn't reach the carrier field anyway.
426        // Without the scope, two modules each declaring a `Natural`
427        // with different predicates would both pick up whichever
428        // smart constructor walked first.
429        let Some(info) =
430            crate::codegen::common::refinement_info_for_in_scope(name, inputs, module_prefix)
431        else {
432            continue;
433        };
434        let invariant = Predicate {
435            free_vars: vec![(
436                info.param_name.to_string(),
437                crate::ir::proof_ir::QuantifierType::Plain(info.carrier_type.to_string()),
438            )],
439            expr: inputs.resolve_expr(info.predicate, module_prefix),
440        };
441        let witness = pick_witness(
442            name,
443            canonical_key,
444            inputs,
445            info.predicate,
446            info.param_name,
447            module_prefix,
448        );
449        // Round-4 finding 1: a `None` witness means we couldn't
450        // exhibit any inhabitant satisfying the predicate. Inserting
451        // the slot anyway makes Dafny silently fall back to
452        // `witness 0` even when the predicate excludes 0 — producing
453        // an unsound subset type. Skip the lift entirely: the
454        // backend will emit a plain `datatype` instead, which is
455        // honest about the missing invariant. The pure-fn / law
456        // paths still typecheck against the plain record.
457        let Some(witness) = witness else {
458            continue;
459        };
460        ir.refined_types.insert(
461            canonical_key,
462            RefinedTypeDecl {
463                name: name.clone(),
464                carrier_type: info.carrier_type.to_string(),
465                carrier_field: info.carrier_field.to_string(),
466                predicate_param: info.param_name.to_string(),
467                invariant,
468                witness: Some(witness),
469            },
470        );
471    }
472}
473
474/// Walk `analyze_plans(inputs)` and populate `ProofIR.fn_contracts`.
475///
476/// Translation pass over the classifier output (`RecursionPlan`) —
477/// no re-implementation. The diff test (`tests/proof_ir_diff.rs`)
478/// pins what each `RecursionPlan` variant lowers to so divergence
479/// between the classifier and the IR populator surfaces there.
480/// Coverage today: `IntCountdownGuarded`, `LinearRecurrence2`,
481/// `Sized*` (length / sizeOf / string-pos / int-ascending). Fuel-
482/// only and Mutual* plans don't materialise as `FnContract` (their
483/// recursion shape doesn't need IR-level pre-decisions; backends
484/// emit fuel scaffolding inline).
485pub fn populate_fn_contracts(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
486    // Round-5 finding: walk per-scope so two modules each with a
487    // recursive `foo` (or entry + module both declaring `foo`)
488    // don't collide on the bare-name `plans: HashMap<String, _>`.
489    // Aver's module DAG invariant rules out cross-module recursion
490    // SCCs, so per-scope classification is the canonical view and
491    // each `Module.fn` gets its own slot in `ir.fn_contracts`.
492    for scope in inputs.scopes() {
493        let (plans, issues) =
494            crate::codegen::recursion::analyze_plans_in_scope(inputs, scope.as_deref(), false);
495        ir.unclassified_fns
496            .extend(issues.into_iter().map(|issue| crate::ir::UnclassifiedFn {
497                line: issue.line,
498                message: issue.message,
499            }));
500        populate_fn_contracts_for_scope(inputs, ir, scope.as_deref(), &plans);
501    }
502}
503
504fn populate_fn_contracts_for_scope(
505    inputs: &ProofLowerInputs,
506    ir: &mut ProofIR,
507    scope: Option<&str>,
508    plans: &HashMap<String, RecursionPlan>,
509) {
510    let scoped_fns: Vec<&FnDef> = inputs.pure_fns_in_scope(scope);
511    let qualify = |bare: &str| -> crate::ir::FnKey {
512        match scope {
513            Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), bare),
514            None => crate::ir::FnKey::entry(bare),
515        }
516    };
517    // Contracts key by opaque `FnId`; SymbolTable is always present
518    // (pipeline builds it unconditionally, `ProofLowerInputs.symbol_
519    // table: &SymbolTable`).
520    let symbols = inputs.symbol_table;
521
522    for (fn_name, plan) in plans {
523        let Some(fd) = scoped_fns.iter().find(|fd| fd.name == *fn_name) else {
524            continue;
525        };
526        let fn_key = qualify(fn_name);
527        let Some(canonical_key) = symbols.fn_id_of(&fn_key) else {
528            continue;
529        };
530
531        // IntCountdown — fuel-encoded countdown on a single Int param.
532        // Distinct from IntCountdownGuarded: external callers may pass
533        // negatives (the classifier rejected closed-world status), so
534        // backends emit a fuel helper with `n.natAbs + 1` initial fuel
535        // rather than a native def with a precondition.
536        if let RecursionPlan::IntCountdown { param_index } = plan {
537            if let Some((param_name, _)) = fd.params.get(*param_index) {
538                ir.fn_contracts.insert(
539                    canonical_key,
540                    FnContract {
541                        source_name: fn_name.clone(),
542                        recursion: Some(RecursionContract::Fuel {
543                            fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne {
544                                param: param_name.clone(),
545                            },
546                        }),
547                    },
548                );
549            }
550            continue;
551        }
552
553        // IntAscending — fuel formula `(bound - n).natAbs + 1`. The
554        // bound stays as `Spanned<Expr>` so backends render it through
555        // their own emitters (it can be a literal, a fn param, or a
556        // small arith expression).
557        if let RecursionPlan::IntAscending { param_index, bound } = plan {
558            if let Some((param_name, _)) = fd.params.get(*param_index) {
559                ir.fn_contracts.insert(
560                    canonical_key,
561                    FnContract {
562                        source_name: fn_name.clone(),
563                        recursion: Some(RecursionContract::Fuel {
564                            fuel_metric: crate::ir::FuelMetric::BoundMinusParamNatAbsPlusOne {
565                                param: param_name.clone(),
566                                bound: inputs.resolve_expr(bound, scope),
567                            },
568                        }),
569                    },
570                );
571            }
572            continue;
573        }
574
575        // ListStructural — structural recursion on a List<_> param.
576        // Lean/Dafny don't actually use a fuel helper for this on
577        // recent backends (structural recursion is natively
578        // terminating); the metric stays as `SeqLenPlusOne` for
579        // backend-symmetric framing, and the consumer ignores it
580        // when emitting plain structural recursion.
581        if let RecursionPlan::ListStructural { param_index } = plan {
582            if let Some((param_name, _)) = fd.params.get(*param_index) {
583                ir.fn_contracts.insert(
584                    canonical_key,
585                    FnContract {
586                        source_name: fn_name.clone(),
587                        recursion: Some(RecursionContract::Fuel {
588                            fuel_metric: crate::ir::FuelMetric::SeqLenPlusOne {
589                                param: param_name.clone(),
590                            },
591                        }),
592                    },
593                );
594            }
595            continue;
596        }
597
598        // SizeOfStructural — recursion on a user ADT (e.g. an AST
599        // type). Fuel metric `sizeOf(call_frame) + 1`. The classifier
600        // doesn't pin a single bound param — `sizeOf` measures the
601        // whole frame — so the IR variant carries no param name.
602        if matches!(plan, RecursionPlan::SizeOfStructural) {
603            ir.fn_contracts.insert(
604                canonical_key,
605                FnContract {
606                    source_name: fn_name.clone(),
607                    recursion: Some(RecursionContract::Fuel {
608                        fuel_metric: crate::ir::FuelMetric::SizeOfPlusOne,
609                    }),
610                },
611            );
612            continue;
613        }
614
615        // StringPosAdvance — `(s, pos)`-shape recursion: `s` invariant
616        // (first param, String), `pos` advances (second param, Int).
617        // Fuel formula `s.length - pos`.
618        if matches!(plan, RecursionPlan::StringPosAdvance) {
619            if let (Some((string_param, _)), Some((pos_param, _))) =
620                (fd.params.first(), fd.params.get(1))
621            {
622                ir.fn_contracts.insert(
623                    canonical_key,
624                    FnContract {
625                        source_name: fn_name.clone(),
626                        recursion: Some(RecursionContract::Fuel {
627                            fuel_metric: crate::ir::FuelMetric::StringLenMinusPos {
628                                string_param: string_param.clone(),
629                                pos_param: pos_param.clone(),
630                            },
631                        }),
632                    },
633                );
634            }
635            continue;
636        }
637
638        // Mutual-recursion SCCs — each member of the SCC gets its own
639        // plan with the same family. All three lower to a Lex fuel
640        // metric; the params vector + rank distinguish per-shape /
641        // per-member roles.
642        //
643        // - MutualIntCountdown: every member counts down its first
644        //   Int param; rank stays 0 (no inter-member ranking — every
645        //   edge decreases the shared dimension).
646        // - MutualStringPosAdvance { rank }: (s, pos) shape across
647        //   the SCC; rank distinguishes members for same-measure
648        //   inter-fn edges.
649        // - MutualSizeOfRanked { rank }: sizeOf measures the whole
650        //   call frame; rank distinguishes members. No bound param —
651        //   the empty params vec signals "frame-level measure".
652        match plan {
653            RecursionPlan::MutualIntCountdown => {
654                let params = fd
655                    .params
656                    .first()
657                    .map(|(n, _)| vec![n.clone()])
658                    .unwrap_or_default();
659                ir.fn_contracts.insert(
660                    canonical_key,
661                    FnContract {
662                        source_name: fn_name.clone(),
663                        recursion: Some(RecursionContract::Fuel {
664                            fuel_metric: crate::ir::FuelMetric::Lex { params, rank: 0 },
665                        }),
666                    },
667                );
668                continue;
669            }
670            RecursionPlan::MutualStringPosAdvance { rank } => {
671                let params = fd.params.iter().take(2).map(|(n, _)| n.clone()).collect();
672                ir.fn_contracts.insert(
673                    canonical_key,
674                    FnContract {
675                        source_name: fn_name.clone(),
676                        recursion: Some(RecursionContract::Fuel {
677                            fuel_metric: crate::ir::FuelMetric::Lex {
678                                params,
679                                rank: *rank,
680                            },
681                        }),
682                    },
683                );
684                continue;
685            }
686            RecursionPlan::MutualSizeOfRanked { rank } => {
687                ir.fn_contracts.insert(
688                    canonical_key,
689                    FnContract {
690                        source_name: fn_name.clone(),
691                        recursion: Some(RecursionContract::Fuel {
692                            fuel_metric: crate::ir::FuelMetric::Lex {
693                                params: Vec::new(),
694                                rank: *rank,
695                            },
696                        }),
697                    },
698                );
699                continue;
700            }
701            RecursionPlan::LinearRecurrence2 => {
702                ir.fn_contracts.insert(
703                    canonical_key,
704                    FnContract {
705                        source_name: fn_name.clone(),
706                        recursion: Some(RecursionContract::LinearRecurrence2),
707                    },
708                );
709                continue;
710            }
711            _ => {}
712        }
713
714        let RecursionPlan::IntCountdownGuarded {
715            param_index,
716            base_arm_literal,
717            base_arm_body,
718            wildcard_arm_body,
719            precondition,
720        } = plan
721        else {
722            continue;
723        };
724        let Some((countdown_param_name, _)) = fd.params.get(*param_index) else {
725            continue;
726        };
727
728        let precondition_predicates: Vec<Predicate> = precondition
729            .iter()
730            .map(|clause| Predicate {
731                free_vars: vec![(
732                    countdown_param_name.clone(),
733                    QuantifierType::Plain("Int".to_string()),
734                )],
735                expr: inputs.resolve_expr(clause, scope),
736            })
737            .collect();
738
739        ir.fn_contracts.insert(
740            canonical_key,
741            FnContract {
742                source_name: fn_name.clone(),
743                recursion: Some(RecursionContract::Native {
744                    precondition: precondition_predicates,
745                    measure: Measure::NatAbsInt {
746                        param: countdown_param_name.clone(),
747                    },
748                    preservation: PreservationProof::IntCountdownLiteralZero,
749                    decrease: DecreaseProof::NatAbsCountdown,
750                    body: NativeIntCountdownBody {
751                        base_arm_literal: *base_arm_literal,
752                        base_arm_body: inputs.resolve_expr(base_arm_body, scope),
753                        wildcard_arm_body: inputs.resolve_expr(wildcard_arm_body, scope),
754                    },
755                }),
756            },
757        );
758    }
759}
760
761/// Walk every verify block, lift `VerifyKind::Law` entries into
762/// `ProofIR.law_theorems`.
763///
764/// Extracts the law's shape (quantifiers from `givens`, premises
765/// from `when`, claim from `lhs == rhs`) and pins a `ProofStrategy`
766/// via [`classify_law_strategy`]. Covered strategies: Reflexive,
767/// Commutative / Associative / IdentityElement / AntiCommutative /
768/// UnaryEqualsBinary (arithmetic wrappers), Induction (recursive
769/// ADTs), LibraryAxiom (Map set/get), MapUpdatePostcondition,
770/// MapKeyTrackedIncrement, SpecEquivalence{,SimpNormalized},
771/// LinearIntSpecEquivalence, EffectfulSpecEquivalence (with Oracle
772/// Lift), LinearArithmetic (catch-all over an unfold chain).
773/// Unmatched shapes pin `BackendDispatch` and fall through to the
774/// backend's residual chain (linear_recurrence2 emit + sampled /
775/// guarded-domain fallback).
776pub fn populate_law_theorems(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
777    use crate::ast::{TopLevel, VerifyKind};
778    use crate::ir::{LawTheorem, Predicate, Quantifier, QuantifierType};
779
780    let symbols = inputs.symbol_table;
781
782    let entry_verifies = inputs.entry_items.iter().filter_map(|item| match item {
783        TopLevel::Verify(vb) => Some(vb),
784        _ => None,
785    });
786    // Dep modules don't expose verify blocks today (ModuleInfo carries
787    // type_defs + fn_defs only), so the walk stays entry-side. When
788    // ModuleInfo gains a `verify_blocks` field, extend here.
789    for vb in entry_verifies {
790        let VerifyKind::Law(law) = &vb.kind else {
791            continue;
792        };
793
794        let quantifiers: Vec<Quantifier> = law
795            .givens
796            .iter()
797            .map(|g| Quantifier {
798                name: g.name.clone(),
799                binder_type: QuantifierType::Plain(g.type_name.clone()),
800            })
801            .collect();
802
803        // Scope for resolving the law's expressions: derived from the
804        // target fn's owning module, NOT hardcoded to entry. Today
805        // laws-in-modules isn't shipped, so the lookup falls back to
806        // entry for every fn; once dep modules carry their own verify
807        // blocks (open follow-up), the same resolution path serves
808        // both. Avoids re-introducing the "scope=None means entry"
809        // assumption the rest of phase E worked to eliminate.
810        let law_scope: Option<String> = symbols
811            .fn_id_of(&crate::ir::FnKey::entry(&vb.fn_name))
812            .or_else(|| {
813                inputs.dep_modules.iter().find_map(|m| {
814                    symbols.fn_id_of(&crate::ir::FnKey::in_module(m.prefix.clone(), &vb.fn_name))
815                })
816            })
817            .and_then(|id| symbols.fn_entry(id).key.scope_str().map(|s| s.to_string()));
818        let law_scope_ref = law_scope.as_deref();
819
820        let premises: Vec<Predicate> = match &law.when {
821            Some(when_expr) => vec![Predicate {
822                free_vars: quantifiers
823                    .iter()
824                    .map(|q| (q.name.clone(), q.binder_type.clone()))
825                    .collect(),
826                expr: inputs.resolve_expr(when_expr, law_scope_ref),
827            }],
828            None => Vec::new(),
829        };
830
831        let strategy =
832            classify_law_strategy(law, &vb.fn_name, inputs, &ir.refined_types, law_scope_ref);
833
834        // Verify laws are entry-only per current model — see
835        // `LawTheorem.fn_id` doc. The bare `vb.fn_name` resolves
836        // through the symbol table to an entry-scope `FnId`; when
837        // the fn isn't in the symbol table (verify block targeting
838        // a fn that doesn't exist), skip the law silently — the
839        // typechecker / verify-driver surfaces the missing target
840        // elsewhere.
841        let Some(fn_id) = symbols.fn_id_of(&crate::ir::FnKey::entry(&vb.fn_name)) else {
842            continue;
843        };
844        ir.law_theorems.push(LawTheorem {
845            fn_id,
846            law_name: law.name.clone(),
847            quantifiers,
848            premises,
849            claim_lhs: inputs.resolve_expr(&law.lhs, law_scope_ref),
850            claim_rhs: inputs.resolve_expr(&law.rhs, law_scope_ref),
851            strategy,
852        });
853    }
854}
855
856/// Pick the strategy `LawLower` should pin on a `(fn, law)` pair.
857///
858/// Decision order — specific algebraic properties first, then
859/// generic linear-arithmetic catch-all, then `BackendDispatch`:
860/// 1. `Reflexive` — `law.lhs ≡ law.rhs` syntactically.
861/// 2. `Commutative { op }` — fn body is `a <op> b`, claim is
862///    `f(a, b) = f(b, a)` (op restricted to commutative ones).
863/// 3. `Associative { op }` — same body, 3 givens, assoc claim.
864/// 4. `IdentityElement { op }` — `f(a, e) = a` (or `f(e, a) = a`),
865///    where `e` is the op's identity. Covers Add/Mul both-sided
866///    plus Sub right-sided.
867/// 5. `AntiCommutative { op: Sub, neg_on_rhs }` — `f(a, b) =
868///    -f(b, a)` form. Sub-only (Mul has no anti-commutative law).
869/// 6. `UnaryEqualsBinary { inner_fn }` — outer fn is unary, claim
870///    binds it to the inner binary fn at a constant.
871/// 7. `LinearArithmetic { unfold_fns, ... }` — catch-all when the
872///    law reduces to linear arith after unfolding the call chain.
873/// 8. `BackendDispatch` — backend's ad-hoc chain decides.
874fn classify_law_strategy(
875    law: &crate::ast::VerifyLaw,
876    fn_name: &str,
877    inputs: &ProofLowerInputs,
878    refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
879    scope: Option<&str>,
880) -> crate::ir::ProofStrategy {
881    use crate::ir::ProofStrategy;
882
883    // Match-dispatcher fold equivalence (stage 8c of #232) — two
884    // self-recursive `MatchDispatcherFold` fns over the same list
885    // param. Closes by structural induction on `xs` + `omega` on
886    // each arm.
887    if law.when.is_none()
888        && let Some(s) = detect_match_dispatcher_fold_equivalence(law, fn_name, inputs)
889    {
890        return s;
891    }
892    // Result-pipeline chain equivalence (stage 8b of #232) — `?`
893    // propagation `chain_qm(x)` vs nested-match `chain_manual(x)`.
894    // Both sides unfold to the same nested match; the proof closes
895    // by `unfold + repeat split`.
896    if law.when.is_none()
897        && let Some(s) = detect_result_pipeline_chain_equivalence(law, fn_name, inputs)
898    {
899        return s;
900    }
901    // Wrapper-over-recursion with monoidal accumulator (stage 8 of
902    // #232) — runs before generic induction because its aux-lemma
903    // template closes laws naive induction can't (e.g. `sum(xs) ==
904    // sumDirect(xs)` where `sum(xs) = sumTR(xs, 0)`). Detected
905    // when `fn_name` is registered as a `WrapperOverRecursion`
906    // pattern in `ProgramShape` AND the law shape is
907    // `wrapper(g) == other(g)` AND the inner fn body matches the
908    // monoidal-accumulator template.
909    if law.when.is_none()
910        && let Some(s) = detect_wrapper_over_recursion(law, fn_name, inputs)
911    {
912        return s;
913    }
914    // Structural induction runs first — when any given binds a
915    // recursive ADT, induction over its variants is the canonical
916    // proof. Reflexive could also fire on `f(t) = f(t)` for `t: Tree`
917    // but induction subsumes (one trivial case per variant) and is
918    // the legacy chain's first pick. `when` clauses block induction
919    // — the case-split would lose the premise binding.
920    if law.when.is_none()
921        && let Some(param) = detect_induction_target(law, inputs)
922    {
923        return ProofStrategy::Induction { param };
924    }
925    if law.lhs == law.rhs {
926        return ProofStrategy::Reflexive;
927    }
928    // Binary-wrapper-shaped laws first. `wrapper_binop` returns
929    // `None` for non-binary fns — unary wrappers are tried after
930    // this block falls through.
931    if let Some(op) = wrapper_binop(fn_name, inputs) {
932        if detect_wrapper_commutative(law, fn_name, op) {
933            return ProofStrategy::Commutative { op };
934        }
935        if detect_wrapper_associative(law, fn_name, op) {
936            return ProofStrategy::Associative { op };
937        }
938        if detect_wrapper_identity(law, fn_name, op) {
939            return ProofStrategy::IdentityElement { op };
940        }
941        // Sub right-identity collapses into IdentityElement —
942        // same emit (`simp [fn]`), different lhs/rhs shape. The
943        // detector validates the right-side `f(a, 0) = a` form
944        // (`f(0, a) = -a` doesn't equal `a`, so Sub is one-sided).
945        if matches!(op, crate::ast::BinOp::Sub) && detect_wrapper_sub_right_identity(law, fn_name) {
946            return ProofStrategy::IdentityElement { op };
947        }
948        // Anti-commutative is Sub-specific (Add/Mul are
949        // commutative, no anti-commutativity). The op tag keeps
950        // it parameterised even though only Sub currently fires.
951        if matches!(op, crate::ast::BinOp::Sub)
952            && let Some(neg_on_rhs) = detect_wrapper_sub_anti_commutative(law, fn_name)
953        {
954            return ProofStrategy::AntiCommutative { op, neg_on_rhs };
955        }
956    }
957    // Unary fn equal to binary fn at a constant — `fn_name` is the
958    // unary outer; the binary fn name is captured for backends.
959    if let Some(inner_fn) = detect_wrapper_unary_equivalence(law, fn_name, inputs) {
960        return ProofStrategy::UnaryEqualsBinary { inner_fn };
961    }
962    // Library axiom instances — Map.has-after-set, Map.get-after-set.
963    // Specific shape, single-line `simpa using axiom` emit on Lean.
964    if let Some((axiom, args)) = detect_map_set_axiom(law) {
965        let resolved_args: Vec<_> = args.iter().map(|a| inputs.resolve_expr(a, scope)).collect();
966        return ProofStrategy::LibraryAxiom {
967            axiom,
968            args: resolved_args,
969        };
970    }
971    // Tracked-counter increment: specialised body template + `+ 1`
972    // rhs. Checked before the more general MapUpdatePostcondition so
973    // the tighter strategy wins for this shape.
974    if let Some(inc) = detect_map_key_tracked_increment(law, fn_name, inputs) {
975        return ProofStrategy::MapKeyTrackedIncrement {
976            outer_fn: inc.outer_fn,
977            map_arg: inputs.resolve_expr(&inc.map_arg, scope),
978            key_arg: inputs.resolve_expr(&inc.key_arg, scope),
979        };
980    }
981    // Post-condition of an inline-defined map-update fn — case-split
982    // over `Map.get m k` and apply the `Map.set` axioms.
983    if let Some(post) = detect_map_update_postcondition(law, fn_name, inputs) {
984        return ProofStrategy::MapUpdatePostcondition {
985            outer_fn: post.outer_fn,
986            kind: post.kind,
987            map_arg: inputs.resolve_expr(&post.map_arg, scope),
988            key_arg: inputs.resolve_expr(&post.key_arg, scope),
989            extra_unfolds: post.extra_unfolds,
990        };
991    }
992    // Functional equivalence of `vb.fn_name` and a same-named spec
993    // fn whose body is syntactically identical to the impl's.
994    if let Some(extra_unfolds) = detect_spec_equivalence(law, fn_name, inputs) {
995        return ProofStrategy::SpecEquivalence { extra_unfolds };
996    }
997    // Broader spec equivalence — bodies differ syntactically but
998    // normalize to same under substitution + arithmetic identity
999    // folding. Runs after the strict `SpecEquivalence` so the
1000    // tighter detector wins when both would match.
1001    if let Some(extra_unfolds) = detect_simp_normalized_spec_equivalence(law, fn_name, inputs) {
1002        return ProofStrategy::SpecEquivalenceSimpNormalized { extra_unfolds };
1003    }
1004    // Linear-Int spec equivalence — substituted bodies are pure
1005    // linear arithmetic over Int givens; decided by `omega` / LIA.
1006    if let Some((unfolded_impl, unfolded_spec)) =
1007        detect_linear_int_spec_equivalence(law, fn_name, inputs)
1008    {
1009        return ProofStrategy::LinearIntSpecEquivalence {
1010            unfolded_impl: inputs.resolve_expr(&unfolded_impl, scope),
1011            unfolded_spec: inputs.resolve_expr(&unfolded_spec, scope),
1012        };
1013    }
1014    // Effectful counterpart — Oracle Lift normalises both sides
1015    // (oracle args injected into impl call) and the lowerer matches
1016    // the canonical `impl(args) == spec(args)` shape on the
1017    // rewritten form. Fires on real oracle-spec laws like
1018    // `pickPair() => pairSpec(BranchPath.Root, rnd)`.
1019    if let Some(spec_fn) = detect_effectful_spec_equivalence(law, fn_name, inputs) {
1020        return ProofStrategy::EffectfulSpecEquivalence {
1021            impl_fn: fn_name.to_string(),
1022            spec_fn,
1023        };
1024    }
1025    // Second-order linear recurrence (fib / fibSpec shape). Detector
1026    // validates impl as tail-rec wrapper, spec as direct second-order
1027    // recurrence, helper as their shared affine worker — all three
1028    // shapes pinned in `lean::recurrence`. Backends consume the
1029    // (impl_fn, spec_fn, helper_fn) names from IR; the proof template
1030    // differs per target (Lean Nat-helper + induction; Dafny still
1031    // pending — issue #116).
1032    if let Some((spec_fn, helper_fn)) =
1033        detect_linear_recurrence2_spec_equivalence(law, fn_name, inputs)
1034    {
1035        return ProofStrategy::LinearRecurrence2SpecEquivalence {
1036            impl_fn: fn_name.to_string(),
1037            spec_fn,
1038            helper_fn,
1039        };
1040    }
1041    // Linear arithmetic over an unfold chain — generic catch-all.
1042    // Named for the semantic, not the backend tactic.
1043    if let Some(plan) = detect_simp_omega_unfold(law, fn_name, inputs, refined_types) {
1044        return ProofStrategy::LinearArithmetic {
1045            unfold_fns: plan.unfold_fns,
1046            wrapper_return: plan.wrapper_return,
1047            smart_guard: plan.smart_guard,
1048            lifted: plan.lifted,
1049        };
1050    }
1051    ProofStrategy::BackendDispatch
1052}
1053
1054/// Internal scratch for the simp+omega detector. Carries the
1055/// same fields as the IR variant but lives outside the IR enum so
1056/// callers can build it incrementally before pinning.
1057struct SimpOmegaPlan {
1058    unfold_fns: Vec<String>,
1059    wrapper_return: bool,
1060    smart_guard: Option<crate::ir::SmartGuard>,
1061    /// `true` when at least one law given is used as a refinement
1062    /// carrier in the law body (e.g. `given a: Int` used as
1063    /// `Natural(value = a)`). Subtype/subset lift carries the
1064    /// invariant in the type, so wrapper case-split is unnecessary.
1065    lifted: bool,
1066}
1067
1068fn detect_simp_omega_unfold(
1069    law: &crate::ast::VerifyLaw,
1070    fn_name: &str,
1071    inputs: &ProofLowerInputs,
1072    refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
1073) -> Option<SimpOmegaPlan> {
1074    use std::collections::BTreeSet;
1075
1076    let outer_fd = inputs.find_fn_def_by_call_name(fn_name)?;
1077    // All law givens Int.
1078    if law.givens.is_empty() || law.givens.iter().any(|g| g.type_name != "Int") {
1079        return None;
1080    }
1081    // Detect refinement lifts — when any given is used as a
1082    // `Refined(value = given)` carrier in the law body, the outer
1083    // fn may legitimately take the refined type (`fn add(a:
1084    // Natural, b: Natural)`) and unfold through the smart
1085    // constructor to Int arithmetic. Skip the outer-Int rejection
1086    // for lifted laws.
1087    let symbols = inputs.symbol_table;
1088    let lifted = law.givens.iter().any(|g| {
1089        refinement_lift_for_given_ir(
1090            &g.name,
1091            &law.lhs,
1092            &law.rhs,
1093            refined_types,
1094            symbols,
1095            inputs.dep_modules,
1096        )
1097        .is_some()
1098    });
1099    if !lifted && outer_fd.params.iter().any(|(_, t)| t != "Int") {
1100        return None;
1101    }
1102
1103    // Seed the unfold set from the law's two sides + the outer fn.
1104    let mut fn_names: BTreeSet<String> = BTreeSet::new();
1105    collect_fn_calls_expr(&law.lhs, &mut fn_names);
1106    collect_fn_calls_expr(&law.rhs, &mut fn_names);
1107    fn_names.insert(fn_name.to_string());
1108
1109    // Transitive expansion through entry+dep fn bodies. Each round
1110    // can only add fns reachable from the new set; converges in
1111    // O(items). Without this, cross-module refinement smart
1112    // constructors (`Modules.Natural.Natural.fromInt`) wouldn't be
1113    // in the unfold list and the goal would carry opaque
1114    // match-on-Result branches simp/omega can't close.
1115    loop {
1116        let before = fn_names.len();
1117        let snapshot: Vec<String> = fn_names.iter().cloned().collect();
1118        for fd in iter_all_fn_defs(inputs) {
1119            if !snapshot.contains(&fd.name) {
1120                continue;
1121            }
1122            for stmt in fd.body.stmts() {
1123                match stmt {
1124                    crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => {
1125                        collect_fn_calls_expr(e, &mut fn_names);
1126                    }
1127                }
1128            }
1129        }
1130        if fn_names.len() == before {
1131            break;
1132        }
1133    }
1134
1135    // Self-recursion rejection — `unfold fn` only does one step, so
1136    // a recursive body leaves a stale `fn` in the goal that simp
1137    // can't close. Check against the narrow self-only set; calling
1138    // a peer fn in the unfold list is fine.
1139    let mut wrapper_return = false;
1140    for fd in iter_all_fn_defs(inputs) {
1141        if !fn_names.contains(&fd.name) {
1142            continue;
1143        }
1144        let mut self_only: BTreeSet<String> = BTreeSet::new();
1145        self_only.insert(fd.name.clone());
1146        if body_calls_any_of_inputs(&fd.body, &self_only) {
1147            return None;
1148        }
1149        // Int-only check for the outer law fn — but skip when the
1150        // law is refinement-lifted (outer fn takes the refined
1151        // type, body unfolds through the smart constructor).
1152        if fd.name == fn_name && !lifted && fd.params.iter().any(|(_, t)| t != "Int") {
1153            return None;
1154        }
1155        let ret = fd.return_type.as_str();
1156        if ret != "Int" && ret != "Float" {
1157            wrapper_return = true;
1158        }
1159    }
1160
1161    // Top-level law fn first in the unfold list — Lean needs to see
1162    // it in the goal before transitively-reached callees, otherwise
1163    // `unfold` fails outright at the outermost call layer.
1164    let mut ordered: Vec<String> = Vec::new();
1165    if fn_names.contains(fn_name) {
1166        ordered.push(fn_name.to_string());
1167    }
1168    for n in &fn_names {
1169        if n != fn_name {
1170            ordered.push(n.clone());
1171        }
1172    }
1173
1174    let smart_guard = extract_smart_constructor_guard(&fn_names, inputs);
1175
1176    Some(SimpOmegaPlan {
1177        unfold_fns: ordered,
1178        wrapper_return,
1179        smart_guard,
1180        lifted,
1181    })
1182}
1183
1184/// Backend-neutral analogue of `codegen::common::refinement_lift_
1185/// for_given`. Walks `lhs` / `rhs` looking for a `RecordCreate {
1186/// type_name, fields: [(_, Ident(given))] }` shape where `type_
1187/// name` is a refined type whose carrier matches the given's
1188/// declared type. Returns the refined type name on first match.
1189///
1190/// The legacy version (common.rs) takes `&CodegenContext` and
1191/// borrows the type name from `ctx.items`. The lowerer reads
1192/// `refined_types` directly off the in-progress `ProofIR`
1193/// (populated by `populate_refined_types`, which runs before
1194/// `populate_law_theorems` in `lower(...)`).
1195fn refinement_lift_for_given_ir(
1196    given_name: &str,
1197    lhs: &Spanned<crate::ast::Expr>,
1198    rhs: &Spanned<crate::ast::Expr>,
1199    refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
1200    symbols: &crate::ir::SymbolTable,
1201    dep_modules: &[crate::codegen::ModuleInfo],
1202) -> Option<String> {
1203    let mut result: Option<String> = None;
1204    walk_for_refinement_carrier(
1205        lhs,
1206        given_name,
1207        refined_types,
1208        symbols,
1209        dep_modules,
1210        &mut result,
1211    );
1212    walk_for_refinement_carrier(
1213        rhs,
1214        given_name,
1215        refined_types,
1216        symbols,
1217        dep_modules,
1218        &mut result,
1219    );
1220    result
1221}
1222
1223/// **syntax-discovery-only** (epic #170 Phase 7). Walks raw AST
1224/// looking for a `RecordCreate(type_name, [(field, Ident(given))])`
1225/// pattern that lifts a `given` through a refinement type's smart
1226/// constructor. The recursion descends into nested record-creates,
1227/// fn calls, and binops so a deeply-wrapped lift still gets found.
1228/// Identity is handed off to `resolve_refined_type_in_with_key`,
1229/// which canonicalises through `SymbolTable` before keying the
1230/// `refined_types` map — no bare-name keying past discovery.
1231fn walk_for_refinement_carrier(
1232    expr: &Spanned<crate::ast::Expr>,
1233    given_name: &str,
1234    refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
1235    symbols: &crate::ir::SymbolTable,
1236    dep_modules: &[crate::codegen::ModuleInfo],
1237    result: &mut Option<String>,
1238) {
1239    use crate::ast::Expr;
1240    if result.is_some() {
1241        return;
1242    }
1243    match &expr.node {
1244        Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
1245            let (_, fvalue) = &fields[0];
1246            let matches_var = matches!(
1247                &fvalue.node,
1248                Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == given_name
1249            );
1250            if matches_var
1251                && let Some((type_id, _decl)) =
1252                    crate::codegen::common::resolve_refined_type_in_with_key(
1253                        refined_types,
1254                        symbols,
1255                        dep_modules,
1256                        type_name,
1257                    )
1258            {
1259                // Stringify the canonical name via the symbol table's
1260                // type entry. The only consumer today reads `.is_some()`
1261                // (see `detect_simp_omega_unfold`), but recovering a
1262                // human-readable id keeps the diagnostic path honest.
1263                *result = Some(symbols.type_entry(type_id).key.canonical());
1264                return;
1265            }
1266            // Even non-matching RecordCreate may contain nested
1267            // refinement carriers (e.g. `Foo(value = Bar(value = a))`).
1268            for (_, v) in fields {
1269                walk_for_refinement_carrier(
1270                    v,
1271                    given_name,
1272                    refined_types,
1273                    symbols,
1274                    dep_modules,
1275                    result,
1276                );
1277            }
1278        }
1279        Expr::FnCall(callee, args) => {
1280            walk_for_refinement_carrier(
1281                callee,
1282                given_name,
1283                refined_types,
1284                symbols,
1285                dep_modules,
1286                result,
1287            );
1288            for a in args {
1289                walk_for_refinement_carrier(
1290                    a,
1291                    given_name,
1292                    refined_types,
1293                    symbols,
1294                    dep_modules,
1295                    result,
1296                );
1297            }
1298        }
1299        Expr::BinOp(_, l, r) => {
1300            walk_for_refinement_carrier(l, given_name, refined_types, symbols, dep_modules, result);
1301            walk_for_refinement_carrier(r, given_name, refined_types, symbols, dep_modules, result);
1302        }
1303        Expr::Match { subject, arms, .. } => {
1304            walk_for_refinement_carrier(
1305                subject,
1306                given_name,
1307                refined_types,
1308                symbols,
1309                dep_modules,
1310                result,
1311            );
1312            for arm in arms {
1313                walk_for_refinement_carrier(
1314                    &arm.body,
1315                    given_name,
1316                    refined_types,
1317                    symbols,
1318                    dep_modules,
1319                    result,
1320                );
1321            }
1322        }
1323        Expr::Attr(obj, _) => {
1324            walk_for_refinement_carrier(
1325                obj,
1326                given_name,
1327                refined_types,
1328                symbols,
1329                dep_modules,
1330                result,
1331            );
1332        }
1333        _ => {}
1334    }
1335}
1336
1337fn iter_all_fn_defs<'a>(inputs: &'a ProofLowerInputs<'a>) -> impl Iterator<Item = &'a FnDef> {
1338    inputs
1339        .entry_items
1340        .iter()
1341        .filter_map(|item| match item {
1342            TopLevel::FnDef(fd) => Some(fd),
1343            _ => None,
1344        })
1345        .chain(inputs.dep_modules.iter().flat_map(|m| m.fn_defs.iter()))
1346}
1347
1348fn body_calls_any_of_inputs(
1349    body: &crate::ast::FnBody,
1350    names: &std::collections::BTreeSet<String>,
1351) -> bool {
1352    let mut called = std::collections::BTreeSet::new();
1353    for stmt in body.stmts() {
1354        match stmt {
1355            crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => {
1356                collect_fn_calls_expr(e, &mut called);
1357            }
1358        }
1359    }
1360    called.iter().any(|c| names.contains(c))
1361}
1362
1363fn collect_fn_calls_expr(
1364    expr: &Spanned<crate::ast::Expr>,
1365    out: &mut std::collections::BTreeSet<String>,
1366) {
1367    use crate::ast::Expr;
1368    match &expr.node {
1369        Expr::FnCall(f, args) => {
1370            if let Some(name) = expr_to_dotted_name(&f.node) {
1371                // Skip uppercase namespace handles (`List.len`,
1372                // `Option.Some`) — those are built-in namespaces,
1373                // not user fns the auto-proof can unfold. The leaf
1374                // segment's case discriminates user fns from
1375                // namespace types (cross-module user calls survive
1376                // because the leaf fn name starts lower-case).
1377                let last = name.rsplit('.').next().unwrap_or(&name);
1378                if last.chars().next().is_some_and(|c| c.is_lowercase()) {
1379                    out.insert(name);
1380                }
1381            }
1382            for arg in args {
1383                collect_fn_calls_expr(arg, out);
1384            }
1385        }
1386        Expr::BinOp(_, l, r) => {
1387            collect_fn_calls_expr(l, out);
1388            collect_fn_calls_expr(r, out);
1389        }
1390        Expr::Attr(obj, _) => collect_fn_calls_expr(obj, out),
1391        Expr::Match { subject, arms, .. } => {
1392            collect_fn_calls_expr(subject, out);
1393            for arm in arms {
1394                collect_fn_calls_expr(&arm.body, out);
1395            }
1396        }
1397        Expr::TailCall(boxed) => {
1398            out.insert(boxed.target.clone());
1399            for arg in &boxed.args {
1400                collect_fn_calls_expr(arg, out);
1401            }
1402        }
1403        _ => {}
1404    }
1405}
1406
1407/// Find a single-param smart constructor in the unfold set whose
1408/// body is the canonical `match <bool-subj> { true → Ok; false →
1409/// Err }` shape. Returns the param name + bool subject of the
1410/// first match.
1411fn extract_smart_constructor_guard(
1412    fn_names: &std::collections::BTreeSet<String>,
1413    inputs: &ProofLowerInputs,
1414) -> Option<crate::ir::SmartGuard> {
1415    use crate::ast::{Expr, MatchArm, Pattern, Stmt};
1416    for fd in iter_all_fn_defs(inputs) {
1417        if !fn_names.contains(&fd.name) {
1418            continue;
1419        }
1420        if !fd.return_type.starts_with("Result<") {
1421            continue;
1422        }
1423        if fd.params.len() != 1 {
1424            continue;
1425        }
1426        let (param_name, param_type) = &fd.params[0];
1427        if param_type != "Int" {
1428            continue;
1429        }
1430        let stmts = fd.body.stmts();
1431        if stmts.len() != 1 {
1432            continue;
1433        }
1434        let Stmt::Expr(body_expr) = &stmts[0] else {
1435            continue;
1436        };
1437        let Expr::Match { subject, arms } = &body_expr.node else {
1438            continue;
1439        };
1440        if !arms_match_bool_ok_err(arms) {
1441            continue;
1442        }
1443        let scope = inputs.fn_owning_scope(fd);
1444        return Some(crate::ir::SmartGuard {
1445            param: param_name.clone(),
1446            predicate: inputs.resolve_expr(subject, scope),
1447        });
1448        // Reference the type to satisfy the MatchArm import.
1449        #[allow(unreachable_code)]
1450        {
1451            let _: Option<&MatchArm> = None;
1452            let _: Option<&Pattern> = None;
1453        }
1454    }
1455    None
1456}
1457
1458fn arms_match_bool_ok_err(arms: &[crate::ast::MatchArm]) -> bool {
1459    use crate::ast::{Expr, Literal, Pattern};
1460    if arms.len() != 2 {
1461        return false;
1462    }
1463    let starts_with_ctor = |expr: &Spanned<Expr>, name: &str| -> bool {
1464        match &expr.node {
1465            Expr::Constructor(n, _) => n == name,
1466            Expr::FnCall(callee, _) => {
1467                if let Expr::Attr(obj, field) = &callee.node
1468                    && let Expr::Ident(ns) = &obj.node
1469                {
1470                    format!("{ns}.{field}") == name
1471                } else {
1472                    false
1473                }
1474            }
1475            _ => false,
1476        }
1477    };
1478    let mut saw_true_ok = false;
1479    let mut saw_false_err = false;
1480    for arm in arms {
1481        match &arm.pattern {
1482            Pattern::Literal(Literal::Bool(true)) => {
1483                if starts_with_ctor(&arm.body, "Result.Ok") {
1484                    saw_true_ok = true;
1485                }
1486            }
1487            Pattern::Literal(Literal::Bool(false)) => {
1488                if starts_with_ctor(&arm.body, "Result.Err") {
1489                    saw_false_err = true;
1490                }
1491            }
1492            _ => return false,
1493        }
1494    }
1495    saw_true_ok && saw_false_err
1496}
1497
1498/// Detect a Map library axiom instance:
1499///   `Map.has(Map.set(m, k, v), k) => true`        → `Map.has_set_self`
1500///   `Map.get(Map.set(m, k, v), k) => Option.Some(v)` → `Map.get_set_self`
1501/// Returns `(axiom_name, [m, k, v])` on match, either side
1502/// orientation. Both axioms use the same `[m, k, v]` arg order.
1503fn detect_map_set_axiom(
1504    law: &crate::ast::VerifyLaw,
1505) -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
1506    // `Map.has(Map.set(m, k, v), k) => true`
1507    let has_side = |side: &Spanned<crate::ast::Expr>,
1508                    other: &Spanned<crate::ast::Expr>|
1509     -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
1510        let (m, k, v) = map_has_set_parts(side)?;
1511        if !is_bool_true(other) {
1512            return None;
1513        }
1514        Some((
1515            "Map.has_set_self".to_string(),
1516            vec![m.clone(), k.clone(), v.clone()],
1517        ))
1518    };
1519    if let Some(found) = has_side(&law.lhs, &law.rhs).or_else(|| has_side(&law.rhs, &law.lhs)) {
1520        return Some(found);
1521    }
1522
1523    // `Map.get(Map.set(m, k, v), k) => Option.Some(v)`
1524    let get_side = |side: &Spanned<crate::ast::Expr>,
1525                    other: &Spanned<crate::ast::Expr>|
1526     -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
1527        let (m, k, v) = map_get_set_parts(side)?;
1528        let some_v = option_some_arg(other)?;
1529        if some_v.node != v.node {
1530            return None;
1531        }
1532        Some((
1533            "Map.get_set_self".to_string(),
1534            vec![m.clone(), k.clone(), v.clone()],
1535        ))
1536    };
1537    get_side(&law.lhs, &law.rhs).or_else(|| get_side(&law.rhs, &law.lhs))
1538}
1539
1540/// Internal scratch carrier — mirrors the IR variant but lives in
1541/// the lowerer so callers can build incrementally.
1542struct MapUpdatePostconditionPlan {
1543    outer_fn: String,
1544    kind: crate::ir::MapUpdatePostconditionKind,
1545    map_arg: Spanned<crate::ast::Expr>,
1546    key_arg: Spanned<crate::ast::Expr>,
1547    extra_unfolds: Vec<String>,
1548}
1549
1550/// Detect a post-condition law on an inline map-update fn `outer(m,
1551/// k)`. Two shapes: `Map.has(outer(m, k), k) == true` (`HasAfter`),
1552/// or `Map.get(outer(m, k), k) == Option.Some(...)` (`GetAfter`).
1553/// Both require `outer`'s body to follow the "inspect get, set in
1554/// every arm" template (see `outer_fn_map_update_shape`).
1555fn detect_map_update_postcondition(
1556    law: &crate::ast::VerifyLaw,
1557    fn_name: &str,
1558    inputs: &ProofLowerInputs,
1559) -> Option<MapUpdatePostconditionPlan> {
1560    use crate::ir::MapUpdatePostconditionKind;
1561
1562    outer_fn_map_update_shape(fn_name, inputs)?;
1563
1564    let has_side = |side: &Spanned<crate::ast::Expr>,
1565                    other: &Spanned<crate::ast::Expr>|
1566     -> Option<MapUpdatePostconditionPlan> {
1567        if !is_bool_true(other) {
1568            return None;
1569        }
1570        let (map_arg, key_arg) = map_has_after_fn_call(side, fn_name)?;
1571        Some(MapUpdatePostconditionPlan {
1572            outer_fn: fn_name.to_string(),
1573            kind: MapUpdatePostconditionKind::HasAfter,
1574            map_arg: map_arg.clone(),
1575            key_arg: key_arg.clone(),
1576            extra_unfolds: Vec::new(),
1577        })
1578    };
1579    if let Some(plan) = has_side(&law.lhs, &law.rhs).or_else(|| has_side(&law.rhs, &law.lhs)) {
1580        return Some(plan);
1581    }
1582
1583    let get_side = |side: &Spanned<crate::ast::Expr>,
1584                    other: &Spanned<crate::ast::Expr>|
1585     -> Option<MapUpdatePostconditionPlan> {
1586        option_some_arg(other)?;
1587        let (map_arg, key_arg) = map_get_after_fn_call(side, fn_name)?;
1588        let extra_unfolds = law_helper_unfolds(law, fn_name, inputs);
1589        Some(MapUpdatePostconditionPlan {
1590            outer_fn: fn_name.to_string(),
1591            kind: MapUpdatePostconditionKind::GetAfter,
1592            map_arg: map_arg.clone(),
1593            key_arg: key_arg.clone(),
1594            extra_unfolds,
1595        })
1596    };
1597    get_side(&law.lhs, &law.rhs).or_else(|| get_side(&law.rhs, &law.lhs))
1598}
1599
1600/// Collect user helper-fn source names referenced from the law's
1601/// lhs/rhs/when, expanded transitively through pure (effect-free,
1602/// non-main) fn bodies. The outer fn is excluded — it's carried in
1603/// the IR variant separately. Filters out stdlib / namespace calls
1604/// (`Map.get`, `Option.withDefault`, …) by requiring each name to
1605/// resolve to a user fn def. Sorted for deterministic emit.
1606fn law_helper_unfolds(
1607    law: &crate::ast::VerifyLaw,
1608    outer_fn: &str,
1609    inputs: &ProofLowerInputs,
1610) -> Vec<String> {
1611    use std::collections::BTreeSet;
1612
1613    let resolve_user_fn = |name: &str| -> Option<&FnDef> {
1614        let fd = inputs.find_fn_def_by_call_name(name)?;
1615        if !fd.effects.is_empty() || fd.name == "main" {
1616            return None;
1617        }
1618        Some(fd)
1619    };
1620
1621    // Seed from law sides, immediately filtering to user-fn names.
1622    let mut raw: BTreeSet<String> = BTreeSet::new();
1623    collect_fn_calls_expr(&law.lhs, &mut raw);
1624    collect_fn_calls_expr(&law.rhs, &mut raw);
1625    if let Some(when_expr) = &law.when {
1626        collect_fn_calls_expr(when_expr, &mut raw);
1627    }
1628    let mut names: BTreeSet<String> = raw
1629        .into_iter()
1630        .filter_map(|n| resolve_user_fn(&n).map(|fd| fd.name.clone()))
1631        .collect();
1632
1633    // Transitive expansion through pure user fn bodies.
1634    loop {
1635        let before = names.len();
1636        let snapshot: Vec<String> = names.iter().cloned().collect();
1637        for name in snapshot {
1638            let Some(fd) = resolve_user_fn(&name) else {
1639                continue;
1640            };
1641            let mut called: BTreeSet<String> = BTreeSet::new();
1642            for stmt in fd.body.stmts() {
1643                match stmt {
1644                    crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => {
1645                        collect_fn_calls_expr(e, &mut called);
1646                    }
1647                }
1648            }
1649            for c in called {
1650                if let Some(callee_fd) = resolve_user_fn(&c) {
1651                    names.insert(callee_fd.name.clone());
1652                }
1653            }
1654        }
1655        if names.len() == before {
1656            break;
1657        }
1658    }
1659    names.remove(outer_fn);
1660    names.into_iter().collect()
1661}
1662
1663/// Detect functional equivalence of `fn_name` and a same-named spec
1664/// fn (`spec_fn_name = law.name`). Requires (a) law.name resolves to
1665/// a pure user fn `spec_fd` in `inputs`; (b) law's lhs/rhs are direct
1666/// calls — one to `fn_name`, one to `law.name` — with identical
1667/// argument lists; (c) `impl_fd` and `spec_fd` bodies are single-
1668/// terminal-expression bodies whose AST nodes match exactly.
1669///
1670/// On match, returns the unfold list: impl + spec + any user
1671/// helpers reachable from the law sides. Sorted for deterministic
1672/// emit.
1673fn detect_spec_equivalence(
1674    law: &crate::ast::VerifyLaw,
1675    fn_name: &str,
1676    inputs: &ProofLowerInputs,
1677) -> Option<Vec<String>> {
1678    use crate::ast::Expr;
1679    use std::collections::BTreeSet;
1680
1681    let spec_fn_name = &law.name;
1682    if spec_fn_name == fn_name {
1683        return None;
1684    }
1685    let spec_fd = inputs.find_fn_def_by_call_name(spec_fn_name)?;
1686    if !spec_fd.effects.is_empty() || spec_fd.name == "main" {
1687        return None;
1688    }
1689    let impl_fd = inputs.find_fn_def_by_call_name(fn_name)?;
1690
1691    let direct_call =
1692        |expr: &Spanned<crate::ast::Expr>| -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
1693            let Expr::FnCall(callee, args) = &expr.node else {
1694                return None;
1695            };
1696            let name = match &callee.node {
1697                Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
1698                _ => return None,
1699            };
1700            Some((name, args.clone()))
1701        };
1702    let canonical_shape =
1703        |lhs: &Spanned<crate::ast::Expr>, rhs: &Spanned<crate::ast::Expr>| -> bool {
1704            let Some((l_name, l_args)) = direct_call(lhs) else {
1705                return false;
1706            };
1707            let Some((r_name, r_args)) = direct_call(rhs) else {
1708                return false;
1709            };
1710            l_name == fn_name && r_name == *spec_fn_name && l_args == r_args
1711        };
1712    if !canonical_shape(&law.lhs, &law.rhs) && !canonical_shape(&law.rhs, &law.lhs) {
1713        return None;
1714    }
1715
1716    let impl_body = body_terminal_expr(impl_fd.body.as_ref())?;
1717    let spec_body = body_terminal_expr(spec_fd.body.as_ref())?;
1718    if impl_body.node != spec_body.node {
1719        return None;
1720    }
1721
1722    // Build the unfold set: impl + spec + transitively-reached user
1723    // helpers from law sides. Mirrors the legacy `law_simp_defs`
1724    // semantic but uses inputs (not CodegenContext).
1725    let resolve_user_fn = |name: &str| -> Option<&FnDef> {
1726        let fd = inputs.find_fn_def_by_call_name(name)?;
1727        if !fd.effects.is_empty() || fd.name == "main" {
1728            return None;
1729        }
1730        Some(fd)
1731    };
1732    let mut names: BTreeSet<String> = BTreeSet::new();
1733    names.insert(fn_name.to_string());
1734    names.insert(spec_fn_name.clone());
1735    let mut seed: BTreeSet<String> = BTreeSet::new();
1736    collect_fn_calls_expr(&law.lhs, &mut seed);
1737    collect_fn_calls_expr(&law.rhs, &mut seed);
1738    if let Some(when_expr) = &law.when {
1739        collect_fn_calls_expr(when_expr, &mut seed);
1740    }
1741    for n in seed {
1742        if let Some(fd) = resolve_user_fn(&n) {
1743            names.insert(fd.name.clone());
1744        }
1745    }
1746    loop {
1747        let before = names.len();
1748        let snapshot: Vec<String> = names.iter().cloned().collect();
1749        for name in snapshot {
1750            let Some(fd) = resolve_user_fn(&name) else {
1751                continue;
1752            };
1753            let mut called: BTreeSet<String> = BTreeSet::new();
1754            for stmt in fd.body.stmts() {
1755                match stmt {
1756                    crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => {
1757                        collect_fn_calls_expr(e, &mut called);
1758                    }
1759                }
1760            }
1761            for c in called {
1762                if let Some(callee_fd) = resolve_user_fn(&c) {
1763                    names.insert(callee_fd.name.clone());
1764                }
1765            }
1766        }
1767        if names.len() == before {
1768            break;
1769        }
1770    }
1771    Some(names.into_iter().collect())
1772}
1773
1774/// Detect functional equivalence in the broader "simp-normalized"
1775/// shape: same canonical impl/spec call structure as
1776/// [`detect_spec_equivalence`], but bodies are equivalent only
1777/// after arg substitution + arithmetic identity folding (drop
1778/// `+ 0`, `- 0`, `* 1`, fold `* 0 → 0`). Returns the unfold list
1779/// on match.
1780fn detect_simp_normalized_spec_equivalence(
1781    law: &crate::ast::VerifyLaw,
1782    fn_name: &str,
1783    inputs: &ProofLowerInputs,
1784) -> Option<Vec<String>> {
1785    use crate::ast::Expr;
1786    use std::collections::BTreeSet;
1787
1788    let spec_fn_name = &law.name;
1789    if spec_fn_name == fn_name {
1790        return None;
1791    }
1792    let spec_fd = inputs.find_fn_def_by_call_name(spec_fn_name)?;
1793    if !spec_fd.effects.is_empty() || spec_fd.name == "main" {
1794        return None;
1795    }
1796    let impl_fd = inputs.find_fn_def_by_call_name(fn_name)?;
1797
1798    let direct_call =
1799        |expr: &Spanned<crate::ast::Expr>| -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
1800            let Expr::FnCall(callee, args) = &expr.node else {
1801                return None;
1802            };
1803            let name = match &callee.node {
1804                Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
1805                _ => return None,
1806            };
1807            Some((name, args.clone()))
1808        };
1809    let canonical_shape_args = |lhs: &Spanned<crate::ast::Expr>,
1810                                rhs: &Spanned<crate::ast::Expr>|
1811     -> Option<Vec<Spanned<crate::ast::Expr>>> {
1812        let (l_name, l_args) = direct_call(lhs)?;
1813        let (r_name, r_args) = direct_call(rhs)?;
1814        if l_name != fn_name || r_name != *spec_fn_name || l_args != r_args {
1815            return None;
1816        }
1817        if l_args.len() != impl_fd.params.len() || r_args.len() != spec_fd.params.len() {
1818            return None;
1819        }
1820        Some(l_args)
1821    };
1822    let call_args = canonical_shape_args(&law.lhs, &law.rhs)
1823        .or_else(|| canonical_shape_args(&law.rhs, &law.lhs))?;
1824
1825    let impl_body = body_terminal_expr(impl_fd.body.as_ref())?;
1826    let spec_body = body_terminal_expr(spec_fd.body.as_ref())?;
1827    // Reject the body-identical case — that's covered by the
1828    // strict `SpecEquivalence` detector running before this one.
1829    if impl_body.node == spec_body.node {
1830        return None;
1831    }
1832    let impl_subst: std::collections::HashMap<String, Spanned<crate::ast::Expr>> = impl_fd
1833        .params
1834        .iter()
1835        .zip(call_args.iter())
1836        .map(|((n, _), arg)| (n.clone(), arg.clone()))
1837        .collect();
1838    let spec_subst: std::collections::HashMap<String, Spanned<crate::ast::Expr>> = spec_fd
1839        .params
1840        .iter()
1841        .zip(call_args.iter())
1842        .map(|((n, _), arg)| (n.clone(), arg.clone()))
1843        .collect();
1844    let impl_normalised = simplify_identity_expr(&crate::ast_rewrite::rewrite_idents_scoped(
1845        impl_body,
1846        |name| impl_subst.get(name).cloned(),
1847    ));
1848    let spec_normalised = simplify_identity_expr(&crate::ast_rewrite::rewrite_idents_scoped(
1849        spec_body,
1850        |name| spec_subst.get(name).cloned(),
1851    ));
1852    if impl_normalised.node != spec_normalised.node {
1853        return None;
1854    }
1855
1856    // Same unfold-set walk as `detect_spec_equivalence`.
1857    let resolve_user_fn = |name: &str| -> Option<&FnDef> {
1858        let fd = inputs.find_fn_def_by_call_name(name)?;
1859        if !fd.effects.is_empty() || fd.name == "main" {
1860            return None;
1861        }
1862        Some(fd)
1863    };
1864    let mut names: BTreeSet<String> = BTreeSet::new();
1865    names.insert(fn_name.to_string());
1866    names.insert(spec_fn_name.clone());
1867    let mut seed: BTreeSet<String> = BTreeSet::new();
1868    collect_fn_calls_expr(&law.lhs, &mut seed);
1869    collect_fn_calls_expr(&law.rhs, &mut seed);
1870    if let Some(when_expr) = &law.when {
1871        collect_fn_calls_expr(when_expr, &mut seed);
1872    }
1873    for n in seed {
1874        if let Some(fd) = resolve_user_fn(&n) {
1875            names.insert(fd.name.clone());
1876        }
1877    }
1878    loop {
1879        let before = names.len();
1880        let snapshot: Vec<String> = names.iter().cloned().collect();
1881        for name in snapshot {
1882            let Some(fd) = resolve_user_fn(&name) else {
1883                continue;
1884            };
1885            let mut called: BTreeSet<String> = BTreeSet::new();
1886            for stmt in fd.body.stmts() {
1887                match stmt {
1888                    crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => {
1889                        collect_fn_calls_expr(e, &mut called);
1890                    }
1891                }
1892            }
1893            for c in called {
1894                if let Some(callee_fd) = resolve_user_fn(&c) {
1895                    names.insert(callee_fd.name.clone());
1896                }
1897            }
1898        }
1899        if names.len() == before {
1900            break;
1901        }
1902    }
1903    Some(names.into_iter().collect())
1904}
1905
1906/// Detect "linear Int" spec equivalence: same canonical impl/spec
1907/// call shape as the other spec detectors, all givens are `Int`,
1908/// both impl and spec return `Int`, and the substituted bodies are
1909/// purely linear arithmetic expressions over the law-quantified
1910/// givens (only `Int` literals, given idents, `Add`, `Sub`). On
1911/// match returns the two substituted bodies — backends rewrite to
1912/// `change <impl> = <spec>` and close via their linear-arithmetic
1913/// decision procedure (`omega` on Lean, Z3 LIA on Dafny).
1914fn detect_linear_int_spec_equivalence(
1915    law: &crate::ast::VerifyLaw,
1916    fn_name: &str,
1917    inputs: &ProofLowerInputs,
1918) -> Option<(Spanned<crate::ast::Expr>, Spanned<crate::ast::Expr>)> {
1919    use crate::ast::Expr;
1920    use std::collections::HashSet;
1921
1922    if law.givens.is_empty() || !law.givens.iter().all(|g| g.type_name == "Int") {
1923        return None;
1924    }
1925    let spec_fn_name = &law.name;
1926    if spec_fn_name == fn_name {
1927        return None;
1928    }
1929    let spec_fd = inputs.find_fn_def_by_call_name(spec_fn_name)?;
1930    if !spec_fd.effects.is_empty() || spec_fd.name == "main" {
1931        return None;
1932    }
1933    let impl_fd = inputs.find_fn_def_by_call_name(fn_name)?;
1934    if impl_fd.return_type != "Int" || spec_fd.return_type != "Int" {
1935        return None;
1936    }
1937
1938    let direct_call =
1939        |expr: &Spanned<crate::ast::Expr>| -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
1940            let Expr::FnCall(callee, args) = &expr.node else {
1941                return None;
1942            };
1943            let name = match &callee.node {
1944                Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
1945                _ => return None,
1946            };
1947            Some((name, args.clone()))
1948        };
1949    let canonical_shape_args = |lhs: &Spanned<crate::ast::Expr>,
1950                                rhs: &Spanned<crate::ast::Expr>|
1951     -> Option<Vec<Spanned<crate::ast::Expr>>> {
1952        let (l_name, l_args) = direct_call(lhs)?;
1953        let (r_name, r_args) = direct_call(rhs)?;
1954        if l_name != fn_name || r_name != *spec_fn_name || l_args != r_args {
1955            return None;
1956        }
1957        if l_args.len() != impl_fd.params.len() || r_args.len() != spec_fd.params.len() {
1958            return None;
1959        }
1960        Some(l_args)
1961    };
1962    let call_args = canonical_shape_args(&law.lhs, &law.rhs)
1963        .or_else(|| canonical_shape_args(&law.rhs, &law.lhs))?;
1964
1965    let impl_body = body_terminal_expr(impl_fd.body.as_ref())?;
1966    let spec_body = body_terminal_expr(spec_fd.body.as_ref())?;
1967    let impl_subst: std::collections::HashMap<String, Spanned<crate::ast::Expr>> = impl_fd
1968        .params
1969        .iter()
1970        .zip(call_args.iter())
1971        .map(|((n, _), arg)| (n.clone(), arg.clone()))
1972        .collect();
1973    let spec_subst: std::collections::HashMap<String, Spanned<crate::ast::Expr>> = spec_fd
1974        .params
1975        .iter()
1976        .zip(call_args.iter())
1977        .map(|((n, _), arg)| (n.clone(), arg.clone()))
1978        .collect();
1979    let unfolded_impl =
1980        crate::ast_rewrite::rewrite_idents_scoped(impl_body, |name| impl_subst.get(name).cloned());
1981    let unfolded_spec =
1982        crate::ast_rewrite::rewrite_idents_scoped(spec_body, |name| spec_subst.get(name).cloned());
1983
1984    let allowed_idents: HashSet<&str> = law.givens.iter().map(|g| g.name.as_str()).collect();
1985    if !is_linear_int_expr(&unfolded_impl, &allowed_idents)
1986        || !is_linear_int_expr(&unfolded_spec, &allowed_idents)
1987    {
1988        return None;
1989    }
1990    Some((unfolded_impl, unfolded_spec))
1991}
1992
1993/// Check whether `expr` is purely linear arithmetic over `allowed_
1994/// idents`: only `Int` literals, allowed idents, and `Add`/`Sub`
1995/// BinOps. Mirrors legacy `spec::linear_int::is_linear_int_expr`.
1996fn is_linear_int_expr(
1997    expr: &Spanned<crate::ast::Expr>,
1998    allowed_idents: &std::collections::HashSet<&str>,
1999) -> bool {
2000    use crate::ast::{BinOp, Expr, Literal};
2001    match &expr.node {
2002        Expr::Literal(Literal::Int(_)) => true,
2003        Expr::Ident(name) | Expr::Resolved { name, .. } => allowed_idents.contains(name.as_str()),
2004        Expr::BinOp(BinOp::Add | BinOp::Sub, left, right) => {
2005            is_linear_int_expr(left, allowed_idents) && is_linear_int_expr(right, allowed_idents)
2006        }
2007        _ => false,
2008    }
2009}
2010
2011/// Detect functional equivalence between an effectful impl fn and
2012/// a spec fn (different name). Runs Oracle Lift over both sides of
2013/// the law first — injecting `BranchPath.Root` and oracle givens
2014/// into every classified effectful call site — then matches the
2015/// canonical `impl(args) == spec(args)` direct-call shape with
2016/// identical argument lists on the rewritten form. Returns the
2017/// spec fn name on match.
2018///
2019/// Body-match is not required here (and would fail — impl and spec
2020/// bodies usually differ syntactically; Oracle Lift normalises them
2021/// to a common oracle call only after backend-side `simp` unfolds).
2022fn detect_effectful_spec_equivalence(
2023    law: &crate::ast::VerifyLaw,
2024    fn_name: &str,
2025    inputs: &ProofLowerInputs,
2026) -> Option<String> {
2027    use crate::ast::Expr;
2028
2029    let impl_fd = inputs.find_fn_def_by_call_name(fn_name)?;
2030    if impl_fd.effects.is_empty() {
2031        return None;
2032    }
2033    if !impl_fd
2034        .effects
2035        .iter()
2036        .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
2037    {
2038        return None;
2039    }
2040
2041    let find_fn = |name: &str| -> Option<&crate::ast::FnDef> {
2042        inputs
2043            .entry_items
2044            .iter()
2045            .filter_map(|item| match item {
2046                TopLevel::FnDef(fd) => Some(fd),
2047                _ => None,
2048            })
2049            .find(|fd| fd.name == name)
2050    };
2051    let rewritten_lhs = crate::codegen::common::rewrite_effectful_calls_in_law(
2052        &law.lhs,
2053        law,
2054        find_fn,
2055        crate::codegen::common::OracleInjectionMode::LemmaBindingProjected,
2056    );
2057    let rewritten_rhs = crate::codegen::common::rewrite_effectful_calls_in_law(
2058        &law.rhs,
2059        law,
2060        find_fn,
2061        crate::codegen::common::OracleInjectionMode::LemmaBindingProjected,
2062    );
2063
2064    let direct_call =
2065        |expr: &Spanned<crate::ast::Expr>| -> Option<(String, Vec<Spanned<crate::ast::Expr>>)> {
2066            let Expr::FnCall(callee, args) = &expr.node else {
2067                return None;
2068            };
2069            let name = match &callee.node {
2070                Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
2071                _ => return None,
2072            };
2073            Some((name, args.clone()))
2074        };
2075    let try_side = |impl_side: &Spanned<crate::ast::Expr>,
2076                    spec_side: &Spanned<crate::ast::Expr>|
2077     -> Option<String> {
2078        let (l_name, l_args) = direct_call(impl_side)?;
2079        let (r_name, r_args) = direct_call(spec_side)?;
2080        if l_args != r_args || l_name == r_name || l_name != fn_name {
2081            return None;
2082        }
2083        Some(r_name)
2084    };
2085    try_side(&rewritten_lhs, &rewritten_rhs).or_else(|| try_side(&rewritten_rhs, &rewritten_lhs))
2086}
2087
2088/// Detect second-order linear recurrence spec equivalence (fib /
2089/// fibSpec pattern). impl_fn is a tail-rec wrapper dispatching on
2090/// `n < 0` and calling a 3-arg helper with seed pair; spec_fn is a
2091/// direct recurrence with `match n { 0 / 1 / _ }` arms. The shared
2092/// affine recurrence must match between the helper's worker and the
2093/// spec's `_` arm. Returns `(spec_fn_name, helper_fn_name)` on
2094/// match. Detection lives behind the `lean::recurrence::detect_*`
2095/// helpers because their AST patterns were specced there originally;
2096/// the data they extract is backend-neutral.
2097fn detect_linear_recurrence2_spec_equivalence(
2098    law: &crate::ast::VerifyLaw,
2099    fn_name: &str,
2100    inputs: &ProofLowerInputs,
2101) -> Option<(String, String)> {
2102    use crate::codegen::lean::recurrence::{
2103        detect_second_order_int_linear_recurrence, detect_tailrec_int_linear_pair_worker,
2104        detect_tailrec_int_linear_pair_wrapper,
2105    };
2106
2107    let spec_fn_name = &law.name;
2108    if spec_fn_name == fn_name {
2109        return None;
2110    }
2111    if !law_references_fn(&law.lhs, spec_fn_name) && !law_references_fn(&law.rhs, spec_fn_name) {
2112        return None;
2113    }
2114
2115    let impl_fd = inputs.find_fn_def_by_call_name(fn_name)?;
2116    let spec_fd = inputs.find_fn_def_by_call_name(spec_fn_name)?;
2117    let impl_shape = detect_tailrec_int_linear_pair_wrapper(impl_fd)?;
2118    let spec_shape = detect_second_order_int_linear_recurrence(spec_fd)?;
2119
2120    // AST-strict cross-check: negative branch + seed values must
2121    // match across impl wrapper and spec direct recurrence.
2122    if impl_shape.negative_branch.node != spec_shape.negative_branch.node
2123        || impl_shape.seed_prev.node != spec_shape.base0.node
2124        || impl_shape.seed_curr.node != spec_shape.base1.node
2125    {
2126        return None;
2127    }
2128
2129    let helper_fd = inputs.find_fn_def_by_call_name(&impl_shape.helper_fn_name)?;
2130    let helper_shape = detect_tailrec_int_linear_pair_worker(helper_fd)?;
2131    if helper_shape.recurrence != spec_shape.recurrence {
2132        return None;
2133    }
2134
2135    Some((spec_fn_name.clone(), impl_shape.helper_fn_name))
2136}
2137
2138fn law_references_fn(expr: &Spanned<crate::ast::Expr>, target: &str) -> bool {
2139    use crate::ast::Expr;
2140    match &expr.node {
2141        Expr::FnCall(callee, args) => {
2142            let name = match &callee.node {
2143                Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.as_str()),
2144                _ => None,
2145            };
2146            if name == Some(target) {
2147                return true;
2148            }
2149            args.iter().any(|a| law_references_fn(a, target))
2150        }
2151        Expr::BinOp(_, l, r) => law_references_fn(l, target) || law_references_fn(r, target),
2152        Expr::Attr(base, _) => law_references_fn(base, target),
2153        Expr::Match { subject, arms } => {
2154            law_references_fn(subject, target)
2155                || arms.iter().any(|arm| law_references_fn(&arm.body, target))
2156        }
2157        _ => false,
2158    }
2159}
2160
2161/// Validate the outer fn's body matches the "inspect get, set in
2162/// every arm" template:
2163///
2164/// ```text
2165/// fn outer(m: Map<K, V>, k: K) -> Map<K, V>
2166///   [v = Map.get(m, k);]?
2167///   match (v | Map.get(m, k)) {
2168///     _ -> Map.set(m, k, _)
2169///     _ -> Map.set(m, k, _)
2170///     ...
2171///   }
2172/// ```
2173///
2174/// Returns `Some(())` on match. The map/key params are positional —
2175/// position 0 is the map, position 1 is the key.
2176fn outer_fn_map_update_shape(fn_name: &str, inputs: &ProofLowerInputs) -> Option<()> {
2177    let fd = inputs.find_fn_def_by_call_name(fn_name)?;
2178    if fd.params.len() != 2 {
2179        return None;
2180    }
2181    let map_param = fd.params[0].0.as_str();
2182    let key_param = fd.params[1].0.as_str();
2183    map_update_body_matches(fd.body.stmts(), map_param, key_param).then_some(())
2184}
2185
2186fn map_update_body_matches(stmts: &[crate::ast::Stmt], map_param: &str, key_param: &str) -> bool {
2187    use crate::ast::Stmt;
2188    if stmts.len() < 2 {
2189        // Even the no-let variant requires the match to be the last
2190        // stmt — but a single-stmt body is too short to be this shape.
2191        return matches!(stmts.first(), Some(Stmt::Expr(e)) if map_update_match_expr(e, map_param, key_param, None));
2192    }
2193    let Some(last) = stmts.last() else {
2194        return false;
2195    };
2196    let mut bound_name: Option<&str> = None;
2197    for stmt in &stmts[..stmts.len() - 1] {
2198        match stmt {
2199            Stmt::Binding(name, _, expr) => {
2200                if !is_map_get_of_params(expr, map_param, key_param) {
2201                    return false;
2202                }
2203                bound_name = Some(name);
2204            }
2205            Stmt::Expr(_) => return false,
2206        }
2207    }
2208    match last {
2209        Stmt::Expr(expr) => map_update_match_expr(expr, map_param, key_param, bound_name),
2210        Stmt::Binding(_, _, _) => false,
2211    }
2212}
2213
2214fn map_update_match_expr(
2215    expr: &Spanned<crate::ast::Expr>,
2216    map_param: &str,
2217    key_param: &str,
2218    bound_name: Option<&str>,
2219) -> bool {
2220    use crate::ast::Expr;
2221    let Expr::Match { subject, arms } = &expr.node else {
2222        return false;
2223    };
2224    if arms.len() < 2 {
2225        return false;
2226    }
2227    let subject_ok = match bound_name {
2228        Some(name) => matches_ident_expr(subject, name),
2229        None => is_map_get_of_params(subject, map_param, key_param),
2230    };
2231    if !subject_ok {
2232        return false;
2233    }
2234    arms.iter()
2235        .all(|arm| is_map_set_of_params(&arm.body, map_param, key_param))
2236}
2237
2238fn is_map_get_of_params(
2239    expr: &Spanned<crate::ast::Expr>,
2240    map_param: &str,
2241    key_param: &str,
2242) -> bool {
2243    let Some(args) = call_named_args(expr, "Map.get") else {
2244        return false;
2245    };
2246    args.len() == 2
2247        && matches_ident_expr(&args[0], map_param)
2248        && matches_ident_expr(&args[1], key_param)
2249}
2250
2251fn is_map_set_of_params(
2252    expr: &Spanned<crate::ast::Expr>,
2253    map_param: &str,
2254    key_param: &str,
2255) -> bool {
2256    let Some(args) = call_named_args(expr, "Map.set") else {
2257        return false;
2258    };
2259    args.len() == 3
2260        && matches_ident_expr(&args[0], map_param)
2261        && matches_ident_expr(&args[1], key_param)
2262}
2263
2264struct MapKeyTrackedIncrementPlan {
2265    outer_fn: String,
2266    map_arg: Spanned<crate::ast::Expr>,
2267    key_arg: Spanned<crate::ast::Expr>,
2268}
2269
2270/// Detect the canonical tracked-counter increment law:
2271/// `Option.withDefault(Map.get(outer(m, k), k), 0) ==
2272/// Option.withDefault(Map.get(m, k), 0) + 1`, where `outer` follows
2273/// the increment-by-one body template (see
2274/// [`outer_fn_map_increment_shape`]).
2275fn detect_map_key_tracked_increment(
2276    law: &crate::ast::VerifyLaw,
2277    fn_name: &str,
2278    inputs: &ProofLowerInputs,
2279) -> Option<MapKeyTrackedIncrementPlan> {
2280    use crate::ast::{BinOp, Expr};
2281
2282    outer_fn_map_increment_shape(fn_name, inputs)?;
2283
2284    let side = |after: &Spanned<crate::ast::Expr>,
2285                rhs: &Spanned<crate::ast::Expr>|
2286     -> Option<MapKeyTrackedIncrementPlan> {
2287        let (map_arg, key_arg, default_arg) = defaulted_map_get_after_fn_call(after, fn_name)?;
2288        if !is_int_lit(default_arg, 0) {
2289            return None;
2290        }
2291        let Expr::BinOp(BinOp::Add, base, one) = &rhs.node else {
2292            return None;
2293        };
2294        if !is_int_lit(one, 1) {
2295            return None;
2296        }
2297        let (base_map, base_key, base_default) = defaulted_map_get(base)?;
2298        if map_arg.node != base_map.node
2299            || key_arg.node != base_key.node
2300            || default_arg.node != base_default.node
2301        {
2302            return None;
2303        }
2304        Some(MapKeyTrackedIncrementPlan {
2305            outer_fn: fn_name.to_string(),
2306            map_arg: map_arg.clone(),
2307            key_arg: key_arg.clone(),
2308        })
2309    };
2310    side(&law.lhs, &law.rhs).or_else(|| side(&law.rhs, &law.lhs))
2311}
2312
2313/// Validate `outer(m, k)` follows the canonical tracked-counter
2314/// increment body:
2315///
2316/// ```text
2317/// let v = Map.get m k
2318/// match v {
2319///   Some(n) -> Map.set m k (n + 1)
2320///   None    -> Map.set m k 1
2321/// }
2322/// ```
2323fn outer_fn_map_increment_shape(fn_name: &str, inputs: &ProofLowerInputs) -> Option<()> {
2324    use crate::ast::{BinOp, Expr, Pattern, Stmt};
2325
2326    let fd = inputs.find_fn_def_by_call_name(fn_name)?;
2327    if fd.params.len() != 2 {
2328        return None;
2329    }
2330    let map_param = fd.params[0].0.as_str();
2331    let key_param = fd.params[1].0.as_str();
2332    let stmts = fd.body.stmts();
2333    if stmts.len() != 2 {
2334        return None;
2335    }
2336    let Stmt::Binding(current, _, bound_expr) = &stmts[0] else {
2337        return None;
2338    };
2339    if !is_map_get_of_params(bound_expr, map_param, key_param) {
2340        return None;
2341    }
2342    let Stmt::Expr(last_expr) = &stmts[1] else {
2343        return None;
2344    };
2345    let Expr::Match { subject, arms, .. } = &last_expr.node else {
2346        return None;
2347    };
2348    if !matches_ident_expr(subject, current) || arms.len() != 2 {
2349        return None;
2350    }
2351
2352    let some_arm = arms.iter().find_map(|arm| match &arm.pattern {
2353        Pattern::Constructor(name, vars) if name == "Option.Some" && vars.len() == 1 => {
2354            Some((vars[0].as_str(), arm.body.as_ref()))
2355        }
2356        _ => None,
2357    })?;
2358    let none_arm = arms.iter().find_map(|arm| match &arm.pattern {
2359        Pattern::Constructor(name, vars) if name == "Option.None" && vars.is_empty() => {
2360            Some(arm.body.as_ref())
2361        }
2362        _ => None,
2363    })?;
2364
2365    let (some_bound, some_body) = some_arm;
2366    let some_set = call_named_args(some_body, "Map.set")?;
2367    let none_set = call_named_args(none_arm, "Map.set")?;
2368    if some_set.len() != 3 || none_set.len() != 3 {
2369        return None;
2370    }
2371    if !matches_ident_expr(&some_set[0], map_param)
2372        || !matches_ident_expr(&some_set[1], key_param)
2373        || !matches_ident_expr(&none_set[0], map_param)
2374        || !matches_ident_expr(&none_set[1], key_param)
2375    {
2376        return None;
2377    }
2378    let Expr::BinOp(BinOp::Add, add_left, add_right) = &some_set[2].node else {
2379        return None;
2380    };
2381    if !matches_ident_expr(add_left, some_bound) || !is_int_lit(add_right, 1) {
2382        return None;
2383    }
2384    if !is_int_lit(&none_set[2], 1) {
2385        return None;
2386    }
2387    Some(())
2388}
2389
2390/// `Option.withDefault(Map.get(outer(m, k), k), d)` — extract (m, k,
2391/// d) when the outer call matches `fn_name` and the lookup uses the
2392/// same key as the outer call's key arg.
2393fn defaulted_map_get_after_fn_call<'a>(
2394    expr: &'a Spanned<crate::ast::Expr>,
2395    fn_name: &str,
2396) -> Option<(
2397    &'a Spanned<crate::ast::Expr>,
2398    &'a Spanned<crate::ast::Expr>,
2399    &'a Spanned<crate::ast::Expr>,
2400)> {
2401    let (inner, default) = option_with_default_args(expr)?;
2402    let (map_arg, key_arg) = map_get_after_fn_call(inner, fn_name)?;
2403    Some((map_arg, key_arg, default))
2404}
2405
2406/// `Option.withDefault(Map.get(m, k), d)` — extract (m, k, d) for the
2407/// bare lookup form (no surrounding outer call).
2408fn defaulted_map_get(
2409    expr: &Spanned<crate::ast::Expr>,
2410) -> Option<(
2411    &Spanned<crate::ast::Expr>,
2412    &Spanned<crate::ast::Expr>,
2413    &Spanned<crate::ast::Expr>,
2414)> {
2415    let (inner, default) = option_with_default_args(expr)?;
2416    let get_args = call_named_args(inner, "Map.get")?;
2417    if get_args.len() != 2 {
2418        return None;
2419    }
2420    Some((&get_args[0], &get_args[1], default))
2421}
2422
2423fn option_with_default_args(
2424    expr: &Spanned<crate::ast::Expr>,
2425) -> Option<(&Spanned<crate::ast::Expr>, &Spanned<crate::ast::Expr>)> {
2426    let args = call_named_args(expr, "Option.withDefault")?;
2427    (args.len() == 2).then_some((&args[0], &args[1]))
2428}
2429
2430fn is_int_lit(expr: &Spanned<crate::ast::Expr>, n: i64) -> bool {
2431    use crate::ast::{Expr, Literal};
2432    matches!(&expr.node, Expr::Literal(Literal::Int(m)) if *m == n)
2433}
2434
2435/// `Map.has(outer(m, k), k)` — pick out the (m, k) from the inner
2436/// outer-fn call when the two key positions agree. Returns `None`
2437/// when the outer call doesn't match `fn_name` or shape is off.
2438fn map_has_after_fn_call<'a>(
2439    expr: &'a Spanned<crate::ast::Expr>,
2440    fn_name: &str,
2441) -> Option<(&'a Spanned<crate::ast::Expr>, &'a Spanned<crate::ast::Expr>)> {
2442    use crate::ast::Expr;
2443    let has_args = call_named_args(expr, "Map.has")?;
2444    if has_args.len() != 2 {
2445        return None;
2446    }
2447    let Expr::FnCall(callee, fn_args) = &has_args[0].node else {
2448        return None;
2449    };
2450    if fn_args.len() != 2
2451        || !callee_matches_name(callee, fn_name)
2452        || fn_args[1].node != has_args[1].node
2453    {
2454        return None;
2455    }
2456    Some((&fn_args[0], &fn_args[1]))
2457}
2458
2459/// `Map.get(outer(m, k), k)` — pick out the (m, k) from the inner
2460/// outer-fn call when the two key positions agree.
2461fn map_get_after_fn_call<'a>(
2462    expr: &'a Spanned<crate::ast::Expr>,
2463    fn_name: &str,
2464) -> Option<(&'a Spanned<crate::ast::Expr>, &'a Spanned<crate::ast::Expr>)> {
2465    use crate::ast::Expr;
2466    let get_args = call_named_args(expr, "Map.get")?;
2467    if get_args.len() != 2 {
2468        return None;
2469    }
2470    let Expr::FnCall(callee, fn_args) = &get_args[0].node else {
2471        return None;
2472    };
2473    if fn_args.len() != 2
2474        || !callee_matches_name(callee, fn_name)
2475        || fn_args[1].node != get_args[1].node
2476    {
2477        return None;
2478    }
2479    Some((&fn_args[0], &fn_args[1]))
2480}
2481
2482fn map_has_set_parts(
2483    expr: &Spanned<crate::ast::Expr>,
2484) -> Option<(
2485    &Spanned<crate::ast::Expr>,
2486    &Spanned<crate::ast::Expr>,
2487    &Spanned<crate::ast::Expr>,
2488)> {
2489    let has_args = call_named_args(expr, "Map.has")?;
2490    if has_args.len() != 2 {
2491        return None;
2492    }
2493    let set_args = call_named_args(&has_args[0], "Map.set")?;
2494    if set_args.len() != 3 {
2495        return None;
2496    }
2497    if set_args[1].node != has_args[1].node {
2498        return None;
2499    }
2500    Some((&set_args[0], &set_args[1], &set_args[2]))
2501}
2502
2503fn map_get_set_parts(
2504    expr: &Spanned<crate::ast::Expr>,
2505) -> Option<(
2506    &Spanned<crate::ast::Expr>,
2507    &Spanned<crate::ast::Expr>,
2508    &Spanned<crate::ast::Expr>,
2509)> {
2510    let get_args = call_named_args(expr, "Map.get")?;
2511    if get_args.len() != 2 {
2512        return None;
2513    }
2514    let set_args = call_named_args(&get_args[0], "Map.set")?;
2515    if set_args.len() != 3 {
2516        return None;
2517    }
2518    if set_args[1].node != get_args[1].node {
2519        return None;
2520    }
2521    Some((&set_args[0], &set_args[1], &set_args[2]))
2522}
2523
2524fn option_some_arg(expr: &Spanned<crate::ast::Expr>) -> Option<&Spanned<crate::ast::Expr>> {
2525    let args = call_named_args(expr, "Option.Some")?;
2526    (args.len() == 1).then_some(&args[0])
2527}
2528
2529/// **syntax-discovery-only** (epic #170 Phase 7). Recognises a
2530/// fn-call expression whose callee's dotted source name matches
2531/// `full_name` exactly. Used by `Map.set` / `Map.get` axiom shape
2532/// detection where `full_name` is a builtin namespace path
2533/// (`"Map.get"` etc.) — global identity, no per-scope leak.
2534fn call_named_args<'a>(
2535    expr: &'a Spanned<crate::ast::Expr>,
2536    full_name: &str,
2537) -> Option<&'a [Spanned<crate::ast::Expr>]> {
2538    use crate::ast::Expr;
2539    let Expr::FnCall(callee, args) = &expr.node else {
2540        return None;
2541    };
2542    let callee_name = expr_to_dotted_name(&callee.node)?;
2543    if callee_name == full_name {
2544        Some(args.as_slice())
2545    } else {
2546        None
2547    }
2548}
2549
2550fn is_bool_true(expr: &Spanned<crate::ast::Expr>) -> bool {
2551    use crate::ast::{Expr, Literal};
2552    matches!(&expr.node, Expr::Literal(Literal::Bool(true)))
2553}
2554
2555/// Detect a `given` that binds a recursive sum-typed ADT — the
2556/// induction target. Returns the given's source name on first
2557/// match, or `None` when no given fits.
2558///
2559/// "Recursive" means at least one variant references the type
2560/// itself in its field list (either bare `Tree` or wrapped like
2561/// `List<Tree>` / `Tree, Tree`). Indirect-via-other-types rec
2562/// shapes are rejected here — the backend's emit can't handle
2563/// them and would fail at lake-build time; better to fall through
2564/// to `BackendDispatch` than pin a bad strategy.
2565/// Stage 8b of #232: detect `chain_qm(g) == chain_manual(g)` where
2566/// `chain_qm` is a `ModulePattern::ResultPipelineChain` (the `?`-chain
2567/// fn) and `chain_manual` is a manual `match Result.Err -> Err`
2568/// nested chain over the same step fns. Returns a payload carrying
2569/// both fn names + the ordered step list so backends can emit the
2570/// unfold list without re-walking the AST.
2571/// Stage 8c of #232: detect `fold(g) == spec(g)` where both `fold`
2572/// and `spec` are registered `ModulePattern::MatchDispatcherFold`
2573/// fns over a `List<T>` param. Both sides are list folds with
2574/// identical match structure; structural induction on `xs` plus
2575/// `omega` per arm closes the equivalence — emit lives on the
2576/// backend.
2577fn detect_match_dispatcher_fold_equivalence(
2578    law: &crate::ast::VerifyLaw,
2579    fn_name: &str,
2580    inputs: &ProofLowerInputs,
2581) -> Option<crate::ir::ProofStrategy> {
2582    use crate::analysis::shape::ModulePattern;
2583    use crate::ast::Expr;
2584
2585    fn ident_name(e: &Spanned<Expr>) -> Option<&str> {
2586        match &e.node {
2587            Expr::Ident(n) => Some(n.as_str()),
2588            Expr::Resolved { name, .. } => Some(name.as_str()),
2589            _ => None,
2590        }
2591    }
2592
2593    let shape = inputs.program_shape?;
2594
2595    if law.givens.len() != 1 {
2596        return None;
2597    }
2598    let given_name = &law.givens[0].name;
2599
2600    // fn_name must be a MatchDispatcherFold in the shape.
2601    let fold_fn_pinned = shape.patterns.iter().any(|p| {
2602        matches!(
2603            p,
2604            ModulePattern::MatchDispatcherFold { fn_name: n, .. } if n == fn_name
2605        )
2606    });
2607    if !fold_fn_pinned {
2608        return None;
2609    }
2610
2611    // Extract `fold(g)` and `spec(g)` from law sides.
2612    let extract = |expr: &Spanned<Expr>| -> Option<String> {
2613        let Expr::FnCall(callee, args) = &expr.node else {
2614            return None;
2615        };
2616        let name = ident_name(callee)?;
2617        if args.len() != 1 {
2618            return None;
2619        }
2620        if ident_name(&args[0])? != given_name {
2621            return None;
2622        }
2623        Some(name.to_string())
2624    };
2625    let lhs_call = extract(&law.lhs)?;
2626    let rhs_call = extract(&law.rhs)?;
2627    let (fold_fn, spec_fn) = if lhs_call == fn_name && rhs_call != fn_name {
2628        (lhs_call, rhs_call)
2629    } else if rhs_call == fn_name && lhs_call != fn_name {
2630        (rhs_call, lhs_call)
2631    } else {
2632        return None;
2633    };
2634
2635    // The spec side must also be a MatchDispatcherFold — otherwise
2636    // the structural-induction template's `simp` step on the RHS
2637    // won't have a fold to unfold.
2638    let spec_pinned = shape.patterns.iter().any(|p| {
2639        matches!(
2640            p,
2641            ModulePattern::MatchDispatcherFold { fn_name: n, .. } if n == &spec_fn
2642        )
2643    });
2644    if !spec_pinned {
2645        return None;
2646    }
2647
2648    Some(crate::ir::ProofStrategy::MatchDispatcherFold { fold_fn, spec_fn })
2649}
2650
2651fn detect_result_pipeline_chain_equivalence(
2652    law: &crate::ast::VerifyLaw,
2653    fn_name: &str,
2654    inputs: &ProofLowerInputs,
2655) -> Option<crate::ir::ProofStrategy> {
2656    use crate::analysis::shape::ModulePattern;
2657    use crate::ast::{Expr, Pattern, Stmt};
2658
2659    fn ident_name(e: &Spanned<Expr>) -> Option<&str> {
2660        match &e.node {
2661            Expr::Ident(n) => Some(n.as_str()),
2662            Expr::Resolved { name, .. } => Some(name.as_str()),
2663            _ => None,
2664        }
2665    }
2666
2667    let shape = inputs.program_shape?;
2668
2669    if law.givens.len() != 1 {
2670        return None;
2671    }
2672    let given_name = &law.givens[0].name;
2673
2674    // Confirm fn_name is a ResultPipelineChain in the shape, and
2675    // pull the step fn list from there — post-pipeline AST has
2676    // already desugared `?` into nested matches, so the shape
2677    // (built from pre-resolver source items) is the authoritative
2678    // record of the original step list.
2679    let (chain_qm_fn, step_fns) = shape.patterns.iter().find_map(|p| match p {
2680        ModulePattern::ResultPipelineChain {
2681            fn_name: n,
2682            step_fns,
2683            ..
2684        } if n == fn_name => Some((n.clone(), step_fns.clone())),
2685        _ => None,
2686    })?;
2687
2688    // Law shape: `chain_qm(g) == chain_manual(g)` (either side).
2689    let extract = |expr: &Spanned<Expr>| -> Option<String> {
2690        let Expr::FnCall(callee, args) = &expr.node else {
2691            return None;
2692        };
2693        let name = ident_name(callee)?;
2694        if args.len() != 1 {
2695            return None;
2696        }
2697        if ident_name(&args[0])? != given_name {
2698            return None;
2699        }
2700        Some(name.to_string())
2701    };
2702    let lhs_call = extract(&law.lhs);
2703    let rhs_call = extract(&law.rhs);
2704    let chain_manual_fn = match (lhs_call, rhs_call) {
2705        (Some(l), Some(r)) if l == chain_qm_fn && r != chain_qm_fn => r,
2706        (Some(l), Some(r)) if r == chain_qm_fn && l != chain_qm_fn => l,
2707        _ => return None,
2708    };
2709
2710    if step_fns.len() < 2 {
2711        return None;
2712    }
2713
2714    // Verify the manual fn is a nested `match Result.Err -> Err`
2715    // chain calling the same step fns. Heuristic: walk body, look
2716    // for `Match { subject: Call(step, _), arms: [Err -> Err, Ok -> ...] }`
2717    // pattern. Counts how many step calls show up.
2718    let manual_fd = inputs.find_fn_def_by_call_name(&chain_manual_fn)?;
2719    let mut manual_steps: Vec<String> = Vec::new();
2720    fn walk_manual<'a>(
2721        expr: &'a Spanned<Expr>,
2722        steps: &mut Vec<String>,
2723        ident_name: &dyn Fn(&'a Spanned<Expr>) -> Option<&'a str>,
2724    ) {
2725        if let Expr::Match { subject, arms } = &expr.node
2726            && let Expr::FnCall(callee, _) = &subject.node
2727            && let Some(n) = ident_name(subject).or_else(|| ident_name(callee))
2728        {
2729            let has_err_pass = arms.iter().any(|a| {
2730                let pat_is_err = matches!(
2731                    &a.pattern,
2732                    Pattern::Constructor(c, _) if c == "Result.Err" || c.ends_with(".Err")
2733                );
2734                // Body shape: either `Expr::Constructor("Result.Err", _)`
2735                // (pre-resolver) or `Expr::FnCall(Attr(Ident("Result"), "Err"), _)`
2736                // (post-resolver — `Result.Err(...)` is treated as a
2737                // method-attr call once names are wired up).
2738                let body_is_err = match &a.body.node {
2739                    Expr::Constructor(c, _) => c == "Result.Err" || c.ends_with(".Err"),
2740                    Expr::FnCall(callee, _) => matches!(
2741                        &callee.node,
2742                        Expr::Attr(base, attr)
2743                            if attr == "Err"
2744                                && matches!(&base.node, Expr::Ident(b) if b == "Result")
2745                    ),
2746                    _ => false,
2747                };
2748                pat_is_err && body_is_err
2749            });
2750            if has_err_pass {
2751                steps.push(n.to_string());
2752            }
2753            for a in arms {
2754                walk_manual(&a.body, steps, ident_name);
2755            }
2756        }
2757    }
2758    let manual_stmts = manual_fd.body.stmts();
2759    if manual_stmts.len() != 1 {
2760        return None;
2761    }
2762    let Stmt::Expr(manual_root) = &manual_stmts[0] else {
2763        return None;
2764    };
2765    walk_manual(manual_root, &mut manual_steps, &ident_name);
2766    if manual_steps.len() < 2 {
2767        return None;
2768    }
2769
2770    Some(crate::ir::ProofStrategy::ResultPipelineChain {
2771        chain_qm_fn,
2772        chain_manual_fn,
2773        step_fns,
2774    })
2775}
2776
2777/// Stage 8 of #232: detect `wrapper(g) == other(g)` where `wrapper`
2778/// is a `ModulePattern::WrapperOverRecursion` and the inner fn has a
2779/// monoidal-accumulator shape we know how to emit Dafny support
2780/// theorems for. Returns the typed strategy carrying enough payload
2781/// for the backend to emit the aux acc-decomposition lemma without
2782/// re-walking the AST.
2783fn detect_wrapper_over_recursion(
2784    law: &crate::ast::VerifyLaw,
2785    fn_name: &str,
2786    inputs: &ProofLowerInputs,
2787) -> Option<crate::ir::ProofStrategy> {
2788    use crate::analysis::shape::ModulePattern;
2789    use crate::ast::{BinOp, Expr, Pattern, Stmt};
2790
2791    // Pre-pipeline AST has `Expr::Ident(n)`; post-pipeline the
2792    // resolver rewrites local/param idents to `Expr::Resolved { name }`.
2793    // Both forms identify the same surface name.
2794    fn ident_name(e: &Spanned<Expr>) -> Option<&str> {
2795        match &e.node {
2796            Expr::Ident(n) => Some(n.as_str()),
2797            Expr::Resolved { name, .. } => Some(name.as_str()),
2798            _ => None,
2799        }
2800    }
2801
2802    let shape = inputs.program_shape?;
2803
2804    // Only handle single-given laws — the template is parameterized
2805    // by one universal `xs`.
2806    if law.givens.len() != 1 {
2807        return None;
2808    }
2809    let given_name = &law.givens[0].name;
2810
2811    // Find the matching pattern in the shape — wrapper_fn must
2812    // equal the law's surrounding fn.
2813    let (wrapper_fn, inner_fn) = shape.patterns.iter().find_map(|p| match p {
2814        ModulePattern::WrapperOverRecursion {
2815            wrapper_fn,
2816            inner_fn,
2817            ..
2818        } if wrapper_fn == fn_name => Some((wrapper_fn.clone(), inner_fn.clone())),
2819        _ => None,
2820    })?;
2821
2822    // Law shape: `wrapper(g) == other(g)` (either side).
2823    let extract = |expr: &Spanned<crate::ast::Expr>| -> Option<String> {
2824        let Expr::FnCall(callee, args) = &expr.node else {
2825            return None;
2826        };
2827        let name = ident_name(callee)?;
2828        if args.len() != 1 {
2829            return None;
2830        }
2831        if ident_name(&args[0])? != given_name {
2832            return None;
2833        }
2834        Some(name.to_string())
2835    };
2836    let lhs_call = extract(&law.lhs);
2837    let rhs_call = extract(&law.rhs);
2838    let other_fn = match (lhs_call, rhs_call) {
2839        (Some(l), Some(r)) if l == wrapper_fn && r != wrapper_fn => r,
2840        (Some(l), Some(r)) if r == wrapper_fn && l != wrapper_fn => l,
2841        _ => return None,
2842    };
2843
2844    // Inner fn must have body shape
2845    //   match xs { [] -> acc; [h, ..t] -> inner(t, acc OP h) }
2846    // — extract OP.
2847    let inner_fd = inputs.find_fn_def_by_call_name(&inner_fn)?;
2848    if inner_fd.params.len() != 2 {
2849        return None;
2850    }
2851    let stmts = inner_fd.body.stmts();
2852    if stmts.len() != 1 {
2853        return None;
2854    }
2855    let Stmt::Expr(body) = &stmts[0] else {
2856        return None;
2857    };
2858    let Expr::Match { subject, arms } = &body.node else {
2859        return None;
2860    };
2861    if ident_name(subject)? != inner_fd.params[0].0 {
2862        return None;
2863    }
2864    if arms.len() != 2 {
2865        return None;
2866    }
2867    let mut nil_acc_ok = false;
2868    let mut cons_op: Option<BinOp> = None;
2869    let acc_name = &inner_fd.params[1].0;
2870    for arm in arms {
2871        match &arm.pattern {
2872            Pattern::EmptyList => {
2873                if ident_name(&arm.body) == Some(acc_name.as_str()) {
2874                    nil_acc_ok = true;
2875                }
2876            }
2877            Pattern::Cons(head_name, tail_name) => {
2878                // Body must be `inner(tail, acc OP head)` (or with `head OP acc`).
2879                // Post-pipeline the call can also be a `TailCall`.
2880                let (callee_name, args) = match &arm.body.node {
2881                    Expr::FnCall(c, a) => (ident_name(c)?, a.clone()),
2882                    Expr::TailCall(td) => (td.target.as_str(), td.args.clone()),
2883                    _ => return None,
2884                };
2885                if callee_name != inner_fn {
2886                    return None;
2887                }
2888                if args.len() != 2 {
2889                    return None;
2890                }
2891                if ident_name(&args[0])? != tail_name.as_str() {
2892                    return None;
2893                }
2894                let Expr::BinOp(op, l, r) = &args[1].node else {
2895                    return None;
2896                };
2897                if !matches!(op, BinOp::Add | BinOp::Mul) {
2898                    return None;
2899                }
2900                let l_n = ident_name(l);
2901                let r_n = ident_name(r);
2902                let l_is_acc = l_n == Some(acc_name.as_str());
2903                let r_is_head = r_n == Some(head_name.as_str());
2904                let l_is_head = l_n == Some(head_name.as_str());
2905                let r_is_acc = r_n == Some(acc_name.as_str());
2906                if !((l_is_acc && r_is_head) || (l_is_head && r_is_acc)) {
2907                    return None;
2908                }
2909                cons_op = Some(*op);
2910            }
2911            _ => return None,
2912        }
2913    }
2914    if !nil_acc_ok {
2915        return None;
2916    }
2917    let combine_op = cons_op?;
2918
2919    // Wrapper body must be `inner(<given>, neutral)` where neutral is
2920    // 0 for Add and 1 for Mul. Verify against the fn def to be sure.
2921    let wrapper_fd = inputs.find_fn_def_by_call_name(&wrapper_fn)?;
2922    let wstmts = wrapper_fd.body.stmts();
2923    if wstmts.len() != 1 {
2924        return None;
2925    }
2926    let Stmt::Expr(wbody) = &wstmts[0] else {
2927        return None;
2928    };
2929    let (wcallee_name, wargs) = match &wbody.node {
2930        Expr::FnCall(c, a) => (ident_name(c)?, a.clone()),
2931        Expr::TailCall(td) => (td.target.as_str(), td.args.clone()),
2932        _ => return None,
2933    };
2934    if wcallee_name != inner_fn {
2935        return None;
2936    }
2937    if wargs.len() != 2 {
2938        return None;
2939    }
2940    let expected_neutral: i64 = match combine_op {
2941        BinOp::Add | BinOp::Sub => 0,
2942        BinOp::Mul => 1,
2943        _ => return None,
2944    };
2945    let neutral_matches = matches!(
2946        &wargs[1].node,
2947        Expr::Literal(crate::ast::Literal::Int(n)) if *n == expected_neutral
2948    );
2949    if !neutral_matches {
2950        return None;
2951    }
2952
2953    Some(crate::ir::ProofStrategy::WrapperOverRecursion {
2954        wrapper_fn,
2955        inner_fn,
2956        other_fn,
2957        combine_op,
2958    })
2959}
2960
2961fn detect_induction_target(
2962    law: &crate::ast::VerifyLaw,
2963    inputs: &ProofLowerInputs,
2964) -> Option<String> {
2965    // Stage 7 of #232: read the eligibility set from `ProgramShape`
2966    // when threaded through `ProofLowerInputs`. The shape pass
2967    // computes the same direct-rec + not-indirect-rec predicate
2968    // once per compilation; the inline scan below is the
2969    // pre-shape fallback so callers without a shape (legacy test
2970    // fixtures, tools that build `ProofLowerInputs` by hand) keep
2971    // working unchanged.
2972    if let Some(shape) = inputs.program_shape {
2973        for given in &law.givens {
2974            if shape.inductable_sum_types.contains(&given.type_name) {
2975                return Some(given.name.clone());
2976            }
2977        }
2978        // Builtin `List<T>` given — structural induction via nil/cons (#409).
2979        // Reached only AFTER the other strategy detectors in
2980        // `populate_law_theorems` have declined this law, so a law with a
2981        // working bespoke strategy (e.g. quicksort's ordering/idempotent) keeps
2982        // it; only laws nobody else classified fall through to list induction.
2983        if let Some(given) = list_induction_given(law) {
2984            return Some(given);
2985        }
2986        return None;
2987    }
2988    detect_induction_target_legacy(law, inputs)
2989}
2990
2991/// A `given` whose declared type is a builtin `List<T>` — the structural
2992/// induction target name, if any.
2993fn list_induction_given(law: &crate::ast::VerifyLaw) -> Option<String> {
2994    law.givens
2995        .iter()
2996        .find(|g| g.type_name.trim().starts_with("List<"))
2997        .map(|g| g.name.clone())
2998}
2999
3000fn detect_induction_target_legacy(
3001    law: &crate::ast::VerifyLaw,
3002    inputs: &ProofLowerInputs,
3003) -> Option<String> {
3004    use crate::ast::TypeDef;
3005    for given in &law.givens {
3006        let Some(TypeDef::Sum {
3007            name: type_name,
3008            variants,
3009            ..
3010        }) = inputs.find_type_def(&given.type_name)
3011        else {
3012            continue;
3013        };
3014        let direct_rec = variants.iter().any(|variant| {
3015            variant.fields.iter().any(|field| {
3016                let f = field.trim();
3017                f == type_name
3018                    || f.contains(&format!("<{}", type_name))
3019                    || f.contains(&format!("{}>", type_name))
3020                    || f.contains(&format!(", {}", type_name))
3021                    || f.contains(&format!("{},", type_name))
3022            })
3023        });
3024        if !direct_rec {
3025            continue;
3026        }
3027        if has_indirect_rec_variants(variants, type_name) {
3028            continue;
3029        }
3030        return Some(given.name.clone());
3031    }
3032    list_induction_given(law)
3033}
3034
3035/// Mirror of `lean::law_auto::induction::has_indirect_variants` —
3036/// when a variant's field carries the type wrapped inside another
3037/// generic in a shape the per-variant emit can't decompose
3038/// (e.g. `Some(List<Self>)` past the simple direct-rec case),
3039/// the backend rejects. Replicated here so the lowerer's pin
3040/// matches what the backend would accept.
3041fn has_indirect_rec_variants(variants: &[crate::ast::TypeVariant], type_name: &str) -> bool {
3042    for variant in variants {
3043        for field in &variant.fields {
3044            let f = field.trim();
3045            // Direct match — that's the recursion we want, not "indirect".
3046            if f == type_name {
3047                continue;
3048            }
3049            // Bare `List<Tree>` / `Vec<Tree>` is fine (direct list
3050            // recursion); deeper nesting we conservatively reject.
3051            let opens = f.matches('<').count();
3052            if opens > 1 && f.contains(type_name) {
3053                return true;
3054            }
3055        }
3056    }
3057    false
3058}
3059
3060/// Return `Some(op)` iff `fn_name` resolves to a 2-arg Int wrapper
3061/// `fn f(p1: Int, p2: Int) -> Int :- p1 <op> p2`. The op family
3062/// covers `Add` / `Mul` (commutative + associative lemmas) and
3063/// `Sub` (anti-commutative + right-identity); other ops fall back
3064/// to `BackendDispatch`.
3065fn wrapper_binop(fn_name: &str, inputs: &ProofLowerInputs) -> Option<crate::ast::BinOp> {
3066    use crate::ast::{BinOp, Expr};
3067
3068    let fd = inputs.find_fn_def_by_call_name(fn_name)?;
3069    if fd.params.len() != 2 || fd.return_type != "Int" {
3070        return None;
3071    }
3072    let (p1, t1) = &fd.params[0];
3073    let (p2, t2) = &fd.params[1];
3074    if t1 != "Int" || t2 != "Int" {
3075        return None;
3076    }
3077    let expr = body_terminal_expr(fd.body.as_ref())?;
3078    let Expr::BinOp(op, left, right) = &expr.node else {
3079        return None;
3080    };
3081    if !matches_ident_expr(left, p1) || !matches_ident_expr(right, p2) {
3082        return None;
3083    }
3084    matches!(op, BinOp::Add | BinOp::Mul | BinOp::Sub).then_some(*op)
3085}
3086
3087fn detect_wrapper_commutative(
3088    law: &crate::ast::VerifyLaw,
3089    fn_name: &str,
3090    _op: crate::ast::BinOp,
3091) -> bool {
3092    if law.givens.len() != 2 || law.givens.iter().any(|g| g.type_name != "Int") {
3093        return false;
3094    }
3095    let a = &law.givens[0].name;
3096    let b = &law.givens[1].name;
3097    matches_binary_call(&law.lhs, fn_name, a, b) && matches_binary_call(&law.rhs, fn_name, b, a)
3098        || matches_binary_call(&law.lhs, fn_name, b, a)
3099            && matches_binary_call(&law.rhs, fn_name, a, b)
3100}
3101
3102fn detect_wrapper_associative(
3103    law: &crate::ast::VerifyLaw,
3104    fn_name: &str,
3105    _op: crate::ast::BinOp,
3106) -> bool {
3107    if law.givens.len() != 3 || law.givens.iter().any(|g| g.type_name != "Int") {
3108        return false;
3109    }
3110    let a = &law.givens[0].name;
3111    let b = &law.givens[1].name;
3112    let c = &law.givens[2].name;
3113    let nested = |side| matches_assoc_nested(side, fn_name, a, b, c);
3114    let flat = |side| matches_assoc_flat(side, fn_name, a, b, c);
3115    (nested(&law.lhs) && flat(&law.rhs)) || (nested(&law.rhs) && flat(&law.lhs))
3116}
3117
3118/// Detect a unary↔binary wrapper equivalence shape:
3119/// outer side: `outer(g)` where `fn outer(p) -> p <op> K`
3120/// other side: `inner(g, K)` or `inner(K, g)` where `fn inner(a, b) -> a <op> b`
3121/// Both sides must agree on op + constant + var-position.
3122/// Returns the inner fn's source name, or `None` if no match.
3123fn detect_wrapper_unary_equivalence(
3124    law: &crate::ast::VerifyLaw,
3125    fn_name: &str,
3126    inputs: &ProofLowerInputs,
3127) -> Option<String> {
3128    if law.givens.len() != 1 || law.givens[0].type_name != "Int" {
3129        return None;
3130    }
3131    let unary = unary_int_wrapper(fn_name, inputs)?;
3132    let g = &law.givens[0].name;
3133
3134    let try_side = |call_side: &Spanned<crate::ast::Expr>,
3135                    other_side: &Spanned<crate::ast::Expr>|
3136     -> Option<String> {
3137        if !matches_unary_call(call_side, fn_name, g) {
3138            return None;
3139        }
3140        let (callee_name, var_first, lit) = binary_call_var_const(other_side, g)?;
3141        if lit != unary.constant || var_first != unary.var_first {
3142            return None;
3143        }
3144        let inner_op = wrapper_binop(&callee_name, inputs)?;
3145        if inner_op != unary.op {
3146            return None;
3147        }
3148        Some(callee_name)
3149    };
3150    try_side(&law.lhs, &law.rhs).or_else(|| try_side(&law.rhs, &law.lhs))
3151}
3152
3153#[derive(Debug, Clone, Copy)]
3154struct UnaryIntWrapper {
3155    op: crate::ast::BinOp,
3156    constant: i64,
3157    var_first: bool,
3158}
3159
3160/// Resolve `fn outer(p: Int) -> Int :- p <op> K` or `K <op> p`.
3161/// Returns the op + literal + which side carries the param.
3162fn unary_int_wrapper(fn_name: &str, inputs: &ProofLowerInputs) -> Option<UnaryIntWrapper> {
3163    use crate::ast::{Expr, Literal};
3164
3165    let fd = inputs.find_fn_def_by_call_name(fn_name)?;
3166    if fd.params.len() != 1 || fd.return_type != "Int" {
3167        return None;
3168    }
3169    let (param, param_ty) = &fd.params[0];
3170    if param_ty != "Int" {
3171        return None;
3172    }
3173    let expr = body_terminal_expr(fd.body.as_ref())?;
3174    let Expr::BinOp(op, left, right) = &expr.node else {
3175        return None;
3176    };
3177    let lit_of = |e: &Spanned<Expr>| -> Option<i64> {
3178        match &e.node {
3179            Expr::Literal(Literal::Int(n)) => Some(*n),
3180            _ => None,
3181        }
3182    };
3183    if matches_ident_expr(left, param) {
3184        let n = lit_of(right)?;
3185        return Some(UnaryIntWrapper {
3186            op: *op,
3187            constant: n,
3188            var_first: true,
3189        });
3190    }
3191    if matches_ident_expr(right, param) {
3192        let n = lit_of(left)?;
3193        return Some(UnaryIntWrapper {
3194            op: *op,
3195            constant: n,
3196            var_first: false,
3197        });
3198    }
3199    None
3200}
3201
3202fn matches_unary_call(expr: &Spanned<crate::ast::Expr>, fn_name: &str, arg: &str) -> bool {
3203    use crate::ast::Expr;
3204    let Expr::FnCall(callee, args) = &expr.node else {
3205        return false;
3206    };
3207    args.len() == 1 && callee_matches_name(callee, fn_name) && matches_ident_expr(&args[0], arg)
3208}
3209
3210/// `inner(var, K)` or `inner(K, var)` shape. Returns
3211/// `(callee_name, var_first, K)` on match.
3212/// **syntax-discovery-only** (epic #170 Phase 7). Detects a
3213/// 2-arg fn call whose first arg is `Ident(var_name)` and second
3214/// is an `Int` literal (or symmetric). Returns the callee's dotted
3215/// name + the literal — caller dispatches on the name string. Used
3216/// only against builtin namespace methods (`Int.add`, etc.); the
3217/// returned name string is not stored or used for identity past
3218/// the dispatch site.
3219fn binary_call_var_const(
3220    expr: &Spanned<crate::ast::Expr>,
3221    var_name: &str,
3222) -> Option<(String, bool, i64)> {
3223    use crate::ast::{Expr, Literal};
3224    let Expr::FnCall(callee, args) = &expr.node else {
3225        return None;
3226    };
3227    if args.len() != 2 {
3228        return None;
3229    }
3230    let callee_name = expr_to_dotted_name(&callee.node)?;
3231    match (&args[0].node, &args[1].node) {
3232        (Expr::Ident(v) | Expr::Resolved { name: v, .. }, Expr::Literal(Literal::Int(n)))
3233            if v == var_name =>
3234        {
3235            Some((callee_name, true, *n))
3236        }
3237        (Expr::Literal(Literal::Int(n)), Expr::Ident(v) | Expr::Resolved { name: v, .. })
3238            if v == var_name =>
3239        {
3240            Some((callee_name, false, *n))
3241        }
3242        _ => None,
3243    }
3244}
3245
3246fn detect_wrapper_sub_right_identity(law: &crate::ast::VerifyLaw, fn_name: &str) -> bool {
3247    if law.givens.len() != 1 || law.givens[0].type_name != "Int" {
3248        return false;
3249    }
3250    let g = &law.givens[0].name;
3251    matches_sub_right_identity_side(&law.lhs, &law.rhs, fn_name, g)
3252        || matches_sub_right_identity_side(&law.rhs, &law.lhs, fn_name, g)
3253}
3254
3255/// Detect `sub(a, b) => -sub(b, a)` or the swapped arrangement.
3256/// Returns `Some(neg_on_rhs)` — `true` when the negation is on the
3257/// rhs (canonical direction); `false` when swapped (call on rhs,
3258/// negation on lhs). `None` when the shape doesn't fit.
3259fn detect_wrapper_sub_anti_commutative(law: &crate::ast::VerifyLaw, fn_name: &str) -> Option<bool> {
3260    if law.givens.len() != 2 || law.givens.iter().any(|g| g.type_name != "Int") {
3261        return None;
3262    }
3263    let a = &law.givens[0].name;
3264    let b = &law.givens[1].name;
3265    if matches_binary_call(&law.lhs, fn_name, a, b)
3266        && matches_neg_binary_call(&law.rhs, fn_name, b, a)
3267    {
3268        return Some(true);
3269    }
3270    if matches_binary_call(&law.rhs, fn_name, a, b)
3271        && matches_neg_binary_call(&law.lhs, fn_name, b, a)
3272    {
3273        return Some(false);
3274    }
3275    None
3276}
3277
3278fn detect_wrapper_identity(
3279    law: &crate::ast::VerifyLaw,
3280    fn_name: &str,
3281    op: crate::ast::BinOp,
3282) -> bool {
3283    if law.givens.len() != 1 || law.givens[0].type_name != "Int" {
3284        return false;
3285    }
3286    let identity = match op {
3287        crate::ast::BinOp::Add => 0,
3288        crate::ast::BinOp::Mul => 1,
3289        _ => return false,
3290    };
3291    let g = &law.givens[0].name;
3292    matches_identity_side(&law.lhs, &law.rhs, fn_name, g, identity)
3293        || matches_identity_side(&law.rhs, &law.lhs, fn_name, g, identity)
3294}
3295
3296// ── AST matchers — ported from `lean::law_auto::shared` ─────────
3297//
3298// Kept private to proof_lower to preserve layering (proof_lower
3299// must not reach into lean codegen). The shapes are backend-neutral
3300// — pure AST pattern matching — so the duplication is local-cost
3301// only. A future cleanup could consolidate these into a shared
3302// `codegen::ast_match` module.
3303
3304fn body_terminal_expr(body: &crate::ast::FnBody) -> Option<&Spanned<crate::ast::Expr>> {
3305    use crate::ast::Stmt;
3306    match body.stmts() {
3307        [Stmt::Expr(expr)] => Some(expr),
3308        _ => None,
3309    }
3310}
3311
3312/// Fold the algebraic identities that survive a body-substitution
3313/// pass with a literal-int arg: `a + 0 == a`, `0 + a == a`,
3314/// `a - 0 == a`, `a * 1 == a`, `1 * a == a`, `a * 0 == 0`,
3315/// `0 * a == 0`. Recursive over BinOp / Neg / Attr / FnCall. Used
3316/// by `detect_spec_equivalence` so impl bodies like `n + 0` and
3317/// spec bodies like `n` are recognised as equivalent under arg
3318/// substitution. Matches the legacy `simp_normalized` shape.
3319fn simplify_identity_expr(expr: &Spanned<crate::ast::Expr>) -> Spanned<crate::ast::Expr> {
3320    use crate::ast::{BinOp, Expr, Literal};
3321    let line = expr.line;
3322    let int_lit = |e: &Expr| -> Option<i64> {
3323        match e {
3324            Expr::Literal(Literal::Int(n)) => Some(*n),
3325            _ => None,
3326        }
3327    };
3328    let new_node = match &expr.node {
3329        Expr::BinOp(op, left, right) => {
3330            let left = simplify_identity_expr(left);
3331            let right = simplify_identity_expr(right);
3332            match op {
3333                BinOp::Add => {
3334                    if int_lit(&left.node) == Some(0) {
3335                        return right;
3336                    } else if int_lit(&right.node) == Some(0) {
3337                        return left;
3338                    } else {
3339                        Expr::BinOp(*op, Box::new(left), Box::new(right))
3340                    }
3341                }
3342                BinOp::Sub => {
3343                    if int_lit(&right.node) == Some(0) {
3344                        return left;
3345                    } else {
3346                        Expr::BinOp(*op, Box::new(left), Box::new(right))
3347                    }
3348                }
3349                BinOp::Mul => {
3350                    if int_lit(&left.node) == Some(0) || int_lit(&right.node) == Some(0) {
3351                        Expr::Literal(Literal::Int(0))
3352                    } else if int_lit(&left.node) == Some(1) {
3353                        return right;
3354                    } else if int_lit(&right.node) == Some(1) {
3355                        return left;
3356                    } else {
3357                        Expr::BinOp(*op, Box::new(left), Box::new(right))
3358                    }
3359                }
3360                _ => Expr::BinOp(*op, Box::new(left), Box::new(right)),
3361            }
3362        }
3363        Expr::Neg(inner) => Expr::Neg(Box::new(simplify_identity_expr(inner))),
3364        Expr::Attr(base, field) => {
3365            Expr::Attr(Box::new(simplify_identity_expr(base)), field.clone())
3366        }
3367        Expr::FnCall(callee, args) => Expr::FnCall(
3368            Box::new(simplify_identity_expr(callee)),
3369            args.iter().map(simplify_identity_expr).collect(),
3370        ),
3371        Expr::Match { subject, arms } => Expr::Match {
3372            subject: Box::new(simplify_identity_expr(subject)),
3373            arms: arms
3374                .iter()
3375                .map(|arm| crate::ast::MatchArm {
3376                    pattern: arm.pattern.clone(),
3377                    body: Box::new(simplify_identity_expr(&arm.body)),
3378                    binding_slots: arm.binding_slots.clone(),
3379                })
3380                .collect(),
3381        },
3382        other => other.clone(),
3383    };
3384    Spanned::new(new_node, line)
3385}
3386
3387/// **syntax-discovery-only** (epic #170 Phase 7). Returns true iff
3388/// `expr` is a bare `Ident(name)` / `Resolved { name }` matching
3389/// the given name. Used by shape detectors to recognise "the
3390/// callsite mentions this binder" — no identity lookup, just
3391/// source-name shape.
3392fn matches_ident_expr(expr: &Spanned<crate::ast::Expr>, name: &str) -> bool {
3393    use crate::ast::Expr;
3394    matches!(&expr.node, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == name)
3395}
3396
3397/// **syntax-discovery-only** (epic #170 Phase 7). Compares the
3398/// callee's dotted source name against a target string for shape
3399/// recognition. EXACT MATCH ONLY — the previous suffix-match clause
3400/// (`name.rsplit('.').next() == Some(target)`) was an identity leak:
3401/// it accepted `Foo.helper` as a match for target `"helper"`. Bounded
3402/// today by the parser invariant (`vb.fn_name` is bare entry-only),
3403/// but the suffix clause stayed live as dead code that any future
3404/// module-scoped verify would silently exercise.
3405fn callee_matches_name(expr: &Spanned<crate::ast::Expr>, target: &str) -> bool {
3406    let Some(name) = expr_to_dotted_name(&expr.node) else {
3407        return false;
3408    };
3409    name == target
3410}
3411
3412fn call2_args<'a>(
3413    expr: &'a Spanned<crate::ast::Expr>,
3414    fn_name: &str,
3415) -> Option<(&'a Spanned<crate::ast::Expr>, &'a Spanned<crate::ast::Expr>)> {
3416    use crate::ast::Expr;
3417    let Expr::FnCall(callee, args) = &expr.node else {
3418        return None;
3419    };
3420    if args.len() != 2 || !callee_matches_name(callee, fn_name) {
3421        return None;
3422    }
3423    Some((&args[0], &args[1]))
3424}
3425
3426fn matches_binary_call(expr: &Spanned<crate::ast::Expr>, fn_name: &str, a: &str, b: &str) -> bool {
3427    let Some((x, y)) = call2_args(expr, fn_name) else {
3428        return false;
3429    };
3430    matches_ident_expr(x, a) && matches_ident_expr(y, b)
3431}
3432
3433fn matches_assoc_nested(
3434    expr: &Spanned<crate::ast::Expr>,
3435    fn_name: &str,
3436    a: &str,
3437    b: &str,
3438    c: &str,
3439) -> bool {
3440    let Some((ab, z)) = call2_args(expr, fn_name) else {
3441        return false;
3442    };
3443    let Some((x, y)) = call2_args(ab, fn_name) else {
3444        return false;
3445    };
3446    matches_ident_expr(x, a) && matches_ident_expr(y, b) && matches_ident_expr(z, c)
3447}
3448
3449fn matches_assoc_flat(
3450    expr: &Spanned<crate::ast::Expr>,
3451    fn_name: &str,
3452    a: &str,
3453    b: &str,
3454    c: &str,
3455) -> bool {
3456    let Some((x, bc)) = call2_args(expr, fn_name) else {
3457        return false;
3458    };
3459    let Some((y, z)) = call2_args(bc, fn_name) else {
3460        return false;
3461    };
3462    matches_ident_expr(x, a) && matches_ident_expr(y, b) && matches_ident_expr(z, c)
3463}
3464
3465fn matches_sub_right_identity_side(
3466    call_side: &Spanned<crate::ast::Expr>,
3467    ident_side: &Spanned<crate::ast::Expr>,
3468    fn_name: &str,
3469    given_name: &str,
3470) -> bool {
3471    use crate::ast::{Expr, Literal};
3472    if !matches_ident_expr(ident_side, given_name) {
3473        return false;
3474    }
3475    let Some((x, y)) = call2_args(call_side, fn_name) else {
3476        return false;
3477    };
3478    matches_ident_expr(x, given_name)
3479        && matches!(&y.node, Expr::Literal(Literal::Int(n)) if *n == 0)
3480}
3481
3482fn matches_neg_binary_call(
3483    expr: &Spanned<crate::ast::Expr>,
3484    fn_name: &str,
3485    a: &str,
3486    b: &str,
3487) -> bool {
3488    use crate::ast::Expr;
3489    match &expr.node {
3490        Expr::Neg(inner) => matches_binary_call(inner, fn_name, a, b),
3491        _ => false,
3492    }
3493}
3494
3495fn matches_identity_side(
3496    call_side: &Spanned<crate::ast::Expr>,
3497    ident_side: &Spanned<crate::ast::Expr>,
3498    fn_name: &str,
3499    given_name: &str,
3500    identity: i64,
3501) -> bool {
3502    use crate::ast::{Expr, Literal};
3503    if !matches_ident_expr(ident_side, given_name) {
3504        return false;
3505    }
3506    let Some((x, y)) = call2_args(call_side, fn_name) else {
3507        return false;
3508    };
3509    let is_int_lit = |e: &Spanned<Expr>, n: i64| -> bool {
3510        matches!(&e.node, Expr::Literal(Literal::Int(m)) if *m == n)
3511    };
3512    (matches_ident_expr(x, given_name) && is_int_lit(y, identity))
3513        || (is_int_lit(x, identity) && matches_ident_expr(y, given_name))
3514}
3515
3516/// Pick an inhabitation witness: a literal value of the carrier type
3517/// that satisfies the refinement predicate. Backend-neutral output —
3518/// Dafny consumes it as `witness <W>`, Lean may later use it for a
3519/// `sample_X` helper. First tries the smart constructor's verify-
3520/// block samples (entry-module only — `ModuleInfo` doesn't surface
3521/// verify blocks); falls back to evaluating the predicate against
3522/// `[0, 1, -1]` and returning the first satisfier.
3523fn pick_witness(
3524    type_name: &str,
3525    type_id: crate::ir::TypeId,
3526    inputs: &ProofLowerInputs,
3527    predicate: &Spanned<Expr>,
3528    param_name: &str,
3529    scope: Option<&str>,
3530) -> Option<String> {
3531    // Smart-constructor + verify-block walk, scoped to the same
3532    // module the type lives in. Refinement-via-opaque keeps record
3533    // and constructor in the same module (the carrier field is
3534    // opaque from outside), so this mirrors that constraint. Without
3535    // the scope a module-B `Natural` with predicate `n >= 10` would
3536    // pick up entry's `fromInt(0)` verify case, silently breaking
3537    // the witness invariant.
3538    let smart_ctor_name: Option<String> = match scope {
3539        None => inputs.entry_items.iter().find_map(|item| match item {
3540            TopLevel::FnDef(fd)
3541                if smart_ctor_matches(fd, type_id, type_name, inputs.symbol_table, scope) =>
3542            {
3543                Some(fd.name.clone())
3544            }
3545            _ => None,
3546        }),
3547        Some(prefix) => inputs
3548            .dep_modules
3549            .iter()
3550            .find(|m| m.prefix == prefix)
3551            .and_then(|m| {
3552                m.fn_defs
3553                    .iter()
3554                    .find(|fd| {
3555                        smart_ctor_matches(fd, type_id, type_name, inputs.symbol_table, scope)
3556                    })
3557                    .map(|fd| fd.name.clone())
3558            }),
3559    };
3560    if let Some(smart_ctor_name) = smart_ctor_name {
3561        // Verify blocks live on `inputs.entry_items` only — `ModuleInfo`
3562        // doesn't surface verify cases. Scoped verify-block walk would
3563        // need a separate plumb that's not in `ProofLowerInputs` today.
3564        // Restrict the verify-sample fast path to entry scope; module
3565        // scopes fall through to the predicate-evaluation fallback,
3566        // which now sweeps a wider range so non-trivial predicates
3567        // (`n >= 10`) still get a real witness.
3568        if scope.is_none() {
3569            for item in inputs.entry_items {
3570                let TopLevel::Verify(vb) = item else {
3571                    continue;
3572                };
3573                if vb.fn_name != smart_ctor_name {
3574                    continue;
3575                }
3576                for (lhs, rhs) in &vb.cases {
3577                    if !is_result_ok(&rhs.node) {
3578                        continue;
3579                    }
3580                    let Expr::FnCall(_, args) = &lhs.node else {
3581                        continue;
3582                    };
3583                    if args.len() != 1 {
3584                        continue;
3585                    }
3586                    if let Some(lit) = literal_int_value(&args[0]) {
3587                        return Some(lit);
3588                    }
3589                }
3590            }
3591        }
3592    }
3593    // Predicate-evaluation. Two-stage: first sweep candidates
3594    // *extracted from the predicate AST itself* (any `Literal::Int`
3595    // reachable through `BinOp` / `FnCall` chains, plus the
3596    // neighbours `K`, `K-1`, `K+1` so `n == 5` / `n > 5` / `n < 5`
3597    // all land their witness). Then fall back to a fixed magnitude
3598    // sweep for shapes that don't mention concrete numbers
3599    // (`n != 0`, `Bool.and(n >= 0, n <= max())`, …). Returning
3600    // `None` here is intentional and `populate_refined_types`
3601    // skips the slot — the alternative was Dafny silently emitting
3602    // `witness 0` against a predicate the witness didn't satisfy.
3603    let mut tried = std::collections::HashSet::<i64>::new();
3604    let mut candidates: Vec<i64> = Vec::new();
3605    let mut from_ast: Vec<i64> = Vec::new();
3606    collect_int_literals(predicate, &mut from_ast);
3607    for k in from_ast {
3608        for delta in &[0_i64, 1, -1] {
3609            if let Some(c) = k.checked_add(*delta) {
3610                candidates.push(c);
3611            }
3612        }
3613    }
3614    candidates.extend_from_slice(&[
3615        0, 1, -1, 2, -2, 10, -10, 100, 1_000, 10_000, 100_000, 1_000_000,
3616    ]);
3617    for candidate in candidates {
3618        if !tried.insert(candidate) {
3619            continue;
3620        }
3621        if eval_int_bool_predicate(predicate, param_name, candidate) == Some(true) {
3622            return Some(candidate.to_string());
3623        }
3624    }
3625    None
3626}
3627
3628fn collect_int_literals(expr: &Spanned<Expr>, out: &mut Vec<i64>) {
3629    match &expr.node {
3630        Expr::Literal(Literal::Int(n)) => out.push(*n),
3631        Expr::Neg(inner) => {
3632            if let Expr::Literal(Literal::Int(n)) = &inner.node {
3633                out.push(-n);
3634            } else {
3635                collect_int_literals(inner, out);
3636            }
3637        }
3638        Expr::BinOp(_, l, r) => {
3639            collect_int_literals(l, out);
3640            collect_int_literals(r, out);
3641        }
3642        Expr::FnCall(callee, args) => {
3643            collect_int_literals(callee, out);
3644            for a in args {
3645                collect_int_literals(a, out);
3646            }
3647        }
3648        Expr::Match { subject, arms } => {
3649            collect_int_literals(subject, out);
3650            for arm in arms {
3651                collect_int_literals(&arm.body, out);
3652            }
3653        }
3654        Expr::Attr(o, _) | Expr::ErrorProp(o) => collect_int_literals(o, out),
3655        _ => {}
3656    }
3657}
3658
3659/// Does `fd` look like a smart constructor for `type_id` (1-param
3660/// fn whose return type is `Result<T, _>` where `T` is the
3661/// refined nominal)?
3662///
3663/// Epic #180 Phase 6 follow-up — id-aware comparison. The raw
3664/// `name`-equality path used to silently accept a smart ctor
3665/// from module B whose return type was `Result<A.Natural, _>`
3666/// when looking for module B's own `Natural`, because the
3667/// inner Named's bare `name` field was just `"Natural"` and
3668/// both shapes match on string. Resolving the inner Named
3669/// through the symbol table within the current scope hands us
3670/// the actual `TypeId` and lets us compare opaque identities.
3671///
3672/// `type_name` stays in the signature as the fallback for
3673/// builtins / unresolved refs the symbol table doesn't index
3674/// (matches the [`crate::codegen::common::backend_named_type_key`]
3675/// fallback semantics).
3676fn smart_ctor_matches(
3677    fd: &FnDef,
3678    type_id: crate::ir::TypeId,
3679    type_name: &str,
3680    symbols: &crate::ir::SymbolTable,
3681    scope: Option<&str>,
3682) -> bool {
3683    if fd.params.len() != 1 {
3684        return false;
3685    }
3686    let parsed = crate::types::parse_type_str(&fd.return_type);
3687    let crate::types::Type::Result(ok, _) = parsed else {
3688        return false;
3689    };
3690    // syntax-discovery-only: pulls the inner Named's `name` out
3691    // of the raw `parse_type_str` parse to feed into the
3692    // symbol-table resolution below. The identity decision is
3693    // the `TypeId` comparison built from that name plus scope;
3694    // the bare `name` is just an intermediate handle.
3695    let crate::types::Type::Named { name: n, .. } = &*ok else {
3696        return false;
3697    };
3698    // Prefer id-direct: resolve the inner Named's name against
3699    // the symbol table within the current scope, then compare
3700    // `TypeId`s. Identity-safe across cross-module same-bare-name
3701    // twins by construction.
3702    let name_is_qualified = n.contains('.');
3703    let resolved_id = if name_is_qualified {
3704        n.rsplit_once('.').and_then(|(prefix, bare)| {
3705            symbols.type_id_of(&crate::ir::TypeKey::in_module(prefix.to_string(), bare))
3706        })
3707    } else if let Some(prefix) = scope {
3708        symbols
3709            .type_id_of(&crate::ir::TypeKey::in_module(
3710                prefix.to_string(),
3711                n.clone(),
3712            ))
3713            .or_else(|| symbols.type_id_of(&crate::ir::TypeKey::entry(n.clone())))
3714    } else {
3715        symbols.type_id_of(&crate::ir::TypeKey::entry(n.clone()))
3716    };
3717    match resolved_id {
3718        Some(id) => id == type_id,
3719        None => n == type_name,
3720    }
3721}
3722
3723fn is_result_ok(expr: &Expr) -> bool {
3724    match expr {
3725        Expr::Constructor(name, _) => name == "Result.Ok",
3726        Expr::FnCall(callee, _) => matches!(
3727            &callee.node,
3728            Expr::Attr(obj, field)
3729                if field == "Ok" && matches!(&obj.node, Expr::Ident(n) if n == "Result")
3730        ),
3731        _ => false,
3732    }
3733}
3734
3735fn literal_int_value(expr: &Spanned<Expr>) -> Option<String> {
3736    match &expr.node {
3737        Expr::Literal(Literal::Int(n)) => Some(n.to_string()),
3738        Expr::Neg(inner) => {
3739            let inner_str = literal_int_value(inner)?;
3740            Some(format!("-{inner_str}"))
3741        }
3742        _ => None,
3743    }
3744}
3745
3746fn eval_int_bool_predicate(expr: &Spanned<Expr>, param_name: &str, value: i64) -> Option<bool> {
3747    match &expr.node {
3748        Expr::Literal(Literal::Bool(b)) => Some(*b),
3749        Expr::BinOp(op, l, r) => {
3750            use crate::ast::BinOp::*;
3751            let li = eval_int_arith(l, param_name, value)?;
3752            let ri = eval_int_arith(r, param_name, value)?;
3753            Some(match op {
3754                Lt => li < ri,
3755                Gt => li > ri,
3756                Lte => li <= ri,
3757                Gte => li >= ri,
3758                Eq => li == ri,
3759                Neq => li != ri,
3760                _ => return None,
3761            })
3762        }
3763        Expr::FnCall(callee, args) if args.len() == 2 => {
3764            let name = expr_to_dotted_name(&callee.node)?;
3765            match name.as_str() {
3766                "Bool.and" => Some(
3767                    eval_int_bool_predicate(&args[0], param_name, value)?
3768                        && eval_int_bool_predicate(&args[1], param_name, value)?,
3769                ),
3770                "Bool.or" => Some(
3771                    eval_int_bool_predicate(&args[0], param_name, value)?
3772                        || eval_int_bool_predicate(&args[1], param_name, value)?,
3773                ),
3774                _ => None,
3775            }
3776        }
3777        _ => None,
3778    }
3779}
3780
3781fn eval_int_arith(expr: &Spanned<Expr>, param_name: &str, value: i64) -> Option<i64> {
3782    match &expr.node {
3783        Expr::Literal(Literal::Int(n)) => Some(*n),
3784        Expr::Ident(name) | Expr::Resolved { name, .. } if name == param_name => Some(value),
3785        Expr::BinOp(op, l, r) => {
3786            use crate::ast::BinOp::*;
3787            let li = eval_int_arith(l, param_name, value)?;
3788            let ri = eval_int_arith(r, param_name, value)?;
3789            match op {
3790                Add => Some(li.checked_add(ri)?),
3791                Sub => Some(li.checked_sub(ri)?),
3792                Mul => Some(li.checked_mul(ri)?),
3793                Div => Some(li.checked_div(ri)?),
3794                _ => None,
3795            }
3796        }
3797        Expr::Neg(inner) => Some(-eval_int_arith(inner, param_name, value)?),
3798        _ => None,
3799    }
3800}