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