|
| 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 | +||| General partition-correctness proofs for Chapeliser. |
| 5 | +||| |
| 6 | +||| `Proofs.idr` verifies completeness/disjointness on a handful of *explicit* |
| 7 | +||| slice vectors. This module proves the same invariants **for the whole |
| 8 | +||| contiguous-partition family at once** — quantified over an arbitrary vector |
| 9 | +||| of per-locale counts — and applies that result to the actual block-partition |
| 10 | +||| strategy (the `div`/`mod` counts of `perItemSlices`) for **all** `n` items |
| 11 | +||| and `k` locales. |
| 12 | +||| |
| 13 | +||| The key idea: contiguity is *structural*. A contiguous layout places each |
| 14 | +||| slice exactly where the previous one ended, independently of the count |
| 15 | +||| values, so the non-overlap guarantee needs no reasoning about `div`/`mod` |
| 16 | +||| (which do not reduce at the type level). We capture non-overlap as a |
| 17 | +||| `Tiling` — a gapless, overlap-free cover, strictly stronger than the pairwise |
| 18 | +||| `disjoint` Bool check — and prove the real block partition is one for all |
| 19 | +||| `n`, `k`. The only residual is the completeness identity |
| 20 | +||| `sumNat (perItemCounts n k) = n`, which is isolated below. |
| 21 | + |
| 22 | +module Chapeliser.ABI.Partition |
| 23 | + |
| 24 | +import Chapeliser.ABI.Types |
| 25 | +import Data.Vect |
| 26 | +import Data.Vect.Quantifiers |
| 27 | +import Data.Nat |
| 28 | + |
| 29 | +%default total |
| 30 | + |
| 31 | +-------------------------------------------------------------------------------- |
| 32 | +-- The contiguous layout builder (a reducible, top-level form of perItemSlices) |
| 33 | +-------------------------------------------------------------------------------- |
| 34 | + |
| 35 | +||| Lay out `k` slices contiguously from `start`, one per count: slice i begins |
| 36 | +||| where slice i-1 ended. This is exactly the shape of `perItemSlices`' inner |
| 37 | +||| `go`, lifted to top level so proofs can reduce through it. |
| 38 | +public export |
| 39 | +contiguousFrom : (start : Nat) -> Vect k Nat -> Vect k Slice |
| 40 | +contiguousFrom _ [] = [] |
| 41 | +contiguousFrom start (c :: cs) = MkSlice start c :: contiguousFrom (start + c) cs |
| 42 | + |
| 43 | +||| Sum of a vector of counts. |
| 44 | +public export |
| 45 | +sumNat : Vect k Nat -> Nat |
| 46 | +sumNat [] = 0 |
| 47 | +sumNat (c :: cs) = c + sumNat cs |
| 48 | + |
| 49 | +-------------------------------------------------------------------------------- |
| 50 | +-- Completeness, generalised over ALL count vectors |
| 51 | +-------------------------------------------------------------------------------- |
| 52 | + |
| 53 | +||| For ANY start offset and ANY vector of counts, the contiguous layout's slice |
| 54 | +||| counts sum to exactly the total of the counts — no items dropped or |
| 55 | +||| duplicated. (`Proofs.idr` only checked this for fixed vectors.) |
| 56 | +export |
| 57 | +contiguousComplete : (start : Nat) -> (counts : Vect k Nat) -> |
| 58 | + sliceSum (contiguousFrom start counts) = sumNat counts |
| 59 | +contiguousComplete _ [] = Refl |
| 60 | +contiguousComplete start (c :: cs) = |
| 61 | + rewrite contiguousComplete (start + c) cs in Refl |
| 62 | + |
| 63 | +-------------------------------------------------------------------------------- |
| 64 | +-- Tiny LTE lemmas (propositional; these reduce, unlike the compare-based `<=`) |
| 65 | +-------------------------------------------------------------------------------- |
| 66 | + |
| 67 | +lteReflexive : (n : Nat) -> LTE n n |
| 68 | +lteReflexive Z = LTEZero |
| 69 | +lteReflexive (S k) = LTESucc (lteReflexive k) |
| 70 | + |
| 71 | +lteAddR : (n, m : Nat) -> LTE n (n + m) |
| 72 | +lteAddR Z m = LTEZero |
| 73 | +lteAddR (S k) m = LTESucc (lteAddR k m) |
| 74 | + |
| 75 | +lteTrans' : LTE a b -> LTE b c -> LTE a c |
| 76 | +lteTrans' LTEZero _ = LTEZero |
| 77 | +lteTrans' (LTESucc p) (LTESucc q) = LTESucc (lteTrans' p q) |
| 78 | + |
| 79 | +-------------------------------------------------------------------------------- |
| 80 | +-- Non-overlap as a structural tiling (gapless AND overlap-free by construction) |
| 81 | +-------------------------------------------------------------------------------- |
| 82 | + |
| 83 | +||| `Tiling lo ss` witnesses that `ss` tiles `[lo, …)` with no gaps and no |
| 84 | +||| overlaps: the first slice begins at `lo`, and the rest tile from exactly |
| 85 | +||| where it ends. Strictly stronger than pairwise disjointness. |
| 86 | +public export |
| 87 | +data Tiling : Nat -> Vect k Slice -> Type where |
| 88 | + TNil : Tiling lo [] |
| 89 | + TCons : (c : Nat) -> Tiling (lo + c) rest -> |
| 90 | + Tiling lo (MkSlice lo c :: rest) |
| 91 | + |
| 92 | +||| The contiguous layout is a perfect tiling for ANY start and ANY counts. |
| 93 | +export |
| 94 | +contiguousTiles : (lo : Nat) -> (counts : Vect k Nat) -> |
| 95 | + Tiling lo (contiguousFrom lo counts) |
| 96 | +contiguousTiles _ [] = TNil |
| 97 | +contiguousTiles lo (c :: cs) = TCons c (contiguousTiles (lo + c) cs) |
| 98 | + |
| 99 | +-------------------------------------------------------------------------------- |
| 100 | +-- Tiling ⇒ propositional pairwise non-overlap (no Bool `<=`/`all`) |
| 101 | +-------------------------------------------------------------------------------- |
| 102 | + |
| 103 | +||| Every slice of a tiling based at `lo` starts at `lo` or later. |
| 104 | +export |
| 105 | +tilingStartsGE : {ss : Vect k Slice} -> Tiling lo ss -> |
| 106 | + All (\s => LTE lo s.start) ss |
| 107 | +tilingStartsGE TNil = [] |
| 108 | +tilingStartsGE (TCons {lo} c t) = |
| 109 | + lteReflexive lo |
| 110 | + :: mapProperty (\le => lteTrans' (lteAddR lo c) le) (tilingStartsGE t) |
| 111 | + |
| 112 | +||| Propositional non-overlap of two slices: `a` ends at or before `b` starts. |
| 113 | +public export |
| 114 | +NoOverlap : Slice -> Slice -> Type |
| 115 | +NoOverlap a b = LTE (a.start + a.count) b.start |
| 116 | + |
| 117 | +||| In a tiling, the head slice does not overlap any later slice — its end is |
| 118 | +||| `lo + c`, and every later slice starts there or later. Genuine pairwise |
| 119 | +||| disjointness, proven generically (not per concrete vector). |
| 120 | +export |
| 121 | +tilingHeadNoOverlap : {lo : Nat} -> (c : Nat) -> {rest : Vect k Slice} -> |
| 122 | + Tiling (lo + c) rest -> |
| 123 | + All (\s => NoOverlap (MkSlice lo c) s) rest |
| 124 | +tilingHeadNoOverlap c t = tilingStartsGE t |
| 125 | + |
| 126 | +-------------------------------------------------------------------------------- |
| 127 | +-- The actual block-partition strategy is correct for ALL n and k |
| 128 | +-------------------------------------------------------------------------------- |
| 129 | + |
| 130 | +||| Per-locale counts for the even (block) partition: every locale gets `base` |
| 131 | +||| items and the first `rem` locales get one extra. `idx` is the running locale |
| 132 | +||| index. (This is the `div`/`mod` content of `perItemSlices`, isolated; the |
| 133 | +||| proofs below do not depend on its values.) |
| 134 | +public export |
| 135 | +countsFrom : (idx, base, rem : Nat) -> (k : Nat) -> Vect k Nat |
| 136 | +countsFrom _ _ _ Z = [] |
| 137 | +countsFrom idx base rem (S j) = |
| 138 | + (base + (if idx < rem then 1 else 0)) :: countsFrom (S idx) base rem j |
| 139 | + |
| 140 | +||| The block partition's per-locale counts for `n` items over `k` locales. |
| 141 | +public export |
| 142 | +perItemCounts : (n : Nat) -> (k : Nat) -> Vect k Nat |
| 143 | +perItemCounts n k = countsFrom 0 (n `div` k) (n `mod` k) k |
| 144 | + |
| 145 | +||| The block partition laid out contiguously. |
| 146 | +public export |
| 147 | +blockSlices : (n : Nat) -> (k : Nat) -> Vect k Slice |
| 148 | +blockSlices n k = contiguousFrom 0 (perItemCounts n k) |
| 149 | + |
| 150 | +||| MAIN RESULT: for every item count `n` and every locale count `k`, the block |
| 151 | +||| partition is a gapless, non-overlapping tiling — no item is assigned to two |
| 152 | +||| locales and the assigned ranges abut perfectly. Holds for ALL n, k with no |
| 153 | +||| appeal to how `div`/`mod` evaluate. |
| 154 | +export |
| 155 | +blockPartitionTiles : (n : Nat) -> (k : Nat) -> Tiling 0 (blockSlices n k) |
| 156 | +blockPartitionTiles n k = contiguousTiles 0 (perItemCounts n k) |
| 157 | + |
| 158 | +||| Corollary: completeness of the block partition reduces to the residual |
| 159 | +||| arithmetic identity `sumNat (perItemCounts n k) = n` — the only `div`/`mod` |
| 160 | +||| obligation, which the type checker cannot discharge by reduction (tracked as |
| 161 | +||| future proof work). The structural half (slice counts sum to the count |
| 162 | +||| total) is proven here for all n, k. |
| 163 | +export |
| 164 | +blockPartitionComplete : (n : Nat) -> (k : Nat) -> |
| 165 | + sliceSum (blockSlices n k) = sumNat (perItemCounts n k) |
| 166 | +blockPartitionComplete n k = contiguousComplete 0 (perItemCounts n k) |
| 167 | + |
| 168 | +-------------------------------------------------------------------------------- |
| 169 | +-- Negative control: a wrong-sum partition is genuinely NOT complete |
| 170 | +-------------------------------------------------------------------------------- |
| 171 | + |
| 172 | +||| A 3-locale layout whose counts sum to 9 cannot be a complete partition of 10 |
| 173 | +||| items: `PartitionComplete` for `Partition 10 3` demands `sliceSum = 10`, but |
| 174 | +||| this layout sums to 9. Witnessing the negation keeps completeness honest. |
| 175 | +export |
| 176 | +shortPartitionNotComplete : |
| 177 | + Not (PartitionComplete (MkPartition {n = 10} {k = 3} (contiguousFrom 0 [3, 3, 3]))) |
| 178 | +shortPartitionNotComplete (IsComplete Refl) impossible |
0 commit comments