1#[cfg(not(feature = "std"))]
2use alloc::{collections::BTreeSet, format, string::String, vec::Vec};
3#[cfg(feature = "std")]
4use std::{collections::BTreeSet, format, string::String, vec::Vec};
5
6use crate::{
7 Obligation, ObligationBundle,
8 obligation::{Sort, Term},
9};
10
11pub struct Lean4;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16pub struct LeanImport {
17 pub module: String,
18}
19
20impl LeanImport {
21 pub fn new(module: impl Into<String>) -> Self {
22 Self {
23 module: module.into(),
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct LeanAlias {
31 pub alias: String,
32 pub target: String,
33}
34
35impl LeanAlias {
36 pub fn new(alias: impl Into<String>, target: impl Into<String>) -> Self {
37 Self {
38 alias: alias.into(),
39 target: target.into(),
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
46pub struct LeanPrelude {
47 pub imports: Vec<LeanImport>,
48 pub aliases: Vec<LeanAlias>,
49}
50
51impl LeanPrelude {
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn with_import(mut self, module: impl Into<String>) -> Self {
57 let import = LeanImport::new(module);
58 if !self.imports.contains(&import) {
59 self.imports.push(import);
60 }
61 self
62 }
63
64 pub fn with_alias(mut self, alias: impl Into<String>, target: impl Into<String>) -> Self {
65 let alias = LeanAlias::new(alias, target);
66 if !self
67 .aliases
68 .iter()
69 .any(|existing| existing.target == alias.target)
70 {
71 self.aliases.push(alias);
72 }
73 self
74 }
75
76 pub fn for_obligations(obligations: &[Obligation]) -> Self {
77 let mut prelude = Self::new();
78 let mut symbols = BTreeSet::new();
79
80 for obligation in obligations {
81 collect_term_symbols(&obligation.conclusion, &mut symbols);
82 for assumption in &obligation.assumptions {
83 collect_term_symbols(assumption, &mut symbols);
84 }
85 }
86
87 for symbol in symbols {
88 if !is_lean_identifier(&symbol) {
89 prelude = prelude.with_alias(alias_name(&symbol), symbol);
90 }
91 }
92
93 prelude
94 }
95
96 pub fn symbol_name(&self, target: &str) -> String {
97 self.aliases
98 .iter()
99 .find(|alias| alias.target == target)
100 .map(|alias| alias.alias.clone())
101 .unwrap_or_else(|| render_identifier(target))
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct LeanTheorem {
108 pub obligation_name: String,
109 pub theorem_name: String,
110 pub property: String,
111 pub origin_summary: String,
112 pub declaration_start_line: usize,
113 pub declaration_end_line: usize,
114}
115
116impl LeanTheorem {
117 pub fn witness_ref(&self, module_name: &str) -> String {
118 format!("{module_name}.{}", self.theorem_name)
119 }
120
121 pub fn contains_line(&self, line: usize) -> bool {
122 (self.declaration_start_line..=self.declaration_end_line).contains(&line)
123 }
124
125 pub fn with_line_span(mut self, start_line: usize, end_line: usize) -> Self {
126 self.declaration_start_line = start_line;
127 self.declaration_end_line = end_line.max(start_line);
128 self
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct LeanExport {
135 pub module_name: String,
136 pub source: String,
137 pub prelude: LeanPrelude,
138 pub theorems: Vec<LeanTheorem>,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct LeanProject {
144 pub package_name: String,
145 pub toolchain: String,
146 pub requires_mathlib: bool,
147}
148
149impl LeanProject {
150 pub fn new(package_name: impl Into<String>) -> Self {
151 Self {
152 package_name: package_name.into(),
153 toolchain: "leanprover/lean4:stable".into(),
154 requires_mathlib: false,
155 }
156 }
157
158 pub fn for_export(export: &LeanExport) -> Self {
159 let mut project = Self::new(sanitize(&export.module_name).to_lowercase());
160 project.requires_mathlib = export
161 .prelude
162 .imports
163 .iter()
164 .any(|import| import.module == "Mathlib");
165 project
166 }
167
168 pub fn with_toolchain(mut self, toolchain: impl Into<String>) -> Self {
169 self.toolchain = toolchain.into();
170 self
171 }
172
173 pub fn with_mathlib(mut self, requires_mathlib: bool) -> Self {
174 self.requires_mathlib = requires_mathlib;
175 self
176 }
177
178 pub fn render_lakefile(&self) -> String {
179 let mut lines = vec![
180 format!("package {}", render_identifier(&self.package_name)),
181 String::new(),
182 ];
183
184 if self.requires_mathlib {
185 lines.push("require mathlib from git".into());
186 lines.push(" \"https://github.com/leanprover-community/mathlib4.git\"".into());
187 lines.push(String::new());
188 }
189
190 lines.push(format!(
191 "@[default_target]\nlean_lib {}",
192 render_identifier(&self.package_name)
193 ));
194 lines.join("\n")
195 }
196
197 pub fn render_toolchain(&self) -> String {
198 self.toolchain.clone()
199 }
200}
201
202impl LeanExport {
203 pub fn theorem_for_obligation(&self, obligation_name: &str) -> Option<&LeanTheorem> {
204 self.theorems
205 .iter()
206 .find(|theorem| theorem.obligation_name == obligation_name)
207 }
208
209 pub fn theorem_names(&self) -> Vec<String> {
210 self.theorems
211 .iter()
212 .map(|theorem| theorem.theorem_name.clone())
213 .collect()
214 }
215
216 pub fn project(&self) -> LeanProject {
217 LeanProject::for_export(self)
218 }
219}
220
221impl Lean4 {
222 pub fn export_module(module_name: &str, obligations: &[Obligation]) -> String {
224 export_module(module_name, obligations)
225 }
226
227 pub fn export_module_with_prelude(
229 module_name: &str,
230 obligations: &[Obligation],
231 prelude: LeanPrelude,
232 ) -> String {
233 export_with_prelude(module_name, obligations, prelude).source
234 }
235
236 pub fn export(module_name: &str, obligations: &[Obligation]) -> LeanExport {
238 export(module_name, obligations)
239 }
240
241 pub fn export_with_prelude(
243 module_name: &str,
244 obligations: &[Obligation],
245 prelude: LeanPrelude,
246 ) -> LeanExport {
247 export_with_prelude(module_name, obligations, prelude)
248 }
249
250 pub fn export_bundle(module_name: &str, bundle: &ObligationBundle) -> String {
252 export_module(module_name, bundle.obligations())
253 }
254
255 pub fn export_bundle_structured(module_name: &str, bundle: &ObligationBundle) -> LeanExport {
257 export(module_name, bundle.obligations())
258 }
259
260 pub fn project(export: &LeanExport) -> LeanProject {
262 LeanProject::for_export(export)
263 }
264}
265
266pub fn export(module_name: &str, obligations: &[Obligation]) -> LeanExport {
267 export_with_prelude(
268 module_name,
269 obligations,
270 LeanPrelude::for_obligations(obligations),
271 )
272}
273
274pub fn export_with_prelude(
275 module_name: &str,
276 obligations: &[Obligation],
277 prelude: LeanPrelude,
278) -> LeanExport {
279 let mut theorems = Vec::new();
280 let mut lines = Vec::new();
281
282 for import in &prelude.imports {
283 lines.push(format!("import {}", import.module));
284 }
285 if !prelude.imports.is_empty() {
286 lines.push(String::new());
287 }
288
289 lines.push(format!("namespace {}", module_name));
290 lines.push(String::new());
291
292 for alias in &prelude.aliases {
293 lines.push(format!(
294 "abbrev {} := {}",
295 alias.alias,
296 render_identifier(&alias.target)
297 ));
298 }
299 if !prelude.aliases.is_empty() {
300 lines.push(String::new());
301 }
302
303 for obligation in obligations {
304 let theorem = LeanTheorem::from(obligation);
305 let theorem_start_line = lines.len() + 3;
306 let theorem_text = render_theorem(obligation, &theorem, &prelude);
307 let theorem_line_count = theorem_text.lines().count();
308 let theorem_end_line = theorem_start_line + theorem_line_count.saturating_sub(1);
309
310 lines.push(format!("-- property: {}", obligation.property));
311 lines.push(format!("-- origin: {}", obligation.summary()));
312 lines.push(theorem_text);
313 lines.push(String::new());
314
315 theorems.push(theorem.with_line_span(theorem_start_line, theorem_end_line));
316 }
317
318 lines.push(format!("end {}", module_name));
319
320 LeanExport {
321 module_name: module_name.into(),
322 source: lines.join("\n"),
323 prelude,
324 theorems,
325 }
326}
327
328pub fn export_module(module_name: &str, obligations: &[Obligation]) -> String {
329 export(module_name, obligations).source
330}
331
332pub fn export_module_with_prelude(
333 module_name: &str,
334 obligations: &[Obligation],
335 prelude: LeanPrelude,
336) -> String {
337 export_with_prelude(module_name, obligations, prelude).source
338}
339
340fn render_theorem(obligation: &Obligation, theorem: &LeanTheorem, prelude: &LeanPrelude) -> String {
341 let binders = obligation
342 .declarations
343 .iter()
344 .map(|decl| {
345 format!(
346 "({} : {})",
347 render_identifier(&decl.name),
348 render_sort(&decl.sort)
349 )
350 })
351 .collect::<Vec<_>>()
352 .join(" ");
353
354 let assumptions = obligation
355 .assumptions
356 .iter()
357 .enumerate()
358 .map(|(idx, term)| format!("(h{} : {})", idx + 1, render_term(term, prelude)))
359 .collect::<Vec<_>>()
360 .join(" ");
361
362 let mut header = format!("theorem {}", theorem.theorem_name);
363 if !binders.is_empty() {
364 header.push(' ');
365 header.push_str(&binders);
366 }
367 if !assumptions.is_empty() {
368 header.push(' ');
369 header.push_str(&assumptions);
370 }
371 header.push_str(&format!(
372 " : {} := by",
373 render_term(&obligation.conclusion, prelude)
374 ));
375 header.push_str("\n sorry");
376 header
377}
378
379fn render_sort(sort: &Sort) -> String {
380 match sort {
381 Sort::Bool => "Bool".to_string(),
382 Sort::Int => "Int".to_string(),
383 Sort::Real => "Rat".to_string(),
384 Sort::Named(name) => render_identifier(name),
385 }
386}
387
388fn render_term(term: &Term, prelude: &LeanPrelude) -> String {
389 match term {
390 Term::Var(name) => render_identifier(name),
391 Term::Bool(true) => "True".to_string(),
392 Term::Bool(false) => "False".to_string(),
393 Term::Int(value) => value.to_string(),
394 Term::App { function, args } => {
395 let function = prelude.symbol_name(function);
396 if args.is_empty() {
397 function
398 } else {
399 format!(
400 "({} {})",
401 function,
402 args.iter()
403 .map(|term| render_term(term, prelude))
404 .collect::<Vec<_>>()
405 .join(" ")
406 )
407 }
408 }
409 Term::Eq(left, right) => format!(
410 "{} = {}",
411 render_term(left, prelude),
412 render_term(right, prelude)
413 ),
414 Term::And(terms) => match terms.as_slice() {
415 [] => "True".to_string(),
416 [term] => render_term(term, prelude),
417 _ => terms
418 .iter()
419 .map(|term| render_term(term, prelude))
420 .collect::<Vec<_>>()
421 .join(" ∧ "),
422 },
423 Term::Or(terms) => match terms.as_slice() {
424 [] => "False".to_string(),
425 [term] => render_term(term, prelude),
426 _ => terms
427 .iter()
428 .map(|term| render_term(term, prelude))
429 .collect::<Vec<_>>()
430 .join(" ∨ "),
431 },
432 Term::Not(inner) => format!("¬{}", parenthesize(inner, prelude)),
433 Term::Implies(lhs, rhs) => format!(
434 "{} → {}",
435 parenthesize(lhs, prelude),
436 render_term(rhs, prelude)
437 ),
438 }
439}
440
441fn parenthesize(term: &Term, prelude: &LeanPrelude) -> String {
442 match term {
443 Term::Var(_) | Term::Bool(_) | Term::Int(_) | Term::App { .. } => {
444 render_term(term, prelude)
445 }
446 _ => format!("({})", render_term(term, prelude)),
447 }
448}
449
450fn collect_term_symbols(term: &Term, symbols: &mut BTreeSet<String>) {
451 match term {
452 Term::Var(_) | Term::Bool(_) | Term::Int(_) => {}
453 Term::App { function, args } => {
454 symbols.insert(function.clone());
455 for arg in args {
456 collect_term_symbols(arg, symbols);
457 }
458 }
459 Term::Eq(left, right) | Term::Implies(left, right) => {
460 collect_term_symbols(left, symbols);
461 collect_term_symbols(right, symbols);
462 }
463 Term::And(terms) | Term::Or(terms) => {
464 for term in terms {
465 collect_term_symbols(term, symbols);
466 }
467 }
468 Term::Not(inner) => collect_term_symbols(inner, symbols),
469 }
470}
471
472fn render_identifier(name: &str) -> String {
473 if is_lean_identifier(name) {
474 name.to_string()
475 } else {
476 format!("«{}»", name.replace('»', "_"))
477 }
478}
479
480fn alias_name(name: &str) -> String {
481 let mut alias = String::from("sym_");
482 alias.push_str(&sanitize(name));
483 alias
484}
485
486fn is_lean_identifier(name: &str) -> bool {
487 let mut chars = name.chars();
488 let Some(first) = chars.next() else {
489 return false;
490 };
491
492 if !(first.is_ascii_alphabetic() || first == '_') {
493 return false;
494 }
495
496 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '\'')
497}
498
499fn sanitize(name: &str) -> String {
500 name.chars()
501 .map(|c| {
502 if c.is_ascii_alphanumeric() || c == '_' {
503 c
504 } else {
505 '_'
506 }
507 })
508 .collect()
509}
510
511impl From<&Obligation> for LeanTheorem {
512 fn from(obligation: &Obligation) -> Self {
513 Self {
514 obligation_name: obligation.name.clone(),
515 theorem_name: sanitize(&obligation.name),
516 property: obligation.property.into(),
517 origin_summary: obligation.summary(),
518 declaration_start_line: 0,
519 declaration_end_line: 0,
520 }
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crate::obligation::{Origin, VerificationTier};
528 use karpal_proof::IsCommutative;
529
530 #[test]
531 fn exports_theorem_skeleton() {
532 let obligation = Obligation::for_property::<IsCommutative>(
533 "sum/commutative",
534 Origin::new("karpal-algebra", "AbelianGroup for i16"),
535 VerificationTier::External,
536 Term::eq(
537 Term::app("combine", [Term::var("a"), Term::var("b")]),
538 Term::app("combine", [Term::var("b"), Term::var("a")]),
539 ),
540 )
541 .with_decl("a", Sort::Int)
542 .with_decl("b", Sort::Int);
543
544 let text = export_module("KarpalVerify", &[obligation]);
545 assert!(text.contains("namespace KarpalVerify"));
546 assert!(text.contains("theorem sum_commutative"));
547 assert!(text.contains("sorry"));
548 }
549
550 #[test]
551 fn structured_export_tracks_theorem_identity() {
552 let obligation = Obligation::associativity(
553 "sum-assoc",
554 Origin::new("karpal-core", "Semigroup for i32"),
555 Sort::Int,
556 "combine",
557 );
558
559 let export = export("KarpalVerify", &[obligation]);
560 assert_eq!(export.module_name, "KarpalVerify");
561 assert_eq!(export.theorems.len(), 1);
562 assert_eq!(export.theorems[0].theorem_name, "sum_assoc");
563 assert_eq!(
564 export.theorems[0].witness_ref("KarpalVerify"),
565 "KarpalVerify.sum_assoc"
566 );
567 assert!(export.theorems[0].declaration_start_line > 0);
568 assert!(
569 export.theorems[0].declaration_end_line >= export.theorems[0].declaration_start_line
570 );
571 assert!(export.source.contains("theorem sum_assoc"));
572 }
573
574 #[test]
575 fn derived_prelude_aliases_invalid_symbols() {
576 let obligation = Obligation::for_property::<IsCommutative>(
577 "sum/commutative",
578 Origin::new("karpal-algebra", "AbelianGroup for i16"),
579 VerificationTier::External,
580 Term::eq(
581 Term::app("combine-op", [Term::var("a"), Term::var("b")]),
582 Term::app("combine-op", [Term::var("b"), Term::var("a")]),
583 ),
584 )
585 .with_decl("a", Sort::Int)
586 .with_decl("b", Sort::Int);
587
588 let export = export("KarpalVerify", &[obligation]);
589 assert_eq!(export.prelude.aliases.len(), 1);
590 assert_eq!(export.prelude.aliases[0].alias, "sym_combine_op");
591 assert_eq!(export.prelude.aliases[0].target, "combine-op");
592 assert!(
593 export
594 .source
595 .contains("abbrev sym_combine_op := «combine-op»")
596 );
597 assert!(
598 export
599 .source
600 .contains("(sym_combine_op a b) = (sym_combine_op b a)")
601 );
602 }
603
604 #[test]
605 fn explicit_prelude_renders_imports_before_namespace() {
606 let obligation = Obligation::associativity(
607 "sum-assoc",
608 Origin::new("karpal-core", "Semigroup for i32"),
609 Sort::Int,
610 "combine",
611 );
612 let prelude = LeanPrelude::new().with_import("Mathlib");
613
614 let text = export_module_with_prelude("KarpalVerify", &[obligation], prelude);
615 assert!(text.starts_with("import Mathlib\n\nnamespace KarpalVerify"));
616 }
617
618 #[test]
619 fn project_derives_mathlib_requirement_from_prelude() {
620 let obligation = Obligation::associativity(
621 "sum-assoc",
622 Origin::new("karpal-core", "Semigroup for i32"),
623 Sort::Int,
624 "combine",
625 );
626 let export = export_with_prelude(
627 "KarpalVerify",
628 &[obligation],
629 LeanPrelude::new().with_import("Mathlib"),
630 );
631 let project = export.project();
632
633 assert_eq!(project.package_name, "karpalverify");
634 assert!(project.requires_mathlib);
635 assert!(
636 project
637 .render_lakefile()
638 .contains("require mathlib from git")
639 );
640 assert_eq!(project.render_toolchain(), "leanprover/lean4:stable");
641 }
642}