|
| 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 | +||| Array-update invariants for Idrisiser (ABI Layer 3). |
| 5 | +||| |
| 6 | +||| Layer 2 (`Idrisiser.ABI.Semantics`) proved the *in-bounds totality* of the |
| 7 | +||| generated accessor: an index is representable EXACTLY when it is `< n`, and |
| 8 | +||| out-of-range reads are unrepresentable. That is a property of READING. |
| 9 | +||| |
| 10 | +||| This module proves a genuinely DEEPER and DISTINCT class of property: the |
| 11 | +||| algebraic LAWS that relate a generated WRITE to a subsequent READ — the |
| 12 | +||| "memory model" correctness of the wrapper Idrisiser emits. These are the |
| 13 | +||| canonical store/select axioms of McCarthy's theory of arrays: |
| 14 | +||| |
| 15 | +||| * WRITE-THEN-READ (read-over-write, same index): |
| 16 | +||| reading slot `i` immediately after writing `v` into slot `i` |
| 17 | +||| returns exactly `v`. `safeReadAfterWriteSame` |
| 18 | +||| |
| 19 | +||| * READ-OTHER (read-over-write, different index): |
| 20 | +||| a write at slot `j` leaves every OTHER slot `i /= j` untouched. |
| 21 | +||| `safeReadAfterWriteOther` |
| 22 | +||| |
| 23 | +||| * DECISIVE READ-OVER-WRITE: a single, sound decision procedure on the two |
| 24 | +||| indices that returns the value a post-write read must yield, together |
| 25 | +||| with a proof it is correct. `safeReadOverWrite` |
| 26 | +||| |
| 27 | +||| Everything is built over the SAME model exported by Layer 2 — the same |
| 28 | +||| `SafeArray`, the same `safeIndex` — so the two layers compose. The new |
| 29 | +||| accessor `safeWrite` is the formal dual of `safeIndex`. The Vect-level |
| 30 | +||| lemmas are proved here by hand (induction on the index / vector), NOT |
| 31 | +||| delegated to a library lemma, so this is a self-contained genuine proof. |
| 32 | +||| |
| 33 | +||| Controls: a POSITIVE witness that a concrete write-then-read returns the |
| 34 | +||| written element, a POSITIVE witness that a neighbouring slot is preserved, |
| 35 | +||| and a NEGATIVE / non-vacuity control (`Not (... = oldValue)`) showing the |
| 36 | +||| write genuinely changed the slot — machine-checked. |
| 37 | + |
| 38 | +module Idrisiser.ABI.Invariants |
| 39 | + |
| 40 | +import Idrisiser.ABI.Semantics |
| 41 | +import Data.Fin |
| 42 | +import Data.Vect |
| 43 | +import Decidable.Equality |
| 44 | + |
| 45 | +%default total |
| 46 | + |
| 47 | +-------------------------------------------------------------------------------- |
| 48 | +-- 1. The write accessor: formal dual of Layer 2's `safeIndex` |
| 49 | +-------------------------------------------------------------------------------- |
| 50 | + |
| 51 | +||| Write `v` into slot `i` of a length-indexed buffer. Like `safeIndex`, it |
| 52 | +||| takes a *proven* index `Fin n`, so there is no error path and the length is |
| 53 | +||| preserved in the TYPE: a write can never resize or escape the buffer. This |
| 54 | +||| is the store half of the generated read/write pair. |
| 55 | +public export |
| 56 | +safeWrite : SafeArray n a -> Fin n -> a -> SafeArray n a |
| 57 | +safeWrite (MkSafeArray xs) i v = MkSafeArray (replaceAt i v xs) |
| 58 | + |
| 59 | +-------------------------------------------------------------------------------- |
| 60 | +-- 2. Vect-level lemmas (proved by hand here) |
| 61 | +-------------------------------------------------------------------------------- |
| 62 | + |
| 63 | +||| WRITE-THEN-READ at the Vect level: after replacing slot `i` with `v`, |
| 64 | +||| reading slot `i` yields `v`. Proved by induction on the index, mirroring |
| 65 | +||| the recursive structure of both `index` and `replaceAt`. |
| 66 | +export |
| 67 | +indexReplaceAtSame : (i : Fin n) -> (v : a) -> (xs : Vect n a) -> |
| 68 | + index i (replaceAt i v xs) = v |
| 69 | +indexReplaceAtSame FZ v (_ :: _) = Refl |
| 70 | +indexReplaceAtSame (FS k) v (_ :: ys) = indexReplaceAtSame k v ys |
| 71 | + |
| 72 | +||| READ-OTHER at the Vect level: a write at slot `j` does not disturb a |
| 73 | +||| DIFFERENT slot `i /= j`. Proved by induction on both indices; the two |
| 74 | +||| `FZ`/`FS` cross cases are immediate, the `FS`/`FS` case recurses with the |
| 75 | +||| tail-level distinctness derived from injectivity of `FS`. |
| 76 | +export |
| 77 | +indexReplaceAtOther : (i, j : Fin n) -> Not (i = j) -> (v : a) -> |
| 78 | + (xs : Vect n a) -> |
| 79 | + index i (replaceAt j v xs) = index i xs |
| 80 | +indexReplaceAtOther FZ FZ neq _ (_ :: _) = absurd (neq Refl) |
| 81 | +indexReplaceAtOther FZ (FS _) _ _ (_ :: _) = Refl |
| 82 | +indexReplaceAtOther (FS _) FZ _ _ (_ :: _) = Refl |
| 83 | +indexReplaceAtOther (FS k) (FS m) neq v (_ :: ys) = |
| 84 | + indexReplaceAtOther k m (\eq => neq (cong FS eq)) v ys |
| 85 | + |
| 86 | +-------------------------------------------------------------------------------- |
| 87 | +-- 3. The Layer-3 laws, lifted to the SafeArray model |
| 88 | +-------------------------------------------------------------------------------- |
| 89 | + |
| 90 | +||| WRITE-THEN-READ LAW (read-over-write, same index): reading slot `i` of the |
| 91 | +||| array produced by writing `v` into slot `i` returns exactly `v`. This is |
| 92 | +||| the store/select axiom for matching indices. |
| 93 | +public export |
| 94 | +safeReadAfterWriteSame : (arr : SafeArray n a) -> (i : Fin n) -> (v : a) -> |
| 95 | + safeIndex (safeWrite arr i v) i = v |
| 96 | +safeReadAfterWriteSame (MkSafeArray xs) i v = indexReplaceAtSame i v xs |
| 97 | + |
| 98 | +||| READ-OTHER LAW (read-over-write, different index): writing `v` into slot `j` |
| 99 | +||| leaves every other slot `i /= j` exactly as it was. This is the locality / |
| 100 | +||| non-interference axiom that makes the generated buffer a real array. |
| 101 | +public export |
| 102 | +safeReadAfterWriteOther : (arr : SafeArray n a) -> (i, j : Fin n) -> |
| 103 | + Not (i = j) -> (v : a) -> |
| 104 | + safeIndex (safeWrite arr j v) i = safeIndex arr i |
| 105 | +safeReadAfterWriteOther (MkSafeArray xs) i j neq v = |
| 106 | + indexReplaceAtOther i j neq v xs |
| 107 | + |
| 108 | +-------------------------------------------------------------------------------- |
| 109 | +-- 4. Decisive read-over-write: sound decision procedure |
| 110 | +-------------------------------------------------------------------------------- |
| 111 | + |
| 112 | +||| The value a post-write read MUST return, decided on the two indices. |
| 113 | +||| |
| 114 | +||| `decEq` on `Fin n` is the natural, sound+complete decision here. When the |
| 115 | +||| indices coincide we know (by the write-then-read law) the result is the |
| 116 | +||| written value; when they differ we know (by the read-other law) the result |
| 117 | +||| is the original element. `safeReadOverWriteCorrect` proves this dispatch |
| 118 | +||| agrees with the actual accessor on the actual array — no case is guessed. |
| 119 | +public export |
| 120 | +predictReadOverWrite : (arr : SafeArray n a) -> (i, j : Fin n) -> (v : a) -> a |
| 121 | +predictReadOverWrite arr i j v = case decEq i j of |
| 122 | + Yes _ => v |
| 123 | + No _ => safeIndex arr i |
| 124 | + |
| 125 | +||| SOUNDNESS of the prediction: an actual read after the actual write equals |
| 126 | +||| the predicted value, in BOTH branches of the decision. |
| 127 | +public export |
| 128 | +safeReadOverWriteCorrect : (arr : SafeArray n a) -> (i, j : Fin n) -> (v : a) -> |
| 129 | + safeIndex (safeWrite arr j v) i = |
| 130 | + predictReadOverWrite arr i j v |
| 131 | +safeReadOverWriteCorrect arr i j v with (decEq i j) |
| 132 | + safeReadOverWriteCorrect arr i j v | Yes eq = |
| 133 | + -- i = j, so a read at i is a read at the slot just written. |
| 134 | + rewrite eq in safeReadAfterWriteSame arr j v |
| 135 | + safeReadOverWriteCorrect arr i j v | No neq = |
| 136 | + safeReadAfterWriteOther arr i j neq v |
| 137 | + |
| 138 | +-------------------------------------------------------------------------------- |
| 139 | +-- 5. Controls |
| 140 | +-------------------------------------------------------------------------------- |
| 141 | + |
| 142 | +||| A concrete 3-element array used by the controls (independent of the Layer-2 |
| 143 | +||| `demoArray` so the controls here stand alone). |
| 144 | +public export |
| 145 | +ctrlArray : SafeArray 3 String |
| 146 | +ctrlArray = MkSafeArray ["alpha", "beta", "gamma"] |
| 147 | + |
| 148 | +||| POSITIVE control (write-then-read): writing "delta" into slot 1 and reading |
| 149 | +||| slot 1 back returns "delta". Concrete, so it reduces by `Refl`. |
| 150 | +public export |
| 151 | +writeThenReadDelta : safeIndex (safeWrite Invariants.ctrlArray 1 "delta") 1 = "delta" |
| 152 | +writeThenReadDelta = Refl |
| 153 | + |
| 154 | +||| POSITIVE control (read-other): writing "delta" into slot 1 leaves slot 0 |
| 155 | +||| ("alpha") untouched. Concrete, reduces by `Refl`. |
| 156 | +public export |
| 157 | +otherSlotPreserved : safeIndex (safeWrite Invariants.ctrlArray 1 "delta") 0 = "alpha" |
| 158 | +otherSlotPreserved = Refl |
| 159 | + |
| 160 | +||| POSITIVE control (decision agrees): the predictor for a same-index read |
| 161 | +||| returns the written value "delta". Reduces by `Refl`. |
| 162 | +public export |
| 163 | +predictionAtSameSlot : predictReadOverWrite Invariants.ctrlArray 1 1 "delta" = "delta" |
| 164 | +predictionAtSameSlot = Refl |
| 165 | + |
| 166 | +||| NEGATIVE / non-vacuity control: the write GENUINELY changed slot 1 — after |
| 167 | +||| writing "delta", slot 1 is NOT the old value "beta". Were the laws vacuous |
| 168 | +||| or `safeWrite` a no-op, this refutation would be unprovable. Machine-checked |
| 169 | +||| via `safeReadAfterWriteSame` (slot 1 reads "delta") and the concrete fact |
| 170 | +||| that "delta" /= "beta". |
| 171 | +public export |
| 172 | +writeReallyChangedSlot : |
| 173 | + Not (safeIndex (safeWrite Invariants.ctrlArray 1 "delta") 1 = "beta") |
| 174 | +writeReallyChangedSlot eq = |
| 175 | + -- The accessor reads "delta" (law); chaining gives "delta" = "beta", absurd. |
| 176 | + case trans (sym (safeReadAfterWriteSame Invariants.ctrlArray 1 "delta")) eq of |
| 177 | + Refl impossible |
0 commit comments