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
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}
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}