Skip to main content

provable_contracts/
levels.rs

1//! PVL-001 EV-3 (PVL-3): ONE definition of the proof levels.
2//!
3//! [`ProofLevel`] is authoritative. Before this, three places disagreed:
4//! the enum (L3 Kani, L4 Lean, L5 Lean + bindings), the README table from
5//! `readme_gen` (L4 "Kani BMC", L5 "Lean 4 theorem", L1 "Type system"), and
6//! three copies of the ladder doc (L4 Kani, L5 Lean, plus an L0 the enum does
7//! not have). A reader of the README was told an L4 contract was Kani-checked
8//! when `pv` had computed that it was Lean-proved.
9//!
10//! Now the README's method column comes from [`ProofLevel::method`], and each
11//! ladder doc carries [`ladder_block`] verbatim under [`MARKER`]. The test below
12//! fails if either drifts from the enum.
13
14use crate::proof_status::ProofLevel;
15
16/// The first line of the generated block in every ladder doc copy.
17pub const MARKER: &str = "<!-- generated from ProofLevel; do not edit -->";
18/// The last line of the generated block.
19pub const END_MARKER: &str = "<!-- end generated from ProofLevel -->";
20
21impl ProofLevel {
22    /// Every level, highest first (the order the README and docs print).
23    pub const ALL_DESCENDING: [ProofLevel; 5] = [
24        ProofLevel::L5,
25        ProofLevel::L4,
26        ProofLevel::L3,
27        ProofLevel::L2,
28        ProofLevel::L1,
29    ];
30
31    /// What reaching this level means: the one definition every surface prints. Each
32    /// string states what `compute_proof_level_with_grounding` actually checks (a quorum
33    /// lane on PR #4092 found the first wording claimed less for L3 and the wrong thing
34    /// for L4).
35    #[must_use]
36    pub fn method(self) -> &'static str {
37        match self {
38            ProofLevel::L1 => "Contract YAML with equations",
39            ProofLevel::L2 => "Falsification tests cover every obligation",
40            ProofLevel::L3 => "L2 + at least one Kani bounded-model-check harness",
41            ProofLevel::L4 => {
42                "Every obligation has a sorry-free in-tree Lean 4 theorem or is not applicable, with at least one proved"
43            }
44            ProofLevel::L5 => "L4 + at least one binding, every binding implemented",
45        }
46    }
47}
48
49/// The ladder table every doc copy carries, from [`MARKER`] to [`END_MARKER`].
50#[must_use]
51pub fn ladder_block() -> String {
52    let mut out = String::new();
53    out.push_str(MARKER);
54    out.push('\n');
55    out.push_str("| Level | Method |\n|-------|--------|\n");
56    for level in ProofLevel::ALL_DESCENDING {
57        out.push_str(&format!("| {level} | {} |\n", level.method()));
58    }
59    out.push_str(
60        "\nL4 and L5 are grounded only textually until PVL-001 EV-8b lands: a claimed Lean \
61         proof counts when a sorry-free Lean theorem in this tree matches it (a claim with \
62         none is reported self-declared and excluded from L4), a not-applicable count is \
63         taken from the contract's own verification summary, and no checked lake \
64         discharge summary is read yet.\n",
65    );
66    out.push_str(END_MARKER);
67    out
68}
69
70/// Lines of `doc` OUTSIDE the generated block that pair a level with the wrong tool.
71///
72/// Each tool mention (`kani`, `lean`) is paired with the NEAREST level token on its line
73/// (`L1`..`L5`, `Level 1`..`Level 5`), in either direction, and flagged when that level
74/// does not match the tool: Kani paired with L4/L5 (Kani is L3), or Lean paired with L5
75/// on a line that says nothing about bindings (Lean alone is L4). Nearest-in-either-
76/// direction catches "Level 4 (Kani)" and "Kani is used at Level 4" alike, and leaves
77/// "How Kani (L3) and Lean (L4) Compose" alone. Quorum lanes on PR #4092 found both
78/// gaps: prose below the block still teaching "Level 4 (Kani)" (round 1), and a
79/// detector that only looked forward from the level token (round 3).
80pub fn stale_level_pairings(doc: &str) -> Vec<String> {
81    let outside: String = match (doc.find(MARKER), doc.find(END_MARKER)) {
82        (Some(a), Some(b)) if a < b => format!("{}{}", &doc[..a], &doc[b + END_MARKER.len()..]),
83        _ => doc.to_string(),
84    };
85    let mut stale = Vec::new();
86    for line in outside.lines() {
87        let low = line.to_lowercase();
88        let levels = level_tokens(&low);
89        let wrong = ["kani", "lean"].iter().any(|tool| {
90            word_starts(&low, tool).any(|t| {
91                let t_end = t + tool.len();
92                let nearest = levels.iter().min_by_key(|(start, end, _)| {
93                    if *end <= t {
94                        t - end
95                    } else if *start >= t_end {
96                        start - t_end
97                    } else {
98                        0
99                    }
100                });
101                match (*tool, nearest) {
102                    ("kani", Some((_, _, n))) => *n >= 4,
103                    ("lean", Some((_, _, n))) => *n == 5 && !low.contains("binding"),
104                    _ => false,
105                }
106            })
107        });
108        if wrong {
109            stale.push(line.trim().to_string());
110        }
111    }
112    stale
113}
114
115/// Byte offsets where `needle` starts a word in `hay`.
116fn word_starts<'a>(hay: &'a str, needle: &'a str) -> impl Iterator<Item = usize> + 'a {
117    hay.match_indices(needle)
118        .map(|(i, _)| i)
119        .filter(move |&i| i == 0 || !hay.as_bytes()[i - 1].is_ascii_alphanumeric())
120}
121
122/// `(start, end, n)` for every whole-word level token `lN` / `level N` (N in 1..=5).
123fn level_tokens(low: &str) -> Vec<(usize, usize, u8)> {
124    let mut out = Vec::new();
125    for n in 1u8..=5 {
126        for tok in [format!("l{n}"), format!("level {n}")] {
127            for i in word_starts(low, &tok) {
128                let end = i + tok.len();
129                if !low[end..].starts_with(|c: char| c.is_ascii_alphanumeric()) {
130                    out.push((i, end, n));
131                }
132            }
133        }
134    }
135    out
136}
137
138// The tests live DIRECTLY in `levels` (not in a `tests` submodule): PVL-001 EV-3's accept is
139// `cargo test -p aprender-contracts --lib -- levels::readme_and_ladder_docs_match_enum`, and
140// libtest's filter is a substring of the full path, so under `levels::tests::` that accept
141// command ran ZERO tests and passed.
142
143/// PVL-001 EV-3's accept test: `levels::readme_and_ladder_docs_match_enum`.
144#[test]
145fn readme_and_ladder_docs_match_enum() {
146    // 1. The README's verification table prints, for each level, the enum's method.
147    let readme = crate::readme_gen::verification_ladder_table(&[0, 0, 0, 0, 0]);
148    for level in ProofLevel::ALL_DESCENDING {
149        let row = format!("| {level} | 0 | {} |", level.method());
150        assert!(
151            readme.lines().any(|l| l == row),
152            "README ladder row for {level} is not the enum's definition.\nwant: {row}\nREADME table:\n{readme}"
153        );
154    }
155    // The two guards a vacuous reading would drop (quorum round 4, measured): L4 needs at
156    // least one GROUNDED obligation (ONT-2a: all-not-applicable is not L4), and L5 needs at
157    // least one binding (is_fully_bound: zero bindings is not "all bound"). The code side
158    // is pinned by proof_status_tests::level_all_not_applicable_is_not_l4 and
159    // ::level_l5_needs_at_least_one_binding.
160    assert!(ProofLevel::L4.method().contains("at least one proved"));
161    assert!(ProofLevel::L5.method().contains("at least one binding"));
162    // 2. The doc copies are read by `readme_contract::ladder_doc_copies_carry_the_enum_block`
163    // (aprender-core): a crate test must not read files outside its crate, and the clean-room
164    // copy of the tree drops every `book/` directory, so the book copy cannot be read here.
165}
166
167/// The block itself names every level exactly once, in order: a regression in
168/// `ladder_block` cannot pass by printing nothing.
169#[test]
170fn the_ladder_block_names_each_level_once_highest_first() {
171    let block = ladder_block();
172    let is_level_row =
173        |l: &&str| l.len() > 4 && l.starts_with("| L") && l.as_bytes()[3].is_ascii_digit();
174    let rows: Vec<&str> = block.lines().filter(is_level_row).collect();
175    assert_eq!(rows.len(), 5, "{block}");
176    for (row, level) in rows.iter().zip(ProofLevel::ALL_DESCENDING) {
177        assert!(row.starts_with(&format!("| {level} |")), "{row}");
178    }
179    assert!(block.starts_with(MARKER) && block.ends_with(END_MARKER));
180}
181
182/// The stale-pairing detector's case table: the old doc lines must be flagged, the
183/// corrected ones must not, so neither "flag nothing" nor "flag every level" passes.
184#[cfg(test)]
185#[test]
186fn stale_level_pairings_case_table() {
187    let must_flag = [
188        "| Obligation Type | Level 1 (Types) | Level 3 (probar) | Level 4 (Kani) | Level 5 (Lean) |",
189        "| Obligation Type | L1 (Types) | L3 (probar) | L4 (Kani) | L5 (Lean) |",
190        "3. **L4:** Kani exhaustively verified for ALL inputs within the kernel's",
191        "3. **Level 4:** Kani has exhaustively verified the property for ALL inputs up",
192        "  L5    Theorem proving         Lean 4          True for ALL inputs. Period.",
193        // reversed order: the tool first, the level after it (round 3's gap)
194        "Kani is the tool used at Level 4 for bounded checks.",
195        "The Lean prover is what L5 means here.",
196        // two levels on the line: Kani's nearest is the later L4, not the earlier L2,
197        // so a detector that only looks backward from the tool misses it
198        "Falsification is L2 then Kani at L4.",
199    ];
200    for line in must_flag {
201        assert_eq!(stale_level_pairings(line).len(), 1, "must flag: {line}");
202    }
203    let must_not_flag = [
204        "| Obligation Type | Types (rustc) | L2 (probar/proptest) | L3 (Kani) | L4 (Lean) |",
205        "3. **L3:** Kani exhaustively verified it for ALL inputs within the kernel's",
206        "4. **L4:** a Lean 4 theorem proves it unbounded; **L5** additionally requires",
207        "## How Kani (L3) and Lean (L4) Compose",
208        "| L5 | L4 + at least one binding, every binding implemented |",
209        "E4 and E5 are defined in YAML but not yet run in CI.",
210        "| **E4** | Logic bugs, overflows | Kani `#[kani::proof]` BMC |",
211    ];
212    for line in must_not_flag {
213        assert!(
214            stale_level_pairings(line).is_empty(),
215            "must not flag: {line}"
216        );
217    }
218    // The generated block itself is excluded even though it names L5 and Lean.
219    assert!(stale_level_pairings(&ladder_block()).is_empty());
220}