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