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