Skip to main content

aver/ir/
proof_ir.rs

1//! Proof intermediate representation.
2//!
3//! Single decision substrate the Lean and Dafny proof exporters
4//! consume. Backends render text from a fully-resolved `ProofIR` —
5//! they do not classify shapes, do not derive contracts, do not
6//! decide between native and fuel emit. Every decision happens once
7//! in the `proof_lower` pipeline stage; both backends see the same
8//! decision and either render it consistently or fail consistently.
9//!
10//! Replaces the ad-hoc "guess and emit" pattern that grew across
11//! `src/codegen/{common,recursion,lean,dafny}` during 0.22.0 with a
12//! single typed model. Each variant that says "emit native" or
13//! "lift to subtype" carries inside its payload everything the
14//! backend needs and everything the classifier proved — the type
15//! system makes it impossible to produce a "native" decision
16//! without also producing the side-conditions that justify it.
17//!
18//! Coverage today: `refined_types` (refinement-via-opaque records),
19//! `fn_contracts` (per-pure-fn recursion shape), `law_theorems`
20//! (per-verify-law strategy + quantifier + claim decomposition).
21//!
22//! ## Invariant after #147 phase E PR 12 Scope A
23//!
24//! - **Backend-facing IR fields are resolved.** Every
25//!   `Spanned<...>` on this module's public structs carries
26//!   [`crate::ir::hir::ResolvedExpr`], not raw `ast::Expr`.
27//!   `proof_lower::populate_*` resolves at the producer site through
28//!   the symbol table under the correct scope (entry / dep-module
29//!   prefix); backends walk the resolved form directly via
30//!   `emit_expr`, no `emit_expr_legacy` adapter for IR-sourced
31//!   expressions.
32//! - **Identity-sensitive decisions use typed IDs.**
33//!   `fn_contracts` is keyed by [`FnId`] (not bare name);
34//!   `refined_types` is keyed by [`TypeId`]; `law_theorems` carry
35//!   the target fn's `FnId`. The Lean/Dafny native-guarded rewriter
36//!   pins target by `FnId` (via `fn_id_for_decl`), not bare name —
37//!   regression-pinned by
38//!   `proof_export_module_owned_native_guarded_resolves_correct_fn_id`.
39//! - **`proof_lower` internal AST discovery is intentional.**
40//!   The classifier's pattern matchers still walk raw `ast::Expr`
41//!   inside this stage. Source-pattern matching is the natural
42//!   shape for the discovery work (refinement carrier search,
43//!   `Map.set` axiom detection, spec-equivalence comparison); a
44//!   resolved walker would be the same logic spelled in a different
45//!   enum. Identity-sensitive sites that COULD leak across scope
46//!   were audited and are bounded today by:
47//!     1. `vb.fn_name` parses as a single `Ident` (verify blocks
48//!        target entry-only fns by current grammar)
49//!     2. Builtin matchers (`"Bool.and"`, `"Map.set"`, …) compare
50//!        global namespace methods that have no per-scope identity.
51//! - **Full `ResolvedProofLowerView` + semantic matcher API is
52//!   deferred** until a real trigger lands: module-scoped verify
53//!   blocks, dotted verify targets / laws over dep-module fns, LSP
54//!   rename, inliner / monomorphizer / cross-scope transforms. Each
55//!   of those unbounds the "entry-only" assumption above. When it
56//!   ships, the right architecture is a typed
57//!   `ProofLowerInputs::resolved_fn_view(fd)` + matcher helpers
58//!   (`callee_is_builtin`, `callee_is_fn(fn_id)`, `ctor_is`,
59//!   `ident_name`, `int_lit`) — not a mechanical
60//!   `Expr -> ResolvedExpr` rewrite of the discovery walkers.
61
62use std::collections::HashMap;
63
64use crate::ast::Spanned;
65
66// Identity keys for named declarations crossing module boundaries
67// (`FnKey`, `TypeKey`, `LawKey`) live in `crate::ir::identity` —
68// they're general identity primitives, not proof-specific. Today's
69// proof flow is where the bare-string bug class first surfaced
70// (reviewer rounds 5/6), but other backends with multi-module emit
71// (VM, Rust codegen, WASM) will reach for the same types when they
72// hit the same bug class. Re-exported here for ergonomics.
73pub use crate::ir::identity::{CtorId, FnId, FnKey, LawKey, ModuleId, TypeId, TypeKey};
74
75/// Output of the `proof_lower` pipeline stage. Every decision the
76/// proof backends will make is materialised here; backends become
77/// pure renderers.
78///
79/// `ProofIR` is intentionally NOT a closed superset of the AST — it
80/// only carries facts that proof export needs. Source-faithful
81/// emission of plain fns / verify cases still flows through the
82/// untyped AST path, same as runtime backends (VM, Rust, WASM).
83#[derive(Debug, Clone, Default)]
84pub struct ProofIR {
85    /// Every refinement-lifted user type, keyed by opaque [`TypeId`]
86    /// from the symbol table. Same-bare-name refined records in two
87    /// modules (`A.Natural` vs `B.Natural`) get distinct IDs, so
88    /// their predicates never merge. Includes types declared in the
89    /// entry items and in dependent modules; name resolution happens
90    /// once in `populate_refined_types`, consumers look up directly
91    /// by id through `ctx.symbol_table`.
92    pub refined_types: HashMap<TypeId, RefinedTypeDecl>,
93    /// Per-pure-fn contract describing what proof artifact the fn
94    /// lowers to (native / fuel / structural / linear recurrence).
95    /// Keyed by opaque [`FnId`] from the symbol table — name
96    /// resolution happens once in `populate_fn_contracts`;
97    /// consumers thereafter use `ctx.symbol_table` to resolve
98    /// `&FnDef → FnId` and look up directly. Cross-module
99    /// same-bare-name fns get distinct IDs, so the lookup is
100    /// unambiguous without per-call-site scope plumbing.
101    pub fn_contracts: HashMap<FnId, FnContract>,
102    /// Per-verify-law theorem decomposed into quantifiers, premises,
103    /// and claim with all wrapper-strip / val-projection / drop-vs-
104    /// keep decisions baked in, plus the pinned proof strategy.
105    pub law_theorems: Vec<LawTheorem>,
106    /// Recursive pure fns whose shape fell outside every recognised
107    /// pattern. Surfaced as diagnostics ("recursive function 'foo'
108    /// is outside proof subset (...)") and steers the consumer to
109    /// either skip the fn or emit it as a partial/axiom fallback.
110    /// Carried in ProofIR so consumers don't re-run the classifier
111    /// just to see what failed.
112    pub unclassified_fns: Vec<UnclassifiedFn>,
113}
114
115/// A recursive pure fn the contract classifier couldn't match against
116/// any supported shape. Carries the source line + a human-readable
117/// reason string so backends can render a diagnostic without
118/// inventing prose.
119#[derive(Debug, Clone, Eq, PartialEq)]
120pub struct UnclassifiedFn {
121    pub line: usize,
122    pub message: String,
123}
124
125/// Refinement smart-constructor guard a `SimpOmegaUnfold` strategy
126/// found in the law's fn unfold chain. `param` is the smart
127/// constructor's input parameter name; `predicate` is the Bool
128/// subject of its `match true/false → Ok/Err` body. Backends emit
129/// `by_cases h_<v> : <substituted predicate>` for each law-given by
130/// rewriting `param` to `<v>` inside the predicate.
131#[derive(Debug, Clone)]
132pub struct SmartGuard {
133    pub param: String,
134    pub predicate: Spanned<crate::ir::hir::ResolvedExpr>,
135}
136
137/// A refinement-lifted user type — opaque record with a single
138/// carrier field, paired with a validating smart constructor. The
139/// presence of this decl in `ProofIR.refined_types` is the
140/// decision: "emit this as a subtype on Lean and a subset type on
141/// Dafny". Backends never re-decide.
142#[derive(Debug, Clone)]
143pub struct RefinedTypeDecl {
144    /// Source-level type name (e.g. `"Natural"`). NOT canonicalised
145    /// — backends emit using the source name; canonical form is the
146    /// map key.
147    pub name: String,
148    /// Carrier annotation from the record's single field (typically
149    /// `"Int"`). Drives the Lean Subtype underlying type and the
150    /// Dafny subset type's base.
151    pub carrier_type: String,
152    /// Carrier-field source name (e.g. `"value"`). Lean uses `.val`
153    /// to project Subtype values regardless of source name; Dafny's
154    /// subset binds the source name in its predicate.
155    pub carrier_field: String,
156    /// Smart constructor's input parameter name (e.g. `"n"`) — the
157    /// invariant predicate's free variable.
158    pub predicate_param: String,
159    /// Bool predicate that every value of the refined type must
160    /// satisfy, in terms of `predicate_param`. Comes from the smart
161    /// constructor's `match <pred> { true -> Ok(...); false -> Err(...)
162    /// }` subject.
163    pub invariant: Predicate,
164    /// Inhabitation witness: a literal value of `carrier_type` that
165    /// the lowerer verified satisfies `invariant`. Resolved by first
166    /// trying the smart constructor's verify block (`fromX(K) =>
167    /// Ok(...)` for some literal K — verified by the user via
168    /// `aver verify`), then evaluating the predicate against small
169    /// candidates as a fallback.
170    ///
171    /// Why the IR carries this even though only Dafny's subset type
172    /// strictly *requires* a non-emptiness witness: it's a fact
173    /// about the type (∃ v : carrier, invariant(v) holds), not a
174    /// Dafny-specific syntactic obligation. Backends use it as they
175    /// see fit:
176    ///
177    /// - Dafny: emits `type X = v: int | P v witness <W>`. Required
178    ///   for the subset type to be inhabited and elaborable.
179    /// - Lean: currently unused — propositional `Subtype` may be
180    ///   empty, so `{ v : Int // P v }` elaborates regardless. Step
181    ///   N+1 could emit a `def sample_X : X := ⟨W, by decide⟩` for
182    ///   roundtrip / test convenience.
183    /// - Future Z3 / Coq / etc.: same fact, rendered per target.
184    ///
185    /// `None` when no satisfier was found. Backends that require a
186    /// witness must either reject the type or fall back to a target-
187    /// default (Dafny picks `0` and crosses fingers).
188    pub witness: Option<String>,
189}
190
191/// Per-pure-fn proof contract — what recursion shape (if any) the
192/// lowerer pinned for emit.
193#[derive(Debug, Clone)]
194pub struct FnContract {
195    pub source_name: String,
196    /// `None` means non-recursive (plain emit). `Some` says native /
197    /// fuel / structural / whatever the lowerer decided, with all
198    /// side-conditions inlined.
199    pub recursion: Option<RecursionContract>,
200}
201
202/// Recursion-shape decision. Each variant carries everything its
203/// emit needs AND the side-conditions the lowerer proved to choose
204/// it. The variants intentionally cannot be constructed without
205/// their side-conditions — backends cannot render `Native` without
206/// the lowerer having proved preservation + decrease.
207#[derive(Debug, Clone)]
208pub enum RecursionContract {
209    /// Fuel-encoded fallback. No side-conditions to prove; works
210    /// for any shape the classifier accepted as recursive.
211    Fuel {
212        /// Symbolic measure feeding the wrapper (`natAbs n + 1`,
213        /// `|xs| + 1`, etc.). Backends translate per target.
214        fuel_metric: FuelMetric,
215    },
216    /// Affine second-order linear recurrence on `Int`, shape
217    /// `f(n) = a*f(n-1) + b*f(n-2)` with literal `0`/`1` base cases
218    /// and an `n < 0` guard. Lowered to a private Nat pair-state
219    /// worker (Lean / Dafny both emit native structural recursion on
220    /// the Nat counter, no fuel). The lowerer doesn't carry the
221    /// shape coefficients yet — backends still pattern-match the
222    /// fn body via `lean::recurrence::detect_second_order_int_
223    /// linear_recurrence`. Step N+1 could materialise them here.
224    LinearRecurrence2,
225    /// Native recursion with explicit precondition. Lowerer proved
226    /// both `preservation` (rec args stay in domain) and `decrease`
227    /// (measure strictly drops) before constructing this variant.
228    /// Currently specialised to the IntCountdown-literal-zero shape
229    /// (`match p { 0 -> BASE; _ -> rec(p-1, ...) }`); other native-
230    /// recursion shapes (e.g. linear recurrence on a pair-state
231    /// worker) will land as additional `RecursionContract` variants.
232    Native {
233        /// Conjunction of precondition clauses, kept as a vector so
234        /// backends can render one `requires` per clause (Dafny) or
235        /// fold into a single `&&` chain (Lean). Empty means "no
236        /// caller-derived precondition" — the backend synthesises a
237        /// fibTR-style default (`param ≥ 0`) at emit time.
238        precondition: Vec<Predicate>,
239        /// Symbolic measure (e.g. `natAbs(n)`). Backends render per
240        /// target language (`Int.natAbs n` on Lean, `n` with a
241        /// `requires n >= 0` clause on Dafny).
242        measure: Measure,
243        /// Side-condition tag: lowerer attests the recursive args
244        /// preserve the precondition. Empty enum payload — its
245        /// existence in the type is the proof, not its content.
246        preservation: PreservationProof,
247        /// Same for the decreasing measure.
248        decrease: DecreaseProof,
249        /// Body decomposition for the IntCountdown-literal-zero shape:
250        /// the literal int that selects the base arm, the base arm's
251        /// body, and the wildcard arm's body. Carried so backends can
252        /// render the `if h : p = <lit> then base else rec(p-1, ...)`
253        /// switch without re-walking the source AST. The literal is
254        /// always `0` today — the `IntCountdownLiteralZero`
255        /// preservation marker attests it; carrying the value as data
256        /// keeps the IR shape forward-compatible with future
257        /// preservation proofs that admit other literals.
258        body: NativeIntCountdownBody,
259    },
260}
261
262/// Body decomposition for the `IntCountdown-literal-zero` native
263/// shape. Each field is a slice of the source AST the lowerer
264/// extracted while classifying; backends render them directly
265/// without re-walking the source.
266#[derive(Debug, Clone)]
267pub struct NativeIntCountdownBody {
268    /// The literal int that selects the base arm. Always `0` today;
269    /// future preservation proofs may admit other literals, so the
270    /// value is carried as data rather than baked into the marker.
271    pub base_arm_literal: i64,
272    /// AST for the base arm's body (`match p { 0 -> THIS; _ -> ... }`).
273    pub base_arm_body: Spanned<crate::ir::hir::ResolvedExpr>,
274    /// AST for the wildcard arm's body — the recursive call site.
275    pub wildcard_arm_body: Spanned<crate::ir::hir::ResolvedExpr>,
276}
277
278/// Fuel metric for the fallback fuel-encoded emit path.
279#[derive(Debug, Clone)]
280pub enum FuelMetric {
281    /// `n.natAbs + 1` — classic IntCountdown fuel.
282    NatAbsPlusOne { param: String },
283    /// `(bound - n).natAbs + 1` — IntAscending: param climbs toward
284    /// a bound expression. Backends render the bound through their
285    /// own `Spanned<Expr>` emitter (Lean: `bound_expr_to_lean`,
286    /// Dafny: `emit_expr` over int subset).
287    BoundMinusParamNatAbsPlusOne {
288        param: String,
289        bound: Spanned<crate::ir::hir::ResolvedExpr>,
290    },
291    /// `xs.length + 1` — List/String structural recursion.
292    SeqLenPlusOne { param: String },
293    /// `sizeOf(x) + 1` — structural recursion on a user-defined
294    /// recursive ADT (e.g. `Term::App(f, arg)`). The classifier
295    /// doesn't pin the bound param — sizeOf walks the whole call
296    /// frame — so this variant carries no param name.
297    SizeOfPlusOne,
298    /// `s.length - pos` — StringPosAdvance: a `String` carrier stays
299    /// invariant, an `Int` position climbs toward its length.
300    StringLenMinusPos {
301        string_param: String,
302        pos_param: String,
303    },
304    /// Lexicographic pair for mutual recursion SCCs.
305    Lex { params: Vec<String>, rank: usize },
306}
307
308/// Symbolic termination measure. Backend-agnostic.
309#[derive(Debug, Clone)]
310pub enum Measure {
311    NatAbsInt { param: String },
312    SeqLen { param: String },
313    Lex(Vec<Measure>),
314}
315
316/// Marker that the lowerer constructed a proof of preservation
317/// (recursive args stay in the precondition's domain). The variants
318/// describe HOW the proof was constructed so future maintainers can
319/// trace why a given shape was accepted as native.
320#[derive(Debug, Clone)]
321pub enum PreservationProof {
322    /// `match p { 0 -> base; _ -> rec(p-1, ...) }` under `p ≥ 0`
323    /// precondition. Wildcard arm gives `p ≠ 0`, combined with
324    /// `p ≥ 0` yields `p ≥ 1`, so `p - 1 ≥ 0`.
325    IntCountdownLiteralZero,
326}
327
328/// Symmetric marker for the decreasing measure.
329#[derive(Debug, Clone)]
330pub enum DecreaseProof {
331    /// `natAbs(p - 1) < natAbs(p)` under `p ≥ 0 ∧ p ≠ 0`.
332    NatAbsCountdown,
333}
334
335/// Lowered verify-law theorem. All projection decisions (`.val`
336/// vs bare ident, wrapper strip, when-keep vs when-drop) are
337/// already baked into the fields below; backends render directly.
338#[derive(Debug, Clone)]
339pub struct LawTheorem {
340    /// Opaque identity of the fn this law targets, resolved through
341    /// `SymbolTable` at populate time (phase E3). Verify laws are
342    /// entry-only per the current model, so this is effectively
343    /// always an entry-scope `FnId` today; once laws-in-modules
344    /// lands the same `FnId` will distinguish two same-bare-name
345    /// recursive fns across modules without any per-callsite scope
346    /// plumbing.
347    pub fn_id: FnId,
348    pub law_name: String,
349    pub quantifiers: Vec<Quantifier>,
350    /// Premises in order. Already includes `when` if it carries
351    /// information beyond the refinement invariants (the lowerer
352    /// performs the bijective syntactic equivalence check).
353    pub premises: Vec<Predicate>,
354    /// LHS = RHS claim. Wrapper-stripped, lifted-var-aware (bare
355    /// idents for arg positions, `.val` projections inside
356    /// comparator BinOps if the lowerer determined this is needed).
357    pub claim_lhs: Spanned<crate::ir::hir::ResolvedExpr>,
358    pub claim_rhs: Spanned<crate::ir::hir::ResolvedExpr>,
359    pub strategy: ProofStrategy,
360}
361
362/// A universally-quantified variable in a law theorem. Carries
363/// enough type info for backends to render the binder correctly
364/// (`(a : Natural)` for refined Int, `(a : Int)` for plain int,
365/// `(rng : RandomIntInBounds)` for oracle).
366#[derive(Debug, Clone)]
367pub struct Quantifier {
368    pub name: String,
369    pub binder_type: QuantifierType,
370}
371
372#[derive(Debug, Clone)]
373pub enum QuantifierType {
374    /// Plain Aver type, rendered as-is on each backend.
375    Plain(String),
376    /// Refinement-lifted: source declared `given a: Int`, body used
377    /// `Natural(value = a)`, so the quantifier binds at the refined
378    /// type. The carried `refined_type` key looks up in
379    /// `ProofIR.refined_types`.
380    RefinedTo { refined_type: String },
381    /// Oracle subtype: classified Generative-shape effect-givens
382    /// bind oracles wrapped in a subtype carrier
383    /// (`RandomIntInBounds`, `RandomFloatInUnit`,
384    /// `TimeUnixMsNonneg`).
385    OracleSubtype(String),
386}
387
388/// Algebraic / proof-theoretic shape of a verify-law theorem.
389///
390/// **Naming rule**: variants describe **what the law says**, not
391/// **how a backend proves it**. The IR is target-agnostic — Lean
392/// maps `Commutative { op: Add }` to `simp [fn, Int.add_comm]`,
393/// Dafny maps the same variant to its own lemma vocabulary, a Z3
394/// backend could ship a different tactic again. Tactic names
395/// (`SimpOverLemmas`, `simp+omega`) do not appear in variant names;
396/// `LinearArithmetic` is named for the semantic, not the tactic.
397#[derive(Debug, Clone)]
398pub enum ProofStrategy {
399    /// `rfl` / definitional equality — `lhs ≡ rhs` syntactically.
400    Reflexive,
401    /// `simp` chain over named lemmas (e.g. `[Int.add_comm,
402    /// Int.mul_comm]`). Legacy draft variant; not yet emitted by
403    /// the lowerer — kept for future use when a strategy wants to
404    /// hand the backend a specific lemma list.
405    SimpOverLemmas(Vec<String>),
406    /// `∀ a b, f(a, b) = f(b, a)` — commutativity of the law's fn,
407    /// whose body reduces to `a <op> b`. The `op` tag lets backends
408    /// pick their own lemma vocabulary (Lean: `Int.add_comm`,
409    /// Dafny: built-in arithmetic axioms).
410    Commutative { op: crate::ast::BinOp },
411    /// `∀ a b c, f(f(a,b),c) = f(a,f(b,c))` — associativity of `f`.
412    Associative { op: crate::ast::BinOp },
413    /// `∀ a, f(a, e) = a` (or the swapped `f(e, a) = a`) — the
414    /// identity-element law for the underlying op (`e` = `0` for
415    /// Add / Sub, `1` for Mul). Backends emit `simp [fn]` (the
416    /// wrapper's body unfolds to the identity equation, which simp
417    /// closes); the variant doesn't need a `side` field because
418    /// the emit is symmetric — Sub is naturally one-sided (only
419    /// right-identity), Add/Mul accept either side. The lowerer
420    /// guarantees the law's actual shape matches the op's identity
421    /// behaviour before pinning.
422    IdentityElement { op: crate::ast::BinOp },
423    /// `∀ a b, f(a, b) = -f(b, a)` (or the swapped negation).
424    /// `neg_on_rhs` records which side carries the `-` wrap so
425    /// backends with directional lemmas (Lean's `Int.neg_sub b a :
426    /// -(b - a) = a - b`) can flip via `.symm` correctly.
427    AntiCommutative {
428        op: crate::ast::BinOp,
429        /// `true` for `f(a, b) = -f(b, a)` (negation on rhs);
430        /// `false` for the swapped arrangement.
431        neg_on_rhs: bool,
432    },
433    /// `∀ a, g(a) = f(a, c)` or `f(c, a)` — the unary fn `g` is
434    /// the binary fn `f` with one argument bound to constant `c`.
435    /// Backends unfold both fns to expose the underlying op; the
436    /// IR carries `inner_fn` (the binary's source name) so the
437    /// unfold list is unambiguous.
438    UnaryEqualsBinary {
439        /// Source-level name of the binary fn the unary one equals.
440        inner_fn: String,
441    },
442    /// "Linear arithmetic over an unfold chain" — the law's two
443    /// sides reduce to a flat linear equation on Int once every
444    /// reachable user fn is unfolded. Generic catch-all for Int
445    /// laws that don't fit a named algebraic property. The IR
446    /// captures the unfold list + wrapper-return signal +
447    /// refinement smart-constructor guard; backends translate to
448    /// their decision procedure (Lean: `simp + omega`, Dafny: Z3
449    /// linear int prover). Named for the **semantic** ("linear
450    /// arithmetic"), not the Lean tactic.
451    LinearArithmetic {
452        /// Ordered fn unfold list. Top-level law fn first — Lean's
453        /// `unfold` resolves left-to-right and the call layer the
454        /// tactic peels at each step must match the goal shape.
455        unfold_fns: Vec<String>,
456        /// `true` when at least one fn in `unfold_fns` returns a
457        /// wrapper (Result, Option, …). Drives extra case-split
458        /// machinery in the emit — pure linear-arithmetic provers
459        /// can't close constructor-equality goals, so the wrapper
460        /// case splits on the smart-constructor guard first.
461        wrapper_return: bool,
462        /// Smart-constructor guard pulled from a refinement
463        /// `fromX(p: Int) -> Result<X, _>` in the unfold chain.
464        /// `Some` when one was found; `None` falls back to a
465        /// conservative `(n ≥ 0)` default when `wrapper_return`
466        /// forces case-splitting.
467        smart_guard: Option<SmartGuard>,
468        /// `true` when at least one law given is lifted to a
469        /// refinement type (`given a: Int` used as `Refined(value
470        /// = a)` in the law body). The Subtype/subset lift carries
471        /// the invariant in the type, so the by_cases case-split
472        /// that `wrapper_return` would otherwise force is
473        /// unnecessary — backends emit a plain unfold + simp
474        /// against arithmetic lemmas.
475        lifted: bool,
476    },
477    /// Structural induction on a recursive ADT parameter.
478    Induction { param: String },
479    /// Library axiom instance — the law instantiates a named
480    /// data-structure axiom (e.g. AverMap's `has_set_self` or
481    /// `get_set_self`). Backends map the axiom name to their
482    /// lemma vocabulary (Lean: `AverMap.has_set_self`; Dafny:
483    /// its own set/lookup axioms; Z3: built-in array theory).
484    /// Args carry the call-site expressions the axiom applies to.
485    LibraryAxiom {
486        /// Canonical axiom name. Recognised values today:
487        /// `"Map.has_set_self"`, `"Map.get_set_self"`. Open string
488        /// so future axioms (List, Set, Array, …) extend without
489        /// enum churn.
490        axiom: String,
491        /// Arguments in the order the axiom expects. For Map
492        /// axioms: `[m, k, v]` (the map, key, value the axiom
493        /// reasons about).
494        args: Vec<Spanned<crate::ir::hir::ResolvedExpr>>,
495    },
496    /// Post-condition of an inline-defined map-update fn. The outer
497    /// fn `outer(m, k)` has body shape `let v = Map.get m k; match v
498    /// { Some(_) -> Map.set m k _; None -> Map.set m k _ }` — i.e. it
499    /// inspects the existing value and writes some new value at key
500    /// `k` in every arm. The law asserts a post-condition on that
501    /// update — `Map.has(outer(m, k), k) == true` (`HasAfter`), or
502    /// `Map.get(outer(m, k), k) == Option.Some(...)` (`GetAfter`).
503    ///
504    /// Backends emit a 2-step proof: unfold the outer fn, case-split
505    /// on `Map.get m k` (the same value `outer` inspected), apply the
506    /// `Map.set`-axioms on each branch. Named after the law's
507    /// algebraic content, not the Lean tactic.
508    MapUpdatePostcondition {
509        /// Source name of the outer update fn.
510        outer_fn: String,
511        /// Which post-condition the law asserts.
512        kind: MapUpdatePostconditionKind,
513        /// The map argument as it appears at the law's call site.
514        map_arg: Spanned<crate::ir::hir::ResolvedExpr>,
515        /// The key argument as it appears at the law's call site.
516        key_arg: Spanned<crate::ir::hir::ResolvedExpr>,
517        /// Additional helper-fn source names to unfold on top of
518        /// `outer_fn` — only used for `GetAfter`, where the rhs's
519        /// `Option.Some(...)` typically wraps the prior value via a
520        /// pure user helper (e.g. `addOne(...)`). Source names;
521        /// backends translate to their lemma vocabulary.
522        extra_unfolds: Vec<String>,
523    },
524    /// Counter-increment specialisation of [`MapUpdatePostcondition`].
525    /// The outer fn `outer(m, k)` is the canonical "tracked counter"
526    /// shape:
527    ///
528    /// ```text
529    /// let v = Map.get m k
530    /// match v {
531    ///   Some(n) -> Map.set m k (n + 1)
532    ///   None    -> Map.set m k 1
533    /// }
534    /// ```
535    ///
536    /// The law states the algebraic content:
537    /// `Option.withDefault(Map.get(outer(m, k), k), 0) ==
538    /// Option.withDefault(Map.get(m, k), 0) + 1` — get-or-default
539    /// after the increment equals the prior get-or-default plus 1.
540    /// Tighter than [`MapUpdatePostcondition`] because both the body
541    /// template AND the rhs `+ 1` shape are pinned.
542    MapKeyTrackedIncrement {
543        /// Source name of the outer increment fn.
544        outer_fn: String,
545        /// The map argument as it appears at the law's call site.
546        map_arg: Spanned<crate::ir::hir::ResolvedExpr>,
547        /// The key argument as it appears at the law's call site.
548        key_arg: Spanned<crate::ir::hir::ResolvedExpr>,
549    },
550    /// Functional equivalence between an impl fn and a (declared)
551    /// spec fn — the law states `impl(args) == spec(args)` and the
552    /// two fn bodies are syntactically identical (after typecheck).
553    /// Backends close the goal by unfolding both fns; their bodies
554    /// reduce to the same term and the equality holds by reflexivity
555    /// modulo simp normalisation. Lean emits `simpa [<unfolds>]`,
556    /// Dafny would reveal both and let Z3 prove the equivalence.
557    /// Named for the algebraic content (functional equivalence),
558    /// not the backend tactic.
559    SpecEquivalence {
560        /// All user fn source names to unfold — impl + spec + any
561        /// transitively-reached helpers from law sides. Source
562        /// names; backends translate to their lemma vocabulary.
563        extra_unfolds: Vec<String>,
564    },
565    /// Broader [`SpecEquivalence`] for cases where impl and spec
566    /// bodies are NOT syntactically identical but normalize to the
567    /// same expression under arg substitution + simp arithmetic
568    /// identity folding (`a + 0 == a`, `a * 1 == a`, `a * 0 ==
569    /// 0`). Backends close via `simp` (no `simpa` — there's no
570    /// trivial-rfl goal to discharge; simp normalisation does the
571    /// closing). Same `extra_unfolds` payload as `SpecEquivalence`.
572    SpecEquivalenceSimpNormalized {
573        /// All user fn source names to unfold — impl + spec + any
574        /// transitively-reached helpers from law sides.
575        extra_unfolds: Vec<String>,
576    },
577    /// Linear-Int spec equivalence — impl and spec bodies are both
578    /// linear arithmetic expressions over Int givens (only
579    /// `Literal::Int`, given idents, `Add`, `Sub`) after arg
580    /// substitution. Bodies may differ syntactically but the
581    /// equivalence is decidable by a linear-arithmetic solver
582    /// (Presburger / `omega` / Z3 LIA). Backends emit a `change
583    /// <impl_unfolded> = <spec_unfolded>` rewrite then close via
584    /// their decision procedure; the IR carries the substituted
585    /// expressions so the backend can render them via its own
586    /// `emit_expr`.
587    LinearIntSpecEquivalence {
588        /// Impl body with formal params substituted by call-site
589        /// args. Linear-arithmetic-only after substitution.
590        unfolded_impl: Spanned<crate::ir::hir::ResolvedExpr>,
591        /// Spec body with formal params substituted by call-site
592        /// args. Linear-arithmetic-only after substitution.
593        unfolded_spec: Spanned<crate::ir::hir::ResolvedExpr>,
594    },
595    /// Functional equivalence between an effectful impl fn and a
596    /// spec fn. Same "claim states `impl(args) == spec(args)`"
597    /// content as [`SpecEquivalence`], but the law's source-level
598    /// shape is non-canonical (impl call usually omits oracle args
599    /// the spec call carries explicitly). The lowerer runs an
600    /// Oracle Lift over both sides — injecting oracle args from
601    /// `given oracle: Random.int = ...` into every classified
602    /// effectful call site — and matches the canonical shape on the
603    /// rewritten form. Backends emit `simp [impl, spec]`; both
604    /// definitions unfold to the same oracle call after lifting.
605    EffectfulSpecEquivalence {
606        /// Source name of the impl fn (= `vb.fn_name`).
607        impl_fn: String,
608        /// Source name of the spec fn (the other side of the law).
609        spec_fn: String,
610    },
611    /// Second-order linear recurrence spec equivalence — impl is a
612    /// tail-recursive Int linear-pair wrapper (e.g. `fib` dispatching
613    /// on `n < 0` and calling a 3-arg `fibTR(n, 0, 1)` helper) and
614    /// spec is a direct second-order recurrence (`match n { 0 -> b0;
615    /// 1 -> b1; _ -> recurrence(spec(n-1), spec(n-2)) }`). The
616    /// impl's helper implements the same affine recurrence as the
617    /// spec's `_` arm. Both Lean and Dafny render via a Nat-keyed
618    /// helper + shift lemma + helper-seed bridge; the algebraic
619    /// content (a fixed-point of the recurrence) is the same in both
620    /// targets but the syntactic proof template differs per backend.
621    LinearRecurrence2SpecEquivalence {
622        /// Source name of the impl (tail-recursive wrapper) fn.
623        impl_fn: String,
624        /// Source name of the spec (direct recurrence) fn.
625        spec_fn: String,
626        /// Source name of the worker fn called by `impl_fn`.
627        helper_fn: String,
628    },
629    /// Bounded universal: case-split over the declared `given`
630    /// domain, dispatch each case to a per-sample lemma.
631    BoundedUniversal,
632    /// No automated strategy — emit with `sorry` (Lean) / `assume
633    /// {:axiom}` (Dafny). User fills in manually.
634    Sorry,
635    /// Lowerer has not pinned a strategy for this law; the backend's
636    /// `or_else` chain decides. Today reached by linear-recurrence-
637    /// spec equivalence (Lean-specific, ~50-line support theorems
638    /// stay in the backend) and the sampled / guarded-domain
639    /// fallback. The backend treats `BackendDispatch` as "fall
640    /// through to ad-hoc strategy chain"; pinned variants above
641    /// short-circuit to a known emit.
642    BackendDispatch,
643}
644
645/// Discriminator for [`ProofStrategy::MapUpdatePostcondition`].
646#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647pub enum MapUpdatePostconditionKind {
648    /// Law shape: `Map.has(outer(m, k), k) == true`.
649    HasAfter,
650    /// Law shape: `Map.get(outer(m, k), k) == Option.Some(...)`.
651    GetAfter,
652}
653
654/// A bool predicate with explicit free-variable context. Stays in
655/// `Spanned<Expr>` form so backends can route through their
656/// existing `emit_expr` paths; the context is what gives backends
657/// the information they need to project (e.g. `.val`) without
658/// re-walking the AST.
659#[derive(Debug, Clone)]
660pub struct Predicate {
661    /// Variables the predicate may reference, in declaration order.
662    /// Each entry tells the backend what type the var has in the
663    /// target language — same logic as `Quantifier.binder_type`.
664    pub free_vars: Vec<(String, QuantifierType)>,
665    /// The expression. Already in the target variable space (e.g.
666    /// caller-derived predicates have had caller-arg names
667    /// substituted to callee-param names).
668    pub expr: Spanned<crate::ir::hir::ResolvedExpr>,
669}