Skip to main content

karpal_verify/
artifact.rs

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