use crate::schema::{Contract, LeanStatus, ProofObligation};
#[derive(Debug, Clone)]
pub struct LeanFile {
pub path: String,
pub content: String,
}
pub fn generate_lean_files(contract: &Contract) -> Vec<LeanFile> {
let lean_obligations: Vec<&ProofObligation> = contract
.proof_obligations
.iter()
.filter(|ob| ob.lean.is_some())
.collect();
if lean_obligations.is_empty() {
return Vec::new();
}
let module_name = derive_module_name(&contract.metadata.description);
let mut files = Vec::new();
files.push(generate_defs_file(contract, &module_name));
for ob in &lean_obligations {
if let Some(ref lean) = ob.lean {
files.push(generate_theorem_file(ob, lean, &module_name));
}
}
files
}
pub fn lean_status(contract: &Contract) -> LeanStatusReport {
let mut report = LeanStatusReport {
contract_description: contract.metadata.description.clone(),
#[allow(clippy::cast_possible_truncation)]
total_obligations: contract.proof_obligations.len() as u32,
with_lean: 0,
proved: 0,
sorry: 0,
wip: 0,
not_applicable: 0,
obligations: Vec::new(),
};
for ob in &contract.proof_obligations {
if let Some(ref lean) = ob.lean {
report.with_lean += 1;
match lean.status {
LeanStatus::Proved => report.proved += 1,
LeanStatus::Sorry => report.sorry += 1,
LeanStatus::Wip => report.wip += 1,
LeanStatus::NotApplicable => report.not_applicable += 1,
}
report.obligations.push(ObligationStatus {
property: ob.property.clone(),
theorem: lean.theorem.clone(),
status: lean.status,
});
}
}
report
}
#[derive(Debug, Clone)]
pub struct LeanStatusReport {
pub contract_description: String,
pub total_obligations: u32,
pub with_lean: u32,
pub proved: u32,
pub sorry: u32,
pub wip: u32,
pub not_applicable: u32,
pub obligations: Vec<ObligationStatus>,
}
#[derive(Debug, Clone)]
pub struct ObligationStatus {
pub property: String,
pub theorem: String,
pub status: LeanStatus,
}
pub fn format_status_report(reports: &[LeanStatusReport]) -> String {
let mut out = String::new();
out.push_str(&format!(
"{:<30} {:>5} {:>6} {:>5} {:>3} {:>3}\n",
"Contract", "Oblgs", "Proved", "Sorry", "WIP", "N/A"
));
out.push_str(&"─".repeat(60));
out.push('\n');
let mut total_ob = 0u32;
let mut total_proved = 0u32;
let mut total_sorry = 0u32;
let mut total_wip = 0u32;
let mut total_na = 0u32;
for r in reports {
let name = if r.contract_description.len() > 30 {
let end = r
.contract_description
.char_indices()
.take_while(|(i, _)| *i < 30)
.last()
.map_or(0, |(i, c)| i + c.len_utf8());
&r.contract_description[..end]
} else {
&r.contract_description
};
out.push_str(&format!(
"{:<30} {:>5} {:>6} {:>5} {:>3} {:>3}\n",
name, r.with_lean, r.proved, r.sorry, r.wip, r.not_applicable
));
total_ob += r.with_lean;
total_proved += r.proved;
total_sorry += r.sorry;
total_wip += r.wip;
total_na += r.not_applicable;
}
out.push_str(&"─".repeat(60));
out.push('\n');
out.push_str(&format!(
"{:<30} {:>5} {:>6} {:>5} {:>3} {:>3}\n",
"Total", total_ob, total_proved, total_sorry, total_wip, total_na
));
if total_ob > 0 {
let pct = total_proved * 100 / total_ob;
out.push_str(&format!(
"L4 Coverage: {pct}% ({total_proved}/{total_ob}) Sorry Debt: {total_sorry}\n"
));
}
out
}
fn derive_module_name(description: &str) -> String {
let base = description
.split_whitespace()
.next()
.unwrap_or("Unknown")
.to_string();
let mut chars = base.chars();
match chars.next() {
None => "Unknown".to_string(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn generate_defs_file(contract: &Contract, module_name: &str) -> LeanFile {
let mut content = String::new();
content.push_str(&format!("-- {}\n", contract.metadata.description));
content.push_str(&format!(
"-- Generated from contract v{}\n",
contract.metadata.version
));
content.push_str("-- DO NOT EDIT — regenerate with `pv lean`\n\n");
let mut imports: Vec<&str> = Vec::new();
for ob in &contract.proof_obligations {
if let Some(ref lean) = ob.lean {
for imp in &lean.mathlib_imports {
if !imports.contains(&imp.as_str()) {
imports.push(imp);
}
}
}
}
content.push_str("import Mathlib.Data.Real.Basic\n");
content.push_str("import Mathlib.Data.Finset.Basic\n");
for imp in &imports {
content.push_str(&format!("import {imp}\n"));
}
content.push('\n');
content.push_str(&format!("namespace ProvableContracts.{module_name}\n\n"));
for (name, eq) in &contract.equations {
content.push_str(&format!("-- Equation: {name}\n"));
content.push_str(&format!("-- Formula: {}\n", eq.formula));
if let Some(ref domain) = eq.domain {
content.push_str(&format!("-- Domain: {domain}\n"));
}
content.push_str(&format!("noncomputable def {name} : sorry := sorry\n\n"));
}
content.push_str(&format!("end ProvableContracts.{module_name}\n"));
LeanFile {
path: format!("ProvableContracts/Defs/{module_name}.lean"),
content,
}
}
fn generate_theorem_file(
ob: &ProofObligation,
lean: &crate::schema::LeanProof,
module_name: &str,
) -> LeanFile {
let mut content = String::new();
content.push_str(&format!("-- Theorem: {}\n", lean.theorem));
content.push_str(&format!("-- Property: {}\n", ob.property));
content.push_str(&format!("-- Obligation type: {}\n", ob.obligation_type));
content.push_str("-- Generated with `pv lean`\n\n");
content.push_str(&format!("import ProvableContracts.Defs.{module_name}\n"));
for imp in &lean.mathlib_imports {
content.push_str(&format!("import {imp}\n"));
}
content.push('\n');
if !lean.depends_on.is_empty() {
content.push_str("-- Dependencies:\n");
for dep in &lean.depends_on {
content.push_str(&format!("-- {dep}\n"));
}
content.push('\n');
}
content.push_str(&format!("namespace ProvableContracts.{module_name}\n\n"));
if let Some(ref formal) = ob.formal {
content.push_str(&format!("-- Formal: {formal}\n"));
}
let status_comment = match lean.status {
LeanStatus::Proved => "-- Status: proved",
LeanStatus::Sorry => "-- Status: sorry (proof pending)",
LeanStatus::Wip => "-- Status: work in progress",
LeanStatus::NotApplicable => "-- Status: not applicable",
};
content.push_str(&format!("{status_comment}\n"));
content.push_str(&format!("theorem {} : sorry := by\n", lean.theorem));
content.push_str(" sorry\n");
if let Some(ref notes) = lean.notes {
content.push_str(&format!("\n-- Note: {notes}\n"));
}
content.push_str(&format!("\nend ProvableContracts.{module_name}\n"));
let theorem_file = lean.theorem.split('.').next_back().unwrap_or(&lean.theorem);
LeanFile {
path: format!("ProvableContracts/Theorems/{module_name}/{theorem_file}.lean"),
content,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::parse_contract_str;
#[test]
fn no_lean_obligations_produces_empty() {
let yaml = r#"
metadata:
version: "1.0.0"
description: "Softmax kernel"
references: ["Paper"]
equations:
softmax:
formula: "f(x) = exp(x_i) / sum(exp(x_j))"
proof_obligations:
- type: invariant
property: "Output sums to 1"
falsification_tests: []
"#;
let contract = parse_contract_str(yaml).unwrap();
let files = generate_lean_files(&contract);
assert!(files.is_empty());
}
#[test]
fn generates_defs_and_theorem_files() {
let yaml = r#"
metadata:
version: "1.0.0"
description: "Softmax kernel"
references: ["Paper"]
equations:
softmax:
formula: "f(x) = exp(x_i) / sum(exp(x_j))"
domain: "R^n"
proof_obligations:
- type: invariant
property: "Output sums to 1"
formal: "|sum(f(x)) - 1| < eps"
lean:
theorem: Softmax.partition_of_unity
module: ProvableContracts.Softmax
status: sorry
depends_on:
- Real.exp_pos
mathlib_imports:
- Mathlib.Analysis.SpecialFunctions.ExpDeriv
notes: "Proof over reals"
falsification_tests: []
"#;
let contract = parse_contract_str(yaml).unwrap();
let files = generate_lean_files(&contract);
assert_eq!(files.len(), 2);
let defs = &files[0];
assert!(defs.path.contains("Defs/Softmax"));
assert!(defs.content.contains("noncomputable def softmax"));
assert!(defs.content.contains("namespace ProvableContracts.Softmax"));
assert!(
defs.content
.contains("Mathlib.Analysis.SpecialFunctions.ExpDeriv")
);
let thm = &files[1];
assert!(thm.path.contains("Theorems/Softmax/partition_of_unity"));
assert!(thm.content.contains("theorem Softmax.partition_of_unity"));
assert!(thm.content.contains("sorry"));
assert!(thm.content.contains("Real.exp_pos"));
assert!(thm.content.contains("Proof over reals"));
}
#[test]
fn lean_status_counts_correctly() {
let yaml = r#"
metadata:
version: "1.0.0"
description: "Test kernel"
references: ["Paper"]
equations:
f:
formula: "f(x) = x"
proof_obligations:
- type: invariant
property: "P1"
lean:
theorem: T1
status: proved
- type: bound
property: "P2"
lean:
theorem: T2
status: sorry
- type: monotonicity
property: "P3"
lean:
theorem: T3
status: wip
- type: equivalence
property: "P4 no lean"
falsification_tests: []
"#;
let contract = parse_contract_str(yaml).unwrap();
let report = lean_status(&contract);
assert_eq!(report.total_obligations, 4);
assert_eq!(report.with_lean, 3);
assert_eq!(report.proved, 1);
assert_eq!(report.sorry, 1);
assert_eq!(report.wip, 1);
assert_eq!(report.not_applicable, 0);
}
#[test]
fn format_status_report_renders_table() {
let reports = vec![LeanStatusReport {
contract_description: "Softmax kernel".to_string(),
total_obligations: 5,
with_lean: 3,
proved: 1,
sorry: 1,
wip: 1,
not_applicable: 0,
obligations: vec![],
}];
let table = format_status_report(&reports);
assert!(table.contains("Softmax kernel"));
assert!(table.contains("L4 Coverage: 33%"));
assert!(table.contains("Sorry Debt: 1"));
}
#[test]
fn derive_module_name_capitalizes() {
assert_eq!(derive_module_name("softmax kernel"), "Softmax");
assert_eq!(derive_module_name("RMSNorm"), "RMSNorm");
assert_eq!(derive_module_name(""), "Unknown");
}
#[test]
fn proved_theorem_has_proved_comment() {
let yaml = r#"
metadata:
version: "1.0.0"
description: "Test"
references: ["P"]
equations:
f:
formula: "f(x) = x"
proof_obligations:
- type: invariant
property: "Always true"
lean:
theorem: F.always_true
status: proved
falsification_tests: []
"#;
let contract = parse_contract_str(yaml).unwrap();
let files = generate_lean_files(&contract);
let thm = &files[1];
assert!(thm.content.contains("Status: proved"));
}
}