|
| 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 proof: SEALING THE ABI<->FFI SEAM for Phronesiser. |
| 5 | +||| |
| 6 | +||| The structural gate (scripts/abi-ffi-gate.py) checks the Idris and Zig |
| 7 | +||| result-code enums agree by name+value. This module is the PROOF-SIDE |
| 8 | +||| guarantee that the encoding is SOUND: |
| 9 | +||| |
| 10 | +||| * distinct ABI outcomes never collide on the wire (injectivity), and |
| 11 | +||| * the C integer faithfully round-trips back to the ABI value |
| 12 | +||| (a total decoder `intToResult` is a left inverse of `resultToInt`). |
| 13 | +||| |
| 14 | +||| Injectivity is DERIVED from the round-trip via `justInj` + `cong`, |
| 15 | +||| which is the cleanest route once the decoder's round-trip `Refl`s reduce. |
| 16 | +||| The decoder is written with boolean `Bits32` `==` (which reduces on |
| 17 | +||| concrete literals), so `resultRoundTrip Ok = Refl` etc. all check |
| 18 | +||| definitionally. |
| 19 | +||| |
| 20 | +||| The same injectivity is also proved for the other FFI enum encoders |
| 21 | +||| present in `Types`: `modalityToInt` (DeonticModality) and `severityToInt` |
| 22 | +||| (HarmSeverity). (There is no `ProofStatus`/`statusToInt` in this repo.) |
| 23 | +||| |
| 24 | +||| Controls: positive (concrete decode = Refl), and a machine-checked |
| 25 | +||| NON-VACUITY control that two DISTINCT result codes have distinct ints. |
| 26 | +||| |
| 27 | +||| Genuine proof only — no believe_me / idris_crash / assert_total / |
| 28 | +||| postulate / sorry. %default total. |
| 29 | + |
| 30 | +module Phronesiser.ABI.FfiSeam |
| 31 | + |
| 32 | +import Phronesiser.ABI.Types |
| 33 | + |
| 34 | +%default total |
| 35 | + |
| 36 | +-------------------------------------------------------------------------------- |
| 37 | +-- Generic helper |
| 38 | +-------------------------------------------------------------------------------- |
| 39 | + |
| 40 | +||| `Just` is injective. Used to turn a round-trip equality through the |
| 41 | +||| decoder back into an equality of the underlying ABI values. |
| 42 | +public export |
| 43 | +justInj : {0 a, b : ty} -> Just a = Just b -> a = b |
| 44 | +justInj Refl = Refl |
| 45 | + |
| 46 | +-------------------------------------------------------------------------------- |
| 47 | +-- Result: faithful decoder + round-trip + derived injectivity |
| 48 | +-------------------------------------------------------------------------------- |
| 49 | + |
| 50 | +||| Decode a C integer back into a `Result`. |
| 51 | +||| Written with boolean `Bits32` `==` so it reduces on concrete literals, |
| 52 | +||| making the round-trip `Refl`s check definitionally. Unknown codes |
| 53 | +||| decode to `Nothing` (the wire space is larger than the ABI space). |
| 54 | +public export |
| 55 | +intToResult : Bits32 -> Maybe Result |
| 56 | +intToResult x = |
| 57 | + if x == 0 then Just Ok |
| 58 | + else if x == 1 then Just Error |
| 59 | + else if x == 2 then Just InvalidParam |
| 60 | + else if x == 3 then Just OutOfMemory |
| 61 | + else if x == 4 then Just NullPointer |
| 62 | + else if x == 5 then Just ConstraintViolation |
| 63 | + else if x == 6 then Just ConstraintConflict |
| 64 | + else Nothing |
| 65 | + |
| 66 | +||| The decoder is a left inverse of the encoder: every ABI value round-trips |
| 67 | +||| losslessly through its C integer. FAITHFULNESS of the encoding. |
| 68 | +public export |
| 69 | +resultRoundTrip : (r : Result) -> intToResult (resultToInt r) = Just r |
| 70 | +resultRoundTrip Ok = Refl |
| 71 | +resultRoundTrip Error = Refl |
| 72 | +resultRoundTrip InvalidParam = Refl |
| 73 | +resultRoundTrip OutOfMemory = Refl |
| 74 | +resultRoundTrip NullPointer = Refl |
| 75 | +resultRoundTrip ConstraintViolation = Refl |
| 76 | +resultRoundTrip ConstraintConflict = Refl |
| 77 | + |
| 78 | +||| The encoding is UNAMBIGUOUS: distinct ABI outcomes never collide on the |
| 79 | +||| wire. Derived from the round-trip (a function with a left inverse is |
| 80 | +||| injective) via `cong` + `justInj`. |
| 81 | +public export |
| 82 | +resultToIntInjective : (a, b : Result) -> resultToInt a = resultToInt b -> a = b |
| 83 | +resultToIntInjective a b prf = |
| 84 | + justInj $ |
| 85 | + trans (sym (resultRoundTrip a)) |
| 86 | + (trans (cong intToResult prf) (resultRoundTrip b)) |
| 87 | + |
| 88 | +-------------------------------------------------------------------------------- |
| 89 | +-- DeonticModality: same guarantees (task (c)) |
| 90 | +-------------------------------------------------------------------------------- |
| 91 | + |
| 92 | +||| Decode a C integer back into a `DeonticModality`. |
| 93 | +public export |
| 94 | +intToModality : Bits32 -> Maybe DeonticModality |
| 95 | +intToModality x = |
| 96 | + if x == 0 then Just Obligation |
| 97 | + else if x == 1 then Just Permission |
| 98 | + else if x == 2 then Just Prohibition |
| 99 | + else Nothing |
| 100 | + |
| 101 | +||| Round-trip / faithfulness for the deontic-modality encoder. |
| 102 | +public export |
| 103 | +modalityRoundTrip : (m : DeonticModality) -> |
| 104 | + intToModality (modalityToInt m) = Just m |
| 105 | +modalityRoundTrip Obligation = Refl |
| 106 | +modalityRoundTrip Permission = Refl |
| 107 | +modalityRoundTrip Prohibition = Refl |
| 108 | + |
| 109 | +||| Injectivity for the deontic-modality encoder, derived from round-trip. |
| 110 | +public export |
| 111 | +modalityToIntInjective : (a, b : DeonticModality) -> |
| 112 | + modalityToInt a = modalityToInt b -> a = b |
| 113 | +modalityToIntInjective a b prf = |
| 114 | + justInj $ |
| 115 | + trans (sym (modalityRoundTrip a)) |
| 116 | + (trans (cong intToModality prf) (modalityRoundTrip b)) |
| 117 | + |
| 118 | +-------------------------------------------------------------------------------- |
| 119 | +-- HarmSeverity: same guarantees (task (c)) |
| 120 | +-------------------------------------------------------------------------------- |
| 121 | + |
| 122 | +||| Decode a C integer back into a `HarmSeverity`. |
| 123 | +public export |
| 124 | +intToSeverity : Bits32 -> Maybe HarmSeverity |
| 125 | +intToSeverity x = |
| 126 | + if x == 0 then Just Negligible |
| 127 | + else if x == 1 then Just Minor |
| 128 | + else if x == 2 then Just Moderate |
| 129 | + else if x == 3 then Just Severe |
| 130 | + else if x == 4 then Just Critical |
| 131 | + else Nothing |
| 132 | + |
| 133 | +||| Round-trip / faithfulness for the harm-severity encoder. |
| 134 | +public export |
| 135 | +severityRoundTrip : (s : HarmSeverity) -> |
| 136 | + intToSeverity (severityToInt s) = Just s |
| 137 | +severityRoundTrip Negligible = Refl |
| 138 | +severityRoundTrip Minor = Refl |
| 139 | +severityRoundTrip Moderate = Refl |
| 140 | +severityRoundTrip Severe = Refl |
| 141 | +severityRoundTrip Critical = Refl |
| 142 | + |
| 143 | +||| Injectivity for the harm-severity encoder, derived from round-trip. |
| 144 | +public export |
| 145 | +severityToIntInjective : (a, b : HarmSeverity) -> |
| 146 | + severityToInt a = severityToInt b -> a = b |
| 147 | +severityToIntInjective a b prf = |
| 148 | + justInj $ |
| 149 | + trans (sym (severityRoundTrip a)) |
| 150 | + (trans (cong intToSeverity prf) (severityRoundTrip b)) |
| 151 | + |
| 152 | +-------------------------------------------------------------------------------- |
| 153 | +-- Positive controls (concrete decode = Refl, machine-checked) |
| 154 | +-------------------------------------------------------------------------------- |
| 155 | + |
| 156 | +||| Positive control: 0 decodes to Ok. |
| 157 | +public export |
| 158 | +decodeOkControl : FfiSeam.intToResult 0 = Just Ok |
| 159 | +decodeOkControl = Refl |
| 160 | + |
| 161 | +||| Positive control: 6 decodes to ConstraintConflict (the top code). |
| 162 | +public export |
| 163 | +decodeConflictControl : FfiSeam.intToResult 6 = Just ConstraintConflict |
| 164 | +decodeConflictControl = Refl |
| 165 | + |
| 166 | +||| Positive control: an out-of-range code decodes to Nothing. |
| 167 | +public export |
| 168 | +decodeUnknownControl : FfiSeam.intToResult 7 = Nothing |
| 169 | +decodeUnknownControl = Refl |
| 170 | + |
| 171 | +-------------------------------------------------------------------------------- |
| 172 | +-- Negative / non-vacuity controls (machine-checked) |
| 173 | +-------------------------------------------------------------------------------- |
| 174 | + |
| 175 | +||| NON-VACUITY: two DISTINCT result codes have DISTINCT C integers. |
| 176 | +||| If `resultToInt` were constant the seam would be vacuously "sound"; |
| 177 | +||| this rules that out. `Ok` -> 0, `Error` -> 1, and `0 = 1` is impossible |
| 178 | +||| for distinct primitive `Bits32` literals. |
| 179 | +public export |
| 180 | +okErrorDistinct : Not (resultToInt Ok = resultToInt Error) |
| 181 | +okErrorDistinct = \case Refl impossible |
| 182 | + |
| 183 | +||| Non-vacuity at the other end of the enum: the two highest codes differ. |
| 184 | +public export |
| 185 | +violationConflictDistinct : |
| 186 | + Not (resultToInt ConstraintViolation = resultToInt ConstraintConflict) |
| 187 | +violationConflictDistinct = \case Refl impossible |
| 188 | + |
| 189 | +||| Non-vacuity for the deontic-modality encoder. |
| 190 | +public export |
| 191 | +obligationProhibitionDistinct : |
| 192 | + Not (modalityToInt Obligation = modalityToInt Prohibition) |
| 193 | +obligationProhibitionDistinct = \case Refl impossible |
| 194 | + |
| 195 | +||| Non-vacuity for the harm-severity encoder. |
| 196 | +public export |
| 197 | +negligibleCriticalDistinct : |
| 198 | + Not (severityToInt Negligible = severityToInt Critical) |
| 199 | +negligibleCriticalDistinct = \case Refl impossible |
0 commit comments