|
| 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 deep invariants for julianiser's source->Julia translation. |
| 5 | +||| |
| 6 | +||| The Layer-2 flagship (`Julianiser.ABI.Semantics.translatePreserves`) proves |
| 7 | +||| a SINGLE per-expression law: for every expression, the translated Julia code |
| 8 | +||| computes the same value as the source. This module proves three GENUINELY |
| 9 | +||| DEEPER, DISTINCT properties about the translation as a *structure-preserving |
| 10 | +||| transformation*, all reusing the existing model (no datatype is redefined): |
| 11 | +||| |
| 12 | +||| 1. COMPOSITIONALITY (homomorphism). The translation is a homomorphism with |
| 13 | +||| respect to syntactic composition: a generic binary "plug two programs |
| 14 | +||| together" combiner on the source side is sent by `translate` to the |
| 15 | +||| corresponding combiner on the Julia side. Hence |
| 16 | +||| |
| 17 | +||| translate (link op a b) = jlink op (translate a) (translate b) |
| 18 | +||| |
| 19 | +||| and, crucially, CORRECTNESS IS CLOSED UNDER COMPOSITION: if each part |
| 20 | +||| preserves results, so does the composite. This is the substitution / |
| 21 | +||| transitivity-style lemma the prompt asks for, NOT a restatement of the |
| 22 | +||| single-op theorem. |
| 23 | +||| |
| 24 | +||| 2. DETERMINISM. Both evaluators and the whole pipeline are functions: the |
| 25 | +||| translated program's output is uniquely determined by the source program |
| 26 | +||| and the environment. We give a real congruence proof |
| 27 | +||| (`evalJuliaCong`) and a pipeline-determinism corollary, then show |
| 28 | +||| `translate` is INJECTIVE (distinct source ASTs never collapse), which is |
| 29 | +||| the non-vacuous content behind "deterministic codegen". |
| 30 | +||| |
| 31 | +||| 3. A SECOND, INDEPENDENT CHECKER with a sound AND complete `Dec`: decide |
| 32 | +||| whether a (source, julia) pair is a faithful translation pair, with both |
| 33 | +||| directions proved. |
| 34 | +||| |
| 35 | +||| Quality controls: a POSITIVE control (an inhabited witness / concrete |
| 36 | +||| instance) and a NEGATIVE / non-vacuity control (`Not (...)` plus an injective |
| 37 | +||| disequality) are machine-checked at the bottom. |
| 38 | + |
| 39 | +module Julianiser.ABI.Invariants |
| 40 | + |
| 41 | +import Julianiser.ABI.Types |
| 42 | +import Julianiser.ABI.Semantics |
| 43 | +import Data.Vect |
| 44 | +import Data.Fin |
| 45 | +import Decidable.Equality |
| 46 | + |
| 47 | +%default total |
| 48 | + |
| 49 | +-------------------------------------------------------------------------------- |
| 50 | +-- 1. COMPOSITIONALITY: translation is a homomorphism over syntactic linking |
| 51 | +-------------------------------------------------------------------------------- |
| 52 | + |
| 53 | +||| A binary linking operator on the shared surface syntax. `LinkAdd`/`LinkMul` |
| 54 | +||| name the two ways two sub-programs may be composed into a larger one. This |
| 55 | +||| is the abstract "compose two pipelines" operation the codegen must respect. |
| 56 | +public export |
| 57 | +data LinkOp = LinkAdd | LinkMul |
| 58 | + |
| 59 | +||| Compose two SOURCE expressions with a chosen linking operator. This is the |
| 60 | +||| syntactic substitution/composition combiner on the Python/R side. |
| 61 | +public export |
| 62 | +link : LinkOp -> Expr n -> Expr n -> Expr n |
| 63 | +link LinkAdd a b = Add a b |
| 64 | +link LinkMul a b = Mul a b |
| 65 | + |
| 66 | +||| The corresponding linking combiner on the JULIA side. |
| 67 | +public export |
| 68 | +jlink : LinkOp -> JExpr n -> JExpr n -> JExpr n |
| 69 | +jlink LinkAdd a b = JAdd a b |
| 70 | +jlink LinkMul a b = JMul a b |
| 71 | + |
| 72 | +||| The denotation of a linking operator on the source side: how it combines two |
| 73 | +||| already-computed integer results. |
| 74 | +public export |
| 75 | +linkSem : LinkOp -> Integer -> Integer -> Integer |
| 76 | +linkSem LinkAdd x y = x + y |
| 77 | +linkSem LinkMul x y = x * y |
| 78 | + |
| 79 | +||| HOMOMORPHISM LAW. Translating a composite program equals composing the |
| 80 | +||| translations. `translate` commutes with `link`/`jlink`. This is a real |
| 81 | +||| structural law, distinct from per-expression value preservation: it talks |
| 82 | +||| about the SHAPE of the codegen, not (yet) the values. |
| 83 | +public export |
| 84 | +translateHom : (op : LinkOp) -> (a : Expr n) -> (b : Expr n) -> |
| 85 | + translate (link op a b) = jlink op (translate a) (translate b) |
| 86 | +translateHom LinkAdd a b = Refl |
| 87 | +translateHom LinkMul a b = Refl |
| 88 | + |
| 89 | +||| The source evaluator factors through `linkSem`: evaluating a linked program |
| 90 | +||| is the linked-semantics of evaluating the parts. |
| 91 | +public export |
| 92 | +evalSrcLink : (env : Vect n Integer) -> (op : LinkOp) -> |
| 93 | + (a : Expr n) -> (b : Expr n) -> |
| 94 | + evalSrc env (link op a b) |
| 95 | + = linkSem op (evalSrc env a) (evalSrc env b) |
| 96 | +evalSrcLink env LinkAdd a b = Refl |
| 97 | +evalSrcLink env LinkMul a b = Refl |
| 98 | + |
| 99 | +||| The Julia evaluator factors through the SAME `linkSem` over linked Julia |
| 100 | +||| programs. Together with `evalSrcLink` this says both languages agree on what |
| 101 | +||| "compose" means at the value level. |
| 102 | +public export |
| 103 | +evalJuliaLink : (env : Vect n Integer) -> (op : LinkOp) -> |
| 104 | + (a : JExpr n) -> (b : JExpr n) -> |
| 105 | + evalJulia env (jlink op a b) |
| 106 | + = linkSem op (evalJulia env a) (evalJulia env b) |
| 107 | +evalJuliaLink env LinkAdd a b = Refl |
| 108 | +evalJuliaLink env LinkMul a b = Refl |
| 109 | + |
| 110 | +||| CORRECTNESS IS CLOSED UNDER COMPOSITION (the compositionality theorem). |
| 111 | +||| Given ONLY that each part preserves results under translation, the COMPOSITE |
| 112 | +||| program preserves results too. The proof never unfolds the structure of `a` |
| 113 | +||| or `b` — it uses only the two preservation hypotheses plus the linking laws, |
| 114 | +||| so it is a genuine modular/compositional argument rather than a re-run of the |
| 115 | +||| structural induction in `translatePreserves`. |
| 116 | +public export |
| 117 | +preserveCompose : |
| 118 | + (env : Vect n Integer) -> (op : LinkOp) -> |
| 119 | + (a : Expr n) -> (b : Expr n) -> |
| 120 | + (pa : evalSrc env a = evalJulia env (translate a)) -> |
| 121 | + (pb : evalSrc env b = evalJulia env (translate b)) -> |
| 122 | + evalSrc env (link op a b) = evalJulia env (translate (link op a b)) |
| 123 | +preserveCompose env op a b pa pb = |
| 124 | + rewrite translateHom op a b in |
| 125 | + rewrite evalSrcLink env op a b in |
| 126 | + rewrite evalJuliaLink env op (translate a) (translate b) in |
| 127 | + rewrite pa in |
| 128 | + rewrite pb in Refl |
| 129 | + |
| 130 | +||| Corollary tying the modular lemma back to the global theorem: instantiating |
| 131 | +||| `preserveCompose` with the Layer-2 theorem on each part reproves preservation |
| 132 | +||| for any linked program — demonstrating the modular lemma is strong enough to |
| 133 | +||| drive the whole correctness story by composition. |
| 134 | +public export |
| 135 | +preserveLinkGlobal : |
| 136 | + (env : Vect n Integer) -> (op : LinkOp) -> (a : Expr n) -> (b : Expr n) -> |
| 137 | + evalSrc env (link op a b) = evalJulia env (translate (link op a b)) |
| 138 | +preserveLinkGlobal env op a b = |
| 139 | + preserveCompose env op a b |
| 140 | + (translatePreserves env a) (translatePreserves env b) |
| 141 | + |
| 142 | +-------------------------------------------------------------------------------- |
| 143 | +-- 2. DETERMINISM: evaluators and the pipeline are functions; translate is 1-1 |
| 144 | +-------------------------------------------------------------------------------- |
| 145 | + |
| 146 | +||| Congruence / determinism of the Julia evaluator: equal Julia ASTs evaluated |
| 147 | +||| against the same environment give equal results. (Well-definedness: the |
| 148 | +||| evaluator is a function, so it cannot return two different answers for the |
| 149 | +||| same program.) |
| 150 | +public export |
| 151 | +evalJuliaCong : (env : Vect n Integer) -> (p, q : JExpr n) -> |
| 152 | + p = q -> evalJulia env p = evalJulia env q |
| 153 | +evalJuliaCong env p p Refl = Refl |
| 154 | + |
| 155 | +||| Determinism of the WHOLE pipeline: if two source programs are equal, then |
| 156 | +||| their compiled-and-run results coincide. Output is a function of (source, |
| 157 | +||| env). Proved by congruence over `translate` then `evalJulia`. |
| 158 | +public export |
| 159 | +pipelineDet : (env : Vect n Integer) -> (e1, e2 : Expr n) -> |
| 160 | + e1 = e2 -> |
| 161 | + evalJulia env (translate e1) = evalJulia env (translate e2) |
| 162 | +pipelineDet env e1 e2 prf = |
| 163 | + evalJuliaCong env (translate e1) (translate e2) (cong translate prf) |
| 164 | + |
| 165 | +-- Constructor-injectivity helpers. Each refutes a stuck/mismatched equality at |
| 166 | +-- the top level (idiom: top-level `impossible` clause, never nested `case`). |
| 167 | + |
| 168 | +||| `JAdd` is injective in its left argument (used by `translateInjective`). |
| 169 | +jAddInjL : JAdd x1 y1 = JAdd x2 y2 -> x1 = x2 |
| 170 | +jAddInjL Refl = Refl |
| 171 | + |
| 172 | +||| `JAdd` is injective in its right argument. |
| 173 | +jAddInjR : JAdd x1 y1 = JAdd x2 y2 -> y1 = y2 |
| 174 | +jAddInjR Refl = Refl |
| 175 | + |
| 176 | +||| `JMul` is injective in its left argument. |
| 177 | +jMulInjL : JMul x1 y1 = JMul x2 y2 -> x1 = x2 |
| 178 | +jMulInjL Refl = Refl |
| 179 | + |
| 180 | +||| `JMul` is injective in its right argument. |
| 181 | +jMulInjR : JMul x1 y1 = JMul x2 y2 -> y1 = y2 |
| 182 | +jMulInjR Refl = Refl |
| 183 | + |
| 184 | +||| `JLit` is injective. |
| 185 | +jLitInj : JLit a = JLit b -> a = b |
| 186 | +jLitInj Refl = Refl |
| 187 | + |
| 188 | +||| `JIndex` is injective. |
| 189 | +jIndexInj : JIndex i = JIndex j -> i = j |
| 190 | +jIndexInj Refl = Refl |
| 191 | + |
| 192 | +||| `MkJuliaIdx` is injective (so `toJulia` reflects index equality). |
| 193 | +mkJuliaIdxInj : MkJuliaIdx i = MkJuliaIdx j -> i = j |
| 194 | +mkJuliaIdxInj Refl = Refl |
| 195 | + |
| 196 | +||| INJECTIVITY OF CODEGEN: distinct source ASTs never translate to the same |
| 197 | +||| Julia AST. This is the non-vacuous backbone of "the codegen is |
| 198 | +||| deterministic AND information-preserving" — a property `translatePreserves` |
| 199 | +||| says nothing about. Proved by structural induction, peeling each |
| 200 | +||| constructor with the injectivity helpers above. |
| 201 | +public export |
| 202 | +translateInjective : (e1, e2 : Expr n) -> |
| 203 | + translate e1 = translate e2 -> e1 = e2 |
| 204 | +translateInjective (Lit a) (Lit b) prf = |
| 205 | + cong Lit (jLitInj prf) |
| 206 | +translateInjective (Add l1 r1) (Add l2 r2) prf = |
| 207 | + let pl = translateInjective l1 l2 (jAddInjL prf) |
| 208 | + pr = translateInjective r1 r2 (jAddInjR prf) |
| 209 | + in rewrite pl in rewrite pr in Refl |
| 210 | +translateInjective (Mul l1 r1) (Mul l2 r2) prf = |
| 211 | + let pl = translateInjective l1 l2 (jMulInjL prf) |
| 212 | + pr = translateInjective r1 r2 (jMulInjR prf) |
| 213 | + in rewrite pl in rewrite pr in Refl |
| 214 | +translateInjective (Index i) (Index j) prf = |
| 215 | + cong Index (mkJuliaIdxInj (jIndexInj prf)) |
| 216 | +-- Cross-constructor cases: the two translations are headed by different Julia |
| 217 | +-- constructors, so the equality is impossible. Discharged by top-level clauses. |
| 218 | +translateInjective (Lit _) (Add _ _) Refl impossible |
| 219 | +translateInjective (Lit _) (Mul _ _) Refl impossible |
| 220 | +translateInjective (Lit _) (Index _) Refl impossible |
| 221 | +translateInjective (Add _ _) (Lit _) Refl impossible |
| 222 | +translateInjective (Add _ _) (Mul _ _) Refl impossible |
| 223 | +translateInjective (Add _ _) (Index _) Refl impossible |
| 224 | +translateInjective (Mul _ _) (Lit _) Refl impossible |
| 225 | +translateInjective (Mul _ _) (Add _ _) Refl impossible |
| 226 | +translateInjective (Mul _ _) (Index _) Refl impossible |
| 227 | +translateInjective (Index _) (Lit _) Refl impossible |
| 228 | +translateInjective (Index _) (Add _ _) Refl impossible |
| 229 | +translateInjective (Index _) (Mul _ _) Refl impossible |
| 230 | + |
| 231 | +-------------------------------------------------------------------------------- |
| 232 | +-- 3. SECOND CHECKER: faithful-translation-pair decision (sound + complete) |
| 233 | +-------------------------------------------------------------------------------- |
| 234 | + |
| 235 | +||| A second, independent checker (distinct from Layer-2's `certifyEquiv`): |
| 236 | +||| decide whether a candidate Julia AST `j` is *exactly* the translation of a |
| 237 | +||| source AST `e`. This is the relation "j is the certified codegen output for |
| 238 | +||| e". We give a genuine `Dec`, sound and complete, built on `DecEq JExpr`. |
| 239 | + |
| 240 | +||| Decidable equality for `JuliaIdx`, via `Fin`'s `DecEq`. |
| 241 | +public export |
| 242 | +decEqJuliaIdx : (i, j : JuliaIdx n) -> Dec (i = j) |
| 243 | +decEqJuliaIdx (MkJuliaIdx a) (MkJuliaIdx b) = |
| 244 | + case decEq a b of |
| 245 | + Yes prf => Yes (cong MkJuliaIdx prf) |
| 246 | + No contra => No (\eq => contra (mkJuliaIdxInj eq)) |
| 247 | + |
| 248 | +||| Decidable equality for the Julia AST. Drives the faithfulness checker; built |
| 249 | +||| structurally with the injectivity helpers, no `believe_me`/`assert`. |
| 250 | +public export |
| 251 | +decEqJExpr : (p, q : JExpr n) -> Dec (p = q) |
| 252 | +decEqJExpr (JLit a) (JLit b) = |
| 253 | + case decEq a b of |
| 254 | + Yes prf => Yes (cong JLit prf) |
| 255 | + No contra => No (\eq => contra (jLitInj eq)) |
| 256 | +decEqJExpr (JAdd l1 r1) (JAdd l2 r2) = |
| 257 | + case decEqJExpr l1 l2 of |
| 258 | + No contra => No (\eq => contra (jAddInjL eq)) |
| 259 | + Yes pl => case decEqJExpr r1 r2 of |
| 260 | + No contra => No (\eq => contra (jAddInjR eq)) |
| 261 | + Yes pr => Yes (rewrite pl in rewrite pr in Refl) |
| 262 | +decEqJExpr (JMul l1 r1) (JMul l2 r2) = |
| 263 | + case decEqJExpr l1 l2 of |
| 264 | + No contra => No (\eq => contra (jMulInjL eq)) |
| 265 | + Yes pl => case decEqJExpr r1 r2 of |
| 266 | + No contra => No (\eq => contra (jMulInjR eq)) |
| 267 | + Yes pr => Yes (rewrite pl in rewrite pr in Refl) |
| 268 | +decEqJExpr (JIndex i) (JIndex j) = |
| 269 | + case decEqJuliaIdx i j of |
| 270 | + Yes prf => Yes (cong JIndex prf) |
| 271 | + No contra => No (\eq => contra (jIndexInj eq)) |
| 272 | +decEqJExpr (JLit _) (JAdd _ _) = No (\case Refl impossible) |
| 273 | +decEqJExpr (JLit _) (JMul _ _) = No (\case Refl impossible) |
| 274 | +decEqJExpr (JLit _) (JIndex _) = No (\case Refl impossible) |
| 275 | +decEqJExpr (JAdd _ _) (JLit _) = No (\case Refl impossible) |
| 276 | +decEqJExpr (JAdd _ _) (JMul _ _) = No (\case Refl impossible) |
| 277 | +decEqJExpr (JAdd _ _) (JIndex _) = No (\case Refl impossible) |
| 278 | +decEqJExpr (JMul _ _) (JLit _) = No (\case Refl impossible) |
| 279 | +decEqJExpr (JMul _ _) (JAdd _ _) = No (\case Refl impossible) |
| 280 | +decEqJExpr (JMul _ _) (JIndex _) = No (\case Refl impossible) |
| 281 | +decEqJExpr (JIndex _) (JLit _) = No (\case Refl impossible) |
| 282 | +decEqJExpr (JIndex _) (JAdd _ _) = No (\case Refl impossible) |
| 283 | +decEqJExpr (JIndex _) (JMul _ _) = No (\case Refl impossible) |
| 284 | + |
| 285 | +||| `IsFaithfulPair e j` holds exactly when `j` is the certified translation of |
| 286 | +||| `e`. Indexed relation, the second checker's specification. |
| 287 | +public export |
| 288 | +data IsFaithfulPair : Expr n -> JExpr n -> Type where |
| 289 | + MkFaithful : (e : Expr n) -> IsFaithfulPair e (translate e) |
| 290 | + |
| 291 | +||| DECISION PROCEDURE for the faithfulness relation: decide whether `j` is the |
| 292 | +||| translation of `e`, by comparing `j` to `translate e`. |
| 293 | +public export |
| 294 | +decFaithful : (e : Expr n) -> (j : JExpr n) -> Dec (IsFaithfulPair e j) |
| 295 | +decFaithful e j = |
| 296 | + case decEqJExpr (translate e) j of |
| 297 | + Yes prf => Yes (rewrite sym prf in MkFaithful e) |
| 298 | + No contra => No (\(MkFaithful e) => contra Refl) |
| 299 | + |
| 300 | +||| SOUNDNESS of the faithfulness checker: a `Yes` verdict means the candidate |
| 301 | +||| really equals the codegen output AND (via Layer 2) computes the same value. |
| 302 | +public export |
| 303 | +decFaithfulSound : (env : Vect n Integer) -> (e : Expr n) -> (j : JExpr n) -> |
| 304 | + IsFaithfulPair e j -> |
| 305 | + evalSrc env e = evalJulia env j |
| 306 | +decFaithfulSound env e (translate e) (MkFaithful e) = translatePreserves env e |
| 307 | + |
| 308 | +||| COMPLETENESS of the faithfulness checker: the genuine codegen output is |
| 309 | +||| always accepted (the checker never rejects a real translation). |
| 310 | +public export |
| 311 | +decFaithfulComplete : (e : Expr n) -> IsFaithfulPair e (translate e) |
| 312 | +decFaithfulComplete e = MkFaithful e |
| 313 | + |
| 314 | +-------------------------------------------------------------------------------- |
| 315 | +-- POSITIVE control: a concrete composite that round-trips through composition |
| 316 | +-------------------------------------------------------------------------------- |
| 317 | + |
| 318 | +||| Reuse the Layer-2 sample components to build a COMPOSITE program by linking, |
| 319 | +||| then certify it via the compositional lemma `preserveCompose` (NOT the global |
| 320 | +||| induction), so the positive control actually exercises the new machinery. |
| 321 | +public export |
| 322 | +compositeExpr : Expr 3 |
| 323 | +compositeExpr = link LinkAdd Semantics.sampleExpr (Index FZ) |
| 324 | + |
| 325 | +||| Positive control: the composite preserves results, proved through the |
| 326 | +||| compositional theorem fed with per-part Layer-2 witnesses. Inhabited witness |
| 327 | +||| for compositionality. |
| 328 | +public export |
| 329 | +compositeAgrees : |
| 330 | + evalSrc Semantics.sampleEnv Invariants.compositeExpr |
| 331 | + = evalJulia Semantics.sampleEnv (translate Invariants.compositeExpr) |
| 332 | +compositeAgrees = |
| 333 | + preserveCompose Semantics.sampleEnv LinkAdd |
| 334 | + Semantics.sampleExpr (Index FZ) |
| 335 | + (translatePreserves Semantics.sampleEnv Semantics.sampleExpr) |
| 336 | + (translatePreserves Semantics.sampleEnv (Index FZ)) |
| 337 | + |
| 338 | +||| Positive control for the homomorphism law on the concrete composite, |
| 339 | +||| fully reduced and checked by `Refl`. |
| 340 | +public export |
| 341 | +compositeHom : |
| 342 | + translate Invariants.compositeExpr |
| 343 | + = jlink LinkAdd (translate Semantics.sampleExpr) (translate (Index FZ)) |
| 344 | +compositeHom = Refl |
| 345 | + |
| 346 | +||| Positive control for the faithfulness checker: it ACCEPTS the genuine |
| 347 | +||| translation of the composite. |
| 348 | +public export |
| 349 | +compositeFaithful : IsFaithfulPair Invariants.compositeExpr |
| 350 | + (translate Invariants.compositeExpr) |
| 351 | +compositeFaithful = MkFaithful Invariants.compositeExpr |
| 352 | + |
| 353 | +-------------------------------------------------------------------------------- |
| 354 | +-- NEGATIVE / non-vacuity controls |
| 355 | +-------------------------------------------------------------------------------- |
| 356 | + |
| 357 | +||| Non-vacuity of injectivity: two DIFFERENT source programs really do produce |
| 358 | +||| DIFFERENT Julia programs. Here `Index FZ` vs `Index (FS FZ)`; if `translate` |
| 359 | +||| were constant (vacuous), this `Not` would be unprovable. Machine-checked by |
| 360 | +||| peeling the codegen output down to the underlying `Fin` disequality |
| 361 | +||| `FZ = FS FZ`, which is `Uninhabited`. |
| 362 | +public export |
| 363 | +translateDistinct : Not (translate (the (Expr 3) (Index FZ)) |
| 364 | + = translate (the (Expr 3) (Index (FS FZ)))) |
| 365 | +translateDistinct prf = |
| 366 | + absurd (mkJuliaIdxInj (jIndexInj prf)) |
| 367 | + |
| 368 | +||| Non-vacuity of the faithfulness checker: it REJECTS a WRONG candidate. The |
| 369 | +||| sample's translation is an `Add`, so the checker must say `No` to a `JLit 0` |
| 370 | +||| candidate. Forces the `Dec` to be genuinely discriminating, not a constant |
| 371 | +||| `Yes`. Machine-checked via the decision returning `No`. |
| 372 | +public export |
| 373 | +sampleNotFaithfulToLit : |
| 374 | + Not (IsFaithfulPair Semantics.sampleExpr (JLit 0)) |
| 375 | +sampleNotFaithfulToLit (MkFaithful Semantics.sampleExpr) impossible |
0 commit comments