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