provable_contracts/lean_gen/
mod.rs1use crate::schema::{Contract, LeanStatus, ProofObligation};
13
14#[derive(Debug, Clone)]
16pub struct LeanFile {
17 pub path: String,
19 pub content: String,
21}
22
23pub fn generate_lean_files(contract: &Contract) -> Vec<LeanFile> {
31 let lean_obligations: Vec<&ProofObligation> = contract
32 .proof_obligations
33 .iter()
34 .filter(|ob| ob.lean.is_some())
35 .collect();
36
37 if lean_obligations.is_empty() {
38 return Vec::new();
39 }
40
41 let module_name = derive_module_name(&contract.metadata.description);
42 let mut files = Vec::new();
43
44 files.push(generate_defs_file(contract, &module_name));
46
47 for ob in &lean_obligations {
49 if let Some(ref lean) = ob.lean {
50 files.push(generate_theorem_file(ob, lean, &module_name));
51 }
52 }
53
54 files
55}
56
57pub fn lean_status(contract: &Contract) -> LeanStatusReport {
61 let mut report = LeanStatusReport {
62 contract_description: contract.metadata.description.clone(),
63 #[allow(clippy::cast_possible_truncation)]
64 total_obligations: contract.proof_obligations.len() as u32,
65 with_lean: 0,
66 proved: 0,
67 sorry: 0,
68 wip: 0,
69 not_applicable: 0,
70 obligations: Vec::new(),
71 };
72
73 for ob in &contract.proof_obligations {
74 if let Some(ref lean) = ob.lean {
75 report.with_lean += 1;
76 match lean.status {
77 LeanStatus::Proved => report.proved += 1,
78 LeanStatus::Sorry => report.sorry += 1,
79 LeanStatus::Wip => report.wip += 1,
80 LeanStatus::NotApplicable => report.not_applicable += 1,
81 }
82 report.obligations.push(ObligationStatus {
83 property: ob.property.clone(),
84 theorem: lean.theorem.clone(),
85 status: lean.status,
86 });
87 }
88 }
89
90 report
91}
92
93#[derive(Debug, Clone)]
95pub struct LeanStatusReport {
96 pub contract_description: String,
97 pub total_obligations: u32,
98 pub with_lean: u32,
99 pub proved: u32,
100 pub sorry: u32,
101 pub wip: u32,
102 pub not_applicable: u32,
103 pub obligations: Vec<ObligationStatus>,
104}
105
106#[derive(Debug, Clone)]
108pub struct ObligationStatus {
109 pub property: String,
110 pub theorem: String,
111 pub status: LeanStatus,
112}
113
114pub fn format_status_report(reports: &[LeanStatusReport]) -> String {
116 let mut out = String::new();
117
118 out.push_str(&format!(
119 "{:<30} {:>5} {:>6} {:>5} {:>3} {:>3}\n",
120 "Contract", "Oblgs", "Proved", "Sorry", "WIP", "N/A"
121 ));
122 out.push_str(&"─".repeat(60));
123 out.push('\n');
124
125 let mut total_ob = 0u32;
126 let mut total_proved = 0u32;
127 let mut total_sorry = 0u32;
128 let mut total_wip = 0u32;
129 let mut total_na = 0u32;
130
131 for r in reports {
132 let name = if r.contract_description.len() > 30 {
133 &r.contract_description[..30]
134 } else {
135 &r.contract_description
136 };
137 out.push_str(&format!(
138 "{:<30} {:>5} {:>6} {:>5} {:>3} {:>3}\n",
139 name, r.with_lean, r.proved, r.sorry, r.wip, r.not_applicable
140 ));
141 total_ob += r.with_lean;
142 total_proved += r.proved;
143 total_sorry += r.sorry;
144 total_wip += r.wip;
145 total_na += r.not_applicable;
146 }
147
148 out.push_str(&"─".repeat(60));
149 out.push('\n');
150 out.push_str(&format!(
151 "{:<30} {:>5} {:>6} {:>5} {:>3} {:>3}\n",
152 "Total", total_ob, total_proved, total_sorry, total_wip, total_na
153 ));
154
155 if total_ob > 0 {
156 let pct = total_proved * 100 / total_ob;
157 out.push_str(&format!(
158 "L4 Coverage: {pct}% ({total_proved}/{total_ob}) Sorry Debt: {total_sorry}\n"
159 ));
160 }
161
162 out
163}
164
165fn derive_module_name(description: &str) -> String {
168 let base = description
169 .split_whitespace()
170 .next()
171 .unwrap_or("Unknown")
172 .to_string();
173 let mut chars = base.chars();
175 match chars.next() {
176 None => "Unknown".to_string(),
177 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
178 }
179}
180
181fn generate_defs_file(contract: &Contract, module_name: &str) -> LeanFile {
182 let mut content = String::new();
183
184 content.push_str(&format!("-- {}\n", contract.metadata.description));
185 content.push_str(&format!(
186 "-- Generated from contract v{}\n",
187 contract.metadata.version
188 ));
189 content.push_str("-- DO NOT EDIT — regenerate with `pv lean`\n\n");
190
191 let mut imports: Vec<&str> = Vec::new();
193 for ob in &contract.proof_obligations {
194 if let Some(ref lean) = ob.lean {
195 for imp in &lean.mathlib_imports {
196 if !imports.contains(&imp.as_str()) {
197 imports.push(imp);
198 }
199 }
200 }
201 }
202
203 content.push_str("import Mathlib.Data.Real.Basic\n");
204 content.push_str("import Mathlib.Data.Finset.Basic\n");
205 for imp in &imports {
206 content.push_str(&format!("import {imp}\n"));
207 }
208 content.push('\n');
209
210 content.push_str(&format!("namespace ProvableContracts.{module_name}\n\n"));
211
212 for (name, eq) in &contract.equations {
214 content.push_str(&format!("-- Equation: {name}\n"));
215 content.push_str(&format!("-- Formula: {}\n", eq.formula));
216 if let Some(ref domain) = eq.domain {
217 content.push_str(&format!("-- Domain: {domain}\n"));
218 }
219 content.push_str(&format!("noncomputable def {name} : sorry := sorry\n\n"));
220 }
221
222 content.push_str(&format!("end ProvableContracts.{module_name}\n"));
223
224 LeanFile {
225 path: format!("ProvableContracts/Defs/{module_name}.lean"),
226 content,
227 }
228}
229
230fn generate_theorem_file(
231 ob: &ProofObligation,
232 lean: &crate::schema::LeanProof,
233 module_name: &str,
234) -> LeanFile {
235 let mut content = String::new();
236
237 content.push_str(&format!("-- Theorem: {}\n", lean.theorem));
238 content.push_str(&format!("-- Property: {}\n", ob.property));
239 content.push_str(&format!("-- Obligation type: {}\n", ob.obligation_type));
240 content.push_str("-- Generated with `pv lean`\n\n");
241
242 content.push_str(&format!("import ProvableContracts.Defs.{module_name}\n"));
244 for imp in &lean.mathlib_imports {
245 content.push_str(&format!("import {imp}\n"));
246 }
247 content.push('\n');
248
249 if !lean.depends_on.is_empty() {
251 content.push_str("-- Dependencies:\n");
252 for dep in &lean.depends_on {
253 content.push_str(&format!("-- {dep}\n"));
254 }
255 content.push('\n');
256 }
257
258 content.push_str(&format!("namespace ProvableContracts.{module_name}\n\n"));
259
260 if let Some(ref formal) = ob.formal {
262 content.push_str(&format!("-- Formal: {formal}\n"));
263 }
264
265 let status_comment = match lean.status {
267 LeanStatus::Proved => "-- Status: proved",
268 LeanStatus::Sorry => "-- Status: sorry (proof pending)",
269 LeanStatus::Wip => "-- Status: work in progress",
270 LeanStatus::NotApplicable => "-- Status: not applicable",
271 };
272 content.push_str(&format!("{status_comment}\n"));
273 content.push_str(&format!("theorem {} : sorry := by\n", lean.theorem));
274 content.push_str(" sorry\n");
275
276 if let Some(ref notes) = lean.notes {
277 content.push_str(&format!("\n-- Note: {notes}\n"));
278 }
279
280 content.push_str(&format!("\nend ProvableContracts.{module_name}\n"));
281
282 let theorem_file = lean.theorem.split('.').next_back().unwrap_or(&lean.theorem);
284 LeanFile {
285 path: format!("ProvableContracts/Theorems/{module_name}/{theorem_file}.lean"),
286 content,
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::schema::parse_contract_str;
294
295 #[test]
296 fn no_lean_obligations_produces_empty() {
297 let yaml = r#"
298metadata:
299 version: "1.0.0"
300 description: "Softmax kernel"
301 references: ["Paper"]
302equations:
303 softmax:
304 formula: "f(x) = exp(x_i) / sum(exp(x_j))"
305proof_obligations:
306 - type: invariant
307 property: "Output sums to 1"
308falsification_tests: []
309"#;
310 let contract = parse_contract_str(yaml).unwrap();
311 let files = generate_lean_files(&contract);
312 assert!(files.is_empty());
313 }
314
315 #[test]
316 fn generates_defs_and_theorem_files() {
317 let yaml = r#"
318metadata:
319 version: "1.0.0"
320 description: "Softmax kernel"
321 references: ["Paper"]
322equations:
323 softmax:
324 formula: "f(x) = exp(x_i) / sum(exp(x_j))"
325 domain: "R^n"
326proof_obligations:
327 - type: invariant
328 property: "Output sums to 1"
329 formal: "|sum(f(x)) - 1| < eps"
330 lean:
331 theorem: Softmax.partition_of_unity
332 module: ProvableContracts.Softmax
333 status: sorry
334 depends_on:
335 - Real.exp_pos
336 mathlib_imports:
337 - Mathlib.Analysis.SpecialFunctions.ExpDeriv
338 notes: "Proof over reals"
339falsification_tests: []
340"#;
341 let contract = parse_contract_str(yaml).unwrap();
342 let files = generate_lean_files(&contract);
343 assert_eq!(files.len(), 2); let defs = &files[0];
347 assert!(defs.path.contains("Defs/Softmax"));
348 assert!(defs.content.contains("noncomputable def softmax"));
349 assert!(defs.content.contains("namespace ProvableContracts.Softmax"));
350 assert!(
351 defs.content
352 .contains("Mathlib.Analysis.SpecialFunctions.ExpDeriv")
353 );
354
355 let thm = &files[1];
357 assert!(thm.path.contains("Theorems/Softmax/partition_of_unity"));
358 assert!(thm.content.contains("theorem Softmax.partition_of_unity"));
359 assert!(thm.content.contains("sorry"));
360 assert!(thm.content.contains("Real.exp_pos"));
361 assert!(thm.content.contains("Proof over reals"));
362 }
363
364 #[test]
365 fn lean_status_counts_correctly() {
366 let yaml = r#"
367metadata:
368 version: "1.0.0"
369 description: "Test kernel"
370 references: ["Paper"]
371equations:
372 f:
373 formula: "f(x) = x"
374proof_obligations:
375 - type: invariant
376 property: "P1"
377 lean:
378 theorem: T1
379 status: proved
380 - type: bound
381 property: "P2"
382 lean:
383 theorem: T2
384 status: sorry
385 - type: monotonicity
386 property: "P3"
387 lean:
388 theorem: T3
389 status: wip
390 - type: equivalence
391 property: "P4 no lean"
392falsification_tests: []
393"#;
394 let contract = parse_contract_str(yaml).unwrap();
395 let report = lean_status(&contract);
396 assert_eq!(report.total_obligations, 4);
397 assert_eq!(report.with_lean, 3);
398 assert_eq!(report.proved, 1);
399 assert_eq!(report.sorry, 1);
400 assert_eq!(report.wip, 1);
401 assert_eq!(report.not_applicable, 0);
402 }
403
404 #[test]
405 fn format_status_report_renders_table() {
406 let reports = vec![LeanStatusReport {
407 contract_description: "Softmax kernel".to_string(),
408 total_obligations: 5,
409 with_lean: 3,
410 proved: 1,
411 sorry: 1,
412 wip: 1,
413 not_applicable: 0,
414 obligations: vec![],
415 }];
416 let table = format_status_report(&reports);
417 assert!(table.contains("Softmax kernel"));
418 assert!(table.contains("L4 Coverage: 33%"));
419 assert!(table.contains("Sorry Debt: 1"));
420 }
421
422 #[test]
423 fn derive_module_name_capitalizes() {
424 assert_eq!(derive_module_name("softmax kernel"), "Softmax");
425 assert_eq!(derive_module_name("RMSNorm"), "RMSNorm");
426 assert_eq!(derive_module_name(""), "Unknown");
427 }
428
429 #[test]
430 fn proved_theorem_has_proved_comment() {
431 let yaml = r#"
432metadata:
433 version: "1.0.0"
434 description: "Test"
435 references: ["P"]
436equations:
437 f:
438 formula: "f(x) = x"
439proof_obligations:
440 - type: invariant
441 property: "Always true"
442 lean:
443 theorem: F.always_true
444 status: proved
445falsification_tests: []
446"#;
447 let contract = parse_contract_str(yaml).unwrap();
448 let files = generate_lean_files(&contract);
449 let thm = &files[1];
450 assert!(thm.content.contains("Status: proved"));
451 }
452}