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