Skip to main content

aver/codegen/lean/
mod.rs

1/// Lean 4 backend for the Aver transpiler.
2///
3/// Transpiles only pure core logic: functions without effects, type definitions,
4/// verify blocks (as `example` proofs), and decision blocks (as comments).
5/// Effectful functions and `main` are skipped.
6mod builtins;
7mod expr;
8mod law_auto;
9mod pattern;
10mod prelude;
11pub(crate) mod recurrence;
12mod sample_literal;
13mod shared;
14pub mod tactic_ir;
15#[cfg(test)]
16mod tests;
17mod toplevel;
18mod transpile;
19mod types;
20
21// Crate-wide re-export: committed-lemma handling (`codegen::lemma_discovery`)
22// and the law-auto rungs map Aver fn names to their Lean spelling through the
23// same helper the program defs are emitted with, so a cited lemma references
24// `decode` / `Run` / `++` exactly as generated.
25pub(crate) use expr::aver_name_to_lean;
26
27// `law_auto` cites the prelude spec lemma names via `super::`; the
28// `transpile*` entry points below drive the unified emitter.
29pub(crate) use prelude::prelude_spec_lemmas_for_builtins;
30
31/// `aver proof --explain` residual probe — turns an emitted main law theorem's
32/// source lines into a normalization-only twin so Lean reports its residual
33/// (`unsolved goals`). Used by the `--check` harness in the `aver` binary.
34pub use law_auto::residual_probe_body;
35#[cfg(test)]
36use prelude::generate_prelude;
37use transpile::{LeanEmitMode, transpile_unified};
38
39use std::collections::HashSet;
40
41use crate::ast::{FnDef, Spanned};
42use crate::codegen::{CodegenContext, ProjectOutput};
43
44/// Statement-class channel for emitted law theorems.
45///
46/// `aver proof --check`'s `universal` metric must know, per law theorem,
47/// whether the emitted STATEMENT is genuinely universal or bounded: for a
48/// `when`-law over non-refinement-lifted givens, `law_theorem_prop` prepends
49/// sampled-domain disjunction premises (`a = 0 ∨ a = 1 ∨ …`) — the theorem
50/// then only claims the law on the finite sample domain, even when it is
51/// proven by real tactics with a kernel-clean axiom profile. Refinement-lifted
52/// `when`-laws drop those premises (the Subtype carries the invariant) and
53/// stay genuinely universal.
54///
55/// Only the code that BUILDS the statement knows which premises it prepended,
56/// so the emitter records the class as one structured marker comment per
57/// emitted law theorem, preceding it in the generated `.lean` source
58/// (self-contained artifact — the classification travels with the export):
59///
60/// ```text
61/// -- aver:law-class <theorem_name> universal
62/// -- aver:law-class <theorem_name> bounded-domain
63/// ```
64///
65/// The checker (`lean_universal_proof` in the CLI) consumes these markers and
66/// must NEVER re-derive the class from theorem names or by re-parsing
67/// statements. A law theorem without a marker earns no universal credit
68/// (fail-closed for stale/foreign export dirs).
69pub const LAW_CLASS_MARKER_PREFIX: &str = "-- aver:law-class ";
70/// Marker class tag: no sampled-domain premises — the `∀`-statement is the
71/// law's genuine universal claim.
72pub const LAW_CLASS_UNIVERSAL: &str = "universal";
73/// Marker class tag: sampled-domain disjunction premises bound the statement
74/// to the finite sample domain.
75pub const LAW_CLASS_BOUNDED_DOMAIN: &str = "bounded-domain";
76
77/// How verify blocks should be emitted in generated Lean.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum VerifyEmitMode {
80    /// `example : lhs = rhs := by native_decide`
81    NativeDecide,
82    /// `example : lhs = rhs := by sorry`
83    Sorry,
84    /// Named theorem stubs:
85    /// `theorem <fn>_verify_<n> : lhs = rhs := by`
86    /// `  sorry`
87    TheoremSkeleton,
88}
89
90// RecursionPlan / ProofModeIssue moved to shared `crate::codegen::recursion`
91// so the Dafny backend can reuse the same classifier. Re-export here so
92// existing `lean::RecursionPlan` / `lean::ProofModeIssue` call sites keep
93// working without churn.
94pub use crate::codegen::recursion::{ProofModeIssue, RecursionPlan};
95
96pub use toplevel::PROOF_FUEL_EXHAUSTED_MSG;
97
98/// The marker every Lean runtime panic prints into captured build output.
99/// Empirically pinned against real `lake build` transcripts (both panic
100/// sites below produce it):
101///
102/// ```text
103/// info: ././././FuelProbe.lean:27:0: PANIC at stepSum__fuel FuelProbe:8:9: Aver proof fuel exhausted
104/// PANIC at stepSumAcc__fuel FuelProbe:19:9: Aver proof fuel exhausted
105/// info: ././././PanicProbe.lean:11:0: PANIC at Char.toCode AverCommon:11:12: Char.toCode: string is empty
106/// ```
107///
108/// (lake prefixes the first diagnostic of a build step with `info: <loc>:`,
109/// later ones print raw — both carry `PANIC at `.)
110pub const LEAN_PANIC_LINE_MARKER: &str = "PANIC at ";
111
112/// Count model panic lines in captured `lake build` output.
113///
114/// Lean's `panic!` does NOT abort: at `native_decide` evaluation time it
115/// prints `PANIC at <fn> <file>:<line>: <msg>` (to lake's captured output)
116/// and returns the result type's `default` value. When BOTH sides of a
117/// bounded sample route through the model (`verify f(x) => g(x)` cases,
118/// every `_sample_N` / `_checked_domain` theorem), a panicking evaluation
119/// reduces both sides to `default` and the kernel certifies a vacuous —
120/// possibly FALSE — equation while `lake` exits 0 with zero sorries. The
121/// panic line in the build output is the only trace, so `aver proof --check`
122/// charges ANY hit as a hard failure.
123///
124/// Scans for the generic `PANIC at ` line marker, not a per-site message:
125/// the emitted exports contain `panic!` only at compiler-generated sites —
126/// the fuel wrappers' exhaustion arm ([`PROOF_FUEL_EXHAUSTED_MSG`]) and
127/// partial prelude builtins (e.g. `Char.toCode` on an empty string) — and
128/// every one of them shares the same panic-returns-`default` vacuity vector.
129/// A green check has no legitimate panic, and the generic marker also
130/// catches panics raised inside Lean's own stdlib (e.g. a `get!` deep in a
131/// future prelude helper) that a per-message scan could never enumerate.
132/// Counts matching LINES (one panic prints exactly one line; the same site
133/// can fire on several theorems).
134///
135/// Known false-positive boundary, accepted: on an ALREADY-FAILING build, a
136/// native_decide error can echo the false proposition, and a user string
137/// literal containing `PANIC at ` inside it would inflate the count (and
138/// set `model_panicked`). The verdict stays correct — the build already
139/// failed on exit status — so a spurious match can only turn a red redder,
140/// never a green red.
141pub fn count_model_panic_lines(output: &str) -> usize {
142    output
143        .lines()
144        .filter(|l| l.contains(LEAN_PANIC_LINE_MARKER))
145        .count()
146}
147
148pub(crate) fn pure_fns(ctx: &CodegenContext) -> Vec<&FnDef> {
149    ctx.modules
150        .iter()
151        .flat_map(|m| m.fn_defs.iter())
152        .chain(ctx.fn_defs.iter())
153        .filter(|fd| toplevel::is_pure_fn(fd))
154        .collect()
155}
156
157pub(crate) fn recursive_type_names(ctx: &CodegenContext) -> HashSet<String> {
158    ctx.modules
159        .iter()
160        .flat_map(|m| m.type_defs.iter())
161        .chain(ctx.type_defs.iter())
162        .filter(|td| toplevel::is_recursive_type_def(td))
163        .map(|td| toplevel::type_def_name(td).to_string())
164        .collect()
165}
166
167pub(crate) fn recursive_pure_fn_names(ctx: &CodegenContext) -> HashSet<String> {
168    // `ctx.recursive_fns` is the single source of truth — populated
169    // by `build_context` from analyze in production, by
170    // `refresh_facts()` (called from each `transpile*` entry point)
171    // in test stubs. After phase C it's keyed by opaque `FnId`;
172    // project pure-fn ids back through the symbol table and surface
173    // bare names for Lean's downstream scope-local classifiers.
174    let symbols = &ctx.symbol_table;
175    let pure_ids: HashSet<crate::ir::FnId> = pure_fns(ctx)
176        .into_iter()
177        .filter_map(|fd| crate::codegen::common::fn_id_for_decl(ctx, fd))
178        .collect();
179    ctx.recursive_fns
180        .intersection(&pure_ids)
181        .map(|id| symbols.fn_entry(*id).key.name.clone())
182        .collect()
183}
184
185fn verify_counter_key(vb: &crate::ast::VerifyBlock) -> String {
186    // Shared with the CLI's VM ground-truth collection — see the doc on
187    // `verify_block_counter_key` for why the two sides must not drift.
188    crate::codegen::common::verify_block_counter_key(vb)
189}
190
191fn lean_project_name(ctx: &CodegenContext) -> String {
192    crate::codegen::common::entry_basename(ctx)
193}
194
195pub(super) fn bound_expr_to_lean(expr: &Spanned<crate::ir::hir::ResolvedExpr>) -> String {
196    use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
197    match &expr.node {
198        ResolvedExpr::Literal(crate::ast::Literal::Int(n)) => format!("{}", n),
199        ResolvedExpr::Ident(name) | ResolvedExpr::Resolved { name, .. } => {
200            expr::aver_name_to_lean(name)
201        }
202        ResolvedExpr::Call(callee, args) => {
203            // Bound expressions are linear arithmetic over the param —
204            // typically `List.len(xs)` or an int constant. The resolver
205            // lifts `List.len(...)` to `ResolvedCallee::Builtin`; user-fn
206            // refs in a bound expression would be a malformed metric
207            // anyway, so we only render builtins and fall through to
208            // `"0"` (the same fallback the original AST helper used for
209            // shapes it couldn't classify).
210            let dotted = match callee {
211                ResolvedCallee::Builtin(name) => Some(name.clone()),
212                _ => None,
213            };
214            if let Some(dotted) = dotted {
215                // List.len(xs) → xs.length in Lean
216                if dotted == "List.len" && args.len() == 1 {
217                    return format!("{}.length", bound_expr_to_lean(&args[0]));
218                }
219                let lean_args: Vec<String> = args.iter().map(bound_expr_to_lean).collect();
220                format!(
221                    "({} {})",
222                    expr::aver_name_to_lean(&dotted),
223                    lean_args.join(" ")
224                )
225            } else {
226                "0".to_string()
227            }
228        }
229        ResolvedExpr::Attr(obj, field) => format!(
230            "{}.{}",
231            bound_expr_to_lean(obj),
232            expr::aver_name_to_lean(field)
233        ),
234        _ => "0".to_string(),
235    }
236}
237
238pub(crate) use crate::codegen::recursion::detect::sizeof_measure_param_indices;
239
240/// Proof-mode diagnostics for Lean transpilation.
241///
242/// Returns human-readable notices for recursive shapes that still fall back to
243/// regular `partial` Lean defs instead of total proof-mode emission.
244pub fn proof_mode_findings(ctx: &CodegenContext) -> Vec<ProofModeIssue> {
245    // ProofIR carries `unclassified_fns` populated by the ContractLower
246    // pipeline stage — same data analyze_plans used to return, just
247    // read off the IR instead of re-running the classifier.
248    ctx.proof_ir
249        .unclassified_fns
250        .iter()
251        .map(|uf| ProofModeIssue {
252            line: uf.line,
253            message: uf.message.clone(),
254        })
255        .collect()
256}
257
258pub fn proof_mode_issues(ctx: &CodegenContext) -> Vec<String> {
259    proof_mode_findings(ctx)
260        .into_iter()
261        .map(|issue| issue.message)
262        .collect()
263}
264
265/// Transpile an Aver program to a Lean 4 project.
266///
267/// Takes `&mut ctx` so it can run `ctx.refresh_facts()` upfront — keeps
268/// the derived sets (`mutual_tco_members`, `recursive_fns`) in sync with
269/// the current items + modules. Idempotent: production callers go through
270/// `build_context` which already populated them, so refresh recomputes
271/// the same answer; test stubs that build the ctx piecewise (push items
272/// in-place, bypass `build_context`) get the fresh sets they need.
273pub fn transpile(ctx: &mut CodegenContext) -> ProjectOutput {
274    transpile_with_verify_mode(ctx, VerifyEmitMode::NativeDecide)
275}
276
277/// Proof-mode transpilation.
278///
279/// Uses recursion plans validated by `proof_mode_issues` and emits supported
280/// recursive functions without `partial`, adding `termination_by` scaffolding.
281pub fn transpile_for_proof_mode(
282    ctx: &mut CodegenContext,
283    verify_mode: VerifyEmitMode,
284) -> ProjectOutput {
285    // No refresh_facts call here: production callers go through
286    // build_codegen_context → pipeline, which populates every derived
287    // fact (recursive_fns, mutual_tco_members, proof_ir) once.
288    // Synthetic-AST tests that bypass the pipeline call refresh_facts
289    // themselves before reaching this fn.
290    transpile_unified(ctx, verify_mode, LeanEmitMode::Proof)
291}
292
293/// Transpile an Aver program to a Lean 4 project with configurable verify proof mode.
294///
295/// - `NativeDecide` emits `example ... := by native_decide`
296/// - `Sorry` emits `example ... := by sorry`
297/// - `TheoremSkeleton` emits named theorem skeletons with `sorry`
298pub fn transpile_with_verify_mode(
299    ctx: &mut CodegenContext,
300    verify_mode: VerifyEmitMode,
301) -> ProjectOutput {
302    // No refresh_facts call here — same reasoning as
303    // `transpile_for_proof_mode`. Synthetic-AST tests refresh
304    // themselves; production paths come pre-populated.
305    transpile_unified(ctx, verify_mode, LeanEmitMode::Standard)
306}