Skip to content

Commit 8bd0eb9

Browse files
ABI Layer 3: compositionality — translation is a homomorphism over the AST (#43)
* abi: prove translation preserves results (Julianiser.ABI.Semantics) Add the flagship Layer-2 semantic proof for julianiser. Models a shared arithmetic + array-indexing expression AST with two evaluators: evalSrc (Python/R, 0-based indexing) and evalJulia (translated Julia, 1-based indexing). Proves the headline correctness property: translatePreserves : (env) -> (e) -> evalSrc env e = evalJulia env (translate e) as a genuine propositional equality by structural induction, with the index case discharged by the 0-to-1 re-basing round-trip lemma (juliaRoundTrip), mirroring IndexRemapCorrect in Types.idr. Includes a sound certifier (certifyEquiv/certifyEquivSound), a positive control (sampleAgrees, fully evaluated 10*2+30=50), and a negative control: a deliberately unshifted "buggy" Julia evaluator together with buggyDivergesWitness proving it genuinely diverges (30 /= 20). The property is non-vacuous: an adversarial Refl claiming the buggy translation preserves results is rejected by idris2 (Mismatch 20 vs 30). No believe_me/postulate/assert_total. Builds clean, zero warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6PSzJWpRxtzGDjUCEh7Mx * Add Layer-3 ABI invariants: compositionality, determinism, faithfulness checker Raises the julianiser Idris2 ABI from Layer 2 to Layer 3 with a new Julianiser.ABI.Invariants module built over the existing Semantics model (no datatypes redefined). Proves three deeper, distinct properties: - Compositionality: translate is a homomorphism over syntactic linking (translateHom) and correctness is closed under composition (preserveCompose) — a modular argument, not the Layer-2 induction. - Determinism: evalJulia congruence + pipeline determinism, plus translate injectivity (deterministic, information-preserving codegen). - A second sound+complete Dec checker for faithful (source,julia) pairs (decFaithful + soundness + completeness), with DecEq for JExpr/JuliaIdx. Includes positive controls (composite round-trip via preserveCompose, homomorphism Refl, faithful acceptance) and negative/non-vacuity controls (translate injectivity disequality, checker rejection). %default total, no believe_me/postulate/assert. Clean build, zero warnings; adversarial false-equality check rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6PSzJWpRxtzGDjUCEh7Mx --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a937d93 commit 8bd0eb9

2 files changed

Lines changed: 376 additions & 0 deletions

File tree

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
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

src/interface/abi/julianiser-abi.ipkg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ modules = Julianiser.ABI.Types
1010
, Julianiser.ABI.Foreign
1111
, Julianiser.ABI.Proofs
1212
, Julianiser.ABI.Semantics
13+
, Julianiser.ABI.Invariants

0 commit comments

Comments
 (0)