1use crate::{
2 InvocationPlan, KaniConfig, LeanConfig, LeanExport, LeanProject, ObligationBundle, SmtConfig,
3 export_kani_bundle, export_lean_bundle_structured, export_smt_bundle,
4};
5
6#[cfg(not(feature = "std"))]
7use alloc::{string::String, vec::Vec};
8#[cfg(feature = "std")]
9use std::{
10 fs,
11 path::{Path, PathBuf},
12 string::String,
13 vec::Vec,
14};
15
16pub const LEAN_MANIFEST_SCHEMA_VERSION: &str = "1";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ArtifactRecord {
22 pub name: String,
23 pub path: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct LeanManifestReportFiles {
29 pub schema_version: String,
30 pub json_path: String,
31 pub markdown_path: String,
32 pub lean_diagnostics_json_path: Option<String>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct LeanManifestProject {
38 pub package_name: String,
39 pub toolchain: String,
40 pub requires_mathlib: bool,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct LeanManifestAlias {
46 pub alias: String,
47 pub target: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct LeanManifestPrelude {
53 pub imports: Vec<String>,
54 pub aliases: Vec<LeanManifestAlias>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct LeanManifestTheorem {
60 pub obligation_name: String,
61 pub theorem_name: String,
62 pub witness_ref: String,
63 pub declaration_start_line: usize,
64 pub declaration_end_line: usize,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct LeanManifest {
70 pub schema_version: String,
71 pub module_name: String,
72 pub project: LeanManifestProject,
73 pub prelude: LeanManifestPrelude,
74 pub theorems: Vec<LeanManifestTheorem>,
75 pub report_files: Option<LeanManifestReportFiles>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ArtifactBatch {
81 pub root: String,
82 pub records: Vec<ArtifactRecord>,
83 pub plans: Vec<InvocationPlan>,
84 pub lean_export: Option<LeanExport>,
85 pub lean_project: Option<LeanProject>,
86 pub lean_manifest: Option<LeanManifest>,
87}
88
89#[cfg(feature = "std")]
90#[derive(Debug, Clone)]
91pub struct ArtifactLayout {
92 pub root: PathBuf,
93 pub smt_dir: PathBuf,
94 pub lean_dir: PathBuf,
95 pub kani_dir: PathBuf,
96}
97
98#[cfg(feature = "std")]
99impl ArtifactLayout {
100 pub fn new(root: impl AsRef<Path>) -> Self {
101 let root = root.as_ref().to_path_buf();
102 Self {
103 smt_dir: root.join("smt"),
104 lean_dir: root.join("lean"),
105 kani_dir: root.join("kani"),
106 root,
107 }
108 }
109}
110
111impl LeanManifestReportFiles {
112 pub fn new(json_path: impl Into<String>, markdown_path: impl Into<String>) -> Self {
113 Self {
114 schema_version: LEAN_MANIFEST_SCHEMA_VERSION.into(),
115 json_path: json_path.into(),
116 markdown_path: markdown_path.into(),
117 lean_diagnostics_json_path: None,
118 }
119 }
120
121 pub fn with_lean_diagnostics_json_path(mut self, path: impl Into<String>) -> Self {
122 self.lean_diagnostics_json_path = Some(path.into());
123 self
124 }
125}
126
127impl LeanManifest {
128 pub fn from_export(export: &LeanExport, project: &LeanProject) -> Self {
129 Self {
130 schema_version: LEAN_MANIFEST_SCHEMA_VERSION.into(),
131 module_name: export.module_name.clone(),
132 project: LeanManifestProject {
133 package_name: project.package_name.clone(),
134 toolchain: project.toolchain.clone(),
135 requires_mathlib: project.requires_mathlib,
136 },
137 prelude: LeanManifestPrelude {
138 imports: export
139 .prelude
140 .imports
141 .iter()
142 .map(|import| import.module.clone())
143 .collect(),
144 aliases: export
145 .prelude
146 .aliases
147 .iter()
148 .map(|alias| LeanManifestAlias {
149 alias: alias.alias.clone(),
150 target: alias.target.clone(),
151 })
152 .collect(),
153 },
154 theorems: export
155 .theorems
156 .iter()
157 .map(|theorem| LeanManifestTheorem {
158 obligation_name: theorem.obligation_name.clone(),
159 theorem_name: theorem.theorem_name.clone(),
160 witness_ref: theorem.witness_ref(&export.module_name),
161 declaration_start_line: theorem.declaration_start_line,
162 declaration_end_line: theorem.declaration_end_line,
163 })
164 .collect(),
165 report_files: None,
166 }
167 }
168
169 pub fn with_report_files(mut self, report_files: LeanManifestReportFiles) -> Self {
170 self.report_files = Some(report_files);
171 self
172 }
173
174 pub fn to_json(&self) -> String {
175 fn esc(s: &str) -> String {
176 s.replace('\\', "\\\\")
177 .replace('"', "\\\"")
178 .replace('\n', "\\n")
179 }
180
181 let import_entries = self
182 .prelude
183 .imports
184 .iter()
185 .map(|import| format!("\"{}\"", esc(import)))
186 .collect::<Vec<_>>()
187 .join(",");
188
189 let alias_entries = self
190 .prelude
191 .aliases
192 .iter()
193 .map(|alias| {
194 format!(
195 "{{\"alias\":\"{}\",\"target\":\"{}\"}}",
196 esc(&alias.alias),
197 esc(&alias.target)
198 )
199 })
200 .collect::<Vec<_>>()
201 .join(",");
202
203 let theorem_entries = self
204 .theorems
205 .iter()
206 .map(|theorem| {
207 format!(
208 "{{\"obligation_name\":\"{}\",\"theorem_name\":\"{}\",\"witness_ref\":\"{}\",\"declaration_start_line\":{},\"declaration_end_line\":{}}}",
209 esc(&theorem.obligation_name),
210 esc(&theorem.theorem_name),
211 esc(&theorem.witness_ref),
212 theorem.declaration_start_line,
213 theorem.declaration_end_line
214 )
215 })
216 .collect::<Vec<_>>()
217 .join(",");
218
219 let report_files = self
220 .report_files
221 .as_ref()
222 .map(|report_files| {
223 let mut json = format!(
224 "\"report_files\":{{\"schema_version\":\"{}\",\"json_path\":\"{}\",\"markdown_path\":\"{}\"",
225 esc(&report_files.schema_version),
226 esc(&report_files.json_path),
227 esc(&report_files.markdown_path)
228 );
229 if let Some(path) = &report_files.lean_diagnostics_json_path {
230 json.push_str(&format!(
231 ",\"lean_diagnostics_json_path\":\"{}\"",
232 esc(path)
233 ));
234 }
235 json.push('}');
236 json
237 })
238 .unwrap_or_default();
239
240 let mut json = format!(
241 "{{\"schema_version\":\"{}\",\"module_name\":\"{}\",\"project\":{{\"package_name\":\"{}\",\"toolchain\":\"{}\",\"requires_mathlib\":{}}},\"prelude\":{{\"imports\":[{}],\"aliases\":[{}]}},\"theorems\":[{}]",
242 esc(&self.schema_version),
243 esc(&self.module_name),
244 esc(&self.project.package_name),
245 esc(&self.project.toolchain),
246 self.project.requires_mathlib,
247 import_entries,
248 alias_entries,
249 theorem_entries
250 );
251 if !report_files.is_empty() {
252 json.push(',');
253 json.push_str(&report_files);
254 }
255 json.push('}');
256 json
257 }
258}
259
260#[cfg(feature = "std")]
261pub fn write_bundle_artifacts(
262 bundle: &ObligationBundle,
263 layout: &ArtifactLayout,
264 lean_module_name: &str,
265 smt: &SmtConfig,
266 lean: &LeanConfig,
267) -> std::io::Result<ArtifactBatch> {
268 fs::create_dir_all(&layout.smt_dir)?;
269 fs::create_dir_all(&layout.lean_dir)?;
270 fs::create_dir_all(&layout.kani_dir)?;
271
272 let mut records = Vec::new();
273 let mut plans = Vec::new();
274 let kani = KaniConfig::default();
275
276 for (name, script) in export_smt_bundle(bundle) {
277 let path = layout.smt_dir.join(format!("{name}.smt2"));
278 fs::write(&path, script)?;
279 plans.push(InvocationPlan::smt(smt, &path));
280 records.push(ArtifactRecord {
281 name,
282 path: path_to_string(&path),
283 });
284 }
285
286 for harness in export_kani_bundle(bundle) {
287 let path = layout.kani_dir.join(format!("{}.rs", harness.harness_name));
288 fs::write(&path, harness.source)?;
289 plans.push(InvocationPlan::kani(&kani, &path, &harness.harness_name));
290 records.push(ArtifactRecord {
291 name: format!("{}_kani", harness.obligation_name),
292 path: path_to_string(&path),
293 });
294 }
295
296 let lean_export = export_lean_bundle_structured(lean_module_name, bundle);
297 let lean_project = lean_export.project();
298 let lean_manifest = LeanManifest::from_export(&lean_export, &lean_project);
299 let lean_path = layout.lean_dir.join(format!("{lean_module_name}.lean"));
300 fs::write(&lean_path, &lean_export.source)?;
301 plans.push(InvocationPlan::lean(lean, &lean_path));
302 records.push(ArtifactRecord {
303 name: lean_module_name.into(),
304 path: path_to_string(&lean_path),
305 });
306
307 let manifest_path = layout
308 .lean_dir
309 .join(format!("{lean_module_name}.manifest.json"));
310 fs::write(&manifest_path, lean_manifest.to_json())?;
311 records.push(ArtifactRecord {
312 name: format!("{lean_module_name}_manifest"),
313 path: path_to_string(&manifest_path),
314 });
315
316 let lakefile_path = layout.root.join("lakefile.lean");
317 fs::write(&lakefile_path, lean_project.render_lakefile())?;
318 records.push(ArtifactRecord {
319 name: "lakefile".into(),
320 path: path_to_string(&lakefile_path),
321 });
322
323 let toolchain_path = layout.root.join("lean-toolchain");
324 fs::write(&toolchain_path, lean_project.render_toolchain())?;
325 records.push(ArtifactRecord {
326 name: "lean_toolchain".into(),
327 path: path_to_string(&toolchain_path),
328 });
329
330 Ok(ArtifactBatch {
331 root: path_to_string(&layout.root),
332 records,
333 plans,
334 lean_export: Some(lean_export),
335 lean_project: Some(lean_project),
336 lean_manifest: Some(lean_manifest),
337 })
338}
339
340#[cfg(feature = "std")]
341pub fn dry_run_bundle_artifacts(
342 bundle: &ObligationBundle,
343 layout: &ArtifactLayout,
344 lean_module_name: &str,
345 smt: &SmtConfig,
346 lean: &LeanConfig,
347) -> ArtifactBatch {
348 let mut records = Vec::new();
349 let mut plans = Vec::new();
350 let kani = KaniConfig::default();
351
352 for (name, _) in export_smt_bundle(bundle) {
353 let path = layout.smt_dir.join(format!("{name}.smt2"));
354 plans.push(InvocationPlan::smt(smt, &path));
355 records.push(ArtifactRecord {
356 name,
357 path: path_to_string(&path),
358 });
359 }
360
361 for harness in export_kani_bundle(bundle) {
362 let path = layout.kani_dir.join(format!("{}.rs", harness.harness_name));
363 plans.push(InvocationPlan::kani(&kani, &path, &harness.harness_name));
364 records.push(ArtifactRecord {
365 name: format!("{}_kani", harness.obligation_name),
366 path: path_to_string(&path),
367 });
368 }
369
370 let lean_export = export_lean_bundle_structured(lean_module_name, bundle);
371 let lean_project = lean_export.project();
372 let lean_manifest = LeanManifest::from_export(&lean_export, &lean_project);
373 let lean_path = layout.lean_dir.join(format!("{lean_module_name}.lean"));
374 plans.push(InvocationPlan::lean(lean, &lean_path));
375 records.push(ArtifactRecord {
376 name: lean_module_name.into(),
377 path: path_to_string(&lean_path),
378 });
379
380 let manifest_path = layout
381 .lean_dir
382 .join(format!("{lean_module_name}.manifest.json"));
383 records.push(ArtifactRecord {
384 name: format!("{lean_module_name}_manifest"),
385 path: path_to_string(&manifest_path),
386 });
387
388 let lakefile_path = layout.root.join("lakefile.lean");
389 records.push(ArtifactRecord {
390 name: "lakefile".into(),
391 path: path_to_string(&lakefile_path),
392 });
393
394 let toolchain_path = layout.root.join("lean-toolchain");
395 records.push(ArtifactRecord {
396 name: "lean_toolchain".into(),
397 path: path_to_string(&toolchain_path),
398 });
399
400 ArtifactBatch {
401 root: path_to_string(&layout.root),
402 records,
403 plans,
404 lean_export: Some(lean_export),
405 lean_project: Some(lean_project),
406 lean_manifest: Some(lean_manifest),
407 }
408}
409
410#[cfg(feature = "std")]
411fn path_to_string(path: &Path) -> String {
412 path.to_string_lossy().into_owned()
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::{AlgebraicSignature, Origin, Sort};
419
420 #[cfg(feature = "std")]
421 #[test]
422 fn dry_run_creates_expected_paths_and_plans() {
423 let sig = AlgebraicSignature::monoid(Sort::Int, "combine", "e");
424 let bundle = ObligationBundle::monoid("sum", Origin::new("karpal-core", "Sum<i32>"), &sig);
425 let layout = ArtifactLayout::new("target/karpal-verify-test");
426 let batch = dry_run_bundle_artifacts(
427 &bundle,
428 &layout,
429 "KarpalVerify",
430 &SmtConfig::default(),
431 &LeanConfig::default().with_driver(crate::LeanDriver::LakeEnv),
432 );
433
434 assert_eq!(batch.records.len(), 10);
435 assert_eq!(batch.plans.len(), 7);
436 assert!(
437 batch
438 .records
439 .iter()
440 .any(|r| r.path.ends_with("kani/associativity.rs"))
441 );
442 assert!(
443 batch
444 .plans
445 .iter()
446 .any(|plan| plan.kind == crate::CommandKind::Kani)
447 );
448 assert!(
449 batch
450 .records
451 .iter()
452 .any(|r| r.path.ends_with("KarpalVerify.lean"))
453 );
454 assert!(
455 batch
456 .records
457 .iter()
458 .any(|r| r.path.ends_with("KarpalVerify.manifest.json"))
459 );
460 assert_eq!(
461 batch.lean_export.as_ref().unwrap().module_name,
462 "KarpalVerify"
463 );
464 assert_eq!(
465 batch.lean_project.as_ref().unwrap().package_name,
466 "karpalverify"
467 );
468 assert_eq!(
469 batch.lean_manifest.as_ref().unwrap().schema_version,
470 LEAN_MANIFEST_SCHEMA_VERSION
471 );
472 assert_eq!(
473 batch.lean_manifest.as_ref().unwrap().module_name,
474 "KarpalVerify"
475 );
476 assert!(
477 batch
478 .records
479 .iter()
480 .any(|r| r.path.ends_with("lakefile.lean"))
481 );
482 assert!(
483 batch
484 .records
485 .iter()
486 .any(|r| r.path.ends_with("lean-toolchain"))
487 );
488 assert!(
489 batch
490 .plans
491 .iter()
492 .any(|plan| plan.kind == crate::CommandKind::Lean && plan.executable == "lake")
493 );
494 }
495
496 #[cfg(feature = "std")]
497 #[test]
498 fn write_bundle_artifacts_writes_files() {
499 let sig = AlgebraicSignature::semigroup(Sort::Int, "combine");
500 let bundle =
501 ObligationBundle::semigroup("sum", Origin::new("karpal-core", "Sum<i32>"), &sig);
502 let temp = std::env::temp_dir().join("karpal_verify_artifacts_test");
503 if temp.exists() {
504 let _ = fs::remove_dir_all(&temp);
505 }
506 let layout = ArtifactLayout::new(&temp);
507
508 let batch = write_bundle_artifacts(
509 &bundle,
510 &layout,
511 "KarpalVerify",
512 &SmtConfig::default(),
513 &LeanConfig::default(),
514 )
515 .expect("artifact write should succeed");
516
517 assert!(
518 batch
519 .records
520 .iter()
521 .all(|record| Path::new(&record.path).exists())
522 );
523 assert!(batch.lean_export.is_some());
524 assert!(batch.lean_project.is_some());
525 assert!(batch.lean_manifest.is_some());
526 let manifest = fs::read_to_string(temp.join("lean").join("KarpalVerify.manifest.json"))
527 .expect("lean manifest should be readable");
528 assert!(manifest.contains("\"schema_version\":\"1\""));
529 assert!(temp.join("lakefile.lean").exists());
530 assert!(temp.join("lean-toolchain").exists());
531
532 let _ = fs::remove_dir_all(&temp);
533 }
534}