|
| 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 | +||| Second flagship semantic proof for Bqniser (Idris2 ABI Layer 3). |
| 5 | +||| |
| 6 | +||| The Layer-2 module (`Bqniser.ABI.Semantics`) proves a *fold* rewrite: |
| 7 | +||| `+´⌽𝕩 ==> +´𝕩` (sum invariant under reverse). This module proves a |
| 8 | +||| genuinely DIFFERENT and DEEPER property: an algebraic FUSION law over the |
| 9 | +||| Each (`¨`) primitive — composition of maps fuses into a single map: |
| 10 | +||| |
| 11 | +||| f¨ g¨ 𝕩 ==> (f∘g)¨ 𝕩 (map fusion / Each fusion) |
| 12 | +||| |
| 13 | +||| This is the canonical loop-fusion rewrite BQNiser performs to eliminate an |
| 14 | +||| intermediate array: applying `g` to every element and then `f` to every |
| 15 | +||| element is the same as applying `f∘g` once per element. We prove: |
| 16 | +||| |
| 17 | +||| 1. `bmapFusion` — the headline fusion equation, by induction (∀ f g xs). |
| 18 | +||| 2. `bmapLengthPreserving` — Each preserves array length, so the rewrite |
| 19 | +||| preserves shape as well as element values (a deeper structural fact |
| 20 | +||| the fold theorem could not express, since fold collapses the array). |
| 21 | +||| 3. `decFuses` — a SOUND + COMPLETE decision that the fused form agrees |
| 22 | +||| with the staged form on a concrete input (round-trip checkable). |
| 23 | +||| 4. A certifier into the ABI `Result`, proven sound against the law. |
| 24 | +||| 5. A POSITIVE control (concrete fusion instance) and a NEGATIVE / |
| 25 | +||| non-vacuity control (`bmap` is genuinely effectful — fusing the WRONG |
| 26 | +||| composition order is refuted, so the law is not trivially true). |
| 27 | +||| |
| 28 | +||| It reuses the SAME `List Nat` model as Layer 2 — no datatype is redefined. |
| 29 | +||| (`bmap` is named to avoid clashing with Prelude `map`.) |
| 30 | + |
| 31 | +module Bqniser.ABI.Invariants |
| 32 | + |
| 33 | +import Bqniser.ABI.Types |
| 34 | +import Bqniser.ABI.Semantics |
| 35 | +import Data.Nat |
| 36 | + |
| 37 | +%default total |
| 38 | + |
| 39 | +-------------------------------------------------------------------------------- |
| 40 | +-- The Each (`¨`) primitive over the shared List Nat model |
| 41 | +-------------------------------------------------------------------------------- |
| 42 | + |
| 43 | +||| Each (`¨`): apply a function to every element of an array. |
| 44 | +||| Defined directly so it is convenient to induct over. |
| 45 | +public export |
| 46 | +bmap : (Nat -> Nat) -> List Nat -> List Nat |
| 47 | +bmap _ [] = [] |
| 48 | +bmap f (x :: xs) = f x :: bmap f xs |
| 49 | + |
| 50 | +||| Length, named to avoid any List/Prelude ambiguity through this module. |
| 51 | +public export |
| 52 | +blen : List Nat -> Nat |
| 53 | +blen [] = 0 |
| 54 | +blen (_ :: xs) = S (blen xs) |
| 55 | + |
| 56 | +-------------------------------------------------------------------------------- |
| 57 | +-- Layer-3 theorem 1: map fusion (the headline algebraic law) |
| 58 | +-------------------------------------------------------------------------------- |
| 59 | + |
| 60 | +||| Fusion: mapping `g` then `f` equals mapping `f ∘ g` in a single pass. |
| 61 | +||| Proven by induction over the array; semantics-preserving for all inputs. |
| 62 | +export |
| 63 | +bmapFusion : (f : Nat -> Nat) -> (g : Nat -> Nat) -> (xs : List Nat) -> |
| 64 | + bmap f (bmap g xs) = bmap (\x => f (g x)) xs |
| 65 | +bmapFusion _ _ [] = Refl |
| 66 | +bmapFusion f g (x :: xs) = cong (f (g x) ::) (bmapFusion f g xs) |
| 67 | + |
| 68 | +-------------------------------------------------------------------------------- |
| 69 | +-- Layer-3 theorem 2: Each preserves length (structural soundness) |
| 70 | +-------------------------------------------------------------------------------- |
| 71 | + |
| 72 | +||| Each preserves array length: the rewrite is shape-preserving, not just |
| 73 | +||| value-preserving. This is strictly deeper than the fold theorem, whose |
| 74 | +||| result is a scalar and therefore cannot witness shape. |
| 75 | +export |
| 76 | +bmapLengthPreserving : (f : Nat -> Nat) -> (xs : List Nat) -> |
| 77 | + blen (bmap f xs) = blen xs |
| 78 | +bmapLengthPreserving _ [] = Refl |
| 79 | +bmapLengthPreserving f (x :: xs) = cong S (bmapLengthPreserving f xs) |
| 80 | + |
| 81 | +||| Corollary: the staged form and the fused form have identical length, |
| 82 | +||| obtained purely from the two theorems above (no fresh induction). |
| 83 | +export |
| 84 | +fusionLengthAgrees : (f : Nat -> Nat) -> (g : Nat -> Nat) -> (xs : List Nat) -> |
| 85 | + blen (bmap f (bmap g xs)) = blen (bmap (\x => f (g x)) xs) |
| 86 | +fusionLengthAgrees f g xs = cong blen (bmapFusion f g xs) |
| 87 | + |
| 88 | +-------------------------------------------------------------------------------- |
| 89 | +-- A sound + complete decision for fusion-agreement on a concrete input |
| 90 | +-------------------------------------------------------------------------------- |
| 91 | + |
| 92 | +||| Decide whether the staged form and the fused form agree on a given input. |
| 93 | +||| By `bmapFusion` they ALWAYS agree, so this is a total `Yes`; the point is |
| 94 | +||| that it is genuinely a `Dec` of the propositional equality (sound: the |
| 95 | +||| `Yes` carries a real proof; complete: a `No` is impossible to construct, |
| 96 | +||| witnessed by the proof itself rather than by fiat). |
| 97 | +public export |
| 98 | +decFuses : (f : Nat -> Nat) -> (g : Nat -> Nat) -> (xs : List Nat) -> |
| 99 | + Dec (bmap f (bmap g xs) = bmap (\x => f (g x)) xs) |
| 100 | +decFuses f g xs = Yes (bmapFusion f g xs) |
| 101 | + |
| 102 | +||| Completeness of the decision: there is no input on which the agreement |
| 103 | +||| fails. Anything purporting to refute fusion is itself refuted. |
| 104 | +export |
| 105 | +decFusesComplete : (f : Nat -> Nat) -> (g : Nat -> Nat) -> (xs : List Nat) -> |
| 106 | + Not (Not (bmap f (bmap g xs) = bmap (\x => f (g x)) xs)) |
| 107 | +decFusesComplete f g xs contra = contra (bmapFusion f g xs) |
| 108 | + |
| 109 | +-------------------------------------------------------------------------------- |
| 110 | +-- Certifier into the ABI Result, proven sound against the fusion law |
| 111 | +-------------------------------------------------------------------------------- |
| 112 | + |
| 113 | +||| The fusion rewrite is unconditionally valid, so the certifier reports `Ok`. |
| 114 | +public export |
| 115 | +certifyFusion : (Nat -> Nat) -> (Nat -> Nat) -> List Nat -> Result |
| 116 | +certifyFusion _ _ _ = Ok |
| 117 | + |
| 118 | +||| Soundness: whenever the certifier reports `Ok`, the fused rewrite genuinely |
| 119 | +||| computes the same array as the staged form for that input. |
| 120 | +export |
| 121 | +certifyFusionSound : (f : Nat -> Nat) -> (g : Nat -> Nat) -> (xs : List Nat) -> |
| 122 | + certifyFusion f g xs = Ok -> |
| 123 | + bmap f (bmap g xs) = bmap (\x => f (g x)) xs |
| 124 | +certifyFusionSound f g xs _ = bmapFusion f g xs |
| 125 | + |
| 126 | +-------------------------------------------------------------------------------- |
| 127 | +-- Positive control: a concrete fusion instance, machine-checked |
| 128 | +-------------------------------------------------------------------------------- |
| 129 | + |
| 130 | +||| Doubling then incrementing every element, in two passes, equals doing |
| 131 | +||| `(+1) ∘ (*2)` in one pass, over a concrete array. |
| 132 | +export |
| 133 | +fusionConcrete : bmap (\x => x + 1) (bmap (\x => x * 2) [1, 2, 3]) |
| 134 | + = bmap (\x => (x * 2) + 1) [1, 2, 3] |
| 135 | +fusionConcrete = bmapFusion (\x => x + 1) (\x => x * 2) [1, 2, 3] |
| 136 | + |
| 137 | +||| The fused result is exactly what we expect (fully reduced, asserted by Refl). |
| 138 | +export |
| 139 | +fusionConcreteValue : bmap (\x => (x * 2) + 1) [1, 2, 3] = [3, 5, 7] |
| 140 | +fusionConcreteValue = Refl |
| 141 | + |
| 142 | +-------------------------------------------------------------------------------- |
| 143 | +-- Negative / non-vacuity control |
| 144 | +-------------------------------------------------------------------------------- |
| 145 | + |
| 146 | +||| Non-vacuity: fusion is order-sensitive. Mapping `(*2)` then `(+1)` is NOT |
| 147 | +||| the same as mapping `(+1)` then `(*2)` on this input — i.e. the law genuinely |
| 148 | +||| depends on composing in the right order, it is not "any two maps fuse to |
| 149 | +||| anything". This refutes the WRONG fusion and proves the theorem has content. |
| 150 | +export |
| 151 | +fusionOrderMatters : |
| 152 | + Not (bmap (\x => x + 1) (bmap (\x => x * 2) [1]) |
| 153 | + = bmap (\x => x * 2) (bmap (\x => x + 1) [1])) |
| 154 | +fusionOrderMatters Refl impossible |
| 155 | + |
| 156 | +||| Second non-vacuity control: `bmap` is not the constant-empty function — |
| 157 | +||| it actually produces a non-empty array from a non-empty input, so the |
| 158 | +||| length-preservation theorem is not vacuously about empty lists. |
| 159 | +export |
| 160 | +bmapNonTrivial : Not (bmap (\x => x + 1) [5] = []) |
| 161 | +bmapNonTrivial Refl impossible |
0 commit comments