Skip to main content

karpal_verify/
artifact.rs

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
16/// Schema version for serialized Lean manifest JSON.
17pub const LEAN_MANIFEST_SCHEMA_VERSION: &str = "1";
18
19/// Written artifact metadata.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ArtifactRecord {
22    pub name: String,
23    pub path: String,
24}
25
26/// Report file links attached back onto a generated Lean manifest.
27#[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/// Lean package metadata serialized into the generated manifest.
36#[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/// Lean import alias metadata serialized into the generated manifest.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct LeanManifestAlias {
46    pub alias: String,
47    pub target: String,
48}
49
50/// Lean prelude metadata serialized into the generated manifest.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct LeanManifestPrelude {
53    pub imports: Vec<String>,
54    pub aliases: Vec<LeanManifestAlias>,
55}
56
57/// Lean theorem metadata serialized into the generated manifest.
58#[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/// Typed manifest model for generated Lean verification artifacts.
68#[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/// Result of preparing or writing a verification batch.
79#[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}