|
| 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 | +||| Structural invariants for Affinescriptiser (ABI Layer 3). |
| 5 | +||| |
| 6 | +||| This module proves a SECOND, deeper property over the SAME affine-usage |
| 7 | +||| model defined in `Affinescriptiser.ABI.Semantics` (the `Trace`/`AffineOk` |
| 8 | +||| datatypes are imported, not redefined). Where Layer 2 establishes a |
| 9 | +||| sound+complete *checker* for a single trace ("no variable used twice"), |
| 10 | +||| Layer 3 establishes an *algebraic composition law* for that checker: |
| 11 | +||| |
| 12 | +||| COMPOSITION (the headline Layer-3 theorem): |
| 13 | +||| If two usage traces are each affine-safe AND their variable sets are |
| 14 | +||| disjoint, then their concatenation is affine-safe. Formally |
| 15 | +||| Disjoint t1 t2 -> AffineOk t1 -> AffineOk t2 -> AffineOk (t1 ++ t2) |
| 16 | +||| This is the soundness justification for the codegen *sequencing* two |
| 17 | +||| already-checked program fragments over disjoint locals into one fragment |
| 18 | +||| without re-running the whole affine check. |
| 19 | +||| |
| 20 | +||| DECOMPOSITION (the converse structural law): |
| 21 | +||| Affine-safety is preserved under taking the *suffix* of a concatenation: |
| 22 | +||| AffineOk (t1 ++ t2) -> AffineOk t2 |
| 23 | +||| i.e. dropping a safe prefix can never break affine-safety of the rest. |
| 24 | +||| |
| 25 | +||| Both directions are genuine, machine-checked, total proofs. Disjointness is |
| 26 | +||| given a sound+complete decision procedure (`decDisjoint`). We supply a |
| 27 | +||| POSITIVE control (a concrete inhabited composition witness) and TWO NEGATIVE |
| 28 | +||| controls: a `Not` showing overlapping traces compose to an unsafe trace, and |
| 29 | +||| a `Not` showing a wide-gap double-use trace is rejected. |
| 30 | +||| |
| 31 | +||| Quality bar (identical to Layer 2): no believe_me, no idris_crash, no |
| 32 | +||| assert_total, no postulate, no sorry, no %hint hacks, no asserted equalities. |
| 33 | + |
| 34 | +module Affinescriptiser.ABI.Invariants |
| 35 | + |
| 36 | +import Affinescriptiser.ABI.Types |
| 37 | +import Affinescriptiser.ABI.Semantics |
| 38 | +import Data.List.Elem |
| 39 | +import Decidable.Equality |
| 40 | + |
| 41 | +%default total |
| 42 | + |
| 43 | +-------------------------------------------------------------------------------- |
| 44 | +-- Membership-over-append lemmas (the technical core) |
| 45 | +-------------------------------------------------------------------------------- |
| 46 | + |
| 47 | +||| If `x` occurs in `xs ++ ys` then it occurs in `xs` or in `ys`. |
| 48 | +||| Proved by induction on `xs`. `Here`/`There` are constructor-headed, so the |
| 49 | +||| recursive case-splits reduce cleanly with no stuck `case ... of Refl`. |
| 50 | +export |
| 51 | +elemAppSplit : {0 x : Var} -> (xs : Trace) -> Elem x (xs ++ ys) -> Either (Elem x xs) (Elem x ys) |
| 52 | +elemAppSplit [] el = Right el |
| 53 | +elemAppSplit (y :: ys) Here = Left Here |
| 54 | +elemAppSplit (y :: ys) (There el) = |
| 55 | + case elemAppSplit ys el of |
| 56 | + Left inXs => Left (There inXs) |
| 57 | + Right inYs => Right inYs |
| 58 | + |
| 59 | +||| Conversely, membership in `ys` injects into membership in `xs ++ ys`. |
| 60 | +||| Induction on `xs` again; each step is just one `There`. |
| 61 | +export |
| 62 | +elemAppRight : {0 x : Var} -> (xs : Trace) -> Elem x ys -> Elem x (xs ++ ys) |
| 63 | +elemAppRight [] el = el |
| 64 | +elemAppRight (y :: ys) el = There (elemAppRight ys el) |
| 65 | + |
| 66 | +||| If `x` is in neither `xs` nor `ys`, it is not in `xs ++ ys`. |
| 67 | +||| This is the contrapositive of `elemAppSplit`, packaged for direct use. |
| 68 | +export |
| 69 | +notElemApp : {0 x : Var} |
| 70 | + -> (xs : Trace) |
| 71 | + -> Not (Elem x xs) |
| 72 | + -> Not (Elem x ys) |
| 73 | + -> Not (Elem x (xs ++ ys)) |
| 74 | +notElemApp xs nxs nys el = |
| 75 | + case elemAppSplit xs el of |
| 76 | + Left inXs => nxs inXs |
| 77 | + Right inYs => nys inYs |
| 78 | + |
| 79 | +-------------------------------------------------------------------------------- |
| 80 | +-- Disjointness of variable sets |
| 81 | +-------------------------------------------------------------------------------- |
| 82 | + |
| 83 | +||| Two traces are `Disjoint` when no variable appears in both. This is the |
| 84 | +||| precondition under which sequencing two affine fragments stays affine: each |
| 85 | +||| fragment may reuse names internally only if those names do not collide. |
| 86 | +public export |
| 87 | +Disjoint : Trace -> Trace -> Type |
| 88 | +Disjoint xs ys = (v : Var) -> Elem v xs -> Not (Elem v ys) |
| 89 | + |
| 90 | +||| The empty trace is disjoint from anything (vacuously: nothing is in `[]`). |
| 91 | +export |
| 92 | +disjointNilLeft : (ys : Trace) -> Disjoint [] ys |
| 93 | +disjointNilLeft ys v el = absurd el |
| 94 | + |
| 95 | +||| Drop the head of the left trace and disjointness is preserved. |
| 96 | +export |
| 97 | +disjointTail : {0 x : Var} -> Disjoint (x :: xs) ys -> Disjoint xs ys |
| 98 | +disjointTail dj v inXs = dj v (There inXs) |
| 99 | + |
| 100 | +||| The head of a disjoint left trace is not present in the right trace. |
| 101 | +||| `x` is bound explicitly (quantity 1) because we use it at the term level. |
| 102 | +export |
| 103 | +disjointHeadNotRight : {x : Var} -> Disjoint (x :: xs) ys -> Not (Elem x ys) |
| 104 | +disjointHeadNotRight dj = dj x Here |
| 105 | + |
| 106 | +||| Build a disjointness witness by consing onto the left trace: if the new |
| 107 | +||| head `x` is absent from `ys` and `xs` is already disjoint from `ys`, then |
| 108 | +||| `x :: xs` is disjoint from `ys`. Matching on the LEFT membership refines the |
| 109 | +||| variable cleanly (avoiding the Nat-literal coverage pitfalls). |
| 110 | +export |
| 111 | +consDisjoint : {0 x : Var} -> Not (Elem x ys) -> Disjoint xs ys -> Disjoint (x :: xs) ys |
| 112 | +consDisjoint nx _ v Here = nx |
| 113 | +consDisjoint _ dxs v (There inXs) = dxs v inXs |
| 114 | + |
| 115 | +-------------------------------------------------------------------------------- |
| 116 | +-- Sound + complete decision procedure for disjointness |
| 117 | +-------------------------------------------------------------------------------- |
| 118 | + |
| 119 | +||| Decide whether two traces are disjoint. A `Yes` carries a real witness |
| 120 | +||| (a function refuting any shared membership), a `No` carries a real overlap. |
| 121 | +||| Built only from `isElem` (Data.List.Elem) and the structural lemmas above. |
| 122 | +public export |
| 123 | +decDisjoint : (xs : Trace) -> (ys : Trace) -> Dec (Disjoint xs ys) |
| 124 | +decDisjoint [] ys = Yes (disjointNilLeft ys) |
| 125 | +decDisjoint (x :: xs) ys = |
| 126 | + case isElem x ys of |
| 127 | + -- x is shared: the pair cannot be disjoint, since x is in both sides. |
| 128 | + Yes xInYs => No (\dj => disjointHeadNotRight dj xInYs) |
| 129 | + No xNotYs => |
| 130 | + case decDisjoint xs ys of |
| 131 | + Yes djRest => |
| 132 | + Yes (\v, inHead => |
| 133 | + case inHead of |
| 134 | + Here => xNotYs |
| 135 | + There inXs => djRest v inXs) |
| 136 | + No notRest => |
| 137 | + No (\dj => notRest (disjointTail dj)) |
| 138 | + |
| 139 | +-------------------------------------------------------------------------------- |
| 140 | +-- HEADLINE Layer-3 theorem: composition of affine traces over disjoint vars |
| 141 | +-------------------------------------------------------------------------------- |
| 142 | + |
| 143 | +||| COMPOSITION. Sequencing two affine-safe traces whose variable sets are |
| 144 | +||| disjoint yields an affine-safe trace. Proved by induction on the structure |
| 145 | +||| of the first trace via its `AffineOk` witness: |
| 146 | +||| |
| 147 | +||| * `[] ++ t2` reduces to `t2`, whose safety we already have. |
| 148 | +||| * `(v :: vs) ++ t2` reduces to `v :: (vs ++ t2)`. The new `notLater` |
| 149 | +||| obligation — `v` not in `vs ++ t2` — is discharged by `notElemApp`: |
| 150 | +||| `v` is absent from `vs` (head of t1's witness) and absent from `t2` |
| 151 | +||| (disjointness). The tail is handled by recursion. |
| 152 | +export |
| 153 | +concatAffine : (t1 : Trace) |
| 154 | + -> Disjoint t1 t2 |
| 155 | + -> AffineOk t1 |
| 156 | + -> AffineOk t2 |
| 157 | + -> AffineOk (t1 ++ t2) |
| 158 | +concatAffine [] _ _ ok2 = ok2 |
| 159 | +concatAffine (v :: vs) dj (ConsOk nv ok1) ok2 = |
| 160 | + let notInVs : Not (Elem v vs) := nv |
| 161 | + notInT2 : Not (Elem v t2) := disjointHeadNotRight dj |
| 162 | + notInBoth : Not (Elem v (vs ++ t2)) := notElemApp vs notInVs notInT2 |
| 163 | + restOk : AffineOk (vs ++ t2) := concatAffine vs (disjointTail dj) ok1 ok2 |
| 164 | + in ConsOk notInBoth restOk |
| 165 | + |
| 166 | +-------------------------------------------------------------------------------- |
| 167 | +-- Converse structural law: affine-safety of a suffix |
| 168 | +-------------------------------------------------------------------------------- |
| 169 | + |
| 170 | +||| DECOMPOSITION. If a concatenation is affine-safe, so is its suffix `t2`. |
| 171 | +||| (Dropping a safe prefix never breaks the rest.) Induction on `t1`: each |
| 172 | +||| `ConsOk` we strip exposes the `AffineOk (vs ++ t2)` witness for recursion; |
| 173 | +||| the base case `[] ++ t2 = t2` returns the witness directly. |
| 174 | +export |
| 175 | +suffixAffine : (t1 : Trace) -> AffineOk (t1 ++ t2) -> AffineOk t2 |
| 176 | +suffixAffine [] ok = ok |
| 177 | +suffixAffine (v :: vs) (ConsOk _ rest) = suffixAffine vs rest |
| 178 | + |
| 179 | +-------------------------------------------------------------------------------- |
| 180 | +-- Positive control: a concrete inhabited composition |
| 181 | +-------------------------------------------------------------------------------- |
| 182 | + |
| 183 | +||| Left fragment uses variables 0 and 1 (each once). |
| 184 | +export |
| 185 | +fragLeftOk : AffineOk [0, 1] |
| 186 | +fragLeftOk = |
| 187 | + ConsOk (\el => case el of |
| 188 | + There el1 => absurd el1) |
| 189 | + (ConsOk absurd NilOk) |
| 190 | + |
| 191 | +||| Right fragment uses variables 2 and 3 (each once). |
| 192 | +export |
| 193 | +fragRightOk : AffineOk [2, 3] |
| 194 | +fragRightOk = |
| 195 | + ConsOk (\el => case el of |
| 196 | + There el1 => absurd el1) |
| 197 | + (ConsOk absurd NilOk) |
| 198 | + |
| 199 | +||| 0 is not an element of [2,3]: the diagonal `Here`/`There Here` shapes would |
| 200 | +||| require 0 = 2 / 0 = 3 (automatically pruned), so only the empty tail |
| 201 | +||| `There (There el)` remains, discharged by `absurd`. |
| 202 | +zeroNotIn23 : Not (Elem (the Var 0) [2, 3]) |
| 203 | +zeroNotIn23 (There (There el)) = absurd el |
| 204 | + |
| 205 | +||| 1 is not an element of [2,3]. Same structure as `zeroNotIn23`. |
| 206 | +oneNotIn23 : Not (Elem (the Var 1) [2, 3]) |
| 207 | +oneNotIn23 (There (There el)) = absurd el |
| 208 | + |
| 209 | +||| The two fragments use disjoint variable sets {0,1} and {2,3}. Built |
| 210 | +||| compositionally with `consDisjoint` from the per-element refutations |
| 211 | +||| `zeroNotIn23` / `oneNotIn23` and the vacuous `disjointNilLeft` — a genuine |
| 212 | +||| witness, no assertion. |
| 213 | +export |
| 214 | +fragsDisjoint : Disjoint [0, 1] [2, 3] |
| 215 | +fragsDisjoint = |
| 216 | + consDisjoint zeroNotIn23 (consDisjoint oneNotIn23 (disjointNilLeft [2, 3])) |
| 217 | + |
| 218 | +||| POSITIVE CONTROL: composing the two disjoint affine fragments produces an |
| 219 | +||| affine-safe trace [0,1,2,3] — built by the headline theorem, not by hand. |
| 220 | +export |
| 221 | +composedOk : AffineOk ([0, 1] ++ [2, 3]) |
| 222 | +composedOk = concatAffine [0, 1] fragsDisjoint fragLeftOk fragRightOk |
| 223 | + |
| 224 | +-------------------------------------------------------------------------------- |
| 225 | +-- Negative control 1: overlapping fragments are NOT disjoint |
| 226 | +-------------------------------------------------------------------------------- |
| 227 | + |
| 228 | +||| Fragments [0,1] and [1,2] share variable 1, so they are NOT disjoint. |
| 229 | +||| Machine-checked refutation: any disjointness witness would have to refute |
| 230 | +||| `Elem 1 [1,2]`, but 1 is `Here`. |
| 231 | +export |
| 232 | +overlapNotDisjoint : Not (Disjoint [0, 1] [1, 2]) |
| 233 | +overlapNotDisjoint dj = dj 1 (There Here) Here |
| 234 | + |
| 235 | +||| And their concatenation [0,1,1,2] is genuinely unsafe: variable 1 occurs |
| 236 | +||| twice. No `AffineOk` witness exists — the head-1 obligation would have to |
| 237 | +||| refute `Elem 1 [1,2]`, which is `Here`. |
| 238 | +export |
| 239 | +overlapConcatNotAffine : Not (AffineOk ([0, 1] ++ [1, 2])) |
| 240 | +overlapConcatNotAffine ok = headNotLater (tailOk ok) Here |
| 241 | + |
| 242 | +-------------------------------------------------------------------------------- |
| 243 | +-- Negative control 2: a wide-gap double use is rejected |
| 244 | +-------------------------------------------------------------------------------- |
| 245 | + |
| 246 | +||| A reuse separated by several distinct intervening uses is still caught: |
| 247 | +||| variable 9 appears at the head and again after 4 other variables in |
| 248 | +||| [9, 1, 2, 3, 4, 9]. Refuted because 9 reappears later in the tail. |
| 249 | +export |
| 250 | +wideGapDoubleUseNotAffine : Not (AffineOk [9, 1, 2, 3, 4, 9]) |
| 251 | +wideGapDoubleUseNotAffine ok = |
| 252 | + headNotLater ok (There (There (There (There Here)))) |
0 commit comments