1use crate::{
2 ArtifactBatch, Certificate, DryRunner, ExecutionResult, ExecutionStatus, Obligation,
3 ObligationBundle, VerifierRunner,
4};
5
6#[cfg(feature = "std")]
7use std::fmt::Write as _;
8
9#[cfg(not(feature = "std"))]
10use alloc::{string::String, vec::Vec};
11#[cfg(feature = "std")]
12use std::{string::String, vec::Vec};
13
14pub const VERIFICATION_REPORT_SCHEMA_VERSION: &str = "1";
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ObligationReport {
20 pub obligation_name: String,
21 pub summary: String,
22 pub artifact_path: Option<String>,
23 pub lean_theorem_ref: Option<String>,
24 pub lean_diagnostics: Vec<String>,
25 pub result: Option<ExecutionResult>,
26 pub certificate: Option<Certificate>,
27 pub lean_certificate: Option<Certificate>,
28}
29
30impl ObligationReport {
31 pub fn status(&self) -> Option<ExecutionStatus> {
32 self.result.as_ref().map(|result| result.status)
33 }
34
35 pub fn succeeded(&self) -> bool {
36 self.result
37 .as_ref()
38 .map(|result| result.is_success())
39 .unwrap_or(false)
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ModuleReport {
46 pub module_name: String,
47 pub artifact_path: Option<String>,
48 pub theorem_refs: Vec<String>,
49 pub prelude_imports: Vec<String>,
50 pub prelude_aliases: Vec<String>,
51 pub diagnostics: Vec<String>,
52 pub theorem_failures: Vec<String>,
53 pub result: Option<ExecutionResult>,
54 pub certificate: Option<Certificate>,
55}
56
57impl ModuleReport {
58 pub fn status(&self) -> Option<ExecutionStatus> {
59 self.result.as_ref().map(|result| result.status)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct VerificationReport {
66 pub bundle_name: String,
67 pub root: String,
68 pub obligations: Vec<ObligationReport>,
69 pub lean_module: Option<ModuleReport>,
70}
71
72impl VerificationReport {
73 pub fn obligation_count(&self) -> usize {
74 self.obligations.len()
75 }
76
77 pub fn success_count(&self) -> usize {
78 self.obligations
79 .iter()
80 .filter(|report| report.succeeded())
81 .count()
82 }
83
84 pub fn failure_count(&self) -> usize {
85 self.obligation_count().saturating_sub(self.success_count())
86 }
87
88 pub fn is_success(&self) -> bool {
89 self.failure_count() == 0
90 }
91
92 #[cfg(feature = "std")]
93 pub fn to_json(&self) -> String {
94 fn esc(s: &str) -> String {
95 s.replace('\\', "\\\\")
96 .replace('"', "\\\"")
97 .replace('\n', "\\n")
98 }
99
100 fn render_certificate_json(certificate: &Certificate) -> String {
101 format!(
102 "{{\"backend\":\"{}\",\"witness_ref\":\"{}\"}}",
103 esc(certificate.backend),
104 esc(&certificate.witness_ref)
105 )
106 }
107
108 let mut out = String::new();
109 let _ = write!(
110 out,
111 "{{\"schema_version\":\"{}\",\"bundle_name\":\"{}\",\"root\":\"{}\",\"success_count\":{},\"failure_count\":{},\"obligations\":[",
112 VERIFICATION_REPORT_SCHEMA_VERSION,
113 esc(&self.bundle_name),
114 esc(&self.root),
115 self.success_count(),
116 self.failure_count()
117 );
118
119 for (idx, obligation) in self.obligations.iter().enumerate() {
120 if idx > 0 {
121 out.push(',');
122 }
123 let _ = write!(
124 out,
125 "{{\"name\":\"{}\",\"summary\":\"{}\",\"status\":\"{}\",\"artifact_path\":{},\"lean_theorem_ref\":{},\"lean_diagnostic_count\":{},\"certificate\":{},\"lean_certificate\":{}}}",
126 esc(&obligation.obligation_name),
127 esc(&obligation.summary),
128 obligation
129 .status()
130 .map(|s| format!("{s:?}"))
131 .unwrap_or_else(|| "None".into()),
132 obligation
133 .artifact_path
134 .as_ref()
135 .map(|p| format!("\"{}\"", esc(p)))
136 .unwrap_or_else(|| "null".into()),
137 obligation
138 .lean_theorem_ref
139 .as_ref()
140 .map(|r| format!("\"{}\"", esc(r)))
141 .unwrap_or_else(|| "null".into()),
142 obligation.lean_diagnostics.len(),
143 obligation
144 .certificate
145 .as_ref()
146 .map(render_certificate_json)
147 .unwrap_or_else(|| "null".into()),
148 obligation
149 .lean_certificate
150 .as_ref()
151 .map(render_certificate_json)
152 .unwrap_or_else(|| "null".into())
153 );
154 }
155 out.push(']');
156 if let Some(module) = &self.lean_module {
157 let _ = write!(
158 out,
159 ",\"lean_module\":{{\"module_name\":\"{}\",\"status\":\"{}\",\"theorem_count\":{},\"import_count\":{},\"alias_count\":{},\"diagnostic_count\":{},\"theorem_failure_count\":{},\"certificate\":{}}}",
160 esc(&module.module_name),
161 module
162 .status()
163 .map(|s| format!("{s:?}"))
164 .unwrap_or_else(|| "None".into()),
165 module.theorem_refs.len(),
166 module.prelude_imports.len(),
167 module.prelude_aliases.len(),
168 module.diagnostics.len(),
169 module.theorem_failures.len(),
170 module
171 .certificate
172 .as_ref()
173 .map(render_certificate_json)
174 .unwrap_or_else(|| "null".into())
175 );
176 }
177 out.push('}');
178 out
179 }
180
181 #[cfg(feature = "std")]
182 pub fn to_markdown(&self) -> String {
183 let mut out = String::new();
184 let _ = writeln!(out, "# Verification Report");
185 let _ = writeln!(out);
186 let _ = writeln!(
187 out,
188 "- Schema version: `{}`",
189 VERIFICATION_REPORT_SCHEMA_VERSION
190 );
191 let _ = writeln!(out, "- Bundle: `{}`", self.bundle_name);
192 let _ = writeln!(out, "- Root: `{}`", self.root);
193 let _ = writeln!(out, "- Successes: {}", self.success_count());
194 let _ = writeln!(out, "- Failures: {}", self.failure_count());
195 let _ = writeln!(out);
196 let _ = writeln!(
197 out,
198 "| Obligation | Status | Artifact | Lean theorem | Lean diagnostics | SMT certificate | Lean certificate |"
199 );
200 let _ = writeln!(out, "|---|---|---|---|---|---|---|");
201 for obligation in &self.obligations {
202 let _ = writeln!(
203 out,
204 "| `{}` | `{}` | `{}` | `{}` | `{}` | `{}` | `{}` |",
205 obligation.obligation_name,
206 obligation
207 .status()
208 .map(|s| format!("{s:?}"))
209 .unwrap_or_else(|| "None".into()),
210 obligation.artifact_path.as_deref().unwrap_or("-"),
211 obligation.lean_theorem_ref.as_deref().unwrap_or("-"),
212 if obligation.lean_diagnostics.is_empty() {
213 "-".into()
214 } else {
215 obligation.lean_diagnostics.join("; ")
216 },
217 obligation
218 .certificate
219 .as_ref()
220 .map(|c| c.backend)
221 .unwrap_or("-"),
222 obligation
223 .lean_certificate
224 .as_ref()
225 .map(|c| c.backend)
226 .unwrap_or("-")
227 );
228 }
229 if let Some(module) = &self.lean_module {
230 let _ = writeln!(out);
231 let _ = writeln!(out, "Lean module: `{}`", module.module_name);
232 let _ = writeln!(out, "Lean theorems: {}", module.theorem_refs.len());
233 let _ = writeln!(out, "Lean imports: {}", module.prelude_imports.len());
234 let _ = writeln!(out, "Lean aliases: {}", module.prelude_aliases.len());
235 let _ = writeln!(out, "Lean diagnostics: {}", module.diagnostics.len());
236 let _ = writeln!(
237 out,
238 "Lean theorem failures: {}",
239 module.theorem_failures.len()
240 );
241 let _ = writeln!(
242 out,
243 "Lean certificate: `{}`",
244 module
245 .certificate
246 .as_ref()
247 .map(|c| c.witness_ref.as_str())
248 .unwrap_or("-")
249 );
250 }
251 out
252 }
253}
254
255pub fn dry_run_report(bundle: &ObligationBundle, artifacts: &ArtifactBatch) -> VerificationReport {
257 execute_report(bundle, artifacts, &DryRunner)
258}
259
260pub fn execute_report(
262 bundle: &ObligationBundle,
263 artifacts: &ArtifactBatch,
264 runner: &impl VerifierRunner,
265) -> VerificationReport {
266 let lean_record = artifacts
267 .records
268 .iter()
269 .find(|record| record.path.ends_with(".lean"));
270 let lean_result = lean_record.and_then(|record| {
271 artifacts
272 .plans
273 .iter()
274 .find(|plan| {
275 plan.kind == crate::CommandKind::Lean
276 && plan.input_files.iter().any(|f| f == &record.path)
277 })
278 .map(|plan| runner.run(plan))
279 });
280
281 let mut obligations = Vec::new();
282
283 for obligation in bundle.obligations() {
284 let artifact_path = artifacts
285 .records
286 .iter()
287 .find(|record| record.name == obligation.name)
288 .map(|record| record.path.clone());
289 let result = artifacts
290 .plans
291 .iter()
292 .find(|plan| {
293 plan.kind == crate::CommandKind::Smt
294 && plan
295 .input_files
296 .iter()
297 .any(|f| artifact_path.as_ref() == Some(f))
298 })
299 .map(|plan| runner.run(plan));
300 let certificate = result.as_ref().and_then(|result| {
301 certificate_for_obligation(result, obligation, artifact_path.clone())
302 });
303 let lean_theorem = artifacts
304 .lean_export
305 .as_ref()
306 .and_then(|export| export.theorem_for_obligation(&obligation.name).cloned());
307 let lean_theorem_ref = artifacts.lean_export.as_ref().and_then(|export| {
308 lean_theorem
309 .as_ref()
310 .map(|theorem| theorem.witness_ref(&export.module_name))
311 });
312 let lean_diagnostics = lean_result
313 .as_ref()
314 .and_then(|result| result.lean_output.as_ref())
315 .and_then(|output| {
316 lean_theorem.as_ref().map(|theorem| {
317 output
318 .theorem_diagnostics(theorem)
319 .into_iter()
320 .map(|diagnostic| diagnostic.message.clone())
321 .collect::<Vec<_>>()
322 })
323 })
324 .unwrap_or_default();
325 let lean_certificate = lean_result.as_ref().and_then(|result| {
326 lean_theorem_ref.as_ref().and_then(|witness_ref| {
327 certificate_for_witness::<crate::LeanCertificate>(
328 result,
329 obligation,
330 witness_ref.clone(),
331 lean_record.map(|record| record.path.clone()),
332 )
333 })
334 });
335
336 obligations.push(ObligationReport {
337 obligation_name: obligation.name.clone(),
338 summary: obligation.summary(),
339 artifact_path,
340 lean_theorem_ref,
341 lean_diagnostics,
342 result,
343 certificate,
344 lean_certificate,
345 });
346 }
347
348 let lean_module = lean_record.map(|record| {
349 let theorem_refs = artifacts
350 .lean_export
351 .as_ref()
352 .map(|export| {
353 export
354 .theorems
355 .iter()
356 .map(|theorem| theorem.witness_ref(&export.module_name))
357 .collect::<Vec<_>>()
358 })
359 .unwrap_or_default();
360 let prelude_imports = artifacts
361 .lean_export
362 .as_ref()
363 .map(|export| {
364 export
365 .prelude
366 .imports
367 .iter()
368 .map(|import| import.module.clone())
369 .collect::<Vec<_>>()
370 })
371 .unwrap_or_default();
372 let prelude_aliases = artifacts
373 .lean_export
374 .as_ref()
375 .map(|export| {
376 export
377 .prelude
378 .aliases
379 .iter()
380 .map(|alias| format!("{} := {}", alias.alias, alias.target))
381 .collect::<Vec<_>>()
382 })
383 .unwrap_or_default();
384 let diagnostics = lean_result
385 .as_ref()
386 .and_then(|result| result.lean_output.as_ref())
387 .map(|output| {
388 output
389 .diagnostics
390 .iter()
391 .map(|diagnostic| diagnostic.message.clone())
392 .collect::<Vec<_>>()
393 })
394 .unwrap_or_default();
395 let theorem_failures = match (
396 artifacts.lean_export.as_ref(),
397 lean_result
398 .as_ref()
399 .and_then(|result| result.lean_output.as_ref()),
400 ) {
401 (Some(export), Some(output)) => export
402 .theorems
403 .iter()
404 .filter(|theorem| output.has_theorem_failure(theorem))
405 .map(|theorem| theorem.witness_ref(&export.module_name))
406 .collect::<Vec<_>>(),
407 _ => Vec::new(),
408 };
409 let certificate = lean_result.as_ref().and_then(|result| {
410 certificate_for_module(
411 result,
412 &record.name,
413 Some(record.path.clone()),
414 theorem_refs.len(),
415 )
416 });
417
418 ModuleReport {
419 module_name: record.name.clone(),
420 artifact_path: Some(record.path.clone()),
421 theorem_refs,
422 prelude_imports,
423 prelude_aliases,
424 diagnostics,
425 theorem_failures,
426 result: lean_result.clone(),
427 certificate,
428 }
429 });
430
431 VerificationReport {
432 bundle_name: bundle.name.clone(),
433 root: artifacts.root.clone(),
434 obligations,
435 lean_module,
436 }
437}
438
439fn certificate_for_obligation(
440 result: &ExecutionResult,
441 obligation: &Obligation,
442 artifact_path: Option<String>,
443) -> Option<Certificate> {
444 let policy = result.verification_policy();
445 let witness_ref = format!("{}:{}", result.plan.executable, policy.witness_suffix);
446 match result.plan.kind {
447 crate::CommandKind::Smt => certificate_for_witness::<crate::SmtCertificate>(
448 result,
449 obligation,
450 witness_ref,
451 artifact_path,
452 ),
453 crate::CommandKind::Lean => certificate_for_witness::<crate::LeanCertificate>(
454 result,
455 obligation,
456 witness_ref,
457 artifact_path,
458 ),
459 }
460}
461
462fn certificate_for_witness<B: crate::VerificationBackend>(
463 result: &ExecutionResult,
464 obligation: &Obligation,
465 witness_ref: String,
466 artifact_path: Option<String>,
467) -> Option<Certificate> {
468 if !result.is_success() {
469 return None;
470 }
471
472 let mut cert = Certificate::from_obligation::<B>(obligation, witness_ref);
473
474 if let Some(version) = &result.backend_version {
475 cert = cert.with_backend_version(version.clone());
476 }
477
478 if let Some(path) = artifact_path {
479 cert = cert.with_artifact_path(path);
480 }
481
482 Some(cert)
483}
484
485fn certificate_for_module(
486 result: &ExecutionResult,
487 module_name: &str,
488 artifact_path: Option<String>,
489 theorem_count: usize,
490) -> Option<Certificate> {
491 if !result.is_success() {
492 return None;
493 }
494
495 let mut cert = Certificate::new(
496 <crate::LeanCertificate as crate::VerificationBackend>::NAME,
497 format!("Lean module {module_name}"),
498 crate::LeanCertificate::module_ref(module_name),
499 )
500 .with_notes(format!(
501 "verified module containing {theorem_count} theorem(s)"
502 ));
503
504 if let Some(version) = &result.backend_version {
505 cert = cert.with_backend_version(version.clone());
506 }
507
508 if let Some(path) = artifact_path {
509 cert = cert.with_artifact_path(path);
510 }
511
512 Some(cert)
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::{
519 AlgebraicSignature, ArtifactLayout, LeanConfig, Origin, SmtConfig, Sort,
520 dry_run_bundle_artifacts,
521 };
522
523 #[test]
524 fn dry_run_report_attaches_results_to_all_obligations() {
525 let sig = AlgebraicSignature::monoid(Sort::Int, "combine", "e");
526 let bundle = ObligationBundle::monoid(
527 "sum_monoid",
528 Origin::new("karpal-core", "Monoid for Sum<i32>"),
529 &sig,
530 );
531 let layout = ArtifactLayout::new("target/karpal-verify-report-test");
532 let artifacts = dry_run_bundle_artifacts(
533 &bundle,
534 &layout,
535 "KarpalVerify",
536 &SmtConfig::default(),
537 &LeanConfig::default(),
538 );
539
540 let report = dry_run_report(&bundle, &artifacts);
541 assert_eq!(report.obligation_count(), 3);
542 assert!(
543 report
544 .obligations
545 .iter()
546 .all(|entry| entry.status() == Some(ExecutionStatus::DryRun))
547 );
548 assert_eq!(
549 report.lean_module.as_ref().unwrap().status(),
550 Some(ExecutionStatus::DryRun)
551 );
552 assert_eq!(report.lean_module.as_ref().unwrap().theorem_refs.len(), 3);
553 }
554
555 #[test]
556 fn successful_execution_yields_certificates() {
557 let sig = AlgebraicSignature::semigroup(Sort::Int, "combine");
558 let bundle = ObligationBundle::semigroup(
559 "sum_semigroup",
560 Origin::new("karpal-core", "Semigroup for Sum<i32>"),
561 &sig,
562 );
563 let layout = ArtifactLayout::new("target/karpal-verify-report-test-2");
564 let artifacts = dry_run_bundle_artifacts(
565 &bundle,
566 &layout,
567 "KarpalVerify",
568 &SmtConfig::default(),
569 &LeanConfig::default(),
570 );
571
572 struct SuccessRunner;
573 impl VerifierRunner for SuccessRunner {
574 fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
575 ExecutionResult {
576 plan: plan.clone(),
577 status: match plan.kind {
578 crate::CommandKind::Smt => ExecutionStatus::Unsat,
579 crate::CommandKind::Lean => ExecutionStatus::Success,
580 },
581 stdout: String::new(),
582 stderr: String::new(),
583 exit_code: Some(0),
584 backend_version: Some("tool 1.0".into()),
585 smt_output: Some(crate::SmtOutput {
586 status: Some(ExecutionStatus::Unsat),
587 model: None,
588 reason_unknown: None,
589 }),
590 lean_output: (plan.kind == crate::CommandKind::Lean).then(|| {
591 crate::LeanOutput {
592 diagnostics: vec![crate::LeanDiagnostic {
593 file: Some("lean/KarpalVerify.lean".into()),
594 line: Some(7),
595 column: Some(2),
596 severity: "error".into(),
597 message: "unsolved goals in theorem associativity".into(),
598 theorem_hits: vec!["associativity".into()],
599 }],
600 theorem_hits: vec!["associativity".into()],
601 }
602 }),
603 }
604 }
605 }
606
607 let report = execute_report(&bundle, &artifacts, &SuccessRunner);
608 assert_eq!(report.success_count(), 1);
609 assert!(report.obligations[0].certificate.is_some());
610 assert_eq!(
611 report.obligations[0]
612 .certificate
613 .as_ref()
614 .and_then(|c| c.backend_version.as_deref()),
615 Some("tool 1.0")
616 );
617 assert_eq!(
618 report
619 .lean_module
620 .as_ref()
621 .map(|module| module.theorem_refs.len()),
622 Some(1)
623 );
624 assert_eq!(
625 report.obligations[0].lean_theorem_ref.as_deref(),
626 Some("KarpalVerify.associativity")
627 );
628 assert_eq!(
629 report.obligations[0].lean_diagnostics,
630 vec!["unsolved goals in theorem associativity".to_string()]
631 );
632 assert_eq!(
633 report.obligations[0]
634 .lean_certificate
635 .as_ref()
636 .map(|certificate| certificate.backend),
637 Some("lean4")
638 );
639 assert_eq!(
640 report
641 .lean_module
642 .as_ref()
643 .and_then(|module| module.certificate.as_ref())
644 .map(|certificate| certificate.witness_ref.as_str()),
645 Some("KarpalVerify")
646 );
647 assert_eq!(
648 report
649 .lean_module
650 .as_ref()
651 .map(|module| module.theorem_failures.clone()),
652 Some(vec!["KarpalVerify.associativity".to_string()])
653 );
654 }
655
656 #[test]
657 fn report_serialization_includes_summary_data() {
658 let report = VerificationReport {
659 bundle_name: "demo".into(),
660 root: "target/demo".into(),
661 obligations: vec![ObligationReport {
662 obligation_name: "assoc".into(),
663 summary: "demo::assoc [associativity]".into(),
664 artifact_path: Some("target/demo/smt/assoc.smt2".into()),
665 lean_theorem_ref: None,
666 lean_diagnostics: Vec::new(),
667 result: None,
668 certificate: None,
669 lean_certificate: None,
670 }],
671 lean_module: None,
672 };
673
674 let json = report.to_json();
675 let markdown = report.to_markdown();
676 assert!(json.contains("\"schema_version\":\"1\""));
677 assert!(json.contains("\"bundle_name\":\"demo\""));
678 assert!(markdown.contains("# Verification Report"));
679 assert!(markdown.contains("Schema version: `1`"));
680 assert!(markdown.contains("`assoc`"));
681 }
682
683 #[test]
684 fn theorem_mapping_uses_exported_theorem_identity_not_obligation_name() {
685 let sig = AlgebraicSignature::semiring(Sort::Int, "add", "mul", "zero", "one");
686 let bundle = ObligationBundle::semiring(
687 "sum_semiring",
688 Origin::new("karpal-core", "Semiring for Sum<i32>"),
689 &sig,
690 );
691 let layout = ArtifactLayout::new("target/karpal-verify-report-test-3");
692 let artifacts = dry_run_bundle_artifacts(
693 &bundle,
694 &layout,
695 "KarpalVerify",
696 &SmtConfig::default(),
697 &LeanConfig::default(),
698 );
699
700 struct TheoremNameRunner;
701 impl VerifierRunner for TheoremNameRunner {
702 fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
703 ExecutionResult {
704 plan: plan.clone(),
705 status: match plan.kind {
706 crate::CommandKind::Smt => ExecutionStatus::Unsat,
707 crate::CommandKind::Lean => ExecutionStatus::Failure,
708 },
709 stdout: String::new(),
710 stderr: String::new(),
711 exit_code: Some(1),
712 backend_version: Some("tool 1.0".into()),
713 smt_output: Some(crate::SmtOutput {
714 status: Some(ExecutionStatus::Unsat),
715 model: None,
716 reason_unknown: None,
717 }),
718 lean_output: (plan.kind == crate::CommandKind::Lean).then(|| {
719 crate::LeanOutput {
720 diagnostics: vec![crate::LeanDiagnostic {
721 file: Some("lean/KarpalVerify.lean".into()),
722 line: Some(9),
723 column: Some(2),
724 severity: "error".into(),
725 message: "unsolved goals in theorem left_distributivity".into(),
726 theorem_hits: vec!["left_distributivity".into()],
727 }],
728 theorem_hits: vec!["left_distributivity".into()],
729 }
730 }),
731 }
732 }
733 }
734
735 let report = execute_report(&bundle, &artifacts, &TheoremNameRunner);
736 let left_distributivity = report
737 .obligations
738 .iter()
739 .find(|entry| entry.obligation_name == "left_distributivity")
740 .expect("left distributivity obligation should be present");
741 assert_eq!(
742 left_distributivity.lean_diagnostics,
743 vec!["unsolved goals in theorem left_distributivity".to_string()]
744 );
745 assert_eq!(
746 report
747 .lean_module
748 .as_ref()
749 .map(|module| module.theorem_failures.clone()),
750 Some(vec!["KarpalVerify.left_distributivity".to_string()])
751 );
752 }
753
754 #[test]
755 fn theorem_mapping_can_fall_back_to_exported_line_spans() {
756 let sig = AlgebraicSignature::semiring(Sort::Int, "add", "mul", "zero", "one");
757 let bundle = ObligationBundle::semiring(
758 "sum_semiring",
759 Origin::new("karpal-core", "Semiring for Sum<i32>"),
760 &sig,
761 );
762 let layout = ArtifactLayout::new("target/karpal-verify-report-test-4");
763 let artifacts = dry_run_bundle_artifacts(
764 &bundle,
765 &layout,
766 "KarpalVerify",
767 &SmtConfig::default(),
768 &LeanConfig::default(),
769 );
770
771 let failure_line = artifacts
772 .lean_export
773 .as_ref()
774 .and_then(|export| export.theorem_for_obligation("left_distributivity"))
775 .map(|theorem| theorem.declaration_start_line)
776 .expect("left distributivity theorem span should be present");
777
778 struct LocationRunner {
779 failure_line: usize,
780 }
781 impl VerifierRunner for LocationRunner {
782 fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
783 ExecutionResult {
784 plan: plan.clone(),
785 status: match plan.kind {
786 crate::CommandKind::Smt => ExecutionStatus::Unsat,
787 crate::CommandKind::Lean => ExecutionStatus::Failure,
788 },
789 stdout: String::new(),
790 stderr: String::new(),
791 exit_code: Some(1),
792 backend_version: Some("tool 1.0".into()),
793 smt_output: Some(crate::SmtOutput {
794 status: Some(ExecutionStatus::Unsat),
795 model: None,
796 reason_unknown: None,
797 }),
798 lean_output: (plan.kind == crate::CommandKind::Lean).then(|| {
799 crate::LeanOutput {
800 diagnostics: vec![crate::LeanDiagnostic {
801 file: Some("lean/KarpalVerify.lean".into()),
802 line: Some(self.failure_line),
803 column: Some(2),
804 severity: "error".into(),
805 message: "type mismatch".into(),
806 theorem_hits: Vec::new(),
807 }],
808 theorem_hits: Vec::new(),
809 }
810 }),
811 }
812 }
813 }
814
815 let report = execute_report(&bundle, &artifacts, &LocationRunner { failure_line });
816 let left_distributivity = report
817 .obligations
818 .iter()
819 .find(|entry| entry.obligation_name == "left_distributivity")
820 .expect("left distributivity obligation should be present");
821 assert_eq!(
822 left_distributivity.lean_diagnostics,
823 vec!["type mismatch".to_string()]
824 );
825 assert_eq!(
826 report
827 .lean_module
828 .as_ref()
829 .map(|module| module.theorem_failures.clone()),
830 Some(vec!["KarpalVerify.left_distributivity".to_string()])
831 );
832 }
833}