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