|
| 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 | +||| Layer 4 — ABI<->FFI seam soundness proof for Futharkiser. |
| 5 | +||| |
| 6 | +||| The structural gate (scripts/abi-ffi-gate.py) checks that the Idris |
| 7 | +||| `Result` enum and the Zig FFI enum agree by name and value. This module |
| 8 | +||| supplies the PROOF-SIDE guarantee that the encoding itself is SOUND: |
| 9 | +||| |
| 10 | +||| (a) `resultToIntInjective` — distinct ABI outcomes never collide on the |
| 11 | +||| wire: if two `Result`s encode to the same C integer, they are equal. |
| 12 | +||| (b) `intToResult` / `resultRoundTrip` — the encoding is faithful and |
| 13 | +||| lossless: decoding the encoded integer recovers the original value. |
| 14 | +||| |
| 15 | +||| Together these certify that the C integer crossing the FFI boundary |
| 16 | +||| faithfully round-trips back to the ABI value, with no aliasing. |
| 17 | +||| |
| 18 | +||| The repo's only canonical FFI result-code encoder in the ABI Types module |
| 19 | +||| is `resultToInt`; there is no `ProofStatus`/`statusToInt` here, so part (c) |
| 20 | +||| of the seam mandate is vacuous for this repo. |
| 21 | + |
| 22 | +module Futharkiser.ABI.FfiSeam |
| 23 | + |
| 24 | +import Futharkiser.ABI.Types |
| 25 | + |
| 26 | +%default total |
| 27 | + |
| 28 | +-------------------------------------------------------------------------------- |
| 29 | +-- Primitive Bits32 disequality helpers |
| 30 | +-------------------------------------------------------------------------------- |
| 31 | + |
| 32 | +||| Distinct primitive Bits32 literals are provably unequal. Idris's coverage |
| 33 | +||| checker discharges `Refl impossible` for distinct primitive constants, so |
| 34 | +||| each pair we actually need can be refuted directly at the use site below. |
| 35 | + |
| 36 | +-------------------------------------------------------------------------------- |
| 37 | +-- (a) Injectivity of the encoding (proved directly) |
| 38 | +-------------------------------------------------------------------------------- |
| 39 | + |
| 40 | +||| The result-code encoding is injective: if two outcomes encode to the same |
| 41 | +||| C integer they must be the same outcome. Distinct outcomes therefore never |
| 42 | +||| collide on the wire. Proved by nested case on both results: the diagonal is |
| 43 | +||| `Refl`; every off-diagonal case is refuted because the two distinct integer |
| 44 | +||| literals cannot be equal (`\case Refl impossible`). |
| 45 | +public export |
| 46 | +resultToIntInjective : (a, b : Result) -> |
| 47 | + resultToInt a = resultToInt b -> a = b |
| 48 | +resultToIntInjective Ok Ok _ = Refl |
| 49 | +resultToIntInjective Error Error _ = Refl |
| 50 | +resultToIntInjective InvalidParam InvalidParam _ = Refl |
| 51 | +resultToIntInjective OutOfMemory OutOfMemory _ = Refl |
| 52 | +resultToIntInjective NullPointer NullPointer _ = Refl |
| 53 | +resultToIntInjective CompilationFailed CompilationFailed _ = Refl |
| 54 | +resultToIntInjective BackendUnavailable BackendUnavailable _ = Refl |
| 55 | +resultToIntInjective ShapeMismatch ShapeMismatch _ = Refl |
| 56 | +resultToIntInjective Ok Error prf = case prf of Refl impossible |
| 57 | +resultToIntInjective Ok InvalidParam prf = case prf of Refl impossible |
| 58 | +resultToIntInjective Ok OutOfMemory prf = case prf of Refl impossible |
| 59 | +resultToIntInjective Ok NullPointer prf = case prf of Refl impossible |
| 60 | +resultToIntInjective Ok CompilationFailed prf = case prf of Refl impossible |
| 61 | +resultToIntInjective Ok BackendUnavailable prf = case prf of Refl impossible |
| 62 | +resultToIntInjective Ok ShapeMismatch prf = case prf of Refl impossible |
| 63 | +resultToIntInjective Error Ok prf = case prf of Refl impossible |
| 64 | +resultToIntInjective Error InvalidParam prf = case prf of Refl impossible |
| 65 | +resultToIntInjective Error OutOfMemory prf = case prf of Refl impossible |
| 66 | +resultToIntInjective Error NullPointer prf = case prf of Refl impossible |
| 67 | +resultToIntInjective Error CompilationFailed prf = case prf of Refl impossible |
| 68 | +resultToIntInjective Error BackendUnavailable prf = case prf of Refl impossible |
| 69 | +resultToIntInjective Error ShapeMismatch prf = case prf of Refl impossible |
| 70 | +resultToIntInjective InvalidParam Ok prf = case prf of Refl impossible |
| 71 | +resultToIntInjective InvalidParam Error prf = case prf of Refl impossible |
| 72 | +resultToIntInjective InvalidParam OutOfMemory prf = case prf of Refl impossible |
| 73 | +resultToIntInjective InvalidParam NullPointer prf = case prf of Refl impossible |
| 74 | +resultToIntInjective InvalidParam CompilationFailed prf = case prf of Refl impossible |
| 75 | +resultToIntInjective InvalidParam BackendUnavailable prf = case prf of Refl impossible |
| 76 | +resultToIntInjective InvalidParam ShapeMismatch prf = case prf of Refl impossible |
| 77 | +resultToIntInjective OutOfMemory Ok prf = case prf of Refl impossible |
| 78 | +resultToIntInjective OutOfMemory Error prf = case prf of Refl impossible |
| 79 | +resultToIntInjective OutOfMemory InvalidParam prf = case prf of Refl impossible |
| 80 | +resultToIntInjective OutOfMemory NullPointer prf = case prf of Refl impossible |
| 81 | +resultToIntInjective OutOfMemory CompilationFailed prf = case prf of Refl impossible |
| 82 | +resultToIntInjective OutOfMemory BackendUnavailable prf = case prf of Refl impossible |
| 83 | +resultToIntInjective OutOfMemory ShapeMismatch prf = case prf of Refl impossible |
| 84 | +resultToIntInjective NullPointer Ok prf = case prf of Refl impossible |
| 85 | +resultToIntInjective NullPointer Error prf = case prf of Refl impossible |
| 86 | +resultToIntInjective NullPointer InvalidParam prf = case prf of Refl impossible |
| 87 | +resultToIntInjective NullPointer OutOfMemory prf = case prf of Refl impossible |
| 88 | +resultToIntInjective NullPointer CompilationFailed prf = case prf of Refl impossible |
| 89 | +resultToIntInjective NullPointer BackendUnavailable prf = case prf of Refl impossible |
| 90 | +resultToIntInjective NullPointer ShapeMismatch prf = case prf of Refl impossible |
| 91 | +resultToIntInjective CompilationFailed Ok prf = case prf of Refl impossible |
| 92 | +resultToIntInjective CompilationFailed Error prf = case prf of Refl impossible |
| 93 | +resultToIntInjective CompilationFailed InvalidParam prf = case prf of Refl impossible |
| 94 | +resultToIntInjective CompilationFailed OutOfMemory prf = case prf of Refl impossible |
| 95 | +resultToIntInjective CompilationFailed NullPointer prf = case prf of Refl impossible |
| 96 | +resultToIntInjective CompilationFailed BackendUnavailable prf = case prf of Refl impossible |
| 97 | +resultToIntInjective CompilationFailed ShapeMismatch prf = case prf of Refl impossible |
| 98 | +resultToIntInjective BackendUnavailable Ok prf = case prf of Refl impossible |
| 99 | +resultToIntInjective BackendUnavailable Error prf = case prf of Refl impossible |
| 100 | +resultToIntInjective BackendUnavailable InvalidParam prf = case prf of Refl impossible |
| 101 | +resultToIntInjective BackendUnavailable OutOfMemory prf = case prf of Refl impossible |
| 102 | +resultToIntInjective BackendUnavailable NullPointer prf = case prf of Refl impossible |
| 103 | +resultToIntInjective BackendUnavailable CompilationFailed prf = case prf of Refl impossible |
| 104 | +resultToIntInjective BackendUnavailable ShapeMismatch prf = case prf of Refl impossible |
| 105 | +resultToIntInjective ShapeMismatch Ok prf = case prf of Refl impossible |
| 106 | +resultToIntInjective ShapeMismatch Error prf = case prf of Refl impossible |
| 107 | +resultToIntInjective ShapeMismatch InvalidParam prf = case prf of Refl impossible |
| 108 | +resultToIntInjective ShapeMismatch OutOfMemory prf = case prf of Refl impossible |
| 109 | +resultToIntInjective ShapeMismatch NullPointer prf = case prf of Refl impossible |
| 110 | +resultToIntInjective ShapeMismatch CompilationFailed prf = case prf of Refl impossible |
| 111 | +resultToIntInjective ShapeMismatch BackendUnavailable prf = case prf of Refl impossible |
| 112 | + |
| 113 | +-------------------------------------------------------------------------------- |
| 114 | +-- (b) Faithful (lossless) decoding |
| 115 | +-------------------------------------------------------------------------------- |
| 116 | + |
| 117 | +||| Decode a C integer back into a `Result`. Built with boolean `==` on |
| 118 | +||| concrete `Bits32` literals (which reduces definitionally), so the |
| 119 | +||| round-trip lemma's `Refl`s check. Out-of-range integers decode to |
| 120 | +||| `Nothing` (no spurious ABI value is invented from the wire). |
| 121 | +public export |
| 122 | +intToResult : Bits32 -> Maybe Result |
| 123 | +intToResult x = |
| 124 | + if x == 0 then Just Ok |
| 125 | + else if x == 1 then Just Error |
| 126 | + else if x == 2 then Just InvalidParam |
| 127 | + else if x == 3 then Just OutOfMemory |
| 128 | + else if x == 4 then Just NullPointer |
| 129 | + else if x == 5 then Just CompilationFailed |
| 130 | + else if x == 6 then Just BackendUnavailable |
| 131 | + else if x == 7 then Just ShapeMismatch |
| 132 | + else Nothing |
| 133 | + |
| 134 | +||| Faithful round-trip: encoding a `Result` and decoding the integer recovers |
| 135 | +||| exactly the original value. This certifies the C integer crossing the FFI |
| 136 | +||| boundary loses no information. |
| 137 | +public export |
| 138 | +resultRoundTrip : (r : Result) -> intToResult (resultToInt r) = Just r |
| 139 | +resultRoundTrip Ok = Refl |
| 140 | +resultRoundTrip Error = Refl |
| 141 | +resultRoundTrip InvalidParam = Refl |
| 142 | +resultRoundTrip OutOfMemory = Refl |
| 143 | +resultRoundTrip NullPointer = Refl |
| 144 | +resultRoundTrip CompilationFailed = Refl |
| 145 | +resultRoundTrip BackendUnavailable = Refl |
| 146 | +resultRoundTrip ShapeMismatch = Refl |
| 147 | + |
| 148 | +||| `Just` is injective (local lemma; not exported by the stdlib used here). |
| 149 | +justEq : {0 x, y : Result} -> Just x = Just y -> x = y |
| 150 | +justEq Refl = Refl |
| 151 | + |
| 152 | +||| Injectivity DERIVED from the round-trip (independent of the direct proof |
| 153 | +||| above): if two results encode equally, decoding both gives the same |
| 154 | +||| `Just`, and `Just` is injective. Demonstrates the round-trip is strong |
| 155 | +||| enough to imply non-collision on its own. |
| 156 | +public export |
| 157 | +roundTripInjective : (a, b : Result) -> |
| 158 | + resultToInt a = resultToInt b -> a = b |
| 159 | +roundTripInjective a b prf = |
| 160 | + justEq $ |
| 161 | + rewrite sym (resultRoundTrip a) in |
| 162 | + rewrite sym (resultRoundTrip b) in |
| 163 | + cong intToResult prf |
| 164 | + |
| 165 | +-------------------------------------------------------------------------------- |
| 166 | +-- Positive controls (concrete decodes, machine-checked) |
| 167 | +-------------------------------------------------------------------------------- |
| 168 | + |
| 169 | +||| Positive control: the integer 0 decodes to `Ok`. |
| 170 | +public export |
| 171 | +decodeOk : intToResult 0 = Just Ok |
| 172 | +decodeOk = Refl |
| 173 | + |
| 174 | +||| Positive control: the integer 7 decodes to `ShapeMismatch` (the top code). |
| 175 | +public export |
| 176 | +decodeShapeMismatch : intToResult 7 = Just ShapeMismatch |
| 177 | +decodeShapeMismatch = Refl |
| 178 | + |
| 179 | +||| Positive control: an out-of-range integer decodes to `Nothing` (the wire |
| 180 | +||| cannot smuggle in an undefined ABI outcome). |
| 181 | +public export |
| 182 | +decodeOutOfRange : intToResult 8 = Nothing |
| 183 | +decodeOutOfRange = Refl |
| 184 | + |
| 185 | +-------------------------------------------------------------------------------- |
| 186 | +-- Negative / non-vacuity control (machine-checked) |
| 187 | +-------------------------------------------------------------------------------- |
| 188 | + |
| 189 | +||| Non-vacuity: two DISTINCT result codes have DISTINCT integer encodings. |
| 190 | +||| If this were not machine-checkable, injectivity could hold vacuously. The |
| 191 | +||| coverage checker discharges the impossible `Refl` for distinct primitive |
| 192 | +||| `Bits32` constants (0 vs 1), so the disequality is genuinely inhabited. |
| 193 | +public export |
| 194 | +okErrorDistinct : Not (resultToInt Ok = resultToInt Error) |
| 195 | +okErrorDistinct = \case Refl impossible |
| 196 | + |
| 197 | +||| A second non-vacuity witness across a non-adjacent pair (0 vs 7). |
| 198 | +public export |
| 199 | +okShapeMismatchDistinct : Not (resultToInt Ok = resultToInt ShapeMismatch) |
| 200 | +okShapeMismatchDistinct = \case Refl impossible |
0 commit comments