|
| 1 | +-- SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +-- |
| 3 | +-- SPARK companion spec for src/rust/verification/certificates.rs |
| 4 | +-- |
| 5 | +-- Proved properties |
| 6 | +-- 1. Get_Format_Extension is total over Certificate_Format and injective. |
| 7 | +-- 2. Check_Alethe_Step is the exact boolean characterisation of the Rust |
| 8 | +-- structural validator: |
| 9 | +-- - Assume step: valid iff raw is non-empty |
| 10 | +-- - Step step: valid iff raw has the :rule marker |
| 11 | +-- (Issue #40 roadmap obligation: "Alethe / DRAT / TSTP / OpenTheory |
| 12 | +-- cert parsing — totality + reject-malformed". The totality piece |
| 13 | +-- is Get_Format_Extension; reject-malformed is encoded by the |
| 14 | +-- check predicates + Cap_At_Malformed below.) |
| 15 | +-- 3. Verify_Alethe returns true iff every step in the input array |
| 16 | +-- passes Check_Alethe_Step (cap-at-malformed monotonicity). |
| 17 | +-- 4. Cap_At_Malformed (ghost): any malformed step ⇒ Verify_Alethe = False. |
| 18 | +-- 5. Empty_Verifies (ghost): empty input ⇒ Verify_Alethe = True (vacuous |
| 19 | +-- quantifier over an empty range). |
| 20 | +-- |
| 21 | +-- SPARK doesn't model strings, subprocess, or external checkers, so the |
| 22 | +-- DRAT/LRAT/Lean4/Coq paths (which shell out to drat-trim / lean4checker / |
| 23 | +-- coqchk) are outside this companion's scope. This companion mirrors the |
| 24 | +-- pure-decision Alethe structural validator and the format-extension |
| 25 | +-- mapping — the parts of certificates.rs that are purely deterministic |
| 26 | +-- functions of their inputs and have no IO surface. |
| 27 | + |
| 28 | +pragma Ada_2022; |
| 29 | + |
| 30 | +package Certificates |
| 31 | + with SPARK_Mode => On |
| 32 | +is |
| 33 | + -- ── CertificateFormat ────────────────────────────────────────────── |
| 34 | + -- Mirrors Rust enum CertificateFormat (Rust line 20-33). Same |
| 35 | + -- positional order so Ada and Rust integer encodings agree. |
| 36 | + type Certificate_Format is |
| 37 | + (Alethe, DRAT, LRAT, Lean4_Kernel, Coq_Kernel, TSTP); |
| 38 | + |
| 39 | + -- ── Format extensions ────────────────────────────────────────────── |
| 40 | + -- Rust format_extension returns `&str` literals. SPARK doesn't model |
| 41 | + -- strings ergonomically, so we map each format to a tagged extension |
| 42 | + -- enum. Injectivity is then expressible via the case post-condition. |
| 43 | + type Format_Extension is |
| 44 | + (Ext_Alethe, Ext_Drat, Ext_Lrat, Ext_Lean4cert, Ext_Coqcert, Ext_Tstp); |
| 45 | + |
| 46 | + function Get_Format_Extension (F : Certificate_Format) |
| 47 | + return Format_Extension |
| 48 | + with Post => |
| 49 | + (case F is |
| 50 | + when Alethe => Get_Format_Extension'Result = Ext_Alethe, |
| 51 | + when DRAT => Get_Format_Extension'Result = Ext_Drat, |
| 52 | + when LRAT => Get_Format_Extension'Result = Ext_Lrat, |
| 53 | + when Lean4_Kernel => Get_Format_Extension'Result = Ext_Lean4cert, |
| 54 | + when Coq_Kernel => Get_Format_Extension'Result = Ext_Coqcert, |
| 55 | + when TSTP => Get_Format_Extension'Result = Ext_Tstp); |
| 56 | + |
| 57 | + -- ── AletheStepKind ───────────────────────────────────────────────── |
| 58 | + -- Mirrors Rust private enum AletheStepKind (Rust line 274). |
| 59 | + type Alethe_Step_Kind is (Assume, Step); |
| 60 | + |
| 61 | + -- ── AletheStep (abstracted) ──────────────────────────────────────── |
| 62 | + -- The Rust struct carries the raw string. SPARK can't reason about |
| 63 | + -- arbitrary strings, so we abstract to two boolean attributes that |
| 64 | + -- the Rust predicate Check_Alethe_Step consumes: |
| 65 | + -- Raw_Is_Nonempty -- `!step.raw.is_empty()` |
| 66 | + -- Has_Rule_Marker -- `raw.contains(":rule") && raw.starts_with("(step")` |
| 67 | + -- A string-to-AletheStep adapter on the Rust side (not modeled here) |
| 68 | + -- computes these two booleans at parse time. |
| 69 | + type Alethe_Step is record |
| 70 | + Kind : Alethe_Step_Kind := Assume; |
| 71 | + Raw_Is_Nonempty : Boolean := False; |
| 72 | + Has_Rule_Marker : Boolean := False; |
| 73 | + end record; |
| 74 | + |
| 75 | + -- ── Check_Alethe_Step ────────────────────────────────────────────── |
| 76 | + -- Mirrors check_alethe_step (Rust line 226-234). Expression function |
| 77 | + -- so the body IS the post-condition. |
| 78 | + function Check_Alethe_Step (S : Alethe_Step) return Boolean is |
| 79 | + (case S.Kind is |
| 80 | + when Assume => S.Raw_Is_Nonempty, |
| 81 | + when Step => S.Has_Rule_Marker); |
| 82 | + |
| 83 | + -- ── Bounded step array ───────────────────────────────────────────── |
| 84 | + -- Mirrors the Vec<AletheStep> produced by parse_alethe_steps. 65 536 |
| 85 | + -- caps Alethe proofs at an industrial scale (most CVC5 proofs run in |
| 86 | + -- the hundreds-to-thousands range). |
| 87 | + Max_Steps : constant := 65_536; |
| 88 | + subtype Step_Index is Positive range 1 .. Max_Steps; |
| 89 | + type Step_Array is array (Step_Index range <>) of Alethe_Step; |
| 90 | + |
| 91 | + -- ── Verify_Alethe ────────────────────────────────────────────────── |
| 92 | + -- Mirrors verify_alethe (Rust line 93-124): the result is valid iff |
| 93 | + -- every step passes Check_Alethe_Step. Expression function — body |
| 94 | + -- IS the post-condition; gnatprove discharges directly without VCs. |
| 95 | + function Verify_Alethe (Steps : Step_Array) return Boolean is |
| 96 | + (for all I in Steps'Range => Check_Alethe_Step (Steps (I))); |
| 97 | + |
| 98 | + -- ── Has_Malformed (ghost) ────────────────────────────────────────── |
| 99 | + function Has_Malformed (Steps : Step_Array) return Boolean is |
| 100 | + (for some I in Steps'Range => not Check_Alethe_Step (Steps (I))) |
| 101 | + with Ghost; |
| 102 | + |
| 103 | + -- ── Cap_At_Malformed (ghost lemma) ───────────────────────────────── |
| 104 | + -- Cap-at-malformed monotonicity: any single malformed step forces |
| 105 | + -- the whole certificate to be rejected. Mirrors axiom_tracker's |
| 106 | + -- cap-at-Reject and solver_integrity's cap-at-Tampered. |
| 107 | + -- |
| 108 | + -- Proof: Has_Malformed (S) ≡ (exists I, ¬Check_Alethe_Step (S(I))) |
| 109 | + -- ≡ ¬(forall I, Check_Alethe_Step (S(I))) |
| 110 | + -- ≡ ¬Verify_Alethe (S). |
| 111 | + procedure Cap_At_Malformed (Steps : Step_Array) |
| 112 | + with Ghost, |
| 113 | + Pre => Has_Malformed (Steps), |
| 114 | + Post => not Verify_Alethe (Steps); |
| 115 | + |
| 116 | + -- ── Empty_Verifies (ghost lemma) ─────────────────────────────────── |
| 117 | + -- An empty proof is vacuously valid: the universal quantifier over |
| 118 | + -- an empty range is True. Stated explicitly to document the |
| 119 | + -- pathological-but-correct edge case (parse_alethe_steps strips all |
| 120 | + -- comments + empty lines; pure-comment input yields zero steps). |
| 121 | + procedure Empty_Verifies (Steps : Step_Array) |
| 122 | + with Ghost, |
| 123 | + Pre => Steps'Length = 0, |
| 124 | + Post => Verify_Alethe (Steps); |
| 125 | + |
| 126 | + -- ── Format_Extension_Is_Injective (ghost lemma) ──────────────────── |
| 127 | + -- Distinct certificate formats map to distinct extensions. Documents |
| 128 | + -- the storage-path uniqueness property used by store_certificate |
| 129 | + -- (Rust line 182): different formats yield different filenames, so |
| 130 | + -- two certificates of different formats never collide on disk. |
| 131 | + procedure Format_Extension_Is_Injective |
| 132 | + (F1, F2 : Certificate_Format) |
| 133 | + with Ghost, |
| 134 | + Pre => F1 /= F2, |
| 135 | + Post => Get_Format_Extension (F1) /= Get_Format_Extension (F2); |
| 136 | + |
| 137 | +end Certificates; |
0 commit comments