1use std::collections::BTreeMap;
11use std::fmt;
12
13use serde::{Deserialize, Serialize};
14
15use crate::binding::{BindingRegistry, ImplStatus};
16use crate::schema::Contract;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
29pub enum ProofLevel {
30 L1,
32 L2,
34 L3,
36 L4,
38 L5,
40}
41
42impl fmt::Display for ProofLevel {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 let s = match self {
46 Self::L1 => "L1",
47 Self::L2 => "L2",
48 Self::L3 => "L3",
49 Self::L4 => "L4",
50 Self::L5 => "L5",
51 };
52 write!(f, "{s}")
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ContractProofStatus {
61 pub stem: String,
63 pub proof_level: ProofLevel,
65 pub obligations: u32,
67 #[serde(default)]
71 pub not_applicable: u32,
72 pub falsification_tests: u32,
74 pub kani_harnesses: u32,
76 pub lean_proved: u32,
78 pub lean_grounded: u32,
80 pub l4_self_declared: bool,
82 pub bindings_implemented: u32,
84 pub bindings_total: u32,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct KernelClassSummary {
93 pub label: String,
95 pub description: String,
97 pub contract_stems: Vec<String>,
99 pub min_proof_level: ProofLevel,
101 pub all_bound: bool,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ProofStatusReport {
110 pub schema_version: String,
112 pub timestamp: String,
114 pub contracts: Vec<ContractProofStatus>,
116 pub kernel_classes: Vec<KernelClassSummary>,
118 pub l4_self_declared_excluded: bool,
120 pub totals: ProofStatusTotals,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ProofStatusTotals {
127 pub contracts: u32,
129 pub obligations: u32,
131 #[serde(default)]
134 pub not_applicable: u32,
135 pub falsification_tests: u32,
137 pub kani_harnesses: u32,
139 pub lean_proved: u32,
141 pub lean_grounded: u32,
143 pub l4_self_declared: u32,
145 pub bindings_implemented: u32,
147 pub bindings_total: u32,
149}
150
151fn kernel_class_map() -> Vec<(&'static str, &'static str, &'static [&'static str])> {
162 vec![
163 (
164 "A",
165 "GQA+RMSNorm+SiLU+SwiGLU+RoPE",
166 &[
167 "rmsnorm-kernel-v1",
168 "silu-kernel-v1",
169 "swiglu-kernel-v1",
170 "rope-kernel-v1",
171 "gqa-kernel-v1",
172 "softmax-kernel-v1",
173 "matmul-kernel-v1",
174 ],
175 ),
176 (
177 "B",
178 "MHA+LayerNorm+GELU+AbsPos",
179 &[
180 "layernorm-kernel-v1",
181 "gelu-kernel-v1",
182 "attention-kernel-v1",
183 "softmax-kernel-v1",
184 "matmul-kernel-v1",
185 "absolute-position-v1",
186 ],
187 ),
188 (
189 "C",
190 "MHA+LayerNorm+GELU+ALiBi",
191 &[
192 "layernorm-kernel-v1",
193 "gelu-kernel-v1",
194 "attention-kernel-v1",
195 "softmax-kernel-v1",
196 "alibi-kernel-v1",
197 "matmul-kernel-v1",
198 ],
199 ),
200 (
201 "D",
202 "LayerNorm+GELU+SiLU+GQA",
203 &[
204 "layernorm-kernel-v1",
205 "gelu-kernel-v1",
206 "silu-kernel-v1",
207 "gqa-kernel-v1",
208 "softmax-kernel-v1",
209 "matmul-kernel-v1",
210 ],
211 ),
212 (
213 "E",
214 "RMSNorm+SwiGLU+GQA",
215 &[
216 "rmsnorm-kernel-v1",
217 "swiglu-kernel-v1",
218 "gqa-kernel-v1",
219 "softmax-kernel-v1",
220 "matmul-kernel-v1",
221 ],
222 ),
223 ]
224}
225
226#[must_use]
253pub fn is_lean_proved_with_grounding(contract: &Contract, grounded: u32) -> bool {
254 let total = contract.proof_obligations.len() as u32;
255 if total == 0 {
256 return false;
257 }
258 let not_applicable = contract
266 .verification_summary
267 .as_ref()
268 .map_or(0, |vs| vs.l4_not_applicable);
269 grounded > 0 && grounded + not_applicable >= total
270}
271
272#[must_use]
279pub fn is_l4_self_declared(contract: &Contract) -> bool {
280 is_l4_self_declared_with_grounding(contract, count_lean_theorems_for_contract(contract))
281}
282
283#[must_use]
285pub fn is_l4_self_declared_with_grounding(contract: &Contract, grounded: u32) -> bool {
286 let total = contract.proof_obligations.len() as u32;
287 if total == 0 {
288 return false;
289 }
290 let Some(vs) = contract.verification_summary.as_ref() else {
291 return false;
292 };
293 let claim_covers = vs.l4_lean_proved > 0 && vs.l4_lean_proved + vs.l4_not_applicable >= total;
294 claim_covers && !is_lean_proved_with_grounding(contract, grounded)
295}
296
297#[must_use]
308#[allow(clippy::cast_possible_truncation)]
309pub fn count_not_applicable(contract: &Contract) -> u32 {
310 contract
311 .proof_obligations
312 .iter()
313 .filter(|ob| ob.is_not_applicable())
314 .count() as u32
315}
316
317fn is_fully_bound(binding_status: Option<(u32, u32)>) -> bool {
319 binding_status.is_some_and(|(implemented, total)| total > 0 && implemented == total)
320}
321
322#[allow(clippy::cast_possible_truncation)]
333pub fn compute_proof_level(contract: &Contract, binding_status: Option<(u32, u32)>) -> ProofLevel {
334 compute_proof_level_with_grounding(
335 contract,
336 binding_status,
337 count_lean_theorems_for_contract(contract),
338 )
339}
340
341#[allow(clippy::cast_possible_truncation)]
343#[must_use]
344pub fn compute_proof_level_with_grounding(
345 contract: &Contract,
346 binding_status: Option<(u32, u32)>,
347 grounded: u32,
348) -> ProofLevel {
349 let total_obligations = contract.proof_obligations.len() as u32;
350 let ft_count = contract.falsification_tests.len() as u32;
351 let kani_count = contract.kani_harnesses.len() as u32;
352
353 if is_lean_proved_with_grounding(contract, grounded) {
355 return if is_fully_bound(binding_status) {
356 ProofLevel::L5
357 } else {
358 ProofLevel::L4
359 };
360 }
361
362 let has_tests = ft_count >= total_obligations && total_obligations > 0;
364 if kani_count > 0 && has_tests {
365 return ProofLevel::L3;
366 }
367
368 if has_tests {
370 return ProofLevel::L2;
371 }
372
373 ProofLevel::L1
375}
376
377pub(crate) const LEAN_THEOREM_BASES: &[&str] = &[
384 "crates/aprender-contracts-staging/lean",
385 "lean",
386 "../provable-contracts/lean",
387];
388
389fn insert_name_forms(names: &mut std::collections::HashSet<String>, label: &str) {
391 names.insert(format!("Theorems.{label}"));
392 names.insert(label.to_string());
393 names.insert(label.to_lowercase());
394}
395
396pub(crate) fn camel_case(snake: &str) -> String {
398 snake
399 .split('_')
400 .map(|s| {
401 let mut c = s.chars();
402 match c.next() {
403 None => String::new(),
404 Some(f) => f.to_uppercase().chain(c).collect(),
405 }
406 })
407 .collect()
408}
409
410pub(crate) fn first_camel_word(camel: &str) -> String {
412 camel
413 .chars()
414 .enumerate()
415 .take_while(|(i, c)| *i == 0 || !c.is_uppercase())
416 .map(|(_, c)| c)
417 .collect()
418}
419
420fn insert_theorem_names_from_content(names: &mut std::collections::HashSet<String>, content: &str) {
422 for line in content.lines() {
423 let Some(pos) = line.find("theorem ") else {
424 continue;
425 };
426 let tname: String = line[pos + 8..]
427 .chars()
428 .take_while(|c| c.is_alphanumeric() || *c == '_')
429 .collect();
430 if tname.is_empty() {
431 continue;
432 }
433 let camel = camel_case(&tname);
434 names.insert(format!("Theorems.{camel}"));
435 names.insert(camel.clone());
436 let first_word = first_camel_word(&camel);
437 if first_word.len() >= 3 {
438 names.insert(format!("Theorems.{first_word}"));
439 names.insert(first_word);
440 }
441 }
442}
443
444fn insert_domain_theorems(names: &mut std::collections::HashSet<String>, domain: &std::path::Path) {
449 let domain_name = domain
450 .file_name()
451 .unwrap_or_default()
452 .to_string_lossy()
453 .to_string();
454 let Ok(files) = std::fs::read_dir(domain) else {
455 return;
456 };
457 for file in files.flatten() {
458 let path = file.path();
459 if path.extension().is_none_or(|e| e != "lean") {
460 continue;
461 }
462 let Ok(content) = std::fs::read_to_string(&path) else {
463 continue;
464 };
465 if content.contains("sorry") {
466 continue;
467 }
468 let stem = path
469 .file_stem()
470 .unwrap_or_default()
471 .to_string_lossy()
472 .to_string();
473 insert_name_forms(names, &domain_name);
474 insert_name_forms(names, &stem);
475 insert_theorem_names_from_content(names, &content);
476 }
477}
478
479fn scan_theorem_base(base: &str) -> std::collections::HashSet<String> {
481 let mut names = std::collections::HashSet::new();
482 let search_dir = std::path::Path::new(base).join("ProvableContracts/Theorems");
483 if !search_dir.exists() {
484 return names;
485 }
486 let Ok(domains) = std::fs::read_dir(&search_dir) else {
487 return names;
488 };
489 for domain_entry in domains.flatten() {
490 let path = domain_entry.path();
491 if path.is_dir() {
492 insert_domain_theorems(&mut names, &path);
493 }
494 }
495 names
496}
497
498fn lean_theorem_names() -> &'static std::collections::HashSet<String> {
501 use std::sync::OnceLock;
502 static CACHE: OnceLock<std::collections::HashSet<String>> = OnceLock::new();
503 CACHE.get_or_init(|| {
504 for base in LEAN_THEOREM_BASES {
505 let names = scan_theorem_base(base);
506 if !names.is_empty() {
507 return names;
508 }
509 }
510 std::collections::HashSet::new()
511 })
512}
513
514fn count_lean_theorems_for_contract(contract: &Contract) -> u32 {
517 let theorems = lean_theorem_names();
518 let mut count = 0u32;
519 for eq in contract.equations.values() {
520 if let Some(ref theorem_ref) = eq.lean_theorem {
521 let name = theorem_ref.trim().trim_matches('"');
522 if theorems.contains(name)
524 || theorems.contains(name.strip_prefix("Theorems.").unwrap_or(name))
525 || theorems.contains(&name.to_lowercase())
526 {
527 count += 1;
528 }
529 }
530 }
531 count
532}
533
534#[allow(clippy::cast_possible_truncation)]
540pub fn proof_status_report(
541 contracts: &[(String, &Contract)],
542 binding: Option<&BindingRegistry>,
543 include_classes: bool,
544) -> ProofStatusReport {
545 let mut statuses = Vec::new();
546 let mut totals = ProofStatusTotals {
547 contracts: contracts.len() as u32,
548 obligations: 0,
549 not_applicable: 0,
550 falsification_tests: 0,
551 kani_harnesses: 0,
552 lean_proved: 0,
553 lean_grounded: 0,
554 l4_self_declared: 0,
555 bindings_implemented: 0,
556 bindings_total: 0,
557 };
558
559 for (stem, contract) in contracts {
560 let contract_file = format!("{stem}.yaml");
561
562 let obligations = contract.proof_obligations.len() as u32;
563 let not_applicable = count_not_applicable(contract);
564 let ft_count = contract.falsification_tests.len() as u32;
565 let kani_count = contract.kani_harnesses.len() as u32;
566 let lean_proved = contract
569 .verification_summary
570 .as_ref()
571 .map_or(0, |vs| vs.l4_lean_proved);
572 let lean_grounded = count_lean_theorems_for_contract(contract);
573 let lean_proved = if lean_proved == 0 {
574 lean_grounded
575 } else {
576 lean_proved
577 };
578 let l4_self_declared = is_l4_self_declared_with_grounding(contract, lean_grounded);
579
580 let (b_impl, b_total) = if let Some(reg) = binding {
582 count_bindings(&contract_file, contract, reg)
583 } else {
584 (0, contract.equations.len() as u32)
585 };
586
587 let binding_status = if binding.is_some() {
588 Some((b_impl, b_total))
589 } else {
590 None
591 };
592
593 let proof_level =
594 compute_proof_level_with_grounding(contract, binding_status, lean_grounded);
595
596 totals.obligations += obligations;
597 totals.not_applicable += not_applicable;
598 totals.falsification_tests += ft_count;
599 totals.kani_harnesses += kani_count;
600 totals.lean_proved += lean_proved;
601 totals.lean_grounded += lean_grounded;
602 totals.l4_self_declared += u32::from(l4_self_declared);
603 totals.bindings_implemented += b_impl;
604 totals.bindings_total += b_total;
605
606 statuses.push(ContractProofStatus {
607 stem: stem.clone(),
608 proof_level,
609 obligations,
610 not_applicable,
611 falsification_tests: ft_count,
612 kani_harnesses: kani_count,
613 lean_proved,
614 lean_grounded,
615 l4_self_declared,
616 bindings_implemented: b_impl,
617 bindings_total: b_total,
618 });
619 }
620
621 let kernel_classes = if include_classes {
623 build_kernel_classes(&statuses)
624 } else {
625 Vec::new()
626 };
627
628 let timestamp = current_timestamp();
629
630 ProofStatusReport {
631 schema_version: "1.0.0".to_string(),
632 l4_self_declared_excluded: true,
633 timestamp,
634 contracts: statuses,
635 kernel_classes,
636 totals,
637 }
638}
639
640pub fn format_text(report: &ProofStatusReport) -> String {
642 let mut out = String::new();
643
644 out.push_str(&format!(
645 "Proof Status ({} contracts)\n\n",
646 report.totals.contracts
647 ));
648
649 out.push_str(&format!(
650 " {:<35} {:>5} {:>6} {:>5} {:>4} {:>4} {:>9} {:>13}\n",
651 "Contract", "Level", "Obligs", "Tests", "Kani", "Lean", "Bindings", "L4 evidence"
652 ));
653 out.push_str(&format!(" {}\n", "─".repeat(86)));
654
655 for c in &report.contracts {
656 let l4_evidence = if c.l4_self_declared {
659 "self-declared"
660 } else if c.lean_grounded > 0 {
661 "grounded"
662 } else {
663 "-"
664 };
665 out.push_str(&format!(
666 " {:<35} {:>5} {:>6} {:>5} {:>4} {:>4} {:>4}/{:<4} {:>13}\n",
667 truncate(&c.stem, 35),
668 c.proof_level,
669 c.obligations,
670 c.falsification_tests,
671 c.kani_harnesses,
672 c.lean_proved,
673 c.bindings_implemented,
674 c.bindings_total,
675 l4_evidence,
676 ));
677 }
678
679 if !report.kernel_classes.is_empty() {
680 out.push_str("\nKernel Classes:\n");
681 for kc in &report.kernel_classes {
682 let bound_str = if kc.all_bound { "all bound" } else { "gaps" };
683 out.push_str(&format!(
684 " {} ({}): min={}, {} contracts, {}\n",
685 kc.label,
686 kc.description,
687 kc.min_proof_level,
688 kc.contract_stems.len(),
689 bound_str,
690 ));
691 }
692 }
693
694 out.push_str(&format!(
695 "\nTotals: {} obligations ({} N/A, never counted as proved), {} tests, {} kani, {} lean claimed ({} grounded), {}/{} bound\n\
696 L4 evidence: {} contract(s) self-declared and excluded from L4 (ONT-2a andon); grounded means a \
697 sorry-free in-tree Lean theorem the equation names\n",
698 report.totals.obligations,
699 report.totals.not_applicable,
700 report.totals.falsification_tests,
701 report.totals.kani_harnesses,
702 report.totals.lean_proved,
703 report.totals.lean_grounded,
704 report.totals.bindings_implemented,
705 report.totals.bindings_total,
706 report.totals.l4_self_declared,
707 ));
708
709 out
710}
711
712#[allow(clippy::cast_possible_truncation)]
716pub(crate) fn count_bindings(
717 contract_file: &str,
718 contract: &Contract,
719 binding: &BindingRegistry,
720) -> (u32, u32) {
721 let total = contract.equations.len() as u32;
722 let implemented = binding
723 .bindings_for(contract_file)
724 .iter()
725 .filter(|b| b.status == ImplStatus::Implemented)
726 .count() as u32;
727 (implemented, total)
728}
729
730fn build_kernel_classes(statuses: &[ContractProofStatus]) -> Vec<KernelClassSummary> {
732 let status_map: BTreeMap<&str, &ContractProofStatus> =
733 statuses.iter().map(|s| (s.stem.as_str(), s)).collect();
734
735 kernel_class_map()
736 .into_iter()
737 .map(|(label, desc, stems)| {
738 let found_stems: Vec<String> = stems
739 .iter()
740 .filter(|s| status_map.contains_key(**s))
741 .map(|s| (*s).to_string())
742 .collect();
743
744 let min_level = found_stems
745 .iter()
746 .filter_map(|s| status_map.get(s.as_str()))
747 .map(|c| c.proof_level)
748 .min()
749 .unwrap_or(ProofLevel::L1);
750
751 let all_bound = !found_stems.is_empty()
752 && found_stems.iter().all(|s| {
753 status_map.get(s.as_str()).is_some_and(|c| {
754 c.bindings_total > 0 && c.bindings_implemented == c.bindings_total
755 })
756 });
757
758 KernelClassSummary {
759 label: label.to_string(),
760 description: desc.to_string(),
761 contract_stems: found_stems,
762 min_proof_level: min_level,
763 all_bound,
764 }
765 })
766 .collect()
767}
768
769fn truncate(s: &str, max: usize) -> &str {
771 if s.len() > max {
772 &s[..max]
773 } else {
774 s
775 }
776}
777
778fn current_timestamp() -> String {
780 let duration = std::time::SystemTime::now()
784 .duration_since(std::time::UNIX_EPOCH)
785 .unwrap_or_default();
786 format!("{}Z", duration.as_secs())
787}
788
789#[cfg(test)]
790#[path = "proof_status_tests.rs"]
791mod tests;