|
| 1 | +-- SPDX-License-Identifier: MPL-2.0 |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +||| Flagship semantic proof for Idrisiser (ABI Layer 2). |
| 5 | +||| |
| 6 | +||| Idrisiser's headline promise is to "generate proven-correct wrappers using |
| 7 | +||| Idris2 dependent types". The canonical such wrapper is a *bounded array |
| 8 | +||| accessor*: a generated function that reads element `i` of an `n`-element |
| 9 | +||| array and is GUARANTEED never to read out of bounds at runtime. |
| 10 | +||| |
| 11 | +||| This module gives that promise a real, machine-checked meaning: |
| 12 | +||| |
| 13 | +||| 1. A faithful model of a length-indexed array (`SafeArray n a` wrapping a |
| 14 | +||| `Vect n a`) and its total accessor `safeIndex : SafeArray n a -> Fin n |
| 15 | +||| -> a`. |
| 16 | +||| 2. The headline property `InBounds i n` — inhabited EXACTLY when `i < n` |
| 17 | +||| (it carries an `LT i n` proof). The out-of-range case has no |
| 18 | +||| constructor, and in particular NO index exists into an empty array |
| 19 | +||| (`Uninhabited (InBounds i 0)`): out-of-range access is unrepresentable. |
| 20 | +||| 3. A sound AND complete decision procedure `decInBounds` returning a real |
| 21 | +||| `Dec (InBounds i n)`, backed by `Data.Nat.isLT`. |
| 22 | +||| 4. A totality / no-off-by-one fact: the proven index round-trips exactly to |
| 23 | +||| the requested raw position (`finToNat (toFin ok) = i`), via the |
| 24 | +||| hand-proved `finToNatNatToFinLT`. `safeIndex` is total — no error code, |
| 25 | +||| no partiality. |
| 26 | +||| 5. A certifier into the ABI's `Totality` witness with a soundness proof |
| 27 | +||| (`certifyBounded` returns `Total` only when the index is genuinely in |
| 28 | +||| bounds). |
| 29 | +||| 6. A POSITIVE control (an inhabited witness that reads the expected |
| 30 | +||| element) and NEGATIVE controls (`Not (InBounds 5 3)` and the empty |
| 31 | +||| array), all machine-checked. |
| 32 | + |
| 33 | +module Idrisiser.ABI.Semantics |
| 34 | + |
| 35 | +import Idrisiser.ABI.Types |
| 36 | +import Data.Fin |
| 37 | +import Data.Nat |
| 38 | +import Data.Vect |
| 39 | +import Decidable.Equality |
| 40 | + |
| 41 | +%default total |
| 42 | + |
| 43 | +-------------------------------------------------------------------------------- |
| 44 | +-- 1. Faithful model |
| 45 | +-------------------------------------------------------------------------------- |
| 46 | + |
| 47 | +||| A generated bounded-array wrapper: a length-indexed buffer. The length `n` |
| 48 | +||| is part of the TYPE, so it cannot drift from the data at runtime. |
| 49 | +public export |
| 50 | +record SafeArray (n : Nat) (a : Type) where |
| 51 | + constructor MkSafeArray |
| 52 | + buffer : Vect n a |
| 53 | + |
| 54 | +||| The total, never-failing accessor that Idrisiser's codegen emits. It takes |
| 55 | +||| a *proven* index `Fin n`; there is no error path and no partiality, so the |
| 56 | +||| result is always a genuine element of the array. |
| 57 | +public export |
| 58 | +safeIndex : SafeArray n a -> Fin n -> a |
| 59 | +safeIndex (MkSafeArray xs) i = index i xs |
| 60 | + |
| 61 | +-------------------------------------------------------------------------------- |
| 62 | +-- 2. The headline property: InBounds |
| 63 | +-------------------------------------------------------------------------------- |
| 64 | + |
| 65 | +||| `InBounds i n` is inhabited exactly when the raw natural index `i` is a |
| 66 | +||| legal index into an `n`-element array, i.e. `i < n`. The witness carries an |
| 67 | +||| `LT i n` proof, from which a real `Fin n` safe index is computed. |
| 68 | +||| |
| 69 | +||| There is deliberately no constructor for the out-of-range case: when |
| 70 | +||| `i >= n` there is no `LT i n`, so `InBounds i n` cannot be built. |
| 71 | +public export |
| 72 | +data InBounds : (i : Nat) -> (n : Nat) -> Type where |
| 73 | + MkInBounds : (prf : LT i n) -> InBounds i n |
| 74 | + |
| 75 | +||| Compute the proven safe index from an in-bounds witness. |
| 76 | +public export |
| 77 | +toFin : {i, n : Nat} -> InBounds i n -> Fin n |
| 78 | +toFin (MkInBounds prf) = natToFinLT i {prf} |
| 79 | + |
| 80 | +||| There is NO index at all into an empty array, so any claim that some `i` is |
| 81 | +||| in bounds of a 0-length array is absurd. This is the formal statement that |
| 82 | +||| out-of-range access is unrepresentable in the model. |
| 83 | +public export |
| 84 | +Uninhabited (InBounds i 0) where |
| 85 | + uninhabited (MkInBounds prf) = absurd prf |
| 86 | + |
| 87 | +-------------------------------------------------------------------------------- |
| 88 | +-- 3. Sound + complete decision procedure |
| 89 | +-------------------------------------------------------------------------------- |
| 90 | + |
| 91 | +||| Decide whether raw index `i` is in bounds of an `n`-element array. |
| 92 | +||| |
| 93 | +||| SOUND: a `Yes` carries a genuine `LT i n` proof. |
| 94 | +||| COMPLETE: a `No` is a real refutation — any `InBounds i n` would yield the |
| 95 | +||| very `LT i n` that `isLT` has just shown impossible. |
| 96 | +public export |
| 97 | +decInBounds : (i : Nat) -> (n : Nat) -> Dec (InBounds i n) |
| 98 | +decInBounds i n = case isLT i n of |
| 99 | + Yes prf => Yes (MkInBounds prf) |
| 100 | + No contra => No (\(MkInBounds prf) => contra prf) |
| 101 | + |
| 102 | +-------------------------------------------------------------------------------- |
| 103 | +-- 4. Totality / no-off-by-one fact |
| 104 | +-------------------------------------------------------------------------------- |
| 105 | + |
| 106 | +||| The proven index lands exactly on the requested raw position: converting an |
| 107 | +||| `LT i n` proof to a `Fin n` and back to a `Nat` returns `i`. Proved by |
| 108 | +||| induction on the `LT` witness — no `believe_me`, no `assert`. |
| 109 | +public export |
| 110 | +finToNatNatToFinLT : (i : Nat) -> (prf : LT i n) -> |
| 111 | + finToNat (natToFinLT i {prf}) = i |
| 112 | +finToNatNatToFinLT Z (LTESucc _) = Refl |
| 113 | +finToNatNatToFinLT (S k) (LTESucc p) = cong S (finToNatNatToFinLT k p) |
| 114 | + |
| 115 | +||| For any proven index into any array, the index the accessor uses round-trips |
| 116 | +||| back to the requested raw position `i`. This is the "no off-by-one, no |
| 117 | +||| runtime failure" guarantee: `safeIndex` reads precisely slot `i`. |
| 118 | +public export |
| 119 | +safeIndexUsesExactSlot : {i, n : Nat} -> (arr : SafeArray n a) -> |
| 120 | + (ok : InBounds i n) -> finToNat (toFin ok) = i |
| 121 | +safeIndexUsesExactSlot arr (MkInBounds prf) = finToNatNatToFinLT i prf |
| 122 | + |
| 123 | +-------------------------------------------------------------------------------- |
| 124 | +-- 5. Certifier into the ABI Totality witness |
| 125 | +-------------------------------------------------------------------------------- |
| 126 | + |
| 127 | +||| Certify a candidate (raw index, length) pair: `Total` when the generated |
| 128 | +||| accessor is provably never-failing for that index, `Covering` otherwise. |
| 129 | +public export |
| 130 | +certifyBounded : (i : Nat) -> (n : Nat) -> Totality |
| 131 | +certifyBounded i n = case decInBounds i n of |
| 132 | + Yes _ => Total |
| 133 | + No _ => Covering |
| 134 | + |
| 135 | +||| Soundness of the certifier: it returns `Total` only when the index really |
| 136 | +||| is in bounds (so the generated wrapper really is failure-free). |
| 137 | +public export |
| 138 | +certifyBoundedSound : (i : Nat) -> (n : Nat) -> |
| 139 | + certifyBounded i n = Total -> InBounds i n |
| 140 | +certifyBoundedSound i n eq with (decInBounds i n) |
| 141 | + certifyBoundedSound i n eq | Yes ok = ok |
| 142 | + certifyBoundedSound i n Refl | No _ impossible |
| 143 | + |
| 144 | +-------------------------------------------------------------------------------- |
| 145 | +-- 6. Controls |
| 146 | +-------------------------------------------------------------------------------- |
| 147 | + |
| 148 | +||| A concrete 3-element array used by the controls. |
| 149 | +public export |
| 150 | +demoArray : SafeArray 3 String |
| 151 | +demoArray = MkSafeArray ["alpha", "beta", "gamma"] |
| 152 | + |
| 153 | +||| POSITIVE control: index 2 is in bounds of a 3-element array (explicit, |
| 154 | +||| inhabited witness carrying a real `LT 2 3` proof). |
| 155 | +public export |
| 156 | +idx2InBounds : InBounds 2 3 |
| 157 | +idx2InBounds = MkInBounds (LTESucc (LTESucc (LTESucc LTEZero))) |
| 158 | + |
| 159 | +||| The positive control actually reads the expected element through the total |
| 160 | +||| accessor. Concrete, so it reduces by `Refl`. |
| 161 | +public export |
| 162 | +demoReadsGamma : safeIndex Semantics.demoArray (toFin Semantics.idx2InBounds) = "gamma" |
| 163 | +demoReadsGamma = Refl |
| 164 | + |
| 165 | +||| The positive control's proven index round-trips to raw position 2. |
| 166 | +public export |
| 167 | +idx2RoundTrips : finToNat (toFin Semantics.idx2InBounds) = 2 |
| 168 | +idx2RoundTrips = Refl |
| 169 | + |
| 170 | +||| NEGATIVE control: index 5 is NOT in bounds of a 3-element array — a |
| 171 | +||| machine-checked refutation of the bad case (no `LT 5 3` exists). |
| 172 | +public export |
| 173 | +idx5OutOfBounds : Not (InBounds 5 3) |
| 174 | +idx5OutOfBounds (MkInBounds prf) = absurd prf |
| 175 | + |
| 176 | +||| NEGATIVE control: there is no index at all into an empty array. |
| 177 | +public export |
| 178 | +noIndexIntoEmpty : Not (InBounds i 0) |
| 179 | +noIndexIntoEmpty = absurd |
0 commit comments