|
| 1 | +-- SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +-- EchidnaABI.AxiomTracker |
| 5 | +-- |
| 6 | +-- Idris2 ABI type specification for the axiom-policy enforcement layer. |
| 7 | +-- This file is the *formal specification* that the SPARK implementation |
| 8 | +-- (src/ada/spark/axiom_policy.ads/.adb) must conform to. |
| 9 | +-- |
| 10 | +-- The key invariants stated here as dependent-type propositions are exactly |
| 11 | +-- what GNATprove discharges on the SPARK side. Both must agree. |
| 12 | +-- |
| 13 | +-- NO believe_me — every proof is constructive (Refl, case analysis, or |
| 14 | +-- explicit witnesses). |
| 15 | + |
| 16 | +module EchidnaABI.AxiomTracker |
| 17 | + |
| 18 | +import EchidnaABI.Types -- ProverKind, Platform, etc. |
| 19 | +import Data.List |
| 20 | +import Data.So |
| 21 | +import Decidable.Equality |
| 22 | + |
| 23 | +%default total |
| 24 | + |
| 25 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 26 | +-- DangerLevel (mirrors src/rust/verification/axiom_tracker.rs) |
| 27 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 28 | + |
| 29 | +||| The danger level assigned to an axiom usage. |
| 30 | +||| Total order: Safe < Noted < Warning < Reject. |
| 31 | +public export |
| 32 | +data DangerLevel |
| 33 | + = Safe -- standard library axiom; allowed unconditionally |
| 34 | + | Noted -- classical axiom in constructive system; noted but allowed |
| 35 | + | Warning -- incomplete proof marker (sorry, Admitted); warning issued |
| 36 | + | Reject -- known unsound construct; proof REJECTED |
| 37 | + |
| 38 | +-- C-ABI wire encoding (u8 discriminant, matches Rust PartialOrd order) |
| 39 | +public export |
| 40 | +dangerToU8 : DangerLevel -> Bits8 |
| 41 | +dangerToU8 Safe = 0 |
| 42 | +dangerToU8 Noted = 1 |
| 43 | +dangerToU8 Warning = 2 |
| 44 | +dangerToU8 Reject = 3 |
| 45 | + |
| 46 | +public export |
| 47 | +u8ToDanger : Bits8 -> Maybe DangerLevel |
| 48 | +u8ToDanger 0 = Just Safe |
| 49 | +u8ToDanger 1 = Just Noted |
| 50 | +u8ToDanger 2 = Just Warning |
| 51 | +u8ToDanger 3 = Just Reject |
| 52 | +u8ToDanger _ = Nothing |
| 53 | + |
| 54 | +||| Round-trip: decode . encode = id |
| 55 | +public export |
| 56 | +dangerRoundTrip : (d : DangerLevel) -> u8ToDanger (dangerToU8 d) = Just d |
| 57 | +dangerRoundTrip Safe = Refl |
| 58 | +dangerRoundTrip Noted = Refl |
| 59 | +dangerRoundTrip Warning = Refl |
| 60 | +dangerRoundTrip Reject = Refl |
| 61 | + |
| 62 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 63 | +-- AxiomPolicy (mirrors Rust AxiomPolicy discriminant) |
| 64 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 65 | + |
| 66 | +||| The policy verdict for a scanned proof. |
| 67 | +public export |
| 68 | +data AxiomPolicy |
| 69 | + = PolicyClean -- only Safe axioms; unconditionally accepted |
| 70 | + | PolicyClassical -- classical axioms noted; accepted with note |
| 71 | + | PolicyIncomplete -- sorry/Admitted found; accepted with warning |
| 72 | + | PolicyRejected -- unsound construct; REJECTED |
| 73 | + |
| 74 | +-- C-ABI wire encoding (i32 discriminant) |
| 75 | +public export |
| 76 | +policyToI32 : AxiomPolicy -> Int32 |
| 77 | +policyToI32 PolicyClean = 0 |
| 78 | +policyToI32 PolicyClassical = 1 |
| 79 | +policyToI32 PolicyIncomplete = 2 |
| 80 | +policyToI32 PolicyRejected = 3 |
| 81 | + |
| 82 | +||| A proof is acceptable if and only if the policy is not PolicyRejected. |
| 83 | +public export |
| 84 | +isAcceptable : AxiomPolicy -> Bool |
| 85 | +isAcceptable PolicyRejected = False |
| 86 | +isAcceptable _ = True |
| 87 | + |
| 88 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 89 | +-- The two central soundness invariants |
| 90 | +-- (These are what SPARK GNATprove formally verifies on the Ada side.) |
| 91 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 92 | + |
| 93 | +||| Specification of enforcePolicySpec: the reference function that the |
| 94 | +||| SPARK implementation must replicate. |
| 95 | +||| |
| 96 | +||| This is NOT the live callable; it is the specification the bridge tests |
| 97 | +||| against via cross-check mode. |
| 98 | +public export |
| 99 | +enforcePolicySpec : List DangerLevel -> AxiomPolicy |
| 100 | +enforcePolicySpec usages = |
| 101 | + if any (== Reject) usages then PolicyRejected |
| 102 | + else if any (== Warning) usages then PolicyIncomplete |
| 103 | + else if any (== Noted) usages then PolicyClassical |
| 104 | + else PolicyClean |
| 105 | + |
| 106 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 107 | +-- Invariant 1 (Soundness): Reject-in → PolicyRejected-out |
| 108 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 109 | + |
| 110 | +||| Proof that if any usage is Reject, the spec returns PolicyRejected. |
| 111 | +public export |
| 112 | +soundness |
| 113 | + : (usages : List DangerLevel) |
| 114 | + -> Elem Reject usages |
| 115 | + -> enforcePolicySpec usages = PolicyRejected |
| 116 | +soundness usages prf = |
| 117 | + -- 'any (== Reject) usages' is True whenever Reject is an element |
| 118 | + rewrite anyRejectIsTrue usages prf in Refl |
| 119 | + where |
| 120 | + anyRejectIsTrue |
| 121 | + : (us : List DangerLevel) |
| 122 | + -> Elem Reject us |
| 123 | + -> any (== Reject) us = True |
| 124 | + anyRejectIsTrue (Reject :: _) Here = Refl |
| 125 | + anyRejectIsTrue (_ :: rest) (There p) = anyRejectIsTrue rest p |
| 126 | + anyRejectIsTrue [] p = absurd p |
| 127 | + |
| 128 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 129 | +-- Invariant 2 (Precision): no Reject-in → not PolicyRejected-out |
| 130 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 131 | + |
| 132 | +||| Proof that if no usage is Reject, the spec never returns PolicyRejected. |
| 133 | +public export |
| 134 | +precision |
| 135 | + : (usages : List DangerLevel) |
| 136 | + -> ((e : DangerLevel) -> Elem e usages -> Not (e = Reject)) |
| 137 | + -> Not (enforcePolicySpec usages = PolicyRejected) |
| 138 | +precision usages noReject prf = |
| 139 | + -- If enforcePolicySpec returns PolicyRejected, then 'any (== Reject) usages' |
| 140 | + -- must have been True, so there is a Reject in the list — contradicting noReject. |
| 141 | + let anyFalse = noRejectMeansAnyFalse usages noReject |
| 142 | + in rewrite anyFalse in absurd prf |
| 143 | + where |
| 144 | + noRejectMeansAnyFalse |
| 145 | + : (us : List DangerLevel) |
| 146 | + -> ((e : DangerLevel) -> Elem e us -> Not (e = Reject)) |
| 147 | + -> any (== Reject) us = False |
| 148 | + noRejectMeansAnyFalse [] _ = Refl |
| 149 | + noRejectMeansAnyFalse (x :: xs) noR = |
| 150 | + case decEq x Reject of |
| 151 | + Yes Refl => absurd (noR Reject Here Refl) |
| 152 | + No _ => |
| 153 | + let rest = noRejectMeansAnyFalse xs (\e p => noR e (There p)) |
| 154 | + in rewrite rest in Refl |
| 155 | + |
| 156 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 157 | +-- ABI Layout declarations (for Zig/Rust bridge validation) |
| 158 | +-- ════════════════════════════════════════════════════════════════════════════ |
| 159 | + |
| 160 | +||| C function name exported by the SPARK/Ada layer (via Zig shim). |
| 161 | +public export |
| 162 | +sparkEnforcePolicySymbol : String |
| 163 | +sparkEnforcePolicySymbol = "echidna_spark_enforce_policy" |
| 164 | + |
| 165 | +||| C function name for worst-danger query. |
| 166 | +public export |
| 167 | +sparkWorstDangerSymbol : String |
| 168 | +sparkWorstDangerSymbol = "echidna_spark_worst_danger" |
| 169 | + |
| 170 | +||| Maximum array size accepted by the C bridge. |
| 171 | +||| Must match Axiom_Policy.Max_Usages in src/ada/spark/axiom_policy.ads. |
| 172 | +public export |
| 173 | +maxUsages : Nat |
| 174 | +maxUsages = 1024 |
0 commit comments