|
| 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 for Tlaiser: COMPOSITIONALITY of inductive invariants. |
| 5 | +||| |
| 6 | +||| The Layer-2 flagship (`Tlaiser.ABI.Semantics.safetySound`) proves INV1 for |
| 7 | +||| ONE hand-crafted inductive invariant (`Inv`) and derives mutual exclusion. |
| 8 | +||| That establishes soundness of a *single* invariant. This module proves a |
| 9 | +||| strictly deeper, structurally different fact about the *space of invariants* |
| 10 | +||| itself: |
| 11 | +||| |
| 12 | +||| THE CONJUNCTION OF TWO INDUCTIVE INVARIANTS IS INDUCTIVE. |
| 13 | +||| |
| 14 | +||| In TLA+ practice this is exactly how large invariants are built and how the |
| 15 | +||| TLAPS proof manager decomposes them: one proves several smaller invariants |
| 16 | +||| independently, then conjoins them, relying on the meta-fact that |
| 17 | +||| inductiveness is closed under conjunction. We formalise that meta-fact |
| 18 | +||| abstractly over the SAME state machine, relations and reachability already |
| 19 | +||| defined in `Semantics` (we do NOT redefine `St`, `Init`, `Step`, |
| 20 | +||| `Reachable`). |
| 21 | +||| |
| 22 | +||| Concretely this module: |
| 23 | +||| 1. Abstracts "P is an inductive invariant" as `IsInductive P` |
| 24 | +||| (established by `Init`, preserved by `Step`) — the two INV1 premises, |
| 25 | +||| packaged so they can be manipulated as first-class values. |
| 26 | +||| 2. Proves the generic INV1 closure `inductiveOnReachable` once, for ANY |
| 27 | +||| inductive P (the Layer-2 proof was monomorphic in `Inv`). |
| 28 | +||| 3. Proves the COMPOSITION theorem `andInductive`: if `P` and `Q` are each |
| 29 | +||| inductive then `\s => (P s, Q s)` is inductive. This is the headline, |
| 30 | +||| and it is genuinely distinct from — and used to *strengthen* — the |
| 31 | +||| Layer-2 result. |
| 32 | +||| 4. Exhibits a SECOND, independent concrete invariant `HolderCrit` |
| 33 | +||| ("if a process holds the lock then that process is Critical"), proves |
| 34 | +||| it inductive from scratch, and shows it is NOT the same predicate as |
| 35 | +||| `Inv` (a reachable state distinguishes the *strength* of the two; and |
| 36 | +||| a non-reachable state inhabits one predicate but not the other). |
| 37 | +||| 5. COMPOSES `Inv` and `HolderCrit` via `andInductive` into a single |
| 38 | +||| conjoined inductive invariant that therefore holds on every reachable |
| 39 | +||| state — demonstrating modular invariant construction end to end. |
| 40 | +||| 6. Provides a sound+complete decision procedure for the second invariant, |
| 41 | +||| a positive control (an inhabited reachable witness of the conjunction) |
| 42 | +||| and a negative / non-vacuity control (a machine-checked `Not (...)`). |
| 43 | +||| |
| 44 | +||| Everything is built over the Layer-2 datatypes; no axioms, no `believe_me`, |
| 45 | +||| no `postulate`, no `assert_total`. |
| 46 | +||| |
| 47 | +||| @see https://lamport.azurewebsites.net/tla/tla.html (INV1, invariant conj.) |
| 48 | + |
| 49 | +module Tlaiser.ABI.Invariants |
| 50 | + |
| 51 | +import Tlaiser.ABI.Types |
| 52 | +import Tlaiser.ABI.Semantics |
| 53 | + |
| 54 | +%default total |
| 55 | + |
| 56 | +-------------------------------------------------------------------------------- |
| 57 | +-- 1. "Inductive invariant", abstracted as a first-class value |
| 58 | +-------------------------------------------------------------------------------- |
| 59 | + |
| 60 | +||| A state predicate over the Layer-2 machine. |
| 61 | +public export |
| 62 | +0 Pred : Type |
| 63 | +Pred = St -> Type |
| 64 | + |
| 65 | +||| `IsInductive p` packages the two INV1 premises for an arbitrary predicate |
| 66 | +||| `p`, over the EXISTING `Init` and `Step` relations from `Semantics`: |
| 67 | +||| * `establishes` : `p` holds on every initial state; |
| 68 | +||| * `preserves` : every `Step` from a `p`-state lands in a `p`-state. |
| 69 | +||| Making this a record lets us treat inductiveness as data we can combine |
| 70 | +||| (e.g. conjoin) — the move that distinguishes this layer from the Layer-2 |
| 71 | +||| monomorphic proof. |
| 72 | +public export |
| 73 | +record IsInductive (0 p : Pred) where |
| 74 | + constructor MkIsInductive |
| 75 | + establishes : {0 s : St} -> Init s -> p s |
| 76 | + preserves : {0 s, t : St} -> p s -> Step s t -> p t |
| 77 | + |
| 78 | +-------------------------------------------------------------------------------- |
| 79 | +-- 2. Generic INV1: any inductive predicate holds on all reachable states |
| 80 | +-------------------------------------------------------------------------------- |
| 81 | + |
| 82 | +||| INV1, proved ONCE for an arbitrary inductive predicate (the Layer-2 version, |
| 83 | +||| `invInductive`, was specialised to the single predicate `Inv`). Induction on |
| 84 | +||| the `Reachable` derivation, discharging each case with the packaged premises. |
| 85 | +public export |
| 86 | +inductiveOnReachable : {0 p : Pred} -> IsInductive p -> |
| 87 | + {0 s : St} -> Reachable s -> p s |
| 88 | +inductiveOnReachable ind (ReachInit init) = ind.establishes init |
| 89 | +inductiveOnReachable ind (ReachStep r step) = |
| 90 | + ind.preserves (inductiveOnReachable ind r) step |
| 91 | + |
| 92 | +-------------------------------------------------------------------------------- |
| 93 | +-- 3. HEADLINE: inductiveness is closed under conjunction |
| 94 | +-------------------------------------------------------------------------------- |
| 95 | + |
| 96 | +||| The conjunction of two predicates, as a predicate. |
| 97 | +public export |
| 98 | +0 And : Pred -> Pred -> Pred |
| 99 | +And p q = \s => (p s, q s) |
| 100 | + |
| 101 | +||| THE COMPOSITION THEOREM. If `p` and `q` are each inductive invariants of the |
| 102 | +||| machine, then so is their conjunction `And p q`. Each INV1 premise of the |
| 103 | +||| conjunction is discharged componentwise from the premises of `p` and `q`. |
| 104 | +||| This is the meta-theorem that licenses modular invariant construction. |
| 105 | +public export |
| 106 | +andInductive : {0 p, q : Pred} -> |
| 107 | + IsInductive p -> IsInductive q -> IsInductive (And p q) |
| 108 | +andInductive ip iq = |
| 109 | + MkIsInductive |
| 110 | + (\init => (ip.establishes init, iq.establishes init)) |
| 111 | + (\(hp, hq), step => (ip.preserves hp step, iq.preserves hq step)) |
| 112 | + |
| 113 | +-------------------------------------------------------------------------------- |
| 114 | +-- 4. The Layer-2 invariant `Inv`, repackaged as a first-class IsInductive value |
| 115 | +-------------------------------------------------------------------------------- |
| 116 | + |
| 117 | +||| `Inv` from Layer-2 is inductive — we already have the two discharged |
| 118 | +||| premises (`initEstablishes`, `stepPreserves`); here we merely repackage them |
| 119 | +||| as an `IsInductive` value so the composition machinery can consume it. |
| 120 | +public export |
| 121 | +invIsInductive : IsInductive Inv |
| 122 | +invIsInductive = MkIsInductive initEstablishes stepPreserves |
| 123 | + |
| 124 | +-------------------------------------------------------------------------------- |
| 125 | +-- 5. A SECOND, INDEPENDENT concrete invariant: "holder is Critical" |
| 126 | +-------------------------------------------------------------------------------- |
| 127 | + |
| 128 | +||| Per-process: "if process 0 holds the lock (`HeldBy0`) then it is Critical". |
| 129 | +||| Indexed by P0's location and the lock. This couples the lock to the holder's |
| 130 | +||| location in the *opposite* direction from `Coherent0`'s emphasis: it is the |
| 131 | +||| "held => critical" half, whereas `Inv`'s coherence carries "critical => |
| 132 | +||| held" plus the cross-process exclusion. Distinct content, proved afresh. |
| 133 | +public export |
| 134 | +data Holder0 : Loc -> Lock -> Type where |
| 135 | + ||| Lock held by P0 forces P0 = Critical. |
| 136 | + H0Crit : Holder0 Critical HeldBy0 |
| 137 | + ||| Lock not held by P0: no constraint on P0's location. |
| 138 | + H0Free : Holder0 a Free |
| 139 | + H0Other : Holder0 a HeldBy1 |
| 140 | + |
| 141 | +||| Per-process mirror for P1: "if P1 holds the lock then P1 is Critical". |
| 142 | +public export |
| 143 | +data Holder1 : Loc -> Lock -> Type where |
| 144 | + H1Crit : Holder1 Critical HeldBy1 |
| 145 | + H1Free : Holder1 a Free |
| 146 | + H1Other : Holder1 a HeldBy0 |
| 147 | + |
| 148 | +||| The SECOND whole-state invariant: whichever process holds the single lock |
| 149 | +||| token is in its critical section. Genuinely different predicate from `Inv`. |
| 150 | +public export |
| 151 | +data HolderCrit : St -> Type where |
| 152 | + MkHolderCrit : Holder0 a lk -> Holder1 b lk -> HolderCrit (MkSt a b lk) |
| 153 | + |
| 154 | +-- (5a) `HolderCrit` holds on the initial state --------------------------------- |
| 155 | + |
| 156 | +||| Established by `Init`: the unique initial state `(Idle, Idle, Free)` has the |
| 157 | +||| lock free, so both holder constraints hold vacuously. |
| 158 | +public export |
| 159 | +holderInit : {0 s : St} -> Init s -> HolderCrit s |
| 160 | +holderInit InitState = MkHolderCrit H0Free H1Free |
| 161 | + |
| 162 | +-- (5b) `HolderCrit` is preserved by every `Step` ------------------------------ |
| 163 | +-- Helper lemmas keep each Step clause first-order and total. |
| 164 | + |
| 165 | +||| A request transition keeps the lock unchanged; a `Holder0` fact transports |
| 166 | +||| across any location change because for a *fixed* lock it is determined only |
| 167 | +||| by the lock except in the `HeldBy0` case, and a requesting/releasing process |
| 168 | +||| in the relevant clauses is never the `HeldBy0`/Critical witness we must keep. |
| 169 | +||| We discharge preservation directly per Step constructor below instead, which |
| 170 | +||| is clearer than generic transport; these tiny lemmas cover the lock-retag |
| 171 | +||| cases (acquire/release) where the holder's own slot is fixed. |
| 172 | + |
| 173 | +||| When P1 acquires (lock Free -> HeldBy1), P0's holder fact retags Free->HeldBy1. |
| 174 | +holder0FreeToHB1 : Holder0 a Free -> Holder0 a HeldBy1 |
| 175 | +holder0FreeToHB1 H0Free = H0Other |
| 176 | + |
| 177 | +||| When P0 acquires (lock Free -> HeldBy0), P1's holder fact retags Free->HeldBy0. |
| 178 | +holder1FreeToHB0 : Holder1 b Free -> Holder1 b HeldBy0 |
| 179 | +holder1FreeToHB0 H1Free = H1Other |
| 180 | + |
| 181 | +||| When P0 releases (lock HeldBy0 -> Free), P1's holder fact retags HeldBy0->Free. |
| 182 | +holder1HB0ToFree : Holder1 b HeldBy0 -> Holder1 b Free |
| 183 | +holder1HB0ToFree H1Other = H1Free |
| 184 | + |
| 185 | +||| When P1 releases (lock HeldBy1 -> Free), P0's holder fact retags HeldBy1->Free. |
| 186 | +holder0HB1ToFree : Holder0 a HeldBy1 -> Holder0 a Free |
| 187 | +holder0HB1ToFree H0Other = H0Free |
| 188 | + |
| 189 | +||| Preservation of the second invariant under every transition, by case |
| 190 | +||| analysis on `Step`. Acquire creates the `Critical`/`HeldBy_i` pairing |
| 191 | +||| (`H?Crit`); release returns the lock to `Free` (`H?Free`); request leaves |
| 192 | +||| the lock untouched so the (lock-determined) constraint on the *moving* |
| 193 | +||| process is re-derived and the other process's fact is unchanged. |
| 194 | +public export |
| 195 | +holderStep : {0 s, t : St} -> HolderCrit s -> Step s t -> HolderCrit t |
| 196 | +-- P0 Idle->Waiting, lock l unchanged. P0's constraint for any held-by-other / free |
| 197 | +-- lock is unconstrained (H0Free / H0Other); if l were HeldBy0 the source would need |
| 198 | +-- Holder0 Idle HeldBy0, which is uninhabited, so that case cannot arise. |
| 199 | +holderStep (MkHolderCrit H0Free h1) P0Request = MkHolderCrit H0Free h1 |
| 200 | +holderStep (MkHolderCrit H0Other h1) P0Request = MkHolderCrit H0Other h1 |
| 201 | +-- P1 Idle->Waiting, lock unchanged. Symmetric. |
| 202 | +holderStep (MkHolderCrit h0 H1Free) P1Request = MkHolderCrit h0 H1Free |
| 203 | +holderStep (MkHolderCrit h0 H1Other) P1Request = MkHolderCrit h0 H1Other |
| 204 | +-- P0 acquires: Waiting->Critical, lock Free->HeldBy0. P0 becomes the holder and |
| 205 | +-- is Critical (H0Crit); retag P1's free fact to HeldBy0. |
| 206 | +holderStep (MkHolderCrit _ h1) P0Acquire = MkHolderCrit H0Crit (holder1FreeToHB0 h1) |
| 207 | +-- P1 acquires: symmetric. |
| 208 | +holderStep (MkHolderCrit h0 _) P1Acquire = MkHolderCrit (holder0FreeToHB1 h0) H1Crit |
| 209 | +-- P0 releases: Critical->Idle, lock HeldBy0->Free. P0 no longer holds (H0Free); |
| 210 | +-- retag P1's HeldBy0 fact to Free. |
| 211 | +holderStep (MkHolderCrit _ h1) P0Release = MkHolderCrit H0Free (holder1HB0ToFree h1) |
| 212 | +-- P1 releases: symmetric. |
| 213 | +holderStep (MkHolderCrit h0 _) P1Release = MkHolderCrit (holder0HB1ToFree h0) H1Free |
| 214 | + |
| 215 | +||| The second invariant, packaged as a first-class `IsInductive` value. |
| 216 | +public export |
| 217 | +holderIsInductive : IsInductive HolderCrit |
| 218 | +holderIsInductive = MkIsInductive holderInit holderStep |
| 219 | + |
| 220 | +-------------------------------------------------------------------------------- |
| 221 | +-- 6. COMPOSE the two invariants and reap the conjoined result on reachable states |
| 222 | +-------------------------------------------------------------------------------- |
| 223 | + |
| 224 | +||| The conjoined invariant `Inv ∧ HolderCrit`, proved inductive purely by |
| 225 | +||| feeding the two component proofs through the composition theorem. No new |
| 226 | +||| case analysis: this is the payoff of `andInductive`. |
| 227 | +public export |
| 228 | +combinedIsInductive : IsInductive (And Inv HolderCrit) |
| 229 | +combinedIsInductive = andInductive invIsInductive holderIsInductive |
| 230 | + |
| 231 | +||| Headline Layer-3 corollary: the CONJUNCTION of the two independently-proved |
| 232 | +||| invariants holds on EVERY reachable state — obtained generically, not by |
| 233 | +||| re-running an induction over `Reachable`. |
| 234 | +public export |
| 235 | +combinedOnReachable : {0 s : St} -> Reachable s -> (Inv s, HolderCrit s) |
| 236 | +combinedOnReachable r = inductiveOnReachable combinedIsInductive r |
| 237 | + |
| 238 | +-------------------------------------------------------------------------------- |
| 239 | +-- 7. Decision procedure for the SECOND invariant (sound + complete) |
| 240 | +-------------------------------------------------------------------------------- |
| 241 | + |
| 242 | +||| `Holder0` is decidable for a concrete location/lock pair. |
| 243 | +decHolder0 : (a : Loc) -> (lk : Lock) -> Dec (Holder0 a lk) |
| 244 | +decHolder0 _ Free = Yes H0Free |
| 245 | +decHolder0 _ HeldBy1 = Yes H0Other |
| 246 | +decHolder0 Critical HeldBy0 = Yes H0Crit |
| 247 | +decHolder0 Idle HeldBy0 = No (\case H0Crit impossible) |
| 248 | +decHolder0 Waiting HeldBy0 = No (\case H0Crit impossible) |
| 249 | + |
| 250 | +||| `Holder1` is decidable for a concrete location/lock pair. |
| 251 | +decHolder1 : (b : Loc) -> (lk : Lock) -> Dec (Holder1 b lk) |
| 252 | +decHolder1 _ Free = Yes H1Free |
| 253 | +decHolder1 _ HeldBy0 = Yes H1Other |
| 254 | +decHolder1 Critical HeldBy1 = Yes H1Crit |
| 255 | +decHolder1 Idle HeldBy1 = No (\case H1Crit impossible) |
| 256 | +decHolder1 Waiting HeldBy1 = No (\case H1Crit impossible) |
| 257 | + |
| 258 | +||| Recover the P0-component from a whole-state `HolderCrit`. |
| 259 | +holderProj0 : HolderCrit (MkSt a b lk) -> Holder0 a lk |
| 260 | +holderProj0 (MkHolderCrit h0 _) = h0 |
| 261 | + |
| 262 | +||| Recover the P1-component from a whole-state `HolderCrit`. |
| 263 | +holderProj1 : HolderCrit (MkSt a b lk) -> Holder1 b lk |
| 264 | +holderProj1 (MkHolderCrit _ h1) = h1 |
| 265 | + |
| 266 | +||| Sound AND complete decision of the second invariant for any state. |
| 267 | +public export |
| 268 | +decHolderCrit : (s : St) -> Dec (HolderCrit s) |
| 269 | +decHolderCrit (MkSt a b lk) = |
| 270 | + case decHolder0 a lk of |
| 271 | + No notH0 => No (\hc => notH0 (holderProj0 hc)) |
| 272 | + Yes h0 => case decHolder1 b lk of |
| 273 | + No notH1 => No (\hc => notH1 (holderProj1 hc)) |
| 274 | + Yes h1 => Yes (MkHolderCrit h0 h1) |
| 275 | + |
| 276 | +||| Certifier for the second invariant, reusing the existing `Result` ABI |
| 277 | +||| vocabulary: `Ok` iff the state satisfies `HolderCrit`, else `TlcError`. |
| 278 | +public export |
| 279 | +certifyHolder : (s : St) -> Result |
| 280 | +certifyHolder s = case decHolderCrit s of |
| 281 | + Yes _ => Ok |
| 282 | + No _ => TlcError |
| 283 | + |
| 284 | +||| Soundness of that certifier on reachable states: every reachable state |
| 285 | +||| satisfies `HolderCrit` (it is a conjunct of `combinedOnReachable`), so it |
| 286 | +||| certifies `Ok`. Ties decision + reachability + composition together. |
| 287 | +public export |
| 288 | +certifyHolderReachableSound : (s : St) -> Reachable s -> certifyHolder s = Ok |
| 289 | +certifyHolderReachableSound s r with (decHolderCrit s) |
| 290 | + _ | Yes _ = Refl |
| 291 | + _ | No notHC = absurd (notHC (snd (combinedOnReachable r))) |
| 292 | + |
| 293 | +-------------------------------------------------------------------------------- |
| 294 | +-- 8. POSITIVE control: an inhabited reachable witness of the CONJUNCTION |
| 295 | +-------------------------------------------------------------------------------- |
| 296 | + |
| 297 | +||| POSITIVE control: the concrete reachable state `(Critical, Idle, HeldBy0)` |
| 298 | +||| (reached in `Semantics.reachP0Critical`) satisfies BOTH invariants at once — |
| 299 | +||| obtained through the composed inductive proof, exercising the whole pipeline. |
| 300 | +public export |
| 301 | +positiveControl : (Inv (MkSt Critical Idle HeldBy0), HolderCrit (MkSt Critical Idle HeldBy0)) |
| 302 | +positiveControl = combinedOnReachable reachP0Critical |
| 303 | + |
| 304 | +||| POSITIVE control, second face: the second invariant decides `Yes` on that |
| 305 | +||| same concrete state (the decision procedure agrees with the proof). |
| 306 | +public export |
| 307 | +positiveControlDecided : certifyHolder (MkSt Critical Idle HeldBy0) = Ok |
| 308 | +positiveControlDecided = Refl |
| 309 | + |
| 310 | +-------------------------------------------------------------------------------- |
| 311 | +-- 9. NEGATIVE / NON-VACUITY controls |
| 312 | +-------------------------------------------------------------------------------- |
| 313 | + |
| 314 | +||| NEGATIVE control #1 (the second invariant is non-trivial): the state |
| 315 | +||| `(Idle, Idle, HeldBy0)` — lock held by P0 yet P0 is Idle, NOT Critical — |
| 316 | +||| violates `HolderCrit`. Machine-checked `Not (...)`. This is the witness that |
| 317 | +||| `HolderCrit` actually constrains states (it is not all-inhabited), so the |
| 318 | +||| inductiveness result above is non-vacuous. |
| 319 | +||| There is no `Holder0 Idle HeldBy0` proof (no constructor pairs Idle with |
| 320 | +||| HeldBy0). Stated as a refuting helper with a top-level impossible clause. |
| 321 | +holder0IdleHB0Absurd : Not (Holder0 Idle HeldBy0) |
| 322 | +holder0IdleHB0Absurd H0Crit impossible |
| 323 | + |
| 324 | +public export |
| 325 | +negativeControl : Not (HolderCrit (MkSt Idle Idle HeldBy0)) |
| 326 | +negativeControl (MkHolderCrit h0 _) = holder0IdleHB0Absurd h0 |
| 327 | + |
| 328 | +||| NEGATIVE / DISTINCTNESS control #2 (the two invariants are GENUINELY |
| 329 | +||| DIFFERENT predicates — the second is not a restatement of `Inv`): we exhibit |
| 330 | +||| a single state that SATISFIES the second invariant `HolderCrit` yet VIOLATES |
| 331 | +||| the Layer-2 invariant `Inv`. Such a separating witness proves the two |
| 332 | +||| predicates are not equal as `St -> Type` (HolderCrit does NOT imply Inv), so |
| 333 | +||| this layer adds new, non-derivable content. |
| 334 | +||| |
| 335 | +||| Witness: `(Critical, Critical, Free)`. With the lock Free, neither process |
| 336 | +||| is the holder, so both `Holder0`/`Holder1` constraints hold vacuously |
| 337 | +||| (`H0Free`/`H1Free`) and `HolderCrit` is inhabited. But `Inv` requires both |
| 338 | +||| processes coherent with the (Free) lock, and `Coherent0 Critical Free` is |
| 339 | +||| uninhabited (Critical demands `HeldBy0`), so `Inv` fails. (Note: this state |
| 340 | +||| is mutual-exclusion-violating and is precisely UNREACHABLE — see |
| 341 | +||| `Semantics.badStateUnreachable` — which is *why* the strictly weaker |
| 342 | +||| `HolderCrit` cannot on its own imply mutual exclusion: it needs `Inv` as the |
| 343 | +||| second conjunct. That is the whole point of composing them.) |
| 344 | +public export |
| 345 | +holderHoldsButInvFails : |
| 346 | + (HolderCrit (MkSt Critical Critical Free), Not (Inv (MkSt Critical Critical Free))) |
| 347 | +holderHoldsButInvFails = |
| 348 | + ( MkHolderCrit H0Free H1Free |
| 349 | + , invCriticalFreeAbsurd |
| 350 | + ) |
| 351 | + where |
| 352 | + ||| `Inv (Critical, Critical, Free)` is uninhabited: its P0 component would |
| 353 | + ||| be `Coherent0 Critical Free`, which has no constructor. |
| 354 | + invCriticalFreeAbsurd : Not (Inv (MkSt Critical Critical Free)) |
| 355 | + invCriticalFreeAbsurd (MkInv c0 _) = case c0 of |
| 356 | + C0CritHolds impossible |
0 commit comments