|
| 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 invariant proof for Halideiser: SCHEDULE EQUIVALENCE IS |
| 5 | +||| COMPOSABLE, and a SECOND, DEEPER schedule transformation (TILING, i.e. |
| 6 | +||| split-of-split) preserves the computed result. |
| 7 | +||| |
| 8 | +||| Relationship to the Layer-2 flagship (`Halideiser.ABI.Semantics`) |
| 9 | +||| ------------------------------------------------------------------ |
| 10 | +||| Layer 2 proves the *single* fact that one loop SPLIT is equivalent to |
| 11 | +||| the flat run: `runSplit f outer inner = runFlat f (outer * inner)`. |
| 12 | +||| |
| 13 | +||| This module is genuinely DIFFERENT and DEEPER. It does NOT restate that |
| 14 | +||| theorem. Instead it: |
| 15 | +||| |
| 16 | +||| 1. Defines schedule equivalence as a *relation* between two scheduled |
| 17 | +||| executions (`ScheduleEquiv`), and proves it is a bona-fide |
| 18 | +||| EQUIVALENCE RELATION — reflexive, symmetric, and TRANSITIVE. The |
| 19 | +||| transitivity lemma is the composition principle Layer 2 lacked: |
| 20 | +||| two equivalent reschedules compose into one. |
| 21 | +||| |
| 22 | +||| 2. Models a SECOND transformation that Layer 2 never analysed — the |
| 23 | +||| Halide TILE, which splits the OUTER loop of an existing split |
| 24 | +||| (`tile = split ; split-the-outer`). We prove `runTile` is |
| 25 | +||| equivalent to the flat run by COMPOSING two split-equivalences |
| 26 | +||| through transitivity. This is a composition/transitivity theorem |
| 27 | +||| over the same model, exactly the "distinct, deeper" property the |
| 28 | +||| layer demands. |
| 29 | +||| |
| 30 | +||| The whole development reuses the Layer-2 datatypes and lemmas |
| 31 | +||| (`runFlat`, `runSplit`, `runInner`, `range`, `splitEnumerates`) — it |
| 32 | +||| introduces no new index model. |
| 33 | +module Halideiser.ABI.Invariants |
| 34 | + |
| 35 | +import Halideiser.ABI.Types |
| 36 | +import Halideiser.ABI.Semantics |
| 37 | +import Data.Nat |
| 38 | +import Decidable.Equality |
| 39 | + |
| 40 | +%default total |
| 41 | + |
| 42 | +-------------------------------------------------------------------------------- |
| 43 | +-- Access the Layer-2 result through its PUBLIC interface |
| 44 | +-------------------------------------------------------------------------------- |
| 45 | + |
| 46 | +||| Extract the underlying equality from the Layer-2 flagship. The internal |
| 47 | +||| lemma `splitEnumerates` is private, but the exported theorem |
| 48 | +||| `splitPreservesResult` packages exactly the same equality inside the |
| 49 | +||| (exported) `SplitEquivalent` constructor; we unwrap it here. This is the |
| 50 | +||| sanctioned, public reuse of the Layer-2 model — no redefinition. |
| 51 | +public export |
| 52 | +splitFlatEq : |
| 53 | + (f : Nat -> Nat) -> (outer, inner : Nat) -> |
| 54 | + runSplit f outer inner = runFlat f (outer * inner) |
| 55 | +splitFlatEq f outer inner = case splitPreservesResult f outer inner of |
| 56 | + MkSplitEquivalent prf => prf |
| 57 | + |
| 58 | +-------------------------------------------------------------------------------- |
| 59 | +-- Schedule equivalence as a RELATION (the new, deeper object) |
| 60 | +-------------------------------------------------------------------------------- |
| 61 | + |
| 62 | +||| Two schedule executions (each already a `List Nat` of results, in the |
| 63 | +||| order the schedule visits them) are *schedule-equivalent* when they |
| 64 | +||| produce the identical result list. Halide's correctness invariant is |
| 65 | +||| precisely that a legal reschedule does not change this list. |
| 66 | +||| |
| 67 | +||| Unlike the Layer-2 `SplitEquivalent` (which fixes one side to be the |
| 68 | +||| flat run of a particular split), this is a symmetric relation between |
| 69 | +||| ARBITRARY scheduled runs, which is what lets equivalences COMPOSE. |
| 70 | +public export |
| 71 | +data ScheduleEquiv : (lhs : List Nat) -> (rhs : List Nat) -> Type where |
| 72 | + MkScheduleEquiv : {0 xs, ys : List Nat} -> xs = ys -> ScheduleEquiv xs ys |
| 73 | + |
| 74 | +||| Recover the underlying equality witness from a `ScheduleEquiv`. |
| 75 | +public export |
| 76 | +scheduleEq : {0 xs, ys : List Nat} -> ScheduleEquiv xs ys -> xs = ys |
| 77 | +scheduleEq (MkScheduleEquiv prf) = prf |
| 78 | + |
| 79 | +-------------------------------------------------------------------------------- |
| 80 | +-- ScheduleEquiv is an EQUIVALENCE RELATION |
| 81 | +-------------------------------------------------------------------------------- |
| 82 | + |
| 83 | +||| Reflexivity: any schedule is equivalent to itself. |
| 84 | +public export |
| 85 | +scheduleRefl : {0 xs : List Nat} -> ScheduleEquiv xs xs |
| 86 | +scheduleRefl = MkScheduleEquiv Refl |
| 87 | + |
| 88 | +||| Symmetry: equivalence does not depend on which run we name first. |
| 89 | +public export |
| 90 | +scheduleSym : {0 xs, ys : List Nat} -> |
| 91 | + ScheduleEquiv xs ys -> ScheduleEquiv ys xs |
| 92 | +scheduleSym (MkScheduleEquiv prf) = MkScheduleEquiv (sym prf) |
| 93 | + |
| 94 | +||| TRANSITIVITY — the composition principle. If reschedule A matches B and |
| 95 | +||| reschedule B matches C, then A matches C: two equivalent reschedules |
| 96 | +||| compose into a single equivalence. This is the heart of Layer 3. |
| 97 | +public export |
| 98 | +scheduleTrans : {0 xs, ys, zs : List Nat} -> |
| 99 | + ScheduleEquiv xs ys -> ScheduleEquiv ys zs -> |
| 100 | + ScheduleEquiv xs zs |
| 101 | +scheduleTrans (MkScheduleEquiv p) (MkScheduleEquiv q) = |
| 102 | + MkScheduleEquiv (trans p q) |
| 103 | + |
| 104 | +-------------------------------------------------------------------------------- |
| 105 | +-- Bridge: every Layer-2 split equivalence is a ScheduleEquiv |
| 106 | +-------------------------------------------------------------------------------- |
| 107 | + |
| 108 | +||| A split run is schedule-equivalent to the flat run. This re-expresses |
| 109 | +||| the Layer-2 theorem `splitEnumerates` in the new relational vocabulary |
| 110 | +||| so it can be COMPOSED with other equivalences via `scheduleTrans`. |
| 111 | +public export |
| 112 | +splitIsScheduleEquiv : |
| 113 | + (f : Nat -> Nat) -> (outer, inner : Nat) -> |
| 114 | + ScheduleEquiv (runSplit f outer inner) (runFlat f (outer * inner)) |
| 115 | +splitIsScheduleEquiv f outer inner = |
| 116 | + MkScheduleEquiv (splitFlatEq f outer inner) |
| 117 | + |
| 118 | +-------------------------------------------------------------------------------- |
| 119 | +-- The SECOND transformation: TILING (split the OUTER loop of a split) |
| 120 | +-------------------------------------------------------------------------------- |
| 121 | + |
| 122 | +||| The tiled schedule. A plain split (Layer 2) walks the outer index space |
| 123 | +||| `range (a * b)` directly, running one inner block `runInner f inner o` |
| 124 | +||| per outer index `o`. TILING instead walks the SAME outer index space in |
| 125 | +||| a re-grouped order: the outer indices are themselves produced by an |
| 126 | +||| inner split into `a` groups of `b`. Concretely the tiled outer order is |
| 127 | +||| `runSplit (\o => o) a b` — the identity stage scheduled with its own |
| 128 | +||| split — which enumerates exactly the outer indices `0 .. a*b-1`, but |
| 129 | +||| grouped as the Halide tile prescribes. |
| 130 | +||| |
| 131 | +||| For each tiled outer index `o` we run the same inner block as the plain |
| 132 | +||| split. Visiting indices: this is the genuine two-level loop nest that |
| 133 | +||| Halide's `tile` emits. |
| 134 | +public export |
| 135 | +runTile : (f : Nat -> Nat) -> (a, b, inner : Nat) -> List Nat |
| 136 | +runTile f a b inner = |
| 137 | + concatBlocks (map (runInner f inner) (runSplit (\o => o) a b)) |
| 138 | + |
| 139 | +-------------------------------------------------------------------------------- |
| 140 | +-- Key structural fact: tiling the OUTER index space leaves it unchanged |
| 141 | +-------------------------------------------------------------------------------- |
| 142 | + |
| 143 | +||| The tiled outer enumeration `runSplit (\o => o) a b` is the SAME list as |
| 144 | +||| the plain outer enumeration `range (a * b)`. This is a direct corollary |
| 145 | +||| of the Layer-2 theorem applied to the identity stage: |
| 146 | +||| runSplit id a b = runFlat id (a*b) = map id (range (a*b)) = range (a*b). |
| 147 | +||| |
| 148 | +||| (We do not redefine or re-prove `splitEnumerates`; we instantiate it.) |
| 149 | +public export |
| 150 | +tiledOuterIsRange : |
| 151 | + (a, b : Nat) -> runSplit (\o => o) a b = range (a * b) |
| 152 | +tiledOuterIsRange a b = |
| 153 | + rewrite splitFlatEq (\o => o) a b in |
| 154 | + mapIdRange (a * b) |
| 155 | + where |
| 156 | + ||| `map id xs = xs`, specialised to the identity lambda `\o => o`. |
| 157 | + mapIdRange : (n : Nat) -> map (\o => o) (range n) = range n |
| 158 | + mapIdRange n = mapIdIs (range n) |
| 159 | + where |
| 160 | + mapIdIs : (xs : List Nat) -> map (\o => o) xs = xs |
| 161 | + mapIdIs [] = Refl |
| 162 | + mapIdIs (x :: xs) = cong (x ::) (mapIdIs xs) |
| 163 | + |
| 164 | +-------------------------------------------------------------------------------- |
| 165 | +-- Tiling equals the plain split (same blocks, same order) |
| 166 | +-------------------------------------------------------------------------------- |
| 167 | + |
| 168 | +||| Because the tiled outer enumeration equals the plain outer enumeration |
| 169 | +||| (`tiledOuterIsRange`), mapping `runInner f inner` over either and |
| 170 | +||| flattening gives the SAME result. Hence the tiled run equals the plain |
| 171 | +||| split run `runSplit f (a*b) inner`. |
| 172 | +public export |
| 173 | +tileEqualsSplit : |
| 174 | + (f : Nat -> Nat) -> (a, b, inner : Nat) -> |
| 175 | + runTile f a b inner = runSplit f (a * b) inner |
| 176 | +tileEqualsSplit f a b inner = |
| 177 | + -- `runTile` is definitionally `concatBlocks (map (runInner f inner) (runSplit id a b))` |
| 178 | + -- and `runSplit f (a*b) inner` is `concatBlocks (map (runInner f inner) (range (a*b)))`. |
| 179 | + -- Rewriting the tiled outer enumeration to `range (a*b)` makes the two coincide. |
| 180 | + rewrite tiledOuterIsRange a b in Refl |
| 181 | + |
| 182 | +-------------------------------------------------------------------------------- |
| 183 | +-- Headline Layer-3 theorem: TILING preserves the result, by COMPOSITION |
| 184 | +-------------------------------------------------------------------------------- |
| 185 | + |
| 186 | +||| THE LAYER-3 THEOREM. The tiled schedule is schedule-equivalent to the |
| 187 | +||| FLAT run over the domain of size `(a*b) * inner`. The proof COMPOSES two |
| 188 | +||| equivalences via `scheduleTrans`: |
| 189 | +||| |
| 190 | +||| runTile f a b inner |
| 191 | +||| ==[ tileEqualsSplit ] (tiling = plain split on outer) |
| 192 | +||| runSplit f (a*b) inner |
| 193 | +||| ==[ Layer-2 splitEnumerates ] (plain split = flat) |
| 194 | +||| runFlat f ((a*b) * inner) |
| 195 | +||| |
| 196 | +||| This is strictly deeper than Layer 2: it builds a NEW transformation and |
| 197 | +||| discharges it by transitively chaining the Layer-2 result with a fresh |
| 198 | +||| structural lemma — it does not restate Layer 2. |
| 199 | +public export |
| 200 | +tilePreservesResult : |
| 201 | + (f : Nat -> Nat) -> (a, b, inner : Nat) -> |
| 202 | + ScheduleEquiv (runTile f a b inner) (runFlat f ((a * b) * inner)) |
| 203 | +tilePreservesResult f a b inner = |
| 204 | + scheduleTrans |
| 205 | + (MkScheduleEquiv (tileEqualsSplit f a b inner)) |
| 206 | + (splitIsScheduleEquiv f (a * b) inner) |
| 207 | + |
| 208 | +-------------------------------------------------------------------------------- |
| 209 | +-- A natural, sound + complete decision procedure for ScheduleEquiv |
| 210 | +-------------------------------------------------------------------------------- |
| 211 | + |
| 212 | +||| Decide schedule-equivalence of two CONCRETE result lists. Sound and |
| 213 | +||| complete: it returns `Yes` exactly when the lists are equal (via the |
| 214 | +||| library `DecEq (List Nat)`), and the `No` branch carries a real refuter. |
| 215 | +public export |
| 216 | +decScheduleEquiv : (xs, ys : List Nat) -> Dec (ScheduleEquiv xs ys) |
| 217 | +decScheduleEquiv xs ys = case decEq xs ys of |
| 218 | + Yes prf => Yes (MkScheduleEquiv prf) |
| 219 | + No ctr => No (\se => ctr (scheduleEq se)) |
| 220 | + |
| 221 | +-------------------------------------------------------------------------------- |
| 222 | +-- POSITIVE control: a concrete tiling equivalence witness |
| 223 | +-------------------------------------------------------------------------------- |
| 224 | + |
| 225 | +||| POSITIVE CONTROL. Tile an extent-12 domain as `(a=2) * (b=3)` outer |
| 226 | +||| groups, each of `inner=2` iterations — i.e. (2*3)*2 = 12 — for the |
| 227 | +||| `double` stage from Layer 2. The tiled run is schedule-equivalent to the |
| 228 | +||| flat run. Inhabited witness, discharged by the general theorem. |
| 229 | +public export |
| 230 | +doubleTile2x3x2Equivalent : |
| 231 | + ScheduleEquiv (runTile Semantics.double 2 3 2) |
| 232 | + (runFlat Semantics.double ((2 * 3) * 2)) |
| 233 | +doubleTile2x3x2Equivalent = tilePreservesResult Semantics.double 2 3 2 |
| 234 | + |
| 235 | +||| And the underlying lists are literally, definitionally equal on this |
| 236 | +||| concrete case (the proof reduces to `Refl`). |
| 237 | +public export |
| 238 | +doubleTile2x3x2Concrete : |
| 239 | + runTile Semantics.double 2 3 2 = runFlat Semantics.double 12 |
| 240 | +doubleTile2x3x2Concrete = Refl |
| 241 | + |
| 242 | +||| Tag extractor for a `Dec`: `True` for `Yes`, `False` for `No`. |
| 243 | +||| (Local, total — avoids guessing the Prelude name.) |
| 244 | +public export |
| 245 | +decTag : Dec p -> Bool |
| 246 | +decTag (Yes _) = True |
| 247 | +decTag (No _) = False |
| 248 | + |
| 249 | +||| Decision-procedure smoke test: the concrete tile/flat pair decides Yes. |
| 250 | +||| Only the tag is asserted by `Refl`, not the proof term's internals. |
| 251 | +public export |
| 252 | +decDoubleTileYes : |
| 253 | + decTag (decScheduleEquiv (runTile Semantics.double 2 3 2) |
| 254 | + (runFlat Semantics.double 12)) = True |
| 255 | +decDoubleTileYes = Refl |
| 256 | + |
| 257 | +-------------------------------------------------------------------------------- |
| 258 | +-- NEGATIVE / non-vacuity controls |
| 259 | +-------------------------------------------------------------------------------- |
| 260 | + |
| 261 | +||| NEGATIVE CONTROL #1. A result-CHANGING "tile" is NOT schedule-equivalent |
| 262 | +||| to the flat run. We exhibit a concrete bad run (the flat run of the WRONG |
| 263 | +||| stage `succ`, whose values differ from `double`'s) and prove it is `Not` |
| 264 | +||| schedule-equivalent to the correct flat run. Machine-checked: the two |
| 265 | +||| concrete result lists genuinely differ, so no `ScheduleEquiv` inhabits. |
| 266 | +public export |
| 267 | +mistiledNotEquivalent : |
| 268 | + Not (ScheduleEquiv (runFlat S 12) (runFlat Semantics.double 12)) |
| 269 | +mistiledNotEquivalent (MkScheduleEquiv prf) = case prf of Refl impossible |
| 270 | + |
| 271 | +||| NEGATIVE CONTROL #2 (non-vacuity of transitivity). Transitivity must NOT |
| 272 | +||| be able to "launder" two genuinely different lists into equality. If a |
| 273 | +||| middle list `ys` were equal both to `[0]` and to `[1]`, transitivity |
| 274 | +||| would force `[0] = [1]`, which is impossible. This shows the relation has |
| 275 | +||| real content: composing through any witness preserves true equality. |
| 276 | +public export |
| 277 | +transNonVacuous : |
| 278 | + Not (ScheduleEquiv (the (List Nat) [0]) [1]) |
| 279 | +transNonVacuous (MkScheduleEquiv prf) = case prf of Refl impossible |
| 280 | + |
| 281 | +||| NEGATIVE CONTROL #3 (the `Dec` is not trivially always-Yes). There is |
| 282 | +||| NO proof that the decision procedure answers `Yes` on a genuinely |
| 283 | +||| unequal pair: any such claimed `Yes _` is refuted, because extracting |
| 284 | +||| its witness would prove `[0] = [1]`. We state this as the impossibility |
| 285 | +||| of inhabiting the equivalence on that pair (already shown by |
| 286 | +||| `transNonVacuous`), and additionally that the decider cannot return a |
| 287 | +||| `Yes`-carrying proof here. |
| 288 | +public export |
| 289 | +decDistinctNotYes : |
| 290 | + (prf : ScheduleEquiv (the (List Nat) [0]) [1]) -> Void |
| 291 | +decDistinctNotYes = transNonVacuous |
0 commit comments