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    /// Constant integer interval over-approximating `invariant`, as
190    /// derived by [`crate::ir::interval::interval_of_invariant`] from
191    /// the same predicate. `Some([lo, hi])` when the invariant shape
192    /// was recognized (a comparison / `Bool.and` against integer
193    /// literals); `None` when the analysis declined (unrecognized
194    /// shape — `Bool.or`, non-literal bound, structural carrier).
195    ///
196    /// Persisted here so a carrier-lowering codegen recognizer (the
197    /// next slice of the Int-semantics effort) can read the bound
198    /// directly off the `TypeId`-keyed decl — the same identity key
199    /// the interval analysis uses — without re-running the analysis
200    /// behind the `--explain-passes` diagnostic flag. The value is
201    /// identical to what `aver compile --explain-passes` reports for
202    /// the same type; both paths call the one `interval` analysis.
203    pub interval: Option<crate::ir::interval::Interval>,
204    /// Per-arithmetic-op overflow classification, in module-walk
205    /// order. Each entry pairs the operation's source name with its
206    /// [`crate::ir::interval::OpClass`] — `OverflowFree` when every
207    /// `i64` intermediate across the op body provably fits `i64`
208    /// (the carrier-lowering candidate), `NeedsWiderScratch` /
209    /// `Unbounded` otherwise. Empty when the type exposes no
210    /// carrier arithmetic or when `interval` is `None`.
211    ///
212    /// Populated by [`crate::ir::interval::analyze`] over the same
213    /// `ProofLowerInputs` `populate_refined_types` consumed, so the
214    /// classification is byte-identical to the `--explain-passes`
215    /// report. The codegen recognizer reads this to flag a carrier
216    /// "raw-i64-eligible" per op.
217    pub op_classes: Vec<(String, crate::ir::interval::OpClass)>,
218}
219
220impl RefinedTypeDecl {
221    /// Whether this refined type may have a **raw `i64` carrier** — the
222    /// gate a later codegen slice will trust to lower the bignum `Int`
223    /// carrier to a machine word. Derived ONLY from the persisted
224    /// [`Self::interval`] and [`Self::op_classes`] facts; it never
225    /// re-runs the interval analysis or inspects the invariant syntax.
226    ///
227    /// Returns `true` IFF, conservatively, the type is provably safe to
228    /// store and operate on as a raw `i64`:
229    ///
230    /// - [`Self::interval`] is `Some` — the analysis recognized the
231    ///   invariant shape (a `None` is the analysis's conservative
232    ///   *decline*, never eligible); AND
233    /// - that interval **fits `i64`** ([`crate::ir::interval::Interval::fits_i64`]),
234    ///   which holds IFF **both bounds are finite and within
235    ///   `[i64::MIN, i64::MAX]`**. This single test subsumes
236    ///   "two-sided" (an open / one-sided bound is `±inf`, which never
237    ///   fits) and the `i64`-range check — a `Natural` (`[0, +inf]`) or
238    ///   any interval wider than `i64` is rejected here; AND
239    /// - **every** entry in [`Self::op_classes`] is
240    ///   [`crate::ir::interval::OpClass::OverflowFree`] — a single
241    ///   `NeedsWiderScratch` or `Unbounded` op means some carrier
242    ///   arithmetic can wrap a raw `i64` before the smart constructor's
243    ///   guard re-validates, so the whole carrier stays bignum.
244    ///
245    /// Anything else → `false`. This is conservative in exactly the
246    /// soundness-critical direction: a wrongly-`true` answer would let
247    /// slice 4 lower a carrier whose ops can wrap, silently reintroducing
248    /// the model-vs-runtime gap the whole mechanism exists to close. The
249    /// predicate declines whenever the facts do not *prove* safety.
250    ///
251    /// ## Empty `op_classes`
252    ///
253    /// A type with a finite-`i64` interval but **no** carrier-reading
254    /// arithmetic ops (e.g. only `fromInt` / `toInt`, which
255    /// [`crate::ir::interval::classify_ops_in_scope`] skips) is reported
256    /// **eligible**. The decision is defensible because both soundness
257    /// obligations are met vacuously: storage of any inhabitant fits
258    /// `i64` (it is within the proven interval), and there is no op that
259    /// could overflow a raw `i64` (the `all(...)` over an empty op set is
260    /// `true`). The only thing the raw carrier adds over bignum — an op
261    /// that must not wrap before the guard — has nothing to apply to.
262    /// This is "eligible for storage", which is exactly what the gate
263    /// asks. If a future op is added to the type, it is re-classified and
264    /// can demote the type then; the determination is recomputed from the
265    /// persisted facts every time, never cached.
266    pub fn raw_i64_eligible(&self) -> bool {
267        // Delegates to the single shared recognizer so this persisted-
268        // fact gate and the `--explain-passes` diagnostic can never
269        // diverge about which types are eligible.
270        crate::ir::interval::raw_i64_eligible(
271            self.interval,
272            self.op_classes.iter().map(|(_, class)| class),
273        )
274    }
275}
276
277/// Per-pure-fn proof contract — what recursion shape (if any) the
278/// lowerer pinned for emit.
279#[derive(Debug, Clone)]
280pub struct FnContract {
281    pub source_name: String,
282    /// `None` means non-recursive (plain emit). `Some` says native /
283    /// fuel / structural / whatever the lowerer decided, with all
284    /// side-conditions inlined.
285    pub recursion: Option<RecursionContract>,
286}
287
288/// Recursion-shape decision. Each variant carries everything its
289/// emit needs AND the side-conditions the lowerer proved to choose
290/// it. The variants intentionally cannot be constructed without
291/// their side-conditions — backends cannot render `Native` without
292/// the lowerer having proved preservation + decrease.
293#[derive(Debug, Clone)]
294pub enum RecursionContract {
295    /// Fuel-encoded fallback. No side-conditions to prove; works
296    /// for any shape the classifier accepted as recursive.
297    Fuel {
298        /// Symbolic measure feeding the wrapper (`natAbs n + 1`,
299        /// `|xs| + 1`, etc.). Backends translate per target.
300        fuel_metric: FuelMetric,
301    },
302    /// Affine second-order linear recurrence on `Int`, shape
303    /// `f(n) = a*f(n-1) + b*f(n-2)` with literal `0`/`1` base cases
304    /// and an `n < 0` guard. Lowered to a private Nat pair-state
305    /// worker (Lean / Dafny both emit native structural recursion on
306    /// the Nat counter, no fuel). The lowerer doesn't carry the
307    /// shape coefficients yet — backends still pattern-match the
308    /// fn body via `lean::recurrence::detect_second_order_int_
309    /// linear_recurrence`. Step N+1 could materialise them here.
310    LinearRecurrence2,
311    /// Native recursion with explicit precondition. Lowerer proved
312    /// both `preservation` (rec args stay in domain) and `decrease`
313    /// (measure strictly drops) before constructing this variant.
314    /// Currently specialised to the IntCountdown-literal-zero shape
315    /// (`match p { 0 -> BASE; _ -> rec(p-1, ...) }`); other native-
316    /// recursion shapes (e.g. linear recurrence on a pair-state
317    /// worker) will land as additional `RecursionContract` variants.
318    Native {
319        /// Conjunction of precondition clauses, kept as a vector so
320        /// backends can render one `requires` per clause (Dafny) or
321        /// fold into a single `&&` chain (Lean). Empty means "no
322        /// caller-derived precondition" — the backend synthesises a
323        /// fibTR-style default (`param ≥ 0`) at emit time.
324        precondition: Vec<Predicate>,
325        /// Symbolic measure (e.g. `natAbs(n)`). Backends render per
326        /// target language (`Int.natAbs n` on Lean, `n` with a
327        /// `requires n >= 0` clause on Dafny).
328        measure: Measure,
329        /// Side-condition tag: lowerer attests the recursive args
330        /// preserve the precondition. Empty enum payload — its
331        /// existence in the type is the proof, not its content.
332        preservation: PreservationProof,
333        /// Same for the decreasing measure.
334        decrease: DecreaseProof,
335        /// Body decomposition for the IntCountdown-literal-zero shape:
336        /// the literal int that selects the base arm, the base arm's
337        /// body, and the wildcard arm's body. Carried so backends can
338        /// render the `if h : p = <lit> then base else rec(p-1, ...)`
339        /// switch without re-walking the source AST. The literal is
340        /// always `0` today — the `IntCountdownLiteralZero`
341        /// preservation marker attests it; carrying the value as data
342        /// keeps the IR shape forward-compatible with future
343        /// preservation proofs that admit other literals.
344        body: NativeIntCountdownBody,
345    },
346    /// Well-founded native def on `param.toNat` — graduates a fn out
347    /// of the fuel/partial encoding so it stays kernel-transparent
348    /// (Lean: `termination_by param.toNat` + a `decreasing_by` the
349    /// kernel re-checks; Dafny: `decreases if param >= 0 then param
350    /// else 0` with NO synthesized `requires`, so total callers stay
351    /// wellformed). Two validated sources:
352    ///
353    /// - `floor_div: Some(..)` — every self-call shrinks `param` by a
354    ///   literal-divisor floor division
355    ///   (`Result.withDefault(Int.div(p, k), d)` with literal k >= 2,
356    ///   possibly through a unary wrapper fn), and the classifier
357    ///   verified the guard chain enclosing every self-call site
358    ///   implies `p >= 1` — so `p / k < p` and the measure strictly
359    ///   drops. Never guessed: a fn whose guards don't justify the
360    ///   shrink keeps its prior (partial/opaque) emission.
361    /// - `floor_div: None` — guard-protected subtractive countdown
362    ///   (`p - k`, literal k >= 1, guards imply `p >= 1`), graduated
363    ///   out of fuel on demand by the floor-division window law
364    ///   family, whose proof templates need the fn's defining
365    ///   equations and functional-induction principle.
366    WellFoundedToNat {
367        /// The decreasing Int parameter (source name).
368        param: String,
369        /// `Some` for the floor-division shrink; `None` for the
370        /// guarded subtractive countdown.
371        floor_div: Option<FloorDivShrink>,
372    },
373}
374
375/// Payload of [`RecursionContract::WellFoundedToNat`] for the
376/// floor-division shrink shape.
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct FloorDivShrink {
379    /// The literal divisor (>= 2).
380    pub divisor: i64,
381    /// `Some(name)` when the self-call shrinks through a unary
382    /// wrapper fn whose body is exactly
383    /// `Result.withDefault(Int.div(x, divisor), <int literal>)`;
384    /// `None` when the `Result.withDefault(Int.div(p, k), d)` call
385    /// is inlined at the self-call site. Lean's `decreasing_by`
386    /// unfolds the wrapper by name.
387    pub helper_fn: Option<String>,
388}
389
390/// Body decomposition for the `IntCountdown-literal-zero` native
391/// shape. Each field is a slice of the source AST the lowerer
392/// extracted while classifying; backends render them directly
393/// without re-walking the source.
394#[derive(Debug, Clone)]
395pub struct NativeIntCountdownBody {
396    /// The literal int that selects the base arm. Always `0` today;
397    /// future preservation proofs may admit other literals, so the
398    /// value is carried as data rather than baked into the marker.
399    pub base_arm_literal: i64,
400    /// AST for the base arm's body (`match p { 0 -> THIS; _ -> ... }`).
401    pub base_arm_body: Spanned<crate::ir::hir::ResolvedExpr>,
402    /// AST for the wildcard arm's body — the recursive call site.
403    pub wildcard_arm_body: Spanned<crate::ir::hir::ResolvedExpr>,
404}
405
406/// Fuel metric for the fallback fuel-encoded emit path.
407#[derive(Debug, Clone)]
408pub enum FuelMetric {
409    /// `n.natAbs + 1` — classic IntCountdown fuel.
410    NatAbsPlusOne { param: String },
411    /// `(bound - n).natAbs + 1` — IntAscending: param climbs toward
412    /// a bound expression. Backends render the bound through their
413    /// own `Spanned<Expr>` emitter (Lean: `bound_expr_to_lean`,
414    /// Dafny: `emit_expr` over int subset).
415    BoundMinusParamNatAbsPlusOne {
416        param: String,
417        bound: Spanned<crate::ir::hir::ResolvedExpr>,
418    },
419    /// `xs.length + 1` — List/String structural recursion.
420    SeqLenPlusOne { param: String },
421    /// `sizeOf(x) + 1` — structural recursion on a user-defined
422    /// recursive ADT (e.g. `Term::App(f, arg)`). The classifier
423    /// doesn't pin the bound param — sizeOf walks the whole call
424    /// frame — so this variant carries no param name.
425    SizeOfPlusOne,
426    /// `s.length - pos` — StringPosAdvance: a `String` carrier stays
427    /// invariant, an `Int` position climbs toward its length.
428    StringLenMinusPos {
429        string_param: String,
430        pos_param: String,
431    },
432    /// Lexicographic pair for mutual recursion SCCs.
433    Lex { params: Vec<String>, rank: usize },
434}
435
436/// Symbolic termination measure. Backend-agnostic.
437#[derive(Debug, Clone)]
438pub enum Measure {
439    NatAbsInt { param: String },
440    SeqLen { param: String },
441    Lex(Vec<Measure>),
442}
443
444/// Marker that the lowerer constructed a proof of preservation
445/// (recursive args stay in the precondition's domain). The variants
446/// describe HOW the proof was constructed so future maintainers can
447/// trace why a given shape was accepted as native.
448#[derive(Debug, Clone)]
449pub enum PreservationProof {
450    /// `match p { 0 -> base; _ -> rec(p-1, ...) }` under `p ≥ 0`
451    /// precondition. Wildcard arm gives `p ≠ 0`, combined with
452    /// `p ≥ 0` yields `p ≥ 1`, so `p - 1 ≥ 0`.
453    IntCountdownLiteralZero,
454}
455
456/// Symmetric marker for the decreasing measure.
457#[derive(Debug, Clone)]
458pub enum DecreaseProof {
459    /// `natAbs(p - 1) < natAbs(p)` under `p ≥ 0 ∧ p ≠ 0`.
460    NatAbsCountdown,
461}
462
463/// Lowered verify-law theorem. All projection decisions (`.val`
464/// vs bare ident, wrapper strip, when-keep vs when-drop) are
465/// already baked into the fields below; backends render directly.
466#[derive(Debug, Clone)]
467pub struct LawTheorem {
468    /// Opaque identity of the fn this law targets, resolved through
469    /// `SymbolTable` at populate time (phase E3). Verify laws are
470    /// entry-only per the current model, so this is effectively
471    /// always an entry-scope `FnId` today; once laws-in-modules
472    /// lands the same `FnId` will distinguish two same-bare-name
473    /// recursive fns across modules without any per-callsite scope
474    /// plumbing.
475    pub fn_id: FnId,
476    pub law_name: String,
477    pub quantifiers: Vec<Quantifier>,
478    /// Premises in order. Already includes `when` if it carries
479    /// information beyond the refinement invariants (the lowerer
480    /// performs the bijective syntactic equivalence check).
481    pub premises: Vec<Predicate>,
482    /// LHS = RHS claim. Wrapper-stripped, lifted-var-aware (bare
483    /// idents for arg positions, `.val` projections inside
484    /// comparator BinOps if the lowerer determined this is needed).
485    pub claim_lhs: Spanned<crate::ir::hir::ResolvedExpr>,
486    pub claim_rhs: Spanned<crate::ir::hir::ResolvedExpr>,
487    pub strategy: ProofStrategy,
488}
489
490/// A universally-quantified variable in a law theorem. Carries
491/// enough type info for backends to render the binder correctly
492/// (`(a : Natural)` for refined Int, `(a : Int)` for plain int,
493/// `(rng : RandomIntInBounds)` for oracle).
494#[derive(Debug, Clone)]
495pub struct Quantifier {
496    pub name: String,
497    pub binder_type: QuantifierType,
498}
499
500#[derive(Debug, Clone)]
501pub enum QuantifierType {
502    /// Plain Aver type, rendered as-is on each backend.
503    Plain(String),
504    /// Refinement-lifted: source declared `given a: Int`, body used
505    /// `Natural(value = a)`, so the quantifier binds at the refined
506    /// type. The carried `refined_type` key looks up in
507    /// `ProofIR.refined_types`.
508    RefinedTo { refined_type: String },
509    /// Oracle subtype: classified Generative-shape effect-givens
510    /// bind oracles wrapped in a subtype carrier
511    /// (`RandomIntInBounds`, `RandomFloatInUnit`,
512    /// `TimeUnixMsNonneg`).
513    OracleSubtype(String),
514}
515
516/// Algebraic / proof-theoretic shape of a verify-law theorem.
517///
518/// **Naming rule**: variants describe **what the law says**, not
519/// **how a backend proves it**. The IR is target-agnostic — Lean
520/// maps `Commutative { op: Add }` to `simp [fn, Int.add_comm]`,
521/// Dafny maps the same variant to its own lemma vocabulary, a Z3
522/// backend could ship a different tactic again. Tactic names
523/// (`SimpOverLemmas`, `simp+omega`) do not appear in variant names;
524/// Driver of a [`ProofStrategy::WrapperOverRecursion`] inner loop —
525/// the structure the recursion shrinks. `List` is the original
526/// `sum_acc` shape (`match xs { [] -> acc; h::t -> loop(t, step) }`);
527/// `PeanoNat` is the `factTR` countdown (`match n { Z -> acc; S(m) ->
528/// loop(m, combine(n, acc)) }`). The two need different induction
529/// skeletons (`nil`/`cons` vs `zero`/`succ`) and, for `Mul`, different
530/// closing tactics (`omega` can't discharge a nonlinear residual).
531#[derive(Debug, Clone, PartialEq, Eq)]
532pub enum WrapperDriver {
533    /// Structural fold over a `List<_>` first parameter.
534    List,
535    /// Countdown over a Peano-`Nat` ADT first parameter. Carries the
536    /// ADT's source type name and whether the folded value (the matched
537    /// subject) is the combine fn's FIRST argument (`mul(n, acc)` →
538    /// `true`; `mul(acc, n)` → `false`) so the backend rewrite matches
539    /// the def's actual step term.
540    PeanoNat {
541        type_name: String,
542        value_first: bool,
543    },
544}
545
546/// `LinearArithmetic` is named for the semantic, not the tactic.
547#[derive(Debug, Clone)]
548pub enum ProofStrategy {
549    /// `rfl` / definitional equality — `lhs ≡ rhs` syntactically.
550    Reflexive,
551    /// `simp` chain over named lemmas. The discovery feedback loop
552    /// (`lemma_discovery::committed`) pins this when a committed
553    /// `DiscoveredLemmas.lean` holds kernel-proved lemmas in-scope
554    /// for an `Induction` law: the names are the discovered theorem
555    /// names, and the Lean renderer reuses the induction ladder with
556    /// those lemmas embedded + joined to its simp sets. Pinned by
557    /// the CLI (post-lowering re-pin), never by
558    /// `classify_law_strategy` — discovery feedback is opt-in via
559    /// the committed artifact.
560    SimpOverLemmas(Vec<String>),
561    /// `∀ a b, f(a, b) = f(b, a)` — commutativity of the law's fn,
562    /// whose body reduces to `a <op> b`. The `op` tag lets backends
563    /// pick their own lemma vocabulary (Lean: `Int.add_comm`,
564    /// Dafny: built-in arithmetic axioms).
565    Commutative { op: crate::ast::BinOp },
566    /// `∀ a b c, f(f(a,b),c) = f(a,f(b,c))` — associativity of `f`.
567    Associative { op: crate::ast::BinOp },
568    /// `∀ a, f(a, e) = a` (or the swapped `f(e, a) = a`) — the
569    /// identity-element law for the underlying op (`e` = `0` for
570    /// Add / Sub, `1` for Mul). Backends emit `simp [fn]` (the
571    /// wrapper's body unfolds to the identity equation, which simp
572    /// closes); the variant doesn't need a `side` field because
573    /// the emit is symmetric — Sub is naturally one-sided (only
574    /// right-identity), Add/Mul accept either side. The lowerer
575    /// guarantees the law's actual shape matches the op's identity
576    /// behaviour before pinning.
577    IdentityElement { op: crate::ast::BinOp },
578    /// `∀ a b, f(a, b) = -f(b, a)` (or the swapped negation).
579    /// `neg_on_rhs` records which side carries the `-` wrap so
580    /// backends with directional lemmas (Lean's `Int.neg_sub b a :
581    /// -(b - a) = a - b`) can flip via `.symm` correctly.
582    AntiCommutative {
583        op: crate::ast::BinOp,
584        /// `true` for `f(a, b) = -f(b, a)` (negation on rhs);
585        /// `false` for the swapped arrangement.
586        neg_on_rhs: bool,
587    },
588    /// `∀ a, g(a) = f(a, c)` or `f(c, a)` — the unary fn `g` is
589    /// the binary fn `f` with one argument bound to constant `c`.
590    /// Backends unfold both fns to expose the underlying op; the
591    /// IR carries `inner_fn` (the binary's source name) so the
592    /// unfold list is unambiguous.
593    UnaryEqualsBinary {
594        /// Source-level name of the binary fn the unary one equals.
595        inner_fn: String,
596    },
597    /// "Linear arithmetic over an unfold chain" — the law's two
598    /// sides reduce to a flat linear equation on Int once every
599    /// reachable user fn is unfolded. Generic catch-all for Int
600    /// laws that don't fit a named algebraic property. The IR
601    /// captures the unfold list + wrapper-return signal +
602    /// refinement smart-constructor guard; backends translate to
603    /// their decision procedure (Lean: `simp + omega`, Dafny: Z3
604    /// linear int prover). Named for the **semantic** ("linear
605    /// arithmetic"), not the Lean tactic.
606    LinearArithmetic {
607        /// Ordered fn unfold list. Top-level law fn first — Lean's
608        /// `unfold` resolves left-to-right and the call layer the
609        /// tactic peels at each step must match the goal shape.
610        unfold_fns: Vec<String>,
611        /// `true` when at least one fn in `unfold_fns` returns a
612        /// wrapper (Result, Option, …). Drives extra case-split
613        /// machinery in the emit — pure linear-arithmetic provers
614        /// can't close constructor-equality goals, so the wrapper
615        /// case splits on the smart-constructor guard first.
616        wrapper_return: bool,
617        /// Smart-constructor guard pulled from a refinement
618        /// `fromX(p: Int) -> Result<X, _>` in the unfold chain.
619        /// `Some` when one was found; `None` falls back to a
620        /// conservative `(n ≥ 0)` default when `wrapper_return`
621        /// forces case-splitting.
622        smart_guard: Option<SmartGuard>,
623        /// `true` when at least one law given is lifted to a
624        /// refinement type (`given a: Int` used as `Refined(value
625        /// = a)` in the law body). The Subtype/subset lift carries
626        /// the invariant in the type, so the by_cases case-split
627        /// that `wrapper_return` would otherwise force is
628        /// unnecessary — backends emit a plain unfold + simp
629        /// against arithmetic lemmas.
630        lifted: bool,
631    },
632    /// Structural induction on a recursive ADT parameter.
633    Induction { param: String },
634    /// Library axiom instance — the law instantiates a named
635    /// data-structure axiom (e.g. AverMap's `has_set_self` or
636    /// `get_set_self`). Backends map the axiom name to their
637    /// lemma vocabulary (Lean: `AverMap.has_set_self`; Dafny:
638    /// its own set/lookup axioms; Z3: built-in array theory).
639    /// Args carry the call-site expressions the axiom applies to.
640    LibraryAxiom {
641        /// Canonical axiom name. Recognised values today:
642        /// `"Map.has_set_self"`, `"Map.get_set_self"`. Open string
643        /// so future axioms (List, Set, Array, …) extend without
644        /// enum churn.
645        axiom: String,
646        /// Arguments in the order the axiom expects. For Map
647        /// axioms: `[m, k, v]` (the map, key, value the axiom
648        /// reasons about).
649        args: Vec<Spanned<crate::ir::hir::ResolvedExpr>>,
650    },
651    /// Post-condition of an inline-defined map-update fn. The outer
652    /// fn `outer(m, k)` has body shape `let v = Map.get m k; match v
653    /// { Some(_) -> Map.set m k _; None -> Map.set m k _ }` — i.e. it
654    /// inspects the existing value and writes some new value at key
655    /// `k` in every arm. The law asserts a post-condition on that
656    /// update — `Map.has(outer(m, k), k) == true` (`HasAfter`), or
657    /// `Map.get(outer(m, k), k) == Option.Some(...)` (`GetAfter`).
658    ///
659    /// Backends emit a 2-step proof: unfold the outer fn, case-split
660    /// on `Map.get m k` (the same value `outer` inspected), apply the
661    /// `Map.set`-axioms on each branch. Named after the law's
662    /// algebraic content, not the Lean tactic.
663    MapUpdatePostcondition {
664        /// Source name of the outer update fn.
665        outer_fn: String,
666        /// Which post-condition the law asserts.
667        kind: MapUpdatePostconditionKind,
668        /// The map argument as it appears at the law's call site.
669        map_arg: Spanned<crate::ir::hir::ResolvedExpr>,
670        /// The key argument as it appears at the law's call site.
671        key_arg: Spanned<crate::ir::hir::ResolvedExpr>,
672        /// Additional helper-fn source names to unfold on top of
673        /// `outer_fn` — only used for `GetAfter`, where the rhs's
674        /// `Option.Some(...)` typically wraps the prior value via a
675        /// pure user helper (e.g. `addOne(...)`). Source names;
676        /// backends translate to their lemma vocabulary.
677        extra_unfolds: Vec<String>,
678    },
679    /// Counter-increment specialisation of [`MapUpdatePostcondition`].
680    /// The outer fn `outer(m, k)` is the canonical "tracked counter"
681    /// shape:
682    ///
683    /// ```text
684    /// let v = Map.get m k
685    /// match v {
686    ///   Some(n) -> Map.set m k (n + 1)
687    ///   None    -> Map.set m k 1
688    /// }
689    /// ```
690    ///
691    /// The law states the algebraic content:
692    /// `Option.withDefault(Map.get(outer(m, k), k), 0) ==
693    /// Option.withDefault(Map.get(m, k), 0) + 1` — get-or-default
694    /// after the increment equals the prior get-or-default plus 1.
695    /// Tighter than [`MapUpdatePostcondition`] because both the body
696    /// template AND the rhs `+ 1` shape are pinned.
697    MapKeyTrackedIncrement {
698        /// Source name of the outer increment fn.
699        outer_fn: String,
700        /// The map argument as it appears at the law's call site.
701        map_arg: Spanned<crate::ir::hir::ResolvedExpr>,
702        /// The key argument as it appears at the law's call site.
703        key_arg: Spanned<crate::ir::hir::ResolvedExpr>,
704    },
705    /// Functional equivalence between an impl fn and a (declared)
706    /// spec fn — the law states `impl(args) == spec(args)` and the
707    /// two fn bodies are syntactically identical (after typecheck).
708    /// Backends close the goal by unfolding both fns; their bodies
709    /// reduce to the same term and the equality holds by reflexivity
710    /// modulo simp normalisation. Lean emits `simpa [<unfolds>]`,
711    /// Dafny would reveal both and let Z3 prove the equivalence.
712    /// Named for the algebraic content (functional equivalence),
713    /// not the backend tactic.
714    SpecEquivalence {
715        /// All user fn source names to unfold — impl + spec + any
716        /// transitively-reached helpers from law sides. Source
717        /// names; backends translate to their lemma vocabulary.
718        extra_unfolds: Vec<String>,
719    },
720    /// Broader [`SpecEquivalence`] for cases where impl and spec
721    /// bodies are NOT syntactically identical but normalize to the
722    /// same expression under arg substitution + simp arithmetic
723    /// identity folding (`a + 0 == a`, `a * 1 == a`, `a * 0 ==
724    /// 0`). Backends close via `simp` (no `simpa` — there's no
725    /// trivial-rfl goal to discharge; simp normalisation does the
726    /// closing). Same `extra_unfolds` payload as `SpecEquivalence`.
727    SpecEquivalenceSimpNormalized {
728        /// All user fn source names to unfold — impl + spec + any
729        /// transitively-reached helpers from law sides.
730        extra_unfolds: Vec<String>,
731    },
732    /// Linear-Int spec equivalence — impl and spec bodies are both
733    /// linear arithmetic expressions over Int givens (only
734    /// `Literal::Int`, given idents, `Add`, `Sub`) after arg
735    /// substitution. Bodies may differ syntactically but the
736    /// equivalence is decidable by a linear-arithmetic solver
737    /// (Presburger / `omega` / Z3 LIA). Backends emit a `change
738    /// <impl_unfolded> = <spec_unfolded>` rewrite then close via
739    /// their decision procedure; the IR carries the substituted
740    /// expressions so the backend can render them via its own
741    /// `emit_expr`.
742    LinearIntSpecEquivalence {
743        /// Impl body with formal params substituted by call-site
744        /// args. Linear-arithmetic-only after substitution.
745        unfolded_impl: Spanned<crate::ir::hir::ResolvedExpr>,
746        /// Spec body with formal params substituted by call-site
747        /// args. Linear-arithmetic-only after substitution.
748        unfolded_spec: Spanned<crate::ir::hir::ResolvedExpr>,
749    },
750    /// Functional equivalence between an effectful impl fn and a
751    /// spec fn. Same "claim states `impl(args) == spec(args)`"
752    /// content as [`SpecEquivalence`], but the law's source-level
753    /// shape is non-canonical (impl call usually omits oracle args
754    /// the spec call carries explicitly). The lowerer runs an
755    /// Oracle Lift over both sides — injecting oracle args from
756    /// `given oracle: Random.int = ...` into every classified
757    /// effectful call site — and matches the canonical shape on the
758    /// rewritten form. Backends emit `simp [impl, spec]`; both
759    /// definitions unfold to the same oracle call after lifting.
760    EffectfulSpecEquivalence {
761        /// Source name of the impl fn (= `vb.fn_name`).
762        impl_fn: String,
763        /// Source name of the spec fn (the other side of the law).
764        spec_fn: String,
765    },
766    /// Second-order linear recurrence spec equivalence — impl is a
767    /// tail-recursive Int linear-pair wrapper (e.g. `fib` dispatching
768    /// on `n < 0` and calling a 3-arg `fibTR(n, 0, 1)` helper) and
769    /// spec is a direct second-order recurrence (`match n { 0 -> b0;
770    /// 1 -> b1; _ -> recurrence(spec(n-1), spec(n-2)) }`). The
771    /// impl's helper implements the same affine recurrence as the
772    /// spec's `_` arm. Both Lean and Dafny render via a Nat-keyed
773    /// helper + shift lemma + helper-seed bridge; the algebraic
774    /// content (a fixed-point of the recurrence) is the same in both
775    /// targets but the syntactic proof template differs per backend.
776    LinearRecurrence2SpecEquivalence {
777        /// Source name of the impl (tail-recursive wrapper) fn.
778        impl_fn: String,
779        /// Source name of the spec (direct recurrence) fn.
780        spec_fn: String,
781        /// Source name of the worker fn called by `impl_fn`.
782        helper_fn: String,
783    },
784    /// Bounded universal: case-split over the declared `given`
785    /// domain, dispatch each case to a per-sample lemma.
786    BoundedUniversal,
787    /// `?`-propagating Result chain equals a manual `match`-version:
788    /// the law states `chain_qm(x) == chain_manual(x)` where the
789    /// LHS uses `?` for short-circuit Err propagation and the RHS
790    /// writes the same flow as nested `match Result.Err -> Err`
791    /// arms. Both sides unfold to the same nested match; the proof
792    /// closes by unfolding all step fns and case-splitting on each
793    /// step's Result discriminator. Demonstrated by
794    /// `examples/core/result_chain.av`. Stage 8b of #232.
795    ResultPipelineChain {
796        /// Source name of the `?`-chain fn (the wrapper). LHS of the law.
797        chain_qm_fn: String,
798        /// Source name of the manual `match`-chain fn. RHS of the law.
799        chain_manual_fn: String,
800        /// Source names of every step fn the two chains thread
801        /// through, in pipeline order. Drives the unfold list for
802        /// both backends.
803        step_fns: Vec<String>,
804    },
805    /// Monoidal-accumulator wrapper-over-recursion: a non-recursive
806    /// `wrapper_fn(xs) = inner_fn(xs, neutral)` paired with a direct-
807    /// recurrence `other_fn` such that the law states
808    /// `wrapper_fn(xs) == other_fn(xs)`. The inner fn has shape
809    /// `match xs { [] -> acc; [h, ..t] -> inner_fn(t, acc <op> h) }`
810    /// where `<op>` is monoidal (`Add` / `Mul` / `Sub` on Int) with
811    /// known neutral element. Strategy emits an aux accumulator-
812    /// decomposition lemma plus the main universal lemma; Z3 closes
813    /// both via list induction. Demonstrated by `examples/data/sum_acc.av`.
814    ///
815    /// Stage 8 of #232 — first ProofStrategy variant that consumes
816    /// a `ModulePattern` from `analysis::shape`.
817    WrapperOverRecursion {
818        /// Source name of the non-recursive wrapper (e.g. `"sum"`).
819        wrapper_fn: String,
820        /// Source name of the self-recursive inner (e.g. `"sumTR"`).
821        inner_fn: String,
822        /// Source name of the direct-recurrence fn the law compares
823        /// the wrapper against (e.g. `"sumDirect"`).
824        other_fn: String,
825        /// Binary op the inner threads through its accumulator
826        /// (`Add` / `Mul` / `Sub`). Drives the aux lemma's RHS.
827        combine_op: crate::ast::BinOp,
828        /// Driver of the inner recursion: a `List<_>` structural fold
829        /// (the additive `sum_acc` shape) or a Peano-`Nat` countdown
830        /// (`factTR`). Selects the backend's induction skeleton and the
831        /// closing tactic family.
832        driver: WrapperDriver,
833        /// For a Peano-`Nat` fold whose step is a named monoid fn
834        /// (`mul(n, acc)` / `plus(n, acc)`), the combine fn's source
835        /// name. `None` for an inline-binop `List` fold (`acc + h`).
836        combine_fn: Option<String>,
837    },
838    /// Tail-recursive fold with a FIXED base parameter — the `qexp`
839    /// shape (TIP prop_35). The law `spec(x, y) == loop(x, y, neutral)`
840    /// equates a 2-arg structural recurrence on the DRIVER `y`:
841    /// `spec(x, y) = match y { Z -> neutral; S n -> combine(x, spec(x, n)) }`
842    /// against a 3-arg tail-recursive form carrying an accumulator:
843    /// `loop(x, y, z) = match y { Z -> z; S n -> loop(x, n, combine(x, z)) }`.
844    /// Unlike `WrapperOverRecursion`, the extra param `x` is FIXED across
845    /// the recursion (the base), the combine multiplies the accumulator by
846    /// `x` (not by the matched subject), and the law binds TWO givens with
847    /// the wrapper call written inline (no separate wrapper fn). Backends
848    /// emit the accumulator-generalization lemma
849    /// `loop x y z = combine (loop x y neutral) z` (induct on `y`,
850    /// generalize `z`; `x` fixed) plus the main universal law; the
851    /// multiplicative algebra closes via the `isNatMul` bridge to core
852    /// `Nat.mul_*` (no Mathlib). Today: multiplicative (`Mul`, neutral
853    /// `S(Z)`) and additive (`Add`, neutral `Z`) Peano combines.
854    TailRecFixedBaseFold {
855        /// Source name of the direct recurrence the law's other side calls
856        /// (e.g. `"exp"`). Recurses on the driver, base param fixed.
857        spec_fn: String,
858        /// Source name of the 3-arg tail-recursive loop (e.g. `"qexp"`).
859        loop_fn: String,
860        /// Source name of the binary monoid combine fn (`mult` / `plus`).
861        combine_fn: String,
862        /// Combine op classified from the monoid fn's base arm.
863        combine_op: crate::ast::BinOp,
864        /// Source type name of the driving Peano `Nat` ADT.
865        type_name: String,
866    },
867    /// Ground constant-fold over fixed ADT/enum constructor
868    /// arguments. The law's call(s) pin every non-Int param of the
869    /// verified fn to a constructor literal (`CellContent.Empty`,
870    /// `Color.Black`); any scalar `given`s are quantified but irrelevant
871    /// to the chosen branch (the constructor selects a fixed arm). The
872    /// verified fn and its transitively-reached callees are
873    /// non-recursive, so the whole call tree folds to a closed term and
874    /// the goal becomes a decidable ground equality. Backends unfold the
875    /// fn + callees (the same `unfold_fns` list the LinearArithmetic
876    /// detector builds) and close with a `split`/`rfl`/`decide` cascade.
877    /// Demonstrated by `examples/games/checkers/ai.av`
878    /// (`centerBonus.emptyNeutral`, `pieceValue.antisymmetry`,
879    /// `pieceValue.kingWorthTripleMan`). Named for the algebraic content
880    /// (constant-folding over a fixed constructor), not the Lean tactic.
881    EnumConstantFold {
882        /// Ordered fn unfold list — top-level law fn first, then
883        /// transitively-reached non-recursive callees. Source names;
884        /// backends translate to their lemma vocabulary.
885        unfold_fns: Vec<String>,
886    },
887    /// Closed finite-domain enumeration over the law's givens. Every
888    /// given ranges over a closed, small domain — `Bool` or a
889    /// user-declared enum whose constructors are ALL fieldless — with
890    /// the product of domain sizes ≤ 16, so exhaustive `cases` over
891    /// the givens yields ground goals that compute out (`rfl` /
892    /// `decide`). Fuel-wrapped callees are NOT an obstacle:
893    /// constant-measure constructor args compute through fuel. The
894    /// detector deliberately has NO call-shape inspection, NO
895    /// return-type gate and NO recursion gate — closed enumeration
896    /// makes those irrelevant, which is why this is a NEW strategy and
897    /// not a relaxation of [`ProofStrategy::EnumConstantFold`], whose
898    /// literal-pinning / non-recursive / scalar-return gates are
899    /// load-bearing for its simp cascade. Motivating shapes:
900    /// `examples/data/json.av` `parseLiteral.boolRoundtrip` (closes
901    /// genuinely with `intro b; cases b <;> rfl`) and the `EscapeCode`
902    /// laws (`escapeJsonChar.encodesEscapeCode`,
903    /// `parseEscape.escapeCodeRoundtrip`). A non-closing leaf degrades
904    /// to an honest caught `sorry` — never a build error and never
905    /// `native_decide`.
906    FiniteDomainCases {
907        /// Law given names in intro order — the Lean emitter's
908        /// `cases` targets. Source names; backends translate.
909        givens: Vec<String>,
910    },
911    /// Builtin-roundtrip simp over the prelude's spec-lemma registry —
912    /// the last typed fallback before `BackendDispatch`, and the only
913    /// strategy the Lean backend renders AFTER its legacy ad-hoc chain
914    /// (so it fires precisely where the sampled-sorry fallback used to
915    /// emit a bare-`sorry` universal). A no-when law whose lhs call cone
916    /// reduces to builtin String/Int operations once the user fns
917    /// unfold: the Lean emit is `intro <givens>; first | (simp [<unfold
918    /// set>, <registry lemmas>, Int.add_sub_cancel]; done) | sorry`. The
919    /// `done` + `first | … | sorry` alternation is the honest floor — a
920    /// simp that fails OR leaves a residual goal degrades to a caught
921    /// `sorry`, NEVER a build error and NEVER `native_decide`.
922    /// Motivating shapes: `examples/data/json.av`
923    /// `finishInt.fromCanonicalInt` (closes via
924    /// `Int.fromString_fromInt`), `finishNumber.fromCanonicalIntSlice` /
925    /// `afterIntChar.terminatedIntRoundtrip` (slice-prefix lemmas
926    /// through the `toString` fuel wrapper) and
927    /// `finishString.plainSegmentRoundtrip` (`String.slice_append_prefix`
928    /// + `String.intercalate_singleton`).
929    ///
930    /// Deliberately a NEW variant and not a reuse of
931    /// [`ProofStrategy::SimpOverLemmas`]: that variant is the discovery
932    /// feedback loop's re-pin channel (`lemma_discovery::committed`
933    /// re-pins an `Induction` law when committed *discovered* lemma
934    /// texts are in scope, and the backend routes it through the
935    /// induction emit with embedded lemma bodies). This strategy carries
936    /// no lemma texts and never inducts — it names *static prelude*
937    /// lemmas that the Lean emitter ships demand-driven (see
938    /// `lean::prelude_spec_lemmas_for_builtins`, the single source of
939    /// truth for the builtin → lemma-name registry). Keeping the two
940    /// apart means neither the discovery CLI nor `committed.rs` ever
941    /// has to reason about this variant.
942    SimpOverPreludeLemmas {
943        /// Ordered fn unfold list — law subject fn first, then the
944        /// transitively-reached NON-recursive callees (sorted).
945        /// Source names; backends translate.
946        unfold_fns: Vec<String>,
947        /// Recursive (fuel-emitted) fns called DIRECTLY in the law lhs
948        /// with measure-closed args (constructor-headed over
949        /// scalar-only payloads, or literals) — the fuel value
950        /// computes to a Nat literal, so simp drives the `__fuel`
951        /// equations through. The Lean emitter expands each name to
952        /// wrapper + `<name>__fuel` + its measure-helper names.
953        /// Recursive fns reached only transitively (inside cone
954        /// bodies) are NOT listed: they stay opaque — usually dead
955        /// branches under the law's pinned literal args, and if live
956        /// the simp falls to the honest caught `sorry`.
957        fuel_fns: Vec<String>,
958        /// Builtin call names observed in the law sides + cone bodies
959        /// (sorted; includes the synthetic `String.concat` marker for
960        /// string `+`). Registry keys — the Lean emitter maps them to
961        /// prelude spec lemma names via
962        /// `prelude_spec_lemmas_for_builtins`.
963        builtins: Vec<String>,
964    },
965    /// Decimal-Int parse/serialize roundtrip over the canonical
966    /// single-scanner decimal parser shape: the law states
967    /// `parse(ser(C(n)), 0) = Ok(C(n), String.len(ser(C(n))))` for an
968    /// unconstrained `given n: Int`, where `parse` dispatches the head
969    /// char (`"-"` → sign path, `"0"` → leading-zero scan, `_` → digit
970    /// path), both paths funnel into ONE recognized fuelized
971    /// string-position scanner (`proof_recognize::detect_string_pos_scan`
972    /// — the same gate that makes the Lean backend synthesize the
973    /// scanner's `<fn>__fuel_scan` companion lemma), and the cone
974    /// bottoms out in `String.fromInt` / `Int.fromString`.
975    ///
976    /// The detector validates the ENTIRE canonical shape (arm literals,
977    /// arm order, scanner pins, finish-fn slice + `Int.fromString`
978    /// leaf), so the Lean emission can render the fixed
979    /// sign-split proof skeleton ported from the verified json hand
980    /// proof: serializer reduces by `rfl` (ADT-measure fuel),
981    /// `rcases Int.ofNat | Int.negSucc`, head-char dispatch via
982    /// `String.mk`-form `rfl`, `split` + `digitChar` contradiction
983    /// lemmas, the synthesized scan lemma, and `Int.fromString_fromInt`
984    /// at the `finish_int_fn` leaf. The whole emission is wrapped in
985    /// `first | (… ; done) | sorry` — a non-closing case degrades to a
986    /// caught honest `sorry`, never a build error, and `native_decide`
987    /// never appears. Dafny treats the pin as `BackendDispatch`
988    /// (exports byte-identical).
989    ///
990    /// Demonstrated by `examples/data/json.av`
991    /// `parseNumber.fromIntRoundtrip` — the first universal close
992    /// through the fuel-unfolding barrier on a string whose length is
993    /// symbolic.
994    IntDecimalRoundtrip {
995        /// Subject parser fn (`parseNumber`). Source names throughout.
996        parse_fn: String,
997        /// `"-"`-arm continuation (`parseNumberSign`).
998        neg_fn: String,
999        /// Wildcard-arm digit dispatcher (`startNumberDigits`).
1000        pos_fn: String,
1001        /// Sign path's digit dispatcher (`startSignDigit`).
1002        sign_fn: String,
1003        /// The recognized fuelized scanner (`scanIntTail`).
1004        scanner_fn: String,
1005        /// The scanner's char-class predicate (`isDigit`).
1006        predicate_fn: String,
1007        /// Scanner exit continuation (`finishNumber`).
1008        finish_fn: String,
1009        /// Int leaf — slices + `Int.fromString` (`finishInt`).
1010        finish_int_fn: String,
1011        /// Serializer the law's lhs feeds the parser (`toString`).
1012        serializer_fn: String,
1013    },
1014    /// String escape/parse roundtrip over the canonical
1015    /// segment-chunking string scanner: the law states
1016    /// `parse(<open> + escape(s) + <terminator>, 1) =
1017    /// Ok(StrCtor(s), String.len(escape(s)) + 2)` for an unconstrained
1018    /// `given s: String` (or the same claim entered at the scanner
1019    /// itself with `pos = segmentStart = 1, chunks = []`). The
1020    /// producer is a per-char classifier fold (two-char escape table +
1021    /// hex control escapes + printable passthrough); the consumer is a
1022    /// fuel mutual SCC (scan / escape-dispatch / validate / unicode
1023    /// chain) whose per-arm shapes the detector validates EXACTLY —
1024    /// see [`StringEscapeRoundtripPin`] for every captured name and
1025    /// literal, and `proof_lower::string_escape_roundtrip` for the
1026    /// gates.
1027    ///
1028    /// The Lean emission renders the suffix-invariant proof skeleton
1029    /// ported from the verified json hand proof (kernel-checked on
1030    /// Lean 4.15, #print axioms = [propext, Quot.sound]): a
1031    /// drop-form suffix-cursor prelude, the producer fold's
1032    /// accumulator homomorphism, one step lemma per consumer fuel
1033    /// arm, and a chunk invariant with the carried scanner state
1034    /// (segmentStart, chunks) universally quantified, closed by
1035    /// per-char classification. Every synthesized lemma carries a
1036    /// `first | (…; done) | sorry` floor — a template regression
1037    /// degrades to caught honest sorries (loud budget red), never a
1038    /// build error, and `native_decide` never appears. Dafny treats
1039    /// the pin as `BackendDispatch` (exports byte-identical).
1040    ///
1041    /// Demonstrated by `examples/data/json.av`
1042    /// `escapeJsonString.parseStringRoundtrip` and
1043    /// `parseStringChunk.escapedStringRoundtrip` — the parser
1044    /// workhorse pair that closes json's pinned Lean budget to 0.
1045    StringEscapeRoundtrip(Box<StringEscapeRoundtripPin>),
1046    /// Unconditional ring identity over Int-component records — the
1047    /// algebra-law family of an exact-rationals library (a record
1048    /// with Int numerator/denominator fields, non-normalizing
1049    /// arithmetic, equality by cross-multiplication): add/mul
1050    /// commutativity and associativity, distributivity, neg/sub
1051    /// normal forms, identity elements. The law has no `when`, every
1052    /// given is `Int` or a record whose fields are ALL `Int` (at
1053    /// least one such record given), and the claim's whole unfold
1054    /// cone is non-recursive pure arithmetic — record constructions
1055    /// / field projections, Int literals, `+`, `-`, `*`, unary
1056    /// negation — with the equality bottoming out in Int `==`
1057    /// (a Bool comparator fn applied at the law root, compared to
1058    /// `true`) or direct value equality of two such arithmetic
1059    /// expressions. Both sides are then polynomial identities:
1060    /// distributing products over sums and AC-normalizing monomials
1061    /// and sums makes the two sides' monomial multisets identical,
1062    /// no coefficient collection needed.
1063    ///
1064    /// The Lean emit is `intro <givens>; first | (simp [<unfold
1065    /// cone>, <fixed core AC-ring lemma package>]; done) | sorry` —
1066    /// the honest caught-`sorry` floor; never `native_decide`, never
1067    /// a build error. The package is SCOPED TO THIS STRATEGY's
1068    /// emission: its permutational rewrites (`Int.mul_comm`,
1069    /// `Int.add_comm`, …) loop or destroy the normal forms other
1070    /// strategies' simp sets rely on, so they are never added to the
1071    /// shared prelude registry. Dafny needs no special handling —
1072    /// Z3 decides these nonlinear identities push-button — and
1073    /// treats the pin like `BackendDispatch` (exports stay
1074    /// byte-identical). Demonstrated by `examples/data/rational.av`.
1075    RingIdentity {
1076        /// Ordered fn unfold list — law subject fn first, then the
1077        /// transitively-reached callees (sorted). Source names;
1078        /// backends translate to their lemma vocabulary.
1079        unfold_fns: Vec<String>,
1080    },
1081    /// Nonnegativity / order over a nonlinear Int product (`E >= 0`, or
1082    /// `prod <= prod`) — the inequality sibling of [`RingIdentity`], and
1083    /// the Newton-Raphson error-bound family of the K5 division proof. The
1084    /// claim is `subject(args) = true` for a pure non-recursive `Bool` fn
1085    /// whose body is a nonnegativity `E >= 0` or a product-order comparison
1086    /// `L <= R` (both sides products) over a pure-Int product cone; the law
1087    /// MAY carry a `when` premise constraining the factor signs (it is
1088    /// threaded into the universal statement as a hypothesis). The Lean
1089    /// backend renders it as one generic decision step — the shipped
1090    /// prelude tactic `aver_int_order`, the nonlinear analog of `omega`
1091    /// for the products-and-squares fragment (recurse with `Int.mul_nonneg`
1092    /// / `Int.mul_le_mul`, bottom squares out on `aver_sq_nonneg`, discharge
1093    /// the premise leaves with `omega`) — NOT a per-figure template; an
1094    /// honest `sorry` floor keeps credit fail-closed. A `prod <= var`
1095    /// transitivity figure is NOT admitted (it needs a `≤`-chain witness
1096    /// this step does not synthesize) and keeps its bounded fallback. Dafny
1097    /// needs no special handling (Z3 carries nonlinear arithmetic
1098    /// push-button) and treats the pin like `BackendDispatch`. Demonstrated
1099    /// by `projects/k5_fdiv/domain/estimate.av`.
1100    NonlinearNonneg {
1101        /// Ordered fn unfold list — subject fn first, then the
1102        /// transitively-reached pure-Int callees (sorted). Source names.
1103        unfold_fns: Vec<String>,
1104    },
1105    /// Floor-division window family — laws over a power-of-two fn
1106    /// (`match n <= 0 { true -> 1; false -> 2 * pow(n - 1) }`), a
1107    /// floor-halving binary-exponent fn (the
1108    /// [`RecursionContract::WellFoundedToNat`] class with divisor 2),
1109    /// and the scaled-significand / bit-width window predicates built
1110    /// from them. Each [`FloorWindowFigure`] is a fully-validated
1111    /// shape with a fixed proof template on both backends (Lean: the
1112    /// core `Int.le_ediv_iff_mul_le` / `Int.ediv_lt_iff_lt_mul`
1113    /// floor bridges + power algebra by functional induction; Dafny:
1114    /// a proved division-window prelude + branch-split helper
1115    /// lemmas). The recognizers are deliberately narrow — exactly the
1116    /// hand-validated figures; everything else declines and keeps
1117    /// the prior emission.
1118    FloorDivWindow { figure: FloorWindowFigure },
1119    /// No automated strategy — emit with `sorry` (Lean) / `assume
1120    /// {:axiom}` (Dafny). User fills in manually.
1121    Sorry,
1122    /// Lowerer has not pinned a strategy for this law; the backend's
1123    /// `or_else` chain decides. Today reached by linear-recurrence-
1124    /// spec equivalence (Lean-specific, ~50-line support theorems
1125    /// stay in the backend) and the sampled / guarded-domain
1126    /// fallback. The backend treats `BackendDispatch` as "fall
1127    /// through to ad-hoc strategy chain"; pinned variants above
1128    /// short-circuit to a known emit.
1129    BackendDispatch,
1130}
1131
1132/// The recognized figures of [`ProofStrategy::FloorDivWindow`]. All
1133/// fn names are source names; backends translate. Every figure's
1134/// quantifier names come from the law's givens (captured implicitly —
1135/// backends render them through the law's own given list).
1136#[derive(Debug, Clone, PartialEq, Eq)]
1137pub enum FloorWindowFigure {
1138    /// `pow(n) >= 1 => true` with no premise — positivity of the
1139    /// power-of-two fn, by functional induction.
1140    PowPositive { pow_fn: String },
1141    /// `when m >= 0; n >= 0 -> pow(m + n) == pow(m) * pow(n)` — the
1142    /// power homomorphism, by functional induction on the first
1143    /// exponent.
1144    PowSumSplit { pow_fn: String },
1145    /// `when b >= 1; a >= b; n >= 1 -> window(a, b, n) == true`
1146    /// where `window` checks `pow(n-1) <= sig(a,b,n) < pow(n)`,
1147    /// `sig` scales by `pow(n-1-e)` and floor-divides, and `e` is
1148    /// the floor-halving binary exponent of a/b.
1149    SigWindow {
1150        pow_fn: String,
1151        halve_fn: String,
1152        exp_fn: String,
1153        sig_fn: String,
1154        window_fn: String,
1155    },
1156    /// `when fits(j, m); fits(k, n) -> claim(j, k, m, n) == true`
1157    /// where `fits` is the `pow(m-1) <= j < pow(m)` window predicate
1158    /// and `claim` states the product window
1159    /// `pow(m+n-2) <= j*k < pow(m+n)`.
1160    ProductWindow {
1161        pow_fn: String,
1162        fits_fn: String,
1163        claim_fn: String,
1164    },
1165    /// The recursive-expo-FREE Euclidean floor window over a
1166    /// power-of-two divisor: `window(args) == true` (no premise) where
1167    /// `window`'s body is
1168    /// `pow(E) * floor(N, pow(E)) <= N  &&  N < pow(E) * (floor(N, pow(E)) + 1)`
1169    /// for ARBITRARY numerator expression `N` and exponent expression
1170    /// `E` over the law's givens, `floor` the `Result.withDefault(
1171    /// Int.div(a, d), 0)` Euclidean-floor wrapper, and `pow` any
1172    /// [`is_pow2_shape`] fn. Unlike [`SigWindow`] there is NO binary-
1173    /// exponent recursion — the divisor is `pow(E)` directly — so the
1174    /// figure is generic over `N`/`E` (a bare-given `floorDivWindow(a,
1175    /// k)` and a compound `truncFitsWindow(f, i)` both match). Closed by
1176    /// the core `Int.ediv_add_emod` / `Int.emod_nonneg` /
1177    /// `Int.emod_lt_of_pos` bridge plus power-of-two positivity, generic
1178    /// over the inferred `N`/`pow(E)`. `pow_fn`/`floor_fn` are the
1179    /// (possibly module-qualified) dotted call names; `window_fn` is the
1180    /// law's subject predicate.
1181    FloorPow2Window {
1182        pow_fn: String,
1183        floor_fn: String,
1184        window_fn: String,
1185    },
1186    /// The exact-division cancel sibling of [`FloorPow2Window`]: the
1187    /// EQUATIONAL fact that flooring a manifest multiple of a power of two
1188    /// by that power is exact. The law claim is
1189    /// `floor(s * pow(b), pow(a)) == s * pow(b - a)` with premise
1190    /// `0 <= a && a <= b` — the divisor `pow(a)` divides the dividend
1191    /// `s * pow(b)` because `pow(a) | pow(b)` when `a <= b` (the power-of-two
1192    /// homomorphism `pow(b) = pow(a) * pow(b - a)`), so the Euclidean floor
1193    /// returns the exact quotient `s * pow(b - a)`. Generic over the integer
1194    /// `s` and the two exponents `a`/`b` — "the exact cancel works for any
1195    /// provably-dividing floorDiv". Closed by the homomorphism split plus
1196    /// `Int.mul_ediv_cancel_left` (core, no Mathlib), the same power algebra
1197    /// every floor-window figure proves. `pow_fn`/`floor_fn` are the
1198    /// (possibly module-qualified) dotted call names; `cancel_fn` is the
1199    /// law's subject (the floor wrapper itself), so the lemma sits in the
1200    /// call cone of any rounding-composition law and the keystone cites it
1201    /// by name. Demonstrated by Lemmas 7.2.10 / 7.2.11 of the K5 division
1202    /// proof (`projects/k5_fdiv/domain/round.av`).
1203    FloorPow2Cancel {
1204        pow_fn: String,
1205        floor_fn: String,
1206        cancel_fn: String,
1207    },
1208}
1209
1210/// Parameter pack for [`ProofStrategy::StringEscapeRoundtrip`] —
1211/// every fn name and literal the Lean renderer's suffix-invariant
1212/// proof skeleton quotes. All fn names are source names (backends
1213/// translate); all chars/codes are the SOURCE literals the detector
1214/// read off the validated arm patterns, so the renderer can rebuild
1215/// them as Lean literals without re-walking the AST.
1216#[derive(Debug, Clone)]
1217pub struct StringEscapeRoundtripPin {
1218    /// The scanner SCC member the law enters (`parseStringChunk`).
1219    /// Body: charAt dispatch over { terminator → finish, escape-char
1220    /// → escape dispatch, default → validate }.
1221    pub scan_fn: String,
1222    /// Escape dispatcher (`parseEscape`): slices the open segment,
1223    /// then maps escape letters to decoded chars / the unicode hop.
1224    pub escape_fn: String,
1225    /// Default-arm validator (`validateChar`): control chars error,
1226    /// printable chars extend the open segment.
1227    pub validate_fn: String,
1228    /// Terminator continuation (`finishString`): slice + join + Ok.
1229    pub finish_fn: String,
1230    /// `\uXXXX` reader head (`parseUnicode`): readHex4 + codepoint.
1231    pub unicode_fn: String,
1232    /// Codepoint surrogate filter (`parseUnicodeCodePoint`).
1233    pub codepoint_fn: String,
1234    /// Decoded-codepoint continuation (`applyCodePoint`):
1235    /// `Char.fromCode` + chunk flush back into the scanner.
1236    pub apply_fn: String,
1237    /// Four-hex-digit reader (`readHex4`), separately fueled on
1238    /// `count` climbing to the literal bound 4.
1239    pub read_hex_fn: String,
1240    /// User hex-digit valuation (`hexVal : String -> Option<Int>`).
1241    pub hex_val_fn: String,
1242    /// High-surrogate guard (`isHighSurrogate`): `cp >= MIN && …`.
1243    pub high_surrogate_fn: String,
1244    /// Low-surrogate guard (`isLowSurrogate`).
1245    pub low_surrogate_fn: String,
1246    /// Producer wrapper (`escapeJsonString`): `fold(String.chars(s), "")`.
1247    pub producer_fn: String,
1248    /// Producer accumulator fold (`escapeJsonChars`).
1249    pub fold_fn: String,
1250    /// Per-char classifier (`escapeJsonChar`): two-char escape table
1251    /// + default to the control classifier.
1252    pub classifier_fn: String,
1253    /// Control classifier (`escapeControlChar`): equality ladder +
1254    /// `code < threshold → control escape` + printable passthrough.
1255    pub control_fn: String,
1256    /// Hex control escape (`controlCodeEscape`): `Byte.toHex` +
1257    /// 4-char prefix.
1258    pub control_escape_fn: String,
1259    /// Success ctor of the law's rhs, source spelling
1260    /// (`"ParseResult.Ok"`).
1261    pub ok_ctor: String,
1262    /// String-payload ctor inside the success ctor
1263    /// (`"Json.JsonString"`).
1264    pub str_ctor: String,
1265    /// Scan terminator char (`'"'` — the finish arm's literal).
1266    pub terminator: char,
1267    /// Escape introducer char (`'\\'` — the escape arm's literal,
1268    /// also the first char of every two-char escape output).
1269    pub escape_char: char,
1270    /// Hex-escape letter (`'u'` — second char of the control-escape
1271    /// prefix, the consumer's unicode arm literal).
1272    pub unicode_letter: char,
1273    /// Two-char escape table, producer-derived and consumer-aligned.
1274    pub pairs: Vec<EscapePairSpec>,
1275    /// Control threshold (`32`): producer hex-escapes below it, the
1276    /// consumer validator rejects below it. Gated `<= 256`.
1277    pub control_threshold: i64,
1278    /// `cp >= MIN` bound of the high-surrogate guard (`55296`).
1279    pub high_surrogate_min: i64,
1280    /// `cp >= MIN` bound of the low-surrogate guard (`56320`).
1281    /// The Lean renderer probes the scanner SCC's emitted
1282    /// `averStringPosFuel` rank itself (it must match the emission
1283    /// byte-for-byte), so the rank is deliberately NOT pinned here.
1284    pub low_surrogate_min: i64,
1285}
1286
1287/// One two-char escape: the producer emits `[escape_char, letter]`
1288/// for `decoded`; the consumer's escape dispatcher maps `letter`
1289/// back to `decoded`.
1290#[derive(Debug, Clone)]
1291pub struct EscapePairSpec {
1292    /// The unescaped source char (`'\n'`).
1293    pub decoded: char,
1294    /// The escape letter following the escape introducer (`'n'`).
1295    pub letter: char,
1296    /// `true` when the pair comes from the control classifier's
1297    /// equality ladder (`code == 8 → "\\b"`), `false` for a
1298    /// classifier literal arm (`"\n" → "\\n"`). Drives which
1299    /// disequality form the chunk-invariant ladder cases on
1300    /// (`c.toNat = K` vs `c = '<lit>'`).
1301    pub from_control_ladder: bool,
1302}
1303
1304/// Discriminator for [`ProofStrategy::MapUpdatePostcondition`].
1305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1306pub enum MapUpdatePostconditionKind {
1307    /// Law shape: `Map.has(outer(m, k), k) == true`.
1308    HasAfter,
1309    /// Law shape: `Map.get(outer(m, k), k) == Option.Some(...)`.
1310    GetAfter,
1311}
1312
1313/// A bool predicate with explicit free-variable context. Stays in
1314/// `Spanned<Expr>` form so backends can route through their
1315/// existing `emit_expr` paths; the context is what gives backends
1316/// the information they need to project (e.g. `.val`) without
1317/// re-walking the AST.
1318#[derive(Debug, Clone)]
1319pub struct Predicate {
1320    /// Variables the predicate may reference, in declaration order.
1321    /// Each entry tells the backend what type the var has in the
1322    /// target language — same logic as `Quantifier.binder_type`.
1323    pub free_vars: Vec<(String, QuantifierType)>,
1324    /// The expression. Already in the target variable space (e.g.
1325    /// caller-derived predicates have had caller-arg names
1326    /// substituted to callee-param names).
1327    pub expr: Spanned<crate::ir::hir::ResolvedExpr>,
1328}
1329
1330#[cfg(test)]
1331mod tests {
1332    use super::*;
1333    use crate::ir::hir::ResolvedExpr;
1334    use crate::ir::interval::{Interval, OpClass};
1335
1336    /// A throwaway invariant predicate — `raw_i64_eligible` never reads
1337    /// it (it derives only from the persisted `interval` / `op_classes`),
1338    /// so a trivial `n` stub is enough to build a `RefinedTypeDecl`.
1339    fn stub_predicate() -> Predicate {
1340        Predicate {
1341            free_vars: vec![("n".to_string(), QuantifierType::Plain("Int".to_string()))],
1342            expr: Spanned::new(ResolvedExpr::Ident("n".to_string()), 0),
1343        }
1344    }
1345
1346    /// Build a `RefinedTypeDecl` directly from the two persisted facts
1347    /// the recognizer reads, bypassing the pipeline entirely.
1348    fn decl(interval: Option<Interval>, ops: Vec<(&str, OpClass)>) -> RefinedTypeDecl {
1349        RefinedTypeDecl {
1350            name: "T".to_string(),
1351            carrier_type: "Int".to_string(),
1352            carrier_field: "value".to_string(),
1353            predicate_param: "n".to_string(),
1354            invariant: stub_predicate(),
1355            witness: Some("0".to_string()),
1356            interval,
1357            op_classes: ops.into_iter().map(|(n, c)| (n.to_string(), c)).collect(),
1358        }
1359    }
1360
1361    #[test]
1362    fn two_sided_fits_i64_all_overflow_free_is_eligible() {
1363        // The IntRange shape: [0,100], single `add` op OverflowFree.
1364        let d = decl(
1365            Some(Interval::between(0, 100)),
1366            vec![("add", OpClass::OverflowFree)],
1367        );
1368        assert!(d.raw_i64_eligible());
1369    }
1370
1371    #[test]
1372    fn one_overflow_free_one_unbounded_is_not_eligible() {
1373        // ALL ops must be OverflowFree — a single Unbounded op demotes.
1374        let d = decl(
1375            Some(Interval::between(0, 100)),
1376            vec![
1377                ("add", OpClass::OverflowFree),
1378                ("scaledAdd", OpClass::Unbounded),
1379            ],
1380        );
1381        assert!(!d.raw_i64_eligible());
1382    }
1383
1384    #[test]
1385    fn one_needs_wider_scratch_is_not_eligible() {
1386        let d = decl(
1387            Some(Interval::between(0, 100)),
1388            vec![("widePath", OpClass::NeedsWiderScratch)],
1389        );
1390        assert!(!d.raw_i64_eligible());
1391    }
1392
1393    #[test]
1394    fn two_sided_interval_not_fitting_i64_is_not_eligible() {
1395        // [0, i64::MAX + 1] is two-sided and finite but exceeds i64, so
1396        // the carrier could not be stored in a machine word.
1397        let d = decl(
1398            Some(Interval::between(0, i64::MAX as i128 + 1)),
1399            vec![("add", OpClass::OverflowFree)],
1400        );
1401        assert!(!d.raw_i64_eligible());
1402    }
1403
1404    #[test]
1405    fn declined_interval_none_is_not_eligible() {
1406        // `interval: None` is the analysis's conservative decline (an
1407        // unrecognized invariant shape) — never eligible.
1408        let d = decl(None, vec![("add", OpClass::OverflowFree)]);
1409        assert!(!d.raw_i64_eligible());
1410    }
1411
1412    #[test]
1413    fn one_sided_interval_is_not_eligible() {
1414        // A `Natural`-shaped [0, +inf]: `fits_i64` is false because the
1415        // upper bound is open, so the type is rejected even with no ops.
1416        let d = decl(Some(Interval::ge(0)), vec![]);
1417        assert!(!d.raw_i64_eligible());
1418    }
1419
1420    #[test]
1421    fn two_sided_fits_i64_empty_ops_is_eligible() {
1422        // Empty op_classes: a finite-i64 interval with no carrier-reading
1423        // arithmetic. Decision: ELIGIBLE — storage fits i64 and the
1424        // all-OverflowFree check over an empty op set is vacuously true,
1425        // so there is no op that could wrap a raw i64. See the doc-comment
1426        // on `raw_i64_eligible` for the full reasoning.
1427        let d = decl(Some(Interval::between(0, 100)), vec![]);
1428        assert!(d.raw_i64_eligible());
1429    }
1430}