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 /// Well-founded native def on `param.toNat` — graduates a fn out
261 /// of the fuel/partial encoding so it stays kernel-transparent
262 /// (Lean: `termination_by param.toNat` + a `decreasing_by` the
263 /// kernel re-checks; Dafny: `decreases if param >= 0 then param
264 /// else 0` with NO synthesized `requires`, so total callers stay
265 /// wellformed). Two validated sources:
266 ///
267 /// - `floor_div: Some(..)` — every self-call shrinks `param` by a
268 /// literal-divisor floor division
269 /// (`Result.withDefault(Int.div(p, k), d)` with literal k >= 2,
270 /// possibly through a unary wrapper fn), and the classifier
271 /// verified the guard chain enclosing every self-call site
272 /// implies `p >= 1` — so `p / k < p` and the measure strictly
273 /// drops. Never guessed: a fn whose guards don't justify the
274 /// shrink keeps its prior (partial/opaque) emission.
275 /// - `floor_div: None` — guard-protected subtractive countdown
276 /// (`p - k`, literal k >= 1, guards imply `p >= 1`), graduated
277 /// out of fuel on demand by the floor-division window law
278 /// family, whose proof templates need the fn's defining
279 /// equations and functional-induction principle.
280 WellFoundedToNat {
281 /// The decreasing Int parameter (source name).
282 param: String,
283 /// `Some` for the floor-division shrink; `None` for the
284 /// guarded subtractive countdown.
285 floor_div: Option<FloorDivShrink>,
286 },
287}
288
289/// Payload of [`RecursionContract::WellFoundedToNat`] for the
290/// floor-division shrink shape.
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct FloorDivShrink {
293 /// The literal divisor (>= 2).
294 pub divisor: i64,
295 /// `Some(name)` when the self-call shrinks through a unary
296 /// wrapper fn whose body is exactly
297 /// `Result.withDefault(Int.div(x, divisor), <int literal>)`;
298 /// `None` when the `Result.withDefault(Int.div(p, k), d)` call
299 /// is inlined at the self-call site. Lean's `decreasing_by`
300 /// unfolds the wrapper by name.
301 pub helper_fn: Option<String>,
302}
303
304/// Body decomposition for the `IntCountdown-literal-zero` native
305/// shape. Each field is a slice of the source AST the lowerer
306/// extracted while classifying; backends render them directly
307/// without re-walking the source.
308#[derive(Debug, Clone)]
309pub struct NativeIntCountdownBody {
310 /// The literal int that selects the base arm. Always `0` today;
311 /// future preservation proofs may admit other literals, so the
312 /// value is carried as data rather than baked into the marker.
313 pub base_arm_literal: i64,
314 /// AST for the base arm's body (`match p { 0 -> THIS; _ -> ... }`).
315 pub base_arm_body: Spanned<crate::ir::hir::ResolvedExpr>,
316 /// AST for the wildcard arm's body — the recursive call site.
317 pub wildcard_arm_body: Spanned<crate::ir::hir::ResolvedExpr>,
318}
319
320/// Fuel metric for the fallback fuel-encoded emit path.
321#[derive(Debug, Clone)]
322pub enum FuelMetric {
323 /// `n.natAbs + 1` — classic IntCountdown fuel.
324 NatAbsPlusOne { param: String },
325 /// `(bound - n).natAbs + 1` — IntAscending: param climbs toward
326 /// a bound expression. Backends render the bound through their
327 /// own `Spanned<Expr>` emitter (Lean: `bound_expr_to_lean`,
328 /// Dafny: `emit_expr` over int subset).
329 BoundMinusParamNatAbsPlusOne {
330 param: String,
331 bound: Spanned<crate::ir::hir::ResolvedExpr>,
332 },
333 /// `xs.length + 1` — List/String structural recursion.
334 SeqLenPlusOne { param: String },
335 /// `sizeOf(x) + 1` — structural recursion on a user-defined
336 /// recursive ADT (e.g. `Term::App(f, arg)`). The classifier
337 /// doesn't pin the bound param — sizeOf walks the whole call
338 /// frame — so this variant carries no param name.
339 SizeOfPlusOne,
340 /// `s.length - pos` — StringPosAdvance: a `String` carrier stays
341 /// invariant, an `Int` position climbs toward its length.
342 StringLenMinusPos {
343 string_param: String,
344 pos_param: String,
345 },
346 /// Lexicographic pair for mutual recursion SCCs.
347 Lex { params: Vec<String>, rank: usize },
348}
349
350/// Symbolic termination measure. Backend-agnostic.
351#[derive(Debug, Clone)]
352pub enum Measure {
353 NatAbsInt { param: String },
354 SeqLen { param: String },
355 Lex(Vec<Measure>),
356}
357
358/// Marker that the lowerer constructed a proof of preservation
359/// (recursive args stay in the precondition's domain). The variants
360/// describe HOW the proof was constructed so future maintainers can
361/// trace why a given shape was accepted as native.
362#[derive(Debug, Clone)]
363pub enum PreservationProof {
364 /// `match p { 0 -> base; _ -> rec(p-1, ...) }` under `p ≥ 0`
365 /// precondition. Wildcard arm gives `p ≠ 0`, combined with
366 /// `p ≥ 0` yields `p ≥ 1`, so `p - 1 ≥ 0`.
367 IntCountdownLiteralZero,
368}
369
370/// Symmetric marker for the decreasing measure.
371#[derive(Debug, Clone)]
372pub enum DecreaseProof {
373 /// `natAbs(p - 1) < natAbs(p)` under `p ≥ 0 ∧ p ≠ 0`.
374 NatAbsCountdown,
375}
376
377/// Lowered verify-law theorem. All projection decisions (`.val`
378/// vs bare ident, wrapper strip, when-keep vs when-drop) are
379/// already baked into the fields below; backends render directly.
380#[derive(Debug, Clone)]
381pub struct LawTheorem {
382 /// Opaque identity of the fn this law targets, resolved through
383 /// `SymbolTable` at populate time (phase E3). Verify laws are
384 /// entry-only per the current model, so this is effectively
385 /// always an entry-scope `FnId` today; once laws-in-modules
386 /// lands the same `FnId` will distinguish two same-bare-name
387 /// recursive fns across modules without any per-callsite scope
388 /// plumbing.
389 pub fn_id: FnId,
390 pub law_name: String,
391 pub quantifiers: Vec<Quantifier>,
392 /// Premises in order. Already includes `when` if it carries
393 /// information beyond the refinement invariants (the lowerer
394 /// performs the bijective syntactic equivalence check).
395 pub premises: Vec<Predicate>,
396 /// LHS = RHS claim. Wrapper-stripped, lifted-var-aware (bare
397 /// idents for arg positions, `.val` projections inside
398 /// comparator BinOps if the lowerer determined this is needed).
399 pub claim_lhs: Spanned<crate::ir::hir::ResolvedExpr>,
400 pub claim_rhs: Spanned<crate::ir::hir::ResolvedExpr>,
401 pub strategy: ProofStrategy,
402}
403
404/// A universally-quantified variable in a law theorem. Carries
405/// enough type info for backends to render the binder correctly
406/// (`(a : Natural)` for refined Int, `(a : Int)` for plain int,
407/// `(rng : RandomIntInBounds)` for oracle).
408#[derive(Debug, Clone)]
409pub struct Quantifier {
410 pub name: String,
411 pub binder_type: QuantifierType,
412}
413
414#[derive(Debug, Clone)]
415pub enum QuantifierType {
416 /// Plain Aver type, rendered as-is on each backend.
417 Plain(String),
418 /// Refinement-lifted: source declared `given a: Int`, body used
419 /// `Natural(value = a)`, so the quantifier binds at the refined
420 /// type. The carried `refined_type` key looks up in
421 /// `ProofIR.refined_types`.
422 RefinedTo { refined_type: String },
423 /// Oracle subtype: classified Generative-shape effect-givens
424 /// bind oracles wrapped in a subtype carrier
425 /// (`RandomIntInBounds`, `RandomFloatInUnit`,
426 /// `TimeUnixMsNonneg`).
427 OracleSubtype(String),
428}
429
430/// Algebraic / proof-theoretic shape of a verify-law theorem.
431///
432/// **Naming rule**: variants describe **what the law says**, not
433/// **how a backend proves it**. The IR is target-agnostic — Lean
434/// maps `Commutative { op: Add }` to `simp [fn, Int.add_comm]`,
435/// Dafny maps the same variant to its own lemma vocabulary, a Z3
436/// backend could ship a different tactic again. Tactic names
437/// (`SimpOverLemmas`, `simp+omega`) do not appear in variant names;
438/// `LinearArithmetic` is named for the semantic, not the tactic.
439#[derive(Debug, Clone)]
440pub enum ProofStrategy {
441 /// `rfl` / definitional equality — `lhs ≡ rhs` syntactically.
442 Reflexive,
443 /// `simp` chain over named lemmas. The discovery feedback loop
444 /// (`lemma_discovery::committed`) pins this when a committed
445 /// `DiscoveredLemmas.lean` holds kernel-proved lemmas in-scope
446 /// for an `Induction` law: the names are the discovered theorem
447 /// names, and the Lean renderer reuses the induction ladder with
448 /// those lemmas embedded + joined to its simp sets. Pinned by
449 /// the CLI (post-lowering re-pin), never by
450 /// `classify_law_strategy` — discovery feedback is opt-in via
451 /// the committed artifact.
452 SimpOverLemmas(Vec<String>),
453 /// `∀ a b, f(a, b) = f(b, a)` — commutativity of the law's fn,
454 /// whose body reduces to `a <op> b`. The `op` tag lets backends
455 /// pick their own lemma vocabulary (Lean: `Int.add_comm`,
456 /// Dafny: built-in arithmetic axioms).
457 Commutative { op: crate::ast::BinOp },
458 /// `∀ a b c, f(f(a,b),c) = f(a,f(b,c))` — associativity of `f`.
459 Associative { op: crate::ast::BinOp },
460 /// `∀ a, f(a, e) = a` (or the swapped `f(e, a) = a`) — the
461 /// identity-element law for the underlying op (`e` = `0` for
462 /// Add / Sub, `1` for Mul). Backends emit `simp [fn]` (the
463 /// wrapper's body unfolds to the identity equation, which simp
464 /// closes); the variant doesn't need a `side` field because
465 /// the emit is symmetric — Sub is naturally one-sided (only
466 /// right-identity), Add/Mul accept either side. The lowerer
467 /// guarantees the law's actual shape matches the op's identity
468 /// behaviour before pinning.
469 IdentityElement { op: crate::ast::BinOp },
470 /// `∀ a b, f(a, b) = -f(b, a)` (or the swapped negation).
471 /// `neg_on_rhs` records which side carries the `-` wrap so
472 /// backends with directional lemmas (Lean's `Int.neg_sub b a :
473 /// -(b - a) = a - b`) can flip via `.symm` correctly.
474 AntiCommutative {
475 op: crate::ast::BinOp,
476 /// `true` for `f(a, b) = -f(b, a)` (negation on rhs);
477 /// `false` for the swapped arrangement.
478 neg_on_rhs: bool,
479 },
480 /// `∀ a, g(a) = f(a, c)` or `f(c, a)` — the unary fn `g` is
481 /// the binary fn `f` with one argument bound to constant `c`.
482 /// Backends unfold both fns to expose the underlying op; the
483 /// IR carries `inner_fn` (the binary's source name) so the
484 /// unfold list is unambiguous.
485 UnaryEqualsBinary {
486 /// Source-level name of the binary fn the unary one equals.
487 inner_fn: String,
488 },
489 /// "Linear arithmetic over an unfold chain" — the law's two
490 /// sides reduce to a flat linear equation on Int once every
491 /// reachable user fn is unfolded. Generic catch-all for Int
492 /// laws that don't fit a named algebraic property. The IR
493 /// captures the unfold list + wrapper-return signal +
494 /// refinement smart-constructor guard; backends translate to
495 /// their decision procedure (Lean: `simp + omega`, Dafny: Z3
496 /// linear int prover). Named for the **semantic** ("linear
497 /// arithmetic"), not the Lean tactic.
498 LinearArithmetic {
499 /// Ordered fn unfold list. Top-level law fn first — Lean's
500 /// `unfold` resolves left-to-right and the call layer the
501 /// tactic peels at each step must match the goal shape.
502 unfold_fns: Vec<String>,
503 /// `true` when at least one fn in `unfold_fns` returns a
504 /// wrapper (Result, Option, …). Drives extra case-split
505 /// machinery in the emit — pure linear-arithmetic provers
506 /// can't close constructor-equality goals, so the wrapper
507 /// case splits on the smart-constructor guard first.
508 wrapper_return: bool,
509 /// Smart-constructor guard pulled from a refinement
510 /// `fromX(p: Int) -> Result<X, _>` in the unfold chain.
511 /// `Some` when one was found; `None` falls back to a
512 /// conservative `(n ≥ 0)` default when `wrapper_return`
513 /// forces case-splitting.
514 smart_guard: Option<SmartGuard>,
515 /// `true` when at least one law given is lifted to a
516 /// refinement type (`given a: Int` used as `Refined(value
517 /// = a)` in the law body). The Subtype/subset lift carries
518 /// the invariant in the type, so the by_cases case-split
519 /// that `wrapper_return` would otherwise force is
520 /// unnecessary — backends emit a plain unfold + simp
521 /// against arithmetic lemmas.
522 lifted: bool,
523 },
524 /// Structural induction on a recursive ADT parameter.
525 Induction { param: String },
526 /// Library axiom instance — the law instantiates a named
527 /// data-structure axiom (e.g. AverMap's `has_set_self` or
528 /// `get_set_self`). Backends map the axiom name to their
529 /// lemma vocabulary (Lean: `AverMap.has_set_self`; Dafny:
530 /// its own set/lookup axioms; Z3: built-in array theory).
531 /// Args carry the call-site expressions the axiom applies to.
532 LibraryAxiom {
533 /// Canonical axiom name. Recognised values today:
534 /// `"Map.has_set_self"`, `"Map.get_set_self"`. Open string
535 /// so future axioms (List, Set, Array, …) extend without
536 /// enum churn.
537 axiom: String,
538 /// Arguments in the order the axiom expects. For Map
539 /// axioms: `[m, k, v]` (the map, key, value the axiom
540 /// reasons about).
541 args: Vec<Spanned<crate::ir::hir::ResolvedExpr>>,
542 },
543 /// Post-condition of an inline-defined map-update fn. The outer
544 /// fn `outer(m, k)` has body shape `let v = Map.get m k; match v
545 /// { Some(_) -> Map.set m k _; None -> Map.set m k _ }` — i.e. it
546 /// inspects the existing value and writes some new value at key
547 /// `k` in every arm. The law asserts a post-condition on that
548 /// update — `Map.has(outer(m, k), k) == true` (`HasAfter`), or
549 /// `Map.get(outer(m, k), k) == Option.Some(...)` (`GetAfter`).
550 ///
551 /// Backends emit a 2-step proof: unfold the outer fn, case-split
552 /// on `Map.get m k` (the same value `outer` inspected), apply the
553 /// `Map.set`-axioms on each branch. Named after the law's
554 /// algebraic content, not the Lean tactic.
555 MapUpdatePostcondition {
556 /// Source name of the outer update fn.
557 outer_fn: String,
558 /// Which post-condition the law asserts.
559 kind: MapUpdatePostconditionKind,
560 /// The map argument as it appears at the law's call site.
561 map_arg: Spanned<crate::ir::hir::ResolvedExpr>,
562 /// The key argument as it appears at the law's call site.
563 key_arg: Spanned<crate::ir::hir::ResolvedExpr>,
564 /// Additional helper-fn source names to unfold on top of
565 /// `outer_fn` — only used for `GetAfter`, where the rhs's
566 /// `Option.Some(...)` typically wraps the prior value via a
567 /// pure user helper (e.g. `addOne(...)`). Source names;
568 /// backends translate to their lemma vocabulary.
569 extra_unfolds: Vec<String>,
570 },
571 /// Counter-increment specialisation of [`MapUpdatePostcondition`].
572 /// The outer fn `outer(m, k)` is the canonical "tracked counter"
573 /// shape:
574 ///
575 /// ```text
576 /// let v = Map.get m k
577 /// match v {
578 /// Some(n) -> Map.set m k (n + 1)
579 /// None -> Map.set m k 1
580 /// }
581 /// ```
582 ///
583 /// The law states the algebraic content:
584 /// `Option.withDefault(Map.get(outer(m, k), k), 0) ==
585 /// Option.withDefault(Map.get(m, k), 0) + 1` — get-or-default
586 /// after the increment equals the prior get-or-default plus 1.
587 /// Tighter than [`MapUpdatePostcondition`] because both the body
588 /// template AND the rhs `+ 1` shape are pinned.
589 MapKeyTrackedIncrement {
590 /// Source name of the outer increment fn.
591 outer_fn: String,
592 /// The map argument as it appears at the law's call site.
593 map_arg: Spanned<crate::ir::hir::ResolvedExpr>,
594 /// The key argument as it appears at the law's call site.
595 key_arg: Spanned<crate::ir::hir::ResolvedExpr>,
596 },
597 /// Functional equivalence between an impl fn and a (declared)
598 /// spec fn — the law states `impl(args) == spec(args)` and the
599 /// two fn bodies are syntactically identical (after typecheck).
600 /// Backends close the goal by unfolding both fns; their bodies
601 /// reduce to the same term and the equality holds by reflexivity
602 /// modulo simp normalisation. Lean emits `simpa [<unfolds>]`,
603 /// Dafny would reveal both and let Z3 prove the equivalence.
604 /// Named for the algebraic content (functional equivalence),
605 /// not the backend tactic.
606 SpecEquivalence {
607 /// All user fn source names to unfold — impl + spec + any
608 /// transitively-reached helpers from law sides. Source
609 /// names; backends translate to their lemma vocabulary.
610 extra_unfolds: Vec<String>,
611 },
612 /// Broader [`SpecEquivalence`] for cases where impl and spec
613 /// bodies are NOT syntactically identical but normalize to the
614 /// same expression under arg substitution + simp arithmetic
615 /// identity folding (`a + 0 == a`, `a * 1 == a`, `a * 0 ==
616 /// 0`). Backends close via `simp` (no `simpa` — there's no
617 /// trivial-rfl goal to discharge; simp normalisation does the
618 /// closing). Same `extra_unfolds` payload as `SpecEquivalence`.
619 SpecEquivalenceSimpNormalized {
620 /// All user fn source names to unfold — impl + spec + any
621 /// transitively-reached helpers from law sides.
622 extra_unfolds: Vec<String>,
623 },
624 /// Linear-Int spec equivalence — impl and spec bodies are both
625 /// linear arithmetic expressions over Int givens (only
626 /// `Literal::Int`, given idents, `Add`, `Sub`) after arg
627 /// substitution. Bodies may differ syntactically but the
628 /// equivalence is decidable by a linear-arithmetic solver
629 /// (Presburger / `omega` / Z3 LIA). Backends emit a `change
630 /// <impl_unfolded> = <spec_unfolded>` rewrite then close via
631 /// their decision procedure; the IR carries the substituted
632 /// expressions so the backend can render them via its own
633 /// `emit_expr`.
634 LinearIntSpecEquivalence {
635 /// Impl body with formal params substituted by call-site
636 /// args. Linear-arithmetic-only after substitution.
637 unfolded_impl: Spanned<crate::ir::hir::ResolvedExpr>,
638 /// Spec body with formal params substituted by call-site
639 /// args. Linear-arithmetic-only after substitution.
640 unfolded_spec: Spanned<crate::ir::hir::ResolvedExpr>,
641 },
642 /// Functional equivalence between an effectful impl fn and a
643 /// spec fn. Same "claim states `impl(args) == spec(args)`"
644 /// content as [`SpecEquivalence`], but the law's source-level
645 /// shape is non-canonical (impl call usually omits oracle args
646 /// the spec call carries explicitly). The lowerer runs an
647 /// Oracle Lift over both sides — injecting oracle args from
648 /// `given oracle: Random.int = ...` into every classified
649 /// effectful call site — and matches the canonical shape on the
650 /// rewritten form. Backends emit `simp [impl, spec]`; both
651 /// definitions unfold to the same oracle call after lifting.
652 EffectfulSpecEquivalence {
653 /// Source name of the impl fn (= `vb.fn_name`).
654 impl_fn: String,
655 /// Source name of the spec fn (the other side of the law).
656 spec_fn: String,
657 },
658 /// Second-order linear recurrence spec equivalence — impl is a
659 /// tail-recursive Int linear-pair wrapper (e.g. `fib` dispatching
660 /// on `n < 0` and calling a 3-arg `fibTR(n, 0, 1)` helper) and
661 /// spec is a direct second-order recurrence (`match n { 0 -> b0;
662 /// 1 -> b1; _ -> recurrence(spec(n-1), spec(n-2)) }`). The
663 /// impl's helper implements the same affine recurrence as the
664 /// spec's `_` arm. Both Lean and Dafny render via a Nat-keyed
665 /// helper + shift lemma + helper-seed bridge; the algebraic
666 /// content (a fixed-point of the recurrence) is the same in both
667 /// targets but the syntactic proof template differs per backend.
668 LinearRecurrence2SpecEquivalence {
669 /// Source name of the impl (tail-recursive wrapper) fn.
670 impl_fn: String,
671 /// Source name of the spec (direct recurrence) fn.
672 spec_fn: String,
673 /// Source name of the worker fn called by `impl_fn`.
674 helper_fn: String,
675 },
676 /// Bounded universal: case-split over the declared `given`
677 /// domain, dispatch each case to a per-sample lemma.
678 BoundedUniversal,
679 /// Two `MatchDispatcherFold` fns over the same `List<T>` param
680 /// compute the same result via slightly different but structurally
681 /// identical match-on-list bodies. The law states
682 /// `fold_fn(xs) == spec_fn(xs)`; the proof closes by induction on
683 /// `xs` with both nil and cons cases reducing to arithmetic
684 /// identities. Demonstrated by `examples/data/list_length_fold.av`
685 /// (`lengthFwd(xs) = 1 + lengthFwd(t)` vs
686 /// `lengthSwap(xs) = lengthSwap(t) + 1` — equivalent via
687 /// commutativity, closes via `omega`).
688 ///
689 /// Stage 8c of #232 — third typed-pattern consumer in
690 /// `proof_lower`.
691 MatchDispatcherFold {
692 /// Source name of the LHS fold fn (the law's surrounding fn).
693 fold_fn: String,
694 /// Source name of the RHS spec fn — also a list-fold of the
695 /// same shape.
696 spec_fn: String,
697 },
698 /// `?`-propagating Result chain equals a manual `match`-version:
699 /// the law states `chain_qm(x) == chain_manual(x)` where the
700 /// LHS uses `?` for short-circuit Err propagation and the RHS
701 /// writes the same flow as nested `match Result.Err -> Err`
702 /// arms. Both sides unfold to the same nested match; the proof
703 /// closes by unfolding all step fns and case-splitting on each
704 /// step's Result discriminator. Demonstrated by
705 /// `examples/core/result_chain.av`. Stage 8b of #232.
706 ResultPipelineChain {
707 /// Source name of the `?`-chain fn (the wrapper). LHS of the law.
708 chain_qm_fn: String,
709 /// Source name of the manual `match`-chain fn. RHS of the law.
710 chain_manual_fn: String,
711 /// Source names of every step fn the two chains thread
712 /// through, in pipeline order. Drives the unfold list for
713 /// both backends.
714 step_fns: Vec<String>,
715 },
716 /// Monoidal-accumulator wrapper-over-recursion: a non-recursive
717 /// `wrapper_fn(xs) = inner_fn(xs, neutral)` paired with a direct-
718 /// recurrence `other_fn` such that the law states
719 /// `wrapper_fn(xs) == other_fn(xs)`. The inner fn has shape
720 /// `match xs { [] -> acc; [h, ..t] -> inner_fn(t, acc <op> h) }`
721 /// where `<op>` is monoidal (`Add` / `Mul` / `Sub` on Int) with
722 /// known neutral element. Strategy emits an aux accumulator-
723 /// decomposition lemma plus the main universal lemma; Z3 closes
724 /// both via list induction. Demonstrated by `examples/data/sum_acc.av`.
725 ///
726 /// Stage 8 of #232 — first ProofStrategy variant that consumes
727 /// a `ModulePattern` from `analysis::shape`.
728 WrapperOverRecursion {
729 /// Source name of the non-recursive wrapper (e.g. `"sum"`).
730 wrapper_fn: String,
731 /// Source name of the self-recursive inner (e.g. `"sumTR"`).
732 inner_fn: String,
733 /// Source name of the direct-recurrence fn the law compares
734 /// the wrapper against (e.g. `"sumDirect"`).
735 other_fn: String,
736 /// Binary op the inner threads through its accumulator
737 /// (`Add` / `Mul` / `Sub`). Drives the aux lemma's RHS.
738 combine_op: crate::ast::BinOp,
739 },
740 /// Ground constant-fold over fixed ADT/enum constructor
741 /// arguments. The law's call(s) pin every non-Int param of the
742 /// verified fn to a constructor literal (`CellContent.Empty`,
743 /// `Color.Black`); any scalar `given`s are quantified but irrelevant
744 /// to the chosen branch (the constructor selects a fixed arm). The
745 /// verified fn and its transitively-reached callees are
746 /// non-recursive, so the whole call tree folds to a closed term and
747 /// the goal becomes a decidable ground equality. Backends unfold the
748 /// fn + callees (the same `unfold_fns` list the LinearArithmetic
749 /// detector builds) and close with a `split`/`rfl`/`decide` cascade.
750 /// Demonstrated by `examples/games/checkers/ai.av`
751 /// (`centerBonus.emptyNeutral`, `pieceValue.antisymmetry`,
752 /// `pieceValue.kingWorthTripleMan`). Named for the algebraic content
753 /// (constant-folding over a fixed constructor), not the Lean tactic.
754 EnumConstantFold {
755 /// Ordered fn unfold list — top-level law fn first, then
756 /// transitively-reached non-recursive callees. Source names;
757 /// backends translate to their lemma vocabulary.
758 unfold_fns: Vec<String>,
759 },
760 /// Closed finite-domain enumeration over the law's givens. Every
761 /// given ranges over a closed, small domain — `Bool` or a
762 /// user-declared enum whose constructors are ALL fieldless — with
763 /// the product of domain sizes ≤ 16, so exhaustive `cases` over
764 /// the givens yields ground goals that compute out (`rfl` /
765 /// `decide`). Fuel-wrapped callees are NOT an obstacle:
766 /// constant-measure constructor args compute through fuel. The
767 /// detector deliberately has NO call-shape inspection, NO
768 /// return-type gate and NO recursion gate — closed enumeration
769 /// makes those irrelevant, which is why this is a NEW strategy and
770 /// not a relaxation of [`ProofStrategy::EnumConstantFold`], whose
771 /// literal-pinning / non-recursive / scalar-return gates are
772 /// load-bearing for its simp cascade. Motivating shapes:
773 /// `examples/data/json.av` `parseLiteral.boolRoundtrip` (closes
774 /// genuinely with `intro b; cases b <;> rfl`) and the `EscapeCode`
775 /// laws (`escapeJsonChar.encodesEscapeCode`,
776 /// `parseEscape.escapeCodeRoundtrip`). A non-closing leaf degrades
777 /// to an honest caught `sorry` — never a build error and never
778 /// `native_decide`.
779 FiniteDomainCases {
780 /// Law given names in intro order — the Lean emitter's
781 /// `cases` targets. Source names; backends translate.
782 givens: Vec<String>,
783 },
784 /// Builtin-roundtrip simp over the prelude's spec-lemma registry —
785 /// the last typed fallback before `BackendDispatch`, and the only
786 /// strategy the Lean backend renders AFTER its legacy ad-hoc chain
787 /// (so it fires precisely where the sampled-sorry fallback used to
788 /// emit a bare-`sorry` universal). A no-when law whose lhs call cone
789 /// reduces to builtin String/Int operations once the user fns
790 /// unfold: the Lean emit is `intro <givens>; first | (simp [<unfold
791 /// set>, <registry lemmas>, Int.add_sub_cancel]; done) | sorry`. The
792 /// `done` + `first | … | sorry` alternation is the honest floor — a
793 /// simp that fails OR leaves a residual goal degrades to a caught
794 /// `sorry`, NEVER a build error and NEVER `native_decide`.
795 /// Motivating shapes: `examples/data/json.av`
796 /// `finishInt.fromCanonicalInt` (closes via
797 /// `Int.fromString_fromInt`), `finishNumber.fromCanonicalIntSlice` /
798 /// `afterIntChar.terminatedIntRoundtrip` (slice-prefix lemmas
799 /// through the `toString` fuel wrapper) and
800 /// `finishString.plainSegmentRoundtrip` (`String.slice_append_prefix`
801 /// + `String.intercalate_singleton`).
802 ///
803 /// Deliberately a NEW variant and not a reuse of
804 /// [`ProofStrategy::SimpOverLemmas`]: that variant is the discovery
805 /// feedback loop's re-pin channel (`lemma_discovery::committed`
806 /// re-pins an `Induction` law when committed *discovered* lemma
807 /// texts are in scope, and the backend routes it through the
808 /// induction emit with embedded lemma bodies). This strategy carries
809 /// no lemma texts and never inducts — it names *static prelude*
810 /// lemmas that the Lean emitter ships demand-driven (see
811 /// `lean::prelude_spec_lemmas_for_builtins`, the single source of
812 /// truth for the builtin → lemma-name registry). Keeping the two
813 /// apart means neither the discovery CLI nor `committed.rs` ever
814 /// has to reason about this variant.
815 SimpOverPreludeLemmas {
816 /// Ordered fn unfold list — law subject fn first, then the
817 /// transitively-reached NON-recursive callees (sorted).
818 /// Source names; backends translate.
819 unfold_fns: Vec<String>,
820 /// Recursive (fuel-emitted) fns called DIRECTLY in the law lhs
821 /// with measure-closed args (constructor-headed over
822 /// scalar-only payloads, or literals) — the fuel value
823 /// computes to a Nat literal, so simp drives the `__fuel`
824 /// equations through. The Lean emitter expands each name to
825 /// wrapper + `<name>__fuel` + its measure-helper names.
826 /// Recursive fns reached only transitively (inside cone
827 /// bodies) are NOT listed: they stay opaque — usually dead
828 /// branches under the law's pinned literal args, and if live
829 /// the simp falls to the honest caught `sorry`.
830 fuel_fns: Vec<String>,
831 /// Builtin call names observed in the law sides + cone bodies
832 /// (sorted; includes the synthetic `String.concat` marker for
833 /// string `+`). Registry keys — the Lean emitter maps them to
834 /// prelude spec lemma names via
835 /// `prelude_spec_lemmas_for_builtins`.
836 builtins: Vec<String>,
837 },
838 /// Decimal-Int parse/serialize roundtrip over the canonical
839 /// single-scanner decimal parser shape: the law states
840 /// `parse(ser(C(n)), 0) = Ok(C(n), String.len(ser(C(n))))` for an
841 /// unconstrained `given n: Int`, where `parse` dispatches the head
842 /// char (`"-"` → sign path, `"0"` → leading-zero scan, `_` → digit
843 /// path), both paths funnel into ONE recognized fuelized
844 /// string-position scanner (`proof_recognize::detect_string_pos_scan`
845 /// — the same gate that makes the Lean backend synthesize the
846 /// scanner's `<fn>__fuel_scan` companion lemma), and the cone
847 /// bottoms out in `String.fromInt` / `Int.fromString`.
848 ///
849 /// The detector validates the ENTIRE canonical shape (arm literals,
850 /// arm order, scanner pins, finish-fn slice + `Int.fromString`
851 /// leaf), so the Lean emission can render the fixed
852 /// sign-split proof skeleton ported from the verified json hand
853 /// proof: serializer reduces by `rfl` (ADT-measure fuel),
854 /// `rcases Int.ofNat | Int.negSucc`, head-char dispatch via
855 /// `String.mk`-form `rfl`, `split` + `digitChar` contradiction
856 /// lemmas, the synthesized scan lemma, and `Int.fromString_fromInt`
857 /// at the `finish_int_fn` leaf. The whole emission is wrapped in
858 /// `first | (… ; done) | sorry` — a non-closing case degrades to a
859 /// caught honest `sorry`, never a build error, and `native_decide`
860 /// never appears. Dafny treats the pin as `BackendDispatch`
861 /// (exports byte-identical).
862 ///
863 /// Demonstrated by `examples/data/json.av`
864 /// `parseNumber.fromIntRoundtrip` — the first universal close
865 /// through the fuel-unfolding barrier on a string whose length is
866 /// symbolic.
867 IntDecimalRoundtrip {
868 /// Subject parser fn (`parseNumber`). Source names throughout.
869 parse_fn: String,
870 /// `"-"`-arm continuation (`parseNumberSign`).
871 neg_fn: String,
872 /// Wildcard-arm digit dispatcher (`startNumberDigits`).
873 pos_fn: String,
874 /// Sign path's digit dispatcher (`startSignDigit`).
875 sign_fn: String,
876 /// The recognized fuelized scanner (`scanIntTail`).
877 scanner_fn: String,
878 /// The scanner's char-class predicate (`isDigit`).
879 predicate_fn: String,
880 /// Scanner exit continuation (`finishNumber`).
881 finish_fn: String,
882 /// Int leaf — slices + `Int.fromString` (`finishInt`).
883 finish_int_fn: String,
884 /// Serializer the law's lhs feeds the parser (`toString`).
885 serializer_fn: String,
886 },
887 /// String escape/parse roundtrip over the canonical
888 /// segment-chunking string scanner: the law states
889 /// `parse(<open> + escape(s) + <terminator>, 1) =
890 /// Ok(StrCtor(s), String.len(escape(s)) + 2)` for an unconstrained
891 /// `given s: String` (or the same claim entered at the scanner
892 /// itself with `pos = segmentStart = 1, chunks = []`). The
893 /// producer is a per-char classifier fold (two-char escape table +
894 /// hex control escapes + printable passthrough); the consumer is a
895 /// fuel mutual SCC (scan / escape-dispatch / validate / unicode
896 /// chain) whose per-arm shapes the detector validates EXACTLY —
897 /// see [`StringEscapeRoundtripPin`] for every captured name and
898 /// literal, and `proof_lower::string_escape_roundtrip` for the
899 /// gates.
900 ///
901 /// The Lean emission renders the suffix-invariant proof skeleton
902 /// ported from the verified json hand proof (kernel-checked on
903 /// Lean 4.15, #print axioms = [propext, Quot.sound]): a
904 /// drop-form suffix-cursor prelude, the producer fold's
905 /// accumulator homomorphism, one step lemma per consumer fuel
906 /// arm, and a chunk invariant with the carried scanner state
907 /// (segmentStart, chunks) universally quantified, closed by
908 /// per-char classification. Every synthesized lemma carries a
909 /// `first | (…; done) | sorry` floor — a template regression
910 /// degrades to caught honest sorries (loud budget red), never a
911 /// build error, and `native_decide` never appears. Dafny treats
912 /// the pin as `BackendDispatch` (exports byte-identical).
913 ///
914 /// Demonstrated by `examples/data/json.av`
915 /// `escapeJsonString.parseStringRoundtrip` and
916 /// `parseStringChunk.escapedStringRoundtrip` — the parser
917 /// workhorse pair that closes json's pinned Lean budget to 0.
918 StringEscapeRoundtrip(Box<StringEscapeRoundtripPin>),
919 /// Unconditional ring identity over Int-component records — the
920 /// algebra-law family of an exact-rationals library (a record
921 /// with Int numerator/denominator fields, non-normalizing
922 /// arithmetic, equality by cross-multiplication): add/mul
923 /// commutativity and associativity, distributivity, neg/sub
924 /// normal forms, identity elements. The law has no `when`, every
925 /// given is `Int` or a record whose fields are ALL `Int` (at
926 /// least one such record given), and the claim's whole unfold
927 /// cone is non-recursive pure arithmetic — record constructions
928 /// / field projections, Int literals, `+`, `-`, `*`, unary
929 /// negation — with the equality bottoming out in Int `==`
930 /// (a Bool comparator fn applied at the law root, compared to
931 /// `true`) or direct value equality of two such arithmetic
932 /// expressions. Both sides are then polynomial identities:
933 /// distributing products over sums and AC-normalizing monomials
934 /// and sums makes the two sides' monomial multisets identical,
935 /// no coefficient collection needed.
936 ///
937 /// The Lean emit is `intro <givens>; first | (simp [<unfold
938 /// cone>, <fixed core AC-ring lemma package>]; done) | sorry` —
939 /// the honest caught-`sorry` floor; never `native_decide`, never
940 /// a build error. The package is SCOPED TO THIS STRATEGY's
941 /// emission: its permutational rewrites (`Int.mul_comm`,
942 /// `Int.add_comm`, …) loop or destroy the normal forms other
943 /// strategies' simp sets rely on, so they are never added to the
944 /// shared prelude registry. Dafny needs no special handling —
945 /// Z3 decides these nonlinear identities push-button — and
946 /// treats the pin like `BackendDispatch` (exports stay
947 /// byte-identical). Demonstrated by `examples/data/rational.av`.
948 RingIdentity {
949 /// Ordered fn unfold list — law subject fn first, then the
950 /// transitively-reached callees (sorted). Source names;
951 /// backends translate to their lemma vocabulary.
952 unfold_fns: Vec<String>,
953 },
954 /// Floor-division window family — laws over a power-of-two fn
955 /// (`match n <= 0 { true -> 1; false -> 2 * pow(n - 1) }`), a
956 /// floor-halving binary-exponent fn (the
957 /// [`RecursionContract::WellFoundedToNat`] class with divisor 2),
958 /// and the scaled-significand / bit-width window predicates built
959 /// from them. Each [`FloorWindowFigure`] is a fully-validated
960 /// shape with a fixed proof template on both backends (Lean: the
961 /// core `Int.le_ediv_iff_mul_le` / `Int.ediv_lt_iff_lt_mul`
962 /// floor bridges + power algebra by functional induction; Dafny:
963 /// a proved division-window prelude + branch-split helper
964 /// lemmas). The recognizers are deliberately narrow — exactly the
965 /// hand-validated figures; everything else declines and keeps
966 /// the prior emission.
967 FloorDivWindow { figure: FloorWindowFigure },
968 /// No automated strategy — emit with `sorry` (Lean) / `assume
969 /// {:axiom}` (Dafny). User fills in manually.
970 Sorry,
971 /// Lowerer has not pinned a strategy for this law; the backend's
972 /// `or_else` chain decides. Today reached by linear-recurrence-
973 /// spec equivalence (Lean-specific, ~50-line support theorems
974 /// stay in the backend) and the sampled / guarded-domain
975 /// fallback. The backend treats `BackendDispatch` as "fall
976 /// through to ad-hoc strategy chain"; pinned variants above
977 /// short-circuit to a known emit.
978 BackendDispatch,
979}
980
981/// The recognized figures of [`ProofStrategy::FloorDivWindow`]. All
982/// fn names are source names; backends translate. Every figure's
983/// quantifier names come from the law's givens (captured implicitly —
984/// backends render them through the law's own given list).
985#[derive(Debug, Clone, PartialEq, Eq)]
986pub enum FloorWindowFigure {
987 /// `pow(n) >= 1 => true` with no premise — positivity of the
988 /// power-of-two fn, by functional induction.
989 PowPositive { pow_fn: String },
990 /// `when m >= 0; n >= 0 -> pow(m + n) == pow(m) * pow(n)` — the
991 /// power homomorphism, by functional induction on the first
992 /// exponent.
993 PowSumSplit { pow_fn: String },
994 /// `when b >= 1; a >= b; n >= 1 -> window(a, b, n) == true`
995 /// where `window` checks `pow(n-1) <= sig(a,b,n) < pow(n)`,
996 /// `sig` scales by `pow(n-1-e)` and floor-divides, and `e` is
997 /// the floor-halving binary exponent of a/b.
998 SigWindow {
999 pow_fn: String,
1000 halve_fn: String,
1001 exp_fn: String,
1002 sig_fn: String,
1003 window_fn: String,
1004 },
1005 /// `when fits(j, m); fits(k, n) -> claim(j, k, m, n) == true`
1006 /// where `fits` is the `pow(m-1) <= j < pow(m)` window predicate
1007 /// and `claim` states the product window
1008 /// `pow(m+n-2) <= j*k < pow(m+n)`.
1009 ProductWindow {
1010 pow_fn: String,
1011 fits_fn: String,
1012 claim_fn: String,
1013 },
1014}
1015
1016/// Parameter pack for [`ProofStrategy::StringEscapeRoundtrip`] —
1017/// every fn name and literal the Lean renderer's suffix-invariant
1018/// proof skeleton quotes. All fn names are source names (backends
1019/// translate); all chars/codes are the SOURCE literals the detector
1020/// read off the validated arm patterns, so the renderer can rebuild
1021/// them as Lean literals without re-walking the AST.
1022#[derive(Debug, Clone)]
1023pub struct StringEscapeRoundtripPin {
1024 /// The scanner SCC member the law enters (`parseStringChunk`).
1025 /// Body: charAt dispatch over { terminator → finish, escape-char
1026 /// → escape dispatch, default → validate }.
1027 pub scan_fn: String,
1028 /// Escape dispatcher (`parseEscape`): slices the open segment,
1029 /// then maps escape letters to decoded chars / the unicode hop.
1030 pub escape_fn: String,
1031 /// Default-arm validator (`validateChar`): control chars error,
1032 /// printable chars extend the open segment.
1033 pub validate_fn: String,
1034 /// Terminator continuation (`finishString`): slice + join + Ok.
1035 pub finish_fn: String,
1036 /// `\uXXXX` reader head (`parseUnicode`): readHex4 + codepoint.
1037 pub unicode_fn: String,
1038 /// Codepoint surrogate filter (`parseUnicodeCodePoint`).
1039 pub codepoint_fn: String,
1040 /// Decoded-codepoint continuation (`applyCodePoint`):
1041 /// `Char.fromCode` + chunk flush back into the scanner.
1042 pub apply_fn: String,
1043 /// Four-hex-digit reader (`readHex4`), separately fueled on
1044 /// `count` climbing to the literal bound 4.
1045 pub read_hex_fn: String,
1046 /// User hex-digit valuation (`hexVal : String -> Option<Int>`).
1047 pub hex_val_fn: String,
1048 /// High-surrogate guard (`isHighSurrogate`): `cp >= MIN && …`.
1049 pub high_surrogate_fn: String,
1050 /// Low-surrogate guard (`isLowSurrogate`).
1051 pub low_surrogate_fn: String,
1052 /// Producer wrapper (`escapeJsonString`): `fold(String.chars(s), "")`.
1053 pub producer_fn: String,
1054 /// Producer accumulator fold (`escapeJsonChars`).
1055 pub fold_fn: String,
1056 /// Per-char classifier (`escapeJsonChar`): two-char escape table
1057 /// + default to the control classifier.
1058 pub classifier_fn: String,
1059 /// Control classifier (`escapeControlChar`): equality ladder +
1060 /// `code < threshold → control escape` + printable passthrough.
1061 pub control_fn: String,
1062 /// Hex control escape (`controlCodeEscape`): `Byte.toHex` +
1063 /// 4-char prefix.
1064 pub control_escape_fn: String,
1065 /// Success ctor of the law's rhs, source spelling
1066 /// (`"ParseResult.Ok"`).
1067 pub ok_ctor: String,
1068 /// String-payload ctor inside the success ctor
1069 /// (`"Json.JsonString"`).
1070 pub str_ctor: String,
1071 /// Scan terminator char (`'"'` — the finish arm's literal).
1072 pub terminator: char,
1073 /// Escape introducer char (`'\\'` — the escape arm's literal,
1074 /// also the first char of every two-char escape output).
1075 pub escape_char: char,
1076 /// Hex-escape letter (`'u'` — second char of the control-escape
1077 /// prefix, the consumer's unicode arm literal).
1078 pub unicode_letter: char,
1079 /// Two-char escape table, producer-derived and consumer-aligned.
1080 pub pairs: Vec<EscapePairSpec>,
1081 /// Control threshold (`32`): producer hex-escapes below it, the
1082 /// consumer validator rejects below it. Gated `<= 256`.
1083 pub control_threshold: i64,
1084 /// `cp >= MIN` bound of the high-surrogate guard (`55296`).
1085 pub high_surrogate_min: i64,
1086 /// `cp >= MIN` bound of the low-surrogate guard (`56320`).
1087 /// The Lean renderer probes the scanner SCC's emitted
1088 /// `averStringPosFuel` rank itself (it must match the emission
1089 /// byte-for-byte), so the rank is deliberately NOT pinned here.
1090 pub low_surrogate_min: i64,
1091}
1092
1093/// One two-char escape: the producer emits `[escape_char, letter]`
1094/// for `decoded`; the consumer's escape dispatcher maps `letter`
1095/// back to `decoded`.
1096#[derive(Debug, Clone)]
1097pub struct EscapePairSpec {
1098 /// The unescaped source char (`'\n'`).
1099 pub decoded: char,
1100 /// The escape letter following the escape introducer (`'n'`).
1101 pub letter: char,
1102 /// `true` when the pair comes from the control classifier's
1103 /// equality ladder (`code == 8 → "\\b"`), `false` for a
1104 /// classifier literal arm (`"\n" → "\\n"`). Drives which
1105 /// disequality form the chunk-invariant ladder cases on
1106 /// (`c.toNat = K` vs `c = '<lit>'`).
1107 pub from_control_ladder: bool,
1108}
1109
1110/// Discriminator for [`ProofStrategy::MapUpdatePostcondition`].
1111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112pub enum MapUpdatePostconditionKind {
1113 /// Law shape: `Map.has(outer(m, k), k) == true`.
1114 HasAfter,
1115 /// Law shape: `Map.get(outer(m, k), k) == Option.Some(...)`.
1116 GetAfter,
1117}
1118
1119/// A bool predicate with explicit free-variable context. Stays in
1120/// `Spanned<Expr>` form so backends can route through their
1121/// existing `emit_expr` paths; the context is what gives backends
1122/// the information they need to project (e.g. `.val`) without
1123/// re-walking the AST.
1124#[derive(Debug, Clone)]
1125pub struct Predicate {
1126 /// Variables the predicate may reference, in declaration order.
1127 /// Each entry tells the backend what type the var has in the
1128 /// target language — same logic as `Quantifier.binder_type`.
1129 pub free_vars: Vec<(String, QuantifierType)>,
1130 /// The expression. Already in the target variable space (e.g.
1131 /// caller-derived predicates have had caller-arg names
1132 /// substituted to callee-param names).
1133 pub expr: Spanned<crate::ir::hir::ResolvedExpr>,
1134}