|
| 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-3 deepening proof for k9iser: the CONJUNCTION-OF-CONTRACTS law. |
| 5 | +||| |
| 6 | +||| The Layer-2 flagship (`K9iser.ABI.Semantics`) proves the soundness and |
| 7 | +||| completeness of validating a config against a SINGLE K9 "must"-contract. |
| 8 | +||| This module proves a genuinely different, deeper, algebraic property of the |
| 9 | +||| SAME model: how validation behaves under CONJUNCTION of contracts. |
| 10 | +||| |
| 11 | +||| A config is a self-validating K9 contract; in practice a contract is a |
| 12 | +||| *bundle* of must-rules, so the central question is compositional: does |
| 13 | +||| validating a conjoined bundle decompose into validating each conjunct? |
| 14 | +||| |
| 15 | +||| Headline theorems (over the EXISTING `Validates` from Semantics): |
| 16 | +||| * `validatesAndIff` — a config validates `c1 AND c2` IFF it validates c1 |
| 17 | +||| AND validates c2 (both directions, as a real iso of |
| 18 | +||| witnesses, not a boolean tautology). |
| 19 | +||| * `validatesAllConsIff` — the n-ary version: validating `(c :: cs)` |
| 20 | +||| decomposes into the head and the tail. |
| 21 | +||| * `decValidatesAll` — a sound + COMPLETE decision for the n-ary form, |
| 22 | +||| built compositionally from the Layer-2 decision. |
| 23 | +||| * `validatesAllAppend` — DISTRIBUTION of conjunction over list append: |
| 24 | +||| validating `xs ++ ys` IFF validating xs and ys. |
| 25 | +||| * `validatesAllWeaken` — MONOTONICITY / downward-closure: a config that |
| 26 | +||| validates a bundle validates every sub-bundle |
| 27 | +||| reachable by dropping a contract (here: the tail). |
| 28 | +||| |
| 29 | +||| Plus a positive control (an inhabited witness for a concrete two-contract |
| 30 | +||| bundle) and a negative / non-vacuity control (`Not (...)`). |
| 31 | + |
| 32 | +module K9iser.ABI.Invariants |
| 33 | + |
| 34 | +import K9iser.ABI.Types |
| 35 | +import K9iser.ABI.Semantics |
| 36 | +import Data.Nat |
| 37 | +import Decidable.Equality |
| 38 | +import Decidable.Decidable |
| 39 | + |
| 40 | +%default total |
| 41 | + |
| 42 | +-------------------------------------------------------------------------------- |
| 43 | +-- Local helpers. The model's `resolve` keys on `String`, whose `decEq` does NOT |
| 44 | +-- reduce definitionally (idiom 5), so `resolve k cfg = Just v` cannot be proved |
| 45 | +-- by bare `Refl` outside the defining module. We instead prove the two resolver |
| 46 | +-- steps as genuine lemmas (`with (decEq ...)`), and build range witnesses by |
| 47 | +-- reducing the public `decInRange` (which DOES reduce on concrete Nats). |
| 48 | +-------------------------------------------------------------------------------- |
| 49 | + |
| 50 | +||| Constructor-headed injectivity of `Just` (idiom 3), to transport a resolved |
| 51 | +||| equation at the term level in the negative control. |
| 52 | +jInj : {0 x, y : a} -> Just x = Just y -> x = y |
| 53 | +jInj Refl = Refl |
| 54 | + |
| 55 | +||| The required key sits at the head: `resolve` returns its value. Proved by |
| 56 | +||| `with (decEq k k)`, discharging the impossible `No` branch with reflexivity. |
| 57 | +resolveHead : (k : Key) -> (v : Nat) -> (rest : Config) -> |
| 58 | + resolve k ((k, v) :: rest) = Just v |
| 59 | +resolveHead k v rest with (decEq k k) |
| 60 | + resolveHead k v rest | Yes _ = Refl |
| 61 | + resolveHead k v rest | No contra = absurd (contra Refl) |
| 62 | + |
| 63 | +||| A distinct head key is skipped: `resolve` recurses into the tail. Proved by |
| 64 | +||| `with (decEq k k')`, discharging the impossible `Yes` branch via the |
| 65 | +||| supplied disequality. |
| 66 | +resolveSkip : (k, k' : Key) -> Not (k = k') -> (v' : Nat) -> (rest : Config) -> |
| 67 | + resolve k ((k', v') :: rest) = resolve k rest |
| 68 | +resolveSkip k k' neq v' rest with (decEq k k') |
| 69 | + resolveSkip k k' neq v' rest | Yes eq = absurd (neq eq) |
| 70 | + resolveSkip k k' neq v' rest | No _ = Refl |
| 71 | + |
| 72 | +||| Build an `InRange` witness on CONCRETE operands by reducing the public |
| 73 | +||| `decInRange` (which reduces fully on concrete Nats, unlike a `with`-block on |
| 74 | +||| `String`). The `{auto}` `IsYes` proof forces the reduction to `Yes` at |
| 75 | +||| type-check time, so this is a genuine machine-checked witness — not proof |
| 76 | +||| search, not a postulate. |
| 77 | +inRangeWit : (low, high, val : Nat) -> |
| 78 | + {auto 0 ok : IsYes (decInRange low high val)} -> |
| 79 | + InRange low high val |
| 80 | +inRangeWit low high val {ok} with (decInRange low high val) |
| 81 | + inRangeWit low high val {ok = ItIsYes} | Yes prf = prf |
| 82 | + |
| 83 | +-------------------------------------------------------------------------------- |
| 84 | +-- Binary conjunction of contracts |
| 85 | +-------------------------------------------------------------------------------- |
| 86 | + |
| 87 | +||| Conjunction of two contract-validations against the SAME config. This is a |
| 88 | +||| genuine product of Layer-2 witnesses: to inhabit it you must independently |
| 89 | +||| prove the config validates c1 and validates c2. It is therefore strictly |
| 90 | +||| deeper than the single-contract `Validates`. |
| 91 | +public export |
| 92 | +data ValidatesBoth : (c1, c2 : Contract) -> (cfg : Config) -> Type where |
| 93 | + MkValidatesBoth : Validates c1 cfg -> |
| 94 | + Validates c2 cfg -> |
| 95 | + ValidatesBoth c1 c2 cfg |
| 96 | + |
| 97 | +||| HEADLINE (binary), forward: validating the conjunction gives both conjuncts. |
| 98 | +public export |
| 99 | +validatesAndFwd : {0 c1, c2 : Contract} -> {0 cfg : Config} -> |
| 100 | + ValidatesBoth c1 c2 cfg -> |
| 101 | + (Validates c1 cfg, Validates c2 cfg) |
| 102 | +validatesAndFwd (MkValidatesBoth p q) = (p, q) |
| 103 | + |
| 104 | +||| HEADLINE (binary), backward: both conjuncts give the conjunction. |
| 105 | +public export |
| 106 | +validatesAndBwd : {0 c1, c2 : Contract} -> {0 cfg : Config} -> |
| 107 | + (Validates c1 cfg, Validates c2 cfg) -> |
| 108 | + ValidatesBoth c1 c2 cfg |
| 109 | +validatesAndBwd (p, q) = MkValidatesBoth p q |
| 110 | + |
| 111 | +||| HEADLINE (binary), iso: the two directions compose to the identity on the |
| 112 | +||| pair, witnessing that `ValidatesBoth` is exactly the conjunction (no extra |
| 113 | +||| or missing information). This `= Refl` is a real round-trip equality. |
| 114 | +public export |
| 115 | +validatesAndIff : {0 c1, c2 : Contract} -> {0 cfg : Config} -> |
| 116 | + (pq : (Validates c1 cfg, Validates c2 cfg)) -> |
| 117 | + validatesAndFwd (validatesAndBwd pq) = pq |
| 118 | +validatesAndIff (p, q) = Refl |
| 119 | + |
| 120 | +||| COMMUTATIVITY of conjunction: order of conjuncts does not matter. |
| 121 | +public export |
| 122 | +validatesBothSym : {0 c1, c2 : Contract} -> {0 cfg : Config} -> |
| 123 | + ValidatesBoth c1 c2 cfg -> ValidatesBoth c2 c1 cfg |
| 124 | +validatesBothSym (MkValidatesBoth p q) = MkValidatesBoth q p |
| 125 | + |
| 126 | +-------------------------------------------------------------------------------- |
| 127 | +-- N-ary conjunction: validating a whole bundle of contracts |
| 128 | +-------------------------------------------------------------------------------- |
| 129 | + |
| 130 | +||| A config validates a BUNDLE (list) of contracts iff it validates every one. |
| 131 | +||| The empty bundle is vacuously validated; a cons requires the head to be |
| 132 | +||| validated and the tail to be validated. This is the inductive conjunction |
| 133 | +||| over the Layer-2 `Validates`. |
| 134 | +public export |
| 135 | +data ValidatesAll : (cfg : Config) -> (cs : List Contract) -> Type where |
| 136 | + ValidatesNil : ValidatesAll cfg [] |
| 137 | + ValidatesCons : Validates c cfg -> |
| 138 | + ValidatesAll cfg cs -> |
| 139 | + ValidatesAll cfg (c :: cs) |
| 140 | + |
| 141 | +||| HEADLINE (n-ary), forward: validating `(c :: cs)` gives the head and tail. |
| 142 | +public export |
| 143 | +validatesAllConsFwd : {0 c : Contract} -> {0 cs : List Contract} -> |
| 144 | + {0 cfg : Config} -> |
| 145 | + ValidatesAll cfg (c :: cs) -> |
| 146 | + (Validates c cfg, ValidatesAll cfg cs) |
| 147 | +validatesAllConsFwd (ValidatesCons h t) = (h, t) |
| 148 | + |
| 149 | +||| HEADLINE (n-ary), backward: head + tail rebuild the bundle validation. |
| 150 | +public export |
| 151 | +validatesAllConsBwd : {0 c : Contract} -> {0 cs : List Contract} -> |
| 152 | + {0 cfg : Config} -> |
| 153 | + (Validates c cfg, ValidatesAll cfg cs) -> |
| 154 | + ValidatesAll cfg (c :: cs) |
| 155 | +validatesAllConsBwd (h, t) = ValidatesCons h t |
| 156 | + |
| 157 | +||| HEADLINE (n-ary), iso: round-trip on the cons decomposition is the identity. |
| 158 | +public export |
| 159 | +validatesAllConsIff : {0 c : Contract} -> {0 cs : List Contract} -> |
| 160 | + {0 cfg : Config} -> |
| 161 | + (ht : (Validates c cfg, ValidatesAll cfg cs)) -> |
| 162 | + validatesAllConsFwd (validatesAllConsBwd ht) = ht |
| 163 | +validatesAllConsIff (h, t) = Refl |
| 164 | + |
| 165 | +-------------------------------------------------------------------------------- |
| 166 | +-- Sound + complete decision for the n-ary conjunction |
| 167 | +-------------------------------------------------------------------------------- |
| 168 | + |
| 169 | +||| There is no `ValidatesAll` witness for a cons whose HEAD fails to validate. |
| 170 | +||| Top-level refutation helper (idiom 2/3): peel the cons and discharge the |
| 171 | +||| head with the supplied refutation. |
| 172 | +headFails : {0 c : Contract} -> {0 cs : List Contract} -> {0 cfg : Config} -> |
| 173 | + Not (Validates c cfg) -> Not (ValidatesAll cfg (c :: cs)) |
| 174 | +headFails noH (ValidatesCons h _) = noH h |
| 175 | + |
| 176 | +||| There is no `ValidatesAll` witness for a cons whose TAIL fails to validate. |
| 177 | +tailFails : {0 c : Contract} -> {0 cs : List Contract} -> {0 cfg : Config} -> |
| 178 | + Not (ValidatesAll cfg cs) -> Not (ValidatesAll cfg (c :: cs)) |
| 179 | +tailFails noT (ValidatesCons _ t) = noT t |
| 180 | + |
| 181 | +||| Decide whether a config validates an entire bundle. Built COMPOSITIONALLY |
| 182 | +||| from the Layer-2 `decValidates`: this is the deeper structural result, since |
| 183 | +||| it threads the single-contract decision through induction on the bundle and |
| 184 | +||| stays both sound (a `Yes` is a real witness) and complete (a `No` is a real |
| 185 | +||| refutation) at every step. |
| 186 | +public export |
| 187 | +decValidatesAll : (cfg : Config) -> (cs : List Contract) -> |
| 188 | + Dec (ValidatesAll cfg cs) |
| 189 | +decValidatesAll cfg [] = Yes ValidatesNil |
| 190 | +decValidatesAll cfg (c :: cs) = case decValidates c cfg of |
| 191 | + No noH => No (headFails noH) |
| 192 | + Yes h => case decValidatesAll cfg cs of |
| 193 | + No noT => No (tailFails noT) |
| 194 | + Yes t => Yes (ValidatesCons h t) |
| 195 | + |
| 196 | +-------------------------------------------------------------------------------- |
| 197 | +-- Distribution of conjunction over list append |
| 198 | +-------------------------------------------------------------------------------- |
| 199 | + |
| 200 | +||| DISTRIBUTION (forward): validating `xs ++ ys` lets you split the validation |
| 201 | +||| into the xs-part and the ys-part. Proved by induction on xs. |
| 202 | +public export |
| 203 | +validatesAllAppendFwd : {0 cfg : Config} -> (xs : List Contract) -> |
| 204 | + {0 ys : List Contract} -> |
| 205 | + ValidatesAll cfg (xs ++ ys) -> |
| 206 | + (ValidatesAll cfg xs, ValidatesAll cfg ys) |
| 207 | +validatesAllAppendFwd [] vall = (ValidatesNil, vall) |
| 208 | +validatesAllAppendFwd (x :: xs) (ValidatesCons h t) = |
| 209 | + let (vxs, vys) = validatesAllAppendFwd xs t in |
| 210 | + (ValidatesCons h vxs, vys) |
| 211 | + |
| 212 | +||| DISTRIBUTION (backward): validations of xs and ys recombine into a |
| 213 | +||| validation of `xs ++ ys`. Proved by induction on the xs witness. |
| 214 | +public export |
| 215 | +validatesAllAppendBwd : {0 cfg : Config} -> |
| 216 | + {0 xs, ys : List Contract} -> |
| 217 | + ValidatesAll cfg xs -> ValidatesAll cfg ys -> |
| 218 | + ValidatesAll cfg (xs ++ ys) |
| 219 | +validatesAllAppendBwd ValidatesNil vys = vys |
| 220 | +validatesAllAppendBwd (ValidatesCons h t) vys = |
| 221 | + ValidatesCons h (validatesAllAppendBwd t vys) |
| 222 | + |
| 223 | +-------------------------------------------------------------------------------- |
| 224 | +-- Monotonicity / downward-closure |
| 225 | +-------------------------------------------------------------------------------- |
| 226 | + |
| 227 | +||| MONOTONICITY: validating a bundle implies validating its tail (dropping a |
| 228 | +||| conjunct can only weaken the requirement). This is the downward-closure |
| 229 | +||| direction of the conjunction lattice. |
| 230 | +public export |
| 231 | +validatesAllWeaken : {0 c : Contract} -> {0 cs : List Contract} -> |
| 232 | + {0 cfg : Config} -> |
| 233 | + ValidatesAll cfg (c :: cs) -> ValidatesAll cfg cs |
| 234 | +validatesAllWeaken (ValidatesCons _ t) = t |
| 235 | + |
| 236 | +-------------------------------------------------------------------------------- |
| 237 | +-- Certifier for the n-ary conjunction + soundness/completeness into Result |
| 238 | +-------------------------------------------------------------------------------- |
| 239 | + |
| 240 | +||| Certify a whole bundle into the ABI `Result` code: `Ok` iff every contract |
| 241 | +||| validates, `ConstraintViolation` otherwise. |
| 242 | +public export |
| 243 | +certifyAll : (cfg : Config) -> (cs : List Contract) -> Result |
| 244 | +certifyAll cfg cs = case decValidatesAll cfg cs of |
| 245 | + Yes _ => Ok |
| 246 | + No _ => ConstraintViolation |
| 247 | + |
| 248 | +||| Soundness: a bundle certified `Ok` really validates every contract. |
| 249 | +public export |
| 250 | +certifyAllSound : (cfg : Config) -> (cs : List Contract) -> |
| 251 | + certifyAll cfg cs = Ok -> ValidatesAll cfg cs |
| 252 | +certifyAllSound cfg cs prf with (decValidatesAll cfg cs) |
| 253 | + certifyAllSound cfg cs prf | Yes ok = ok |
| 254 | + certifyAllSound cfg cs Refl | No _ impossible |
| 255 | + |
| 256 | +||| Completeness: if a bundle validates, certification reports `Ok`. |
| 257 | +public export |
| 258 | +certifyAllComplete : (cfg : Config) -> (cs : List Contract) -> |
| 259 | + ValidatesAll cfg cs -> certifyAll cfg cs = Ok |
| 260 | +certifyAllComplete cfg cs vall with (decValidatesAll cfg cs) |
| 261 | + certifyAllComplete cfg cs vall | Yes _ = Refl |
| 262 | + certifyAllComplete cfg cs vall | No bad = absurd (bad vall) |
| 263 | + |
| 264 | +-------------------------------------------------------------------------------- |
| 265 | +-- Controls (non-vacuity) |
| 266 | +-------------------------------------------------------------------------------- |
| 267 | + |
| 268 | +||| First concrete contract over the SAME model: key "replicas" in [1, 10]. |
| 269 | +||| (A literal-key local copy of the Layer-2 contract so the `key` projection |
| 270 | +||| reduces inside this module; the bundle below mixes two distinct contracts.) |
| 271 | +public export |
| 272 | +replicasContract : Contract |
| 273 | +replicasContract = MkContract "replicas" 1 10 |
| 274 | + |
| 275 | +||| Second concrete contract over the SAME model: key "timeout" in [1, 30]. |
| 276 | +public export |
| 277 | +timeoutContract : Contract |
| 278 | +timeoutContract = MkContract "timeout" 1 30 |
| 279 | + |
| 280 | +||| A single config carrying both required keys, used for the controls. Each |
| 281 | +||| required key is placed so that `resolve` reduces on concrete data. |
| 282 | +public export |
| 283 | +twoConfig : Config |
| 284 | +twoConfig = [("replicas", 5), ("timeout", 20)] |
| 285 | + |
| 286 | +||| `"timeout"` and `"replicas"` are distinct keys (primitive String literals). |
| 287 | +neqTimeoutReplicas : Not ("timeout" = "replicas") |
| 288 | +neqTimeoutReplicas = \case Refl impossible |
| 289 | + |
| 290 | +||| Witness that `twoConfig` validates the replicas contract. The resolver |
| 291 | +||| equation comes from `resolveHead`; `inRangeWit` supplies the machine-checked |
| 292 | +||| range proof for 5 in [1, 10]. |
| 293 | +public export |
| 294 | +twoValidatesReplicas : Validates Invariants.replicasContract Invariants.twoConfig |
| 295 | +twoValidatesReplicas = |
| 296 | + MkValidates 5 (resolveHead "replicas" 5 [("timeout", 20)]) (inRangeWit 1 10 5) |
| 297 | + |
| 298 | +||| Witness that `twoConfig` validates the timeout contract. `resolve "timeout"` |
| 299 | +||| skips the head ("replicas", via `resolveSkip`) then matches ("timeout", via |
| 300 | +||| `resolveHead`); the two steps compose with `trans`. |
| 301 | +public export |
| 302 | +twoValidatesTimeout : Validates Invariants.timeoutContract Invariants.twoConfig |
| 303 | +twoValidatesTimeout = |
| 304 | + MkValidates 20 |
| 305 | + (trans (resolveSkip "timeout" "replicas" neqTimeoutReplicas 5 [("timeout", 20)]) |
| 306 | + (resolveHead "timeout" 20 [])) |
| 307 | + (inRangeWit 1 30 20) |
| 308 | + |
| 309 | +||| POSITIVE CONTROL (binary): `twoConfig` validates the conjunction. |
| 310 | +public export |
| 311 | +twoValidatesBoth : ValidatesBoth Invariants.replicasContract |
| 312 | + Invariants.timeoutContract Invariants.twoConfig |
| 313 | +twoValidatesBoth = MkValidatesBoth twoValidatesReplicas twoValidatesTimeout |
| 314 | + |
| 315 | +||| POSITIVE CONTROL (n-ary): `twoConfig` validates the BUNDLE |
| 316 | +||| `[replicasContract, timeoutContract]`. |
| 317 | +public export |
| 318 | +twoValidatesBundle : ValidatesAll Invariants.twoConfig |
| 319 | + [Invariants.replicasContract, Invariants.timeoutContract] |
| 320 | +twoValidatesBundle = |
| 321 | + ValidatesCons twoValidatesReplicas |
| 322 | + (ValidatesCons twoValidatesTimeout ValidatesNil) |
| 323 | + |
| 324 | +||| NEGATIVE / NON-VACUITY CONTROL: a config satisfying the replicas contract |
| 325 | +||| but NOT the timeout contract (timeout = 99 is out of [1, 30]) does NOT |
| 326 | +||| validate the conjunction. This rules out a vacuous `ValidatesAll`/`Both`. |
| 327 | +public export |
| 328 | +mixedConfig : Config |
| 329 | +mixedConfig = [("timeout", 99), ("replicas", 5)] |
| 330 | + |
| 331 | +||| `timeout = 99` is out of range [1, 30]. `resolveHead` pins |
| 332 | +||| `resolve "timeout" mixedConfig = Just 99`; composing with the witness's own |
| 333 | +||| equation and `jInj` yields `99 = v` (idiom 3); then `LTE v 30` becomes the |
| 334 | +||| uninhabited `LTE 99 30`. |
| 335 | +timeoutFailsMixed : Not (Validates Invariants.timeoutContract Invariants.mixedConfig) |
| 336 | +timeoutFailsMixed (MkValidates v prf (MkInRange _ hiOk)) = |
| 337 | + let resolved = the (resolve "timeout" Invariants.mixedConfig = Just 99) |
| 338 | + (resolveHead "timeout" 99 [("replicas", 5)]) |
| 339 | + vEq = the (99 = v) (jInj (trans (sym resolved) prf)) in |
| 340 | + absurd (replace {p = \w => LTE w 30} (sym vEq) hiOk) |
| 341 | + |
| 342 | +||| NEGATIVE CONTROL (binary): `mixedConfig` does not validate the conjunction, |
| 343 | +||| because the timeout conjunct fails. |
| 344 | +public export |
| 345 | +mixedNotBoth : Not (ValidatesBoth Invariants.replicasContract |
| 346 | + Invariants.timeoutContract Invariants.mixedConfig) |
| 347 | +mixedNotBoth (MkValidatesBoth _ vt) = timeoutFailsMixed vt |
| 348 | + |
| 349 | +||| NEGATIVE CONTROL (n-ary): `mixedConfig` does not validate the bundle |
| 350 | +||| `[replicasContract, timeoutContract]`, because the tail (timeout) fails. |
| 351 | +public export |
| 352 | +mixedNotBundle : Not (ValidatesAll Invariants.mixedConfig |
| 353 | + [Invariants.replicasContract, Invariants.timeoutContract]) |
| 354 | +mixedNotBundle (ValidatesCons _ (ValidatesCons vt _)) = timeoutFailsMixed vt |
0 commit comments